@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,39 @@
1
+ /* ================================================================
2
+ components/table.css
3
+ Tabell med header + rader. Tabular-nums for nummerkolumner.
4
+ ================================================================ */
5
+ .table {
6
+ width: 100%;
7
+ border-collapse: collapse;
8
+ font-size: var(--fs-13);
9
+ }
10
+
11
+ .table th {
12
+ font-size: var(--fs-10);
13
+ font-weight: var(--fw-medium);
14
+ letter-spacing: 0.1em;
15
+ text-transform: uppercase;
16
+ color: var(--text-muted);
17
+ text-align: left;
18
+ padding: var(--space-8) var(--space-6);
19
+ border-bottom: 1px solid var(--border-default);
20
+ }
21
+
22
+ .table td {
23
+ padding: var(--space-12) var(--space-6);
24
+ border-bottom: 1px solid var(--border-subtle);
25
+ color: var(--text-default);
26
+ }
27
+
28
+ .table tbody tr:last-child td { border-bottom: none; }
29
+
30
+ @media (hover: hover) and (pointer: fine) {
31
+ .table tbody tr:hover { background: var(--surface-hover); }
32
+ }
33
+
34
+ .table td.num,
35
+ .table th.num {
36
+ text-align: right;
37
+ font-weight: var(--fw-medium);
38
+ font-variant-numeric: tabular-nums;
39
+ }
@@ -0,0 +1,50 @@
1
+ /* ================================================================
2
+ components/tooltip.css
3
+ Kortfattad hjalptext vid hover/focus. Pure CSS - ingen JS.
4
+
5
+ Markup-pattern:
6
+ <span class="tooltip-wrapper">
7
+ <button class="btn btn--icon">...</button>
8
+ <span class="tooltip" role="tooltip">Hjalptext har</span>
9
+ </span>
10
+
11
+ Tooltip visas vid hover OCH focus-within sa tangentbordsanvandare
12
+ far samma feedback som musanvandare. Pointer-events:none pa
13
+ tooltip-elementet sa det inte snor klick fran wrappern.
14
+
15
+ prefers-reduced-motion respekteras (opacity-fade tas bort).
16
+ ================================================================ */
17
+ .tooltip-wrapper {
18
+ position: relative;
19
+ display: inline-flex;
20
+ }
21
+
22
+ .tooltip {
23
+ position: absolute;
24
+ bottom: calc(100% + var(--space-6));
25
+ left: 50%;
26
+ transform: translateX(-50%);
27
+ background: var(--surface-overlay);
28
+ color: var(--text-default);
29
+ font-size: var(--fs-12);
30
+ line-height: var(--lh-snug);
31
+ padding: var(--space-4) var(--space-8);
32
+ border-radius: var(--radius-6);
33
+ border: 1px solid var(--border-default);
34
+ white-space: nowrap;
35
+ pointer-events: none;
36
+ opacity: 0;
37
+ transition: opacity var(--dur-fast) var(--ease-out);
38
+ z-index: var(--z-tooltip, 50);
39
+ }
40
+
41
+ .tooltip-wrapper:hover .tooltip,
42
+ .tooltip-wrapper:focus-within .tooltip {
43
+ opacity: 1;
44
+ }
45
+
46
+ @media (prefers-reduced-motion: reduce) {
47
+ .tooltip {
48
+ transition: none;
49
+ }
50
+ }
@@ -0,0 +1,55 @@
1
+ /* ================================================================
2
+ components/upload-spinner.css
3
+ Stor loading-overlay som tacker viewport. Anvands for OCR-uploads,
4
+ AI-anrop och andra langa async-operations dar appen ar blockerad.
5
+
6
+ .upload-spinner-overlay ar fixed-positionerat overlay som visas
7
+ nar .is-visible-klassen togglas via JS.
8
+ ================================================================ */
9
+ .upload-spinner-overlay {
10
+ position: fixed;
11
+ inset: 0;
12
+ background: var(--backdrop-spinner);
13
+ -webkit-backdrop-filter: blur(8px);
14
+ backdrop-filter: blur(8px);
15
+ display: none;
16
+ align-items: center;
17
+ justify-content: center;
18
+ flex-direction: column;
19
+ gap: var(--space-20);
20
+ z-index: var(--z-overlay, 70);
21
+ padding: var(--space-32);
22
+ text-align: center;
23
+ }
24
+
25
+ .upload-spinner-overlay.is-visible { display: flex; }
26
+
27
+ .upload-spinner {
28
+ width: 56px;
29
+ height: 56px;
30
+ border: 3px solid var(--accent-a3);
31
+ border-top-color: var(--accent-9);
32
+ border-radius: var(--radius-full);
33
+ animation: upload-spinner-rotate var(--dur-slowest, 0.9s) linear infinite;
34
+ }
35
+
36
+ .upload-spinner-overlay__label {
37
+ font-size: var(--fs-15);
38
+ font-weight: var(--fw-medium);
39
+ color: var(--text-default);
40
+ letter-spacing: -0.01em;
41
+ }
42
+
43
+ .upload-spinner-overlay__hint {
44
+ font-size: var(--fs-12);
45
+ color: var(--text-muted);
46
+ max-width: 280px;
47
+ }
48
+
49
+ @keyframes upload-spinner-rotate {
50
+ to { transform: rotate(360deg); }
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .upload-spinner { animation-duration: 2.4s; }
55
+ }
package/css/index.css CHANGED
@@ -13,10 +13,17 @@
13
13
  @import '@klodd/ds/css/apps/jubb.css';
14
14
  ================================================================ */
15
15
 
16
+ /* Foundation */
16
17
  @import './00-primitives.css';
17
18
  @import './10-semantic.css';
19
+ @import './utilities.css';
20
+
21
+ /* Base layer */
18
22
  @import './base/pwa.css';
19
23
  @import './base/typography.css';
24
+ @import './base/layout.css';
25
+
26
+ /* Komponenter */
20
27
  @import './components/button.css';
21
28
  @import './components/input.css';
22
29
  @import './components/badge.css';
@@ -26,3 +33,28 @@
26
33
  @import './components/overlay.css';
27
34
  @import './components/icon.css';
28
35
  @import './components/hero-roll.css';
36
+ @import './components/divider.css';
37
+ @import './components/progress.css';
38
+ @import './components/tooltip.css';
39
+ @import './components/dropdown.css';
40
+
41
+ /* v2.0.0 - flyttade fran app-repona */
42
+ @import './components/banner.css';
43
+ @import './components/panel.css';
44
+ @import './components/hub-card.css';
45
+ @import './components/stat.css';
46
+ @import './components/form.css';
47
+ @import './components/setting-row.css';
48
+ @import './components/collapsible.css';
49
+ @import './components/hbar.css';
50
+ @import './components/split-bar.css';
51
+ @import './components/hero.css';
52
+ @import './components/chip.css';
53
+ @import './components/avatar.css';
54
+ @import './components/list-row.css';
55
+ @import './components/table.css';
56
+ @import './components/auth.css';
57
+ @import './components/swipe-stack.css';
58
+ @import './components/inline-edit.css';
59
+ @import './components/upload-spinner.css';
60
+ @import './components/tab-bar.css';
@@ -0,0 +1,81 @@
1
+ /* ================================================================
2
+ utilities.css
3
+ Single-purpose helper-classes for sma layout-detaljer.
4
+ Bygger pa pixel-numerisk konvention (.m-0, .mt-12, .gap-8).
5
+
6
+ Ingen styling-logik - bara en CSS-property per klass.
7
+ ================================================================ */
8
+
9
+ /* Margin */
10
+ .m-0 { margin: 0; }
11
+ .mt-0 { margin-top: 0; }
12
+ .mt-4 { margin-top: var(--space-4); }
13
+ .mt-8 { margin-top: var(--space-8); }
14
+ .mt-12 { margin-top: var(--space-12); }
15
+ .mt-16 { margin-top: var(--space-16); }
16
+ .mt-20 { margin-top: var(--space-20); }
17
+ .mt-24 { margin-top: var(--space-24); }
18
+ .mb-0 { margin-bottom: 0; }
19
+ .mb-4 { margin-bottom: var(--space-4); }
20
+ .mb-8 { margin-bottom: var(--space-8); }
21
+ .mb-12 { margin-bottom: var(--space-12); }
22
+ .mb-16 { margin-bottom: var(--space-16); }
23
+ .mb-20 { margin-bottom: var(--space-20); }
24
+ .mb-24 { margin-bottom: var(--space-24); }
25
+
26
+ /* Padding */
27
+ .p-0 { padding: 0; }
28
+ .pt-0 { padding-top: 0; }
29
+ .pb-0 { padding-bottom: 0; }
30
+
31
+ /* Display */
32
+ .is-hidden { display: none !important; }
33
+ .flex { display: flex; }
34
+ .inline-flex { display: inline-flex; }
35
+
36
+ /* Width */
37
+ .w-full { width: 100%; }
38
+
39
+ /* Flex */
40
+ .gap-4 { gap: var(--space-4); }
41
+ .gap-8 { gap: var(--space-8); }
42
+ .gap-12 { gap: var(--space-12); }
43
+ .gap-16 { gap: var(--space-16); }
44
+ .flex-column { flex-direction: column; }
45
+ .align-center { align-items: center; }
46
+ .align-self-start { align-self: flex-start; }
47
+ .justify-between { justify-content: space-between; }
48
+
49
+ /* Position (CSP-migration: ersatter style="position: ...") */
50
+ .absolute { position: absolute; }
51
+ .absolute-fill { position: absolute; inset: 0; }
52
+ .relative { position: relative; }
53
+
54
+ /* Text */
55
+ .text-center { text-align: center; }
56
+ .text-right { text-align: right; }
57
+ .break-word { word-break: break-word; overflow-wrap: anywhere; }
58
+ .tabular-nums { font-variant-numeric: tabular-nums; }
59
+
60
+ /* Text-color (semantic shortcuts) */
61
+ .text-default { color: var(--text-default); }
62
+ .text-subtle { color: var(--text-subtle); }
63
+ .text-muted { color: var(--text-muted); }
64
+ .text-disabled { color: var(--text-disabled); }
65
+ .text-positive { color: var(--positive); }
66
+ .text-negative { color: var(--negative); }
67
+ .text-warning { color: var(--warning); }
68
+ .text-accent { color: var(--accent-text); }
69
+
70
+ /* Visibility */
71
+ .visually-hidden {
72
+ position: absolute;
73
+ width: 1px;
74
+ height: 1px;
75
+ padding: 0;
76
+ margin: -1px;
77
+ overflow: hidden;
78
+ clip: rect(0, 0, 0, 0);
79
+ white-space: nowrap;
80
+ border: 0;
81
+ }
@@ -0,0 +1,37 @@
1
+ /* bar-styles.js - applicerar dynamiska CSS-varden via data-attribut.
2
+ * CSP-sakert: JS-runtime bypass:ar HTML-parser-restriktioner pa
3
+ * style-src 'unsafe-inline'. Element kan deklarera dynamic dimensions
4
+ * via data-attribut istallet for inline style="..."-attribut.
5
+ *
6
+ * Stodda attribut:
7
+ * data-bar-width="N" sa style.width = N + '%'
8
+ * data-bar-height="N" sa style.height = N + '%'
9
+ * data-z-index="N" sa style.zIndex = N
10
+ *
11
+ * Re-init pa turbo:swap + turbo:navigated for SPA-flow sa nyrenderade
12
+ * element pa swapped-DOM aktiveras.
13
+ */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ function applyBarStyles() {
18
+ document.querySelectorAll('[data-bar-width]').forEach(function (el) {
19
+ el.style.width = el.dataset.barWidth + '%';
20
+ });
21
+ document.querySelectorAll('[data-bar-height]').forEach(function (el) {
22
+ el.style.height = el.dataset.barHeight + '%';
23
+ });
24
+ document.querySelectorAll('[data-z-index]').forEach(function (el) {
25
+ el.style.zIndex = el.dataset.zIndex;
26
+ });
27
+ }
28
+
29
+ if (document.readyState === 'loading') {
30
+ document.addEventListener('DOMContentLoaded', applyBarStyles);
31
+ } else {
32
+ applyBarStyles();
33
+ }
34
+
35
+ document.addEventListener('turbo:swap', applyBarStyles);
36
+ document.addEventListener('turbo:navigated', applyBarStyles);
37
+ })();
@@ -0,0 +1,287 @@
1
+ /* Hero Roll Animation - delad komponent
2
+ * Krav: .hero-amount[data-animate-roll] i HTML
3
+ * Krav: font-variant-numeric: tabular-nums pa elementet
4
+ * Krav: --lh-tight definierad i CSS (anvands som rullhojd)
5
+ * Turbo: triggar via turbo:swap-event om Turbo finns, annars fresh-session only
6
+ *
7
+ * Delad mellan Jubb och Ekonom (extraherad fran Ekonom 2026-05-07).
8
+ * Filen ar oforandrad fran Ekonom-originalet - portabel utan wrapper.
9
+ */
10
+
11
+ /*--------------------------------------------------------------
12
+ Sprint J + J.4 + J.5: Hero-siffra rullnings-animation pa /avstamning.
13
+
14
+ Inspirerat av kodlas-rullning (parallella siffror som rullar med
15
+ olika sluttider). Triggers:
16
+ 1. **Fresh session** (J.5): kallstart-detektering via sessionStorage-
17
+ marker. Animation kor vid forsta page-load efter att PWA/tab
18
+ stangts helt. Sprint D3:s `body[data-first-load]` (cookie-baserad)
19
+ fungerar bara for helt nya anvandare; J.5-trigger fungerar for
20
+ return-anvandare som har `ekonom_seen`-cookie satt sedan tidigare.
21
+ 2. Manadsbyte via < / > i manads-pillen pa /avstamning
22
+
23
+ Sprint J.4 (CE-rapport: 000 kr syns lite val lange + hela processen
24
+ ar lang):
25
+ - Turbo-nav.js skip:ar VT vid month-change. Original DOM byts
26
+ omedelbart till rollers - ingen 160ms VT-fade-paus. Animation
27
+ borjar pa samma frame som DOM-swap.
28
+ - Korta animation-durations: BASE 800ms -> 500ms, STEP 200ms ->
29
+ 100ms. Total animation 500/600/700ms istallet for 800/1000/1200ms.
30
+ - Riv ut J.3:s tva-fas-arkitektur (BUILD/ANIMATE separation). Allt
31
+ sker synchron inom buildAndAnimate.
32
+ - Behaller force-reflow-pattern (`void el.offsetHeight`) sa browser
33
+ registrerar initial state innan transition triggas.
34
+
35
+ Total perceived tid (klick till slutvarde): ~720ms vid month-change
36
+ (network ~50ms + animation 700ms). Tidigare: ~1400ms.
37
+
38
+ Bredd-glitch-fix (J.3 oforandrat):
39
+ Default `<p>`-block-flow med inline-block rollers. Content-driven
40
+ width via tabular-nums - identisk packning som server-text.
41
+ --------------------------------------------------------------*/
42
+ ( function () {
43
+ 'use strict';
44
+
45
+ // Sprint J.4: kortare durations. 500/600/700ms for tre siffror.
46
+ const ROLLER_DURATION_BASE = 500; // var 800ms
47
+ const ROLLER_DURATION_STEP = 100; // var 200ms
48
+ const EXTRA_ROUNDS = 3;
49
+ // Sprint J.6: linear istallet for cubic-bezier(0.22, 1, 0.36, 1)
50
+ // (ease-out-quart). Quart-easing packar 95%+ av rorelsen i forsta
51
+ // halvan av tiden - sista 10% av tiden tacker bara 0.01% av rorelse,
52
+ // vilket gor sista rollers slutfas visuellt ororlig ("byts ut" istallet
53
+ // for "rullar"). Linear ger konstant hastighet sa alla siffror rullar
54
+ // synligt fran start till slut. Aven mer mekaniskt-korrekt - faktiska
55
+ // kodlas rullar med moment-konstant rotation.
56
+ const EASING = 'linear';
57
+
58
+ // Sprint J.5: sessionStorage-marker for fresh-session-detektering.
59
+ // sessionStorage rensas vid PWA-stangning/tab-close, persisterar
60
+ // mellan reloads/nav inom samma session.
61
+ // Delad sessionStorage-key for hero-roll. Bytt fran ekonom_hero_session
62
+ // 2026-05-08 nar komponenten porterades tillbaka till Ekonom som shared-
63
+ // system. Befintliga session-markers fran gamla nyckeln tappar persist
64
+ // engang per anvandare - ny markering satts vid nasta kallstart.
65
+ const SESSION_KEY = 'ds_hero_session';
66
+
67
+ let hasRunInitial = false;
68
+ let lastSeenUrl = window.location.href;
69
+
70
+ function getHeroEl () {
71
+ return document.querySelector( '.hero-amount[data-animate-roll]' );
72
+ }
73
+
74
+ function prefersReducedMotion () {
75
+ return window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
76
+ }
77
+
78
+ function buildRoller ( targetDigit ) {
79
+ const roller = document.createElement( 'span' );
80
+ roller.className = 'hero-digit-roller';
81
+
82
+ const track = document.createElement( 'span' );
83
+ track.className = 'hero-digit-track';
84
+
85
+ const totalSteps = targetDigit + ( EXTRA_ROUNDS * 10 );
86
+ for ( let i = 0; i <= totalSteps; i++ ) {
87
+ const span = document.createElement( 'span' );
88
+ span.textContent = String( i % 10 );
89
+ track.appendChild( span );
90
+ }
91
+
92
+ roller.appendChild( track );
93
+ return { roller: roller, track: track, steps: totalSteps };
94
+ }
95
+
96
+ function buildStaticChar ( text ) {
97
+ const span = document.createElement( 'span' );
98
+ span.className = 'hero-static-char';
99
+ span.textContent = text;
100
+ return span;
101
+ }
102
+
103
+ /*--------------------------------------------------------------
104
+ Bygg digit-rollers + starta transition synchron i samma frame.
105
+ Idempotent via data-rolled-attribut.
106
+ --------------------------------------------------------------*/
107
+ function buildAndAnimate ( el ) {
108
+ if ( ! el ) return;
109
+ if ( el.dataset.rolled === 'true' ) return;
110
+ const target = parseInt( el.dataset.animateRoll, 10 );
111
+ if ( isNaN( target ) ) return;
112
+
113
+ // No-op for 0
114
+ if ( target === 0 ) {
115
+ el.dataset.rolled = 'true';
116
+ return;
117
+ }
118
+
119
+ el.dataset.rolled = 'true';
120
+
121
+ const originalText = el.textContent;
122
+ const isNegative = target < 0;
123
+ const absTarget = Math.abs( target );
124
+ const digits = String( absTarget ).split( '' ).map( Number );
125
+ const len = digits.length;
126
+
127
+ const fragment = document.createDocumentFragment();
128
+
129
+ if ( isNegative ) {
130
+ fragment.appendChild( buildStaticChar( '−' ) );
131
+ }
132
+
133
+ const rollers = [];
134
+ digits.forEach( function ( d, i ) {
135
+ const r = buildRoller( d );
136
+ fragment.appendChild( r.roller );
137
+ rollers.push( r );
138
+ const fromRight = len - 1 - i;
139
+ if ( fromRight > 0 && fromRight % 3 === 0 ) {
140
+ fragment.appendChild( buildStaticChar( ' ' ) );
141
+ }
142
+ } );
143
+
144
+ fragment.appendChild( buildStaticChar( ' kr' ) );
145
+
146
+ // Mat font-line-height for digit-roller-hojd.
147
+ const computed = window.getComputedStyle( el );
148
+ const fontSize = parseFloat( computed.fontSize );
149
+ const lineHeightRaw = computed.lineHeight;
150
+ let lineHeight;
151
+ if ( lineHeightRaw === 'normal' ) {
152
+ lineHeight = fontSize * 1.05;
153
+ } else {
154
+ lineHeight = parseFloat( lineHeightRaw );
155
+ }
156
+ el.style.setProperty( '--hero-digit-height', lineHeight + 'px' );
157
+
158
+ // Replace content
159
+ el.innerHTML = '';
160
+ el.appendChild( fragment );
161
+
162
+ // Reduced-motion: hopppa direkt till slutposition utan transition.
163
+ if ( prefersReducedMotion() ) {
164
+ rollers.forEach( function ( r ) {
165
+ r.track.style.transform =
166
+ 'translateY(calc(-1 * var(--hero-digit-height) * ' + r.steps + '))';
167
+ } );
168
+ requestAnimationFrame( function () {
169
+ el.textContent = originalText;
170
+ } );
171
+ return;
172
+ }
173
+
174
+ // Sprint J.4: force reflow sa browser registrerar initial state
175
+ // (translateY default 0) innan transition triggas. Utan reflow
176
+ // hoppar browser direkt till slut-state utan animation.
177
+ void el.offsetHeight;
178
+
179
+ // Trigga transition synchron i samma frame.
180
+ rollers.forEach( function ( r, i ) {
181
+ const duration = ROLLER_DURATION_BASE + ( i * ROLLER_DURATION_STEP );
182
+ r.track.style.transition =
183
+ 'transform ' + duration + 'ms ' + EASING;
184
+ r.track.style.transform =
185
+ 'translateY(calc(-1 * var(--hero-digit-height) * ' + r.steps + '))';
186
+ } );
187
+
188
+ // Vid done: restora till server-text for klean DOM-state.
189
+ const totalDuration = ROLLER_DURATION_BASE + ( ( rollers.length - 1 ) * ROLLER_DURATION_STEP );
190
+ setTimeout( function () {
191
+ el.textContent = originalText;
192
+ }, totalDuration + 60 );
193
+ }
194
+
195
+ /*--------------------------------------------------------------
196
+ Trigger-detektering.
197
+ --------------------------------------------------------------*/
198
+ function isMonthChange ( fromUrl, toUrl ) {
199
+ if ( ! fromUrl ) return false;
200
+ try {
201
+ const f = new URL( fromUrl );
202
+ const t = new URL( toUrl );
203
+ if ( f.pathname !== '/avstamning' || t.pathname !== '/avstamning' ) return false;
204
+ const fromMonth = f.searchParams.get( 'month' );
205
+ const toMonth = t.searchParams.get( 'month' );
206
+ return fromMonth !== toMonth;
207
+ } catch ( _ ) {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /*--------------------------------------------------------------
213
+ Sprint J.5: trigger animation vid fresh session (kallstart fran
214
+ stangd PWA/tab). sessionStorage-marker satts forsta gang per
215
+ session - om saknad ar det en ny session.
216
+
217
+ Behaller `data-first-load`-trigger som fallback for browsers utan
218
+ sessionStorage-stod (extremt sallsynt) och for konsistens med
219
+ Sprint D3 splash-animations.
220
+ --------------------------------------------------------------*/
221
+ function tryAnimateInitial () {
222
+ if ( hasRunInitial ) return;
223
+
224
+ // Fresh-session-detektering via sessionStorage.
225
+ let isFreshSession = false;
226
+ try {
227
+ if ( ! sessionStorage.getItem( SESSION_KEY ) ) {
228
+ isFreshSession = true;
229
+ sessionStorage.setItem( SESSION_KEY, '1' );
230
+ }
231
+ } catch ( _ ) {
232
+ // sessionStorage kan kasta i restricted contexts (extremt
233
+ // sallsynt). Falla tillbaka till data-first-load.
234
+ }
235
+
236
+ const isFirstEver = document.body.dataset.firstLoad === 'true';
237
+
238
+ if ( ! isFreshSession && ! isFirstEver ) return;
239
+
240
+ const el = getHeroEl();
241
+ if ( ! el ) return;
242
+ hasRunInitial = true;
243
+ buildAndAnimate( el );
244
+ }
245
+
246
+ /*--------------------------------------------------------------
247
+ First-load via DOMContentLoaded.
248
+ --------------------------------------------------------------*/
249
+ if ( document.readyState === 'loading' ) {
250
+ document.addEventListener( 'DOMContentLoaded', tryAnimateInitial );
251
+ } else {
252
+ tryAnimateInitial();
253
+ }
254
+
255
+ /*--------------------------------------------------------------
256
+ Sprint J.4: month-change via turbo:swap (synchron inom turbo-nav.js
257
+ swap-kornplats). Eftersom turbo-nav.js skip:ar VT vid month-change,
258
+ ar swap synchron och original DOM ar synlig direkt - animation
259
+ borjar pa samma frame som DOM-byte.
260
+ --------------------------------------------------------------*/
261
+ document.addEventListener( 'turbo:swap', function ( e ) {
262
+ const detail = e.detail || {};
263
+ const newUrl = detail.url || window.location.href;
264
+
265
+ if ( ! isMonthChange( lastSeenUrl, newUrl ) ) {
266
+ lastSeenUrl = newUrl;
267
+ return;
268
+ }
269
+
270
+ const el = getHeroEl();
271
+ if ( ! el ) {
272
+ lastSeenUrl = newUrl;
273
+ return;
274
+ }
275
+
276
+ buildAndAnimate( el );
277
+ lastSeenUrl = newUrl;
278
+ } );
279
+
280
+ /*--------------------------------------------------------------
281
+ Fallback: turbo:navigated (efter VT-end) for first-load om
282
+ DOMContentLoaded inte hunnit.
283
+ --------------------------------------------------------------*/
284
+ document.addEventListener( 'turbo:navigated', function () {
285
+ tryAnimateInitial();
286
+ } );
287
+ } )();
package/js/index.js ADDED
@@ -0,0 +1,95 @@
1
+ /* @klodd/ds - JavaScript entry point.
2
+ *
3
+ * Convenience-aggregator som kor enskilda init-funktioner. Default
4
+ * fungerar utan konfiguration (LLM-vanligt). Konfig-objektet kan
5
+ * over-rida vad som ska aktiveras.
6
+ *
7
+ * Anvandning:
8
+ *
9
+ * <script src="https://unpkg.com/@klodd/ds/js/index.js" defer></script>
10
+ * <script>
11
+ * window.addEventListener('DOMContentLoaded', () => {
12
+ * KloddDS.init();
13
+ * });
14
+ * </script>
15
+ *
16
+ * Eller med konfig:
17
+ *
18
+ * KloddDS.init({
19
+ * swPath: '/service-worker.js',
20
+ * lucide: true,
21
+ * barStyles: true,
22
+ * heroRoll: true,
23
+ * pullToRefresh: false,
24
+ * turboNav: false,
25
+ * navOptimistic: true,
26
+ * sheetDrag: true,
27
+ * pwaRegister: false, // satt true + swPath for SW + install-prompts
28
+ * });
29
+ *
30
+ * Individuella moduler kan ocksa importeras separat fran js/-mappen.
31
+ * KloddDS.init() ar bara en convenience-wrapper - den inkluderar inte
32
+ * filerna sjalv, dvs varje modul-fil maste laddas via <script>-tag.
33
+ *
34
+ * Defaults ar konservativa: heavy beteenden (PtR, Turbo Drive, PWA
35
+ * register) ar OFF default. App-mantainern aktiverar opt-in.
36
+ */
37
+ 'use strict';
38
+
39
+ (function (root) {
40
+ const defaults = {
41
+ lucide: true,
42
+ barStyles: true,
43
+ heroRoll: true,
44
+ navOptimistic: true,
45
+ sheetDrag: true,
46
+ pullToRefresh: false,
47
+ turboNav: false,
48
+ pwaRegister: false,
49
+ swPath: '/static/sw.js',
50
+ };
51
+
52
+ const KloddDS = {
53
+ init(config) {
54
+ const cfg = Object.assign({}, defaults, config || {});
55
+
56
+ if (cfg.lucide && typeof root.initLucide === 'function') {
57
+ root.initLucide();
58
+ } else if (cfg.lucide && root.lucide && typeof root.lucide.createIcons === 'function') {
59
+ root.lucide.createIcons();
60
+ }
61
+
62
+ if (cfg.barStyles && typeof root.initBarStyles === 'function') {
63
+ root.initBarStyles();
64
+ }
65
+
66
+ if (cfg.heroRoll && typeof root.initHeroRoll === 'function') {
67
+ root.initHeroRoll();
68
+ }
69
+
70
+ if (cfg.navOptimistic && typeof root.initNavOptimistic === 'function') {
71
+ root.initNavOptimistic();
72
+ }
73
+
74
+ if (cfg.sheetDrag && typeof root.initSheetDrag === 'function') {
75
+ root.initSheetDrag();
76
+ }
77
+
78
+ if (cfg.pullToRefresh && typeof root.initPullToRefresh === 'function') {
79
+ root.initPullToRefresh();
80
+ }
81
+
82
+ if (cfg.turboNav && typeof root.initTurboNav === 'function') {
83
+ root.initTurboNav();
84
+ }
85
+
86
+ if (cfg.pwaRegister && typeof root.initPwaRegister === 'function') {
87
+ root.initPwaRegister({ swPath: cfg.swPath });
88
+ }
89
+ },
90
+
91
+ version: '2.0.0',
92
+ };
93
+
94
+ root.KloddDS = KloddDS;
95
+ })(typeof window !== 'undefined' ? window : globalThis);