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