@getvitops/generator 0.6.0 → 0.8.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
@@ -62,7 +62,8 @@ const Pattern = desc(z.object({
62
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
63
  overrides: z.optional(desc(CssDecls, "Per-pattern token overrides, emitted as `--<prop>-<name>-group` values.")),
64
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)."),
65
+ class: desc(z.optional(z.string()), "Class name to emit (defaults to the pattern's key when no `element` is set). Combined with `element`, the pattern emits one zero-specificity `:where(<element>, .<class>)` rule so the class works on any tag and any explicit class overrides it."),
66
+ fill: desc(z.optional(z.boolean()), "Whether this pattern is colour-filled (states/roles drive `background-color` + `on-solid` text) or text-coloured (they drive `color`). Defaults to true when `base` declares a background."),
66
67
  default_role: desc(z.optional(z.string()), "Semantic colour role applied to the bare/default variant."),
67
68
  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
69
  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)`."),
@@ -121,13 +122,20 @@ const jsonSchema = {
121
122
  $id: SCHEMA_URL,
122
123
  ...z.toJSONSchema(DesignSystemSchema, { target: "draft-2020-12" })
123
124
  };
124
- /** Validate an unknown value against the design-system schema. */
125
+ /**
126
+ * Validate an unknown value against the design-system schema.
127
+ *
128
+ * `warnings` are configurations that parse and generate, but produce output that
129
+ * won't behave as authored — currently only the token-namespace collisions the
130
+ * flat `--<prop>-<name>` grammar makes possible.
131
+ */
125
132
  function validate(input) {
126
133
  const result = z.safeParse(DesignSystemSchema, input);
127
134
  if (!result.success) return {
128
135
  ok: false,
129
136
  data: void 0,
130
- errors: result.error.issues
137
+ errors: result.error.issues,
138
+ warnings: []
131
139
  };
132
140
  const { palette, roles } = result.data.colors;
133
141
  for (const [role, hue] of Object.entries(roles)) if (palette[hue] == null) return {
@@ -141,12 +149,18 @@ function validate(input) {
141
149
  role
142
150
  ],
143
151
  message: `role "${role}" references unknown palette hue "${hue}"`
144
- }]
152
+ }],
153
+ warnings: []
145
154
  };
155
+ const warnings = [];
156
+ const radii = Object.keys(result.data.patterns?.radii ?? {});
157
+ const items = new Set(Object.keys(result.data.patterns?.items ?? {}));
158
+ for (const name of radii) if (items.has(name)) warnings.push(`patterns.radii.${name} collides with the "${name}" pattern on --br-${name}; the pattern's override hook wins — rename the radius`);
146
159
  return {
147
160
  ok: true,
148
161
  data: result.data,
149
- errors: []
162
+ errors: [],
163
+ warnings
150
164
  };
151
165
  }
152
166
 
@@ -159,8 +173,9 @@ function validate(input) {
159
173
  */
160
174
  /**
161
175
  * 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.
176
+ * wraps the authored value (`padding: var(--p-btn, 0.4em 0.8em)`) so a
177
+ * consumer restyles one pattern by setting one variable. The hook is named after
178
+ * the pattern's key, not its class.
164
179
  */
165
180
  const BASE_HOOK = {
166
181
  padding: "p",
@@ -1012,9 +1027,10 @@ pattern CSS is assembled — token cascade, \`--p-<pattern>\`-style override hoo
1012
1027
  shortcuts — is explained in [/concepts/patterns.md](../concepts/patterns.md).)
1013
1028
 
1014
1029
  - Patterns: ${code(patterns)}.
1015
- - Roles (for coloured patterns like \`badge\`, \`tag\`, \`status\`, \`button\`):
1016
- ${code(patternRoles)} — e.g. \`badge-success\`, \`button-danger\`. The default (unsuffixed)
1017
- variant uses the brand-primary role.
1030
+ - Roles (for coloured patterns — \`badge\`, \`tag\`, \`status\`, \`cta\`, \`btn\`, …):
1031
+ ${code(patternRoles)} — e.g. \`badge-success\`, \`cta-danger\`. A pattern that also styles an
1032
+ element accepts the bare role class too (\`<button class="danger">\`). The default
1033
+ (unsuffixed) variant uses the pattern's \`default_role\`.
1018
1034
  - Shape primitives: \`--br-<name>\` radii — ${code(radii)}.
1019
1035
  `;
1020
1036
  }
@@ -1343,16 +1359,25 @@ Shared geometry resolves through a variable chain, most-specific first:
1343
1359
 
1344
1360
  ## 2 — Base declarations & override hooks
1345
1361
 
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**:
1362
+ Each pattern's \`base\` block is emitted on its selector:
1363
+
1364
+ - \`class\` only → \`.<class ?? name>\`, at normal class specificity.
1365
+ - \`element\` only → a **zero-specificity** \`:where(<element>)\` rule, so author CSS can always
1366
+ override it.
1367
+ - **both** → one \`:where(<element>, .<class>)\` rule (e.g. \`:where(button, .btn)\`). The element
1368
+ gets the styling with no class needed, the class carries it to any other tag, and — because the
1369
+ whole thing sits at zero specificity — any explicit class wins, including a louder pattern
1370
+ (\`.cta\`) or a component's own rule (\`.dialog__close\`).
1371
+
1372
+ Geometry properties are wrapped in a **per-pattern override hook**:
1349
1373
 
1350
1374
  | base property | hook variable |
1351
1375
  | --- | --- |
1352
1376
  ${hookTable}
1353
1377
 
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.
1378
+ e.g. \`padding: var(--p-btn, 0.4em 0.8em)\` — a consumer restyles every button by setting
1379
+ \`--p-btn\` on \`:root\`, without touching the pattern. The hook is named after the pattern's
1380
+ **key**, not its class.
1356
1381
 
1357
1382
  ## 3 — States
1358
1383
 
@@ -1374,7 +1399,10 @@ rules are wrapped in \`@media (hover: hover)\` so touch devices never stick.
1374
1399
  ## 4 — Role variants
1375
1400
 
1376
1401
  \`roles\` lists semantic colour variants: class patterns emit \`.<pattern>-<role>\`
1377
- (\`.badge-success\`), element patterns emit \`<element>.<role>\` (\`button.danger\`). Fill
1402
+ (\`.badge-success\`). A pattern with an \`element\` emits both the bare role class on the
1403
+ element and the \`-<role>\` form (\`:where(button, .btn).danger, .btn-danger\`), so the same
1404
+ variant works on a non-element host — both at class specificity, so neither outranks a
1405
+ plain class. Fill
1378
1406
  patterns set \`background-color: var(--<role>-solid)\` + \`color: var(--<role>-on-solid)\`;
1379
1407
  text patterns use the role's \`bold\` emphasis stop. \`default_role\` colours the bare,
1380
1408
  unsuffixed pattern. States re-apply per variant with the variant's role.
@@ -1477,6 +1505,22 @@ function generateDocs(ds, assetsDir) {
1477
1505
  * and the pre-built JS bundles) live under `assets/` in this package and are read /
1478
1506
  * copied from there — they are the same for every consumer.
1479
1507
  */
1508
+ /**
1509
+ * Selector the dark functional-token flip hangs off.
1510
+ *
1511
+ * `data-brx-theme` is Bricks' own attribute — Bricks sets it, so it's what the
1512
+ * WordPress target needs. Nothing sets it anywhere else, which meant the dark
1513
+ * flip was unreachable outside Bricks: the shipped `<color-scheme-toggle>` web
1514
+ * component writes `documentElement.dataset.theme` (i.e. `data-theme`), so
1515
+ * clicking "Dark" changed an attribute no rule matched. Matching both makes the
1516
+ * component work on every target without changing what Bricks already does.
1517
+ *
1518
+ * Note this covers the explicit choice only — there is deliberately no
1519
+ * `prefers-color-scheme` block, so the toggle's "System" position currently
1520
+ * resolves to light. Adding one would flip every existing consumer site dark for
1521
+ * dark-OS users, which is a product decision, not a bug fix.
1522
+ */
1523
+ const DARK_SEL = ":root[data-brx-theme=\"dark\"], :root[data-theme=\"dark\"]";
1480
1524
  const DEFAULT_ASSETS = join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
1481
1525
  const UTILITY_PROPS = {
1482
1526
  bg: "background-color",
@@ -1616,7 +1660,7 @@ function build(ds, format, assetsDir) {
1616
1660
  if (surfaceRole) fb += ` --overlay: color-mix(in oklch, var(--color-${surfaceRole.hue}-950) 60%, transparent);\n`;
1617
1661
  if (fb) {
1618
1662
  css += `/* Functional role tokens (dark) */\n`;
1619
- css += `:root[data-brx-theme="dark"] {\n color-scheme: dark;\n${fb}}\n\n`;
1663
+ css += `${DARK_SEL} {\n color-scheme: dark;\n${fb}}\n\n`;
1620
1664
  }
1621
1665
  }
1622
1666
  const allTokens = [];
@@ -1704,6 +1748,7 @@ function build(ds, format, assetsDir) {
1704
1748
  generated["typography.css"] = typo;
1705
1749
  }
1706
1750
  const stateRules = (sel, role, states, colorProp = "background-color") => {
1751
+ const sels = Array.isArray(sel) ? sel : [sel];
1707
1752
  let out = "";
1708
1753
  for (const [state, spec] of Object.entries(states)) {
1709
1754
  const body = [];
@@ -1721,7 +1766,8 @@ function build(ds, format, assetsDir) {
1721
1766
  }
1722
1767
  if (spec.css && typeof spec.css === "object") for (const [k, v] of Object.entries(spec.css)) body.push(`${k}: ${v};`);
1723
1768
  if (!body.length) continue;
1724
- const rule = `${sel}${state === "hover" ? ":hover" : state === "active" ? ":active" : state === "focus-visible" ? ":focus-visible" : `:${state}`} {\n ${body.join("\n ")}\n}\n`;
1769
+ const pseudo = state === "hover" ? ":hover" : state === "active" ? ":active" : state === "focus-visible" ? ":focus-visible" : `:${state}`;
1770
+ const rule = `${sels.map((s) => `${s}${pseudo}`).join(",\n")} {\n ${body.join("\n ")}\n}\n`;
1725
1771
  out += state === "hover" ? `@media (hover: hover) {\n${rule}}\n` : rule;
1726
1772
  }
1727
1773
  return out;
@@ -1731,11 +1777,12 @@ function build(ds, format, assetsDir) {
1731
1777
  for (const [pname, p] of Object.entries(patterns?.items ?? {})) {
1732
1778
  if (!p.base) continue;
1733
1779
  const states = p.states ?? {};
1734
- const isElement = !!p.element;
1735
- const defaultSel = isElement ? `:where(${p.element})` : `.${p.class ?? pname}`;
1780
+ const cls = p.class ?? (p.element ? void 0 : pname);
1781
+ const elementSel = (extra = "") => `:where(${[p.element, cls && `.${cls}`].filter(Boolean).join(", ")})${extra}`;
1782
+ const defaultSel = p.element ? elementSel() : `.${cls}`;
1736
1783
  const defaultRole = p.default_role ?? null;
1737
1784
  const base = { ...p.base };
1738
- const fills = pname === "button" || pname === "badge" || base["background-color"] != null || base["background"] != null;
1785
+ const fills = p.fill ?? (pname === "button" || pname === "badge" || base["background-color"] != null || base["background"] != null);
1739
1786
  const colorProp = fills ? "background-color" : "color";
1740
1787
  pat += `${defaultSel} {\n`;
1741
1788
  if (Object.keys(states).length > 0) {
@@ -1753,10 +1800,10 @@ function build(ds, format, assetsDir) {
1753
1800
  pat += decls(wrappedBase) + "\n}\n";
1754
1801
  pat += stateRules(defaultSel, defaultRole, states, colorProp);
1755
1802
  for (const role of p.roles ?? []) {
1756
- const variantSel = isElement ? `${p.element}.${role}` : `.${p.class ?? pname}-${role}`;
1803
+ const variantSels = p.element ? [elementSel(`.${role}`), ...cls ? [`.${cls}-${role}`] : []] : [`.${cls}-${role}`];
1757
1804
  const variantColorDecl = fills ? `background-color: var(--${role}-solid); color: var(--${role}-on-solid)` : `color: var(--color-${role}-bold)`;
1758
- pat += `${variantSel} { ${variantColorDecl}; }\n`;
1759
- pat += stateRules(variantSel, role, states, colorProp);
1805
+ pat += `${variantSels.join(",\n")} { ${variantColorDecl}; }\n`;
1806
+ pat += stateRules(variantSels, role, states, colorProp);
1760
1807
  }
1761
1808
  pat += "\n";
1762
1809
  }
@@ -1918,11 +1965,25 @@ function build(ds, format, assetsDir) {
1918
1965
  }, null, 2),
1919
1966
  designManifest: (() => {
1920
1967
  const reverseIndex = {};
1921
- for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) reverseIndex[`--color-${name}-${n}`] = `colors.palette.${name}.seed`;
1968
+ for (const [name, hue] of Object.entries(expandedPalette)) for (const n of Object.keys(hue.numeric)) reverseIndex[`--color-${name}-${n}`] = `colors.palette.${name}.anchors.${n}`;
1922
1969
  const colorRoles = Object.entries(roleMap).map(([name, ramp]) => ({
1923
1970
  name,
1924
1971
  ramp
1925
1972
  }));
1973
+ const roleTokens = Object.fromEntries(Object.entries(expandedPalette).map(([hueName, hue]) => {
1974
+ const def = functionalRole("_", hueName, hue);
1975
+ const sfc = functionalRole("surface", hueName, hue);
1976
+ return [hueName, {
1977
+ default: {
1978
+ light: def.light,
1979
+ dark: def.dark
1980
+ },
1981
+ surface: {
1982
+ light: sfc.light,
1983
+ dark: sfc.dark
1984
+ }
1985
+ }];
1986
+ }));
1926
1987
  const typoRoles = typography?.roles ?? {};
1927
1988
  const hooks = {};
1928
1989
  for (const [key, [prop, sfx]] of Object.entries(TYPO_KEYMAP)) hooks[sfx] = {
@@ -1935,7 +1996,6 @@ function build(ds, format, assetsDir) {
1935
1996
  const pGroups = patterns?.groups ?? {};
1936
1997
  const pItems = patterns?.items ?? {};
1937
1998
  for (const prop of Object.keys(pDefaults)) reverseIndex[`--${prop}-default`] = `patterns.defaults.${prop}`;
1938
- for (const name of Object.keys(pRadii)) reverseIndex[`--br-${name}`] = `patterns.radii.${name}`;
1939
1999
  for (const [group, props] of Object.entries(pGroups)) for (const prop of Object.keys(props)) reverseIndex[`--${prop}-${group}`] = `patterns.groups.${group}.${prop}`;
1940
2000
  const items = Object.entries(pItems).map(([name, p]) => {
1941
2001
  const base = p.base ?? {};
@@ -1952,12 +2012,14 @@ function build(ds, format, assetsDir) {
1952
2012
  roles: p.roles ?? []
1953
2013
  };
1954
2014
  });
2015
+ for (const name of Object.keys(pRadii)) if (!(`--br-${name}` in reverseIndex)) reverseIndex[`--br-${name}`] = `patterns.radii.${name}`;
1955
2016
  return JSON.stringify({
1956
2017
  colors: {
1957
2018
  ramps: Object.keys(expandedPalette),
1958
2019
  steps: NUMERIC_STEPS,
1959
2020
  palette: numericPalette,
1960
- roles: colorRoles
2021
+ roles: colorRoles,
2022
+ roleTokens
1961
2023
  },
1962
2024
  typography: {
1963
2025
  roles: Object.keys(typoRoles),
@@ -1985,14 +2047,10 @@ function build(ds, format, assetsDir) {
1985
2047
  items
1986
2048
  },
1987
2049
  shadows: shadows ?? {},
1988
- interaction: {
1989
- duration: "200ms",
1990
- easing: "ease"
1991
- },
1992
2050
  reverseIndex
1993
2051
  }, null, 2);
1994
2052
  })(),
1995
- tailwind: emitTailwind({
2053
+ tailwind: format !== "tailwind" ? "" : emitTailwind({
1996
2054
  ds,
1997
2055
  expandedPalette,
1998
2056
  scaleRoles,
@@ -2052,7 +2110,7 @@ function emitTailwind(ctx) {
2052
2110
  const surface = scaleRoles.find((r) => r.role === "surface");
2053
2111
  if (surface) darkBlock += ` --overlay: color-mix(in oklch, var(--color-${surface.hue}-950) 60%, transparent);\n`;
2054
2112
  }
2055
- if (darkBlock) parts.push(`/* Functional role tokens (dark) */\n:root[data-brx-theme="dark"] {\n color-scheme: dark;\n${darkBlock}}\n`);
2113
+ if (darkBlock) parts.push(`/* Functional role tokens (dark) */\n${DARK_SEL} {\n color-scheme: dark;\n${darkBlock}}\n`);
2056
2114
  parts.push("/* State variant for the active flip (hover/focus-visible are built in). */\n@custom-variant is-active (&.is-active, &[data-active]);\n");
2057
2115
  const util = [];
2058
2116
  if (scaleRoles.length) {
@@ -2309,6 +2367,7 @@ function loadConfig(input) {
2309
2367
  const lines = result.errors.slice(0, 12).map((e) => ` • ${e.path.join(".") || "(root)"}: ${e.message}`).join("\n");
2310
2368
  throw new Error(`Invalid design-system.json:\n${lines}`);
2311
2369
  }
2370
+ for (const w of result.warnings) console.warn(`[vitops] ${w}`);
2312
2371
  return result.data;
2313
2372
  }
2314
2373
  async function generate(options) {
@@ -3087,21 +3146,73 @@ function defaultConfig() {
3087
3146
  top: 1e3
3088
3147
  },
3089
3148
  items: {
3090
- button: {
3149
+ btn: {
3091
3150
  group: "control",
3092
3151
  element: "button",
3093
- default_role: "brand-primary",
3152
+ class: "btn",
3153
+ fill: false,
3154
+ overrides: {
3155
+ b: "1px solid transparent",
3156
+ fs: "inherit"
3157
+ },
3158
+ base: {
3159
+ display: "inline-flex",
3160
+ "align-items": "center",
3161
+ "justify-content": "center",
3162
+ gap: "0.4em",
3163
+ padding: "var(--p-btn-group)",
3164
+ "border-radius": "var(--br-btn-group)",
3165
+ border: "var(--b-btn-group)",
3166
+ "box-shadow": "var(--ds-btn-group)",
3167
+ background: "transparent",
3168
+ color: "inherit",
3169
+ "font-family": "inherit",
3170
+ "font-size": "var(--fs-btn-group)",
3171
+ "font-weight": "inherit",
3172
+ "text-decoration": "none",
3173
+ cursor: "pointer"
3174
+ },
3175
+ states: {
3176
+ hover: {
3177
+ step: 1,
3178
+ css: { "background-color": "var(--color-surface-muted)" }
3179
+ },
3180
+ "focus-visible": { ring: true }
3181
+ },
3182
+ roles: []
3183
+ },
3184
+ cta: {
3185
+ group: "control",
3186
+ class: "cta",
3187
+ fill: true,
3188
+ default_role: "ui-primary",
3189
+ overrides: {
3190
+ p: "0.75em 1.5em",
3191
+ b: "none",
3192
+ ds: "var(--shadow-sm)",
3193
+ fs: "inherit"
3194
+ },
3094
3195
  base: {
3095
- padding: "0.6em 1.2em",
3096
- "border-radius": "var(--br-control, 0.25rem)",
3097
- border: "none",
3196
+ display: "inline-flex",
3197
+ "align-items": "center",
3198
+ "justify-content": "center",
3199
+ gap: "0.5em",
3200
+ padding: "var(--p-cta-group)",
3201
+ "border-radius": "var(--br-cta-group)",
3202
+ border: "var(--b-cta-group)",
3203
+ "font-family": "inherit",
3204
+ "font-size": "var(--fs-cta-group)",
3098
3205
  "font-weight": "600",
3099
- color: "#fff",
3206
+ "text-decoration": "none",
3100
3207
  cursor: "pointer",
3101
- "box-shadow": "var(--shadow-sm)"
3208
+ color: "var(--ui-primary-on-solid)",
3209
+ "box-shadow": "var(--ds-cta-group)"
3102
3210
  },
3103
3211
  states: {
3104
- hover: { step: 1 },
3212
+ hover: {
3213
+ step: 1,
3214
+ lift: "1px"
3215
+ },
3105
3216
  active: { scale: .97 },
3106
3217
  "focus-visible": { ring: true }
3107
3218
  },