@klodd/ds 4.1.0 → 4.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/utilities.css CHANGED
@@ -148,6 +148,30 @@
148
148
  vertical-align: -2px;
149
149
  }
150
150
 
151
+ /* Skip-link - a11y-pattern for keyboard-anvandare. Visas vid :focus
152
+ (top: var(--space-8) overrider top: -100px). HTML: lagg som forsta
153
+ element i body, target valfri main-content-id.
154
+ <a href="#main" class="skip-link">Hoppa till innehall</a>
155
+ Lyft fran Ekonom + Jubb v4.3.0 (identical implementation efter
156
+ konsekvens-cykel 2026-05-14 Sprint 5). */
157
+ .skip-link {
158
+ position: absolute;
159
+ top: -100px;
160
+ left: var(--space-8);
161
+ padding: var(--space-8) var(--space-16);
162
+ background: var(--accent-9);
163
+ color: var(--surface-page);
164
+ border-radius: var(--radius-full);
165
+ z-index: var(--z-spinner);
166
+ font-weight: var(--fw-medium);
167
+ }
168
+
169
+ .skip-link:focus {
170
+ top: var(--space-8);
171
+ outline: 2px solid var(--accent-9);
172
+ outline-offset: 2px;
173
+ }
174
+
151
175
  /* Pre-formatted code/text. Sunken bakgrund, monospace, pre-wrap
152
176
  for line-break-preservering med word-wrap pa lang text. */
153
177
  .code-block {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klodd/ds",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "Klodd shared design system - tokens, components, JS",
5
5
  "main": "css/index.css",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "css/",
11
11
  "js/",
12
12
  "bin/",
13
+ "stylelint-plugin/",
13
14
  "SKILL.md",
14
15
  "references/"
15
16
  ],
@@ -0,0 +1,192 @@
1
+ /* @klodd/ds stylelint-plugin
2
+ *
3
+ * Custom design-rule-validation som stylelint-config-standard inte
4
+ * tacker. Tre regler, alla "klodd/"-prefixade:
5
+ *
6
+ * - klodd/no-forbidden-radius:
7
+ * border-radius maste vara en av {var(--radius-4), var(--radius-14),
8
+ * 50%, var(--radius-full), 9999px} per ADR 0013 + DESIGN-LANGUAGE
9
+ * sektion 1 (BR-skala 4 varden).
10
+ *
11
+ * - klodd/no-forbidden-shadow:
12
+ * box-shadow maste vara `none`, `inset ...` (focus-rings),
13
+ * `0 0 0 Npx var(--accent-aN)` (focus-rings via spread), eller
14
+ * `var(--shadow-card)` per regel 10 + DESIGN-LANGUAGE sektion 2.
15
+ *
16
+ * - klodd/no-forbidden-padding:
17
+ * padding-varden maste anvanda var(--space-N) dar N matchar en
18
+ * existerande token i 00-primitives.css. Hardkodade px/em/%-
19
+ * varden (utom 0/auto) flaggas. Per-komponent-typ-disciplin
20
+ * (list-row 10/0, banner 14/16 etc) ar fortfarande mansklig
21
+ * review mot ADR 0017-tabellen - plugin validerar bara token-
22
+ * existens, inte semantisk val per komponent.
23
+ *
24
+ * Konfiguration i app-repos:
25
+ * .stylelintrc.json:
26
+ * {
27
+ * "plugins": ["@klodd/ds/stylelint-plugin"],
28
+ * "rules": {
29
+ * "klodd/no-forbidden-radius": true,
30
+ * "klodd/no-forbidden-shadow": true,
31
+ * "klodd/no-forbidden-padding": true
32
+ * }
33
+ * }
34
+ */
35
+
36
+ const stylelint = require('stylelint');
37
+
38
+ const ALLOWED_RADIUS = new Set([
39
+ '0',
40
+ '50%',
41
+ '9999px',
42
+ 'var(--radius-4)',
43
+ 'var(--radius-14)',
44
+ 'var(--radius-full)',
45
+ 'inherit',
46
+ 'initial',
47
+ 'unset',
48
+ ]);
49
+
50
+ // Tillatna --space-N tokens enligt 00-primitives.css.
51
+ // Komponent-padding bor halla sig inom 4-24-skalan per ADR 0017 men
52
+ // mini-elements (badge, pill-nav) far anvanda --space-2 och
53
+ // layout-section-padding far anvanda --space-28/-32/-40+. Pluginet
54
+ // validerar bara att tokenen EXISTERAR - per-komponent-typ-disciplin
55
+ // ar fortfarande mansklig review mot ADR 0017-tabellen.
56
+ const ALLOWED_PADDING_TOKENS = new Set([
57
+ '2', '4', '6', '8', '10', '12', '14', '16', '18', '20',
58
+ '22', '24', '28', '32', '40', '48', '56', '64', '80',
59
+ ]);
60
+
61
+ const SHADOW_NONE = /^none$/i;
62
+ const SHADOW_INSET = /\binset\b/i;
63
+ const SHADOW_FOCUS_RING = /^0\s+0\s+0\s+\d+px\s+(var\(--accent-a\d+\)|color-mix\(.+\))/i;
64
+ const SHADOW_CARD_TOKEN = /^var\(--shadow-card\)$/i;
65
+
66
+ const RADIUS_RULE = 'klodd/no-forbidden-radius';
67
+ const SHADOW_RULE = 'klodd/no-forbidden-shadow';
68
+ const PADDING_RULE = 'klodd/no-forbidden-padding';
69
+
70
+ const messages = {
71
+ radius: stylelint.utils.ruleMessages(RADIUS_RULE, {
72
+ forbidden: (value) =>
73
+ `Forbjudet border-radius "${value}" - anvand en av: var(--radius-4) | var(--radius-14) | 50% | var(--radius-full) (ADR 0013 + DESIGN-LANGUAGE 1)`,
74
+ }),
75
+ shadow: stylelint.utils.ruleMessages(SHADOW_RULE, {
76
+ forbidden: (value) =>
77
+ `Forbjudet box-shadow "${value}" - tillatet: none | inset focus-rings | var(--shadow-card) (DESIGN-LANGUAGE 2 + regel 10)`,
78
+ }),
79
+ padding: stylelint.utils.ruleMessages(PADDING_RULE, {
80
+ forbidden: (value) =>
81
+ `Forbjudet padding-varde "${value}" - anvand var(--space-N) (existerande token i 00-primitives.css). Hardkodade px-varden inte tillatna.`,
82
+ }),
83
+ };
84
+
85
+ function checkRadius (root, result, primary) {
86
+ if (!primary) return;
87
+ root.walkDecls(/^border(-(top|right|bottom|left))?-?radius$/, (decl) => {
88
+ const values = decl.value.split(/\s+/);
89
+ values.forEach((v) => {
90
+ const normalized = v.trim();
91
+ if (!normalized) return;
92
+ if (ALLOWED_RADIUS.has(normalized)) return;
93
+ stylelint.utils.report({
94
+ ruleName: RADIUS_RULE,
95
+ result,
96
+ node: decl,
97
+ message: messages.radius.forbidden(normalized),
98
+ });
99
+ });
100
+ });
101
+ }
102
+
103
+ function checkShadow (root, result, primary) {
104
+ if (!primary) return;
105
+ root.walkDecls(/^box-shadow$/, (decl) => {
106
+ const value = decl.value.trim();
107
+ if (SHADOW_NONE.test(value)) return;
108
+ if (SHADOW_INSET.test(value)) return;
109
+ if (SHADOW_FOCUS_RING.test(value)) return;
110
+ if (SHADOW_CARD_TOKEN.test(value)) return;
111
+ stylelint.utils.report({
112
+ ruleName: SHADOW_RULE,
113
+ result,
114
+ node: decl,
115
+ message: messages.shadow.forbidden(value),
116
+ });
117
+ });
118
+ }
119
+
120
+ function checkPadding (root, result, primary) {
121
+ if (!primary) return;
122
+ root.walkDecls(/^padding(-(top|right|bottom|left))?$/, (decl) => {
123
+ const values = decl.value.split(/\s+/);
124
+ values.forEach((v) => {
125
+ const normalized = v.trim();
126
+ if (!normalized) return;
127
+ if (normalized === '0' || normalized === 'auto' || normalized === 'inherit') return;
128
+ // var(--space-N)?
129
+ const tokenMatch = normalized.match(/^var\(--space-(\d+)\)$/);
130
+ if (tokenMatch) {
131
+ if (ALLOWED_PADDING_TOKENS.has(tokenMatch[1])) return;
132
+ stylelint.utils.report({
133
+ ruleName: PADDING_RULE,
134
+ result,
135
+ node: decl,
136
+ message: messages.padding.forbidden(normalized),
137
+ });
138
+ return;
139
+ }
140
+ // Hardkodad px / em / % - flagga
141
+ if (/^[\d.]+(px|em|rem|%)$/.test(normalized)) {
142
+ stylelint.utils.report({
143
+ ruleName: PADDING_RULE,
144
+ result,
145
+ node: decl,
146
+ message: messages.padding.forbidden(normalized),
147
+ });
148
+ }
149
+ });
150
+ });
151
+ }
152
+
153
+ const radiusPlugin = stylelint.createPlugin(RADIUS_RULE, (primary) => {
154
+ return (root, result) => {
155
+ const validOptions = stylelint.utils.validateOptions(result, RADIUS_RULE, {
156
+ actual: primary,
157
+ possible: [true, false],
158
+ });
159
+ if (!validOptions) return;
160
+ checkRadius(root, result, primary);
161
+ };
162
+ });
163
+ radiusPlugin.ruleName = RADIUS_RULE;
164
+ radiusPlugin.messages = messages.radius;
165
+
166
+ const shadowPlugin = stylelint.createPlugin(SHADOW_RULE, (primary) => {
167
+ return (root, result) => {
168
+ const validOptions = stylelint.utils.validateOptions(result, SHADOW_RULE, {
169
+ actual: primary,
170
+ possible: [true, false],
171
+ });
172
+ if (!validOptions) return;
173
+ checkShadow(root, result, primary);
174
+ };
175
+ });
176
+ shadowPlugin.ruleName = SHADOW_RULE;
177
+ shadowPlugin.messages = messages.shadow;
178
+
179
+ const paddingPlugin = stylelint.createPlugin(PADDING_RULE, (primary) => {
180
+ return (root, result) => {
181
+ const validOptions = stylelint.utils.validateOptions(result, PADDING_RULE, {
182
+ actual: primary,
183
+ possible: [true, false],
184
+ });
185
+ if (!validOptions) return;
186
+ checkPadding(root, result, primary);
187
+ };
188
+ });
189
+ paddingPlugin.ruleName = PADDING_RULE;
190
+ paddingPlugin.messages = messages.padding;
191
+
192
+ module.exports = [radiusPlugin, shadowPlugin, paddingPlugin];