@getvitops/generator 8.2.0-alpha.0 → 8.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +550 -25
  2. package/assets/css/index.css +7 -0
  3. package/assets/css/patterns/banner.css +20 -0
  4. package/assets/css/patterns/callout.css +39 -0
  5. package/assets/css/patterns/copy.css +5 -0
  6. package/assets/css/patterns/embed.css +39 -0
  7. package/assets/css/patterns/forms.css +19 -0
  8. package/assets/css/patterns/logos.css +45 -0
  9. package/assets/css/patterns/marquee.css +16 -0
  10. package/assets/css/patterns/section.css +53 -0
  11. package/assets/css/patterns/sitenav.css +262 -0
  12. package/assets/css/patterns/splitter.css +11 -0
  13. package/assets/css/patterns/stats.css +61 -0
  14. package/assets/css/patterns/steps.css +98 -0
  15. package/assets/css/patterns/table.css +10 -0
  16. package/assets/css/patterns/tone.css +38 -0
  17. package/assets/js/consent.js +1 -1
  18. package/assets/js/elements.js +5 -5
  19. package/assets/js/polyfills/adoptedStyleSheets.js +1 -1
  20. package/assets/js/polyfills/css-anchor-positioning-fn.js +2 -2
  21. package/assets/js/polyfills/popover.js +2 -2
  22. package/config.schema.json +257 -120
  23. package/dist/config-BfcWKH-5.mjs +1743 -0
  24. package/dist/config-BfcWKH-5.mjs.map +1 -0
  25. package/dist/{config-DvbYx3Gi.mjs → config-Bhg3Xv3D.mjs} +10 -207
  26. package/dist/config-Bhg3Xv3D.mjs.map +1 -0
  27. package/dist/config-D5xuz8--.mjs +1807 -0
  28. package/dist/config-D5xuz8--.mjs.map +1 -0
  29. package/dist/config.d.mts +157 -211
  30. package/dist/config.d.mts.map +1 -1
  31. package/dist/config.mjs +1 -1
  32. package/dist/{icons-sprite-Bf_KmKeX.d.mts → icons-sprite-CQ5Zj4go.d.mts} +4 -6
  33. package/dist/icons-sprite-CQ5Zj4go.d.mts.map +1 -0
  34. package/dist/icons-sprite.d.mts +1 -1
  35. package/dist/icons-sprite.mjs +1 -2
  36. package/dist/index.d.mts +49 -35
  37. package/dist/index.d.mts.map +1 -1
  38. package/dist/index.mjs +607 -75
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{schema-Dmq1J7S3.d.mts → schema-BV2n2phj.d.mts} +2 -3
  41. package/dist/{schema-Dmq1J7S3.d.mts.map → schema-BV2n2phj.d.mts.map} +1 -1
  42. package/dist/schema-C-P9dF9F.d.mts +337 -0
  43. package/dist/schema-C-P9dF9F.d.mts.map +1 -0
  44. package/dist/{tokens-8SXMDAd1.d.mts → tokens-BzBbTeTi.d.mts} +2 -6
  45. package/dist/tokens-BzBbTeTi.d.mts.map +1 -0
  46. package/dist/{tokens-zi594IZ3.mjs → tokens-C1RJwFeG.mjs} +3 -5
  47. package/dist/{tokens-zi594IZ3.mjs.map → tokens-C1RJwFeG.mjs.map} +1 -1
  48. package/dist/tokens-CaIGQQLk.mjs +740 -0
  49. package/dist/tokens-CaIGQQLk.mjs.map +1 -0
  50. package/dist/tokens-DH5cpvKd.d.mts +242 -0
  51. package/dist/tokens-DH5cpvKd.d.mts.map +1 -0
  52. package/dist/tokens.d.mts +1 -1
  53. package/dist/tokens.mjs +1 -1
  54. package/package.json +6 -5
  55. package/schema.json +95 -5
  56. package/dist/config-DvbYx3Gi.mjs.map +0 -1
  57. package/dist/icons-sprite-Bf_KmKeX.d.mts.map +0 -1
  58. package/dist/tokens-8SXMDAd1.d.mts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as SYSTEM_DARK_SEL, a as expandPalette, b as roleHue, c as monotonicityErrors, d as tokenVar, f as validateColorContract, g as ROLE_TOKEN_KEYS, h as REQUIRED_ROLES, i as checkContrast, l as patchToOverrideCss, m as DARK_SEL, n as NUMERIC_STEPS, o as functionalRole, p as BASE_HOOK, r as buildReverseIndex, s as ladderWarnings, t as CONTRAST, u as tokenClass, v as TW_CLASH, x as roleKind, y as TYPO_KEYMAP } from "./tokens-zi594IZ3.mjs";
2
- import { A as jsonSchema, C as resolveTheme, D as DesignSystemSchema, E as validateConfig, M as movedTokens, O as SCHEMA_LOCAL_PATH, S as resolvePrivacyContact, T as stripNulls, _ as getAt, a as CONFIG_VERSION, b as resolveConfig, c as JURISDICTIONS, d as SiteFontSchema, f as configJsonSchema, g as formatConfig, h as experimentDimensionWarnings, i as CONFIG_SCHEMA_URL, j as validate, k as SCHEMA_URL, l as JURISDICTION_COUNTRIES, m as designSystemPath, n as BIGIN_DATA_CENTERS, o as CRM_PROVIDERS, p as deepMerge, r as CONFIG_SCHEMA_LOCAL_PATH, s as ConfigSchema, t as AD_PROVIDERS, u as PLAN_CAPABILITIES, v as isConfig, w as setAt, x as resolveInput, y as mergePatch } from "./config-DvbYx3Gi.mjs";
1
+ import { C as roleKind, S as roleHue, _ as ROLE_TOKEN_KEYS, a as expandPalette, b as TYPO_KEYMAP, c as monotonicityErrors, d as tokenVar, f as validateColorContract, g as REQUIRED_ROLES, h as LAYOUT_TOKENS, i as checkContrast, l as patchToOverrideCss, m as DARK_SEL, n as NUMERIC_STEPS, o as functionalRole, p as BASE_HOOK, r as buildReverseIndex, s as ladderWarnings, t as CONTRAST, u as tokenClass, v as SYSTEM_DARK_SEL, w as roleSolidOptions, x as layoutDecls, y as TW_CLASH } from "./tokens-CaIGQQLk.mjs";
2
+ import { A as jsonSchema, C as resolveTheme, D as DesignSystemSchema, E as validateConfig, M as movedTokens, O as SCHEMA_LOCAL_PATH, S as resolvePrivacyContact, T as stripNulls, _ as getAt, a as CONFIG_VERSION, b as resolveConfig, c as JURISDICTIONS, d as SiteFontSchema, f as configJsonSchema, g as formatConfig, h as experimentDimensionWarnings, i as CONFIG_SCHEMA_URL, j as validate, k as SCHEMA_URL, l as JURISDICTION_COUNTRIES, m as designSystemPath, n as BIGIN_DATA_CENTERS, o as CRM_PROVIDERS, p as deepMerge, r as CONFIG_SCHEMA_LOCAL_PATH, s as ConfigSchema, t as AD_PROVIDERS, u as PLAN_CAPABILITIES, v as isConfig, w as setAt, x as resolveInput, y as mergePatch } from "./config-D5xuz8--.mjs";
3
3
  import { buildIconSprite, loadIconSvg, spriteId } from "./icons-sprite.mjs";
4
4
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
@@ -10,7 +10,7 @@ import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
10
10
  import { generateIconInclude, nodesToHtml, resolveIcon, t } from "@getvitops/utils";
11
11
  import { resolveSitemapUrl } from "@getvitops/utils/indexing";
12
12
 
13
- //#region src/tiers.ts
13
+ // src/tiers.ts
14
14
  const c = (classes, partial, generated = false) => ({
15
15
  ...partial ? { partial } : {},
16
16
  classes,
@@ -45,9 +45,11 @@ const TIERS = {
45
45
  adds: "WAI-ARIA tree semantics, roving-tabindex keyboard navigation, filter, expand/collapse all, hash deep-linking"
46
46
  },
47
47
  astro: a("Tree", "wc"),
48
+ emdashExempt: "needs nested data (branches within branches) that a flat repeater cannot hold; revisit if EmDash block fields gain nesting",
48
49
  use: "`<Tree items={…} />` — it emits `<wc-tree>` itself, so do NOT add your own wrapper."
49
50
  },
50
51
  carousel: {
52
+ emdash: ["vitops.carousel"],
51
53
  css: c([
52
54
  "carousel",
53
55
  "carousel__track",
@@ -65,6 +67,7 @@ const TIERS = {
65
67
  use: "`<Carousel slides={…} label=\"…\" />` — it emits `<wc-carousel>` itself, so do NOT add your own wrapper. `.carousel` is the shell and `.carousel__track` the scroller; add `loop` for the cloned infinite strip (`autoplay` implies it)."
66
68
  },
67
69
  gallery: {
70
+ emdash: ["vitops.gallery"],
68
71
  css: c([
69
72
  "gallery",
70
73
  "gallery__item",
@@ -80,6 +83,7 @@ const TIERS = {
80
83
  use: "`<Gallery images={…} label=\"…\" />` — it emits `<wc-gallery>` wrapping a `<Carousel />` (which emits its own `<wc-carousel>`) itself, so do NOT add your own wrapper around either. Each thumbnail is a plain `<a href=\"#slide-id\">` into the carousel's own `<li id=\"slide-id\">` inside a single `<dialog class=\"lightbox-dialog\">` (see `lightbox`), so it is a working modal lightbox — WITH prev/next — before any script runs. Stepping between images is the carousel's job, not the gallery's."
81
84
  },
82
85
  entries: {
86
+ emdash: ["vitops.entries"],
83
87
  css: c([
84
88
  "entries__table",
85
89
  "entries__scroll",
@@ -90,10 +94,12 @@ const TIERS = {
90
94
  registered: true,
91
95
  adds: "parses heading + `<dl>` pairs into a table above a breakpoint"
92
96
  },
97
+ astro: a("Entries", "wc"),
93
98
  bricks: "vitops-entries",
94
- use: "`<wc-entries>` around `<h3>` + `<dl>` pairs. The tier-2 exemplar: semantic pairs with no JS, a table with it."
99
+ use: "`<Entries entries={[{ heading, pairs: [{ term, value }] }]} />` — it emits `<wc-entries>` itself, so do NOT add your own wrapper. By hand: `<wc-entries>` around `<h3>` + `<dl>` pairs. The tier-2 exemplar: semantic pairs with no JS, a table with it."
95
100
  },
96
101
  copy: {
102
+ emdash: ["vitops.copyButton"],
97
103
  css: c([
98
104
  "copy",
99
105
  "copy__button",
@@ -109,6 +115,7 @@ const TIERS = {
109
115
  use: "`<wc-copy>` around the content and a `[data-copy]` button."
110
116
  },
111
117
  dismissable: {
118
+ emdash: ["vitops.banner"],
112
119
  css: c(["banner__close", "notification__close"], "patterns/banner.css"),
113
120
  wc: {
114
121
  tag: "wc-dismissable",
@@ -124,6 +131,7 @@ const TIERS = {
124
131
  "splitter__panel",
125
132
  "splitter__handle"
126
133
  ], "patterns/splitter.css"),
134
+ emdashExempt: "app UI (resizable panes), not editorial content; an editor does not drag-resize a published page",
127
135
  wc: {
128
136
  tag: "wc-split-panel",
129
137
  registered: true,
@@ -133,6 +141,7 @@ const TIERS = {
133
141
  use: "`<wc-split-panel>` with two children. Unenhanced they simply stack."
134
142
  },
135
143
  "image-compare": {
144
+ emdash: ["vitops.imageCompare"],
136
145
  css: c([
137
146
  "image-compare",
138
147
  "image-compare__before",
@@ -147,6 +156,7 @@ const TIERS = {
147
156
  use: "`<wc-image-compare>` with two images. Both are visible without JS."
148
157
  },
149
158
  marquee: {
159
+ emdash: ["vitops.marquee"],
150
160
  css: c([
151
161
  "marquee",
152
162
  "marquee__content",
@@ -160,7 +170,85 @@ const TIERS = {
160
170
  astro: a("Marquee", "wc"),
161
171
  use: "`<Marquee>` around one `.marquee__content`, or `<wc-marquee>` by hand. Scrolls via CSS alone; JS only removes the seam."
162
172
  },
173
+ tone: {
174
+ css: c([
175
+ "tone",
176
+ "tone--solid",
177
+ "accent"
178
+ ], "patterns/tone.css"),
179
+ use: "Add `.tone` and set `--tone-bg` / `--tone-fg` / `--tone-border` / `--tone-solid` / `--tone-on-solid` as custom properties — never a class built from a value, which Tailwind cannot see. `@getvitops/emdash`'s `resolveTone(tone, color)` writes them: role, then semantic fallbacks, then `neutral`, or a palette `color` escape hatch. `.accent` colours a run inside a heading."
180
+ },
181
+ section: {
182
+ emdash: ["vitops_section"],
183
+ css: c([
184
+ "section",
185
+ "section__head",
186
+ "section__kicker",
187
+ "section__title",
188
+ "section__sub",
189
+ "section__body",
190
+ "section--split",
191
+ "section--center"
192
+ ], "patterns/section.css"),
193
+ use: "`.section.region` > `.centered` > `.section__inner` holding `.section__head` (kicker, heading, sub) and `.section__body`. `.section--split` puts the head beside the body when wide. In EmDash it is a block TYPE (`vitops_section`) with a Portable Text body that accepts any other block — see `vitopsBlockTypes()`."
194
+ },
195
+ steps: {
196
+ emdash: ["vitops.steps"],
197
+ css: c([
198
+ "steps",
199
+ "steps__item",
200
+ "steps__body",
201
+ "steps__title",
202
+ "steps__text",
203
+ "steps--row"
204
+ ], "patterns/steps.css"),
205
+ use: "`<ol class=\"steps tone\">` of `<li class=\"steps__item\">` each holding a `.steps__body` with a real heading. The number is a CSS counter and the connector line a pseudo-element; `.steps--row` runs it sideways when wide."
206
+ },
207
+ stats: {
208
+ emdash: ["vitops.stats"],
209
+ css: c([
210
+ "stats",
211
+ "stats__item",
212
+ "stats__figure",
213
+ "stats__label",
214
+ "stats__group",
215
+ "stats__delta",
216
+ "stats--tiles"
217
+ ], "patterns/stats.css"),
218
+ use: "`<ul class=\"stats\">` of `.stats__item`, each figure a `<Counter>`. `.stats--tiles` adds `.stats__group` and `.stats__delta`; `data-state=\"up|down\"` colours the delta through the success/danger roles."
219
+ },
220
+ callout: {
221
+ emdash: ["vitops.callout"],
222
+ css: c([
223
+ "callout",
224
+ "callout__body",
225
+ "callout__title",
226
+ "callout__text",
227
+ "callout__actions",
228
+ "callout--center"
229
+ ], "patterns/callout.css"),
230
+ use: "`.callout.tone` with a `.callout__body` and `.callout__actions`. A call-to-action band that stays on the page — NOT `banner`, which is a dismissible announcement."
231
+ },
232
+ logos: {
233
+ emdash: ["vitops.logos"],
234
+ css: c([
235
+ "logos",
236
+ "logos__item",
237
+ "logos--grayscale"
238
+ ], "patterns/logos.css"),
239
+ use: "`<ul class=\"logos\">` of `.logos__item` images (alt text = the name). A still grid; `marquee` is the scrolling version."
240
+ },
241
+ embed: {
242
+ emdash: ["vitops.embed"],
243
+ css: c([
244
+ "embed",
245
+ "embed__frame",
246
+ "embed__placeholder"
247
+ ], "patterns/embed.css"),
248
+ use: "`.embed` around an `<iframe class=\"embed__frame\" data-consent=\"marketing\" data-consent-src=\"…\">` with NO `src`, and a `.embed__placeholder`. The consent runtime assigns the address after the grant; the placeholder holds a real link. Never give the iframe a live `src`."
249
+ },
163
250
  counter: {
251
+ emdash: ["vitops.stats"],
164
252
  css: c([
165
253
  "counter",
166
254
  "counter__value",
@@ -175,6 +263,7 @@ const TIERS = {
175
263
  use: "`<Counter value=\"94%\" />` — it emits `<wc-counter>` itself, so do NOT add your own wrapper. The final value is parsed from the fallback text, never passed separately."
176
264
  },
177
265
  "multi-field": {
266
+ emdashExempt: "repeats rows a visitor adds at submit time (several phone numbers, guests); `vitops.form` has a fixed, editor-defined field list and no such field type",
178
267
  css: c([
179
268
  "form-group",
180
269
  "input-group",
@@ -188,7 +277,18 @@ const TIERS = {
188
277
  bricks: "vitops-multi-field",
189
278
  use: "`<wc-multi-field>` around a field template. One row submits fine with no JS."
190
279
  },
280
+ turnstile: {
281
+ emdash: ["vitops.form"],
282
+ css: c(["form-group"], "patterns/forms.css"),
283
+ wc: {
284
+ tag: "wc-turnstile",
285
+ registered: true,
286
+ adds: "loads Cloudflare Turnstile on first interaction with the form and holds a submit until its token arrives"
287
+ },
288
+ use: "`<wc-turnstile sitekey=\"…\">` inside a `<form>`, around a paragraph saying the spam check needs JavaScript. Pair it with `createConversionRoute({ turnstile })`; the `vitops.form` block does both."
289
+ },
191
290
  "color-scheme-toggle": {
291
+ emdashExempt: "site chrome (header/footer), not page content; belongs in the nav/layout",
192
292
  css: c([], "patterns/icon.css"),
193
293
  wc: {
194
294
  tag: "wc-color-scheme-toggle",
@@ -199,6 +299,7 @@ const TIERS = {
199
299
  use: "`<wc-color-scheme-toggle></wc-color-scheme-toggle>`. Renamed from `<color-scheme-toggle>` in 4.0. Shadow DOM, so it is absent rather than broken without JS."
200
300
  },
201
301
  consent: {
302
+ emdashExempt: "site chrome: the layout renders `<CookieConsent />` once; an editor-placed copy would duplicate the gate",
202
303
  css: c([
203
304
  "consent",
204
305
  "consent__body",
@@ -218,6 +319,7 @@ const TIERS = {
218
319
  use: "`<CookieConsent />`. Its own Lit-free bundle so a site needing consent does not download a rendering framework."
219
320
  },
220
321
  "theme-editor": {
322
+ emdashExempt: "tooling, not content; it is mounted in the EmDash admin by `designSystemEditor`, never in a page",
221
323
  css: c([
222
324
  "ed-panel",
223
325
  "ed-launch",
@@ -272,6 +374,7 @@ const TIERS = {
272
374
  use: "Not shipped. See `color-wheel`."
273
375
  },
274
376
  details: {
377
+ emdash: ["vitops.details", "vitops.accordion"],
275
378
  css: c([
276
379
  "details",
277
380
  "details-trigger",
@@ -309,6 +412,7 @@ const TIERS = {
309
412
  use: "`<NavShell>` with a `nav` slot, plus `<NavShellToggle>` if the toggle must live outside the shell."
310
413
  },
311
414
  subgrid: {
415
+ emdash: ["vitops.pricing"],
312
416
  css: c([
313
417
  "subgrid",
314
418
  "subgrid-card",
@@ -318,6 +422,7 @@ const TIERS = {
318
422
  use: "`<Subgrid>` — emits `<ul class=\"subgrid\" role=\"list\">`; author `<li class=\"card subgrid-card\">` items yourself, so tranches align across the set."
319
423
  },
320
424
  card: {
425
+ emdash: ["vitops.cards"],
321
426
  css: c(["card"], "patterns/card.css", true),
322
427
  wc: {
323
428
  tag: "wc-cards",
@@ -338,6 +443,7 @@ const TIERS = {
338
443
  use: "`<Icon name=\"menu\" />`. Names are semantic and resolve per configured set; a name containing `:` passes through."
339
444
  },
340
445
  split: {
446
+ emdash: ["vitops.hero"],
341
447
  css: c([
342
448
  "split",
343
449
  "split-1-2",
@@ -360,10 +466,14 @@ const TIERS = {
360
466
  css: c([
361
467
  "sitenav",
362
468
  "sitenav__link",
363
- "sitenav--drawer-start"
469
+ "sitenav--drawer-start",
470
+ "navlist",
471
+ "navlist--columns"
364
472
  ], "patterns/sitenav.css"),
473
+ astro: [...a("SiteNav", "css"), ...a("SiteFooter", "css")],
474
+ emdash: ["vitops.nav"],
365
475
  bricks: "vitops-sitenav",
366
- use: "`.sitenav` with `--bp-*` and a drawer direction. `<details>` + Invoker Commands, no JS."
476
+ use: "`.sitenav` with `--bp-*` and a drawer direction. `<details>` + Invoker Commands, no JS. In Astro: `<SiteNav {...await loadNavMenus(getMenu)} />` renders EmDash menu `primary` (the default: dropdowns from `md`, drawer below) and, if they exist, `primary-mobile` (depth-1 nav below `md`) and `primary-mega` (mega panels from `xl`) — adding the menu in the admin is the whole change. An item's `titleAttr` is its description. `<SiteFooter sitemap legal name>` renders `footer-sitemap` + `footer-legal` + the copyright line. For a menu inside page content (a site map, a section's links, footer-style columns) use `.navlist` — plain nested lists, `--inline` / `--columns` — or, in EmDash, the `vitops.nav` block (menu, layout `list|inline|columns|sitenav`, depth 1–3, heading, tone). Name each `<nav>` uniquely: the header is `Primary` and the footer `Site map` / `Legal`, and the block never reuses those."
367
477
  },
368
478
  "split-link": {
369
479
  css: c([
@@ -375,14 +485,17 @@ const TIERS = {
375
485
  use: "`.split-link` — a link plus a disclosure toggle. No JS."
376
486
  },
377
487
  btn: {
488
+ emdash: ["vitops.actionLink"],
378
489
  css: c(["btn"], void 0, true),
379
490
  use: "`<button>` gets it with no class; `.btn` carries it to other tags. `fill: false`, so states drive `color`."
380
491
  },
381
492
  cta: {
493
+ emdash: ["vitops.actionLink"],
382
494
  css: c(["cta", "cta-danger"], void 0, true),
383
495
  use: "`.cta` on any element — usually `<a>`, since a CTA navigates. Filled, with role variants."
384
496
  },
385
497
  link: {
498
+ emdash: ["vitops.actionLink"],
386
499
  css: c([
387
500
  "link",
388
501
  "skip-link",
@@ -457,7 +570,8 @@ const TIERS = {
457
570
  "review__source"
458
571
  ], "patterns/review.css", true),
459
572
  astro: a("ReviewCard", "css"),
460
- use: "`<ReviewCard review={…} />` — one card, from `vitops reviews`' `.vitops/reviews.json` cache. Put several inside `<Carousel>` slides or `<Marquee>` items; there is no group component and no `wc-review`."
573
+ emdash: ["vitops.reviews"],
574
+ use: "`<ReviewCard review={…} />` — one card, from `vitops reviews`' `.vitops/reviews.json` cache. Put several inside `<Carousel>` slides or `<Marquee>` items; there is no group component and no `wc-review`. In EmDash, `vitops.reviews` is that whole row: it reads the cache as a build-time import (`virtual:getvitops/reviews`, served by `vitops()`; empty when the file is absent), picks reviews with `selectReviews()`, and lays them out as a grid, carousel or marquee. A new review appears after `vitops reviews` is re-run, committed and the site rebuilt."
461
575
  },
462
576
  tabs: {
463
577
  css: c(["tabs"], void 0, true),
@@ -481,6 +595,7 @@ const TIERS = {
481
595
  use: "`.navbar` with `--start/--center/--end`."
482
596
  },
483
597
  banner: {
598
+ emdash: ["vitops.banner"],
484
599
  css: c([
485
600
  "banner",
486
601
  "banner__content",
@@ -648,6 +763,7 @@ function tierPatterns(tier) {
648
763
  case "wc": return !!e.wc;
649
764
  case "astro": return !!e.astro?.length;
650
765
  case "bricks": return !!e.bricks;
766
+ case "emdash": return !!e.emdash?.length;
651
767
  }
652
768
  };
653
769
  return TIER_NAMES.filter((name) => has(TIERS[name])).map((name) => ({
@@ -656,8 +772,7 @@ function tierPatterns(tier) {
656
772
  }));
657
773
  }
658
774
 
659
- //#endregion
660
- //#region src/docs.ts
775
+ // src/docs.ts
661
776
  /**
662
777
  * OKF documentation bundle generator (ported from the repo's `generate-docs.ts`).
663
778
  *
@@ -2267,7 +2382,8 @@ function renderComponentsConcept(ds) {
2267
2382
  "css",
2268
2383
  "wc",
2269
2384
  "astro",
2270
- "bricks"
2385
+ "bricks",
2386
+ "emdash"
2271
2387
  ].filter((t) => tierPatterns(t).some((p) => p.name === name));
2272
2388
  const overview = TIER_NAMES.map((name) => {
2273
2389
  const e = TIERS[name];
@@ -2280,6 +2396,8 @@ function renderComponentsConcept(ds) {
2280
2396
  }).join("\n");
2281
2397
  const astro = tierPatterns("astro").flatMap(({ name, entry }) => entry.astro.map((a) => `| ${code(a.component)} | ${code(name)} | ${a.wraps === "wc" ? `the ${code(`<${entry.wc.tag}>`)} tag, fallback inside` : "tier-1 markup — no web component"} |`)).join("\n");
2282
2398
  const bricks = tierPatterns("bricks").map(({ name, entry }) => `| ${code(entry.bricks)} | ${code(name)} |`).join("\n");
2399
+ const emdash = tierPatterns("emdash").flatMap(({ name, entry }) => entry.emdash.map((type) => `| ${code(type)} | ${code(name)} | ${entry.astro?.length ? list(entry.astro.map((a) => `<${a.component.split("/").pop().replace(".astro", "")} />`)) : "tier-1 markup"} |`)).join("\n");
2400
+ const exempt = TIER_NAMES.filter((n) => TIERS[n].emdashExempt).map((n) => `- ${code(TIERS[n].wc?.tag ?? n)} — ${TIERS[n].emdashExempt}`).join("\n");
2283
2401
  const css = tierPatterns("css").map(({ name, entry }) => {
2284
2402
  const c = entry.css;
2285
2403
  const config = !c.generated ? "—" : declared.has(name) ? "declared" : "**absent from this config — no CSS emitted**";
@@ -2290,13 +2408,14 @@ function renderComponentsConcept(ds) {
2290
2408
  return `${frontmatter({
2291
2409
  type: "Design Concept",
2292
2410
  title: "Vitops components — which tier provides a pattern, and what to write",
2293
- description: "Every UI pattern across the three tiers (CSS classes, wc-* web components, and the platform wrappers: Astro components and Bricks elements): which tiers provide it, which call to make, and how the tiers compose.",
2411
+ description: "Every UI pattern across the three tiers (CSS classes, wc-* web components, and the platform wrappers: Astro components, Bricks elements and EmDash blocks): which tiers provide it, which call to make, and how the tiers compose.",
2294
2412
  resource: DS_PATH,
2295
2413
  tags: [
2296
2414
  "components",
2297
2415
  "web-components",
2298
2416
  "astro",
2299
2417
  "bricks",
2418
+ "emdash",
2300
2419
  "tiers",
2301
2420
  "design-system"
2302
2421
  ]
@@ -2313,13 +2432,17 @@ A pattern is provided by up to **three tiers**, and they **compose** rather than
2313
2432
  3. **Platform wrappers** — authoring conveniences that generate the correct markup at build
2314
2433
  time, using the classes and elements of tiers 1 and 2. They must not require runtime JS.
2315
2434
  Where a pattern has a web component, the wrapper emits that tag **with the accessible
2316
- fallback inside it**. Two platforms:
2435
+ fallback inside it**. Three platforms:
2317
2436
  - **Astro components** (${code("@getvitops/astro")})
2318
2437
  - **Bricks elements** (WordPress / Bricks Builder)
2438
+ - **EmDash blocks** (${code("@getvitops/emdash")}) — Portable Text blocks an editor inserts;
2439
+ each is an adapter over the pattern's Astro component
2319
2440
 
2320
- **Astro and Bricks are the same tier**, not tiers 3 and 4. They are siblings chosen by which
2321
- platform you are on — no project uses both, and neither outranks the other. The tables below
2322
- give them separate columns because they are separate packages, not separate levels.
2441
+ **Astro, Bricks and EmDash are the same tier**, not tiers 3, 4 and 5. They are siblings chosen
2442
+ by which platform you are on — neither outranks the others. (An EmDash site is an Astro site,
2443
+ so it uses the Astro components directly in templates and the blocks inside editor content.)
2444
+ The tables below give them separate columns because they are separate packages, not separate
2445
+ levels.
2323
2446
 
2324
2447
  ## Choosing
2325
2448
 
@@ -2366,6 +2489,24 @@ Controls, defaults and seeded children for each are in [the elements reference](
2366
2489
  | --- | --- |
2367
2490
  ${bricks}
2368
2491
 
2492
+ ## EmDash blocks
2493
+
2494
+ Portable Text block ${code("_type")}s from ${code("@getvitops/emdash")}. Repeating patterns are
2495
+ repeater fields. A Portable Text block cannot contain other blocks, so a pattern that must hold
2496
+ them is a schema-level block TYPE with a Portable Text body instead — ${code("vitops_section")},
2497
+ from ${code("vitopsBlockTypes()")}. Its body accepts every ${code("vitops.*")} block below.
2498
+
2499
+ | Block | Pattern | Renders via |
2500
+ | --- | --- | --- |
2501
+ ${emdash}
2502
+
2503
+ ### Web components with no block, on purpose
2504
+
2505
+ Every shipped ${code("wc-*")} element either has a block above or a stated reason here
2506
+ (${code("emdashExempt")} in the manifest, enforced by ${code("tiers.test.ts")}):
2507
+
2508
+ ${exempt}
2509
+
2369
2510
  ## CSS
2370
2511
 
2371
2512
  ${code("patterns.items")} patterns get the full token cascade — ${code("base")} declarations,
@@ -3113,8 +3254,7 @@ function generateDocs(ds, assetsDir) {
3113
3254
  };
3114
3255
  }
3115
3256
 
3116
- //#endregion
3117
- //#region src/design-md.ts
3257
+ // src/design-md.ts
3118
3258
  const q = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
3119
3259
  /** Keys are quoted only when they would not survive as a plain scalar. */
3120
3260
  const key = (k) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(k) ? k : q(k);
@@ -3541,8 +3681,7 @@ across every pattern. Focus rings are never removed, only restyled.
3541
3681
  `;
3542
3682
  }
3543
3683
 
3544
- //#endregion
3545
- //#region src/legal/providers.ts
3684
+ // src/legal/providers.ts
3546
3685
  /**
3547
3686
  * What the config already tells us about third parties that receive personal
3548
3687
  * information.
@@ -3839,8 +3978,7 @@ function biginLocation(processor, site) {
3839
3978
  } : processor;
3840
3979
  }
3841
3980
 
3842
- //#endregion
3843
- //#region src/legal/derive.ts
3981
+ // src/legal/derive.ts
3844
3982
  /**
3845
3983
  * Site config → the facts a legal document asserts.
3846
3984
  *
@@ -3957,7 +4095,7 @@ function derivePolicyVars(cfg) {
3957
4095
  const fields = formFields(site);
3958
4096
  const hasForms = fields.length > 0;
3959
4097
  const hasAnalytics = Object.values(site.analytics ?? {}).some((v) => typeof v === "string" ? v.length > 0 : !!v);
3960
- processors.filter((p) => p.cookies?.length);
4098
+ const cookieProcessors = processors.filter((p) => p.cookies?.length);
3961
4099
  const consent = legal?.cookieConsent;
3962
4100
  const contact = resolvePrivacyContact(cfg);
3963
4101
  const businessName = org?.legalName ?? (org?.name != null ? t(org.name, locale, locale) : void 0) ?? "we";
@@ -4070,6 +4208,12 @@ function derivePolicyVars(cfg) {
4070
4208
  const operatorCountries = list(uniq(processors.map((p) => p.operatorCountry)).filter((c) => foreign(c) && !named.has(norm(c))));
4071
4209
  const countries = list(uniq(processors.flatMap((p) => [...(p.storage ?? []).map((s) => s.country), p.operatorCountry])));
4072
4210
  const address = contact?.address ?? org?.address;
4211
+ const trackingTechnologies = uniq([
4212
+ ...cookieProcessors.map((p) => `${p.name} (${p.purpose})`),
4213
+ hasSessionReplay ? "session recording of how you interact with a page" : void 0,
4214
+ hasAdTracking ? "advertising attribution (the `_ac` cookie described in our Cookie Notice)" : void 0,
4215
+ ...Object.keys(site.ads ?? {}).map((a) => `${a} advertising measurement`)
4216
+ ]);
4073
4217
  return {
4074
4218
  businessName,
4075
4219
  url: site.domains?.canonical ?? "",
@@ -4101,12 +4245,16 @@ function derivePolicyVars(cfg) {
4101
4245
  operatorCountries: operatorCountries || void 0,
4102
4246
  countries: countries || void 0,
4103
4247
  consentModel: consent?.enabled ? consent.type ?? "opt-in" : "none",
4104
- cookieCategories: consent?.categories ?? []
4248
+ cookieCategories: consent?.categories ?? [],
4249
+ privacyOfficerName: contact?.name,
4250
+ privacyOfficerTitle: privacy?.privacyOfficerTitle,
4251
+ transferAssessment: privacy?.impactAssessments === true,
4252
+ trackingTechnologies,
4253
+ newsletterSignup: fields.some((f) => /newsletter|subscribe|mailing/i.test(f.name))
4105
4254
  };
4106
4255
  }
4107
4256
 
4108
- //#endregion
4109
- //#region src/legal/render.ts
4257
+ // src/legal/render.ts
4110
4258
  /**
4111
4259
  * The legal documents are authored in markdown; consumers need markdown, HTML
4112
4260
  * and EmDash Portable Text. This parses the markdown **once** into a small block
@@ -4316,8 +4464,7 @@ function toPortableText(blocks, prefix) {
4316
4464
  return out;
4317
4465
  }
4318
4466
 
4319
- //#endregion
4320
- //#region src/legal/templates/shared.ts
4467
+ // src/legal/templates/shared.ts
4321
4468
  /**
4322
4469
  * The banner every generated document opens with.
4323
4470
  *
@@ -4328,6 +4475,18 @@ function toPortableText(blocks, prefix) {
4328
4475
  * renders as a callout in HTML and as a warning banner in EmDash.
4329
4476
  */
4330
4477
  const REVIEW_NOTICE = "> Generated from this site's configuration. It is a starting point, not legal advice — have it reviewed before you publish it, and make sure it matches what your site actually does.";
4478
+ /**
4479
+ * The second banner the `qc` documents carry, after `REVIEW_NOTICE`.
4480
+ *
4481
+ * Quebec adds obligations a generic review prompt does not name, and each is a
4482
+ * thing this generator cannot check: that a person in charge of privacy has been
4483
+ * designated and is published, that a privacy impact assessment precedes any
4484
+ * communication outside Quebec, that consent mechanics match the Act, and — easy
4485
+ * to miss — that content directed at Quebec consumers is available in French. It
4486
+ * is a second blockquote (a second `vitops.banner` in Portable Text) so it cannot
4487
+ * be skimmed past as part of the first.
4488
+ */
4489
+ const QC_REVIEW_NOTICE = "> Quebec (Law 25): this document is written for the Quebec privacy regime and has NOT been reviewed by counsel. Before publishing, have a lawyer confirm that a person in charge of the protection of personal information is designated, that any transfer of information outside Quebec is preceded by a privacy impact assessment, that your consent and cookie mechanisms match the Act, and whether a French version is required for your audience. It is not legal advice.";
4331
4490
  /** A markdown bullet list. Empty in, empty out — never a stray `-`. */
4332
4491
  function bullets(items) {
4333
4492
  return items.map((item) => `- ${item}`).join("\n");
@@ -4340,8 +4499,7 @@ function updatedLine(v) {
4340
4499
  return v.lastUpdated ? `\nLast updated: ${v.lastUpdated}\n` : "";
4341
4500
  }
4342
4501
 
4343
- //#endregion
4344
- //#region src/legal/templates/cookies.ca.ts
4502
+ // src/legal/templates/cookies.ca.ts
4345
4503
  /**
4346
4504
  * Cookie notice — Canada.
4347
4505
  *
@@ -4373,7 +4531,7 @@ Cookies used on this Site may track:
4373
4531
 
4374
4532
  ${bullets(v.cookieTracked)}
4375
4533
 
4376
- ${providersSection$1(v)}${categoriesSection(v)}
4534
+ ${providersSection$2(v)}${categoriesSection(v)}
4377
4535
  ## Your Choices
4378
4536
 
4379
4537
  ${bullets(v.cookieOptOutOptions)}
@@ -4395,7 +4553,7 @@ If you have questions about our use of cookies, please contact us at ${v.mailing
4395
4553
  * attribution with a cookieless analytics provider sets `_ac` and no third-party
4396
4554
  * cookie at all, and the old wording would have declared it cookie-free.
4397
4555
  */
4398
- function providersSection$1(v) {
4556
+ function providersSection$2(v) {
4399
4557
  const setting = v.processors.filter((p) => p.cookies?.length);
4400
4558
  const cookieless = v.processors.filter((p) => p.cookies?.length === 0);
4401
4559
  const own = v.firstPartyCookies;
@@ -4424,8 +4582,69 @@ ${bullets(v.cookieCategories)}
4424
4582
  `;
4425
4583
  }
4426
4584
 
4427
- //#endregion
4428
- //#region src/legal/templates/privacy.ca.ts
4585
+ // src/legal/templates/cookies.qc.ts
4586
+ function cookiesQc(v) {
4587
+ return `# Cookie Notice
4588
+ ${updatedLine(v)}
4589
+ ${REVIEW_NOTICE}
4590
+ ${QC_REVIEW_NOTICE}
4591
+
4592
+ This notice explains how ${v.businessName} uses cookies and similar technologies on ${v.url || "this website"} (the "Site"). It supplements our Privacy Policy, which sets out your rights under Quebec's Act respecting the protection of personal information in the private sector.
4593
+
4594
+ ## What Cookies Are
4595
+
4596
+ A cookie is a small text file that a website asks your browser to store. Cookies let a site remember things between pages and visits. Similar technologies, such as local storage, do the same.
4597
+
4598
+ ## What We Use Them For
4599
+
4600
+ ${bullets(v.cookiePurposes)}
4601
+
4602
+ They may record:
4603
+
4604
+ ${bullets(v.cookieTracked)}
4605
+
4606
+ ${consentSection(v)}${providersSection$2(v)}${categoriesSection(v)}
4607
+ ## Your Choices
4608
+
4609
+ ${bullets(v.cookieOptOutOptions)}
4610
+
4611
+ Most browsers let you see the cookies stored, delete them one by one or all at once, and block them. Blocking every cookie will stop parts of the Site from working as intended.
4612
+
4613
+ ## Contact
4614
+
4615
+ Questions about cookies, or a request concerning your personal information, can be sent to ${v.mailingAddress}. If you are not satisfied with our response you may contact the Commission d'accès à l'information du Québec (www.cai.gouv.qc.ca).
4616
+ `;
4617
+ }
4618
+ /**
4619
+ * Only when something on the Site can actually identify or profile a visitor —
4620
+ * a notice that announced profiling technology the config does not enable would
4621
+ * be a false statement in the other direction.
4622
+ */
4623
+ function consentSection(v) {
4624
+ if (v.trackingTechnologies.length === 0) return "";
4625
+ const assurance = v.consentModel === "opt-in" ? "These are not activated until you choose to allow them using the consent controls on the Site. You can change your choice at any time." : v.consentModel === "opt-out" ? "You can deactivate them at any time using the consent controls on the Site." : "You can block them through your browser settings.";
4626
+ return `## Technologies That Can Identify, Locate or Profile You
4627
+
4628
+ Some technologies on this Site can identify you, or collect information about your behaviour in order to profile you:
4629
+
4630
+ ${bullets(v.trackingTechnologies)}
4631
+
4632
+ ${assurance}
4633
+
4634
+ `;
4635
+ }
4636
+
4637
+ // src/legal/templates/casl.ts
4638
+ function caslSection(v) {
4639
+ if (!v.newsletterSignup) return "";
4640
+ return `
4641
+ ### Commercial Electronic Messages
4642
+
4643
+ If you subscribe to our newsletter or otherwise ask to receive commercial electronic messages from us, we treat that request as your consent to receive them. Under Canada's Anti-Spam Legislation, each such message must identify who sent it and include a way to unsubscribe, and you may withdraw your consent at any time using the "unsubscribe" link in a message or by contacting us at ${v.mailingAddress}.
4644
+ `;
4645
+ }
4646
+
4647
+ // src/legal/templates/privacy.ca.ts
4429
4648
  /**
4430
4649
  * Privacy policy — Canada (PIPEDA).
4431
4650
  *
@@ -4505,14 +4724,14 @@ For example, we may use your personal information to:
4505
4724
 
4506
4725
  ${bullets(v.usePurposes)}
4507
4726
 
4508
- ${providersSection(v)}
4727
+ ${providersSection$1(v)}
4509
4728
 
4510
4729
  Business reserves the right to transfer personal information in the event that we merge with or are acquired by a third party. We also may disclose your personal information for any other purpose required or permitted by law or to which you consent.
4511
4730
 
4512
4731
  ### Your Choices
4513
4732
 
4514
4733
  ${bullets(v.optOutOptions)}
4515
-
4734
+ ${caslSection(v)}
4516
4735
  ### How We Protect Personal Information
4517
4736
 
4518
4737
  The security of your personal information is important to us. We protect your personal information in our control through appropriate security safeguards against loss, theft, unauthorized access, disclosure, copying, use or modification. These safeguards include physical measures (such as locked filing cabinets), organizational measures (such as employee training and restricted access), and technological measures (such as encryption and passwords). Personal information may be accessed by persons within our organization who require such access to carry out the purposes described in this Privacy Policy, or such other purposes as permitted or required by law. Personal information we collect is maintained with the source of the information at ${v.url}.
@@ -4520,7 +4739,7 @@ The security of your personal information is important to us. We protect your pe
4520
4739
  We retain personal information that we collect only as long as necessary for the purposes for which it was collected or to meet legal requirements. We destroy personal information when it is no longer needed for such purposes.${v.retention ? ` In general, we keep personal information for ${v.retention}.` : ""}
4521
4740
 
4522
4741
  It is important to understand that no security measures are absolute. We cannot guarantee the safety of any information you provide to us.
4523
- ${transferSection(v)}
4742
+ ${transferSection$1(v)}
4524
4743
  ### Accessing and Correcting Your Personal Information
4525
4744
 
4526
4745
  You have a right to access your personal information and to request a correction to it if you believe it is inaccurate. If you have submitted personal information and would like to have access to it, or if you would like to have it corrected, please contact us using the contact information provided below. We may require you verify your identity before allowing you to access your personal information.
@@ -4536,7 +4755,7 @@ If you have any questions regarding this Privacy Policy, to submit a complaint,
4536
4755
  * "we may share with service providers" without naming them is the vaguest and
4537
4756
  * least useful sentence a policy can contain.
4538
4757
  */
4539
- function providersSection(v) {
4758
+ function providersSection$1(v) {
4540
4759
  if (v.processors.length === 0) return "We do not transfer personal information to third party service providers.";
4541
4760
  return `We may transfer personal information to third party service providers that assist us with carrying out these purposes. When we transfer personal information to service providers, we require them to protect the information in a manner consistent with our privacy practices and applicable privacy laws. These service providers are:
4542
4761
 
@@ -4555,7 +4774,7 @@ We remain responsible for personal information in the possession of our service
4555
4774
  *
4556
4775
  * The closer is shared: it is true of whichever clauses appear.
4557
4776
  */
4558
- function transferSection(v) {
4777
+ function transferSection$1(v) {
4559
4778
  const clauses = [storageClause(v), operatorClause(v)].filter(Boolean);
4560
4779
  if (clauses.length === 0) return "";
4561
4780
  return `
@@ -4585,8 +4804,185 @@ function operatorClause(v) {
4585
4804
  return `Some of the service providers we use are established in, or controlled from, jurisdictions outside of Canada, including ${v.operatorCountries}. Personal information those providers handle on our behalf may be subject to the laws of those jurisdictions even when it is stored in Canada.`;
4586
4805
  }
4587
4806
 
4588
- //#endregion
4589
- //#region src/legal/templates/terms.ca.ts
4807
+ // src/legal/templates/privacy.qc.ts
4808
+ /**
4809
+ * Privacy policy — Quebec (Act respecting the protection of personal information
4810
+ * in the private sector, as amended by Law 25).
4811
+ *
4812
+ * Written fresh rather than adapted from `privacy.ca.ts`: the Quebec regime asks
4813
+ * for things PIPEDA does not — a published person in charge, a stated position on
4814
+ * technologies that identify or profile, a privacy impact assessment before
4815
+ * information leaves Quebec, portability, de-indexing, the right to be told about
4816
+ * automated decisions — and frames "abroad" as outside Quebec rather than outside
4817
+ * Canada. Reusing the `ca` prose would assert PIPEDA's framing under Law 25's name.
4818
+ *
4819
+ * Same rule as everywhere: the config records facts, this file owns prose, and a
4820
+ * fact the config cannot know is never asserted. So the assessment assurance
4821
+ * renders only when `impactAssessments` is attested, the profiling assurance only
4822
+ * under an opt-in consent model, and the person in charge is named only when the
4823
+ * config names one. What remains is the statute's own wording of the visitor's
4824
+ * rights, which holds regardless of how the business operates.
4825
+ */
4826
+ function privacyQc(v) {
4827
+ return `# Privacy Policy
4828
+ ${updatedLine(v)}
4829
+ ${REVIEW_NOTICE}
4830
+ ${QC_REVIEW_NOTICE}
4831
+
4832
+ This is the privacy policy of ${v.businessName} ("we", "us"). It explains what personal information we collect through ${v.url || "our website"} (the "Site"), why, how it is used and shared, how long it is kept, and the rights you have over it under Quebec's Act respecting the protection of personal information in the private sector (the "Act").
4833
+
4834
+ We may update this policy. The version published on the Site is the one that applies, and we will state the date of the latest revision at the top of it.
4835
+
4836
+ **Summary.**
4837
+
4838
+ ${bullets([
4839
+ "The person in charge of the protection of personal information",
4840
+ "What we collect, and why",
4841
+ "Technologies that can identify, locate or profile you",
4842
+ "Who receives your information, and transfers outside Quebec",
4843
+ "How long we keep it",
4844
+ "Your rights, and how to exercise them"
4845
+ ])}
4846
+
4847
+ ## The Person in Charge of the Protection of Personal Information
4848
+
4849
+ ${responsible(v)}
4850
+
4851
+ ## What We Collect, and Why
4852
+
4853
+ Personal information is information that relates to a natural person and allows that person to be identified. We collect only the personal information we need for a specific purpose that we have identified, and we tell you that purpose at or before the time we collect it. We ask for your consent to use or communicate it for that purpose; where the Act requires it, your consent is requested separately from any other information, in clear and simple terms. We collect personal information when you:
4854
+
4855
+ ${bullets(v.collectionReasons)}
4856
+
4857
+ Depending on what you ask of us, this may include:
4858
+
4859
+ ${bullets(v.piiCollected)}
4860
+
4861
+ When you visit the Site, your browser also sends us certain technical information automatically:
4862
+
4863
+ ${bullets(v.techInfo)}
4864
+
4865
+ We use it to:
4866
+
4867
+ ${bullets(v.techInfoPurposes)}
4868
+
4869
+ Cookies and similar technologies on the Site may record:
4870
+
4871
+ ${bullets(v.cookieTracked)}
4872
+
4873
+ We use them to:
4874
+
4875
+ ${bullets(v.cookiePurposes)}
4876
+
4877
+ Our Cookie Notice lists them in detail.${v.cookieTool ? ` ${v.cookieTool}` : ""}
4878
+
4879
+ ${trackingSection(v)}## How We Use and Share Personal Information
4880
+
4881
+ We use personal information for the purposes for which it was collected, as described in this policy, or for another purpose to which you consent. For example, we may use it to:
4882
+
4883
+ ${bullets(v.usePurposes)}
4884
+
4885
+ ${providersSection(v)}
4886
+
4887
+ We may also communicate personal information where the Act permits or requires it without your consent, for example in response to a lawful order.
4888
+ ${transferSection(v)}
4889
+ ## How Long We Keep Personal Information
4890
+
4891
+ We keep personal information only for as long as it is needed for the purposes for which it was collected, or as the law requires. When it is no longer needed, we destroy it or anonymize it for serious and legitimate purposes, as the Act provides.${v.retention ? ` In general, we keep personal information for ${v.retention}.` : ""}
4892
+
4893
+ ## How We Protect Personal Information
4894
+
4895
+ We protect personal information with security measures appropriate to its sensitivity, the purposes for which it is used, its quantity and distribution, and the medium on which it is held. Access is limited to the people who need it to carry out these purposes. No measure is absolute, and we cannot guarantee the security of information you send us.
4896
+
4897
+ If a confidentiality incident involving your personal information presents a risk of serious injury, the Act requires us to notify the Commission d'accès à l'information du Québec and the people concerned, and to take measures to reduce the risk of harm.
4898
+
4899
+ ## Your Rights
4900
+
4901
+ Subject to the limits the Act provides, you have the right to:
4902
+
4903
+ ${bullets([
4904
+ "be informed of the personal information we hold about you, and to obtain a copy of it (access)",
4905
+ "have inaccurate, incomplete or equivocal information corrected, or information collected or held unlawfully deleted (rectification)",
4906
+ "receive your computerized personal information in a structured, commonly used technological format, or have it sent to another organization you name (portability)",
4907
+ "withdraw your consent to our use or communication of your personal information at any time, subject to legal and contractual restrictions",
4908
+ "ask us to stop disseminating your personal information, or to de-index any hyperlink attached to your name that gives access to it, where the dissemination contravenes the law or a court order",
4909
+ "be informed if a decision about you is made exclusively through automated processing of your personal information, to be told what information and main factors were used, and to have the decision reviewed by a person"
4910
+ ])}
4911
+
4912
+ To exercise any of these rights, write to the person in charge at the address below. We may ask you to verify your identity first. The Act requires us to respond to a request within 30 days of receiving it, and if we refuse it, to tell you why and what recourse you have.
4913
+ ${caslSection(v)}
4914
+ ## Complaints
4915
+
4916
+ If you are not satisfied with our response, you may file a complaint with the Commission d'accès à l'information du Québec (www.cai.gouv.qc.ca).
4917
+
4918
+ ## How to Contact Us
4919
+
4920
+ Questions, requests and complaints about this policy or your personal information can be sent to ${v.mailingAddress}.
4921
+ `;
4922
+ }
4923
+ /**
4924
+ * Law 25 requires the title and contact details of the person in charge to be
4925
+ * published. The name and title are printed only if the config states them; the
4926
+ * contact is always the resolved privacy contact. Nothing here asserts that the
4927
+ * business "designated" anyone — whether someone was is for the owner to confirm,
4928
+ * which the banner asks of them.
4929
+ */
4930
+ function responsible(v) {
4931
+ const who = [v.privacyOfficerName, v.privacyOfficerTitle && `(${v.privacyOfficerTitle})`].filter(Boolean).join(" ");
4932
+ const role = `The person in charge of the protection of personal information at ${v.businessName}`;
4933
+ return who ? `${role} is ${who}, who can be reached at ${v.mailingAddress}.` : `${role} can be reached at ${v.mailingAddress}.`;
4934
+ }
4935
+ /**
4936
+ * Section 8.1: where a technology can identify, locate or profile you, the Act
4937
+ * requires telling you and, for the profiling functions, that they be off until
4938
+ * you turn them on. We say "off until you choose" only under an opt-in consent
4939
+ * model, which is the one configuration where that is true.
4940
+ */
4941
+ function trackingSection(v) {
4942
+ if (v.trackingTechnologies.length === 0) return "";
4943
+ const assurance = v.consentModel === "opt-in" ? "None of these is activated until you choose to allow it using the consent controls on the Site, and you can change your choice at any time." : v.consentModel === "opt-out" ? "You can deactivate them at any time using the consent controls on the Site." : "You can block them through your browser settings.";
4944
+ return `### Technologies That Can Identify, Locate or Profile You
4945
+
4946
+ The Site uses technologies that can identify you, or collect information about your behaviour in order to profile you:
4947
+
4948
+ ${bullets(v.trackingTechnologies)}
4949
+
4950
+ ${assurance}
4951
+
4952
+ `;
4953
+ }
4954
+ function providersSection(v) {
4955
+ if (v.processors.length === 0) return "We do not communicate personal information to third-party service providers.";
4956
+ return `We may entrust personal information to service providers who carry out a mandate or service contract on our behalf. We provide them only the information they need, require them to use it only for that mandate and not keep it afterwards, and we remain responsible for it. They are:
4957
+
4958
+ ${bullets(v.processors.map((p) => `${p.name}, for ${p.purpose}${p.privacyUrl ? ` (privacy policy: ${p.privacyUrl})` : ""}`))}`;
4959
+ }
4960
+ /**
4961
+ * Law 25 s. 17: before communicating personal information outside Quebec, an
4962
+ * organization must assess the privacy factors and, in broad terms, that the
4963
+ * information would receive adequate protection. Two kinds of fact feed this
4964
+ * section — where information rests, and which foreign law can reach it — and
4965
+ * the assessment assurance is a third that only `impactAssessments` can supply.
4966
+ *
4967
+ * "Canada" is rewritten because for this template set "outside Quebec" includes
4968
+ * the rest of Canada, and a bare "including Canada" would read as a contradiction.
4969
+ */
4970
+ function transferSection(v) {
4971
+ const where = (s) => s.replace(/\bCanada\b/g, "elsewhere in Canada");
4972
+ const clauses = [];
4973
+ const scoped = list(v.scopedStorage.map((s) => `${where(s.country)}, in the case of ${s.scope}`));
4974
+ if (v.storageCountries) clauses.push(`Some or all of the personal information we collect may be stored or processed outside Quebec, including in ${where(v.storageCountries)}.${scoped ? ` Certain categories are also held outside Quebec even where the rest is not: ${scoped}.` : ""}`);
4975
+ else if (scoped) clauses.push(`Certain categories of the personal information we collect are stored or processed outside Quebec: ${scoped}.`);
4976
+ if (v.operatorCountries) clauses.push(`Some of our service providers are established in, or controlled from, ${where(v.operatorCountries)}, so information they handle for us may be subject to the laws of those places even when it is stored in Quebec.`);
4977
+ if (clauses.length === 0) return "";
4978
+ return `
4979
+ ### Transfers Outside Quebec
4980
+
4981
+ ${clauses.join("\n\n")}
4982
+ ${v.transferAssessment ? "\nBefore we communicate personal information outside Quebec, we carry out a privacy impact assessment that takes into account, among other things, the sensitivity of the information, the purposes for which it is used, the safeguards that apply, and the legal framework of the place where it will be held.\n" : ""}`;
4983
+ }
4984
+
4985
+ // src/legal/templates/terms.ca.ts
4590
4986
  function termsCa(v) {
4591
4987
  return `# Terms of Service
4592
4988
  ${updatedLine(v)}
@@ -4638,7 +5034,7 @@ Our handling of personal information is described in our Privacy Policy, which f
4638
5034
 
4639
5035
  ## Governing Law
4640
5036
 
4641
- These terms are governed by the laws of ${governingLaw(v)}. You agree to the exclusive jurisdiction of the courts of that jurisdiction for any dispute arising out of or in connection with these terms or the Site.
5037
+ These terms are governed by the laws of ${governingLaw$1(v)}. You agree to the exclusive jurisdiction of the courts of that jurisdiction for any dispute arising out of or in connection with these terms or the Site.
4642
5038
 
4643
5039
  ## Contact
4644
5040
 
@@ -4650,18 +5046,90 @@ If you have questions about these terms, please contact us at ${v.mailingAddress
4650
5046
  * conventional clause names both. Without a province we can only name the
4651
5047
  * country — better a broader clause than one asserting a province we guessed.
4652
5048
  */
4653
- function governingLaw(v) {
5049
+ function governingLaw$1(v) {
4654
5050
  if (v.governingLaw) return v.governingLaw;
4655
5051
  return v.governingProvince ? `${v.governingProvince} and the federal laws of ${v.governingCountry} applicable therein` : v.governingCountry;
4656
5052
  }
4657
5053
 
4658
- //#endregion
4659
- //#region src/legal/templates/index.ts
4660
- const TEMPLATES = { ca: {
4661
- privacy: privacyCa,
4662
- terms: termsCa,
4663
- cookies: cookiesCa
4664
- } };
5054
+ // src/legal/templates/terms.qc.ts
5055
+ function termsQc(v) {
5056
+ return `# Terms of Use
5057
+ ${updatedLine(v)}
5058
+ ${REVIEW_NOTICE}
5059
+ ${QC_REVIEW_NOTICE}
5060
+
5061
+ These terms govern your use of ${v.url || "this website"} (the "Site"), operated by ${v.businessName} ("we", "us"). By using the Site you accept them. If you do not accept them, please do not use the Site.
5062
+
5063
+ We may revise these terms. The version published on the Site is the one that applies, and using the Site after a revision means you accept it.
5064
+
5065
+ ## Using the Site
5066
+
5067
+ You may use the Site only for lawful purposes. You agree not to:
5068
+
5069
+ ${bullets([
5070
+ "use the Site in a way that contravenes any law or regulation that applies to you or to us",
5071
+ "try to gain unauthorized access to the Site, its servers or any system connected to it",
5072
+ "interfere with the Site or disrupt it, including by introducing malicious code",
5073
+ "collect or harvest personal information about other visitors",
5074
+ "copy, redistribute or resell any part of the Site, except as these terms allow"
5075
+ ])}
5076
+
5077
+ We may suspend or restrict access to the Site, in whole or in part, at any time.
5078
+
5079
+ ## Intellectual Property
5080
+
5081
+ Unless otherwise indicated, the content of the Site — its text, graphics, logos and layout — belongs to us or is used under licence, and is protected by copyright and other laws. You may view, download and print content for your own personal, non-commercial use. Any other use requires our prior written permission.
5082
+
5083
+ ## Information on the Site
5084
+
5085
+ We take reasonable care to keep the Site accurate and current, but we do not guarantee that its content is complete or up to date. It is provided for general information and is not advice suited to your particular situation.
5086
+
5087
+ ## Other Websites and Services
5088
+
5089
+ The Site may link to or depend on websites and services run by others. We do not control them and are not responsible for their content, availability or practices. A link is not an endorsement.
5090
+
5091
+ ## Warranty and Liability
5092
+
5093
+ The Site is provided "as is" and "as available". Subject to the next paragraph, we give no warranty that it will be uninterrupted, secure or free of errors, and we are not liable for indirect or consequential damages arising from your use of the Site.
5094
+
5095
+ **Nothing in these terms excludes or limits our liability for our intentional or gross fault, for bodily or moral injury, or for anything else that the law of Quebec does not allow us to exclude or limit.** If you are a consumer, nothing in these terms deprives you of the rights and recourses the Consumer Protection Act gives you.
5096
+
5097
+ ## Personal Information
5098
+
5099
+ How we handle personal information is described in our Privacy Policy, which is part of these terms.
5100
+
5101
+ ## Governing Law
5102
+
5103
+ These terms are governed by the laws of ${governingLaw(v)}. Any dispute arising from them or from your use of the Site is subject to the jurisdiction of the courts of that jurisdiction, subject to any mandatory rule that gives you the right to bring a proceeding elsewhere.
5104
+
5105
+ ## Contact
5106
+
5107
+ Questions about these terms can be sent to ${v.mailingAddress}.
5108
+ `;
5109
+ }
5110
+ /**
5111
+ * The address decides, as in the `ca` terms, because that is the one fact the
5112
+ * config holds — but with no province to go on this falls back to Quebec rather
5113
+ * than to the country, since these are the Quebec terms.
5114
+ */
5115
+ function governingLaw(v) {
5116
+ if (v.governingLaw) return v.governingLaw;
5117
+ return `${v.governingProvince ?? "Quebec"} and the federal laws of Canada applicable therein`;
5118
+ }
5119
+
5120
+ // src/legal/templates/index.ts
5121
+ const TEMPLATES = {
5122
+ ca: {
5123
+ privacy: privacyCa,
5124
+ terms: termsCa,
5125
+ cookies: cookiesCa
5126
+ },
5127
+ qc: {
5128
+ privacy: privacyQc,
5129
+ terms: termsQc,
5130
+ cookies: cookiesQc
5131
+ }
5132
+ };
4665
5133
  /** Stable output order, independent of which docs a given config enables. */
4666
5134
  const DOC_ORDER = [
4667
5135
  "privacy",
@@ -4675,8 +5143,7 @@ const DOC_SLUGS = {
4675
5143
  cookies: "cookie-notice"
4676
5144
  };
4677
5145
 
4678
- //#endregion
4679
- //#region src/legal/index.ts
5146
+ // src/legal/index.ts
4680
5147
  /**
4681
5148
  * Generate a site's legal documents from its config.
4682
5149
  *
@@ -4740,8 +5207,7 @@ function generateLegal(cfg, opts = {}) {
4740
5207
  return out;
4741
5208
  }
4742
5209
 
4743
- //#endregion
4744
- //#region src/generate.ts
5210
+ // src/generate.ts
4745
5211
  /**
4746
5212
  * Pure, parameterized port of the Vitops design-system generator.
4747
5213
  *
@@ -4896,13 +5362,11 @@ const buildScale = (s) => {
4896
5362
  };
4897
5363
  /**
4898
5364
  * Wrong spellings worth naming explicitly, because each is a plausible reading of
4899
- * the key it isn't. `transform`/`decoration` were documented in the schema for a
5365
+ * the key it isn't. (`transform`/`decoration` used to live here; they are aliases now, below.) They were documented in the schema for a
4900
5366
  * while and shipped title-case navigation to production; `letter-spacing` and
4901
5367
  * `font-*` are the natural guesses for keys we chose to shorten.
4902
5368
  */
4903
5369
  const ALSO_MEANT = {
4904
- transform: "text-transform",
4905
- decoration: "text-decoration",
4906
5370
  "letter-spacing": "tracking",
4907
5371
  wrap: "text-wrap",
4908
5372
  "font-family": "family",
@@ -4911,6 +5375,33 @@ const ALSO_MEANT = {
4911
5375
  "font-style": "style",
4912
5376
  lineHeight: "line-height"
4913
5377
  };
5378
+ /**
5379
+ * Short spellings accepted for the two keys named after a full CSS property.
5380
+ * `transform`/`decoration` read naturally beside `tracking`/`weight` and were the
5381
+ * documented names for a while, so they are accepted and normalised to the
5382
+ * canonical `text-transform`/`text-decoration` (which wins if both are present)
5383
+ * rather than warned about and dropped.
5384
+ */
5385
+ const TYPO_ALIASES = {
5386
+ transform: "text-transform",
5387
+ decoration: "text-decoration"
5388
+ };
5389
+ const normalizeTypographyKeys = (typography) => {
5390
+ if (!typography?.roles) return typography;
5391
+ const roles = Object.fromEntries(Object.entries(typography.roles).map(([role, spec]) => {
5392
+ const out = {};
5393
+ for (const [k, v] of Object.entries(spec)) {
5394
+ const canonical = TYPO_ALIASES[k];
5395
+ if (canonical == null) out[k] = v;
5396
+ else if (!(canonical in spec)) out[canonical] = v;
5397
+ }
5398
+ return [role, out];
5399
+ }));
5400
+ return {
5401
+ ...typography,
5402
+ roles
5403
+ };
5404
+ };
4914
5405
  const TYPO_IDENTITY = {
4915
5406
  "font-style": "normal",
4916
5407
  "letter-spacing": "normal",
@@ -4946,12 +5437,12 @@ function build(ds, format, assetsDir, buildOpts = {}) {
4946
5437
  const hueName = roleHue(spec);
4947
5438
  const hue = expandedPalette[hueName];
4948
5439
  if (hue == null) throw new Error(`role "${role}" references unknown palette hue "${hueName}"`);
4949
- scaleRoles.push(functionalRole(role, hueName, hue, roleKind(spec)));
5440
+ scaleRoles.push(functionalRole(role, hueName, hue, roleKind(spec), roleSolidOptions(spec)));
4950
5441
  }
4951
5442
  const surfaceRole = scaleRoles.find((r) => r.role === "surface" && "bg" in r.light);
4952
5443
  if (!surfaceRole && roleMap["surface"] != null) console.warn("[vitops] colors.roles.surface is chromatic, so no --color-bg-surface exists — skipping --surface-glass, --overlay and .glass. Declare it as { \"hue\": \"…\", \"kind\": \"surface\" } if you meant a surface role.");
4953
5444
  const focusRole = scaleRoles.find((r) => r.role === "ui-primary") ?? scaleRoles[0];
4954
- for (const w of ladderWarnings(expandedPalette)) console.warn(`[vitops] ${w}`);
5445
+ for (const w of ladderWarnings(expandedPalette, ds.colors.palette)) console.warn(`[vitops] ${w}`);
4955
5446
  {
4956
5447
  const errors = monotonicityErrors(expandedPalette);
4957
5448
  if (errors.length) throw new Error(errors.join("\n"));
@@ -4971,12 +5462,32 @@ function build(ds, format, assetsDir, buildOpts = {}) {
4971
5462
  light: planeKeys.flatMap((k) => surfaceRole.light[k] ? [resolve(surfaceRole.light[k])] : []),
4972
5463
  dark: planeKeys.flatMap((k) => surfaceRole.dark[k] ? [resolve(surfaceRole.dark[k])] : [])
4973
5464
  } : void 0;
4974
- const failures = scaleRoles.flatMap((fr) => checkContrast(fr, numericOf, surfaceBg));
5465
+ const failures = scaleRoles.flatMap((fr) => checkContrast(fr, numericOf, surfaceBg, buildOpts.darkOnly ? ["dark"] : void 0));
4975
5466
  if (failures.length) throw new Error(`colour contrast contract failed (${failures.length} pairing${failures.length === 1 ? "" : "s"}):\n ${failures.join("\n ")}`);
4976
5467
  }
5468
+ if (buildOpts.darkOnly) scaleRoles.forEach((fr, i) => {
5469
+ scaleRoles[i] = {
5470
+ ...fr,
5471
+ light: fr.dark
5472
+ };
5473
+ });
4977
5474
  const patterns = ds.patterns;
4978
- const shadows = ds.shadows;
4979
- const typography = ds.typography;
5475
+ const shadowDrops = {};
5476
+ const shadows = ds.shadows ? Object.fromEntries(Object.entries(ds.shadows).map(([name, v]) => {
5477
+ if (typeof v === "string") {
5478
+ shadowDrops[name] = {
5479
+ token: `--shadow-${name}`,
5480
+ value: v
5481
+ };
5482
+ return [name, v];
5483
+ }
5484
+ if (v.drop != null) shadowDrops[name] = {
5485
+ token: `--shadow-${name}-drop`,
5486
+ value: v.drop
5487
+ };
5488
+ return [name, v.box ?? v.drop];
5489
+ })) : void 0;
5490
+ const typography = normalizeTypographyKeys(ds.typography);
4980
5491
  const UTILITIES = (ds.colors.utilities ?? [
4981
5492
  "bg",
4982
5493
  "text",
@@ -4997,7 +5508,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
4997
5508
  if (BRICKS) generated["color.css"] = "/* Colours provided by Bricks (palette import generates tokens + utilities). */\n" + NON_PALETTE_BG;
4998
5509
  else {
4999
5510
  let css = `/* GENERATED from design-system.json — do not edit by hand. */\n:root {\n`;
5000
- css += ` color-scheme: light;\n`;
5511
+ css += ` color-scheme: ${buildOpts.darkOnly ? "dark" : "light"};\n`;
5001
5512
  css += ` /* Hue scales (numeric steps, 50 = tinted near-white … 950 = tinted near-black) */\n`;
5002
5513
  for (const [name, hue] of Object.entries(expandedPalette)) for (const [n, hex] of Object.entries(hue.numeric)) css += ` --color-${name}-${n}: ${hex};\n`;
5003
5514
  css += `\n /* Role tokens (the public API) */\n`;
@@ -5015,7 +5526,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5015
5526
  {
5016
5527
  let fb = "";
5017
5528
  for (const fr of scaleRoles) for (const [t, val] of Object.entries(fr.dark)) if (fr.light[t] !== val) fb += ` ${tokenVar(fr.role, t)}: ${val};\n`;
5018
- if (surfaceRole) fb += ` --overlay: color-mix(in oklch, var(--color-${surfaceRole.hue}-950) 60%, transparent);\n`;
5529
+ if (surfaceRole && !buildOpts.darkOnly) fb += ` --overlay: color-mix(in oklch, var(--color-${surfaceRole.hue}-950) 60%, transparent);\n`;
5019
5530
  if (fb) {
5020
5531
  css += `/* Functional role tokens (dark) */\n`;
5021
5532
  css += `${DARK_SEL} {\n color-scheme: dark;\n${fb}}\n\n`;
@@ -5037,9 +5548,16 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5037
5548
  let sh = `/* GENERATED drop-shadow tokens + utilities — do not edit by hand. */\n`;
5038
5549
  if (entries.length) {
5039
5550
  sh += `:root {\n`;
5040
- for (const [name, value] of entries) sh += ` --shadow-${name}: ${value};\n`;
5551
+ for (const [name, value] of entries) {
5552
+ sh += ` --shadow-${name}: ${value};\n`;
5553
+ const drop = shadowDrops[name];
5554
+ if (drop && drop.token !== `--shadow-${name}`) sh += ` ${drop.token}: ${drop.value};\n`;
5555
+ }
5041
5556
  sh += `}\n\n`;
5042
- for (const [name] of entries) sh += `.drop-shadow-${name} { filter: drop-shadow(var(--shadow-${name})); }\n`;
5557
+ for (const [name] of entries) {
5558
+ const drop = shadowDrops[name];
5559
+ if (drop) sh += `.drop-shadow-${name} { filter: drop-shadow(var(${drop.token})); }\n`;
5560
+ }
5043
5561
  }
5044
5562
  generated["shadows.css"] = sh;
5045
5563
  }
@@ -5090,6 +5608,8 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5090
5608
  sp += `}\n`;
5091
5609
  }
5092
5610
  }
5611
+ const layoutVars = layoutDecls(ds.layout);
5612
+ if (Object.keys(layoutVars).length) sp += `\n/* layout tokens (from \`layout\`) */\n:root {\n${decls(layoutVars)}\n}\n`;
5093
5613
  generated["spacing.css"] = sp;
5094
5614
  }
5095
5615
  let twPatternTokensCss = "";
@@ -5137,7 +5657,8 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5137
5657
  }
5138
5658
  if (typeof spec.scale === "number") body.push(`scale: ${spec.scale};`);
5139
5659
  if (typeof spec.lift === "string") body.push(`translate: 0 calc(-1 * ${spec.lift});`);
5140
- if (typeof spec.shadow === "string" && shadows?.[spec.shadow]) body.push(`filter: drop-shadow(var(--shadow-${spec.shadow}));`);
5660
+ if (typeof spec.shadow === "string" && shadowDrops[spec.shadow]) body.push(`filter: drop-shadow(var(${shadowDrops[spec.shadow]?.token}));`);
5661
+ else if (typeof spec.shadow === "string" && shadows?.[spec.shadow]) console.warn(`[vitops] pattern state "shadow: ${spec.shadow}" skipped: shadows.${spec.shadow} has no \`drop\` form`);
5141
5662
  else if (spec.shadow === true) body.push(`box-shadow: var(--lift-shadow, 0 8px 20px -6px rgb(0 0 0 / 0.25));`);
5142
5663
  if (spec.ring === true) {
5143
5664
  const ringColor = role ? `var(--color-bg-${role}-solid)` : `var(--color-border-focus)`;
@@ -5373,6 +5894,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5373
5894
  key
5374
5895
  };
5375
5896
  for (const role of Object.keys(typoRoles)) for (const [key, [, sfx]] of Object.entries(TYPO_KEYMAP)) reverseIndex[`--${role}-${sfx}`] = `typography.roles.${role}.${key}`;
5897
+ for (const [path, token] of Object.entries(LAYOUT_TOKENS)) reverseIndex[token] = `layout.${path}`;
5376
5898
  const pDefaults = patterns?.defaults ?? {};
5377
5899
  const pRadii = patterns?.radii ?? {};
5378
5900
  const pGroups = patterns?.groups ?? {};
@@ -5429,6 +5951,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5429
5951
  items
5430
5952
  },
5431
5953
  shadows: shadows ?? {},
5954
+ layout: ds.layout ?? {},
5432
5955
  reverseIndex
5433
5956
  }, null, 2);
5434
5957
  })(),
@@ -5446,7 +5969,8 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5446
5969
  patternsCss: pat,
5447
5970
  assetsDir,
5448
5971
  utilities: UTILITIES,
5449
- systemColorScheme: buildOpts.systemColorScheme === true
5972
+ systemColorScheme: buildOpts.systemColorScheme === true,
5973
+ darkOnly: buildOpts.darkOnly === true
5450
5974
  }),
5451
5975
  designMd: format !== "design" ? "" : emitDesignMd({
5452
5976
  ds,
@@ -5479,6 +6003,12 @@ function emitTailwind(ctx) {
5479
6003
  theme.push(`\n /* Container breakpoints → @sm:/@md:/@lg:/@xl: */`);
5480
6004
  for (const [name, val] of BREAKPOINTS) theme.push(` --container-${name}: ${val};`);
5481
6005
  parts.push(`@theme {\n${theme.join("\n")}\n}\n`);
6006
+ const dropTokens = Object.entries(ds.shadows ?? {}).flatMap(([n, v]) => typeof v !== "string" && v.drop != null ? [` --shadow-${n}-drop: ${v.drop};`] : []);
6007
+ if (dropTokens.length) parts.push(`/* drop-shadow() forms of split shadows */\n:root {\n${dropTokens.join("\n")}\n}\n`);
6008
+ {
6009
+ const layoutVars = layoutDecls(ds.layout);
6010
+ if (Object.keys(layoutVars).length) parts.push(`/* Layout tokens (from \`layout\`) */\n:root {\n${decls(layoutVars)}\n}\n`);
6011
+ }
5482
6012
  if (spaceSteps.length) parts.push(`/* Fluid space scale — var(--space-<name>) or p-(--space-<name>) */\n:root {\n` + spaceSteps.map((s) => ` --space-${s.name}: ${s.value};`).join("\n") + `\n}\n`);
5483
6013
  if (scaleRoles.length) {
5484
6014
  let fn = "";
@@ -5490,6 +6020,7 @@ function emitTailwind(ctx) {
5490
6020
  fn += ` --surface-glass: color-mix(in oklch, var(--color-bg-surface) 72%, transparent);\n`;
5491
6021
  fn += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 45%, transparent);\n`;
5492
6022
  }
6023
+ if (ctx.darkOnly) fn = ` color-scheme: dark;\n${fn}`;
5493
6024
  parts.push(`/* Role tokens (the public API) */\n:root {\n${fn}}\n`);
5494
6025
  }
5495
6026
  parts.push(`/* ── Framework tokens (consumed by the inlined components) ── */\n${twTypeTokensCss}\n${twPatternTokensCss}`);
@@ -5497,7 +6028,7 @@ function emitTailwind(ctx) {
5497
6028
  for (const fr of scaleRoles) for (const [t, val] of Object.entries(fr.dark)) if (fr.light[t] !== val) darkBlock += ` ${tokenVar(fr.role, t)}: ${val};\n`;
5498
6029
  {
5499
6030
  const surface = scaleRoles.find((r) => r.role === "surface");
5500
- if (surface) darkBlock += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 60%, transparent);\n`;
6031
+ if (surface && !ctx.darkOnly) darkBlock += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 60%, transparent);\n`;
5501
6032
  }
5502
6033
  if (darkBlock) {
5503
6034
  parts.push(`/* Functional role tokens (dark) */\n${DARK_SEL} {\n color-scheme: dark;\n${darkBlock}}\n`);
@@ -5982,7 +6513,10 @@ async function generate(options) {
5982
6513
  ...options.siteEnv != null ? { siteEnv: options.siteEnv } : {}
5983
6514
  });
5984
6515
  const config = options.site != null ? loadConfigFile(options.site, options.siteEnv) : embedded;
5985
- const built = build(ds, format, assetsDir, { systemColorScheme: options.systemColorScheme ?? config?.designSystem?.defaultColorScheme === "system" });
6516
+ const built = build(ds, format, assetsDir, {
6517
+ systemColorScheme: options.systemColorScheme ?? config?.designSystem?.defaultColorScheme === "system",
6518
+ darkOnly: config?.designSystem?.defaultColorScheme === "dark"
6519
+ });
5986
6520
  mkdirSync(outDir, { recursive: true });
5987
6521
  const written = [];
5988
6522
  const put = (rel, content) => {
@@ -5995,6 +6529,7 @@ async function generate(options) {
5995
6529
  else if (format === "tailwind") {
5996
6530
  put("tailwind.css", built.tailwind);
5997
6531
  put("tokens.json", built.tokensJson);
6532
+ put("design-manifest.json", built.designManifest);
5998
6533
  } else if (format === "css") {
5999
6534
  const { code } = bundleCss(built.generated, assetsDir, true);
6000
6535
  put("styles.css", code);
@@ -6057,8 +6592,7 @@ function loadConfigFile(input, siteEnv) {
6057
6592
  return resolveConfig(raw, siteEnv);
6058
6593
  }
6059
6594
 
6060
- //#endregion
6061
- //#region src/robots.ts
6595
+ // src/robots.ts
6062
6596
  /**
6063
6597
  * `robots.txt`, generated from the same fact the indexing gate and the
6064
6598
  * `<meta name="robots">` tag already read.
@@ -6119,8 +6653,7 @@ function renderRobotsTxt(cfg, options = {}) {
6119
6653
  return lines.join("\n") + "\n";
6120
6654
  }
6121
6655
 
6122
- //#endregion
6123
- //#region src/index.ts
6656
+ // src/index.ts
6124
6657
  /**
6125
6658
  * A minimal but complete starter design system, used by `vitops init`. It
6126
6659
  * validates against the schema and generates real output in every format, so a
@@ -6428,6 +6961,5 @@ function defaultSiteConfig(opts) {
6428
6961
  };
6429
6962
  }
6430
6963
 
6431
- //#endregion
6432
- export { AD_PROVIDERS, BASE_HOOK, BIGIN_DATA_CENTERS, CONFIG_SCHEMA_LOCAL_PATH, CONFIG_SCHEMA_URL, CONFIG_VERSION, CONTRAST, CRM_PROVIDERS, ConfigSchema, DARK_SEL, DOC_ORDER, DOC_SLUGS, DesignSystemSchema, JURISDICTIONS, JURISDICTION_COUNTRIES, KNOWN_PROCESSORS, NUMERIC_STEPS, PLAN_CAPABILITIES, REQUIRED_ROLES, ROLE_TOKEN_KEYS, SCHEMA_LOCAL_PATH, SCHEMA_URL, SiteFontSchema, TEMPLATES, TIERS, TIER_NAMES, TW_CLASH, TYPO_KEYMAP, buildIconSprite, buildReverseIndex, checkContrast, configJsonSchema, deepMerge, defaultConfig, defaultSiteConfig, derivePolicyVars, designSystemPath, detectProcessorKeys, enabledDocs, expandPalette, experimentDimensionWarnings, formatConfig, functionalRole, generate, generateDocs, generateLegal, getAt, isConfig, jsonSchema, loadIconSvg, mergePatch, movedTokens, parseMarkdown, patchToOverrideCss, processorsMissingLocation, renderInlineMarkdown, renderMarkdown, renderNodes, renderRobotsTxt, resolveConfig, resolveInput, resolvePrivacyContact, resolveProcessors, resolveTheme, roleColorUtilities, roleHue, roleKind, schemaTreeNodes, setAt, spriteId, stripNulls, tierPatterns, tierTags, toContentNodes, toHtmlFragment, toPortableText, tokenClass, tokenVar, validate, validateColorContract, validateConfig };
6964
+ export { AD_PROVIDERS, BASE_HOOK, BIGIN_DATA_CENTERS, CONFIG_SCHEMA_LOCAL_PATH, CONFIG_SCHEMA_URL, CONFIG_VERSION, CONTRAST, CRM_PROVIDERS, ConfigSchema, DARK_SEL, DOC_ORDER, DOC_SLUGS, DesignSystemSchema, JURISDICTIONS, JURISDICTION_COUNTRIES, KNOWN_PROCESSORS, NUMERIC_STEPS, PLAN_CAPABILITIES, REQUIRED_ROLES, ROLE_TOKEN_KEYS, SCHEMA_LOCAL_PATH, SCHEMA_URL, SiteFontSchema, TEMPLATES, TIERS, TIER_NAMES, TW_CLASH, TYPO_KEYMAP, buildIconSprite, buildReverseIndex, checkContrast, configJsonSchema, deepMerge, defaultConfig, defaultSiteConfig, derivePolicyVars, designSystemPath, detectProcessorKeys, enabledDocs, expandPalette, experimentDimensionWarnings, formatConfig, functionalRole, generate, generateDocs, generateLegal, getAt, isConfig, jsonSchema, loadIconSvg, mergePatch, movedTokens, parseMarkdown, patchToOverrideCss, processorsMissingLocation, renderInlineMarkdown, renderMarkdown, renderNodes, renderRobotsTxt, resolveConfig, resolveInput, resolvePrivacyContact, resolveProcessors, resolveTheme, roleColorUtilities, roleHue, roleKind, roleSolidOptions, schemaTreeNodes, setAt, spriteId, stripNulls, tierPatterns, tierTags, toContentNodes, toHtmlFragment, toPortableText, tokenClass, tokenVar, validate, validateColorContract, validateConfig };
6433
6965
  //# sourceMappingURL=index.mjs.map