@getvitops/generator 0.8.0 → 0.9.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 +171 -0
- package/assets/css/animation.css +12 -2
- package/assets/css/patterns/details.css +27 -8
- package/assets/css/patterns/drawer.css +15 -0
- package/assets/css/patterns/grouped.css +1 -1
- package/assets/css/patterns/subgrid.css +62 -16
- package/assets/css/patterns/theme-editor.css +8 -0
- package/assets/css/utilities.css +5 -3
- package/assets/js/editor.js +1 -1
- package/dist/index.d.mts +96 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +424 -165
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/schema.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -6,6 +6,149 @@ import { transform } from "lightningcss";
|
|
|
6
6
|
import * as z from "zod/mini";
|
|
7
7
|
import { generateOklchPalette, hexToOklch, oklchStringToHex, pickOn } from "@getvitops/utils/color";
|
|
8
8
|
|
|
9
|
+
//#region src/shared.ts
|
|
10
|
+
/**
|
|
11
|
+
* Data shared between the generator (`generate.ts`) and the docs bundle
|
|
12
|
+
* (`docs.ts`). Lives in its own module because `generate.ts` imports `docs.ts` —
|
|
13
|
+
* importing back the other way would be a cycle.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Selector the dark functional-token flip hangs off.
|
|
17
|
+
*
|
|
18
|
+
* `data-brx-theme` is Bricks' own attribute — Bricks sets it, so it's what the
|
|
19
|
+
* WordPress target needs. Nothing sets it anywhere else, which meant the dark
|
|
20
|
+
* flip was unreachable outside Bricks: the shipped `<color-scheme-toggle>` web
|
|
21
|
+
* component writes `documentElement.dataset.theme` (i.e. `data-theme`), so
|
|
22
|
+
* clicking "Dark" changed an attribute no rule matched. Matching both makes the
|
|
23
|
+
* component work on every target without changing what Bricks already does.
|
|
24
|
+
*
|
|
25
|
+
* Note this covers the explicit choice only — there is deliberately no
|
|
26
|
+
* `prefers-color-scheme` block, so the toggle's "System" position currently
|
|
27
|
+
* resolves to light. Adding one would flip every existing consumer site dark for
|
|
28
|
+
* dark-OS users, which is a product decision, not a bug fix.
|
|
29
|
+
*
|
|
30
|
+
* Shared so the docs quote the selector the generator actually emits.
|
|
31
|
+
*/
|
|
32
|
+
const DARK_SEL = ":root[data-brx-theme=\"dark\"], :root[data-theme=\"dark\"]";
|
|
33
|
+
/**
|
|
34
|
+
* Roles the shipped CSS partials reference with NO fallback, so a config that
|
|
35
|
+
* omits one leaves those rules resolving to nothing — an invalid declaration,
|
|
36
|
+
* not a graceful degrade.
|
|
37
|
+
*
|
|
38
|
+
* `colors.roles` is an open record: any name works and generates a full token
|
|
39
|
+
* set, utilities and dark flip. But the framework's own component CSS is
|
|
40
|
+
* written against these six, so "roles are arbitrary" is only half true — they
|
|
41
|
+
* are *extensible*, over a required core. `validate()` warns when one is
|
|
42
|
+
* missing; `required-roles.test.ts` re-derives this list from the partials so
|
|
43
|
+
* it cannot drift as components are added.
|
|
44
|
+
*
|
|
45
|
+
* Kept as a constant rather than scanned at call time because `validate()` is
|
|
46
|
+
* a pure function — the CLI, the Vite plugin and `toJSONSchema` all import it,
|
|
47
|
+
* and none of them should touch the filesystem to answer "is this config ok?".
|
|
48
|
+
*/
|
|
49
|
+
const REQUIRED_ROLES = [
|
|
50
|
+
"brand-primary",
|
|
51
|
+
"danger",
|
|
52
|
+
"neutral",
|
|
53
|
+
"surface",
|
|
54
|
+
"ui-primary",
|
|
55
|
+
"warning"
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* The functional-token and emphasis-stop suffixes a role resolves to. Shared so
|
|
59
|
+
* the required-roles drift test greps for exactly what the generator emits.
|
|
60
|
+
*/
|
|
61
|
+
const ROLE_TOKEN_SUFFIXES = [
|
|
62
|
+
"bg",
|
|
63
|
+
"bg-muted",
|
|
64
|
+
"bg-bold",
|
|
65
|
+
"border",
|
|
66
|
+
"border-bold",
|
|
67
|
+
"solid",
|
|
68
|
+
"solid-bold",
|
|
69
|
+
"on-solid",
|
|
70
|
+
"text",
|
|
71
|
+
"text-muted",
|
|
72
|
+
"text-x-muted"
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Pattern base geometry props → per-pattern override-hook shorthand. Each hook
|
|
76
|
+
* wraps the authored value (`padding: var(--p-btn, 0.4em 0.8em)`) so a
|
|
77
|
+
* consumer restyles one pattern by setting one variable. The hook is named after
|
|
78
|
+
* the pattern's key, not its class.
|
|
79
|
+
*/
|
|
80
|
+
const BASE_HOOK = {
|
|
81
|
+
padding: "p",
|
|
82
|
+
"border-radius": "br",
|
|
83
|
+
border: "b",
|
|
84
|
+
"box-shadow": "ds",
|
|
85
|
+
"font-size": "fs"
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Framework utility classes whose names collide with Tailwind's own defaults.
|
|
89
|
+
* The `tailwind` format strips any rule targeting these — Tailwind provides
|
|
90
|
+
* them, so consumers use Tailwind's utilities for these names instead.
|
|
91
|
+
*/
|
|
92
|
+
const TW_CLASH = new Set([
|
|
93
|
+
"block",
|
|
94
|
+
"inline",
|
|
95
|
+
"inline-block",
|
|
96
|
+
"flow-root",
|
|
97
|
+
"flex",
|
|
98
|
+
"inline-flex",
|
|
99
|
+
"grid",
|
|
100
|
+
"inline-grid",
|
|
101
|
+
"table",
|
|
102
|
+
"inline-table",
|
|
103
|
+
"table-caption",
|
|
104
|
+
"table-cell",
|
|
105
|
+
"table-row",
|
|
106
|
+
"contents",
|
|
107
|
+
"hidden",
|
|
108
|
+
"list-item",
|
|
109
|
+
"static",
|
|
110
|
+
"fixed",
|
|
111
|
+
"absolute",
|
|
112
|
+
"relative",
|
|
113
|
+
"sticky",
|
|
114
|
+
"isolate",
|
|
115
|
+
"visible",
|
|
116
|
+
"invisible",
|
|
117
|
+
"collapse",
|
|
118
|
+
"sr-only",
|
|
119
|
+
"not-sr-only",
|
|
120
|
+
"flex-row",
|
|
121
|
+
"flex-row-reverse",
|
|
122
|
+
"flex-col",
|
|
123
|
+
"flex-col-reverse",
|
|
124
|
+
"flex-wrap",
|
|
125
|
+
"flex-nowrap",
|
|
126
|
+
"items-start",
|
|
127
|
+
"items-end",
|
|
128
|
+
"items-center",
|
|
129
|
+
"items-baseline",
|
|
130
|
+
"items-stretch",
|
|
131
|
+
"justify-start",
|
|
132
|
+
"justify-end",
|
|
133
|
+
"justify-center",
|
|
134
|
+
"justify-between",
|
|
135
|
+
"justify-around",
|
|
136
|
+
"justify-evenly",
|
|
137
|
+
"content-start",
|
|
138
|
+
"content-end",
|
|
139
|
+
"content-center",
|
|
140
|
+
"content-between",
|
|
141
|
+
"content-around",
|
|
142
|
+
"content-evenly",
|
|
143
|
+
"text-left",
|
|
144
|
+
"text-center",
|
|
145
|
+
"text-right",
|
|
146
|
+
"text-justify",
|
|
147
|
+
"text-start",
|
|
148
|
+
"text-end"
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
9
152
|
//#region src/schema.ts
|
|
10
153
|
/**
|
|
11
154
|
* Canonical `design-system.json` schema — the single source of truth.
|
|
@@ -42,7 +185,7 @@ const UtilityName = z.enum([
|
|
|
42
185
|
]);
|
|
43
186
|
const Colors = desc(z.object({
|
|
44
187
|
palette: desc(z.record(z.string(), Ramp), "Palette hues by name. Each becomes an 11-step numeric OKLCH scale (`--color-<hue>-50…950`)."),
|
|
45
|
-
roles: desc(z.record(z.string(), z.string()),
|
|
188
|
+
roles: desc(z.record(z.string(), z.string()), `Maps semantic role names onto palette hues. Role names are ARBITRARY — add a key and the generator emits that role's full FUNCTIONAL token set (\`--<role>-{bg,bg-muted,border,border-bold,solid,solid-bold,on-solid,text,text-muted,text-x-muted}\`), its emphasis stops, its dark-mode flip and its utility classes. Dark mode flips automatically; there is no per-appearance scheme grammar. Six roles are a required core, because the shipped framework CSS references them with no fallback: ${REQUIRED_ROLES.join(", ")}. Conventional additions are ui-secondary/accent, brand-secondary, info and success.`),
|
|
46
189
|
utilities: z.optional(desc(z.array(UtilityName), "Which colour utility-class families to emit (`bg-*`, `text-*`, `border-*`, `outline-*`, `fill-*`, `stroke-*`). Defaults to bg, text, border."))
|
|
47
190
|
}), "The colour system (the only required section): `palette` hues become generated OKLCH scales; `roles` map semantic roles onto those hues, from which all functional tokens and dark mode derive.");
|
|
48
191
|
const Scale = z.object({
|
|
@@ -153,6 +296,8 @@ function validate(input) {
|
|
|
153
296
|
warnings: []
|
|
154
297
|
};
|
|
155
298
|
const warnings = [];
|
|
299
|
+
const missing = REQUIRED_ROLES.filter((r) => roles[r] == null);
|
|
300
|
+
if (missing.length) warnings.push(`colors.roles is missing ${missing.map((r) => `"${r}"`).join(", ")} — the framework's own component CSS references ${missing.length === 1 ? "it" : "them"} with no fallback, so those components will render uncoloured. Add any role you like, but keep these defined.`);
|
|
156
301
|
const radii = Object.keys(result.data.patterns?.radii ?? {});
|
|
157
302
|
const items = new Set(Object.keys(result.data.patterns?.items ?? {}));
|
|
158
303
|
for (const name of radii) if (items.has(name)) warnings.push(`patterns.radii.${name} collides with the "${name}" pattern on --br-${name}; the pattern's override hook wins — rename the radius`);
|
|
@@ -164,90 +309,6 @@ function validate(input) {
|
|
|
164
309
|
};
|
|
165
310
|
}
|
|
166
311
|
|
|
167
|
-
//#endregion
|
|
168
|
-
//#region src/shared.ts
|
|
169
|
-
/**
|
|
170
|
-
* Data shared between the generator (`generate.ts`) and the docs bundle
|
|
171
|
-
* (`docs.ts`). Lives in its own module because `generate.ts` imports `docs.ts` —
|
|
172
|
-
* importing back the other way would be a cycle.
|
|
173
|
-
*/
|
|
174
|
-
/**
|
|
175
|
-
* Pattern base geometry props → per-pattern override-hook shorthand. Each hook
|
|
176
|
-
* wraps the authored value (`padding: var(--p-btn, 0.4em 0.8em)`) so a
|
|
177
|
-
* consumer restyles one pattern by setting one variable. The hook is named after
|
|
178
|
-
* the pattern's key, not its class.
|
|
179
|
-
*/
|
|
180
|
-
const BASE_HOOK = {
|
|
181
|
-
padding: "p",
|
|
182
|
-
"border-radius": "br",
|
|
183
|
-
border: "b",
|
|
184
|
-
"box-shadow": "ds",
|
|
185
|
-
"font-size": "fs"
|
|
186
|
-
};
|
|
187
|
-
/**
|
|
188
|
-
* Framework utility classes whose names collide with Tailwind's own defaults.
|
|
189
|
-
* The `tailwind` format strips any rule targeting these — Tailwind provides
|
|
190
|
-
* them, so consumers use Tailwind's utilities for these names instead.
|
|
191
|
-
*/
|
|
192
|
-
const TW_CLASH = new Set([
|
|
193
|
-
"block",
|
|
194
|
-
"inline",
|
|
195
|
-
"inline-block",
|
|
196
|
-
"flow-root",
|
|
197
|
-
"flex",
|
|
198
|
-
"inline-flex",
|
|
199
|
-
"grid",
|
|
200
|
-
"inline-grid",
|
|
201
|
-
"table",
|
|
202
|
-
"inline-table",
|
|
203
|
-
"table-caption",
|
|
204
|
-
"table-cell",
|
|
205
|
-
"table-row",
|
|
206
|
-
"contents",
|
|
207
|
-
"hidden",
|
|
208
|
-
"list-item",
|
|
209
|
-
"static",
|
|
210
|
-
"fixed",
|
|
211
|
-
"absolute",
|
|
212
|
-
"relative",
|
|
213
|
-
"sticky",
|
|
214
|
-
"isolate",
|
|
215
|
-
"visible",
|
|
216
|
-
"invisible",
|
|
217
|
-
"collapse",
|
|
218
|
-
"sr-only",
|
|
219
|
-
"not-sr-only",
|
|
220
|
-
"flex-row",
|
|
221
|
-
"flex-row-reverse",
|
|
222
|
-
"flex-col",
|
|
223
|
-
"flex-col-reverse",
|
|
224
|
-
"flex-wrap",
|
|
225
|
-
"flex-nowrap",
|
|
226
|
-
"items-start",
|
|
227
|
-
"items-end",
|
|
228
|
-
"items-center",
|
|
229
|
-
"items-baseline",
|
|
230
|
-
"items-stretch",
|
|
231
|
-
"justify-start",
|
|
232
|
-
"justify-end",
|
|
233
|
-
"justify-center",
|
|
234
|
-
"justify-between",
|
|
235
|
-
"justify-around",
|
|
236
|
-
"justify-evenly",
|
|
237
|
-
"content-start",
|
|
238
|
-
"content-end",
|
|
239
|
-
"content-center",
|
|
240
|
-
"content-between",
|
|
241
|
-
"content-around",
|
|
242
|
-
"content-evenly",
|
|
243
|
-
"text-left",
|
|
244
|
-
"text-center",
|
|
245
|
-
"text-right",
|
|
246
|
-
"text-justify",
|
|
247
|
-
"text-start",
|
|
248
|
-
"text-end"
|
|
249
|
-
]);
|
|
250
|
-
|
|
251
312
|
//#endregion
|
|
252
313
|
//#region src/tokens.ts
|
|
253
314
|
/**
|
|
@@ -386,6 +447,11 @@ const EMPHASIS_DARK = {
|
|
|
386
447
|
bold: "300",
|
|
387
448
|
"x-bold": "100"
|
|
388
449
|
};
|
|
450
|
+
/**
|
|
451
|
+
* The emphasis-stop names, derived from the table rather than re-listed, so the
|
|
452
|
+
* utility emitter in `generate.ts` cannot drift from the tokens it emits.
|
|
453
|
+
*/
|
|
454
|
+
const EMPHASIS_STOPS = Object.keys(EMPHASIS_LIGHT);
|
|
389
455
|
/** Resolve the functional token set for one role over its hue. */
|
|
390
456
|
function functionalRole(role, hueName, hue) {
|
|
391
457
|
const numeric = hue.numeric;
|
|
@@ -981,9 +1047,17 @@ Families: ${code(families)} (\`--font-*\`).
|
|
|
981
1047
|
## Colour
|
|
982
1048
|
|
|
983
1049
|
**Functional tokens are the primary vocabulary** — classes name the *job*, not the tone, and
|
|
984
|
-
every one remaps automatically under
|
|
1050
|
+
every one remaps automatically under \`${DARK_SEL}\` (background/text ends
|
|
985
1051
|
swap; \`solid\` fills stay mode-stable with a computed \`on-\` foreground). Prefer these over
|
|
986
|
-
raw steps.
|
|
1052
|
+
raw steps.
|
|
1053
|
+
|
|
1054
|
+
**Role names are yours.** \`colors.roles\` is an open map: add a key, and the generator emits
|
|
1055
|
+
that role's full functional token set, its dark flip and every utility below. Your config
|
|
1056
|
+
currently defines ${code(roles)}. Six of those are a **required core** — the framework's own
|
|
1057
|
+
component CSS references \`${REQUIRED_ROLES.join("`, `")}\` with no fallback, so removing one
|
|
1058
|
+
leaves those components uncoloured (\`vitops validate\` warns). Everything beyond them is free.
|
|
1059
|
+
|
|
1060
|
+
Rules (role ∈ ${code(roles)}, or whatever you add):
|
|
987
1061
|
|
|
988
1062
|
- **Surfaces** — \`bg-<role>\` (the role's background wash; for \`surface\` this is the
|
|
989
1063
|
card/panel plane), \`bg-<role>-muted\` (subtle / sunken), \`bg-surface-bold\` (raised plane).
|
|
@@ -997,9 +1071,10 @@ raw steps. Rules (role ∈ ${code(roles)}):
|
|
|
997
1071
|
\`--color-<role>-{x-muted,muted,bold,x-bold}\` — \`muted\` recedes toward the background
|
|
998
1072
|
extreme, \`bold\` advances toward the foreground, in *either* appearance.
|
|
999
1073
|
|
|
1000
|
-
Everyday pairings
|
|
1001
|
-
\`
|
|
1002
|
-
\`text
|
|
1074
|
+
Everyday pairings, using the roles this config defines: page
|
|
1075
|
+
\`bg-${roles[0] ?? "neutral"}\` + \`text-${roles[0] ?? "neutral"}\`; captions
|
|
1076
|
+
\`text-${roles[0] ?? "neutral"}-muted\`; buttons
|
|
1077
|
+
\`bg-${roles[2] ?? roles[0] ?? "ui-primary"}-solid text-on-${roles[2] ?? roles[0] ?? "ui-primary"}\`.
|
|
1003
1078
|
|
|
1004
1079
|
**Raw scale** (secondary / fine control) — rule \`<util>-<hue>-<step>\` with util ∈
|
|
1005
1080
|
${code(utils)}: every hue is an 11-step OKLCH scale generated from its seed (or fixed brand
|
|
@@ -1007,6 +1082,28 @@ tones), numeric steps \`50\` … \`950\` (tinted near-white → tinted near-blac
|
|
|
1007
1082
|
\`bg-${ramps[0] ?? "brand"}-100\`, \`text-${ramps[0] ?? "brand"}-800\`. Hues: ${code(ramps)}.
|
|
1008
1083
|
The **bare** role name (\`bg-<role>\`, \`text-<role>\`) is always the functional token.
|
|
1009
1084
|
|
|
1085
|
+
> **Raw scale classes are frozen — they do NOT remap in dark mode.**
|
|
1086
|
+
> \`bg-${ramps[0] ?? "brand"}-800\` is that exact colour in every appearance. The automatic
|
|
1087
|
+
> dark flip described above applies **only** to the functional role tokens, because
|
|
1088
|
+
> \`--color-<hue>-<step>\` is emitted once and never re-pointed under \`${DARK_SEL}\`.
|
|
1089
|
+
> A raw step on a page that can switch appearance is a latent bug: it looks correct in
|
|
1090
|
+
> whichever mode you built it in and inverts in the other.
|
|
1091
|
+
>
|
|
1092
|
+
> If you are reaching for a raw step, you usually want a **role** instead — and roles are
|
|
1093
|
+
> extensible, so adding one is a two-line config change:
|
|
1094
|
+
>
|
|
1095
|
+
> | instead of | use | why |
|
|
1096
|
+
> | --- | --- | --- |
|
|
1097
|
+
> | \`bg-<hue>-50\` / \`-950\` | \`bg-<role>\` | the page/backdrop plane, flips automatically |
|
|
1098
|
+
> | \`bg-<hue>-100\` / \`-900\` | \`bg-<role>-muted\` | one plane in from the page |
|
|
1099
|
+
> | \`bg-<hue>-500\`…\`-700\` | \`bg-<role>-solid\` | vivid fill, mode-stable, pairs with \`text-on-<role>\` |
|
|
1100
|
+
> | \`text-<hue>-950\` / \`-50\` | \`text-<role>\` | contrast-guaranteed body text |
|
|
1101
|
+
> | \`text-<hue>-800\` / \`-200\` | \`text-<role>-muted\` | secondary text |
|
|
1102
|
+
> | \`border-<hue>-200\` / \`-800\` | \`border-<role>\` | |
|
|
1103
|
+
>
|
|
1104
|
+
> Raw steps stay the right tool for genuinely fixed colours — a brand mark, a chart series,
|
|
1105
|
+
> an illustration — where the value must not move between appearances.
|
|
1106
|
+
|
|
1010
1107
|
## Shadows
|
|
1011
1108
|
|
|
1012
1109
|
Rule: **\`drop-shadow-<size>\`** — size ∈ ${code(shadows)} (applied as a \`filter\`, so it
|
|
@@ -1168,9 +1265,27 @@ ${clashList.map((c) => `\`${c}\``).join(", ")}
|
|
|
1168
1265
|
Other Tailwind-specific behaviour:
|
|
1169
1266
|
|
|
1170
1267
|
- **Variants are Tailwind's job.** No pre-expanded breakpoint/state classes are emitted
|
|
1171
|
-
(the framework's \`@container (min-width: …)\` variant blocks are dropped
|
|
1172
|
-
|
|
1268
|
+
(the framework's \`@container (min-width: …)\` **variant** blocks are dropped; component
|
|
1269
|
+
container queries such as the sitenav's desktop switch are kept); use Tailwind syntax —
|
|
1270
|
+
\`@md:split-1-2\`, \`hover:flip-fade-in\`. Container breakpoints are registered as
|
|
1173
1271
|
\`--container-{sm,md,lg,xl}\` = 30/48/64/80rem, backing the \`@sm:\`…\`@xl:\` variants.
|
|
1272
|
+
|
|
1273
|
+
> **Three spellings, and only two of them work here.**
|
|
1274
|
+
>
|
|
1275
|
+
> | you write | in \`tailwind\` | note |
|
|
1276
|
+
> | --- | --- | --- |
|
|
1277
|
+
> | \`@md:flex-row\` | ✅ container query | the framework's breakpoints (48rem) |
|
|
1278
|
+
> | \`md:flex-row\` | ✅ media query | **Tailwind's** breakpoints, which differ — \`sm:\` is 40rem where \`@sm:\` is 30rem |
|
|
1279
|
+
> | \`md-flex-row\` | ❌ silently nothing | the css/bricks spelling; not emitted in this format |
|
|
1280
|
+
>
|
|
1281
|
+
> \`md-*\` is the trap: it is a real class in \`css\`/\`bricks\` and a no-op here, and
|
|
1282
|
+
> nothing errors — the element simply never changes at the breakpoint. Prefer \`@md:\`
|
|
1283
|
+
> so one vocabulary of breakpoints applies throughout.
|
|
1284
|
+
|
|
1285
|
+
- **Overriding \`--container-*\` also moves Tailwind's width scale.** Registering the
|
|
1286
|
+
framework breakpoints in \`@theme\` re-points \`max-w-sm\`…\`max-w-xl\` at the same values,
|
|
1287
|
+
so \`max-w-md\` is 48rem here rather than Tailwind's stock 28rem. Use
|
|
1288
|
+
\`max-w-(--container-md)\` style arbitrary values if you need to be explicit.
|
|
1174
1289
|
- **The space scale is NOT mapped into Tailwind's \`--spacing-*\` namespace** (that would
|
|
1175
1290
|
corrupt Tailwind's numeric multipliers and \`max-w-*\` sizes). Numeric utilities like
|
|
1176
1291
|
\`p-4\` keep Tailwind's 0.25rem meaning; use the design-system scale via arbitrary
|
|
@@ -1180,6 +1295,18 @@ Other Tailwind-specific behaviour:
|
|
|
1180
1295
|
(\`bg-<role>\`, \`text-<role>-muted\`, …) is the public API. The raw hue scales ARE
|
|
1181
1296
|
\`@theme\` colours, so native \`bg-<hue>-500\`-style utilities work.
|
|
1182
1297
|
|
|
1298
|
+
This split is deliberate and load-bearing. When a token sits in \`@theme\` *and* an
|
|
1299
|
+
\`@utility\` of the derived name exists, Tailwind merges both into one rule with the
|
|
1300
|
+
\`@theme\` declaration last — regardless of source order. Promoting the role tokens
|
|
1301
|
+
would therefore silently replace the functional plane with the emphasis stop on
|
|
1302
|
+
\`bg-<role>-muted\`, \`text-<role>-muted\`, \`text-<role>-x-muted\`, \`border-<role>-bold\`
|
|
1303
|
+
and \`bg-surface-bold\`, two of which are the contrast-guaranteed text tokens.
|
|
1304
|
+
- **\`colors.utilities\` is a floor here, not a ceiling.** It controls which families get
|
|
1305
|
+
explicit role \`@utility\` rules, exactly as in \`css\`/\`bricks\`. But the raw hue scales
|
|
1306
|
+
are \`@theme\` colours, and Tailwind derives *every* colour family from those on demand
|
|
1307
|
+
(\`ring-\`, \`divide-\`, \`accent-\`, \`caret-\`, …), so hue-step utilities you did not enable
|
|
1308
|
+
still resolve in this format.
|
|
1309
|
+
|
|
1183
1310
|
## \`css\` — standalone bundle
|
|
1184
1311
|
|
|
1185
1312
|
Emits a bundled, self-contained \`styles.css\` + \`tokens.json\` + \`design-manifest.json\`.
|
|
@@ -1188,6 +1315,26 @@ pre-expanded — including breakpoint/state variants with \`-\` separators
|
|
|
1188
1315
|
(\`md-split-1-2\`, \`hover-fade-in\`). For non-Bricks, non-Tailwind consumers and the
|
|
1189
1316
|
docs build.
|
|
1190
1317
|
|
|
1318
|
+
**Cascade layers.** The bundle ships three, in precedence order:
|
|
1319
|
+
|
|
1320
|
+
\`\`\`
|
|
1321
|
+
@layer vitops.base, vitops.components, vitops.utilities;
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
|
|
1324
|
+
- \`vitops.base\` — the UA reset and the pure \`:root\` token blocks.
|
|
1325
|
+
- \`vitops.components\` — the animation engine, structural layout, and every pattern.
|
|
1326
|
+
- \`vitops.utilities\` — \`bg-*\`, \`text-*\`, \`border-*\`, \`drop-shadow-*\`, \`font-*\`,
|
|
1327
|
+
animation effects, and the display/\`sr-only\` families.
|
|
1328
|
+
|
|
1329
|
+
So a utility overrides a pattern: \`class="card bg-danger-muted"\` tints the card. Your own
|
|
1330
|
+
unlayered CSS beats all three — see [concepts/patterns.md](concepts/patterns.md) for the
|
|
1331
|
+
override story and the one gotcha (a reset must be layered and ordered first).
|
|
1332
|
+
|
|
1333
|
+
Known gap: \`layout.css\` is a single partial mixing structural rules (\`.rhythm\`,
|
|
1334
|
+
\`.centered\`) with utilities (\`.m-*\`, \`.flex\`, \`.split-*\`), so it sits in
|
|
1335
|
+
\`vitops.components\` whole — its utility half cannot yet override a pattern. Splitting it is
|
|
1336
|
+
tracked separately.
|
|
1337
|
+
|
|
1191
1338
|
## \`bricks\` — WordPress / Bricks Builder payload
|
|
1192
1339
|
|
|
1193
1340
|
Emits the full deployable theme payload: \`styles.min.css\`, the Bricks import JSONs
|
|
@@ -1261,11 +1408,16 @@ Classes name the token, not the tone: \`bg-<role>\`, \`text-<role>-muted\`, \`te
|
|
|
1261
1408
|
|
|
1262
1409
|
## Automatic dark mode
|
|
1263
1410
|
|
|
1264
|
-
Dark mode is a **functional flip** under \`:
|
|
1411
|
+
Dark mode is a **functional flip** under \`${DARK_SEL}\`: background and
|
|
1265
1412
|
text ends of each scale swap, while \`solid\` fills stay mode-stable with a recomputed
|
|
1266
1413
|
\`on-solid\` foreground. There is no per-appearance scheme grammar to author and no named
|
|
1267
1414
|
steps — a role token means the same *job* in both appearances.
|
|
1268
1415
|
|
|
1416
|
+
Two attributes, one flip: \`data-brx-theme\` is Bricks' own (Bricks sets it on the
|
|
1417
|
+
WordPress target), \`data-theme\` is what the shipped \`<color-scheme-toggle>\` writes on
|
|
1418
|
+
\`<html>\`, so the toggle works on every other target. Set either. There is deliberately
|
|
1419
|
+
no \`prefers-color-scheme\` block — the flip follows an explicit choice only.
|
|
1420
|
+
|
|
1269
1421
|
## Contrast guarantees
|
|
1270
1422
|
|
|
1271
1423
|
Text tokens target APCA Lc ≥ 75 and muted text Lc ≥ 60, in **both** appearances —
|
|
@@ -1342,9 +1494,34 @@ function renderPatternsConcept(ds) {
|
|
|
1342
1494
|
# Component patterns — the CSS chain
|
|
1343
1495
|
|
|
1344
1496
|
Patterns (currently ${code(patternNames)}) are authored declaratively under \`patterns\` in
|
|
1345
|
-
\`design-system.json\` (see [authoring.md](../authoring.md)) and compiled to CSS in
|
|
1346
|
-
|
|
1347
|
-
|
|
1497
|
+
\`design-system.json\` (see [authoring.md](../authoring.md)) and compiled to CSS in a
|
|
1498
|
+
**components cascade layer** — so utility classes, which live in a later layer, always win over
|
|
1499
|
+
pattern styling without specificity fights. \`class="card bg-danger-muted"\` tints the card.
|
|
1500
|
+
|
|
1501
|
+
The layer names differ by format, because each defers to its host:
|
|
1502
|
+
|
|
1503
|
+
| format | layer stack (last wins) |
|
|
1504
|
+
| --- | --- |
|
|
1505
|
+
| \`css\`, \`bricks\` | \`vitops.base\` → \`vitops.components\` → \`vitops.utilities\` |
|
|
1506
|
+
| \`tailwind\` | Tailwind's own \`theme\` → \`base\` → \`components\` → \`utilities\` |
|
|
1507
|
+
|
|
1508
|
+
**Your own CSS beats all of it.** Unlayered styles outrank every cascade layer regardless of
|
|
1509
|
+
specificity, so a plain stylesheet, an Astro scoped \`<style>\`, or a Bricks-authored class
|
|
1510
|
+
overrides the framework with no \`!important\` and no specificity escalation. That is the
|
|
1511
|
+
intended override story.
|
|
1512
|
+
|
|
1513
|
+
The one thing to watch: a **reset** must be layered too, and ordered *below* the framework —
|
|
1514
|
+
left unlayered it beats the very component rules it is meant to sit under (a bare
|
|
1515
|
+
\`p { margin: 0 }\` would defeat \`.rhythm\`). Declare the order before you load the stylesheet,
|
|
1516
|
+
since layer precedence is fixed by first declaration:
|
|
1517
|
+
|
|
1518
|
+
\`\`\`html
|
|
1519
|
+
<style>@layer my.reset, vitops.base, vitops.components, vitops.utilities;</style>
|
|
1520
|
+
<link rel="stylesheet" href="/styles.css">
|
|
1521
|
+
\`\`\`
|
|
1522
|
+
|
|
1523
|
+
Put it *after* the link and \`my.reset\` becomes a new name introduced later — which sorts
|
|
1524
|
+
**last**, i.e. highest priority, and the reset wins. That is the one non-obvious step.
|
|
1348
1525
|
|
|
1349
1526
|
## 1 — Token cascade
|
|
1350
1527
|
|
|
@@ -1505,22 +1682,6 @@ function generateDocs(ds, assetsDir) {
|
|
|
1505
1682
|
* and the pre-built JS bundles) live under `assets/` in this package and are read /
|
|
1506
1683
|
* copied from there — they are the same for every consumer.
|
|
1507
1684
|
*/
|
|
1508
|
-
/**
|
|
1509
|
-
* Selector the dark functional-token flip hangs off.
|
|
1510
|
-
*
|
|
1511
|
-
* `data-brx-theme` is Bricks' own attribute — Bricks sets it, so it's what the
|
|
1512
|
-
* WordPress target needs. Nothing sets it anywhere else, which meant the dark
|
|
1513
|
-
* flip was unreachable outside Bricks: the shipped `<color-scheme-toggle>` web
|
|
1514
|
-
* component writes `documentElement.dataset.theme` (i.e. `data-theme`), so
|
|
1515
|
-
* clicking "Dark" changed an attribute no rule matched. Matching both makes the
|
|
1516
|
-
* component work on every target without changing what Bricks already does.
|
|
1517
|
-
*
|
|
1518
|
-
* Note this covers the explicit choice only — there is deliberately no
|
|
1519
|
-
* `prefers-color-scheme` block, so the toggle's "System" position currently
|
|
1520
|
-
* resolves to light. Adding one would flip every existing consumer site dark for
|
|
1521
|
-
* dark-OS users, which is a product decision, not a bug fix.
|
|
1522
|
-
*/
|
|
1523
|
-
const DARK_SEL = ":root[data-brx-theme=\"dark\"], :root[data-theme=\"dark\"]";
|
|
1524
1685
|
const DEFAULT_ASSETS = join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
|
|
1525
1686
|
const UTILITY_PROPS = {
|
|
1526
1687
|
bg: "background-color",
|
|
@@ -1531,24 +1692,55 @@ const UTILITY_PROPS = {
|
|
|
1531
1692
|
stroke: "stroke"
|
|
1532
1693
|
};
|
|
1533
1694
|
const fnVar = (role, token) => token.startsWith("stop-") ? `--color-${role}-${token.slice(5)}` : `--${role}-${token}`;
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1695
|
+
/**
|
|
1696
|
+
* Every role colour utility, each class name emitted EXACTLY once.
|
|
1697
|
+
*
|
|
1698
|
+
* Two axes share the `<family>-<role>-<modifier>` namespace: the functional
|
|
1699
|
+
* background/text/border **planes** (`--<role>-bg-muted`) and the
|
|
1700
|
+
* appearance-relative emphasis **stops** (`--color-<role>-muted`). Where both
|
|
1701
|
+
* would produce the same class the plane wins.
|
|
1702
|
+
*
|
|
1703
|
+
* That precedence used to be implicit — the css path emitted the stop matrix,
|
|
1704
|
+
* then the planes, and relied on the CSS minifier dropping the shadowed
|
|
1705
|
+
* earlier rule. The tailwind path never ran the stop half at all, so the two
|
|
1706
|
+
* formats shipped different vocabularies (87 role classes existed in css and
|
|
1707
|
+
* not in tailwind) with nothing to catch it. Deciding it here, once, is what
|
|
1708
|
+
* keeps all three formats in step; `format-parity.test.ts` holds them there.
|
|
1709
|
+
*
|
|
1710
|
+
* Order is stops-then-planes to match the css bundle's historical rule order.
|
|
1711
|
+
*/
|
|
1712
|
+
function roleColorUtilities(roles, utilities) {
|
|
1713
|
+
const plane = (cls, prop, value) => ({
|
|
1714
|
+
cls,
|
|
1715
|
+
prop,
|
|
1716
|
+
value,
|
|
1717
|
+
axis: "plane"
|
|
1718
|
+
});
|
|
1719
|
+
const planes = [];
|
|
1720
|
+
for (const { role } of roles) {
|
|
1721
|
+
if (utilities.includes("bg")) {
|
|
1722
|
+
planes.push(plane(`bg-${role}`, "background-color", `var(--${role}-bg)`), plane(`bg-${role}-muted`, "background-color", `var(--${role}-bg-muted)`), plane(`bg-${role}-solid`, "background-color", `var(--${role}-solid)`), plane(`bg-${role}-solid-bold`, "background-color", `var(--${role}-solid-bold)`));
|
|
1723
|
+
if (role === "surface") planes.push(plane(`bg-${role}-bold`, "background-color", `var(--${role}-bg-bold)`));
|
|
1724
|
+
}
|
|
1725
|
+
if (utilities.includes("text")) planes.push(plane(`text-${role}`, "color", `var(--${role}-text)`), plane(`text-${role}-muted`, "color", `var(--${role}-text-muted)`), plane(`text-${role}-x-muted`, "color", `var(--${role}-text-x-muted)`), plane(`text-on-${role}`, "color", `var(--${role}-on-solid)`));
|
|
1726
|
+
if (utilities.includes("border")) planes.push(plane(`border-${role}`, "border-color", `var(--${role}-border)`), plane(`border-${role}-bold`, "border-color", `var(--${role}-border-bold)`));
|
|
1542
1727
|
}
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1728
|
+
const claimed = new Set(planes.map((p) => p.cls));
|
|
1729
|
+
const stops = [];
|
|
1730
|
+
for (const { role } of roles) for (const stop of EMPHASIS_STOPS) for (const fam of utilities) {
|
|
1731
|
+
const cls = `${fam}-${role}-${stop}`;
|
|
1732
|
+
if (claimed.has(cls) || !(fam in UTILITY_PROPS)) continue;
|
|
1733
|
+
stops.push({
|
|
1734
|
+
cls,
|
|
1735
|
+
prop: UTILITY_PROPS[fam],
|
|
1736
|
+
value: `var(--color-${role}-${stop})`,
|
|
1737
|
+
axis: "stop"
|
|
1738
|
+
});
|
|
1548
1739
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1740
|
+
return [...stops, ...planes];
|
|
1741
|
+
}
|
|
1742
|
+
/** Render the shared set as plain rules (css/bricks formats). */
|
|
1743
|
+
const colorUtilityRules = (u) => u.map(({ cls, prop, value }) => `.${cls} { ${prop}: ${value}; }`).join("\n");
|
|
1552
1744
|
const decls = (obj, indent = " ") => Object.entries(obj).map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
|
|
1553
1745
|
const round = (n) => Number(n.toFixed(4)).toString();
|
|
1554
1746
|
const rem = (v) => parseFloat(v);
|
|
@@ -1663,18 +1855,10 @@ function build(ds, format, assetsDir) {
|
|
|
1663
1855
|
css += `${DARK_SEL} {\n color-scheme: dark;\n${fb}}\n\n`;
|
|
1664
1856
|
}
|
|
1665
1857
|
}
|
|
1666
|
-
const allTokens = [];
|
|
1667
|
-
for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) allTokens.push(`${name}-${n}`);
|
|
1668
|
-
for (const fr of scaleRoles) for (const stop of [
|
|
1669
|
-
"x-muted",
|
|
1670
|
-
"muted",
|
|
1671
|
-
"bold",
|
|
1672
|
-
"x-bold"
|
|
1673
|
-
]) allTokens.push(`${fr.role}-${stop}`);
|
|
1674
1858
|
css += `/* Colour utilities — ${UTILITIES.join(", ")} */\n`;
|
|
1675
|
-
for (const
|
|
1676
|
-
css += `\n/*
|
|
1677
|
-
|
|
1859
|
+
for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) css += UTILITIES.map((cls) => `.${cls}-${name}-${n} { ${UTILITY_PROPS[cls]}: var(--color-${name}-${n}); }`).join("\n") + "\n";
|
|
1860
|
+
css += `\n/* Role utilities (the public API) */\n`;
|
|
1861
|
+
css += `${colorUtilityRules(roleColorUtilities(scaleRoles, UTILITIES))}\n`;
|
|
1678
1862
|
if (surfaceRole) css += `.glass { background-color: var(--surface-glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }\n`;
|
|
1679
1863
|
generated["color.css"] = css;
|
|
1680
1864
|
}
|
|
@@ -2062,7 +2246,8 @@ function build(ds, format, assetsDir) {
|
|
|
2062
2246
|
twTypeTokensCss,
|
|
2063
2247
|
twPatternTokensCss,
|
|
2064
2248
|
patternsCss: pat,
|
|
2065
|
-
assetsDir
|
|
2249
|
+
assetsDir,
|
|
2250
|
+
utilities: UTILITIES
|
|
2066
2251
|
}),
|
|
2067
2252
|
twTypeTokensCss,
|
|
2068
2253
|
twPatternTokensCss,
|
|
@@ -2070,10 +2255,10 @@ function build(ds, format, assetsDir) {
|
|
|
2070
2255
|
};
|
|
2071
2256
|
}
|
|
2072
2257
|
function emitTailwind(ctx) {
|
|
2073
|
-
const { ds, expandedPalette, scaleRoles, shadows, typeSteps, spaceSteps, typography, roleDecls, twTypeTokensCss, twPatternTokensCss, patternsCss, assetsDir } = ctx;
|
|
2258
|
+
const { ds, expandedPalette, scaleRoles, shadows, typeSteps, spaceSteps, typography, roleDecls, twTypeTokensCss, twPatternTokensCss, patternsCss, assetsDir, utilities } = ctx;
|
|
2074
2259
|
const cssDir = join(assetsDir, "css");
|
|
2075
2260
|
const nl = (arr) => arr.filter(Boolean).join("\n");
|
|
2076
|
-
const parts = [
|
|
2261
|
+
const parts = ["/* GENERATED for Tailwind v4 (Astro) — do not edit by hand.\n Class reference: npx vitops docs classes · All topics: npx vitops docs */"];
|
|
2077
2262
|
parts.push(`@import "tailwindcss";\n`);
|
|
2078
2263
|
const theme = [];
|
|
2079
2264
|
theme.push(` /* Hue scales → bg-<hue>-<step> text-* border-* (numeric, 50…950) */`);
|
|
@@ -2114,15 +2299,8 @@ function emitTailwind(ctx) {
|
|
|
2114
2299
|
parts.push("/* State variant for the active flip (hover/focus-visible are built in). */\n@custom-variant is-active (&.is-active, &[data-active]);\n");
|
|
2115
2300
|
const util = [];
|
|
2116
2301
|
if (scaleRoles.length) {
|
|
2117
|
-
util.push(`/*
|
|
2118
|
-
for (const
|
|
2119
|
-
"bg",
|
|
2120
|
-
"text",
|
|
2121
|
-
"border"
|
|
2122
|
-
], fr.role === "surface").split("\n")) {
|
|
2123
|
-
const m = /^\.([^ ]+) \{ (.*) \}$/.exec(row);
|
|
2124
|
-
if (m) util.push(`@utility ${m[1]} {\n ${m[2]}\n}`);
|
|
2125
|
-
}
|
|
2302
|
+
util.push(`/* Role colour utilities (the public API) */`);
|
|
2303
|
+
for (const { cls, prop, value } of roleColorUtilities(scaleRoles, utilities)) util.push(`@utility ${cls} {\n ${prop}: ${value};\n}`);
|
|
2126
2304
|
if (scaleRoles.some((r) => r.role === "surface")) util.push(`@utility glass {\n background-color: var(--surface-glass);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n}`);
|
|
2127
2305
|
}
|
|
2128
2306
|
util.push(`/* Typography roles */`);
|
|
@@ -2250,11 +2428,42 @@ body {
|
|
|
2250
2428
|
}
|
|
2251
2429
|
}
|
|
2252
2430
|
`);
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2431
|
+
const animPath = join(cssDir, "animation.css");
|
|
2432
|
+
const animEngine = existsSync(animPath) ? readFileSync(animPath, "utf8") : "";
|
|
2433
|
+
if (animEngine) parts.push(`/* ── Animation engine (animation.css) ── */\n@layer components {\n${animEngine}\n}`);
|
|
2255
2434
|
parts.push(`/* ── Component patterns ── */\n@layer components {\n${patternsCss}\n}`);
|
|
2256
2435
|
const droppedClashes = /* @__PURE__ */ new Set();
|
|
2257
2436
|
let droppedVariantBlocks = 0;
|
|
2437
|
+
/**
|
|
2438
|
+
* Is this `@container (min-width: …)` block a PRE-EXPANDED VARIANT block —
|
|
2439
|
+
* i.e. only `.sm-*`/`.md-*`/`.lg-*`/`.xl-*` utility classes, which Tailwind
|
|
2440
|
+
* regenerates on demand as `@md:`?
|
|
2441
|
+
*
|
|
2442
|
+
* Anything else in a container query is component *behaviour*, not a variant,
|
|
2443
|
+
* and dropping it breaks the component. `sitenav.css` is the case that caught
|
|
2444
|
+
* this: its `@container (min-width: 48rem) { .sitenav--bp-md { … } }` is what
|
|
2445
|
+
* switches the nav to its desktop layout, and blanket-dropping every
|
|
2446
|
+
* container block meant the tailwind format shipped a nav stuck in mobile.
|
|
2447
|
+
*/
|
|
2448
|
+
const isVariantBlock = (raw) => {
|
|
2449
|
+
const open = raw.indexOf("{");
|
|
2450
|
+
const body = raw.slice(open + 1, raw.lastIndexOf("}"));
|
|
2451
|
+
const selectors = [];
|
|
2452
|
+
let depth = 0;
|
|
2453
|
+
let start = 0;
|
|
2454
|
+
for (let i = 0; i < body.length; i++) {
|
|
2455
|
+
const ch = body[i];
|
|
2456
|
+
if (ch === "{") {
|
|
2457
|
+
if (depth === 0) selectors.push(body.slice(start, i));
|
|
2458
|
+
depth++;
|
|
2459
|
+
} else if (ch === "}") {
|
|
2460
|
+
depth--;
|
|
2461
|
+
if (depth === 0) start = i + 1;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
const names = selectors.flatMap((s) => s.split(",")).map((s) => s.replace(/\/\*[\s\S]*?\*\//g, "").trim()).filter(Boolean);
|
|
2465
|
+
return names.length > 0 && names.every((n) => /^\.(sm|md|lg|xl)-/.test(n));
|
|
2466
|
+
};
|
|
2258
2467
|
const stripForTailwind = (css) => {
|
|
2259
2468
|
const kept = [];
|
|
2260
2469
|
let depth = 0;
|
|
@@ -2281,7 +2490,7 @@ body {
|
|
|
2281
2490
|
const raw = css.slice(start, i + 1).trim();
|
|
2282
2491
|
const prelude = css.slice(start, preludeEnd).replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
2283
2492
|
const clash = /^\.([A-Za-z0-9_-]+)/.exec(prelude);
|
|
2284
|
-
if (/^@container\s*\(\s*min-width:/.test(prelude)) droppedVariantBlocks++;
|
|
2493
|
+
if (/^@container\s*\(\s*min-width:/.test(prelude) && isVariantBlock(raw)) droppedVariantBlocks++;
|
|
2285
2494
|
else if (clash && TW_CLASH.has(clash[1])) droppedClashes.add(clash[1]);
|
|
2286
2495
|
else if (raw) kept.push(raw);
|
|
2287
2496
|
start = i + 1;
|
|
@@ -2293,7 +2502,7 @@ body {
|
|
|
2293
2502
|
const componentCss = componentPartialOrder(cssDir).map((rel) => {
|
|
2294
2503
|
return `/* ── ${rel} ── */\n${stripForTailwind(readFileSync(join(cssDir, rel), "utf8"))}`;
|
|
2295
2504
|
}).join("\n\n");
|
|
2296
|
-
parts.push(`/* ── Component + structural partials (Tailwind-clashing utilities dropped) ── */\n@layer components {\n${componentCss}\n}`);
|
|
2505
|
+
if (componentCss) parts.push(`/* ── Component + structural partials (Tailwind-clashing utilities dropped) ── */\n/* Dropped ${droppedVariantBlocks} pre-expanded breakpoint block(s) — write @sm:/@md:/@lg:/@xl: instead.\n Dropped ${droppedClashes.size} clashing utilit(ies) Tailwind provides itself:\n ${[...droppedClashes].sort().join(" ") || "(none)"} */\n@layer components {\n${componentCss}\n}`);
|
|
2297
2506
|
return nl(parts) + "\n";
|
|
2298
2507
|
}
|
|
2299
2508
|
/**
|
|
@@ -2330,11 +2539,61 @@ function componentPartialOrder(cssDir) {
|
|
|
2330
2539
|
}
|
|
2331
2540
|
return order;
|
|
2332
2541
|
}
|
|
2542
|
+
/** The bundle's cascade layers, in precedence order (last wins). */
|
|
2543
|
+
const CSS_LAYERS = [
|
|
2544
|
+
"vitops.base",
|
|
2545
|
+
"vitops.components",
|
|
2546
|
+
"vitops.utilities"
|
|
2547
|
+
];
|
|
2548
|
+
/**
|
|
2549
|
+
* Which layer each `index.css` chunk belongs to. Anything unmapped defaults to
|
|
2550
|
+
* `vitops.components` — the safe default, since an unrecognised partial is far
|
|
2551
|
+
* more likely to be a component than a utility family.
|
|
2552
|
+
*
|
|
2553
|
+
* The split is by what the file *emits*, not what it's named:
|
|
2554
|
+
* • base — the UA reset and the pure `:root` token blocks.
|
|
2555
|
+
* • components — the animation engine, structural layout, and every pattern.
|
|
2556
|
+
* • utilities — the single-purpose classes that must be able to override a
|
|
2557
|
+
* pattern (`bg-*`, `drop-shadow-*`, `font-*`, effects, …).
|
|
2558
|
+
*
|
|
2559
|
+
* `color.css` and `shadows.css` each mix a `:root` block with utility classes
|
|
2560
|
+
* and go to `utilities` whole. That is safe because custom-property resolution
|
|
2561
|
+
* is NOT source-order dependent — a `:root` block declared after a rule that
|
|
2562
|
+
* reads it still resolves.
|
|
2563
|
+
*
|
|
2564
|
+
* `layout.css` is genuinely mixed (structural `.rhythm`/`.centered` AND
|
|
2565
|
+
* `.m-*`/`.flex`/`.split-*` utilities) and stays in `components` whole, so its
|
|
2566
|
+
* utility half still can't override a pattern. Splitting it is a separate
|
|
2567
|
+
* change; see the 0.9.0 changelog.
|
|
2568
|
+
*/
|
|
2569
|
+
const CHUNK_LAYER = {
|
|
2570
|
+
"global.css": "vitops.base",
|
|
2571
|
+
"generated/type-tokens.css": "vitops.base",
|
|
2572
|
+
"generated/tokens.css": "vitops.base",
|
|
2573
|
+
"generated/color.css": "vitops.utilities",
|
|
2574
|
+
"generated/shadows.css": "vitops.utilities",
|
|
2575
|
+
"generated/typography.css": "vitops.utilities",
|
|
2576
|
+
"generated/animation-effects.css": "vitops.utilities",
|
|
2577
|
+
"utilities.css": "vitops.utilities"
|
|
2578
|
+
};
|
|
2333
2579
|
/**
|
|
2334
2580
|
* Assemble the full stylesheet by resolving index.css's @import order against the
|
|
2335
2581
|
* static partials (from assets) and the freshly generated token partials (in
|
|
2336
2582
|
* memory), then minify with lightningcss. No @import survives, so no disk
|
|
2337
2583
|
* resolution / shared `generated/` dir is needed.
|
|
2584
|
+
*
|
|
2585
|
+
* Each chunk is wrapped in a cascade layer. This is what lets a single-purpose
|
|
2586
|
+
* utility override a component pattern — `class="card bg-danger-muted"` — which
|
|
2587
|
+
* previously depended on source order and so silently did nothing here while
|
|
2588
|
+
* working in the tailwind format, where Tailwind's own layers already ordered
|
|
2589
|
+
* them correctly.
|
|
2590
|
+
*
|
|
2591
|
+
* Consequence worth knowing: unlayered CSS beats ALL layered CSS regardless of
|
|
2592
|
+
* specificity, so a consumer's own stylesheet now wins over the framework by
|
|
2593
|
+
* default. That is deliberate — it is the override story every layered
|
|
2594
|
+
* framework ships — but it means an unlayered *reset* will beat framework
|
|
2595
|
+
* component rules it used to lose to. Such a reset belongs in a layer declared
|
|
2596
|
+
* before `vitops.base` (see the `<style>` block in this repo's index.html).
|
|
2338
2597
|
*/
|
|
2339
2598
|
function bundleCss(generated, assetsDir, minify) {
|
|
2340
2599
|
const cssDir = join(assetsDir, "css");
|
|
@@ -2344,12 +2603,13 @@ function bundleCss(generated, assetsDir, minify) {
|
|
|
2344
2603
|
const m = /@import\s+['"]\.\/([^'"]+)['"]/.exec(line);
|
|
2345
2604
|
if (!m) continue;
|
|
2346
2605
|
const rel = m[1];
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
} else chunks.push(readFileSync(join(cssDir, rel), "utf8"));
|
|
2606
|
+
const body = rel.startsWith("generated/") ? generated[rel.slice(10)] ?? "" : readFileSync(join(cssDir, rel), "utf8");
|
|
2607
|
+
if (!body.trim()) continue;
|
|
2608
|
+
chunks.push(`@layer ${CHUNK_LAYER[rel] ?? "vitops.components"} {\n${body}\n}`);
|
|
2351
2609
|
}
|
|
2352
|
-
const merged =
|
|
2610
|
+
const merged = `/*! GENERATED by @getvitops/generator — do not edit by hand.
|
|
2611
|
+
Class reference: npx vitops docs classes · All topics: npx vitops docs */
|
|
2612
|
+
@layer ${CSS_LAYERS.join(", ")};\n` + chunks.join("\n");
|
|
2353
2613
|
const res = transform({
|
|
2354
2614
|
filename: "styles.css",
|
|
2355
2615
|
code: Buffer.from(merged),
|
|
@@ -3113,15 +3373,14 @@ function defaultConfig() {
|
|
|
3113
3373
|
patterns: {
|
|
3114
3374
|
defaults: {
|
|
3115
3375
|
ds: "none",
|
|
3116
|
-
b: "1px solid var(--
|
|
3376
|
+
b: "1px solid var(--surface-bg-muted)",
|
|
3117
3377
|
br: "0.375rem",
|
|
3118
3378
|
p: "1rem",
|
|
3119
3379
|
fs: "1rem"
|
|
3120
3380
|
},
|
|
3121
3381
|
radii: {
|
|
3122
3382
|
pill: "999px",
|
|
3123
|
-
circle: "50%"
|
|
3124
|
-
card: "0.5rem"
|
|
3383
|
+
circle: "50%"
|
|
3125
3384
|
},
|
|
3126
3385
|
groups: {
|
|
3127
3386
|
control: {
|
|
@@ -3224,7 +3483,7 @@ function defaultConfig() {
|
|
|
3224
3483
|
base: {
|
|
3225
3484
|
"border-radius": "var(--br-card-group)",
|
|
3226
3485
|
padding: "1.5rem",
|
|
3227
|
-
background: "var(--
|
|
3486
|
+
background: "var(--surface-bg)",
|
|
3228
3487
|
border: "var(--b-card-group)",
|
|
3229
3488
|
"box-shadow": "var(--ds-card-group)"
|
|
3230
3489
|
},
|
|
@@ -3293,5 +3552,5 @@ function defaultConfig() {
|
|
|
3293
3552
|
}
|
|
3294
3553
|
|
|
3295
3554
|
//#endregion
|
|
3296
|
-
export { BASE_HOOK, DesignSystemSchema, SCHEMA_URL, SITE_SCHEMA_URL, SiteConfigSchema, TW_CLASH, deepMerge, defaultConfig, generate, generateDocs, jsonSchema, resolveSiteConfig, resolveTheme, siteJsonSchema, stripNulls, validate, validateSite };
|
|
3555
|
+
export { BASE_HOOK, DARK_SEL, DesignSystemSchema, REQUIRED_ROLES, ROLE_TOKEN_SUFFIXES, SCHEMA_URL, SITE_SCHEMA_URL, SiteConfigSchema, TW_CLASH, deepMerge, defaultConfig, expandPalette, functionalRole, generate, generateDocs, jsonSchema, resolveSiteConfig, resolveTheme, roleColorUtilities, siteJsonSchema, stripNulls, validate, validateSite };
|
|
3297
3556
|
//# sourceMappingURL=index.mjs.map
|