@getvitops/generator 0.3.0 → 0.5.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/dist/index.mjs CHANGED
@@ -13,17 +13,25 @@ import { generateOklchPalette, hexToOklch, oklchStringToHex, pickOn } from "@get
13
13
  * Authored with `zod/mini` (tree-shakeable Zod v4). Everything else derives from it:
14
14
  * • the `DesignSystem` TS type (`z.infer`), used throughout the generator,
15
15
  * • the published JSON Schema (`toJSONSchema`), emitted to `schema.json` for `$schema` refs,
16
- * • runtime validation (`validate`) for the CLI and to gate `generate`.
16
+ * • runtime validation (`validate`) for the CLI and to gate `generate`,
17
+ * • the generated `authoring.md` reference doc (walks `jsonSchema`'s descriptions).
17
18
  */
18
- const CssDecls = z.record(z.string(), z.string());
19
+ /**
20
+ * Attach a JSON-Schema `description`. zod/mini has no `.describe()` method; the
21
+ * mini-API equivalent is `.check(z.describe(…))`, and `.check()` clones — so a
22
+ * shared sub-schema (`Scale`, `CssDecls`, …) can carry a different description
23
+ * per use-site without mutating the original.
24
+ */
25
+ const desc = (schema, text) => schema.check(z.describe(text));
26
+ const CssDecls = desc(z.record(z.string(), z.string()), "A CSS declaration block: property → value. Values stay strings (they can be hex, var(), clamp(), keywords, …); the generator, not the schema, interprets them.");
19
27
  const Scalar = z.union([z.string(), z.number()]);
20
28
  const VarMap = z.record(z.string(), Scalar);
21
- const SeededRamp = z.object({
22
- seed: z.string(),
23
- anchors: z.optional(z.record(z.string(), z.string()))
24
- });
25
- const FixedRamp = z.object({ tones: z.union([z.array(z.string()), z.record(z.string(), z.string())]) });
26
- const Ramp = z.union([SeededRamp, FixedRamp]);
29
+ const SeededRamp = desc(z.object({
30
+ seed: desc(z.string(), "Seed colour (hex or oklch()). An 11-step numeric scale (50…950, tinted near-white → tinted near-black) is generated in OKLCH from it; the seed is preserved at its natural step."),
31
+ anchors: z.optional(desc(z.record(z.string(), z.string()), "Step → colour overrides (hex or oklch()) that pin specific steps of the generated scale; the remaining steps interpolate around them."))
32
+ }), "Seeded hue: the 11-step scale is GENERATED in OKLCH from `seed` (anchors pin specific steps).");
33
+ const FixedRamp = desc(z.object({ tones: desc(z.union([z.array(z.string()), z.record(z.string(), z.string())]), "Fixed brand kit: authored tones placed verbatim at their nearest steps plus tinted off-white/off-black endpoints; no interpolation. Either an ordered light → dark array or a step → colour map.") }), "Fixed hue: authored brand tones used verbatim; no generation.");
34
+ const Ramp = desc(z.union([SeededRamp, FixedRamp]), "A palette hue, authored one of two ways: `{ seed, anchors? }` generates an 11-step numeric OKLCH scale (50…950) from the seed, or `{ tones }` supplies a fixed brand kit used verbatim.");
27
35
  const UtilityName = z.enum([
28
36
  "bg",
29
37
  "text",
@@ -32,66 +40,66 @@ const UtilityName = z.enum([
32
40
  "fill",
33
41
  "stroke"
34
42
  ]);
35
- const Colors = z.object({
36
- palette: z.record(z.string(), Ramp),
37
- roles: z.record(z.string(), z.string()),
38
- utilities: z.optional(z.array(UtilityName))
39
- });
43
+ const Colors = desc(z.object({
44
+ palette: desc(z.record(z.string(), Ramp), "Palette hues by name. Each becomes an 11-step numeric OKLCH scale (`--color-<hue>-50…950`)."),
45
+ roles: desc(z.record(z.string(), z.string()), "Maps each semantic role (neutral, surface, ui-primary/secondary/accent, brand-primary/secondary, info, success, warning, danger) to the palette hue backing it. The generator derives every role's FUNCTIONAL tokens (`--<role>-{bg,bg-muted,border,border-bold,solid,solid-bold,on-solid,text,text-muted,text-x-muted}`) plus emphasis stops from the hue's scale, and dark mode flips automatically — there is no per-appearance scheme grammar."),
46
+ utilities: z.optional(desc(z.array(UtilityName), "Which colour utility-class families to emit (`bg-*`, `text-*`, `border-*`, `outline-*`, `fill-*`, `stroke-*`). Defaults to bg, text, border."))
47
+ }), "The colour system (the only required section): `palette` hues become generated OKLCH scales; `roles` map semantic roles onto those hues, from which all functional tokens and dark mode derive.");
40
48
  const Scale = z.object({
41
- base: z.string(),
42
- ratio: z.number(),
43
- steps: z.optional(z.number()),
44
- names: z.optional(z.array(z.string())),
45
- baseStep: z.optional(z.number()),
46
- baseline: z.optional(z.string()),
47
- fluid: z.optional(z.object({
48
- minVw: z.string(),
49
- maxVw: z.string(),
50
- minRatio: z.number()
51
- }))
52
- });
53
- const Pattern = z.object({
54
- group: z.optional(z.string()),
55
- overrides: z.optional(CssDecls),
56
- element: z.optional(z.string()),
57
- class: z.optional(z.string()),
58
- default_role: z.optional(z.string()),
59
- base: z.optional(CssDecls),
60
- states: z.optional(z.record(z.string(), z.record(z.string(), z.unknown()))),
61
- roles: z.optional(z.array(z.string()))
62
- });
63
- const Patterns = z.object({
64
- defaults: z.optional(CssDecls),
65
- radii: z.optional(CssDecls),
66
- groups: z.optional(z.record(z.string(), CssDecls)),
67
- z: z.optional(z.record(z.string(), z.number())),
68
- items: z.optional(z.record(z.string(), Pattern))
69
- });
70
- const TypographyRole = z.record(z.string(), Scalar);
71
- const Typography = z.object({
72
- families: z.optional(z.record(z.string(), z.string())),
73
- roles: z.optional(z.record(z.string(), TypographyRole)),
74
- headings: z.optional(z.record(z.string(), z.string()))
75
- });
76
- const AnimEffect = z.object({
77
- kf: z.string(),
78
- css: z.optional(VarMap),
79
- vars: z.optional(VarMap)
80
- });
81
- const Animations = z.object({
82
- effects: z.optional(z.record(z.string(), AnimEffect)),
83
- journeys: z.optional(z.object({
84
- base: z.optional(z.record(z.string(), VarMap)),
85
- compose: z.optional(z.array(z.array(z.string())))
86
- }))
49
+ base: desc(z.string(), "Anchor size (a CSS length, e.g. \"1rem\") — the value at `baseStep`."),
50
+ ratio: desc(z.number(), "Modular ratio between adjacent steps at large viewports."),
51
+ steps: desc(z.optional(z.number()), "Token count when `names` is absent (steps are then named 1..steps)."),
52
+ names: desc(z.optional(z.array(z.string())), "Step names, smallest → largest (e.g. [\"xs\",\"sm\",\"md\",…]); each becomes a token suffix."),
53
+ baseStep: desc(z.optional(z.number()), "1-based index of the step whose value is `base`."),
54
+ baseline: desc(z.optional(z.string()), "Named step used as the fluid pivot / GUI scale centre (defaults to `baseStep`)."),
55
+ fluid: desc(z.optional(z.object({
56
+ minVw: desc(z.string(), "Viewport width (CSS length) where fluid scaling bottoms out."),
57
+ maxVw: desc(z.string(), "Viewport width (CSS length) where fluid scaling tops out."),
58
+ minRatio: desc(z.number(), "Modular ratio at/below `minVw` (usually < `ratio`).")
59
+ })), "Makes the scale fluid: each step compiles to a clamp() that interpolates from `minRatio` at `minVw` to `ratio` at `maxVw`.")
87
60
  });
61
+ const Pattern = desc(z.object({
62
+ group: desc(z.optional(z.string()), "Token-cascade group this pattern belongs to (e.g. tag / control / panel); base declarations resolve through `--<prop>-<group>` before `--<prop>-default`."),
63
+ overrides: z.optional(desc(CssDecls, "Per-pattern token overrides, emitted as `--<prop>-<name>-group` values.")),
64
+ element: desc(z.optional(z.string()), "Style at element level via zero-specificity `:where(<element>)` (instead of, or alongside, a class)."),
65
+ class: desc(z.optional(z.string()), "Class name to emit (defaults to the pattern's key)."),
66
+ default_role: desc(z.optional(z.string()), "Semantic colour role applied to the bare/default variant."),
67
+ base: z.optional(desc(CssDecls, "Base CSS declarations. Geometry properties (padding, border-radius, border, box-shadow, font-size) are wrapped in per-pattern override hooks (`--p-<name>`, `--br-<name>`, `--b-<name>`, `--ds-<name>`, `--fs-<name>`) so consumers can restyle one pattern by setting one variable.")),
68
+ states: desc(z.optional(z.record(z.string(), z.record(z.string(), z.unknown()))), "Interaction states (hover / active / focus-visible), each a map of shortcuts: `step` (intensify fills/text by n emphasis stops), `scale` (transform scale), `lift` (translateY + shadow), `shadow` (a shadow name → drop-shadow(var(--shadow-<name>)), or true → lift shadow), `ring` (focus ring), or raw `css` declarations. Hover rules are wrapped in `@media (hover: hover)`."),
69
+ roles: desc(z.optional(z.array(z.string())), "Semantic colour role variants to emit as `<pattern>-<role>` classes (fills use the role solid / on-solid tokens).")
70
+ }), "One component pattern (button, link, badge, card, …): base declarations + interaction states + semantic role variants, resolved through the pattern token cascade.");
71
+ const Patterns = desc(z.object({
72
+ defaults: z.optional(desc(CssDecls, "Cascade-wide fallback tokens, emitted as `--<prop>-default`.")),
73
+ radii: z.optional(desc(CssDecls, "Shape primitives, emitted as `--br-<name>` (referenced by pattern bases).")),
74
+ groups: desc(z.optional(z.record(z.string(), CssDecls)), "Group-level tokens, emitted as `--<prop>-<group>`; patterns opt in via their `group` key."),
75
+ z: desc(z.optional(z.record(z.string(), z.number())), "Z-index tiers → `--z-tier-<name>`."),
76
+ items: desc(z.optional(z.record(z.string(), Pattern)), "The component patterns to emit, keyed by name.")
77
+ }), "Component patterns and their token cascade: `defaults` → `groups` → per-pattern `overrides`, plus shape (`radii`) and z-index primitives.");
78
+ const TypographyRole = desc(z.record(z.string(), Scalar), "An open bag of CSS-ish keys (family / size / weight / line-height / tracking / transform / decoration / text-wrap / …); the generator maps known keys and passes the rest through.");
79
+ const Typography = desc(z.object({
80
+ families: desc(z.optional(z.record(z.string(), z.string())), "Role-facing family aliases → CSS font values, usually referencing the top-level `fonts` tokens (e.g. \"var(--font-display)\")."),
81
+ roles: desc(z.optional(z.record(z.string(), TypographyRole)), "Semantic type roles (display, title, heading, body, quote, caption, eyebrow, code, lead, footnote, tag, …), each emitted as a `font-<role>` class."),
82
+ headings: desc(z.optional(z.record(z.string(), z.string())), "Maps heading elements (h1…h6) to type roles so bare headings pick up role styling.")
83
+ }), "Typography: family aliases, semantic type roles (→ `font-<role>` classes), and the heading-element → role mapping.");
84
+ const AnimEffect = desc(z.object({
85
+ kf: desc(z.string(), "Keyframe family driving the effect: composite (transform/opacity), paint, or layout."),
86
+ css: z.optional(desc(VarMap, "Extra literal declarations merged into the effect class as-is.")),
87
+ vars: z.optional(desc(VarMap, "Effect endpoint variables (`--<key>: <value>`, e.g. opacity-from, translate-y-to) that override the keyframe defaults."))
88
+ }), "A named animation effect class — a pure value layer (`--_anim` + `--<prop>-from/-to`) over the static keyframe engine.");
89
+ const Animations = desc(z.object({
90
+ effects: desc(z.optional(z.record(z.string(), AnimEffect)), "Effect classes to emit (`.fade-in`, `.reveal-left`, …), keyed by class name."),
91
+ journeys: desc(z.optional(z.object({
92
+ base: desc(z.optional(z.record(z.string(), VarMap)), "Named journey building blocks: part name → var map."),
93
+ compose: desc(z.optional(z.array(z.array(z.string()))), "Combinations of base parts, each emitted as a `.<parts>-journey` class.")
94
+ })), "Multi-part journey classes composed from `base` building blocks.")
95
+ }), "Animation effect + journey classes (pure value layers). The animation engine itself — keyframes, drivers, floats, utilities — is static framework CSS, not configured here.");
88
96
  const DesignSystemSchema = z.object({
89
- $schema: z.optional(z.string()),
97
+ $schema: desc(z.optional(z.string()), "URL of the published JSON Schema (stamped by `vitops init`) so editors provide autocomplete + validation."),
90
98
  colors: Colors,
91
- shadows: z.optional(z.record(z.string(), z.string())),
92
- fonts: z.optional(z.record(z.string(), z.string())),
93
- typeScale: z.optional(Scale),
94
- spaceScale: z.optional(Scale),
99
+ shadows: desc(z.optional(z.record(z.string(), z.string())), "Named shadows → `--shadow-<name>` tokens and `.drop-shadow-<name>` utilities. Values are shadow parameter lists (offset/blur/colour), applied via `filter: drop-shadow(…)`."),
100
+ fonts: desc(z.optional(z.record(z.string(), z.string())), "Raw font stacks by name, emitted as `--font-<name>` tokens (referenced by `typography.families`)."),
101
+ typeScale: desc(z.optional(Scale), "Fluid modular TYPE scale → `--text-<name>` tokens, consumed by typography roles and text-size utilities."),
102
+ spaceScale: desc(z.optional(Scale), "Fluid modular SPACE scale → `--space-<name>` tokens, consumed by spacing/gap utilities and vertical rhythm."),
95
103
  patterns: z.optional(Patterns),
96
104
  typography: z.optional(Typography),
97
105
  animations: z.optional(Animations)
@@ -142,6 +150,89 @@ function validate(input) {
142
150
  };
143
151
  }
144
152
 
153
+ //#endregion
154
+ //#region src/shared.ts
155
+ /**
156
+ * Data shared between the generator (`generate.ts`) and the docs bundle
157
+ * (`docs.ts`). Lives in its own module because `generate.ts` imports `docs.ts` —
158
+ * importing back the other way would be a cycle.
159
+ */
160
+ /**
161
+ * Pattern base geometry props → per-pattern override-hook shorthand. Each hook
162
+ * wraps the authored value (`padding: var(--p-button, 0.6em 1.2em)`) so a
163
+ * consumer restyles one pattern by setting one variable.
164
+ */
165
+ const BASE_HOOK = {
166
+ padding: "p",
167
+ "border-radius": "br",
168
+ border: "b",
169
+ "box-shadow": "ds",
170
+ "font-size": "fs"
171
+ };
172
+ /**
173
+ * Framework utility classes whose names collide with Tailwind's own defaults.
174
+ * The `tailwind` format strips any rule targeting these — Tailwind provides
175
+ * them, so consumers use Tailwind's utilities for these names instead.
176
+ */
177
+ const TW_CLASH = new Set([
178
+ "block",
179
+ "inline",
180
+ "inline-block",
181
+ "flow-root",
182
+ "flex",
183
+ "inline-flex",
184
+ "grid",
185
+ "inline-grid",
186
+ "table",
187
+ "inline-table",
188
+ "table-caption",
189
+ "table-cell",
190
+ "table-row",
191
+ "contents",
192
+ "hidden",
193
+ "list-item",
194
+ "static",
195
+ "fixed",
196
+ "absolute",
197
+ "relative",
198
+ "sticky",
199
+ "isolate",
200
+ "visible",
201
+ "invisible",
202
+ "collapse",
203
+ "sr-only",
204
+ "not-sr-only",
205
+ "flex-row",
206
+ "flex-row-reverse",
207
+ "flex-col",
208
+ "flex-col-reverse",
209
+ "flex-wrap",
210
+ "flex-nowrap",
211
+ "items-start",
212
+ "items-end",
213
+ "items-center",
214
+ "items-baseline",
215
+ "items-stretch",
216
+ "justify-start",
217
+ "justify-end",
218
+ "justify-center",
219
+ "justify-between",
220
+ "justify-around",
221
+ "justify-evenly",
222
+ "content-start",
223
+ "content-end",
224
+ "content-center",
225
+ "content-between",
226
+ "content-around",
227
+ "content-evenly",
228
+ "text-left",
229
+ "text-center",
230
+ "text-right",
231
+ "text-justify",
232
+ "text-start",
233
+ "text-end"
234
+ ]);
235
+
145
236
  //#endregion
146
237
  //#region src/tokens.ts
147
238
  /**
@@ -822,6 +913,11 @@ This is a **rule reference**, not an exhaustive list: each family below is a nam
822
913
  plus the set of tokens it expands over. Applying a rule to any listed token yields a valid
823
914
  class (e.g. rule \`bg-<color>\` + colour \`pine-xl\` → \`bg-pine-xl\`).
824
915
 
916
+ The tokens themselves are authored in \`design-system.json\` — see
917
+ [/authoring.md](../authoring.md); the systems behind them are explained in
918
+ [/concepts/](../concepts/index.md); per-format output differences (including which of
919
+ these utilities Tailwind provides natively) in [/formats.md](../formats.md).
920
+
825
921
  ## Responsive & state variant grammar
826
922
 
827
923
  Every utility accepts a **container-breakpoint prefix**; animation utilities also accept a
@@ -911,7 +1007,9 @@ element; scroll-linked entrances resolve within the first portion of the element
911
1007
  ## Component patterns
912
1008
 
913
1009
  Each pattern is a base class \`<pattern>\` with interaction states (hover/active/focus-visible)
914
- baked in; coloured patterns add role variants via rule **\`<pattern>-<role>\`**.
1010
+ baked in; coloured patterns add role variants via rule **\`<pattern>-<role>\`**. (How the
1011
+ pattern CSS is assembled — token cascade, \`--p-<pattern>\`-style override hooks, state
1012
+ shortcuts — is explained in [/concepts/patterns.md](../concepts/patterns.md).)
915
1013
 
916
1014
  - Patterns: ${code(patterns)}.
917
1015
  - Roles (for coloured patterns like \`badge\`, \`tag\`, \`status\`, \`button\`):
@@ -920,6 +1018,368 @@ baked in; coloured patterns add role variants via rule **\`<pattern>-<role>\`**.
920
1018
  - Shape primitives: \`--br-<name>\` radii — ${code(radii)}.
921
1019
  `;
922
1020
  }
1021
+ const isPrimitive = (n) => !n.properties && !n.anyOf && n.type != null && n.type !== "object" && n.type !== "array";
1022
+ function typeLabel(node) {
1023
+ if (node.enum) return node.enum.map(String).join(" | ");
1024
+ if (node.anyOf) return node.anyOf.every(isPrimitive) ? node.anyOf.map((v) => v.type).join(" | ") : "one of";
1025
+ if (node.type === "array") {
1026
+ const it = node.items;
1027
+ if (it?.enum) return `array of ${it.enum.map(String).join(" | ")}`;
1028
+ return it?.type && it.type !== "object" ? `array of ${it.type}` : "array";
1029
+ }
1030
+ if (node.type === "object") {
1031
+ const ap = node.additionalProperties;
1032
+ return ap && typeof ap === "object" ? "map" : "object";
1033
+ }
1034
+ return node.type ?? "value";
1035
+ }
1036
+ /** A node's documentable children: object properties, map values, or array items. */
1037
+ function childEntries(node) {
1038
+ if (node.properties) {
1039
+ const req = node.required ?? [];
1040
+ return Object.entries(node.properties).map(([k, v]) => ({
1041
+ label: k,
1042
+ node: v,
1043
+ required: req.includes(k)
1044
+ }));
1045
+ }
1046
+ const ap = node.additionalProperties;
1047
+ if (ap && typeof ap === "object" && (ap.description || ap.properties || ap.anyOf)) return [{
1048
+ label: "<name>",
1049
+ node: ap,
1050
+ required: false
1051
+ }];
1052
+ if (node.items && (node.items.description || node.items.properties || node.items.anyOf)) return [{
1053
+ label: "[items]",
1054
+ node: node.items,
1055
+ required: false
1056
+ }];
1057
+ return [];
1058
+ }
1059
+ function renderSchemaNode(label, node, required, depth, out) {
1060
+ const indent = " ".repeat(depth);
1061
+ const meta = `(${typeLabel(node)}${required ? ", required" : ""})`;
1062
+ out.push(`${indent}- \`${label}\` ${meta}${node.description ? ` — ${node.description}` : ""}`);
1063
+ if (depth >= 3) return;
1064
+ if (node.anyOf && !node.anyOf.every(isPrimitive)) {
1065
+ for (const variant of node.anyOf) {
1066
+ out.push(`${indent} - *one of*${variant.description ? ` — ${variant.description}` : ""}`);
1067
+ for (const c of childEntries(variant)) renderSchemaNode(c.label, c.node, c.required, depth + 2, out);
1068
+ }
1069
+ return;
1070
+ }
1071
+ for (const c of childEntries(node)) renderSchemaNode(c.label, c.node, c.required, depth + 1, out);
1072
+ }
1073
+ function renderAuthoring() {
1074
+ const schema = jsonSchema;
1075
+ const required = schema.required ?? [];
1076
+ const sections = [];
1077
+ for (const [key, node] of Object.entries(schema.properties ?? {})) {
1078
+ if (key === "$schema") continue;
1079
+ const parts = [`## \`${key}\`${required.includes(key) ? "" : " *(optional)*"}`];
1080
+ if (node.description) parts.push("", node.description);
1081
+ const bullets = [];
1082
+ for (const c of childEntries(node)) renderSchemaNode(c.label, c.node, c.required, 0, bullets);
1083
+ if (bullets.length) parts.push("", ...bullets);
1084
+ sections.push(parts.join("\n"));
1085
+ }
1086
+ return `${frontmatter({
1087
+ type: "Config Reference",
1088
+ title: "Vitops — design-system.json authoring reference",
1089
+ description: "Every field of the design-system.json config, generated from the published JSON Schema so it always matches validation.",
1090
+ resource: DS_PATH,
1091
+ tags: [
1092
+ "config",
1093
+ "schema",
1094
+ "authoring",
1095
+ "design-system"
1096
+ ]
1097
+ })}
1098
+
1099
+ # \`design-system.json\` authoring reference
1100
+
1101
+ The single source of truth every output format is generated from. Each consumer authors
1102
+ their own config — there is no shared canonical token set. The field docs below are
1103
+ rendered from the published JSON Schema, so they always match what \`vitops validate\`
1104
+ enforces.
1105
+
1106
+ - Set \`"$schema": "${SCHEMA_URL}"\` in the config for editor autocomplete + validation.
1107
+ - Scaffold a starter config with \`vitops init\`; check one with \`vitops validate\`.
1108
+ - The *why* behind each section: [colour system](concepts/color.md),
1109
+ [type & space scales](concepts/scales.md), [component patterns](concepts/patterns.md).
1110
+ - What each output format does with these tokens: [formats.md](formats.md).
1111
+
1112
+ ${sections.join("\n\n")}
1113
+ `;
1114
+ }
1115
+ function renderFormats(ds) {
1116
+ const clashList = Array.from(TW_CLASH).sort();
1117
+ const spaceName = ds.spaceScale?.baseline ?? ds.spaceScale?.names?.[0] ?? "m";
1118
+ return `${frontmatter({
1119
+ type: "Formats Reference",
1120
+ title: "Vitops — output formats (tailwind vs css vs bricks)",
1121
+ description: "What each vitops generate format emits, what the target platform provides instead, and the Tailwind-specific rules (which framework utilities are stripped in favour of Tailwind defaults).",
1122
+ resource: DS_PATH,
1123
+ tags: [
1124
+ "formats",
1125
+ "tailwind",
1126
+ "bricks",
1127
+ "css",
1128
+ "design-system"
1129
+ ]
1130
+ })}
1131
+
1132
+ # Output formats — \`tailwind\` vs \`css\` vs \`bricks\`
1133
+
1134
+ One config, three targets: \`vitops generate --format <tailwind|css|bricks>\`. The **class
1135
+ vocabulary is the same** everywhere (see [css/classes.md](css/classes.md)); what differs is
1136
+ which layers the generator emits versus which the platform provides, and the variant
1137
+ separator (\`-\` in CSS/Bricks, \`:\` / \`@\` in Tailwind).
1138
+
1139
+ ## \`tailwind\` — single-file Tailwind v4 layer
1140
+
1141
+ Emits one self-contained \`tailwind.css\` (plus \`tokens.json\`): \`@import "tailwindcss"\`,
1142
+ \`@theme\` tokens, the framework's structural CSS + component patterns inlined, and
1143
+ \`@utility\` definitions for the bespoke families (type roles, animation effects,
1144
+ split ratios, track placement).
1145
+
1146
+ **Use Tailwind's own utilities for these class names.** The framework's rules for them are
1147
+ deliberately stripped from the bundle because Tailwind provides them natively — writing
1148
+ them still works, but they are Tailwind's, not the framework's:
1149
+
1150
+ ${clashList.map((c) => `\`${c}\``).join(", ")}
1151
+
1152
+ Other Tailwind-specific behaviour:
1153
+
1154
+ - **Variants are Tailwind's job.** No pre-expanded breakpoint/state classes are emitted
1155
+ (the framework's \`@container (min-width: …)\` variant blocks are dropped); use Tailwind
1156
+ syntax — \`@md:split-1-2\`, \`hover:flip-fade-in\`. Container breakpoints are registered as
1157
+ \`--container-{sm,md,lg,xl}\` = 30/48/64/80rem, backing the \`@sm:\`…\`@xl:\` variants.
1158
+ - **The space scale is NOT mapped into Tailwind's \`--spacing-*\` namespace** (that would
1159
+ corrupt Tailwind's numeric multipliers and \`max-w-*\` sizes). Numeric utilities like
1160
+ \`p-4\` keep Tailwind's 0.25rem meaning; use the design-system scale via arbitrary
1161
+ values — \`p-(--space-${spaceName})\`, \`gap-(--space-${spaceName})\`.
1162
+ - **Functional colour roles are plain \`:root\` variables, not \`@theme\` colours**, so
1163
+ Tailwind doesn't auto-derive utilities from them; the emitted \`@utility\` set
1164
+ (\`bg-<role>\`, \`text-<role>-muted\`, …) is the public API. The raw hue scales ARE
1165
+ \`@theme\` colours, so native \`bg-<hue>-500\`-style utilities work.
1166
+
1167
+ ## \`css\` — standalone bundle
1168
+
1169
+ Emits a bundled, self-contained \`styles.css\` + \`tokens.json\` + \`design-manifest.json\`.
1170
+ The colour and font/scale layers are fully included, and every utility family is
1171
+ pre-expanded — including breakpoint/state variants with \`-\` separators
1172
+ (\`md-split-1-2\`, \`hover-fade-in\`). For non-Bricks, non-Tailwind consumers and the
1173
+ docs build.
1174
+
1175
+ ## \`bricks\` — WordPress / Bricks Builder payload
1176
+
1177
+ Emits the full deployable theme payload: \`styles.min.css\`, the Bricks import JSONs
1178
+ (\`bricks-colors-{named,semantic}.json\`, \`bricks-variables.json\`), \`tokens.json\`, the JS
1179
+ bundles (polyfills / elements / deferred), the Bricks element PHP under \`bricks/\`, and
1180
+ this docs bundle under \`docs/\`.
1181
+
1182
+ - **Bricks provides the token layer.** \`color.css\` and \`type-tokens.css\` are one-line
1183
+ stubs: the colour \`:root\` tokens, dark-mode overrides, colour utility classes, fonts,
1184
+ and type/space scales are generated live by Bricks' Color / Font / Variables Managers
1185
+ from the imported JSONs. Semantic palette entries carry \`darkEnabled\` + a \`dark\` ref so
1186
+ Bricks emits the dark-mode overrides on import.
1187
+ - Everything else (patterns, shadows, typography roles, animation effects, structural
1188
+ framework CSS) ships in \`styles.min.css\` as in the other formats.
1189
+ - Pattern states reference shadows by name, compiled to
1190
+ \`filter: drop-shadow(var(--shadow-<name>))\`.
1191
+ `;
1192
+ }
1193
+ function renderColorConcept(ds) {
1194
+ const hues = Object.keys(ds.colors.palette ?? {});
1195
+ const roles = Object.keys(ds.colors.roles ?? {});
1196
+ return `${frontmatter({
1197
+ type: "Design Concept",
1198
+ title: "Vitops colour system — seeded scales, functional tokens, automatic dark mode",
1199
+ description: "How palette hues become 11-step OKLCH scales, how semantic roles derive functional tokens from them, and how dark mode flips automatically.",
1200
+ resource: DS_PATH,
1201
+ tags: [
1202
+ "color",
1203
+ "oklch",
1204
+ "dark-mode",
1205
+ "design-system"
1206
+ ]
1207
+ })}
1208
+
1209
+ # Colour system
1210
+
1211
+ Authoring is two maps in \`design-system.json\` (see [authoring.md](../authoring.md)):
1212
+ \`colors.palette\` (hues) and \`colors.roles\` (semantic role → hue). Everything else —
1213
+ scales, functional tokens, utilities, dark mode — is derived.
1214
+
1215
+ ## From seed to scale
1216
+
1217
+ Each palette hue becomes an **11-step numeric OKLCH scale**, \`--color-<hue>-50…950\`,
1218
+ running tinted near-white → tinted near-black. Two authoring modes:
1219
+
1220
+ - **Seeded** (\`{ seed, anchors? }\`): the scale is generated from one colour. The seed is
1221
+ preserved at its natural lightness step; \`anchors\` pin specific steps and the rest
1222
+ interpolate around them.
1223
+ - **Fixed** (\`{ tones }\`): a brand kit used verbatim — each authored tone lands at its
1224
+ nearest step, endpoints are tinted off-white/off-black, no interpolation.
1225
+
1226
+ Current hues: ${code(hues)}.
1227
+
1228
+ ## Functional tokens (the public vocabulary)
1229
+
1230
+ Each role in \`colors.roles\` (currently ${code(roles)}) derives a family of
1231
+ **job-named** tokens from its hue's scale:
1232
+
1233
+ - \`--<role>-bg\`, \`--<role>-bg-muted\` — background washes.
1234
+ - \`--<role>-border\`, \`--<role>-border-bold\` — borders.
1235
+ - \`--<role>-solid\`, \`--<role>-solid-bold\` — opaque fills (buttons, badges), paired with
1236
+ \`--<role>-on-solid\` for a guaranteed-contrast foreground.
1237
+ - \`--<role>-text\`, \`--<role>-text-muted\`, \`--<role>-text-x-muted\` — content colours.
1238
+ - **Emphasis stops** \`--color-<role>-{x-muted,muted,bold,x-bold}\` — appearance-relative:
1239
+ \`muted\` recedes toward the background extreme and \`bold\` advances toward the
1240
+ foreground, in *either* light or dark.
1241
+ - Plus \`--surface-glass\` (translucent surface) and \`--overlay\` (scrim).
1242
+
1243
+ Classes name the token, not the tone: \`bg-<role>\`, \`text-<role>-muted\`, \`text-on-<role>\`,
1244
+ \`border-<role>\` (see [css/classes.md](../css/classes.md)).
1245
+
1246
+ ## Automatic dark mode
1247
+
1248
+ Dark mode is a **functional flip** under \`:root[data-brx-theme="dark"]\`: background and
1249
+ text ends of each scale swap, while \`solid\` fills stay mode-stable with a recomputed
1250
+ \`on-solid\` foreground. There is no per-appearance scheme grammar to author and no named
1251
+ steps — a role token means the same *job* in both appearances.
1252
+
1253
+ ## Contrast guarantees
1254
+
1255
+ Text tokens target APCA Lc ≥ 75 and muted text Lc ≥ 60, in **both** appearances —
1256
+ enforced by the generator's unit tests, not left to the author.
1257
+ `;
1258
+ }
1259
+ function renderScalesConcept(ds) {
1260
+ const t = ds.typeScale;
1261
+ const s = ds.spaceScale;
1262
+ const scaleLine = (label, sc) => sc ? `- **${label}**: base \`${sc.base}\` at step \`${sc.names?.[(sc.baseStep ?? 1) - 1] ?? sc.baseStep}\`, ratio ${sc.ratio}${sc.fluid ? ` (${sc.fluid.minRatio} below ${sc.fluid.minVw})` : ""}; steps ${code(sc.names ?? [])}.` : `- **${label}**: not configured.`;
1263
+ return `${frontmatter({
1264
+ type: "Design Concept",
1265
+ title: "Vitops type & space scales — fluid modular scales",
1266
+ description: "How typeScale and spaceScale compile to clamp()-based fluid modular scales, and which utilities consume the resulting tokens.",
1267
+ resource: DS_PATH,
1268
+ tags: [
1269
+ "typography",
1270
+ "spacing",
1271
+ "fluid",
1272
+ "modular-scale",
1273
+ "design-system"
1274
+ ]
1275
+ })}
1276
+
1277
+ # Type & space scales
1278
+
1279
+ Both scales share one model (authored in \`design-system.json\` as \`typeScale\` /
1280
+ \`spaceScale\` — see [authoring.md](../authoring.md)): a **fluid modular scale**.
1281
+
1282
+ ## The model
1283
+
1284
+ - \`base\` anchors the value at \`baseStep\`; every other step is a power of the \`ratio\`
1285
+ away (step n = base × ratio^(n − baseStep)).
1286
+ - With \`fluid\`, each step compiles to a **\`clamp()\`**: the scale uses \`fluid.minRatio\`
1287
+ at/below \`fluid.minVw\` and grows to \`ratio\` at \`fluid.maxVw\`, interpolating between —
1288
+ so large steps spread apart on wide viewports and compress on phones, with no
1289
+ breakpoint jumps. \`baseline\` names the pivot step that stays closest to \`base\`.
1290
+ - \`names\` become the token suffixes.
1291
+
1292
+ ${scaleLine("Type", t)}
1293
+ ${scaleLine("Space", s)}
1294
+
1295
+ ## What consumes them
1296
+
1297
+ - **Type**: \`--text-<name>\` tokens → typography role sizes (\`font-<role>\` classes,
1298
+ heading defaults) and text-size utilities.
1299
+ - **Space**: \`--space-<name>\` tokens → gap (\`g\`), spacing utilities, and \`rhythm\`
1300
+ (relationship-based vertical margins between headings, paragraphs, lists, media).
1301
+ Prefer \`rhythm\` for vertical flow over per-element margins.
1302
+ - In the \`tailwind\` format these stay in their own namespaces (\`--text-*\` via \`@theme\`,
1303
+ \`--space-*\` as plain vars) — see [formats.md](../formats.md) for why \`--spacing-*\` is
1304
+ deliberately not used.
1305
+ `;
1306
+ }
1307
+ function renderPatternsConcept(ds) {
1308
+ const items = ds.patterns?.items ?? {};
1309
+ const groups = Object.keys(ds.patterns?.groups ?? {});
1310
+ const patternNames = Object.keys(items);
1311
+ const hookTable = Object.entries(BASE_HOOK).map(([prop, sfx]) => `| \`${prop}\` | \`--${sfx}-<pattern>\` |`).join("\n");
1312
+ return `${frontmatter({
1313
+ type: "Design Concept",
1314
+ title: "Vitops component patterns — token cascade, override hooks, states",
1315
+ description: "How pattern CSS is assembled: the token cascade (defaults → groups → per-pattern overrides), per-pattern override-hook variables, state shortcuts, and role variants.",
1316
+ resource: DS_PATH,
1317
+ tags: [
1318
+ "patterns",
1319
+ "components",
1320
+ "cascade",
1321
+ "css",
1322
+ "design-system"
1323
+ ]
1324
+ })}
1325
+
1326
+ # Component patterns — the CSS chain
1327
+
1328
+ Patterns (currently ${code(patternNames)}) are authored declaratively under \`patterns\` in
1329
+ \`design-system.json\` (see [authoring.md](../authoring.md)) and compiled to CSS in the
1330
+ **\`components\` cascade layer** — so utility classes (declared in a later layer) always win
1331
+ over pattern styling without specificity fights.
1332
+
1333
+ ## 1 — Token cascade
1334
+
1335
+ Shared geometry resolves through a variable chain, most-specific first:
1336
+
1337
+ - \`patterns.defaults\` → \`--<prop>-default\` (cascade-wide fallbacks).
1338
+ - \`patterns.groups.<group>\` → \`--<prop>-<group>\` (e.g. groups ${code(groups)}); a pattern
1339
+ opts in via its \`group\` key.
1340
+ - Per-pattern: each grouped pattern gets aliases \`--<prop>-<name>-group\` →
1341
+ \`var(--<prop>-<group>)\`, and \`overrides\` replace individual aliases with literal values.
1342
+ - \`patterns.radii\` → \`--br-<name>\` shape primitives; \`patterns.z\` → \`--z-tier-<name>\`.
1343
+
1344
+ ## 2 — Base declarations & override hooks
1345
+
1346
+ Each pattern's \`base\` block is emitted on its selector — a class (\`.<class ?? name>\`) or,
1347
+ with \`element\`, a **zero-specificity** \`:where(<element>)\` rule so author CSS can always
1348
+ override it. Geometry properties are wrapped in a **per-pattern override hook**:
1349
+
1350
+ | base property | hook variable |
1351
+ | --- | --- |
1352
+ ${hookTable}
1353
+
1354
+ e.g. \`padding: var(--p-button, 0.6em 1.2em)\` — a consumer restyles every button by setting
1355
+ \`--p-button\` on \`:root\`, without touching the pattern.
1356
+
1357
+ ## 3 — States
1358
+
1359
+ \`states\` (hover / active / focus-visible) compile from shortcuts:
1360
+
1361
+ - \`step: n\` — intensify the pattern's colour one emphasis stop: fills swap
1362
+ \`--<role>-solid\` → \`--<role>-solid-bold\`; text patterns swap the \`bold\` emphasis stop →
1363
+ \`x-bold\`.
1364
+ - \`scale: 0.97\` — transform scale; \`lift: "<length>"\` — \`translate: 0 calc(-1 * <length>)\`.
1365
+ - \`shadow: "<name>"\` — \`filter: drop-shadow(var(--shadow-<name>))\`; \`shadow: true\` — the
1366
+ generic lift box-shadow.
1367
+ - \`ring: true\` — focus ring (\`box-shadow\` in the role's \`muted\` stop, outline removed).
1368
+ - \`css: { … }\` — raw declarations escape hatch.
1369
+
1370
+ Any pattern with states also gets a composed transition block (translate / scale / filter /
1371
+ box-shadow / colours, \`--interact-duration\` / \`--interact-easing\` overridable). Hover
1372
+ rules are wrapped in \`@media (hover: hover)\` so touch devices never stick.
1373
+
1374
+ ## 4 — Role variants
1375
+
1376
+ \`roles\` lists semantic colour variants: class patterns emit \`.<pattern>-<role>\`
1377
+ (\`.badge-success\`), element patterns emit \`<element>.<role>\` (\`button.danger\`). Fill
1378
+ patterns set \`background-color: var(--<role>-solid)\` + \`color: var(--<role>-on-solid)\`;
1379
+ text patterns use the role's \`bold\` emphasis stop. \`default_role\` colours the bare,
1380
+ unsuffixed pattern. States re-apply per variant with the variant's role.
1381
+ `;
1382
+ }
923
1383
  function renderTopIndex() {
924
1384
  return `${INDEX_NOTE}
925
1385
 
@@ -931,10 +1391,25 @@ variable-driven CSS framework plus progressively-enhanced web components, genera
931
1391
 
932
1392
  # Contents
933
1393
 
1394
+ * [Authoring reference](authoring.md) - every design-system.json field, generated from the JSON Schema
1395
+ * [Output formats](formats.md) - tailwind vs css vs bricks: what's emitted, what the platform provides, which utilities Tailwind owns
1396
+ * [Concepts](concepts/) - the colour system, type/space scales, and pattern CSS architecture
934
1397
  * [CSS framework](css/) - the class vocabulary (colour, type, space, layout, animation, component patterns), stated as naming rules
935
1398
  * [Bricks Builder](bricks/) - custom elements and how to style them
936
1399
  `;
937
1400
  }
1401
+ function renderConceptsIndex() {
1402
+ return `${INDEX_NOTE}
1403
+
1404
+ # Design-system concepts
1405
+
1406
+ # Contents
1407
+
1408
+ * [Colour system](color.md) - seeded OKLCH scales, functional tokens, automatic dark mode
1409
+ * [Type & space scales](scales.md) - fluid modular scales and the tokens they emit
1410
+ * [Component patterns](patterns.md) - token cascade, override hooks, states, role variants
1411
+ `;
1412
+ }
938
1413
  function renderCssIndex() {
939
1414
  return `${INDEX_NOTE}
940
1415
 
@@ -974,12 +1449,66 @@ function generateDocs(ds, assetsDir) {
974
1449
  const elementsDir = join(assetsDir, "bricks", "elements");
975
1450
  return {
976
1451
  "index.md": renderTopIndex(),
1452
+ "authoring.md": renderAuthoring(),
1453
+ "formats.md": renderFormats(ds),
1454
+ "concepts/index.md": renderConceptsIndex(),
1455
+ "concepts/color.md": renderColorConcept(ds),
1456
+ "concepts/scales.md": renderScalesConcept(ds),
1457
+ "concepts/patterns.md": renderPatternsConcept(ds),
977
1458
  "css/index.md": renderCssIndex(),
978
1459
  "css/classes.md": renderCssClasses(ds),
979
1460
  "bricks/index.md": renderBricksIndex(),
980
1461
  "bricks/elements.md": renderElementsDoc(elementsDir)
981
1462
  };
982
1463
  }
1464
+ /**
1465
+ * Render the generated agent skill (`SKILL.md`) that fronts the docs bundle.
1466
+ * The bundle is expected to live beside it under `references/` (the layout
1467
+ * `vitops agents` writes); paths in the body are relative to the skill dir.
1468
+ */
1469
+ function renderSkill(ds) {
1470
+ const patternNames = Object.keys(ds.patterns?.items ?? {});
1471
+ return `---
1472
+ name: vitops-design-system
1473
+ description: >-
1474
+ Work with this project's Vitops design system. Use when styling or authoring
1475
+ pages or components (choosing utility/pattern classes, colours, spacing,
1476
+ typography), when editing design-system.json (tokens, palette, patterns,
1477
+ scales, animations), when generating output (vitops generate --format
1478
+ tailwind|css|bricks), or when asking what the design system provides on a
1479
+ given platform (Tailwind vs Bricks vs standalone CSS).
1480
+ ---
1481
+
1482
+ <!-- GENERATED by @getvitops/cli — do not hand-edit; regenerate with \`vitops agents\`. -->
1483
+
1484
+ # Vitops design system
1485
+
1486
+ This project is styled with the Vitops design system (\`@getvitops/*\`): a variable-driven
1487
+ CSS framework generated from \`design-system.json\` (single source of truth — semantic
1488
+ colour roles ${code(Object.keys(ds.colors.roles ?? {}))}, fluid type/space scales, component patterns
1489
+ ${code(patternNames)}, animation effects).
1490
+
1491
+ **Core rule:** prefer the framework's utility + component classes over hand-written CSS or
1492
+ ad-hoc values — they encode the tokens, respond to dark mode, and update with the system.
1493
+
1494
+ ## Which reference to read when
1495
+
1496
+ - Choosing classes (layout, colour, type, spacing, animation, patterns) →
1497
+ [references/css/classes.md](references/css/classes.md)
1498
+ - Editing \`design-system.json\` (what a field means / what's valid) →
1499
+ [references/authoring.md](references/authoring.md)
1500
+ - Output questions per platform — what Tailwind provides natively (and which framework
1501
+ utilities are deliberately NOT emitted for it), what Bricks' managers own, what the
1502
+ standalone CSS bundle contains → [references/formats.md](references/formats.md)
1503
+ - Understanding the systems (seeded OKLCH colour + automatic dark mode, fluid modular
1504
+ scales, pattern CSS cascade/override hooks) → [references/concepts/](references/concepts/index.md)
1505
+ - Building in Bricks Builder (custom elements + controls) →
1506
+ [references/bricks/elements.md](references/bricks/elements.md)
1507
+
1508
+ Regenerate this skill and its references after changing \`design-system.json\`:
1509
+ \`vitops agents\`.
1510
+ `;
1511
+ }
983
1512
 
984
1513
  //#endregion
985
1514
  //#region src/generate.ts
@@ -1094,13 +1623,6 @@ const STATE_DEFAULT = {
1094
1623
  "hue-rotate": "0deg",
1095
1624
  clip: "none"
1096
1625
  };
1097
- const BASE_HOOK = {
1098
- padding: "p",
1099
- "border-radius": "br",
1100
- border: "b",
1101
- "box-shadow": "ds",
1102
- "font-size": "fs"
1103
- };
1104
1626
  const id = (key) => createHash("sha256").update(key).digest("hex").slice(0, 6);
1105
1627
  function build(ds, format, assetsDir) {
1106
1628
  const BRICKS = format === "bricks";
@@ -1721,64 +2243,6 @@ body {
1721
2243
  const animEngine = readFileSync(join(cssDir, "animation.css"), "utf8");
1722
2244
  parts.push(`/* ── Animation engine (animation.css) ── */\n@layer components {\n${animEngine}\n}`);
1723
2245
  parts.push(`/* ── Component patterns ── */\n@layer components {\n${patternsCss}\n}`);
1724
- const TW_CLASH = new Set([
1725
- "block",
1726
- "inline",
1727
- "inline-block",
1728
- "flow-root",
1729
- "flex",
1730
- "inline-flex",
1731
- "grid",
1732
- "inline-grid",
1733
- "table",
1734
- "inline-table",
1735
- "table-caption",
1736
- "table-cell",
1737
- "table-row",
1738
- "contents",
1739
- "hidden",
1740
- "list-item",
1741
- "static",
1742
- "fixed",
1743
- "absolute",
1744
- "relative",
1745
- "sticky",
1746
- "isolate",
1747
- "visible",
1748
- "invisible",
1749
- "collapse",
1750
- "sr-only",
1751
- "not-sr-only",
1752
- "flex-row",
1753
- "flex-row-reverse",
1754
- "flex-col",
1755
- "flex-col-reverse",
1756
- "flex-wrap",
1757
- "flex-nowrap",
1758
- "items-start",
1759
- "items-end",
1760
- "items-center",
1761
- "items-baseline",
1762
- "items-stretch",
1763
- "justify-start",
1764
- "justify-end",
1765
- "justify-center",
1766
- "justify-between",
1767
- "justify-around",
1768
- "justify-evenly",
1769
- "content-start",
1770
- "content-end",
1771
- "content-center",
1772
- "content-between",
1773
- "content-around",
1774
- "content-evenly",
1775
- "text-left",
1776
- "text-center",
1777
- "text-right",
1778
- "text-justify",
1779
- "text-start",
1780
- "text-end"
1781
- ]);
1782
2246
  const droppedClashes = /* @__PURE__ */ new Set();
1783
2247
  let droppedVariantBlocks = 0;
1784
2248
  const stripForTailwind = (css) => {
@@ -1960,8 +2424,7 @@ async function generate(options) {
1960
2424
  * Pure: no YAML/env/fs. `resolveSiteConfig` takes an already-parsed object; a
1961
2425
  * thin loader (reading YAML, injecting `SITE_ENV`) lives outside this module.
1962
2426
  */
1963
- /** A string, or a per-locale map of strings (`{ en: "…", fr: "…" }`). */
1964
- const LocalizableSchema = z.union([z.string(), z.record(z.string(), z.string())]);
2427
+ const LocalizableSchema = desc(z.union([z.string(), z.record(z.string(), z.string())]), "A string, or a per-locale map of strings (`{ en: \"…\", fr: \"…\" }`).");
1965
2428
  const ImageRefSchema = z.union([z.string(), z.object({
1966
2429
  src: z.string(),
1967
2430
  alt: z.string(),
@@ -1986,13 +2449,13 @@ const DayOfWeek = z.enum([
1986
2449
  "Sunday"
1987
2450
  ]);
1988
2451
  const DesignSystemEntry = z.extend(DesignSystemPatchSchema, {
1989
- displayName: z.optional(LocalizableSchema),
1990
- extends: z.optional(z.string())
2452
+ displayName: desc(z.optional(LocalizableSchema), "Human-readable theme name (localisable)."),
2453
+ extends: desc(z.optional(z.string()), "Another `designSystem` key to inherit from; this entry then only supplies what it overrides.")
1991
2454
  });
1992
2455
  const LocaleSchema = z.object({
1993
- name: z.string(),
1994
- tagline: z.optional(z.string()),
1995
- basePath: z.optional(z.string())
2456
+ name: desc(z.string(), "Display name of the locale (e.g. \"English\")."),
2457
+ tagline: desc(z.optional(z.string()), "Site tagline in this locale."),
2458
+ basePath: desc(z.optional(z.string()), "URL prefix for this locale (e.g. \"/fr\").")
1996
2459
  });
1997
2460
  const HttpRedirect = z.union([
1998
2461
  z.literal(301),
@@ -2006,14 +2469,13 @@ const DomainAliasSchema = z.object({
2006
2469
  redirectTo: z.string(),
2007
2470
  environment: z.optional(z.string())
2008
2471
  });
2009
- /** schema.org `PostalAddress` field names, so JSON-LD generation is lossless. */
2010
- const PostalAddressSchema = z.object({
2472
+ const PostalAddressSchema = desc(z.object({
2011
2473
  streetAddress: z.string(),
2012
- addressLocality: z.string(),
2013
- addressRegion: z.optional(z.string()),
2474
+ addressLocality: desc(z.string(), "City."),
2475
+ addressRegion: desc(z.optional(z.string()), "State / province."),
2014
2476
  postalCode: z.optional(z.string()),
2015
- addressCountry: z.string()
2016
- });
2477
+ addressCountry: desc(z.string(), "Country (ISO 3166-1 alpha-2 preferred).")
2478
+ }), "schema.org `PostalAddress` field names, so JSON-LD generation is lossless.");
2017
2479
  const ContactObjectSchema = z.object({
2018
2480
  name: z.optional(z.string()),
2019
2481
  email: z.optional(z.email()),
@@ -2072,8 +2534,7 @@ const OrganizationSchema = z.object({
2072
2534
  address: z.optional(PostalAddressSchema),
2073
2535
  taxID: z.optional(z.string()),
2074
2536
  vatID: z.optional(z.string()),
2075
- /** Profile URLs → JSON-LD `sameAs` (mirrors `links`). */
2076
- sameAs: z.optional(z.array(z.url()))
2537
+ sameAs: desc(z.optional(z.array(z.url())), "Profile URLs → JSON-LD `sameAs` (mirrors `links`).")
2077
2538
  });
2078
2539
  const DnsRecordType = z.enum([
2079
2540
  "A",
@@ -2101,11 +2562,11 @@ const DnsDomainSchema = z.object({
2101
2562
  });
2102
2563
  const RobotsSchema = z.string();
2103
2564
  const EnvironmentSchema = z.object({
2104
- url: z.url(),
2105
- api: z.optional(z.url()),
2106
- analytics: z.optional(z.boolean()),
2107
- robots: z.optional(RobotsSchema),
2108
- variant: z.optional(z.string())
2565
+ url: desc(z.url(), "Public origin of this environment."),
2566
+ api: desc(z.optional(z.url()), "API origin, when different from `url`."),
2567
+ analytics: desc(z.optional(z.boolean()), "Whether analytics fire in this environment."),
2568
+ robots: desc(z.optional(RobotsSchema), "Robots policy (e.g. \"noindex,nofollow\" for dev)."),
2569
+ variant: desc(z.optional(z.string()), "Active `abTesting.variants` key for this environment.")
2109
2570
  });
2110
2571
  const AbVariantSchema = z.object({
2111
2572
  environment: z.string(),
@@ -2152,11 +2613,10 @@ const FontSchema = z.object({
2152
2613
  formats: z.optional(z.array(z.string())),
2153
2614
  unicodeRange: z.optional(z.array(z.string())),
2154
2615
  variants: z.optional(z.array(FontVariantSchema)),
2155
- /** Emit `<Font preload />` for this family. */
2156
- preload: z.optional(z.union([z.boolean(), z.array(z.object({
2616
+ preload: desc(z.optional(z.union([z.boolean(), z.array(z.object({
2157
2617
  weight: FontWeight,
2158
2618
  style: z.optional(FontStyle)
2159
- }))]))
2619
+ }))])), "Emit `<Font preload />` for this family (true, or a list of weight/style faces).")
2160
2620
  });
2161
2621
  const PresentSchema = z.optional(z.record(z.string(), z.unknown()));
2162
2622
  const NavItemSchema = z.object({
@@ -2289,18 +2749,18 @@ const TwitterSchema = z.object({
2289
2749
  image: z.optional(ImageRefSchema)
2290
2750
  });
2291
2751
  const SiteConfigSchema = z.object({
2292
- defaultLocale: z.string(),
2293
- locales: z.record(z.string(), LocaleSchema),
2294
- organization: z.optional(OrganizationSchema),
2295
- contact: z.optional(ContactConfigSchema),
2296
- domains: z.optional(z.object({
2297
- canonical: z.url(),
2298
- aliases: z.optional(z.array(DomainAliasSchema))
2299
- })),
2300
- primaryLocation: z.optional(z.string()),
2301
- locations: z.optional(z.record(z.string(), LocationSchema)),
2302
- services: z.optional(z.record(z.string(), ServiceSchema)),
2303
- links: z.optional(z.object({
2752
+ defaultLocale: desc(z.string(), "The locale used when none is specified; must be a `locales` key."),
2753
+ locales: desc(z.record(z.string(), LocaleSchema), "Locales the site is published in, keyed by BCP 47 tag (e.g. \"en\", \"fr\")."),
2754
+ organization: desc(z.optional(OrganizationSchema), "schema.org Organization details for JSON-LD (name, legalName, logo, tax IDs, sameAs profiles)."),
2755
+ contact: desc(z.optional(ContactConfigSchema), "Primary contact: either a `locations` key (string reference) or an inline name/email/phone/address object."),
2756
+ domains: desc(z.optional(z.object({
2757
+ canonical: desc(z.url(), "The canonical origin used for absolute URLs and SEO."),
2758
+ aliases: desc(z.optional(z.array(DomainAliasSchema)), "Alias domains and how they redirect to the canonical one.")
2759
+ })), "Canonical domain + redirecting aliases."),
2760
+ primaryLocation: desc(z.optional(z.string()), "The main `locations` key (for JSON-LD and defaults)."),
2761
+ locations: desc(z.optional(z.record(z.string(), LocationSchema)), "Physical locations (schema.org LocalBusiness): address, geo, opening hours, service area."),
2762
+ services: desc(z.optional(z.record(z.string(), ServiceSchema)), "Services offered (schema.org Service/Offer): name, description, slug, price offers."),
2763
+ links: desc(z.optional(z.object({
2304
2764
  googleMaps: z.optional(z.url()),
2305
2765
  instagram: z.optional(z.url()),
2306
2766
  facebook: z.optional(z.url()),
@@ -2308,52 +2768,52 @@ const SiteConfigSchema = z.object({
2308
2768
  linkedin: z.optional(z.url()),
2309
2769
  youtube: z.optional(z.url()),
2310
2770
  github: z.optional(z.url())
2311
- })),
2312
- dns: z.optional(z.record(z.string(), DnsDomainSchema)),
2313
- cloudflare: z.optional(z.record(z.string(), z.unknown())),
2314
- environments: z.record(z.string(), EnvironmentSchema),
2315
- abTesting: z.optional(z.object({
2771
+ })), "Public profile URLs (also feed JSON-LD `sameAs` via `organization.sameAs`)."),
2772
+ dns: desc(z.optional(z.record(z.string(), DnsDomainSchema)), "Desired DNS state per domain (provider, nameservers, records) — declarative reference, not applied automatically."),
2773
+ cloudflare: desc(z.optional(z.record(z.string(), z.unknown())), "Deliberate escape hatch for provider-specific Cloudflare settings — out of scope to model here."),
2774
+ environments: desc(z.record(z.string(), EnvironmentSchema), "Deploy environments (production, dev, …): URL, API origin, analytics toggle, robots policy, active A/B variant."),
2775
+ abTesting: desc(z.optional(z.object({
2316
2776
  enabled: z.boolean(),
2317
2777
  cookieName: z.optional(z.string()),
2318
- cookieMaxAge: z.optional(z.number().check(z.int(), z.positive())),
2319
- splitRatio: z.optional(z.number().check(z.gte(0), z.lte(1))),
2320
- variants: z.optional(z.record(z.string(), AbVariantSchema))
2321
- })),
2322
- designSystem: z.record(z.string(), DesignSystemEntry),
2323
- defaultTheme: z.optional(z.string()),
2324
- defaultColorScheme: z.optional(z.enum(["light", "dark"])),
2325
- respectSystemPreference: z.optional(z.boolean()),
2326
- fonts: z.optional(z.array(FontSchema)),
2327
- tags: z.optional(z.record(z.string(), z.unknown())),
2328
- postTypes: z.optional(z.record(z.string(), z.unknown())),
2329
- galleries: z.optional(z.record(z.string(), GallerySchema)),
2330
- testimonials: z.optional(z.record(z.string(), TestimonialGroupSchema)),
2331
- templates: z.optional(z.record(z.string(), TemplateSchema)),
2332
- navigation: z.optional(z.object({ activeTemplate: z.optional(z.object({
2333
- default: z.optional(z.string()),
2334
- breakpoints: z.optional(z.record(z.string(), z.string()))
2335
- })) })),
2336
- seo: z.optional(z.object({
2337
- titleTemplate: z.optional(z.string()),
2778
+ cookieMaxAge: desc(z.optional(z.number().check(z.int(), z.positive())), "Seconds."),
2779
+ splitRatio: desc(z.optional(z.number().check(z.gte(0), z.lte(1))), "Fraction of traffic sent to the variant (0–1)."),
2780
+ variants: desc(z.optional(z.record(z.string(), AbVariantSchema)), "Named variants; each targets an environment and may deep-merge config `overrides`.")
2781
+ })), "A/B testing: cookie-based split plus named variants whose `overrides` patch the config per environment."),
2782
+ designSystem: desc(z.record(z.string(), DesignSystemEntry), "Named-theme map of design systems. `default` is the base; other entries may `extends` another and supply a partial patch. Light/dark is automatic (functional tokens flip per appearance), not a separate theme entry. A bare design system (with `colors`) is shorthand for `{ default: … }`."),
2783
+ defaultTheme: desc(z.optional(z.string()), "Which `designSystem` entry to use by default (convention: \"default\")."),
2784
+ defaultColorScheme: desc(z.optional(z.enum(["light", "dark"])), "Initial appearance to render before any user/system preference applies."),
2785
+ respectSystemPreference: desc(z.optional(z.boolean()), "Follow the user's OS light/dark preference."),
2786
+ fonts: desc(z.optional(z.array(FontSchema)), "Font LOADING (a serialisable projection of the Astro Fonts API: provider, weights, subsets, preload). Font TOKENS live in `designSystem.<theme>.fonts` and reference the same `cssVariable`."),
2787
+ tags: desc(z.optional(z.record(z.string(), z.unknown())), "Free-form content-tag taxonomy (site-specific; not interpreted by the generator)."),
2788
+ postTypes: desc(z.optional(z.record(z.string(), z.unknown())), "Free-form content-type definitions (site-specific; not interpreted by the generator)."),
2789
+ galleries: desc(z.optional(z.record(z.string(), GallerySchema)), "Named image galleries (title, tags, images with localisable alt/caption)."),
2790
+ testimonials: desc(z.optional(z.record(z.string(), TestimonialGroupSchema)), "Named testimonial groups (quote, name, role, rating, date)."),
2791
+ templates: desc(z.optional(z.record(z.string(), TemplateSchema)), "Named content templates rendered at SSR time: `nav` (menu trees), `form` (field lists + submit), or `nodes` (raw element trees)."),
2792
+ navigation: desc(z.optional(z.object({ activeTemplate: desc(z.optional(z.object({
2793
+ default: desc(z.optional(z.string()), "A `templates` key."),
2794
+ breakpoints: desc(z.optional(z.record(z.string(), z.string())), "Breakpoint name → `templates` key overrides.")
2795
+ })), "Which nav template is active, optionally per breakpoint.") })), "Site navigation settings (which nav template renders where)."),
2796
+ seo: desc(z.optional(z.object({
2797
+ titleTemplate: desc(z.optional(z.string()), "Page-title pattern (e.g. \"%s · Acme\")."),
2338
2798
  descriptionTemplate: z.optional(z.string()),
2339
- robots: z.optional(RobotsSchema),
2799
+ robots: desc(z.optional(RobotsSchema), "e.g. \"index,follow\" / \"noindex,nofollow\"."),
2340
2800
  googleSiteVerification: z.optional(z.string()),
2341
2801
  bingSiteVerification: z.optional(z.string()),
2342
2802
  openGraph: z.optional(OpenGraphSchema),
2343
2803
  twitter: z.optional(TwitterSchema)
2344
- })),
2345
- analytics: z.optional(z.object({
2804
+ })), "SEO defaults: title/description templates, robots policy, verification tokens, Open Graph + Twitter cards."),
2805
+ analytics: desc(z.optional(z.object({
2346
2806
  googleAnalyticsId: z.optional(z.string()),
2347
2807
  plausibleDomain: z.optional(z.string()),
2348
2808
  googleTagManagerId: z.optional(z.string())
2349
- })),
2350
- notifications: z.optional(z.object({ email: z.optional(z.email()) })),
2351
- tracking: z.optional(z.object({
2809
+ })), "Analytics provider IDs (gated per environment via `environments.<env>.analytics`)."),
2810
+ notifications: desc(z.optional(z.object({ email: z.optional(z.email()) })), "Where site notifications (e.g. form submissions) are sent."),
2811
+ tracking: desc(z.optional(z.object({
2352
2812
  enabled: z.optional(z.boolean()),
2353
2813
  platforms: z.optional(z.array(z.string()))
2354
- })),
2355
- security: z.optional(z.object({ turnstile: z.optional(z.object({ siteKey: z.optional(z.string()) })) })),
2356
- legal: z.optional(z.object({
2814
+ })), "Marketing/conversion tracking toggles per platform."),
2815
+ security: desc(z.optional(z.object({ turnstile: z.optional(z.object({ siteKey: desc(z.optional(z.string()), "Cloudflare Turnstile site key (public).") })) })), "Security integrations (bot protection)."),
2816
+ legal: desc(z.optional(z.object({
2357
2817
  privacyPolicy: z.optional(z.object({
2358
2818
  enabled: z.boolean(),
2359
2819
  url: z.optional(z.url()),
@@ -2366,7 +2826,7 @@ const SiteConfigSchema = z.object({
2366
2826
  })),
2367
2827
  cookieConsent: z.optional(z.object({
2368
2828
  enabled: z.boolean(),
2369
- type: z.optional(z.enum(["opt-in", "opt-out"])),
2829
+ type: desc(z.optional(z.enum(["opt-in", "opt-out"])), "Consent model."),
2370
2830
  position: z.optional(z.enum([
2371
2831
  "top",
2372
2832
  "bottom",
@@ -2374,11 +2834,11 @@ const SiteConfigSchema = z.object({
2374
2834
  ])),
2375
2835
  categories: z.optional(z.array(z.string()))
2376
2836
  }))
2377
- })),
2378
- icons: z.optional(z.object({
2379
- ui: z.optional(z.string()),
2380
- brand: z.optional(z.string()),
2381
- semantic: z.optional(z.array(z.string())),
2837
+ })), "Legal pages + cookie-consent configuration."),
2838
+ icons: desc(z.optional(z.object({
2839
+ ui: desc(z.optional(z.string()), "Icon set used for UI chrome (e.g. \"lucide\")."),
2840
+ brand: desc(z.optional(z.string()), "Icon set used for brand marks."),
2841
+ semantic: desc(z.optional(z.array(z.string())), "Named semantic icons to include."),
2382
2842
  "fa7-solid": z.optional(z.array(z.string())),
2383
2843
  "fa7-regular": z.optional(z.array(z.string())),
2384
2844
  "fa7-light": z.optional(z.array(z.string())),
@@ -2387,24 +2847,21 @@ const SiteConfigSchema = z.object({
2387
2847
  "simple-icons": z.optional(z.array(z.string())),
2388
2848
  "material-symbols": z.optional(z.array(z.string())),
2389
2849
  lucide: z.optional(z.array(z.string()))
2390
- })),
2391
- favicon: z.optional(z.object({
2392
- source: z.string(),
2393
- lowResSource: z.optional(z.string()),
2394
- /** App name for the generated web manifest / PWA. */
2395
- name: z.optional(z.string()),
2396
- /** PWA theme color (also `<meta name="theme-color">`). */
2397
- themeColor: z.optional(z.string()),
2398
- /** PWA background color. */
2399
- backgroundColor: z.optional(z.string())
2400
- })),
2401
- deployment: z.optional(z.object({
2850
+ })), "Icon sets and the specific icons to bundle from each (keys are iconify collection names)."),
2851
+ favicon: desc(z.optional(z.object({
2852
+ source: desc(z.string(), "Source image (SVG/PNG) the favicon set is generated from."),
2853
+ lowResSource: desc(z.optional(z.string()), "Alternate source for small raster sizes (16/32px) when the main source scales down poorly."),
2854
+ name: desc(z.optional(z.string()), "App name for the generated web manifest / PWA."),
2855
+ themeColor: desc(z.optional(z.string()), "PWA theme color (also `<meta name=\"theme-color\">`)."),
2856
+ backgroundColor: desc(z.optional(z.string()), "PWA background color.")
2857
+ })), "Favicon/PWA asset generation (consumed by `@getvitops/utils` favicon tooling)."),
2858
+ deployment: desc(z.optional(z.object({
2402
2859
  platform: z.optional(z.string()),
2403
2860
  buildCommand: z.optional(z.string()),
2404
2861
  deployCommand: z.optional(z.string()),
2405
2862
  outputDirectory: z.optional(z.string()),
2406
2863
  type: z.optional(z.string())
2407
- }))
2864
+ })), "How the site is built and deployed (platform, commands, output directory) — informational for tooling.")
2408
2865
  });
2409
2866
  const SITE_SCHEMA_URL = "https://unpkg.com/@getvitops/generator/site.schema.json";
2410
2867
  /** The published JSON Schema (draft-2020-12), derived from the zod schema. */
@@ -2773,5 +3230,5 @@ function defaultConfig() {
2773
3230
  }
2774
3231
 
2775
3232
  //#endregion
2776
- export { DesignSystemSchema, SCHEMA_URL, SITE_SCHEMA_URL, SiteConfigSchema, deepMerge, defaultConfig, generate, generateDocs, jsonSchema, resolveSiteConfig, resolveTheme, siteJsonSchema, stripNulls, validate, validateSite };
3233
+ export { BASE_HOOK, DesignSystemSchema, SCHEMA_URL, SITE_SCHEMA_URL, SiteConfigSchema, TW_CLASH, deepMerge, defaultConfig, generate, generateDocs, jsonSchema, renderSkill, resolveSiteConfig, resolveTheme, siteJsonSchema, stripNulls, validate, validateSite };
2777
3234
  //# sourceMappingURL=index.mjs.map