@conduction/theme 1.1.61 → 1.1.62

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 (83) hide show
  1. package/.claude/commands/nlds-reference.md +1216 -0
  2. package/.claude/skills/nlds-new/SKILL.md +48 -0
  3. package/.claude/skills/nlds-update/SKILL.md +53 -0
  4. package/.gitattributes +64 -0
  5. package/README.md +1 -0
  6. package/conduction-design-tokens/src/component/utrecht/extra-tokens/link.tokens.json +2 -1
  7. package/municipalities/vaals-design-tokens/LICENSE.md +17 -0
  8. package/municipalities/vaals-design-tokens/README.md +23 -0
  9. package/municipalities/vaals-design-tokens/documentation/color.stories.mdx +17 -0
  10. package/municipalities/vaals-design-tokens/documentation/components.stories.mdx +11 -0
  11. package/municipalities/vaals-design-tokens/documentation/design-tokens.stories.mdx +14 -0
  12. package/municipalities/vaals-design-tokens/documentation/readme.stories.mdx +7 -0
  13. package/municipalities/vaals-design-tokens/package.json +37 -0
  14. package/municipalities/vaals-design-tokens/src/brand/vaals/color.tokens.json +129 -0
  15. package/municipalities/vaals-design-tokens/src/brand/vaals/font-size.tokens.json +54 -0
  16. package/municipalities/vaals-design-tokens/src/brand/vaals/size.tokens.json +17 -0
  17. package/municipalities/vaals-design-tokens/src/brand/vaals/typography.tokens.json +40 -0
  18. package/municipalities/vaals-design-tokens/src/common/utrecht/action.tokens.json +9 -0
  19. package/municipalities/vaals-design-tokens/src/common/utrecht/space.tokens.json +28 -0
  20. package/municipalities/vaals-design-tokens/src/component/conduction/card-header.tokens.json +34 -0
  21. package/municipalities/vaals-design-tokens/src/component/conduction/card-wrapper.tokens.json +33 -0
  22. package/municipalities/vaals-design-tokens/src/component/conduction/logo.tokens.json +27 -0
  23. package/municipalities/vaals-design-tokens/src/component/conduction/pagination.tokens.json +95 -0
  24. package/municipalities/vaals-design-tokens/src/component/conduction/select.tokens.json +39 -0
  25. package/municipalities/vaals-design-tokens/src/component/conduction/table-wrapper.tokens.json +21 -0
  26. package/municipalities/vaals-design-tokens/src/component/conduction/tooltip.tokens.json +17 -0
  27. package/municipalities/vaals-design-tokens/src/component/utrecht/accordion.tokens.json +68 -0
  28. package/municipalities/vaals-design-tokens/src/component/utrecht/alert.tokens.json +46 -0
  29. package/municipalities/vaals-design-tokens/src/component/utrecht/badge-counter.tokens.json +13 -0
  30. package/municipalities/vaals-design-tokens/src/component/utrecht/badge-status.tokens.json +8 -0
  31. package/municipalities/vaals-design-tokens/src/component/utrecht/badge.tokens.json +15 -0
  32. package/municipalities/vaals-design-tokens/src/component/utrecht/blockquote.tokens.json +27 -0
  33. package/municipalities/vaals-design-tokens/src/component/utrecht/breadcrumb.tokens.json +44 -0
  34. package/municipalities/vaals-design-tokens/src/component/utrecht/button.tokens.json +152 -0
  35. package/municipalities/vaals-design-tokens/src/component/utrecht/calendar.tokens.json +80 -0
  36. package/municipalities/vaals-design-tokens/src/component/utrecht/checkbox.tokens.json +57 -0
  37. package/municipalities/vaals-design-tokens/src/component/utrecht/code.tokens.json +26 -0
  38. package/municipalities/vaals-design-tokens/src/component/utrecht/data-list.tokens.json +28 -0
  39. package/municipalities/vaals-design-tokens/src/component/utrecht/document.tokens.json +12 -0
  40. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/accordion.tokens.json +36 -0
  41. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/alert.tokens.json +9 -0
  42. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/badge-counter.tokens.json +11 -0
  43. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/breadcrumb.tokens.json +21 -0
  44. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/form-field.tokens.json +13 -0
  45. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/form-input.tokens.json +26 -0
  46. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/heading.tokens.json +28 -0
  47. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/icon.tokens.json +7 -0
  48. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/link.tokens.json +11 -0
  49. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/page-footer.tokens.json +46 -0
  50. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/page-header.tokens.json +15 -0
  51. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/radio-button.tokens.json +14 -0
  52. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/skip-link.tokens.json +24 -0
  53. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/table.tokens.json +37 -0
  54. package/municipalities/vaals-design-tokens/src/component/utrecht/extra-tokens/textbox.tokens.json +26 -0
  55. package/municipalities/vaals-design-tokens/src/component/utrecht/focus.tokens.json +15 -0
  56. package/municipalities/vaals-design-tokens/src/component/utrecht/form-field.tokens.json +17 -0
  57. package/municipalities/vaals-design-tokens/src/component/utrecht/form-input.tokens.json +31 -0
  58. package/municipalities/vaals-design-tokens/src/component/utrecht/form-label.tokens.json +23 -0
  59. package/municipalities/vaals-design-tokens/src/component/utrecht/heading.tokens.json +49 -0
  60. package/municipalities/vaals-design-tokens/src/component/utrecht/icon.tokens.json +12 -0
  61. package/municipalities/vaals-design-tokens/src/component/utrecht/link.tokens.json +33 -0
  62. package/municipalities/vaals-design-tokens/src/component/utrecht/list.tokens.json +31 -0
  63. package/municipalities/vaals-design-tokens/src/component/utrecht/page-footer.tokens.json +13 -0
  64. package/municipalities/vaals-design-tokens/src/component/utrecht/page-header.tokens.json +10 -0
  65. package/municipalities/vaals-design-tokens/src/component/utrecht/page.tokens.json +11 -0
  66. package/municipalities/vaals-design-tokens/src/component/utrecht/paragraph.tokens.json +25 -0
  67. package/municipalities/vaals-design-tokens/src/component/utrecht/radio-button.tokens.json +68 -0
  68. package/municipalities/vaals-design-tokens/src/component/utrecht/select.tokens.json +47 -0
  69. package/municipalities/vaals-design-tokens/src/component/utrecht/separator.tokens.json +10 -0
  70. package/municipalities/vaals-design-tokens/src/component/utrecht/skip-link.tokens.json +16 -0
  71. package/municipalities/vaals-design-tokens/src/component/utrecht/spotlight-section.tokens.json +24 -0
  72. package/municipalities/vaals-design-tokens/src/component/utrecht/surface.tokens.json +8 -0
  73. package/municipalities/vaals-design-tokens/src/component/utrecht/table.tokens.json +60 -0
  74. package/municipalities/vaals-design-tokens/src/component/utrecht/textbox.tokens.json +32 -0
  75. package/municipalities/vaals-design-tokens/src/config.json +73 -0
  76. package/municipalities/vaals-design-tokens/src/font/GT-Walsheim-Bold.woff2 +0 -0
  77. package/municipalities/vaals-design-tokens/src/font/GT-Walsheim-Light.woff2 +0 -0
  78. package/municipalities/vaals-design-tokens/src/font/GT-Walsheim-Medium.woff2 +0 -0
  79. package/municipalities/vaals-design-tokens/src/font/GT-Walsheim-Regular.woff2 +0 -0
  80. package/municipalities/vaals-design-tokens/src/font.scss +31 -0
  81. package/municipalities/vaals-design-tokens/src/index.scss +8 -0
  82. package/municipalities/vaals-design-tokens/style-dictionary.config.js +6 -0
  83. package/package.json +7 -3
@@ -0,0 +1,1216 @@
1
+ # NL Design System Token Set — Shared Reference
2
+
3
+ This document is the shared reference for both `/nlds:new` and `/nlds:update`. It contains all extraction scripts, file templates, component mappings, and build/test procedures.
4
+
5
+ **Do not invoke this file directly** — it is read by the skill wrappers which provide the organisation name, slug, URLs, and mode (new vs update).
6
+
7
+ ## Variables provided by the calling skill
8
+
9
+ | Variable | Example | Description |
10
+ | ---------------- | --------------------------------------------------- | -------------------------------------------------- |
11
+ | `<slug>` | `den-haag` | Lowercase, hyphenated identifier |
12
+ | `<prefix>` | `den-haag` | Same as slug |
13
+ | `<fullName>` | `Den Haag` | Proper display name |
14
+ | `<packageName>` | `@nl-design-system-unstable/den-haag-design-tokens` | npm package name |
15
+ | `<folderName>` | `municipalities/den-haag-design-tokens` | Directory path |
16
+ | `<websiteUrl>` | `https://www.denhaag.nl/` | Organisation website (may be empty) |
17
+ | `<huisstijlUrl>` | (url) | Style guide URL (may be empty) |
18
+ | `<mode>` | `new` or `update` | Whether creating from scratch or updating existing |
19
+
20
+ > **Notation convention:**
21
+ >
22
+ > - `<slug>`, `<fullName>`, `<fontKey>` etc. in angle brackets = **template variables** — substitute with the actual derived value
23
+ > - `{token.reference}` in curly braces = **Style Dictionary token references** — write literally into the output file (after substituting any `<angle-bracket>` parts inside them)
24
+ > - Example: `"{<slug>.size.xs}"` → written as `"{gouda.size.xs}"` in the output file for organisation "Gouda"
25
+
26
+ ---
27
+
28
+ ## Part A — Extract design info
29
+
30
+ If a website URL or huisstijlhandboek URL was provided, use **Playwright MCP** to extract the organisation's visual design. If neither was provided, skip to Part B and use placeholder values.
31
+
32
+ ### A1 — Navigate and screenshot
33
+
34
+ 1. Navigate to the homepage using `mcp__browser-{N}__browser_navigate`.
35
+ 2. Take a **full-page screenshot** (`mcp__browser-{N}__browser_take_screenshot` with `fullPage: true`).
36
+
37
+ ### A2 — Run the extraction script on the homepage
38
+
39
+ Run the following JavaScript via `mcp__browser-{N}__browser_evaluate` **exactly as written** — do not modify or simplify it. Copy-paste the entire function:
40
+
41
+ ```js
42
+ () => {
43
+ const rgbToHex = (rgb) => {
44
+ if (!rgb || rgb === "transparent" || rgb === "rgba(0, 0, 0, 0)")
45
+ return null;
46
+ const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
47
+ if (!m) return rgb;
48
+ const hex =
49
+ "#" +
50
+ [m[1], m[2], m[3]]
51
+ .map((x) => parseInt(x).toString(16).padStart(2, "0"))
52
+ .join("");
53
+ if (m[4] !== undefined && parseFloat(m[4]) < 1) {
54
+ const alpha = Math.round(parseFloat(m[4]) * 255)
55
+ .toString(16)
56
+ .padStart(2, "0");
57
+ return hex + alpha;
58
+ }
59
+ return hex;
60
+ };
61
+ const hslLightness = (hex) => {
62
+ const r = parseInt(hex.slice(1, 3), 16) / 255,
63
+ g = parseInt(hex.slice(3, 5), 16) / 255,
64
+ b = parseInt(hex.slice(5, 7), 16) / 255;
65
+ return Math.round(((Math.max(r, g, b) + Math.min(r, g, b)) / 2) * 100);
66
+ };
67
+ const getStyles = (el) => {
68
+ if (!el) return null;
69
+ const cs = getComputedStyle(el);
70
+ return {
71
+ backgroundColor: rgbToHex(cs.backgroundColor),
72
+ color: rgbToHex(cs.color),
73
+ fontFamily: cs.fontFamily,
74
+ fontSize: cs.fontSize,
75
+ fontWeight: cs.fontWeight,
76
+ lineHeight: cs.lineHeight,
77
+ borderColor: rgbToHex(cs.borderColor),
78
+ borderRadius: cs.borderRadius,
79
+ borderWidth: cs.borderWidth,
80
+ paddingBlockStart: cs.paddingTop,
81
+ paddingBlockEnd: cs.paddingBottom,
82
+ paddingInlineStart: cs.paddingLeft,
83
+ paddingInlineEnd: cs.paddingRight,
84
+ textDecoration: cs.textDecoration,
85
+ };
86
+ };
87
+ const first = (sels) => {
88
+ for (const s of sels) {
89
+ const el = document.querySelector(s);
90
+ if (el) return el;
91
+ }
92
+ return null;
93
+ };
94
+
95
+ const result = {
96
+ components: {},
97
+ allColors: [],
98
+ allFonts: [],
99
+ loadedFonts: [],
100
+ fontFaceRules: [],
101
+ };
102
+
103
+ // Component extraction — multiple selectors per component for robustness
104
+ const componentMap = {
105
+ h1: ["h1"],
106
+ h2: ["h2"],
107
+ h3: ["h3"],
108
+ h4: ["h4"],
109
+ paragraph: ["article p", "main p", ".content p", "p"],
110
+ link: [
111
+ "article a",
112
+ "main a",
113
+ ".content a",
114
+ 'a:not([class*="logo"]):not([class*="nav"])',
115
+ ],
116
+ button: [
117
+ 'button[type="submit"]',
118
+ ".btn-primary",
119
+ "button.primary",
120
+ "form button",
121
+ "button",
122
+ ],
123
+ searchInput: [
124
+ 'input[type="search"]',
125
+ 'input[name*="search"]',
126
+ 'input[name*="zoek"]',
127
+ 'input[type="text"]',
128
+ ],
129
+ pageHeader: [
130
+ "header",
131
+ '[class*="header"]:not(th)',
132
+ 'nav[role="navigation"]',
133
+ ],
134
+ pageFooter: ["footer", '[class*="footer"]'],
135
+ breadcrumb: [
136
+ 'nav[aria-label*="breadcrumb"] a',
137
+ '[class*="breadcrumb"] a',
138
+ 'ol[class*="breadcrumb"] li a',
139
+ ],
140
+ card: [
141
+ '[class*="card"]',
142
+ '[class*="tile"]',
143
+ '[class*="teaser"]',
144
+ "article",
145
+ ],
146
+ separator: ["hr", '[class*="separator"]', '[class*="divider"]'],
147
+ };
148
+ for (const [name, sels] of Object.entries(componentMap)) {
149
+ const el = first(sels);
150
+ result.components[name] = getStyles(el);
151
+ }
152
+
153
+ // Collect ALL unique colors on the page
154
+ const uniqueColors = new Set();
155
+ document.querySelectorAll("*").forEach((el) => {
156
+ const cs = getComputedStyle(el);
157
+ [cs.backgroundColor, cs.color, cs.borderColor].forEach((v) => {
158
+ const h = rgbToHex(v);
159
+ if (h && h.length >= 7) uniqueColors.add(h.slice(0, 7));
160
+ });
161
+ });
162
+ result.allColors = [...uniqueColors].map((hex) => ({
163
+ hex,
164
+ lightness: hslLightness(hex),
165
+ }));
166
+
167
+ // Font families
168
+ const uniqueFonts = new Set();
169
+ document
170
+ .querySelectorAll("h1,h2,h3,h4,p,a,button,input,body")
171
+ .forEach((el) => {
172
+ uniqueFonts.add(getComputedStyle(el).fontFamily);
173
+ });
174
+ result.allFonts = [...uniqueFonts];
175
+
176
+ // Loaded font faces
177
+ document.fonts.forEach((f) => {
178
+ result.loadedFonts.push({
179
+ family: f.family,
180
+ weight: f.weight,
181
+ style: f.style,
182
+ status: f.status,
183
+ });
184
+ });
185
+
186
+ // @font-face rules from stylesheets
187
+ try {
188
+ for (const sheet of document.styleSheets) {
189
+ try {
190
+ for (const rule of sheet.cssRules) {
191
+ if (rule instanceof CSSFontFaceRule) {
192
+ const src = rule.style.getPropertyValue("src");
193
+ const family = rule.style.getPropertyValue("font-family");
194
+ const weight = rule.style.getPropertyValue("font-weight");
195
+ result.fontFaceRules.push({
196
+ family: family.replace(/['"]/g, ""),
197
+ weight,
198
+ src: src.substring(0, 300),
199
+ });
200
+ }
201
+ }
202
+ } catch (e) {}
203
+ }
204
+ } catch (e) {}
205
+
206
+ // Google Fonts links
207
+ result.googleFontLinks = [];
208
+ document
209
+ .querySelectorAll(
210
+ 'link[href*="fonts.googleapis"], link[href*="fonts.gstatic"]',
211
+ )
212
+ .forEach((l) => {
213
+ result.googleFontLinks.push(l.href);
214
+ });
215
+
216
+ return result;
217
+ };
218
+ ```
219
+
220
+ **Record the returned JSON** — you will use it to set token values.
221
+
222
+ ### A3 — Visit additional pages and re-run extraction
223
+
224
+ Navigate to **each** of the following pages (skip if not found), take a full-page screenshot, and re-run the same extraction script:
225
+
226
+ 1. A **content/service page** — click on the first article/service link visible on the homepage. This reveals body text, heading hierarchy, and link styles in context.
227
+ 2. The **search page** — try these URLs in order: `/zoeken`, `/search`, `?s=test`. If none work, look for a search icon/link in the header. This reveals form inputs, result cards, and pagination.
228
+ 3. A **contact page** — try `/contact`, `/over-ons`, or look in the footer. This reveals forms with multiple input types.
229
+
230
+ After each page, **merge the results**: if a component was `null` on the homepage but found on a later page, use the later value. If both have values, keep the homepage value (it's the primary design).
231
+
232
+ ### A4 — Resolve font file URLs
233
+
234
+ From the `fontFaceRules` array in the extraction result, resolve each `src` URL to an absolute URL:
235
+
236
+ ```js
237
+ () => {
238
+ const fonts = [];
239
+ for (const sheet of document.styleSheets) {
240
+ try {
241
+ for (const rule of sheet.cssRules) {
242
+ if (rule instanceof CSSFontFaceRule) {
243
+ const family = rule.style
244
+ .getPropertyValue("font-family")
245
+ .replace(/['"]/g, "");
246
+ const weight = rule.style.getPropertyValue("font-weight") || "400";
247
+ const src = rule.style.getPropertyValue("src");
248
+ // Extract URLs from src
249
+ const urls = [...src.matchAll(/url\("?([^")\s]+)"?\)/g)].map((m) => {
250
+ try {
251
+ return new URL(m[1], document.baseURI).href;
252
+ } catch (e) {
253
+ return m[1];
254
+ }
255
+ });
256
+ // Prefer woff2 > woff > ttf > otf
257
+ const preferred =
258
+ urls.find((u) => u.endsWith(".woff2")) ||
259
+ urls.find((u) => u.endsWith(".woff")) ||
260
+ urls.find((u) => u.endsWith(".ttf")) ||
261
+ urls.find((u) => u.endsWith(".otf")) ||
262
+ urls[0];
263
+ if (preferred)
264
+ fonts.push({
265
+ family,
266
+ weight,
267
+ url: preferred,
268
+ ext: preferred.split(".").pop().split("?")[0],
269
+ });
270
+ }
271
+ }
272
+ } catch (e) {}
273
+ }
274
+ return fonts;
275
+ };
276
+ ```
277
+
278
+ **Record the returned array** — you will download these font files later.
279
+
280
+ ### A5 — Extract from huisstijlhandboek (if applicable)
281
+
282
+ If a huisstijlhandboek URL was provided:
283
+
284
+ 1. Navigate to the URL using `mcp__browser-{N}__browser_navigate`.
285
+ 2. Take a screenshot.
286
+ 3. Look for: colour palettes (hex values), typography specifications (font names, sizes, weights), logo usage guidelines.
287
+ 4. Extract any values not already found from the website. The huisstijlhandboek takes precedence for brand palette definitions (exact hex codes) and font specifications.
288
+
289
+ ### A6 — Build the design summary
290
+
291
+ From the extraction results, build this structured summary (write it out as text in your response so it's clear and traceable):
292
+
293
+ | Property | Value | Source |
294
+ | ------------------------ | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
295
+ | **Primary brand color** | (hex) — the most prominent non-black, non-white, non-grey color used for headings, links, or the header | components.h3.color or components.link.color |
296
+ | **Primary hover color** | (hex) — slightly darker variant, OR darken primary by 10% lightness | Observed or calculated |
297
+ | **Body font family** | (font stack) | components.paragraph.fontFamily |
298
+ | **Heading font family** | (font stack) — may be the same as body | components.h1.fontFamily |
299
+ | **Body font size** | (px → rem) | components.paragraph.fontSize |
300
+ | **Body font weight** | (number) | components.paragraph.fontWeight |
301
+ | **Heading font weight** | (number) | components.h1.fontWeight |
302
+ | **H1 font size** | (px → rem) | components.h1.fontSize |
303
+ | **H2 font size** | (px → rem) | components.h2.fontSize |
304
+ | **H3 font size** | (px → rem) | components.h3.fontSize |
305
+ | **H4 font size** | (px → rem, estimated if not found) | components.h4.fontSize |
306
+ | **Page header bg** | (hex) | components.pageHeader.backgroundColor |
307
+ | **Page header text** | (hex) | components.pageHeader.color |
308
+ | **Page footer bg** | (hex) | components.pageFooter.backgroundColor |
309
+ | **Page footer text** | (hex) | components.pageFooter.color |
310
+ | **Link color** | (hex) | components.link.color |
311
+ | **Button bg** | (hex) | components.button.backgroundColor |
312
+ | **Button text** | (hex) | components.button.color |
313
+ | **Button border-radius** | (px) | components.button.borderRadius |
314
+ | **Input border-color** | (hex) | components.searchInput.borderColor |
315
+ | **Input border-radius** | (px) | components.searchInput.borderRadius |
316
+ | **Separator color** | (hex) | components.separator.backgroundColor or borderColor |
317
+ | **Font file URLs** | (list) | From Step A4 |
318
+
319
+ **This table is your source of truth for all token values.** Every token value you write must trace back to a row in this table. If a property could not be extracted (returned `null`), note it as "not found — using conduction default" and do NOT change that token from the conduction baseline.
320
+
321
+ If no URL was provided, use placeholder values and inform the user they should update the color and font values.
322
+
323
+ ---
324
+
325
+ ## Part B — Create all files (new mode only)
326
+
327
+ > **Skip this part in `update` mode** — go directly to Part C.
328
+
329
+ Create the full directory structure at `municipalities/<slug>-design-tokens/` modeled on `conduction-design-tokens/`. Create every file listed below.
330
+
331
+ > **Do not create or copy the `dist/` folder.** This folder is generated by `npm run build` and will always be overwritten.
332
+
333
+ ---
334
+
335
+ ### `municipalities/<slug>-design-tokens/package.json`
336
+
337
+ ```json
338
+ {
339
+ "version": "1.0.0-alpha.1",
340
+ "author": "Community for NL Design System",
341
+ "description": "NL Design System design tokens for <fullName>",
342
+ "website": "<websiteUrl>",
343
+ "keywords": ["nl-design-system", "conduction"],
344
+ "license": "SEE LICENSE IN LICENSE.md",
345
+ "name": "<packageName>",
346
+ "private": false,
347
+ "publishConfig": {
348
+ "access": "public"
349
+ },
350
+ "repository": {
351
+ "type": "git+ssh",
352
+ "url": "git@github.com:nl-design-system/themes.git"
353
+ },
354
+ "scripts": {
355
+ "clean": "rimraf -rf dist/",
356
+ "prebuild": "npm run clean",
357
+ "watch": "npm-run-all watch:**",
358
+ "watch:style-dictionary": "chokidar --follow-symlinks --command 'npm run --ignore-scripts build' 'src/**/*.tokens.json'",
359
+ "build": "npm-run-all build:**",
360
+ "build:scss": "sass --no-source-map src/:dist/",
361
+ "build:style-dictionary": "style-dictionary build --config ./style-dictionary.config.js"
362
+ },
363
+ "devDependencies": {
364
+ "@nl-design-system-unstable/theme-toolkit": "workspace:*",
365
+ "chokidar-cli": "3.0.0",
366
+ "npm-run-all": "4.1.5",
367
+ "rimraf": "3.0.2",
368
+ "style-dictionary": "3.8.0"
369
+ },
370
+ "bugs": {
371
+ "url": "https://github.com/ConductionNL/conduction-theme/issues"
372
+ },
373
+ "homepage": "https://github.com/ConductionNL/conduction-theme#readme"
374
+ }
375
+ ```
376
+
377
+ ---
378
+
379
+ ### `municipalities/<slug>-design-tokens/style-dictionary.config.js`
380
+
381
+ ```js
382
+ const config = require("./src/config.json");
383
+ const { createConfig } = require("../../style-dictionary-config");
384
+
385
+ module.exports = createConfig({
386
+ selector: `.${config.prefix}-theme`,
387
+ });
388
+ ```
389
+
390
+ ---
391
+
392
+ ### `municipalities/<slug>-design-tokens/src/config.json`
393
+
394
+ Read `conduction-design-tokens/src/config.json`, copy the full `stories` array, and replace only the top-level fields:
395
+
396
+ ```json
397
+ {
398
+ "fullName": "<fullName>",
399
+ "name": "<fullName>",
400
+ "prefix": "<slug>",
401
+ "npm": "@conduction/theme",
402
+ "stories": [
403
+ "react-utrecht-alert--default",
404
+ "react-utrecht-alert--warning",
405
+ "react-utrecht-alert--error",
406
+ "react-utrecht-alert--ok",
407
+ "react-utrecht-badge-counter--default",
408
+ "react-utrecht-breadcrumb-nac--default",
409
+ "react-utrecht-breadcrumb-nac--separator",
410
+ "react-utrecht-button--default",
411
+ "react-utrecht-button--hover",
412
+ "react-utrecht-button--primary-action-button",
413
+ "react-utrecht-button--secondary-action-button",
414
+ "react-utrecht-calendar--default",
415
+ "react-utrecht-calendar--limited-range-calendar",
416
+ "react-utrecht-checkbox--default",
417
+ "react-utrecht-checkbox--checked",
418
+ "react-utrecht-checkbox--disabled",
419
+ "react-utrecht-checkbox--checked-and-disabled",
420
+ "react-utrecht-checkbox--hover",
421
+ "react-utrecht-checkbox--focus",
422
+ "react-utrecht-checkbox--focus-visible",
423
+ "react-utrecht-code--default",
424
+ "react-utrecht-code-block--default",
425
+ "react-utrecht-data-badge--default",
426
+ "react-utrecht-document--default",
427
+ "react-utrecht-heading-1--default",
428
+ "react-utrecht-heading-2--default",
429
+ "react-utrecht-heading-3--default",
430
+ "react-utrecht-heading-4--default",
431
+ "react-utrecht-heading-5--default",
432
+ "react-utrecht-link--default",
433
+ "react-utrecht-link--hover",
434
+ "react-utrecht-link--focus",
435
+ "react-utrecht-ordered-list--default",
436
+ "react-utrecht-unordered-list--default",
437
+ "react-utrecht-page--default",
438
+ "react-utrecht-page-header--default",
439
+ "react-utrecht-page-footer--default",
440
+ "react-utrecht-paragraph--default",
441
+ "react-utrecht-radio-button--default",
442
+ "react-utrecht-radio-button--hover",
443
+ "react-utrecht-radio-button--focus",
444
+ "react-utrecht-radio-button--checked",
445
+ "react-utrecht-radio-button--checked-and-disabled",
446
+ "react-utrecht-radio-button--disabled",
447
+ "react-utrecht-separator--default",
448
+ "react-utrecht-skip-link--default",
449
+ "react-utrecht-spotlicht-section--default",
450
+ "react-utrecht-spotlicht-section--info",
451
+ "react-utrecht-spotlicht-section--warning",
452
+ "react-utrecht-surface--default",
453
+ "react-utrecht-table--default",
454
+ "react-utrecht-textbox--default",
455
+ "react-conduction-card-header--default",
456
+ "react-conduction-card-header--hover",
457
+ "react-conduction-card-wrapper--default",
458
+ "react-conduction-card-wrapper--hover",
459
+ "react-conduction-pagination--default",
460
+ "react-conduction-input-select--default",
461
+ "react-conduction-input-select--list-option",
462
+ "react-conduction-input-select--placeholder",
463
+ "react-conduction-tabs--default",
464
+ "react-conduction-tabs--selected",
465
+ "react-conduction-tabs--hover",
466
+ "react-conduction-tabs--list",
467
+ "react-conduction-tabs--panel"
468
+ ]
469
+ }
470
+ ```
471
+
472
+ ---
473
+
474
+ ### `municipalities/<slug>-design-tokens/src/index.scss`
475
+
476
+ ```scss
477
+ /**
478
+ * @license SEE LICENSE.md
479
+ * Copyright (c) 2021 NL Design System Community
480
+ * All rights reserved
481
+ */
482
+
483
+ @import "./design-tokens.css";
484
+ @import "./font.css";
485
+ ```
486
+
487
+ ---
488
+
489
+ ### `municipalities/<slug>-design-tokens/src/font/` — Download font files
490
+
491
+ For every font file URL found in Part A (`.woff2`, `.woff`, `.ttf`, `.otf`):
492
+
493
+ 1. Download the file using the Bash tool (`curl -L -o ...`) into `municipalities/<slug>-design-tokens/src/font/`.
494
+ 2. Use a clean filename: `<FontName>-<weight>.<ext>` (e.g. `RijksoverheidSans-Regular.woff2`, `RijksoverheidSans-Bold.woff2`).
495
+ 3. If the font is served via Google Fonts CSS (a `<link>` to `fonts.googleapis.com`), fetch that CSS URL with `WebFetch` to get the individual `@font-face` blocks, then download each `.woff2` URL from those blocks.
496
+ 4. If no font files were found via Part A, try to find them by:
497
+ - Searching for `@font-face` rules in the page's `<style>` tags or linked CSS files using `mcp__browser-{N}__browser_evaluate`
498
+ - Looking up the font name on Google Fonts (`fonts.google.com`) or the font foundry website
499
+ - If found from an external source, download and store them the same way
500
+
501
+ ---
502
+
503
+ ### `municipalities/<slug>-design-tokens/src/font.scss`
504
+
505
+ Write `@font-face` declarations for every font file downloaded above. The `font.scss` file lives in `src/` but is compiled to `dist/font.css`, so paths must be relative to `dist/` — use `url("../src/font/<FileName>-<weight>.<ext>")` (e.g. `url("../src/font/RijksoverheidSans-Regular.woff2")`). Model the structure after `conduction-design-tokens/src/font.scss`. Include the correct `font-weight` value for each file.
506
+
507
+ If no font files could be found or downloaded anywhere, leave a placeholder comment:
508
+
509
+ ```scss
510
+ /* TODO: Add @font-face declarations for <fontName> */
511
+ ```
512
+
513
+ ---
514
+
515
+ ### Brand token files
516
+
517
+ #### `municipalities/<slug>-design-tokens/src/brand/<slug>/color.tokens.json`
518
+
519
+ Use colors extracted from the website. If not found, use these placeholders:
520
+
521
+ - primary: `#000000`
522
+ - primary-hover: `#333333`
523
+
524
+ **Always read `conduction-design-tokens/src/brand/conduction/color.tokens.json` first.** Component tokens reference specific named shades by path (e.g. `<slug>.color.grey.82`). If any referenced path is missing the build fails with "Reference doesn't exist". Start with the full conduction baseline below, replacing `conduction` with `<slug>`, then add the organisation's own brand palette groups on top. After all component files are written, scan every `*.tokens.json` for references into `<slug>.color.*` and remove any palette entries that are not referenced by any file.
525
+
526
+ ##### Color naming convention
527
+
528
+ **Lightness percentage** — The number in a color key represents the HSL lightness of that color. Calculate it with this formula:
529
+
530
+ ```
531
+ R, G, B = hex channels / 255
532
+ lightness = round( (max(R,G,B) + min(R,G,B)) / 2 × 100 )
533
+ ```
534
+
535
+ The extraction script in A2 already returns `lightness` for every color in the `allColors` array — use those values directly.
536
+
537
+ Examples:
538
+
539
+ - `#ffffff` (white) = `100`
540
+ - `#000000` (black) = `0`
541
+ - `#808080` (mid grey) = `50`
542
+ - `#e10a17` (bright red) = `44`
543
+ - `#012c9d` (dark blue) = `31`
544
+
545
+ So a color group key like `"31"` means that color sits at 31% on the lightness scale.
546
+
547
+ **Transparency suffix** — When a color has opacity, append `-{n}t` to the key where `{n}` is the opacity percentage. The hex value gets a 2-digit alpha suffix appended:
548
+
549
+ | Opacity | Hex suffix |
550
+ | ------- | -------------------------- |
551
+ | 100% | `FF` (omit — fully opaque) |
552
+ | 80% | `CC` |
553
+ | 60% | `99` |
554
+ | 50% | `80` |
555
+ | 10% | `1A` |
556
+
557
+ Full reference: https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4
558
+
559
+ Only add transparency variants when they are actually used on the organisation website.
560
+
561
+ **Color palette ordering inside the `color` object:**
562
+
563
+ 1. Semantic aliases (`primary`, `primary-hover`, `error`, alert variants, etc.)
564
+ 2. Brand-specific palette groups from the website (e.g. `blue`, `green`, `red`, `yellow`) — ordered lightest group last
565
+ 3. `grey` (full baseline)
566
+ 4. `lightgrey` (if present — sits between grey and white)
567
+ 5. `white` (full baseline)
568
+ 6. `black` (full baseline)
569
+
570
+ **Building the brand palette from extracted colors:**
571
+
572
+ 1. Look at `allColors` from A2. Filter out greys (R≈G≈B), white (#ffffff-area), and black (#000000-area).
573
+ 2. Group the remaining colors by dominant hue: red, blue, green, orange, etc.
574
+ 3. For each group, create a palette object keyed by lightness percentage.
575
+ 4. The **primary brand color** (from the design summary) must be in one of these groups.
576
+ 5. Set `"primary"` to reference that color: `"{<slug>.color.<group>.<lightness>}"`.
577
+ 6. Set `"primary-hover"` to a darker shade in the same group, or darken by ~10% lightness.
578
+
579
+ Start with this full baseline (replace `conduction` with `<slug>`), then insert the organisation's own brand palette groups between the semantic aliases and `grey`:
580
+
581
+ ```json
582
+ {
583
+ "<slug>": {
584
+ "color": {
585
+ "primary": {
586
+ "value": "{<slug>.color.<paletteGroup>.<shade>}"
587
+ },
588
+ "primary-hover": {
589
+ "value": "{<slug>.color.<paletteGroup>.<darkerShade>}"
590
+ },
591
+ "error": { "value": "#dc3545" },
592
+ "alert-error": { "value": "#721c24" },
593
+ "alert-error-background": { "value": "#f8d7da" },
594
+ "warning": { "value": "#ffc107" },
595
+ "alert-warning": { "value": "#856404" },
596
+ "alert-warning-background": { "value": "#fff3cd" },
597
+ "succes": { "value": "#28a745" },
598
+ "alert-succes": { "value": "#155724" },
599
+ "alert-succes-background": { "value": "#d4edda" },
600
+ "info": { "value": "{<slug>.color.primary}" },
601
+ "alert-info": { "value": "#004085" },
602
+ "alert-info-background": { "value": "#cce5ff" },
603
+ "grey": {
604
+ "27": {
605
+ "value": "#444444"
606
+ },
607
+ "29": {
608
+ "value": "#4a4a4a"
609
+ },
610
+ "31": {
611
+ "value": "#4f4f4f"
612
+ },
613
+ "46": {
614
+ "value": "#767676"
615
+ },
616
+ "48": {
617
+ "value": "#7a7a7a"
618
+ },
619
+ "50": {
620
+ "value": "#808080",
621
+ "comment": "Base/Grey"
622
+ },
623
+ "70": {
624
+ "value": "#b3b3b3"
625
+ },
626
+ "82": {
627
+ "value": "#d1d1d1"
628
+ },
629
+ "87": {
630
+ "value": "#dddddd"
631
+ },
632
+ "90": {
633
+ "value": "#e6e6e6"
634
+ },
635
+ "95": {
636
+ "value": "#f2f2f2"
637
+ },
638
+ "97": {
639
+ "value": "#f7f7f7"
640
+ }
641
+ },
642
+ "lightgrey": {
643
+ "96": {
644
+ "value": "#f5f5f5",
645
+ "comment": "Base/LightGrey"
646
+ }
647
+ },
648
+ "white": {
649
+ "98": {
650
+ "value": "#fafafa"
651
+ },
652
+ "100": {
653
+ "value": "#ffffff",
654
+ "comment": "Base/White"
655
+ }
656
+ },
657
+ "black": {
658
+ "0": {
659
+ "value": "#000000",
660
+ "comment": "Base/Black"
661
+ },
662
+ "0-60t": {
663
+ "value": "#00000099",
664
+ "comment": "Black with 60% transparency"
665
+ },
666
+ "30": {
667
+ "value": "#4d4d4d"
668
+ }
669
+ }
670
+ }
671
+ }
672
+ }
673
+ ```
674
+
675
+ After all component files are written, scan every `*.tokens.json` for which `<slug>.color.*` paths are actually referenced. Remove any individual shade keys from the palette that are not referenced anywhere. Do not remove semantic aliases (`primary`, `error`, etc.) even if unreferenced.
676
+
677
+ ---
678
+
679
+ #### `municipalities/<slug>-design-tokens/src/brand/<slug>/font-size.tokens.json`
680
+
681
+ Set the values based on the computed font sizes extracted from the website. The mapping between font-size keys and components is:
682
+
683
+ | Key | Used by component |
684
+ | ----- | -------------------------------------------------------------------------------------- |
685
+ | `md` | `paragraph`, `heading-5` — **set to the most-used paragraph font-size on the website** |
686
+ | `lg` | `heading-4` |
687
+ | `xl` | `heading-3` |
688
+ | `2xl` | `heading-2` |
689
+ | `3xl` | `heading-1` |
690
+
691
+ Extract the computed `font-size` of `<p>`, `<h1>`, `<h2>`, `<h3>`, `<h4>` from the website and set the corresponding keys. Convert px values to rem (divide by 16). If you find additional distinct sizes used on the website (e.g. for captions, labels, small text), map them to the nearest smaller key.
692
+
693
+ **Scale validation** — after assigning all heading sizes, verify that the scale is well-spaced (each step should be meaningfully larger than the previous). If sizes are too close together (e.g. `3xl` = 52px, `2xl` = 50px, `xl` = 38px) this indicates a scale problem — set them to better-distributed values, keep the heading sizes as close as possible to the website, and include a note in the summary telling the user which sizes were adjusted and why.
694
+
695
+ ```json
696
+ {
697
+ "<slug>": {
698
+ "font-size": {
699
+ "4xs": { "value": "0.625rem", "comment": "10px" },
700
+ "3xs": { "value": "0.75rem", "comment": "12px" },
701
+ "2xs": { "value": "0.875rem", "comment": "14px" },
702
+ "xs": { "value": "1rem", "comment": "16px" },
703
+ "sm": { "value": "1.125rem", "comment": "18px" },
704
+ "md": { "value": "1.25rem", "comment": "20px" },
705
+ "lg": { "value": "1.5rem", "comment": "24px" },
706
+ "xl": { "value": "1.75rem", "comment": "28px" },
707
+ "2xl": { "value": "2rem", "comment": "32px" },
708
+ "3xl": { "value": "2.5rem", "comment": "40px" },
709
+ "4xl": { "value": "3rem", "comment": "48px" },
710
+ "5xl": { "value": "3.625rem", "comment": "58px" }
711
+ }
712
+ }
713
+ }
714
+ ```
715
+
716
+ ---
717
+
718
+ #### `municipalities/<slug>-design-tokens/src/brand/<slug>/size.tokens.json`
719
+
720
+ ```json
721
+ {
722
+ "<slug>": {
723
+ "size": {
724
+ "4xs": { "value": "1px" },
725
+ "3xs": { "value": "2px" },
726
+ "2xs": { "value": "4px" },
727
+ "xs": { "value": "8px" },
728
+ "sm": { "value": "14px" },
729
+ "md": { "value": "18px" },
730
+ "lg": { "value": "24px" },
731
+ "xl": { "value": "32px" },
732
+ "2xl": { "value": "48px" },
733
+ "3xl": { "value": "72px" },
734
+ "4xl": { "value": "96px" }
735
+ }
736
+ }
737
+ }
738
+ ```
739
+
740
+ ---
741
+
742
+ #### `municipalities/<slug>-design-tokens/src/brand/<slug>/typography.tokens.json`
743
+
744
+ Use the font family found on the website, or `Arial, sans-serif` as fallback.
745
+ `<fontKey>` = lowercase font name with hyphens (e.g. `open-sans`, `source-sans-pro`).
746
+
747
+ **`sans-serif` is required** — component tokens (button, paragraph, document, form inputs, etc.) reference `<slug>.typography.sans-serif.font-family`. Set it to the body font of the organisation.
748
+
749
+ ```json
750
+ {
751
+ "<slug>": {
752
+ "typography": {
753
+ "<fontKey>": {
754
+ "font-family": {
755
+ "value": "\"<FontName>\", Arial, sans-serif"
756
+ }
757
+ },
758
+ "sans-serif": {
759
+ "font-family": {
760
+ "value": "\"<FontName>\", Arial, sans-serif"
761
+ }
762
+ },
763
+ "monospace": {
764
+ "font-family": {
765
+ "value": "Monospace, \"Lucida Console\""
766
+ }
767
+ },
768
+ "font-weight": {
769
+ "bold": { "value": "700" },
770
+ "semibold": { "value": "600" },
771
+ "normal": { "value": "400" },
772
+ "light": { "value": "100" }
773
+ },
774
+ "scale": {
775
+ "4xs": { "value": "{<slug>.font-size.4xs}" },
776
+ "3xs": { "value": "{<slug>.font-size.3xs}" },
777
+ "2xs": { "value": "{<slug>.font-size.2xs}" },
778
+ "xs": { "value": "{<slug>.font-size.xs}" },
779
+ "sm": { "value": "{<slug>.font-size.sm}" },
780
+ "md": { "value": "{<slug>.font-size.md}" },
781
+ "lg": { "value": "{<slug>.font-size.lg}" },
782
+ "xl": { "value": "{<slug>.font-size.xl}" },
783
+ "2xl": { "value": "{<slug>.font-size.2xl}" },
784
+ "3xl": { "value": "{<slug>.font-size.3xl}" },
785
+ "4xl": { "value": "{<slug>.font-size.4xl}" }
786
+ }
787
+ }
788
+ }
789
+ }
790
+ ```
791
+
792
+ ---
793
+
794
+ ### Common token files
795
+
796
+ #### `municipalities/<slug>-design-tokens/src/common/utrecht/action.tokens.json`
797
+
798
+ ```json
799
+ {
800
+ "utrecht": {
801
+ "action": {
802
+ "busy": { "cursor": { "value": "wait" } },
803
+ "disabled": { "cursor": { "value": "not-allowed" } },
804
+ "submit": { "cursor": { "value": "pointer" } }
805
+ }
806
+ }
807
+ }
808
+ ```
809
+
810
+ ---
811
+
812
+ #### `municipalities/<slug>-design-tokens/src/common/utrecht/space.tokens.json`
813
+
814
+ Read `conduction-design-tokens/src/common/utrecht/space.tokens.json` and replace `conduction` with `<slug>`:
815
+
816
+ ```json
817
+ {
818
+ "utrecht": {
819
+ "space": {
820
+ "block": {
821
+ "3xs": { "value": "{<slug>.size.3xs}" },
822
+ "2xs": { "value": "{<slug>.size.2xs}" },
823
+ "xs": { "value": "{<slug>.size.xs}" },
824
+ "sm": { "value": "{<slug>.size.sm}" },
825
+ "md": { "value": "{<slug>.size.md}" },
826
+ "lg": { "value": "{<slug>.size.lg}" },
827
+ "xl": { "value": "{<slug>.size.xl}" },
828
+ "2xl": { "value": "{<slug>.size.2xl}" },
829
+ "3xl": { "value": "{<slug>.size.3xl}" }
830
+ },
831
+ "inline": {
832
+ "3xs": { "value": "{<slug>.size.3xs}" },
833
+ "2xs": { "value": "{<slug>.size.2xs}" },
834
+ "xs": { "value": "{<slug>.size.xs}" },
835
+ "sm": { "value": "{<slug>.size.sm}" },
836
+ "md": { "value": "{<slug>.size.md}" },
837
+ "lg": { "value": "{<slug>.size.lg}" },
838
+ "xl": { "value": "{<slug>.size.xl}" },
839
+ "2xl": { "value": "{<slug>.size.2xl}" },
840
+ "3xl": { "value": "{<slug>.size.3xl}" }
841
+ }
842
+ }
843
+ }
844
+ }
845
+ ```
846
+
847
+ ---
848
+
849
+ ### Component token files
850
+
851
+ For each file listed below:
852
+
853
+ 1. **Read** the source file from `conduction-design-tokens/src/` using the Read tool.
854
+ 2. **Copy the full file content verbatim** into memory.
855
+ 3. Apply only these text substitutions to the copied content:
856
+ - `conduction` → `<slug>`
857
+ - `aldritch` → `<fontKey>`
858
+ 4. **Write** the result to the equivalent path under `municipalities/<slug>-design-tokens/src/` using the Write tool.
859
+
860
+ > **CRITICAL — Indentation must be preserved exactly.**
861
+ > The JSON files use a specific indent style (spaces or tabs) that is intentional.
862
+ > Do **not** reformat, reindent, pretty-print, or restructure the JSON in any way.
863
+ > The only changes allowed are the text substitutions listed above.
864
+ > If the source uses 2-space indentation, the output must also use 2-space indentation — character for character.
865
+
866
+ **Known reference fix:** The conduction baseline has `{conduction.color.blue.95}` in `calendar.tokens.json` (used for the "today" highlight). After replacement this becomes `{<slug>.color.blue.95}` which will fail if the organisation doesn't have a `blue` palette. **Replace this reference** with `{<slug>.color.grey.95}` (a neutral light highlight) unless the organisation's palette has a light brand color shade that makes more sense.
867
+
868
+ Create these files:
869
+
870
+ **`src/component/utrecht/`** (read from conduction and replace prefix):
871
+
872
+ - `accordion.tokens.json`
873
+ - `alert.tokens.json`
874
+ - `badge-counter.tokens.json`
875
+ - `badge-status.tokens.json`
876
+ - `badge.tokens.json`
877
+ - `blockquote.tokens.json`
878
+ - `breadcrumb.tokens.json`
879
+ - `button.tokens.json`
880
+ - `calendar.tokens.json`
881
+ - `checkbox.tokens.json`
882
+ - `code.tokens.json`
883
+ - `data-list.tokens.json`
884
+ - `document.tokens.json`
885
+ - `focus.tokens.json`
886
+ - `form-field.tokens.json`
887
+ - `form-input.tokens.json`
888
+ - `form-label.tokens.json`
889
+ - `heading.tokens.json`
890
+ - `icon.tokens.json`
891
+ - `link.tokens.json`
892
+ - `list.tokens.json`
893
+ - `page.tokens.json`
894
+ - `page-footer.tokens.json`
895
+ - `page-header.tokens.json`
896
+ - `paragraph.tokens.json`
897
+ - `radio-button.tokens.json`
898
+ - `select.tokens.json`
899
+ - `separator.tokens.json`
900
+ - `skip-link.tokens.json`
901
+ - `spotlight-section.tokens.json`
902
+ - `surface.tokens.json`
903
+ - `table.tokens.json`
904
+ - `textbox.tokens.json`
905
+
906
+ **`src/component/utrecht/extra-tokens/`** (read from conduction and replace prefix):
907
+
908
+ - `accordion.tokens.json`
909
+ - `alert.tokens.json`
910
+ - `badge-counter.tokens.json`
911
+ - `breadcrumb.tokens.json`
912
+ - `form-field.tokens.json`
913
+ - `form-input.tokens.json`
914
+ - `heading.tokens.json`
915
+ - `icon.tokens.json`
916
+ - `link.tokens.json`
917
+ - `page-footer.tokens.json`
918
+ - `page-header.tokens.json`
919
+ - `radio-button.tokens.json`
920
+ - `skip-link.tokens.json`
921
+ - `table.tokens.json`
922
+ - `textbox.tokens.json`
923
+
924
+ **`src/component/conduction/`** — these are Conduction framework components. The destination folder is **always** `municipalities/<slug>-design-tokens/src/component/conduction/` — **never rename this folder to `<slug>`**.
925
+
926
+ For file contents: replace only brand token references inside curly braces (`{conduction.` → `{<slug>.`), but **keep the root JSON key `"conduction"` unchanged** so that CSS variables remain `--conduction-*` as expected by the Conduction framework components.
927
+
928
+ - `card-header.tokens.json`
929
+ - `card-wrapper.tokens.json`
930
+ - `logo.tokens.json`
931
+ - `pagination.tokens.json`
932
+ - `select.tokens.json`
933
+ - `table-wrapper.tokens.json`
934
+ - `tooltip.tokens.json`
935
+
936
+ ---
937
+
938
+ ### Documentation files
939
+
940
+ #### `municipalities/<slug>-design-tokens/documentation/color.stories.mdx`
941
+
942
+ ```mdx
943
+ import { Meta, ColorPalette, ColorItem } from "@storybook/addon-docs";
944
+ import tokens from "../dist/tokens.json";
945
+ import { ColorSearch } from "@nl-design-system-unstable/theme-toolkit/src/ColorSearch";
946
+ import { ColorTable } from "@nl-design-system-unstable/theme-toolkit/src/ColorTable";
947
+ import config from "../src/config.json";
948
+
949
+ <Meta title={`${config.name}/Color`} />
950
+
951
+ # Color
952
+
953
+ ## Find a color
954
+
955
+ <ColorSearch tokens={tokens[config.prefix]["color"]}></ColorSearch>
956
+
957
+ ## Color palette
958
+
959
+ <ColorTable tokens={tokens[config.prefix]["color"]}></ColorTable>
960
+ ```
961
+
962
+ #### `municipalities/<slug>-design-tokens/documentation/components.stories.mdx`
963
+
964
+ Read `conduction-design-tokens/documentation/components.stories.mdx` and copy its content verbatim (it uses `config.json` dynamically).
965
+
966
+ #### `municipalities/<slug>-design-tokens/documentation/design-tokens.stories.mdx`
967
+
968
+ Read `conduction-design-tokens/documentation/design-tokens.stories.mdx` and copy its content verbatim.
969
+
970
+ #### `municipalities/<slug>-design-tokens/documentation/readme.stories.mdx`
971
+
972
+ Read `conduction-design-tokens/documentation/readme.stories.mdx` and copy its content verbatim.
973
+
974
+ ---
975
+
976
+ ### `municipalities/<slug>-design-tokens/LICENSE.md`
977
+
978
+ Generate a new LICENSE.md file for the organisation. If the organisation is a municipality, use `Gemeente <fullName>` as the name; otherwise use `<fullName>`. Use this template:
979
+
980
+ ```md
981
+ # Auteursrecht <OrgName>
982
+
983
+ Copyright (c) <currentYear> <OrgName>
984
+
985
+ ## Logo en huisstijl
986
+
987
+ Op het huisstijl en logo zijn auteursrechten van toepassing. Het gebruik van logo en huisstijl is alleen toegestaan voor gebruik door <OrgName>.
988
+
989
+ Wanneer je een bewerking van de software wilt gebruiken voor andere doeleinden, mag je niet het logo van <OrgName> gebruiken en je ontwerpt een eigen huisstijl.
990
+
991
+ ## Lettertype
992
+
993
+ Lettertypes die worden gebruikt voor de huisstijl zijn niet allemaal gratis en open source. Let op dat bij gebruik van die bijgeleverde lettertypes je een (betaalde) licentie regelt. Pas anders de configuratie aan om minder of andere lettertypes te gebruiken.
994
+
995
+ ## Toestemming
996
+
997
+ Wanneer je het logo of de huisstijl wilt gebruiken kun je voor toestemming contact opnemen met <OrgName>.
998
+ ```
999
+
1000
+ Where `<OrgName>` = `Gemeente <fullName>` for municipalities, or `<fullName>` for other organisation types.
1001
+
1002
+ ### `municipalities/<slug>-design-tokens/README.md`
1003
+
1004
+ ```md
1005
+ # <fullName> Design Tokens
1006
+
1007
+ NL Design System design tokens for <fullName>.
1008
+
1009
+ ## Usage
1010
+
1011
+ Install the package:
1012
+
1013
+ \`\`\`sh
1014
+ npm install <packageName>
1015
+ \`\`\`
1016
+
1017
+ Apply the theme class to your root element:
1018
+
1019
+ \`\`\`html
1020
+
1021
+ <html class="<slug>-theme">
1022
+ \`\`\`
1023
+
1024
+ ## Building
1025
+
1026
+ \`\`\`sh
1027
+ npm run build
1028
+ \`\`\`
1029
+ ```
1030
+
1031
+ ---
1032
+
1033
+ ## Part C — Update component tokens to match the website
1034
+
1035
+ This part applies in **both** `new` and `update` modes. In update mode, read the existing token files and update only the values that differ from the extraction.
1036
+
1037
+ **Rules:**
1038
+
1039
+ - **Only change values you have data for** — if the design summary says "not found — using conduction default", do NOT change that token.
1040
+ - **Always use palette references** (e.g. `{<slug>.color.red.44}`) rather than hardcoded hex values. If the color is not yet in the palette, add it to `color.tokens.json` first with the correct lightness key.
1041
+ - **Never empty a token that had a value** — only replace values with different values.
1042
+
1043
+ **Use this exact mapping from design summary → token files:**
1044
+
1045
+ ### `heading.tokens.json`
1046
+
1047
+ | Token path | Set to |
1048
+ | --------------------------- | -------------------------------------------------------------------------------------------------- |
1049
+ | `heading-1.color` | Design summary: **Primary brand color** if headings are colored, or leave empty if black |
1050
+ | `heading-1.font-family` | `{<slug>.typography.<fontKey>.font-family}` using heading font |
1051
+ | `heading-1.font-weight` | `{<slug>.typography.font-weight.bold}` or `.semibold` or `.normal` — match **Heading font weight** |
1052
+ | `heading-2.color` | Same as h1, or different if h2 has a distinct color |
1053
+ | `heading-2.font-family` | Same as h1 |
1054
+ | `heading-3.color` | From design summary **H3** — often the primary brand color for links/headings |
1055
+ | `heading-3.font-family` | Same as h1 (or body font if h3 uses body font) |
1056
+ | All `heading-*.font-weight` | Match the extracted weight. Map: 300→light, 400→normal, 500/600→semibold, 700→bold |
1057
+
1058
+ ### `font-size.tokens.json`
1059
+
1060
+ | Token key | Set to |
1061
+ | --------- | --------------------------------------------------------------------------- |
1062
+ | `md` | **Body font size** (px / 16 = rem) — this is the paragraph/h5 size |
1063
+ | `lg` | **H4 font size** in rem. If H4 not found, set to midpoint between md and xl |
1064
+ | `xl` | **H3 font size** in rem |
1065
+ | `2xl` | **H2 font size** in rem |
1066
+ | `3xl` | **H1 font size** in rem |
1067
+ | `4xl` | H1 x 1.25 (for oversized display headings if needed) |
1068
+
1069
+ Validate the scale is well-spaced: each step should be >= 20% larger than the previous. Adjust if needed.
1070
+
1071
+ ### `paragraph.tokens.json`
1072
+
1073
+ | Token path | Set to |
1074
+ | ----------------------- | --------------------------------------------------------------------------------- |
1075
+ | `paragraph.font-weight` | `{<slug>.typography.font-weight.light}` or `.normal` — match **Body font weight** |
1076
+
1077
+ ### `link.tokens.json`
1078
+
1079
+ | Token path | Set to |
1080
+ | ------------------ | ----------------------------------------------------------- |
1081
+ | `link.color` | `{<slug>.color.primary}` (which references the brand color) |
1082
+ | `link.hover.color` | `{<slug>.color.primary-hover}` |
1083
+
1084
+ ### `page-header.tokens.json`
1085
+
1086
+ | Token path | Set to |
1087
+ | ------------------------------ | ------------------------------------------------------------------- |
1088
+ | `page-header.background-color` | From **Page header bg**. Use palette ref if a matching color exists |
1089
+ | `page-header.color` | From **Page header text** |
1090
+
1091
+ ### `page-footer.tokens.json`
1092
+
1093
+ | Token path | Set to |
1094
+ | ------------------------------ | ------------------------- |
1095
+ | `page-footer.background-color` | From **Page footer bg** |
1096
+ | `page-footer.color` | From **Page footer text** |
1097
+
1098
+ ### `button.tokens.json`
1099
+
1100
+ | Token path | Set to |
1101
+ | ------------------------------- | ------------------------------------------------------- |
1102
+ | `button.background-color` | From **Button bg** — typically `{<slug>.color.primary}` |
1103
+ | `button.border-color` | Same as background, or from extraction |
1104
+ | `button.border-radius` | From **Button border-radius** |
1105
+ | `button.color` | From **Button text** |
1106
+ | `button.hover.background-color` | `{<slug>.color.primary-hover}` |
1107
+ | `button.hover.border-color` | `{<slug>.color.primary-hover}` |
1108
+
1109
+ ### `textbox.tokens.json`
1110
+
1111
+ | Token path | Set to |
1112
+ | ----------------------- | ---------------------------- |
1113
+ | `textbox.border-color` | From **Input border-color** |
1114
+ | `textbox.border-radius` | From **Input border-radius** |
1115
+
1116
+ ### `document.tokens.json` and `surface.tokens.json`
1117
+
1118
+ | Token path | Set to |
1119
+ | --------------------------- | ---------------------------------------------------------------- |
1120
+ | `document.background-color` | `{<slug>.color.white.100}` or the page's background if not white |
1121
+ | `surface.background-color` | Same as document |
1122
+
1123
+ ### `separator.tokens.json`
1124
+
1125
+ | Token path | Set to |
1126
+ | ----------------- | ------------------------ |
1127
+ | `separator.color` | From **Separator color** |
1128
+
1129
+ ### `focus.tokens.json`
1130
+
1131
+ | Token path | Set to |
1132
+ | --------------------- | --------------------------------------------------------- |
1133
+ | `focus.outline-color` | `{<slug>.color.primary}` (common pattern) or keep default |
1134
+
1135
+ ### `breadcrumb.tokens.json`
1136
+
1137
+ | Token path | Set to |
1138
+ | --------------------------------- | ------------------------------ |
1139
+ | `breadcrumb-nav.link.color` | `{<slug>.color.primary}` |
1140
+ | `breadcrumb-nav.link.hover.color` | `{<slug>.color.primary-hover}` |
1141
+
1142
+ **For all other component files** (alert, badge, calendar, checkbox, code, etc.) — leave the conduction defaults with only the prefix replaced. These components are rarely visible on the organisation's public website, so the defaults are acceptable.
1143
+
1144
+ ---
1145
+
1146
+ ## Part D — Build and test
1147
+
1148
+ ### D1 — Build the token set
1149
+
1150
+ Run the build:
1151
+
1152
+ ```sh
1153
+ cd municipalities/<slug>-design-tokens && npm run build
1154
+ ```
1155
+
1156
+ Capture all output. If the build fails:
1157
+
1158
+ - Parse every "Reference doesn't exist" error — note the exact token path that is missing
1159
+ - Check whether the missing token exists in `conduction-design-tokens/src/brand/conduction/color.tokens.json` or another conduction token file
1160
+ - If it does, add it to the corresponding token file for this organisation
1161
+ - Retry the build until it succeeds or you have identified all unresolvable errors
1162
+
1163
+ ### D2 — Test against woo-website-template-apiv2
1164
+
1165
+ After a successful build, test the theme against the `woo-website-template-apiv2` repository:
1166
+
1167
+ 1. Check if `../woo-website-template-apiv2` exists relative to the current repository root. If not, report that the repository was not found at the expected path and skip the remaining steps.
1168
+ 2. If found, check out (or confirm) the `Theme-test-branch` branch in that repository.
1169
+ 3. Install the new package into the template repo (or copy the built dist files from `municipalities/<slug>-design-tokens/dist/`).
1170
+ 4. **Update `pwa/src/styling/index.css`**: Check this file to confirm the theme CSS is imported. If the new theme is not yet imported, add an import for the built CSS file.
1171
+ 5. **Update `pwa/src/layout/head.tsx`**: Update the theme class applied to the root element to use `<slug>-theme` so the new theme is applied.
1172
+ 6. Start the development server or run a build (`npm run build` or `npm run dev`) in the template repository.
1173
+ 7. **Navigate to `/theme`** using Playwright MCP to see all available components rendered with the new theme.
1174
+ 8. Take screenshots of the `/theme` page and each visible component section.
1175
+ 9. Compare the rendered components against the component token files you created. Note any visual discrepancies.
1176
+
1177
+ ### D3 — Error report
1178
+
1179
+ Produce a **detailed error report** with these sections:
1180
+
1181
+ #### Build errors
1182
+
1183
+ - List every build error with the exact error message and file
1184
+ - For "Reference doesn't exist" errors: state the missing token path and whether it was fixable
1185
+ - State whether the build ultimately succeeded or failed
1186
+
1187
+ #### Theme integration errors (woo-website-template-apiv2)
1188
+
1189
+ - State whether the repository was found and the branch was checked out successfully
1190
+ - List any errors encountered when updating config files
1191
+ - List any build or server errors
1192
+
1193
+ #### Visual component review (from /theme page)
1194
+
1195
+ - For each component visible at `/theme` that is part of the created token set: state whether it looks correct or note the specific visual issue
1196
+
1197
+ #### Summary of issues requiring manual attention
1198
+
1199
+ - List any colors or fonts that could not be determined and were left as placeholders
1200
+ - List any build, integration, or visual issues that could not be auto-resolved
1201
+
1202
+ ---
1203
+
1204
+ ## Part E — Summary
1205
+
1206
+ After creating/updating all files and completing the build and test, output a summary:
1207
+
1208
+ 1. List all files created or modified
1209
+ 2. Note which colors were extracted from the website (or indicate placeholders were used)
1210
+ 3. Note which font was found (or indicate placeholder was used)
1211
+ 4. State the build result (success / failed with N errors)
1212
+ 5. State the test result against `woo-website-template-apiv2` (success / failed / repo not found)
1213
+ 6. Remind the user to:
1214
+ - Verify and refine colors in `src/brand/<slug>/color.tokens.json`
1215
+ - Add `@font-face` rules in `src/font.scss` if not auto-generated
1216
+ - Add a build script entry in the root `package.json`: `"build:<slug>": "cd ./municipalities/<slug>-design-tokens && npm run build"`