diff --git a/beslutninger.md b/beslutninger.md index e2d5d68e..41e55f26 100644 --- a/beslutninger.md +++ b/beslutninger.md @@ -5,6 +5,18 @@ Vi skriver også om hvilke alternativer som ble vurdert og hvorfor enkelte alter Vi sorterer valgene etter tidspunkt for når de ble gjort, med de siste valgene øverst. +## Layout-komponenter (juni 2026) + +Layout-komponentene (`nve-stack`, `nve-cluster`, `nve-grid`, `nve-box`) er strukturelle byggeklosser basert på prinsippene fra [Every Layout](https://every-layout.dev/). + +Fordelen med layout-komponenter er at man slipper å gjenta den samme layout CSS-en overalt i applikasjonen. Vanlige oppsett som vertikal stabling, horisontal gruppering og rutenett løses med en tag. + +En av ulempene med layout-komponenter er at de legger til et ekstra lag i DOM-en, og at man må sette seg inn i Every Layout for å bruke dem riktig. + +Komponentene for layout brukes frivillig, og er kun ment som et hjelpeverktøy for de som vil bruke dem. Trenger man egendefinert CSS eller andre verdier enn spacing tokens fra Designsystemet, kan man ikke bruke disse komponentene og må definere egen CSS. + +Hovedformålet med disse er å gjøre applikasjonene responsive og forbedre utvikleropplevelsen. + ## Versjonering på pakkenivå fremfor komponentnivå (desember 2025) Versjonering på komponentnivå ville tillatt team å oppdatere kun de komponentene de trenger uten å måtte ta inn hele pakken. Dette kan virke attraktivt, men skaper flere utfordringer enn fordeler. diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js index 6eea69d8..04902a84 100644 --- a/custom-elements-manifest.config.js +++ b/custom-elements-manifest.config.js @@ -35,7 +35,7 @@ const cemInheritancePluginOptions = { }; export default { - globs: ['src/components/**/*.component.ts'], + globs: ['src/components/**/*.component.ts', 'src/components/layouts/nve-layout-base.ts'], exclude: ['**/*.styles.ts', '**/*.test.ts'], dependencies: true, plugins: [ diff --git a/doc-site/.vitepress/config.mts b/doc-site/.vitepress/config.mts index 944d63be..8cea5556 100644 --- a/doc-site/.vitepress/config.mts +++ b/doc-site/.vitepress/config.mts @@ -12,6 +12,17 @@ const componentsLinks = componentFiles.map((file) => { return { text: name, link: `/components/${name}` }; }); +// Henter navn på alle filene i 'layout' mappe +const layoutFiles = fs.readdirSync(path.resolve(__dirname, '../layout')); + +// Generer sidebar layout lenker (ekskluder oversiktssida) +const layoutLinks = layoutFiles + .filter((file) => path.basename(file, path.extname(file)) !== 'layout-oversikt') + .map((file) => { + const name = path.basename(file, path.extname(file)); + return { text: name, link: `/layout/${name}` }; + }); + const figmaIcon = { svg: 'Figma.logoCreated using Figma', }; @@ -32,6 +43,7 @@ export default defineConfig({ nav: [ { text: 'Introduksjon', link: '/introduction/home' }, { text: 'Komponenter', link: `/components/Komponentoversikt.html` }, + { text: 'Layout', link: '/layout/layout-oversikt' }, { component: 'ThemeSelect' }, ], outlineTitle: 'På denne sida', @@ -60,7 +72,7 @@ export default defineConfig({ { text: 'Commit-meldinger', link: '/introduction/forDesigner/commitMessages' }, ], }, - { + { text: 'Designelementer', items: [ { text: 'Tokens', link: '/introduction/designelementer/tokens' }, @@ -75,6 +87,10 @@ export default defineConfig({ ], }, { text: 'Komponenter', items: componentsLinks }, + { + text: 'Layout', + items: [{ text: 'Layout-komponenter', link: '/layout/layout-oversikt' }, ...layoutLinks], + }, ], socialLinks: [ { icon: 'github', link: 'https://github.com/NVE/Designsystem', ariaLabel: 'Link til kildekoden i Github' }, diff --git a/doc-site/.vitepress/theme/index.ts b/doc-site/.vitepress/theme/index.ts index 6345c103..3aa5f32a 100644 --- a/doc-site/.vitepress/theme/index.ts +++ b/doc-site/.vitepress/theme/index.ts @@ -16,7 +16,6 @@ import LinkButton from './components/LinkButton.vue'; import PageHeader from './components/PageHeader.vue'; import ComponentOverview from './components/ComponentOverview.vue'; import ThemeSelect from './components/ThemeSelect.vue'; -import ColorList from './components/ColorList.vue'; import TypographyTable from './components/TypographyTable.vue'; import { cssTokenState } from './cssTokenState'; import { useCurrentTheme, Theme } from './composables/useCurrentTheme'; @@ -70,7 +69,7 @@ export default { if (!import.meta.env.SSR) { // siden VitePress bygges via SSR, vi må sikre at våre web komponenter lastes ned i nettleseren bare // derfor importerer vi alle komponenter når miljø ikke er SSR - const components = import.meta.glob('../../../src/components/*/*.component.ts'); + const components = import.meta.glob('../../../src/components/**/*.component.ts'); // Lese inn nve_theme.css for å hente ut css variabler const styles = import.meta.glob('./styles/nve_theme.css', { query: '?raw', import: 'default' }); @@ -97,7 +96,6 @@ export default { app.component('PageHeader', PageHeader); app.component('ComponentOverview', ComponentOverview); app.component('ThemeSelect', ThemeSelect); - app.component('ColorList', ColorList); app.component('TypographyTable', TypographyTable); app.component('NveTableDemo', NveTableDemo); diff --git a/doc-site/assets/images/layout-visualisering.png b/doc-site/assets/images/layout-visualisering.png new file mode 100644 index 00000000..efde39d1 Binary files /dev/null and b/doc-site/assets/images/layout-visualisering.png differ diff --git a/doc-site/layout/layout-oversikt.md b/doc-site/layout/layout-oversikt.md new file mode 100644 index 00000000..8c2889ab --- /dev/null +++ b/doc-site/layout/layout-oversikt.md @@ -0,0 +1,54 @@ + + +Layout-komponentene er strukturelle byggeklosser basert på prinsippene fra [Every Layout](https://every-layout.dev/). +De håndterer plassering og fordeling av innhold uten å binde seg til visuell stil og kan kombineres fritt for å bygge opp sider og seksjoner. + +## Hvorfor bruke dem? + +Du slipper å skrive egen CSS for vanlige oppsett, og avstander hentes fra designsystemets tokens. Det gir konsistente sider, raskere utvikling og mindre vedlikehold når designet endres. + +## Når skal du bruke layout-komponenter? + +Bruk layout-komponentene når du vil: + +- Stable elementer vertikalt med konsistent mellomrom (`nve-stack`) +- Gruppere elementer horisontalt med automatisk linjebryting (`nve-cluster`) +- Lage et responsivt rutenett (`nve-grid`) +- Gi et element definert padding og bakgrunn (`nve-box`) + +## Prinsipper + +Layout-komponentene har ingen farger, fonter eller annen visuell stil, de styrer kun struktur og plassering. De er ment å kombineres, og bryr seg ikke om hva som ligger inni dem. + +## Eksempel på bruk + +Layout-komponentene er laget for å kombineres. Et kontaktskjema er et typisk eksempel som bruker alle fire: `nve-box` gir kortet ramme og padding, `nve-stack` stabler feltene vertikalt, `nve-grid` legger fornavn og etternavn ved siden av hverandre og bryter til en kolonne på smale skjermer. `nve-cluster` plasserer knappene nederst. + + + +```html + + + Kontakt oss +

Fyll ut skjemaet, så tar vi kontakt så raskt vi kan.

+ + + + + + + + Avbryt + Send + +
+
+``` + +
+ +Visualisering av layoutkomponentene (Grid, Stack, Cluster og Box) i Designsystemet. + +## Felles spacing-props + +Alle layout-komponenter arver `padding`, `margin`, `padding-block`, `padding-inline`, `margin-block` og `margin-inline` fra en basis layoutklasse. Verdiene er låst til spacing-tokenene i designsystemet. diff --git a/doc-site/layout/nve-box.md b/doc-site/layout/nve-box.md new file mode 100644 index 00000000..6e59dea8 --- /dev/null +++ b/doc-site/layout/nve-box.md @@ -0,0 +1,108 @@ +# nve-box + +`nve-box` pakker innhold i en boks med konsistent padding. + +I de aller fleste tilfeller skal du bruke `padding`. `padding` er knyttet direkte til spacing-tokenene i designsystemet og sikrer at paddingen er konsistent på tvers av sider og komponenter. + +## Padding + +`padding` setter padding rundt innholdet ved bruk av et spacing-token. Hvis `padding` ikke er satt, brukes `medium` som standard. + + + +```html + +
+
+ +
+
+ +
+
+``` + +
+ +## Bakgrunn + +`background` setter en tokenbasert bakgrunnsfarge på boksen. Gyldige verdier kommer fra neutrals background-tokensene. + + + +```html + + canvas + + + primary + + + primary-contrast + + + secondary + + + secondary-dim + + + tertiary-dim + +``` + + + +## Nøsting + +Bokser kan nøstes for å bygge opp et hierarki av padding. + + + +```html + + +
+
+
+``` + +
+ +## Eksempel på bruk + +En boks med konsistent padding rundt en tekst. + + + +```html + + Innhold med jevn padding rundt. + +``` + + + +Samme innhold uten `nve-box` hvor teksten ligger helt inntil kanten. + + + +```html +
+ Innhold med null padding rundt. +
+``` + +
+ +## Egenskaper + +| Egenskap | Type | Standard | Beskrivelse | +| ---------------- | --------------- | -------- | ---------------------------------------------------------------------------------------- | +| `background` | `BoxBackground` | — | Tokenbasert bakgrunnsfarge. Gyldige verdier er `--color-neutrals-background-*`-tokenene. | +| `padding` | `SpacingToken` | `medium` | Tokenbasert padding på alle sider. Visuell standard er `medium` via CSS. | +| `padding-block` | `SpacingToken` | — | Overstyrer `padding` i blokk-retning (topp/bunn). | +| `padding-inline` | `SpacingToken` | — | Overstyrer `padding` i inline-retning (venstre/høyre). | +| `margin` | `SpacingToken` | — | Tokenbasert margin på alle sider. | +| `margin-block` | `SpacingToken` | — | Overstyrer `margin` i blokk-retning (topp/bunn). | +| `margin-inline` | `SpacingToken` | — | Overstyrer `margin` i inline-retning (venstre/høyre). | diff --git a/doc-site/layout/nve-cluster.md b/doc-site/layout/nve-cluster.md new file mode 100644 index 00000000..f99104a5 --- /dev/null +++ b/doc-site/layout/nve-cluster.md @@ -0,0 +1,158 @@ +# nve-cluster + +`nve-cluster` grupperer barn-elementer horisontalt med konsistent mellomrom og automatisk linjebryting. Brukes typisk for knappegrupper, tag-lister og navigasjon. + +I de aller fleste tilfeller skal du bruke `gap`. `gap` er knyttet direkte til spacing-tokenene i designsystemet og sikrer at avstandene er konsistente på tvers av sider og komponenter. + +## Mellomrom + +`gap` setter avstanden mellom barn-elementene ved bruk av et spacing-token. Hvis `gap` ikke er satt, brukes `medium` som standard. + + + +```html + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+``` + +
+ +## Justering + +`justify` styrer hvordan barn-elementene fordeles horisontalt. Tilsvarer CSS-egenskapen `justify-content`. + + + +```html + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+``` + +
+ +## Vertikal justering + +`align` styrer hvordan barn-elementene plasseres vertikalt når de har ulik høyde. Tilsvarer CSS-egenskapen `align-items`. Standard er `center`. + + + +```html + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+``` + +
+ +## Eksempel på bruk + +En knapperad som automatisk bryter til ny linje på smalere skjermer. + + + +```html +
+ + Lagre + Forhåndsvis + Avbryt + +
+``` + +
+ +Til sammenligning, samme knapper i en vanlig `
` uten `nve-cluster`. Det er ingen mellomrom mellom knappene, og når de bryter til ny linje, klistrer de seg sammen. + + + +```html +
+ Lagre + Forhåndsvis + Avbryt +
+``` + +
+ +## Egenskaper + +| Egenskap | Type | Standard | Beskrivelse | +| ---------------- | --------------- | ------------ | ----------------------------------------------------------------------------------------- | +| `gap` | `SpacingToken` | `medium` | Tokenbasert mellomrom mellom barn-elementer. | +| `justify` | `LayoutJustify` | `flex-start` | Horisontal fordeling av barn-elementer. Gyldige verdier er CSS `justify-content`-verdier. | +| `align` | `ClusterAlign` | `center` | Vertikal justering av barn-elementer. Gyldige verdier er CSS `align-items`-verdier. | +| `padding` | `SpacingToken` | — | Tokenbasert padding på alle sider. | +| `padding-block` | `SpacingToken` | — | Overstyrer `padding` i blokk-retning (topp/bunn). | +| `padding-inline` | `SpacingToken` | — | Overstyrer `padding` i inline-retning (venstre/høyre). | +| `margin` | `SpacingToken` | — | Tokenbasert margin på alle sider. | +| `margin-block` | `SpacingToken` | — | Overstyrer `margin` i blokk-retning (topp/bunn). | +| `margin-inline` | `SpacingToken` | — | Overstyrer `margin` i inline-retning (venstre/høyre). | diff --git a/doc-site/layout/nve-grid.md b/doc-site/layout/nve-grid.md new file mode 100644 index 00000000..f031acb6 --- /dev/null +++ b/doc-site/layout/nve-grid.md @@ -0,0 +1,125 @@ +# nve-grid + +`nve-grid` lager et responsivt rutenett som automatisk bryter til nye linjer basert på en minste kolonnebredde. + +I de aller fleste tilfeller skal du bruke `gap` for mellomrom. `gap` er knyttet direkte til spacing-tokenene i designsystemet og sikrer at avstandene er konsistente på tvers av sider og komponenter. + +## Minste kolonnebredde + +`min` bestemmer hvor smal en kolonne kan bli før rutenettet bryter til ny linje. Standard er `250px`. + + + + + +```html +
+ +
+
+
+
+
+
+
+
+``` + +
+ +## Mellomrom + +`gap` setter mellomrommet mellom rutene ved bruk av et spacing-token. Hvis `gap` ikke er satt, brukes `medium` som standard. + + + +```html + +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+``` + +
+ +## Eksempel på bruk + +Et responsivt rutenett med navigasjonskort som tilpasser antall kolonner etter tilgjengelig plass. + + + +```html + + + + + + + + +``` + + + +## Egenskaper + +| Egenskap | Type | Standard | Beskrivelse | +| ---------------- | -------------- | -------- | --------------------------------------------------------------------------------------- | +| `min` | `string` | `250px` | Minste kolonnebredde før rutenettet bryter til ny linje (f.eks. `250px` eller `16rem`). | +| `gap` | `SpacingToken` | `medium` | Tokenbasert mellomrom mellom kolonner og rader. | +| `padding` | `SpacingToken` | — | Tokenbasert padding på alle sider. | +| `padding-block` | `SpacingToken` | — | Overstyrer `padding` i blokk-retning (topp/bunn). | +| `padding-inline` | `SpacingToken` | — | Overstyrer `padding` i inline-retning (venstre/høyre). | +| `margin` | `SpacingToken` | — | Tokenbasert margin på alle sider. | +| `margin-block` | `SpacingToken` | — | Overstyrer `margin` i blokk-retning (topp/bunn). | +| `margin-inline` | `SpacingToken` | — | Overstyrer `margin` i inline-retning (venstre/høyre). | diff --git a/doc-site/layout/nve-stack.md b/doc-site/layout/nve-stack.md new file mode 100644 index 00000000..b3755d51 --- /dev/null +++ b/doc-site/layout/nve-stack.md @@ -0,0 +1,163 @@ +# nve-stack + +`nve-stack` stabler barn-elementer vertikalt med mellomrom. + +I de aller fleste tilfeller skal du bruke `gap`. `gap` er knyttet direkte til spacing-tokenene i designsystemet og er det som sikrer at avstandene er konsistente på tvers av sider og komponenter. + +## Mellomrom + +`gap` setter avstanden mellom barn-elementene ved bruk av et spacing-token. + + + +```html + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+``` + +
+ +## Justering + +`justify` styrer hvordan barn-elementene fordeles vertikalt. Tilsvarer CSS-egenskapen `justify-content`. + + + +```html + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+``` + +
+ +## Nøsting + +Stacks kan nøstes for å bygge opp et hierarki av avstander. + + + +```html + + +
+
+
+
+ +
+
+
+
+``` + +
+ +## Eksempel på bruk + +Med `nve-stack` får du konsistent avstand fra et token, uten egen CSS. + + + +```html + + + + + Send inn + +``` + + + +Uten `nve-stack` klistrer elementene seg sammen og må styles individuelt for å få luft mellom seg. + + + +```html +
+ + + + Send inn +
+``` + +
+ +## Egenskaper + +| Egenskap | Type | Standard | Beskrivelse | +| ---------------- | --------------- | ------------ | -------------------------------------------------------------------------------------- | +| `gap` | `SpacingToken` | `medium` | Tokenbasert mellomrom mellom barn-elementer. | +| `justify` | `LayoutJustify` | `flex-start` | Fordeling langs den vertikale aksen. Gyldige verdier er CSS `justify-content`-verdier. | +| `padding` | `SpacingToken` | — | Tokenbasert padding på alle sider. | +| `padding-block` | `SpacingToken` | — | Overstyrer `padding` i blokk-retning (topp/bunn). | +| `padding-inline` | `SpacingToken` | — | Overstyrer `padding` i inline-retning (venstre/høyre). | +| `margin` | `SpacingToken` | — | Tokenbasert margin på alle sider. | +| `margin-block` | `SpacingToken` | — | Overstyrer `margin` i blokk-retning (topp/bunn). | +| `margin-inline` | `SpacingToken` | — | Overstyrer `margin` i inline-retning (venstre/høyre). | diff --git a/src/components/layouts/nve-box/nve-box.component.ts b/src/components/layouts/nve-box/nve-box.component.ts new file mode 100644 index 00000000..9f12fa6a --- /dev/null +++ b/src/components/layouts/nve-box/nve-box.component.ts @@ -0,0 +1,56 @@ +import { customElement, property } from 'lit/decorators.js'; +import styles from './nve-box.styles'; +import { html, PropertyValues } from 'lit'; +import { NveLayoutBase, SpacingToken } from '../nve-layout-base'; + +/** + * Pakker innhold i en boks med padding og bakgrunnsfarge. + * Basert på Box-primitiven fra Every Layout. + * + * Arver padding/margin-props fra NveLayoutBase. + * Bakgrunnsfarge styres av `background`. + * + * @property {SpacingToken} padding - Tokenbasert padding (arvet). Standard: medium. + * @property {string} background - Tokenbasert bakgrunnsfarge. + */ +export type BoxLayoutPadding = SpacingToken; + +export type BoxBackground = + | '--color-neutrals-background-canvas' + | '--color-neutrals-background-primary' + | '--color-neutrals-background-primary-contrast' + | '--color-neutrals-background-secondary' + | '--color-neutrals-background-secondary-dim' + | '--color-neutrals-background-tertiary-dim'; + +@customElement('nve-box') +export default class NveBox extends NveLayoutBase { + static styles = [styles]; + + /** Tokenbasert bakgrunnsfarge. */ + @property({ type: String, reflect: true }) background?: BoxBackground; + + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + if ( + changedProperties.has('background') && + (changedProperties.get('background') !== undefined || this.background !== undefined) + ) { + if (this.background !== undefined) { + this.style.setProperty('background', `var(${this.background})`); + } else { + this.style.removeProperty('background'); + } + } + } + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'nve-box': NveBox; + } +} diff --git a/src/components/layouts/nve-box/nve-box.styles.ts b/src/components/layouts/nve-box/nve-box.styles.ts new file mode 100644 index 00000000..828026c8 --- /dev/null +++ b/src/components/layouts/nve-box/nve-box.styles.ts @@ -0,0 +1,9 @@ +import { css } from 'lit'; + +export default css` + :host { + display: block; + padding: var(--spacing-medium); + background: transparent; + } +`; diff --git a/src/components/layouts/nve-cluster/nve-cluster.component.ts b/src/components/layouts/nve-cluster/nve-cluster.component.ts new file mode 100644 index 00000000..b7f412d1 --- /dev/null +++ b/src/components/layouts/nve-cluster/nve-cluster.component.ts @@ -0,0 +1,48 @@ +import { customElement, property } from 'lit/decorators.js'; +import styles from './nve-cluster.styles'; +import { html, PropertyValues } from 'lit'; +import { NveLayoutBase, SpacingToken, LayoutJustify } from '../nve-layout-base'; + +/** + * Grupperer barn-elementer horisontalt med automatisk linjebryting. + * Basert på Cluster-primitiven fra Every Layout. + * + * Mellomrommet styres av `gap` og er låst til spacing-tokenene i designsystemet. + * Arver padding/margin-props fra NveLayoutBase. + * + * @property {ClusterLayoutGap} gap - Forhåndsdefinert tokenbasert mellomrom. + * @property {LayoutJustify} justify - justify-content-verdi. Standard: flex-start. + * @property {ClusterAlign} align - align-items-verdi. Standard: center. + */ +export type ClusterLayoutGap = SpacingToken; +export type ClusterAlign = 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'stretch' | 'start' | 'end'; + +@customElement('nve-cluster') +export default class NveCluster extends NveLayoutBase { + static styles = [...styles]; + + /** Forhåndsdefinert tokenbasert mellomrom. Mapper til `--spacing-`. */ + @property({ type: String, reflect: true }) gap?: ClusterLayoutGap; + + /** justify-content på flex-containeren. Standard: flex-start. */ + @property({ type: String, reflect: true }) justify: LayoutJustify = 'flex-start'; + + /** align-items på flex-containeren. Standard: center. */ + @property({ type: String, reflect: true }) align: ClusterAlign = 'center'; + + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + this.style.setProperty('--_cluster-justify', this.justify); + this.style.setProperty('--_cluster-align', this.align); + } + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'nve-cluster': NveCluster; + } +} diff --git a/src/components/layouts/nve-cluster/nve-cluster.styles.ts b/src/components/layouts/nve-cluster/nve-cluster.styles.ts new file mode 100644 index 00000000..4d40136f --- /dev/null +++ b/src/components/layouts/nve-cluster/nve-cluster.styles.ts @@ -0,0 +1,15 @@ +import { css } from 'lit'; +import { gapStyles } from '../nve-layout-gap.styles'; + +export default [ + gapStyles, + css` + :host { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-medium); + justify-content: var(--_cluster-justify, flex-start); + align-items: var(--_cluster-align, center); + } + `, +]; diff --git a/src/components/layouts/nve-grid/nve-grid.component.ts b/src/components/layouts/nve-grid/nve-grid.component.ts new file mode 100644 index 00000000..9ff31da4 --- /dev/null +++ b/src/components/layouts/nve-grid/nve-grid.component.ts @@ -0,0 +1,42 @@ +import { customElement, property } from 'lit/decorators.js'; +import styles from './nve-grid.styles'; +import { html, PropertyValues } from 'lit'; +import { NveLayoutBase, SpacingToken } from '../nve-layout-base'; + +/** + * Et responsivt rutenett som automatisk bryter til nye linjer basert på en minste kolonnebredde. + * Basert på Grid-primitiven fra Every Layout. + * + * Mellomrommet styres av `gap` og er låst til spacing-tokenene i designsystemet. + * Arver padding/margin-props fra NveLayoutBase. + * + * @property {string} min - Minste kolonnebredde, f.eks. "250px" eller "16rem". Bestemmer når rutenettet bryter til ny linje. + * @property {GridLayoutGap} gap - Forhåndsdefinert tokenbasert mellomrom. + */ +export type GridLayoutGap = SpacingToken; + +@customElement('nve-grid') +export default class NveGrid extends NveLayoutBase { + static styles = [...styles]; + + /** Minste kolonnebredde. Bestemmer når rutenettet bryter til ny linje. Standard: 250px. */ + @property({ type: String, reflect: true }) min: string = '250px'; + + /** Forhåndsdefinert tokenbasert mellomrom. Mapper til `--spacing-`. */ + @property({ type: String, reflect: true }) gap?: GridLayoutGap; + + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + this.style.setProperty('--_grid-min', this.min); + } + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'nve-grid': NveGrid; + } +} diff --git a/src/components/layouts/nve-grid/nve-grid.styles.ts b/src/components/layouts/nve-grid/nve-grid.styles.ts new file mode 100644 index 00000000..eab1ec72 --- /dev/null +++ b/src/components/layouts/nve-grid/nve-grid.styles.ts @@ -0,0 +1,13 @@ +import { css } from 'lit'; +import { gapStyles } from '../nve-layout-gap.styles'; + +export default [ + gapStyles, + css` + :host { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(var(--_grid-min, 250px), 100%), 1fr)); + gap: var(--spacing-medium); + } + `, +]; diff --git a/src/components/layouts/nve-layout-base.ts b/src/components/layouts/nve-layout-base.ts new file mode 100644 index 00000000..6ad27afa --- /dev/null +++ b/src/components/layouts/nve-layout-base.ts @@ -0,0 +1,83 @@ +import { LitElement, PropertyValues } from 'lit'; +import { property } from 'lit/decorators.js'; + +/** + * Felles spacing-tokens brukt av alle layout-komponenter. + * Mapper direkte til `--spacing-` i designsystemet. + */ +export type SpacingToken = + | 'none' + | '2x-small' + | 'x-small' + | 'small' + | 'medium' + | 'large' + | 'x-large' + | '2x-large' + | '3x-large' + | '4x-large' + | '5x-large'; + +export type LayoutJustify = + | 'flex-start' + | 'flex-end' + | 'center' + | 'space-between' + | 'space-around' + | 'space-evenly' + | 'start' + | 'end' + | 'left' + | 'right'; + +/** + * Basisklasse for alle layout-komponenter. + * Gir felles props for padding og margin låst til spacing-tokenene i designsystemet. + * + * Rekkefølge for overstyringsregler: + * `padding` settes først, deretter `padding-block` og `padding-inline`. + * Samme prinsipp gjelder for `margin`. + */ +export class NveLayoutBase extends LitElement { + /** Tokenbasert padding på alle sider. */ + @property({ type: String, reflect: true }) padding?: SpacingToken; + + /** Tokenbasert margin på alle sider. */ + @property({ type: String, reflect: true }) margin?: SpacingToken; + + /** Tokenbasert padding i blokk-retning (topp og bunn). Overstyrer `padding` i blokk-retning. */ + @property({ type: String, reflect: true, attribute: 'padding-block' }) paddingBlock?: SpacingToken; + + /** Tokenbasert padding i inline-retning (venstre og høyre). Overstyrer `padding` i inline-retning. */ + @property({ type: String, reflect: true, attribute: 'padding-inline' }) paddingInline?: SpacingToken; + + /** Tokenbasert margin i blokk-retning (topp og bunn). Overstyrer `margin` i blokk-retning. */ + @property({ type: String, reflect: true, attribute: 'margin-block' }) marginBlock?: SpacingToken; + + /** Tokenbasert margin i inline-retning (venstre og høyre). Overstyrer `margin` i inline-retning. */ + @property({ type: String, reflect: true, attribute: 'margin-inline' }) marginInline?: SpacingToken; + + private _applySpacingProp(cssProp: string, value?: SpacingToken) { + if (value !== undefined) { + this.style.setProperty(cssProp, `var(--spacing-${value})`); + } else { + this.style.removeProperty(cssProp); + } + } + + protected override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + // Sjekk om prop faktisk har endret seg fra en satt verdi, slik at vi ikke fjerner + // inline-stiler brukeren har satt via style-attributtet ved første render. + const changed = (key: string) => + changedProperties.has(key) && + (changedProperties.get(key) !== undefined || (this as Record)[key] !== undefined); + + if (changed('padding')) this._applySpacingProp('padding', this.padding); + if (changed('margin')) this._applySpacingProp('margin', this.margin); + if (changed('paddingBlock')) this._applySpacingProp('padding-block', this.paddingBlock); + if (changed('paddingInline')) this._applySpacingProp('padding-inline', this.paddingInline); + if (changed('marginBlock')) this._applySpacingProp('margin-block', this.marginBlock); + if (changed('marginInline')) this._applySpacingProp('margin-inline', this.marginInline); + } +} diff --git a/src/components/layouts/nve-layout-gap.styles.ts b/src/components/layouts/nve-layout-gap.styles.ts new file mode 100644 index 00000000..a994e074 --- /dev/null +++ b/src/components/layouts/nve-layout-gap.styles.ts @@ -0,0 +1,51 @@ +import { css } from 'lit'; + +/** + * Delt gap-attributt-selector-stiler for layout-komponenter som eksponerer en `gap`-prop. + * Brukes av nve-stack, nve-cluster og nve-grid. + */ +export const gapStyles = css` + :host([gap='none']) { + gap: var(--spacing-none); + } + + :host([gap='2x-small']) { + gap: var(--spacing-2x-small); + } + + :host([gap='x-small']) { + gap: var(--spacing-x-small); + } + + :host([gap='small']) { + gap: var(--spacing-small); + } + + :host([gap='medium']) { + gap: var(--spacing-medium); + } + + :host([gap='large']) { + gap: var(--spacing-large); + } + + :host([gap='x-large']) { + gap: var(--spacing-x-large); + } + + :host([gap='2x-large']) { + gap: var(--spacing-2x-large); + } + + :host([gap='3x-large']) { + gap: var(--spacing-3x-large); + } + + :host([gap='4x-large']) { + gap: var(--spacing-4x-large); + } + + :host([gap='5x-large']) { + gap: var(--spacing-5x-large); + } +`; diff --git a/src/components/layouts/nve-stack/nve-stack.component.ts b/src/components/layouts/nve-stack/nve-stack.component.ts new file mode 100644 index 00000000..c2e6c1cf --- /dev/null +++ b/src/components/layouts/nve-stack/nve-stack.component.ts @@ -0,0 +1,42 @@ +import { customElement, property } from 'lit/decorators.js'; +import styles from './nve-stack.styles'; +import { html, PropertyValues } from 'lit'; +import { NveLayoutBase, SpacingToken, LayoutJustify } from '../nve-layout-base'; + +/** + * Stabler barn-elementer vertikalt med konsistent mellomrom. + * Basert på Stack-primitiven fra Every Layout. + * + * Mellomrommet styres av `gap` og er låst til spacing-tokenene i designsystemet. + * Arver padding/margin-props fra NveLayoutBase. + * + * @property {StackLayoutGap} gap - Forhåndsdefinert tokenbasert mellomrom. + * @property {string} justify - justify-content-verdi. Standard: flex-start. + */ +export type StackLayoutGap = SpacingToken; + +@customElement('nve-stack') +export default class NveStack extends NveLayoutBase { + static styles = [...styles]; + + /** Forhåndsdefinert tokenbasert mellomrom. Mapper til `--spacing-`. */ + @property({ type: String, reflect: true }) gap?: StackLayoutGap; + + /** justify-content på flex-containeren. Standard: flex-start. */ + @property({ type: String, reflect: true }) justify: LayoutJustify = 'flex-start'; + + override updated(changedProperties: PropertyValues) { + super.updated(changedProperties); + this.style.setProperty('--_stack-justify', this.justify); + } + + render() { + return html``; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'nve-stack': NveStack; + } +} diff --git a/src/components/layouts/nve-stack/nve-stack.styles.ts b/src/components/layouts/nve-stack/nve-stack.styles.ts new file mode 100644 index 00000000..643452a9 --- /dev/null +++ b/src/components/layouts/nve-stack/nve-stack.styles.ts @@ -0,0 +1,14 @@ +import { css } from 'lit'; +import { gapStyles } from '../nve-layout-gap.styles'; + +export default [ + gapStyles, + css` + :host { + display: flex; + flex-direction: column; + gap: var(--spacing-medium); + justify-content: var(--_stack-justify, flex-start); + } + `, +]; diff --git a/src/nve-designsystem.ts b/src/nve-designsystem.ts index d3660f55..0582f3d0 100644 --- a/src/nve-designsystem.ts +++ b/src/nve-designsystem.ts @@ -4,6 +4,15 @@ export { default as NveAccordion } from './components/nve-accordion/nve-accordio export { default as NveAccordionItem } from './components/nve-accordion-item/nve-accordion-item.component'; export { default as NveAlert } from './components/nve-alert/nve-alert.component'; export { default as NveBadge } from './components/nve-badge/nve-badge.component'; +export { NveLayoutBase, type SpacingToken, type LayoutJustify } from './components/layouts/nve-layout-base'; +export { default as NveBox, type BoxBackground } from './components/layouts/nve-box/nve-box.component'; +export { + default as NveCluster, + type ClusterLayoutGap, + type ClusterAlign, +} from './components/layouts/nve-cluster/nve-cluster.component'; +export { default as NveGrid } from './components/layouts/nve-grid/nve-grid.component'; +export { default as NveStack } from './components/layouts/nve-stack/nve-stack.component'; export { default as NveButton } from './components/nve-button/nve-button.component'; export { default as NveCarousel } from './components/nve-carousel/nve-carousel.component'; export { default as NveCarouselItem } from './components/nve-carousel-item/nve-carousel-item.component';