@klodd/ds 1.1.0 → 3.0.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.
Files changed (43) hide show
  1. package/css/00-primitives.css +62 -0
  2. package/css/10-semantic.css +81 -20
  3. package/css/apps/ekonom.css +9 -0
  4. package/css/base/layout.css +45 -0
  5. package/css/components/auth.css +28 -0
  6. package/css/components/avatar.css +31 -0
  7. package/css/components/badge.css +2 -2
  8. package/css/components/banner.css +63 -0
  9. package/css/components/chip.css +257 -0
  10. package/css/components/collapsible.css +64 -0
  11. package/css/components/divider.css +25 -0
  12. package/css/components/dropdown.css +88 -0
  13. package/css/components/feedback.css +6 -0
  14. package/css/components/form.css +67 -0
  15. package/css/components/hbar.css +64 -0
  16. package/css/components/hero.css +85 -0
  17. package/css/components/hub-card.css +100 -0
  18. package/css/components/inline-edit.css +67 -0
  19. package/css/components/input.css +1 -1
  20. package/css/components/list-row.css +115 -0
  21. package/css/components/overlay.css +3 -0
  22. package/css/components/panel.css +95 -0
  23. package/css/components/progress.css +39 -0
  24. package/css/components/setting-row.css +186 -0
  25. package/css/components/split-bar.css +53 -0
  26. package/css/components/stat.css +55 -0
  27. package/css/components/swipe-stack.css +200 -0
  28. package/css/components/tab-bar.css +58 -0
  29. package/css/components/table.css +39 -0
  30. package/css/components/tooltip.css +50 -0
  31. package/css/components/upload-spinner.css +55 -0
  32. package/css/index.css +32 -0
  33. package/css/utilities.css +81 -0
  34. package/js/bar-styles.js +37 -0
  35. package/js/hero-roll.js +287 -0
  36. package/js/index.js +95 -0
  37. package/js/lucide-init.js +27 -0
  38. package/js/nav-optimistic.js +74 -0
  39. package/js/pull-to-refresh.js +162 -0
  40. package/js/pwa-register.js +132 -0
  41. package/js/sheet-drag.js +163 -0
  42. package/js/turbo-nav.js +353 -0
  43. package/package.json +4 -3
@@ -0,0 +1,353 @@
1
+ /*--------------------------------------------------------------
2
+ Sprint I + I.1: Turbo Drive-monstret for Ekonom.
3
+
4
+ Bakgrund (CE 2026-04-27):
5
+ Cross-document View Transitions glitchar pa iOS Safari + PWA-
6
+ standalone (snapshot-bug pa fixed-position-element + dvh-
7
+ instabilitet pa korta sidor utan scroll). Sprint G3+G4 patchade
8
+ symptom utan att losa rotorsaken. Industri-konsensus: skip cross-
9
+ document VT, ga till same-document via fetch + DOM-swap.
10
+
11
+ Detta ar Hotwire/Turbo-monstret applicerat pa Ekonom. Vi behaller
12
+ hela MPA-arkitekturen (Jinja-render, server-routes, Alembic, auth,
13
+ forms via 303-redirect) men intercepterar `<a>`-klick pa interna
14
+ lankar och swappar `<main>` + `<title>` + body-data-attribut
15
+ istallet for full document-navigation. document.startViewTransition
16
+ ger same-document VT som inte triggar iOS-snapshot-bugen.
17
+
18
+ Sprint I.1 (post-Chat-ADR-review): polish + risk-mitigation:
19
+ - history.state merge (inte radera) sa browser-internals (scroll-
20
+ position-cache, etc.) bevaras
21
+ - scrollY save/restore via history.state for korrekt back/forward-
22
+ beteende (browser cachar inte scrollY pa same-document swap)
23
+ - inFlight-guard mot overlappande VT (race condition vid snabba
24
+ dubbel-tap)
25
+ - Focus flyttas till <main> efter swap for screen reader-support
26
+ - Toast vid fetch-fel innan fallback till full document-load
27
+ - Title-safety vid saknat <title>-element
28
+
29
+ Vad vi swappar:
30
+ - <main id="main"> (per-template content inkl. topbar)
31
+ - <title>
32
+ - body data-ptr-enabled (Sprint E1)
33
+ - active-class pa bottom-nav (server-render ar sanning)
34
+
35
+ Vad vi INTE rorr:
36
+ - Bottom-nav-element (sitter utanfor main, oforandrad mellan sidor)
37
+ - <dialog class="sheet"> (inkluderas via base.html, oforandrad)
38
+ - data-first-load (sätts bara forsta besoket, inte vid swap)
39
+ - Form-submits (full document-load via 303-redirect, oforandrat)
40
+
41
+ Fallback:
42
+ - Modifier-klick (cmd/ctrl/shift/alt) eller middle-click: browser
43
+ default (oppna i ny flik)
44
+ - target="_blank" eller download: browser default
45
+ - data-turbo="false" pa <a>: full navigation
46
+ - External origin: full navigation
47
+ - Hash-only links (#anchor): browser default
48
+ - Fetch-fel: toast + fallback till window.location.href
49
+ - Browser utan startViewTransition: instant swap utan animation
50
+
51
+ JS-state-leaks-vigilance (Sprint I.1):
52
+ Eftersom vi inte langre gor full document-reload kan module-level
53
+ state lacka over nav-byten. Konventioner:
54
+ - All event-handling SKA vara delegerad pa document/window-niva
55
+ eller pa element som inte swappas (body, <dialog>-elements i
56
+ base.html). INGA listeners attached till elements i <main>.
57
+ - Inga setInterval utan motsvarande clearInterval-cleanup-trigger
58
+ - Inga module-level array/object-collections utan cleanup-mekanism
59
+ - MutationObserver-pattern: observer maste targeta element utanfor
60
+ <main>, eller disconnect:as via turbo:navigated-event
61
+ --------------------------------------------------------------*/
62
+ ( function () {
63
+ 'use strict';
64
+
65
+ const ORIGIN = window.location.origin;
66
+ let currentController = null;
67
+ let inFlightSwap = null; // Promise som resolvar nar pagaende VT/swap ar klar.
68
+
69
+ /*--------------------------------------------------------------
70
+ Lank-eligibility: ska denna <a> hanteras av Turbo eller browsern?
71
+ --------------------------------------------------------------*/
72
+ function isInternalLink ( a ) {
73
+ if ( ! a || ! a.href ) return false;
74
+ if ( a.dataset.turbo === 'false' ) return false;
75
+ if ( a.target && a.target !== '' && a.target !== '_self' ) return false;
76
+ if ( a.hasAttribute( 'download' ) ) return false;
77
+ const rawHref = a.getAttribute( 'href' ) || '';
78
+ if ( rawHref.startsWith( '#' ) ) return false;
79
+ if ( rawHref.startsWith( 'mailto:' ) ) return false;
80
+ if ( rawHref.startsWith( 'tel:' ) ) return false;
81
+ try {
82
+ const url = new URL( a.href );
83
+ if ( url.origin !== ORIGIN ) return false;
84
+ } catch ( _ ) {
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ function isModifierClick ( e ) {
91
+ if ( e.metaKey || e.ctrlKey || e.shiftKey || e.altKey ) return true;
92
+ if ( e.button !== undefined && e.button !== 0 ) return true;
93
+ return false;
94
+ }
95
+
96
+ /*--------------------------------------------------------------
97
+ Sprint J.4: detektera month-change-nav for att skip:a VT.
98
+ Skalet: VT-snapshot doljer original DOM under fade-tid (~160ms).
99
+ Hero-roll-animation kan inte synkroniseras med VT-fade eftersom
100
+ snapshot blockerar live-DOM. Skip VT vid month-change ger
101
+ anvandaren omedelbar feedback (rollers borjar rulla direkt).
102
+ VT behalls for andra nav-byten (Hem -> Kategorier etc) dar
103
+ cross-fade ar fortsatt vardefull.
104
+ --------------------------------------------------------------*/
105
+ function isMonthChangeNav ( fromUrl, toUrl ) {
106
+ try {
107
+ const f = new URL( fromUrl );
108
+ const t = new URL( toUrl );
109
+ if ( f.origin !== t.origin ) return false;
110
+ if ( f.pathname !== t.pathname ) return false;
111
+ const fromMonth = f.searchParams.get( 'month' );
112
+ const toMonth = t.searchParams.get( 'month' );
113
+ return fromMonth !== toMonth;
114
+ } catch ( _ ) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /*--------------------------------------------------------------
120
+ history.state-helpers. Sprint I.1: merge-pattern istallet for
121
+ radera, sa eventuella browser-cachade state-falt (scroll, etc.)
122
+ bevaras nar vi adderar var turbo-flagga.
123
+ --------------------------------------------------------------*/
124
+ function mergeState ( extra ) {
125
+ return Object.assign( {}, history.state || {}, extra, { turbo: true } );
126
+ }
127
+
128
+ function saveScrollOnCurrentEntry () {
129
+ const merged = mergeState( { scrollY: window.scrollY } );
130
+ history.replaceState( merged, '', window.location.href );
131
+ }
132
+
133
+ /*--------------------------------------------------------------
134
+ Hamta + parse HTML-respons fran Turbo-fetch.
135
+ --------------------------------------------------------------*/
136
+ async function turboFetch ( url, signal ) {
137
+ const resp = await fetch( url, {
138
+ headers: { 'X-Turbo': 'true', 'Accept': 'text/html' },
139
+ credentials: 'same-origin',
140
+ signal: signal,
141
+ } );
142
+ if ( ! resp.ok ) {
143
+ throw new Error( 'HTTP ' + resp.status );
144
+ }
145
+ const text = await resp.text();
146
+ const doc = new DOMParser().parseFromString( text, 'text/html' );
147
+ return { doc: doc, finalUrl: resp.url };
148
+ }
149
+
150
+ /*--------------------------------------------------------------
151
+ Utfor sjalva DOM-bytet. Kor inom document.startViewTransition-
152
+ callback for same-document VT.
153
+ --------------------------------------------------------------*/
154
+ function performSwap ( newDoc ) {
155
+ // Title - safety mot saknat title-element (osannolikt men ratt-pa-papper).
156
+ const newTitle = newDoc.querySelector( 'title' );
157
+ if ( newTitle && newTitle.textContent ) {
158
+ document.title = newTitle.textContent;
159
+ }
160
+
161
+ // Body data-attribut. Synka data-ptr-enabled (Sprint E1) sa pull-to-
162
+ // refresh aktiveras/inaktiveras per route. Lat data-first-load styras
163
+ // av initial-render bara - om server skickade ny first-load-true (t.ex.
164
+ // inkognito-besok via turbo) ignorerar vi det for att inte trigga
165
+ // splash-animation per turbo-byte.
166
+ const newBody = newDoc.body;
167
+ if ( newBody.hasAttribute( 'data-ptr-enabled' ) ) {
168
+ document.body.setAttribute( 'data-ptr-enabled', newBody.getAttribute( 'data-ptr-enabled' ) );
169
+ } else {
170
+ document.body.removeAttribute( 'data-ptr-enabled' );
171
+ }
172
+
173
+ // Main content (inkl. per-template topbar). Replace bevarar element-
174
+ // referenser for VT-name-mapping i same-document VT.
175
+ const oldMain = document.getElementById( 'main' );
176
+ const newMain = newDoc.getElementById( 'main' );
177
+ if ( oldMain && newMain ) {
178
+ oldMain.replaceWith( newMain );
179
+ }
180
+
181
+ // Bottom-nav active-class. Server-render ar sanning - om optimistic
182
+ // active-flytt pa F1 hamnade fel (t.ex. server-redirect till annan
183
+ // route) sa korrigerar vi den har.
184
+ const newNav = newDoc.querySelector( '.bottom-nav' );
185
+ const liveNav = document.querySelector( '.bottom-nav' );
186
+ if ( newNav && liveNav ) {
187
+ const newActiveHref = newNav.querySelector( '.nav-pod.active' );
188
+ const liveActive = liveNav.querySelector( '.nav-pod.active' );
189
+ if ( liveActive ) liveActive.classList.remove( 'active' );
190
+ if ( newActiveHref ) {
191
+ const target = liveNav.querySelector(
192
+ 'a[href="' + newActiveHref.getAttribute( 'href' ) + '"]'
193
+ );
194
+ if ( target ) target.classList.add( 'active' );
195
+ }
196
+ }
197
+ }
198
+
199
+ /*--------------------------------------------------------------
200
+ Focus-management efter swap. Sprint I.1: flytta focus till
201
+ <main>-elementet sa screen readers far en logisk forflyttnings-
202
+ punkt. Native MPA-nav rensar focus implicit; same-document swap
203
+ maste vi gora explicit.
204
+ --------------------------------------------------------------*/
205
+ function moveFocusToMain () {
206
+ const main = document.getElementById( 'main' );
207
+ if ( ! main ) return;
208
+ // tabindex=-1 om saknat sa programmatic focus funkar utan att
209
+ // adda main till tab-ordningen.
210
+ if ( ! main.hasAttribute( 'tabindex' ) ) {
211
+ main.setAttribute( 'tabindex', '-1' );
212
+ }
213
+ try { main.focus( { preventScroll: true } ); } catch ( _ ) { /* ignore */ }
214
+ }
215
+
216
+ /*--------------------------------------------------------------
217
+ Kornplats: hamta + swappa + uppdatera history. Anropas vid
218
+ nav-klick OCH popstate (back/forward).
219
+
220
+ Sprint I.1: vantar pa pagaende inFlightSwap innan ny startas
221
+ sa overlappande VT inte ger artifakter vid snabba dubbel-tap.
222
+ --------------------------------------------------------------*/
223
+ async function navigateTo ( url, options ) {
224
+ options = options || {};
225
+
226
+ // Sprint J: spara fromUrl FORE history.pushState eller swap sa
227
+ // turbo:navigated-subscribers (t.ex. hero-roll.js) kan jamfora
228
+ // gammal vs ny route for trigger-detektering (manadsbyte etc.).
229
+ const fromUrl = window.location.href;
230
+
231
+ // Avbryt pagaende fetch om en ny startar (snabba klick).
232
+ if ( currentController ) currentController.abort();
233
+ currentController = new AbortController();
234
+ const myController = currentController;
235
+
236
+ // Vanta pa eventuell pagaende swap (VT) sa vi inte staplar dem.
237
+ if ( inFlightSwap ) {
238
+ try { await inFlightSwap; } catch ( _ ) { /* ignore */ }
239
+ if ( myController.signal.aborted ) return;
240
+ }
241
+
242
+ // Sprint I.1: spara nuvarande scrollY i history.state SA fort en
243
+ // nav startar - sa popstate-handlern kan restore:a scrollY nar
244
+ // anvandaren back:ar tillbaka till denna sida.
245
+ if ( ! options.popstate ) {
246
+ saveScrollOnCurrentEntry();
247
+ }
248
+
249
+ try {
250
+ const { doc, finalUrl } = await turboFetch( url, myController.signal );
251
+ if ( myController.signal.aborted ) return;
252
+
253
+ // Server kan ha redirected (t.ex. /bolanet -> /bolanet?month=2026-04).
254
+ // Anvand finalUrl for history-state sa back/forward funkar korrekt.
255
+ const swap = function () {
256
+ performSwap( doc );
257
+ // Sprint J.3: dispatcha SYNKRON 'turbo:swap'-event INOM
258
+ // swap-callback fore VT slut-snapshot tas. Subscribers (hero-
259
+ // roll.js) kan modifiera nya DOM (t.ex. byta ut hero-amount
260
+ // till digit-rollers) sa VT slut-state inkluderar deras
261
+ // modifikationer. Forhindrar 0.2s-flash av server-text
262
+ // under VT-fade vid month-change.
263
+ document.dispatchEvent( new CustomEvent( 'turbo:swap', {
264
+ detail: { url: finalUrl, fromUrl: fromUrl, popstate: !! options.popstate },
265
+ } ) );
266
+ if ( options.popstate ) {
267
+ // Restore scrollY fran history.state om sparat. Annars topp.
268
+ const cachedScroll = history.state && typeof history.state.scrollY === 'number'
269
+ ? history.state.scrollY : 0;
270
+ window.scrollTo( 0, cachedScroll );
271
+ } else {
272
+ window.scrollTo( 0, 0 );
273
+ }
274
+ moveFocusToMain();
275
+ };
276
+
277
+ // Wrappa swap + VT i en Promise sa nasta nav vantar pa den.
278
+ let swapResolve;
279
+ inFlightSwap = new Promise( function ( r ) { swapResolve = r; } );
280
+
281
+ // Sprint J.4: skip VT vid month-change. VT-snapshot doljer
282
+ // original DOM under fade-tid, vilket blockerar hero-roll-
283
+ // animation. Vid month-change vill vi att rollers ska borja
284
+ // rulla omedelbart - skip VT ger detta. VT behalls for andra
285
+ // nav-byten dar cross-fade ar vardefull (Hem -> Kategorier etc).
286
+ const skipVT = isMonthChangeNav( fromUrl, finalUrl );
287
+
288
+ try {
289
+ if ( typeof document.startViewTransition === 'function' && ! skipVT ) {
290
+ const transition = document.startViewTransition( swap );
291
+ try { await transition.finished; } catch ( _ ) { /* ignore */ }
292
+ } else {
293
+ swap();
294
+ }
295
+ } finally {
296
+ if ( inFlightSwap && swapResolve ) swapResolve();
297
+ inFlightSwap = null;
298
+ }
299
+
300
+ if ( ! options.popstate ) {
301
+ history.pushState( mergeState( { scrollY: 0 } ), '', finalUrl );
302
+ }
303
+
304
+ // Notify subscribers (nav-optimistic.js, hero-roll.js, future modules).
305
+ document.dispatchEvent( new CustomEvent( 'turbo:navigated', {
306
+ detail: { url: finalUrl, fromUrl: fromUrl, popstate: !! options.popstate },
307
+ } ) );
308
+ } catch ( err ) {
309
+ if ( err.name === 'AbortError' ) return;
310
+ // Sprint I.1: toast for synlighet innan vi gor full document-load
311
+ // fallback. Anvandaren far visuell feedback att natet failade.
312
+ if ( window.showToast ) {
313
+ try { window.showToast( 'Natverksfel - laddar om sidan.', 'error', 1800 ); } catch ( _ ) {}
314
+ }
315
+ // Fallback: full document-load. Browsern visar offline-page om
316
+ // SW har den cachad.
317
+ window.location.href = url;
318
+ } finally {
319
+ if ( myController === currentController ) currentController = null;
320
+ }
321
+ }
322
+
323
+ /*--------------------------------------------------------------
324
+ Click-delegation: lyssna pa <a>-klick globalt. Kor INNAN nav-
325
+ optimistic.js pa samma click sa active-flytten visas direkt
326
+ medan turbo-fetchen pagar i bakgrunden.
327
+ --------------------------------------------------------------*/
328
+ document.addEventListener( 'click', function ( e ) {
329
+ if ( isModifierClick( e ) ) return;
330
+ const a = e.target.closest( 'a[href]' );
331
+ if ( ! a ) return;
332
+ if ( ! isInternalLink( a ) ) return;
333
+ // Sheet-trigger eller annan delegation-handler kan ha redan
334
+ // preventDefault:at - skippa i sa fall.
335
+ if ( e.defaultPrevented ) return;
336
+ e.preventDefault();
337
+ navigateTo( a.href );
338
+ } );
339
+
340
+ /*--------------------------------------------------------------
341
+ Back/forward via popstate. Initial pageload markeras med
342
+ turbo: true via mergeState sa popstate-handling vet att vi ar
343
+ i turbo-modell hela vagen. Browser-internals (scroll-cache,
344
+ etc.) bevaras eftersom vi merge:ar istallet for replace:ar.
345
+ --------------------------------------------------------------*/
346
+ window.addEventListener( 'popstate', function () {
347
+ navigateTo( window.location.href, { popstate: true } );
348
+ } );
349
+
350
+ // Initial state: merga in turbo-flag + spara forsta scrollY (0)
351
+ // utan att radera browser-cachade falt.
352
+ history.replaceState( mergeState( { scrollY: window.scrollY || 0 } ), '', window.location.href );
353
+ } )();
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@klodd/ds",
3
- "version": "1.1.0",
4
- "description": "Klodd Design System - shared tokens, typography, and components for Jubb, Ekonom, and future apps",
3
+ "version": "3.0.0",
4
+ "description": "Klodd Design System - shared tokens, typography, components and JS for Jubb, Ekonom, and future apps. v2.0 inkluderar all komponentkod och delad JS - app-repona haller bara data och affarslogik.",
5
5
  "main": "css/index.css",
6
6
  "files": [
7
- "css/"
7
+ "css/",
8
+ "js/"
8
9
  ],
9
10
  "keywords": [
10
11
  "design-system",