@getvitops/generator 6.0.0 → 8.0.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,5 +1,423 @@
1
1
  # @getvitops/generator
2
2
 
3
+ ## 8.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Rebuild the lightbox on one dialog + the existing carousel, fixing the flicker on
8
+ prev/next and the stutter on open.
9
+
10
+ **Breaking (`css`/`bricks` and `tailwind` formats — `.lightbox-dialog`'s markup
11
+ contract changed):**
12
+
13
+ - `<Gallery />` now emits ONE `<dialog class="lightbox-dialog">` per gallery,
14
+ wrapping a `<Carousel />`, instead of one dialog per image. A `.gallery__trigger`
15
+ is now a plain `<a href="#slide-id">`, not a `command="show-modal"` button.
16
+ - `.lightbox-dialog__content` / `__image` / `__nav` / `__nav-button` /
17
+ `__caption` are gone. The dialog's child is `.lightbox-dialog__track` (a
18
+ `.carousel`) — prev/next, dots, arrow keys and swipe are the carousel's,
19
+ reused rather than reimplemented. `.lightbox-dialog__close` is unchanged.
20
+ - `<wc-gallery>` no longer builds nav buttons or steps between images — the
21
+ nested `<wc-carousel>` does. It fires `gallery-open` (unchanged shape) and
22
+ re-emits the carousel's own stepping as `gallery-change`.
23
+ - `packages/core/src/web-components/utils/gallery.ts` dropped `keyToAction` and
24
+ `swipeAction` (the carousel's own key/swipe handling covers both now).
25
+ `stepIndex`/`preloadTargets` are unchanged.
26
+
27
+ **Why:** stepping used to close one `<dialog>` and open another, which made
28
+ `animation.css`'s top-layer driver play a full exit-then-entry transition on
29
+ every step — visible as a flicker on prev/next, by construction, not a bug to
30
+ tune away. Opening also awaited the full image's `decode()` before starting the
31
+ view-transition morph, so the dialog visibly stalled on a slow network. One
32
+ dialog containing one carousel makes the first failure mode unrepresentable
33
+ (stepping is a scroll, not a close/open pair) and removes the wait for the
34
+ second (the morph starts immediately, painting the thumbnail as a CSS
35
+ background placeholder until the full image arrives).
36
+
37
+ **Also improved, not just fixed:** the no-JS fallback is no longer a _stripped_
38
+ lightbox — it's a genuine one. Each trigger is ordinary fragment navigation
39
+ (`:has(:target)` in `lightbox.css` reveals the dialog), which needs no Invoker
40
+ Commands polyfill in any engine, and it now has working prev/next with no
41
+ JavaScript at all — the previous per-dialog fallback had no way to express that.
42
+
43
+ **Migration:** a consumer using `<Gallery />` needs no changes — the component's
44
+ props are unchanged. Anyone who hand-authored `.lightbox-dialog` markup (per
45
+ `docs/concepts/patterns.md` / `docs/css/classes.md`) must move to the new single-
46
+ dialog-wrapping-a-carousel shape; see `patterns.astro`'s "Gallery, hand-authored"
47
+ demo for the reference markup. `<wc-gallery>` consumers listening for
48
+ `gallery-change` keep the same event shape (`{ index, total }`).
49
+
50
+ ### Minor Changes
51
+
52
+ - Add Google Business Profile reviews: fetch, cache, render.
53
+
54
+ - **`vitops reviews`** (new CLI command) fetches a client's GBP reviews (legacy v4 API — the only
55
+ one that has them) and writes them to `.vitops/reviews.json`, meant to be committed so a build
56
+ never needs a live Google credential. `--dry` prints the plan; `--check` diffs against the cache
57
+ without writing. Requires the same `VITOPS_GOOGLE_CLIENT_ID`/`_CLIENT_SECRET`/`_REFRESH_TOKEN` (or
58
+ ADC login) `search setup` uses, scoped to `business.manage` — **and the calling GCP project must
59
+ be allowlisted by Google first**, a separate one-time approval from every other Google API here.
60
+ - **`site.reviews`** (config): which `organization.locations` to fetch, and per-location
61
+ `featured`/`minRating`/`sort`/`limit` selection. `schema` (JSON-LD) and `avatars` both default to
62
+ **off** — see the migration notes below.
63
+ - **`@getvitops/utils/reviews`** (new subpath): the pure plan/select/merge functions plus the GBP
64
+ fetcher and cache reader/writer, mirroring `./indexing`'s pure/IO split.
65
+ - **`<ReviewCard>`** (new Astro component, `@getvitops/astro`): one review card from a cache entry.
66
+ No web component — nothing here needs progressive enhancement. Composes the existing
67
+ `schemas/Review.astro` for JSON-LD only when both `schema` and `itemReviewed` are passed.
68
+ - **`<Marquee>`** (new Astro component): the tier-3 wrapper `wc-marquee`/`marquee.css` was missing.
69
+ - **`.review` pattern** (`css`/`bricks` and `tailwind` formats): a new config-generated
70
+ `patterns.items.review` entry plus `packages/core/css/patterns/review.css` for the structural
71
+ parts (rating, identity, body, reply, source link). Two new semantic icon names, `star` and
72
+ `star-half`, across every icon set.
73
+
74
+ **Migration:** no breaking change. A site adding `site.reviews` should read the "Google reviews"
75
+ section in AGENTS.md before enabling `schema` — Google's structured-data policy treats a business
76
+ marking up reviews of itself as ineligible for review rich results and risks a manual action, which
77
+ is why it defaults off.
78
+
79
+ - Repo-wide simplification pass: de-boilerplate the CLI, dedupe shared cache/plan
80
+ primitives in `@getvitops/utils`, drop dead code, and tighten dependency hygiene.
81
+
82
+ No breaking changes. (`vitops icons`'s `--site` flag was briefly going to be renamed to
83
+ `--input` for consistency with the other `Config`-anchored commands — deferred rather than
84
+ spending a major bump on a rename with no other breaking change riding along; it stays
85
+ `--site` for now.)
86
+
87
+ **Fixed:**
88
+
89
+ - `@getvitops/core`'s 12 runtime deps (`lit`, both `@oddbird` polyfills, both
90
+ `@webcomponents` packages, `colorjs.io`, etc.) were listed as `dependencies` despite
91
+ being bundled into `dist/elements.js`/`dist/polyfills.js`/`dist/deferred.js` — a
92
+ consumer installed them twice for nothing. Moved to `devDependencies`; no change to
93
+ what ships.
94
+ - Deleted `old-css-lib/` (59 files), superseded by `packages/core/css/` since the
95
+ in-memory bundling change — no consumer-facing effect, it was not part of any
96
+ published output.
97
+ - Deleted a dead Astro module (`packages/astro/src/scripts/deferred.ts`) that had
98
+ silently diverged from `packages/core/src/js/deferred.ts` and had zero importers.
99
+
100
+ **No consumer-facing change in `@getvitops/vite`** beyond the fixed-group version bump —
101
+ task-graph and caching fixes internal to this repo's own build.
102
+
103
+ Everything else (the `cli.ts` file split into `commands/{search,ads}.ts`, shared
104
+ `json-store.ts`/`plan-format.ts` helpers in `@getvitops/utils`, a `.github/actions/setup`
105
+ composite action, restatement-comment cleanup) is internal refactoring with no change to
106
+ any published API, CLI flag, or generated output.
107
+
108
+ ### Patch Changes
109
+
110
+ - Updated dependencies
111
+ - Updated dependencies
112
+ - Updated dependencies
113
+ - Updated dependencies [a84839b]
114
+ - @getvitops/utils@8.0.0
115
+
116
+ ## 7.0.0
117
+
118
+ ### Major Changes
119
+
120
+ - aad43b7: A gallery/lightbox that is a fully working modal lightbox with **zero JavaScript**, plus
121
+ `ImageMetadata` support in `<Carousel />`.
122
+
123
+ Only `@getvitops/core`, `generator` and `astro` change here. This changeset makes **no
124
+ consumer-facing change** to `utils`, `cli` or `vite` — they still ship in this release because
125
+ the six share one version, but from a different changeset (`localbusiness-schema.md`) that
126
+ touches `utils` and `generator` for an unrelated reason.
127
+
128
+ **Breaking — `css`/`bricks`/`tailwind` (all formats):** the `.lightbox` thumbnail grid is now
129
+ `.gallery`, so it composes with the dialog it opens (`.lightbox-dialog`, unchanged) rather than
130
+ sharing its name. `vitops lint --fix` cannot cover this: `movedTokens()` renames colour token
131
+ names only, and there is no class-rename mechanism in `@getvitops/cli`. Migrate by hand:
132
+
133
+ | Old | New |
134
+ | ------------------ | ----------------- |
135
+ | `.lightbox` | `.gallery` |
136
+ | `.lightbox__item` | `.gallery__item` |
137
+ | `.lightbox__thumb` | `.gallery__thumb` |
138
+
139
+ `.lightbox-dialog` and its `__content` / `__image` / `__close` / `__nav` / `__nav-button` /
140
+ `__caption` parts are unchanged.
141
+
142
+ **Added — `css`/`bricks`/`tailwind` (all formats):** `patterns/gallery.css` — the `.gallery` grid,
143
+ now on the token cascade (`--br-gallery` etc.) it only half-had before.
144
+
145
+ **Added — `wc`:** `<wc-gallery>` (`@getvitops/core/elements`). Wraps a `.gallery` grid of
146
+ `command="show-modal"` triggers and the `.lightbox-dialog`s they open. With no JS, each trigger is
147
+ a real Invoker Commands button for its own dialog — open and close both work. `<wc-gallery>` adds
148
+ what the fallback deliberately cannot contain: prev/next, arrow keys, swipe (horizontal to step,
149
+ vertical to dismiss), an announced image counter, neighbour preload, and a thumbnail → full-image
150
+ `document.startViewTransition` morph where supported. Prev/next can't exist in the fallback — a
151
+ second `show-modal` invoker inside an already-open dialog stacks a second modal rather than
152
+ replacing the first — so it is generated by the element, never authored, the same rule
153
+ `<wc-tree>`'s search field follows.
154
+
155
+ **Added — `astro`:** `<Gallery />` (`@getvitops/astro/components/Gallery.astro`) — emits a
156
+ `.gallery` grid plus one `.lightbox-dialog` per image and its own `<wc-gallery>`; do not wrap it in
157
+ one. Images take `string | ImageMetadata`, rendered through a new internal `<ResponsiveImage />`
158
+ helper: a plain string renders the same raw `<img>` every component here used before, unchanged;
159
+ an `astro:assets` import renders through `<Picture />` for AVIF/WebP derivatives.
160
+
161
+ **Changed — `astro`:** `<Carousel />`'s `CarouselSlide.src` now also accepts `ImageMetadata`,
162
+ through the same `<ResponsiveImage />` helper, plus a new `formats` prop. Every existing
163
+ string-`src` call site renders byte-identical markup — purely additive.
164
+
165
+ **Added — `wc`:** the Invoker Commands API (`<button command commandfor>`) polyfill
166
+ (`invokers-polyfill`, feature-detected via `@getvitops/core/polyfills`) — resolves the TODO
167
+ `polyfills.ts` carried since 4.0. `<wc-gallery>`'s fallback and `<Drawer />` (which already emitted
168
+ `command="show-modal"` unconditionally) both now get real cross-browser coverage rather than
169
+ depending on it being "too new to rely on yet." Fetched only in an engine that doesn't support the
170
+ API natively, so a current browser downloads none of it.
171
+
172
+ **Migration:** rename `.lightbox` → `.gallery` and `.lightbox__item`/`.lightbox__thumb` →
173
+ `.gallery__item`/`.gallery__thumb` wherever authored (see table above); everything else is
174
+ additive. Consumers using the CSS pattern with no JS need no other change — the fallback markup is
175
+ unchanged in shape, only the class names moved.
176
+
177
+ - eb9ac79: `<wc-tree>` gains WAI-ARIA tree semantics and roving-tabindex keyboard navigation, and the
178
+ keyboard-navigation code behind it lands as shared utilities the next roving-tabindex component
179
+ (Dropdown, Tabs, Combobox) can reuse.
180
+
181
+ **Breaking — `wc` (`@getvitops/core`).** On upgrade, `<wc-tree>` now unwraps every branch's
182
+ `<details>`/`<summary>` into a plain, JS-owned structure (`role="tree"` on the root,
183
+ `role="treeitem"` on every item, `role="group"` on every nested list, a roving `tabindex`, and
184
+ `aria-expanded` mirroring expand state). This is a deliberate, named exception to the framework's
185
+ normal "parse the fallback and augment it in place" rule — the alternative, an ARIA-attribute
186
+ overlay left on top of the native `<details>`/`<summary>` structure, was tried first and rejected
187
+ because `<summary>` stays focusable regardless, which leaves a permanent duplicate `role="button"`
188
+ node inside every branch. The no-JS fallback is completely unaffected: it is still native nested
189
+ `<details>`, exactly as `<Tree />` has always rendered it, and `enhance={false}` still gets the
190
+ bare fragment with no `<wc-tree>` at all.
191
+
192
+ Two concrete things break for a consumer that reached into the enhanced DOM directly:
193
+ - A script holding a reference to the original `<details>`/`<summary>` nodes — including one
194
+ captured via `getElementById` before upgrade, since the flat markup shape's `<details>` carries
195
+ the item's `id` — loses that reference once JS runs. `tree-toggle` (mirroring the existing
196
+ `tree-filter` custom event) is the replacement surface for reacting to expand/collapse.
197
+ - `<wc-tree>` becomes a single tab stop. Tabbing through every row in a large tree no longer
198
+ works; arrow keys replace it (Up/Down between visible nodes, Right/Left to open/close a branch or
199
+ step in/out, Home/End, type-ahead), matching the WAI-ARIA tree pattern. Left/Right flip under
200
+ `direction: rtl`.
201
+
202
+ **Fixed — `wc` (`@getvitops/core`).** `<wc-carousel>`'s dot-nav arrow keys now follow writing
203
+ direction. In an RTL document, `ArrowLeft` moves the roving tabindex to the visually-left dot and
204
+ `ArrowRight` to the visually-right one — previously both were index-based and moved the focus ring
205
+ against the direction the dots are actually drawn in. `ArrowUp`/`ArrowDown` are unchanged and never
206
+ flip (`direction` is a horizontal-axis property). The prev/next buttons are unchanged too — they
207
+ still step to the previous/next slide in both directions, and only their glyph flips. Also fixed:
208
+ two arrow presses faster than the dot nav's smooth scroll can settle now reliably advance two dots
209
+ instead of sometimes landing on the same one twice.
210
+
211
+ **Changed — `css`/`bricks`/`tailwind` (all formats).** `patterns/tree.css` gains a `.tree__summary`
212
+ row class (the enhanced-state replacement for `<summary>`), a hand-rolled expand/collapse animation
213
+ replacing the native `<details>` one that no longer applies post-upgrade, and a focus ring scoped to
214
+ the row rather than the whole item (the roving tabindex now lands on `.tree__item`, whose box wraps
215
+ its entire subtree once expanded). Visually equivalent to before; no consumer class rename needed.
216
+
217
+ **Added — `wc` (`@getvitops/core`, internal).** `web-components/utils/keynav.ts` (generic
218
+ key-decoding + type-ahead primitives) and `utils/tree-nav.ts` (tree-specific navigation decisions)
219
+ are new pure modules, and `utils/RovingTabindex.ts` is a new shared `ReactiveController` — modelled
220
+ on the existing `DragController` — that owns roving-tabindex bookkeeping for any keyboard-navigable
221
+ group. `<wc-carousel>`'s dot nav now runs on it too, and `<wc-gallery>` reuses `keynav.ts`'s
222
+ `stepIndex`. None of the three are in `@getvitops/core`'s public `exports` map. Only
223
+ `RovingTabindex` is re-exported from `web-components/index.ts`, the same way `DragController`
224
+ already is, not as new published API; `keynav.ts` and `tree-nav.ts` stay internal modules imported
225
+ directly by path (as `RovingTabindex.ts` and `gallery.ts` already do) since neither is consumed
226
+ outside the components that already import them directly.
227
+
228
+ **This changeset makes no consumer-facing change in `@getvitops/generator` beyond documentation
229
+ output.** `tiers.ts`'s `tree` entry gains `tree__summary` to its class list and describes the new
230
+ keyboard navigation and ARIA semantics — this changes what `vitops docs components` /
231
+ `concepts/components.md` render for the `tree` pattern, nothing else. `generator`'s real change in
232
+ this release is unrelated to `wc-tree` — see `localbusiness-schema.md`.
233
+
234
+ ### Minor Changes
235
+
236
+ - c0c092b: `vitops search` now works from a `gcloud` login, and attributes API usage per site.
237
+
238
+ **Being logged in is enough.** With no `VITOPS_GOOGLE_*` set, an Application Default Credentials
239
+ login is used — the credential `gcloud auth application-default login` already wrote. It needs the
240
+ Search Console scopes, which are not in the default ADC set:
241
+
242
+ ```
243
+ gcloud auth application-default login \
244
+ --scopes=openid,https://www.googleapis.com/auth/siteverification,\
245
+ https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/cloud-platform
246
+ ```
247
+
248
+ Explicit `VITOPS_GOOGLE_*` vars still win. Ambient credentials are only a fallback, because a stale
249
+ local login silently overriding a deliberate CI secret is the worst available precedence.
250
+
251
+ Beyond convenience, this removes a documented footgun: an OAuth client you create yourself sits in
252
+ _Testing_ publishing status, where Google **expires the refresh token after 7 days**. The Cloud SDK
253
+ client is published, so an ADC credential doesn't rot between onboarding runs.
254
+
255
+ **New: `site.google.project`** — the Google Cloud project API usage is attributed to, sent as
256
+ `x-goog-user-project`. One project per site is what keeps each site's usage and billing separate,
257
+ and it is the same field Maps or anything else will read later, rather than a `quotaProject` here
258
+ and a `mapsProject` there.
259
+
260
+ It is **required for a user credential and wrong for a service account**, which is why it is not
261
+ sent unconditionally. A user credential authenticates through a shared OAuth client that owns no
262
+ project, so Google refuses the call outright — measured: `403 "requires a quota project, which is
263
+ not set by default"`. A service account already belongs to a project, and pointing it at a
264
+ different one would newly require `serviceusage.services.use` there, turning a working CI
265
+ credential into a 403 for no benefit.
266
+
267
+ Using it needs both APIs (`searchconsole.googleapis.com`, `siteverification.googleapis.com`)
268
+ enabled on that project, and `serviceusage.services.use` for the identity — one grant per site,
269
+ which suits an agency administering many sites from one admin identity.
270
+
271
+ An ADC credential with no `site.google.project` is now **refused before any request is made**,
272
+ naming the file the credential came from and the project gcloud recorded with it:
273
+
274
+ ```
275
+ ✖ this Google credential comes from ~/.config/gcloud/application_default_credentials.json,
276
+ which authenticates through a shared OAuth client that owns no project — so it needs
277
+ "site.google.project" in site.json to say which project the API usage belongs to.
278
+ gcloud recorded "acme-web" as the quota project for that login, so that is probably the value.
279
+ ```
280
+
281
+ That is the mistake waiting for the _second_ site: the command gets copied from whatever
282
+ onboarded the first one, the field is left out, and Google's own answer names neither the config
283
+ field nor the credential.
284
+
285
+ **Fixed: an ADC file in `GOOGLE_APPLICATION_CREDENTIALS` no longer hard-exits.** That variable is
286
+ Google's own convention _and_ where gcloud writes ADC, so the same variable carries two kinds of
287
+ credential. Parsing an `authorized_user` file as a service account failed with `service account
288
+ JSON is missing client_email or private_key` — while holding a credential the command already knew
289
+ how to use. It is now discriminated on `type`, the field Google's own libraries switch on.
290
+
291
+ **Also fixed — `search setup --dry` and `--check` are not offline.** The docs implied otherwise. The
292
+ state gather runs before the planner, so both read from Cloudflare and Google and both need
293
+ credentials. They still create and change nothing; `notify --dry` remains genuinely request-free.
294
+
295
+ New in `@getvitops/utils/indexing`: `parseAdcUser`, `adcQuotaProject`, `adcCredentialsPath` (pure,
296
+ so what is decidable is asserted without a filesystem), plus `googleHeaders` and the `GoogleAuth` /
297
+ `GoogleAuthLike` types. The seven exported Google request functions now take a token **or** a
298
+ `{ token, quotaProject }` — a bare string still works everywhere, so nothing needs changing.
299
+
300
+ - 2b50f9e: Extract the LocalBusiness JSON-LD builder out of `<LocalBusiness />` into a reusable
301
+ `localBusinessGraph()`, and add the location fields listing platforms (GBP, Bing Places, Apple
302
+ Business Connect) actually need.
303
+
304
+ **Added — `@getvitops/utils`.** `localBusinessGraph(options)` (+ its `LocalBusinessGraphOptions`,
305
+ `OpeningHoursSpecification` and `SpecialHoursSpecification` types) is now exported from
306
+ `@getvitops/utils`, alongside the existing `organizationGraph`/`articleGraph`/`breadcrumbGraph`/
307
+ `faqGraph` builders — same module, same shape, usable anywhere a JSON-LD graph is built outside an
308
+ Astro component.
309
+
310
+ **Added — `authoring`/`config` (`@getvitops/generator`).** Four new `organization.locations.<slug>`
311
+ fields: `hoursSpecial` (dated deviations from the recurring `hours` — holiday hours, a one-off
312
+ closure), `photos` (`ImageRefSchema[]` → JSON-LD `image`), `sameAs` (this location's own listing
313
+ URLs, distinct from `organization.sameAs`, which is the company overall), and `listings` (external
314
+ listing ids keyed by platform — `google` | `bing` | `apple` — not consumed by the generator today;
315
+ recorded so a future listings-sync command has a stable id to match against).
316
+
317
+ **Changed — `astro`.** `<LocalBusiness />`'s `Props` type is now `LocalBusinessGraphOptions`
318
+ (re-exported, not redefined) rather than a local interface that had drifted from what the builder
319
+ actually accepted. No markup or prop-name change for an existing consumer.
320
+
321
+ - a6a8b39: `radii.pill` / `--br-pill` is gone. `999px` is a shape, not a design decision, so it no longer
322
+ has a config-editable token of its own — it's now the `rounded-full` utility, named and valued
323
+ to match Tailwind's own class, alongside a new `rounded-none`.
324
+
325
+ **Breaking — `css`/`bricks` only** (the tailwind format never emitted `--br-pill`; Tailwind
326
+ already ships `rounded-full`/`rounded-none` natively and always has). A consumer who overrode
327
+ `--br-pill` directly, or set `radii.pill` in their own `design-system.json` expecting it to
328
+ reshape the switch track, the facet-list count or the badge-indicator bubble, has to migrate:
329
+ - The switch track, facet count and badge indicator each already had their own consumer hook
330
+ (`--br-switch-track`, `--br-count`, `--br-badge-indicator`) and now fall back straight to the
331
+ literal `999px` instead of chaining through `--br-pill`. Override the specific hook instead of
332
+ the removed shared one.
333
+ - `.badge` is unaffected — it already resolved to `999px` and still does, now via its own
334
+ override (`patterns.items.badge.overrides.br`) rather than a shared primitive.
335
+ - Anything that wants a fully round corner outside those three patterns should reach for
336
+ `class="rounded-full"` (or `rounded-none` to zero one out) rather than a `--br-*` variable.
337
+
338
+ This is **not** reachable by `vitops lint --fix` — its token-rename table is built from colour
339
+ role names, and there is no equivalent static table for a shape primitive going away — so this
340
+ entry is the migration.
341
+
342
+ `radii.circle` / `--br-circle` (50%) is untouched: `status`, the radio and the switch thumb all
343
+ depend on it, and Tailwind has no equivalent to defer to.
344
+
345
+ - 7b391b3: Add `<wc-counter>` — animates a number from a start value to the value already in its fallback
346
+ text, on intersection. The stat/percentage figure in a media tile ("0 → 94%"), a KPI row, a
347
+ metrics band.
348
+
349
+ No consumer-facing change in this release outside `core`, `generator` and `astro` — `utils`,
350
+ `cli` and `vite` still ship in this release only because the six share one version.
351
+
352
+ **Added — `css`/`bricks`/`tailwind` (all formats):** `.counter` / `.counter__value` /
353
+ `.counter__presentation` (`patterns/counter.css`). A registered `--_n` custom property drives the
354
+ count via a plain CSS `@keyframes` animation — the interpolation and the easing curve are CSS's,
355
+ never duplicated in JS.
356
+
357
+ **Added — `wc` (`@getvitops/core`).** `<wc-counter>` parses the final value out of its own
358
+ `.counter__value` fallback text — never from a separate attribute, so the no-JS reading and the
359
+ animated reading can't disagree — and renders it back through `Intl.NumberFormat`, seeded from the
360
+ fallback text's own separators (a grouped `1,284` never animates through ungrouped intermediates).
361
+ `counter()` cannot do that part: no decimals, no locale grouping, which is the one thing here that
362
+ genuinely needs JavaScript.
363
+ - Triggers on intersection via a single shared `IntersectionObserver`, not on upgrade — a counter
364
+ below the fold that finished before the visitor scrolled to it would have done nothing.
365
+ - `prefers-reduced-motion: reduce` means don't animate at all, checked before the observer is even
366
+ created, with `patterns/counter.css`'s own reduced-motion query as a second belt.
367
+ - The accessible name stays the final value for the whole animation: `.counter__value` is toggled
368
+ to the framework's `.sr-only` utility while running, never hidden — `display: none`/`visibility:
369
+ hidden` would pull it out of the accessible-name computation along with the visual hiding, and a
370
+ screen reader must never be read ~60 intermediate numbers. The animated text lives in a separate
371
+ `aria-hidden` presentation span it never reaches.
372
+ - `data-from` (default `0`), `data-duration` (ms, default `1200`), `data-easing` (`linear` |
373
+ `ease-in` | `ease-out` | `float-sine` | `float-bounce` — maps onto the real
374
+ `--custom-ease-*`/`--ease-float-*` tokens, so a curve edited in `animation.css` cannot drift out
375
+ of step with this element), `data-decimals` (inferred from the fallback text when absent).
376
+ - Never leaves an intermediate value on screen as the resting state: disconnecting mid-animation
377
+ restores the fallback immediately, and it was never mutated in the first place — only the
378
+ presentation span was ever written to.
379
+
380
+ **Added — `astro`.** `<Counter value="94%" />` emits its own `<wc-counter>` — do NOT wrap it in
381
+ one, same rule as `Gallery`/`Carousel`/`Tree`. `value` is the only source of the final figure;
382
+ there is no separate `to` prop, for the same one-source-of-truth reason the element itself parses
383
+ its fallback rather than taking a duplicate attribute. `enhance={false}` emits the bare fallback
384
+ for a page that cannot run `elements.js`.
385
+
386
+ ### Patch Changes
387
+
388
+ - **Added — `utils`.** `itemListGraph()`, a pure schema.org `ItemList` JSON-LD builder — a sibling
389
+ of `breadcrumbGraph()`/`localBusinessGraph()`. Extracted from `@getvitops/astro`'s `<Carousel />`
390
+ so any consumer can build a valid `ItemList` (Google's "all-in-one page" carousel format:
391
+ `ListItem.item` carrying `@type`/`name`/`url`) for content that isn't one of the six Google
392
+ carousel rich-result types (`Article`/`Recipe`/`Movie`/`Course`/`Restaurant`/`Product`) — the
393
+ entity type is no longer constrained to that set.
394
+
395
+ **Changed — `astro`.** `<Carousel />`'s `CarouselItem['type']` now accepts any string, not just
396
+ the six carousel rich-result types — non-breaking, the union only widens. It's a thin wrapper
397
+ over `itemListGraph()` now; behaviour for existing callers is unchanged.
398
+
399
+ No consumer-facing change in this release outside `utils` and `astro` — `generator`, `cli` and
400
+ `vite` still ship in this release only because the six share one version.
401
+
402
+ **Fixed — `core` (`css`/`bricks`/`tailwind` build output, all formats).** The CSS and HTML
403
+ authored inside Lit's `css`/`html` tagged templates in `<wc-*>` component sources previously
404
+ survived JS minification verbatim — a JS minifier treats a template literal's cooked text as
405
+ opaque, so `elements.js` shipped ~690 lines of indented, unminified CSS (and the equivalent in
406
+ HTML) inside an otherwise-minified 886-line file. Two source-level build transforms now collapse
407
+ both before bundling: `postcss` + `postcss-lit` + `cssnano` for `css` templates, `minify-html-literals`
408
+ (`conservativeCollapse: true`, so whitespace between inline elements in light-DOM components never
409
+ collapses to zero) for `html` templates. `elements.js` drops from 886 lines / ~101 KB to 49 lines /
410
+ ~96 KB. A second build step deduplicates the 17 `@license` comment blocks (only 6 are textually
411
+ distinct) down to one verbatim copy of each — required by BSD-3/MIT/Apache-2.0's notice-retention
412
+ terms either way, just no longer repeated once per source file that carries it. No API change;
413
+ build-time only, and none of the added tooling (`postcss`, `postcss-lit`, `cssnano`,
414
+ `minify-html-literals`) is a runtime or published dependency.
415
+
416
+ - Updated dependencies [c0c092b]
417
+ - Updated dependencies
418
+ - Updated dependencies [2b50f9e]
419
+ - @getvitops/utils@7.0.0
420
+
3
421
  ## 6.0.0
4
422
 
5
423
  ### Major Changes
@@ -707,28 +1125,20 @@
707
1125
  {
708
1126
  "designSystem": {
709
1127
  "themes": {
710
- "default": {
711
- /* … */
712
- },
1128
+ "default": {/* … */},
713
1129
  },
714
1130
  },
715
1131
  "organization": {
716
1132
  "name": "Acme",
717
1133
  "contact": "hq",
718
1134
  "locations": {
719
- "hq": {
720
- /* … */
721
- },
1135
+ "hq": {/* … */},
722
1136
  },
723
1137
  },
724
1138
  "site": {
725
1139
  "defaultLocale": "en",
726
- "domains": {
727
- /* … */
728
- },
729
- "analytics": {
730
- /* … */
731
- },
1140
+ "domains": {/* … */},
1141
+ "analytics": {/* … */},
732
1142
  },
733
1143
  }
734
1144
  ```
@@ -893,7 +1303,7 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
893
1303
  Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
894
1304
 
895
1305
  ```js
896
- import vitops, { gitLastmod } from '@getvitops/astro';
1306
+ import vitops, { gitLastmod } from "@getvitops/astro";
897
1307
  vitops({ sitemap: { serialize: await gitLastmod() } });
898
1308
  ```
899
1309
 
@@ -1078,8 +1488,8 @@ open-fade-in"` composes slide and fade. Applied at zero specificity with identit
1078
1488
 
1079
1489
  ```js
1080
1490
  vitops({
1081
- analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
1082
- consent: { policyUrl: '/legal/cookies' },
1491
+ analytics: { googleAnalytics: "G-XXXXXXXXXX", plausible: "acme.com" },
1492
+ consent: { policyUrl: "/legal/cookies" },
1083
1493
  });
1084
1494
  ```
1085
1495
 
@@ -1219,7 +1629,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1219
1629
 
1220
1630
  ```js
1221
1631
  // astro.config.mjs
1222
- getvitops({ css: { input: 'company.json', format: 'css' } });
1632
+ getvitops({ css: { input: "company.json", format: "css" } });
1223
1633
  ```
1224
1634
 
1225
1635
  ```
@@ -1270,11 +1680,11 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
1270
1680
  getvitops({
1271
1681
  fonts: [
1272
1682
  {
1273
- name: 'League Spartan',
1274
- provider: 'fontsource',
1275
- cssVariable: '--font-league-spartan',
1276
- weights: ['100 900'],
1277
- subsets: ['latin'],
1683
+ name: "League Spartan",
1684
+ provider: "fontsource",
1685
+ cssVariable: "--font-league-spartan",
1686
+ weights: ["100 900"],
1687
+ subsets: ["latin"],
1278
1688
  preload: true,
1279
1689
  },
1280
1690
  ],
@@ -2145,7 +2555,7 @@ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting
2145
2555
  ```js
2146
2556
  // astro.config.mjs
2147
2557
  getvitops({
2148
- css: { input: 'design-system.json', format: 'css', out: 'src/styles' },
2558
+ css: { input: "design-system.json", format: "css", out: "src/styles" },
2149
2559
  editor: true,
2150
2560
  });
2151
2561
  ```
@@ -71,6 +71,15 @@
71
71
  animation-duration: var(--animation-duration-xslow);
72
72
  }
73
73
 
74
+ /* The gallery/lightbox thumbnail → full-image morph (see patterns/lightbox.css)
75
+ needs a shorter duration than a whole-page navigation. Scoped to `*` because a
76
+ repo-wide `::view-transition-old/new` selector belongs at the engine layer, not
77
+ inside the one pattern partial that happens to use it first. */
78
+ ::view-transition-old(*),
79
+ ::view-transition-new(*) {
80
+ animation-duration: 0.3s;
81
+ }
82
+
74
83
  /* ===========================================
75
84
  * Keyframes - Layout Stage (most expensive)
76
85
  * =========================================== */
@@ -75,3 +75,31 @@ body {
75
75
  scroll-padding-block-start: var(--top-offset, 0);
76
76
  }
77
77
  }
78
+
79
+ /* The focus ring must survive Windows High Contrast Mode, and as generated it
80
+ does not. Every pattern's `ring: true` state (`generate.ts`) emits `outline:
81
+ none; box-shadow: 0 0 0 3px <color>;` — a deliberate choice the rest of the
82
+ time, since a box-shadow ring can follow a border-radius where a native
83
+ outline cannot. But `forced-colors: active` forces `box-shadow` to `none`
84
+ unconditionally (CSS Color Adjust Module), and this framework already asked
85
+ for no native outline either — so a keyboard user in forced-colors mode gets
86
+ NO focus indicator at all, on every `:focus-visible` element in every
87
+ format. This is the single most severe instance of the class of bug
88
+ `forced-colors`/`prefers-contrast` coverage exists to catch (AGENTS.md's
89
+ design-system-gaps notes on `--surface-glass`/`--overlay`/role solids are
90
+ the same class, decorative rather than safety-critical).
91
+
92
+ `!important`, deliberately, for the same reason `:root`'s `font-size` above
93
+ gets it: this must win regardless of which pattern's `vitops.components`
94
+ rule set `outline: none`, and `!important` outranks a normal-priority
95
+ declaration regardless of cascade layer. The colour is the token every
96
+ other ring already uses (`--color-border-focus`, from `ui-primary`,
97
+ contrast-checked as a non-text boundary) — forced-colors mode remaps
98
+ `outline-color` to a system colour automatically, so this is really only
99
+ asserting THAT an outline renders, not which colour it renders in. */
100
+ @media (forced-colors: active) {
101
+ :focus-visible {
102
+ outline: 2px solid var(--color-border-focus) !important;
103
+ outline-offset: 2px !important;
104
+ }
105
+ }
@@ -6,6 +6,7 @@
6
6
  @import './generated/type-tokens.css';
7
7
  @import './generated/tokens.css';
8
8
  @import './generated/typography.css';
9
+ @import './generated/typography-headings.css';
9
10
  @import './layout.css';
10
11
  /* Must precede utilities.css: both are `vitops.utilities`, and utilities.css
11
12
  re-emits `.<bp>-flex` AFTER its own `.hidden` so `class="hidden md-flex"`
@@ -43,6 +44,7 @@
43
44
  panel surface navshell's desktop block has to undo. */
44
45
  @import './patterns/navshell.css';
45
46
  @import './patterns/notification.css';
47
+ @import './patterns/gallery.css';
46
48
  @import './patterns/lightbox.css';
47
49
  @import './patterns/overlay.css';
48
50
  /* was forms.css */
@@ -52,6 +54,7 @@
52
54
  @import './patterns/table.css';
53
55
  @import './patterns/details.css';
54
56
  @import './patterns/comment.css';
57
+ @import './patterns/review.css';
55
58
  @import './patterns/banner.css';
56
59
  @import './patterns/consent.css';
57
60
  @import './patterns/list.css';
@@ -69,6 +72,7 @@
69
72
  @import './patterns/text-effects.css';
70
73
  @import './patterns/svg.css';
71
74
  @import './patterns/marquee.css';
75
+ @import './patterns/counter.css';
72
76
  @import './patterns/nav.css';
73
77
  @import './patterns/carousel.css';
74
78
  @import './patterns/copy.css';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Counter — animates a number from a start value to the value already in its
3
+ * fallback text (`.counter__value`), e.g. the "0 → 94%" stat in a media tile.
4
+ * Consumes the generated token cascade (src/css/generated/tokens.css) only for
5
+ * `--custom-ease-out` (animation.css's engine layer).
6
+ *
7
+ * `<wc-counter>` owns the reading; this partial owns the interpolation. `--_n`
8
+ * is a registered custom property so the browser can smoothly animate it as a
9
+ * number — `counter()` cannot do decimals or locale grouping, so JS mirrors
10
+ * `--_n` into `.counter__presentation`'s text each frame via
11
+ * `Intl.NumberFormat`, formatted the same way the fallback text was. CSS still
12
+ * owns the interpolation, the easing curve and (as a second belt alongside the
13
+ * JS check) `prefers-reduced-motion`.
14
+ *
15
+ * `.counter__value` is toggled to the framework's `.sr-only` utility while
16
+ * running, not hidden — an element pulled out of the accessibility tree via
17
+ * `display: none`/`visibility: hidden` stops contributing to the accessible
18
+ * name, and that name must read the final value for the whole animation, not
19
+ * just at rest.
20
+ */
21
+ @property --_n {
22
+ syntax: '<number>';
23
+ initial-value: 0;
24
+ inherits: false;
25
+ }
26
+
27
+ .counter__presentation {
28
+ display: none;
29
+ }
30
+
31
+ wc-counter[running] .counter__presentation {
32
+ display: inline;
33
+ animation: counter-run var(--counter-duration, 1.2s) var(--counter-easing, var(--custom-ease-out))
34
+ forwards;
35
+ }
36
+
37
+ @keyframes counter-run {
38
+ from {
39
+ --_n: var(--counter-from, 0);
40
+ }
41
+ to {
42
+ --_n: var(--counter-to);
43
+ }
44
+ }
45
+
46
+ @media (prefers-reduced-motion: reduce) {
47
+ wc-counter[running] .counter__presentation {
48
+ animation: none;
49
+ }
50
+ }
@@ -287,7 +287,7 @@ input[type='color'] {
287
287
  position: relative;
288
288
  inline-size: var(--_switch-width);
289
289
  block-size: var(--_switch-height);
290
- border-radius: var(--br-switch-track, var(--br-pill, 999px));
290
+ border-radius: var(--br-switch-track, 999px);
291
291
  background: var(--color-bg-surface-muted);
292
292
  transition: background-color 0.2s ease;
293
293
  }