@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
|
package/js/async-progress.js
CHANGED
|
@@ -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
|
|
60
|
-
throw new Error("AsyncProgress: progressEl
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
@@ -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".
|