@klodd/ds 5.7.1 → 5.9.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/SKILL.md CHANGED
@@ -7,10 +7,13 @@ description: Design memory for the @klodd/ds shared design system used by the tw
7
7
 
8
8
  ## Status
9
9
  Designsystem-auditen (2026-05-20, se `audits/00-summary.md`) slutförd -
10
- alla tre sprintar klara:
10
+ alla sex sprintar klara:
11
11
  - Sprint 1: dead-code-städning + topbar-bump (5.4.3)
12
12
  - Sprint 2: component-token-konsolidering, ADR 0024-backfill, light-theme-removal (5.5.0)
13
13
  - Sprint 3: base.css-extraktion till paketets base/interactions.css (5.6.0)
14
+ - Sprint 4: JS-extraktion (sheet + toast) + deprecated-selektor-grep i verify (5.7.0 + 5.7.1)
15
+ - Sprint 5: width/height-tokens (--measure-*, --content-max-*) + stylelint-regel no-literal-dimension (5.8.0)
16
+ - Sprint 6: clerk-init-extraktion (waitForClerkBase, clerkLogout, KloddDS.clerkReady, fetchPatch opt-in) + async-progress clerkReady-race-fix (5.9.0)
14
17
 
15
18
  ## Vad detta är
16
19
  Gemensamt designsystem på npm (`@klodd/ds@5.x`). Tre-lagers tokens,
@@ -415,6 +415,14 @@
415
415
  --max-w-nav-desktop: 720px;
416
416
 
417
417
  --content-max-width: 640px;
418
+ --content-max-narrow: 480px; /* .main-content--narrow */
419
+ --content-max-default: 600px; /* .main-content (default) */
420
+ --content-max-wide: 800px; /* .main-content--wide */
421
+
422
+ /* Matt-constraints for komponenters width/height. */
423
+ --measure-text: 320px; /* max-width for brodtext */
424
+ --measure-ui-sm: 160px; /* min-width dropdown/input */
425
+ --measure-overlay: 480px; /* max-width overlay/dialog */
418
426
  }
419
427
 
420
428
 
@@ -120,6 +120,7 @@ button, a, [role="button"],
120
120
  top: 0;
121
121
  left: 0;
122
122
  right: 0;
123
+ /* stylelint-disable-next-line klodd/no-literal-dimension -- dekorativ hero-glow-gradienthojd, en-off */
123
124
  height: 420px;
124
125
  background: var(--hero-glow);
125
126
  pointer-events: none;
@@ -204,8 +205,8 @@ body[data-first-load="true"] .bottom-nav {
204
205
  transform: translate(-50%, -40px);
205
206
  z-index: var(--z-overlay);
206
207
  pointer-events: none;
207
- width: 36px;
208
- height: 36px;
208
+ width: var(--circle-size);
209
+ height: var(--circle-size);
209
210
  border-radius: 50%;
210
211
  background: var(--surface-strong);
211
212
  display: flex;
@@ -215,8 +216,8 @@ body[data-first-load="true"] .bottom-nav {
215
216
  }
216
217
 
217
218
  .ptr-spinner {
218
- width: 18px;
219
- height: 18px;
219
+ width: var(--space-18);
220
+ height: var(--space-18);
220
221
  border: 2px solid var(--text-muted);
221
222
  border-top-color: var(--accent-9);
222
223
  border-radius: 50%;
@@ -4,15 +4,15 @@
4
4
  Anvands som default-skelett pa varje vy.
5
5
  ================================================================ */
6
6
  .main-content {
7
- max-width: var(--content-max-default, 600px);
7
+ max-width: var(--content-max-default);
8
8
  margin: 0 auto;
9
9
  padding: calc(var(--safe-top) + var(--space-16)) var(--space-14) var(--bottom-nav-clearance);
10
10
  position: relative;
11
11
  min-height: calc(100dvh - var(--bottom-nav-clearance));
12
12
  }
13
13
 
14
- .main-content--narrow { max-width: var(--content-max-narrow, 480px); }
15
- .main-content--wide { max-width: var(--content-max-wide, 800px); }
14
+ .main-content--narrow { max-width: var(--content-max-narrow); }
15
+ .main-content--wide { max-width: var(--content-max-wide); }
16
16
 
17
17
  .page-header {
18
18
  display: flex;
package/css/base/pwa.css CHANGED
@@ -62,7 +62,7 @@ body {
62
62
 
63
63
  .kds-scroll-buffer {
64
64
  display: block;
65
- height: 4px;
65
+ height: var(--space-4);
66
66
  pointer-events: none;
67
67
  }
68
68
 
@@ -50,7 +50,7 @@
50
50
  }
51
51
 
52
52
  .async-progress__bar-track {
53
- height: 6px;
53
+ height: var(--space-6);
54
54
  background: var(--surface-sunken);
55
55
  border-radius: var(--radius-full);
56
56
  overflow: hidden;
@@ -29,7 +29,7 @@
29
29
 
30
30
  .auth-container--narrow {
31
31
  min-height: auto;
32
- max-width: 480px;
32
+ max-width: var(--measure-overlay);
33
33
  margin-left: auto;
34
34
  margin-right: auto;
35
35
  }
@@ -239,8 +239,8 @@
239
239
  content: "";
240
240
  position: absolute;
241
241
  inset: 50% auto auto 50%;
242
- width: 16px;
243
- height: 16px;
242
+ width: var(--space-16);
243
+ height: var(--space-16);
244
244
  margin: -8px 0 0 -8px;
245
245
  border: 2px solid currentColor;
246
246
  border-top-color: transparent;
@@ -35,8 +35,8 @@
35
35
 
36
36
  .chip::before {
37
37
  content: '';
38
- width: 6px;
39
- height: 6px;
38
+ width: var(--space-6);
39
+ height: var(--space-6);
40
40
  border-radius: var(--radius-full);
41
41
  background: currentColor;
42
42
  display: inline-block;
@@ -62,7 +62,7 @@
62
62
  display: inline-flex;
63
63
  align-items: center;
64
64
  gap: var(--space-6);
65
- height: 32px;
65
+ height: var(--space-32);
66
66
  padding: 0 var(--space-12);
67
67
  background: var(--surface-default);
68
68
  border: 1px solid var(--border-subtle);
@@ -91,8 +91,8 @@
91
91
  display: inline-flex;
92
92
  align-items: center;
93
93
  justify-content: center;
94
- width: 18px;
95
- height: 18px;
94
+ width: var(--space-18);
95
+ height: var(--space-18);
96
96
  padding: 0;
97
97
  background: transparent;
98
98
  border: 0;
@@ -192,7 +192,7 @@
192
192
  display: inline-flex;
193
193
  align-items: center;
194
194
  gap: var(--space-4);
195
- height: 36px;
195
+ height: var(--circle-size);
196
196
  padding: 0 var(--space-6);
197
197
  background: var(--surface-default);
198
198
  border: 1px solid var(--border-subtle);
@@ -25,7 +25,7 @@
25
25
  position: absolute;
26
26
  top: calc(100% + var(--space-4));
27
27
  left: 0;
28
- min-width: 160px;
28
+ min-width: var(--measure-ui-sm);
29
29
  background: var(--surface-overlay);
30
30
  border: 1px solid var(--border-default);
31
31
  border-radius: var(--radius-14);
@@ -122,7 +122,7 @@
122
122
  font-size: var(--fs-14);
123
123
  line-height: var(--lh-base);
124
124
  color: var(--text-subtle);
125
- max-width: 320px;
125
+ max-width: var(--measure-text);
126
126
  margin: 0;
127
127
  }
128
128
 
@@ -158,7 +158,7 @@
158
158
  }
159
159
 
160
160
  .skeleton--card {
161
- height: 80px;
161
+ height: var(--space-80);
162
162
  border-radius: var(--radius-14);
163
163
  }
164
164
 
@@ -182,8 +182,8 @@
182
182
  ================================================================ */
183
183
  .spinner {
184
184
  display: inline-block;
185
- width: 24px;
186
- height: 24px;
185
+ width: var(--space-24);
186
+ height: var(--space-24);
187
187
  border: 2px solid currentColor;
188
188
  border-top-color: transparent;
189
189
  border-radius: var(--radius-full);
@@ -193,14 +193,14 @@
193
193
  }
194
194
 
195
195
  .spinner--sm {
196
- width: 16px;
197
- height: 16px;
196
+ width: var(--space-16);
197
+ height: var(--space-16);
198
198
  border-width: 2px;
199
199
  }
200
200
 
201
201
  .spinner--lg {
202
- width: 40px;
203
- height: 40px;
202
+ width: var(--space-40);
203
+ height: var(--space-40);
204
204
  border-width: 3px;
205
205
  }
206
206
 
@@ -24,7 +24,7 @@
24
24
  border: 1px solid var(--border-subtle);
25
25
  border-radius: var(--radius-14);
26
26
  padding: var(--space-20);
27
- max-width: 480px;
27
+ max-width: var(--measure-overlay);
28
28
  margin-bottom: var(--space-14);
29
29
  }
30
30
 
@@ -62,7 +62,7 @@
62
62
  position: absolute;
63
63
  left: 0;
64
64
  right: 0;
65
- height: 14px;
65
+ height: var(--space-14);
66
66
  pointer-events: none;
67
67
  z-index: 1;
68
68
  background: transparent;
@@ -62,7 +62,7 @@
62
62
 
63
63
  .inline-edit__input {
64
64
  display: inline-block;
65
- height: 32px;
65
+ height: var(--space-32);
66
66
  padding: 0 var(--space-10);
67
67
  background: var(--surface-sunken);
68
68
  border: 1px solid var(--border-default);
@@ -55,6 +55,7 @@
55
55
  }
56
56
 
57
57
  .textarea {
58
+ /* stylelint-disable-next-line klodd/no-literal-dimension -- textarea min-height, dokumenterat en-off */
58
59
  min-height: 96px;
59
60
  padding: var(--space-12) var(--space-14);
60
61
  line-height: var(--lh-base);
@@ -136,7 +137,7 @@
136
137
  .select.input--compact {
137
138
  min-height: auto;
138
139
  width: auto;
139
- max-width: 160px;
140
+ max-width: var(--measure-ui-sm);
140
141
  padding: var(--space-6) var(--space-10);
141
142
  font-size: var(--fs-16);
142
143
  }
@@ -36,7 +36,7 @@
36
36
  gap: var(--space-12);
37
37
  padding: var(--space-10) 0;
38
38
  border-bottom: 1px solid var(--border-subtle);
39
- min-height: 56px;
39
+ min-height: var(--space-56);
40
40
  }
41
41
 
42
42
  .list-row:last-child { border-bottom: none; }
@@ -54,8 +54,8 @@
54
54
  }
55
55
 
56
56
  .list-row__icon svg {
57
- width: 18px;
58
- height: 18px;
57
+ width: var(--space-18);
58
+ height: var(--space-18);
59
59
  stroke: currentColor;
60
60
  fill: none;
61
61
  stroke-width: 2;
@@ -25,8 +25,8 @@
25
25
  }
26
26
 
27
27
  .offline-icon {
28
- width: 64px;
29
- height: 64px;
28
+ width: var(--space-64);
29
+ height: var(--space-64);
30
30
  background: var(--surface-raised);
31
31
  border: 1px solid var(--border-default);
32
32
  border-radius: var(--radius-14);
@@ -47,7 +47,7 @@
47
47
  .offline-text {
48
48
  font-size: var(--fs-14);
49
49
  color: var(--text-subtle);
50
- max-width: 320px;
50
+ max-width: var(--measure-text);
51
51
  line-height: var(--lh-base);
52
52
  margin-bottom: var(--space-8);
53
53
  }
@@ -40,7 +40,7 @@ dialog.sheet::backdrop {
40
40
  transform: translate(-50%, -50%);
41
41
  z-index: var(--z-overlay);
42
42
  width: calc(100vw - 2 * var(--space-20));
43
- max-width: 480px;
43
+ max-width: var(--measure-overlay);
44
44
  max-height: calc(100dvh - 2 * var(--space-40));
45
45
  margin: 0;
46
46
  padding: 0;
@@ -117,7 +117,7 @@ dialog.dialog:modal {
117
117
  top: auto;
118
118
  z-index: var(--z-overlay);
119
119
  width: 100%;
120
- max-width: 600px;
120
+ max-width: var(--content-max-default);
121
121
  max-height: 90svh;
122
122
  margin: 0 auto;
123
123
  padding: var(--space-8) var(--space-20) calc(var(--space-28) + var(--safe-bottom));
@@ -136,8 +136,8 @@ dialog.dialog:modal {
136
136
  }
137
137
 
138
138
  .sheet__handle {
139
- width: 32px;
140
- height: 4px;
139
+ width: var(--space-32);
140
+ height: var(--space-4);
141
141
  margin: 0 auto var(--space-16);
142
142
  background: var(--text-disabled);
143
143
  border-radius: var(--radius-full);
@@ -90,7 +90,7 @@
90
90
  .panel__pill {
91
91
  display: inline-flex;
92
92
  align-items: center;
93
- height: 24px;
93
+ height: var(--space-24);
94
94
  padding: 0 var(--space-10);
95
95
  background: var(--surface-default);
96
96
  color: var(--text-muted);
@@ -119,7 +119,7 @@
119
119
  .panel__step-badge {
120
120
  display: inline-flex;
121
121
  align-items: center;
122
- height: 20px;
122
+ height: var(--space-20);
123
123
  padding: 0 var(--space-8);
124
124
  background: var(--surface-active);
125
125
  color: var(--accent-text);
@@ -49,7 +49,7 @@
49
49
  display: inline-flex;
50
50
  align-items: center;
51
51
  justify-content: center;
52
- height: 36px;
52
+ height: var(--circle-size);
53
53
  padding: 0 var(--space-16);
54
54
  background: var(--surface-default);
55
55
  border: 1px solid var(--border-subtle);
@@ -45,7 +45,7 @@
45
45
  }
46
46
 
47
47
  .pipeline-progress__bar-track {
48
- height: 6px;
48
+ height: var(--space-6);
49
49
  background: var(--surface-sunken);
50
50
  border-radius: var(--radius-full);
51
51
  overflow: hidden;
@@ -64,7 +64,7 @@
64
64
  }
65
65
 
66
66
  .progress--thin { height: var(--space-4); }
67
- .progress--thick { height: 12px; }
67
+ .progress--thick { height: var(--space-12); }
68
68
 
69
69
  .progress__bar {
70
70
  height: 100%;
@@ -133,7 +133,7 @@
133
133
  .setting-row__pill {
134
134
  display: inline-flex;
135
135
  align-items: center;
136
- height: 28px;
136
+ height: var(--space-28);
137
137
  padding: 0 var(--space-12);
138
138
  background: var(--accent-9);
139
139
  color: var(--text-on-accent);
@@ -158,7 +158,8 @@
158
158
  .setting-toggle {
159
159
  position: relative;
160
160
  display: inline-block;
161
- width: 44px;
161
+ width: var(--touch-min);
162
+ /* stylelint-disable-next-line klodd/no-literal-dimension -- toggle-track 26px (22px thumb + 2x2px inset), dokumenterat en-off */
162
163
  height: 26px;
163
164
  flex-shrink: 0;
164
165
  }
@@ -185,8 +186,8 @@
185
186
  position: absolute;
186
187
  top: 2px;
187
188
  left: 2px;
188
- width: 22px;
189
- height: 22px;
189
+ width: var(--space-22);
190
+ height: var(--space-22);
190
191
  background: var(--surface-page);
191
192
  border-radius: 50%;
192
193
  transition: transform var(--dur-medium) var(--ease-spring-snappy);
@@ -92,7 +92,7 @@
92
92
  background: var(--surface-sunken);
93
93
  border: 1px solid var(--border-default);
94
94
  border-radius: var(--radius-14);
95
- height: 48px;
95
+ height: var(--space-48);
96
96
  transition: background var(--dur-fast) var(--ease-spring-snappy);
97
97
  }
98
98
 
@@ -309,8 +309,8 @@
309
309
  }
310
310
 
311
311
  .sheet__toggle-input {
312
- width: 22px;
313
- height: 22px;
312
+ width: var(--space-22);
313
+ height: var(--space-22);
314
314
  accent-color: var(--accent-9);
315
315
  cursor: pointer;
316
316
  flex-shrink: 0;
@@ -338,7 +338,7 @@
338
338
  .sheet__btn--save {
339
339
  display: block;
340
340
  width: 100%;
341
- height: 48px;
341
+ height: var(--space-48);
342
342
  margin: var(--space-8) 0 0;
343
343
  background: var(--accent-9);
344
344
  color: var(--text-on-accent);
@@ -372,7 +372,7 @@
372
372
  .sheet__btn--delete {
373
373
  display: block;
374
374
  width: 100%;
375
- height: 44px;
375
+ height: var(--touch-min);
376
376
  margin: var(--space-8) 0 0;
377
377
  background: transparent;
378
378
  color: var(--accent-danger);
@@ -41,8 +41,10 @@
41
41
  .swipe-stack {
42
42
  position: relative;
43
43
  width: 100%;
44
+ /* stylelint-disable-next-line klodd/no-literal-dimension -- swipe-stack-bredd, dokumenterat en-off */
44
45
  max-width: 480px;
45
46
  margin: 0 auto;
47
+ /* stylelint-disable-next-line klodd/no-literal-dimension -- swipe-stack-hojd, dokumenterat en-off */
46
48
  height: 480px;
47
49
  isolation: isolate;
48
50
  }
@@ -20,7 +20,7 @@
20
20
  display: inline-flex;
21
21
  align-items: center;
22
22
  justify-content: center;
23
- min-height: 44px;
23
+ min-height: var(--touch-min);
24
24
  padding: 0 var(--space-12);
25
25
  background: transparent;
26
26
  border: 0;
@@ -57,6 +57,7 @@
57
57
  .upload-spinner-overlay__hint {
58
58
  font-size: var(--fs-12);
59
59
  color: var(--text-muted);
60
+ /* stylelint-disable-next-line klodd/no-literal-dimension -- spinner-hint textbredd 280px, dokumenterat en-off */
60
61
  max-width: 280px;
61
62
  }
62
63
 
@@ -91,12 +91,23 @@
91
91
  this.failed = false;
92
92
  this.consecutiveErrors = 0;
93
93
  if (this.pollTimer !== null) return;
94
- // Forsta poll efter 100ms sa ev. fetch-patches (auth-tokens etc)
95
- // hinner appliceras.
96
- setTimeout(() => this._poll(), 100);
94
+ // Forsta poll vantar in KloddDS.clerkReady (clerk-init.js) sa en
95
+ // ev. fetch-Bearer-patch ar installerad innan vi traffar /api/.
96
+ // Ersatter en tidigare blind setTimeout(_poll, 100)-gissning som
97
+ // tappade racet vid langsam Clerk.load(). Saknas clerkReady
98
+ // (clerk-init.js ej laddad) pollar _firstPoll direkt - graceful
99
+ // degradation.
100
+ this._firstPoll();
97
101
  this.pollTimer = setInterval(() => this._poll(), this.cfg.pollInterval);
98
102
  }
99
103
 
104
+ async _firstPoll() {
105
+ if (window.KloddDS && window.KloddDS.clerkReady) {
106
+ await window.KloddDS.clerkReady;
107
+ }
108
+ this._poll();
109
+ }
110
+
100
111
  stop() {
101
112
  if (this.pollTimer !== null) {
102
113
  clearInterval(this.pollTimer);
@@ -0,0 +1,109 @@
1
+ /* @klodd/ds - clerk-init
2
+ *
3
+ * Delad Clerk-initialisering for apparna som konsumerar @klodd/ds.
4
+ * Extraherad ur appernas app.js (tidigare byte-identiska kopior med
5
+ * tva divergenser: fetch-Bearer-patchen och logout-redirecten).
6
+ *
7
+ * Exponerar tre globals:
8
+ *
9
+ * window.waitForClerkBase(cb, attempts)
10
+ * Pollar window.Clerk var 200ms (max 50 forsok) och kor cb nar
11
+ * SDK:n finns. Anvands ocksa av page-login.js.
12
+ *
13
+ * window.clerkLogout()
14
+ * Async. signOut() + redirect till config.logoutRedirect. Satts
15
+ * upp av initClerk eftersom den behover config-vardet.
16
+ *
17
+ * KloddDS.clerkReady
18
+ * Promise som resolvar nar Clerk.load() ar klar. Resolvar i ett
19
+ * finally - alltsa aven om Clerk.load() kastar - sa konsumenter
20
+ * inte hanger for evigt vid en Clerk-storning. Resolvar oavsett
21
+ * fetchPatch-konfiguration. Representerar "Clerk ar redo", inte
22
+ * "patchen ar installerad". Tidiga /api/-fetchers (AsyncProgress)
23
+ * awaitar den istallet for att gissa en setTimeout-delay.
24
+ *
25
+ * KloddDS.initClerk(config) startar allt. config-falt:
26
+ *
27
+ * fetchPatch (default false)
28
+ * Om true: patcha window.fetch sa /api/-anrop far en
29
+ * Authorization: Bearer-header. Fallback nar cookies inte gar
30
+ * igenom (PWA-standalone pa iOS). Opt-in per app.
31
+ *
32
+ * logoutRedirect (default "/")
33
+ * Dit clerkLogout navigerar efter signOut().
34
+ *
35
+ * initClerk anropas via en liten extern per-app config-fil
36
+ * (clerk-config.js) - CSP utan 'unsafe-inline' tillater inte
37
+ * inline-script. defer-ordning garanterar att initClerk finns nar
38
+ * config-filen kor.
39
+ */
40
+ (function () {
41
+ "use strict";
42
+
43
+ window.KloddDS = window.KloddDS || {};
44
+
45
+ // clerkReady skapas direkt nar denna fil kor, sa konsumenter kan
46
+ // awaita den aven innan initClerk hunnit anropas. resolve-funktionen
47
+ // sparas sa initClerk-callbacken kan resolva den.
48
+ let resolveClerkReady;
49
+ window.KloddDS.clerkReady = new Promise(function (resolve) {
50
+ resolveClerkReady = resolve;
51
+ });
52
+
53
+ function waitForClerkBase(cb, attempts) {
54
+ if (window.Clerk) return cb();
55
+ if ((attempts || 0) > 50) return console.error("Clerk JS failed to load");
56
+ setTimeout(function () { waitForClerkBase(cb, (attempts || 0) + 1); }, 200);
57
+ }
58
+ window.waitForClerkBase = waitForClerkBase;
59
+
60
+ // Patchar window.fetch sa /api/-anrop far Authorization: Bearer som
61
+ // fallback nar cookies inte gar igenom (PWA-standalone pa iOS).
62
+ // Oforandrad logik fran Jubbs tidigare app.js-inline-version.
63
+ function installFetchPatch() {
64
+ if (!window.Clerk.session) return;
65
+ const originalFetch = window.fetch.bind(window);
66
+ window.fetch = async function (input, init) {
67
+ try {
68
+ const url = typeof input === "string" ? input : input.url;
69
+ if (url && (url.startsWith("/api/") || url.startsWith(window.location.origin + "/api/"))) {
70
+ const token = await window.Clerk.session.getToken();
71
+ if (token) {
72
+ init = init || {};
73
+ init.headers = new Headers(init.headers || {});
74
+ init.headers.set("Authorization", "Bearer " + token);
75
+ }
76
+ }
77
+ } catch (e) {
78
+ console.warn("Clerk token fetch failed", e);
79
+ }
80
+ return originalFetch(input, init);
81
+ };
82
+ }
83
+
84
+ function initClerk(config) {
85
+ const cfg = Object.assign(
86
+ { fetchPatch: false, logoutRedirect: "/" },
87
+ config || {}
88
+ );
89
+ waitForClerkBase(async function () {
90
+ try {
91
+ await window.Clerk.load();
92
+ if (cfg.fetchPatch) installFetchPatch();
93
+ } catch (e) {
94
+ console.log("Clerk init:", e);
95
+ } finally {
96
+ // finally: clerkReady resolvar aven om Clerk.load() kastade,
97
+ // sa await KloddDS.clerkReady aldrig hanger for evigt.
98
+ resolveClerkReady();
99
+ window.clerkLogout = async function () {
100
+ if (window.Clerk) {
101
+ await window.Clerk.signOut();
102
+ }
103
+ window.location.href = cfg.logoutRedirect;
104
+ };
105
+ }
106
+ }, 0);
107
+ }
108
+ window.KloddDS.initClerk = initClerk;
109
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klodd/ds",
3
- "version": "5.7.1",
3
+ "version": "5.9.0",
4
4
  "description": "Klodd shared design system - tokens, components, JS",
5
5
  "main": "css/index.css",
6
6
  "bin": {
@@ -0,0 +1,43 @@
1
+ # 0025 - content-max token-konsolidering
2
+
3
+ ## Status
4
+
5
+ Pending.
6
+
7
+ ## Context
8
+
9
+ Tre överlappande page-width-tokenfamiljer finns i paketet efter
10
+ width/height-arbetet (5.8.0):
11
+
12
+ 1. `--content-max-*` (`-narrow` 480px, `-default` 600px, `-wide`
13
+ 800px) - definierade i 00-primitives.css, konsumeras av
14
+ base/layout.css (`.main-content`-varianterna).
15
+ 2. `--content-max-width: 640px` - orphan i 00-primitives.css, noll
16
+ användning i paketet eller apparna.
17
+ 3. `--max-w-*` (sex tokens: `-mobile` 600, `-tablet` 680, `-desktop`
18
+ 1280, `-form` 560, `-bottom-nav` 572, `-nav-desktop` 720) i
19
+ 00-primitives.css. `--max-w-mobile: 600px` överlappar exakt
20
+ `--content-max-default: 600px`.
21
+
22
+ ## Decision needed
23
+
24
+ Konsolidera till en page-width-tokenfamilj: behåll sannolikt
25
+ `--content-max-*`, radera orphan `--content-max-width`, mappa eller
26
+ radera `--max-w-*`-familjen.
27
+
28
+ ## Considered options
29
+
30
+ ### A. Konsolidera nu
31
+ Risk: `--max-w-*` och `--content-max-width` har okänd konsument-yta.
32
+ Apparnas domain-CSS och templates greppades inte. Att radera eller
33
+ mappa dem kan ge visuell drift på okänt antal vyer.
34
+
35
+ ### B. Skjut till separat sprint (vald interim)
36
+ Lämna alla tre familjer orörda i 5.8.0. Konsolidera när
37
+ konsument-ytan kartlagts.
38
+
39
+ ## Pending decision criteria
40
+
41
+ Kräver en survey av `--max-w-*`- och `--content-max-width`-användning
42
+ i båda app-repona (CSS + templates) innan konsolidering. Tas i egen
43
+ sprint.
@@ -1,7 +1,7 @@
1
1
  /* @klodd/ds stylelint-plugin
2
2
  *
3
3
  * Custom design-rule-validation som stylelint-config-standard inte
4
- * tacker. Tre regler, alla "klodd/"-prefixade:
4
+ * tacker. Fyra regler, alla "klodd/"-prefixade:
5
5
  *
6
6
  * - klodd/no-forbidden-radius:
7
7
  * border-radius maste vara en av {var(--radius-4), var(--radius-14),
@@ -21,6 +21,11 @@
21
21
  * review mot ADR 0017-tabellen - plugin validerar bara token-
22
22
  * existens, inte semantisk val per komponent.
23
23
  *
24
+ * - klodd/no-literal-dimension:
25
+ * width/height/min/max-width/height maste anvanda token (var--*)
26
+ * eller relativ enhet. Hardkodad px flaggas, utom 1px/3px
27
+ * (hairlines + dekorativ dot). utilities.css exkluderas helt.
28
+ *
24
29
  * Konfiguration i app-repos:
25
30
  * .stylelintrc.json:
26
31
  * {
@@ -28,7 +33,8 @@
28
33
  * "rules": {
29
34
  * "klodd/no-forbidden-radius": true,
30
35
  * "klodd/no-forbidden-shadow": true,
31
- * "klodd/no-forbidden-padding": true
36
+ * "klodd/no-forbidden-padding": true,
37
+ * "klodd/no-literal-dimension": true
32
38
  * }
33
39
  * }
34
40
  */
@@ -66,9 +72,17 @@ const SHADOW_INSET = /\binset\b/i;
66
72
  const SHADOW_FOCUS_RING = /^0\s+0\s+0\s+\d+px\s+(var\(--accent-a\d+\)|color-mix\(.+\))/i;
67
73
  const SHADOW_CARD_TOKEN = /^var\(--shadow-card\)$/i;
68
74
 
75
+ // no-literal-dimension: width/height-varden som INTE ar hardkodad px.
76
+ const DIMENSION_PROPS = /^(min-|max-)?(width|height)$/;
77
+ const DIMENSION_KEYWORD = /^(0|auto|none|inherit|initial|unset|fit-content|min-content|max-content)$/i;
78
+ const DIMENSION_REL_UNIT = /^[\d.]+(%|vw|vh|dvh|svh|vmin|vmax|rem)$/i;
79
+ const DIMENSION_PX = /^[\d.]+px$/i;
80
+ const DIMENSION_PX_OK = /^(1|3)px$/;
81
+
69
82
  const RADIUS_RULE = 'klodd/no-forbidden-radius';
70
83
  const SHADOW_RULE = 'klodd/no-forbidden-shadow';
71
84
  const PADDING_RULE = 'klodd/no-forbidden-padding';
85
+ const DIMENSION_RULE = 'klodd/no-literal-dimension';
72
86
 
73
87
  const messages = {
74
88
  radius: stylelint.utils.ruleMessages(RADIUS_RULE, {
@@ -83,6 +97,10 @@ const messages = {
83
97
  forbidden: (value) =>
84
98
  `Forbjudet padding-varde "${value}" - anvand var(--space-N) (existerande token i 00-primitives.css). Hardkodade px-varden inte tillatna.`,
85
99
  }),
100
+ dimension: stylelint.utils.ruleMessages(DIMENSION_RULE, {
101
+ forbidden: (value) =>
102
+ `Forbjudet width/height-varde "${value}" - anvand var(--space-N) | var(--measure-*) | var(--circle-size*) | var(--touch-*). Hardkodad px inte tillaten (undantag: 1px/3px).`,
103
+ }),
86
104
  };
87
105
 
88
106
  function checkRadius (root, result, primary) {
@@ -153,6 +171,30 @@ function checkPadding (root, result, primary) {
153
171
  });
154
172
  }
155
173
 
174
+ function checkDimension (root, result, primary) {
175
+ if (!primary) return;
176
+ // utilities.css ar fixed-dimension-utility-lagret - exkluderas helt.
177
+ const file = root.source && root.source.input && root.source.input.file;
178
+ if (file && (file.endsWith('/utilities.css') || file.endsWith('\\utilities.css'))) return;
179
+ root.walkDecls(DIMENSION_PROPS, (decl) => {
180
+ const value = decl.value.trim();
181
+ if (!value) return;
182
+ if (DIMENSION_KEYWORD.test(value)) return;
183
+ if (/^var\(/i.test(value)) return;
184
+ if (/^calc\(/i.test(value)) return;
185
+ if (DIMENSION_REL_UNIT.test(value)) return;
186
+ if (DIMENSION_PX_OK.test(value)) return;
187
+ if (DIMENSION_PX.test(value)) {
188
+ stylelint.utils.report({
189
+ ruleName: DIMENSION_RULE,
190
+ result,
191
+ node: decl,
192
+ message: messages.dimension.forbidden(value),
193
+ });
194
+ }
195
+ });
196
+ }
197
+
156
198
  const radiusPlugin = stylelint.createPlugin(RADIUS_RULE, (primary) => {
157
199
  return (root, result) => {
158
200
  const validOptions = stylelint.utils.validateOptions(result, RADIUS_RULE, {
@@ -192,4 +234,17 @@ const paddingPlugin = stylelint.createPlugin(PADDING_RULE, (primary) => {
192
234
  paddingPlugin.ruleName = PADDING_RULE;
193
235
  paddingPlugin.messages = messages.padding;
194
236
 
195
- module.exports = [radiusPlugin, shadowPlugin, paddingPlugin];
237
+ const dimensionPlugin = stylelint.createPlugin(DIMENSION_RULE, (primary) => {
238
+ return (root, result) => {
239
+ const validOptions = stylelint.utils.validateOptions(result, DIMENSION_RULE, {
240
+ actual: primary,
241
+ possible: [true, false],
242
+ });
243
+ if (!validOptions) return;
244
+ checkDimension(root, result, primary);
245
+ };
246
+ });
247
+ dimensionPlugin.ruleName = DIMENSION_RULE;
248
+ dimensionPlugin.messages = messages.dimension;
249
+
250
+ module.exports = [radiusPlugin, shadowPlugin, paddingPlugin, dimensionPlugin];