@klodd/ds 5.2.7 → 5.3.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.
@@ -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,207 @@
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
+ };
56
+
57
+ class AsyncProgress {
58
+ constructor(config) {
59
+ if (!config || !config.progressEl || !config.pollUrl) {
60
+ throw new Error("AsyncProgress: progressEl + pollUrl är obligatoriska");
61
+ }
62
+ this.cfg = Object.assign({}, DEFAULTS, config);
63
+ this.progressEl = this.cfg.progressEl;
64
+ this.barEl = this.progressEl.querySelector(".async-progress__bar");
65
+ this.labelEl = this.progressEl.querySelector(".async-progress__label");
66
+ this.pctEl = this.progressEl.querySelector(".async-progress__pct");
67
+
68
+ this.pollTimer = null;
69
+ this.consecutiveErrors = 0;
70
+ this.simulationStart = null;
71
+ this.lastStage = null;
72
+ this.completed = false;
73
+ this.failed = false;
74
+ }
75
+
76
+ start() {
77
+ this.progressEl.removeAttribute("hidden");
78
+ this._setBar(this.cfg.initialPct, this.cfg.initialLabel);
79
+ this.simulationStart = null;
80
+ this.lastStage = null;
81
+ this.completed = false;
82
+ this.failed = false;
83
+ this.consecutiveErrors = 0;
84
+ if (this.pollTimer !== null) return;
85
+ // Forsta poll efter 100ms sa ev. fetch-patches (auth-tokens etc)
86
+ // hinner appliceras.
87
+ setTimeout(() => this._poll(), 100);
88
+ this.pollTimer = setInterval(() => this._poll(), this.cfg.pollInterval);
89
+ }
90
+
91
+ stop() {
92
+ if (this.pollTimer !== null) {
93
+ clearInterval(this.pollTimer);
94
+ this.pollTimer = null;
95
+ }
96
+ }
97
+
98
+ _setBar(pct, label) {
99
+ const clampedPct = Math.max(0, Math.min(100, Math.round(pct)));
100
+ if (this.barEl) this.barEl.style.width = clampedPct + "%";
101
+ if (this.pctEl) this.pctEl.textContent = clampedPct + "%";
102
+ if (this.labelEl && label) this.labelEl.textContent = label;
103
+ }
104
+
105
+ _getLabel(data, elapsedSim) {
106
+ if (data.status === "done") return this.cfg.doneLabel;
107
+ const stageEntry = this.cfg.stageLabels[data.stage];
108
+ if (typeof stageEntry === "function") {
109
+ return stageEntry(data, elapsedSim);
110
+ }
111
+ if (typeof stageEntry === "string") {
112
+ return stageEntry;
113
+ }
114
+ // Fallback: backend's label
115
+ return data.label || this.cfg.initialLabel;
116
+ }
117
+
118
+ _applySimulation(realPct, data) {
119
+ const sim = this.cfg.simulation;
120
+ if (!sim || data.stage !== sim.triggerStage) return realPct;
121
+ if (this.simulationStart == null) return realPct;
122
+ const elapsedSec = (Date.now() - this.simulationStart) / 1000;
123
+ const simulated = sim.startPct + elapsedSec * sim.ratePerSec;
124
+ const capped = Math.min(simulated, sim.cap);
125
+ return Math.max(realPct, capped);
126
+ }
127
+
128
+ async _poll() {
129
+ try {
130
+ const res = await fetch(this.cfg.pollUrl);
131
+ if (!res.ok) {
132
+ this._handleFetchError(`HTTP ${res.status}`);
133
+ return;
134
+ }
135
+ let data = await res.json();
136
+ if (typeof this.cfg.transform === "function") {
137
+ data = this.cfg.transform(data);
138
+ }
139
+ this.consecutiveErrors = 0;
140
+
141
+ // Stage-byte: trigga simulation-timer
142
+ const sim = this.cfg.simulation;
143
+ if (sim) {
144
+ if (data.stage === sim.triggerStage && this.lastStage !== sim.triggerStage) {
145
+ this.simulationStart = Date.now();
146
+ } else if (data.stage !== sim.triggerStage) {
147
+ this.simulationStart = null;
148
+ }
149
+ }
150
+ this.lastStage = data.stage;
151
+
152
+ // Terminal states
153
+ if (data.status === "failed") {
154
+ if (this.failed) return;
155
+ this.failed = true;
156
+ this.stop();
157
+ this.progressEl.setAttribute("hidden", "");
158
+ if (typeof this.cfg.onError === "function") {
159
+ this.cfg.onError(data.label || "Operationen misslyckades.");
160
+ }
161
+ return;
162
+ }
163
+
164
+ if (data.status === "done") {
165
+ if (this.completed) return;
166
+ this.completed = true;
167
+ this._setBar(100, this.cfg.doneLabel);
168
+ this.stop();
169
+ if (typeof this.cfg.onComplete === "function") {
170
+ this.cfg.onComplete(data);
171
+ }
172
+ return;
173
+ }
174
+
175
+ // Pågående: render pct + label
176
+ const realPct = data.pct || 0;
177
+ const elapsedSim = this.simulationStart
178
+ ? Date.now() - this.simulationStart
179
+ : null;
180
+ const effectivePct = this._applySimulation(realPct, data);
181
+ const label = this._getLabel(data, elapsedSim);
182
+ this._setBar(effectivePct, label);
183
+ } catch (err) {
184
+ this._handleFetchError(err.message || String(err));
185
+ }
186
+ }
187
+
188
+ _handleFetchError(msg) {
189
+ this.consecutiveErrors += 1;
190
+ if (this.consecutiveErrors >= this.cfg.maxErrors) {
191
+ if (this.failed) return;
192
+ this.failed = true;
193
+ this.stop();
194
+ this.progressEl.setAttribute("hidden", "");
195
+ if (typeof this.cfg.onError === "function") {
196
+ this.cfg.onError(`Polling-fel efter ${this.cfg.maxErrors} forsok: ${msg}`);
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ // Exponera pa KloddDS-namespace. Skapa objektet om det inte finns
203
+ // (defensiv mot oklar laddningsordning - om async-progress.js laddas
204
+ // fore index.js).
205
+ root.KloddDS = root.KloddDS || {};
206
+ root.KloddDS.AsyncProgress = AsyncProgress;
207
+ })(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.3.0",
4
4
  "description": "Klodd shared design system - tokens, components, JS",
5
5
  "main": "css/index.css",
6
6
  "bin": {