@klodd/ds 5.3.0 → 5.4.1

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/SKILL.md CHANGED
@@ -42,6 +42,65 @@ Denna Skill fångar både reglerna ("vad") och resonemanget ("varför").
42
42
  - `references/04-locked-decisions/` — låsta beslut. Förklara, ändra inte.
43
43
  - `references/05-open-decisions/` — öppna punkter. Presentera alternativen.
44
44
 
45
+ ## Asynkrona operationer
46
+
47
+ Apps med polling-driven progress (AI-analys, bank-synk, batch-jobs)
48
+ delar `KloddDS.AsyncProgress` (5.3.0+).
49
+
50
+ **CSS-komponent**: `.async-progress` (i `css/components/async-progress.css`).
51
+ BEM-element: `__header`, `__bar-track`, `__bar`, `__hint`, `__label`,
52
+ `__pct`. Döljs via `[hidden]`-attributet i alla states utom under
53
+ pågående polling. Bar-fyll via `--async-progress-accent`-token
54
+ (default `--accent-9`) — app/scope kan override:a utan att forka.
55
+
56
+ **JS-klass**: `window.KloddDS.AsyncProgress` (i `js/async-progress.js`).
57
+ Konfigurerbar via konstruktor-objekt:
58
+
59
+ ```javascript
60
+ const progress = new KloddDS.AsyncProgress({
61
+ progressEl: HTMLElement, // .async-progress-elementet
62
+ pollUrl: String, // GET-endpoint som returnerar JSON-kontrakt
63
+ pollInterval: 3000, // ms (default)
64
+ initialPct: 5,
65
+ initialLabel: "Startar...",
66
+ stageLabels: { // map: stage → string ELLER function(data, elapsedMs)
67
+ fetching: "Hämtar data...",
68
+ processing: (data, elapsedMs) =>
69
+ elapsedMs > 15000 ? "Slutför..." : "Bearbetar..."
70
+ },
71
+ simulation: { // valfri smooth-progress när backend pinnar pct
72
+ triggerStage: "processing",
73
+ startPct: 75, cap: 95,
74
+ ratePerSec: 0.25
75
+ },
76
+ transform: (data) => ({...}), // valfri: normalisera app-specifik payload
77
+ onComplete: (data) => {},
78
+ onError: (msg) => {},
79
+ maxErrors: 5,
80
+ doneLabel: "Klar"
81
+ });
82
+ progress.start(); // visar progressEl + startar polling
83
+ progress.stop(); // stoppar interval (terminal states gör det automatiskt)
84
+ ```
85
+
86
+ **Backend-kontrakt**: `pollUrl` ska returnera JSON med
87
+ `{ status, stage, pct, label }`. Status-domän:
88
+
89
+ - `pending` — inte startad
90
+ - `done` — klar (triggar onComplete)
91
+ - `failed` — fel (triggar onError efter maxErrors)
92
+ - Anything else (app-definierad) — pågående; app kontrollerar stage-labels
93
+
94
+ Apps vars endpoint redan returnerar exakt kontraktet behöver inte
95
+ `transform`-callbacken. Apps med prefix-payload (t.ex. Jubb's
96
+ `analysis_*`/`application_*`-fält i samma endpoint) använder
97
+ `transform` för att normalisera klient-sidigt — undviker fult
98
+ fetch-patch-mönster.
99
+
100
+ **Använd när**: en operation tar >2s och användaren ska se att den
101
+ händer. Apps idag: Jubb bolagsanalys + brevgenerering (5.3.0
102
+ migration). Ekonom bank-synk har endpoint, UI kommer i nästa sprint.
103
+
45
104
  ## Komponent-val
46
105
  1. Kolla `references/02-components.md` för befintlig komponent
47
106
  2. Föredra komposition över fork
@@ -52,12 +52,21 @@
52
52
  // som analysis_*/application_*) och behover normaliseras till
53
53
  // kontraktet. Default = identity.
54
54
  transform: null,
55
+ // fetchFn() -> Promise<{ status, stage, pct, label, ... }>.
56
+ // Overskriver intern fetch(pollUrl). Anvands av demo-sidor (mock-
57
+ // poller utan riktigt endpoint) och tester (deterministisk
58
+ // injection utan global fetch-patch). Per ADR 0016. Default
59
+ // = null -> intern fetch.
60
+ fetchFn: null,
55
61
  };
56
62
 
57
63
  class AsyncProgress {
58
64
  constructor(config) {
59
- if (!config || !config.progressEl || !config.pollUrl) {
60
- throw new Error("AsyncProgress: progressEl + pollUrl är obligatoriska");
65
+ if (!config || !config.progressEl) {
66
+ throw new Error("AsyncProgress: progressEl ar obligatorisk");
67
+ }
68
+ if (!config.pollUrl && !config.fetchFn) {
69
+ throw new Error("AsyncProgress: pollUrl eller fetchFn maste anges");
61
70
  }
62
71
  this.cfg = Object.assign({}, DEFAULTS, config);
63
72
  this.progressEl = this.cfg.progressEl;
@@ -127,12 +136,20 @@
127
136
 
128
137
  async _poll() {
129
138
  try {
130
- const res = await fetch(this.cfg.pollUrl);
131
- if (!res.ok) {
132
- this._handleFetchError(`HTTP ${res.status}`);
133
- return;
139
+ // fetchFn overskriver intern fetch (ADR 0016). Konsumenten
140
+ // ansvarar for HTTP-errors -> rejicera promise. Intern fetch
141
+ // mappar !res.ok till _handleFetchError som vanligt.
142
+ let data;
143
+ if (typeof this.cfg.fetchFn === "function") {
144
+ data = await this.cfg.fetchFn();
145
+ } else {
146
+ const res = await fetch(this.cfg.pollUrl);
147
+ if (!res.ok) {
148
+ this._handleFetchError(`HTTP ${res.status}`);
149
+ return;
150
+ }
151
+ data = await res.json();
134
152
  }
135
- let data = await res.json();
136
153
  if (typeof this.cfg.transform === "function") {
137
154
  data = this.cfg.transform(data);
138
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klodd/ds",
3
- "version": "5.3.0",
3
+ "version": "5.4.1",
4
4
  "description": "Klodd shared design system - tokens, components, JS",
5
5
  "main": "css/index.css",
6
6
  "bin": {
@@ -71,6 +71,57 @@ För varje entry gäller:
71
71
  - **Anvand:** Native `<dialog>` (centrerad modal) eller `<dialog class="sheet">` (bottom-attached)
72
72
  - **INTE:** Tooltip eller dropdown (egna komponenter)
73
73
  - **Tokens:** `--surface-raised/-overlay`, `--border-subtle`, `--shadow-float`, `--z-overlay`, `--safe-bottom`
74
+ - **Centrering:** `dialog.dialog:modal`-regeln (5.2.5) override:ar UA-stylesheet
75
+ med `!important` pa position/inset/margin/transform. Doumenterat undantag fran
76
+ kvalitetsbar regel 7 - UA's `dialog:modal` har implicit prioritering bortom
77
+ standard specificity-cascading.
78
+
79
+ #### Completion-dialog (async-operation done)
80
+
81
+ Vanlig instans: visa "X ar klart" + primar-action efter att en
82
+ asynkron operation (AsyncProgress) slagit `done`. Anvands av Jubb's
83
+ bolagsanalys + brevgenerering, och planerat for Ekonom bank-synk.
84
+
85
+ ```html
86
+ <dialog class="dialog" id="my-complete-dialog">
87
+ <div class="dialog__header">
88
+ <h2 class="heading-3">Operationen ar klar</h2>
89
+ </div>
90
+ <div class="dialog__body">
91
+ <p>Beskrivande text + ev. CTA-kontext.</p>
92
+ </div>
93
+ <div class="dialog__footer">
94
+ <button class="btn btn--primary" id="view-btn">Visa resultat</button>
95
+ </div>
96
+ </dialog>
97
+ ```
98
+
99
+ JS-monster:
100
+
101
+ ```javascript
102
+ const dlg = document.getElementById("my-complete-dialog");
103
+
104
+ // Oppna via showModal() - native focus-trap + ESC + ::backdrop
105
+ dlg.showModal();
106
+
107
+ // Stang via backdrop-klick (event.target == dialog-elementet)
108
+ dlg.addEventListener("click", (e) => {
109
+ if (e.target === dlg) dlg.close();
110
+ });
111
+
112
+ // Primar action - app-specifik (oftast navigation + reload)
113
+ document.getElementById("view-btn").addEventListener("click", () => {
114
+ window.location.href = "/result#anchor";
115
+ window.location.reload();
116
+ });
117
+ ```
118
+
119
+ Footer-varianter:
120
+ - En primar knapp (Visa resultat / Bekrafta) - default
121
+ - Sekundar + primar (Avbryt + Bekrafta) - destruktiva eller reversibla actions
122
+ - `.btn--danger` som primar - actions som tar bort/forstor data
123
+
124
+ Se /design-sidan "Completion Dialog"-sektionen for visuella exempel.
74
125
 
75
126
  ### icon (`icon.css`)
76
127
  - **Blocks:** `.icon`, `.icon-custom` (sibling - wrapper for handritade SVG som Lucide saknar)
@@ -0,0 +1,112 @@
1
+ # 0016 - AsyncProgress fetchFn-option
2
+
3
+ ## Status
4
+
5
+ Locked (2026-05-19).
6
+
7
+ ## Context
8
+
9
+ `KloddDS.AsyncProgress` (introducerad 5.3.0) använder intern `fetch()`
10
+ mot `pollUrl` för att hämta progress-payload. Konstruktor-validering
11
+ kastar fel om `pollUrl` saknas. Det fungerar för app-konsumenter
12
+ (Jubb's bolagsanalys, Ekonom's bank-synk) men blockerar två scenarier:
13
+
14
+ 1. **Demo-sidor (`/design` i Jubb)**: ingen riktig polling-endpoint
15
+ existerar. För att demonstrera komponenten visuellt har vi tidigare
16
+ tvingats köra parallell setTimeout-driven simulering vid sidan av
17
+ AsyncProgress-klassen - dupplicerar logik och visar inte hur
18
+ konsumenten faktiskt använder klassen.
19
+
20
+ 2. **Enhetstester**: tester måste idag mocka `window.fetch` globalt
21
+ eller köra ett riktigt test-endpoint för att verifiera
22
+ stage-byten, simulation-curve, done/failed-callbacks etc.
23
+ fetch-monkey-patch är fult och påverkar andra tester i samma
24
+ suite.
25
+
26
+ Båda problemen löser sig genom en opt-in `fetchFn`-option som
27
+ överskuggar den interna fetch-mekanismen.
28
+
29
+ ## Decision
30
+
31
+ Lägg till `fetchFn`-option på AsyncProgress-konstruktor:
32
+
33
+ ```javascript
34
+ new KloddDS.AsyncProgress({
35
+ // Ny option:
36
+ fetchFn: null, // default: använd intern fetch(pollUrl)
37
+
38
+ // Existerande:
39
+ progressEl, pollUrl, pollInterval, ...
40
+ })
41
+ ```
42
+
43
+ **Signatur**: `fetchFn: () => Promise<PollPayload>` där
44
+ `PollPayload = { status, stage, pct, label, ...passthrough }`
45
+ (samma kontrakt som backend-endpoint).
46
+
47
+ **Använding i poll-loopen**:
48
+
49
+ ```javascript
50
+ const data = this._options.fetchFn
51
+ ? await this._options.fetchFn()
52
+ : await fetch(this._options.pollUrl).then(r => r.json());
53
+ ```
54
+
55
+ **Validering**: konstruktor kastar fel om **både** `pollUrl` och
56
+ `fetchFn` saknas. Tidigare räckte det med pollUrl - nu accepteras
57
+ endera.
58
+
59
+ ## Consequences
60
+
61
+ **Positiva**:
62
+ - Design-sidan kan använda den faktiska AsyncProgress-komponenten
63
+ med mock-poller istället för parallell setTimeout-simulering. En
64
+ källa till sanning för komponentens beteende.
65
+ - Enhetstester kan injicera deterministisk mock-poller utan att
66
+ monkey-patcha global `fetch`.
67
+ - API-tillägget är opt-in: existerande konsumenter (Jubb's
68
+ brief-progress.js wrapper, framtida Ekonom bank-synk UI) påverkas
69
+ inte - de fortsätter använda `pollUrl` och får default-beteende.
70
+
71
+ **Negativa**:
72
+ - Publik API-utökning kräver minor-bump (5.3.0 → 5.4.0).
73
+ - Två kodvägar i poll-loopen (intern fetch vs fetchFn) - liten
74
+ komplexitetsökning. Defensiv if-branch.
75
+
76
+ ## Implementation notes
77
+
78
+ - `transform`-callbacken (introducerad 5.3.0) appliceras på resultatet
79
+ oavsett om data kommer från fetchFn eller intern fetch. Konsekvens:
80
+ apps kan kombinera fetchFn (mock) + transform (normalize app-payload)
81
+ i samma instans.
82
+ - Vid HTTP-fel via `fetchFn` ansvarar konsumenten för att kasta
83
+ Error/rejicera promise. AsyncProgress räknar konsekutiva
84
+ fel mot `maxErrors` som vanligt.
85
+
86
+ ## Användning
87
+
88
+ ```javascript
89
+ function createMockPoller(steps) {
90
+ let i = 0;
91
+ return () => Promise.resolve(
92
+ i < steps.length
93
+ ? steps[i++]
94
+ : { status: "done", stage: null, pct: 100, label: "Klar" }
95
+ );
96
+ }
97
+
98
+ new KloddDS.AsyncProgress({
99
+ progressEl: document.getElementById("demo-progress"),
100
+ fetchFn: createMockPoller([
101
+ { status: "generating", stage: null, pct: 25, label: "Steg 1..." },
102
+ { status: "generating", stage: null, pct: 50, label: "Steg 2..." },
103
+ { status: "generating", stage: null, pct: 75, label: "Steg 3..." },
104
+ { status: "done", stage: null, pct: 100, label: "Klar" },
105
+ ]),
106
+ pollInterval: 1500,
107
+ onComplete: () => console.log("done!"),
108
+ });
109
+ ```
110
+
111
+ Skapad ihop med deploy 5.4.0 (2026-05-19). Diskussion i sprint
112
+ "async-progress-fetch-fn-deploy".