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: '',
};
@@ -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 b67ab444..3aa5f32a 100644
--- a/doc-site/.vitepress/theme/index.ts
+++ b/doc-site/.vitepress/theme/index.ts
@@ -69,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' });
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
+
+
+
+```
+
+
+
+
+
+## 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..46e2d2b7
--- /dev/null
+++ b/doc-site/layout/nve-box.md
@@ -0,0 +1,125 @@
+# nve-box
+
+`nve-box` pakker innhold i en boks med konsistent padding.
+
+`padding` er knyttet direkte til spacing-tokenene i designsystemet og sikrer at paddingen er konsistent på tvers av sider og komponenter.
+
+## Import
+
+Layout-komponentene ligger et nivå under de andre komponentene. For å bruke disse kan du følge stien med `/layout` etterfulgt av komponentet du vil bruke. Eksempel:
+
+```javascript
+import 'nve-designsystem/components/layouts/nve-box/nve-box.component.js';
+```
+
+## Padding
+
+`padding` setter padding rundt innholdet ved bruk av et spacing-token. Hvis `padding` ikke er satt, brukes `medium` som standard.
+
+### Gyldige spacing-verdier
+
+Følgende verdier er gyldige for `padding`:
+`none`, `2x-small`, `x-small`, `small`, `medium` (default), `large`, `x-large`, `2x-large`, `3x-large`, `4x-large` og `5x-large`.
+
+
+
+```html
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## Bakgrunn
+
+`background` setter en tokenbasert bakgrunnsfarge på boksen. Gyldige verdier kommer fra neutrals background-tokensene.
+
+### Gyldige bakgrunnsverdier
+
+Følgende verdier er gyldige for `background`: `--color-neutrals-background-canvas`, `--color-neutrals-background-primary`, `--color-neutrals-background-primary-contrast`, `--color-neutrals-background-secondary`, `--color-neutrals-background-secondary-dim` og `--color-neutrals-background-tertiary-dim`.
+
+
+
+```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..d0443b35
--- /dev/null
+++ b/doc-site/layout/nve-cluster.md
@@ -0,0 +1,179 @@
+# nve-cluster
+
+`nve-cluster` grupperer barn-elementer horisontalt med konsistent mellomrom og automatisk linjebryting. Brukes typisk for knappegrupper, tag-lister og navigasjon.
+
+`gap` er knyttet direkte til spacing-tokenene i designsystemet og sikrer at avstandene er konsistente på tvers av sider og komponenter.
+
+## Import
+
+Layout-komponentene ligger et nivå under de andre komponentene. For å bruke disse kan du følge stien med `/layout` etterfulgt av komponentet du vil bruke. Eksempel:
+
+```javascript
+import 'nve-designsystem/components/layouts/nve-cluster/nve-cluster.component.js';
+```
+
+## Mellomrom
+
+`gap` setter avstanden mellom barn-elementene ved bruk av et spacing-token. Hvis `gap` ikke er satt, brukes `medium` som standard.
+
+### Gyldige spacing-verdier
+
+Følgende verdier er gyldige for `gap`:
+`none`, `2x-small`, `x-small`, `small`, `medium` (default), `large`, `x-large`, `2x-large`, `3x-large`, `4x-large` og `5x-large`.
+
+
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## Justering
+
+`justify` styrer hvordan barn-elementene fordeles horisontalt. Tilsvarer CSS-egenskapen `justify-content`.
+
+### Gyldige justering-verdier
+
+Følgende verdier er gyldige for `justify`: `flex-start` (default), `flex-end`, `center`, `space-between`, `space-around`, `space-evenly`, `start`, `end`, `left` og `right`.
+
+
+
+```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`.
+
+### Gyldige vertikal justering-verdier
+
+Følgende verdier er gyldige for `align`: `flex-start`, `flex-end`, `center` (default), `baseline`, `stretch`, `start` og `end`.
+
+
+
+```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..bebb0045
--- /dev/null
+++ b/doc-site/layout/nve-grid.md
@@ -0,0 +1,137 @@
+# nve-grid
+
+`nve-grid` lager et responsivt rutenett som automatisk bryter til nye linjer basert på en minste kolonnebredde.
+
+`gap` er knyttet direkte til spacing-tokenene i designsystemet og sikrer at avstandene er konsistente på tvers av sider og komponenter.
+
+## Import
+
+Layout-komponentene ligger et nivå under de andre komponentene. For å bruke disse kan du følge stien med `/layout` etterfulgt av komponentet du vil bruke. Eksempel:
+
+```javascript
+import 'nve-designsystem/components/layouts/nve-grid/nve-grid.component.js';
+```
+
+## 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.
+
+### Gyldige spacing-verdier
+
+Følgende verdier er gyldige for `gap`:
+`none`, `2x-small`, `x-small`, `small`, `medium` (default), `large`, `x-large`, `2x-large`, `3x-large`, `4x-large` og `5x-large`.
+
+
+```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..a38d21c8
--- /dev/null
+++ b/doc-site/layout/nve-stack.md
@@ -0,0 +1,180 @@
+# nve-stack
+
+`nve-stack` stabler barn-elementer vertikalt med mellomrom.
+
+`gap` er knyttet direkte til spacing-tokenene i designsystemet og er det som sikrer at avstandene er konsistente på tvers av sider og komponenter.
+
+## Import
+
+Layout-komponentene ligger et nivå under de andre komponentene. For å bruke disse kan du følge stien med `/layout` etterfulgt av komponentet du vil bruke. Eksempel:
+
+```javascript
+import 'nve-designsystem/components/layouts/nve-stack/nve-stack.component.js';
+```
+
+## Mellomrom
+
+`gap` setter avstanden mellom barn-elementene ved bruk av et spacing-token.
+
+### Gyldige spacing-verdier
+
+Følgende verdier er gyldige for `gap`:
+`none`, `2x-small`, `x-small`, `small`, `medium` (default), `large`, `x-large`, `2x-large`, `3x-large`, `4x-large` og `5x-large`.
+
+
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## Justering
+
+`justify` styrer hvordan barn-elementene fordeles vertikalt. Tilsvarer CSS-egenskapen `justify-content`.
+
+### Gyldige justering-verdier
+
+Følgende verdier er gyldige for `justify`: `flex-start` (default), `flex-end`, `center`, `space-between`, `space-around`, `space-evenly`, `start`, `end`, `left` og `right`.
+
+
+
+```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..8cebd512
--- /dev/null
+++ b/src/components/layouts/nve-box/nve-box.component.ts
@@ -0,0 +1,81 @@
+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';
+
+/**
+ * Validerer at en verdi er en gyldig background-verdi.
+ */
+function isValidBoxBackground(value: unknown): value is BoxBackground {
+ const validBackgrounds: 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',
+ ];
+ return validBackgrounds.includes(value as BoxBackground);
+}
+
+@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);
+
+ // Valider og sett fallback for background
+ if (
+ changedProperties.has('background') &&
+ (changedProperties.get('background') !== undefined || this.background !== undefined)
+ ) {
+ if (this.background !== undefined && !isValidBoxBackground(this.background)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-box] Ugyldig verdi for 'background': "${this.background}". Bakgrunnsfargen fjernes. Gyldige verdier er: --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.`
+ );
+ 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..268fc574
--- /dev/null
+++ b/src/components/layouts/nve-cluster/nve-cluster.component.ts
@@ -0,0 +1,107 @@
+import { customElement, property } from 'lit/decorators.js';
+import styles from './nve-cluster.styles';
+import { html, PropertyValues } from 'lit';
+import {
+ NveLayoutBase,
+ SpacingToken,
+ LayoutJustify,
+ isValidSpacingToken,
+ isValidLayoutJustify,
+} 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';
+
+/**
+ * Validerer at en verdi er en gyldig align-verdi.
+ */
+function isValidClusterAlign(value: unknown): value is ClusterAlign {
+ const validAlign: ClusterAlign[] = ['flex-start', 'flex-end', 'center', 'baseline', 'stretch', 'start', 'end'];
+ return validAlign.includes(value as ClusterAlign);
+}
+
+@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);
+
+ // Valider gap. Ugyldige verdier ignoreres.
+ if (changedProperties.has('gap')) {
+ if (this.gap !== undefined && !isValidSpacingToken(this.gap)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-cluster] Ugyldig verdi for 'gap': "${this.gap}". Verdien ignoreres. Gyldige verdier er: none, 2x-small, x-small, small, medium, large, x-large, 2x-large, 3x-large, 4x-large, 5x-large.`
+ );
+ this.gap = undefined;
+ }
+ }
+
+ // Valider justify. Ugyldige verdier ignoreres.
+ let justifyValue: LayoutJustify | undefined = this.justify;
+ if (changedProperties.has('justify')) {
+ if (this.justify !== undefined && !isValidLayoutJustify(this.justify)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-cluster] Ugyldig verdi for 'justify': "${this.justify}". Verdien ignoreres. Gyldige verdier er: flex-start, flex-end, center, space-between, space-around, space-evenly, start, end, left, right.`
+ );
+ justifyValue = undefined;
+ }
+ }
+
+ // Valider align. Ugyldige verdier ignoreres.
+ let alignValue: ClusterAlign | undefined = this.align;
+ if (changedProperties.has('align')) {
+ if (this.align !== undefined && !isValidClusterAlign(this.align)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-cluster] Ugyldig verdi for 'align': "${this.align}". Verdien ignoreres. Gyldige verdier er: flex-start, flex-end, center, baseline, stretch, start, end.`
+ );
+ alignValue = undefined;
+ }
+ }
+
+ if (justifyValue !== undefined) {
+ this.style.setProperty('--_cluster-justify', justifyValue);
+ } else {
+ this.style.removeProperty('--_cluster-justify');
+ }
+
+ if (alignValue !== undefined) {
+ this.style.setProperty('--_cluster-align', alignValue);
+ } else {
+ this.style.removeProperty('--_cluster-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..fed187b6
--- /dev/null
+++ b/src/components/layouts/nve-grid/nve-grid.component.ts
@@ -0,0 +1,54 @@
+import { customElement, property } from 'lit/decorators.js';
+import styles from './nve-grid.styles';
+import { html, PropertyValues } from 'lit';
+import { NveLayoutBase, SpacingToken, isValidSpacingToken } 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);
+
+ // Valider gap. Ugyldige verdier ignoreres.
+ if (changedProperties.has('gap')) {
+ if (this.gap !== undefined && !isValidSpacingToken(this.gap)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-grid] Ugyldig verdi for 'gap': "${this.gap}". Verdien ignoreres. Gyldige verdier er: none, 2x-small, x-small, small, medium, large, x-large, 2x-large, 3x-large, 4x-large, 5x-large.`
+ );
+ this.gap = undefined;
+ }
+ }
+
+ 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..ee064696
--- /dev/null
+++ b/src/components/layouts/nve-layout-base.ts
@@ -0,0 +1,145 @@
+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';
+
+/**
+ * Validerer at en verdi er en gyldig spacing-token.
+ */
+export function isValidSpacingToken(value: unknown): value is SpacingToken {
+ const validTokens: SpacingToken[] = [
+ 'none',
+ '2x-small',
+ 'x-small',
+ 'small',
+ 'medium',
+ 'large',
+ 'x-large',
+ '2x-large',
+ '3x-large',
+ '4x-large',
+ '5x-large',
+ ];
+ return validTokens.includes(value as SpacingToken);
+}
+
+/**
+ * Validerer at en verdi er en gyldig justify-verdi.
+ */
+export function isValidLayoutJustify(value: unknown): value is LayoutJustify {
+ const validJustify: LayoutJustify[] = [
+ 'flex-start',
+ 'flex-end',
+ 'center',
+ 'space-between',
+ 'space-around',
+ 'space-evenly',
+ 'start',
+ 'end',
+ 'left',
+ 'right',
+ ];
+ return validJustify.includes(value as LayoutJustify);
+}
+
+/**
+ * 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 _warnInvalidSpacingProp(propName: string, value: unknown) {
+ const tagName = this.tagName.toLowerCase();
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[${tagName}] Ugyldig verdi for '${propName}': "${value}". Verdien ignoreres. Gyldige verdier er: none, 2x-small, x-small, small, medium, large, x-large, 2x-large, 3x-large, 4x-large, 5x-large.`
+ );
+ }
+
+ 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);
+
+ const getSpacingValue = (key: keyof NveLayoutBase): SpacingToken | undefined => {
+ const value = this[key] as unknown;
+ if (value === undefined) {
+ return undefined;
+ }
+
+ if (!isValidSpacingToken(value)) {
+ this._warnInvalidSpacingProp(key, value);
+ (this as Record)[key] = undefined;
+ return undefined;
+ }
+
+ return value;
+ };
+
+ if (changed('padding')) this._applySpacingProp('padding', getSpacingValue('padding'));
+ if (changed('margin')) this._applySpacingProp('margin', getSpacingValue('margin'));
+ if (changed('paddingBlock')) this._applySpacingProp('padding-block', getSpacingValue('paddingBlock'));
+ if (changed('paddingInline')) this._applySpacingProp('padding-inline', getSpacingValue('paddingInline'));
+ if (changed('marginBlock')) this._applySpacingProp('margin-block', getSpacingValue('marginBlock'));
+ if (changed('marginInline')) this._applySpacingProp('margin-inline', getSpacingValue('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..828edd26
--- /dev/null
+++ b/src/components/layouts/nve-stack/nve-stack.component.ts
@@ -0,0 +1,76 @@
+import { customElement, property } from 'lit/decorators.js';
+import styles from './nve-stack.styles';
+import { html, PropertyValues } from 'lit';
+import {
+ NveLayoutBase,
+ SpacingToken,
+ LayoutJustify,
+ isValidSpacingToken,
+ isValidLayoutJustify,
+} 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);
+
+ // Valider gap. Ugyldige verdier ignoreres.
+ if (changedProperties.has('gap')) {
+ if (this.gap !== undefined && !isValidSpacingToken(this.gap)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-stack] Ugyldig verdi for 'gap': "${this.gap}". Verdien ignoreres. Gyldige verdier er: none, 2x-small, x-small, small, medium, large, x-large, 2x-large, 3x-large, 4x-large, 5x-large.`
+ );
+ this.gap = undefined;
+ }
+ }
+
+ // Valider justify. Ugyldige verdier ignoreres.
+ let justifyValue: LayoutJustify | undefined = this.justify;
+ if (changedProperties.has('justify')) {
+ if (this.justify !== undefined && !isValidLayoutJustify(this.justify)) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[nve-stack] Ugyldig verdi for 'justify': "${this.justify}". Verdien ignoreres. Gyldige verdier er: flex-start, flex-end, center, space-between, space-around, space-evenly, start, end, left, right.`
+ );
+ justifyValue = undefined;
+ }
+ }
+
+ if (justifyValue !== undefined) {
+ this.style.setProperty('--_stack-justify', justifyValue);
+ } else {
+ this.style.removeProperty('--_stack-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 db933f8f..fca4773c 100644
--- a/src/nve-designsystem.ts
+++ b/src/nve-designsystem.ts
@@ -6,6 +6,15 @@ export { default as NveAccordionItem } from './components/nve-accordion-item/nve
export { default as NveAlert } from './components/nve-alert/nve-alert.component';
export { default as NveAspectRose } from './components/nve-aspect-rose/nve-aspect-rose.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';