@getvitops/generator 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 +815 -0
- package/README.md +8 -1
- package/assets/bricks/elements/icon.php +102 -0
- package/assets/bricks/elements/split.php +8 -5
- package/assets/bricks/load.php +91 -0
- package/assets/css/animation.css +145 -34
- package/assets/css/global.css +19 -3
- package/assets/css/index.css +8 -0
- package/assets/css/layout-utilities.css +764 -0
- package/assets/css/layout.css +50 -584
- package/assets/css/patterns/consent.css +117 -0
- package/assets/css/patterns/details.css +1 -1
- package/assets/css/patterns/reveal.css +92 -0
- package/assets/css/patterns/subgrid.css +7 -2
- package/assets/css/patterns/theme-editor.css +37 -0
- package/assets/css/utilities.css +19 -98
- package/assets/js/consent.js +1 -0
- package/assets/js/deferred.js +1 -1
- package/assets/js/editor.js +1 -1
- package/dist/index.d.mts +736 -252
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1002 -135
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/schema.json +3 -3
- package/site.schema.json +609 -498
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,820 @@
|
|
|
1
1
|
# @getvitops/generator
|
|
2
2
|
|
|
3
|
+
## 2.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 20e518e: Add `vitops indexing` — tell search engines about a deploy, instead of opening Search Console by hand.
|
|
8
|
+
|
|
9
|
+
Configure it in your site config under `seo.indexing`, then run it after deploying:
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
"seo": {
|
|
13
|
+
"indexing": {
|
|
14
|
+
"indexNow": { "key": "…" },
|
|
15
|
+
"searchConsole": { "siteUrl": "sc-domain:acme.ca" },
|
|
16
|
+
"priorityUrls": ["https://acme.ca/", "https://acme.ca/services"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
vitops indexing --dry # print the plan, make no requests
|
|
23
|
+
vitops indexing # submit
|
|
24
|
+
vitops indexing --check # a day or two later: did Google actually index them?
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
It reads your sitemap, diffs it against the previous run, and submits only what changed —
|
|
28
|
+
pinging **IndexNow** and re-submitting your sitemap through the **Search Console API**.
|
|
29
|
+
`--check` inspects `priorityUrls` and exits non-zero on a page Google hasn't indexed, so a
|
|
30
|
+
scheduled CI job can catch a page that quietly fell out of the index.
|
|
31
|
+
|
|
32
|
+
**Be clear-eyed about what this can do.** Google exposes no API that requests indexing — the
|
|
33
|
+
button in Search Console isn't available anywhere, URL Inspection is read-only, and the sitemap
|
|
34
|
+
ping endpoint was removed in 2023. IndexNow reaches Bing, Yandex, Naver, Seznam and Yep; Google
|
|
35
|
+
doesn't participate. So this automates every sanctioned step and then verifies the outcome; it
|
|
36
|
+
does not make Google re-index on demand, and nothing can. Google's Indexing API is deliberately
|
|
37
|
+
not wired: it's scoped to job postings and livestreams, and using it for ordinary pages violates
|
|
38
|
+
its terms.
|
|
39
|
+
|
|
40
|
+
Also new, and worth wiring at the same time — **`gitLastmod()` for real sitemap dates**:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import vitops, { gitLastmod } from '@getvitops/astro';
|
|
44
|
+
vitops({ sitemap: { serialize: await gitLastmod() } });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`@astrojs/sitemap` emits no `<lastmod>` by default, which means a crawler is told your pages
|
|
48
|
+
exist but never that one changed — and it's what lets `vitops indexing` submit a handful of URLs instead
|
|
49
|
+
of all of them. `gitLastmod()` derives each date from the source file's last commit, and leaves a
|
|
50
|
+
page alone rather than guessing (dynamic routes, ambiguous slugs, shallow clones): an inaccurate
|
|
51
|
+
`lastmod` is worse than none, because Google stops trusting the field site-wide.
|
|
52
|
+
|
|
53
|
+
Requires `fetch-depth: 0` in CI — the default shallow clone has no history to read, and it warns
|
|
54
|
+
when that's the case.
|
|
55
|
+
|
|
56
|
+
**Credentials.** IndexNow's key is public by design (it's served at `/<key>.txt` as the ownership
|
|
57
|
+
proof), so it lives in your config, and the Astro integration writes the file into `public/` for
|
|
58
|
+
you — `vitops indexing --new-key` generates one, `--write-key <dir>` writes it for non-Astro stacks.
|
|
59
|
+
Search Console needs a service account in `VITOPS_GSC_SERVICE_ACCOUNT` or
|
|
60
|
+
`GOOGLE_APPLICATION_CREDENTIALS`, added as an owner of the property; it's never read from the
|
|
61
|
+
config file.
|
|
62
|
+
|
|
63
|
+
Persist `.vitops/` between runs (a CI cache) — that's the changed-URL state, and without it every
|
|
64
|
+
run submits everything.
|
|
65
|
+
|
|
66
|
+
An environment whose `robots` policy says `noindex` is refused outright, so pointing this at
|
|
67
|
+
staging can't publish it to a search engine.
|
|
68
|
+
|
|
69
|
+
### Patch Changes
|
|
70
|
+
|
|
71
|
+
- Updated dependencies [20e518e]
|
|
72
|
+
- Updated dependencies [9bf975a]
|
|
73
|
+
- @getvitops/utils@2.1.0
|
|
74
|
+
|
|
75
|
+
## 2.0.0
|
|
76
|
+
|
|
77
|
+
### Major Changes
|
|
78
|
+
|
|
79
|
+
- bf453b0: The framework now ships a border-box reset.
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
*,
|
|
83
|
+
*::before,
|
|
84
|
+
*::after {
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**This is a behavioural change for the `css` and `bricks` formats.** If your project does not
|
|
90
|
+
already set border-box — Tailwind's preflight does, as does normalize and almost every modern base
|
|
91
|
+
stylesheet — elements with padding or a border now measure their declared width inclusive of it, so
|
|
92
|
+
some boxes will come out narrower than before. The `tailwind` format is unaffected: preflight
|
|
93
|
+
already did this.
|
|
94
|
+
|
|
95
|
+
The framework used to refuse this on the grounds that a global reset silently reflows consumer
|
|
96
|
+
layouts, and that reasoning was sound when nothing was layered. Two things changed:
|
|
97
|
+
- **It is layered.** The rule lives in `vitops.base`, the lowest of the three framework layers, so
|
|
98
|
+
it loses to any unlayered consumer CSS. The opt-out is one rule in your own stylesheet:
|
|
99
|
+
`*, *::before, *::after { box-sizing: content-box }`.
|
|
100
|
+
- **Patterns are only correct under it.** `.split`'s ratio is a flex basis, which sizes the border
|
|
101
|
+
box; under content-box a padded column came out wider than its sibling by exactly its padding.
|
|
102
|
+
Stating the assumption once beats every pattern re-asserting it and the ones that forget being
|
|
103
|
+
quietly wrong.
|
|
104
|
+
|
|
105
|
+
- bf453b0: **Breaking (site config):** `designSystem` is now an object, and the toggle's "System" position works.
|
|
106
|
+
|
|
107
|
+
`<color-scheme-toggle>` has always shipped three segments, and "System" did nothing. It removes the
|
|
108
|
+
theme attribute, and with no `prefers-color-scheme` block in the generated CSS the page fell straight
|
|
109
|
+
through to light on every machine — so a third of the control was inert, and a no-JS page could never
|
|
110
|
+
follow the OS at all.
|
|
111
|
+
|
|
112
|
+
The fix is a second, opt-in copy of the dark delta under
|
|
113
|
+
`@media (prefers-color-scheme: dark)`, scoped to "no explicit choice has been made". An explicit
|
|
114
|
+
light choice still wins over a dark OS. It costs **+303 B gzipped** on the colour layer — the block
|
|
115
|
+
repeats only the tokens whose dark value differs, and two identical runs of text compress to almost
|
|
116
|
+
nothing. It is opt-in because switching it on visibly flips an existing site dark for dark-OS
|
|
117
|
+
visitors.
|
|
118
|
+
|
|
119
|
+
Turning it on is a site-level fact, which needed somewhere to live:
|
|
120
|
+
|
|
121
|
+
```jsonc
|
|
122
|
+
"designSystem": {
|
|
123
|
+
"themes": { "default": { … }, "elegant": { "extends": "default", … } },
|
|
124
|
+
"defaultTheme": "default",
|
|
125
|
+
"defaultColorScheme": "system" // 'light' | 'dark' | 'system'
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**What changed.** `designSystem` was a bare map of themeName → design system, so _every_ key was a
|
|
130
|
+
theme name and there was nowhere to put a system-wide field without it colliding with a theme of that
|
|
131
|
+
name. The map moves under `themes`, and `defaultTheme` + `defaultColorScheme` move inside the block.
|
|
132
|
+
`respectSystemPreference` is gone — `defaultColorScheme: "system"` says the same thing, and the two
|
|
133
|
+
were always read together, so the incoherent combination is no longer expressible.
|
|
134
|
+
|
|
135
|
+
**Migration is automatic at runtime.** `resolveSiteConfig` accepts all three spellings — the
|
|
136
|
+
canonical shape, a bare theme map (the old schema), and a bare design system written inline. But
|
|
137
|
+
`site.schema.json` is published to a stable URL, so an editor pinned to `$schema` will flag the old
|
|
138
|
+
shape; update it when convenient.
|
|
139
|
+
|
|
140
|
+
One behavioural change worth knowing: shorthand normalisation now runs **before** the A/B variant
|
|
141
|
+
merge, not after. Previously an `abTesting` override's key path depended on which shorthand the base
|
|
142
|
+
config happened to use, so the same patch landed in different places in two otherwise-equivalent
|
|
143
|
+
configs. Overrides now always address `designSystem.themes.<name>`.
|
|
144
|
+
|
|
145
|
+
Also: `getvitops({ site: { input } })` gives the Astro integration one place to name the site config
|
|
146
|
+
— `legal`, `fonts` and the colour scheme all read from it — and `css.systemColorScheme` sets the
|
|
147
|
+
appearance directly for consumers who have no site config.
|
|
148
|
+
|
|
149
|
+
Light/dark remains **derived**, not a theme: `functionalRole()` builds both appearances from one ramp,
|
|
150
|
+
which is what lets the contrast contract check both at build time and what gives every consumer a
|
|
151
|
+
working dark mode without authoring one. `themes` is for authored variants.
|
|
152
|
+
|
|
153
|
+
- bf453b0: Utilities now beat patterns, in every format. Cascade layers are assigned by what a rule **is**,
|
|
154
|
+
not by which file it happens to live in.
|
|
155
|
+
|
|
156
|
+
`vitops.base → vitops.components → vitops.utilities` has existed for a while, and its whole purpose
|
|
157
|
+
is to let `class="card bg-danger-muted"` work without `!important`. But the layer was chosen per
|
|
158
|
+
**partial**, and `layout.css` was one file holding both the structural patterns (`.rhythm`,
|
|
159
|
+
`.centered`, `.split`) and roughly three quarters of the framework's utilities (`.m-*`, `.flex-*`,
|
|
160
|
+
`.items-*`, `.justify-*`, `.text-*`, `.split-<a>-<b>`, track placement). Unmapped, it defaulted to
|
|
161
|
+
`vitops.components` — so all of those utilities were shelved _below_ the patterns they are meant to
|
|
162
|
+
override, and quietly did nothing. `utilities.css` had the mirror-image problem: it held the
|
|
163
|
+
`.reveal` component family, which therefore outranked every pattern _and_ the display utilities.
|
|
164
|
+
|
|
165
|
+
Both are now split — `layout.css` / `layout-utilities.css`, and `patterns/reveal.css` — and the
|
|
166
|
+
classification is asserted across formats by a shared `LAYER_CONTRACT`, so the two halves cannot
|
|
167
|
+
drift apart again.
|
|
168
|
+
|
|
169
|
+
**What visibly changes.** Each of these was silent before and now takes effect. That is the point,
|
|
170
|
+
but if you wrote one of these combinations and never noticed it did nothing, you will see a change:
|
|
171
|
+
|
|
172
|
+
| markup | before | after |
|
|
173
|
+
| ---------------------------------------- | ------------------------------------------ | ---------------------- |
|
|
174
|
+
| `class="table text-center"` | `.table { text-align: start }` won | `.text-center` wins |
|
|
175
|
+
| `class="banner items-start"` | `.banner`'s centring won | the utility wins |
|
|
176
|
+
| `class="cluster-between justify-center"` | `.cluster-between` won | `.justify-center` wins |
|
|
177
|
+
| `class="icon justify-start"` | `.icon { justify-content: center }` won | `.justify-start` wins |
|
|
178
|
+
| `class="combobox flex-row"` | `.combobox { flex-direction: column }` won | `.flex-row` wins |
|
|
179
|
+
| `class="media items-center"` | `.media { align-items: flex-start }` won | `.items-center` wins |
|
|
180
|
+
| `<details>` first content child + `m-0` | `details.css`'s `> summary + *` margin won | `.m-0` wins |
|
|
181
|
+
| `class="split flex-col"` | worked, but on source order | works by layer |
|
|
182
|
+
|
|
183
|
+
The reveal family moves the other way, having been mis-shelved as a utility: `class="reveal hidden"`
|
|
184
|
+
and `class="reveal-fade block"` used to keep the reveal's own `display` and now lose to the display
|
|
185
|
+
utility. The duplicate `details::details-content` rules in `utilities.css` are deleted —
|
|
186
|
+
`patterns/details.css` was always the owner, and its selector is a superset — so the collapsed
|
|
187
|
+
content box is now `overflow: hidden` rather than `clip`.
|
|
188
|
+
|
|
189
|
+
**Removed: the bare `<bp>-split` classes** (`sm-split`, `md-split`, `lg-split`, `xl-split`).
|
|
190
|
+
`.split` is a pattern now, and `@utility` cannot live in a cascade layer — measured against
|
|
191
|
+
tailwindcss@4.3.3, it throws inside `@layer` _and_ inside a file imported with `layer(…)`, and a
|
|
192
|
+
`@custom-variant` can't reach a components-layer class either — so `@md:split` became impossible and
|
|
193
|
+
the css/bricks counterpart had to go with it. Use `md-flex-row` for "become a row at md"; it says
|
|
194
|
+
the same thing in every format. The one thing lost is resetting a bare ratio back to equal at a
|
|
195
|
+
breakpoint, which had no usages; apply the ratio at the breakpoint instead.
|
|
196
|
+
|
|
197
|
+
**Also fixed, in the tailwind format only** — three divergences found while auditing the layers:
|
|
198
|
+
- `body` was emitted with `container-type` but not `container-name: body`, so the
|
|
199
|
+
`@container body (…)` queries in the scroll/TOC patterns never matched: `.toc-layout` and
|
|
200
|
+
`.toc-sidebar` were stuck in their narrow layout.
|
|
201
|
+
- `grid-auto` was missing the `:is(ul, ol) > li + li` margin reset, so a `<ul class="grid-auto">`
|
|
202
|
+
inside `.rhythm` got a stray top margin on every item but the first.
|
|
203
|
+
- **`.sticky` was being deleted outright.** `sticky` is a Tailwind utility name, and the strip that
|
|
204
|
+
defers those to Tailwind matched a rule's leading class — taking `patterns/sticky.css` with it,
|
|
205
|
+
including `--sticky-offset`, the z-index wiring and every `.sticky--bottom` / `--inline-start` /
|
|
206
|
+
`--inline-end` variant. Components whose names collide with Tailwind utilities are now allowlisted.
|
|
207
|
+
|
|
208
|
+
**Migration.** If a layout utility on a patterned element now "suddenly works" and you preferred the
|
|
209
|
+
old result, remove the utility — it was never doing anything. To keep a pattern winning over the
|
|
210
|
+
framework's utilities, write the declaration in your own stylesheet: unlayered CSS still beats all
|
|
211
|
+
three framework layers.
|
|
212
|
+
|
|
213
|
+
### Minor Changes
|
|
214
|
+
|
|
215
|
+
- 4756788: Add `<Analytics />` and a general-purpose consent gate.
|
|
216
|
+
|
|
217
|
+
`getvitops({ analytics })` configures Google Analytics 4, Microsoft Clarity, Matomo and Plausible;
|
|
218
|
+
`<Analytics />` emits their tags. Nothing touches the critical path — `strategy` defaults to `'idle'`,
|
|
219
|
+
which loads every tag after `load` on an idle callback (`'async'` and `'interaction'` are the other
|
|
220
|
+
options), and no `preconnect` is emitted, since warming a third-party connection during parse is the
|
|
221
|
+
cost `idle` exists to avoid.
|
|
222
|
+
|
|
223
|
+
`getvitops({ consent })` adds the gate: `@getvitops/core/consent`, a 2.3 KB gzipped Lit-free bundle,
|
|
224
|
+
plus `<CookieConsent />`.
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
vitops({
|
|
228
|
+
analytics: { googleAnalytics: 'G-XXXXXXXXXX', plausible: 'acme.com' },
|
|
229
|
+
consent: { policyUrl: '/legal/cookies' },
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Consent is not an analytics feature.** The gate is general — mark anything
|
|
234
|
+
`data-consent="<category>"` and it waits on the same choice, so A/B assignment, personalisation and
|
|
235
|
+
third-party embeds use it too, and a site can enable the banner with no analytics at all. Categories
|
|
236
|
+
are `necessary` / `analytics` / `marketing` / `preferences`, and `window.vitopsConsent` plus a
|
|
237
|
+
`vitops:consent` event on `document` are how your own code reads the answer.
|
|
238
|
+
|
|
239
|
+
**Which category a provider needs is derived, not declared.** It follows from whether that provider
|
|
240
|
+
sets cookies, which follows from its own config: Matomo runs cookieless by default (`disableCookies`)
|
|
241
|
+
and so needs no banner; `cookies: true` opts in and moves it behind `analytics`. You can't mark
|
|
242
|
+
Google Analytics `necessary` to skip the banner — but you can pick a genuinely cookieless provider.
|
|
243
|
+
|
|
244
|
+
Gated tags render as `<script type="text/plain">` with the URL on `data-src`, so an undecided or
|
|
245
|
+
declining visitor's page issues **no third-party request at all**. For Google Analytics that is basic
|
|
246
|
+
consent mode rather than Consent Mode v2 advanced: nothing reaches Google until the visitor accepts.
|
|
247
|
+
Clarity additionally receives `clarity('consentv2', …)`, because Microsoft enforces the signal
|
|
248
|
+
separately for EEA/UK/CH traffic. Nothing is stored until a choice is made — the banner can't be the
|
|
249
|
+
thing that needs consent — and revoking clears the provider's cookies and reloads, because an
|
|
250
|
+
already-executing tracker can't be unloaded any other way.
|
|
251
|
+
|
|
252
|
+
The banner is shown in the top layer via `popover="manual"`, like `.tooltip`: a plain fixed banner
|
|
253
|
+
resolves against the nearest containing block, and `body { container-type: inline-size }` — ordinary
|
|
254
|
+
in a framework whose breakpoints are container queries — would otherwise trap it mid-page.
|
|
255
|
+
|
|
256
|
+
**The site config gained `analytics.clarityId` and `analytics.matomo`**, so `vitops legal` discloses
|
|
257
|
+
them. Clarity and Matomo join the processor table with their real cookie names, cookieless Matomo
|
|
258
|
+
asserts positively that it sets none, and a Clarity site's privacy policy now describes session
|
|
259
|
+
replay rather than filing it under page-view analytics. Configure `legal` alongside `analytics` and
|
|
260
|
+
the Astro integration cross-checks the two, naming any provider you'd otherwise run without
|
|
261
|
+
disclosing; it also warns when a cookie-setting provider has no `consent` gate.
|
|
262
|
+
|
|
263
|
+
Existing configs are unaffected — both options are off unless provided. One behaviour change if you
|
|
264
|
+
use the ad-conversion tracking script: `packages/astro/src/scripts/tracking.ts` now waits for
|
|
265
|
+
`marketing` consent before writing its 90-day `_ac` click-ID cookie, when the gate is present. With
|
|
266
|
+
no gate on the page it behaves exactly as before.
|
|
267
|
+
|
|
268
|
+
- bf453b0: Animation families that never actually animated, and a driver that fired before you could see it.
|
|
269
|
+
|
|
270
|
+
Each of these looked correct in source and failed somewhere else — in the bundler, in hit-testing,
|
|
271
|
+
or in the difference between a time-based and a progress-based timeline. Nothing here needs a
|
|
272
|
+
markup change.
|
|
273
|
+
|
|
274
|
+
**Changed: entrances are now timed off the element's midpoint.** Every driver used to key off a
|
|
275
|
+
fraction of the element's _own height_, which meant the same class behaved differently on a 4rem
|
|
276
|
+
card and a full-bleed section — and on small elements it was over before they appeared
|
|
277
|
+
(`entry 20%` is about 17px of scroll for a 5.5rem tile). Motion now starts once the element's
|
|
278
|
+
**midpoint is 10% of the viewport in**, and a one-shot entrance completes at **25%**. This applies
|
|
279
|
+
to `animate-view` and to the `.is-active` observer that drives `animate-trigger` and the
|
|
280
|
+
`active-<fx>` transition variants, so everything on a page starts at the same moment on screen.
|
|
281
|
+
|
|
282
|
+
The pivot is `entry 50%` — the one point that means "midpoint on the viewport edge" for an element
|
|
283
|
+
of any height — plus a viewport length. Shift the window with `--anim-start` / `--anim-end`, or
|
|
284
|
+
replace it with `--anim-range`. `--stagger-range-step` is now a viewport length (`5vh`) to match.
|
|
285
|
+
|
|
286
|
+
**Fixed: every journey ran on a truncated range.** The generator emitted `animation-range: entry
|
|
287
|
+
exit`; lightningcss parsed the end of that shorthand as `exit 0%` rather than the spec's `exit
|
|
288
|
+
100%`, so the bundle shipped `entry exit 0%`. Journeys reach their `100%` keyframe — the hidden
|
|
289
|
+
`from` state — as the element hits the top of the viewport, which is to say they faded out while
|
|
290
|
+
still fully on screen. Journeys now run `entry calc(50% + 10vh) → exit 100%`: they start on the same
|
|
291
|
+
midpoint pivot as everything else and keep the full entry → hold → exit arc, so the hold sits in the
|
|
292
|
+
middle of the crossing. `animation-effects.test.ts` asserts on the **bundled** css, because the
|
|
293
|
+
emitter was right the whole time.
|
|
294
|
+
|
|
295
|
+
**Fixed: `slide-journey` had no distance to travel.** `animations.journeys.base.slide` was `{}` in
|
|
296
|
+
both `defaultConfig()` and the shipped example, so the keyframe animated `translate: 0 → 0`. It now
|
|
297
|
+
declares `translate-y-from: var(--slide-distance, 2rem)` — the same var the `slide-up` effect uses,
|
|
298
|
+
so one knob tunes both. If your own config has an empty `slide` base, add the same line.
|
|
299
|
+
|
|
300
|
+
**Fixed: `reveal-*` on hover was unreachable.** A `hover-reveal-left` element rests at `clip-path:
|
|
301
|
+
inset(0 100% 0 0)`, and `clip-path` clips **hit-testing** as well as painting — so it had zero
|
|
302
|
+
hittable area and could never receive the hover that would reveal it. The state variants now match
|
|
303
|
+
the element **or its direct parent**, mirroring what `animation.css` already did for the trigger
|
|
304
|
+
driver (`:is(.is-active, [data-active]) > .animate-trigger`):
|
|
305
|
+
|
|
306
|
+
```css
|
|
307
|
+
.hover-<fx>:hover, :hover > .hover-<fx> { … }
|
|
308
|
+
.focus-<fx>:focus-visible, :focus-within > .focus-<fx> { … }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Specificity is unchanged (0-2-0), so nothing reorders. **Behaviour change:** a `hover-<fx>` element
|
|
312
|
+
that is a direct child of a hovered element now flips with its parent. Wrap it in an intermediate
|
|
313
|
+
element if you need the old element-only behaviour.
|
|
314
|
+
|
|
315
|
+
**Fixed: `.stagger` did nothing on a scroll-driven timeline.** It offsets children with
|
|
316
|
+
`animation-delay`, which is time-based and is ignored outright on a `view()` / `scroll()` timeline,
|
|
317
|
+
so `.stagger > .animate-view` arrived all at once. It now also offsets each child's view
|
|
318
|
+
`animation-range` by the same index, so one class works under both driver families — tune the
|
|
319
|
+
scroll-driven step with `--stagger-range-step` (default `5vh`). Journeys declare their own range and
|
|
320
|
+
opt out by construction.
|
|
321
|
+
|
|
322
|
+
Two smaller faults found alongside it: `@supports (--x: sibling-index())` is **always true** — any
|
|
323
|
+
token stream is a valid custom-property value — so the guard around `sibling-index()` guarded
|
|
324
|
+
nothing in both `.stagger` and `.subgrid`'s row index, and on an engine without support the
|
|
325
|
+
declaration went invalid rather than being skipped. Both now test a real property. And the CSS path
|
|
326
|
+
was 1-based while the JS fallback wrote 0-based, so the two disagreed by one step wherever both ran.
|
|
327
|
+
|
|
328
|
+
**Fixed: the pre-paint `<html>` class script was missing entirely.** `<Head />` now emits it again,
|
|
329
|
+
outside the `webComponents` block since it gates stylesheet behaviour rather than the element
|
|
330
|
+
runtime. It does two things `animation.css` depends on and that three other files documented as
|
|
331
|
+
already shipping:
|
|
332
|
+
- `no-js` → `js`, so `.animate-trigger` is paused only where JS can un-pause it;
|
|
333
|
+
- `.no-scroll-timeline` when `animation-timeline: view()` is unsupported, which is what cancels
|
|
334
|
+
`.animate-view` / `.animate-scroll`. Scroll-driven animations are deliberately **not** polyfilled,
|
|
335
|
+
so without this flag those elements sit at their `from` keyframe — `opacity: 0`, i.e. invisible
|
|
336
|
+
content — on any engine without support. It had been dropped in the move to publishable packages,
|
|
337
|
+
leaving the cancel rule as dead code.
|
|
338
|
+
|
|
339
|
+
**New: `hover-size-grow` and friends exist.** The layout effects (`size-grow`, `size-shrink`) were
|
|
340
|
+
the one family with no state variants, on the grounds that `.transition` doesn't cover `height`.
|
|
341
|
+
That wasn't a platform limit — `height: 0 → auto` just needs `interpolate-size: allow-keywords`,
|
|
342
|
+
which the framework already sets on `:root`, and which the `layout` **keyframe** depended on
|
|
343
|
+
equally. So the exclusion bought no portability; it only made `hover-size-grow` a class the docs
|
|
344
|
+
advertised and the stylesheet didn't define. `.transition` now declares `height` inside an
|
|
345
|
+
`@supports (interpolate-size: allow-keywords)` block, and the generator emits the full
|
|
346
|
+
`hover-`/`focus-`/`active-` set for layout effects, carrying each effect's own `overflow: clip` so a
|
|
347
|
+
collapsed box hides its content instead of spilling it. Where `interpolate-size` is missing, the
|
|
348
|
+
height simply doesn't transition — the same degradation the keyframe path has. (For the record:
|
|
349
|
+
`transition-behavior: allow-discrete` is _not_ the tool here — it only lets genuinely discrete
|
|
350
|
+
properties transition, and would snap at the midpoint.)
|
|
351
|
+
|
|
352
|
+
`.transition` switched from the `transition` shorthand to `transition-property` +
|
|
353
|
+
`transition-duration` + `transition-timing-function`, so the gated block can append `height` without
|
|
354
|
+
restating the list. If you override `.transition`'s timing, override the longhands.
|
|
355
|
+
|
|
356
|
+
The generated class reference (`vitops docs classes`) was the upstream source of that mismatch — it
|
|
357
|
+
listed every effect under "with the state prefixes above", including the layout ones. It now states
|
|
358
|
+
which stage needs the feature gate, when each driver plays, and how `stagger` composes with both.
|
|
359
|
+
|
|
360
|
+
- bf453b0: Anywhere the toolchain takes a config, that file may now be a `design-system.json` **or** the larger
|
|
361
|
+
site config that embeds one.
|
|
362
|
+
|
|
363
|
+
If you keep your tokens inside a `company.json` / `site.json`, you had to maintain a second file for
|
|
364
|
+
the tooling's sake — or duplicate your whole token set, because a site config must carry a complete
|
|
365
|
+
`designSystem`. Now you point the same option at the file you already have:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
// astro.config.mjs
|
|
369
|
+
getvitops({ css: { input: 'company.json', format: 'css' } });
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
vitops generate --input company.json --format css --out dist
|
|
374
|
+
vitops lint --input company.json --src src
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The design system is taken from `designSystem.themes[theme]` with its `extends` chain resolved —
|
|
378
|
+
`defaultTheme`, else `default`, else whatever `--theme` / the `theme` option names. Nothing changes
|
|
379
|
+
for a plain `design-system.json`; the two are told apart by shape, not by filename, so you can call
|
|
380
|
+
the file anything.
|
|
381
|
+
|
|
382
|
+
**A site config also supplies the site-level facts generation reads**, so the path is declared once
|
|
383
|
+
rather than per option:
|
|
384
|
+
- `designSystem.defaultColorScheme: "system"` — emits the `prefers-color-scheme` block, which is
|
|
385
|
+
what makes `<color-scheme-toggle>`'s System position do anything.
|
|
386
|
+
- `legal.*.enabled` — renders the documents. `legal: {}` is now the whole declaration; `legal.input`
|
|
387
|
+
is optional.
|
|
388
|
+
- `icons.sprite` — builds `icons.svg`.
|
|
389
|
+
- `fonts` — `fonts: true` reads the families from it.
|
|
390
|
+
|
|
391
|
+
Each of those is already gated on a field in that config, so nothing new appears unless the config
|
|
392
|
+
asks for it. An explicit `site` option still wins when the two are genuinely different files.
|
|
393
|
+
|
|
394
|
+
Also:
|
|
395
|
+
- **`vitops validate` routes on the file's shape.** Pointed at a site config it used to report a
|
|
396
|
+
single `unrecognized_keys` for `designSystem` and nothing about the file's actual contents — a
|
|
397
|
+
wrong answer that reads like a right one. It now validates a site config as one, including the
|
|
398
|
+
cross-field integrity JSON Schema can't express, and checks every theme resolves to a complete
|
|
399
|
+
design system (`validateSite` only checked that the `extends` chain resolved, not what it resolved
|
|
400
|
+
to).
|
|
401
|
+
- **`generate()` gained `theme` and `siteEnv`.** The A/B variant for `siteEnv` is applied before the
|
|
402
|
+
theme is selected, so a variant can override tokens.
|
|
403
|
+
- **The theme editor's Save to source follows the design system into a site config.** Its patch is
|
|
404
|
+
design-system-relative, so the dev server merges into that subtree and writes only the surrounding
|
|
405
|
+
file whole. It locates the subtree in the raw on-disk object rather than the normalised one, so an
|
|
406
|
+
author who wrote either `designSystem` shorthand still gets their own keys edited.
|
|
407
|
+
|
|
408
|
+
- bf453b0: `getvitops({ fonts })` — load webfonts through Astro's Fonts API instead of hand-rolling it.
|
|
409
|
+
|
|
410
|
+
The design system names fonts and loads none of them, and there was no seam for the loading half —
|
|
411
|
+
so the only available path was installing a `@fontsource*` package and importing its CSS, which
|
|
412
|
+
renders correctly while silently giving up subsetting, preload, and the `size-adjust` /
|
|
413
|
+
`ascent-override` fallback metrics. That is a CLS regression that looks like a working setup. This
|
|
414
|
+
is the counterpart to `vitopsHosting()`: declare the family, let the integration wire it.
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
getvitops({
|
|
418
|
+
fonts: [
|
|
419
|
+
{
|
|
420
|
+
name: 'League Spartan',
|
|
421
|
+
provider: 'fontsource',
|
|
422
|
+
cssVariable: '--font-league-spartan',
|
|
423
|
+
weights: ['100 900'],
|
|
424
|
+
subsets: ['latin'],
|
|
425
|
+
preload: true,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
```jsonc
|
|
432
|
+
// design-system.json — the token points at the variable, not at a literal stack
|
|
433
|
+
"fonts": { "display": "var(--font-league-spartan), sans-serif" }
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`fonts` also takes a **string**: the path to a site config, whose `fonts` array has carried exactly
|
|
437
|
+
these declarations (provider / weights / subsets / preload) since it was written — nothing had ever
|
|
438
|
+
read it. Or `{ input, families, siteEnv }` for both.
|
|
439
|
+
|
|
440
|
+
`<Head />` now emits `<Font />` for each declared family, with `preload` driven by the declaration.
|
|
441
|
+
That half is not optional: Astro's `fonts:` config resolves the files, but `<Font />` is what puts
|
|
442
|
+
the `@font-face` on the page, so a declaration without `<Head />` in your layout loads nothing.
|
|
443
|
+
|
|
444
|
+
Notes:
|
|
445
|
+
- Independent of `css` — the wiring runs in `astro:config:setup`, not the Vite plugin.
|
|
446
|
+
- Only families declared here get a `<Font />`. One from your own `astro:config` `fonts:` array or
|
|
447
|
+
from another integration is left alone, because `<Font />` throws on a `cssVariable` Astro cannot
|
|
448
|
+
resolve. Astro concatenates the arrays, so the three coexist; two entries claiming one variable
|
|
449
|
+
is the collision that matters, and it now throws (within our set) or warns (against yours)
|
|
450
|
+
instead of silently dropping a family.
|
|
451
|
+
- `provider: 'adobe'` throws with instructions: `fontProviders.adobe({ id })` needs a key from the
|
|
452
|
+
environment, and a JSON declaration has nowhere to hold one.
|
|
453
|
+
- New exports from `@getvitops/generator`: `SiteFontSchema` and the `SiteFont` type.
|
|
454
|
+
|
|
455
|
+
- bf453b0: New `gap-*` utilities over the fluid space scale, in all three formats.
|
|
456
|
+
|
|
457
|
+
`gap-<name>`, `gap-x-<name>` (column) and `gap-y-<name>` (row), for every step of your
|
|
458
|
+
`spaceScale` — `gap-2xs` … `gap-7xl` — each breakpoint-prefixable (`md-gap-l` in css/bricks,
|
|
459
|
+
`@md:gap-l` in tailwind).
|
|
460
|
+
|
|
461
|
+
There was no gap utility at all before. `vitops docs css` advertised a `g` class that was never
|
|
462
|
+
emitted, so the honest answer for a css or bricks consumer was an inline `style="gap: …"`. Tailwind
|
|
463
|
+
did not fill the hole either: the fluid steps are deliberately kept **out** of Tailwind's
|
|
464
|
+
`--spacing-*` namespace, because named keys there shadow the size scales (`max-w-7xl` would resolve
|
|
465
|
+
to `var(--spacing-7xl)` and collapse layouts), so `gap-l` was not something Tailwind could derive.
|
|
466
|
+
Measured against tailwindcss@4.3.3: the emitted `@utility gap-l` is honoured, accepts variants, and
|
|
467
|
+
coexists with the built-in numeric `gap-4`, which keeps Tailwind's own multiplier.
|
|
468
|
+
|
|
469
|
+
The whole matrix is emitted rather than a plausible subset. An undefined step produces no rule and
|
|
470
|
+
no error in either format, so a missing `md-gap-x-2xl` would be indistinguishable from a working
|
|
471
|
+
one.
|
|
472
|
+
|
|
473
|
+
In the css and bricks bundles these land in `vitops.utilities`, so `class="cluster gap-l"` beats
|
|
474
|
+
the pattern's own gap.
|
|
475
|
+
|
|
476
|
+
- 04a51d8: Icons: one semantic vocabulary across icon sets, with the bundle derived from your source.
|
|
477
|
+
|
|
478
|
+
Name icons by meaning — `<Icon name="menu" />` resolves to `fa7-solid:bars`, `ph:list` or
|
|
479
|
+
`lucide:menu` depending on the set you configure, so swapping sets is a config edit rather than a
|
|
480
|
+
find-and-replace. A name containing `:` still passes through untouched, which is the escape hatch
|
|
481
|
+
for a set-specific glyph.
|
|
482
|
+
|
|
483
|
+
**New `icons` option on `getvitops()`.** Configures the sets once per site and, under
|
|
484
|
+
`output: 'server'`, derives astro-icon's `include` map by scanning your source — the list most
|
|
485
|
+
projects end up maintaining by hand. On a static build no `include` is passed at all, because
|
|
486
|
+
astro-icon is already zero-config there and a list could only drop a glyph the scan couldn't see.
|
|
487
|
+
Names you declare that don't resolve fail the build; names only the scan found just warn; names
|
|
488
|
+
computed at runtime are reported with file and line so you can declare them.
|
|
489
|
+
|
|
490
|
+
**New `<Icon />` component**, and a real fix with it: `Popover`, `Details` and `Drawer` imported
|
|
491
|
+
`astro-icon/components` at module scope, so the _optional_ peer was resolved whether or not an icon
|
|
492
|
+
ever rendered — hard-failing anyone who hadn't installed it. Engines now load dynamically. The
|
|
493
|
+
per-component `iconResolver` prop still works but is deprecated in favour of the integration option.
|
|
494
|
+
|
|
495
|
+
**New SVG sprite** (`icons.sprite: true` in your site config) for consumers that can't run an icon
|
|
496
|
+
integration — Bricks/WordPress, EmDash renderers, plain HTML. `<use href="…/icons.svg#ph--list">`,
|
|
497
|
+
no JavaScript and no icon-API request. Every semantic name also gets a set-independent `icon-<name>`
|
|
498
|
+
alias, so sprite markup survives an icon-set change. WordPress gets `vitops_icon()`, a
|
|
499
|
+
`[vitops_icon]` shortcode and a **Vitops → Icon** Bricks element.
|
|
500
|
+
|
|
501
|
+
**New `vitops icons`** command: reports which icons your source uses, which names couldn't be
|
|
502
|
+
resolved, and which are computed at runtime; `--sprite` builds the sprite, `--json` for CI.
|
|
503
|
+
|
|
504
|
+
**Renamed, and completed to all four directions.** Chevrons and arrows are the
|
|
505
|
+
one family whose _meaning is_ its direction, and that direction is physical, not
|
|
506
|
+
logical — a chevron in a details marker points down in every writing mode. So they
|
|
507
|
+
are named for where they point: `expand-more`/`expand-less` →
|
|
508
|
+
`chevron-down`/`chevron-up`, and `arrow-forward`/`arrow-back` →
|
|
509
|
+
`arrow-right`/`arrow-left`. `chevron-left`/`chevron-right` keep their names.
|
|
510
|
+
`arrow-up`/`arrow-down` are new, so both families now cover all four directions.
|
|
511
|
+
`lightning` is new.
|
|
512
|
+
|
|
513
|
+
If you passed any of the old names to `<Icon />`, `resolveIcon` or an `icons`
|
|
514
|
+
config, update them. They fail loudly rather than silently: an unresolvable
|
|
515
|
+
declared name throws, and `vitops icons` reports scanned ones.
|
|
516
|
+
|
|
517
|
+
**Fixed three Font Awesome mappings that named no real glyph** and so rendered an
|
|
518
|
+
empty box: `login`/`logout` were mapped to `login`/`logout` (Font Awesome calls
|
|
519
|
+
them `right-to-bracket`/`right-from-bracket`) and `backup` to `backup`
|
|
520
|
+
(`cloud-arrow-up`). Every value in all four UI sets is now checked against the
|
|
521
|
+
installed collection.
|
|
522
|
+
|
|
523
|
+
**Phosphor (`ph`) joins the semantic map**, with all 83 names verified against the real icon set.
|
|
524
|
+
Phosphor keeps every weight in one collection and varies the name (`list`, `list-bold`), unlike Font
|
|
525
|
+
Awesome's per-weight collections, so `resolveIcon` and `generateIconInclude` gained a `weight`
|
|
526
|
+
option for sets shaped that way.
|
|
527
|
+
|
|
528
|
+
Fixes: `site.icons` was a closed object, so any icon collection not in its hand-written key list was
|
|
529
|
+
silently dropped during validation — your config passed and the icons never bundled. It accepts any
|
|
530
|
+
collection name now.
|
|
531
|
+
|
|
532
|
+
- bf453b0: A chroma-0 seed now produces an actual neutral instead of a pink one.
|
|
533
|
+
|
|
534
|
+
Beyond a ramp's outermost authored colour, chroma decays towards a small endpoint value (0.008
|
|
535
|
+
light / 0.015 dark) so the near-white and near-black ends keep a whisper of the hue. That was
|
|
536
|
+
written as a lerp _target_, and the interpolation factor reaches exactly 1 at steps 50 and 950 for
|
|
537
|
+
every anchor position — so the endpoints were those constants **unconditionally**. Seeds at chroma
|
|
538
|
+
0.001, 0.002, 0.05 and 0.2 all produced a byte-identical `#f5f9fe` at step 50.
|
|
539
|
+
|
|
540
|
+
There was therefore no seed that yielded a plain neutral, and chroma 0 was the worst case: a true
|
|
541
|
+
achromatic colour has no hue (colorjs returns NaN), which collapses to 0 — red — so asking for grey
|
|
542
|
+
gave you `#fdf6f8`, a pink. A brand wanting plain white with grey panels had to discover `anchors`
|
|
543
|
+
to get there.
|
|
544
|
+
|
|
545
|
+
The endpoint is now a **ceiling** rather than a target, so a seed below it keeps its own chroma:
|
|
546
|
+
|
|
547
|
+
| seed chroma | step 50 before | step 50 after |
|
|
548
|
+
| ----------- | -------------- | ------------- |
|
|
549
|
+
| 0 | `#fdf6f8` 🩷 | `#f8f8f8` |
|
|
550
|
+
| 0.001 | `#f5f9fe` | `#f8f8f9` |
|
|
551
|
+
| 0.003 | `#f5f9fe` | `#f7f8fa` |
|
|
552
|
+
| 0.05 | `#f5f9fe` | `#f5f9fe` |
|
|
553
|
+
|
|
554
|
+
`seed: "#808080"` now gives a real grey ramp — `#f8f8f8` / `#eee` / `#808080` / `#2b2b2b` / `#181818`.
|
|
555
|
+
|
|
556
|
+
**No existing palette moves.** The ceiling only binds below itself, and every ordinary brand hue
|
|
557
|
+
sits above it; the repo's own `tokens.json` is byte-identical across this change.
|
|
558
|
+
|
|
559
|
+
- bf453b0: Reject palettes whose ramps don't darken from 50 to 950.
|
|
560
|
+
|
|
561
|
+
Anchors and `tones` are the one thing allowed off the shared lightness ladder — that is the point of
|
|
562
|
+
them. But nothing checked the result was still ordered, so an anchor could put a step _above_ the
|
|
563
|
+
one below it. Anchoring a navy at 600 gave `600 #2c3b4e` (L 0.348) sitting above `700 #4e5c6f`
|
|
564
|
+
(L 0.470): every hover from `bg-<role>-solid` to `-solid-bold` then got **lighter**, which is the
|
|
565
|
+
exact class of drift the fixed ladder was introduced to eliminate.
|
|
566
|
+
|
|
567
|
+
Nothing caught it. `ladderWarnings` compares each pinned step against _its own_ ladder rung, never
|
|
568
|
+
against its neighbours — so it warned about the deviation, called the ramp "Legal", and described
|
|
569
|
+
the wrong defect. And since its tolerance (0.03) is the same size as the ladder's own 50→100 gap,
|
|
570
|
+
two anchors could each sit within tolerance and still invert with no warning at all. The one test
|
|
571
|
+
covering monotonicity built its palette from anchor-free seeds, so it never exercised the path.
|
|
572
|
+
|
|
573
|
+
The build now fails, in the same place and for the same reason as the contrast contract: the order
|
|
574
|
+
is load-bearing (`snap` picks the nearest step by ladder lightness, the mode-stable solid family
|
|
575
|
+
hard-codes 600/700/800, the dark tables re-point steps on the same assumption), and an inversion is
|
|
576
|
+
always author-caused and therefore always actionable. The message names the pair, the consequence,
|
|
577
|
+
and where the colour you wrote actually belongs:
|
|
578
|
+
|
|
579
|
+
```
|
|
580
|
+
colors.palette.navy: step 700 (#4e5c6f, L 0.470) is LIGHTER than step 600 (#2c3b4e, L 0.348).
|
|
581
|
+
A ramp must darken from 50 to 950 — inverted, a hover from `bg-<role>-solid` to `-solid-bold`
|
|
582
|
+
gets lighter, and the dark-mode tables re-point steps assuming the order.
|
|
583
|
+
#2c3b4e (pinned at 600) sits at L 0.348, nearest step 800.
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Sparse `tones` kits are compared over present steps only, and gamut-mapping noise is tolerated, so
|
|
587
|
+
neither reports a false inversion. `ladderWarnings` is unchanged and still a warning.
|
|
588
|
+
|
|
589
|
+
- bf453b0: `.split` gets a stacking convention, a reversal, and a ratio that actually holds.
|
|
590
|
+
|
|
591
|
+
**Fixed: a padded column silently broke the ratio.** `.split > *` was `flex: 1` — a zero basis plus
|
|
592
|
+
a grow factor — and `flex-grow` shares out only the _free_ space, which a child's padding is not
|
|
593
|
+
part of. So a padded column came out wider than its sibling by exactly its horizontal padding, and
|
|
594
|
+
`.split-1-2` quietly stopped being 1:2. Measured in Chrome: an equal `.split` 1000px wide with 40px
|
|
595
|
+
of padding a side on one child resolved 540/460, and `split-1-2` resolved 387/613. The ratio is now
|
|
596
|
+
a **flex basis**, which sizes the border box, so padding sits inside the share: 500/500 and 333/667.
|
|
597
|
+
Nothing to change in your markup — ratios that looked slightly off now land where they read.
|
|
598
|
+
|
|
599
|
+
Two consequences worth knowing. The basis applies only to a **two-child** split, because a ratio is
|
|
600
|
+
a pair contract (only `:first-child` and `:last-child` ever carried one) and with three or more
|
|
601
|
+
children a middle child would collapse to zero; those keep the previous behaviour. And the ratio
|
|
602
|
+
only holds on **border-box** children — a basis sizes the border box, and under content-box the
|
|
603
|
+
padding lands outside the share again, reproducing the exact defect. That is what the new global
|
|
604
|
+
border-box reset is for (see its own note); `.split > *` restates it so the pattern still holds if
|
|
605
|
+
you override the reset in a layer of your own.
|
|
606
|
+
|
|
607
|
+
**Fixed: long unbreakable content stretched a column.** `.split > *` now carries
|
|
608
|
+
`min-inline-size: 0`. A flex item's automatic minimum is its min-content size, so a URL, a code span
|
|
609
|
+
or a `<pre>` used to push its column past its share — and every consumer re-derived `min-w-0` to
|
|
610
|
+
stop it. It belongs to the pattern.
|
|
611
|
+
|
|
612
|
+
**Stacking is now a two-class idiom, using `flex-col` you already have.**
|
|
613
|
+
|
|
614
|
+
```html
|
|
615
|
+
<div class="split flex-col md-split-1-2 gap-l"></div>
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Stacked below 48rem, 1:2 above. What happens when there isn't room for two columns was the question
|
|
619
|
+
`.split` never answered, so every project re-derived
|
|
620
|
+
`flex flex-col gap-… lg-flex-row lg-items-start lg-split`. The `sm-`/`md-`/`lg-`/`xl-` ratio classes
|
|
621
|
+
now assert `flex-direction: row`, which is what un-stacks the pair at the breakpoint. There is
|
|
622
|
+
deliberately **no** `.split-stack`: it would be `flex-col` under a second name.
|
|
623
|
+
|
|
624
|
+
`.flex-col` wins because `.split` is a **pattern** and every utility sits one cascade layer above
|
|
625
|
+
it — see the layering note in this release. `.md-split-1-2` then wins because a breakpoint-scoped
|
|
626
|
+
utility sorts after an unscoped one, which is stable in both formats. (`.split` itself now states
|
|
627
|
+
`flex-direction: row`, so a host that defaults an element to `column` — Bricks' block element —
|
|
628
|
+
can no longer stack it by accident, as long as that host's rule is itself layered.)
|
|
629
|
+
|
|
630
|
+
While stacked the ratio goes inert on its own and needs no reset: a percentage flex basis resolved
|
|
631
|
+
against an auto-height column container behaves as `content`. It does apply if you give the split a
|
|
632
|
+
definite `block-size`, which is a ratio in the block axis — what asking for one on a fixed-height
|
|
633
|
+
column means.
|
|
634
|
+
|
|
635
|
+
**New `.split-reverse`** (breakpoint-prefixable) — swaps the two panels. It is `order` on the first
|
|
636
|
+
child rather than `row-reverse`, so it reverses on **whichever axis the split is currently on**:
|
|
637
|
+
bare, it swaps the columns in a row _and_ the rows in a stack; scoped to a breakpoint
|
|
638
|
+
(`md-split-reverse`) it swaps only once there are two columns — put the media first in source and it
|
|
639
|
+
leads on mobile while sitting on the right at width. The ratio stays attached to the source-first
|
|
640
|
+
child, not to visual position.
|
|
641
|
+
|
|
642
|
+
> **Accessibility.** Reversing puts visual order out of step with DOM order, and focus order follows
|
|
643
|
+
> the DOM (WCAG 2.4.3 Focus Order) — so a keyboard user tabs through a reversed split in the order it
|
|
644
|
+
> is written, not the order it is seen. Put focusable content in **only one** of the two panels. The
|
|
645
|
+
> pattern declares `reading-flow: flex-visual`, which fixes this properly where it is supported;
|
|
646
|
+
> support is not yet broad enough to rely on, and an unknown property is inert, so treat it as an
|
|
647
|
+
> enhancement rather than a guarantee.
|
|
648
|
+
|
|
649
|
+
**New `.flex-row-reverse` / `.flex-col-reverse`** (breakpoint-prefixable) in the `css` and `bricks`
|
|
650
|
+
formats. These names were already in the list the `tailwind` format defers to Tailwind for, but
|
|
651
|
+
they were never defined here — so a mirrored row was inexpressible in framework classes in the two
|
|
652
|
+
formats with no fallback.
|
|
653
|
+
|
|
654
|
+
- bf453b0: `validate()` now rejects what the published JSON Schema already rejected.
|
|
655
|
+
|
|
656
|
+
Runtime validation and `schema.json` derive from the same zod schema, but they disagreed:
|
|
657
|
+
`toJSONSchema` emits `additionalProperties: false` in 16 places, while a plain `z.object` _strips_
|
|
658
|
+
unknown keys at runtime. An editor honouring `$schema` therefore flagged configs that
|
|
659
|
+
`vitops validate` called `✓ valid` — and the keys it waved through were being silently discarded at
|
|
660
|
+
generate time, which is exactly when you want to hear about them.
|
|
661
|
+
|
|
662
|
+
The worst case was a palette hue carrying both `seed` and `tones`: it validated, and the `tones`
|
|
663
|
+
were then ignored, so the authored brand colours simply never appeared.
|
|
664
|
+
|
|
665
|
+
**This is a breaking change for configs that were already wrong.** A config with a stray or
|
|
666
|
+
misspelled key now fails instead of silently dropping it. Both design systems this was tested
|
|
667
|
+
against (a live client site and the vitops site itself) validate unchanged.
|
|
668
|
+
|
|
669
|
+
Error messages got the attention too — a failed union previously reported a bare `Invalid input`
|
|
670
|
+
that named neither key:
|
|
671
|
+
|
|
672
|
+
```
|
|
673
|
+
colors.palette.brand: a hue is either seeded (`seed`, with optional `anchors`) or fixed
|
|
674
|
+
(`tones`) — not both. Drop one: with both present the `tones` are ignored at generate time.
|
|
675
|
+
colors.palette.brand: unknown key "anchor". A hue takes `seed` + optional `anchors`, or `tones`.
|
|
676
|
+
spaceScale: unknown key "nope" — not part of the schema, so ignored at generate time.
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Note the site config (`resolveSiteConfig`) still strips unknown keys; aligning it is tracked
|
|
680
|
+
separately.
|
|
681
|
+
|
|
682
|
+
### Patch Changes
|
|
683
|
+
|
|
684
|
+
- bf453b0: Fix a filled CTA's text turning dark on hover.
|
|
685
|
+
|
|
686
|
+
`<a class="cta">` and every `.cta-<role>` variant flipped from their `text-on-<role>` foreground to
|
|
687
|
+
the link colour while hovered — dark green text on a dark green button. The declaration was always
|
|
688
|
+
right; it was being out-cascaded.
|
|
689
|
+
|
|
690
|
+
Element patterns (`link`, `btn`) wrap their selector in `:where()` so any explicit class beats them
|
|
691
|
+
without `!important`. But the state pseudo was appended **outside** that wrapper, and a pseudo-class
|
|
692
|
+
outside `:where()` still carries weight — so `:where(a, .link):hover` was 0-1-0, exactly tying
|
|
693
|
+
`.cta-brand-primary`, and won on source order because `link` is emitted after `cta`. The pseudo now
|
|
694
|
+
goes inside: `:where(a:hover, .link:hover)` is a true 0-0-0 and loses to any class, which is what the
|
|
695
|
+
resting rule already promised.
|
|
696
|
+
|
|
697
|
+
This affects any element pattern layered under a class pattern — `<a class="cta">`, `<a class="badge">`,
|
|
698
|
+
`<button class="cta">` — in the `css` and `bricks` formats. Nothing else about the emitted rules
|
|
699
|
+
changes. If you worked around it with a hand-written `color` override on a CTA, you can drop it.
|
|
700
|
+
|
|
701
|
+
- bf453b0: Say plainly that `fonts` in `design-system.json` is stacks only and loads nothing.
|
|
702
|
+
|
|
703
|
+
The field's description was complete and self-consistent — "raw font stacks by name, emitted as
|
|
704
|
+
`--font-<name>` tokens" — and told you nothing about where the `@font-face` comes from. Following
|
|
705
|
+
it literally leads to installing a `@fontsource*` package and importing its CSS in a layout, which
|
|
706
|
+
renders correctly and silently gives up subsetting, preload, and the `size-adjust` /
|
|
707
|
+
`ascent-override` fallback metrics: a CLS regression that looks like a working setup.
|
|
708
|
+
|
|
709
|
+
The field is unchanged. What changed is that four surfaces now name the boundary and point at the
|
|
710
|
+
fix — declare the family in Astro's `fonts:` config and point the token at its `cssVariable`:
|
|
711
|
+
- the `fonts` description in the JSON Schema, and therefore `vitops docs authoring`
|
|
712
|
+
- `SKILL.md`, which previously never mentioned fonts at all
|
|
713
|
+
- the generated `tailwind.css` header and its `@theme` fonts comment
|
|
714
|
+
- the generated `type-tokens.css` header (css format)
|
|
715
|
+
|
|
716
|
+
- bf453b0: `vitops lint` now catches hand-written CSS that re-implements a framework primitive.
|
|
717
|
+
|
|
718
|
+
The existing linter finds classes that resolve to nothing — you named something and it isn't there.
|
|
719
|
+
This is the costlier inverse, where the code **works**: a centred container written by hand, a
|
|
720
|
+
two-column split behind a media query, a flex row. Nothing is broken, so nothing ever surfaces it,
|
|
721
|
+
and the design system quietly stops being where those decisions live.
|
|
722
|
+
|
|
723
|
+
Three rules to start, each requiring the combination that makes the intent unambiguous
|
|
724
|
+
(`margin-inline: auto` alone is not a centred track):
|
|
725
|
+
|
|
726
|
+
| Found | Suggested |
|
|
727
|
+
| ----------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
728
|
+
| `max-inline-size: var(--width-*)` + `margin-inline: auto` | `.centered`, widening a child with `breakout` / `spotlight` / `fullbleed` |
|
|
729
|
+
| `grid-template-columns: 1fr 2fr` inside a `min-width` query | `md-split-1-2` — or `@md:split-1-2` in the tailwind format |
|
|
730
|
+
| `display: flex` + `align-items: center` | `flex items-center`, or `.cluster-start` |
|
|
731
|
+
|
|
732
|
+
**Findings now carry a severity, and suggestions do not fail the command.** These are judgement
|
|
733
|
+
calls, and a reuse hint that broke CI on the day it shipped would be a worse defect than the drift it
|
|
734
|
+
reports. `--strict` promotes them for anyone who wants the ratchet. Everything the linter reported
|
|
735
|
+
before is an `error` and still exits 1.
|
|
736
|
+
|
|
737
|
+
`.css` files and `<style>` blocks are now read (the class linter only ever looked at markup). The
|
|
738
|
+
generated stylesheet is skipped by its `GENERATED … do not edit by hand` banner rather than by path —
|
|
739
|
+
the `css` format writes `styles.css` into `src/styles`, squarely inside what `--src` scans, and
|
|
740
|
+
linting it made the framework report `.split` as reinventing `.split`.
|
|
741
|
+
|
|
742
|
+
Two other gaps closed:
|
|
743
|
+
- **The format-spelling check ran in one direction only.** `md-split-1-2` in a tailwind project was
|
|
744
|
+
caught; `@md:split-1-2` in a css/bricks project was not, because variant stripping removed the
|
|
745
|
+
`@md:` regardless of format and left a bare class that matched nothing. The same silent no-op, in
|
|
746
|
+
the other direction, unreported. Stripping is format-aware now.
|
|
747
|
+
- **`grid-auto` and the whole `m-*` rhythm family were missing from the tailwind format entirely** —
|
|
748
|
+
not dropped via `TW_CLASH`, just never re-emitted when `layout.css` is skipped and a subset
|
|
749
|
+
re-emitted in its place. Unlike the `<bp>-` classes they have no Tailwind equivalent, and unlike a
|
|
750
|
+
misspelt class the linter cannot flag them, because they aren't anchored to your config. They emit
|
|
751
|
+
as `@utility` definitions now, keyed to the same `--rhythm-*` variables as the css format.
|
|
752
|
+
|
|
753
|
+
Run against this repo's own docs site, the rules found one real instance on the first pass; it is
|
|
754
|
+
fixed in the same change.
|
|
755
|
+
|
|
756
|
+
- 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.
|
|
757
|
+
|
|
758
|
+
`text-wrap: balance` for headings and `pretty` for copy belong to the **type role**, not to a
|
|
759
|
+
reset — the role already owns the property (`--<role>-tw`, editable in the live theme editor),
|
|
760
|
+
and `typography.headings` projects it onto bare `h1`/`body` so unclassed markup gets it with no
|
|
761
|
+
class at all. Three things stopped that working end to end.
|
|
762
|
+
|
|
763
|
+
**Omitting `text-wrap` was never "inherit".** Like `style`, `text-transform` and
|
|
764
|
+
`text-decoration`, it is emitted on every role at its identity value so that applying one role
|
|
765
|
+
class over another fully resets it. So a role that left it out emitted `text-wrap: wrap` and
|
|
766
|
+
**cancelled** the `pretty` it would otherwise inherit from a `body`-mapped role — captions and
|
|
767
|
+
footnotes quietly opted out of the thing they most benefit from. The behaviour is unchanged
|
|
768
|
+
(the reset is what makes role classes composable); what changed is that the schema description
|
|
769
|
+
now says so, and the shipped configs state the value on every role rather than leaving it to
|
|
770
|
+
the identity.
|
|
771
|
+
|
|
772
|
+
`vitops init` now scaffolds `balance` on `display`/`heading` and `pretty` on `body`. If you
|
|
773
|
+
already have a `design-system.json`, add `text-wrap` to each role deliberately — `balance` for
|
|
774
|
+
heading-like roles, `pretty` for copy, `wrap` for short single-line labels.
|
|
775
|
+
|
|
776
|
+
**The tailwind format dropped `typography.headings` entirely.** It emitted the
|
|
777
|
+
`@utility font-<role>` half of the typography layer and none of the bare-element bindings, so a
|
|
778
|
+
Tailwind consumer's `<h1>` and `<body>` carried no role styling at all — no family, no size, no
|
|
779
|
+
`text-wrap` — while the css and bricks formats styled them. They are now emitted into Tailwind's
|
|
780
|
+
`base` layer, which keeps `font-<role>` and the patterns able to override them.
|
|
781
|
+
|
|
782
|
+
**This visibly changes existing tailwind sites.** Elements named in your `typography.headings`
|
|
783
|
+
map start picking up their role's typography, which for most projects is the styling they were
|
|
784
|
+
missing — but if you have been compensating with your own `h1`/`body` rules, they now stack.
|
|
785
|
+
Your own unlayered CSS still wins; utilities layered by Tailwind will not. Either drop the
|
|
786
|
+
compensating rules or remove the entry from `typography.headings`.
|
|
787
|
+
|
|
788
|
+
**New utilities** `text-wrap`, `text-nowrap`, `text-balance`, `text-pretty` (css/bricks only —
|
|
789
|
+
Tailwind ships these natively, so they are in `TW_CLASH` and the tailwind bundle drops our
|
|
790
|
+
copies). They are the per-element escape hatch for markup that carries no role class, such as a
|
|
791
|
+
Bricks-authored heading.
|
|
792
|
+
|
|
793
|
+
- bf453b0: Warn on unrecognised `typography.roles` keys, and stop the schema documenting two that never worked.
|
|
794
|
+
|
|
795
|
+
The role key set is closed, and an unrecognised key is **dropped** — the role still renders, just
|
|
796
|
+
without the declaration you asked for. The schema description made that worse in two directions at
|
|
797
|
+
once: it advertised `transform` and `decoration`, when the generator only accepts `text-transform`
|
|
798
|
+
and `text-decoration`, and it claimed unknown keys were "passed through" when they are discarded. So
|
|
799
|
+
the two documented short forms were exactly the two that silently did nothing. In one case that
|
|
800
|
+
shipped title-case navigation to production across two deploys.
|
|
801
|
+
|
|
802
|
+
The behaviour is unchanged — unknown keys are still ignored, because `transform` is a real CSS
|
|
803
|
+
property and emitting it would break layout rather than do nothing. What changed:
|
|
804
|
+
- the generator now warns per role and key, naming the intended spelling where there is an obvious
|
|
805
|
+
one (`transform` → `text-transform`, `letter-spacing` → `tracking`, `font-size` → `size`, …) and
|
|
806
|
+
otherwise listing the recognised set;
|
|
807
|
+
- the schema description names the real keys and says unrecognised ones are ignored, which
|
|
808
|
+
propagates to `vitops docs authoring` and to editor hovers.
|
|
809
|
+
|
|
810
|
+
Recognised keys, for reference: `family`, `size`, `weight`, `style`, `line-height`, `tracking`,
|
|
811
|
+
`text-transform`, `text-decoration`, `text-wrap`, `color`.
|
|
812
|
+
|
|
813
|
+
- Updated dependencies [04a51d8]
|
|
814
|
+
- Updated dependencies [bf453b0]
|
|
815
|
+
- Updated dependencies [bf453b0]
|
|
816
|
+
- @getvitops/utils@2.0.0
|
|
817
|
+
|
|
3
818
|
## 1.0.0
|
|
4
819
|
|
|
5
820
|
### Major Changes
|