@getvitops/generator 0.7.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 +233 -0
- package/assets/css/animation.css +12 -2
- package/assets/css/global.css +10 -0
- package/assets/css/index.css +1 -0
- package/assets/css/patterns/details.css +27 -8
- package/assets/css/patterns/drawer.css +15 -0
- package/assets/css/patterns/forms.css +31 -0
- package/assets/css/patterns/grouped.css +1 -1
- package/assets/css/patterns/popover.css +18 -1
- package/assets/css/patterns/subgrid.css +62 -16
- package/assets/css/patterns/theme-editor.css +122 -0
- package/assets/css/utilities.css +5 -3
- package/assets/js/editor.js +2 -0
- package/assets/js/elements.js +2 -2
- package/dist/index.d.mts +105 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +464 -164
- 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({
|
|
@@ -122,13 +265,20 @@ const jsonSchema = {
|
|
|
122
265
|
$id: SCHEMA_URL,
|
|
123
266
|
...z.toJSONSchema(DesignSystemSchema, { target: "draft-2020-12" })
|
|
124
267
|
};
|
|
125
|
-
/**
|
|
268
|
+
/**
|
|
269
|
+
* Validate an unknown value against the design-system schema.
|
|
270
|
+
*
|
|
271
|
+
* `warnings` are configurations that parse and generate, but produce output that
|
|
272
|
+
* won't behave as authored — currently only the token-namespace collisions the
|
|
273
|
+
* flat `--<prop>-<name>` grammar makes possible.
|
|
274
|
+
*/
|
|
126
275
|
function validate(input) {
|
|
127
276
|
const result = z.safeParse(DesignSystemSchema, input);
|
|
128
277
|
if (!result.success) return {
|
|
129
278
|
ok: false,
|
|
130
279
|
data: void 0,
|
|
131
|
-
errors: result.error.issues
|
|
280
|
+
errors: result.error.issues,
|
|
281
|
+
warnings: []
|
|
132
282
|
};
|
|
133
283
|
const { palette, roles } = result.data.colors;
|
|
134
284
|
for (const [role, hue] of Object.entries(roles)) if (palette[hue] == null) return {
|
|
@@ -142,99 +292,23 @@ function validate(input) {
|
|
|
142
292
|
role
|
|
143
293
|
],
|
|
144
294
|
message: `role "${role}" references unknown palette hue "${hue}"`
|
|
145
|
-
}]
|
|
295
|
+
}],
|
|
296
|
+
warnings: []
|
|
146
297
|
};
|
|
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.`);
|
|
301
|
+
const radii = Object.keys(result.data.patterns?.radii ?? {});
|
|
302
|
+
const items = new Set(Object.keys(result.data.patterns?.items ?? {}));
|
|
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`);
|
|
147
304
|
return {
|
|
148
305
|
ok: true,
|
|
149
306
|
data: result.data,
|
|
150
|
-
errors: []
|
|
307
|
+
errors: [],
|
|
308
|
+
warnings
|
|
151
309
|
};
|
|
152
310
|
}
|
|
153
311
|
|
|
154
|
-
//#endregion
|
|
155
|
-
//#region src/shared.ts
|
|
156
|
-
/**
|
|
157
|
-
* Data shared between the generator (`generate.ts`) and the docs bundle
|
|
158
|
-
* (`docs.ts`). Lives in its own module because `generate.ts` imports `docs.ts` —
|
|
159
|
-
* importing back the other way would be a cycle.
|
|
160
|
-
*/
|
|
161
|
-
/**
|
|
162
|
-
* Pattern base geometry props → per-pattern override-hook shorthand. Each hook
|
|
163
|
-
* wraps the authored value (`padding: var(--p-btn, 0.4em 0.8em)`) so a
|
|
164
|
-
* consumer restyles one pattern by setting one variable. The hook is named after
|
|
165
|
-
* the pattern's key, not its class.
|
|
166
|
-
*/
|
|
167
|
-
const BASE_HOOK = {
|
|
168
|
-
padding: "p",
|
|
169
|
-
"border-radius": "br",
|
|
170
|
-
border: "b",
|
|
171
|
-
"box-shadow": "ds",
|
|
172
|
-
"font-size": "fs"
|
|
173
|
-
};
|
|
174
|
-
/**
|
|
175
|
-
* Framework utility classes whose names collide with Tailwind's own defaults.
|
|
176
|
-
* The `tailwind` format strips any rule targeting these — Tailwind provides
|
|
177
|
-
* them, so consumers use Tailwind's utilities for these names instead.
|
|
178
|
-
*/
|
|
179
|
-
const TW_CLASH = new Set([
|
|
180
|
-
"block",
|
|
181
|
-
"inline",
|
|
182
|
-
"inline-block",
|
|
183
|
-
"flow-root",
|
|
184
|
-
"flex",
|
|
185
|
-
"inline-flex",
|
|
186
|
-
"grid",
|
|
187
|
-
"inline-grid",
|
|
188
|
-
"table",
|
|
189
|
-
"inline-table",
|
|
190
|
-
"table-caption",
|
|
191
|
-
"table-cell",
|
|
192
|
-
"table-row",
|
|
193
|
-
"contents",
|
|
194
|
-
"hidden",
|
|
195
|
-
"list-item",
|
|
196
|
-
"static",
|
|
197
|
-
"fixed",
|
|
198
|
-
"absolute",
|
|
199
|
-
"relative",
|
|
200
|
-
"sticky",
|
|
201
|
-
"isolate",
|
|
202
|
-
"visible",
|
|
203
|
-
"invisible",
|
|
204
|
-
"collapse",
|
|
205
|
-
"sr-only",
|
|
206
|
-
"not-sr-only",
|
|
207
|
-
"flex-row",
|
|
208
|
-
"flex-row-reverse",
|
|
209
|
-
"flex-col",
|
|
210
|
-
"flex-col-reverse",
|
|
211
|
-
"flex-wrap",
|
|
212
|
-
"flex-nowrap",
|
|
213
|
-
"items-start",
|
|
214
|
-
"items-end",
|
|
215
|
-
"items-center",
|
|
216
|
-
"items-baseline",
|
|
217
|
-
"items-stretch",
|
|
218
|
-
"justify-start",
|
|
219
|
-
"justify-end",
|
|
220
|
-
"justify-center",
|
|
221
|
-
"justify-between",
|
|
222
|
-
"justify-around",
|
|
223
|
-
"justify-evenly",
|
|
224
|
-
"content-start",
|
|
225
|
-
"content-end",
|
|
226
|
-
"content-center",
|
|
227
|
-
"content-between",
|
|
228
|
-
"content-around",
|
|
229
|
-
"content-evenly",
|
|
230
|
-
"text-left",
|
|
231
|
-
"text-center",
|
|
232
|
-
"text-right",
|
|
233
|
-
"text-justify",
|
|
234
|
-
"text-start",
|
|
235
|
-
"text-end"
|
|
236
|
-
]);
|
|
237
|
-
|
|
238
312
|
//#endregion
|
|
239
313
|
//#region src/tokens.ts
|
|
240
314
|
/**
|
|
@@ -373,6 +447,11 @@ const EMPHASIS_DARK = {
|
|
|
373
447
|
bold: "300",
|
|
374
448
|
"x-bold": "100"
|
|
375
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);
|
|
376
455
|
/** Resolve the functional token set for one role over its hue. */
|
|
377
456
|
function functionalRole(role, hueName, hue) {
|
|
378
457
|
const numeric = hue.numeric;
|
|
@@ -968,9 +1047,17 @@ Families: ${code(families)} (\`--font-*\`).
|
|
|
968
1047
|
## Colour
|
|
969
1048
|
|
|
970
1049
|
**Functional tokens are the primary vocabulary** — classes name the *job*, not the tone, and
|
|
971
|
-
every one remaps automatically under
|
|
1050
|
+
every one remaps automatically under \`${DARK_SEL}\` (background/text ends
|
|
972
1051
|
swap; \`solid\` fills stay mode-stable with a computed \`on-\` foreground). Prefer these over
|
|
973
|
-
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):
|
|
974
1061
|
|
|
975
1062
|
- **Surfaces** — \`bg-<role>\` (the role's background wash; for \`surface\` this is the
|
|
976
1063
|
card/panel plane), \`bg-<role>-muted\` (subtle / sunken), \`bg-surface-bold\` (raised plane).
|
|
@@ -984,9 +1071,10 @@ raw steps. Rules (role ∈ ${code(roles)}):
|
|
|
984
1071
|
\`--color-<role>-{x-muted,muted,bold,x-bold}\` — \`muted\` recedes toward the background
|
|
985
1072
|
extreme, \`bold\` advances toward the foreground, in *either* appearance.
|
|
986
1073
|
|
|
987
|
-
Everyday pairings
|
|
988
|
-
\`
|
|
989
|
-
\`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"}\`.
|
|
990
1078
|
|
|
991
1079
|
**Raw scale** (secondary / fine control) — rule \`<util>-<hue>-<step>\` with util ∈
|
|
992
1080
|
${code(utils)}: every hue is an 11-step OKLCH scale generated from its seed (or fixed brand
|
|
@@ -994,6 +1082,28 @@ tones), numeric steps \`50\` … \`950\` (tinted near-white → tinted near-blac
|
|
|
994
1082
|
\`bg-${ramps[0] ?? "brand"}-100\`, \`text-${ramps[0] ?? "brand"}-800\`. Hues: ${code(ramps)}.
|
|
995
1083
|
The **bare** role name (\`bg-<role>\`, \`text-<role>\`) is always the functional token.
|
|
996
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
|
+
|
|
997
1107
|
## Shadows
|
|
998
1108
|
|
|
999
1109
|
Rule: **\`drop-shadow-<size>\`** — size ∈ ${code(shadows)} (applied as a \`filter\`, so it
|
|
@@ -1155,9 +1265,27 @@ ${clashList.map((c) => `\`${c}\``).join(", ")}
|
|
|
1155
1265
|
Other Tailwind-specific behaviour:
|
|
1156
1266
|
|
|
1157
1267
|
- **Variants are Tailwind's job.** No pre-expanded breakpoint/state classes are emitted
|
|
1158
|
-
(the framework's \`@container (min-width: …)\` variant blocks are dropped
|
|
1159
|
-
|
|
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
|
|
1160
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.
|
|
1161
1289
|
- **The space scale is NOT mapped into Tailwind's \`--spacing-*\` namespace** (that would
|
|
1162
1290
|
corrupt Tailwind's numeric multipliers and \`max-w-*\` sizes). Numeric utilities like
|
|
1163
1291
|
\`p-4\` keep Tailwind's 0.25rem meaning; use the design-system scale via arbitrary
|
|
@@ -1167,6 +1295,18 @@ Other Tailwind-specific behaviour:
|
|
|
1167
1295
|
(\`bg-<role>\`, \`text-<role>-muted\`, …) is the public API. The raw hue scales ARE
|
|
1168
1296
|
\`@theme\` colours, so native \`bg-<hue>-500\`-style utilities work.
|
|
1169
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
|
+
|
|
1170
1310
|
## \`css\` — standalone bundle
|
|
1171
1311
|
|
|
1172
1312
|
Emits a bundled, self-contained \`styles.css\` + \`tokens.json\` + \`design-manifest.json\`.
|
|
@@ -1175,6 +1315,26 @@ pre-expanded — including breakpoint/state variants with \`-\` separators
|
|
|
1175
1315
|
(\`md-split-1-2\`, \`hover-fade-in\`). For non-Bricks, non-Tailwind consumers and the
|
|
1176
1316
|
docs build.
|
|
1177
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
|
+
|
|
1178
1338
|
## \`bricks\` — WordPress / Bricks Builder payload
|
|
1179
1339
|
|
|
1180
1340
|
Emits the full deployable theme payload: \`styles.min.css\`, the Bricks import JSONs
|
|
@@ -1248,11 +1408,16 @@ Classes name the token, not the tone: \`bg-<role>\`, \`text-<role>-muted\`, \`te
|
|
|
1248
1408
|
|
|
1249
1409
|
## Automatic dark mode
|
|
1250
1410
|
|
|
1251
|
-
Dark mode is a **functional flip** under \`:
|
|
1411
|
+
Dark mode is a **functional flip** under \`${DARK_SEL}\`: background and
|
|
1252
1412
|
text ends of each scale swap, while \`solid\` fills stay mode-stable with a recomputed
|
|
1253
1413
|
\`on-solid\` foreground. There is no per-appearance scheme grammar to author and no named
|
|
1254
1414
|
steps — a role token means the same *job* in both appearances.
|
|
1255
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
|
+
|
|
1256
1421
|
## Contrast guarantees
|
|
1257
1422
|
|
|
1258
1423
|
Text tokens target APCA Lc ≥ 75 and muted text Lc ≥ 60, in **both** appearances —
|
|
@@ -1329,9 +1494,34 @@ function renderPatternsConcept(ds) {
|
|
|
1329
1494
|
# Component patterns — the CSS chain
|
|
1330
1495
|
|
|
1331
1496
|
Patterns (currently ${code(patternNames)}) are authored declaratively under \`patterns\` in
|
|
1332
|
-
\`design-system.json\` (see [authoring.md](../authoring.md)) and compiled to CSS in
|
|
1333
|
-
|
|
1334
|
-
|
|
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.
|
|
1335
1525
|
|
|
1336
1526
|
## 1 — Token cascade
|
|
1337
1527
|
|
|
@@ -1502,24 +1692,55 @@ const UTILITY_PROPS = {
|
|
|
1502
1692
|
stroke: "stroke"
|
|
1503
1693
|
};
|
|
1504
1694
|
const fnVar = (role, token) => token.startsWith("stop-") ? `--color-${role}-${token.slice(5)}` : `--${role}-${token}`;
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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)`));
|
|
1513
1727
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
+
});
|
|
1519
1739
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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");
|
|
1523
1744
|
const decls = (obj, indent = " ") => Object.entries(obj).map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
|
|
1524
1745
|
const round = (n) => Number(n.toFixed(4)).toString();
|
|
1525
1746
|
const rem = (v) => parseFloat(v);
|
|
@@ -1631,21 +1852,13 @@ function build(ds, format, assetsDir) {
|
|
|
1631
1852
|
if (surfaceRole) fb += ` --overlay: color-mix(in oklch, var(--color-${surfaceRole.hue}-950) 60%, transparent);\n`;
|
|
1632
1853
|
if (fb) {
|
|
1633
1854
|
css += `/* Functional role tokens (dark) */\n`;
|
|
1634
|
-
css +=
|
|
1855
|
+
css += `${DARK_SEL} {\n color-scheme: dark;\n${fb}}\n\n`;
|
|
1635
1856
|
}
|
|
1636
1857
|
}
|
|
1637
|
-
const allTokens = [];
|
|
1638
|
-
for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) allTokens.push(`${name}-${n}`);
|
|
1639
|
-
for (const fr of scaleRoles) for (const stop of [
|
|
1640
|
-
"x-muted",
|
|
1641
|
-
"muted",
|
|
1642
|
-
"bold",
|
|
1643
|
-
"x-bold"
|
|
1644
|
-
]) allTokens.push(`${fr.role}-${stop}`);
|
|
1645
1858
|
css += `/* Colour utilities — ${UTILITIES.join(", ")} */\n`;
|
|
1646
|
-
for (const
|
|
1647
|
-
css += `\n/*
|
|
1648
|
-
|
|
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`;
|
|
1649
1862
|
if (surfaceRole) css += `.glass { background-color: var(--surface-glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }\n`;
|
|
1650
1863
|
generated["color.css"] = css;
|
|
1651
1864
|
}
|
|
@@ -1936,11 +2149,25 @@ function build(ds, format, assetsDir) {
|
|
|
1936
2149
|
}, null, 2),
|
|
1937
2150
|
designManifest: (() => {
|
|
1938
2151
|
const reverseIndex = {};
|
|
1939
|
-
for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) reverseIndex[`--color-${name}-${n}`] = `colors.palette.${name}.
|
|
2152
|
+
for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) reverseIndex[`--color-${name}-${n}`] = `colors.palette.${name}.anchors.${n}`;
|
|
1940
2153
|
const colorRoles = Object.entries(roleMap).map(([name, ramp]) => ({
|
|
1941
2154
|
name,
|
|
1942
2155
|
ramp
|
|
1943
2156
|
}));
|
|
2157
|
+
const roleTokens = Object.fromEntries(Object.entries(expandedPalette).map(([hueName, hue]) => {
|
|
2158
|
+
const def = functionalRole("_", hueName, hue);
|
|
2159
|
+
const sfc = functionalRole("surface", hueName, hue);
|
|
2160
|
+
return [hueName, {
|
|
2161
|
+
default: {
|
|
2162
|
+
light: def.light,
|
|
2163
|
+
dark: def.dark
|
|
2164
|
+
},
|
|
2165
|
+
surface: {
|
|
2166
|
+
light: sfc.light,
|
|
2167
|
+
dark: sfc.dark
|
|
2168
|
+
}
|
|
2169
|
+
}];
|
|
2170
|
+
}));
|
|
1944
2171
|
const typoRoles = typography?.roles ?? {};
|
|
1945
2172
|
const hooks = {};
|
|
1946
2173
|
for (const [key, [prop, sfx]] of Object.entries(TYPO_KEYMAP)) hooks[sfx] = {
|
|
@@ -1953,7 +2180,6 @@ function build(ds, format, assetsDir) {
|
|
|
1953
2180
|
const pGroups = patterns?.groups ?? {};
|
|
1954
2181
|
const pItems = patterns?.items ?? {};
|
|
1955
2182
|
for (const prop of Object.keys(pDefaults)) reverseIndex[`--${prop}-default`] = `patterns.defaults.${prop}`;
|
|
1956
|
-
for (const name of Object.keys(pRadii)) reverseIndex[`--br-${name}`] = `patterns.radii.${name}`;
|
|
1957
2183
|
for (const [group, props] of Object.entries(pGroups)) for (const prop of Object.keys(props)) reverseIndex[`--${prop}-${group}`] = `patterns.groups.${group}.${prop}`;
|
|
1958
2184
|
const items = Object.entries(pItems).map(([name, p]) => {
|
|
1959
2185
|
const base = p.base ?? {};
|
|
@@ -1970,12 +2196,14 @@ function build(ds, format, assetsDir) {
|
|
|
1970
2196
|
roles: p.roles ?? []
|
|
1971
2197
|
};
|
|
1972
2198
|
});
|
|
2199
|
+
for (const name of Object.keys(pRadii)) if (!(`--br-${name}` in reverseIndex)) reverseIndex[`--br-${name}`] = `patterns.radii.${name}`;
|
|
1973
2200
|
return JSON.stringify({
|
|
1974
2201
|
colors: {
|
|
1975
2202
|
ramps: Object.keys(expandedPalette),
|
|
1976
2203
|
steps: NUMERIC_STEPS,
|
|
1977
2204
|
palette: numericPalette,
|
|
1978
|
-
roles: colorRoles
|
|
2205
|
+
roles: colorRoles,
|
|
2206
|
+
roleTokens
|
|
1979
2207
|
},
|
|
1980
2208
|
typography: {
|
|
1981
2209
|
roles: Object.keys(typoRoles),
|
|
@@ -2003,10 +2231,6 @@ function build(ds, format, assetsDir) {
|
|
|
2003
2231
|
items
|
|
2004
2232
|
},
|
|
2005
2233
|
shadows: shadows ?? {},
|
|
2006
|
-
interaction: {
|
|
2007
|
-
duration: "200ms",
|
|
2008
|
-
easing: "ease"
|
|
2009
|
-
},
|
|
2010
2234
|
reverseIndex
|
|
2011
2235
|
}, null, 2);
|
|
2012
2236
|
})(),
|
|
@@ -2022,7 +2246,8 @@ function build(ds, format, assetsDir) {
|
|
|
2022
2246
|
twTypeTokensCss,
|
|
2023
2247
|
twPatternTokensCss,
|
|
2024
2248
|
patternsCss: pat,
|
|
2025
|
-
assetsDir
|
|
2249
|
+
assetsDir,
|
|
2250
|
+
utilities: UTILITIES
|
|
2026
2251
|
}),
|
|
2027
2252
|
twTypeTokensCss,
|
|
2028
2253
|
twPatternTokensCss,
|
|
@@ -2030,10 +2255,10 @@ function build(ds, format, assetsDir) {
|
|
|
2030
2255
|
};
|
|
2031
2256
|
}
|
|
2032
2257
|
function emitTailwind(ctx) {
|
|
2033
|
-
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;
|
|
2034
2259
|
const cssDir = join(assetsDir, "css");
|
|
2035
2260
|
const nl = (arr) => arr.filter(Boolean).join("\n");
|
|
2036
|
-
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 */"];
|
|
2037
2262
|
parts.push(`@import "tailwindcss";\n`);
|
|
2038
2263
|
const theme = [];
|
|
2039
2264
|
theme.push(` /* Hue scales → bg-<hue>-<step> text-* border-* (numeric, 50…950) */`);
|
|
@@ -2070,19 +2295,12 @@ function emitTailwind(ctx) {
|
|
|
2070
2295
|
const surface = scaleRoles.find((r) => r.role === "surface");
|
|
2071
2296
|
if (surface) darkBlock += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 60%, transparent);\n`;
|
|
2072
2297
|
}
|
|
2073
|
-
if (darkBlock) parts.push(`/* Functional role tokens (dark) */\n
|
|
2298
|
+
if (darkBlock) parts.push(`/* Functional role tokens (dark) */\n${DARK_SEL} {\n color-scheme: dark;\n${darkBlock}}\n`);
|
|
2074
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");
|
|
2075
2300
|
const util = [];
|
|
2076
2301
|
if (scaleRoles.length) {
|
|
2077
|
-
util.push(`/*
|
|
2078
|
-
for (const
|
|
2079
|
-
"bg",
|
|
2080
|
-
"text",
|
|
2081
|
-
"border"
|
|
2082
|
-
], fr.role === "surface").split("\n")) {
|
|
2083
|
-
const m = /^\.([^ ]+) \{ (.*) \}$/.exec(row);
|
|
2084
|
-
if (m) util.push(`@utility ${m[1]} {\n ${m[2]}\n}`);
|
|
2085
|
-
}
|
|
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}`);
|
|
2086
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}`);
|
|
2087
2305
|
}
|
|
2088
2306
|
util.push(`/* Typography roles */`);
|
|
@@ -2210,11 +2428,42 @@ body {
|
|
|
2210
2428
|
}
|
|
2211
2429
|
}
|
|
2212
2430
|
`);
|
|
2213
|
-
const
|
|
2214
|
-
|
|
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}`);
|
|
2215
2434
|
parts.push(`/* ── Component patterns ── */\n@layer components {\n${patternsCss}\n}`);
|
|
2216
2435
|
const droppedClashes = /* @__PURE__ */ new Set();
|
|
2217
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
|
+
};
|
|
2218
2467
|
const stripForTailwind = (css) => {
|
|
2219
2468
|
const kept = [];
|
|
2220
2469
|
let depth = 0;
|
|
@@ -2241,7 +2490,7 @@ body {
|
|
|
2241
2490
|
const raw = css.slice(start, i + 1).trim();
|
|
2242
2491
|
const prelude = css.slice(start, preludeEnd).replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
2243
2492
|
const clash = /^\.([A-Za-z0-9_-]+)/.exec(prelude);
|
|
2244
|
-
if (/^@container\s*\(\s*min-width:/.test(prelude)) droppedVariantBlocks++;
|
|
2493
|
+
if (/^@container\s*\(\s*min-width:/.test(prelude) && isVariantBlock(raw)) droppedVariantBlocks++;
|
|
2245
2494
|
else if (clash && TW_CLASH.has(clash[1])) droppedClashes.add(clash[1]);
|
|
2246
2495
|
else if (raw) kept.push(raw);
|
|
2247
2496
|
start = i + 1;
|
|
@@ -2253,7 +2502,7 @@ body {
|
|
|
2253
2502
|
const componentCss = componentPartialOrder(cssDir).map((rel) => {
|
|
2254
2503
|
return `/* ── ${rel} ── */\n${stripForTailwind(readFileSync(join(cssDir, rel), "utf8"))}`;
|
|
2255
2504
|
}).join("\n\n");
|
|
2256
|
-
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}`);
|
|
2257
2506
|
return nl(parts) + "\n";
|
|
2258
2507
|
}
|
|
2259
2508
|
/**
|
|
@@ -2290,11 +2539,61 @@ function componentPartialOrder(cssDir) {
|
|
|
2290
2539
|
}
|
|
2291
2540
|
return order;
|
|
2292
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
|
+
};
|
|
2293
2579
|
/**
|
|
2294
2580
|
* Assemble the full stylesheet by resolving index.css's @import order against the
|
|
2295
2581
|
* static partials (from assets) and the freshly generated token partials (in
|
|
2296
2582
|
* memory), then minify with lightningcss. No @import survives, so no disk
|
|
2297
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).
|
|
2298
2597
|
*/
|
|
2299
2598
|
function bundleCss(generated, assetsDir, minify) {
|
|
2300
2599
|
const cssDir = join(assetsDir, "css");
|
|
@@ -2304,12 +2603,13 @@ function bundleCss(generated, assetsDir, minify) {
|
|
|
2304
2603
|
const m = /@import\s+['"]\.\/([^'"]+)['"]/.exec(line);
|
|
2305
2604
|
if (!m) continue;
|
|
2306
2605
|
const rel = m[1];
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
} 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}`);
|
|
2311
2609
|
}
|
|
2312
|
-
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");
|
|
2313
2613
|
const res = transform({
|
|
2314
2614
|
filename: "styles.css",
|
|
2315
2615
|
code: Buffer.from(merged),
|
|
@@ -2327,6 +2627,7 @@ function loadConfig(input) {
|
|
|
2327
2627
|
const lines = result.errors.slice(0, 12).map((e) => ` • ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
|
|
2328
2628
|
throw new Error(`Invalid design-system.json:\n${lines}`);
|
|
2329
2629
|
}
|
|
2630
|
+
for (const w of result.warnings) console.warn(`[vitops] ${w}`);
|
|
2330
2631
|
return result.data;
|
|
2331
2632
|
}
|
|
2332
2633
|
async function generate(options) {
|
|
@@ -3072,15 +3373,14 @@ function defaultConfig() {
|
|
|
3072
3373
|
patterns: {
|
|
3073
3374
|
defaults: {
|
|
3074
3375
|
ds: "none",
|
|
3075
|
-
b: "1px solid var(--
|
|
3376
|
+
b: "1px solid var(--surface-bg-muted)",
|
|
3076
3377
|
br: "0.375rem",
|
|
3077
3378
|
p: "1rem",
|
|
3078
3379
|
fs: "1rem"
|
|
3079
3380
|
},
|
|
3080
3381
|
radii: {
|
|
3081
3382
|
pill: "999px",
|
|
3082
|
-
circle: "50%"
|
|
3083
|
-
card: "0.5rem"
|
|
3383
|
+
circle: "50%"
|
|
3084
3384
|
},
|
|
3085
3385
|
groups: {
|
|
3086
3386
|
control: {
|
|
@@ -3144,7 +3444,7 @@ function defaultConfig() {
|
|
|
3144
3444
|
group: "control",
|
|
3145
3445
|
class: "cta",
|
|
3146
3446
|
fill: true,
|
|
3147
|
-
default_role: "
|
|
3447
|
+
default_role: "ui-primary",
|
|
3148
3448
|
overrides: {
|
|
3149
3449
|
p: "0.75em 1.5em",
|
|
3150
3450
|
b: "none",
|
|
@@ -3164,7 +3464,7 @@ function defaultConfig() {
|
|
|
3164
3464
|
"font-weight": "600",
|
|
3165
3465
|
"text-decoration": "none",
|
|
3166
3466
|
cursor: "pointer",
|
|
3167
|
-
color: "var(--
|
|
3467
|
+
color: "var(--ui-primary-on-solid)",
|
|
3168
3468
|
"box-shadow": "var(--ds-cta-group)"
|
|
3169
3469
|
},
|
|
3170
3470
|
states: {
|
|
@@ -3183,7 +3483,7 @@ function defaultConfig() {
|
|
|
3183
3483
|
base: {
|
|
3184
3484
|
"border-radius": "var(--br-card-group)",
|
|
3185
3485
|
padding: "1.5rem",
|
|
3186
|
-
background: "var(--
|
|
3486
|
+
background: "var(--surface-bg)",
|
|
3187
3487
|
border: "var(--b-card-group)",
|
|
3188
3488
|
"box-shadow": "var(--ds-card-group)"
|
|
3189
3489
|
},
|
|
@@ -3252,5 +3552,5 @@ function defaultConfig() {
|
|
|
3252
3552
|
}
|
|
3253
3553
|
|
|
3254
3554
|
//#endregion
|
|
3255
|
-
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 };
|
|
3256
3556
|
//# sourceMappingURL=index.mjs.map
|