@getvitops/generator 8.3.0-alpha.1 → 8.3.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @getvitops/generator
2
2
 
3
+ ## 8.3.0-alpha.3
4
+
5
+ ### Patch Changes
6
+
7
+ - @getvitops/config@8.3.0-alpha.3
8
+ - @getvitops/utils@8.3.0-alpha.3
9
+
10
+ ## 8.3.0-alpha.2
11
+
12
+ ### Minor Changes
13
+
14
+ - 0a47856: Build correctness and gaps from the second upstream round.
15
+
16
+ - **`@getvitops/emdash`: fixes a circular import that emptied the plugin block map in the static build.** `Section.astro` and `WidgetArea.astro` imported `emdash/ui` statically, which imports `virtual:emdash/block-components`, which is `@getvitops/emdash/astro` again. Depending on import order, every `vitops_*` block inside a section body was dropped from prerendered pages with only a `PortableText [components.type] is missing …` warning. Both now import it lazily, and a test fails if any renderer imports `emdash/ui` statically. No code change needed in sites.
17
+ - **`@getvitops/cli`: `vitops emdash build`** runs `astro build` and then `check-prerender`, and fails when the build reports a block type with no component (reported once per type). The template's `build`, `deploy` and `deploy:dev` scripts use it; existing sites replace `astro build && vitops emdash check-prerender` with `vitops emdash build`. `check-prerender` also warns (fails with `--strict`) on a page without exactly one `<h1>`.
18
+ - **`@getvitops/emdash`: `vitops_hero` and `vitops_section` heading level** accepts `h1`–`h6` and `p` (a heading that is styled but not in the outline). The hero defaults to `h1`; set the level to `2` on a page whose layout already renders an `h1` (the template's seed does).
19
+ - **`@getvitops/emdash`: `vitops_logos`** shows an entry's name as text (`.logos__name`) when it has no image. `@getvitops/core` adds the rule (all formats).
20
+ - **`@getvitops/emdash`: an empty content widget** (no text, no blocks) no longer renders its title, and an area whose widgets are all empty renders nothing.
21
+ - **`@getvitops/create`:** the template's `pages` collection gets `urlPattern: "/{slug}"` (the sitemap listed `/pages/<slug>`, which 404s; existing sites change it under Content Types), and `.gitignore` ignores only `src/styles/tailwind.css` and `tokens.json`, so site CSS kept in `src/styles/` is committed.
22
+ - **`@getvitops/emdash`: per-item colour.** The rows of `vitops_cards`, `vitops_stats`, `vitops_steps` and the hero's `cards` take their own `tone` / `color` (blank inherits the block's), so one block can hold a pine item and an amber one. Hero cards also take an `eyebrow` and a button (`ctaLabel` / `ctaHref`; with a button the card is no longer a stretched link).
23
+ - **`@getvitops/generator` / `@getvitops/emdash`: solid fills read.** The generator emits `--color-<hue>-<step>-on` for every step of every hue, the foreground for a fill at that step picked by the same APCA rule as `text-on-<role>` (css and tailwind formats, a plain `:root`, not `@theme`). A block fill chosen by `color` (hue and step) uses it instead of a fixed white..
24
+ - **`@getvitops/core`: `.counter__value.sr-only`** hides itself, so a build that never emitted the `.sr-only` utility no longer shows a counting figure twice (all formats).
25
+ - **`@getvitops/astro` / `@getvitops/core`: `SiteFooter` groups.** A top-level item of the `footer-sitemap` menu with children becomes a column (its label is the heading, its children the links, one level only); childless items stay in the flat list. Each column is a named `<nav>`.
26
+ - **`@getvitops/astro`: planned links and header buttons.** A menu item with `draft` in its CSS classes stays in the menu but is not rendered (at every level, in the header and the footer). `<SiteNav>` has an `actions` slot for header buttons that are not menu items, and an item with `cta` in its CSS classes renders as a button.
27
+ - **`@getvitops/cli`: `check-prerender` warnings** (fail with `--strict`): links to pages the build did not produce, a form with no delivery address (no `site.notifications.email`, location `email` or `site.crm`), and form fields missing from every `site.templates` form entry (the generated privacy policy reads those). It reads `./site.json`, or `--site=<path>`. `vitops setup … --dry-run` is accepted as `--dry`.
28
+ - **`@getvitops/emdash` / `@getvitops/astro`: "tick any number" form field** (`vitops_form` field kind `checkboxes`): a fieldset of checkboxes sharing one name. The conversion route joins repeated values (`IT, Marketing`) instead of keeping the last. The "required" toggle is not applied to the group (HTML cannot require one of several).
29
+ - **`@getvitops/core`: size hooks.** `.steps__title`, `.stats__figure` and `.card__title` read `--steps-title-size` (default one step below `heading`), `--stats-figure-size` and `--card-title-size`, set on the block through its `style` field. They set the `--heading-fs` / `--display-fs` properties the type-role utilities already read, so no extra class or prop is needed (all formats).
30
+ - **`@getvitops/create`:** the template's `site.json` lists its contact form under `site.templates`, so the privacy policy inventory and the new check agree.
31
+ - **`@getvitops/emdash`: `theme` on every block, on widget areas and on your own markup.** A `theme` select (`<role>` tinted, `<role>:solid` filled, populated from the site's roles by a new `themes/list` route) is a common field on every `vitops_*` block, applied to the block's own root with the role's computed readable text. `<VitopsWidgetArea theme="…">` takes the same, and `themeAttrs('surface')` gives `{ class, style }` for a layout `<body>`. `vitops_section`'s existing `theme` now goes through the same path.
32
+ - **`@getvitops/cli` / `@getvitops/emdash`: pages with a dropped block fail the build.** `vitops_section` and widget areas emit an invisible `<template data-vitops-unrendered="…">` for any body block with no renderer, and `check-prerender` fails the page naming the types (nested bodies only; `vitops emdash build` also scans the build log, which covers top-level bodies). A regression test imports the plugin entry first with a mock of the real virtual module, so the old import cycle fails it.
33
+ - **`@getvitops/emdash`: `formTemplateFromBlock(block)`** turns a saved `vitops_form` value into a `site.templates` form entry (names as submitted, a `checkboxes` field as a `checkbox` with options), to paste into `site.json` so the generated privacy policy lists what the form collects. The build cannot read the CMS database, so this is run by hand; `check-prerender` warns when fields are missing.
34
+ - **`@getvitops/utils`: `hasFormDelivery`** (`@getvitops/utils/notify`): the one test for "has this form anywhere to go", used by `check-prerender`.
35
+ - **`@getvitops/emdash`:** a `color` hue tint now flips with the colour scheme (`light-dark()`); hero option cards sit at `h2` under an `h1` hero; menu items also accept `hidden: true` (same as the `draft` class); `vitops docs blocks` documents `theme`, the per-row colours, the `--<role>-fs` type-size hook and the form checks.
36
+ - **`@getvitops/create`:** the template redirects `/home` to `/`, and the guides use `vp exec` instead of `vpx`.
37
+
38
+ ### Patch Changes
39
+
40
+ - 0a47856: **Breaking: every block type is now `vitops_<name>`** (`vitops_cards`, `vitops_hero`, `vitops_actionLink`, …), where the Portable Text blocks were `vitops.<name>` and the section block type was already `vitops_section`. EmDash validates a block type's slug as `[a-z][a-z0-9_]*`, so `vitops.section` was never possible and the toolkit had two styles for one kind of thing. Underscore is the one form valid everywhere, so it is now the only one.
41
+
42
+ Migration: in stored content and in `seed/seed.json`, rename `_type` values from `vitops.<name>` to `vitops_<name>`; in `PortableText` `components`, rename the keys the same way; the `blocks: { include, exclude }` option takes full type names, so rename those too. `vt_*` names in a site are that site's own types and are unaffected. A search-and-replace of `vitops.(actionLink|imageCompare|copyButton|banner|details|carousel|gallery|cards|accordion|hero|marquee|logos|steps|stats|callout|entries|pricing|embed|form|reviews|nav|badges|widgetArea)` with `vitops_$1` covers it. `vitops emdash build` reports any block left on the old name as having no component.
43
+
44
+ - Updated dependencies [0a47856]
45
+ - @getvitops/utils@8.3.0-alpha.2
46
+ - @getvitops/config@8.3.0-alpha.2
47
+
3
48
  ## 8.3.0-alpha.1
4
49
 
5
50
  ### Minor Changes
@@ -33,3 +33,9 @@ wc-cards {
33
33
  [data-card-link] {
34
34
  cursor: pointer;
35
35
  }
36
+
37
+ /* A card's title keeps the heading role's size unless `--card-title-size` is set on the block
38
+ (its `style` field). The role utility reads --heading-fs; `inherit` passes through the site's. */
39
+ .card__title {
40
+ --heading-fs: var(--card-title-size, inherit);
41
+ }
@@ -17,6 +17,11 @@
17
17
  * `display: none`/`visibility: hidden` stops contributing to the accessible
18
18
  * name, and that name must read the final value for the whole animation, not
19
19
  * just at rest.
20
+ *
21
+ * The visually-hidden rule is restated here for `.counter__value.sr-only` rather than left to the
22
+ * utility: a site whose build does not emit `.sr-only` (the tailwind format only generates
23
+ * utilities it sees used, and does not scan packages) would otherwise show the figure twice
24
+ * while it counts, once as text and once as the animation.
20
25
  */
21
26
  @property --_n {
22
27
  syntax: '<number>';
@@ -28,6 +33,18 @@
28
33
  display: none;
29
34
  }
30
35
 
36
+ .counter__value.sr-only {
37
+ position: absolute;
38
+ inline-size: 1px;
39
+ block-size: 1px;
40
+ margin: -1px;
41
+ padding: 0;
42
+ overflow: hidden;
43
+ clip-path: inset(50%);
44
+ white-space: nowrap;
45
+ border: 0;
46
+ }
47
+
31
48
  wc-counter[running] .counter__presentation {
32
49
  display: inline;
33
50
  animation: counter-run var(--counter-duration, 1.2s) var(--counter-easing, var(--custom-ease-out))
@@ -6,6 +6,9 @@
6
6
  * Each item is a plain image, or a link around one. Marks are normalised to a common
7
7
  * height rather than a common width, because logos vary far more in aspect ratio than
8
8
  * in how tall they should read. `--grayscale` quiets them until hover or focus.
9
+ *
10
+ * An entry with no image is its name set as text (`.logos__name`): a partner without a wordmark
11
+ * still belongs in the row. The name is sized from the same `--logos-height` so it reads as a mark.
9
12
  */
10
13
  .logos {
11
14
  display: flex;
@@ -30,6 +33,23 @@
30
33
  object-fit: contain;
31
34
  }
32
35
 
36
+ .logos__name {
37
+ font-size: calc(var(--logos-height, 2.5rem) * 0.55);
38
+ line-height: 1.1;
39
+ font-weight: 600;
40
+ text-align: center;
41
+ text-wrap: balance;
42
+ }
43
+
44
+ &.logos--grayscale .logos__item .logos__name {
45
+ opacity: 0.7;
46
+ transition: opacity 0.2s;
47
+ }
48
+
49
+ &.logos--grayscale .logos__item:is(:hover, :focus-within) .logos__name {
50
+ opacity: 1;
51
+ }
52
+
33
53
  &.logos--grayscale .logos__item img {
34
54
  filter: grayscale(1);
35
55
  opacity: 0.7;
@@ -536,7 +536,7 @@
536
536
  }
537
537
  }
538
538
 
539
- /* ── In-page menu (`vitops.nav` block) ─────────────────────────────────────────
539
+ /* ── In-page menu (`vitops_nav` block) ─────────────────────────────────────────
540
540
  *
541
541
  * A menu read in the body of a page — a site map, a section's links, footer-style columns —
542
542
  * as plain nested lists. No disclosure and no drawer: that is the header's job (`.sitenav`).
@@ -613,6 +613,14 @@
613
613
  }
614
614
  }
615
615
 
616
+ /* Header buttons that are not menu items (`<SiteNav><a slot="actions" class="cta">…`). */
617
+ .sitenav__actions {
618
+ display: flex;
619
+ flex-wrap: wrap;
620
+ align-items: center;
621
+ gap: 0.5rem;
622
+ }
623
+
616
624
  /* ── Footer (`<SiteFooter>`) ───────────────────────────────────────────────── */
617
625
  .sitefooter {
618
626
  margin-block-start: var(--space-2xl, 3rem);
@@ -639,6 +647,16 @@
639
647
  gap: 0.25rem 1.25rem;
640
648
  }
641
649
 
650
+ .sitefooter__heading {
651
+ margin: 0 0 0.5rem;
652
+ }
653
+
654
+ /* A column from a menu item with children: a heading over a stacked list. */
655
+ .sitefooter__list--column {
656
+ flex-direction: column;
657
+ gap: 0.125rem;
658
+ }
659
+
642
660
  .sitefooter__link {
643
661
  color: inherit;
644
662
  text-decoration: underline;
@@ -34,6 +34,9 @@
34
34
  }
35
35
 
36
36
  .stats__figure {
37
+ /* Display size by default; --stats-figure-size on the block overrides it (the role utility
38
+ reads --display-fs). `inherit` passes through whatever the site set for the role. */
39
+ --display-fs: var(--stats-figure-size, inherit);
37
40
  color: var(--tone-fg, inherit);
38
41
  font-variant-numeric: tabular-nums;
39
42
  }
@@ -69,6 +69,13 @@
69
69
  .steps__text {
70
70
  margin: 0;
71
71
  }
72
+
73
+ /* A step title is a sub-heading, not a section heading: one notch down from the role's size.
74
+ Set --steps-title-size on the block (its `style` field) to change it. The role utility reads
75
+ --heading-fs, so this sets the property it already consumes rather than fighting its layer. */
76
+ .steps__title {
77
+ --heading-fs: var(--steps-title-size, var(--text-l, 1.2rem));
78
+ }
72
79
  }
73
80
 
74
81
  /* Side by side once there is room: the marker sits above its text and the line runs across. */
package/dist/index.d.mts CHANGED
@@ -67,7 +67,7 @@ interface GenerateOptions {
67
67
  * `tailwind` format only: directories (or globs) Tailwind should scan for class names, written
68
68
  * as `@source` lines after `@import "tailwindcss"`. Paths are relative to the emitted
69
69
  * `tailwind.css`. Tailwind v4 does not scan `node_modules`, so a utility used only by a package's
70
- * own renderer (`font-display` in `vitops.stats`) is otherwise never generated — silently.
70
+ * own renderer (`font-display` in `vitops_stats`) is otherwise never generated — silently.
71
71
  * Ignored by the css and bricks formats, which ship every framework class.
72
72
  */
73
73
  sources?: string[];
@@ -273,9 +273,9 @@ interface TierEntry {
273
273
  bricks?: string;
274
274
  /**
275
275
  * `@getvitops/emdash` Portable Text block `_type`s that render this pattern, e.g.
276
- * `vitops.carousel`. An array because one pattern can have several blocks
276
+ * `vitops_carousel`. An array because one pattern can have several blocks
277
277
  * (`details` has a single disclosure and an accordion group) and one block can span
278
- * patterns (`vitops.actionLink` is `link`, `btn` and `cta`).
278
+ * patterns (`vitops_actionLink` is `link`, `btn` and `cta`).
279
279
  *
280
280
  * Tier 3, a sibling of `astro` and `bricks`: a block is a platform wrapper that an
281
281
  * editor drops into a page, and each one is an adapter over this entry's Astro
@@ -686,7 +686,7 @@ export declare function toHtmlFragment(blocks: Block[]): string;
686
686
  * Two deliberate mappings:
687
687
  * - the `# ` heading is **dropped** — in EmDash the page title is a field
688
688
  * (`data.title`), and repeating it as the first content block renders twice;
689
- * - the review blockquote becomes a `vitops.banner` with `tone: "warning"`,
689
+ * - the review blockquote becomes a `vitops_banner` with `tone: "warning"`,
690
690
  * which is exactly the block the hand-written placeholder pages used.
691
691
  *
692
692
  * `_key`s are derived from position rather than randomness, so regenerating an
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/generate.ts","../src/docs.ts","../src/tiers.ts","../src/token-refs.ts","../src/legal/providers.ts","../src/legal/derive.ts","../src/legal/templates/cookies.ca.ts","../src/legal/templates/cookies.qc.ts","../src/legal/templates/privacy.ca.ts","../src/legal/templates/privacy.qc.ts","../src/legal/templates/terms.ca.ts","../src/legal/templates/terms.qc.ts","../src/legal/templates/index.ts","../src/legal/render.ts","../src/legal/index.ts","../src/robots.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/generate.ts","../src/docs.ts","../src/tiers.ts","../src/token-refs.ts","../src/legal/providers.ts","../src/legal/derive.ts","../src/legal/templates/cookies.ca.ts","../src/legal/templates/cookies.qc.ts","../src/legal/templates/privacy.ca.ts","../src/legal/templates/privacy.qc.ts","../src/legal/templates/terms.ca.ts","../src/legal/templates/terms.qc.ts","../src/legal/templates/index.ts","../src/legal/render.ts","../src/legal/index.ts","../src/robots.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -6,6 +6,7 @@ import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createHash } from "node:crypto";
8
8
  import { transform } from "lightningcss";
9
+ import { pickOn } from "@getvitops/utils/color";
9
10
  import { AD_PLATFORMS, AD_PROVIDER_KEYS } from "@getvitops/utils/ads";
10
11
  import { generateIconInclude, nodesToHtml, resolveIcon, t } from "@getvitops/utils";
11
12
  import { resolveSitemapUrl } from "@getvitops/utils/indexing";
@@ -49,7 +50,7 @@ const TIERS = {
49
50
  use: "`<Tree items={…} />` — it emits `<wc-tree>` itself, so do NOT add your own wrapper."
50
51
  },
51
52
  carousel: {
52
- emdash: ["vitops.carousel"],
53
+ emdash: ["vitops_carousel"],
53
54
  css: c([
54
55
  "carousel",
55
56
  "carousel__track",
@@ -67,7 +68,7 @@ const TIERS = {
67
68
  use: "`<Carousel slides={…} label=\"…\" />` — it emits `<wc-carousel>` itself, so do NOT add your own wrapper. `.carousel` is the shell and `.carousel__track` the scroller; add `loop` for the cloned infinite strip (`autoplay` implies it)."
68
69
  },
69
70
  gallery: {
70
- emdash: ["vitops.gallery"],
71
+ emdash: ["vitops_gallery"],
71
72
  css: c([
72
73
  "gallery",
73
74
  "gallery__item",
@@ -83,7 +84,7 @@ const TIERS = {
83
84
  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."
84
85
  },
85
86
  entries: {
86
- emdash: ["vitops.entries"],
87
+ emdash: ["vitops_entries"],
87
88
  css: c([
88
89
  "entries__table",
89
90
  "entries__scroll",
@@ -99,7 +100,7 @@ const TIERS = {
99
100
  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."
100
101
  },
101
102
  copy: {
102
- emdash: ["vitops.copyButton"],
103
+ emdash: ["vitops_copyButton"],
103
104
  css: c([
104
105
  "copy",
105
106
  "copy__button",
@@ -115,7 +116,7 @@ const TIERS = {
115
116
  use: "`<wc-copy>` around the content and a `[data-copy]` button."
116
117
  },
117
118
  dismissable: {
118
- emdash: ["vitops.banner"],
119
+ emdash: ["vitops_banner"],
119
120
  css: c(["banner__close", "notification__close"], "patterns/banner.css"),
120
121
  wc: {
121
122
  tag: "wc-dismissable",
@@ -141,7 +142,7 @@ const TIERS = {
141
142
  use: "`<wc-split-panel>` with two children. Unenhanced they simply stack."
142
143
  },
143
144
  "image-compare": {
144
- emdash: ["vitops.imageCompare"],
145
+ emdash: ["vitops_imageCompare"],
145
146
  css: c([
146
147
  "image-compare",
147
148
  "image-compare__before",
@@ -156,7 +157,7 @@ const TIERS = {
156
157
  use: "`<wc-image-compare>` with two images. Both are visible without JS."
157
158
  },
158
159
  marquee: {
159
- emdash: ["vitops.marquee"],
160
+ emdash: ["vitops_marquee"],
160
161
  css: c([
161
162
  "marquee",
162
163
  "marquee__content",
@@ -194,7 +195,7 @@ const TIERS = {
194
195
  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
  },
196
197
  steps: {
197
- emdash: ["vitops.steps"],
198
+ emdash: ["vitops_steps"],
198
199
  css: c([
199
200
  "steps",
200
201
  "steps__item",
@@ -206,7 +207,7 @@ const TIERS = {
206
207
  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
  },
208
209
  stats: {
209
- emdash: ["vitops.stats"],
210
+ emdash: ["vitops_stats"],
210
211
  css: c([
211
212
  "stats",
212
213
  "stats__item",
@@ -219,7 +220,7 @@ const TIERS = {
219
220
  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
  },
221
222
  callout: {
222
- emdash: ["vitops.callout"],
223
+ emdash: ["vitops_callout"],
223
224
  css: c([
224
225
  "callout",
225
226
  "callout__body",
@@ -231,16 +232,17 @@ const TIERS = {
231
232
  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
  },
233
234
  logos: {
234
- emdash: ["vitops.logos"],
235
+ emdash: ["vitops_logos"],
235
236
  css: c([
236
237
  "logos",
237
238
  "logos__item",
239
+ "logos__name",
238
240
  "logos--grayscale"
239
241
  ], "patterns/logos.css"),
240
- use: "`<ul class=\"logos\">` of `.logos__item` images (alt text = the name). A still grid; `marquee` is the scrolling version."
242
+ use: "`<ul class=\"logos\">` of `.logos__item` images (alt text = the name), or `.logos__name` text when there is no image. A still grid; `marquee` is the scrolling version."
241
243
  },
242
244
  embed: {
243
- emdash: ["vitops.embed"],
245
+ emdash: ["vitops_embed"],
244
246
  css: c([
245
247
  "embed",
246
248
  "embed__frame",
@@ -249,7 +251,7 @@ const TIERS = {
249
251
  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
252
  },
251
253
  counter: {
252
- emdash: ["vitops.stats"],
254
+ emdash: ["vitops_stats"],
253
255
  css: c([
254
256
  "counter",
255
257
  "counter__value",
@@ -264,7 +266,7 @@ const TIERS = {
264
266
  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."
265
267
  },
266
268
  "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",
269
+ 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",
268
270
  css: c([
269
271
  "form-group",
270
272
  "input-group",
@@ -279,14 +281,14 @@ const TIERS = {
279
281
  use: "`<wc-multi-field>` around a field template. One row submits fine with no JS."
280
282
  },
281
283
  turnstile: {
282
- emdash: ["vitops.form"],
284
+ emdash: ["vitops_form"],
283
285
  css: c(["form-group"], "patterns/forms.css"),
284
286
  wc: {
285
287
  tag: "wc-turnstile",
286
288
  registered: true,
287
289
  adds: "loads Cloudflare Turnstile on first interaction with the form and holds a submit until its token arrives"
288
290
  },
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."
291
+ 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
292
  },
291
293
  "color-scheme-toggle": {
292
294
  emdashExempt: "site chrome (header/footer), not page content; belongs in the nav/layout",
@@ -375,7 +377,7 @@ const TIERS = {
375
377
  use: "Not shipped. See `color-wheel`."
376
378
  },
377
379
  details: {
378
- emdash: ["vitops.details", "vitops.accordion"],
380
+ emdash: ["vitops_details", "vitops_accordion"],
379
381
  css: c([
380
382
  "details",
381
383
  "details-trigger",
@@ -413,7 +415,7 @@ const TIERS = {
413
415
  use: "`<NavShell>` with a `nav` slot, plus `<NavShellToggle>` if the toggle must live outside the shell."
414
416
  },
415
417
  subgrid: {
416
- emdash: ["vitops.pricing"],
418
+ emdash: ["vitops_pricing"],
417
419
  css: c([
418
420
  "subgrid",
419
421
  "subgrid-card",
@@ -423,8 +425,8 @@ const TIERS = {
423
425
  use: "`<Subgrid>` — emits `<ul class=\"subgrid\" role=\"list\">`; author `<li class=\"card subgrid-card\">` items yourself, so tranches align across the set."
424
426
  },
425
427
  card: {
426
- emdash: ["vitops.cards"],
427
- css: c(["card"], "patterns/card.css", true),
428
+ emdash: ["vitops_cards"],
429
+ css: c(["card", "card__title"], "patterns/card.css", true),
428
430
  wc: {
429
431
  tag: "wc-cards",
430
432
  registered: true,
@@ -444,7 +446,7 @@ const TIERS = {
444
446
  use: "`<Icon name=\"menu\" />`. Names are semantic and resolve per configured set; a name containing `:` passes through."
445
447
  },
446
448
  split: {
447
- emdash: ["vitops.hero"],
449
+ emdash: ["vitops_hero"],
448
450
  css: c([
449
451
  "split",
450
452
  "split-1-2",
@@ -472,9 +474,9 @@ const TIERS = {
472
474
  "navlist--columns"
473
475
  ], "patterns/sitenav.css"),
474
476
  astro: [...a("SiteNav", "css"), ...a("SiteFooter", "css")],
475
- emdash: ["vitops.nav"],
477
+ emdash: ["vitops_nav"],
476
478
  bricks: "vitops-sitenav",
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."
479
+ 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."
478
480
  },
479
481
  "split-link": {
480
482
  css: c([
@@ -486,17 +488,17 @@ const TIERS = {
486
488
  use: "`.split-link` — a link plus a disclosure toggle. No JS."
487
489
  },
488
490
  btn: {
489
- emdash: ["vitops.actionLink"],
491
+ emdash: ["vitops_actionLink"],
490
492
  css: c(["btn"], void 0, true),
491
493
  use: "`<button>` gets it with no class; `.btn` carries it to other tags. `fill: false`, so states drive `color`."
492
494
  },
493
495
  cta: {
494
- emdash: ["vitops.actionLink"],
496
+ emdash: ["vitops_actionLink"],
495
497
  css: c(["cta", "cta-danger"], void 0, true),
496
498
  use: "`.cta` on any element — usually `<a>`, since a CTA navigates. Filled, with role variants."
497
499
  },
498
500
  link: {
499
- emdash: ["vitops.actionLink"],
501
+ emdash: ["vitops_actionLink"],
500
502
  css: c([
501
503
  "link",
502
504
  "skip-link",
@@ -506,18 +508,18 @@ const TIERS = {
506
508
  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."
507
509
  },
508
510
  badge: {
509
- emdash: ["vitops.badges"],
511
+ emdash: ["vitops_badges"],
510
512
  css: c(["badge", "badge-indicator"], "patterns/tag.css", true),
511
513
  use: "`.badge` — a STATIC label. Use `tag` if it is dismissable or editable."
512
514
  },
513
515
  "widget-area": {
514
- emdash: ["vitops.widgetArea"],
516
+ emdash: ["vitops_widgetArea"],
515
517
  css: c([
516
518
  "widget-area",
517
519
  "widget__title",
518
520
  "widget__menu"
519
521
  ], "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."
522
+ 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
523
  },
522
524
  tag: {
523
525
  css: c([
@@ -581,8 +583,8 @@ const TIERS = {
581
583
  "review__source"
582
584
  ], "patterns/review.css", true),
583
585
  astro: a("ReviewCard", "css"),
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."
586
+ emdash: ["vitops_reviews"],
587
+ 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."
586
588
  },
587
589
  tabs: {
588
590
  css: c(["tabs"], void 0, true),
@@ -606,7 +608,7 @@ const TIERS = {
606
608
  use: "`.navbar` with `--start/--center/--end`."
607
609
  },
608
610
  banner: {
609
- emdash: ["vitops.banner"],
611
+ emdash: ["vitops_banner"],
610
612
  css: c([
611
613
  "banner",
612
614
  "banner__content",
@@ -2505,7 +2507,7 @@ ${bricks}
2505
2507
  Portable Text block ${code("_type")}s from ${code("@getvitops/emdash")}. Repeating patterns are
2506
2508
  repeater fields. A Portable Text block cannot contain other blocks, so a pattern that must hold
2507
2509
  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.
2510
+ from ${code("vitopsBlockTypes()")}. Its body accepts every ${code("vitops_*")} block below.
2509
2511
 
2510
2512
  | Block | Pattern | Renders via |
2511
2513
  | --- | --- | --- |
@@ -4429,7 +4431,7 @@ function toHtmlFragment(blocks) {
4429
4431
  * Two deliberate mappings:
4430
4432
  * - the `# ` heading is **dropped** — in EmDash the page title is a field
4431
4433
  * (`data.title`), and repeating it as the first content block renders twice;
4432
- * - the review blockquote becomes a `vitops.banner` with `tone: "warning"`,
4434
+ * - the review blockquote becomes a `vitops_banner` with `tone: "warning"`,
4433
4435
  * which is exactly the block the hand-written placeholder pages used.
4434
4436
  *
4435
4437
  * `_key`s are derived from position rather than randomness, so regenerating an
@@ -4442,7 +4444,7 @@ function toPortableText(blocks, prefix) {
4442
4444
  if (b.kind === "heading" && b.level === 1) return;
4443
4445
  if (b.kind === "quote") {
4444
4446
  out.push({
4445
- _type: "vitops.banner",
4447
+ _type: "vitops_banner",
4446
4448
  _key: key,
4447
4449
  message: b.spans.map((s) => s.text).join(""),
4448
4450
  tone: "warning",
@@ -4494,7 +4496,7 @@ const REVIEW_NOTICE = "> Generated from this site's configuration. It is a start
4494
4496
  * designated and is published, that a privacy impact assessment precedes any
4495
4497
  * communication outside Quebec, that consent mechanics match the Act, and — easy
4496
4498
  * 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
4499
+ * is a second blockquote (a second `vitops_banner` in Portable Text) so it cannot
4498
4500
  * be skimmed past as part of the first.
4499
4501
  */
4500
4502
  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.";
@@ -5234,6 +5236,18 @@ function generateLegal(cfg, opts = {}) {
5234
5236
  */
5235
5237
  const HERE = dirname(fileURLToPath(import.meta.url));
5236
5238
  const DEFAULT_ASSETS = join(HERE, "..", "assets");
5239
+ /**
5240
+ * `--color-<hue>-<step>-on`: the readable text colour for a fill at that step, computed with the
5241
+ * same APCA pick as `text-on-<role>`. A role has one (`--color-text-on-<role>`); a raw hue step
5242
+ * did not, so a block filling with `color: amber-500` had no site-defined foreground to use. These
5243
+ * live in a plain `:root`, never `@theme`: a `--color-*` theme key would mint `text-<hue>-<step>-on`
5244
+ * utilities nobody asked for.
5245
+ */
5246
+ function hueOnTokens(palette) {
5247
+ const out = [];
5248
+ for (const [name, hue] of Object.entries(palette)) for (const [n, hex] of Object.entries(hue.numeric)) out.push(`--color-${name}-${n}-on: ${pickOn(hex)};`);
5249
+ return out;
5250
+ }
5237
5251
  const UTILITY_PROPS = {
5238
5252
  bg: "background-color",
5239
5253
  text: "color",
@@ -5522,6 +5536,8 @@ function build(ds, format, assetsDir, buildOpts = {}) {
5522
5536
  css += ` color-scheme: ${buildOpts.darkOnly ? "dark" : "light"};\n`;
5523
5537
  css += ` /* Hue scales (numeric steps, 50 = tinted near-white … 950 = tinted near-black) */\n`;
5524
5538
  for (const [name, hue] of Object.entries(expandedPalette)) for (const [n, hex] of Object.entries(hue.numeric)) css += ` --color-${name}-${n}: ${hex};\n`;
5539
+ css += `\n /* Foreground for a fill at a hue step (--color-<hue>-<step>-on) */\n`;
5540
+ for (const line of hueOnTokens(expandedPalette)) css += ` ${line}\n`;
5525
5541
  css += `\n /* Role tokens (the public API) */\n`;
5526
5542
  for (const fr of scaleRoles) for (const [t, val] of Object.entries(fr.light)) css += ` ${tokenVar(fr.role, t)}: ${val};\n`;
5527
5543
  if (focusRole) {
@@ -6034,6 +6050,7 @@ function emitTailwind(ctx) {
6034
6050
  fn += ` --surface-glass: color-mix(in oklch, var(--color-bg-surface) 72%, transparent);\n`;
6035
6051
  fn += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 45%, transparent);\n`;
6036
6052
  }
6053
+ for (const line of hueOnTokens(expandedPalette)) fn += ` ${line}\n`;
6037
6054
  if (ctx.darkOnly) fn = ` color-scheme: dark;\n${fn}`;
6038
6055
  parts.push(`/* Role tokens (the public API) */\n:root {\n${fn}}\n`);
6039
6056
  }