@getvitops/core 1.0.0 → 2.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,465 @@
1
1
  # @getvitops/core
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - bf453b0: The framework now ships a border-box reset.
8
+
9
+ ```css
10
+ *,
11
+ *::before,
12
+ *::after {
13
+ box-sizing: border-box;
14
+ }
15
+ ```
16
+
17
+ **This is a behavioural change for the `css` and `bricks` formats.** If your project does not
18
+ already set border-box — Tailwind's preflight does, as does normalize and almost every modern base
19
+ stylesheet — elements with padding or a border now measure their declared width inclusive of it, so
20
+ some boxes will come out narrower than before. The `tailwind` format is unaffected: preflight
21
+ already did this.
22
+
23
+ The framework used to refuse this on the grounds that a global reset silently reflows consumer
24
+ layouts, and that reasoning was sound when nothing was layered. Two things changed:
25
+ - **It is layered.** The rule lives in `vitops.base`, the lowest of the three framework layers, so
26
+ it loses to any unlayered consumer CSS. The opt-out is one rule in your own stylesheet:
27
+ `*, *::before, *::after { box-sizing: content-box }`.
28
+ - **Patterns are only correct under it.** `.split`'s ratio is a flex basis, which sizes the border
29
+ box; under content-box a padded column came out wider than its sibling by exactly its padding.
30
+ Stating the assumption once beats every pattern re-asserting it and the ones that forget being
31
+ quietly wrong.
32
+
33
+ - bf453b0: Utilities now beat patterns, in every format. Cascade layers are assigned by what a rule **is**,
34
+ not by which file it happens to live in.
35
+
36
+ `vitops.base → vitops.components → vitops.utilities` has existed for a while, and its whole purpose
37
+ is to let `class="card bg-danger-muted"` work without `!important`. But the layer was chosen per
38
+ **partial**, and `layout.css` was one file holding both the structural patterns (`.rhythm`,
39
+ `.centered`, `.split`) and roughly three quarters of the framework's utilities (`.m-*`, `.flex-*`,
40
+ `.items-*`, `.justify-*`, `.text-*`, `.split-<a>-<b>`, track placement). Unmapped, it defaulted to
41
+ `vitops.components` — so all of those utilities were shelved _below_ the patterns they are meant to
42
+ override, and quietly did nothing. `utilities.css` had the mirror-image problem: it held the
43
+ `.reveal` component family, which therefore outranked every pattern _and_ the display utilities.
44
+
45
+ Both are now split — `layout.css` / `layout-utilities.css`, and `patterns/reveal.css` — and the
46
+ classification is asserted across formats by a shared `LAYER_CONTRACT`, so the two halves cannot
47
+ drift apart again.
48
+
49
+ **What visibly changes.** Each of these was silent before and now takes effect. That is the point,
50
+ but if you wrote one of these combinations and never noticed it did nothing, you will see a change:
51
+
52
+ | markup | before | after |
53
+ | ---------------------------------------- | ------------------------------------------ | ---------------------- |
54
+ | `class="table text-center"` | `.table { text-align: start }` won | `.text-center` wins |
55
+ | `class="banner items-start"` | `.banner`'s centring won | the utility wins |
56
+ | `class="cluster-between justify-center"` | `.cluster-between` won | `.justify-center` wins |
57
+ | `class="icon justify-start"` | `.icon { justify-content: center }` won | `.justify-start` wins |
58
+ | `class="combobox flex-row"` | `.combobox { flex-direction: column }` won | `.flex-row` wins |
59
+ | `class="media items-center"` | `.media { align-items: flex-start }` won | `.items-center` wins |
60
+ | `<details>` first content child + `m-0` | `details.css`'s `> summary + *` margin won | `.m-0` wins |
61
+ | `class="split flex-col"` | worked, but on source order | works by layer |
62
+
63
+ The reveal family moves the other way, having been mis-shelved as a utility: `class="reveal hidden"`
64
+ and `class="reveal-fade block"` used to keep the reveal's own `display` and now lose to the display
65
+ utility. The duplicate `details::details-content` rules in `utilities.css` are deleted —
66
+ `patterns/details.css` was always the owner, and its selector is a superset — so the collapsed
67
+ content box is now `overflow: hidden` rather than `clip`.
68
+
69
+ **Removed: the bare `<bp>-split` classes** (`sm-split`, `md-split`, `lg-split`, `xl-split`).
70
+ `.split` is a pattern now, and `@utility` cannot live in a cascade layer — measured against
71
+ tailwindcss@4.3.3, it throws inside `@layer` _and_ inside a file imported with `layer(…)`, and a
72
+ `@custom-variant` can't reach a components-layer class either — so `@md:split` became impossible and
73
+ the css/bricks counterpart had to go with it. Use `md-flex-row` for "become a row at md"; it says
74
+ the same thing in every format. The one thing lost is resetting a bare ratio back to equal at a
75
+ breakpoint, which had no usages; apply the ratio at the breakpoint instead.
76
+
77
+ **Also fixed, in the tailwind format only** — three divergences found while auditing the layers:
78
+ - `body` was emitted with `container-type` but not `container-name: body`, so the
79
+ `@container body (…)` queries in the scroll/TOC patterns never matched: `.toc-layout` and
80
+ `.toc-sidebar` were stuck in their narrow layout.
81
+ - `grid-auto` was missing the `:is(ul, ol) > li + li` margin reset, so a `<ul class="grid-auto">`
82
+ inside `.rhythm` got a stray top margin on every item but the first.
83
+ - **`.sticky` was being deleted outright.** `sticky` is a Tailwind utility name, and the strip that
84
+ defers those to Tailwind matched a rule's leading class — taking `patterns/sticky.css` with it,
85
+ including `--sticky-offset`, the z-index wiring and every `.sticky--bottom` / `--inline-start` /
86
+ `--inline-end` variant. Components whose names collide with Tailwind utilities are now allowlisted.
87
+
88
+ **Migration.** If a layout utility on a patterned element now "suddenly works" and you preferred the
89
+ old result, remove the utility — it was never doing anything. To keep a pattern winning over the
90
+ framework's utilities, write the declaration in your own stylesheet: unlayered CSS still beats all
91
+ three framework layers.
92
+
93
+ ### Minor Changes
94
+
95
+ - 4756788: Add `<Analytics />` and a general-purpose consent gate.
96
+
97
+ `getvitops({ analytics })` configures Google Analytics 4, Microsoft Clarity, Matomo and Plausible;
98
+ `<Analytics />` emits their tags. Nothing touches the critical path — `strategy` defaults to `'idle'`,
99
+ which loads every tag after `load` on an idle callback (`'async'` and `'interaction'` are the other
100
+ options), and no `preconnect` is emitted, since warming a third-party connection during parse is the
101
+ cost `idle` exists to avoid.
102
+
103
+ `getvitops({ consent })` adds the gate: `@getvitops/core/consent`, a 2.3 KB gzipped Lit-free bundle,
104
+ plus `<CookieConsent />`.
105
+
106
+ ```js
107
+ vitops({
108
+ analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
109
+ consent: { policyUrl: '/legal/cookies' },
110
+ });
111
+ ```
112
+
113
+ **Consent is not an analytics feature.** The gate is general — mark anything
114
+ `data-consent="<category>"` and it waits on the same choice, so A/B assignment, personalisation and
115
+ third-party embeds use it too, and a site can enable the banner with no analytics at all. Categories
116
+ are `necessary` / `analytics` / `marketing` / `preferences`, and `window.vitopsConsent` plus a
117
+ `vitops:consent` event on `document` are how your own code reads the answer.
118
+
119
+ **Which category a provider needs is derived, not declared.** It follows from whether that provider
120
+ sets cookies, which follows from its own config: Matomo runs cookieless by default (`disableCookies`)
121
+ and so needs no banner; `cookies: true` opts in and moves it behind `analytics`. You can't mark
122
+ Google Analytics `necessary` to skip the banner — but you can pick a genuinely cookieless provider.
123
+
124
+ Gated tags render as `<script type="text/plain">` with the URL on `data-src`, so an undecided or
125
+ declining visitor's page issues **no third-party request at all**. For Google Analytics that is basic
126
+ consent mode rather than Consent Mode v2 advanced: nothing reaches Google until the visitor accepts.
127
+ Clarity additionally receives `clarity('consentv2', …)`, because Microsoft enforces the signal
128
+ separately for EEA/UK/CH traffic. Nothing is stored until a choice is made — the banner can't be the
129
+ thing that needs consent — and revoking clears the provider's cookies and reloads, because an
130
+ already-executing tracker can't be unloaded any other way.
131
+
132
+ The banner is shown in the top layer via `popover="manual"`, like `.tooltip`: a plain fixed banner
133
+ resolves against the nearest containing block, and `body { container-type: inline-size }` — ordinary
134
+ in a framework whose breakpoints are container queries — would otherwise trap it mid-page.
135
+
136
+ **The site config gained `analytics.clarityId` and `analytics.matomo`**, so `vitops legal` discloses
137
+ them. Clarity and Matomo join the processor table with their real cookie names, cookieless Matomo
138
+ asserts positively that it sets none, and a Clarity site's privacy policy now describes session
139
+ replay rather than filing it under page-view analytics. Configure `legal` alongside `analytics` and
140
+ the Astro integration cross-checks the two, naming any provider you'd otherwise run without
141
+ disclosing; it also warns when a cookie-setting provider has no `consent` gate.
142
+
143
+ Existing configs are unaffected — both options are off unless provided. One behaviour change if you
144
+ use the ad-conversion tracking script: `packages/astro/src/scripts/tracking.ts` now waits for
145
+ `marketing` consent before writing its 90-day `_ac` click-ID cookie, when the gate is present. With
146
+ no gate on the page it behaves exactly as before.
147
+
148
+ - bf453b0: Animation families that never actually animated, and a driver that fired before you could see it.
149
+
150
+ Each of these looked correct in source and failed somewhere else — in the bundler, in hit-testing,
151
+ or in the difference between a time-based and a progress-based timeline. Nothing here needs a
152
+ markup change.
153
+
154
+ **Changed: entrances are now timed off the element's midpoint.** Every driver used to key off a
155
+ fraction of the element's _own height_, which meant the same class behaved differently on a 4rem
156
+ card and a full-bleed section — and on small elements it was over before they appeared
157
+ (`entry 20%` is about 17px of scroll for a 5.5rem tile). Motion now starts once the element's
158
+ **midpoint is 10% of the viewport in**, and a one-shot entrance completes at **25%**. This applies
159
+ to `animate-view` and to the `.is-active` observer that drives `animate-trigger` and the
160
+ `active-<fx>` transition variants, so everything on a page starts at the same moment on screen.
161
+
162
+ The pivot is `entry 50%` — the one point that means "midpoint on the viewport edge" for an element
163
+ of any height — plus a viewport length. Shift the window with `--anim-start` / `--anim-end`, or
164
+ replace it with `--anim-range`. `--stagger-range-step` is now a viewport length (`5vh`) to match.
165
+
166
+ **Fixed: every journey ran on a truncated range.** The generator emitted `animation-range: entry
167
+ exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
168
+ 100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
169
+ `from` state — as the element hits the top of the viewport, which is to say they faded out while
170
+ still fully on screen. Journeys now run `entry calc(50% + 10vh) → exit 100%`: they start on the same
171
+ midpoint pivot as everything else and keep the full entry → hold → exit arc, so the hold sits in the
172
+ middle of the crossing. `animation-effects.test.ts` asserts on the **bundled** css, because the
173
+ emitter was right the whole time.
174
+
175
+ **Fixed: `slide-journey` had no distance to travel.** `animations.journeys.base.slide` was `{}` in
176
+ both `defaultConfig()` and the shipped example, so the keyframe animated `translate: 0 → 0`. It now
177
+ declares `translate-y-from: var(--slide-distance, 2rem)` — the same var the `slide-up` effect uses,
178
+ so one knob tunes both. If your own config has an empty `slide` base, add the same line.
179
+
180
+ **Fixed: `reveal-*` on hover was unreachable.** A `hover-reveal-left` element rests at `clip-path:
181
+ inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
182
+ hittable area and could never receive the hover that would reveal it. The state variants now match
183
+ the element **or its direct parent**, mirroring what `animation.css` already did for the trigger
184
+ driver (`:is(.is-active, [data-active]) > .animate-trigger`):
185
+
186
+ ```css
187
+ .hover-<fx>:hover, :hover > .hover-<fx> { … }
188
+ .focus-<fx>:focus-visible, :focus-within > .focus-<fx> { … }
189
+ ```
190
+
191
+ Specificity is unchanged (0-2-0), so nothing reorders. **Behaviour change:** a `hover-<fx>` element
192
+ that is a direct child of a hovered element now flips with its parent. Wrap it in an intermediate
193
+ element if you need the old element-only behaviour.
194
+
195
+ **Fixed: `.stagger` did nothing on a scroll-driven timeline.** It offsets children with
196
+ `animation-delay`, which is time-based and is ignored outright on a `view()` / `scroll()` timeline,
197
+ so `.stagger > .animate-view` arrived all at once. It now also offsets each child's view
198
+ `animation-range` by the same index, so one class works under both driver families — tune the
199
+ scroll-driven step with `--stagger-range-step` (default `5vh`). Journeys declare their own range and
200
+ opt out by construction.
201
+
202
+ Two smaller faults found alongside it: `@supports (--x: sibling-index())` is **always true** — any
203
+ token stream is a valid custom-property value — so the guard around `sibling-index()` guarded
204
+ nothing in both `.stagger` and `.subgrid`'s row index, and on an engine without support the
205
+ declaration went invalid rather than being skipped. Both now test a real property. And the CSS path
206
+ was 1-based while the JS fallback wrote 0-based, so the two disagreed by one step wherever both ran.
207
+
208
+ **Fixed: the pre-paint `<html>` class script was missing entirely.** `<Head />` now emits it again,
209
+ outside the `webComponents` block since it gates stylesheet behaviour rather than the element
210
+ runtime. It does two things `animation.css` depends on and that three other files documented as
211
+ already shipping:
212
+ - `no-js` → `js`, so `.animate-trigger` is paused only where JS can un-pause it;
213
+ - `.no-scroll-timeline` when `animation-timeline: view()` is unsupported, which is what cancels
214
+ `.animate-view` / `.animate-scroll`. Scroll-driven animations are deliberately **not** polyfilled,
215
+ so without this flag those elements sit at their `from` keyframe — `opacity: 0`, i.e. invisible
216
+ content — on any engine without support. It had been dropped in the move to publishable packages,
217
+ leaving the cancel rule as dead code.
218
+
219
+ **New: `hover-size-grow` and friends exist.** The layout effects (`size-grow`, `size-shrink`) were
220
+ the one family with no state variants, on the grounds that `.transition` doesn't cover `height`.
221
+ That wasn't a platform limit — `height: 0 → auto` just needs `interpolate-size: allow-keywords`,
222
+ which the framework already sets on `:root`, and which the `layout` **keyframe** depended on
223
+ equally. So the exclusion bought no portability; it only made `hover-size-grow` a class the docs
224
+ advertised and the stylesheet didn't define. `.transition` now declares `height` inside an
225
+ `@supports (interpolate-size: allow-keywords)` block, and the generator emits the full
226
+ `hover-`/`focus-`/`active-` set for layout effects, carrying each effect's own `overflow: clip` so a
227
+ collapsed box hides its content instead of spilling it. Where `interpolate-size` is missing, the
228
+ height simply doesn't transition — the same degradation the keyframe path has. (For the record:
229
+ `transition-behavior: allow-discrete` is _not_ the tool here — it only lets genuinely discrete
230
+ properties transition, and would snap at the midpoint.)
231
+
232
+ `.transition` switched from the `transition` shorthand to `transition-property` +
233
+ `transition-duration` + `transition-timing-function`, so the gated block can append `height` without
234
+ restating the list. If you override `.transition`'s timing, override the longhands.
235
+
236
+ The generated class reference (`vitops docs classes`) was the upstream source of that mismatch — it
237
+ listed every effect under "with the state prefixes above", including the layout ones. It now states
238
+ which stage needs the feature gate, when each driver plays, and how `stagger` composes with both.
239
+
240
+ - bf453b0: New `gap-*` utilities over the fluid space scale, in all three formats.
241
+
242
+ `gap-<name>`, `gap-x-<name>` (column) and `gap-y-<name>` (row), for every step of your
243
+ `spaceScale` — `gap-2xs` … `gap-7xl` — each breakpoint-prefixable (`md-gap-l` in css/bricks,
244
+ `@md:gap-l` in tailwind).
245
+
246
+ There was no gap utility at all before. `vitops docs css` advertised a `g` class that was never
247
+ emitted, so the honest answer for a css or bricks consumer was an inline `style="gap: …"`. Tailwind
248
+ did not fill the hole either: the fluid steps are deliberately kept **out** of Tailwind's
249
+ `--spacing-*` namespace, because named keys there shadow the size scales (`max-w-7xl` would resolve
250
+ to `var(--spacing-7xl)` and collapse layouts), so `gap-l` was not something Tailwind could derive.
251
+ Measured against tailwindcss@4.3.3: the emitted `@utility gap-l` is honoured, accepts variants, and
252
+ coexists with the built-in numeric `gap-4`, which keeps Tailwind's own multiplier.
253
+
254
+ The whole matrix is emitted rather than a plausible subset. An undefined step produces no rule and
255
+ no error in either format, so a missing `md-gap-x-2xl` would be indistinguishable from a working
256
+ one.
257
+
258
+ In the css and bricks bundles these land in `vitops.utilities`, so `class="cluster gap-l"` beats
259
+ the pattern's own gap.
260
+
261
+ - 04a51d8: Icons: one semantic vocabulary across icon sets, with the bundle derived from your source.
262
+
263
+ Name icons by meaning — `<Icon name="menu" />` resolves to `fa7-solid:bars`, `ph:list` or
264
+ `lucide:menu` depending on the set you configure, so swapping sets is a config edit rather than a
265
+ find-and-replace. A name containing `:` still passes through untouched, which is the escape hatch
266
+ for a set-specific glyph.
267
+
268
+ **New `icons` option on `getvitops()`.** Configures the sets once per site and, under
269
+ `output: 'server'`, derives astro-icon's `include` map by scanning your source — the list most
270
+ projects end up maintaining by hand. On a static build no `include` is passed at all, because
271
+ astro-icon is already zero-config there and a list could only drop a glyph the scan couldn't see.
272
+ Names you declare that don't resolve fail the build; names only the scan found just warn; names
273
+ computed at runtime are reported with file and line so you can declare them.
274
+
275
+ **New `<Icon />` component**, and a real fix with it: `Popover`, `Details` and `Drawer` imported
276
+ `astro-icon/components` at module scope, so the _optional_ peer was resolved whether or not an icon
277
+ ever rendered — hard-failing anyone who hadn't installed it. Engines now load dynamically. The
278
+ per-component `iconResolver` prop still works but is deprecated in favour of the integration option.
279
+
280
+ **New SVG sprite** (`icons.sprite: true` in your site config) for consumers that can't run an icon
281
+ integration — Bricks/WordPress, EmDash renderers, plain HTML. `<use href="…/icons.svg#ph--list">`,
282
+ no JavaScript and no icon-API request. Every semantic name also gets a set-independent `icon-<name>`
283
+ alias, so sprite markup survives an icon-set change. WordPress gets `vitops_icon()`, a
284
+ `[vitops_icon]` shortcode and a **Vitops → Icon** Bricks element.
285
+
286
+ **New `vitops icons`** command: reports which icons your source uses, which names couldn't be
287
+ resolved, and which are computed at runtime; `--sprite` builds the sprite, `--json` for CI.
288
+
289
+ **Renamed, and completed to all four directions.** Chevrons and arrows are the
290
+ one family whose _meaning is_ its direction, and that direction is physical, not
291
+ logical — a chevron in a details marker points down in every writing mode. So they
292
+ are named for where they point: `expand-more`/`expand-less` →
293
+ `chevron-down`/`chevron-up`, and `arrow-forward`/`arrow-back` →
294
+ `arrow-right`/`arrow-left`. `chevron-left`/`chevron-right` keep their names.
295
+ `arrow-up`/`arrow-down` are new, so both families now cover all four directions.
296
+ `lightning` is new.
297
+
298
+ If you passed any of the old names to `<Icon />`, `resolveIcon` or an `icons`
299
+ config, update them. They fail loudly rather than silently: an unresolvable
300
+ declared name throws, and `vitops icons` reports scanned ones.
301
+
302
+ **Fixed three Font Awesome mappings that named no real glyph** and so rendered an
303
+ empty box: `login`/`logout` were mapped to `login`/`logout` (Font Awesome calls
304
+ them `right-to-bracket`/`right-from-bracket`) and `backup` to `backup`
305
+ (`cloud-arrow-up`). Every value in all four UI sets is now checked against the
306
+ installed collection.
307
+
308
+ **Phosphor (`ph`) joins the semantic map**, with all 83 names verified against the real icon set.
309
+ Phosphor keeps every weight in one collection and varies the name (`list`, `list-bold`), unlike Font
310
+ Awesome's per-weight collections, so `resolveIcon` and `generateIconInclude` gained a `weight`
311
+ option for sets shaped that way.
312
+
313
+ Fixes: `site.icons` was a closed object, so any icon collection not in its hand-written key list was
314
+ silently dropped during validation — your config passed and the icons never bundled. It accepts any
315
+ collection name now.
316
+
317
+ - bf453b0: Detect JavaScript and scroll-driven timelines in CSS, so no-JS visitors stop getting invisible
318
+ content.
319
+
320
+ `animation.css` gated two rules on classes that had to be put on `<html>`: `no-js`, which the
321
+ framework expected an author to write into their own markup, and `.no-scroll-timeline`, which an
322
+ inline `<head>` snippet was supposed to set. **That snippet never shipped.** Three source comments
323
+ described it and nothing implemented it, so on every Astro and Bricks site the `:root:not(.no-js)`
324
+ gate matched unconditionally and `.animate-trigger` stayed `animation-play-state: paused` with no
325
+ `IntersectionObserver` coming to release it. An entrance animation that never runs is `opacity: 0`.
326
+
327
+ Both gates are now platform queries:
328
+
329
+ ```css
330
+ @media (scripting: enabled) {
331
+ .animate-trigger {
332
+ animation-play-state: paused;
333
+ }
334
+ /* …released by .is-active */
335
+ }
336
+
337
+ @supports not (animation-timeline: view()) {
338
+ :where(.animate-view, .animate-scroll) {
339
+ animation: none;
340
+ }
341
+ }
342
+ ```
343
+
344
+ Nothing to install, nothing to remember, and correct for consumers the old approach could not reach —
345
+ a Bricks site, or anyone rendering their own `<head>`. It also fails the right way round: an engine
346
+ that doesn't know either feature drops the block and the animation simply runs, costing an
347
+ enhancement rather than hiding the page.
348
+
349
+ **Nothing to do to adopt this.** Drop `class="no-js"` from your `<html>` and any script that removes
350
+ it if you like — both are inert now, not harmful. `<Head />` no longer emits a class-flipping script.
351
+
352
+ Support is baseline: `scripting` shipped in Firefox 113, Safari 17 and Chrome 120, all in 2023.
353
+
354
+ - bf453b0: `.split` gets a stacking convention, a reversal, and a ratio that actually holds.
355
+
356
+ **Fixed: a padded column silently broke the ratio.** `.split > *` was `flex: 1` — a zero basis plus
357
+ a grow factor — and `flex-grow` shares out only the _free_ space, which a child's padding is not
358
+ part of. So a padded column came out wider than its sibling by exactly its horizontal padding, and
359
+ `.split-1-2` quietly stopped being 1:2. Measured in Chrome: an equal `.split` 1000px wide with 40px
360
+ of padding a side on one child resolved 540/460, and `split-1-2` resolved 387/613. The ratio is now
361
+ a **flex basis**, which sizes the border box, so padding sits inside the share: 500/500 and 333/667.
362
+ Nothing to change in your markup — ratios that looked slightly off now land where they read.
363
+
364
+ Two consequences worth knowing. The basis applies only to a **two-child** split, because a ratio is
365
+ a pair contract (only `:first-child` and `:last-child` ever carried one) and with three or more
366
+ children a middle child would collapse to zero; those keep the previous behaviour. And the ratio
367
+ only holds on **border-box** children — a basis sizes the border box, and under content-box the
368
+ padding lands outside the share again, reproducing the exact defect. That is what the new global
369
+ border-box reset is for (see its own note); `.split > *` restates it so the pattern still holds if
370
+ you override the reset in a layer of your own.
371
+
372
+ **Fixed: long unbreakable content stretched a column.** `.split > *` now carries
373
+ `min-inline-size: 0`. A flex item's automatic minimum is its min-content size, so a URL, a code span
374
+ or a `<pre>` used to push its column past its share — and every consumer re-derived `min-w-0` to
375
+ stop it. It belongs to the pattern.
376
+
377
+ **Stacking is now a two-class idiom, using `flex-col` you already have.**
378
+
379
+ ```html
380
+ <div class="split flex-col md-split-1-2 gap-l"></div>
381
+ ```
382
+
383
+ Stacked below 48rem, 1:2 above. What happens when there isn't room for two columns was the question
384
+ `.split` never answered, so every project re-derived
385
+ `flex flex-col gap-… lg-flex-row lg-items-start lg-split`. The `sm-`/`md-`/`lg-`/`xl-` ratio classes
386
+ now assert `flex-direction: row`, which is what un-stacks the pair at the breakpoint. There is
387
+ deliberately **no** `.split-stack`: it would be `flex-col` under a second name.
388
+
389
+ `.flex-col` wins because `.split` is a **pattern** and every utility sits one cascade layer above
390
+ it — see the layering note in this release. `.md-split-1-2` then wins because a breakpoint-scoped
391
+ utility sorts after an unscoped one, which is stable in both formats. (`.split` itself now states
392
+ `flex-direction: row`, so a host that defaults an element to `column` — Bricks' block element —
393
+ can no longer stack it by accident, as long as that host's rule is itself layered.)
394
+
395
+ While stacked the ratio goes inert on its own and needs no reset: a percentage flex basis resolved
396
+ against an auto-height column container behaves as `content`. It does apply if you give the split a
397
+ definite `block-size`, which is a ratio in the block axis — what asking for one on a fixed-height
398
+ column means.
399
+
400
+ **New `.split-reverse`** (breakpoint-prefixable) — swaps the two panels. It is `order` on the first
401
+ child rather than `row-reverse`, so it reverses on **whichever axis the split is currently on**:
402
+ bare, it swaps the columns in a row _and_ the rows in a stack; scoped to a breakpoint
403
+ (`md-split-reverse`) it swaps only once there are two columns — put the media first in source and it
404
+ leads on mobile while sitting on the right at width. The ratio stays attached to the source-first
405
+ child, not to visual position.
406
+
407
+ > **Accessibility.** Reversing puts visual order out of step with DOM order, and focus order follows
408
+ > the DOM (WCAG 2.4.3 Focus Order) — so a keyboard user tabs through a reversed split in the order it
409
+ > is written, not the order it is seen. Put focusable content in **only one** of the two panels. The
410
+ > pattern declares `reading-flow: flex-visual`, which fixes this properly where it is supported;
411
+ > support is not yet broad enough to rely on, and an unknown property is inert, so treat it as an
412
+ > enhancement rather than a guarantee.
413
+
414
+ **New `.flex-row-reverse` / `.flex-col-reverse`** (breakpoint-prefixable) in the `css` and `bricks`
415
+ formats. These names were already in the list the `tailwind` format defers to Tailwind for, but
416
+ they were never defined here — so a mirrored row was inexpressible in framework classes in the two
417
+ formats with no fallback.
418
+
419
+ ### Patch Changes
420
+
421
+ - bf453b0: Make `text-wrap` a stated decision on every type role, emit the `typography.headings` bindings in the tailwind format, and add the four `text-{wrap,nowrap,balance,pretty}` utilities.
422
+
423
+ `text-wrap: balance` for headings and `pretty` for copy belong to the **type role**, not to a
424
+ reset — the role already owns the property (`--<role>-tw`, editable in the live theme editor),
425
+ and `typography.headings` projects it onto bare `h1`/`body` so unclassed markup gets it with no
426
+ class at all. Three things stopped that working end to end.
427
+
428
+ **Omitting `text-wrap` was never "inherit".** Like `style`, `text-transform` and
429
+ `text-decoration`, it is emitted on every role at its identity value so that applying one role
430
+ class over another fully resets it. So a role that left it out emitted `text-wrap: wrap` and
431
+ **cancelled** the `pretty` it would otherwise inherit from a `body`-mapped role — captions and
432
+ footnotes quietly opted out of the thing they most benefit from. The behaviour is unchanged
433
+ (the reset is what makes role classes composable); what changed is that the schema description
434
+ now says so, and the shipped configs state the value on every role rather than leaving it to
435
+ the identity.
436
+
437
+ `vitops init` now scaffolds `balance` on `display`/`heading` and `pretty` on `body`. If you
438
+ already have a `design-system.json`, add `text-wrap` to each role deliberately — `balance` for
439
+ heading-like roles, `pretty` for copy, `wrap` for short single-line labels.
440
+
441
+ **The tailwind format dropped `typography.headings` entirely.** It emitted the
442
+ `@utility font-<role>` half of the typography layer and none of the bare-element bindings, so a
443
+ Tailwind consumer's `<h1>` and `<body>` carried no role styling at all — no family, no size, no
444
+ `text-wrap` — while the css and bricks formats styled them. They are now emitted into Tailwind's
445
+ `base` layer, which keeps `font-<role>` and the patterns able to override them.
446
+
447
+ **This visibly changes existing tailwind sites.** Elements named in your `typography.headings`
448
+ map start picking up their role's typography, which for most projects is the styling they were
449
+ missing — but if you have been compensating with your own `h1`/`body` rules, they now stack.
450
+ Your own unlayered CSS still wins; utilities layered by Tailwind will not. Either drop the
451
+ compensating rules or remove the entry from `typography.headings`.
452
+
453
+ **New utilities** `text-wrap`, `text-nowrap`, `text-balance`, `text-pretty` (css/bricks only —
454
+ Tailwind ships these natively, so they are in `TW_CLASH` and the tailwind bundle drops our
455
+ copies). They are the per-element escape hatch for markup that carries no role class, such as a
456
+ Bricks-authored heading.
457
+
458
+ - Updated dependencies [04a51d8]
459
+ - Updated dependencies [bf453b0]
460
+ - Updated dependencies [bf453b0]
461
+ - @getvitops/utils@2.0.0
462
+
3
463
  ## 1.0.0
4
464
 
5
465
  ### Major Changes
package/README.md CHANGED
@@ -10,8 +10,23 @@ turns into platform output and that `@getvitops/bricks` / `@getvitops/astro` bui
10
10
  - **Browser polyfills** — a feature-detected loader (Anchor Positioning, Popover, Scroll
11
11
  Timeline, …) used by both the CSS patterns and the web components.
12
12
 
13
+ ## Most consumers never install this package
14
+
15
+ `@getvitops/astro` copies the JS bundles into your `public/vitops/` and links them from
16
+ `<Head />`, and `@getvitops/generator` inlines the CSS partials into the stylesheet it emits
17
+ from your `design-system.json`. Both reach core through their own dependency, so a normal
18
+ site lists neither the package nor the imports below.
19
+
13
20
  ## Subpath exports
14
21
 
22
+ Reach for these only when you're bypassing the integration and the generator. Install it
23
+ directly first — core is otherwise a transitive dependency, and under pnpm (or any strict
24
+ `node_modules` layout) these specifiers won't resolve from your app code:
25
+
26
+ ```sh
27
+ npm i -D @getvitops/core
28
+ ```
29
+
15
30
  ```ts
16
31
  import '@getvitops/core/polyfills'; // feature-detected polyfill loader (load high in <head>)
17
32
  import '@getvitops/core/elements'; // register the Lit web components
@@ -22,11 +37,10 @@ import '@getvitops/core/deferred'; // late progressive-enhancement behaviour
22
37
  @import '@getvitops/core/css/index.css'; /* framework partials (needs generated tokens) */
23
38
  ```
24
39
 
25
- Most consumers don't import the CSS directly — they run
26
- [`@getvitops/generator`](https://www.npmjs.com/package/@getvitops/generator) (via the
40
+ The CSS import in particular is inert on its own: the partials resolve against the token layer
41
+ [`@getvitops/generator`](https://www.npmjs.com/package/@getvitops/generator) emits (via the
27
42
  [CLI](https://www.npmjs.com/package/@getvitops/cli) or
28
- [Vite plugin](https://www.npmjs.com/package/@getvitops/vite)) to emit a bundled stylesheet
29
- from their `design-system.json`, and load these JS bundles for interactivity.
43
+ [Vite plugin](https://www.npmjs.com/package/@getvitops/vite)), so you need that output too.
30
44
 
31
45
  ## Changelog
32
46