@getvitops/core 2.1.0 → 4.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,361 @@
1
1
  # @getvitops/core
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - c6b99e7: Consent is now demand-driven: the banner appears when something actually needs permission, not on every first visit.
8
+
9
+ Previously, enabling the gate showed a banner to every new visitor regardless of what the site did — including sites whose only analytics provider was cookieless, where there was nothing to consent to. The build decided whether the machinery shipped and the runtime showed the banner because no cookie existed yet.
10
+
11
+ Now those are two separate facts. The build decides what the banner _can_ ask (rows in the markup); the runtime decides what it _does_ ask. A gated tag registers its demand when it reaches its loading strategy, and `<color-scheme-toggle>` registers `preferences` when a visitor picks a scheme. A site that gates nothing never interrupts anyone.
12
+
13
+ **Breaking — consent cookie schema (v1 → v2).** Consent is now recorded per category as granted / declined / **not yet asked**. The third state is what lets a later demand ask about a category an earlier prompt didn't cover: accepting an analytics banner no longer silently declines preferences. Every stored v1 choice is invalid and re-prompts once — a v1 cookie asserted a definite answer for categories the visitor was never shown.
14
+
15
+ **Breaking — `ConsentApi`.**
16
+ - `needed()` now means "something demanded a category the visitor hasn't answered", not "no cookie yet". A custom banner calling it will now stay hidden until something asks.
17
+ - `ConsentState.decided` (a single boolean) is gone. Use `decidedFor(state, category)`, or `undecidedCategories(state)`.
18
+ - New: `require(category)` registers a demand and reports whether it is granted; `request(category)` resolves once the visitor answers it; `demanded()` lists what has asked so far.
19
+ - `acceptAll()` / `rejectAll()` still mean every optional category. The banner no longer routes its buttons through them — "Accept" applies to the categories on screen.
20
+
21
+ **Breaking — `<color-scheme-toggle>` persistence.** The chosen scheme still applies immediately, but writing it to `localStorage` now waits on the `preferences` category **when the site has a consent gate**. Sites without `consent` enabled are unaffected and keep storing as before. If your site enables consent and offers a theme toggle, make sure `preferences` is among the offered categories (it is by default).
22
+
23
+ **Changed — offered categories.** `consent.categories` defaults to `['analytics', 'preferences']` (plus `marketing` when a configured tag needs it) rather than only what analytics detection could see. An unused row is hidden markup, so offering broadly costs nothing and covers `data-consent` markup no build-time scan can find. Pass `categories` explicitly to narrow it.
24
+
25
+ **Added — `<Head />` emits a small inline consent stub** when the gate is on. `consent.js` and `elements.js` are both deferred with no ordering between them, so without it a theme toggle could be clicked before the gate existed, read "no gate", and store. The stub answers `false` and queues, and the runtime replays the queue on load.
26
+
27
+ **Fixed — the generated cookie notice** now discloses the stored display preference when `preferences` is offered, and no longer promises a banner "shown when you first visit", which is no longer how the banner works.
28
+
29
+ - Every custom element now carries the `wc-` prefix. **Two renames affect existing markup:**
30
+ - `<color-scheme-toggle>` → **`<wc-color-scheme-toggle>`**
31
+ - `<wc-multifield>` → **`<wc-multi-field>`**
32
+
33
+ Update your templates. An unknown element name is not an error — the browser treats it as an
34
+ inert `HTMLUnknownElement` — so a missed rename shows up as a control that renders and simply
35
+ never works, with nothing in the console.
36
+
37
+ `<wc-multifield>` was itself renamed in 3.0 (from `<multi-field>`), and hyphenating it now
38
+ means the tag matches its pattern name and its Bricks element everywhere else in the toolchain;
39
+ `3.0` shipped the inconsistency by accident.
40
+
41
+ **Unchanged:** the Bricks element keys (`vitops-color-scheme-toggle`, `vitops-multi-field`), so
42
+ elements already placed on a Bricks page keep working; the custom properties; and the events.
43
+
44
+ The remaining renames are prefix-only and reach no consumer: `<color-wheel>`, `<icon-picker>`,
45
+ `<oklch-color-picker>` and `<typography-config>` are registered but shipped in **no bundle** (the
46
+ editor-v2 track), so those tags were already inert in a consumer project — `vitops docs components`
47
+ now says so per element rather than leaving it to be discovered.
48
+
49
+ ### Minor Changes
50
+
51
+ - 14813fa: Add `<wc-tree>`, and a schema walker that feeds it.
52
+
53
+ `<wc-tree>` is progressive enhancement for the existing `.tree` pattern: given a nested
54
+ `<details>` disclosure tree it adds a filter, expand-all / collapse-all, and hash deep-linking.
55
+ It ships in `elements.js`.
56
+
57
+ `@getvitops/astro` gains `./components/Tree.astro` and `./components/tree.ts` (its `TreeItem`
58
+ type) to build that markup from data. As with every wrapper over a web component, `<Tree />`
59
+ emits the `<wc-tree>` tag itself with the accessible fallback inside — so `<Tree items={…} />`
60
+ is the whole call, and wrapping it in your own `<wc-tree>` nests two elements on one tree.
61
+
62
+ The slotted markup is the whole content and works without it — every node readable, expandable
63
+ and linkable — so the element only adds what CSS cannot do. Four things worth knowing:
64
+ - **The toolbar is generated, never authored.** A search field that does nothing is worse than
65
+ no search field, so the controls exist only once the element upgrades.
66
+ - **Deep-linking is the non-obvious one.** A node inside a closed `<details>` has no layout box,
67
+ so the browser's own fragment navigation finds nothing and silently stays at the top of the
68
+ page. `<wc-tree>` opens the target's ancestors — and the target's own disclosure — then scrolls.
69
+ - **Filtering matches a node's own label and description, never its subtree's text.** The obvious
70
+ `textContent` implementation makes every ancestor of a hit match, so the root matches any query
71
+ and the filter narrows nothing. That decision lives in a pure, tested module (`matchTree`)
72
+ rather than in the DOM wiring.
73
+ - **It initialises even when upgraded mid-insertion.** An element connected during an
74
+ `innerHTML` write — a view-transition swap, a client-side navigation — has no children yet
75
+ (measured: zero). Setup retries once the insertion completes, because the failure mode was
76
+ silent: no toolbar, no filter, no deep link, no error.
77
+
78
+ `patterns/tree.css` now supports two markup shapes: the item may be the `<details>` (the
79
+ pattern's original contract) or an `<li class="tree__item">` wrapping one, which is what gives
80
+ assistive tech list position and depth.
81
+
82
+ **`.tree` indent is fixed and now fluid**, which matters for any deep tree. Three separate leaks
83
+ compounded, each invisible in the CSS and each charging roughly one extra indent per level:
84
+ - the rule set both `margin-inline-start` and `padding-inline-start` to `--tree-indent`;
85
+ - `.tree { padding: var(--_p) }` applied to every nested `.tree`, not just the outermost — on all
86
+ four sides, so it narrowed from the end as well;
87
+ - `patterns/details.css` gives every non-summary child of a `<details>`
88
+ `margin-inline: var(--_content-margin)`, and its `details, .details` selector takes `:is()`
89
+ specificity under CSS nesting (**0-1-1**), so `.tree { margin: 0 }` at 0-1-0 _loses_ to it. A
90
+ nested tree is exactly such a child.
91
+
92
+ Measured in a browser: **41px per level against a 24px design**, so a 9-deep tree spent 382px on
93
+ indent and left its deepest label 162px wide. Now 24px per level, 198px total, and 653px of label.
94
+ The nested rule resets both box axes before applying the one indent it owes, and the default is
95
+ `clamp(0.75rem, 2.5vw, 1.5rem)` — 12px on a phone, 24px on a desktop — because a tree's depth is a
96
+ property of the data and cannot be known when the pattern is written. `--tree-indent` still
97
+ overrides it, and the `--lines` elbow derives from the same resolved value so the two cannot
98
+ disagree.
99
+
100
+ **Leaf rows now align with their branch siblings.** A branch spends a toggle column on its chevron
101
+ before its label; a leaf has none, so every leaf label sat one full column (24px, measured) to the
102
+ inline-start of its siblings and nothing at a given depth lined up. `.tree` exposes
103
+ `--_node-indent` (the toggle column) and `--_leaf-indent` (what a row without a toggle owes to
104
+ reach the same label column); `.tree__content` and `.tree__desc` both read the latter rather than
105
+ re-deriving the sum.
106
+
107
+ `<wc-tree>` also wraps its generated filter in `.input-group`, because `forms.css` styles text
108
+ controls as `.form-group > input` / `.input-group > input` — a bare `<input>` rendered with the
109
+ browser's 2px inset border and square corners next to the framework's own rounded controls.
110
+
111
+ `schemaTreeNodes(schema, { idPrefix, maxDepth, prune })` is new in `@getvitops/generator`: it walks
112
+ a JSON Schema — the same walk behind the `authoring.md` / `config.md` references — and returns tree
113
+ **data**, not markup, so each medium renders it (markdown for agents, an accessible `<details>`
114
+ tree for a site). Field ids are dotted config paths (`site.analytics.clarityId`), so any field can
115
+ be linked to directly.
116
+
117
+ `prune` exists for a specific hazard. The project config declares `designSystem.themes` as a
118
+ looser shape than `DesignSystemSchema`, because the full one is applied separately by
119
+ `resolveTheme` — so its embedded copy is an **approximation**, and measurably missing the
120
+ descriptions for `colors`, `colors.palette`, `colors.roles` and `colors.utilities`. Anything
121
+ rendering that copy as the token reference would silently document the colour system less well
122
+ than the toolchain already does; `prune` lets a caller stop at the wrapper and render `jsonSchema`
123
+ alongside instead.
124
+
125
+ `renderInlineMarkdown()` is exported for schema description text. It **lifts code spans out before
126
+ applying emphasis**, and that ordering is load-bearing rather than tidy: `colors.utilities`
127
+ describes its families as `` `bg-*` ``, `` `text-*` ``, `` `border-*` `` — literal asterisk
128
+ _wildcards_. Run emphasis over the raw string and the `*` closing `bg-*` pairs with the one
129
+ closing `text-*`, italicising the text between two unrelated utilities and eating both asterisks,
130
+ leaving prose that names families which don't exist. It also now renders single-asterisk emphasis,
131
+ which the schema does use (`*presentation*`, `*domain properties*`) and which previously printed
132
+ the asterisks literally.
133
+
134
+ Also fixed: `config.md` claimed "Only the wrapper is listed here" under `designSystem` and then
135
+ emitted the entire token schema anyway — `themes.<name>` _is_ a design system, so the walk
136
+ descended into it. It now stops at the wrapper and delegates to `authoring.md` as it always said
137
+ it did.
138
+
139
+ ### Patch Changes
140
+
141
+ - 14813fa: Fix `<wc-entries>`, `<wc-carousel>` and `<wc-marquee>` silently failing to enhance when inserted
142
+ dynamically.
143
+
144
+ All three parse their slotted markup in `connectedCallback` and return early when they find
145
+ nothing. That is fine when `elements.js` loads as a deferred module, because the document is
146
+ fully parsed before any definition registers. But an element upgraded _during_ insertion is
147
+ connected **before its children exist** — verified: an `innerHTML` write runs
148
+ `connectedCallback` with zero children.
149
+
150
+ So on an Astro view-transition swap, a client-side navigation, an `innerHTML` write or a cloned
151
+ template, `<wc-entries>` never built its table, `<wc-carousel>` never cloned its slides or
152
+ started autoplay, and `<wc-marquee>` never took over from the CSS-only path. Nothing errored;
153
+ the un-enhanced fallback just stayed on screen, which is why this went unnoticed.
154
+
155
+ Initialisation is now expressed as a function that reports whether it found its markup, and is
156
+ retried once the insertion completes (`initFromLightDom`). Setup is guarded so a retry after a
157
+ genuinely empty state can't double-apply — an `<wc-entries>` building its table twice would
158
+ have been the obvious way to fix this badly.
159
+
160
+ `<wc-tree>` uses the same helper. If you write a light-DOM component, use it too; the hazard is
161
+ documented in `web-components/utils/upgrade.ts`.
162
+
163
+ - a9d7916: Fix `sitenav` shipping its drawer geometry into its navbar state.
164
+
165
+ A `.sitenav` carrying both a drawer-direction modifier and a breakpoint promotion rendered its navbar one full panel-width toward the inline-end at and above the promotion width — off the page, with a horizontal scrollbar and anything laid out after the panel pushed out of view. Reported downstream against `sitenav--bp-lg sitenav--drawer-end`; it affected every `--bp-*` value and both drawer directions.
166
+
167
+ One cause, three symptoms. `@container` contributes no specificity, so when the four per-breakpoint blocks were consolidated into a single style query in 3.0.0, the wide-state reset lost the `.sitenav--bp-*` class that had been winning it the tie. Every narrow-state rule with a heavier selector then won _inside the wide state_:
168
+ - `.sitenav--drawer-end .sitenav__panel` (0,2,0) beat the navbar's `translate: none` (0,1,0) — the reported bug.
169
+ - The mobile accordion's `.sitenav__item--branch > .sitenav__submenu { overflow: hidden }` (0,2,0) beat the dropdown's `overflow: visible`, which **clipped away every third-level megamenu** at width. Its `padding-inline-start: 1rem` leaked in the same way.
170
+ - The wide block's counter-rule for depth caps beat the base `.sitenav__submenu { display: flex }`, so a `sitenav__item--desktop-branch` dropdown lost its column direction and gap.
171
+
172
+ The drawer and the navbar are now mutually exclusive branches of the same style query — `@container style(--_sitenav-wide: 0)` alongside the existing `: 1` — so there is no cascade contest to lose rather than a contest that is currently being won. A new test in core asserts the invariant across every two-state pattern.
173
+
174
+ **Two visible changes at width, both intended:** dropdown padding is now symmetric `0.5rem` (it was `1rem` on the inline-start side only), and depth-capped dropdowns regain their `0.25rem` gap.
175
+
176
+ Also fixed in the same pattern: `.sitenav__panel` never undid the UA `[popover]` sheet's `margin: auto` and `height: fit-content`, so `inset-block: 0` bought nothing and the drawer rendered as a content-height card floating in the vertical centre instead of filling the edge. It is now a full-height drawer, and the auto margins no longer absorb free space when a promoted `.sitenav` is stretched rather than content-sized.
177
+
178
+ Note the narrow state now also depends on `@container style()` (Chrome 111 / Safari 18 / Firefox 128), which the wide state already required. On an older engine the sitenav degrades to a popover holding an expanded link list — usable and accessible, but neither drawer nor navbar.
179
+
180
+ - Updated dependencies [c6b99e7]
181
+ - Updated dependencies [f7bc0a0]
182
+ - Updated dependencies [ceed51f]
183
+ - @getvitops/utils@4.0.0
184
+
185
+ ## 3.0.0
186
+
187
+ ### Major Changes
188
+
189
+ - 6e68ace: **Breaking:** the site config is now a three-section `Config` — `designSystem`,
190
+ `organization`, `site`.
191
+
192
+ The flat `SiteConfig` held the company (`organization`, `contact`, `locations`) and the
193
+ deployment (`analytics`, `environments`, `seo`, `legal`) as peers, so no single noun
194
+ described it, and a second site sharing the same company had no way to say so. The three
195
+ sections split those apart: several sites can now carry one `organization` and differ only
196
+ in `site`.
197
+
198
+ **Migrating.** `designSystem` stays at the root, and the fields already under
199
+ `organization` stay where they are. Everything else moves into one of two sections:
200
+ - → `site`: `defaultLocale`, `locales`, `domains`, `dns`, `cloudflare`, `environments`,
201
+ `abTesting`, `fonts`, `tags`, `postTypes`, `galleries`, `testimonials`, `templates`,
202
+ `navigation`, `seo`, `analytics`, `notifications`, `tracking`, `security`, `legal`,
203
+ `icons`, `favicon`, `deployment`
204
+ - → `organization`: `contact`, `primaryLocation`, `locations`, `services`, `links`
205
+
206
+ You do not have to work this out from the list: `vitops validate` detects a pre-3.0 flat
207
+ config and prints every move by name, rather than reporting a dozen unknown keys.
208
+
209
+ ```jsonc
210
+ {
211
+ "designSystem": {
212
+ "themes": {
213
+ "default": {
214
+ /* … */
215
+ },
216
+ },
217
+ },
218
+ "organization": {
219
+ "name": "Acme",
220
+ "contact": "hq",
221
+ "locations": {
222
+ "hq": {
223
+ /* … */
224
+ },
225
+ },
226
+ },
227
+ "site": {
228
+ "defaultLocale": "en",
229
+ "domains": {
230
+ /* … */
231
+ },
232
+ "analytics": {
233
+ /* … */
234
+ },
235
+ },
236
+ }
237
+ ```
238
+
239
+ **Renamed exports** (`@getvitops/generator`): `SiteConfigSchema` → `ConfigSchema`,
240
+ `SiteConfig` → `Config`, `validateSite` → `validateConfig`, `resolveSiteConfig` →
241
+ `resolveConfig`, `isSiteConfig` → `isConfig`, `siteJsonSchema` → `configJsonSchema`,
242
+ `SITE_SCHEMA_URL` → `CONFIG_SCHEMA_URL`, `SiteValidationResult` →
243
+ `ConfigValidationResult`. `ResolvedInput.site` is now `ResolvedInput.config`. New:
244
+ `OrganizationConfig` and `SiteSection` for the two sections.
245
+
246
+ **Renamed schema file:** `@getvitops/generator/site.schema.json` →
247
+ `@getvitops/generator/config.schema.json`. Update the `$schema` key in your config.
248
+
249
+ Option names are unchanged — `vitops({ site: { input } })`, the Vite plugin's `site`,
250
+ and `generate({ site })` still point at the config file.
251
+
252
+ **The Astro integration is now `vitops()`, not `getvitops()`.** It is a default export,
253
+ so the name is yours and nothing breaks on upgrade, but every example now reads
254
+ `import vitops from '@getvitops/astro'` — matching the Vite plugin, which has always been
255
+ `vitops`. The `@getvitops/*` package scope and the internal `virtual:getvitops/*` module
256
+ ids are unchanged; neither is an import name.
257
+
258
+ **Added:** a generated config authoring reference — `vitops docs config`, `docs/config.md`
259
+ in the OKF bundle, and _Config reference_ on the docs site. It is walked from the published
260
+ JSON Schema by the same helper that renders the `design-system.json` reference, so it
261
+ cannot describe a field validation does not accept.
262
+
263
+ - b115993: Nav shells, a top-layer animation driver, and a pile of fixes to things that never worked
264
+
265
+ ### Breaking
266
+ - **Two custom elements renamed.** `<copy-button>` → `<wc-copy>`, `<multi-field>` →
267
+ `<wc-multifield>`. Update your markup. The Bricks element keys
268
+ (`vitops-copy-button`, `vitops-multi-field`) are **unchanged**, so elements already
269
+ placed on a Bricks page keep working; so are the `--multi-field-*` custom properties
270
+ and the `multi-field-*` events.
271
+ - **`<details>` now animates open and closed** where `prefers-reduced-motion` allows.
272
+ It was previously instant by deliberate choice — the transition used to deadlock the
273
+ disclosure shut. Re-verified on Chrome 149 with a real click; both `block-size` and
274
+ `content-visibility` must stay in the transition list, and dropping the latter
275
+ reproduces the deadlock.
276
+ - **Drawers and dialogs now animate on close.** Both drove their entry with
277
+ `animation:` on `[open]`, which by construction plays once — so the close was
278
+ instant. They now use the top-layer driver and animate both ways.
279
+ - **`.rhythm` gives every non-heading block heading-spacing before a heading.** The
280
+ pairs used to enumerate `p` (later `p, pre, blockquote, table, dl, ul, ol`), so a
281
+ heading after anything else got paragraph spacing. They are now defined by what a
282
+ heading _is_ — `h1`–`h6` plus the `font-display` / `font-title` / `font-heading` type
283
+ roles — and inverted to `:not(<heading>) + <heading>`. Expect slightly more space
284
+ above headings that follow a code block, table, figure-like `<div>` or component.
285
+ - **`popover.css` no longer pins `[popover]:popover-open { opacity: 1 }`.** It never
286
+ animated anything on its own, but it outranked `.transition` and made every
287
+ top-layer fade impossible. Removing it is what lets the driver work.
288
+ - **`nav.css`'s `.drawer-menu` timing** now follows `--animation-duration` /
289
+ `--custom-ease-out` instead of hand-set 0.4s / 0.7s.
290
+
291
+ ### Fixed
292
+ - **`.sitenav--bp-sm` dropdowns could never open.** Its desktop block had drifted onto
293
+ an older markup shape, selecting `.sitenav__disclosure > .sitenav__submenu` where the
294
+ submenu is the disclosure's _sibling_. The four "intentionally parallel" breakpoint
295
+ blocks are now one shared block behind a style query — `md`/`lg`/`xl` were
296
+ byte-identical and only `sm` had rotted, which is exactly the drift the arrangement
297
+ invited. 583 → 361 lines.
298
+ - **Hover dropdowns rendered off-screen.** A closed popover is not in the top layer but
299
+ keeps the UA's `position: fixed`, and `.dropdown--show-on-hover` reset only `inset` —
300
+ so the panel faithfully faded in ~3300px from its trigger. Now anchored to it, and it
301
+ follows on scroll. Affects `.split-link--show-on-hover` too.
302
+ - **Horizontal overflow on narrow screens.** `.centered > *` floors at
303
+ `min-inline-size: 0`; `body` gets `overflow-wrap: break-word`; `patterns/code.css`
304
+ gains the `pre` rules it never had (`max-inline-size: 100%`, `overflow-x: auto`) and
305
+ gives inline `<code>` `overflow-wrap: anywhere`. Note a scroll container does _not_
306
+ zero its min-content contribution in Chrome, which is what made a single `<pre>` hold
307
+ a 390px viewport open at 797px.
308
+ - **`.split-<a>-<b>` now publishes `--_flex-direction: row`.** Only `.flex-*` did, so
309
+ `class="split flex-col md-split-1-2"` left the variable reading `column` at every
310
+ width and a nested `.grouped` collapsed its borders on the wrong axis.
311
+ - **`.toc-layout` uses `minmax(0, 1fr)`** instead of a bare `1fr`, whose automatic
312
+ minimum is min-content.
313
+
314
+ ### Added
315
+ - **`patterns/navshell.css` + `<NavShell>` / `<NavShellToggle>`** — a navigation aside
316
+ beside content at width, collapsing to a toggle and drawer below it. **It nests**: a
317
+ site nav wrapping an on-this-page nav, each promoting at its own breakpoint, via a
318
+ style query on an inherited flag rather than four copied blocks. Its content column
319
+ is a container, so an inner shell measures the space it actually has. The toggle can
320
+ live outside the shell (a site header) with `toggle="external"`.
321
+ - **`patterns/navbar.css`** — `.navbar` extracted from `nav.css`, where it was only
322
+ half of that file's drawer⇄navbar pair, plus `--start` / `--center` / `--end`,
323
+ `__spacer` and `--sticky` (old `.navbar-sticky` aliased).
324
+ - **A top-layer animation driver.** `animation.css` gained the fourth driver alongside
325
+ `animate-view` / `animate-scroll` / `animate-trigger` / `transition`, and every effect
326
+ gained an `open-<fx>` state variant. Overlays now state _where they start_
327
+ (`--translate-x-from`) instead of owning a keyframe: `class="drawer drawer--right
328
+ open-fade-in"` composes slide and fade. Applied at zero specificity with identity
329
+ defaults, so a popover that sets no effect vars is unchanged.
330
+ - **One scrim.** `--scrim` / `--scrim-filter` tokens and `.no-scrim` in `popover.css`,
331
+ replacing 18 `::backdrop` blocks across seven partials. `.drawer--modeless` is aliased.
332
+ - **`<wc-marquee>`** — clones the content enough times to cover the track, so every gap
333
+ matches including the seam. The CSS-only `.marquee` is unchanged and still works
334
+ without it; the element only makes the spacing right. `--marquee-gap` added.
335
+ - **`--width-nav`** joins `--width-measure` / `-breakout` / `-spotlight` as the nav
336
+ column width.
337
+ - **`.skip-link`** in `patterns/anchor-link.css`, using a clip rather than the
338
+ `-100vw` idiom (which overflows in RTL and ignores the scrollbar gutter).
339
+ - **`.table-wrapper`** documented as the scroll wrapper for wide tables.
340
+
341
+ ### Notes
342
+ - `patterns/nav.css` is marked legacy. Its header promised a Lit nav component that was
343
+ never written and is not planned — the house pattern is native. Use `navbar`,
344
+ `sitenav` or `navshell`; removal is a later change.
345
+ - `scroll-target.css`'s `.is-current` no longer claims to be a JS scroll-spy fallback.
346
+ There is no such code; `:target-current` is the only working highlight, which today
347
+ means Chrome.
348
+ - `elements.js` gains one element (`wc-marquee`), so the shared bundle is slightly
349
+ larger for every consumer.
350
+ - A registered custom property's `initial-value` must be computationally independent —
351
+ `16rem` is not, so `@property` silently drops the whole rule. `navshell` uses an
352
+ inline fallback instead.
353
+
354
+ ### Patch Changes
355
+
356
+ - Updated dependencies [6e68ace]
357
+ - @getvitops/utils@3.0.0
358
+
3
359
  ## 2.1.0
4
360
 
5
361
  ### Patch Changes
package/css/animation.css CHANGED
@@ -529,6 +529,85 @@
529
529
  }
530
530
  }
531
531
 
532
+ /* ===========================================
533
+ * Tier 1c: Top layer (popover / dialog open-close)
534
+ * The `open-<fx>` state variants (generated, like hover-/focus-/active-) already
535
+ * supply the VALUES. What a top-layer element additionally needs is mechanism,
536
+ * and that is the same for every effect — so it lives here, once:
537
+ *
538
+ * <div popover class="drawer drawer--start transition open-slide-right">
539
+ *
540
+ * Before this existed there was no driver for the top layer, so every overlay
541
+ * pattern hand-rolled one: drawer.css drove entry with four @keyframes and
542
+ * `animation:`, which only plays on OPEN (its close was instant), while
543
+ * sitenav.css used a transition and animated both ways. Same job, two
544
+ * mechanisms, two behaviours.
545
+ * =========================================== */
546
+
547
+ /* Applied to the top layer BY DEFAULT, and at `:where()` zero specificity.
548
+ *
549
+ * Not an opt-in class, unlike the other three drivers, for two reasons. It is
550
+ * free: every endpoint below defaults to identity (opacity 1, translate 0,
551
+ * scale 1), so a popover that sets no effect vars transitions between two
552
+ * identical states and looks exactly as it did before. And it preserves the DX
553
+ * the keyframes had — `.drawer--right` animated without asking the author for a
554
+ * driver class, and it still does; it just states its start as
555
+ * `--translate-x-from: 100%` instead of owning a keyframe.
556
+ *
557
+ * Zero specificity means any pattern that wants different motion simply
558
+ * declares it. */
559
+ :where([popover], dialog) {
560
+ opacity: var(--t-opacity, var(--opacity-from, 1));
561
+ translate: var(--t-translate-x, var(--translate-x-from, 0))
562
+ var(--t-translate-y, var(--translate-y-from, 0));
563
+ scale: var(--t-scale-x, var(--scale-x-from, 1)) var(--t-scale-y, var(--scale-y-from, 1));
564
+ rotate: var(--t-rotate, var(--rotate-from, 0deg));
565
+ clip-path: var(--t-clip, var(--clip-from, none));
566
+
567
+ /* `display` and `overlay` are discrete, and BOTH must transition or the
568
+ element is yanked out of the top layer the instant it closes — you get an
569
+ entry animation and no exit, which is exactly the asymmetry drawer.css had.
570
+ `allow-discrete` is what keeps it rendered for the duration. */
571
+ transition-property: opacity, translate, scale, rotate, clip-path, display, overlay;
572
+ transition-duration: var(--animation-duration);
573
+ transition-timing-function: var(--custom-ease-out);
574
+ transition-behavior: allow-discrete;
575
+ }
576
+
577
+ /* The open flip. Mirrors what the generated `open-<fx>` variants do, so the
578
+ common case needs no effect class at all; an explicit `open-<fx>` still wins
579
+ on specificity and can pick a different effect. */
580
+ :where([popover]:popover-open, dialog[open]) {
581
+ --t-opacity: var(--opacity-to, 1);
582
+ --t-translate-x: var(--translate-x-to, 0);
583
+ --t-translate-y: var(--translate-y-to, 0);
584
+ --t-scale-x: var(--scale-x-to, 1);
585
+ --t-scale-y: var(--scale-y-to, 1);
586
+ --t-rotate: var(--rotate-to, 0deg);
587
+ --t-clip: var(--clip-to, none);
588
+ }
589
+
590
+ /* The entry half. A closed popover is `display: none`, so it has no
591
+ before-change style and the open transition would simply not run —
592
+ @starting-style supplies one.
593
+ *
594
+ * It declares the PROPERTIES rather than resetting the `--t-*` custom
595
+ * properties, which sidesteps a specificity trap: the generated
596
+ * `.open-<fx>:popover-open` rules are (0,2,0), set `--t-*`, and are imported
597
+ * after this file, so a custom-property reset here would have to outrank them.
598
+ * Setting `opacity`/`translate`/… directly at (0,2,0) instead competes only with
599
+ * the (0,0,0) base rule above, which it beats regardless of what any effect
600
+ * class does. */
601
+ @starting-style {
602
+ :is([popover], dialog):is(:popover-open, [open]) {
603
+ opacity: var(--opacity-from, 1);
604
+ translate: var(--translate-x-from, 0) var(--translate-y-from, 0);
605
+ scale: var(--scale-x-from, 1) var(--scale-y-from, 1);
606
+ rotate: var(--rotate-from, 0deg);
607
+ clip-path: var(--clip-from, none);
608
+ }
609
+ }
610
+
532
611
  /* Respect reduced-motion: drop keyframes, transitions, and continuous floats so
533
612
  elements settle at their natural resolved state. Plain selectors + !important
534
613
  so this reliably overrides the driver/float rules (a legitimate a11y override). */
@@ -543,6 +622,17 @@
543
622
  .transition {
544
623
  transition: none !important;
545
624
  }
625
+ /* The top-layer driver is applied by default rather than by class, so it needs
626
+ naming here explicitly — `.transition` above does not cover it. `display`
627
+ and `overlay` must still change, they just must not be animated, and
628
+ `transition: none` is exactly that. */
629
+ [popover],
630
+ dialog {
631
+ transition: none !important;
632
+ }
633
+ :where([popover], dialog)::backdrop {
634
+ transition: none !important;
635
+ }
546
636
  }
547
637
 
548
638
  /* ===========================================
package/css/global.css CHANGED
@@ -32,6 +32,16 @@
32
32
  around them. */
33
33
  body {
34
34
  margin: 0;
35
+
36
+ /* Long unbreakable strings are the single most common source of horizontal
37
+ page scroll, and prose is full of them — URLs, file paths, CSS selectors.
38
+ `break-word` rather than `anywhere` on purpose: it only takes effect when a
39
+ word would otherwise overflow, and it does NOT contribute to min-content
40
+ sizing, so no existing layout's intrinsic measurements move. (`anywhere`
41
+ does affect min-content, which would silently resize every intrinsically
42
+ sized box in a consumer's site.) Inline <code> gets the stronger `anywhere`
43
+ in patterns/code.css, where a break mid-token is the correct trade. */
44
+ overflow-wrap: break-word;
35
45
  }
36
46
 
37
47
  @media (prefers-reduced-motion: no-preference) {
package/css/index.css CHANGED
@@ -33,7 +33,12 @@
33
33
  @import './patterns/tooltip.css';
34
34
  @import './patterns/dropdown.css';
35
35
  @import './patterns/split-link.css';
36
+ /* navbar before sitenav: sitenav's wide state composes it. */
37
+ @import './patterns/navbar.css';
36
38
  @import './patterns/sitenav.css';
39
+ /* After sitenav (the nav family stays contiguous) and after drawer.css, whose
40
+ panel surface navshell's desktop block has to undo. */
41
+ @import './patterns/navshell.css';
37
42
  @import './patterns/notification.css';
38
43
  @import './patterns/lightbox.css';
39
44
  @import './patterns/overlay.css';