@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
@@ -68,6 +68,10 @@
68
68
  "type": "string"
69
69
  },
70
70
  "description": "Step → colour overrides (hex or oklch()) pinned VERBATIM at those steps. Every other step takes its lightness from the shared ladder, with chroma and hue interpolated between the anchors — so an anchor is reproduced exactly and is the only step allowed off the ladder. An explicit anchor overrides the seed at that step; two anchors that resolve to the same step are an error."
71
+ },
72
+ "acknowledgeOffLadder": {
73
+ "type": "boolean",
74
+ "description": "Acknowledge that this hue deliberately pins colours off the shared lightness ladder (typically to match an existing brand palette exactly). Silences the per-step \"off the ladder\" build warnings for THIS hue only; the monotonicity and contrast checks still run."
71
75
  }
72
76
  },
73
77
  "required": [
@@ -98,6 +102,10 @@
98
102
  }
99
103
  ],
100
104
  "description": "Fixed brand kit: authored tones placed verbatim at their nearest steps plus tinted off-white/off-black endpoints; no interpolation. Either an ordered light → dark array or a step → colour map."
105
+ },
106
+ "acknowledgeOffLadder": {
107
+ "type": "boolean",
108
+ "description": "Acknowledge that this hue deliberately pins colours off the shared lightness ladder (typically to match an existing brand palette exactly). Silences the per-step \"off the ladder\" build warnings for THIS hue only; the monotonicity and contrast checks still run."
101
109
  }
102
110
  },
103
111
  "required": [
@@ -135,6 +143,21 @@
135
143
  "chromatic"
136
144
  ],
137
145
  "description": "`surface` — a page/panel colour: gets a bare `bg-<role>` plus the full emphasis range and text scale. `chromatic` (default) — a signal colour: tints and solids only, no bare `bg-<role>`."
146
+ },
147
+ "solid": {
148
+ "description": "Ramp step (100…900) the role's solid family starts from; default 600. `bg-<role>-solid` is this step and `-solid-bold` / `-solid-x-bold` walk one and two rungs DARKER, so `solid: 400` gives 400 → 500 → 600. Use it when the brand colour is bright and lives lower on the ramp (a yellow at 400) instead of reaching for raw `bg-<hue>-<step>` classes. Like the default family it is the same in light and dark. The contrast check still runs on the pair this produces.",
149
+ "type": [
150
+ "number",
151
+ "string"
152
+ ]
153
+ },
154
+ "foreground": {
155
+ "type": "string",
156
+ "enum": [
157
+ "light",
158
+ "dark"
159
+ ],
160
+ "description": "Polarity of `text-on-<role>`, the foreground for a filled surface. Default is whichever of near-white / near-black has more contrast on the solid. Set `\"dark\"` so a bright fill carries dark text (or `\"light\"` for the reverse). The build still fails if the forced pair is below APCA Lc 60."
138
161
  }
139
162
  },
140
163
  "required": [
@@ -164,13 +187,31 @@
164
187
  "additionalProperties": false
165
188
  },
166
189
  "shadows": {
167
- "description": "Named shadows → `--shadow-<name>` tokens and `.drop-shadow-<name>` utilities. Values are shadow parameter lists (offset/blur/colour). Each token feeds two consumers with different grammars — `box-shadow` (pattern geometry, via the `--ds-*` group aliases) and `filter: drop-shadow(…)` (the utilities and the `shadow:` state shortcut) — so values must stay in the intersection: **one layer, no spread radius, no `inset`**. `drop-shadow()` rejects all three, and rejecting them invalidates the whole filter, so the shadow vanishes rather than degrading.",
190
+ "description": "Named shadows → `--shadow-<name>` tokens and `.drop-shadow-<name>` utilities. A value is either a string (below) or `{ \"box\"?: string, \"drop\"?: string }` for a shadow that needs the two grammars to differ: `box` is what `--shadow-<name>` holds and what `box-shadow` consumers (pattern geometry, `--ds-*`) read, so it MAY use spread, `inset` and several layers; `drop` is emitted as `--shadow-<name>-drop` and is what `.drop-shadow-<name>` and the `shadow:` state shortcut read, so it must obey the drop-shadow() limits. Omit `drop` and no drop-shadow utility is emitted for that name; omit `box` and `--shadow-<name>` takes the `drop` value. The plain string form keeps working and feeds both consumers from one value. For that string form: each token feeds two consumers with different grammars — `box-shadow` (pattern geometry, via the `--ds-*` group aliases) and `filter: drop-shadow(…)` (the utilities and the `shadow:` state shortcut) — so values must stay in the intersection: **one layer, no spread radius, no `inset`**. `drop-shadow()` rejects all three, and rejecting them invalidates the whole filter, so the shadow vanishes rather than degrading.",
168
191
  "type": "object",
169
192
  "propertyNames": {
170
193
  "type": "string"
171
194
  },
172
195
  "additionalProperties": {
173
- "type": "string"
196
+ "anyOf": [
197
+ {
198
+ "type": "string"
199
+ },
200
+ {
201
+ "type": "object",
202
+ "properties": {
203
+ "box": {
204
+ "type": "string",
205
+ "description": "`box-shadow` value: spread, `inset` and multiple layers allowed."
206
+ },
207
+ "drop": {
208
+ "type": "string",
209
+ "description": "`drop-shadow()` value: one layer, at most three lengths, no `inset`."
210
+ }
211
+ },
212
+ "additionalProperties": false
213
+ }
214
+ ]
174
215
  }
175
216
  },
176
217
  "fonts": {
@@ -184,7 +225,7 @@
184
225
  }
185
226
  },
186
227
  "typeScale": {
187
- "description": "Fluid modular TYPE scale → `--text-<name>` tokens, consumed by typography roles and text-size utilities.",
228
+ "description": "Fluid modular TYPE scale → `--text-<name>` tokens, consumed by typography roles and text-size utilities.\n\n**Recipe for marketing headings:** one ratio cannot serve both a ~16px body and a ~76px hero — a ratio big enough for the hero collapses the small end to unreadable. Extend `names` upward instead of raising the ratio: keep `base` ≈ \"1rem\" at the body step, `ratio` ≈ 1.2 at `maxVw` (≈ 1.125–1.15 `minRatio` at `minVw` so headings tame on phones), and add names (`4xl`, `5xl`) for the display sizes — the steps above `baseStep` are what headings read, the steps below it only need `xs`/`sm`. Aim the largest named step at your hero (≈ 4.5–5rem at `maxVw`), then point the `display`/`title` roles at those steps rather than hand-writing `clamp()` in a role. If a step still needs its own value, set that role's `size` literally — roles may bypass the scale.",
188
229
  "type": "object",
189
230
  "properties": {
190
231
  "base": {
@@ -245,6 +286,55 @@
245
286
  ],
246
287
  "additionalProperties": false
247
288
  },
289
+ "layout": {
290
+ "type": "object",
291
+ "properties": {
292
+ "widths": {
293
+ "type": "object",
294
+ "properties": {
295
+ "measure": {
296
+ "type": "string",
297
+ "description": "Reading column → `--width-measure` (framework default 65ch)."
298
+ },
299
+ "breakout": {
300
+ "type": "string",
301
+ "description": "`.breakout` track → `--width-breakout` (default 90ch)."
302
+ },
303
+ "spotlight": {
304
+ "type": "string",
305
+ "description": "`.spotlight` track → `--width-spotlight` (default 120ch)."
306
+ },
307
+ "nav": {
308
+ "type": "string",
309
+ "description": "Navigation column beside content → `--width-nav` (default 16rem)."
310
+ }
311
+ },
312
+ "additionalProperties": false,
313
+ "description": "Content track widths for `.centered`. In `ch` so the reading measure stays character-based; keep measure ≤ breakout ≤ spotlight."
314
+ },
315
+ "regionSpace": {
316
+ "type": "object",
317
+ "properties": {
318
+ "min": {
319
+ "type": "string",
320
+ "description": "Floor → `--region-space-min` (default 3rem)."
321
+ },
322
+ "preferred": {
323
+ "type": "string",
324
+ "description": "Preferred value → `--region-space` (default 8vh)."
325
+ },
326
+ "max": {
327
+ "type": "string",
328
+ "description": "Ceiling → `--region-space-max` (default 8rem)."
329
+ }
330
+ },
331
+ "additionalProperties": false,
332
+ "description": "Vertical padding of `.region`: `padding-block: clamp(min, preferred, max)`."
333
+ }
334
+ },
335
+ "additionalProperties": false,
336
+ "description": "Optional layout tokens. Each key you set is emitted as a `:root` custom property (css, bricks and tailwind formats); each you omit keeps the framework default. Lets the live editor tune widths and region spacing instead of a hand-written override stylesheet."
337
+ },
248
338
  "spaceScale": {
249
339
  "description": "Fluid modular SPACE scale → `--space-<name>` tokens, consumed by spacing/gap utilities and vertical rhythm.",
250
340
  "type": "object",
@@ -466,7 +556,7 @@
466
556
  "number"
467
557
  ]
468
558
  },
469
- "description": "A bag of CSS-ish keys, each mapped to a declaration plus a `--<role>-<sfx>` override hook. The recognised set is **closed**: `family`, `size`, `weight`, `style`, `line-height`, `tracking` (→ `letter-spacing`), `text-transform`, `text-decoration`, `text-wrap`, `color`. Note the last four are spelled with their full CSS property names — `transform` and `decoration` are NOT accepted. Anything unrecognised is **ignored**, not passed through, so the generator warns rather than emitting it: a silently-dropped `transform: uppercase` is how title-case navigation reaches production. Note also that `style`, `text-transform`, `text-decoration` and `text-wrap` are emitted on **every** role at their identity value (`normal`/`none`/`none`/`wrap`) whether declared or not, so applying one role class over another fully resets it — which means **omitting `text-wrap` is not \"inherit\"**: it emits `text-wrap: wrap` and cancels the `pretty` the role would otherwise inherit from a `pretty` ancestor such as a `body`-mapped role. Declare it on every role — `balance` for heading-like roles, `pretty` for copy, `wrap` for short single-line labels."
559
+ "description": "A bag of CSS-ish keys, each mapped to a declaration plus a `--<role>-<sfx>` override hook. The recognised set is **closed**: `family`, `size`, `weight`, `style`, `line-height`, `tracking` (→ `letter-spacing`), `text-transform`, `text-decoration`, `text-wrap`, `color`. The canonical spellings of the last four are their full CSS property names (`text-transform`, `text-decoration`, …); `transform` and `decoration` are accepted as aliases of the first two and normalised to them. Anything unrecognised is **ignored**, not passed through, so the generator warns rather than emitting it: a silently-dropped key (a typo for `text-transform`, say) is how title-case navigation reaches production. Note also that `style`, `text-transform`, `text-decoration` and `text-wrap` are emitted on **every** role at their identity value (`normal`/`none`/`none`/`wrap`) whether declared or not, so applying one role class over another fully resets it — which means **omitting `text-wrap` is not \"inherit\"**: it emits `text-wrap: wrap` and cancels the `pretty` the role would otherwise inherit from a `pretty` ancestor such as a `body`-mapped role. Declare it on every role — `balance` for heading-like roles, `pretty` for copy, `wrap` for short single-line labels."
470
560
  }
471
561
  },
472
562
  "headings": {
@@ -604,7 +694,7 @@
604
694
  "type": "string"
605
695
  },
606
696
  "defaultColorScheme": {
607
- "description": "Initial appearance. `\"system\"` follows the OS via `prefers-color-scheme` and is what makes `<wc-color-scheme-toggle>`'s \"System\" position resolve to anything — it removes the theme attribute, so without this the page falls through to light. It also gives a no-JS page the OS appearance. Defaults to `\"light\"`, because switching an existing site to `\"system\"` visibly flips it dark for dark-OS visitors.",
697
+ "description": "Initial appearance. `\"dark\"` makes the site DARK-ONLY: the dark tokens become the `:root` appearance (with `color-scheme: dark`), no light block or `data-theme` flip is emitted, and the light-mode contrast checks are skipped — so a palette that only works dark (bright fills with dark text, for example) builds. Do not ship `<wc-color-scheme-toggle>` on such a site. `\"system\"` follows the OS via `prefers-color-scheme` and is what makes `<wc-color-scheme-toggle>`'s \"System\" position resolve to anything — it removes the theme attribute, so without this the page falls through to light. It also gives a no-JS page the OS appearance. Defaults to `\"light\"`, because switching an existing site to `\"system\"` visibly flips it dark for dark-OS visitors.",
608
698
  "type": "string",
609
699
  "enum": [
610
700
  "light",
@@ -694,6 +784,7 @@
694
784
  "type": "object",
695
785
  "properties": {
696
786
  "streetAddress": {
787
+ "description": "Street line. Optional: a business with no public street address (home-based, service-area) omits it, and the generated legal documents and JSON-LD simply leave the line out.",
697
788
  "type": "string"
698
789
  },
699
790
  "addressLocality": {
@@ -713,12 +804,28 @@
713
804
  }
714
805
  },
715
806
  "required": [
716
- "streetAddress",
717
807
  "addressLocality",
718
808
  "addressCountry"
719
809
  ],
720
810
  "additionalProperties": false
721
811
  },
812
+ "description": {
813
+ "description": "One or two sentences on what the organization does → JSON-LD `description` and a fallback for SEO. Localisable.",
814
+ "anyOf": [
815
+ {
816
+ "type": "string"
817
+ },
818
+ {
819
+ "type": "object",
820
+ "propertyNames": {
821
+ "type": "string"
822
+ },
823
+ "additionalProperties": {
824
+ "type": "string"
825
+ }
826
+ }
827
+ ]
828
+ },
722
829
  "taxID": {
723
830
  "description": "Tax identification number.",
724
831
  "type": "string"
@@ -759,6 +866,7 @@
759
866
  "type": "object",
760
867
  "properties": {
761
868
  "streetAddress": {
869
+ "description": "Street line. Optional: a business with no public street address (home-based, service-area) omits it, and the generated legal documents and JSON-LD simply leave the line out.",
762
870
  "type": "string"
763
871
  },
764
872
  "addressLocality": {
@@ -778,7 +886,6 @@
778
886
  }
779
887
  },
780
888
  "required": [
781
- "streetAddress",
782
889
  "addressLocality",
783
890
  "addressCountry"
784
891
  ],
@@ -849,6 +956,7 @@
849
956
  "type": "object",
850
957
  "properties": {
851
958
  "streetAddress": {
959
+ "description": "Street line. Optional: a business with no public street address (home-based, service-area) omits it, and the generated legal documents and JSON-LD simply leave the line out.",
852
960
  "type": "string"
853
961
  },
854
962
  "addressLocality": {
@@ -868,7 +976,6 @@
868
976
  }
869
977
  },
870
978
  "required": [
871
- "streetAddress",
872
979
  "addressLocality",
873
980
  "addressCountry"
874
981
  ],
@@ -2226,70 +2333,115 @@
2226
2333
  "additionalProperties": false
2227
2334
  },
2228
2335
  "seo": {
2229
- "description": "SEO defaults: title/description templates, robots policy, verification tokens, Open Graph + Twitter cards, and post-deploy indexing notification.",
2336
+ "description": "SEO defaults: site name, title/description defaults, robots policy, verification tokens, Open Graph + Twitter cards, and post-deploy indexing notification. Read by `<Seo />`, `vitops robots` and `vitops search notify`.",
2230
2337
  "type": "object",
2231
2338
  "properties": {
2339
+ "siteName": {
2340
+ "description": "`og:site_name`, and the `<title>` when a page passes none. Defaults to `organization.name`, so most sites never state it.",
2341
+ "anyOf": [
2342
+ {
2343
+ "type": "string"
2344
+ },
2345
+ {
2346
+ "type": "object",
2347
+ "propertyNames": {
2348
+ "type": "string"
2349
+ },
2350
+ "additionalProperties": {
2351
+ "type": "string"
2352
+ }
2353
+ }
2354
+ ]
2355
+ },
2232
2356
  "titleTemplate": {
2233
- "description": "Page-title pattern (e.g. \"%s · Acme\").",
2234
- "type": "string"
2357
+ "description": "Page-title pattern; `%s` is the page title (e.g. \"%s · Acme\"). Applies to `<title>` only, never to `og:title`, and is skipped when the page title already equals the site name.",
2358
+ "anyOf": [
2359
+ {
2360
+ "type": "string"
2361
+ },
2362
+ {
2363
+ "type": "object",
2364
+ "propertyNames": {
2365
+ "type": "string"
2366
+ },
2367
+ "additionalProperties": {
2368
+ "type": "string"
2369
+ }
2370
+ }
2371
+ ]
2235
2372
  },
2236
- "descriptionTemplate": {
2237
- "type": "string"
2373
+ "defaultTitle": {
2374
+ "description": "`<title>` for pages that pass none. Used verbatim — the template is not applied. Defaults to the site name.",
2375
+ "anyOf": [
2376
+ {
2377
+ "type": "string"
2378
+ },
2379
+ {
2380
+ "type": "object",
2381
+ "propertyNames": {
2382
+ "type": "string"
2383
+ },
2384
+ "additionalProperties": {
2385
+ "type": "string"
2386
+ }
2387
+ }
2388
+ ]
2389
+ },
2390
+ "defaultDescription": {
2391
+ "description": "`<meta name=\"description\">` for pages that pass none.",
2392
+ "anyOf": [
2393
+ {
2394
+ "type": "string"
2395
+ },
2396
+ {
2397
+ "type": "object",
2398
+ "propertyNames": {
2399
+ "type": "string"
2400
+ },
2401
+ "additionalProperties": {
2402
+ "type": "string"
2403
+ }
2404
+ }
2405
+ ]
2238
2406
  },
2239
2407
  "robots": {
2240
- "description": "e.g. \"index,follow\" / \"noindex,nofollow\".",
2408
+ "description": "Site-wide robots directive (e.g. \"index,follow\" / \"noindex,nofollow\"). `site.environments.<env>.robots` wins over it.",
2241
2409
  "type": "string"
2242
2410
  },
2243
2411
  "googleSiteVerification": {
2412
+ "description": "Google Search Console verification token (`<meta name=\"google-site-verification\">`).",
2244
2413
  "type": "string"
2245
2414
  },
2246
2415
  "bingSiteVerification": {
2416
+ "description": "Bing Webmaster Tools verification token (`<meta name=\"msvalidate.01\">`).",
2247
2417
  "type": "string"
2248
2418
  },
2249
2419
  "openGraph": {
2420
+ "description": "Open Graph defaults. Per-page `<Seo />` props override them.",
2250
2421
  "type": "object",
2251
2422
  "properties": {
2252
2423
  "type": {
2253
- "type": "string"
2424
+ "description": "Default `og:type`. Default `website`.",
2425
+ "type": "string",
2426
+ "enum": [
2427
+ "website",
2428
+ "article",
2429
+ "profile"
2430
+ ]
2254
2431
  },
2255
- "siteName": {
2432
+ "locale": {
2433
+ "description": "`og:locale`. `en-CA` and `en_CA` both emit `en_CA`.",
2256
2434
  "type": "string"
2257
2435
  },
2258
- "title": {
2259
- "anyOf": [
2260
- {
2261
- "type": "string"
2262
- },
2263
- {
2264
- "type": "object",
2265
- "propertyNames": {
2266
- "type": "string"
2267
- },
2268
- "additionalProperties": {
2269
- "type": "string"
2270
- }
2271
- }
2272
- ],
2273
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2274
- },
2275
- "description": {
2276
- "anyOf": [
2277
- {
2278
- "type": "string"
2279
- },
2280
- {
2281
- "type": "object",
2282
- "propertyNames": {
2283
- "type": "string"
2284
- },
2285
- "additionalProperties": {
2286
- "type": "string"
2287
- }
2288
- }
2289
- ],
2290
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2436
+ "localeAlternates": {
2437
+ "description": "Other locales the same content exists in → `og:locale:alternate`.",
2438
+ "type": "array",
2439
+ "items": {
2440
+ "type": "string"
2441
+ }
2291
2442
  },
2292
2443
  "image": {
2444
+ "description": "Fallback share image for pages that bring none.",
2293
2445
  "anyOf": [
2294
2446
  {
2295
2447
  "type": "string"
@@ -2298,7 +2450,8 @@
2298
2450
  "type": "object",
2299
2451
  "properties": {
2300
2452
  "src": {
2301
- "type": "string"
2453
+ "type": "string",
2454
+ "description": "Absolute, or relative to `site` (resolved at render time)."
2302
2455
  },
2303
2456
  "alt": {
2304
2457
  "type": "string"
@@ -2308,26 +2461,28 @@
2308
2461
  },
2309
2462
  "height": {
2310
2463
  "type": "number"
2464
+ },
2465
+ "type": {
2466
+ "description": "MIME type, e.g. `image/png`.",
2467
+ "type": "string"
2311
2468
  }
2312
2469
  },
2313
2470
  "required": [
2314
- "src",
2315
- "alt"
2471
+ "src"
2316
2472
  ],
2317
2473
  "additionalProperties": false
2318
2474
  }
2319
2475
  ]
2320
- },
2321
- "locale": {
2322
- "type": "string"
2323
2476
  }
2324
2477
  },
2325
2478
  "additionalProperties": false
2326
2479
  },
2327
2480
  "twitter": {
2481
+ "description": "Twitter/X card defaults.",
2328
2482
  "type": "object",
2329
2483
  "properties": {
2330
2484
  "card": {
2485
+ "description": "Card type when no image resolves (default `summary`).",
2331
2486
  "type": "string",
2332
2487
  "enum": [
2333
2488
  "summary",
@@ -2337,73 +2492,12 @@
2337
2492
  ]
2338
2493
  },
2339
2494
  "site": {
2495
+ "description": "The site's own handle, e.g. \"@acme\".",
2340
2496
  "type": "string"
2341
2497
  },
2342
2498
  "creator": {
2499
+ "description": "Default author handle, e.g. \"@ada\".",
2343
2500
  "type": "string"
2344
- },
2345
- "title": {
2346
- "anyOf": [
2347
- {
2348
- "type": "string"
2349
- },
2350
- {
2351
- "type": "object",
2352
- "propertyNames": {
2353
- "type": "string"
2354
- },
2355
- "additionalProperties": {
2356
- "type": "string"
2357
- }
2358
- }
2359
- ],
2360
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2361
- },
2362
- "description": {
2363
- "anyOf": [
2364
- {
2365
- "type": "string"
2366
- },
2367
- {
2368
- "type": "object",
2369
- "propertyNames": {
2370
- "type": "string"
2371
- },
2372
- "additionalProperties": {
2373
- "type": "string"
2374
- }
2375
- }
2376
- ],
2377
- "description": "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`)."
2378
- },
2379
- "image": {
2380
- "anyOf": [
2381
- {
2382
- "type": "string"
2383
- },
2384
- {
2385
- "type": "object",
2386
- "properties": {
2387
- "src": {
2388
- "type": "string"
2389
- },
2390
- "alt": {
2391
- "type": "string"
2392
- },
2393
- "width": {
2394
- "type": "number"
2395
- },
2396
- "height": {
2397
- "type": "number"
2398
- }
2399
- },
2400
- "required": [
2401
- "src",
2402
- "alt"
2403
- ],
2404
- "additionalProperties": false
2405
- }
2406
- ]
2407
2501
  }
2408
2502
  },
2409
2503
  "additionalProperties": false
@@ -2671,6 +2765,40 @@
2671
2765
  }
2672
2766
  ]
2673
2767
  },
2768
+ "conversionActionId": {
2769
+ "description": "Google Ads only: the conversion action **resource id(s)** (a numeric id, or a full `customers/…/conversionActions/…` resource name) used for server-side offline conversion upload — NOT the same value as `conversionLabel`, which is the opaque label the web tag uses and which the Ads API's upload endpoint does not accept. Only meaningful when `reporting: \"server\"`.",
2770
+ "anyOf": [
2771
+ {
2772
+ "type": "string"
2773
+ },
2774
+ {
2775
+ "type": "object",
2776
+ "properties": {
2777
+ "call": {
2778
+ "description": "Fired on a tel: link click.",
2779
+ "type": "string"
2780
+ },
2781
+ "email": {
2782
+ "description": "Fired on a mailto: link click.",
2783
+ "type": "string"
2784
+ },
2785
+ "form": {
2786
+ "description": "Fired on a form submission.",
2787
+ "type": "string"
2788
+ }
2789
+ },
2790
+ "additionalProperties": false
2791
+ }
2792
+ ]
2793
+ },
2794
+ "reporting": {
2795
+ "description": "Google Ads only: `\"client\"` (default) fires the conversion via `gtag.js` in the browser at click/submit time; `\"server\"` uploads it via the Ads API from the conversion endpoint instead (see `conversionActionId`). An action with no matching `conversionActionId` keeps firing client-side regardless of this setting, so setting it before `conversionActionId` is filled in never silently drops a conversion.",
2796
+ "type": "string",
2797
+ "enum": [
2798
+ "client",
2799
+ "server"
2800
+ ]
2801
+ },
2674
2802
  "domain": {
2675
2803
  "description": "Bare hostname to verify with this platform. Defaults to the host of `domains.canonical` — set it only when the ad account is verified against a different domain.",
2676
2804
  "type": "string"
@@ -2933,7 +3061,8 @@
2933
3061
  "description": "Which legal template set the generated documents use. Default `ca`.",
2934
3062
  "type": "string",
2935
3063
  "enum": [
2936
- "ca"
3064
+ "ca",
3065
+ "qc"
2937
3066
  ]
2938
3067
  },
2939
3068
  "privacyPolicy": {
@@ -2974,6 +3103,7 @@
2974
3103
  "type": "object",
2975
3104
  "properties": {
2976
3105
  "streetAddress": {
3106
+ "description": "Street line. Optional: a business with no public street address (home-based, service-area) omits it, and the generated legal documents and JSON-LD simply leave the line out.",
2977
3107
  "type": "string"
2978
3108
  },
2979
3109
  "addressLocality": {
@@ -2993,7 +3123,6 @@
2993
3123
  }
2994
3124
  },
2995
3125
  "required": [
2996
- "streetAddress",
2997
3126
  "addressLocality",
2998
3127
  "addressCountry"
2999
3128
  ],
@@ -3005,6 +3134,14 @@
3005
3134
  }
3006
3135
  ]
3007
3136
  },
3137
+ "privacyOfficerTitle": {
3138
+ "description": "Title of the person in charge of the protection of personal information (e.g. \"President\"). Quebec Law 25 requires the title and contact details to be published; the `qc` templates print it beside the `privacyOfficer` contact. Not asserted when absent.",
3139
+ "type": "string"
3140
+ },
3141
+ "impactAssessments": {
3142
+ "description": "Attests that a privacy impact assessment is carried out before personal information is communicated outside Quebec (Law 25, s. 17). The `qc` privacy policy states that assurance only when this is `true` — a fact the config cannot otherwise know, so it is never assumed.",
3143
+ "type": "boolean"
3144
+ },
3008
3145
  "retention": {
3009
3146
  "description": "How long personal information is kept, in prose (e.g. \"24 months after last contact\").",
3010
3147
  "type": "string"