@calibrate-ds/generator-react 0.1.87 → 0.1.88

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.
@@ -1,4 +1,5 @@
1
1
  import { getPTBTokenVariableName, formatPx } from "@calibrate-ds/core";
2
+ import { isEmittableColorValue } from "./token-value-utils.js";
2
3
  function getActiveValue(value) {
3
4
  if (value === null || value === undefined)
4
5
  return value;
@@ -201,18 +202,6 @@ function formatCompositeToken(rawValue) {
201
202
  function formatPxLocal(value) {
202
203
  return `${Math.round(value * 1000) / 1000}px`;
203
204
  }
204
- /**
205
- * E-9: a value that looks like a hex color (leading '#') but contains non-hex
206
- * digits (e.g. "#GGGGGG") or the wrong length is invalid CSS and must not be
207
- * emitted verbatim — it silently breaks the whole custom-property. Returns true
208
- * only for well-formed #RGB / #RGBA / #RRGGBB / #RRGGBBAA. Non-'#' strings are
209
- * not our concern here (they take other paths) and return true.
210
- */
211
- function isEmittableColorValue(value) {
212
- if (!value.startsWith("#"))
213
- return true;
214
- return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value);
215
- }
216
205
  /**
217
206
  * Formats a resolved scalar value for CSS output (px suffix, units, font fallbacks).
218
207
  */
@@ -426,7 +415,11 @@ export function generateTokenCssContent(collection, richGlobalMap) {
426
415
  }
427
416
  const header = `/**\n * Generated Design Tokens: ${collection.name} (CSS Variables)\n * Do not edit. This file is auto-generated by Calibrate.\n */\n`;
428
417
  const skippedComment = skippedCount > 0 ? `\n/* Note: ${skippedCount} tokens were skipped from this local CSS output (unresolved or non-scalar). */\n` : "";
429
- return `${header}${skippedComment}\n:root {\n${cssVars}}\n`;
418
+ // E-19 parity: per-collection CSS also emits [data-theme] override blocks for
419
+ // multi-mode collections, mirroring the unified tokens.css path. Uses resolveMap
420
+ // (local + any cross-collection alias targets) so alias mode values still resolve.
421
+ const modeBlocks = generateModeOverrideBlocks([collection], resolveMap);
422
+ return `${header}${skippedComment}\n:root {\n${cssVars}}\n${modeBlocks}`;
430
423
  }
431
424
  /**
432
425
  * Generates a unified CSS Variables file containing all tokens from all collections.
@@ -567,7 +560,7 @@ export function generateUnifiedTokenCssContent(model) {
567
560
  // re-emitted — alias tokens stay in :root as var() and cascade automatically,
568
561
  // which is exactly the theming architecture (primitives flip, semantics follow).
569
562
  // Modes with the same (slugified) name across collections merge into one block.
570
- const modeBlocks = generateModeOverrideBlocks(model, allTokens);
563
+ const modeBlocks = generateModeOverrideBlocks(model.tokens, allTokens);
571
564
  const content = `/**
572
565
  * Generated Design Tokens (Unified CSS Variables)
573
566
  * Do not edit. This file is auto-generated by Calibrate.
@@ -596,10 +589,10 @@ function slugifyModeName(name) {
596
589
  * common case and adds nothing). Emits a token override only when that mode's value
597
590
  * actually differs from the default mode's — so the block stays minimal.
598
591
  */
599
- function generateModeOverrideBlocks(model, allTokens) {
592
+ function generateModeOverrideBlocks(collections, allTokens) {
600
593
  // slug → mode display name (first wins) and accumulated CSS lines
601
594
  const blocks = new Map();
602
- for (const collection of model.tokens) {
595
+ for (const collection of collections) {
603
596
  const modes = collection.modes;
604
597
  if (!modes || modes.length < 2)
605
598
  continue;
@@ -632,6 +625,9 @@ function generateModeOverrideBlocks(model, allTokens) {
632
625
  continue;
633
626
  }
634
627
  }
628
+ // E-9 parity: never emit a malformed hex override.
629
+ if (typeof value === "string" && !isEmittableColorValue(value))
630
+ continue;
635
631
  const varName = getPTBTokenVariableName(collection.name, t.name);
636
632
  const line = ` ${varName}: ${formatCssValue(value, t.type, t.name)};`;
637
633
  const bucket = blocks.get(slug) ?? { name: mode.name, lines: [] };
@@ -1,4 +1,5 @@
1
1
  import { formatCollectionImportName } from "../components/token-helpers.js";
2
+ import { isEmittableColorValue } from "./token-value-utils.js";
2
3
  /**
3
4
  * Converts a raw Figma token name to a camelCase JS identifier key.
4
5
  * Must produce the same result as the camelCase logic in token-helpers.ts selectTokensForSlots,
@@ -10,6 +11,10 @@ export function tokenNameToKey(name) {
10
11
  // Guard against keys that start with a digit (invalid unquoted JS identifier)
11
12
  return /^\d/.test(camel) ? `_${camel}` : (camel || "unknown");
12
13
  }
14
+ /** Slugify a design-tool mode name into a stable object key ("Dark Mode" → "dark-mode"). */
15
+ function slugifyModeKey(name) {
16
+ return (name ?? "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "mode";
17
+ }
13
18
  /**
14
19
  * Generates TypeScript constant file content for a Token Collection.
15
20
  */
@@ -20,16 +25,40 @@ export function generateTokenFileContent(collection) {
20
25
  const exportIdent = `${formatCollectionImportName(collection.name)}Tokens`;
21
26
  let tokensContent = "";
22
27
  for (const t of collection.tokens ?? []) {
28
+ // E-9 parity: a malformed hex value (e.g. "#GGGGGG") is skipped here just as it is
29
+ // in the CSS emitters, so invalid values never leak into the TS export either.
30
+ if (typeof t.value === "string" && !isEmittableColorValue(t.value))
31
+ continue;
23
32
  // Key is camelCase-normalized to match dot-accessor references generated by token-helpers.ts.
24
33
  // E.g. Figma token "color/brand/700" → key "colorBrand700" → accessible as tokens.colorBrand700.
25
34
  const key = tokenNameToKey(t.name);
26
35
  const jsonValue = JSON.stringify(t.value ?? null, null, 4).replace(/\n/g, "\n ");
36
+ // E-19 parity: for multi-mode collections, also emit the per-mode values (keyed by
37
+ // slugified mode name, e.g. { light, dark }) so the TS export carries every theme —
38
+ // not just the default mode — mirroring the [data-theme] blocks in the CSS output.
39
+ // Malformed hex mode values are dropped, consistent with the default value above.
40
+ let byModeLine = "";
41
+ if (t.valuesByMode && collection.modes && collection.modes.length > 1) {
42
+ const byMode = {};
43
+ for (const mode of collection.modes) {
44
+ const mv = t.valuesByMode[mode.id];
45
+ if (mv === undefined)
46
+ continue;
47
+ if (typeof mv === "string" && !isEmittableColorValue(mv))
48
+ continue;
49
+ byMode[slugifyModeKey(mode.name)] = mv;
50
+ }
51
+ if (Object.keys(byMode).length > 0) {
52
+ const byModeJson = JSON.stringify(byMode, null, 4).replace(/\n/g, "\n ");
53
+ byModeLine = `,\n valuesByMode: ${byModeJson}`;
54
+ }
55
+ }
27
56
  tokensContent += `
28
57
  "${key}": {
29
58
  id: "${t.id}",
30
59
  name: "${t.name}",
31
60
  type: "${t.type}",
32
- value: ${jsonValue}
61
+ value: ${jsonValue}${byModeLine}
33
62
  },`;
34
63
  }
35
64
  return `/**
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared token-value guards used by every token emitter (unified CSS, per-collection
3
+ * CSS, and the TypeScript export) so a fix applies uniformly instead of to one emitter.
4
+ */
5
+ /**
6
+ * E-9: a value that looks like a hex color (leading '#') but is malformed — contains
7
+ * non-hex digits (e.g. "#GGGGGG") or is the wrong length — is invalid and must not be
8
+ * emitted verbatim, whether into CSS or the TS token export. Returns true only for
9
+ * well-formed #RGB / #RGBA / #RRGGBB / #RRGGBBAA. Non-'#' strings take other paths and
10
+ * are not this function's concern (returns true).
11
+ */
12
+ export declare function isEmittableColorValue(value: string): boolean;
13
+ //# sourceMappingURL=token-value-utils.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared token-value guards used by every token emitter (unified CSS, per-collection
3
+ * CSS, and the TypeScript export) so a fix applies uniformly instead of to one emitter.
4
+ */
5
+ /**
6
+ * E-9: a value that looks like a hex color (leading '#') but is malformed — contains
7
+ * non-hex digits (e.g. "#GGGGGG") or is the wrong length — is invalid and must not be
8
+ * emitted verbatim, whether into CSS or the TS token export. Returns true only for
9
+ * well-formed #RGB / #RGBA / #RRGGBB / #RRGGBBAA. Non-'#' strings take other paths and
10
+ * are not this function's concern (returns true).
11
+ */
12
+ export function isEmittableColorValue(value) {
13
+ if (!value.startsWith("#"))
14
+ return true;
15
+ return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value);
16
+ }
17
+ //# sourceMappingURL=token-value-utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calibrate-ds/generator-react",
3
- "version": "0.1.87",
3
+ "version": "0.1.88",
4
4
  "license": "FSL-1.1-MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,8 +35,8 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "react": "^19.0.0",
38
- "@calibrate-ds/shared-types": "^0.1.87",
39
- "@calibrate-ds/core": "^0.1.87"
38
+ "@calibrate-ds/core": "^0.1.88",
39
+ "@calibrate-ds/shared-types": "^0.1.88"
40
40
  },
41
41
  "devDependencies": {
42
42
  "typescript": "^5.7.3",