@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.
- package/css/components/async-progress.css +70 -0
- package/css/components/progress-bar.css +10 -55
- package/css/index.css +1 -0
- package/js/async-progress.js +207 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
@@ -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);
|