@sb1/indeks-web 0.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SpareBank 1 Utvikling
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @sb1/indeks-web
2
+
3
+ Web components for Indeks designsystemet. Komponenter som fungerer i alle rammeverk — og uten rammeverk.
4
+
5
+ ## 📦 Del av Indeks
6
+
7
+ Denne pakken er en del av [Indeks designsystemet](https://github.com/SpareBank1/indeks). Komponentene bruker CSS-klasser fra `@sb1/indeks-css`, som må lastes separat.
8
+
9
+ ## 📥 Installasjon
10
+
11
+ ### Via CDN (anbefalt)
12
+
13
+ ```html
14
+ <!-- Last inn CSS -->
15
+ <link rel="stylesheet" href="https://cdn.sparebank1.no/indeks/css/<versjon>.css" />
16
+
17
+ <!-- Last inn og registrer web components -->
18
+ <script type="module" src="https://cdn.sparebank1.no/indeks/web/<versjon>/index.js"></script>
19
+ ```
20
+
21
+ Etter at scriptet er lastet inn er alle komponenter tilgjengelige som HTML-elementer:
22
+
23
+ ```html
24
+ <ix-field>
25
+ <label>E-postadresse</label>
26
+ <input type="email" name="email" />
27
+ <span data-field="error"></span>
28
+ </ix-field>
29
+ ```
30
+
31
+ ### Via npm
32
+
33
+ ```bash
34
+ npm install @sb1/indeks-web @sb1/indeks-css
35
+ ```
36
+
37
+ ```js
38
+ import '@sb1/indeks-css';
39
+ import '@sb1/indeks-web'; // registrerer alle custom elements
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 🧩 Bidra — lage nye web components
45
+
46
+ ### Når hører noe hjemme i `indeks-web`?
47
+
48
+ En komponent hører hjemme her når **begge** disse er sanne:
49
+
50
+ 1. **Rammeverk-agnostisk behov** — komponenten skal fungere i prosjekter som ikke bruker React, f.eks. Vue, Angular, vanilla JS eller server-rendered HTML.
51
+
52
+ 2. **Ingen god native HTML-ekvivalent** — behovet kan ikke løses ved å bare legge CSS-klasser på et eksisterende HTML-element. Det mangler enten et passende element, eller koblingen mellom flere elementer er kompleks nok til at en komponent gir reell verdi.
53
+
54
+ Dersom behovet kun finnes i React-kontekst, hører komponenten hjemme i `indeks-react`. Dersom styling alene er nok, er en CSS-klasse i `indeks-css` riktig nivå.
55
+
56
+ ---
57
+
58
+ ### Designprinsipper
59
+
60
+ #### Enhance, ikke erstatt native HTML
61
+
62
+ Web components i denne pakken wrapper eller kobler native HTML-elementer — de erstatter dem ikke. Det betyr at:
63
+
64
+ - Nettleserens native atferd (validering, fokushåndtering, form-tilstand) forblir intakt.
65
+ - Alle native attributter (`required`, `disabled`, `name`, `value`, osv.) settes direkte på det native elementet av forfatteren.
66
+ - Komponenten bidrar med det som mangler: ARIA-koblinger, reaktiv tilstand, og koordinering mellom flere elementer.
67
+
68
+ #### Light DOM — ikke Shadow DOM
69
+
70
+ Komponentene bruker ikke Shadow DOM. Årsaken er todelt:
71
+
72
+ - CSS fra `@sb1/indeks-css` må nå inn til alle delelementer uten at forfatteren trenger å konfigurere noe.
73
+ - ARIA-attributter som `aria-describedby` og `aria-labelledby` refererer til IDer. Disse IDene må eksistere i det samme DOM-treet som elementene de kobler — noe Shadow DOM bryter.
74
+
75
+ #### `data-[komponent]="rolle"` som slot-konvensjon
76
+
77
+ Siden Shadow DOM-slots ikke brukes, identifiserer komponenter delelementer via `data-`-attributter:
78
+
79
+ ```
80
+ data-[komponentnavn]="rolle"
81
+ ```
82
+
83
+ For en komponent som heter `IxField` (`ix-field`) ser det slik ut:
84
+
85
+ ```html
86
+ <ix-field>
87
+ <label>Navn</label>
88
+ <span data-field="description">Fullt navn som det står i passet</span>
89
+ <input type="text" name="name" />
90
+ <span data-field="error"></span>
91
+ </ix-field>
92
+ ```
93
+
94
+ Bruk `slot`-attributtet til Shadow DOM-innhold er et reservert nøkkelord knytt til Shadow DOM. `data-[komponent]` er eksplisitt egendefinert og kolliderer ikke med noe nettleser-API.
95
+
96
+ ---
97
+
98
+ ### Slik lager du en ny komponent
99
+
100
+ #### 1. Filstruktur
101
+
102
+ ```
103
+ lib/
104
+ └── components/
105
+ └── [navn]/
106
+ └── Ix[Navn].ts
107
+ ```
108
+
109
+ Eksempel for en ny `IxTooltip`:
110
+
111
+ ```
112
+ lib/
113
+ └── components/
114
+ └── tooltip/
115
+ └── IxTooltip.ts
116
+ ```
117
+
118
+ #### 2. Klassen
119
+
120
+ Extend `HTMLElement` direkte. Ikke bruk basisklasser eller hjelpefunksjoner med mindre det er et dokumentert mønster i pakken.
121
+
122
+ ```ts
123
+ export class IxTooltip extends HTMLElement {
124
+ connectedCallback(): void {
125
+ // Sett opp koblingene her
126
+ }
127
+
128
+ disconnectedCallback(): void {
129
+ // Rydd opp observers og event listeners her
130
+ }
131
+ }
132
+ ```
133
+
134
+ `connectedCallback` kalles når elementet kobles til DOM. `disconnectedCallback` kalles når det fjernes — rydd alltid opp `MutationObserver`, `ResizeObserver` og event listeners her for å unngå minnelekkasje.
135
+
136
+ #### 3. Registrer i `index.ts`
137
+
138
+ ```ts
139
+ // index.ts
140
+ import { IxTooltip } from './lib/components/tooltip/IxTooltip.js';
141
+
142
+ customElements.define('ix-tooltip', IxTooltip);
143
+
144
+ export { IxTooltip };
145
+ ```
146
+
147
+ Navnekonvensjon: klassen heter `Ix[PascalCase]`, custom element-taggen heter `ix-[kebab-case]`.
148
+
149
+ ---
150
+
151
+ ### CSS
152
+
153
+ Komponenter skal bruke eksisterende CSS-klasser fra `@sb1/indeks-css`. Ikke bundle CSS inn i JS-en, og ikke skriv komponent-spesifikk CSS i denne pakken — CSS-en hører hjemme i `indeks-css`.
154
+
155
+ Legg til CSS-klasser på elementer i `connectedCallback`:
156
+
157
+ ```ts
158
+ this.classList.add('ix-input-wrapper');
159
+ ```
160
+
161
+ ---
162
+
163
+ ### Bygg og verifiser
164
+
165
+ ```bash
166
+ # npm-bygg (med TypeScript-typer)
167
+ pnpm --filter @sb1/indeks-web build
168
+
169
+ # CDN-bygg (minifisert, self-contained bundle)
170
+ pnpm --filter @sb1/indeks-web build:cdn
171
+ ```
172
+
173
+ `build` produserer `dist/npm/index.js` og `dist/npm/index.d.ts`.
174
+ `build:cdn` produserer `dist/cdn/index.js` — en minifisert ESM-bundle klar for opplasting til CDN.
175
+
176
+ ---
177
+
178
+ ## 🔗 Relaterte pakker
179
+
180
+ - [@sb1/indeks-css](https://www.npmjs.com/package/@sb1/indeks-css) — CSS-styling (kreves på siden)
181
+ - [@sb1/indeks-react](https://www.npmjs.com/package/@sb1/indeks-react) — React-komponenter
182
+ - [@sb1/indeks-tokens](https://www.npmjs.com/package/@sb1/indeks-tokens) — Design tokens
183
+
184
+ ## 📚 Dokumentasjon
185
+
186
+ Full dokumentasjon finnes på:
187
+
188
+ **[Midlertidig lenke Indeks](https://automatic-meme-yv23n9e.pages.github.io/)**
189
+
190
+ ## 📄 Lisens
191
+
192
+ MIT
@@ -0,0 +1,2 @@
1
+ import { IxField } from './lib/components/field/IxField.js';
2
+ export { IxField };
@@ -0,0 +1,92 @@
1
+ //#region \0@oxc-project+runtime@0.115.0/helpers/typeof.js
2
+ function _typeof(o) {
3
+ "@babel/helpers - typeof";
4
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
5
+ return typeof o;
6
+ } : function(o) {
7
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
8
+ }, _typeof(o);
9
+ }
10
+ //#endregion
11
+ //#region \0@oxc-project+runtime@0.115.0/helpers/toPrimitive.js
12
+ function toPrimitive(t, r) {
13
+ if ("object" != _typeof(t) || !t) return t;
14
+ var e = t[Symbol.toPrimitive];
15
+ if (void 0 !== e) {
16
+ var i = e.call(t, r || "default");
17
+ if ("object" != _typeof(i)) return i;
18
+ throw new TypeError("@@toPrimitive must return a primitive value.");
19
+ }
20
+ return ("string" === r ? String : Number)(t);
21
+ }
22
+ //#endregion
23
+ //#region \0@oxc-project+runtime@0.115.0/helpers/toPropertyKey.js
24
+ function toPropertyKey(t) {
25
+ var i = toPrimitive(t, "string");
26
+ return "symbol" == _typeof(i) ? i : i + "";
27
+ }
28
+ //#endregion
29
+ //#region \0@oxc-project+runtime@0.115.0/helpers/defineProperty.js
30
+ function _defineProperty(e, r, t) {
31
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
32
+ value: t,
33
+ enumerable: !0,
34
+ configurable: !0,
35
+ writable: !0
36
+ }) : e[r] = t, e;
37
+ }
38
+ //#endregion
39
+ //#region lib/components/field/IxField.ts
40
+ var fieldCounter = 0;
41
+ var IxField = class extends HTMLElement {
42
+ constructor(..._args) {
43
+ super(..._args);
44
+ _defineProperty(this, "_observer", null);
45
+ }
46
+ connectedCallback() {
47
+ this._wire();
48
+ }
49
+ disconnectedCallback() {
50
+ this._observer?.disconnect();
51
+ this._observer = null;
52
+ }
53
+ _wire() {
54
+ const control = this.querySelector("input, select, textarea");
55
+ const label = this.querySelector("label");
56
+ const description = this.querySelector("[data-field=\"description\"]");
57
+ const error = this.querySelector("[data-field=\"error\"]");
58
+ if (!control) return;
59
+ if (!control.id) control.id = `ix-field-${++fieldCounter}`;
60
+ if (label && !label.htmlFor) label.htmlFor = control.id;
61
+ const describedBy = [];
62
+ if (description) {
63
+ if (!description.id) description.id = `${control.id}-description`;
64
+ describedBy.push(description.id);
65
+ }
66
+ if (error) {
67
+ if (!error.id) error.id = `${control.id}-error`;
68
+ error.setAttribute("role", "alert");
69
+ error.setAttribute("aria-live", "polite");
70
+ describedBy.push(error.id);
71
+ this._observer = new MutationObserver(() => {
72
+ this._syncInvalid(control, error);
73
+ });
74
+ this._observer.observe(error, {
75
+ childList: true,
76
+ characterData: true,
77
+ subtree: true
78
+ });
79
+ this._syncInvalid(control, error);
80
+ }
81
+ if (describedBy.length > 0) control.setAttribute("aria-describedby", describedBy.join(" "));
82
+ }
83
+ _syncInvalid(control, error) {
84
+ if (error.textContent?.trim()) control.setAttribute("aria-invalid", "true");
85
+ else control.removeAttribute("aria-invalid");
86
+ }
87
+ };
88
+ //#endregion
89
+ //#region index.ts
90
+ customElements.define("ix-field", IxField);
91
+ //#endregion
92
+ export { IxField };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * IxField — tilgjengelighetslim mellom label, input og hjelpetekster.
3
+ *
4
+ * ## Designfilosofi
5
+ *
6
+ * Komponenten erstatter ingen native HTML-elementer. `<label>`, `<input>`,
7
+ * `<select>`, `<textarea>` og hjelpetekster skrives som vanlig HTML inne i
8
+ * `<ix-field>`. Komponenten sin eneste jobb er å sette opp ARIA-koblingene
9
+ * mellom dem — arbeid som er kjedelig å gjøre manuelt og lett å gjøre feil.
10
+ *
11
+ * Denne tilnærmingen betyr at:
12
+ * - Native nettleser-atferd, validering og styling forblir intakt.
13
+ * - Alle form-attributter (`required`, `disabled`, `name`, `value`, osv.)
14
+ * settes direkte på native elementet — ikke som props til en wrapper.
15
+ * - Komponenten kan brukes med alle typer input uten kodeendringer.
16
+ *
17
+ * ## Hva komponenten gjør
18
+ *
19
+ * 1. Kobler `<label>` til input via `for`/`id`-paret.
20
+ * Genererer en stabil ID på input om den mangler en.
21
+ *
22
+ * 2. Setter `aria-describedby` på input med IDer til description og
23
+ * error-elementet, slik at skjermlesere leser dem opp etter label og verdi.
24
+ *
25
+ * 3. Setter `role="alert"` og `aria-live="polite"` på error-elementet.
26
+ * Error-elementet skal alltid ligge i DOM — tomt betyr ingen feil.
27
+ * Dette er et krav fra ARIA Live Regions: elementet må eksistere i DOM
28
+ * *før* innholdet settes inn for at skjermlesere skal fange opp endringen.
29
+ * Hadde vi injisert elementet dynamisk ville mange skjermlesere gått glipp
30
+ * av meldingen.
31
+ *
32
+ * 4. Bruker MutationObserver på error-elementet. Når innholdet endres:
33
+ * - Ikke-tomt → `aria-invalid="true"` på input (visuell og semantisk feil)
34
+ * - Tomt → `aria-invalid` fjernes
35
+ * Applikasjonen styrer altså feilmelding ved å sette/tømme textContent —
36
+ * komponenten holder input-tilstanden synkronisert automatisk.
37
+ *
38
+ * ## Identifisering av delelementer
39
+ *
40
+ * Komponenten finner delelementer via:
41
+ * - `<label>` → første label-element
42
+ * - `input | select | textarea` → første native form-kontroll
43
+ * - `[data-field="description"]` → hjelpetekst
44
+ * - `[data-field="error"]` → feilmelding (alltid til stede, kan være tom)
45
+ *
46
+ * `data-field` brukes fremfor `slot`-attributtet selv om Shadow DOM ikke er i
47
+ * bruk. `slot` er et reservert nøkkelord knytt til Shadow DOM og ville vært
48
+ * misvisende, samt kunne krasje om `<ix-field>` senere pakkes inn i en
49
+ * komponent med Shadow DOM.
50
+ *
51
+ * ## Ingen Shadow DOM
52
+ *
53
+ * Komponenten bruker light DOM. Det betyr at CSS fra `@sb1/indeks-css` når
54
+ * alle delelementer uten videre, og at ARIA-attributtene er fullt synlige i
55
+ * den tilgjengelighetstreet nettleseren eksponerer til hjelpemiddelteknologi.
56
+ *
57
+ * @example Grunnleggende bruk
58
+ * <ix-field>
59
+ * <label>E-postadresse</label>
60
+ * <span data-field="description">Vi sender kvittering til denne adressen</span>
61
+ * <input type="email" name="email" />
62
+ * <span data-field="error"></span>
63
+ * </ix-field>
64
+ *
65
+ * @example Sett feilmelding programmatisk
66
+ * const errorEl = document.querySelector('ix-field [data-field="error"]');
67
+ * errorEl.textContent = 'Oppgi en gyldig e-postadresse';
68
+ * // → input får automatisk aria-invalid="true"
69
+ *
70
+ * errorEl.textContent = '';
71
+ * // → aria-invalid fjernes
72
+ */
73
+ export declare class IxField extends HTMLElement {
74
+ private _observer;
75
+ connectedCallback(): void;
76
+ disconnectedCallback(): void;
77
+ private _wire;
78
+ private _syncInvalid;
79
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@sb1/indeks-web",
3
+ "version": "0.2.20",
4
+ "type": "module",
5
+ "description": "Web components for Indeks design system",
6
+ "homepage": "https://design.sparebank1.no",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/SpareBank1/indeks",
10
+ "directory": "indeks-web"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/SpareBank1/indeks/issues"
14
+ },
15
+ "keywords": [
16
+ "indeks",
17
+ "design-system",
18
+ "web-components",
19
+ "custom-elements",
20
+ "sparebank1",
21
+ "ui"
22
+ ],
23
+ "main": "dist/npm/index.js",
24
+ "module": "dist/npm/index.js",
25
+ "types": "dist/npm/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/npm/index.js",
29
+ "types": "./dist/npm/index.d.ts"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist/npm",
34
+ "CHANGELOG.md",
35
+ "README.md"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "author": "",
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "@types/node": "^25.3.0",
44
+ "typescript": "~5.9.3",
45
+ "vite": "^8.0.0",
46
+ "vite-plugin-dts": "^4.5.4"
47
+ },
48
+ "scripts": {
49
+ "build": "rm -rf ./dist && pnpm run build:npm",
50
+ "build:npm": "rm -rf ./dist/npm && vite build --mode npm",
51
+ "build:cdn": "rm -rf ./dist/cdn && vite build --mode cdn",
52
+ "lint": "eslint .",
53
+ "test": "echo \"Ingen tester\""
54
+ }
55
+ }