@getvitops/generator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/assets/bricks/elements/carousel.php +89 -0
- package/assets/bricks/elements/centered.php +128 -0
- package/assets/bricks/elements/color-scheme-toggle.php +61 -0
- package/assets/bricks/elements/copy-button.php +65 -0
- package/assets/bricks/elements/dismissable.php +93 -0
- package/assets/bricks/elements/entries.php +96 -0
- package/assets/bricks/elements/image-compare.php +132 -0
- package/assets/bricks/elements/multi-field.php +148 -0
- package/assets/bricks/elements/sitenav.php +298 -0
- package/assets/bricks/elements/split-link.php +170 -0
- package/assets/bricks/elements/split-panel.php +155 -0
- package/assets/bricks/elements/split.php +83 -0
- package/assets/bricks/load.php +129 -0
- package/assets/css/animation.css +526 -0
- package/assets/css/global.css +17 -0
- package/assets/css/index.css +58 -0
- package/assets/css/layout.css +723 -0
- package/assets/css/patterns/anchor-link.css +66 -0
- package/assets/css/patterns/banner.css +64 -0
- package/assets/css/patterns/carousel.css +269 -0
- package/assets/css/patterns/cluster.css +64 -0
- package/assets/css/patterns/code.css +17 -0
- package/assets/css/patterns/combobox.css +97 -0
- package/assets/css/patterns/comment.css +88 -0
- package/assets/css/patterns/copy.css +82 -0
- package/assets/css/patterns/details.css +87 -0
- package/assets/css/patterns/dialog.css +99 -0
- package/assets/css/patterns/drawer.css +108 -0
- package/assets/css/patterns/dropdown.css +45 -0
- package/assets/css/patterns/forms.css +335 -0
- package/assets/css/patterns/grouped.css +86 -0
- package/assets/css/patterns/horizontal-scroll.css +40 -0
- package/assets/css/patterns/icon.css +72 -0
- package/assets/css/patterns/lightbox.css +156 -0
- package/assets/css/patterns/list.css +272 -0
- package/assets/css/patterns/marquee.css +68 -0
- package/assets/css/patterns/masonry.css +40 -0
- package/assets/css/patterns/media.css +70 -0
- package/assets/css/patterns/nav.css +241 -0
- package/assets/css/patterns/notification.css +114 -0
- package/assets/css/patterns/overlay.css +28 -0
- package/assets/css/patterns/popover.css +68 -0
- package/assets/css/patterns/pull-quote.css +71 -0
- package/assets/css/patterns/scroll-based.css +227 -0
- package/assets/css/patterns/scroll-target.css +124 -0
- package/assets/css/patterns/separator.css +130 -0
- package/assets/css/patterns/sitenav.css +582 -0
- package/assets/css/patterns/split-link.css +113 -0
- package/assets/css/patterns/splitter.css +212 -0
- package/assets/css/patterns/stack.css +67 -0
- package/assets/css/patterns/sticky.css +127 -0
- package/assets/css/patterns/subgrid.css +178 -0
- package/assets/css/patterns/svg.css +166 -0
- package/assets/css/patterns/table.css +106 -0
- package/assets/css/patterns/tag.css +75 -0
- package/assets/css/patterns/text-effects.css +317 -0
- package/assets/css/patterns/tooltip.css +68 -0
- package/assets/css/patterns/tree.css +126 -0
- package/assets/css/utilities.css +351 -0
- package/assets/js/deferred.js +1 -0
- package/assets/js/elements.js +887 -0
- package/assets/js/polyfills/adoptedStyleSheets.js +1 -0
- package/assets/js/polyfills/css-anchor-positioning-fn.js +17 -0
- package/assets/js/polyfills/global.esm.js +1 -0
- package/assets/js/polyfills/popover.js +63 -0
- package/assets/js/polyfills/template-shadowroot.js +24 -0
- package/assets/js/polyfills/webcomponents-bundle.js +27 -0
- package/assets/js/polyfills.js +1 -0
- package/dist/index.d.mts +1425 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2611 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +33 -0
- package/schema.json +508 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2611 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { transform } from "lightningcss";
|
|
6
|
+
import * as z from "zod/mini";
|
|
7
|
+
|
|
8
|
+
//#region src/schema.ts
|
|
9
|
+
/**
|
|
10
|
+
* Canonical `design-system.json` schema — the single source of truth.
|
|
11
|
+
*
|
|
12
|
+
* Authored with `zod/mini` (tree-shakeable Zod v4). Everything else derives from it:
|
|
13
|
+
* • the `DesignSystem` TS type (`z.infer`), used throughout the generator,
|
|
14
|
+
* • the published JSON Schema (`toJSONSchema`), emitted to `schema.json` for `$schema` refs,
|
|
15
|
+
* • runtime validation (`validate`) for the CLI and to gate `generate`.
|
|
16
|
+
*/
|
|
17
|
+
const CssDecls = z.record(z.string(), z.string());
|
|
18
|
+
const Scalar = z.union([z.string(), z.number()]);
|
|
19
|
+
const VarMap = z.record(z.string(), Scalar);
|
|
20
|
+
const Ramp = z.object({
|
|
21
|
+
xxd: z.optional(z.string()),
|
|
22
|
+
xd: z.optional(z.string()),
|
|
23
|
+
d: z.optional(z.string()),
|
|
24
|
+
base: z.optional(z.string()),
|
|
25
|
+
l: z.optional(z.string()),
|
|
26
|
+
xl: z.optional(z.string()),
|
|
27
|
+
xxl: z.optional(z.string())
|
|
28
|
+
});
|
|
29
|
+
const RoleSpec = z.union([z.string(), z.object({
|
|
30
|
+
ramp: z.optional(z.string()),
|
|
31
|
+
invert: z.optional(z.boolean()),
|
|
32
|
+
shift: z.optional(z.number()),
|
|
33
|
+
steps: z.optional(z.record(z.string(), z.string())),
|
|
34
|
+
value: z.optional(z.record(z.string(), z.string()))
|
|
35
|
+
})]);
|
|
36
|
+
const Scheme = z.object({
|
|
37
|
+
appearance: z.enum(["light", "dark"]),
|
|
38
|
+
semantic: z.record(z.string(), RoleSpec)
|
|
39
|
+
});
|
|
40
|
+
const UtilityName = z.enum([
|
|
41
|
+
"bg",
|
|
42
|
+
"text",
|
|
43
|
+
"border",
|
|
44
|
+
"outline",
|
|
45
|
+
"fill",
|
|
46
|
+
"stroke"
|
|
47
|
+
]);
|
|
48
|
+
const Colors = z.object({
|
|
49
|
+
palette: z.record(z.string(), Ramp),
|
|
50
|
+
utilities: z.optional(z.array(UtilityName)),
|
|
51
|
+
schemes: z.record(z.string(), Scheme)
|
|
52
|
+
});
|
|
53
|
+
const Scale = z.object({
|
|
54
|
+
base: z.string(),
|
|
55
|
+
ratio: z.number(),
|
|
56
|
+
steps: z.optional(z.number()),
|
|
57
|
+
names: z.optional(z.array(z.string())),
|
|
58
|
+
baseStep: z.optional(z.number()),
|
|
59
|
+
baseline: z.optional(z.string()),
|
|
60
|
+
fluid: z.optional(z.object({
|
|
61
|
+
minVw: z.string(),
|
|
62
|
+
maxVw: z.string(),
|
|
63
|
+
minRatio: z.number()
|
|
64
|
+
}))
|
|
65
|
+
});
|
|
66
|
+
const Pattern = z.object({
|
|
67
|
+
group: z.optional(z.string()),
|
|
68
|
+
overrides: z.optional(CssDecls),
|
|
69
|
+
element: z.optional(z.string()),
|
|
70
|
+
class: z.optional(z.string()),
|
|
71
|
+
default_role: z.optional(z.string()),
|
|
72
|
+
base: z.optional(CssDecls),
|
|
73
|
+
states: z.optional(z.record(z.string(), z.record(z.string(), z.unknown()))),
|
|
74
|
+
roles: z.optional(z.array(z.string()))
|
|
75
|
+
});
|
|
76
|
+
const Patterns = z.object({
|
|
77
|
+
defaults: z.optional(CssDecls),
|
|
78
|
+
radii: z.optional(CssDecls),
|
|
79
|
+
groups: z.optional(z.record(z.string(), CssDecls)),
|
|
80
|
+
z: z.optional(z.record(z.string(), z.number())),
|
|
81
|
+
items: z.optional(z.record(z.string(), Pattern))
|
|
82
|
+
});
|
|
83
|
+
const TypographyRole = z.record(z.string(), Scalar);
|
|
84
|
+
const Typography = z.object({
|
|
85
|
+
families: z.optional(z.record(z.string(), z.string())),
|
|
86
|
+
roles: z.optional(z.record(z.string(), TypographyRole)),
|
|
87
|
+
headings: z.optional(z.record(z.string(), z.string()))
|
|
88
|
+
});
|
|
89
|
+
const AnimEffect = z.object({
|
|
90
|
+
kf: z.string(),
|
|
91
|
+
css: z.optional(VarMap),
|
|
92
|
+
vars: z.optional(VarMap)
|
|
93
|
+
});
|
|
94
|
+
const Animations = z.object({
|
|
95
|
+
effects: z.optional(z.record(z.string(), AnimEffect)),
|
|
96
|
+
journeys: z.optional(z.object({
|
|
97
|
+
base: z.optional(z.record(z.string(), VarMap)),
|
|
98
|
+
compose: z.optional(z.array(z.array(z.string())))
|
|
99
|
+
}))
|
|
100
|
+
});
|
|
101
|
+
const DesignSystemSchema = z.object({
|
|
102
|
+
$schema: z.optional(z.string()),
|
|
103
|
+
colors: Colors,
|
|
104
|
+
shadows: z.optional(z.record(z.string(), z.string())),
|
|
105
|
+
fonts: z.optional(z.record(z.string(), z.string())),
|
|
106
|
+
typeScale: z.optional(Scale),
|
|
107
|
+
spaceScale: z.optional(Scale),
|
|
108
|
+
patterns: z.optional(Patterns),
|
|
109
|
+
typography: z.optional(Typography),
|
|
110
|
+
animations: z.optional(Animations)
|
|
111
|
+
});
|
|
112
|
+
const ColorsPatch = z.object({
|
|
113
|
+
palette: z.optional(z.record(z.string(), Ramp)),
|
|
114
|
+
utilities: z.optional(z.array(UtilityName)),
|
|
115
|
+
schemes: z.optional(z.record(z.string(), z.object({
|
|
116
|
+
appearance: z.optional(z.enum(["light", "dark"])),
|
|
117
|
+
semantic: z.optional(z.record(z.string(), RoleSpec))
|
|
118
|
+
})))
|
|
119
|
+
});
|
|
120
|
+
const DesignSystemPatchSchema = z.extend(z.partial(DesignSystemSchema), { colors: z.optional(ColorsPatch) });
|
|
121
|
+
/**
|
|
122
|
+
* Stable URL of the published JSON Schema. Resolves once `@getvitops/generator` is on
|
|
123
|
+
* npm (unpkg serves the packaged `schema.json`); `vitops init` stamps this into a
|
|
124
|
+
* scaffolded config's `$schema` so editors light up autocomplete + validation.
|
|
125
|
+
*/
|
|
126
|
+
const SCHEMA_URL = "https://unpkg.com/@getvitops/generator/schema.json";
|
|
127
|
+
/** The published JSON Schema (draft-2020-12), derived from the zod schema. */
|
|
128
|
+
const jsonSchema = {
|
|
129
|
+
$id: SCHEMA_URL,
|
|
130
|
+
...z.toJSONSchema(DesignSystemSchema, { target: "draft-2020-12" })
|
|
131
|
+
};
|
|
132
|
+
/** Validate an unknown value against the design-system schema. */
|
|
133
|
+
function validate(input) {
|
|
134
|
+
const result = z.safeParse(DesignSystemSchema, input);
|
|
135
|
+
if (!result.success) return {
|
|
136
|
+
ok: false,
|
|
137
|
+
data: void 0,
|
|
138
|
+
errors: result.error.issues
|
|
139
|
+
};
|
|
140
|
+
if (result.data.colors.schemes.default == null) return {
|
|
141
|
+
ok: false,
|
|
142
|
+
data: void 0,
|
|
143
|
+
errors: [{
|
|
144
|
+
code: "custom",
|
|
145
|
+
path: [
|
|
146
|
+
"colors",
|
|
147
|
+
"schemes",
|
|
148
|
+
"default"
|
|
149
|
+
],
|
|
150
|
+
message: "colors.schemes must include a \"default\" (base) scheme"
|
|
151
|
+
}]
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
data: result.data,
|
|
156
|
+
errors: []
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/docs.ts
|
|
162
|
+
/**
|
|
163
|
+
* OKF documentation bundle generator (ported from the repo's `generate-docs.ts`).
|
|
164
|
+
*
|
|
165
|
+
* Pure: `generateDocs(ds, assetsDir)` returns a `{ relPath: markdown }` map for the
|
|
166
|
+
* Open Knowledge Format tree (top index, css/, bricks/), reading the Bricks element
|
|
167
|
+
* PHP from `<assetsDir>/bricks/elements`. No filesystem writes — the caller decides
|
|
168
|
+
* where the bundle lands (e.g. `<outDir>/docs`).
|
|
169
|
+
*/
|
|
170
|
+
const DS_PATH = "design-system.json";
|
|
171
|
+
const ORDER = [
|
|
172
|
+
"split",
|
|
173
|
+
"centered",
|
|
174
|
+
"carousel",
|
|
175
|
+
"color-scheme-toggle",
|
|
176
|
+
"copy-button",
|
|
177
|
+
"dismissable",
|
|
178
|
+
"entries",
|
|
179
|
+
"image-compare",
|
|
180
|
+
"sitenav",
|
|
181
|
+
"multi-field",
|
|
182
|
+
"split-link",
|
|
183
|
+
"split-panel"
|
|
184
|
+
];
|
|
185
|
+
function tokenize(src) {
|
|
186
|
+
const toks = [];
|
|
187
|
+
let i = 0;
|
|
188
|
+
const n = src.length;
|
|
189
|
+
while (i < n) {
|
|
190
|
+
const c = src[i];
|
|
191
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
196
|
+
while (i < n && src[i] !== "\n") i++;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (c === "#") {
|
|
200
|
+
while (i < n && src[i] !== "\n") i++;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
204
|
+
i += 2;
|
|
205
|
+
while (i < n && !(src[i] === "*" && src[i + 1] === "/")) i++;
|
|
206
|
+
i += 2;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (c === "'" || c === "\"") {
|
|
210
|
+
const quote = c;
|
|
211
|
+
i++;
|
|
212
|
+
let s = "";
|
|
213
|
+
while (i < n && src[i] !== quote) if (src[i] === "\\" && i + 1 < n) {
|
|
214
|
+
const next = src[i + 1];
|
|
215
|
+
s += next === "n" ? "\n" : next === "t" ? " " : next;
|
|
216
|
+
i += 2;
|
|
217
|
+
} else s += src[i++];
|
|
218
|
+
i++;
|
|
219
|
+
toks.push({
|
|
220
|
+
t: "str",
|
|
221
|
+
v: s
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (c === "=" && src[i + 1] === ">") {
|
|
226
|
+
toks.push({
|
|
227
|
+
t: "arrow",
|
|
228
|
+
v: "=>"
|
|
229
|
+
});
|
|
230
|
+
i += 2;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if ("[](){},".includes(c)) {
|
|
234
|
+
toks.push({
|
|
235
|
+
t: "punc",
|
|
236
|
+
v: c
|
|
237
|
+
});
|
|
238
|
+
i++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (c >= "0" && c <= "9" || c === "-" && src[i + 1] >= "0" && src[i + 1] <= "9") {
|
|
242
|
+
let s = c;
|
|
243
|
+
i++;
|
|
244
|
+
while (i < n && src[i] >= "0" && src[i] <= "9") s += src[i++];
|
|
245
|
+
toks.push({
|
|
246
|
+
t: "num",
|
|
247
|
+
v: s
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (/[A-Za-z_\\]/.test(c)) {
|
|
252
|
+
let s = c;
|
|
253
|
+
i++;
|
|
254
|
+
while (i < n && /[A-Za-z0-9_\\]/.test(src[i])) s += src[i++];
|
|
255
|
+
toks.push({
|
|
256
|
+
t: "id",
|
|
257
|
+
v: s
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
i++;
|
|
262
|
+
}
|
|
263
|
+
return toks;
|
|
264
|
+
}
|
|
265
|
+
var Parser = class {
|
|
266
|
+
p = 0;
|
|
267
|
+
toks;
|
|
268
|
+
constructor(toks) {
|
|
269
|
+
this.toks = toks;
|
|
270
|
+
}
|
|
271
|
+
peek() {
|
|
272
|
+
return this.toks[this.p];
|
|
273
|
+
}
|
|
274
|
+
next() {
|
|
275
|
+
return this.toks[this.p++];
|
|
276
|
+
}
|
|
277
|
+
parseValue() {
|
|
278
|
+
const t = this.peek();
|
|
279
|
+
if (!t) return null;
|
|
280
|
+
if (t.t === "str") {
|
|
281
|
+
this.next();
|
|
282
|
+
return t.v;
|
|
283
|
+
}
|
|
284
|
+
if (t.t === "num") {
|
|
285
|
+
this.next();
|
|
286
|
+
return Number(t.v);
|
|
287
|
+
}
|
|
288
|
+
if (t.t === "id") {
|
|
289
|
+
this.next();
|
|
290
|
+
if (t.v === "true") return true;
|
|
291
|
+
if (t.v === "false") return false;
|
|
292
|
+
if (t.v === "null") return null;
|
|
293
|
+
if (this.peek()?.t === "punc" && this.peek()?.v === "(") {
|
|
294
|
+
this.next();
|
|
295
|
+
if (t.v === "array") return this.parseBody(")");
|
|
296
|
+
const args = this.parseCallArgs();
|
|
297
|
+
return args.find((a) => typeof a === "string") ?? args[0] ?? "";
|
|
298
|
+
}
|
|
299
|
+
return t.v;
|
|
300
|
+
}
|
|
301
|
+
if (t.t === "punc" && t.v === "[") {
|
|
302
|
+
this.next();
|
|
303
|
+
return this.parseBody("]");
|
|
304
|
+
}
|
|
305
|
+
if (t.t === "punc" && t.v === "(") {
|
|
306
|
+
this.next();
|
|
307
|
+
return this.parseBody(")");
|
|
308
|
+
}
|
|
309
|
+
this.next();
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
parseCallArgs() {
|
|
313
|
+
const args = [];
|
|
314
|
+
while (this.peek() && !(this.peek().t === "punc" && this.peek().v === ")")) {
|
|
315
|
+
args.push(this.parseValue());
|
|
316
|
+
if (this.peek()?.t === "punc" && this.peek()?.v === ",") this.next();
|
|
317
|
+
}
|
|
318
|
+
this.next();
|
|
319
|
+
return args;
|
|
320
|
+
}
|
|
321
|
+
parseBody(close) {
|
|
322
|
+
const list = [];
|
|
323
|
+
const map = {};
|
|
324
|
+
let keyed = false;
|
|
325
|
+
while (this.peek() && !(this.peek().t === "punc" && this.peek().v === close)) {
|
|
326
|
+
const first = this.parseValue();
|
|
327
|
+
if (this.peek()?.t === "arrow") {
|
|
328
|
+
this.next();
|
|
329
|
+
map[String(first)] = this.parseValue();
|
|
330
|
+
keyed = true;
|
|
331
|
+
} else list.push(first);
|
|
332
|
+
if (this.peek()?.t === "punc" && this.peek()?.v === ",") this.next();
|
|
333
|
+
}
|
|
334
|
+
this.next();
|
|
335
|
+
return keyed ? map : list;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
function parsePhpLiteral(text) {
|
|
339
|
+
return new Parser(tokenize(text)).parseValue();
|
|
340
|
+
}
|
|
341
|
+
function matchBalanced(src, open) {
|
|
342
|
+
const pairs = {
|
|
343
|
+
"(": ")",
|
|
344
|
+
"[": "]",
|
|
345
|
+
"{": "}"
|
|
346
|
+
};
|
|
347
|
+
const stack = [];
|
|
348
|
+
let i = open;
|
|
349
|
+
const n = src.length;
|
|
350
|
+
while (i < n) {
|
|
351
|
+
const c = src[i];
|
|
352
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
353
|
+
while (i < n && src[i] !== "\n") i++;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
357
|
+
i += 2;
|
|
358
|
+
while (i < n && !(src[i] === "*" && src[i + 1] === "/")) i++;
|
|
359
|
+
i += 2;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (c === "'" || c === "\"") {
|
|
363
|
+
i++;
|
|
364
|
+
while (i < n && src[i] !== c) {
|
|
365
|
+
if (src[i] === "\\") i++;
|
|
366
|
+
i++;
|
|
367
|
+
}
|
|
368
|
+
i++;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (c in pairs) stack.push(pairs[c]);
|
|
372
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
373
|
+
if (stack.pop() !== c) {}
|
|
374
|
+
if (stack.length === 0) return i + 1;
|
|
375
|
+
}
|
|
376
|
+
i++;
|
|
377
|
+
}
|
|
378
|
+
return n;
|
|
379
|
+
}
|
|
380
|
+
function methodBody(src, name) {
|
|
381
|
+
const m = new RegExp(`function\\s+${name}\\s*\\([^)]*\\)`).exec(src);
|
|
382
|
+
if (!m) return null;
|
|
383
|
+
const brace = src.indexOf("{", m.index + m[0].length);
|
|
384
|
+
if (brace < 0) return null;
|
|
385
|
+
const end = matchBalanced(src, brace);
|
|
386
|
+
return src.slice(brace + 1, end - 1);
|
|
387
|
+
}
|
|
388
|
+
function literalAt(src, fromIdx) {
|
|
389
|
+
let i = fromIdx;
|
|
390
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
391
|
+
let open;
|
|
392
|
+
if (src[i] === "[") open = i;
|
|
393
|
+
else if (src.startsWith("array", i)) open = src.indexOf("(", i);
|
|
394
|
+
else return null;
|
|
395
|
+
const end = matchBalanced(src, open);
|
|
396
|
+
return {
|
|
397
|
+
text: src.slice(i, end),
|
|
398
|
+
end
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function firstString(re, src) {
|
|
402
|
+
const m = re.exec(src);
|
|
403
|
+
return m ? m[1] ?? null : null;
|
|
404
|
+
}
|
|
405
|
+
function extractDocblock(src) {
|
|
406
|
+
const m = /\/\*\*([\s\S]*?)\*\//.exec(src);
|
|
407
|
+
if (!m) return "";
|
|
408
|
+
const lines = m[1].split("\n").map((l) => l.replace(/^\s*\*?\s?/, "").replace(/\s+$/, ""));
|
|
409
|
+
while (lines.length && lines[0] === "") lines.shift();
|
|
410
|
+
if (lines.length && /Bricks Builder\)\./.test(lines[0])) lines.shift();
|
|
411
|
+
const owned = lines.findIndex((l) => l.startsWith("Owned by the framework repo"));
|
|
412
|
+
const kept = (owned >= 0 ? lines.slice(0, owned) : lines).map((l) => l.replace(/^•\s*/, "- "));
|
|
413
|
+
while (kept.length && kept[kept.length - 1] === "") kept.pop();
|
|
414
|
+
while (kept.length && kept[0] === "") kept.shift();
|
|
415
|
+
return kept.join("\n");
|
|
416
|
+
}
|
|
417
|
+
function extractControls(src) {
|
|
418
|
+
const body = methodBody(src, "set_controls");
|
|
419
|
+
if (!body) return [];
|
|
420
|
+
const controls = [];
|
|
421
|
+
const re = /\$this->controls\[\s*'([^']+)'\s*\](\[\s*'([^']+)'\s*\])?\s*=\s*/g;
|
|
422
|
+
let m;
|
|
423
|
+
while (m = re.exec(body)) {
|
|
424
|
+
const key = m[1];
|
|
425
|
+
if (m[2]) continue;
|
|
426
|
+
const lit = literalAt(body, m.index + m[0].length);
|
|
427
|
+
if (!lit) continue;
|
|
428
|
+
const def = parsePhpLiteral(lit.text);
|
|
429
|
+
if (def && typeof def === "object" && !Array.isArray(def)) controls.push({
|
|
430
|
+
key,
|
|
431
|
+
def
|
|
432
|
+
});
|
|
433
|
+
re.lastIndex = lit.end;
|
|
434
|
+
}
|
|
435
|
+
return controls;
|
|
436
|
+
}
|
|
437
|
+
function extractChildren(src) {
|
|
438
|
+
const body = methodBody(src, "get_nestable_children");
|
|
439
|
+
if (!body) return [];
|
|
440
|
+
const labels = [];
|
|
441
|
+
const re = /'label'\s*=>\s*esc_html__\(\s*'([^']*)'/g;
|
|
442
|
+
let m;
|
|
443
|
+
while (m = re.exec(body)) labels.push(m[1]);
|
|
444
|
+
return labels;
|
|
445
|
+
}
|
|
446
|
+
function extractKeywords(src) {
|
|
447
|
+
const body = methodBody(src, "get_keywords");
|
|
448
|
+
if (!body) return [];
|
|
449
|
+
const ret = /return\s+/.exec(body);
|
|
450
|
+
if (!ret) return [];
|
|
451
|
+
const lit = literalAt(body, ret.index + ret[0].length);
|
|
452
|
+
if (!lit) return [];
|
|
453
|
+
const arr = parsePhpLiteral(lit.text);
|
|
454
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === "string") : [];
|
|
455
|
+
}
|
|
456
|
+
function parseElement(src) {
|
|
457
|
+
const name = firstString(/public\s+\$name\s*=\s*'vitops-([^']+)'/, src);
|
|
458
|
+
if (!name) return null;
|
|
459
|
+
return {
|
|
460
|
+
name,
|
|
461
|
+
label: firstString(/function get_label\(\)[\s\S]*?return\s+esc_html__\(\s*'([^']*)'/, src) ?? name,
|
|
462
|
+
nestable: /public\s+\$nestable\s*=\s*true/.test(src),
|
|
463
|
+
docblock: extractDocblock(src),
|
|
464
|
+
controls: extractControls(src),
|
|
465
|
+
cssClass: firstString(/\$this->controls\[\s*'_cssClasses'\s*\]\[\s*'default'\s*\]\s*=\s*'([^']*)'/, src),
|
|
466
|
+
children: extractChildren(src),
|
|
467
|
+
keywords: extractKeywords(src)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function controlLine(c) {
|
|
471
|
+
const def = c.def;
|
|
472
|
+
if (def.type === "info") return `- _Note:_ ${def.content ?? ""}`;
|
|
473
|
+
const label = def.label || c.key;
|
|
474
|
+
const options = def.options && typeof def.options === "object" && !Array.isArray(def.options) ? def.options : null;
|
|
475
|
+
const meta = [];
|
|
476
|
+
let type = def.type || "text";
|
|
477
|
+
if (def.units === true) type += "+units";
|
|
478
|
+
meta.push(type);
|
|
479
|
+
if (def.default !== void 0) {
|
|
480
|
+
const raw = String(def.default);
|
|
481
|
+
const shown = options && options[raw] ? options[raw] : raw;
|
|
482
|
+
meta.push(`default \`${shown}\``);
|
|
483
|
+
}
|
|
484
|
+
if (def.placeholder !== void 0 && def.placeholder !== "") meta.push(`placeholder \`${def.placeholder}\``);
|
|
485
|
+
if (def.min !== void 0 || def.max !== void 0) meta.push(`range ${def.min ?? "–"}–${def.max ?? "∞"}`);
|
|
486
|
+
const tail = [];
|
|
487
|
+
if (def.description) tail.push(String(def.description));
|
|
488
|
+
if (options) tail.push(`Options: ${Object.values(options).map((o) => `\`${o}\``).join(", ")}.`);
|
|
489
|
+
if (Array.isArray(def.css) && def.css.length) {
|
|
490
|
+
const first = def.css[0];
|
|
491
|
+
if (first && typeof first === "object" && !Array.isArray(first) && "property" in first) tail.push(`Bound to \`${first.property}\`.`);
|
|
492
|
+
}
|
|
493
|
+
if (def.type === "repeater" && def.fields && typeof def.fields === "object") {
|
|
494
|
+
const fields = Object.values(def.fields).map((f) => f && typeof f === "object" && !Array.isArray(f) ? f.label : null).filter(Boolean);
|
|
495
|
+
if (fields.length) tail.push(`Fields: ${fields.join(", ")}.`);
|
|
496
|
+
}
|
|
497
|
+
const head = `- **${label}** (${meta.join(", ")})`;
|
|
498
|
+
return tail.length ? `${head} — ${tail.join(" ")}` : head;
|
|
499
|
+
}
|
|
500
|
+
function renderElement(el) {
|
|
501
|
+
const out = [];
|
|
502
|
+
const nest = el.nestable ? "nestable" : "not nestable";
|
|
503
|
+
out.push(`## ${el.label} — \`vitops-${el.name}\` · ${nest}`);
|
|
504
|
+
out.push("");
|
|
505
|
+
if (el.docblock) {
|
|
506
|
+
out.push(el.docblock);
|
|
507
|
+
out.push("");
|
|
508
|
+
}
|
|
509
|
+
if (el.controls.filter((c) => c.def.type !== "info").length) {
|
|
510
|
+
out.push("**Controls**");
|
|
511
|
+
out.push("");
|
|
512
|
+
for (const c of el.controls) out.push(controlLine(c));
|
|
513
|
+
out.push("");
|
|
514
|
+
} else {
|
|
515
|
+
const notes = el.controls.filter((c) => c.def.type === "info");
|
|
516
|
+
if (notes.length) {
|
|
517
|
+
for (const c of notes) out.push(controlLine(c));
|
|
518
|
+
out.push("");
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (el.cssClass) {
|
|
522
|
+
out.push(`**Base CSS class:** \`${el.cssClass}\` (applied automatically; add ratio/modifier classes alongside it in the element's "CSS classes" field).`);
|
|
523
|
+
out.push("");
|
|
524
|
+
}
|
|
525
|
+
if (el.children.length) {
|
|
526
|
+
out.push(`**Seeded children:** ${el.children.join(", ")}.`);
|
|
527
|
+
out.push("");
|
|
528
|
+
}
|
|
529
|
+
if (el.keywords.length) {
|
|
530
|
+
out.push(`**Keywords:** ${el.keywords.filter((k) => k !== "vitops").join(", ")}.`);
|
|
531
|
+
out.push("");
|
|
532
|
+
}
|
|
533
|
+
return out.join("\n");
|
|
534
|
+
}
|
|
535
|
+
function frontmatter(fm) {
|
|
536
|
+
const q = (s) => `"${s.replace(/"/g, "\\\"")}"`;
|
|
537
|
+
const lines = [
|
|
538
|
+
`type: ${q(fm.type)}`,
|
|
539
|
+
`title: ${q(fm.title)}`,
|
|
540
|
+
`description: ${q(fm.description)}`
|
|
541
|
+
];
|
|
542
|
+
if (fm.resource) lines.push(`resource: ${q(fm.resource)}`);
|
|
543
|
+
if (fm.tags) lines.push(`tags: [${fm.tags.join(", ")}]`);
|
|
544
|
+
lines.push("generator: \"@getvitops/generator\"");
|
|
545
|
+
return `---\n${lines.join("\n")}\n---`;
|
|
546
|
+
}
|
|
547
|
+
const INDEX_NOTE = `<!-- GENERATED by @getvitops/generator — do not edit; regenerate with \`vitops generate\`. -->`;
|
|
548
|
+
const ELEMENTS_PREAMBLE = `${frontmatter({
|
|
549
|
+
type: "Bricks Elements Reference",
|
|
550
|
+
title: "Vitops — Bricks Builder elements reference",
|
|
551
|
+
description: "Every repo-owned Bricks element, its controls, defaults, seeded children and keywords, for driving the Bricks Builder UI.",
|
|
552
|
+
resource: "bricks/elements",
|
|
553
|
+
tags: [
|
|
554
|
+
"bricks",
|
|
555
|
+
"wordpress",
|
|
556
|
+
"elements",
|
|
557
|
+
"design-system"
|
|
558
|
+
]
|
|
559
|
+
})}
|
|
560
|
+
|
|
561
|
+
# Vitops — Bricks Builder elements reference
|
|
562
|
+
|
|
563
|
+
Context for driving the Bricks Builder UI (e.g. Claude for Chrome). Every section below
|
|
564
|
+
is generated from the element's own source, so this reference always matches the elements
|
|
565
|
+
currently deployed. See also the CSS class vocabulary in [/css/classes.md](../css/classes.md)
|
|
566
|
+
and the integration guidance in [/bricks/index.md](index.md).
|
|
567
|
+
|
|
568
|
+
## How they appear in the builder
|
|
569
|
+
|
|
570
|
+
- The elements register from \`dist/bricks/load.php\` and group under a **"Vitops"** category
|
|
571
|
+
pinned to the **top** of the element panel. Insert one by searching its **label** (e.g.
|
|
572
|
+
"Split", "Carousel") or any of its **keywords** (listed per element).
|
|
573
|
+
- Two rendering families:
|
|
574
|
+
- **Web-component elements** render a Lit custom element (\`<wc-*>\`, \`<copy-button>\`, …)
|
|
575
|
+
from \`dist/elements.js\` and progressively enhance. A few (Image Compare, Split Panel)
|
|
576
|
+
show their children stacked in the builder canvas until the client-side upgrade runs —
|
|
577
|
+
that is expected, not broken.
|
|
578
|
+
- **CSS-pattern elements** (Split, Centered, Menu, Split Link) render plain markup styled
|
|
579
|
+
by the framework CSS — no JS dependency.
|
|
580
|
+
- **Nestable** elements accept child elements dropped into them; some seed starter children
|
|
581
|
+
(listed as "Seeded children"). **Non-nestable** elements are configured entirely through
|
|
582
|
+
their controls.
|
|
583
|
+
|
|
584
|
+
## Configuring layout
|
|
585
|
+
|
|
586
|
+
Some layout is set through **controls**; some through **CSS classes typed into the element's
|
|
587
|
+
built-in "CSS classes" field** (called out per element as modifier classes). Responsive
|
|
588
|
+
suffixes on those classes engage from a container breakpoint: \`-sm\` = 30rem, \`-md\` = 48rem,
|
|
589
|
+
\`-lg\` = 64rem, \`-xl\` = 80rem.
|
|
590
|
+
|
|
591
|
+
Each control below lists its type, default/placeholder, any select options, and the CSS
|
|
592
|
+
custom property it writes (when it drives one). Info notes flagged _Note:_ carry the
|
|
593
|
+
modifier-class and structural guidance from the element itself.
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
`;
|
|
597
|
+
function renderElementsDoc(elementsDir) {
|
|
598
|
+
const files = existsSync(elementsDir) ? readdirSync(elementsDir).filter((f) => f.endsWith(".php")) : [];
|
|
599
|
+
const elements = [];
|
|
600
|
+
for (const f of files) try {
|
|
601
|
+
const el = parseElement(readFileSync(join(elementsDir, f), "utf8"));
|
|
602
|
+
if (el) elements.push(el);
|
|
603
|
+
} catch {}
|
|
604
|
+
elements.sort((a, b) => {
|
|
605
|
+
const ia = ORDER.indexOf(a.name);
|
|
606
|
+
const ib = ORDER.indexOf(b.name);
|
|
607
|
+
if (ia >= 0 && ib >= 0) return ia - ib;
|
|
608
|
+
if (ia >= 0) return -1;
|
|
609
|
+
if (ib >= 0) return 1;
|
|
610
|
+
return a.name.localeCompare(b.name);
|
|
611
|
+
});
|
|
612
|
+
return `${ELEMENTS_PREAMBLE}\n${elements.map(renderElement).join("\n---\n\n")}`;
|
|
613
|
+
}
|
|
614
|
+
const code = (xs) => xs.map((x) => `\`${x}\``).join(", ");
|
|
615
|
+
function renderCssClasses(ds) {
|
|
616
|
+
const palette = ds.colors.palette;
|
|
617
|
+
const ramps = Object.keys(palette);
|
|
618
|
+
const firstRamp = ramps[0];
|
|
619
|
+
const nonBase = (firstRamp ? Object.keys(palette[firstRamp] ?? {}) : []).filter((s) => s !== "base");
|
|
620
|
+
const roles = Object.keys(ds.colors.schemes.default?.semantic ?? {});
|
|
621
|
+
const utils = ds.colors.utilities ?? [
|
|
622
|
+
"bg",
|
|
623
|
+
"text",
|
|
624
|
+
"border"
|
|
625
|
+
];
|
|
626
|
+
const typeRoles = Object.keys(ds.typography?.roles ?? {});
|
|
627
|
+
const shadows = Object.keys(ds.shadows ?? {});
|
|
628
|
+
const effects = Object.keys(ds.animations?.effects ?? {});
|
|
629
|
+
const items = ds.patterns?.items ?? {};
|
|
630
|
+
const patterns = Object.keys(items);
|
|
631
|
+
const patternRoles = Array.from(new Set(Object.values(items).flatMap((p) => [p.default_role, ...p.roles ?? []].filter(Boolean))));
|
|
632
|
+
const spaceNames = ds.spaceScale?.names ?? [];
|
|
633
|
+
const typeNames = ds.typeScale?.names ?? [];
|
|
634
|
+
const families = Object.keys(ds.typography?.families ?? {});
|
|
635
|
+
const radii = Object.keys(ds.patterns?.radii ?? {});
|
|
636
|
+
return `${frontmatter({
|
|
637
|
+
type: "CSS Framework Reference",
|
|
638
|
+
title: "Vitops CSS framework — class vocabulary",
|
|
639
|
+
description: "Every utility and component class in the Vitops CSS framework, stated as a naming rule over the design tokens it expands.",
|
|
640
|
+
resource: DS_PATH,
|
|
641
|
+
tags: [
|
|
642
|
+
"css",
|
|
643
|
+
"utilities",
|
|
644
|
+
"patterns",
|
|
645
|
+
"design-system"
|
|
646
|
+
]
|
|
647
|
+
})}
|
|
648
|
+
|
|
649
|
+
# Vitops CSS framework — class vocabulary (LLM reference)
|
|
650
|
+
|
|
651
|
+
A variable-driven CSS framework. Classes encode the design system's **tokens** (colour,
|
|
652
|
+
type, space, shadow), **responsive grammar**, and **interaction states** — so styling with
|
|
653
|
+
these classes stays consistent and theme-/dark-mode-aware. Prefer them over hand-written
|
|
654
|
+
CSS or ad-hoc property values.
|
|
655
|
+
|
|
656
|
+
This is a **rule reference**, not an exhaustive list: each family below is a naming rule
|
|
657
|
+
plus the set of tokens it expands over. Applying a rule to any listed token yields a valid
|
|
658
|
+
class (e.g. rule \`bg-<color>\` + colour \`pine-xl\` → \`bg-pine-xl\`).
|
|
659
|
+
|
|
660
|
+
## Responsive & state variant grammar
|
|
661
|
+
|
|
662
|
+
Every utility accepts a **container-breakpoint prefix**; animation utilities also accept a
|
|
663
|
+
**state prefix**. In CSS/Bricks the separator is \`-\`; in Tailwind it is \`:\` / \`@\`.
|
|
664
|
+
|
|
665
|
+
| Intent | CSS / Bricks | Tailwind |
|
|
666
|
+
| ----------------- | -------------------------- | ----------------------------------------- |
|
|
667
|
+
| responsive split | \`md-split-1-2\` | \`@md:split-1-2\` |
|
|
668
|
+
| responsive align | \`md-items-center\` | \`@md:items-center\` |
|
|
669
|
+
| hover effect | \`transition hover-fade-in\` | \`transition fade-in hover:flip-fade-in\` |
|
|
670
|
+
|
|
671
|
+
- Breakpoint prefixes (bare): \`sm-\` = 30rem, \`md-\` = 48rem, \`lg-\` = 64rem, \`xl-\` = 80rem.
|
|
672
|
+
- State prefixes (animation effects only): \`hover-\`, \`active-\`, \`focus-\`, and \`flip-<effect>\`
|
|
673
|
+
(plays the effect in reverse on toggle). Effects require \`transition\` on the element.
|
|
674
|
+
|
|
675
|
+
## Layout & structure
|
|
676
|
+
|
|
677
|
+
- **\`centered\`** — named-track grid centering content in the reading \`measure\` track.
|
|
678
|
+
Widen a **direct child** by adding \`breakout\`, \`spotlight\`, or \`fullbleed\` (each
|
|
679
|
+
breakpoint-prefixable) to that child. Track widths are set via \`--width-measure\` /
|
|
680
|
+
\`--width-breakout\` / \`--width-spotlight\` and \`--gutter\`.
|
|
681
|
+
- **\`rhythm\`** — relationship-based vertical spacing (margins between headings, paragraphs,
|
|
682
|
+
lists, media) driven by the space scale. Usually paired with \`centered\`.
|
|
683
|
+
- **\`split\`** — equal flex columns. Ratio rule: **\`split-<a>-<b>\`** where \`<a>-<b>\` ∈
|
|
684
|
+
\`1-2\`, \`2-1\`, \`1-3\`, \`3-1\`, \`1-4\`, \`4-1\`, \`2-3\`, \`3-2\` (breakpoint-prefixable).
|
|
685
|
+
- **Flex** — \`flex\`, \`flex-row\`, \`flex-col\`; \`g\` for gap (space-scale token).
|
|
686
|
+
- **Alignment** — \`items-{start,center,end,stretch}\`, \`justify-{start,center,end,between}\`,
|
|
687
|
+
text align \`text-{start,center,end}\` (all breakpoint-prefixable).
|
|
688
|
+
- **Display** — \`block\`, \`inline\`, \`inline-block\`, \`flex\`, \`grid\`, \`hidden\`
|
|
689
|
+
(breakpoint-prefixable, e.g. \`md-hidden\`).
|
|
690
|
+
- **Accessibility** — \`sr-only\` / \`not-sr-only\` (breakpoint-prefixable).
|
|
691
|
+
- **State hooks** — \`is-active\`, \`is-open\` (styling flags toggled by JS / native state).
|
|
692
|
+
|
|
693
|
+
## Spacing
|
|
694
|
+
|
|
695
|
+
The space scale is ${code(spaceNames)} exposed as \`--space-<name>\` tokens.
|
|
696
|
+
Gap (\`g\`) and \`rhythm\` margins consume these tokens; prefer \`rhythm\` for vertical flow
|
|
697
|
+
rather than per-element margins.
|
|
698
|
+
|
|
699
|
+
## Typography
|
|
700
|
+
|
|
701
|
+
Rule: **\`font-<role>\`** — role ∈ ${code(typeRoles)}. Each role carries its own family,
|
|
702
|
+
size (from the type scale ${code(typeNames)}), tracking, transform, and weight.
|
|
703
|
+
Families: ${code(families)} (\`--font-*\`).
|
|
704
|
+
|
|
705
|
+
## Colour
|
|
706
|
+
|
|
707
|
+
Rule: **\`<util>-<color>\`** — util ∈ ${code(utils)}; \`<color>\` is either a **named ramp**
|
|
708
|
+
step or a **semantic role**.
|
|
709
|
+
|
|
710
|
+
- Named ramps: ${code(ramps)}. Steps: base (no suffix) + ${code(nonBase)} — e.g. \`bg-pine\`
|
|
711
|
+
(base), \`text-navy-xl\`, \`border-grey-d\`.
|
|
712
|
+
- Semantic roles: ${code(roles)} — e.g. \`bg-brand-primary\`, \`text-danger\`. Roles remap
|
|
713
|
+
automatically under \`:root[data-brx-theme="dark"]\`.
|
|
714
|
+
|
|
715
|
+
## Shadows
|
|
716
|
+
|
|
717
|
+
Rule: **\`drop-shadow-<size>\`** — size ∈ ${code(shadows)} (applied as a \`filter\`, so it
|
|
718
|
+
follows non-rectangular shapes).
|
|
719
|
+
|
|
720
|
+
## Animation
|
|
721
|
+
|
|
722
|
+
Rule: **\`<effect>\`** (with the state/flip prefixes above) — effect ∈ ${code(effects)}.
|
|
723
|
+
Composed **journeys** chain multiple effects into one entrance: rule \`<parts>-journey\`
|
|
724
|
+
(e.g. \`fade-slide-journey\`, \`fade-scale-blur-journey\`). All require \`transition\` on the
|
|
725
|
+
element; scroll-linked entrances resolve within the first portion of the element's scroll.
|
|
726
|
+
|
|
727
|
+
## Component patterns
|
|
728
|
+
|
|
729
|
+
Each pattern is a base class \`<pattern>\` with interaction states (hover/active/focus-visible)
|
|
730
|
+
baked in; coloured patterns add role variants via rule **\`<pattern>-<role>\`**.
|
|
731
|
+
|
|
732
|
+
- Patterns: ${code(patterns)}.
|
|
733
|
+
- Roles (for coloured patterns like \`badge\`, \`tag\`, \`status\`, \`button\`):
|
|
734
|
+
${code(patternRoles)} — e.g. \`badge-success\`, \`button-danger\`. The default (unsuffixed)
|
|
735
|
+
variant uses the brand-primary role.
|
|
736
|
+
- Shape primitives: \`--br-<name>\` radii — ${code(radii)}.
|
|
737
|
+
`;
|
|
738
|
+
}
|
|
739
|
+
function renderTopIndex() {
|
|
740
|
+
return `${INDEX_NOTE}
|
|
741
|
+
|
|
742
|
+
# Vitops design system
|
|
743
|
+
|
|
744
|
+
LLM-oriented documentation bundle (Open Knowledge Format) for the Vitops design system — a
|
|
745
|
+
variable-driven CSS framework plus progressively-enhanced web components, generated from
|
|
746
|
+
\`design-system.json\` and published alongside the deployed theme.
|
|
747
|
+
|
|
748
|
+
# Contents
|
|
749
|
+
|
|
750
|
+
* [CSS framework](css/) - the class vocabulary (colour, type, space, layout, animation, component patterns), stated as naming rules
|
|
751
|
+
* [Bricks Builder](bricks/) - custom elements and how to style them
|
|
752
|
+
`;
|
|
753
|
+
}
|
|
754
|
+
function renderCssIndex() {
|
|
755
|
+
return `${INDEX_NOTE}
|
|
756
|
+
|
|
757
|
+
# CSS framework
|
|
758
|
+
|
|
759
|
+
# Contents
|
|
760
|
+
|
|
761
|
+
* [Class vocabulary](classes.md) - every utility and component class, summarized by naming rule
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
function renderBricksIndex() {
|
|
765
|
+
return `${INDEX_NOTE}
|
|
766
|
+
|
|
767
|
+
# Bricks Builder integration
|
|
768
|
+
|
|
769
|
+
# Styling
|
|
770
|
+
|
|
771
|
+
Prefer adding the framework **CSS classes** — see [/css/classes.md](../css/classes.md) — in a
|
|
772
|
+
Bricks element's "CSS classes" field over hand-setting spacing, colour, or typography through
|
|
773
|
+
the Bricks Builder property panels. The classes encode the design system's tokens, responsive
|
|
774
|
+
grammar, and interaction states, so they stay consistent, respond to theme / dark mode, and
|
|
775
|
+
update with the system. Values typed directly into Bricks' property controls are one-off,
|
|
776
|
+
drift from the system, and don't react to theme changes — reserve them for genuinely bespoke,
|
|
777
|
+
one-element cases.
|
|
778
|
+
|
|
779
|
+
# Contents
|
|
780
|
+
|
|
781
|
+
* [Elements reference](elements.md) - every custom Vitops element, its controls, defaults, seeded children and keywords
|
|
782
|
+
* [Class vocabulary](../css/classes.md) - the framework CSS classes to apply to elements
|
|
783
|
+
`;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Build the OKF docs bundle for a config. Returns a `{ relPath: content }` map;
|
|
787
|
+
* the caller writes it wherever the bundle should live (e.g. `<outDir>/docs`).
|
|
788
|
+
*/
|
|
789
|
+
function generateDocs(ds, assetsDir) {
|
|
790
|
+
const elementsDir = join(assetsDir, "bricks", "elements");
|
|
791
|
+
return {
|
|
792
|
+
"index.md": renderTopIndex(),
|
|
793
|
+
"css/index.md": renderCssIndex(),
|
|
794
|
+
"css/classes.md": renderCssClasses(ds),
|
|
795
|
+
"bricks/index.md": renderBricksIndex(),
|
|
796
|
+
"bricks/elements.md": renderElementsDoc(elementsDir)
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/generate.ts
|
|
802
|
+
/**
|
|
803
|
+
* Pure, parameterized port of the Vitops design-system generator.
|
|
804
|
+
*
|
|
805
|
+
* `generate({ input, format, outDir })` reads a consumer's `design-system.json`,
|
|
806
|
+
* validates it, and emits consumer-ready output for the chosen format into
|
|
807
|
+
* `outDir`. Unlike the original CWD-bound script it mutates no shared state: every
|
|
808
|
+
* generated token layer is built in memory and either bundled (css/bricks) or
|
|
809
|
+
* inlined (tailwind) before writing.
|
|
810
|
+
*
|
|
811
|
+
* Framework-static assets (the hand-written CSS partials, the Bricks PHP + loader,
|
|
812
|
+
* and the pre-built JS bundles) live under `assets/` in this package and are read /
|
|
813
|
+
* copied from there — they are the same for every consumer.
|
|
814
|
+
*/
|
|
815
|
+
const DEFAULT_ASSETS = join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
|
|
816
|
+
const STEP_ORDER = [
|
|
817
|
+
"xxd",
|
|
818
|
+
"xd",
|
|
819
|
+
"d",
|
|
820
|
+
"base",
|
|
821
|
+
"l",
|
|
822
|
+
"xl",
|
|
823
|
+
"xxl"
|
|
824
|
+
];
|
|
825
|
+
const suffix = (step) => step === "base" ? "" : `-${step}`;
|
|
826
|
+
const MIRROR = {
|
|
827
|
+
xxd: "xxl",
|
|
828
|
+
xd: "xl",
|
|
829
|
+
d: "l",
|
|
830
|
+
base: "base",
|
|
831
|
+
l: "d",
|
|
832
|
+
xl: "xd",
|
|
833
|
+
xxl: "xxd"
|
|
834
|
+
};
|
|
835
|
+
const STEP_INDEX = Object.fromEntries(STEP_ORDER.map((s, i) => [s, i]));
|
|
836
|
+
const shiftStep = (step, by) => {
|
|
837
|
+
const i = STEP_INDEX[step] ?? STEP_ORDER.indexOf("base");
|
|
838
|
+
return STEP_ORDER[Math.max(0, Math.min(STEP_ORDER.length - 1, i - by))];
|
|
839
|
+
};
|
|
840
|
+
const BASE_STEP = "d";
|
|
841
|
+
const UTILITY_PROPS = {
|
|
842
|
+
bg: "background-color",
|
|
843
|
+
text: "color",
|
|
844
|
+
border: "border-color",
|
|
845
|
+
outline: "outline-color",
|
|
846
|
+
fill: "fill",
|
|
847
|
+
stroke: "stroke"
|
|
848
|
+
};
|
|
849
|
+
const roleRamp = (spec, inherited = "") => typeof spec === "string" ? spec : spec.ramp ?? inherited;
|
|
850
|
+
const resolveSlot = (spec, step, inherited) => {
|
|
851
|
+
if (typeof spec === "string") return {
|
|
852
|
+
ramp: spec,
|
|
853
|
+
step
|
|
854
|
+
};
|
|
855
|
+
if (spec.value?.[step] != null) return { value: spec.value[step] };
|
|
856
|
+
return {
|
|
857
|
+
ramp: spec.ramp ?? inherited,
|
|
858
|
+
step: spec.steps?.[step] ?? shiftStep(spec.invert ? MIRROR[step] : step, spec.shift ?? 0)
|
|
859
|
+
};
|
|
860
|
+
};
|
|
861
|
+
const srcVar = (s) => "value" in s ? s.value : `var(--color-${s.ramp}${suffix(s.step)})`;
|
|
862
|
+
const srcEq = (a, b) => "value" in a || "value" in b ? "value" in a && "value" in b && a.value === b.value : a.ramp === b.ramp && a.step === b.step;
|
|
863
|
+
const decls = (obj, indent = " ") => Object.entries(obj).map(([k, v]) => `${indent}${k}: ${v};`).join("\n");
|
|
864
|
+
const round = (n) => Number(n.toFixed(4)).toString();
|
|
865
|
+
const rem = (v) => parseFloat(v);
|
|
866
|
+
const buildScale = (s) => {
|
|
867
|
+
const names = s.names ?? Array.from({ length: s.steps ?? 0 }, (_, i) => String(i + 1));
|
|
868
|
+
const base = rem(s.base);
|
|
869
|
+
const baseStep = s.baseStep ?? Math.ceil(names.length / 2);
|
|
870
|
+
const baselineIdx = s.baseline ? names.indexOf(s.baseline) : baseStep - 1;
|
|
871
|
+
return names.map((name, i) => {
|
|
872
|
+
const k = i - (baseStep - 1);
|
|
873
|
+
const vMax = base * Math.pow(s.ratio, k);
|
|
874
|
+
const max = `${round(vMax)}rem`;
|
|
875
|
+
let value = max;
|
|
876
|
+
if (s.fluid) {
|
|
877
|
+
const { minVw, maxVw, minRatio } = s.fluid;
|
|
878
|
+
const vMin = base * Math.pow(minRatio, k);
|
|
879
|
+
const lo = Math.min(vMin, vMax);
|
|
880
|
+
const hi = Math.max(vMin, vMax);
|
|
881
|
+
if (lo !== hi) {
|
|
882
|
+
const slope = (vMax - vMin) / (rem(maxVw) - rem(minVw));
|
|
883
|
+
const intercept = vMin - slope * rem(minVw);
|
|
884
|
+
const sign = slope < 0 ? "-" : "+";
|
|
885
|
+
const pref = `calc(${round(intercept)}rem ${sign} ${round(Math.abs(slope * 100))}vw)`;
|
|
886
|
+
value = `clamp(${round(lo)}rem, ${pref}, ${round(hi)}rem)`;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return {
|
|
890
|
+
name,
|
|
891
|
+
value,
|
|
892
|
+
max,
|
|
893
|
+
offset: i - baselineIdx
|
|
894
|
+
};
|
|
895
|
+
});
|
|
896
|
+
};
|
|
897
|
+
const TYPO_KEYMAP = {
|
|
898
|
+
family: ["font-family", "ff"],
|
|
899
|
+
size: ["font-size", "fs"],
|
|
900
|
+
weight: ["font-weight", "fw"],
|
|
901
|
+
style: ["font-style", "fst"],
|
|
902
|
+
"line-height": ["line-height", "lh"],
|
|
903
|
+
tracking: ["letter-spacing", "ls"],
|
|
904
|
+
"text-decoration": ["text-decoration", "td"],
|
|
905
|
+
"text-transform": ["text-transform", "tt"],
|
|
906
|
+
"text-wrap": ["text-wrap", "tw"],
|
|
907
|
+
color: ["color", "color"]
|
|
908
|
+
};
|
|
909
|
+
const TYPO_IDENTITY = {
|
|
910
|
+
"font-style": "normal",
|
|
911
|
+
"letter-spacing": "normal",
|
|
912
|
+
"text-decoration": "none",
|
|
913
|
+
"text-transform": "none",
|
|
914
|
+
"text-wrap": "wrap"
|
|
915
|
+
};
|
|
916
|
+
const STATE_DEFAULT = {
|
|
917
|
+
opacity: "1",
|
|
918
|
+
"translate-x": "0",
|
|
919
|
+
"translate-y": "0",
|
|
920
|
+
"scale-x": "1",
|
|
921
|
+
"scale-y": "1",
|
|
922
|
+
rotate: "0deg",
|
|
923
|
+
blur: "0px",
|
|
924
|
+
shadow: "0 0 0 transparent",
|
|
925
|
+
brightness: "1",
|
|
926
|
+
grayscale: "0",
|
|
927
|
+
contrast: "1",
|
|
928
|
+
saturate: "1",
|
|
929
|
+
sepia: "0",
|
|
930
|
+
"hue-rotate": "0deg",
|
|
931
|
+
clip: "none"
|
|
932
|
+
};
|
|
933
|
+
const BASE_HOOK = {
|
|
934
|
+
padding: "p",
|
|
935
|
+
"border-radius": "br",
|
|
936
|
+
border: "b",
|
|
937
|
+
"box-shadow": "ds",
|
|
938
|
+
"font-size": "fs"
|
|
939
|
+
};
|
|
940
|
+
const id = (key) => createHash("sha256").update(key).digest("hex").slice(0, 6);
|
|
941
|
+
function build(ds, format, assetsDir) {
|
|
942
|
+
const BRICKS = format === "bricks";
|
|
943
|
+
const palette = ds.colors.palette;
|
|
944
|
+
const schemes = ds.colors.schemes;
|
|
945
|
+
const base = schemes.default;
|
|
946
|
+
if (base == null) throw new Error("colors.schemes must include a \"default\" scheme");
|
|
947
|
+
const defaultSemantic = base.semantic;
|
|
948
|
+
const altSchemes = Object.entries(schemes).filter(([k]) => k !== "default");
|
|
949
|
+
const darkScheme = altSchemes.map(([, s]) => s).find((s) => s.appearance === "dark");
|
|
950
|
+
const patterns = ds.patterns;
|
|
951
|
+
const shadows = ds.shadows;
|
|
952
|
+
const typography = ds.typography;
|
|
953
|
+
const UTILITIES = (ds.colors.utilities ?? [
|
|
954
|
+
"bg",
|
|
955
|
+
"text",
|
|
956
|
+
"border"
|
|
957
|
+
]).filter((u) => u in UTILITY_PROPS);
|
|
958
|
+
const generated = {};
|
|
959
|
+
if (BRICKS) generated["color.css"] = "/* Colours provided by Bricks (palette import generates tokens + utilities). */\n";
|
|
960
|
+
else {
|
|
961
|
+
let css = `/* GENERATED from design-system.json — do not edit by hand. */\n:root {\n`;
|
|
962
|
+
css += ` color-scheme: ${base.appearance};\n`;
|
|
963
|
+
css += ` /* Palette ramps */\n`;
|
|
964
|
+
for (const [name, steps] of Object.entries(palette)) for (const step of STEP_ORDER) if (steps[step] != null) css += ` --color-${name}${suffix(step)}: ${steps[step]};\n`;
|
|
965
|
+
css += `\n /* Semantic roles → ramps (default: ${base.appearance}) */\n`;
|
|
966
|
+
for (const [role, spec] of Object.entries(defaultSemantic)) {
|
|
967
|
+
const ramp = roleRamp(spec);
|
|
968
|
+
for (const step of STEP_ORDER) if (palette[ramp]?.[step] != null) css += ` --color-${role}${suffix(step)}: ${srcVar(resolveSlot(spec, step, ramp))};\n`;
|
|
969
|
+
}
|
|
970
|
+
css += `}\n\n`;
|
|
971
|
+
for (const [key, scheme] of altSchemes) {
|
|
972
|
+
let block = "";
|
|
973
|
+
for (const [role, spec] of Object.entries(scheme.semantic)) {
|
|
974
|
+
const baseSpec = defaultSemantic[role];
|
|
975
|
+
if (baseSpec == null) continue;
|
|
976
|
+
const ramp = roleRamp(baseSpec);
|
|
977
|
+
for (const step of STEP_ORDER) {
|
|
978
|
+
if (palette[ramp]?.[step] == null) continue;
|
|
979
|
+
const alt = resolveSlot(spec, step, ramp);
|
|
980
|
+
if (srcEq(alt, resolveSlot(baseSpec, step, ramp))) continue;
|
|
981
|
+
block += ` --color-${role}${suffix(step)}: ${srcVar(alt)};\n`;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (block) {
|
|
985
|
+
css += `/* Semantic roles → ramps (${key}: ${scheme.appearance}) */\n`;
|
|
986
|
+
css += `:root[data-brx-theme="${scheme.appearance}"] {\n color-scheme: ${scheme.appearance};\n${block}}\n\n`;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const allTokens = [];
|
|
990
|
+
for (const [name, steps] of Object.entries(palette)) for (const step of STEP_ORDER) if (steps[step] != null) allTokens.push(`${name}${suffix(step)}`);
|
|
991
|
+
for (const [role, spec] of Object.entries(defaultSemantic)) {
|
|
992
|
+
const ramp = roleRamp(spec);
|
|
993
|
+
for (const step of STEP_ORDER) if (palette[ramp]?.[step] != null) allTokens.push(`${role}${suffix(step)}`);
|
|
994
|
+
}
|
|
995
|
+
css += `/* Colour utilities — ${UTILITIES.join(", ")} */\n`;
|
|
996
|
+
for (const token of allTokens) css += UTILITIES.map((cls) => `.${cls}-${token} { ${UTILITY_PROPS[cls]}: var(--color-${token}); }`).join("\n") + "\n";
|
|
997
|
+
generated["color.css"] = css;
|
|
998
|
+
}
|
|
999
|
+
{
|
|
1000
|
+
const entries = Object.entries(shadows ?? {});
|
|
1001
|
+
let sh = `/* GENERATED drop-shadow tokens + utilities — do not edit by hand. */\n`;
|
|
1002
|
+
if (entries.length) {
|
|
1003
|
+
sh += `:root {\n`;
|
|
1004
|
+
for (const [name, value] of entries) sh += ` --shadow-${name}: ${value};\n`;
|
|
1005
|
+
sh += `}\n\n`;
|
|
1006
|
+
for (const [name] of entries) sh += `.drop-shadow-${name} { filter: drop-shadow(var(--shadow-${name})); }\n`;
|
|
1007
|
+
}
|
|
1008
|
+
generated["shadows.css"] = sh;
|
|
1009
|
+
}
|
|
1010
|
+
const typeSteps = ds.typeScale ? buildScale(ds.typeScale) : [];
|
|
1011
|
+
const spaceSteps = ds.spaceScale ? buildScale(ds.spaceScale) : [];
|
|
1012
|
+
const textMax = Object.fromEntries(typeSteps.map((s) => [`text-${s.name}`, s.max]));
|
|
1013
|
+
const typographyFamilies = typography?.families ?? {};
|
|
1014
|
+
const withScaleFallback = (v) => v.replace(/var\(\s*(--text-[\w-]+)\s*\)/g, (m, name) => textMax[name.slice(2)] ? `var(${name}, ${textMax[name.slice(2)]})` : m);
|
|
1015
|
+
const roleDecls = (role, spec) => {
|
|
1016
|
+
const out = {};
|
|
1017
|
+
for (const [key, [prop, sfx]] of Object.entries(TYPO_KEYMAP)) {
|
|
1018
|
+
const identity = TYPO_IDENTITY[prop];
|
|
1019
|
+
if (spec[key] == null && identity == null) continue;
|
|
1020
|
+
out[prop] = `var(--${role}-${sfx}, ${spec[key] == null ? identity : key === "family" ? typographyFamilies[spec.family] ?? String(spec.family) : key === "size" ? withScaleFallback(String(spec[key])) : String(spec[key])})`;
|
|
1021
|
+
}
|
|
1022
|
+
return out;
|
|
1023
|
+
};
|
|
1024
|
+
let twTypeTokensCss = "";
|
|
1025
|
+
if (BRICKS) generated["type-tokens.css"] = "/* Fonts + type/space scales provided by Bricks (Font + Variables managers). */\n";
|
|
1026
|
+
else {
|
|
1027
|
+
const root = {};
|
|
1028
|
+
for (const [name, stack] of Object.entries(ds.fonts ?? {})) root[`--font-${name}`] = stack;
|
|
1029
|
+
for (const s of typeSteps) root[`--text-${s.name}`] = s.value;
|
|
1030
|
+
for (const s of spaceSteps) root[`--space-${s.name}`] = s.value;
|
|
1031
|
+
const css = `/* GENERATED font families + fluid type/space scales — do not edit by hand. */\n:root {\n${decls(root)}\n}\n`;
|
|
1032
|
+
twTypeTokensCss = css;
|
|
1033
|
+
generated["type-tokens.css"] = css;
|
|
1034
|
+
}
|
|
1035
|
+
let twPatternTokensCss = "";
|
|
1036
|
+
{
|
|
1037
|
+
const tDefaults = patterns?.defaults ?? {};
|
|
1038
|
+
const tRadii = patterns?.radii ?? {};
|
|
1039
|
+
const tGroups = patterns?.groups ?? {};
|
|
1040
|
+
const tZ = patterns?.z ?? {};
|
|
1041
|
+
const tItems = patterns?.items ?? {};
|
|
1042
|
+
const root = {};
|
|
1043
|
+
for (const [prop, val] of Object.entries(tDefaults)) root[`--${prop}-default`] = val;
|
|
1044
|
+
if (!BRICKS) for (const [name, val] of Object.entries(tRadii)) root[`--br-${name}`] = val;
|
|
1045
|
+
for (const [group, props] of Object.entries(tGroups)) for (const [prop, val] of Object.entries(props)) root[`--${prop}-${group}`] = val;
|
|
1046
|
+
for (const [name, n] of Object.entries(tZ)) root[`--z-tier-${name}`] = String(n);
|
|
1047
|
+
const aliases = {};
|
|
1048
|
+
for (const [name, p] of Object.entries(tItems)) {
|
|
1049
|
+
if (!p.group) continue;
|
|
1050
|
+
for (const prop of Object.keys(tGroups[p.group] ?? {})) aliases[`--${prop}-${name}-group`] = `var(--${prop}-${p.group})`;
|
|
1051
|
+
for (const [prop, val] of Object.entries(p.overrides ?? {})) aliases[`--${prop}-${name}-group`] = val;
|
|
1052
|
+
}
|
|
1053
|
+
let tok = `/* GENERATED pattern token cascade — do not edit by hand. */\n:root {\n`;
|
|
1054
|
+
tok += decls(root) + "\n";
|
|
1055
|
+
if (Object.keys(aliases).length) tok += "\n" + decls(aliases) + "\n";
|
|
1056
|
+
tok += `}\n`;
|
|
1057
|
+
twPatternTokensCss = tok;
|
|
1058
|
+
generated["tokens.css"] = tok;
|
|
1059
|
+
}
|
|
1060
|
+
{
|
|
1061
|
+
const roles = typography?.roles ?? {};
|
|
1062
|
+
const headings = typography?.headings ?? {};
|
|
1063
|
+
let typo = `/* GENERATED typography roles — do not edit by hand. */\n`;
|
|
1064
|
+
for (const [role, spec] of Object.entries(roles)) typo += `.font-${role} {\n${decls(roleDecls(role, spec))}\n}\n`;
|
|
1065
|
+
for (const [tag, role] of Object.entries(headings)) if (roles[role]) typo += `${tag} {\n${decls(roleDecls(role, roles[role]))}\n}\n`;
|
|
1066
|
+
generated["typography.css"] = typo;
|
|
1067
|
+
}
|
|
1068
|
+
const stateRules = (sel, role, states, colorProp = "background-color") => {
|
|
1069
|
+
let out = "";
|
|
1070
|
+
const roleVar = (step) => role ? `var(--color-${role}-${step})` : "";
|
|
1071
|
+
for (const [state, spec] of Object.entries(states)) {
|
|
1072
|
+
const body = [];
|
|
1073
|
+
if (typeof spec.step === "number" && role) body.push(`${colorProp}: ${roleVar(shiftStep(BASE_STEP, spec.step))};`);
|
|
1074
|
+
if (typeof spec.scale === "number") body.push(`scale: ${spec.scale};`);
|
|
1075
|
+
if (typeof spec.lift === "string") body.push(`translate: 0 calc(-1 * ${spec.lift});`);
|
|
1076
|
+
if (typeof spec.shadow === "string" && shadows?.[spec.shadow]) body.push(`filter: drop-shadow(var(--shadow-${spec.shadow}));`);
|
|
1077
|
+
else if (spec.shadow === true) body.push(`box-shadow: var(--lift-shadow, 0 8px 20px -6px rgb(0 0 0 / 0.25));`);
|
|
1078
|
+
if (spec.ring === true) {
|
|
1079
|
+
const ringColor = role ? `var(--color-${role}-l)` : `var(--color-brand-primary-l)`;
|
|
1080
|
+
body.push(`outline: none;`, `box-shadow: 0 0 0 3px ${ringColor};`);
|
|
1081
|
+
}
|
|
1082
|
+
if (spec.css && typeof spec.css === "object") for (const [k, v] of Object.entries(spec.css)) body.push(`${k}: ${v};`);
|
|
1083
|
+
if (!body.length) continue;
|
|
1084
|
+
const rule = `${sel}${state === "hover" ? ":hover" : state === "active" ? ":active" : state === "focus-visible" ? ":focus-visible" : `:${state}`} {\n ${body.join("\n ")}\n}\n`;
|
|
1085
|
+
out += state === "hover" ? `@media (hover: hover) {\n${rule}}\n` : rule;
|
|
1086
|
+
}
|
|
1087
|
+
return out;
|
|
1088
|
+
};
|
|
1089
|
+
let pat = `/* GENERATED component patterns — do not edit by hand. */\n`;
|
|
1090
|
+
pat += `/* Interaction transitions for patterned elements (independent props compose). */\n`;
|
|
1091
|
+
for (const [pname, p] of Object.entries(patterns?.items ?? {})) {
|
|
1092
|
+
if (!p.base) continue;
|
|
1093
|
+
const states = p.states ?? {};
|
|
1094
|
+
const isElement = !!p.element;
|
|
1095
|
+
const defaultSel = isElement ? `:where(${p.element})` : `.${p.class ?? pname}`;
|
|
1096
|
+
const defaultRole = p.default_role ?? null;
|
|
1097
|
+
const base = { ...p.base };
|
|
1098
|
+
const fills = pname === "button" || pname === "badge" || base["background-color"] != null || base["background"] != null;
|
|
1099
|
+
const colorProp = fills ? "background-color" : "color";
|
|
1100
|
+
pat += `${defaultSel} {\n`;
|
|
1101
|
+
if (Object.keys(states).length > 0) {
|
|
1102
|
+
pat += ` transition-property: translate, scale, rotate, filter, box-shadow, background-color, color;\n`;
|
|
1103
|
+
pat += ` transition-duration: var(--interact-duration, 200ms);\n`;
|
|
1104
|
+
pat += ` transition-timing-function: var(--interact-easing, ease);\n`;
|
|
1105
|
+
pat += ` translate: 0 0; scale: 1; rotate: 0deg;\n`;
|
|
1106
|
+
}
|
|
1107
|
+
if (defaultRole && fills && base["background-color"] == null && base["background"] == null) base["background-color"] = `var(--color-${defaultRole}-${BASE_STEP})`;
|
|
1108
|
+
const wrappedBase = {};
|
|
1109
|
+
for (const [prop, val] of Object.entries(base)) {
|
|
1110
|
+
const sfx = BASE_HOOK[prop];
|
|
1111
|
+
wrappedBase[prop] = sfx ? `var(--${sfx}-${pname}, ${val})` : String(val);
|
|
1112
|
+
}
|
|
1113
|
+
pat += decls(wrappedBase) + "\n}\n";
|
|
1114
|
+
pat += stateRules(defaultSel, defaultRole, states, colorProp);
|
|
1115
|
+
for (const role of p.roles ?? []) {
|
|
1116
|
+
const variantSel = isElement ? `${p.element}.${role}` : `.${p.class ?? pname}-${role}`;
|
|
1117
|
+
const variantColorDecl = fills ? `background-color: var(--color-${role}-${BASE_STEP})` : `color: var(--color-${role}-${BASE_STEP})`;
|
|
1118
|
+
pat += `${variantSel} { ${variantColorDecl}; }\n`;
|
|
1119
|
+
pat += stateRules(variantSel, role, states, colorProp);
|
|
1120
|
+
}
|
|
1121
|
+
pat += "\n";
|
|
1122
|
+
}
|
|
1123
|
+
generated["patterns.css"] = pat;
|
|
1124
|
+
{
|
|
1125
|
+
const anim = ds.animations ?? {};
|
|
1126
|
+
const asStr = (v) => String(v);
|
|
1127
|
+
const block = (sel, d) => `.${sel} {\n${decls(d)}\n}\n`;
|
|
1128
|
+
let out = `/* GENERATED animation effects + journeys — do not edit by hand. */\n`;
|
|
1129
|
+
for (const [name, e] of Object.entries(anim.effects ?? {})) {
|
|
1130
|
+
const d = { "--_anim": e.kf };
|
|
1131
|
+
for (const [k, v] of Object.entries(e.css ?? {})) d[k] = asStr(v);
|
|
1132
|
+
for (const [k, v] of Object.entries(e.vars ?? {})) d[`--${k}`] = asStr(v);
|
|
1133
|
+
out += block(name, d);
|
|
1134
|
+
}
|
|
1135
|
+
const base = anim.journeys?.base ?? {};
|
|
1136
|
+
for (const parts of anim.journeys?.compose ?? []) {
|
|
1137
|
+
const d = {
|
|
1138
|
+
"--_anim": parts.map((p) => `${p}-journey`).join(", "),
|
|
1139
|
+
"animation-range": "entry exit"
|
|
1140
|
+
};
|
|
1141
|
+
for (const p of parts) for (const [k, v] of Object.entries(base[p] ?? {})) d[`--${k}`] = asStr(v);
|
|
1142
|
+
out += block(`${parts.join("-")}-journey`, d);
|
|
1143
|
+
}
|
|
1144
|
+
const STATES = [
|
|
1145
|
+
["hover", (s) => `.${s}:hover`],
|
|
1146
|
+
["focus", (s) => `.${s}:focus-visible`],
|
|
1147
|
+
["active", (s) => `.${s}.is-active, .${s}[data-active]`]
|
|
1148
|
+
];
|
|
1149
|
+
out += `\n/* State-flip variants — compose with .transition (hover-/focus-/active-<fx>). */\n`;
|
|
1150
|
+
for (const [name, e] of Object.entries(anim.effects ?? {})) {
|
|
1151
|
+
if (e.kf !== "composite" && e.kf !== "paint") continue;
|
|
1152
|
+
const props = {};
|
|
1153
|
+
for (const [k, v] of Object.entries(e.vars ?? {})) {
|
|
1154
|
+
const m = /^(.*)-(from|to)$/.exec(k);
|
|
1155
|
+
if (m) (props[m[1]] ??= {})[m[2]] = asStr(v);
|
|
1156
|
+
}
|
|
1157
|
+
const rest = {};
|
|
1158
|
+
for (const [p, ft] of Object.entries(props)) {
|
|
1159
|
+
if (ft.from != null && ft.from !== STATE_DEFAULT[p]) rest[`--${p}-from`] = ft.from;
|
|
1160
|
+
if (ft.to != null && ft.to !== STATE_DEFAULT[p]) rest[`--${p}-to`] = ft.to;
|
|
1161
|
+
}
|
|
1162
|
+
const flip = Object.fromEntries(Object.keys(props).map((p) => [`--t-${p}`, `var(--${p}-to, ${STATE_DEFAULT[p]})`]));
|
|
1163
|
+
if (Object.keys(rest).length) out += `${STATES.map(([s]) => `.${s}-${name}`).join(", ")} {\n${decls(rest)}\n}\n`;
|
|
1164
|
+
for (const [s, sel] of STATES) out += `${sel(`${s}-${name}`)} {\n${decls(flip)}\n}\n`;
|
|
1165
|
+
}
|
|
1166
|
+
generated["animation-effects.css"] = out;
|
|
1167
|
+
}
|
|
1168
|
+
const namedColors = [];
|
|
1169
|
+
for (const [name, steps] of Object.entries(palette)) for (const step of STEP_ORDER) {
|
|
1170
|
+
const hex = steps[step];
|
|
1171
|
+
if (hex == null) continue;
|
|
1172
|
+
const token = `${name}${suffix(step)}`;
|
|
1173
|
+
namedColors.push({
|
|
1174
|
+
raw: `var(--color-${token})`,
|
|
1175
|
+
light: hex,
|
|
1176
|
+
id: id(`named:${token}`),
|
|
1177
|
+
utilityClasses: UTILITIES
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
const semanticColors = [];
|
|
1181
|
+
for (const [role, spec] of Object.entries(defaultSemantic)) {
|
|
1182
|
+
const ramp = roleRamp(spec);
|
|
1183
|
+
const altSpec = darkScheme?.semantic[role];
|
|
1184
|
+
for (const step of STEP_ORDER) {
|
|
1185
|
+
if (palette[ramp]?.[step] == null) continue;
|
|
1186
|
+
const lightSrc = resolveSlot(spec, step, ramp);
|
|
1187
|
+
const darkSrc = altSpec != null ? resolveSlot(altSpec, step, ramp) : lightSrc;
|
|
1188
|
+
const hasDark = !srcEq(darkSrc, lightSrc);
|
|
1189
|
+
semanticColors.push({
|
|
1190
|
+
raw: `var(--color-${role}${suffix(step)})`,
|
|
1191
|
+
light: srcVar(lightSrc),
|
|
1192
|
+
darkModeEnabled: hasDark,
|
|
1193
|
+
dark: srcVar(darkSrc),
|
|
1194
|
+
id: id(`semantic:${role}${suffix(step)}`),
|
|
1195
|
+
utilityClasses: UTILITIES
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
generated,
|
|
1201
|
+
bricksColorsNamed: JSON.stringify({
|
|
1202
|
+
id: id("palette:named"),
|
|
1203
|
+
name: "Named",
|
|
1204
|
+
colors: namedColors
|
|
1205
|
+
}, null, 2),
|
|
1206
|
+
bricksColorsSemantic: JSON.stringify({
|
|
1207
|
+
id: id("palette:semantic"),
|
|
1208
|
+
name: "Semantic",
|
|
1209
|
+
colors: semanticColors
|
|
1210
|
+
}, null, 2),
|
|
1211
|
+
bricksVariables: (() => {
|
|
1212
|
+
const scaleMeta = (scope, prefix, s, names) => {
|
|
1213
|
+
const px = Math.round(rem(s.base) * 16);
|
|
1214
|
+
const lo = s.fluid?.minRatio ?? s.ratio;
|
|
1215
|
+
return {
|
|
1216
|
+
scaleScope: scope,
|
|
1217
|
+
scaleType: "tshirt",
|
|
1218
|
+
scaleNames: names,
|
|
1219
|
+
prefix,
|
|
1220
|
+
minFontSize: px,
|
|
1221
|
+
minScaleRatio: lo,
|
|
1222
|
+
minScaleRatioSelect: lo,
|
|
1223
|
+
maxFontSize: px,
|
|
1224
|
+
maxScaleRatio: s.ratio,
|
|
1225
|
+
maxScaleRatioSelect: s.ratio,
|
|
1226
|
+
baseline: s.baseline ?? names[Math.floor((names.length - 1) / 2)]
|
|
1227
|
+
};
|
|
1228
|
+
};
|
|
1229
|
+
const categories = [];
|
|
1230
|
+
const variables = [];
|
|
1231
|
+
const addScale = (label, scope, prefix, cfg, steps) => {
|
|
1232
|
+
if (!cfg || !steps.length) return;
|
|
1233
|
+
const names = steps.map((s) => s.name);
|
|
1234
|
+
const catId = id(`varcat:${scope}`);
|
|
1235
|
+
categories.push({
|
|
1236
|
+
id: catId,
|
|
1237
|
+
name: label,
|
|
1238
|
+
scale: scaleMeta(scope, prefix, cfg, names),
|
|
1239
|
+
utilityClasses: []
|
|
1240
|
+
});
|
|
1241
|
+
for (const st of steps) variables.push({
|
|
1242
|
+
id: id(`var:${prefix}${st.name}`),
|
|
1243
|
+
name: `${prefix}${st.name}`,
|
|
1244
|
+
value: st.value,
|
|
1245
|
+
category: catId,
|
|
1246
|
+
scale: {
|
|
1247
|
+
scale: st.offset,
|
|
1248
|
+
scaleName: st.name
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
};
|
|
1252
|
+
addScale("Typography", "typography", "text-", ds.typeScale, typeSteps);
|
|
1253
|
+
addScale("Spacing", "spacing", "space-", ds.spaceScale, spaceSteps);
|
|
1254
|
+
for (const [name, stack] of Object.entries(ds.fonts ?? {})) variables.push({
|
|
1255
|
+
id: id(`var:font-${name}`),
|
|
1256
|
+
name: `font-${name}`,
|
|
1257
|
+
value: stack
|
|
1258
|
+
});
|
|
1259
|
+
for (const [name, val] of Object.entries(ds.patterns?.radii ?? {})) variables.push({
|
|
1260
|
+
id: id(`var:br-${name}`),
|
|
1261
|
+
name: `br-${name}`,
|
|
1262
|
+
value: val
|
|
1263
|
+
});
|
|
1264
|
+
return JSON.stringify({
|
|
1265
|
+
variables,
|
|
1266
|
+
categories
|
|
1267
|
+
}, null, 2);
|
|
1268
|
+
})(),
|
|
1269
|
+
tokensJson: JSON.stringify({
|
|
1270
|
+
colors: {
|
|
1271
|
+
palette,
|
|
1272
|
+
semantic: Object.fromEntries(Object.entries(defaultSemantic).map(([role, spec]) => [role, roleRamp(spec)]))
|
|
1273
|
+
},
|
|
1274
|
+
fonts: ds.fonts ?? {},
|
|
1275
|
+
text: Object.fromEntries(typeSteps.map((s) => [s.name, s.value])),
|
|
1276
|
+
spacing: Object.fromEntries(spaceSteps.map((s) => [s.name, s.value])),
|
|
1277
|
+
shadows: shadows ?? {},
|
|
1278
|
+
radii: ds.patterns?.radii ?? {}
|
|
1279
|
+
}, null, 2),
|
|
1280
|
+
designManifest: (() => {
|
|
1281
|
+
const reverseIndex = {};
|
|
1282
|
+
for (const [name, steps] of Object.entries(palette)) for (const step of STEP_ORDER) if (steps[step] != null) reverseIndex[`--color-${name}${suffix(step)}`] = `colors.palette.${name}.${step}`;
|
|
1283
|
+
const colorRoles = Object.entries(defaultSemantic).map(([name, spec]) => ({
|
|
1284
|
+
name,
|
|
1285
|
+
ramp: roleRamp(spec)
|
|
1286
|
+
}));
|
|
1287
|
+
const typoRoles = typography?.roles ?? {};
|
|
1288
|
+
const hooks = {};
|
|
1289
|
+
for (const [key, [prop, sfx]] of Object.entries(TYPO_KEYMAP)) hooks[sfx] = {
|
|
1290
|
+
prop,
|
|
1291
|
+
key
|
|
1292
|
+
};
|
|
1293
|
+
for (const [role, spec] of Object.entries(typoRoles)) for (const [key, [, sfx]] of Object.entries(TYPO_KEYMAP)) if (spec[key] != null) reverseIndex[`--${role}-${sfx}`] = `typography.roles.${role}.${key}`;
|
|
1294
|
+
const pDefaults = patterns?.defaults ?? {};
|
|
1295
|
+
const pRadii = patterns?.radii ?? {};
|
|
1296
|
+
const pGroups = patterns?.groups ?? {};
|
|
1297
|
+
const pItems = patterns?.items ?? {};
|
|
1298
|
+
for (const prop of Object.keys(pDefaults)) reverseIndex[`--${prop}-default`] = `patterns.defaults.${prop}`;
|
|
1299
|
+
for (const name of Object.keys(pRadii)) reverseIndex[`--br-${name}`] = `patterns.radii.${name}`;
|
|
1300
|
+
for (const [group, props] of Object.entries(pGroups)) for (const prop of Object.keys(props)) reverseIndex[`--${prop}-${group}`] = `patterns.groups.${group}.${prop}`;
|
|
1301
|
+
const items = Object.entries(pItems).map(([name, p]) => {
|
|
1302
|
+
const base = p.base ?? {};
|
|
1303
|
+
const wrappable = Object.keys(base).filter((prop) => prop in BASE_HOOK).map((prop) => ({
|
|
1304
|
+
sfx: BASE_HOOK[prop],
|
|
1305
|
+
prop
|
|
1306
|
+
}));
|
|
1307
|
+
for (const { sfx, prop } of wrappable) reverseIndex[`--${sfx}-${name}`] = `patterns.items.${name}.base.${prop}`;
|
|
1308
|
+
return {
|
|
1309
|
+
name,
|
|
1310
|
+
group: p.group ?? null,
|
|
1311
|
+
base,
|
|
1312
|
+
wrappable,
|
|
1313
|
+
roles: p.roles ?? []
|
|
1314
|
+
};
|
|
1315
|
+
});
|
|
1316
|
+
return JSON.stringify({
|
|
1317
|
+
colors: {
|
|
1318
|
+
ramps: Object.keys(palette),
|
|
1319
|
+
steps: STEP_ORDER,
|
|
1320
|
+
palette,
|
|
1321
|
+
roles: colorRoles
|
|
1322
|
+
},
|
|
1323
|
+
typography: {
|
|
1324
|
+
roles: Object.keys(typoRoles),
|
|
1325
|
+
hooks,
|
|
1326
|
+
families: typographyFamilies,
|
|
1327
|
+
specs: typoRoles
|
|
1328
|
+
},
|
|
1329
|
+
scales: {
|
|
1330
|
+
text: typeSteps.map((s) => ({
|
|
1331
|
+
name: s.name,
|
|
1332
|
+
value: s.value,
|
|
1333
|
+
max: s.max
|
|
1334
|
+
})),
|
|
1335
|
+
space: spaceSteps.map((s) => ({
|
|
1336
|
+
name: s.name,
|
|
1337
|
+
value: s.value,
|
|
1338
|
+
max: s.max
|
|
1339
|
+
}))
|
|
1340
|
+
},
|
|
1341
|
+
fonts: ds.fonts ?? {},
|
|
1342
|
+
patterns: {
|
|
1343
|
+
defaults: pDefaults,
|
|
1344
|
+
radii: pRadii,
|
|
1345
|
+
groups: pGroups,
|
|
1346
|
+
items
|
|
1347
|
+
},
|
|
1348
|
+
shadows: shadows ?? {},
|
|
1349
|
+
interaction: {
|
|
1350
|
+
duration: "200ms",
|
|
1351
|
+
easing: "ease"
|
|
1352
|
+
},
|
|
1353
|
+
reverseIndex
|
|
1354
|
+
}, null, 2);
|
|
1355
|
+
})(),
|
|
1356
|
+
tailwind: emitTailwind({
|
|
1357
|
+
ds,
|
|
1358
|
+
palette,
|
|
1359
|
+
defaultSemantic,
|
|
1360
|
+
darkScheme,
|
|
1361
|
+
shadows,
|
|
1362
|
+
typeSteps,
|
|
1363
|
+
spaceSteps,
|
|
1364
|
+
typography,
|
|
1365
|
+
roleDecls,
|
|
1366
|
+
twTypeTokensCss,
|
|
1367
|
+
twPatternTokensCss,
|
|
1368
|
+
patternsCss: pat,
|
|
1369
|
+
assetsDir
|
|
1370
|
+
}),
|
|
1371
|
+
twTypeTokensCss,
|
|
1372
|
+
twPatternTokensCss,
|
|
1373
|
+
patternsCss: pat
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function emitTailwind(ctx) {
|
|
1377
|
+
const { ds, palette, defaultSemantic, darkScheme, shadows, typeSteps, spaceSteps, typography, roleDecls, twTypeTokensCss, twPatternTokensCss, patternsCss, assetsDir } = ctx;
|
|
1378
|
+
const cssDir = join(assetsDir, "css");
|
|
1379
|
+
const nl = (arr) => arr.filter(Boolean).join("\n");
|
|
1380
|
+
const parts = [`/* GENERATED for Tailwind v4 (Astro) — do not edit by hand. */`];
|
|
1381
|
+
parts.push(`@import "tailwindcss";\n`);
|
|
1382
|
+
const theme = [];
|
|
1383
|
+
theme.push(` /* Colour ramps → bg-* text-* border-* */`);
|
|
1384
|
+
for (const [name, steps] of Object.entries(palette)) for (const step of STEP_ORDER) if (steps[step] != null) theme.push(` --color-${name}${suffix(step)}: ${steps[step]};`);
|
|
1385
|
+
theme.push(`\n /* Semantic roles → ramps (default scheme) */`);
|
|
1386
|
+
for (const [role, spec] of Object.entries(defaultSemantic)) {
|
|
1387
|
+
const ramp = roleRamp(spec);
|
|
1388
|
+
for (const step of STEP_ORDER) if (palette[ramp]?.[step] != null) theme.push(` --color-${role}${suffix(step)}: ${srcVar(resolveSlot(spec, step, ramp))};`);
|
|
1389
|
+
}
|
|
1390
|
+
theme.push(`\n /* Fonts → font-* */`);
|
|
1391
|
+
for (const [name, stack] of Object.entries(ds.fonts ?? {})) theme.push(` --font-${name}: ${stack};`);
|
|
1392
|
+
theme.push(`\n /* Fluid type scale → text-* */`);
|
|
1393
|
+
for (const s of typeSteps) theme.push(` --text-${s.name}: ${s.value};`);
|
|
1394
|
+
theme.push(`\n /* Fluid spacing scale → p-* m-* gap-* */`);
|
|
1395
|
+
for (const s of spaceSteps) theme.push(` --spacing-${s.name}: ${s.value};`);
|
|
1396
|
+
theme.push(`\n /* Drop-shadows → shadow-* */`);
|
|
1397
|
+
for (const [name, value] of Object.entries(shadows ?? {})) theme.push(` --shadow-${name}: ${value};`);
|
|
1398
|
+
theme.push(`\n /* Container breakpoints → @sm:/@md:/@lg:/@xl: */`);
|
|
1399
|
+
for (const [name, val] of [
|
|
1400
|
+
["sm", "30rem"],
|
|
1401
|
+
["md", "48rem"],
|
|
1402
|
+
["lg", "64rem"],
|
|
1403
|
+
["xl", "80rem"]
|
|
1404
|
+
]) theme.push(` --container-${name}: ${val};`);
|
|
1405
|
+
parts.push(`@theme {\n${theme.join("\n")}\n}\n`);
|
|
1406
|
+
parts.push(`/* ── Framework tokens (consumed by the inlined components) ── */\n${twTypeTokensCss}\n${twPatternTokensCss}`);
|
|
1407
|
+
let darkBlock = "";
|
|
1408
|
+
if (darkScheme) for (const [role, spec] of Object.entries(darkScheme.semantic)) {
|
|
1409
|
+
const baseSpec = defaultSemantic[role];
|
|
1410
|
+
if (baseSpec == null) continue;
|
|
1411
|
+
const ramp = roleRamp(baseSpec);
|
|
1412
|
+
for (const step of STEP_ORDER) {
|
|
1413
|
+
if (palette[ramp]?.[step] == null) continue;
|
|
1414
|
+
const alt = resolveSlot(spec, step, ramp);
|
|
1415
|
+
if (srcEq(alt, resolveSlot(baseSpec, step, ramp))) continue;
|
|
1416
|
+
darkBlock += ` --color-${role}${suffix(step)}: ${srcVar(alt)};\n`;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (darkBlock && darkScheme) parts.push(`/* Dark-mode semantic overrides */\n:root[data-brx-theme="${darkScheme.appearance}"] {\n${darkBlock}}\n`);
|
|
1420
|
+
parts.push("/* State variant for the active flip (hover/focus-visible are built in). */\n@custom-variant is-active (&.is-active, &[data-active]);\n");
|
|
1421
|
+
const util = [];
|
|
1422
|
+
util.push(`/* Typography roles */`);
|
|
1423
|
+
for (const [role, spec] of Object.entries(typography?.roles ?? {})) util.push(`@utility font-${role} {\n${decls(roleDecls(role, spec))}\n}`);
|
|
1424
|
+
util.push(`\n/* Animation effects (pair with .transition or a driver) */`);
|
|
1425
|
+
const asStr = (v) => String(v);
|
|
1426
|
+
for (const [name, e] of Object.entries(ds.animations?.effects ?? {})) {
|
|
1427
|
+
const d = { "--_anim": e.kf };
|
|
1428
|
+
for (const [k, v] of Object.entries(e.css ?? {})) d[k] = asStr(v);
|
|
1429
|
+
for (const [k, v] of Object.entries(e.vars ?? {})) d[`--${k}`] = asStr(v);
|
|
1430
|
+
util.push(`@utility ${name} {\n${decls(d)}\n}`);
|
|
1431
|
+
if (e.kf !== "composite" && e.kf !== "paint") continue;
|
|
1432
|
+
const props = /* @__PURE__ */ new Set();
|
|
1433
|
+
for (const k of Object.keys(e.vars ?? {})) {
|
|
1434
|
+
const m = /^(.*)-(from|to)$/.exec(k);
|
|
1435
|
+
if (m) props.add(m[1]);
|
|
1436
|
+
}
|
|
1437
|
+
const flip = Object.fromEntries([...props].map((p) => [`--t-${p}`, `var(--${p}-to, ${STATE_DEFAULT[p]})`]));
|
|
1438
|
+
util.push(`@utility flip-${name} {\n${decls(flip)}\n}`);
|
|
1439
|
+
}
|
|
1440
|
+
util.push(`\n/* Split — internal fractions (pair split with a ratio) */`);
|
|
1441
|
+
util.push("@utility split {\n display: flex;\n & > * { flex: 1; }\n & > :first-child { flex: var(--_split-a, 1); }\n & > :last-child { flex: var(--_split-b, 1); }\n}");
|
|
1442
|
+
for (const [a, b] of [
|
|
1443
|
+
[1, 2],
|
|
1444
|
+
[2, 1],
|
|
1445
|
+
[1, 3],
|
|
1446
|
+
[3, 1],
|
|
1447
|
+
[1, 4],
|
|
1448
|
+
[4, 1],
|
|
1449
|
+
[2, 3],
|
|
1450
|
+
[3, 2]
|
|
1451
|
+
]) util.push(`@utility split-${a}-${b} {\n --_split-a: ${a};\n --_split-b: ${b};\n}`);
|
|
1452
|
+
util.push(`\n/* Track placement (inside .centered) */`);
|
|
1453
|
+
for (const track of [
|
|
1454
|
+
"measure",
|
|
1455
|
+
"breakout",
|
|
1456
|
+
"spotlight",
|
|
1457
|
+
"fullbleed"
|
|
1458
|
+
]) util.push(`@utility ${track} {\n grid-column: ${track};\n}`);
|
|
1459
|
+
parts.push(util.join("\n") + "\n");
|
|
1460
|
+
parts.push(`/* ── Structure (mirrors src/css/layout.css) ── */
|
|
1461
|
+
:root {
|
|
1462
|
+
--rhythm-scale: 1;
|
|
1463
|
+
--rhythm-base: calc(1rlh * var(--rhythm-scale));
|
|
1464
|
+
--rhythm-h-p: 0.4;
|
|
1465
|
+
--rhythm-p-p: 0.9;
|
|
1466
|
+
--rhythm-p-h: 1.6;
|
|
1467
|
+
--rhythm-h-h: 0.6;
|
|
1468
|
+
--rhythm-p-list: 0.5;
|
|
1469
|
+
--rhythm-list-p: 0.9;
|
|
1470
|
+
--rhythm-li-li: 0.3;
|
|
1471
|
+
--rhythm-text-media: 1.2;
|
|
1472
|
+
--rhythm-media-text: 1.2;
|
|
1473
|
+
--width-measure: 65ch;
|
|
1474
|
+
--width-breakout: 90ch;
|
|
1475
|
+
--width-spotlight: 120ch;
|
|
1476
|
+
--gutter: clamp(1rem, 4cqi, 3rem);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
body {
|
|
1480
|
+
container-type: inline-size;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.rhythm {
|
|
1484
|
+
display: flow-root;
|
|
1485
|
+
}
|
|
1486
|
+
.rhythm > * + * {
|
|
1487
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-p-p));
|
|
1488
|
+
}
|
|
1489
|
+
.rhythm > :where(h1, h2, h3, h4, h5, h6) + p {
|
|
1490
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-h-p));
|
|
1491
|
+
}
|
|
1492
|
+
.rhythm > p + :where(h1, h2, h3, h4, h5, h6) {
|
|
1493
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-p-h));
|
|
1494
|
+
}
|
|
1495
|
+
.rhythm > :where(h1, h2, h3, h4, h5, h6) + :where(h1, h2, h3, h4, h5, h6) {
|
|
1496
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-h-h));
|
|
1497
|
+
}
|
|
1498
|
+
.rhythm > p + :where(ul, ol) {
|
|
1499
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-p-list));
|
|
1500
|
+
}
|
|
1501
|
+
.rhythm > :where(ul, ol) + p {
|
|
1502
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-list-p));
|
|
1503
|
+
}
|
|
1504
|
+
.rhythm :where(ul, ol) > li + li {
|
|
1505
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-li-li));
|
|
1506
|
+
}
|
|
1507
|
+
.rhythm > :where(p, h1, h2, h3, h4, h5, h6, ul, ol) + :where(figure, img, picture, video) {
|
|
1508
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-text-media));
|
|
1509
|
+
}
|
|
1510
|
+
.rhythm > :where(figure, img, picture, video) + :where(p, h1, h2, h3, h4, h5, h6, ul, ol) {
|
|
1511
|
+
margin-top: calc(var(--rhythm-base) * var(--rhythm-media-text));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.centered {
|
|
1515
|
+
--_measure: min(var(--width-measure), 100% - var(--gutter) * 2);
|
|
1516
|
+
--_breakout: minmax(0, calc((var(--width-breakout) - var(--width-measure)) / 2));
|
|
1517
|
+
--_spotlight: minmax(0, calc((var(--width-spotlight) - var(--width-breakout)) / 2));
|
|
1518
|
+
position: relative;
|
|
1519
|
+
display: grid;
|
|
1520
|
+
grid-template-columns:
|
|
1521
|
+
[fullbleed-start] minmax(var(--gutter), 1fr)
|
|
1522
|
+
[spotlight-start] var(--_spotlight)
|
|
1523
|
+
[breakout-start] var(--_breakout)
|
|
1524
|
+
[measure-start] var(--_measure) [measure-end]
|
|
1525
|
+
var(--_breakout) [breakout-end]
|
|
1526
|
+
var(--_spotlight) [spotlight-end]
|
|
1527
|
+
minmax(var(--gutter), 1fr) [fullbleed-end];
|
|
1528
|
+
}
|
|
1529
|
+
.centered > * {
|
|
1530
|
+
grid-column: measure;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.region {
|
|
1534
|
+
padding-block: clamp(
|
|
1535
|
+
var(--region-space-min, 3rem),
|
|
1536
|
+
var(--region-space, 8vh),
|
|
1537
|
+
var(--region-space-max, 8rem)
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
`);
|
|
1541
|
+
const animEngine = readFileSync(join(cssDir, "animation.css"), "utf8");
|
|
1542
|
+
parts.push(`/* ── Animation engine (animation.css) ── */\n${animEngine}`);
|
|
1543
|
+
parts.push(`/* ── Component patterns ── */\n${patternsCss}`);
|
|
1544
|
+
const TW_CLASH = new Set([
|
|
1545
|
+
"block",
|
|
1546
|
+
"inline",
|
|
1547
|
+
"inline-block",
|
|
1548
|
+
"flow-root",
|
|
1549
|
+
"flex",
|
|
1550
|
+
"inline-flex",
|
|
1551
|
+
"grid",
|
|
1552
|
+
"inline-grid",
|
|
1553
|
+
"table",
|
|
1554
|
+
"inline-table",
|
|
1555
|
+
"table-caption",
|
|
1556
|
+
"table-cell",
|
|
1557
|
+
"table-row",
|
|
1558
|
+
"contents",
|
|
1559
|
+
"hidden",
|
|
1560
|
+
"list-item",
|
|
1561
|
+
"static",
|
|
1562
|
+
"fixed",
|
|
1563
|
+
"absolute",
|
|
1564
|
+
"relative",
|
|
1565
|
+
"sticky",
|
|
1566
|
+
"isolate",
|
|
1567
|
+
"visible",
|
|
1568
|
+
"invisible",
|
|
1569
|
+
"collapse",
|
|
1570
|
+
"sr-only",
|
|
1571
|
+
"not-sr-only",
|
|
1572
|
+
"flex-row",
|
|
1573
|
+
"flex-row-reverse",
|
|
1574
|
+
"flex-col",
|
|
1575
|
+
"flex-col-reverse",
|
|
1576
|
+
"flex-wrap",
|
|
1577
|
+
"flex-nowrap",
|
|
1578
|
+
"items-start",
|
|
1579
|
+
"items-end",
|
|
1580
|
+
"items-center",
|
|
1581
|
+
"items-baseline",
|
|
1582
|
+
"items-stretch",
|
|
1583
|
+
"justify-start",
|
|
1584
|
+
"justify-end",
|
|
1585
|
+
"justify-center",
|
|
1586
|
+
"justify-between",
|
|
1587
|
+
"justify-around",
|
|
1588
|
+
"justify-evenly",
|
|
1589
|
+
"content-start",
|
|
1590
|
+
"content-end",
|
|
1591
|
+
"content-center",
|
|
1592
|
+
"content-between",
|
|
1593
|
+
"content-around",
|
|
1594
|
+
"content-evenly",
|
|
1595
|
+
"text-left",
|
|
1596
|
+
"text-center",
|
|
1597
|
+
"text-right",
|
|
1598
|
+
"text-justify",
|
|
1599
|
+
"text-start",
|
|
1600
|
+
"text-end"
|
|
1601
|
+
]);
|
|
1602
|
+
const droppedClashes = /* @__PURE__ */ new Set();
|
|
1603
|
+
let droppedVariantBlocks = 0;
|
|
1604
|
+
const stripForTailwind = (css) => {
|
|
1605
|
+
const kept = [];
|
|
1606
|
+
let depth = 0;
|
|
1607
|
+
let start = 0;
|
|
1608
|
+
let preludeEnd = -1;
|
|
1609
|
+
for (let i = 0; i < css.length; i++) {
|
|
1610
|
+
const ch = css[i];
|
|
1611
|
+
if (ch === "/" && css[i + 1] === "*") {
|
|
1612
|
+
const end = css.indexOf("*/", i + 2);
|
|
1613
|
+
i = end === -1 ? css.length : end + 1;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (ch === "\"" || ch === "'") {
|
|
1617
|
+
i += 1;
|
|
1618
|
+
while (i < css.length && css[i] !== ch) i += css[i] === "\\" ? 2 : 1;
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
if (ch === "{") {
|
|
1622
|
+
if (depth === 0) preludeEnd = i;
|
|
1623
|
+
depth++;
|
|
1624
|
+
} else if (ch === "}") {
|
|
1625
|
+
depth--;
|
|
1626
|
+
if (depth === 0) {
|
|
1627
|
+
const raw = css.slice(start, i + 1).trim();
|
|
1628
|
+
const prelude = css.slice(start, preludeEnd).replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
1629
|
+
const clash = /^\.([A-Za-z0-9_-]+)/.exec(prelude);
|
|
1630
|
+
if (/^@container\s*\(\s*min-width:/.test(prelude)) droppedVariantBlocks++;
|
|
1631
|
+
else if (clash && TW_CLASH.has(clash[1])) droppedClashes.add(clash[1]);
|
|
1632
|
+
else if (raw) kept.push(raw);
|
|
1633
|
+
start = i + 1;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return kept.join("\n\n");
|
|
1638
|
+
};
|
|
1639
|
+
const componentCss = componentPartialOrder(cssDir).map((rel) => {
|
|
1640
|
+
return `/* ── ${rel} ── */\n${stripForTailwind(readFileSync(join(cssDir, rel), "utf8"))}`;
|
|
1641
|
+
}).join("\n\n");
|
|
1642
|
+
parts.push(`/* ── Component + structural partials (Tailwind-clashing utilities dropped) ── */\n${componentCss}`);
|
|
1643
|
+
return nl(parts) + "\n";
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* The component/structural CSS partials to inline into the Tailwind bundle, in
|
|
1647
|
+
* index.css cascade order. Everything the aggregator imports EXCEPT the engine
|
|
1648
|
+
* (global/animation), the layout utilities (Tailwind owns them), and the generated
|
|
1649
|
+
* token layer (Tailwind builds its own via @theme).
|
|
1650
|
+
*/
|
|
1651
|
+
function componentPartialOrder(cssDir) {
|
|
1652
|
+
const indexPath = join(cssDir, "index.css");
|
|
1653
|
+
const skip = new Set([
|
|
1654
|
+
"global.css",
|
|
1655
|
+
"animation.css",
|
|
1656
|
+
"layout.css"
|
|
1657
|
+
]);
|
|
1658
|
+
const order = [];
|
|
1659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1660
|
+
const text = existsSync(indexPath) ? readFileSync(indexPath, "utf8") : "";
|
|
1661
|
+
for (const m of text.matchAll(/@import\s+['"]\.\/([^'"]+)['"]/g)) {
|
|
1662
|
+
const rel = m[1];
|
|
1663
|
+
if (rel.startsWith("generated/")) continue;
|
|
1664
|
+
if (skip.has(rel)) continue;
|
|
1665
|
+
if (!seen.has(rel) && existsSync(join(cssDir, rel))) {
|
|
1666
|
+
seen.add(rel);
|
|
1667
|
+
order.push(rel);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
if (!order.length) {
|
|
1671
|
+
if (existsSync(join(cssDir, "utilities.css"))) order.push("utilities.css");
|
|
1672
|
+
const pdir = join(cssDir, "patterns");
|
|
1673
|
+
if (existsSync(pdir)) {
|
|
1674
|
+
for (const f of readdirSync(pdir).sort()) if (f.endsWith(".css")) order.push(`patterns/${f}`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return order;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Assemble the full stylesheet by resolving index.css's @import order against the
|
|
1681
|
+
* static partials (from assets) and the freshly generated token partials (in
|
|
1682
|
+
* memory), then minify with lightningcss. No @import survives, so no disk
|
|
1683
|
+
* resolution / shared `generated/` dir is needed.
|
|
1684
|
+
*/
|
|
1685
|
+
function bundleCss(generated, assetsDir, minify) {
|
|
1686
|
+
const cssDir = join(assetsDir, "css");
|
|
1687
|
+
const index = readFileSync(join(cssDir, "index.css"), "utf8");
|
|
1688
|
+
const chunks = [];
|
|
1689
|
+
for (const line of index.split("\n")) {
|
|
1690
|
+
const m = /@import\s+['"]\.\/([^'"]+)['"]/.exec(line);
|
|
1691
|
+
if (!m) continue;
|
|
1692
|
+
const rel = m[1];
|
|
1693
|
+
if (rel.startsWith("generated/")) {
|
|
1694
|
+
const name = rel.slice(10);
|
|
1695
|
+
chunks.push(generated[name] ?? "");
|
|
1696
|
+
} else chunks.push(readFileSync(join(cssDir, rel), "utf8"));
|
|
1697
|
+
}
|
|
1698
|
+
const merged = chunks.join("\n");
|
|
1699
|
+
const res = transform({
|
|
1700
|
+
filename: "styles.css",
|
|
1701
|
+
code: Buffer.from(merged),
|
|
1702
|
+
minify,
|
|
1703
|
+
sourceMap: minify
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
code: res.code.toString(),
|
|
1707
|
+
map: res.map ? res.map.toString() : void 0
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
function loadConfig(input) {
|
|
1711
|
+
const result = validate(typeof input === "string" ? JSON.parse(readFileSync(input, "utf8")) : input);
|
|
1712
|
+
if (!result.ok) {
|
|
1713
|
+
const lines = result.errors.slice(0, 12).map((e) => ` • ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
|
|
1714
|
+
throw new Error(`Invalid design-system.json:\n${lines}`);
|
|
1715
|
+
}
|
|
1716
|
+
return result.data;
|
|
1717
|
+
}
|
|
1718
|
+
async function generate(options) {
|
|
1719
|
+
const format = options.format ?? "bricks";
|
|
1720
|
+
const outDir = options.outDir ?? "dist";
|
|
1721
|
+
const assetsDir = options.assetsDir ?? DEFAULT_ASSETS;
|
|
1722
|
+
const ds = loadConfig(options.input);
|
|
1723
|
+
const built = build(ds, format, assetsDir);
|
|
1724
|
+
mkdirSync(outDir, { recursive: true });
|
|
1725
|
+
const written = [];
|
|
1726
|
+
const put = (rel, content) => {
|
|
1727
|
+
const p = join(outDir, rel);
|
|
1728
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
1729
|
+
writeFileSync(p, content);
|
|
1730
|
+
written.push(p);
|
|
1731
|
+
};
|
|
1732
|
+
if (format === "tailwind") {
|
|
1733
|
+
put("tailwind.css", built.tailwind);
|
|
1734
|
+
put("tokens.json", built.tokensJson);
|
|
1735
|
+
} else if (format === "css") {
|
|
1736
|
+
const { code } = bundleCss(built.generated, assetsDir, true);
|
|
1737
|
+
put("styles.css", code);
|
|
1738
|
+
put("tokens.json", built.tokensJson);
|
|
1739
|
+
put("design-manifest.json", built.designManifest);
|
|
1740
|
+
} else {
|
|
1741
|
+
const { code, map } = bundleCss(built.generated, assetsDir, true);
|
|
1742
|
+
put("styles.min.css", map ? code + `\n/*# sourceMappingURL=styles.min.css.map */` : code);
|
|
1743
|
+
if (map) put("styles.min.css.map", map);
|
|
1744
|
+
put("bricks-colors-named.json", built.bricksColorsNamed);
|
|
1745
|
+
put("bricks-colors-semantic.json", built.bricksColorsSemantic);
|
|
1746
|
+
put("bricks-variables.json", built.bricksVariables);
|
|
1747
|
+
put("tokens.json", built.tokensJson);
|
|
1748
|
+
const jsDir = join(assetsDir, "js");
|
|
1749
|
+
if (existsSync(jsDir)) {
|
|
1750
|
+
cpSync(jsDir, outDir, { recursive: true });
|
|
1751
|
+
for (const f of readdirSync(jsDir)) written.push(join(outDir, f));
|
|
1752
|
+
}
|
|
1753
|
+
const bricksSrc = join(assetsDir, "bricks");
|
|
1754
|
+
if (existsSync(bricksSrc)) {
|
|
1755
|
+
cpSync(bricksSrc, join(outDir, "bricks"), { recursive: true });
|
|
1756
|
+
written.push(join(outDir, "bricks"));
|
|
1757
|
+
}
|
|
1758
|
+
const docs = generateDocs(ds, assetsDir);
|
|
1759
|
+
for (const [rel, content] of Object.entries(docs)) put(join("docs", rel), content);
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
format,
|
|
1763
|
+
outDir,
|
|
1764
|
+
written
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
//#endregion
|
|
1769
|
+
//#region src/site.ts
|
|
1770
|
+
/**
|
|
1771
|
+
* Company / site-level configuration schema — the umbrella around a
|
|
1772
|
+
* `design-system.json`-compliant `designSystem`.
|
|
1773
|
+
*
|
|
1774
|
+
* Authored with `zod/mini` (same instance the design-system schema uses), so the
|
|
1775
|
+
* two compose: `DesignSystemSchema` is embedded as `designSystem.<theme>` and the
|
|
1776
|
+
* whole thing serialises to one JSON Schema via `z.toJSONSchema`. Everything a
|
|
1777
|
+
* generator needs derives from here: the `SiteConfig` type (`z.infer`), the
|
|
1778
|
+
* published JSON Schema (`siteJsonSchema`), and runtime validation (`validateSite`).
|
|
1779
|
+
*
|
|
1780
|
+
* Pure: no YAML/env/fs. `resolveSiteConfig` takes an already-parsed object; a
|
|
1781
|
+
* thin loader (reading YAML, injecting `SITE_ENV`) lives outside this module.
|
|
1782
|
+
*/
|
|
1783
|
+
/** A string, or a per-locale map of strings (`{ en: "…", fr: "…" }`). */
|
|
1784
|
+
const LocalizableSchema = z.union([z.string(), z.record(z.string(), z.string())]);
|
|
1785
|
+
const ImageRefSchema = z.union([z.string(), z.object({
|
|
1786
|
+
src: z.string(),
|
|
1787
|
+
alt: z.string(),
|
|
1788
|
+
width: z.optional(z.number()),
|
|
1789
|
+
height: z.optional(z.number())
|
|
1790
|
+
})]);
|
|
1791
|
+
const AttrValue = z.union([
|
|
1792
|
+
z.string(),
|
|
1793
|
+
z.number(),
|
|
1794
|
+
z.boolean()
|
|
1795
|
+
]);
|
|
1796
|
+
const AttrsSchema = z.optional(z.record(z.string(), AttrValue));
|
|
1797
|
+
const IsoDate = z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/));
|
|
1798
|
+
const Iso4217 = z.string().check(z.regex(/^[A-Z]{3}$/));
|
|
1799
|
+
const DayOfWeek = z.enum([
|
|
1800
|
+
"Monday",
|
|
1801
|
+
"Tuesday",
|
|
1802
|
+
"Wednesday",
|
|
1803
|
+
"Thursday",
|
|
1804
|
+
"Friday",
|
|
1805
|
+
"Saturday",
|
|
1806
|
+
"Sunday"
|
|
1807
|
+
]);
|
|
1808
|
+
const DesignSystemEntry = z.extend(DesignSystemPatchSchema, {
|
|
1809
|
+
displayName: z.optional(LocalizableSchema),
|
|
1810
|
+
extends: z.optional(z.string())
|
|
1811
|
+
});
|
|
1812
|
+
const LocaleSchema = z.object({
|
|
1813
|
+
name: z.string(),
|
|
1814
|
+
tagline: z.optional(z.string()),
|
|
1815
|
+
basePath: z.optional(z.string())
|
|
1816
|
+
});
|
|
1817
|
+
const HttpRedirect = z.union([
|
|
1818
|
+
z.literal(301),
|
|
1819
|
+
z.literal(302),
|
|
1820
|
+
z.literal(307),
|
|
1821
|
+
z.literal(308)
|
|
1822
|
+
]);
|
|
1823
|
+
const DomainAliasSchema = z.object({
|
|
1824
|
+
domain: z.string(),
|
|
1825
|
+
redirectType: HttpRedirect,
|
|
1826
|
+
redirectTo: z.string(),
|
|
1827
|
+
environment: z.optional(z.string())
|
|
1828
|
+
});
|
|
1829
|
+
/** schema.org `PostalAddress` field names, so JSON-LD generation is lossless. */
|
|
1830
|
+
const PostalAddressSchema = z.object({
|
|
1831
|
+
streetAddress: z.string(),
|
|
1832
|
+
addressLocality: z.string(),
|
|
1833
|
+
addressRegion: z.optional(z.string()),
|
|
1834
|
+
postalCode: z.optional(z.string()),
|
|
1835
|
+
addressCountry: z.string()
|
|
1836
|
+
});
|
|
1837
|
+
const ContactObjectSchema = z.object({
|
|
1838
|
+
name: z.optional(z.string()),
|
|
1839
|
+
email: z.optional(z.email()),
|
|
1840
|
+
phone: z.optional(z.string()),
|
|
1841
|
+
address: z.optional(PostalAddressSchema)
|
|
1842
|
+
});
|
|
1843
|
+
const ContactConfigSchema = z.union([z.string(), ContactObjectSchema]);
|
|
1844
|
+
const GeoSchema = z.object({
|
|
1845
|
+
latitude: z.number().check(z.gte(-90), z.lte(90)),
|
|
1846
|
+
longitude: z.number().check(z.gte(-180), z.lte(180))
|
|
1847
|
+
});
|
|
1848
|
+
/** schema.org `OpeningHoursSpecification` — per-day open/close (24h `HH:MM`). */
|
|
1849
|
+
const OpeningHoursSchema = z.object({
|
|
1850
|
+
dayOfWeek: z.array(DayOfWeek),
|
|
1851
|
+
opens: z.string().check(z.regex(/^\d{2}:\d{2}$/)),
|
|
1852
|
+
closes: z.string().check(z.regex(/^\d{2}:\d{2}$/))
|
|
1853
|
+
});
|
|
1854
|
+
const LocalBusinessType = z.enum([
|
|
1855
|
+
"LocalBusiness",
|
|
1856
|
+
"Store",
|
|
1857
|
+
"Restaurant",
|
|
1858
|
+
"CafeOrCoffeeShop",
|
|
1859
|
+
"ProfessionalService",
|
|
1860
|
+
"MedicalBusiness",
|
|
1861
|
+
"HealthAndBeautyBusiness",
|
|
1862
|
+
"HomeAndConstructionBusiness",
|
|
1863
|
+
"AutomotiveBusiness",
|
|
1864
|
+
"FinancialService",
|
|
1865
|
+
"LegalService",
|
|
1866
|
+
"RealEstateAgent",
|
|
1867
|
+
"Organization"
|
|
1868
|
+
]);
|
|
1869
|
+
const LocationSchema = z.object({
|
|
1870
|
+
slug: z.optional(LocalizableSchema),
|
|
1871
|
+
name: LocalizableSchema,
|
|
1872
|
+
email: z.optional(z.email()),
|
|
1873
|
+
phone: z.optional(z.string()),
|
|
1874
|
+
address: z.optional(PostalAddressSchema),
|
|
1875
|
+
geo: z.optional(GeoSchema),
|
|
1876
|
+
hours: z.optional(z.array(OpeningHoursSchema)),
|
|
1877
|
+
type: z.optional(LocalBusinessType),
|
|
1878
|
+
description: z.optional(LocalizableSchema),
|
|
1879
|
+
areaServed: z.optional(z.array(z.string())),
|
|
1880
|
+
priceRange: z.optional(z.string()),
|
|
1881
|
+
paymentAccepted: z.optional(z.array(z.string())),
|
|
1882
|
+
currenciesAccepted: z.optional(Iso4217),
|
|
1883
|
+
knowsLanguage: z.optional(z.array(z.string()))
|
|
1884
|
+
});
|
|
1885
|
+
const OrganizationSchema = z.object({
|
|
1886
|
+
name: z.optional(LocalizableSchema),
|
|
1887
|
+
legalName: z.optional(z.string()),
|
|
1888
|
+
foundingDate: z.optional(IsoDate),
|
|
1889
|
+
logo: z.optional(ImageRefSchema),
|
|
1890
|
+
email: z.optional(z.email()),
|
|
1891
|
+
phone: z.optional(z.string()),
|
|
1892
|
+
address: z.optional(PostalAddressSchema),
|
|
1893
|
+
taxID: z.optional(z.string()),
|
|
1894
|
+
vatID: z.optional(z.string()),
|
|
1895
|
+
/** Profile URLs → JSON-LD `sameAs` (mirrors `links`). */
|
|
1896
|
+
sameAs: z.optional(z.array(z.url()))
|
|
1897
|
+
});
|
|
1898
|
+
const DnsRecordType = z.enum([
|
|
1899
|
+
"A",
|
|
1900
|
+
"AAAA",
|
|
1901
|
+
"CNAME",
|
|
1902
|
+
"TXT",
|
|
1903
|
+
"MX",
|
|
1904
|
+
"NS",
|
|
1905
|
+
"SRV",
|
|
1906
|
+
"CAA",
|
|
1907
|
+
"PTR",
|
|
1908
|
+
"SOA"
|
|
1909
|
+
]);
|
|
1910
|
+
const DnsRecordSchema = z.object({
|
|
1911
|
+
type: DnsRecordType,
|
|
1912
|
+
name: z.optional(z.string()),
|
|
1913
|
+
value: z.string(),
|
|
1914
|
+
ttl: z.optional(z.number().check(z.int(), z.positive())),
|
|
1915
|
+
proxied: z.optional(z.boolean())
|
|
1916
|
+
});
|
|
1917
|
+
const DnsDomainSchema = z.object({
|
|
1918
|
+
provider: z.optional(z.string()),
|
|
1919
|
+
nameservers: z.optional(z.array(z.string())),
|
|
1920
|
+
records: z.optional(z.array(DnsRecordSchema))
|
|
1921
|
+
});
|
|
1922
|
+
const RobotsSchema = z.string();
|
|
1923
|
+
const EnvironmentSchema = z.object({
|
|
1924
|
+
url: z.url(),
|
|
1925
|
+
api: z.optional(z.url()),
|
|
1926
|
+
analytics: z.optional(z.boolean()),
|
|
1927
|
+
robots: z.optional(RobotsSchema),
|
|
1928
|
+
variant: z.optional(z.string())
|
|
1929
|
+
});
|
|
1930
|
+
const AbVariantSchema = z.object({
|
|
1931
|
+
environment: z.string(),
|
|
1932
|
+
description: z.optional(z.string()),
|
|
1933
|
+
overrides: z.optional(z.record(z.string(), z.unknown()))
|
|
1934
|
+
});
|
|
1935
|
+
const FontProvider = z.enum([
|
|
1936
|
+
"google",
|
|
1937
|
+
"fontsource",
|
|
1938
|
+
"adobe",
|
|
1939
|
+
"bunny",
|
|
1940
|
+
"fontshare",
|
|
1941
|
+
"googleicons",
|
|
1942
|
+
"npm",
|
|
1943
|
+
"local"
|
|
1944
|
+
]);
|
|
1945
|
+
const FontStyle = z.enum(["normal", "italic"]);
|
|
1946
|
+
const FontDisplay = z.enum([
|
|
1947
|
+
"auto",
|
|
1948
|
+
"block",
|
|
1949
|
+
"swap",
|
|
1950
|
+
"fallback",
|
|
1951
|
+
"optional"
|
|
1952
|
+
]);
|
|
1953
|
+
const FontWeight = z.union([z.number(), z.string()]);
|
|
1954
|
+
/** A local-font face (`provider: 'local'`). */
|
|
1955
|
+
const FontVariantSchema = z.object({
|
|
1956
|
+
src: z.array(z.string()),
|
|
1957
|
+
weight: z.optional(FontWeight),
|
|
1958
|
+
style: z.optional(FontStyle),
|
|
1959
|
+
unicodeRange: z.optional(z.array(z.string())),
|
|
1960
|
+
display: z.optional(FontDisplay)
|
|
1961
|
+
});
|
|
1962
|
+
const FontSchema = z.object({
|
|
1963
|
+
name: z.string(),
|
|
1964
|
+
provider: FontProvider,
|
|
1965
|
+
cssVariable: z.string().check(z.regex(/^--/)),
|
|
1966
|
+
weights: z.optional(z.array(FontWeight)),
|
|
1967
|
+
styles: z.optional(z.array(FontStyle)),
|
|
1968
|
+
subsets: z.optional(z.array(z.string())),
|
|
1969
|
+
fallbacks: z.optional(z.array(z.string())),
|
|
1970
|
+
optimizedFallbacks: z.optional(z.boolean()),
|
|
1971
|
+
display: z.optional(FontDisplay),
|
|
1972
|
+
formats: z.optional(z.array(z.string())),
|
|
1973
|
+
unicodeRange: z.optional(z.array(z.string())),
|
|
1974
|
+
variants: z.optional(z.array(FontVariantSchema)),
|
|
1975
|
+
/** Emit `<Font preload />` for this family. */
|
|
1976
|
+
preload: z.optional(z.union([z.boolean(), z.array(z.object({
|
|
1977
|
+
weight: FontWeight,
|
|
1978
|
+
style: z.optional(FontStyle)
|
|
1979
|
+
}))]))
|
|
1980
|
+
});
|
|
1981
|
+
const PresentSchema = z.optional(z.record(z.string(), z.unknown()));
|
|
1982
|
+
const NavItemSchema = z.object({
|
|
1983
|
+
label: z.optional(LocalizableSchema),
|
|
1984
|
+
image: z.optional(ImageRefSchema),
|
|
1985
|
+
href: z.optional(z.string()),
|
|
1986
|
+
icon: z.optional(z.string()),
|
|
1987
|
+
attrs: AttrsSchema,
|
|
1988
|
+
itemAttrs: AttrsSchema,
|
|
1989
|
+
submenu: z.optional(z.object({
|
|
1990
|
+
present: PresentSchema,
|
|
1991
|
+
get items() {
|
|
1992
|
+
return z.optional(z.array(NavItemSchema));
|
|
1993
|
+
}
|
|
1994
|
+
}))
|
|
1995
|
+
});
|
|
1996
|
+
const FormFieldType = z.enum([
|
|
1997
|
+
"text",
|
|
1998
|
+
"email",
|
|
1999
|
+
"tel",
|
|
2000
|
+
"url",
|
|
2001
|
+
"number",
|
|
2002
|
+
"password",
|
|
2003
|
+
"textarea",
|
|
2004
|
+
"select",
|
|
2005
|
+
"checkbox",
|
|
2006
|
+
"radio",
|
|
2007
|
+
"date",
|
|
2008
|
+
"file",
|
|
2009
|
+
"hidden"
|
|
2010
|
+
]);
|
|
2011
|
+
const FormFieldSchema = z.object({
|
|
2012
|
+
name: z.string(),
|
|
2013
|
+
type: FormFieldType,
|
|
2014
|
+
label: z.optional(LocalizableSchema),
|
|
2015
|
+
required: z.optional(z.boolean()),
|
|
2016
|
+
validation: z.optional(z.string()),
|
|
2017
|
+
options: z.optional(z.union([z.string(), z.array(z.object({
|
|
2018
|
+
label: z.string(),
|
|
2019
|
+
value: z.string()
|
|
2020
|
+
}))])),
|
|
2021
|
+
attrs: AttrsSchema
|
|
2022
|
+
});
|
|
2023
|
+
const ContentNodeSchema = z.object({
|
|
2024
|
+
tag: z.string(),
|
|
2025
|
+
attrs: AttrsSchema,
|
|
2026
|
+
text: z.optional(z.string()),
|
|
2027
|
+
get children() {
|
|
2028
|
+
return z.optional(z.array(ContentNodeSchema));
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
const TemplateSchema = z.union([
|
|
2032
|
+
z.object({
|
|
2033
|
+
type: z.literal("nav"),
|
|
2034
|
+
tags: z.optional(z.array(z.string())),
|
|
2035
|
+
present: PresentSchema,
|
|
2036
|
+
items: z.optional(z.array(NavItemSchema))
|
|
2037
|
+
}),
|
|
2038
|
+
z.object({
|
|
2039
|
+
type: z.literal("form"),
|
|
2040
|
+
tags: z.optional(z.array(z.string())),
|
|
2041
|
+
present: PresentSchema,
|
|
2042
|
+
action: z.optional(z.string()),
|
|
2043
|
+
method: z.optional(z.enum(["get", "post"])),
|
|
2044
|
+
fields: z.optional(z.array(FormFieldSchema)),
|
|
2045
|
+
submit: z.optional(z.record(z.string(), AttrValue)),
|
|
2046
|
+
honeypot: z.optional(z.boolean())
|
|
2047
|
+
}),
|
|
2048
|
+
z.object({
|
|
2049
|
+
type: z.literal("nodes"),
|
|
2050
|
+
tags: z.optional(z.array(z.string())),
|
|
2051
|
+
nodes: z.optional(z.array(ContentNodeSchema))
|
|
2052
|
+
})
|
|
2053
|
+
]);
|
|
2054
|
+
const GalleryImageSchema = z.object({
|
|
2055
|
+
src: z.string(),
|
|
2056
|
+
alt: LocalizableSchema,
|
|
2057
|
+
caption: z.optional(LocalizableSchema)
|
|
2058
|
+
});
|
|
2059
|
+
const GallerySchema = z.object({
|
|
2060
|
+
title: z.optional(LocalizableSchema),
|
|
2061
|
+
tags: z.optional(z.array(z.string())),
|
|
2062
|
+
images: z.array(GalleryImageSchema)
|
|
2063
|
+
});
|
|
2064
|
+
const TestimonialItemSchema = z.object({
|
|
2065
|
+
quote: LocalizableSchema,
|
|
2066
|
+
name: z.string(),
|
|
2067
|
+
role: z.optional(z.string()),
|
|
2068
|
+
rating: z.optional(z.number().check(z.gte(0), z.lte(5))),
|
|
2069
|
+
date: z.optional(IsoDate)
|
|
2070
|
+
});
|
|
2071
|
+
const TestimonialGroupSchema = z.object({
|
|
2072
|
+
title: z.optional(LocalizableSchema),
|
|
2073
|
+
items: z.array(TestimonialItemSchema)
|
|
2074
|
+
});
|
|
2075
|
+
const OfferSchema = z.object({
|
|
2076
|
+
name: z.optional(LocalizableSchema),
|
|
2077
|
+
price: z.optional(z.union([z.string(), z.number()])),
|
|
2078
|
+
priceCurrency: z.optional(Iso4217),
|
|
2079
|
+
availability: z.optional(z.string()),
|
|
2080
|
+
url: z.optional(z.url()),
|
|
2081
|
+
validFrom: z.optional(IsoDate),
|
|
2082
|
+
priceValidUntil: z.optional(IsoDate)
|
|
2083
|
+
});
|
|
2084
|
+
const ServiceSchema = z.object({
|
|
2085
|
+
name: LocalizableSchema,
|
|
2086
|
+
description: z.optional(LocalizableSchema),
|
|
2087
|
+
slug: z.optional(LocalizableSchema),
|
|
2088
|
+
offers: z.optional(z.array(OfferSchema))
|
|
2089
|
+
});
|
|
2090
|
+
const OpenGraphSchema = z.object({
|
|
2091
|
+
type: z.optional(z.string()),
|
|
2092
|
+
siteName: z.optional(z.string()),
|
|
2093
|
+
title: z.optional(LocalizableSchema),
|
|
2094
|
+
description: z.optional(LocalizableSchema),
|
|
2095
|
+
image: z.optional(ImageRefSchema),
|
|
2096
|
+
locale: z.optional(z.string())
|
|
2097
|
+
});
|
|
2098
|
+
const TwitterSchema = z.object({
|
|
2099
|
+
card: z.optional(z.enum([
|
|
2100
|
+
"summary",
|
|
2101
|
+
"summary_large_image",
|
|
2102
|
+
"app",
|
|
2103
|
+
"player"
|
|
2104
|
+
])),
|
|
2105
|
+
site: z.optional(z.string()),
|
|
2106
|
+
creator: z.optional(z.string()),
|
|
2107
|
+
title: z.optional(LocalizableSchema),
|
|
2108
|
+
description: z.optional(LocalizableSchema),
|
|
2109
|
+
image: z.optional(ImageRefSchema)
|
|
2110
|
+
});
|
|
2111
|
+
const SiteConfigSchema = z.object({
|
|
2112
|
+
defaultLocale: z.string(),
|
|
2113
|
+
locales: z.record(z.string(), LocaleSchema),
|
|
2114
|
+
organization: z.optional(OrganizationSchema),
|
|
2115
|
+
contact: z.optional(ContactConfigSchema),
|
|
2116
|
+
domains: z.optional(z.object({
|
|
2117
|
+
canonical: z.url(),
|
|
2118
|
+
aliases: z.optional(z.array(DomainAliasSchema))
|
|
2119
|
+
})),
|
|
2120
|
+
primaryLocation: z.optional(z.string()),
|
|
2121
|
+
locations: z.optional(z.record(z.string(), LocationSchema)),
|
|
2122
|
+
services: z.optional(z.record(z.string(), ServiceSchema)),
|
|
2123
|
+
links: z.optional(z.object({
|
|
2124
|
+
googleMaps: z.optional(z.url()),
|
|
2125
|
+
instagram: z.optional(z.url()),
|
|
2126
|
+
facebook: z.optional(z.url()),
|
|
2127
|
+
x: z.optional(z.url()),
|
|
2128
|
+
linkedin: z.optional(z.url()),
|
|
2129
|
+
youtube: z.optional(z.url()),
|
|
2130
|
+
github: z.optional(z.url())
|
|
2131
|
+
})),
|
|
2132
|
+
dns: z.optional(z.record(z.string(), DnsDomainSchema)),
|
|
2133
|
+
cloudflare: z.optional(z.record(z.string(), z.unknown())),
|
|
2134
|
+
environments: z.record(z.string(), EnvironmentSchema),
|
|
2135
|
+
abTesting: z.optional(z.object({
|
|
2136
|
+
enabled: z.boolean(),
|
|
2137
|
+
cookieName: z.optional(z.string()),
|
|
2138
|
+
cookieMaxAge: z.optional(z.number().check(z.int(), z.positive())),
|
|
2139
|
+
splitRatio: z.optional(z.number().check(z.gte(0), z.lte(1))),
|
|
2140
|
+
variants: z.optional(z.record(z.string(), AbVariantSchema))
|
|
2141
|
+
})),
|
|
2142
|
+
designSystem: z.record(z.string(), DesignSystemEntry),
|
|
2143
|
+
defaultTheme: z.optional(z.string()),
|
|
2144
|
+
defaultColorScheme: z.optional(z.enum(["light", "dark"])),
|
|
2145
|
+
respectSystemPreference: z.optional(z.boolean()),
|
|
2146
|
+
fonts: z.optional(z.array(FontSchema)),
|
|
2147
|
+
tags: z.optional(z.record(z.string(), z.unknown())),
|
|
2148
|
+
postTypes: z.optional(z.record(z.string(), z.unknown())),
|
|
2149
|
+
galleries: z.optional(z.record(z.string(), GallerySchema)),
|
|
2150
|
+
testimonials: z.optional(z.record(z.string(), TestimonialGroupSchema)),
|
|
2151
|
+
templates: z.optional(z.record(z.string(), TemplateSchema)),
|
|
2152
|
+
navigation: z.optional(z.object({ activeTemplate: z.optional(z.object({
|
|
2153
|
+
default: z.optional(z.string()),
|
|
2154
|
+
breakpoints: z.optional(z.record(z.string(), z.string()))
|
|
2155
|
+
})) })),
|
|
2156
|
+
seo: z.optional(z.object({
|
|
2157
|
+
titleTemplate: z.optional(z.string()),
|
|
2158
|
+
descriptionTemplate: z.optional(z.string()),
|
|
2159
|
+
robots: z.optional(RobotsSchema),
|
|
2160
|
+
googleSiteVerification: z.optional(z.string()),
|
|
2161
|
+
bingSiteVerification: z.optional(z.string()),
|
|
2162
|
+
openGraph: z.optional(OpenGraphSchema),
|
|
2163
|
+
twitter: z.optional(TwitterSchema)
|
|
2164
|
+
})),
|
|
2165
|
+
analytics: z.optional(z.object({
|
|
2166
|
+
googleAnalyticsId: z.optional(z.string()),
|
|
2167
|
+
plausibleDomain: z.optional(z.string()),
|
|
2168
|
+
googleTagManagerId: z.optional(z.string())
|
|
2169
|
+
})),
|
|
2170
|
+
notifications: z.optional(z.object({ email: z.optional(z.email()) })),
|
|
2171
|
+
tracking: z.optional(z.object({
|
|
2172
|
+
enabled: z.optional(z.boolean()),
|
|
2173
|
+
platforms: z.optional(z.array(z.string()))
|
|
2174
|
+
})),
|
|
2175
|
+
security: z.optional(z.object({ turnstile: z.optional(z.object({ siteKey: z.optional(z.string()) })) })),
|
|
2176
|
+
legal: z.optional(z.object({
|
|
2177
|
+
privacyPolicy: z.optional(z.object({
|
|
2178
|
+
enabled: z.boolean(),
|
|
2179
|
+
url: z.optional(z.url()),
|
|
2180
|
+
lastUpdated: z.optional(IsoDate)
|
|
2181
|
+
})),
|
|
2182
|
+
termsOfService: z.optional(z.object({
|
|
2183
|
+
enabled: z.boolean(),
|
|
2184
|
+
url: z.optional(z.url()),
|
|
2185
|
+
lastUpdated: z.optional(z.nullable(IsoDate))
|
|
2186
|
+
})),
|
|
2187
|
+
cookieConsent: z.optional(z.object({
|
|
2188
|
+
enabled: z.boolean(),
|
|
2189
|
+
type: z.optional(z.enum(["opt-in", "opt-out"])),
|
|
2190
|
+
position: z.optional(z.enum([
|
|
2191
|
+
"top",
|
|
2192
|
+
"bottom",
|
|
2193
|
+
"center"
|
|
2194
|
+
])),
|
|
2195
|
+
categories: z.optional(z.array(z.string()))
|
|
2196
|
+
}))
|
|
2197
|
+
})),
|
|
2198
|
+
icons: z.optional(z.object({
|
|
2199
|
+
ui: z.optional(z.string()),
|
|
2200
|
+
brand: z.optional(z.string()),
|
|
2201
|
+
semantic: z.optional(z.array(z.string())),
|
|
2202
|
+
"fa7-solid": z.optional(z.array(z.string())),
|
|
2203
|
+
"fa7-regular": z.optional(z.array(z.string())),
|
|
2204
|
+
"fa7-light": z.optional(z.array(z.string())),
|
|
2205
|
+
"fa7-thin": z.optional(z.array(z.string())),
|
|
2206
|
+
"fa7-brands": z.optional(z.array(z.string())),
|
|
2207
|
+
"simple-icons": z.optional(z.array(z.string())),
|
|
2208
|
+
"material-symbols": z.optional(z.array(z.string())),
|
|
2209
|
+
lucide: z.optional(z.array(z.string()))
|
|
2210
|
+
})),
|
|
2211
|
+
favicon: z.optional(z.object({
|
|
2212
|
+
source: z.string(),
|
|
2213
|
+
lowResSource: z.optional(z.string())
|
|
2214
|
+
})),
|
|
2215
|
+
deployment: z.optional(z.object({
|
|
2216
|
+
platform: z.optional(z.string()),
|
|
2217
|
+
buildCommand: z.optional(z.string()),
|
|
2218
|
+
deployCommand: z.optional(z.string()),
|
|
2219
|
+
outputDirectory: z.optional(z.string()),
|
|
2220
|
+
type: z.optional(z.string())
|
|
2221
|
+
}))
|
|
2222
|
+
});
|
|
2223
|
+
const SITE_SCHEMA_URL = "https://unpkg.com/@getvitops/generator/site.schema.json";
|
|
2224
|
+
/** The published JSON Schema (draft-2020-12), derived from the zod schema. */
|
|
2225
|
+
const siteJsonSchema = {
|
|
2226
|
+
$id: SITE_SCHEMA_URL,
|
|
2227
|
+
...z.toJSONSchema(SiteConfigSchema, { target: "draft-2020-12" })
|
|
2228
|
+
};
|
|
2229
|
+
const issue = (path, message) => ({
|
|
2230
|
+
code: "custom",
|
|
2231
|
+
path,
|
|
2232
|
+
message
|
|
2233
|
+
});
|
|
2234
|
+
/**
|
|
2235
|
+
* Validate a raw (already parsed) config: shape via zod, then the cross-field
|
|
2236
|
+
* integrity that JSON Schema can't express (referential keys, extends acyclicity).
|
|
2237
|
+
* These live only here — the published JSON Schema enforces shape, not integrity.
|
|
2238
|
+
*/
|
|
2239
|
+
function validateSite(input) {
|
|
2240
|
+
const parsed = z.safeParse(SiteConfigSchema, input);
|
|
2241
|
+
if (!parsed.success) return {
|
|
2242
|
+
ok: false,
|
|
2243
|
+
data: void 0,
|
|
2244
|
+
errors: parsed.error.issues
|
|
2245
|
+
};
|
|
2246
|
+
const cfg = parsed.data;
|
|
2247
|
+
const errors = [];
|
|
2248
|
+
const has = (m, k) => !!m && k in m;
|
|
2249
|
+
if (!has(cfg.locales, cfg.defaultLocale)) errors.push(issue(["defaultLocale"], `defaultLocale "${cfg.defaultLocale}" is not in locales`));
|
|
2250
|
+
if (!has(cfg.designSystem, "default") && cfg.defaultTheme == null) errors.push(issue(["designSystem"], "designSystem must include a \"default\" theme"));
|
|
2251
|
+
if (cfg.defaultTheme != null && !has(cfg.designSystem, cfg.defaultTheme)) errors.push(issue(["defaultTheme"], `defaultTheme "${cfg.defaultTheme}" is not in designSystem`));
|
|
2252
|
+
for (const [key, entry] of Object.entries(cfg.designSystem)) if (entry.extends != null && !has(cfg.designSystem, entry.extends)) errors.push(issue([
|
|
2253
|
+
"designSystem",
|
|
2254
|
+
key,
|
|
2255
|
+
"extends"
|
|
2256
|
+
], `extends "${entry.extends}" is not a designSystem key`));
|
|
2257
|
+
for (const key of Object.keys(cfg.designSystem)) try {
|
|
2258
|
+
resolveTheme(cfg.designSystem, key);
|
|
2259
|
+
} catch (e) {
|
|
2260
|
+
errors.push(issue(["designSystem", key], e.message));
|
|
2261
|
+
}
|
|
2262
|
+
if (cfg.primaryLocation != null && !has(cfg.locations, cfg.primaryLocation)) errors.push(issue(["primaryLocation"], `primaryLocation "${cfg.primaryLocation}" is not in locations`));
|
|
2263
|
+
if (typeof cfg.contact === "string" && !has(cfg.locations, cfg.contact)) errors.push(issue(["contact"], `contact "${cfg.contact}" is not a locations key`));
|
|
2264
|
+
for (const [name, v] of Object.entries(cfg.abTesting?.variants ?? {})) if (!has(cfg.environments, v.environment)) errors.push(issue([
|
|
2265
|
+
"abTesting",
|
|
2266
|
+
"variants",
|
|
2267
|
+
name,
|
|
2268
|
+
"environment"
|
|
2269
|
+
], `environment "${v.environment}" is not in environments`));
|
|
2270
|
+
for (const [i, a] of (cfg.domains?.aliases ?? []).entries()) if (a.environment != null && !has(cfg.environments, a.environment)) errors.push(issue([
|
|
2271
|
+
"domains",
|
|
2272
|
+
"aliases",
|
|
2273
|
+
i,
|
|
2274
|
+
"environment"
|
|
2275
|
+
], `environment "${a.environment}" is not in environments`));
|
|
2276
|
+
const templates = cfg.templates;
|
|
2277
|
+
const at = cfg.navigation?.activeTemplate;
|
|
2278
|
+
if (at?.default != null && !has(templates, at.default)) errors.push(issue([
|
|
2279
|
+
"navigation",
|
|
2280
|
+
"activeTemplate",
|
|
2281
|
+
"default"
|
|
2282
|
+
], `template "${at.default}" is not in templates`));
|
|
2283
|
+
for (const [bp, tpl] of Object.entries(at?.breakpoints ?? {})) if (!has(templates, tpl)) errors.push(issue([
|
|
2284
|
+
"navigation",
|
|
2285
|
+
"activeTemplate",
|
|
2286
|
+
"breakpoints",
|
|
2287
|
+
bp
|
|
2288
|
+
], `template "${tpl}" is not in templates`));
|
|
2289
|
+
if (errors.length) return {
|
|
2290
|
+
ok: false,
|
|
2291
|
+
data: void 0,
|
|
2292
|
+
errors
|
|
2293
|
+
};
|
|
2294
|
+
return {
|
|
2295
|
+
ok: true,
|
|
2296
|
+
data: cfg,
|
|
2297
|
+
errors: []
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Resolve a design-system theme by following its `extends` chain and deep-merging
|
|
2302
|
+
* base → child (child wins; arrays replace wholesale, correct for token overrides).
|
|
2303
|
+
* Throws on unknown keys or an `extends` cycle. The result is a plain
|
|
2304
|
+
* `DesignSystem` (validate it against `DesignSystemSchema` for completeness).
|
|
2305
|
+
*/
|
|
2306
|
+
function resolveTheme(designSystem, key) {
|
|
2307
|
+
const chain = [];
|
|
2308
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2309
|
+
let cur = key;
|
|
2310
|
+
while (cur != null) {
|
|
2311
|
+
if (seen.has(cur)) throw new Error(`extends cycle in designSystem at "${cur}"`);
|
|
2312
|
+
seen.add(cur);
|
|
2313
|
+
const entry = designSystem[cur];
|
|
2314
|
+
if (entry == null) throw new Error(`designSystem theme "${cur}" not found`);
|
|
2315
|
+
chain.unshift(entry);
|
|
2316
|
+
cur = entry.extends;
|
|
2317
|
+
}
|
|
2318
|
+
const out = {};
|
|
2319
|
+
for (const entry of chain) {
|
|
2320
|
+
const { extends: _e, displayName: _d, ...ds } = entry;
|
|
2321
|
+
deepMerge(out, ds);
|
|
2322
|
+
}
|
|
2323
|
+
return out;
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Resolve a raw (already-parsed) config into a validated `SiteConfig`:
|
|
2327
|
+
* 1. strip YAML nulls,
|
|
2328
|
+
* 2. apply the active A/B variant's `overrides` (by `siteEnv`),
|
|
2329
|
+
* 3. normalise a bare-`DesignSystem` `designSystem` shorthand to `{ default: … }`,
|
|
2330
|
+
* 4. validate (shape + cross-field).
|
|
2331
|
+
* Pure — no YAML/env access. A loader supplies `siteEnv`.
|
|
2332
|
+
*/
|
|
2333
|
+
function resolveSiteConfig(input, siteEnv = "production") {
|
|
2334
|
+
const raw = stripNulls(input);
|
|
2335
|
+
const envConfig = raw.environments?.[siteEnv];
|
|
2336
|
+
if (envConfig?.variant) {
|
|
2337
|
+
const overrides = raw.abTesting?.variants?.[envConfig.variant]?.overrides;
|
|
2338
|
+
if (overrides) deepMerge(raw, overrides);
|
|
2339
|
+
}
|
|
2340
|
+
const ds = raw.designSystem;
|
|
2341
|
+
if (ds != null && "colors" in ds) raw.designSystem = { default: ds };
|
|
2342
|
+
const result = validateSite(raw);
|
|
2343
|
+
if (!result.ok) {
|
|
2344
|
+
const msg = result.errors.map((e) => ` ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
2345
|
+
throw new Error(`Invalid site config:\n${msg}`);
|
|
2346
|
+
}
|
|
2347
|
+
return result.data;
|
|
2348
|
+
}
|
|
2349
|
+
/** Strip nulls (YAML `~`/`null`) → undefined so optional fields validate. */
|
|
2350
|
+
function stripNulls(obj) {
|
|
2351
|
+
if (obj === null) return void 0;
|
|
2352
|
+
if (Array.isArray(obj)) return obj.map(stripNulls);
|
|
2353
|
+
if (obj && typeof obj === "object") {
|
|
2354
|
+
const result = {};
|
|
2355
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2356
|
+
const stripped = stripNulls(value);
|
|
2357
|
+
if (stripped !== void 0) result[key] = stripped;
|
|
2358
|
+
}
|
|
2359
|
+
return result;
|
|
2360
|
+
}
|
|
2361
|
+
return obj;
|
|
2362
|
+
}
|
|
2363
|
+
/** Deep-merge `source` into `target` (in place). Arrays replace wholesale. */
|
|
2364
|
+
function deepMerge(target, source) {
|
|
2365
|
+
for (const key of Object.keys(source)) {
|
|
2366
|
+
const s = source[key];
|
|
2367
|
+
const t = target[key];
|
|
2368
|
+
if (s && typeof s === "object" && !Array.isArray(s) && t && typeof t === "object" && !Array.isArray(t)) deepMerge(t, s);
|
|
2369
|
+
else target[key] = s;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
//#endregion
|
|
2374
|
+
//#region src/index.ts
|
|
2375
|
+
/**
|
|
2376
|
+
* A minimal but complete starter design system, used by `vitops init`. It
|
|
2377
|
+
* validates against the schema and generates real output in every format, so a
|
|
2378
|
+
* new project can `vitops generate` immediately and then tune from here.
|
|
2379
|
+
*/
|
|
2380
|
+
function defaultConfig() {
|
|
2381
|
+
return {
|
|
2382
|
+
$schema: SCHEMA_URL,
|
|
2383
|
+
colors: {
|
|
2384
|
+
palette: {
|
|
2385
|
+
brand: {
|
|
2386
|
+
xxd: "#0a1f17",
|
|
2387
|
+
xd: "#123a2b",
|
|
2388
|
+
d: "#1f6b4f",
|
|
2389
|
+
base: "#2e9b73",
|
|
2390
|
+
l: "#5cbf9a",
|
|
2391
|
+
xl: "#93dcc0",
|
|
2392
|
+
xxl: "#c8f0e2"
|
|
2393
|
+
},
|
|
2394
|
+
ink: {
|
|
2395
|
+
xxd: "#0b0e13",
|
|
2396
|
+
xd: "#151a22",
|
|
2397
|
+
d: "#2a323f",
|
|
2398
|
+
base: "#3f4a5c",
|
|
2399
|
+
l: "#6b7688",
|
|
2400
|
+
xl: "#a3abb8",
|
|
2401
|
+
xxl: "#d7dbe2"
|
|
2402
|
+
}
|
|
2403
|
+
},
|
|
2404
|
+
utilities: [
|
|
2405
|
+
"bg",
|
|
2406
|
+
"text",
|
|
2407
|
+
"border"
|
|
2408
|
+
],
|
|
2409
|
+
schemes: {
|
|
2410
|
+
default: {
|
|
2411
|
+
appearance: "light",
|
|
2412
|
+
semantic: {
|
|
2413
|
+
"brand-primary": "brand",
|
|
2414
|
+
surface: "ink",
|
|
2415
|
+
neutral: "ink"
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
alternate: {
|
|
2419
|
+
appearance: "dark",
|
|
2420
|
+
semantic: {
|
|
2421
|
+
surface: { invert: true },
|
|
2422
|
+
neutral: { invert: true }
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
},
|
|
2427
|
+
shadows: {
|
|
2428
|
+
sm: "0 1px 2px rgb(0 0 0 / 0.08)",
|
|
2429
|
+
md: "0 4px 6px rgb(0 0 0 / 0.12)",
|
|
2430
|
+
lg: "0 10px 15px rgb(0 0 0 / 0.18)"
|
|
2431
|
+
},
|
|
2432
|
+
fonts: {
|
|
2433
|
+
display: "system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
|
2434
|
+
sans: "system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif",
|
|
2435
|
+
mono: "ui-monospace, \"SF Mono\", Menlo, monospace"
|
|
2436
|
+
},
|
|
2437
|
+
typeScale: {
|
|
2438
|
+
base: "1rem",
|
|
2439
|
+
names: [
|
|
2440
|
+
"2xs",
|
|
2441
|
+
"xs",
|
|
2442
|
+
"s",
|
|
2443
|
+
"m",
|
|
2444
|
+
"l",
|
|
2445
|
+
"xl",
|
|
2446
|
+
"2xl",
|
|
2447
|
+
"3xl"
|
|
2448
|
+
],
|
|
2449
|
+
baseStep: 4,
|
|
2450
|
+
baseline: "m",
|
|
2451
|
+
ratio: 1.2,
|
|
2452
|
+
fluid: {
|
|
2453
|
+
minVw: "22.5rem",
|
|
2454
|
+
maxVw: "80rem",
|
|
2455
|
+
minRatio: 1.12
|
|
2456
|
+
}
|
|
2457
|
+
},
|
|
2458
|
+
spaceScale: {
|
|
2459
|
+
base: "1rem",
|
|
2460
|
+
names: [
|
|
2461
|
+
"2xs",
|
|
2462
|
+
"xs",
|
|
2463
|
+
"s",
|
|
2464
|
+
"m",
|
|
2465
|
+
"l",
|
|
2466
|
+
"xl",
|
|
2467
|
+
"2xl",
|
|
2468
|
+
"3xl"
|
|
2469
|
+
],
|
|
2470
|
+
baseStep: 4,
|
|
2471
|
+
baseline: "m",
|
|
2472
|
+
ratio: 1.25,
|
|
2473
|
+
fluid: {
|
|
2474
|
+
minVw: "22.5rem",
|
|
2475
|
+
maxVw: "80rem",
|
|
2476
|
+
minRatio: 1.2
|
|
2477
|
+
}
|
|
2478
|
+
},
|
|
2479
|
+
patterns: {
|
|
2480
|
+
defaults: {
|
|
2481
|
+
ds: "none",
|
|
2482
|
+
b: "1px solid var(--color-surface-xl)",
|
|
2483
|
+
br: "0.375rem",
|
|
2484
|
+
p: "1rem",
|
|
2485
|
+
fs: "1rem"
|
|
2486
|
+
},
|
|
2487
|
+
radii: {
|
|
2488
|
+
pill: "999px",
|
|
2489
|
+
circle: "50%",
|
|
2490
|
+
card: "0.5rem"
|
|
2491
|
+
},
|
|
2492
|
+
groups: {
|
|
2493
|
+
control: {
|
|
2494
|
+
ds: "var(--ds-default)",
|
|
2495
|
+
b: "var(--b-default)",
|
|
2496
|
+
br: "var(--br-default)",
|
|
2497
|
+
p: "0.5em 1em",
|
|
2498
|
+
fs: "var(--fs-default)"
|
|
2499
|
+
},
|
|
2500
|
+
panel: {
|
|
2501
|
+
ds: "var(--shadow-md)",
|
|
2502
|
+
b: "var(--b-default)",
|
|
2503
|
+
br: "0.5rem",
|
|
2504
|
+
p: "1rem",
|
|
2505
|
+
fs: "var(--fs-default)"
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
z: {
|
|
2509
|
+
raised: 1,
|
|
2510
|
+
sticky: 100,
|
|
2511
|
+
overlay: 200,
|
|
2512
|
+
top: 1e3
|
|
2513
|
+
},
|
|
2514
|
+
items: {
|
|
2515
|
+
button: {
|
|
2516
|
+
group: "control",
|
|
2517
|
+
element: "button",
|
|
2518
|
+
default_role: "brand-primary",
|
|
2519
|
+
base: {
|
|
2520
|
+
padding: "0.6em 1.2em",
|
|
2521
|
+
"border-radius": "var(--br-control, 0.25rem)",
|
|
2522
|
+
border: "none",
|
|
2523
|
+
"font-weight": "600",
|
|
2524
|
+
color: "#fff",
|
|
2525
|
+
cursor: "pointer",
|
|
2526
|
+
"box-shadow": "var(--shadow-sm)"
|
|
2527
|
+
},
|
|
2528
|
+
states: {
|
|
2529
|
+
hover: { step: 1 },
|
|
2530
|
+
active: { scale: .97 },
|
|
2531
|
+
"focus-visible": { ring: true }
|
|
2532
|
+
},
|
|
2533
|
+
roles: []
|
|
2534
|
+
},
|
|
2535
|
+
card: {
|
|
2536
|
+
group: "panel",
|
|
2537
|
+
class: "card",
|
|
2538
|
+
base: {
|
|
2539
|
+
"border-radius": "var(--br-card-group)",
|
|
2540
|
+
padding: "1.5rem",
|
|
2541
|
+
background: "var(--color-surface-xxl)",
|
|
2542
|
+
border: "var(--b-card-group)",
|
|
2543
|
+
"box-shadow": "var(--ds-card-group)"
|
|
2544
|
+
},
|
|
2545
|
+
states: {},
|
|
2546
|
+
roles: []
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
},
|
|
2550
|
+
typography: {
|
|
2551
|
+
families: {
|
|
2552
|
+
display: "var(--font-display)",
|
|
2553
|
+
sans: "var(--font-sans)",
|
|
2554
|
+
code: "var(--font-mono, ui-monospace, monospace)"
|
|
2555
|
+
},
|
|
2556
|
+
roles: {
|
|
2557
|
+
display: {
|
|
2558
|
+
family: "display",
|
|
2559
|
+
size: "var(--text-3xl)",
|
|
2560
|
+
weight: 700,
|
|
2561
|
+
"line-height": "1.05",
|
|
2562
|
+
tracking: "-0.03em"
|
|
2563
|
+
},
|
|
2564
|
+
heading: {
|
|
2565
|
+
family: "display",
|
|
2566
|
+
size: "var(--text-xl)",
|
|
2567
|
+
weight: 600,
|
|
2568
|
+
"line-height": "1.3"
|
|
2569
|
+
},
|
|
2570
|
+
body: {
|
|
2571
|
+
family: "sans",
|
|
2572
|
+
size: "var(--text-m)",
|
|
2573
|
+
weight: 400,
|
|
2574
|
+
"line-height": "1.55"
|
|
2575
|
+
}
|
|
2576
|
+
},
|
|
2577
|
+
headings: {
|
|
2578
|
+
h1: "display",
|
|
2579
|
+
h2: "heading",
|
|
2580
|
+
h3: "heading"
|
|
2581
|
+
}
|
|
2582
|
+
},
|
|
2583
|
+
animations: {
|
|
2584
|
+
effects: {
|
|
2585
|
+
"fade-in": {
|
|
2586
|
+
kf: "composite",
|
|
2587
|
+
vars: { "opacity-from": 0 }
|
|
2588
|
+
},
|
|
2589
|
+
"slide-up": {
|
|
2590
|
+
kf: "composite",
|
|
2591
|
+
vars: { "translate-y-from": "var(--slide-distance, 2rem)" }
|
|
2592
|
+
}
|
|
2593
|
+
},
|
|
2594
|
+
journeys: {
|
|
2595
|
+
base: {
|
|
2596
|
+
fade: { "opacity-from": 0 },
|
|
2597
|
+
slide: {}
|
|
2598
|
+
},
|
|
2599
|
+
compose: [
|
|
2600
|
+
["fade"],
|
|
2601
|
+
["slide"],
|
|
2602
|
+
["fade", "slide"]
|
|
2603
|
+
]
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
//#endregion
|
|
2610
|
+
export { DesignSystemSchema, SCHEMA_URL, SITE_SCHEMA_URL, SiteConfigSchema, deepMerge, defaultConfig, generate, generateDocs, jsonSchema, resolveSiteConfig, resolveTheme, siteJsonSchema, stripNulls, validate, validateSite };
|
|
2611
|
+
//# sourceMappingURL=index.mjs.map
|