@klodd/ds 5.2.7 → 5.4.0

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
@@ -0,0 +1,70 @@
1
+ /* ================================================================
2
+ components/async-progress.css
3
+
4
+ Generisk polling-driven progress-UI for asynkrona operationer.
5
+ Backend-kontrakt: { status, stage, pct, label }.
6
+
7
+ Anvands av:
8
+ - Jubb /jobb/{id} (bolagsanalys + brevgenerering)
9
+ - Ekonom bank-synk (planerad)
10
+ - Framtida appar med asynkrona operationer
11
+
12
+ Ersatter .brief-progress (Jubb-specifik) som lyftes till
13
+ generaliserad .async-progress under 5.3.0-sprinten.
14
+
15
+ Samma visuella formspraket som .pipeline-progress (6px-bar,
16
+ 0.6s ease-in-out width-transition, sunken-track) men sibling-
17
+ block i stallet for shared - tydligare BEM-namn per use-case.
18
+
19
+ Blocks:
20
+ .async-progress - root, dolj med [hidden] vid pending/done/failed
21
+
22
+ BEM-element:
23
+ __header - flex-row: label vänster, pct hoger
24
+ __bar-track - bakgrund-track 6px hojd
25
+ __bar - accent-fyllning, width transition
26
+ __hint - hint-text under bar (valfri)
27
+ __label - inline label-element
28
+ __pct - inline pct-element
29
+
30
+ Tokens: --surface-sunken, --async-progress-accent (default
31
+ --accent-9), --text-default, --text-muted, --space-6/-8/-12/-16,
32
+ --radius-full, --fs-12/-13.
33
+
34
+ --async-progress-accent-token tillater app/scope-override av
35
+ bar-fillfargen utan att forka komponenten. Default = --accent-9
36
+ (app:s accent via data-app-theming).
37
+ ================================================================ */
38
+
39
+ .async-progress {
40
+ margin: var(--space-12) 0 var(--space-16);
41
+ }
42
+
43
+ .async-progress__header {
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: baseline;
47
+ margin-bottom: var(--space-8);
48
+ font-size: var(--fs-13);
49
+ color: var(--text-default);
50
+ }
51
+
52
+ .async-progress__bar-track {
53
+ height: 6px;
54
+ background: var(--surface-sunken);
55
+ border-radius: var(--radius-full);
56
+ overflow: hidden;
57
+ }
58
+
59
+ .async-progress__bar {
60
+ height: 100%;
61
+ background: var(--async-progress-accent, var(--accent-9));
62
+ border-radius: inherit;
63
+ transition: width 0.6s ease-in-out;
64
+ }
65
+
66
+ .async-progress__hint {
67
+ margin: var(--space-6) 0 0;
68
+ font-size: var(--fs-12);
69
+ color: var(--text-muted);
70
+ }
@@ -1,37 +1,28 @@
1
1
  /* ================================================================
2
2
  components/progress-bar.css
3
3
 
4
- Två sibling-blocks for polling-driven progress-UI:
5
-
6
4
  .pipeline-progress - overst pa /korningar nar DigestRun pagar
7
- (cron-morning/cron-alert/cron-signals)
8
- .brief-progress - pa /jobb/{id} nar bolagsanalys eller
9
- brevgenerering pagar (job_briefs)
5
+ (cron-morning/cron-alert/cron-signals i Jubb)
6
+
7
+ Visuellt formspraket: 6px-bar, 0.6s ease-in-out width-transition,
8
+ accent-9-fill, sunken-track.
10
9
 
11
- Samma visuella formspraket (6px-bar, 0.6s ease-in-out width-transition,
12
- accent-9-fill, sunken-track) men separata block-klasser sa de inte krockar
13
- med varandra. Olika DOM-id:n och olika underlag (digest_runs vs
14
- job_briefs) - hade kunnat slas ihop till en .progress-bar med
15
- modifiers, men kept separata for tydligare BEM-block per use-case.
10
+ Sibling-block .async-progress lever i async-progress.css for
11
+ generiska asynkrona operationer (Jubb bolagsanalys, Ekonom bank-
12
+ synk, framtida appar). Bagge delar visuellt språk men har egna
13
+ block-namn for tydligare BEM-blockning per use-case.
16
14
 
17
15
  Blocks:
18
16
  .pipeline-progress - root, /korningar
19
- .brief-progress - root, /jobb/{id}
20
17
 
21
- BEM-element (samma per block):
18
+ BEM-element:
22
19
  __header - flex-row: label vänster, pct hoger
23
20
  __bar-track - bakgrund-track 6px hojd
24
21
  __bar - accent-fyllning, width transition
25
- __sub - sub-text under bar (.pipeline-progress)
26
- __hint - hint-text under bar (.brief-progress)
22
+ __sub - sub-text under bar
27
23
  __label - inline label-element (anvandbart pa /design)
28
24
  __pct - inline pct-element (anvandbart pa /design)
29
25
 
30
- Lyftad fran jubb.css 2026-05-18 till komponent-mappen sa /design
31
- kan ladda bara denna fil utan att dra in hela Jubb-domain-CSS:n
32
- (per Calles brief 2026-05-07: ingenting i styleguiden far paverka
33
- appens overiga CSS).
34
-
35
26
  Tokens: --surface-sunken, --accent-9, --text-default, --text-muted,
36
27
  --space-6/-8/-12/-16, --radius-full, --fs-12/-13.
37
28
  ================================================================ */
@@ -72,39 +63,3 @@
72
63
  font-size: var(--fs-12);
73
64
  color: var(--text-muted);
74
65
  }
75
-
76
-
77
- /* ─── .brief-progress (/jobb/{id}) ─────────────────────────────── */
78
-
79
- .brief-progress {
80
- margin: var(--space-12) 0 var(--space-16);
81
- }
82
-
83
- .brief-progress__header {
84
- display: flex;
85
- justify-content: space-between;
86
- align-items: baseline;
87
- margin-bottom: var(--space-8);
88
- font-size: var(--fs-13);
89
- color: var(--text-default);
90
- }
91
-
92
- .brief-progress__bar-track {
93
- height: 6px;
94
- background: var(--surface-sunken);
95
- border-radius: var(--radius-full);
96
- overflow: hidden;
97
- }
98
-
99
- .brief-progress__bar {
100
- height: 100%;
101
- background: var(--accent-9);
102
- border-radius: inherit;
103
- transition: width 0.6s ease-in-out;
104
- }
105
-
106
- .brief-progress__hint {
107
- margin: var(--space-6) 0 0;
108
- font-size: var(--fs-12);
109
- color: var(--text-muted);
110
- }
package/css/index.css CHANGED
@@ -36,6 +36,7 @@
36
36
  @import './components/divider.css';
37
37
  @import './components/progress.css';
38
38
  @import './components/progress-bar.css';
39
+ @import './components/async-progress.css';
39
40
  @import './components/tooltip.css';
40
41
  @import './components/dropdown.css';
41
42
 
@@ -0,0 +1,224 @@
1
+ /* @klodd/ds - AsyncProgress
2
+ *
3
+ * Generisk polling-driven progress-controller for asynkrona
4
+ * operationer. Backend-kontrakt: GET pollUrl → JSON
5
+ * { status, stage, pct, label }.
6
+ *
7
+ * Status-domän:
8
+ * pending - inte startat
9
+ * <ev. app> - app-specifika status under generating
10
+ * done - klart (triggar onComplete)
11
+ * failed - fel (triggar onError efter maxErrors)
12
+ *
13
+ * Lyftad fran Jubb's brief-progress.js (sprint 5.3.0). Vet inte
14
+ * om AI, Claude, banker eller specifika apps - bara om kontraktet.
15
+ *
16
+ * Anvandning:
17
+ *
18
+ * const progress = new KloddDS.AsyncProgress({
19
+ * progressEl: document.getElementById("my-progress"),
20
+ * pollUrl: "/api/foo/123/progress",
21
+ * stageLabels: {
22
+ * step_a: "Forsta steget...",
23
+ * step_b: (data) => data.pct < 50 ? "Forsta..." : "Slutfor...",
24
+ * },
25
+ * simulation: { triggerStage: "step_b", startPct: 75, cap: 95, ratePerSec: 0.25 },
26
+ * onComplete: () => openModal(),
27
+ * onError: (msg) => showToast(msg, "error"),
28
+ * });
29
+ * progress.start();
30
+ *
31
+ * Krav pa progressEl (BEM .async-progress):
32
+ * - id="..." (en barn-element querySelector via id)
33
+ * - innehåller __label, __pct och __bar med data-element-attribut
34
+ * ELLER querySelectable via .async-progress__label etc.
35
+ * - kan ha [hidden] satt - start() tar bort attributet
36
+ */
37
+ (function (root) {
38
+ "use strict";
39
+
40
+ const DEFAULTS = {
41
+ pollInterval: 3000,
42
+ initialPct: 5,
43
+ initialLabel: "Startar...",
44
+ stageLabels: {},
45
+ simulation: null,
46
+ onComplete: null,
47
+ onError: null,
48
+ maxErrors: 5,
49
+ doneLabel: "Klar",
50
+ // transform(data) -> { status, stage, pct, label, ...passthrough }.
51
+ // Anvands nar backend-payload har app-specifika faltnamn (prefix
52
+ // som analysis_*/application_*) och behover normaliseras till
53
+ // kontraktet. Default = identity.
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,
61
+ };
62
+
63
+ class AsyncProgress {
64
+ constructor(config) {
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");
70
+ }
71
+ this.cfg = Object.assign({}, DEFAULTS, config);
72
+ this.progressEl = this.cfg.progressEl;
73
+ this.barEl = this.progressEl.querySelector(".async-progress__bar");
74
+ this.labelEl = this.progressEl.querySelector(".async-progress__label");
75
+ this.pctEl = this.progressEl.querySelector(".async-progress__pct");
76
+
77
+ this.pollTimer = null;
78
+ this.consecutiveErrors = 0;
79
+ this.simulationStart = null;
80
+ this.lastStage = null;
81
+ this.completed = false;
82
+ this.failed = false;
83
+ }
84
+
85
+ start() {
86
+ this.progressEl.removeAttribute("hidden");
87
+ this._setBar(this.cfg.initialPct, this.cfg.initialLabel);
88
+ this.simulationStart = null;
89
+ this.lastStage = null;
90
+ this.completed = false;
91
+ this.failed = false;
92
+ this.consecutiveErrors = 0;
93
+ if (this.pollTimer !== null) return;
94
+ // Forsta poll efter 100ms sa ev. fetch-patches (auth-tokens etc)
95
+ // hinner appliceras.
96
+ setTimeout(() => this._poll(), 100);
97
+ this.pollTimer = setInterval(() => this._poll(), this.cfg.pollInterval);
98
+ }
99
+
100
+ stop() {
101
+ if (this.pollTimer !== null) {
102
+ clearInterval(this.pollTimer);
103
+ this.pollTimer = null;
104
+ }
105
+ }
106
+
107
+ _setBar(pct, label) {
108
+ const clampedPct = Math.max(0, Math.min(100, Math.round(pct)));
109
+ if (this.barEl) this.barEl.style.width = clampedPct + "%";
110
+ if (this.pctEl) this.pctEl.textContent = clampedPct + "%";
111
+ if (this.labelEl && label) this.labelEl.textContent = label;
112
+ }
113
+
114
+ _getLabel(data, elapsedSim) {
115
+ if (data.status === "done") return this.cfg.doneLabel;
116
+ const stageEntry = this.cfg.stageLabels[data.stage];
117
+ if (typeof stageEntry === "function") {
118
+ return stageEntry(data, elapsedSim);
119
+ }
120
+ if (typeof stageEntry === "string") {
121
+ return stageEntry;
122
+ }
123
+ // Fallback: backend's label
124
+ return data.label || this.cfg.initialLabel;
125
+ }
126
+
127
+ _applySimulation(realPct, data) {
128
+ const sim = this.cfg.simulation;
129
+ if (!sim || data.stage !== sim.triggerStage) return realPct;
130
+ if (this.simulationStart == null) return realPct;
131
+ const elapsedSec = (Date.now() - this.simulationStart) / 1000;
132
+ const simulated = sim.startPct + elapsedSec * sim.ratePerSec;
133
+ const capped = Math.min(simulated, sim.cap);
134
+ return Math.max(realPct, capped);
135
+ }
136
+
137
+ async _poll() {
138
+ try {
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();
152
+ }
153
+ if (typeof this.cfg.transform === "function") {
154
+ data = this.cfg.transform(data);
155
+ }
156
+ this.consecutiveErrors = 0;
157
+
158
+ // Stage-byte: trigga simulation-timer
159
+ const sim = this.cfg.simulation;
160
+ if (sim) {
161
+ if (data.stage === sim.triggerStage && this.lastStage !== sim.triggerStage) {
162
+ this.simulationStart = Date.now();
163
+ } else if (data.stage !== sim.triggerStage) {
164
+ this.simulationStart = null;
165
+ }
166
+ }
167
+ this.lastStage = data.stage;
168
+
169
+ // Terminal states
170
+ if (data.status === "failed") {
171
+ if (this.failed) return;
172
+ this.failed = true;
173
+ this.stop();
174
+ this.progressEl.setAttribute("hidden", "");
175
+ if (typeof this.cfg.onError === "function") {
176
+ this.cfg.onError(data.label || "Operationen misslyckades.");
177
+ }
178
+ return;
179
+ }
180
+
181
+ if (data.status === "done") {
182
+ if (this.completed) return;
183
+ this.completed = true;
184
+ this._setBar(100, this.cfg.doneLabel);
185
+ this.stop();
186
+ if (typeof this.cfg.onComplete === "function") {
187
+ this.cfg.onComplete(data);
188
+ }
189
+ return;
190
+ }
191
+
192
+ // Pågående: render pct + label
193
+ const realPct = data.pct || 0;
194
+ const elapsedSim = this.simulationStart
195
+ ? Date.now() - this.simulationStart
196
+ : null;
197
+ const effectivePct = this._applySimulation(realPct, data);
198
+ const label = this._getLabel(data, elapsedSim);
199
+ this._setBar(effectivePct, label);
200
+ } catch (err) {
201
+ this._handleFetchError(err.message || String(err));
202
+ }
203
+ }
204
+
205
+ _handleFetchError(msg) {
206
+ this.consecutiveErrors += 1;
207
+ if (this.consecutiveErrors >= this.cfg.maxErrors) {
208
+ if (this.failed) return;
209
+ this.failed = true;
210
+ this.stop();
211
+ this.progressEl.setAttribute("hidden", "");
212
+ if (typeof this.cfg.onError === "function") {
213
+ this.cfg.onError(`Polling-fel efter ${this.cfg.maxErrors} forsok: ${msg}`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // Exponera pa KloddDS-namespace. Skapa objektet om det inte finns
220
+ // (defensiv mot oklar laddningsordning - om async-progress.js laddas
221
+ // fore index.js).
222
+ root.KloddDS = root.KloddDS || {};
223
+ root.KloddDS.AsyncProgress = AsyncProgress;
224
+ })(typeof window !== "undefined" ? window : globalThis);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klodd/ds",
3
- "version": "5.2.7",
3
+ "version": "5.4.0",
4
4
  "description": "Klodd shared design system - tokens, components, JS",
5
5
  "main": "css/index.css",
6
6
  "bin": {
@@ -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".