From 252bd65a053ab41f166f3fb46a31df152000911a Mon Sep 17 00:00:00 2001 From: gruble Date: Wed, 10 Jun 2026 13:36:34 +0200 Subject: [PATCH 1/4] feat(nve-aspect-rose): Ny komponent for utsatte himmelretninger --- doc-site/components/Komponentoversikt.md | 7 ++ doc-site/components/nve-aspect-rose.md | 70 ++++++++++++++ .../nve-aspect-rose.component.ts | 93 ++++++++++++++++++ .../nve-aspect-rose/nve-aspect-rose.styles.ts | 26 +++++ .../nve-aspect-rose/nve-aspect-rose.test.ts | 94 +++++++++++++++++++ src/nve-designsystem.ts | 2 + 6 files changed, 292 insertions(+) create mode 100644 doc-site/components/nve-aspect-rose.md create mode 100644 src/components/nve-aspect-rose/nve-aspect-rose.component.ts create mode 100644 src/components/nve-aspect-rose/nve-aspect-rose.styles.ts create mode 100644 src/components/nve-aspect-rose/nve-aspect-rose.test.ts diff --git a/doc-site/components/Komponentoversikt.md b/doc-site/components/Komponentoversikt.md index a2b09374..df73bdc2 100644 --- a/doc-site/components/Komponentoversikt.md +++ b/doc-site/components/Komponentoversikt.md @@ -27,6 +27,13 @@ nodeId er ID til komponent-sida i Figma. Den ligger som en parameter i URL'en ti statusDesign: 'Ferdig', statusCode: 'Ferdig' }, + { + name: 'nve-aspect-rose', + nodeId: 'TODO', + description: undefined, + statusDesign: 'Ferdig', + statusCode: 'Ferdig' + }, { name: 'nve-attachments', nodeId: '647-192139', diff --git a/doc-site/components/nve-aspect-rose.md b/doc-site/components/nve-aspect-rose.md new file mode 100644 index 00000000..8b334471 --- /dev/null +++ b/doc-site/components/nve-aspect-rose.md @@ -0,0 +1,70 @@ +--- +layout: component +outline: [2, 3] +--- + + + +```html + +``` + + + +## Eksempler + +Legg eksempler på funksjonalitet her. Hvert tema skal ha egen overskrift på nivå 3. + +### Utsatte himmelretninger + +Bruk `value` for å sette hvilke himmelretninger som er eksponert. + + +```html + Ingen Kun nord + øst, sør-øst, sør og sør-vest + Alle +``` + + + +### Størrelse + +Bruk css-variabelen `--aspect-rose-size` for å endre bredde og høyde. 90px er standard. + + + +```html + 60px + 90px (standard) + 120px +``` + + + +### Språk + +Bruk `lang` for å angi språk for kompassretningene. `no` (norsk) er standard. Kun norsk og engelsk støttes. + + +```html + Norsk + Engelsk +``` + + + +### Aria-label og svg-title + +Bruk `label` for å overstyre standard-tekstene for aria-label og svg title. + + + +```html +Hold muspeker over for å se label.
+Med standard norsk tekst +Med standard engelsk tekst +Overstyrt tekst +``` + +
diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.component.ts b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts new file mode 100644 index 00000000..f801b35f --- /dev/null +++ b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts @@ -0,0 +1,93 @@ +import { html, svg, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { INveComponent } from '@interfaces/NveComponent.interface'; +import styles from './nve-aspect-rose.styles'; + +/** + * Viser utsatte himmelretninger som en kompassrose. + * Rosen er delt opp i 8 sektorer. Utsatte sektorer vises i rødt. + */ +@customElement('nve-aspect-rose') +export default class NveAspectRose extends LitElement implements INveComponent { + @property({ type: String }) testId: string | undefined = undefined; + + /** + * 8-tegns binærtekst som representerer utsatte sektorer. + * Starter med nordlig sektor og går deretter med klokka. + * Eksempel: "00111110" + */ + @property({ type: String }) value: string = '00000000'; + + /** Språk for himmelretningene. 'no' for norsk, 'en' for engelsk. */ + @property({ type: String }) lang: 'no' | 'en' = 'no'; + + /** + * Tilgjengelig tittel. + * Vises som aria-label på SVG-elementet og som i SVG. + * Standardverdi avhenger av språket: 'Eksponerte sektorer' for norsk, 'Affected aspects' for engelsk. + * Du kan overstyre denne teksten. + */ + @property({ type: String }) label: string | undefined = undefined; + + static styles = [styles]; + + private get effectiveLabel() { + return this.label ?? (this.lang === 'no' ? 'Eksponerte sektorer' : 'Affected aspects'); + } + + // Etiketter for de fire himmelretningene. Brukes til å plassere sirklene og teksten inni dem. + private get directions() { + return [ + { cx: 45, cy: 9, label: 'N' }, + { cx: 81, cy: 45, label: this.lang === 'no' ? 'Ø' : 'E' }, + { cx: 45, cy: 81, label: 'S' }, + { cx: 9, cy: 45, label: this.lang === 'no' ? 'V' : 'W' }, + ]; + } + + // Gradene for å rotere sektorene slik at de dekker riktig himmelretning. Starter med nordlig sektor og går deretter med klokka. + private readonly rotations = [-22.5, 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5]; + + render() { + return html` + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 90 90" + width="100%" + height="100%" + role="img" + aria-label=${this.effectiveLabel} + > + ${svg`<title>${this.effectiveLabel}`} + + ${this.rotations.map( + (rot, i) => svg` + + ` + )} + + + ${this.directions.map( + ({ cx, cy, label }) => svg` + + + ${label} + + ` + )} + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'nve-aspect-rose': NveAspectRose; + } +} diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts new file mode 100644 index 00000000..28be3c6b --- /dev/null +++ b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts @@ -0,0 +1,26 @@ +import { css } from 'lit'; + +export default css` + :host { + display: inline-block; + width: var(--aspect-rose-size, 90px); + height: var(--aspect-rose-size, 90px); + } + + text { + font-family: 'Source Sans 3', sans-serif; + font-weight: var(--font-weight-regular); + } + + .circle-outline { + stroke: #c6c6c5; + } + + .sector--affected { + fill: #d21523; + } + + .sector--unaffected { + fill: #e3e3e3; + } +`; diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.test.ts b/src/components/nve-aspect-rose/nve-aspect-rose.test.ts new file mode 100644 index 00000000..eea635fb --- /dev/null +++ b/src/components/nve-aspect-rose/nve-aspect-rose.test.ts @@ -0,0 +1,94 @@ +import { afterAll, describe, expect, it } from 'vitest'; +import { fixture, fixtureCleanup } from '@open-wc/testing'; +import { html } from 'lit'; +import NveAspectRose from './nve-aspect-rose.component'; + +if (!customElements.get('nve-aspect-rose')) { + customElements.define('nve-aspect-rose', NveAspectRose); +} + +describe('nve-aspect-rose', () => { + afterAll(() => { + fixtureCleanup(); + }); + + it('is attached to the DOM', async () => { + const el = await fixture(html``); + expect(document.body.contains(el)).toBe(true); + }); + + it('has correct default properties', async () => { + const el = await fixture(html``); + expect(el.value).toBe('00000000'); + expect(el.lang).toBe('no'); + expect(el.label).toBeUndefined(); + }); + + it('renders 8 sectors', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + expect(paths?.length).toBe(8); + }); + + it('marks affected sectors with correct css class', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + expect(paths?.[0].classList.contains('sector--affected')).toBe(true); + expect(paths?.[1].classList.contains('sector--unaffected')).toBe(true); + expect(paths?.[7].classList.contains('sector--affected')).toBe(true); + }); + + it('marks all sectors as unaffected when value is all zeros', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + paths?.forEach((path) => { + expect(path.classList.contains('sector--unaffected')).toBe(true); + }); + }); + + it('marks all sectors as affected when value is all ones', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + paths?.forEach((path) => { + expect(path.classList.contains('sector--affected')).toBe(true); + }); + }); + + it('shows norwegian direction labels by default', async () => { + const el = await fixture(html``); + const texts = el.shadowRoot?.querySelectorAll('text'); + const labels = Array.from(texts ?? []).map((t) => t.textContent); + expect(labels).toContain('N'); + expect(labels).toContain('Ø'); + expect(labels).toContain('S'); + expect(labels).toContain('V'); + }); + + it('shows english direction labels when lang is en', async () => { + const el = await fixture(html``); + const texts = el.shadowRoot?.querySelectorAll('text'); + const labels = Array.from(texts ?? []).map((t) => t.textContent); + expect(labels).toContain('N'); + expect(labels).toContain('E'); + expect(labels).toContain('S'); + expect(labels).toContain('W'); + }); + + it('uses custom label as aria-label', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Min tittel'); + }); + + it('uses default norwegian label when no label is set', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Eksponerte sektorer'); + }); + + it('uses default english label when lang is en and no label is set', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Affected aspects'); + }); +}); diff --git a/src/nve-designsystem.ts b/src/nve-designsystem.ts index 7f16f435..764696ce 100644 --- a/src/nve-designsystem.ts +++ b/src/nve-designsystem.ts @@ -1,8 +1,10 @@ /** Alle komponenter som er tilgjengelige, i alfabetisk rekkefølge. */ /** Denne filen blir genererert av pnpm run add-component */ + export { default as NveAccordion } from './components/nve-accordion/nve-accordion.component'; 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 NveAspectRose } from './components/nve-aspect-rose/nve-aspect-rose.component'; export { default as NveBadge } from './components/nve-badge/nve-badge.component'; export { default as NveButton } from './components/nve-button/nve-button.component'; export { default as NveCarousel } from './components/nve-carousel/nve-carousel.component'; From 3e923b6eba6a73d3d31ddf4a95b88e0bdb971bdd Mon Sep 17 00:00:00 2001 From: gruble Date: Wed, 10 Jun 2026 14:46:40 +0200 Subject: [PATCH 2/4] Fikset etter kommentarer fra copilot i PR --- doc-site/components/Komponentoversikt.md | 2 +- doc-site/components/nve-aspect-rose.md | 22 +++++++++--- .../nve-aspect-rose.component.ts | 19 +++++++---- .../nve-aspect-rose/nve-aspect-rose.styles.ts | 12 +++---- .../nve-aspect-rose/nve-aspect-rose.test.ts | 34 ++++++++++++++++--- 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/doc-site/components/Komponentoversikt.md b/doc-site/components/Komponentoversikt.md index df73bdc2..0521ae63 100644 --- a/doc-site/components/Komponentoversikt.md +++ b/doc-site/components/Komponentoversikt.md @@ -29,7 +29,7 @@ nodeId er ID til komponent-sida i Figma. Den ligger som en parameter i URL'en ti }, { name: 'nve-aspect-rose', - nodeId: 'TODO', + nodeId: undefined, description: undefined, statusDesign: 'Ferdig', statusCode: 'Ferdig' diff --git a/doc-site/components/nve-aspect-rose.md b/doc-site/components/nve-aspect-rose.md index 8b334471..debce8c8 100644 --- a/doc-site/components/nve-aspect-rose.md +++ b/doc-site/components/nve-aspect-rose.md @@ -13,8 +13,6 @@ outline: [2, 3] ## Eksempler -Legg eksempler på funksjonalitet her. Hvert tema skal ha egen overskrift på nivå 3. - ### Utsatte himmelretninger Bruk `value` for å sette hvilke himmelretninger som er eksponert. @@ -22,8 +20,9 @@ Bruk `value` for å sette hvilke himmelretninger som er eksponert. ```html Ingen Kun nord - øst, sør-øst, sør og sør-vest - Alle + øst, sør-øst, sør og sør-vest + Alle Verdien +blir ignorert hvis vi ikke har eksakt 8 sifre ``` @@ -68,3 +67,18 @@ Hold muspeker over for å se label.
``` + +### Farger + +Bruk css-variablene`--aspect-rose-outline-color`, `--aspect-rose-affected-color`og`--aspect-rose-unaffected-color` for å overstyre fargene. + + + +```html + +``` + + diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.component.ts b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts index f801b35f..53744778 100644 --- a/src/components/nve-aspect-rose/nve-aspect-rose.component.ts +++ b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts @@ -2,10 +2,15 @@ import { html, svg, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { INveComponent } from '@interfaces/NveComponent.interface'; import styles from './nve-aspect-rose.styles'; +import { ifDefined } from 'lit/directives/if-defined.js'; /** * Viser utsatte himmelretninger som en kompassrose. * Rosen er delt opp i 8 sektorer. Utsatte sektorer vises i rødt. + * @cssproperty --aspect-rose-size - Høyde og bredde på komponenten. 90px er standard. + * @cssproperty --aspect-rose-outline-color - Farge på sirkelens omriss. Standard er #c6c6c5. + * @cssproperty --aspect-rose-affected-color - Farge på utsatte sektorer. Standard er #d21523. + * @cssproperty --aspect-rose-unaffected-color - Farge på ikke-utsatte sektorer. Standard er #e3e3e3. */ @customElement('nve-aspect-rose') export default class NveAspectRose extends LitElement implements INveComponent { @@ -32,16 +37,16 @@ export default class NveAspectRose extends LitElement implements INveComponent { static styles = [styles]; private get effectiveLabel() { - return this.label ?? (this.lang === 'no' ? 'Eksponerte sektorer' : 'Affected aspects'); + return this.label ?? (this.lang === 'en' ? 'Affected aspects' : 'Eksponerte sektorer'); } // Etiketter for de fire himmelretningene. Brukes til å plassere sirklene og teksten inni dem. private get directions() { return [ { cx: 45, cy: 9, label: 'N' }, - { cx: 81, cy: 45, label: this.lang === 'no' ? 'Ø' : 'E' }, + { cx: 81, cy: 45, label: this.lang === 'en' ? 'E' : 'Ø' }, { cx: 45, cy: 81, label: 'S' }, - { cx: 9, cy: 45, label: this.lang === 'no' ? 'V' : 'W' }, + { cx: 9, cy: 45, label: this.lang === 'en' ? 'W' : 'V' }, ]; } @@ -49,6 +54,7 @@ export default class NveAspectRose extends LitElement implements INveComponent { private readonly rotations = [-22.5, 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5]; render() { + const value = /^[01]{8}$/.test(this.value) ? this.value : '00000000'; // Ignorer value hvis den ikke er gyldig return html` ${svg`${this.effectiveLabel}`} @@ -66,8 +73,8 @@ export default class NveAspectRose extends LitElement implements INveComponent { d="M 45 9 A 36 36 0 0 1 70.45584412271572 19.54415587728429 L 45 45 Z" transform="rotate(${rot} 45 45)" stroke-width="1" - stroke="#c6c6c5" - class=${this.value[i] === '1' ? 'sector--affected' : 'sector--unaffected'} + stroke="var(--aspect-rose-outline-color, #c6c6c5)" + class=${value[i] === '1' ? 'sector-affected' : 'sector-unaffected'} > ` )} @@ -76,7 +83,7 @@ export default class NveAspectRose extends LitElement implements INveComponent { ${this.directions.map( ({ cx, cy, label }) => svg` - + ${label} ` diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts index 28be3c6b..57f1c7b1 100644 --- a/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts +++ b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts @@ -13,14 +13,12 @@ export default css` } .circle-outline { - stroke: #c6c6c5; + stroke: var(--aspect-rose-outline-color, #c6c6c5); } - - .sector--affected { - fill: #d21523; + .sector-affected { + fill: var(--aspect-rose-affected-color, #d21523); } - - .sector--unaffected { - fill: #e3e3e3; + .sector-unaffected { + fill: var(--aspect-rose-unaffected-color, #e3e3e3); } `; diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.test.ts b/src/components/nve-aspect-rose/nve-aspect-rose.test.ts index eea635fb..fb5cfedb 100644 --- a/src/components/nve-aspect-rose/nve-aspect-rose.test.ts +++ b/src/components/nve-aspect-rose/nve-aspect-rose.test.ts @@ -33,16 +33,16 @@ describe('nve-aspect-rose', () => { it('marks affected sectors with correct css class', async () => { const el = await fixture(html``); const paths = el.shadowRoot?.querySelectorAll('path'); - expect(paths?.[0].classList.contains('sector--affected')).toBe(true); - expect(paths?.[1].classList.contains('sector--unaffected')).toBe(true); - expect(paths?.[7].classList.contains('sector--affected')).toBe(true); + expect(paths?.[0].classList.contains('sector-affected')).toBe(true); + expect(paths?.[1].classList.contains('sector-unaffected')).toBe(true); + expect(paths?.[7].classList.contains('sector-affected')).toBe(true); }); it('marks all sectors as unaffected when value is all zeros', async () => { const el = await fixture(html``); const paths = el.shadowRoot?.querySelectorAll('path'); paths?.forEach((path) => { - expect(path.classList.contains('sector--unaffected')).toBe(true); + expect(path.classList.contains('sector-unaffected')).toBe(true); }); }); @@ -50,7 +50,15 @@ describe('nve-aspect-rose', () => { const el = await fixture(html``); const paths = el.shadowRoot?.querySelectorAll('path'); paths?.forEach((path) => { - expect(path.classList.contains('sector--affected')).toBe(true); + expect(path.classList.contains('sector-affected')).toBe(true); + }); + }); + + it('marks all sectors as unaffected when value is not valid', async () => { + const el = await fixture(html``); + const paths = el.shadowRoot?.querySelectorAll('path'); + paths?.forEach((path) => { + expect(path.classList.contains('sector-unaffected')).toBe(true); }); }); @@ -64,6 +72,16 @@ describe('nve-aspect-rose', () => { expect(labels).toContain('V'); }); + it('shows norwegian direction labels by default even if lang is set to something else', async () => { + const el = await fixture(html``); + const texts = el.shadowRoot?.querySelectorAll('text'); + const labels = Array.from(texts ?? []).map((t) => t.textContent); + expect(labels).toContain('N'); + expect(labels).toContain('Ø'); + expect(labels).toContain('S'); + expect(labels).toContain('V'); + }); + it('shows english direction labels when lang is en', async () => { const el = await fixture(html``); const texts = el.shadowRoot?.querySelectorAll('text'); @@ -86,6 +104,12 @@ describe('nve-aspect-rose', () => { expect(svg?.getAttribute('aria-label')).toBe('Eksponerte sektorer'); }); + it('uses default norwegian label when no label is set even if lang is set to something else', async () => { + const el = await fixture(html``); + const svg = el.shadowRoot?.querySelector('svg'); + expect(svg?.getAttribute('aria-label')).toBe('Eksponerte sektorer'); + }); + it('uses default english label when lang is en and no label is set', async () => { const el = await fixture(html``); const svg = el.shadowRoot?.querySelector('svg'); From 5628a3bcf757f79b715564b97769b34c02d3dcfe Mon Sep 17 00:00:00 2001 From: gruble Date: Wed, 10 Jun 2026 14:48:47 +0200 Subject: [PATCH 3/4] Fikset begrep som var feil --- doc-site/components/nve-aspect-rose.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc-site/components/nve-aspect-rose.md b/doc-site/components/nve-aspect-rose.md index debce8c8..46d78da3 100644 --- a/doc-site/components/nve-aspect-rose.md +++ b/doc-site/components/nve-aspect-rose.md @@ -15,7 +15,7 @@ outline: [2, 3] ### Utsatte himmelretninger -Bruk `value` for å sette hvilke himmelretninger som er eksponert. +Bruk `value` for å sette hvilke himmelretninger som er utsatt (farlige). ```html From f83b0af9a1b4a8270ebb2db820a42da0e7b296c9 Mon Sep 17 00:00:00 2001 From: gruble Date: Fri, 12 Jun 2026 09:21:11 +0200 Subject: [PATCH 4/4] =?UTF-8?q?Flyttet=20tekst-st=C3=B8rrelse=20til=20css-?= =?UTF-8?q?fila=20og=20brukt=20rem=20i=20stedet=20for=20px?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/nve-aspect-rose/nve-aspect-rose.component.ts | 2 +- src/components/nve-aspect-rose/nve-aspect-rose.styles.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.component.ts b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts index 53744778..3ab680b3 100644 --- a/src/components/nve-aspect-rose/nve-aspect-rose.component.ts +++ b/src/components/nve-aspect-rose/nve-aspect-rose.component.ts @@ -84,7 +84,7 @@ export default class NveAspectRose extends LitElement implements INveComponent { ({ cx, cy, label }) => svg` - ${label} + ${label} ` )} diff --git a/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts index 57f1c7b1..8badcd9d 100644 --- a/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts +++ b/src/components/nve-aspect-rose/nve-aspect-rose.styles.ts @@ -10,6 +10,7 @@ export default css` text { font-family: 'Source Sans 3', sans-serif; font-weight: var(--font-weight-regular); + font-size: 0.67rem; } .circle-outline {