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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +550 -25
  2. package/assets/css/index.css +7 -0
  3. package/assets/css/patterns/banner.css +20 -0
  4. package/assets/css/patterns/callout.css +39 -0
  5. package/assets/css/patterns/copy.css +5 -0
  6. package/assets/css/patterns/embed.css +39 -0
  7. package/assets/css/patterns/forms.css +19 -0
  8. package/assets/css/patterns/logos.css +45 -0
  9. package/assets/css/patterns/marquee.css +16 -0
  10. package/assets/css/patterns/section.css +53 -0
  11. package/assets/css/patterns/sitenav.css +262 -0
  12. package/assets/css/patterns/splitter.css +11 -0
  13. package/assets/css/patterns/stats.css +61 -0
  14. package/assets/css/patterns/steps.css +98 -0
  15. package/assets/css/patterns/table.css +10 -0
  16. package/assets/css/patterns/tone.css +38 -0
  17. package/assets/js/consent.js +1 -1
  18. package/assets/js/elements.js +5 -5
  19. package/assets/js/polyfills/adoptedStyleSheets.js +1 -1
  20. package/assets/js/polyfills/css-anchor-positioning-fn.js +2 -2
  21. package/assets/js/polyfills/popover.js +2 -2
  22. package/config.schema.json +257 -120
  23. package/dist/config-BfcWKH-5.mjs +1743 -0
  24. package/dist/config-BfcWKH-5.mjs.map +1 -0
  25. package/dist/{config-DvbYx3Gi.mjs → config-Bhg3Xv3D.mjs} +10 -207
  26. package/dist/config-Bhg3Xv3D.mjs.map +1 -0
  27. package/dist/config-D5xuz8--.mjs +1807 -0
  28. package/dist/config-D5xuz8--.mjs.map +1 -0
  29. package/dist/config.d.mts +157 -211
  30. package/dist/config.d.mts.map +1 -1
  31. package/dist/config.mjs +1 -1
  32. package/dist/{icons-sprite-Bf_KmKeX.d.mts → icons-sprite-CQ5Zj4go.d.mts} +4 -6
  33. package/dist/icons-sprite-CQ5Zj4go.d.mts.map +1 -0
  34. package/dist/icons-sprite.d.mts +1 -1
  35. package/dist/icons-sprite.mjs +1 -2
  36. package/dist/index.d.mts +49 -35
  37. package/dist/index.d.mts.map +1 -1
  38. package/dist/index.mjs +607 -75
  39. package/dist/index.mjs.map +1 -1
  40. package/dist/{schema-Dmq1J7S3.d.mts → schema-BV2n2phj.d.mts} +2 -3
  41. package/dist/{schema-Dmq1J7S3.d.mts.map → schema-BV2n2phj.d.mts.map} +1 -1
  42. package/dist/schema-C-P9dF9F.d.mts +337 -0
  43. package/dist/schema-C-P9dF9F.d.mts.map +1 -0
  44. package/dist/{tokens-8SXMDAd1.d.mts → tokens-BzBbTeTi.d.mts} +2 -6
  45. package/dist/tokens-BzBbTeTi.d.mts.map +1 -0
  46. package/dist/{tokens-zi594IZ3.mjs → tokens-C1RJwFeG.mjs} +3 -5
  47. package/dist/{tokens-zi594IZ3.mjs.map → tokens-C1RJwFeG.mjs.map} +1 -1
  48. package/dist/tokens-CaIGQQLk.mjs +740 -0
  49. package/dist/tokens-CaIGQQLk.mjs.map +1 -0
  50. package/dist/tokens-DH5cpvKd.d.mts +242 -0
  51. package/dist/tokens-DH5cpvKd.d.mts.map +1 -0
  52. package/dist/tokens.d.mts +1 -1
  53. package/dist/tokens.mjs +1 -1
  54. package/package.json +6 -5
  55. package/schema.json +95 -5
  56. package/dist/config-DvbYx3Gi.mjs.map +0 -1
  57. package/dist/icons-sprite-Bf_KmKeX.d.mts.map +0 -1
  58. package/dist/tokens-8SXMDAd1.d.mts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,538 @@
1
1
  # @getvitops/generator
2
2
 
3
+ ## 8.3.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 93d2067: New framework patterns and an `Entries` component for the EmDash block library.
8
+
9
+ **CSS, all formats** (css, bricks and inlined into tailwind's `components` layer; no existing class changes): `tone` (a shared tint/solid colour convention driven by `--tone-*` custom properties, plus `.accent` for a run inside a heading), `section` (head and heading-left `--split` layout), `steps` (numbered `<ol>` with a connector line), `stats` (figures and `--tiles`), `callout` (a call-to-action band), `logos` (a still partner grid) and `embed` (a consent-gated iframe frame). All are structural partials, classified `vitops.components`.
10
+
11
+ **`@getvitops/astro`:** new `Entries.astro` (`@getvitops/astro/components/Entries.astro`) emits `<wc-entries>` itself with the semantic heading + `<dl>` fallback.
12
+
13
+ **`@getvitops/generator`:** `TierEntry` gains `emdashExempt` (a reason a shipped web component has no EmDash block). `tiers.test.ts` now fails if a shipped `wc-*` has neither `emdash` nor `emdashExempt`, and the components doc and the docs site list the exemptions.
14
+
15
+ No other consumer-facing change in `@getvitops/core`, `@getvitops/utils`, `@getvitops/cli` or `@getvitops/vite` beyond the version bump.
16
+
17
+ - 161c7f3: A contact form that works end to end: `vitops setup turnstile`, `<wc-turnstile>`, the `vitops.form` EmDash block, and the route the emdash template ships to receive it.
18
+
19
+ **CSS, all formats** (no existing class changes): `forms.css` gains `.form-notice--target:not(:target)` (a message revealed only when the URL fragment names it, which is how a prerendered page shows a no-JS form's result) and `:where(wc-turnstile) { display: block }`.
20
+
21
+ **`@getvitops/core`:** new `<wc-turnstile sitekey="…">` in `elements.js`. It loads Cloudflare Turnstile on the first interaction with its form (no request to `challenges.cloudflare.com` for a visitor who only reads the page), holds an early submit until the token arrives, and leaves its slotted no-JS explanation in place until JS takes over. Turnstile is category `necessary`, so it is not behind the consent banner.
22
+
23
+ **`@getvitops/utils` / `@getvitops/cli`:** new `@getvitops/utils/turnstile` and `vitops setup turnstile [--dry] [--check] [--no-write] [--no-secret]`. It plans from the site's hostnames, creates a managed widget unless one already covers them (an existing widget is adopted, never edited, rotated or deleted), writes the public key to `site.security.turnstile.siteKey`, and pipes the one-time secret to `wrangler secret put VITOPS_TURNSTILE_SECRET` without writing it to any file. Needs `CLOUDFLARE_API_TOKEN` with **Account > Turnstile > Edit** and `CLOUDFLARE_ACCOUNT_ID`.
24
+
25
+ **`@getvitops/astro`:** `virtual:getvitops/head` gains `turnstileSiteKey` (read from `site.security.turnstile.siteKey`) so blocks can render the widget. No other change.
26
+
27
+ **`@getvitops/generator`:** `TIERS` gains a `turnstile` entry (wc-turnstile, claimed by `vitops.form`); `wc-multi-field`'s `emdashExempt` reason is reworded now the form block exists.
28
+
29
+ **`@getvitops/emdash`:** new `vitops.form` block: a field repeater (text, email, phone, long text, choose one, tick box), required flags, help text, success/error messages, optional Turnstile. It renders a semantic `<form method="post">` that works with no JavaScript, with a honeypot named per `createConversionRoute`'s defaults. A field named `phone` submits as `tel`, because the route reserves `phone` and `event`.
30
+
31
+ **`@getvitops/create`:** the emdash template ships `src/pages/api/track.ts` (`prerender = false`, `createConversionRoute()`), a seeded contact form, and `challenges.cloudflare.com` in both templates' Report-Only CSP (`script-src`, `frame-src`). The route refuses a post with nowhere to deliver and fails closed when a site key exists without `VITOPS_TURNSTILE_SECRET`. SETUP.html gains the Turnstile step. Existing sites: copy the route and the CSP change, then run `vitops setup turnstile`.
32
+
33
+ - 175d16e: **Added: `vitops.reviews` and `vitops.nav` EmDash blocks.** Non-breaking.
34
+
35
+ - **`vitops.reviews`** renders Google reviews from the committed `.vitops/reviews.json` cache (`vitops reviews`) as a grid, carousel or marquee of `<ReviewCard>`. Fields: location key, layout, columns, limit, lowest rating, order, featured ids, reviewer photos (off by default), tone/colour. Which reviews show is `selectReviews()`, the one shared decision. Review JSON-LD is not offered (Google treats self-marked-up reviews as ineligible for rich results). A new review reaches the page after `vitops reviews` is re-run, committed and the site rebuilt, not when an editor saves; with no cache file the block renders nothing (a note in `astro dev`).
36
+ - **`vitops.nav`** renders a named EmDash menu inside page content (site map, section links, footer-style columns), never the header. Fields: menu (picked from the site's menus via a new `menus/list` plugin route, with `footer-sitemap`/`footer-legal`/`primary` as the static fallback), accessible name, visible heading and level, layout `list | inline | columns | sitenav`, depth 1-3, tone/colour. It is a `<nav>` with a name that is never `Primary`, `Site map` or `Legal` (the layout's landmarks), `aria-current="page"`, no runtime JS; a missing or empty menu renders nothing.
37
+ - `@getvitops/astro`: **`virtual:getvitops/reviews`**, served by `vitops()`, is `.vitops/reviews.json` as a build-time import (an empty cache when absent or unreadable), because a Worker cannot read the file. `<ReviewCard>` now renders the review's source link (`.review__source`, `rel="nofollow noopener noreferrer"`), which the pattern already styled. **Fix:** `src/json-ld.ts` was missing from the published `files`, so any consumer importing `<ReviewCard>` (which composes `schemas/Review.astro`) failed to build with an unresolved import. `@getvitops/emdash` now needs this version of `@getvitops/astro` for the `vitops.reviews` block.
38
+ - `@getvitops/utils`: new Worker-safe subpath **`@getvitops/utils/reviews/select`** (`selectReviews` and the review types, no `node:fs`). `selectReviews` now takes only the display fields (`featured`, `minRating`, `limit`, `sort`), so a caller with no configured `locationId` can use it; existing calls are unchanged.
39
+ - `@getvitops/core`: new `.navlist` pattern in `patterns/sitenav.css` (`.navlist`, `__item`, `__link`, `__label`, `__sub`, `--inline`, `--columns`). **All formats** (css, bricks, tailwind). No existing class changes.
40
+ - `@getvitops/generator`: `TIERS.review` lists `vitops.reviews` and `TIERS.sitenav` lists `vitops.nav` (and `.navlist`), so `vitops docs components` and the docsite show them.
41
+
42
+ - 83d6f3c: Theming escape hatches, so a real palette builds without raw ramp steps. All additive; existing configs build unchanged.
43
+
44
+ - **Dark-only sites.** `designSystem.defaultColorScheme: "dark"` (accepted by the schema before, but ignored) now makes the dark tokens the `:root` appearance with `color-scheme: dark`, emits no light block or `data-theme` flip, and skips light-mode contrast checks. Reaches css, bricks and tailwind formats. Do not ship `<wc-color-scheme-toggle>` on such a site.
45
+ - **Role solid step and foreground.** `colors.roles.<role>` may be `{ hue, kind?, solid?, foreground? }`. `solid: 400` starts the solid family at step 400 (bold and x-bold walk one and two rungs darker); `foreground: "dark" | "light"` pins `text-on-<role>`. The APCA check (Lc 60) still runs on the pair emitted. The string shorthand is unchanged. All formats. The live editor's `roleTokens` still assume the default 600 family when re-pointing a role.
46
+ - **Split shadows.** `shadows.<name>` may be `{ box?, drop? }`: `box` (spread, inset, layers) is `--shadow-<name>`; `drop` is `--shadow-<name>-drop` and feeds `.drop-shadow-<name>` and the `shadow:` state shortcut. The string form is unchanged. css/bricks emit `.drop-shadow-*`; tailwind emits the `-drop` token only.
47
+ - **Layout tokens.** New optional `layout` block (`widths.{measure,breakout,spotlight,nav}`, `regionSpace.{min,preferred,max}`) emitted as `--width-*` / `--region-space*` on `:root` in css, bricks and tailwind formats, and exposed in `design-manifest.json` (`layout`, `reverseIndex`). Keys you omit keep the framework defaults.
48
+ - **`acknowledgeOffLadder: true`** on a palette hue silences that hue's off-ladder anchor warnings.
49
+ - **Typography:** `transform` and `decoration` are now accepted as aliases of `text-transform` and `text-decoration` (canonical wins when both are present) instead of warning and being dropped.
50
+ - **Docs strings** (`authoring.md`, JSON Schema hovers) state the Lc 60 muted-text requirement against the page plane and give a type-scale recipe for marketing headings.
51
+ - `@getvitops/utils`: `pickOn` takes an optional `prefer: 'light' | 'dark'`.
52
+
53
+ - 892b204: Generated legal documents gain a **Quebec (Law 25) jurisdiction**: `site.legal.jurisdiction: "qc"` renders the privacy policy, terms of use and cookie notice from new, Quebec-specific templates (not the `ca` prose). The privacy policy adds the person in charge of the protection of personal information, technologies that identify, locate or profile, transfers _outside Quebec_ (so a provider elsewhere in Canada counts), retention and anonymization, and the rights of access, rectification, portability, withdrawal, de-indexing and review of automated decisions, with the Commission d'accès à l'information as the complaint body. Every `qc` document carries a second, prominent review banner (counsel review, designated person, impact assessment, French-language requirement). Two optional config facts back it: `privacyPolicy.privacyOfficerTitle` and `privacyPolicy.impactAssessments` (the impact-assessment sentence renders only when attested). The "off until you choose" profiling assurance renders only under an opt-in consent model. **Not legal advice and not reviewed by counsel.**
54
+
55
+ CASL: when a form collects a newsletter sign-up, the `ca` **and** `qc` privacy policies now include a short "Commercial Electronic Messages" paragraph (consent, sender identification, unsubscribe). Existing `ca` documents change only for sites with such a form. No CSS change; no change to `ca` documents otherwise.
56
+
57
+ - 06d6112: Fix the EmDash `/design-system` admin page erroring on a tailwind site. It fetches `/vitops/design-manifest.json`, which the `tailwind` format never emitted and the integration only mirrored under `editor: true` (which also loads `editor.js` on every public page).
58
+
59
+ - **generator** (`tailwind` format only): `design-manifest.json` is now emitted alongside `tailwind.css`. No change to the CSS.
60
+ - **astro**: new `vitops({ editorManifest: true })` writes everything an outside host needs into `public/vitops/` (`design-manifest.json`, `editor.js`, a standalone `editor.css`, and `design-system-editor.html`) without loading the editor on public pages. `editor: true` still loads it from `<Head />`.
61
+ - **vite**: `editorManifestDir` also writes `vitops/editor.css`, from a throwaway `css`-format pass on tailwind sites.
62
+ - **emdash**: the `/design-system` admin page was unstyled and overflowed the admin, because the editor borrows the framework CSS and the admin never loads it. It now embeds `/vitops/design-system-editor.html` in an iframe, so the design system can't leak into the admin's styles. **Requires `editorManifest: true`** in `vitops({ … })`.
63
+ - **create**: the EmDash template sets `editorManifest: true`. Existing sites: add it to `vitops({ … })` in `astro.config.mjs`.
64
+
65
+ No consumer-facing change in the other toolchain packages.
66
+
67
+ - 6297337: `organization.address.streetAddress` is now optional (a home-based or service-area business with no public street address), and `organization.description` is a new optional, localisable field. The generated legal documents omit an absent street line; JSON-LD builders already skipped absent address fields. Non-breaking; no CSS change.
68
+ - 55061e2: **Added: `<SiteNav>` and `<SiteFooter>` for Astro/EmDash sites.** Both are tier-1 markup over the existing `sitenav` pattern (no runtime JS: `<details>` accordions + Popover API drawer). Non-breaking.
69
+
70
+ - `<SiteNav {...await loadNavMenus(getMenu)} />` renders EmDash menu `primary` (the default: depth 3, dropdowns from the `md` container width, drawer below). If a `primary-mobile` menu exists it replaces the nav below `md` (depth 1); if `primary-mega` exists it replaces it from `xl` (mega panels). Adding the menu in the admin is the whole change. Only one tier is in the accessibility tree at a time. An item's `titleAttr` is its description (EmDash has no description field yet).
71
+ - `<SiteFooter sitemap legal name>` renders `footer-sitemap` + `footer-legal` + a copyright line.
72
+ - `@getvitops/astro/components/sitenav.ts` exports the pure helpers (`loadNavMenus`, `selectTiers`, ...).
73
+ - CSS (`patterns/sitenav.css`, **all formats**): tier visibility classes (`.sitenav-tier--below-*`/`--from-*`), `.sitenav--mega`, `.sitenav__desc`, `.sitefooter*`, `aria-current` styling, reduced-motion and forced-colors handling. Existing `.sitenav` rules are unchanged, so the Bricks element is unaffected.
74
+ - `@getvitops/create` (emdash template): the layout now renders header and footer through these components, with the colour-scheme toggle in the header. It reads menus `primary`, `footer-sitemap` and `footer-legal`; a template whose seed still has the old `footer` menu renders no footer links until the seed is updated.
75
+ - `@getvitops/generator`: `TIERS.sitenav` now lists the two Astro components.
76
+
77
+ ### Patch Changes
78
+
79
+ - Updated dependencies [161c7f3]
80
+ - Updated dependencies [175d16e]
81
+ - Updated dependencies [83d6f3c]
82
+ - Updated dependencies [b47df4d]
83
+ - @getvitops/utils@8.3.0-alpha.0
84
+ - @getvitops/config@8.3.0-alpha.0
85
+
86
+ ## 8.2.0
87
+
88
+ ### Minor Changes
89
+
90
+ - 65a13cc: **New: `site.experiments`** — runtime, per-visitor A/B content experiments for a page that
91
+ must stay at one URL (unlike `site.abTesting`'s existing build-time variant merge, which is
92
+ for a full design-system/CSS-level test via two manual deploys — `site.experiments` is the
93
+ opposite granularity: one page, one cookie, content branched in the page itself). Keyed by
94
+ an open experiment name, so more than one can run at once, each with its own consent
95
+ category, cookie name/lifetime, split ratio and pair of variant names.
96
+
97
+ **New `vitopsAbTesting()` middleware** (`@getvitops/astro/middleware`) — the first Astro
98
+ middleware this toolchain ships. Wire it into your own `src/middleware.ts`:
99
+
100
+ ```ts
101
+ import { vitopsAbTesting } from '@getvitops/astro/middleware';
102
+ import config from '../site.json' with { type: 'json' };
103
+
104
+ export const onRequest = vitopsAbTesting({
105
+ experiments: config.site.experiments ?? {},
106
+ });
107
+ ```
108
+
109
+ It assigns and cookie-pins a visitor to one of an experiment's two variants on first visit,
110
+ by weighted random (`splitRatio`) — and never touches an already-set cookie, so a recognized
111
+ query parameter named after the cookie itself (`?_ab_<key>=<variant>`) reliably forces that
112
+ variant, even overriding an existing assignment. Combined with `splitRatio: 0`, this is a
113
+ blue/green-style "100% on control, but a shared link reveals the other variant" mode with no
114
+ special-cased logic.
115
+
116
+ A page reads its own experiment's cookie (`Astro.cookies.get(cookieName)?.value`) and
117
+ renders whichever of its two hand-written blocks matches — there's no config-schema
118
+ representation of "the variant content," which is what keeps this lightweight regardless of
119
+ how many experiments are configured.
120
+
121
+ **`<Analytics />` gained a reporting hook**: each configured provider's bootstrap now
122
+ attaches an enabled experiment's assignment to the real pageview via that provider's own
123
+ mechanism — Plausible via `plausible.init({customProperties})`, Matomo via
124
+ `setCustomDimension` (needs a new `site.analytics.matomo.abVariantDimensions` entry — the
125
+ dimension must exist in your Matomo instance first), GA4 via a runtime-merged
126
+ `gtag('config', …)` call. A no-op when `site.experiments` is unset — zero behavior change
127
+ for every existing site.
128
+
129
+ Also new: `experimentDimensionWarnings(site)` (`@getvitops/generator`) — a build-time warning
130
+ (not a hard error) when an enabled experiment's site also configures Matomo but has no
131
+ matching `abVariantDimensions` entry.
132
+
133
+ **Not included in this pass, deliberately**: a mechanism for a fully prerendered
134
+ (`output: 'static'`) page — this needs a genuinely separate Cloudflare Worker Route + two
135
+ pre-built static variants, real but bounded follow-up work. Everything above applies to an
136
+ on-demand-rendered page (the current EmDash templates' default), which needs zero new
137
+ deploy infrastructure.
138
+
139
+ - 67e2088: **Config validation now rejects unknown keys instead of silently stripping them.** Every
140
+ object in `ConfigSchema` (the `Config` type — `organization`/`site` and everything nested under
141
+ them) switched from `z.object` to `z.strictObject`, matching what the published JSON Schema has
142
+ always asserted (`additionalProperties: false` at ~95 points) but the runtime validator never
143
+ actually enforced. A typo'd field (`site.ntoifications`, an add-on key on the wrong nesting
144
+ level, …) now fails `validateConfig()`/`vitops validate` with an `unrecognized_keys` error naming
145
+ the bad key, instead of validating successfully and doing nothing. `site.icons` stays
146
+ deliberately loose (`z.looseObject`, unchanged) — its per-set keys are open-ended by design.
147
+
148
+ **Breaking for any config carrying a field that isn't in the schema** — this is the point of the
149
+ change, but if a real config was relying on the old silent-strip behavior (an extra key kept
150
+ around for another tool to read), move it under a block the schema already permits `z.unknown()`
151
+ on (`site.cloudflare`, `site.tags`, `site.postTypes`) or open an issue.
152
+
153
+ Also:
154
+
155
+ - Added `configVersion` (optional, currently `1`) to `Config` — the document's own SHAPE
156
+ version, distinct from this package's release version (which is CSS-driven and bumps on every
157
+ design-system change regardless of `Config`'s shape). `defaultSiteConfig()` stamps it.
158
+ Exported as `CONFIG_VERSION`.
159
+ - Re-exported several `Config`-related types/values that were defined but missing from the
160
+ package root: `SiteReviews`, `SiteReviewLocation`, `SiteSearchConsoleSetup`,
161
+ `CrmProviderConfig`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`.
162
+ - Added a `@getvitops/generator/config` subpath export — `ConfigSchema`, `Config`,
163
+ `validateConfig`, `resolveConfig`, `isConfig`, `resolveInput` and friends, on their own, with no
164
+ `node:fs`/`node:crypto`/lightningcss in the import graph (verified: `publint` clean, and the
165
+ built chunk's own imports are `@zod/mini` + `@getvitops/utils`'s Worker-safe `ads`/`tracking`/
166
+ `color` subpaths only). This is what makes `Config` bundleable in a Cloudflare Worker — the
167
+ EmDash plugin's own admin route being the motivating case — without pulling in code that only
168
+ ever runs at build time.
169
+
170
+ - 89265af: **New package: `@getvitops/config`** — the site-config provider blocks (`site.crm`, `site.ads`,
171
+ `site.reviews`, `site.seo.indexing`, `site.searchConsole`, `site.notifications`, `site.tracking`,
172
+ `site.security`, `site.deployment`) as zod schemas and inferred types, with the closed provider lists
173
+ (`AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`) on a zod-free `./providers` entry. It joins the
174
+ `fixed` group.
175
+
176
+ Why: `@getvitops/utils` holds the provider code but could not import its configuration types — the schema
177
+ lived in `@getvitops/generator`, a layer above — so it kept a hand-copied mirror beside each module,
178
+ an adapter converting into it, and a test holding the copy to the schema. The mirrors are gone.
179
+
180
+ - **`@getvitops/generator`**: `AD_PROVIDERS`, `CRM_PROVIDERS`, `BIGIN_DATA_CENTERS`, `AdProvider` and
181
+ `CrmProviderConfig` are re-exported unchanged. No import path changes.
182
+ - **`@getvitops/utils`**: the config types in `./crm`, `./ads`, `./indexing`, `./onboarding`, `./reviews`,
183
+ `./tracking` and `./notify` (`CrmConfig`, `HubSpotConfig`, `BiginConfig`, `JobberConfig`,
184
+ `IndexNowConfig`, `SearchConsoleConfig`, `ReviewsConfig`, `ReviewLocationConfig`, `TrackingConfig`,
185
+ `NotificationsConfig`, `EmailChannelConfig`, `DomainSetup`, `AdPropertySetup`) are now inferred from the
186
+ schema. Same names, same fields, but **optional fields now follow zod's inference**, so a hand-written
187
+ object literal typed against them may need `| undefined` handled. `AdPropertySetup` gains the fields the
188
+ schema always had (`conversionActionId`, `reporting`).
189
+ - **`@getvitops/astro`**: `GetvitopsAdProperty` and `GetvitopsTrackingOptions` are aliases of the schema
190
+ types. `toCrmConfig()` and `toGoogleAdsConfig()` are **deprecated** — they returned their argument's
191
+ field unchanged; pass `config.site.crm` / `config.site.ads?.google` directly.
192
+ - **`@getvitops/cli`**: `vitops ads` now passes `conversionActionId` and `reporting` through to the planner;
193
+ it previously copied a hand-picked subset of the block and dropped them.
194
+
195
+ Formats reached: none — no CSS change.
196
+
197
+ - 0574388: Add `site.crm` — push form submissions into HubSpot, Zoho Bigin, or Jobber.
198
+
199
+ Client sites need form submissions to land in the client's CRM. The options available before this
200
+ — a HubSpot/Bigin/Jobber iframe or JS embed — fail on three counts: the markup can't be
201
+ white-labelled against this design system, nothing can operate on the submission before it leaves
202
+ the page, and the submission never touched `createConversionRoute()`, so `_ac` ad-click attribution
203
+ was lost and no conversion notification was planned.
204
+
205
+ This adds one server-side path for all three CRMs, hung off the conversion route that already
206
+ exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
207
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
208
+ option, and are never config. A failed CRM write never fails the request and is folded into the
209
+ notification email's new "Delivery" section, so a broken CRM token is never silent.
210
+
211
+ New `@getvitops/utils/crm` subpath: `deliverToCrm()`, `mapSubmission()`, `deriveLastName()` (Bigin
212
+ requires a last name; this derives one from an explicit field, a full name, or an email address,
213
+ refusing rather than fabricating one), `submitToHubSpot()`, `submitToBigin()`, `submitToJobber()`.
214
+ `identifySubmitter()` was promoted out of `notify`'s private name-guessing cascade into
215
+ `@getvitops/utils/tracking`, since the CRM mapper needed the whole identity (first/last name,
216
+ email, phone, company), not just a display name.
217
+
218
+ Jobber is GraphQL, not REST, and answers `200 OK` for failures — a rejected `clientCreate` returns
219
+ `userErrors[]`, a malformed request returns top-level `errors[]`. `submitToJobber()` classifies on
220
+ the response body, not just the status. Its refresh tokens also rotate by default: **Refresh Token
221
+ Rotation must be turned off** in the app's Developer Center settings, since a Worker reading a
222
+ static `VITOPS_JOBBER_REFRESH_TOKEN` cannot persist a rotated one — a rotated token is detected at
223
+ exchange time and reported with the fix, rather than surfacing an hour later as an opaque 401.
224
+ Attribution and unmapped extra fields render into a best-effort note by default (works on every
225
+ Jobber plan); a resubmitting visitor creates a second Jobber client unless `crm.jobber.dedupe: true`
226
+ is set once its lookup query has been verified against a live account (defaults `false` pending
227
+ that verification — see `jobber.ts`'s docblock).
228
+
229
+ New `site.crm` config block (`provider`, `hubspot`/`bigin`/`jobber`, `fieldMap`, `trackingFields`)
230
+ and two CLI commands: `vitops crm check` (fetches the live HubSpot property list or Bigin field
231
+ list and reports every mapped name that doesn't exist there — HubSpot rejects an unknown field
232
+ outright, Bigin silently drops it; Jobber has nothing live to check yet, since its mapped values
233
+ currently render as note text) and `vitops crm setup` (creates missing HubSpot properties; Bigin
234
+ and Jobber have no such API, so both always print a manual reminder).
235
+
236
+ The generated privacy policy now names HubSpot, Zoho Bigin, or Jobber when `site.crm` configures
237
+ one. Bigin gets the same cross-border-transfer derivation Matomo Cloud already gets: a
238
+ `dataCenter: 'ca'` asserts no transfer outside Canada, any other data centre does. Jobber's
239
+ operator (Octopusapp Inc. d/b/a Jobber) is Canadian, but its own privacy policy names the United
240
+ States as a processing location too, so the policy always asserts a cross-border transfer for
241
+ Jobber regardless of config — a Canadian operator is not read as Canadian-only storage.
242
+
243
+ Credentials: `VITOPS_HUBSPOT_ACCESS_TOKEN` for HubSpot; `VITOPS_ZOHO_CLIENT_ID` /
244
+ `VITOPS_ZOHO_CLIENT_SECRET` / `VITOPS_ZOHO_REFRESH_TOKEN` for Bigin; `VITOPS_JOBBER_CLIENT_ID` /
245
+ `VITOPS_JOBBER_CLIENT_SECRET` / `VITOPS_JOBBER_REFRESH_TOKEN` for Jobber. Never config.
246
+
247
+ No consumer-facing change in `@getvitops/astro` beyond the new `crm` route option and a build
248
+ warning when `site.crm` is configured with no route answering `TRACKING_ENDPOINT` — CSS/Bricks/
249
+ tailwind outputs are unaffected.
250
+
251
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
252
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
253
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
254
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
255
+ offered, now usable by the client directly, no dev server required. An edit is visible
256
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
257
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
258
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
259
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
260
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
261
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
262
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
263
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
264
+
265
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
266
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
267
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
268
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
269
+ plugin-install time.
270
+
271
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
272
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
273
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
274
+ script delivery). No change if you don't use `designSystemEditor`.
275
+
276
+ Also new, supporting the above:
277
+
278
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
279
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
280
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
281
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
282
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
283
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
284
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
285
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
286
+ `site.deployment.repo`/`branch` config fields.
287
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
288
+ wrapper over GitHub's Contents API.
289
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
290
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
291
+ route instead of the dev-only Vite endpoint.
292
+
293
+ - 0574388: Add `vitops email setup|check` — connect `site.notifications.email.from` to whether its domain is
294
+ actually onboarded onto an outbound email provider.
295
+
296
+ Before this, nothing checked that. `EMAIL.send()` throws `E_SENDER_NOT_VERIFIED` at runtime with no
297
+ build or deploy-time warning — the failure mode is silent and production-only.
298
+
299
+ `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
300
+ and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
301
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
302
+ scripts now run first (`--strict` makes it a real gate; opt out per-site with
303
+ `site.notifications.email.verifySender: false`, or globally with `--skip` /
304
+ `VITOPS_SKIP_EMAIL_CHECK`).
305
+
306
+ Two things worth knowing before running it:
307
+
308
+ - **The onboarded entity is an exact name, and it is usually a subdomain**
309
+ (`send.yourdomain.com`), never the apex your `from` address names. When the exact domain isn't
310
+ onboarded but a sibling in the same zone already is, the command reports that as the fix — send
311
+ from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
312
+ `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
313
+ - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
314
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
315
+ at a computed `from` value.
316
+
317
+ New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
318
+ Needs `CLOUDFLARE_API_TOKEN` with an Email Sending scope in addition to the `Zone:Read` other
319
+ commands already use.
320
+
321
+ - 1a8231d: **EmDash blocks are now a tier of the component manifest, and there are four more of them.**
322
+
323
+ `@getvitops/emdash` adds `vitops.gallery`, `vitops.cards`, `vitops.accordion` and `vitops.marquee`
324
+ (each a thin adapter over the matching `@getvitops/astro` component), and `vitops.carousel` now edits
325
+ its slides as a **Slides repeater** instead of two newline-separated fields. Blocks saved before this
326
+ keep rendering: with no `slides`, the carousel still reads `images`/`alts`, and both legacy fields stay
327
+ in the editor, labelled as legacy. The editor's field type now admits `repeater` and `media_picker`.
328
+
329
+ - **New `blocks` plugin option** — `vitopsEmdash({ blocks: { include, exclude, extra } })`. `include` is
330
+ an allowlist, `exclude` drops blocks, `extra` adds the site's own **editor** definitions (the site
331
+ supplies their renderers via `<PortableText components={{ type }}>`, the same route that overrides a
332
+ `vitops.*` renderer). An unknown name or a colliding `type` throws at plugin construction.
333
+ - Link and image URLs in blocks with a scheme other than `http`, `https`, `mailto` or `tel` are
334
+ dropped (see the security entry).
335
+
336
+ `@getvitops/generator`: `TierEntry` gains an optional `emdash: string[]` (block `_type`s) and `Tier`
337
+ gains `'emdash'`, so `tierPatterns('emdash')` projects it. **Breaking only for code that exhaustively
338
+ switches on `Tier` or builds a `Record<Tier, …>`** — add an `emdash` case. The agent doc
339
+ `vitops docs components` gains an "EmDash blocks" table and the docsite a `components/emdash` page. The
340
+ guard that every block and every `emdash` entry agree lives in `@getvitops/emdash`'s `blocks.test.ts`,
341
+ because the generator cannot import the plugin.
342
+
343
+ `@getvitops/create`: the EmDash template's agent guidance explains the blocks, their limits (a block
344
+ cannot contain blocks; `vitops.*` blocks do not render inside EmDash's built-in `columns`) and the
345
+ `blocks` option.
346
+
347
+ Formats reached: none — no CSS change. Other packages in the `fixed` group: No consumer-facing change
348
+ in this package.
349
+
350
+ - 8d8aad7: **New: server-side Google Ads offline conversion upload.** Set
351
+ `site.ads.google.reporting: "server"` and a matching
352
+ `conversionActionId` (a numeric id or full
353
+ `customers/…/conversionActions/…` resource name — **not** the same value
354
+ as `conversionLabel`, which is the opaque label the web tag uses and
355
+ which the Ads API's upload endpoint doesn't accept) and
356
+ `createConversionRoute`'s new `googleAdsOffline` option uploads the
357
+ conversion via `ConversionUploadService.UploadClickConversions` instead
358
+ of relying on the visitor's browser to fire `gtag.js`. An action with no
359
+ matching `conversionActionId` keeps firing client-side regardless of
360
+ `reporting`, so turning it on before the id is filled in never silently
361
+ drops a conversion. `@getvitops/astro/routes` exports a matching
362
+ `toGoogleAdsConfig(config)` adapter, alongside `toCrmConfig`.
363
+
364
+ Structurally typed against Google's documented REST shape, not verified
365
+ against a live Ads account — no developer-token/MCC access exists
366
+ anywhere in this toolchain's own setup to test against. Wrapped so a
367
+ wrong guess degrades to a stated failure reason (folded into the
368
+ conversion email's "Delivery" section, same as a failed CRM write) rather
369
+ than breaking, the same caveat `@getvitops/utils/crm`'s Jobber executor
370
+ already carries for its own unverified surfaces.
371
+
372
+ `vitops ads lint` now flags a `reporting: "server"` property whose
373
+ configured actions have no matching `conversionActionId` — not a
374
+ failure, since that action keeps firing client-side, but worth surfacing
375
+ rather than leaving someone to wonder why nothing changed.
376
+
377
+ Not built: `vitops ads provision` (upserting conversion actions via
378
+ `ConversionActionService` and printing back both the label and the
379
+ resource id) — same credential blocker, and deliberately left unscoped
380
+ until there's a live account to verify the request/response shape
381
+ against.
382
+
383
+ - dde3df3: **`site.ads.google.conversionLabel` now accepts a per-action map** —
384
+ `{ call?, email?, form? }` — instead of one bare string shared by every
385
+ interaction. A bare string still works (back-compat, treated as `call`), but a
386
+ call and an email click used to report as the **same** Google Ads conversion
387
+ action, indistinguishable in the account; each action now gets its own
388
+ `send_to` target.
389
+
390
+ **New: `vitopsFormConversion(formEl)`**, exposed as `window.vitopsFormConversion`
391
+ by `<Tracking />`'s capture script — call it from your own fetch-based form
392
+ submit handler once the POST to `createConversionRoute()` succeeds, to fire the
393
+ `form` Google Ads conversion action client-side (mirroring the existing
394
+ tel:/mailto: click handler, which now fires `call`/`email` instead of sharing
395
+ one label). Idempotent per form element. There's deliberately no server-side
396
+ equivalent for a no-JS `<form method="post">` submission — that's a full-page
397
+ redirect with no script to fire from, and a real server-side Google Ads
398
+ conversion needs the Ads API's click-conversion upload (developer token + MCC
399
+ access), tracked separately in the vNext backlog.
400
+
401
+ **Fixed: the conversion route's rate limiter no longer trusts the client's
402
+ raw `event` field.** It used to key on whatever string a client sent
403
+ (`` `${ip}:${body.event}` ``), so varying that value on every request let a
404
+ form submission dodge the per-IP rate limit entirely — only `'call'`/`'email'`
405
+ were ever recognized, and anything else got its own fresh bucket. The key now
406
+ uses the server's own classification (`call` | `email` | `form`), closing that
407
+ bypass without changing the existing call-vs-email-don't-compete behavior.
408
+
409
+ **New: `site.notifications.actionUrl`** — an optional action-link template
410
+ added to the conversion-notification email as an "Actions" section.
411
+ `{clickId}` is replaced with the ad click ID captured on `_ac` (gclid, fbclid,
412
+ etc.); the section is omitted when the event has no click ID to interpolate,
413
+ rather than emailing a broken link.
414
+
415
+ - d9f213b: **New: `site.plan`** — records a commercial plan/add-on entitlement (`{ key, addons? }`),
416
+ distinct from a section's own `enabled` flag: `enabled` answers "does this site do X at all"
417
+ (a capability switch); `plan` answers "is this client entitled to turn X on" (a commercial
418
+ switch one layer above). Absence means unrestricted, matching every other opt-in block in
419
+ `Config`.
420
+
421
+ `PLAN_CAPABILITIES` (new export) is the small, hand-authored table of which add-on gates
422
+ which `Config` field — today: `ab_testing` gates `site.abTesting.enabled`, any
423
+ `environments.*.variant`, **and any enabled `site.experiments` entry** (that last one was
424
+ missed in this feature's first pass and fixed before release — found while planning this
425
+ toolchain's downstream adoption, not by a consumer hitting it), `server_side_tracking` gates
426
+ `site.tracking.enabled`. `validateConfig` now rejects a config that turns on a gated feature
427
+ without listing the owning add-on in `site.plan.addons`, naming the add-on in the error.
428
+
429
+ **Breaking**: an existing config using `site.abTesting.enabled`/`environments.*.variant` or
430
+ `site.tracking.enabled` now needs a matching `site.plan.addons` entry, or `validateConfig`
431
+ rejects it. Add `site.plan: { key: '<your-plan>', addons: ['ab_testing'] }` (and/or
432
+ `'server_side_tracking'`) to a config already using either feature.
433
+
434
+ - 7fd5f8f: **`site.seo` is now the single source of site-level SEO defaults** (ADR-0002). Until now `site.seo`
435
+ declared `titleTemplate`, verification tokens, `openGraph` and `twitter` that nothing read, while
436
+ `<Seo />` took a second, differently-shaped vocabulary through `vitops({ seo })` that no other stack
437
+ could see.
438
+
439
+ - `SeoSchema` moves to `@getvitops/config` and gains what only the integration had: `siteName`,
440
+ `defaultTitle`, `defaultDescription`, `openGraph.localeAlternates` and `openGraph.image` as
441
+ `{ src, alt, width, height, type }`. Text fields are localisable.
442
+ - `@getvitops/astro` exports `resolveSeoDefaults(config, { locale, env })`. The integration reads
443
+ `site.seo` from the site config it already loads; `siteName` falls back to `organization.name`, and
444
+ the environment's `robots` wins over `site.seo.robots`, the same fact `vitops robots` and
445
+ `search notify` read.
446
+ - `vitops({ seo })` keeps working unchanged and is now an **override** merged over the config, field by
447
+ field (`openGraph` and `twitter` merge key by key).
448
+ - **Removed from the config schema** (nothing read them): `descriptionTemplate` → use
449
+ `defaultDescription`; `openGraph.siteName`/`title`/`description` and
450
+ `twitter.title`/`description`/`image` → set per page on `<Seo />`, or the site-wide values above.
451
+ A config that still carries one now fails `vitops validate` with an unrecognised-key error.
452
+ - `openGraph.type` is now `website | article | profile`.
453
+
454
+ ### Patch Changes
455
+
456
+ - 779d22e: Add a `vitops.hero` EmDash block (eyebrow, heading, text, two links, optional image; centered or split layout) built on the `.centered`/`.split` layout primitives. The EmDash template's home page now seeds one of each of hero, cards, accordion and marquee. `@getvitops/generator`: the `split` pattern lists the new block in `TIERS`.
457
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
458
+ both projects' GA releases.
459
+
460
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
461
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
462
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
463
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
464
+ code here needed to change, only the declared range.
465
+
466
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
467
+ the version bump:
468
+
469
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
470
+ `css.format: 'tailwind'` without installing either, which threw on build.
471
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
472
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
473
+ the build never generated.
474
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
475
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
476
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
477
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
478
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
479
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
480
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
481
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
482
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
483
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
484
+ Uncomment it once scaffolded into its own repo.
485
+
486
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
487
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
488
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
489
+ now names the bad key instead of reporting a bare "Invalid input").
490
+
491
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
492
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
493
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
494
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
495
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
496
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
497
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
498
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
499
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
500
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
501
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
502
+ evolving default) and making the template a real pnpm workspace member (would change how its
503
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
504
+
505
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
506
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
507
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
508
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
509
+ compose with vite-plus's own `defineConfig`). A type-level shim
510
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
511
+ runtime `vp test` failure for this one file is still open.
512
+
513
+ - 1a3e811: **Fixed: `<Icon />` no longer crashes a Cloudflare Workers SSR page.** It statically imported
514
+ `loadIconSvg`/`spriteId` from `@getvitops/generator`'s root entry, which also re-exports
515
+ `generate.ts` — reaching lightningcss, whose native binary Workers cannot `require()`.
516
+ Rendering `<Icon>` anywhere in an SSR project 500'd every request, regardless of
517
+ `icons.engine`, since the import itself (not just calling the function) pulled lightningcss
518
+ into the bundle. Fixed by publishing a new Worker-safe `@getvitops/generator/icons-sprite`
519
+ subpath (`buildIconSprite`/`loadIconSvg`/`spriteId`, no lightningcss in its import graph) and
520
+ switching `<Icon />` to import from there.
521
+ - Updated dependencies [89265af]
522
+ - Updated dependencies [0574388]
523
+ - Updated dependencies [c3c1bcb]
524
+ - Updated dependencies [0574388]
525
+ - Updated dependencies [0574388]
526
+ - Updated dependencies [8d8aad7]
527
+ - Updated dependencies [0574388]
528
+ - Updated dependencies [0574388]
529
+ - Updated dependencies [dde3df3]
530
+ - Updated dependencies [7f4f50e]
531
+ - Updated dependencies [7fd5f8f]
532
+ - Updated dependencies [ceb8310]
533
+ - @getvitops/config@8.2.0
534
+ - @getvitops/utils@8.2.0
535
+
3
536
  ## 8.2.0-alpha.0
4
537
 
5
538
  ### Minor Changes
@@ -93,7 +626,7 @@
93
626
 
94
627
  This adds one server-side path for all three CRMs, hung off the conversion route that already
95
628
  exists. `createConversionRoute()` gets a new `crm` option (`{ config, credentials, fetch?,
96
- timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
629
+ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the existing `binding`
97
630
  option, and are never config. A failed CRM write never fails the request and is folded into the
98
631
  notification email's new "Delivery" section, so a broken CRM token is never silent.
99
632
 
@@ -138,8 +671,8 @@ timeoutMs? }`); credentials arrive as a function of `locals`, exactly like the e
138
671
  tailwind outputs are unaffected.
139
672
 
140
673
  - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
141
- designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
142
- './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
674
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
675
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
143
676
  `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
144
677
  offered, now usable by the client directly, no dev server required. An edit is visible
145
678
  site-wide immediately (a runtime `:root` override, served via a new plugin API route and
@@ -186,7 +719,7 @@ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import c
186
719
 
187
720
  `vitops email setup [domain]` onboards a sending domain (currently Cloudflare Email Sending only)
188
721
  and polls DNS until its SPF/DKIM/DMARC records resolve or a timeout is reached; `vitops email
189
- check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
722
+ check` is the non-blocking deploy guard both `@getvitops/create` templates' `deploy`/`deploy:dev`
190
723
  scripts now run first (`--strict` makes it a real gate; opt out per-site with
191
724
  `site.notifications.email.verifySender: false`, or globally with `--skip` /
192
725
  `VITOPS_SKIP_EMAIL_CHECK`).
@@ -198,7 +731,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
198
731
  from the onboarded subdomain — rather than silently trying to onboard the apex, which rewrites
199
732
  `_dmarc.<domain>` and can tighten DMARC policy on every other mail your domain sends.
200
733
  - Candidates come from `site.notifications.email.from` (or its derived `noreply@<domains
201
- .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
734
+ .canonical>` default) first; a best-effort scan of `--src` only supplements it, and never guesses
202
735
  at a computed `from` value.
203
736
 
204
737
  New `@getvitops/utils/email` subpath and `site.notifications.email.verifySender` config field.
@@ -280,7 +813,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
280
813
  needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
281
814
  which only a human can set — run `emdash migrate --status` against the real deployed database,
282
815
  review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
283
- <value>`. Deploys fail clearly on the migration-apply step until it's set.
816
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
284
817
  - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
285
818
  not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
286
819
  errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
@@ -623,7 +1156,7 @@ check` is the non-blocking deploy guard both `@getvitops/create` templates' `dep
623
1156
  It is **required for a user credential and wrong for a service account**, which is why it is not
624
1157
  sent unconditionally. A user credential authenticates through a shared OAuth client that owns no
625
1158
  project, so Google refuses the call outright — measured: `403 "requires a quota project, which is
626
- not set by default"`. A service account already belongs to a project, and pointing it at a
1159
+ not set by default"`. A service account already belongs to a project, and pointing it at a
627
1160
  different one would newly require `serviceusage.services.use` there, turning a working CI
628
1161
  credential into a 403 for no benefit.
629
1162
 
@@ -648,7 +1181,7 @@ not set by default"`. A service account already belongs to a project, and pointi
648
1181
  **Fixed: an ADC file in `GOOGLE_APPLICATION_CREDENTIALS` no longer hard-exits.** That variable is
649
1182
  Google's own convention _and_ where gcloud writes ADC, so the same variable carries two kinds of
650
1183
  credential. Parsing an `authorized_user` file as a service account failed with `service account
651
- JSON is missing client_email or private_key` — while holding a credential the command already knew
1184
+ JSON is missing client_email or private_key` — while holding a credential the command already knew
652
1185
  how to use. It is now discriminated on `type`, the field Google's own libraries switch on.
653
1186
 
654
1187
  **Also fixed — `search setup --dry` and `--check` are not offline.** The docs implied otherwise. The
@@ -729,7 +1262,7 @@ JSON is missing client_email or private_key` — while holding a credential the
729
1262
  created, with `patterns/counter.css`'s own reduced-motion query as a second belt.
730
1263
  - The accessible name stays the final value for the whole animation: `.counter__value` is toggled
731
1264
  to the framework's `.sr-only` utility while running, never hidden — `display: none`/`visibility:
732
- hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
1265
+ hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
733
1266
  screen reader must never be read ~60 intermediate numbers. The animated text lives in a separate
734
1267
  `aria-hidden` presentation span it never reaches.
735
1268
  - `data-from` (default `0`), `data-duration` (ms, default `1200`), `data-easing` (`linear` |
@@ -1488,28 +2021,20 @@ hidden` would pull it out of the accessible-name computation along with the visu
1488
2021
  {
1489
2022
  "designSystem": {
1490
2023
  "themes": {
1491
- "default": {
1492
- /* … */
1493
- },
2024
+ "default": {/* … */},
1494
2025
  },
1495
2026
  },
1496
2027
  "organization": {
1497
2028
  "name": "Acme",
1498
2029
  "contact": "hq",
1499
2030
  "locations": {
1500
- "hq": {
1501
- /* … */
1502
- },
2031
+ "hq": {/* … */},
1503
2032
  },
1504
2033
  },
1505
2034
  "site": {
1506
2035
  "defaultLocale": "en",
1507
- "domains": {
1508
- /* … */
1509
- },
1510
- "analytics": {
1511
- /* … */
1512
- },
2036
+ "domains": {/* … */},
2037
+ "analytics": {/* … */},
1513
2038
  },
1514
2039
  }
1515
2040
  ```
@@ -1603,7 +2128,7 @@ hidden` would pull it out of the accessible-name computation along with the visu
1603
2128
  `animate-view` / `animate-scroll` / `animate-trigger` / `transition`, and every effect
1604
2129
  gained an `open-<fx>` state variant. Overlays now state _where they start_
1605
2130
  (`--translate-x-from`) instead of owning a keyframe: `class="drawer drawer--right
1606
- open-fade-in"` composes slide and fade. Applied at zero specificity with identity
2131
+ open-fade-in"` composes slide and fade. Applied at zero specificity with identity
1607
2132
  defaults, so a popover that sets no effect vars is unchanged.
1608
2133
  - **One scrim.** `--scrim` / `--scrim-filter` tokens and `.no-scrim` in `popover.css`,
1609
2134
  replacing 18 `::backdrop` blocks across seven partials. `.drawer--modeless` is aliased.
@@ -1918,8 +2443,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
1918
2443
  replace it with `--anim-range`. `--stagger-range-step` is now a viewport length (`5vh`) to match.
1919
2444
 
1920
2445
  **Fixed: every journey ran on a truncated range.** The generator emitted `animation-range: entry
1921
- exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
1922
- 100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
2446
+ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
2447
+ 100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
1923
2448
  `from` state — as the element hits the top of the viewport, which is to say they faded out while
1924
2449
  still fully on screen. Journeys now run `entry calc(50% + 10vh) → exit 100%`: they start on the same
1925
2450
  midpoint pivot as everything else and keep the full entry → hold → exit arc, so the hold sits in the
@@ -1932,7 +2457,7 @@ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than th
1932
2457
  so one knob tunes both. If your own config has an empty `slide` base, add the same line.
1933
2458
 
1934
2459
  **Fixed: `reveal-*` on hover was unreachable.** A `hover-reveal-left` element rests at `clip-path:
1935
- inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
2460
+ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
1936
2461
  hittable area and could never receive the hover that would reveal it. The state variants now match
1937
2462
  the element **or its direct parent**, mirroring what `animation.css` already did for the trigger
1938
2463
  driver (`:is(.is-active, [data-active]) > .animate-trigger`):