@getvitops/core 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.
package/CHANGELOG.md CHANGED
@@ -1,12 +1,194 @@
1
1
  # @getvitops/core
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
+ - 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.
43
+
44
+ - `<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).
45
+ - `<SiteFooter sitemap legal name>` renders `footer-sitemap` + `footer-legal` + a copyright line.
46
+ - `@getvitops/astro/components/sitenav.ts` exports the pure helpers (`loadNavMenus`, `selectTiers`, ...).
47
+ - 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.
48
+ - `@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.
49
+ - `@getvitops/generator`: `TIERS.sitenav` now lists the two Astro components.
50
+
51
+ ## 8.2.0
52
+
53
+ ### Minor Changes
54
+
55
+ - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
56
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
57
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
58
+ `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
59
+ offered, now usable by the client directly, no dev server required. An edit is visible
60
+ site-wide immediately (a runtime `:root` override, served via a new plugin API route and
61
+ `page:fragments`) and — once `site.deployment.repo`/`branch` and a `VITOPS_GITHUB_TOKEN`
62
+ secret are set (see the scaffold's `SETUP.html`) — auto-commits to your `dev` branch, so it
63
+ reaches the git-tracked `site.json` through your repo's normal review path rather than
64
+ becoming a second, unreviewed source of truth. Until that's configured, the live half still
65
+ works; only the auto-commit half is skipped (and says so in the admin page). The save route
66
+ runs the exact same accessibility (APCA contrast) gate `generate()` enforces at build time,
67
+ synchronously, before persisting — nothing non-compliant ever goes live even momentarily.
68
+
69
+ Requires `@astrojs/react` + `react` + `react-dom` (already scaffold dependencies, for
70
+ EmDash's own admin) and `@getvitops/core` (added as a new scaffold dependency this release).
71
+ Declares two new plugin capabilities when enabled: `network:request` (for the GitHub
72
+ auto-commit) and `hooks.page-fragments:register` (for the live override) — reviewed once at
73
+ plugin-install time.
74
+
75
+ **Breaking for `@getvitops/emdash`**: `vitopsEmdash()`'s `hooks.page-fragments:register`
76
+ capability is now also granted whenever `designSystemEditor` is set, independent of the
77
+ existing `scripts: 'fragments'` option (which governs something unrelated — web-component
78
+ script delivery). No change if you don't use `designSystemEditor`.
79
+
80
+ Also new, supporting the above:
81
+
82
+ - `@getvitops/generator`: `validateColorContract()` and `buildReverseIndex()` (both
83
+ `/tokens` subpath, Worker-safe) — the monotonicity/contrast gate and the
84
+ `design-manifest.json` reverse-index, each extracted from `generate()`'s `build()` into
85
+ standalone, non-throwing functions. `patchToOverrideCss()` turns a design-system patch
86
+ into the `:root` override `<style>` block. `designSystemPath`/`getAt`/`setAt`/
87
+ `mergePatch`/`formatConfig` moved from `@getvitops/vite` into `@getvitops/generator/config`
88
+ (re-exported from `@getvitops/vite` unchanged) — one "patch a config file's design-system
89
+ subtree" implementation shared by the dev endpoint and the new plugin route. New
90
+ `site.deployment.repo`/`branch` config fields.
91
+ - `@getvitops/utils`: new `/git` subpath — `commitFile()`, a Worker-safe (fetch-only)
92
+ wrapper over GitHub's Contents API.
93
+ - `@getvitops/core`: `<wc-theme-editor>` gains a `save-endpoint` attribute (default
94
+ unchanged — `/__vitops/design-system`) so the same element can target the new plugin
95
+ route instead of the dev-only Vite endpoint.
96
+
97
+ - 88b465a: **Accessibility and layout fixes for the web components.**
98
+
99
+ - **`<wc-entries>`: the column selector is no longer `aria-hidden`.** Its buttons are real and
100
+ focusable, so hiding the `<nav>` left a keyboard user on a control that assistive tech never
101
+ announced. It is now exposed as a `Column selector` navigation. The generated table stays
102
+ `aria-hidden` (it duplicates the `<dl>`, which is what assistive tech reads) and contains nothing
103
+ focusable.
104
+ - **`prefers-reduced-motion` is honoured by programmatic scrolling.** `<wc-carousel>` (prev/next and
105
+ marker clicks) and `<wc-tree>` (deep-link scroll) always passed `behavior: 'smooth'` to
106
+ `scrollIntoView`, which overrides the stylesheet's `scroll-behavior` and so ignored the setting.
107
+ They now scroll instantly when it is set, reading the preference at call time.
108
+ - **Pre-upgrade `display` for six more elements** — `wc-entries`, `wc-image-compare`,
109
+ `wc-split-panel`, `wc-multi-field`, `wc-dismissable` (all `block`), `wc-copy` (`inline-flex`) and
110
+ `wc-marquee` (`block`, as a fallback under the `.marquee` class). Each is declared in its pattern's
111
+ partial inside `:where()` (zero specificity, so any consumer rule wins) and matches the element's
112
+ upgraded `:host`, so registering no longer shifts layout. `wc-split-panel`, which declared no
113
+ display at all, is now a block after upgrade too — a visible change if a layout relied on it being
114
+ inline. `wc-counter` is deliberately left inline.
115
+ - **A dismiss button no longer shows before it can work.**
116
+ `wc-dismissable:not(:defined)` hides `.banner__close`, `.notification__close` and `[data-dismiss]`
117
+ buttons, so with no JS a banner has no dead ✕. (`:not(:defined)` is also true until `elements.js`
118
+ has run, so the button appears on upgrade.)
119
+
120
+ Formats reached for the CSS changes: all formats (the partials are components-layer patterns, inlined
121
+ by the tailwind format as well). Other packages in the `fixed` group: No consumer-facing change in this
122
+ package.
123
+
124
+ ### Patch Changes
125
+
126
+ - 734ab60: EmDash gallery and carousel blocks now derive their element id from the block's `_key`, so two blocks with the same label on one page no longer open each other's dialog. `<wc-entries>` column navigation now honours `prefers-reduced-motion`.
127
+ - 0574388: Bump `emdash`/`@emdash-cms/cloudflare` peers to `>=1.0.0 <2` and `vite-plus` to `^1.0.0`, following
128
+ both projects' GA releases.
129
+
130
+ **Breaking for `@getvitops/emdash` consumers on EmDash 0.x**: the peer range no longer admits
131
+ `emdash@0.31.x`/`@emdash-cms/cloudflare@0.31.x`. Upgrade `emdash` and `@emdash-cms/cloudflare` to
132
+ `^1.0.1` first — nothing in `@getvitops/emdash`'s own plugin surface (`admin.portableTextBlocks`,
133
+ the `hooks.page-fragments:register` capability, `definePlugin()`) changed against EmDash 1.0, so no
134
+ code here needed to change, only the declared range.
135
+
136
+ The `emdash` scaffold template (`vp create @getvitops:emdash`) picks up several fixes alongside
137
+ the version bump:
138
+
139
+ - `tailwindcss`/`@tailwindcss/vite` are now real dependencies — the template selected
140
+ `css.format: 'tailwind'` without installing either, which threw on build.
141
+ - `site.json` now sets `site.icons.sprite: true` plus the full `semantic` list the
142
+ `vitops.actionLink` block's icon picker offers — previously its sprite icons pointed at a file
143
+ the build never generated.
144
+ - `astro.config.mjs` sets `migrations: { runtime: 'check', dev: 'auto' }`, and
145
+ `site-deploy-emdash.yml` runs EmDash 1.0's documented `build → status → apply → deploy → check`
146
+ migration sequence explicitly, rather than relying solely on first-request auto-migration. This
147
+ needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
148
+ which only a human can set — run `emdash migrate --status` against the real deployed database,
149
+ review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
150
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
151
+ - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
152
+ not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
153
+ errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
154
+ Uncomment it once scaffolded into its own repo.
155
+
156
+ No config schema changes. `@getvitops/generator` also switches from the `zod/mini` subpath import
157
+ to the standalone `@zod/mini` package (zod 4.5+; same API) and fixes `validateConfig()` silently
158
+ losing the offending key name on an `unrecognized_keys` error (e.g. a typo'd `site.ads` platform
159
+ now names the bad key instead of reporting a bare "Invalid input").
160
+
161
+ Also fixed: `vp check`'s `tsconfig-error` for `packages/create/templates/emdash/tsconfig.json`
162
+ (`extends: "astro/tsconfigs/strict"`, unresolvable from inside this monorepo since the template
163
+ has no `node_modules` of its own — a pre-existing, structural gap, newly surfaced as a hard error
164
+ by oxlint 1.85's deeper config discovery, and blocking on every commit since this repo's pre-commit
165
+ hook runs `vp check --fix`). Fixed with a new `scripts/link-template-deps.mjs`, wired as the root
166
+ `postinstall` script: symlinks `astro`/`emdash`/`@emdash-cms/cloudflare` into the template's own
167
+ `node_modules/` (resolved fresh from sibling packages that already depend on them for real, not a
168
+ hardcoded `.pnpm` hash) and writes a minimal `astro:content` ambient shim at `.astro/types.d.ts`
169
+ (the same path `astro sync` would generate for real). Both are gitignored and only ever exist
170
+ inside this monorepo's copy of the template — a real scaffold installs its own real copies instead.
171
+ Considered and rejected first: inlining astro's strict preset (drift risk against astro's own
172
+ evolving default) and making the template a real pnpm workspace member (would change how its
173
+ `@getvitops/*: "latest"` deps resolve locally, which `templates.test.ts` exists to protect).
174
+
175
+ **Known gap, not fixed here**: `packages/astro/src/components/container.test.ts` (the
176
+ `AstroContainer`-based render tests) fails to even load under vite-plus 1.0's Vite 8 — direct
177
+ `.astro` imports in a Vitest file need the Astro Vite plugin registered, which nothing in this
178
+ repo's test config actually does (Astro's own docs call for `getViteConfig()`, which doesn't
179
+ compose with vite-plus's own `defineConfig`). A type-level shim
180
+ (`packages/astro/src/components/astro-modules.d.ts`) fixes `vp check`'s type-aware pass, but the
181
+ runtime `vp test` failure for this one file is still open.
182
+
183
+ - fdc3f7e: `<wc-marquee>` adds a "Pause animation" button beside the track (WCAG 2.2.2), and the marquee also pauses while focus is inside it (`data-paused` and `:focus-within`). Pattern CSS: all formats. `<wc-consent>` moves focus into the banner when a visitor opens it from a settings control and returns it on close; an unprompted banner still doesn't take focus. `<wc-image-compare>`'s before/after panes are `role="group"` so their labels are announced. The conversion route format-checks `phone`/`email` beacon payloads and imports the Worker-safe `@getvitops/utils/content` instead of the utils root barrel.
184
+
3
185
  ## 8.2.0-alpha.0
4
186
 
5
187
  ### Minor Changes
6
188
 
7
189
  - c3c1bcb: **New: the design-system editor, mounted inside the EmDash admin.** `vitopsEmdash({
8
- designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
9
- './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
190
+ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import config from
191
+ './site.json' with { type: 'json' }`) adds a `/design-system` admin page mounting
10
192
  `<wc-theme-editor>` — the same live token editor `@getvitops/vite`'s dev server has always
11
193
  offered, now usable by the client directly, no dev server required. An edit is visible
12
194
  site-wide immediately (a runtime `:root` override, served via a new plugin API route and
@@ -69,7 +251,7 @@ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import c
69
251
  needs a new GitHub Environment variable, `EMDASH_TARGET_FINGERPRINT` (one per environment),
70
252
  which only a human can set — run `emdash migrate --status` against the real deployed database,
71
253
  review the printed identity, then `gh variable set EMDASH_TARGET_FINGERPRINT --env <env> --body
72
- <value>`. Deploys fail clearly on the migration-apply step until it's set.
254
+ <value>`. Deploys fail clearly on the migration-apply step until it's set.
73
255
  - `lint.options.typeAware`/`typeCheck` in the template's own `vite.config.ts` are commented out,
74
256
  not deleted: vite-plus 1.0 only honours that option in a project's actual root config, and hard
75
257
  errors on it in a nested one (which this file is, sitting inside the `design-system` monorepo).
@@ -431,7 +613,7 @@ designSystemEditor: { config } })` (`config` is your `site.json`, e.g. `import c
431
613
  created, with `patterns/counter.css`'s own reduced-motion query as a second belt.
432
614
  - The accessible name stays the final value for the whole animation: `.counter__value` is toggled
433
615
  to the framework's `.sr-only` utility while running, never hidden — `display: none`/`visibility:
434
- hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
616
+ hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
435
617
  screen reader must never be read ~60 intermediate numbers. The animated text lives in a separate
436
618
  `aria-hidden` presentation span it never reaches.
437
619
  - `data-from` (default `0`), `data-duration` (ms, default `1200`), `data-easing` (`linear` |
@@ -978,28 +1160,20 @@ hidden` would pull it out of the accessible-name computation along with the visu
978
1160
  {
979
1161
  "designSystem": {
980
1162
  "themes": {
981
- "default": {
982
- /* … */
983
- },
1163
+ "default": {/* … */},
984
1164
  },
985
1165
  },
986
1166
  "organization": {
987
1167
  "name": "Acme",
988
1168
  "contact": "hq",
989
1169
  "locations": {
990
- "hq": {
991
- /* … */
992
- },
1170
+ "hq": {/* … */},
993
1171
  },
994
1172
  },
995
1173
  "site": {
996
1174
  "defaultLocale": "en",
997
- "domains": {
998
- /* … */
999
- },
1000
- "analytics": {
1001
- /* … */
1002
- },
1175
+ "domains": {/* … */},
1176
+ "analytics": {/* … */},
1003
1177
  },
1004
1178
  }
1005
1179
  ```
@@ -1093,7 +1267,7 @@ hidden` would pull it out of the accessible-name computation along with the visu
1093
1267
  `animate-view` / `animate-scroll` / `animate-trigger` / `transition`, and every effect
1094
1268
  gained an `open-<fx>` state variant. Overlays now state _where they start_
1095
1269
  (`--translate-x-from`) instead of owning a keyframe: `class="drawer drawer--right
1096
- open-fade-in"` composes slide and fade. Applied at zero specificity with identity
1270
+ open-fade-in"` composes slide and fade. Applied at zero specificity with identity
1097
1271
  defaults, so a popover that sets no effect vars is unchanged.
1098
1272
  - **One scrim.** `--scrim` / `--scrim-filter` tokens and `.no-scrim` in `popover.css`,
1099
1273
  replacing 18 `::backdrop` blocks across seven partials. `.drawer--modeless` is aliased.
@@ -1296,8 +1470,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
1296
1470
  replace it with `--anim-range`. `--stagger-range-step` is now a viewport length (`5vh`) to match.
1297
1471
 
1298
1472
  **Fixed: every journey ran on a truncated range.** The generator emitted `animation-range: entry
1299
- exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
1300
- 100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
1473
+ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
1474
+ 100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
1301
1475
  `from` state — as the element hits the top of the viewport, which is to say they faded out while
1302
1476
  still fully on screen. Journeys now run `entry calc(50% + 10vh) → exit 100%`: they start on the same
1303
1477
  midpoint pivot as everything else and keep the full entry → hold → exit arc, so the hold sits in the
@@ -1310,7 +1484,7 @@ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than th
1310
1484
  so one knob tunes both. If your own config has an empty `slide` base, add the same line.
1311
1485
 
1312
1486
  **Fixed: `reveal-*` on hover was unreachable.** A `hover-reveal-left` element rests at `clip-path:
1313
- inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
1487
+ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
1314
1488
  hittable area and could never receive the hover that would reveal it. The state variants now match
1315
1489
  the element **or its direct parent**, mirroring what `animation.css` already did for the trigger
1316
1490
  driver (`:is(.is-active, [data-active]) > .animate-trigger`):
package/css/index.css CHANGED
@@ -72,6 +72,13 @@
72
72
  @import './patterns/text-effects.css';
73
73
  @import './patterns/svg.css';
74
74
  @import './patterns/marquee.css';
75
+ @import './patterns/tone.css';
76
+ @import './patterns/section.css';
77
+ @import './patterns/steps.css';
78
+ @import './patterns/stats.css';
79
+ @import './patterns/callout.css';
80
+ @import './patterns/logos.css';
81
+ @import './patterns/embed.css';
75
82
  @import './patterns/counter.css';
76
83
  @import './patterns/nav.css';
77
84
  @import './patterns/carousel.css';
@@ -69,3 +69,23 @@
69
69
  .banner[hidden] {
70
70
  display: none;
71
71
  }
72
+
73
+ /*
74
+ * `<wc-dismissable>` wraps a banner or notification — block content — and animates its own
75
+ * opacity/scale on dismiss, so it needs a box of its own: `display: contents` would drop
76
+ * that animation. Pre-upgrade it would otherwise be an inline box around a block child.
77
+ */
78
+ :where(wc-dismissable) {
79
+ display: block;
80
+ }
81
+
82
+ /*
83
+ * A dismiss button does nothing until `<wc-dismissable>` upgrades — an inert control that
84
+ * looks live is worse than none. `:not(:defined)` is true with no JS (and until elements.js
85
+ * has run), so the close affordance only appears once something can act on it. The banner
86
+ * itself stays fully readable. Markup that puts a `[data-dismiss]` button inside the element
87
+ * gets the same treatment, whatever its class.
88
+ */
89
+ wc-dismissable:not(:defined) :is(.banner__close, .notification__close, [data-dismiss]) {
90
+ display: none;
91
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Callout — a call-to-action band: a short message and the action it points at, set
3
+ * apart from the page. Not a banner: `banner` is a dismissible announcement; this is
4
+ * content that stays.
5
+ *
6
+ * Message left and actions right when there is room, stacked when there is not. Colour
7
+ * is `.tone` (tone.css); `--center` stacks and centres at every width.
8
+ */
9
+ .callout {
10
+ display: grid;
11
+ gap: var(--callout-gap, 1rem 2rem);
12
+ align-items: center;
13
+ padding: var(--callout-padding, clamp(1.25rem, 4cqi, 2.5rem));
14
+ border: var(--b-card, var(--b-card-group, var(--b-default)));
15
+ border-radius: var(--br-card, var(--br-card-group, var(--br-default)));
16
+
17
+ .callout__body {
18
+ display: grid;
19
+ gap: 0.5rem;
20
+ min-inline-size: 0;
21
+ }
22
+
23
+ .callout__title,
24
+ .callout__text,
25
+ .callout__actions {
26
+ margin: 0;
27
+ }
28
+
29
+ &.callout--center {
30
+ text-align: center;
31
+ justify-items: center;
32
+ }
33
+ }
34
+
35
+ @container (min-width: 48rem) {
36
+ .callout:not(.callout--center) {
37
+ grid-template-columns: minmax(0, 1fr) auto;
38
+ }
39
+ }
@@ -80,3 +80,8 @@
80
80
  visibility: visible;
81
81
  }
82
82
  }
83
+
84
+ /* Matches `<wc-copy>`'s upgraded `:host` (inline-flex) so registering causes no reflow. */
85
+ :where(wc-copy) {
86
+ display: inline-flex;
87
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Embed — a third-party iframe (map, video) held at a fixed aspect ratio, behind consent.
3
+ *
4
+ * The iframe ships with NO `src`: the address rides on `data-consent-src`, and the consent
5
+ * runtime assigns it only once the visitor has granted the category. Until then the
6
+ * `.embed__placeholder` — a real link to the same content, so it is useful with no JS — is
7
+ * what the page shows; `[data-vitops-activated]` on the iframe (set by the runtime) hides
8
+ * it. No third-party request is made before consent.
9
+ */
10
+ .embed {
11
+ position: relative;
12
+ aspect-ratio: var(--embed-ratio, 16 / 9);
13
+ overflow: hidden;
14
+ border: var(--b-card, var(--b-card-group, var(--b-default)));
15
+ border-radius: var(--br-card, var(--br-card-group, var(--br-default)));
16
+
17
+ .embed__frame {
18
+ position: absolute;
19
+ inset: 0;
20
+ inline-size: 100%;
21
+ block-size: 100%;
22
+ border: 0;
23
+ }
24
+
25
+ .embed__placeholder {
26
+ position: absolute;
27
+ inset: 0;
28
+ display: grid;
29
+ place-content: center;
30
+ gap: 0.5rem;
31
+ padding: 1rem;
32
+ text-align: center;
33
+ }
34
+
35
+ /* Once the runtime has loaded the frame, the placeholder has nothing left to say. */
36
+ .embed__frame[data-vitops-activated] ~ .embed__placeholder {
37
+ display: none;
38
+ }
39
+ }
@@ -364,3 +364,22 @@ select.custom-select {
364
364
  outline-offset: 2px;
365
365
  }
366
366
  }
367
+
368
+ /* `<wc-multi-field>` is a group of controls, a block. Its shadow `:host` declares no display. */
369
+ :where(wc-multi-field) {
370
+ display: block;
371
+ }
372
+
373
+ /* `<wc-turnstile>` wraps a fallback paragraph and the widget box, both blocks (light DOM, no `:host`). */
374
+ :where(wc-turnstile) {
375
+ display: block;
376
+ }
377
+
378
+ /*
379
+ * A message shown only when the URL fragment names it. A prerendered page cannot know whether a
380
+ * no-JS post succeeded, so a form's success and error notices are always in the markup and the
381
+ * endpoint's redirect (`#form-sent` / `#form-error`) reveals one.
382
+ */
383
+ .form-notice--target:not(:target) {
384
+ display: none;
385
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Logos — a static grid of partner/client marks. `marquee` is the scrolling version;
3
+ * this one holds still, which is what most "key partners" rows should do (nothing to
4
+ * pause, nothing to track, no motion to opt out of).
5
+ *
6
+ * Each item is a plain image, or a link around one. Marks are normalised to a common
7
+ * height rather than a common width, because logos vary far more in aspect ratio than
8
+ * in how tall they should read. `--grayscale` quiets them until hover or focus.
9
+ */
10
+ .logos {
11
+ display: flex;
12
+ flex-wrap: wrap;
13
+ align-items: center;
14
+ justify-content: var(--logos-justify, center);
15
+ gap: var(--logos-gap, 1.5rem 2.5rem);
16
+ margin: 0;
17
+ padding: 0;
18
+ list-style: none;
19
+
20
+ .logos__item {
21
+ display: grid;
22
+ place-items: center;
23
+ margin: 0;
24
+ }
25
+
26
+ .logos__item img {
27
+ block-size: var(--logos-height, 2.5rem);
28
+ inline-size: auto;
29
+ max-inline-size: 100%;
30
+ object-fit: contain;
31
+ }
32
+
33
+ &.logos--grayscale .logos__item img {
34
+ filter: grayscale(1);
35
+ opacity: 0.7;
36
+ transition:
37
+ filter 0.2s,
38
+ opacity 0.2s;
39
+ }
40
+
41
+ &.logos--grayscale .logos__item:is(:hover, :focus-within) img {
42
+ filter: none;
43
+ opacity: 1;
44
+ }
45
+ }
@@ -56,6 +56,13 @@
56
56
  animation-play-state: paused;
57
57
  }
58
58
 
59
+ /* Keyboard users can't hover; focusing anything inside (a link in an item) also pauses, and
60
+ `<wc-marquee>`'s pause button sets `data-paused` (WCAG 2.2.2). */
61
+ &:focus-within .marquee__content,
62
+ &[data-paused] .marquee__content {
63
+ animation-play-state: paused;
64
+ }
65
+
59
66
  &.marquee--reverse {
60
67
  --_marquee-direction: reverse;
61
68
  }
@@ -100,3 +107,12 @@
100
107
  translate: 0 calc(-1 * var(--_marquee-distance, calc(100% + var(--marquee-gap, 1rem))));
101
108
  }
102
109
  }
110
+
111
+ /*
112
+ * The `.marquee` class carries `display: flex` when present (the Astro component always sets
113
+ * it). This is the fallback for a hand-written `<wc-marquee>` without the class, and it is
114
+ * zero specificity so `.marquee` still wins.
115
+ */
116
+ :where(wc-marquee) {
117
+ display: block;
118
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Section — the editorial wrapper most pages are made of: an optional kicker, a heading,
3
+ * a lead-in line, then a body of other blocks.
4
+ *
5
+ * Composed from the layout primitives rather than reinventing them: the outer element
6
+ * is `.region` (vertical rhythm) and the content sits on `.centered`'s tracks, so this
7
+ * partial only adds what neither has — the head's own rhythm and the `--split` layout,
8
+ * which puts the head beside the body (heading-left) above the container breakpoint.
9
+ * Backdrop and colour come from `.tone` (tone.css), never from here.
10
+ */
11
+ .section {
12
+ .section__head {
13
+ display: grid;
14
+ gap: var(--section-head-gap, 0.75rem);
15
+ align-content: start;
16
+ }
17
+
18
+ .section__kicker,
19
+ .section__title {
20
+ margin: 0;
21
+ }
22
+
23
+ .section__sub {
24
+ margin: 0;
25
+ max-inline-size: var(--width-measure, 65ch);
26
+ }
27
+
28
+ .section__body {
29
+ display: grid;
30
+ gap: var(--section-body-gap, 1.5rem);
31
+ min-inline-size: 0;
32
+ }
33
+
34
+ /* Head and body stack until there is room for two columns (below). */
35
+ &.section--split .section__inner {
36
+ display: grid;
37
+ gap: var(--section-split-gap, clamp(1.5rem, 4cqi, 3rem));
38
+ }
39
+
40
+ &.section--center .section__head {
41
+ text-align: center;
42
+ justify-items: center;
43
+ }
44
+ }
45
+
46
+ @container (min-width: 48rem) {
47
+ .section.section--split .section__inner {
48
+ grid-template-columns:
49
+ minmax(0, var(--section-split-head, 1fr))
50
+ minmax(0, var(--section-split-body, 2fr));
51
+ align-items: start;
52
+ }
53
+ }