@getvitops/generator 8.2.0 → 8.3.0-alpha.1

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 (39) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/assets/css/index.css +8 -0
  3. package/assets/css/patterns/callout.css +39 -0
  4. package/assets/css/patterns/embed.css +39 -0
  5. package/assets/css/patterns/forms.css +14 -0
  6. package/assets/css/patterns/logos.css +45 -0
  7. package/assets/css/patterns/section.css +61 -0
  8. package/assets/css/patterns/sitenav.css +262 -0
  9. package/assets/css/patterns/stats.css +61 -0
  10. package/assets/css/patterns/steps.css +98 -0
  11. package/assets/css/patterns/tone.css +38 -0
  12. package/assets/css/patterns/widget-area.css +22 -0
  13. package/assets/js/elements.js +3 -3
  14. package/config.schema.json +126 -10
  15. package/dist/{config-BfcWKH-5.mjs → config-D5xuz8--.mjs} +79 -15
  16. package/dist/config-D5xuz8--.mjs.map +1 -0
  17. package/dist/config.d.mts +77 -18
  18. package/dist/config.d.mts.map +1 -1
  19. package/dist/config.mjs +1 -1
  20. package/dist/index.d.mts +40 -3
  21. package/dist/index.d.mts.map +1 -1
  22. package/dist/index.mjs +574 -42
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{schema-BV2n2phj.d.mts → schema-C-P9dF9F.d.mts} +49 -3
  25. package/dist/schema-C-P9dF9F.d.mts.map +1 -0
  26. package/dist/{tokens-C1RJwFeG.mjs → tokens-CaIGQQLk.mjs} +56 -11
  27. package/dist/tokens-CaIGQQLk.mjs.map +1 -0
  28. package/dist/{tokens-BzBbTeTi.d.mts → tokens-DH5cpvKd.d.mts} +24 -7
  29. package/dist/tokens-DH5cpvKd.d.mts.map +1 -0
  30. package/dist/tokens.d.mts +1 -1
  31. package/dist/tokens.mjs +1 -1
  32. package/package.json +4 -4
  33. package/schema.json +95 -5
  34. package/dist/config-BfcWKH-5.mjs.map +0 -1
  35. package/dist/config-Bhg3Xv3D.mjs +0 -1774
  36. package/dist/config-Bhg3Xv3D.mjs.map +0 -1
  37. package/dist/schema-BV2n2phj.d.mts.map +0 -1
  38. package/dist/tokens-BzBbTeTi.d.mts.map +0 -1
  39. package/dist/tokens-C1RJwFeG.mjs.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-C1RJwFeG.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-BfcWKH-5.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";
@@ -45,6 +45,7 @@ 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: {
@@ -82,6 +83,7 @@ const TIERS = {
82
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."
83
84
  },
84
85
  entries: {
86
+ emdash: ["vitops.entries"],
85
87
  css: c([
86
88
  "entries__table",
87
89
  "entries__scroll",
@@ -92,8 +94,9 @@ const TIERS = {
92
94
  registered: true,
93
95
  adds: "parses heading + `<dl>` pairs into a table above a breakpoint"
94
96
  },
97
+ astro: a("Entries", "wc"),
95
98
  bricks: "vitops-entries",
96
- 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."
97
100
  },
98
101
  copy: {
99
102
  emdash: ["vitops.copyButton"],
@@ -112,6 +115,7 @@ const TIERS = {
112
115
  use: "`<wc-copy>` around the content and a `[data-copy]` button."
113
116
  },
114
117
  dismissable: {
118
+ emdash: ["vitops.banner"],
115
119
  css: c(["banner__close", "notification__close"], "patterns/banner.css"),
116
120
  wc: {
117
121
  tag: "wc-dismissable",
@@ -127,6 +131,7 @@ const TIERS = {
127
131
  "splitter__panel",
128
132
  "splitter__handle"
129
133
  ], "patterns/splitter.css"),
134
+ emdashExempt: "app UI (resizable panes), not editorial content; an editor does not drag-resize a published page",
130
135
  wc: {
131
136
  tag: "wc-split-panel",
132
137
  registered: true,
@@ -165,7 +170,86 @@ const TIERS = {
165
170
  astro: a("Marquee", "wc"),
166
171
  use: "`<Marquee>` around one `.marquee__content`, or `<wc-marquee>` by hand. Scrolls via CSS alone; JS only removes the seam."
167
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
+ "block-anchor"
193
+ ], "patterns/section.css"),
194
+ 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()`."
195
+ },
196
+ steps: {
197
+ emdash: ["vitops.steps"],
198
+ css: c([
199
+ "steps",
200
+ "steps__item",
201
+ "steps__body",
202
+ "steps__title",
203
+ "steps__text",
204
+ "steps--row"
205
+ ], "patterns/steps.css"),
206
+ 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."
207
+ },
208
+ stats: {
209
+ emdash: ["vitops.stats"],
210
+ css: c([
211
+ "stats",
212
+ "stats__item",
213
+ "stats__figure",
214
+ "stats__label",
215
+ "stats__group",
216
+ "stats__delta",
217
+ "stats--tiles"
218
+ ], "patterns/stats.css"),
219
+ 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."
220
+ },
221
+ callout: {
222
+ emdash: ["vitops.callout"],
223
+ css: c([
224
+ "callout",
225
+ "callout__body",
226
+ "callout__title",
227
+ "callout__text",
228
+ "callout__actions",
229
+ "callout--center"
230
+ ], "patterns/callout.css"),
231
+ 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."
232
+ },
233
+ logos: {
234
+ emdash: ["vitops.logos"],
235
+ css: c([
236
+ "logos",
237
+ "logos__item",
238
+ "logos--grayscale"
239
+ ], "patterns/logos.css"),
240
+ use: "`<ul class=\"logos\">` of `.logos__item` images (alt text = the name). A still grid; `marquee` is the scrolling version."
241
+ },
242
+ embed: {
243
+ emdash: ["vitops.embed"],
244
+ css: c([
245
+ "embed",
246
+ "embed__frame",
247
+ "embed__placeholder"
248
+ ], "patterns/embed.css"),
249
+ 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`."
250
+ },
168
251
  counter: {
252
+ emdash: ["vitops.stats"],
169
253
  css: c([
170
254
  "counter",
171
255
  "counter__value",
@@ -180,6 +264,7 @@ const TIERS = {
180
264
  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."
181
265
  },
182
266
  "multi-field": {
267
+ 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",
183
268
  css: c([
184
269
  "form-group",
185
270
  "input-group",
@@ -193,7 +278,18 @@ const TIERS = {
193
278
  bricks: "vitops-multi-field",
194
279
  use: "`<wc-multi-field>` around a field template. One row submits fine with no JS."
195
280
  },
281
+ turnstile: {
282
+ emdash: ["vitops.form"],
283
+ css: c(["form-group"], "patterns/forms.css"),
284
+ wc: {
285
+ tag: "wc-turnstile",
286
+ registered: true,
287
+ adds: "loads Cloudflare Turnstile on first interaction with the form and holds a submit until its token arrives"
288
+ },
289
+ 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."
290
+ },
196
291
  "color-scheme-toggle": {
292
+ emdashExempt: "site chrome (header/footer), not page content; belongs in the nav/layout",
197
293
  css: c([], "patterns/icon.css"),
198
294
  wc: {
199
295
  tag: "wc-color-scheme-toggle",
@@ -204,6 +300,7 @@ const TIERS = {
204
300
  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."
205
301
  },
206
302
  consent: {
303
+ emdashExempt: "site chrome: the layout renders `<CookieConsent />` once; an editor-placed copy would duplicate the gate",
207
304
  css: c([
208
305
  "consent",
209
306
  "consent__body",
@@ -223,6 +320,7 @@ const TIERS = {
223
320
  use: "`<CookieConsent />`. Its own Lit-free bundle so a site needing consent does not download a rendering framework."
224
321
  },
225
322
  "theme-editor": {
323
+ emdashExempt: "tooling, not content; it is mounted in the EmDash admin by `designSystemEditor`, never in a page",
226
324
  css: c([
227
325
  "ed-panel",
228
326
  "ed-launch",
@@ -315,6 +413,7 @@ const TIERS = {
315
413
  use: "`<NavShell>` with a `nav` slot, plus `<NavShellToggle>` if the toggle must live outside the shell."
316
414
  },
317
415
  subgrid: {
416
+ emdash: ["vitops.pricing"],
318
417
  css: c([
319
418
  "subgrid",
320
419
  "subgrid-card",
@@ -368,10 +467,14 @@ const TIERS = {
368
467
  css: c([
369
468
  "sitenav",
370
469
  "sitenav__link",
371
- "sitenav--drawer-start"
470
+ "sitenav--drawer-start",
471
+ "navlist",
472
+ "navlist--columns"
372
473
  ], "patterns/sitenav.css"),
474
+ astro: [...a("SiteNav", "css"), ...a("SiteFooter", "css")],
475
+ emdash: ["vitops.nav"],
373
476
  bricks: "vitops-sitenav",
374
- use: "`.sitenav` with `--bp-*` and a drawer direction. `<details>` + Invoker Commands, no JS."
477
+ 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."
375
478
  },
376
479
  "split-link": {
377
480
  css: c([
@@ -403,9 +506,19 @@ const TIERS = {
403
506
  use: "`<a>` gets it at zero specificity; `.link` for other tags. `.stretched-link` makes one link cover its positioned ancestor (whole-card click, no JS — but the card text stops being selectable); `.raised` lifts content back above that overlay."
404
507
  },
405
508
  badge: {
509
+ emdash: ["vitops.badges"],
406
510
  css: c(["badge", "badge-indicator"], "patterns/tag.css", true),
407
511
  use: "`.badge` — a STATIC label. Use `tag` if it is dismissable or editable."
408
512
  },
513
+ "widget-area": {
514
+ emdash: ["vitops.widgetArea"],
515
+ css: c([
516
+ "widget-area",
517
+ "widget__title",
518
+ "widget__menu"
519
+ ], "patterns/widget-area.css"),
520
+ use: "Editor-managed regions: `<VitopsWidgetArea name=\"footer-columns\" />` (`@getvitops/emdash/astro`) renders an area from Widgets in the admin — titled groups, a menu as a labelled `<nav>`, content as Portable Text. In a page, the `vitops.widgetArea` block places one. An area holding a `core:*` component widget falls back to EmDash's own renderer, and an area cannot hold another. Prerendered pages show an edit after the next deploy."
521
+ },
409
522
  tag: {
410
523
  css: c([
411
524
  "tag",
@@ -468,7 +581,8 @@ const TIERS = {
468
581
  "review__source"
469
582
  ], "patterns/review.css", true),
470
583
  astro: a("ReviewCard", "css"),
471
- 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`."
584
+ emdash: ["vitops.reviews"],
585
+ 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."
472
586
  },
473
587
  tabs: {
474
588
  css: c(["tabs"], void 0, true),
@@ -2294,6 +2408,7 @@ function renderComponentsConcept(ds) {
2294
2408
  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");
2295
2409
  const bricks = tierPatterns("bricks").map(({ name, entry }) => `| ${code(entry.bricks)} | ${code(name)} |`).join("\n");
2296
2410
  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");
2411
+ const exempt = TIER_NAMES.filter((n) => TIERS[n].emdashExempt).map((n) => `- ${code(TIERS[n].wc?.tag ?? n)} — ${TIERS[n].emdashExempt}`).join("\n");
2297
2412
  const css = tierPatterns("css").map(({ name, entry }) => {
2298
2413
  const c = entry.css;
2299
2414
  const config = !c.generated ? "—" : declared.has(name) ? "declared" : "**absent from this config — no CSS emitted**";
@@ -2388,13 +2503,21 @@ ${bricks}
2388
2503
  ## EmDash blocks
2389
2504
 
2390
2505
  Portable Text block ${code("_type")}s from ${code("@getvitops/emdash")}. Repeating patterns are
2391
- repeater fields. A block cannot contain other blocks — a section holding a card and a button is a
2392
- ${code("blocks")} field in the EmDash schema, not a Portable Text block.
2506
+ repeater fields. A Portable Text block cannot contain other blocks, so a pattern that must hold
2507
+ them is a schema-level block TYPE with a Portable Text body instead — ${code("vitops_section")},
2508
+ from ${code("vitopsBlockTypes()")}. Its body accepts every ${code("vitops.*")} block below.
2393
2509
 
2394
2510
  | Block | Pattern | Renders via |
2395
2511
  | --- | --- | --- |
2396
2512
  ${emdash}
2397
2513
 
2514
+ ### Web components with no block, on purpose
2515
+
2516
+ Every shipped ${code("wc-*")} element either has a block above or a stated reason here
2517
+ (${code("emdashExempt")} in the manifest, enforced by ${code("tiers.test.ts")}):
2518
+
2519
+ ${exempt}
2520
+
2398
2521
  ## CSS
2399
2522
 
2400
2523
  ${code("patterns.items")} patterns get the full token cascade — ${code("base")} declarations,
@@ -3983,7 +4106,7 @@ function derivePolicyVars(cfg) {
3983
4106
  const fields = formFields(site);
3984
4107
  const hasForms = fields.length > 0;
3985
4108
  const hasAnalytics = Object.values(site.analytics ?? {}).some((v) => typeof v === "string" ? v.length > 0 : !!v);
3986
- processors.filter((p) => p.cookies?.length);
4109
+ const cookieProcessors = processors.filter((p) => p.cookies?.length);
3987
4110
  const consent = legal?.cookieConsent;
3988
4111
  const contact = resolvePrivacyContact(cfg);
3989
4112
  const businessName = org?.legalName ?? (org?.name != null ? t(org.name, locale, locale) : void 0) ?? "we";
@@ -4096,6 +4219,12 @@ function derivePolicyVars(cfg) {
4096
4219
  const operatorCountries = list(uniq(processors.map((p) => p.operatorCountry)).filter((c) => foreign(c) && !named.has(norm(c))));
4097
4220
  const countries = list(uniq(processors.flatMap((p) => [...(p.storage ?? []).map((s) => s.country), p.operatorCountry])));
4098
4221
  const address = contact?.address ?? org?.address;
4222
+ const trackingTechnologies = uniq([
4223
+ ...cookieProcessors.map((p) => `${p.name} (${p.purpose})`),
4224
+ hasSessionReplay ? "session recording of how you interact with a page" : void 0,
4225
+ hasAdTracking ? "advertising attribution (the `_ac` cookie described in our Cookie Notice)" : void 0,
4226
+ ...Object.keys(site.ads ?? {}).map((a) => `${a} advertising measurement`)
4227
+ ]);
4099
4228
  return {
4100
4229
  businessName,
4101
4230
  url: site.domains?.canonical ?? "",
@@ -4127,7 +4256,12 @@ function derivePolicyVars(cfg) {
4127
4256
  operatorCountries: operatorCountries || void 0,
4128
4257
  countries: countries || void 0,
4129
4258
  consentModel: consent?.enabled ? consent.type ?? "opt-in" : "none",
4130
- cookieCategories: consent?.categories ?? []
4259
+ cookieCategories: consent?.categories ?? [],
4260
+ privacyOfficerName: contact?.name,
4261
+ privacyOfficerTitle: privacy?.privacyOfficerTitle,
4262
+ transferAssessment: privacy?.impactAssessments === true,
4263
+ trackingTechnologies,
4264
+ newsletterSignup: fields.some((f) => /newsletter|subscribe|mailing/i.test(f.name))
4131
4265
  };
4132
4266
  }
4133
4267
 
@@ -4352,6 +4486,18 @@ function toPortableText(blocks, prefix) {
4352
4486
  * renders as a callout in HTML and as a warning banner in EmDash.
4353
4487
  */
4354
4488
  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.";
4489
+ /**
4490
+ * The second banner the `qc` documents carry, after `REVIEW_NOTICE`.
4491
+ *
4492
+ * Quebec adds obligations a generic review prompt does not name, and each is a
4493
+ * thing this generator cannot check: that a person in charge of privacy has been
4494
+ * designated and is published, that a privacy impact assessment precedes any
4495
+ * communication outside Quebec, that consent mechanics match the Act, and — easy
4496
+ * to miss — that content directed at Quebec consumers is available in French. It
4497
+ * is a second blockquote (a second `vitops.banner` in Portable Text) so it cannot
4498
+ * be skimmed past as part of the first.
4499
+ */
4500
+ 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.";
4355
4501
  /** A markdown bullet list. Empty in, empty out — never a stray `-`. */
4356
4502
  function bullets(items) {
4357
4503
  return items.map((item) => `- ${item}`).join("\n");
@@ -4396,7 +4542,7 @@ Cookies used on this Site may track:
4396
4542
 
4397
4543
  ${bullets(v.cookieTracked)}
4398
4544
 
4399
- ${providersSection$1(v)}${categoriesSection(v)}
4545
+ ${providersSection$2(v)}${categoriesSection(v)}
4400
4546
  ## Your Choices
4401
4547
 
4402
4548
  ${bullets(v.cookieOptOutOptions)}
@@ -4418,7 +4564,7 @@ If you have questions about our use of cookies, please contact us at ${v.mailing
4418
4564
  * attribution with a cookieless analytics provider sets `_ac` and no third-party
4419
4565
  * cookie at all, and the old wording would have declared it cookie-free.
4420
4566
  */
4421
- function providersSection$1(v) {
4567
+ function providersSection$2(v) {
4422
4568
  const setting = v.processors.filter((p) => p.cookies?.length);
4423
4569
  const cookieless = v.processors.filter((p) => p.cookies?.length === 0);
4424
4570
  const own = v.firstPartyCookies;
@@ -4447,6 +4593,68 @@ ${bullets(v.cookieCategories)}
4447
4593
  `;
4448
4594
  }
4449
4595
 
4596
+ // src/legal/templates/cookies.qc.ts
4597
+ function cookiesQc(v) {
4598
+ return `# Cookie Notice
4599
+ ${updatedLine(v)}
4600
+ ${REVIEW_NOTICE}
4601
+ ${QC_REVIEW_NOTICE}
4602
+
4603
+ 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.
4604
+
4605
+ ## What Cookies Are
4606
+
4607
+ 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.
4608
+
4609
+ ## What We Use Them For
4610
+
4611
+ ${bullets(v.cookiePurposes)}
4612
+
4613
+ They may record:
4614
+
4615
+ ${bullets(v.cookieTracked)}
4616
+
4617
+ ${consentSection(v)}${providersSection$2(v)}${categoriesSection(v)}
4618
+ ## Your Choices
4619
+
4620
+ ${bullets(v.cookieOptOutOptions)}
4621
+
4622
+ 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.
4623
+
4624
+ ## Contact
4625
+
4626
+ 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).
4627
+ `;
4628
+ }
4629
+ /**
4630
+ * Only when something on the Site can actually identify or profile a visitor —
4631
+ * a notice that announced profiling technology the config does not enable would
4632
+ * be a false statement in the other direction.
4633
+ */
4634
+ function consentSection(v) {
4635
+ if (v.trackingTechnologies.length === 0) return "";
4636
+ 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.";
4637
+ return `## Technologies That Can Identify, Locate or Profile You
4638
+
4639
+ Some technologies on this Site can identify you, or collect information about your behaviour in order to profile you:
4640
+
4641
+ ${bullets(v.trackingTechnologies)}
4642
+
4643
+ ${assurance}
4644
+
4645
+ `;
4646
+ }
4647
+
4648
+ // src/legal/templates/casl.ts
4649
+ function caslSection(v) {
4650
+ if (!v.newsletterSignup) return "";
4651
+ return `
4652
+ ### Commercial Electronic Messages
4653
+
4654
+ 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}.
4655
+ `;
4656
+ }
4657
+
4450
4658
  // src/legal/templates/privacy.ca.ts
4451
4659
  /**
4452
4660
  * Privacy policy — Canada (PIPEDA).
@@ -4527,14 +4735,14 @@ For example, we may use your personal information to:
4527
4735
 
4528
4736
  ${bullets(v.usePurposes)}
4529
4737
 
4530
- ${providersSection(v)}
4738
+ ${providersSection$1(v)}
4531
4739
 
4532
4740
  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.
4533
4741
 
4534
4742
  ### Your Choices
4535
4743
 
4536
4744
  ${bullets(v.optOutOptions)}
4537
-
4745
+ ${caslSection(v)}
4538
4746
  ### How We Protect Personal Information
4539
4747
 
4540
4748
  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}.
@@ -4542,7 +4750,7 @@ The security of your personal information is important to us. We protect your pe
4542
4750
  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}.` : ""}
4543
4751
 
4544
4752
  It is important to understand that no security measures are absolute. We cannot guarantee the safety of any information you provide to us.
4545
- ${transferSection(v)}
4753
+ ${transferSection$1(v)}
4546
4754
  ### Accessing and Correcting Your Personal Information
4547
4755
 
4548
4756
  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.
@@ -4558,7 +4766,7 @@ If you have any questions regarding this Privacy Policy, to submit a complaint,
4558
4766
  * "we may share with service providers" without naming them is the vaguest and
4559
4767
  * least useful sentence a policy can contain.
4560
4768
  */
4561
- function providersSection(v) {
4769
+ function providersSection$1(v) {
4562
4770
  if (v.processors.length === 0) return "We do not transfer personal information to third party service providers.";
4563
4771
  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:
4564
4772
 
@@ -4577,7 +4785,7 @@ We remain responsible for personal information in the possession of our service
4577
4785
  *
4578
4786
  * The closer is shared: it is true of whichever clauses appear.
4579
4787
  */
4580
- function transferSection(v) {
4788
+ function transferSection$1(v) {
4581
4789
  const clauses = [storageClause(v), operatorClause(v)].filter(Boolean);
4582
4790
  if (clauses.length === 0) return "";
4583
4791
  return `
@@ -4607,6 +4815,184 @@ function operatorClause(v) {
4607
4815
  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.`;
4608
4816
  }
4609
4817
 
4818
+ // src/legal/templates/privacy.qc.ts
4819
+ /**
4820
+ * Privacy policy — Quebec (Act respecting the protection of personal information
4821
+ * in the private sector, as amended by Law 25).
4822
+ *
4823
+ * Written fresh rather than adapted from `privacy.ca.ts`: the Quebec regime asks
4824
+ * for things PIPEDA does not — a published person in charge, a stated position on
4825
+ * technologies that identify or profile, a privacy impact assessment before
4826
+ * information leaves Quebec, portability, de-indexing, the right to be told about
4827
+ * automated decisions — and frames "abroad" as outside Quebec rather than outside
4828
+ * Canada. Reusing the `ca` prose would assert PIPEDA's framing under Law 25's name.
4829
+ *
4830
+ * Same rule as everywhere: the config records facts, this file owns prose, and a
4831
+ * fact the config cannot know is never asserted. So the assessment assurance
4832
+ * renders only when `impactAssessments` is attested, the profiling assurance only
4833
+ * under an opt-in consent model, and the person in charge is named only when the
4834
+ * config names one. What remains is the statute's own wording of the visitor's
4835
+ * rights, which holds regardless of how the business operates.
4836
+ */
4837
+ function privacyQc(v) {
4838
+ return `# Privacy Policy
4839
+ ${updatedLine(v)}
4840
+ ${REVIEW_NOTICE}
4841
+ ${QC_REVIEW_NOTICE}
4842
+
4843
+ 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").
4844
+
4845
+ 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.
4846
+
4847
+ **Summary.**
4848
+
4849
+ ${bullets([
4850
+ "The person in charge of the protection of personal information",
4851
+ "What we collect, and why",
4852
+ "Technologies that can identify, locate or profile you",
4853
+ "Who receives your information, and transfers outside Quebec",
4854
+ "How long we keep it",
4855
+ "Your rights, and how to exercise them"
4856
+ ])}
4857
+
4858
+ ## The Person in Charge of the Protection of Personal Information
4859
+
4860
+ ${responsible(v)}
4861
+
4862
+ ## What We Collect, and Why
4863
+
4864
+ 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:
4865
+
4866
+ ${bullets(v.collectionReasons)}
4867
+
4868
+ Depending on what you ask of us, this may include:
4869
+
4870
+ ${bullets(v.piiCollected)}
4871
+
4872
+ When you visit the Site, your browser also sends us certain technical information automatically:
4873
+
4874
+ ${bullets(v.techInfo)}
4875
+
4876
+ We use it to:
4877
+
4878
+ ${bullets(v.techInfoPurposes)}
4879
+
4880
+ Cookies and similar technologies on the Site may record:
4881
+
4882
+ ${bullets(v.cookieTracked)}
4883
+
4884
+ We use them to:
4885
+
4886
+ ${bullets(v.cookiePurposes)}
4887
+
4888
+ Our Cookie Notice lists them in detail.${v.cookieTool ? ` ${v.cookieTool}` : ""}
4889
+
4890
+ ${trackingSection(v)}## How We Use and Share Personal Information
4891
+
4892
+ 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:
4893
+
4894
+ ${bullets(v.usePurposes)}
4895
+
4896
+ ${providersSection(v)}
4897
+
4898
+ We may also communicate personal information where the Act permits or requires it without your consent, for example in response to a lawful order.
4899
+ ${transferSection(v)}
4900
+ ## How Long We Keep Personal Information
4901
+
4902
+ 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}.` : ""}
4903
+
4904
+ ## How We Protect Personal Information
4905
+
4906
+ 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.
4907
+
4908
+ 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.
4909
+
4910
+ ## Your Rights
4911
+
4912
+ Subject to the limits the Act provides, you have the right to:
4913
+
4914
+ ${bullets([
4915
+ "be informed of the personal information we hold about you, and to obtain a copy of it (access)",
4916
+ "have inaccurate, incomplete or equivocal information corrected, or information collected or held unlawfully deleted (rectification)",
4917
+ "receive your computerized personal information in a structured, commonly used technological format, or have it sent to another organization you name (portability)",
4918
+ "withdraw your consent to our use or communication of your personal information at any time, subject to legal and contractual restrictions",
4919
+ "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",
4920
+ "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"
4921
+ ])}
4922
+
4923
+ 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.
4924
+ ${caslSection(v)}
4925
+ ## Complaints
4926
+
4927
+ 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).
4928
+
4929
+ ## How to Contact Us
4930
+
4931
+ Questions, requests and complaints about this policy or your personal information can be sent to ${v.mailingAddress}.
4932
+ `;
4933
+ }
4934
+ /**
4935
+ * Law 25 requires the title and contact details of the person in charge to be
4936
+ * published. The name and title are printed only if the config states them; the
4937
+ * contact is always the resolved privacy contact. Nothing here asserts that the
4938
+ * business "designated" anyone — whether someone was is for the owner to confirm,
4939
+ * which the banner asks of them.
4940
+ */
4941
+ function responsible(v) {
4942
+ const who = [v.privacyOfficerName, v.privacyOfficerTitle && `(${v.privacyOfficerTitle})`].filter(Boolean).join(" ");
4943
+ const role = `The person in charge of the protection of personal information at ${v.businessName}`;
4944
+ return who ? `${role} is ${who}, who can be reached at ${v.mailingAddress}.` : `${role} can be reached at ${v.mailingAddress}.`;
4945
+ }
4946
+ /**
4947
+ * Section 8.1: where a technology can identify, locate or profile you, the Act
4948
+ * requires telling you and, for the profiling functions, that they be off until
4949
+ * you turn them on. We say "off until you choose" only under an opt-in consent
4950
+ * model, which is the one configuration where that is true.
4951
+ */
4952
+ function trackingSection(v) {
4953
+ if (v.trackingTechnologies.length === 0) return "";
4954
+ 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.";
4955
+ return `### Technologies That Can Identify, Locate or Profile You
4956
+
4957
+ The Site uses technologies that can identify you, or collect information about your behaviour in order to profile you:
4958
+
4959
+ ${bullets(v.trackingTechnologies)}
4960
+
4961
+ ${assurance}
4962
+
4963
+ `;
4964
+ }
4965
+ function providersSection(v) {
4966
+ if (v.processors.length === 0) return "We do not communicate personal information to third-party service providers.";
4967
+ 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:
4968
+
4969
+ ${bullets(v.processors.map((p) => `${p.name}, for ${p.purpose}${p.privacyUrl ? ` (privacy policy: ${p.privacyUrl})` : ""}`))}`;
4970
+ }
4971
+ /**
4972
+ * Law 25 s. 17: before communicating personal information outside Quebec, an
4973
+ * organization must assess the privacy factors and, in broad terms, that the
4974
+ * information would receive adequate protection. Two kinds of fact feed this
4975
+ * section — where information rests, and which foreign law can reach it — and
4976
+ * the assessment assurance is a third that only `impactAssessments` can supply.
4977
+ *
4978
+ * "Canada" is rewritten because for this template set "outside Quebec" includes
4979
+ * the rest of Canada, and a bare "including Canada" would read as a contradiction.
4980
+ */
4981
+ function transferSection(v) {
4982
+ const where = (s) => s.replace(/\bCanada\b/g, "elsewhere in Canada");
4983
+ const clauses = [];
4984
+ const scoped = list(v.scopedStorage.map((s) => `${where(s.country)}, in the case of ${s.scope}`));
4985
+ 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}.` : ""}`);
4986
+ else if (scoped) clauses.push(`Certain categories of the personal information we collect are stored or processed outside Quebec: ${scoped}.`);
4987
+ 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.`);
4988
+ if (clauses.length === 0) return "";
4989
+ return `
4990
+ ### Transfers Outside Quebec
4991
+
4992
+ ${clauses.join("\n\n")}
4993
+ ${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" : ""}`;
4994
+ }
4995
+
4610
4996
  // src/legal/templates/terms.ca.ts
4611
4997
  function termsCa(v) {
4612
4998
  return `# Terms of Service
@@ -4659,7 +5045,7 @@ Our handling of personal information is described in our Privacy Policy, which f
4659
5045
 
4660
5046
  ## Governing Law
4661
5047
 
4662
- 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.
5048
+ 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.
4663
5049
 
4664
5050
  ## Contact
4665
5051
 
@@ -4671,17 +5057,90 @@ If you have questions about these terms, please contact us at ${v.mailingAddress
4671
5057
  * conventional clause names both. Without a province we can only name the
4672
5058
  * country — better a broader clause than one asserting a province we guessed.
4673
5059
  */
4674
- function governingLaw(v) {
5060
+ function governingLaw$1(v) {
4675
5061
  if (v.governingLaw) return v.governingLaw;
4676
5062
  return v.governingProvince ? `${v.governingProvince} and the federal laws of ${v.governingCountry} applicable therein` : v.governingCountry;
4677
5063
  }
4678
5064
 
5065
+ // src/legal/templates/terms.qc.ts
5066
+ function termsQc(v) {
5067
+ return `# Terms of Use
5068
+ ${updatedLine(v)}
5069
+ ${REVIEW_NOTICE}
5070
+ ${QC_REVIEW_NOTICE}
5071
+
5072
+ 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.
5073
+
5074
+ 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.
5075
+
5076
+ ## Using the Site
5077
+
5078
+ You may use the Site only for lawful purposes. You agree not to:
5079
+
5080
+ ${bullets([
5081
+ "use the Site in a way that contravenes any law or regulation that applies to you or to us",
5082
+ "try to gain unauthorized access to the Site, its servers or any system connected to it",
5083
+ "interfere with the Site or disrupt it, including by introducing malicious code",
5084
+ "collect or harvest personal information about other visitors",
5085
+ "copy, redistribute or resell any part of the Site, except as these terms allow"
5086
+ ])}
5087
+
5088
+ We may suspend or restrict access to the Site, in whole or in part, at any time.
5089
+
5090
+ ## Intellectual Property
5091
+
5092
+ 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.
5093
+
5094
+ ## Information on the Site
5095
+
5096
+ 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.
5097
+
5098
+ ## Other Websites and Services
5099
+
5100
+ 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.
5101
+
5102
+ ## Warranty and Liability
5103
+
5104
+ 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.
5105
+
5106
+ **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.
5107
+
5108
+ ## Personal Information
5109
+
5110
+ How we handle personal information is described in our Privacy Policy, which is part of these terms.
5111
+
5112
+ ## Governing Law
5113
+
5114
+ 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.
5115
+
5116
+ ## Contact
5117
+
5118
+ Questions about these terms can be sent to ${v.mailingAddress}.
5119
+ `;
5120
+ }
5121
+ /**
5122
+ * The address decides, as in the `ca` terms, because that is the one fact the
5123
+ * config holds — but with no province to go on this falls back to Quebec rather
5124
+ * than to the country, since these are the Quebec terms.
5125
+ */
5126
+ function governingLaw(v) {
5127
+ if (v.governingLaw) return v.governingLaw;
5128
+ return `${v.governingProvince ?? "Quebec"} and the federal laws of Canada applicable therein`;
5129
+ }
5130
+
4679
5131
  // src/legal/templates/index.ts
4680
- const TEMPLATES = { ca: {
4681
- privacy: privacyCa,
4682
- terms: termsCa,
4683
- cookies: cookiesCa
4684
- } };
5132
+ const TEMPLATES = {
5133
+ ca: {
5134
+ privacy: privacyCa,
5135
+ terms: termsCa,
5136
+ cookies: cookiesCa
5137
+ },
5138
+ qc: {
5139
+ privacy: privacyQc,
5140
+ terms: termsQc,
5141
+ cookies: cookiesQc
5142
+ }
5143
+ };
4685
5144
  /** Stable output order, independent of which docs a given config enables. */
4686
5145
  const DOC_ORDER = [
4687
5146
  "privacy",
@@ -4914,13 +5373,11 @@ const buildScale = (s) => {
4914
5373
  };
4915
5374
  /**
4916
5375
  * Wrong spellings worth naming explicitly, because each is a plausible reading of
4917
- * the key it isn't. `transform`/`decoration` were documented in the schema for a
5376
+ * the key it isn't. (`transform`/`decoration` used to live here; they are aliases now, below.) They were documented in the schema for a
4918
5377
  * while and shipped title-case navigation to production; `letter-spacing` and
4919
5378
  * `font-*` are the natural guesses for keys we chose to shorten.
4920
5379
  */
4921
5380
  const ALSO_MEANT = {
4922
- transform: "text-transform",
4923
- decoration: "text-decoration",
4924
5381
  "letter-spacing": "tracking",
4925
5382
  wrap: "text-wrap",
4926
5383
  "font-family": "family",
@@ -4929,6 +5386,33 @@ const ALSO_MEANT = {
4929
5386
  "font-style": "style",
4930
5387
  lineHeight: "line-height"
4931
5388
  };
5389
+ /**
5390
+ * Short spellings accepted for the two keys named after a full CSS property.
5391
+ * `transform`/`decoration` read naturally beside `tracking`/`weight` and were the
5392
+ * documented names for a while, so they are accepted and normalised to the
5393
+ * canonical `text-transform`/`text-decoration` (which wins if both are present)
5394
+ * rather than warned about and dropped.
5395
+ */
5396
+ const TYPO_ALIASES = {
5397
+ transform: "text-transform",
5398
+ decoration: "text-decoration"
5399
+ };
5400
+ const normalizeTypographyKeys = (typography) => {
5401
+ if (!typography?.roles) return typography;
5402
+ const roles = Object.fromEntries(Object.entries(typography.roles).map(([role, spec]) => {
5403
+ const out = {};
5404
+ for (const [k, v] of Object.entries(spec)) {
5405
+ const canonical = TYPO_ALIASES[k];
5406
+ if (canonical == null) out[k] = v;
5407
+ else if (!(canonical in spec)) out[canonical] = v;
5408
+ }
5409
+ return [role, out];
5410
+ }));
5411
+ return {
5412
+ ...typography,
5413
+ roles
5414
+ };
5415
+ };
4932
5416
  const TYPO_IDENTITY = {
4933
5417
  "font-style": "normal",
4934
5418
  "letter-spacing": "normal",
@@ -4964,12 +5448,12 @@ function build(ds, format, assetsDir, buildOpts = {}) {
4964
5448
  const hueName = roleHue(spec);
4965
5449
  const hue = expandedPalette[hueName];
4966
5450
  if (hue == null) throw new Error(`role "${role}" references unknown palette hue "${hueName}"`);
4967
- scaleRoles.push(functionalRole(role, hueName, hue, roleKind(spec)));
5451
+ scaleRoles.push(functionalRole(role, hueName, hue, roleKind(spec), roleSolidOptions(spec)));
4968
5452
  }
4969
5453
  const surfaceRole = scaleRoles.find((r) => r.role === "surface" && "bg" in r.light);
4970
5454
  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.");
4971
5455
  const focusRole = scaleRoles.find((r) => r.role === "ui-primary") ?? scaleRoles[0];
4972
- for (const w of ladderWarnings(expandedPalette)) console.warn(`[vitops] ${w}`);
5456
+ for (const w of ladderWarnings(expandedPalette, ds.colors.palette)) console.warn(`[vitops] ${w}`);
4973
5457
  {
4974
5458
  const errors = monotonicityErrors(expandedPalette);
4975
5459
  if (errors.length) throw new Error(errors.join("\n"));
@@ -4989,12 +5473,32 @@ function build(ds, format, assetsDir, buildOpts = {}) {
4989
5473
  light: planeKeys.flatMap((k) => surfaceRole.light[k] ? [resolve(surfaceRole.light[k])] : []),
4990
5474
  dark: planeKeys.flatMap((k) => surfaceRole.dark[k] ? [resolve(surfaceRole.dark[k])] : [])
4991
5475
  } : void 0;
4992
- const failures = scaleRoles.flatMap((fr) => checkContrast(fr, numericOf, surfaceBg));
5476
+ const failures = scaleRoles.flatMap((fr) => checkContrast(fr, numericOf, surfaceBg, buildOpts.darkOnly ? ["dark"] : void 0));
4993
5477
  if (failures.length) throw new Error(`colour contrast contract failed (${failures.length} pairing${failures.length === 1 ? "" : "s"}):\n ${failures.join("\n ")}`);
4994
5478
  }
5479
+ if (buildOpts.darkOnly) scaleRoles.forEach((fr, i) => {
5480
+ scaleRoles[i] = {
5481
+ ...fr,
5482
+ light: fr.dark
5483
+ };
5484
+ });
4995
5485
  const patterns = ds.patterns;
4996
- const shadows = ds.shadows;
4997
- const typography = ds.typography;
5486
+ const shadowDrops = {};
5487
+ const shadows = ds.shadows ? Object.fromEntries(Object.entries(ds.shadows).map(([name, v]) => {
5488
+ if (typeof v === "string") {
5489
+ shadowDrops[name] = {
5490
+ token: `--shadow-${name}`,
5491
+ value: v
5492
+ };
5493
+ return [name, v];
5494
+ }
5495
+ if (v.drop != null) shadowDrops[name] = {
5496
+ token: `--shadow-${name}-drop`,
5497
+ value: v.drop
5498
+ };
5499
+ return [name, v.box ?? v.drop];
5500
+ })) : void 0;
5501
+ const typography = normalizeTypographyKeys(ds.typography);
4998
5502
  const UTILITIES = (ds.colors.utilities ?? [
4999
5503
  "bg",
5000
5504
  "text",
@@ -5015,7 +5519,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5015
5519
  if (BRICKS) generated["color.css"] = "/* Colours provided by Bricks (palette import generates tokens + utilities). */\n" + NON_PALETTE_BG;
5016
5520
  else {
5017
5521
  let css = `/* GENERATED from design-system.json — do not edit by hand. */\n:root {\n`;
5018
- css += ` color-scheme: light;\n`;
5522
+ css += ` color-scheme: ${buildOpts.darkOnly ? "dark" : "light"};\n`;
5019
5523
  css += ` /* Hue scales (numeric steps, 50 = tinted near-white … 950 = tinted near-black) */\n`;
5020
5524
  for (const [name, hue] of Object.entries(expandedPalette)) for (const [n, hex] of Object.entries(hue.numeric)) css += ` --color-${name}-${n}: ${hex};\n`;
5021
5525
  css += `\n /* Role tokens (the public API) */\n`;
@@ -5033,7 +5537,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5033
5537
  {
5034
5538
  let fb = "";
5035
5539
  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`;
5036
- if (surfaceRole) fb += ` --overlay: color-mix(in oklch, var(--color-${surfaceRole.hue}-950) 60%, transparent);\n`;
5540
+ if (surfaceRole && !buildOpts.darkOnly) fb += ` --overlay: color-mix(in oklch, var(--color-${surfaceRole.hue}-950) 60%, transparent);\n`;
5037
5541
  if (fb) {
5038
5542
  css += `/* Functional role tokens (dark) */\n`;
5039
5543
  css += `${DARK_SEL} {\n color-scheme: dark;\n${fb}}\n\n`;
@@ -5055,9 +5559,16 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5055
5559
  let sh = `/* GENERATED drop-shadow tokens + utilities — do not edit by hand. */\n`;
5056
5560
  if (entries.length) {
5057
5561
  sh += `:root {\n`;
5058
- for (const [name, value] of entries) sh += ` --shadow-${name}: ${value};\n`;
5562
+ for (const [name, value] of entries) {
5563
+ sh += ` --shadow-${name}: ${value};\n`;
5564
+ const drop = shadowDrops[name];
5565
+ if (drop && drop.token !== `--shadow-${name}`) sh += ` ${drop.token}: ${drop.value};\n`;
5566
+ }
5059
5567
  sh += `}\n\n`;
5060
- for (const [name] of entries) sh += `.drop-shadow-${name} { filter: drop-shadow(var(--shadow-${name})); }\n`;
5568
+ for (const [name] of entries) {
5569
+ const drop = shadowDrops[name];
5570
+ if (drop) sh += `.drop-shadow-${name} { filter: drop-shadow(var(${drop.token})); }\n`;
5571
+ }
5061
5572
  }
5062
5573
  generated["shadows.css"] = sh;
5063
5574
  }
@@ -5108,6 +5619,8 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5108
5619
  sp += `}\n`;
5109
5620
  }
5110
5621
  }
5622
+ const layoutVars = layoutDecls(ds.layout);
5623
+ if (Object.keys(layoutVars).length) sp += `\n/* layout tokens (from \`layout\`) */\n:root {\n${decls(layoutVars)}\n}\n`;
5111
5624
  generated["spacing.css"] = sp;
5112
5625
  }
5113
5626
  let twPatternTokensCss = "";
@@ -5155,7 +5668,8 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5155
5668
  }
5156
5669
  if (typeof spec.scale === "number") body.push(`scale: ${spec.scale};`);
5157
5670
  if (typeof spec.lift === "string") body.push(`translate: 0 calc(-1 * ${spec.lift});`);
5158
- if (typeof spec.shadow === "string" && shadows?.[spec.shadow]) body.push(`filter: drop-shadow(var(--shadow-${spec.shadow}));`);
5671
+ if (typeof spec.shadow === "string" && shadowDrops[spec.shadow]) body.push(`filter: drop-shadow(var(${shadowDrops[spec.shadow]?.token}));`);
5672
+ 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`);
5159
5673
  else if (spec.shadow === true) body.push(`box-shadow: var(--lift-shadow, 0 8px 20px -6px rgb(0 0 0 / 0.25));`);
5160
5674
  if (spec.ring === true) {
5161
5675
  const ringColor = role ? `var(--color-bg-${role}-solid)` : `var(--color-border-focus)`;
@@ -5391,6 +5905,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5391
5905
  key
5392
5906
  };
5393
5907
  for (const role of Object.keys(typoRoles)) for (const [key, [, sfx]] of Object.entries(TYPO_KEYMAP)) reverseIndex[`--${role}-${sfx}`] = `typography.roles.${role}.${key}`;
5908
+ for (const [path, token] of Object.entries(LAYOUT_TOKENS)) reverseIndex[token] = `layout.${path}`;
5394
5909
  const pDefaults = patterns?.defaults ?? {};
5395
5910
  const pRadii = patterns?.radii ?? {};
5396
5911
  const pGroups = patterns?.groups ?? {};
@@ -5447,6 +5962,7 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5447
5962
  items
5448
5963
  },
5449
5964
  shadows: shadows ?? {},
5965
+ layout: ds.layout ?? {},
5450
5966
  reverseIndex
5451
5967
  }, null, 2);
5452
5968
  })(),
@@ -5464,7 +5980,9 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5464
5980
  patternsCss: pat,
5465
5981
  assetsDir,
5466
5982
  utilities: UTILITIES,
5467
- systemColorScheme: buildOpts.systemColorScheme === true
5983
+ systemColorScheme: buildOpts.systemColorScheme === true,
5984
+ sources: buildOpts.sources ?? [],
5985
+ darkOnly: buildOpts.darkOnly === true
5468
5986
  }),
5469
5987
  designMd: format !== "design" ? "" : emitDesignMd({
5470
5988
  ds,
@@ -5484,7 +6002,9 @@ function emitTailwind(ctx) {
5484
6002
  const cssDir = join(assetsDir, "css");
5485
6003
  const nl = (arr) => arr.filter(Boolean).join("\n");
5486
6004
  const parts = ["/* GENERATED for Tailwind v4 (Astro) — do not edit by hand.\n Class reference: npx vitops docs classes · All topics: npx vitops docs\n Fonts below are STACKS ONLY — this file loads no webfonts. Load them with\n Astro's fonts: config and point the token at the family's cssVariable. */"];
5487
- parts.push(`@import "tailwindcss";\n`);
6005
+ parts.push(`@import "tailwindcss";`);
6006
+ for (const src of ctx.sources) parts.push(`@source ${JSON.stringify(src)};`);
6007
+ parts.push("");
5488
6008
  const theme = [];
5489
6009
  theme.push(` /* Hue scales → bg-<hue>-<step> text-* border-* (numeric, 50…950) */`);
5490
6010
  for (const [name, hue] of Object.entries(expandedPalette)) for (const [n, hex] of Object.entries(hue.numeric)) theme.push(` --color-${name}-${n}: ${hex};`);
@@ -5497,6 +6017,12 @@ function emitTailwind(ctx) {
5497
6017
  theme.push(`\n /* Container breakpoints → @sm:/@md:/@lg:/@xl: */`);
5498
6018
  for (const [name, val] of BREAKPOINTS) theme.push(` --container-${name}: ${val};`);
5499
6019
  parts.push(`@theme {\n${theme.join("\n")}\n}\n`);
6020
+ const dropTokens = Object.entries(ds.shadows ?? {}).flatMap(([n, v]) => typeof v !== "string" && v.drop != null ? [` --shadow-${n}-drop: ${v.drop};`] : []);
6021
+ if (dropTokens.length) parts.push(`/* drop-shadow() forms of split shadows */\n:root {\n${dropTokens.join("\n")}\n}\n`);
6022
+ {
6023
+ const layoutVars = layoutDecls(ds.layout);
6024
+ if (Object.keys(layoutVars).length) parts.push(`/* Layout tokens (from \`layout\`) */\n:root {\n${decls(layoutVars)}\n}\n`);
6025
+ }
5500
6026
  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`);
5501
6027
  if (scaleRoles.length) {
5502
6028
  let fn = "";
@@ -5508,6 +6034,7 @@ function emitTailwind(ctx) {
5508
6034
  fn += ` --surface-glass: color-mix(in oklch, var(--color-bg-surface) 72%, transparent);\n`;
5509
6035
  fn += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 45%, transparent);\n`;
5510
6036
  }
6037
+ if (ctx.darkOnly) fn = ` color-scheme: dark;\n${fn}`;
5511
6038
  parts.push(`/* Role tokens (the public API) */\n:root {\n${fn}}\n`);
5512
6039
  }
5513
6040
  parts.push(`/* ── Framework tokens (consumed by the inlined components) ── */\n${twTypeTokensCss}\n${twPatternTokensCss}`);
@@ -5515,7 +6042,7 @@ function emitTailwind(ctx) {
5515
6042
  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`;
5516
6043
  {
5517
6044
  const surface = scaleRoles.find((r) => r.role === "surface");
5518
- if (surface) darkBlock += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 60%, transparent);\n`;
6045
+ if (surface && !ctx.darkOnly) darkBlock += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 60%, transparent);\n`;
5519
6046
  }
5520
6047
  if (darkBlock) {
5521
6048
  parts.push(`/* Functional role tokens (dark) */\n${DARK_SEL} {\n color-scheme: dark;\n${darkBlock}}\n`);
@@ -6000,7 +6527,11 @@ async function generate(options) {
6000
6527
  ...options.siteEnv != null ? { siteEnv: options.siteEnv } : {}
6001
6528
  });
6002
6529
  const config = options.site != null ? loadConfigFile(options.site, options.siteEnv) : embedded;
6003
- const built = build(ds, format, assetsDir, { systemColorScheme: options.systemColorScheme ?? config?.designSystem?.defaultColorScheme === "system" });
6530
+ const built = build(ds, format, assetsDir, {
6531
+ systemColorScheme: options.systemColorScheme ?? config?.designSystem?.defaultColorScheme === "system",
6532
+ darkOnly: config?.designSystem?.defaultColorScheme === "dark",
6533
+ ...options.sources?.length ? { sources: options.sources } : {}
6534
+ });
6004
6535
  mkdirSync(outDir, { recursive: true });
6005
6536
  const written = [];
6006
6537
  const put = (rel, content) => {
@@ -6013,6 +6544,7 @@ async function generate(options) {
6013
6544
  else if (format === "tailwind") {
6014
6545
  put("tailwind.css", built.tailwind);
6015
6546
  put("tokens.json", built.tokensJson);
6547
+ put("design-manifest.json", built.designManifest);
6016
6548
  } else if (format === "css") {
6017
6549
  const { code } = bundleCss(built.generated, assetsDir, true);
6018
6550
  put("styles.css", code);
@@ -6444,5 +6976,5 @@ function defaultSiteConfig(opts) {
6444
6976
  };
6445
6977
  }
6446
6978
 
6447
- 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 };
6979
+ 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 };
6448
6980
  //# sourceMappingURL=index.mjs.map