@muloka/figma-console-mcp 0.5.0 → 0.5.2

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.
Files changed (111) hide show
  1. package/README.md +3 -1
  2. package/dist/core/port-discovery.d.ts +9 -0
  3. package/dist/core/port-discovery.d.ts.map +1 -1
  4. package/dist/core/port-discovery.js +40 -14
  5. package/dist/core/port-discovery.js.map +1 -1
  6. package/dist/core/websocket-connector.d.ts +2 -1
  7. package/dist/core/websocket-connector.d.ts.map +1 -1
  8. package/dist/core/websocket-connector.js +17 -2
  9. package/dist/core/websocket-connector.js.map +1 -1
  10. package/dist/core/websocket-server.d.ts +8 -1
  11. package/dist/core/websocket-server.d.ts.map +1 -1
  12. package/dist/core/websocket-server.js +18 -2
  13. package/dist/core/websocket-server.js.map +1 -1
  14. package/dist/core/write-tools.d.ts +3 -1
  15. package/dist/core/write-tools.d.ts.map +1 -1
  16. package/dist/core/write-tools.js +34 -14
  17. package/dist/core/write-tools.js.map +1 -1
  18. package/dist/local.d.ts +2 -1
  19. package/dist/local.d.ts.map +1 -1
  20. package/dist/local.js +10 -14
  21. package/dist/local.js.map +1 -1
  22. package/package.json +1 -1
  23. package/dist/apps/design-system-dashboard/mcp-app.html +0 -521
  24. package/dist/apps/token-browser/mcp-app.html +0 -410
  25. package/dist/cloudflare/apps/design-system-dashboard/scoring/accessibility.js +0 -277
  26. package/dist/cloudflare/apps/design-system-dashboard/scoring/component-metadata.js +0 -357
  27. package/dist/cloudflare/apps/design-system-dashboard/scoring/consistency.js +0 -341
  28. package/dist/cloudflare/apps/design-system-dashboard/scoring/coverage.js +0 -230
  29. package/dist/cloudflare/apps/design-system-dashboard/scoring/engine.js +0 -92
  30. package/dist/cloudflare/apps/design-system-dashboard/scoring/naming-semantics.js +0 -308
  31. package/dist/cloudflare/apps/design-system-dashboard/scoring/token-architecture.js +0 -349
  32. package/dist/cloudflare/apps/design-system-dashboard/scoring/types.js +0 -40
  33. package/dist/cloudflare/apps/design-system-dashboard/server.js +0 -159
  34. package/dist/cloudflare/apps/token-browser/server.js +0 -136
  35. package/dist/cloudflare/browser-manager.js +0 -157
  36. package/dist/cloudflare/core/accessibility-tools.js +0 -361
  37. package/dist/cloudflare/core/annotation-tools.js +0 -230
  38. package/dist/cloudflare/core/cloud-websocket-connector.js +0 -419
  39. package/dist/cloudflare/core/cloud-websocket-relay.js +0 -198
  40. package/dist/cloudflare/core/comment-tools.js +0 -292
  41. package/dist/cloudflare/core/config.js +0 -153
  42. package/dist/cloudflare/core/console-monitor.js +0 -427
  43. package/dist/cloudflare/core/deep-component-tools.js +0 -128
  44. package/dist/cloudflare/core/design-code-tools.js +0 -2793
  45. package/dist/cloudflare/core/design-system-manifest.js +0 -265
  46. package/dist/cloudflare/core/design-system-tools.js +0 -882
  47. package/dist/cloudflare/core/diagnose-tool.js +0 -100
  48. package/dist/cloudflare/core/diff/changelog-formatter.js +0 -275
  49. package/dist/cloudflare/core/diff/diff-engine.js +0 -334
  50. package/dist/cloudflare/core/diff/property-compare.js +0 -36
  51. package/dist/cloudflare/core/diff/version-cache.js +0 -74
  52. package/dist/cloudflare/core/enrichment/enrichment-service.js +0 -374
  53. package/dist/cloudflare/core/enrichment/index.js +0 -7
  54. package/dist/cloudflare/core/enrichment/relationship-mapper.js +0 -351
  55. package/dist/cloudflare/core/enrichment/style-resolver.js +0 -346
  56. package/dist/cloudflare/core/figjam-tools.js +0 -547
  57. package/dist/cloudflare/core/figma-api.js +0 -509
  58. package/dist/cloudflare/core/figma-connector.js +0 -7
  59. package/dist/cloudflare/core/figma-reconstruction-spec.js +0 -402
  60. package/dist/cloudflare/core/figma-style-extractor.js +0 -311
  61. package/dist/cloudflare/core/figma-tools.js +0 -3353
  62. package/dist/cloudflare/core/identity.js +0 -96
  63. package/dist/cloudflare/core/library-tools.js +0 -580
  64. package/dist/cloudflare/core/logger.js +0 -53
  65. package/dist/cloudflare/core/port-discovery.js +0 -935
  66. package/dist/cloudflare/core/resolve-package-root.js +0 -11
  67. package/dist/cloudflare/core/slides-tools.js +0 -714
  68. package/dist/cloudflare/core/slot-tools.js +0 -334
  69. package/dist/cloudflare/core/snippet-injector.js +0 -96
  70. package/dist/cloudflare/core/tokens/alias-resolver.js +0 -205
  71. package/dist/cloudflare/core/tokens/config.js +0 -294
  72. package/dist/cloudflare/core/tokens/dialect.js +0 -232
  73. package/dist/cloudflare/core/tokens/figma-converter.js +0 -323
  74. package/dist/cloudflare/core/tokens/formatters/css-vars.js +0 -338
  75. package/dist/cloudflare/core/tokens/formatters/dtcg.js +0 -376
  76. package/dist/cloudflare/core/tokens/formatters/index.js +0 -45
  77. package/dist/cloudflare/core/tokens/formatters/json.js +0 -205
  78. package/dist/cloudflare/core/tokens/formatters/less.js +0 -4
  79. package/dist/cloudflare/core/tokens/formatters/scss.js +0 -258
  80. package/dist/cloudflare/core/tokens/formatters/stubs.js +0 -13
  81. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +0 -213
  82. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +0 -237
  83. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +0 -335
  84. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +0 -256
  85. package/dist/cloudflare/core/tokens/formatters/ts-module.js +0 -198
  86. package/dist/cloudflare/core/tokens/index.js +0 -16
  87. package/dist/cloudflare/core/tokens/parsers/css-vars.js +0 -4
  88. package/dist/cloudflare/core/tokens/parsers/dtcg.js +0 -280
  89. package/dist/cloudflare/core/tokens/parsers/index.js +0 -138
  90. package/dist/cloudflare/core/tokens/parsers/json.js +0 -7
  91. package/dist/cloudflare/core/tokens/parsers/scss.js +0 -4
  92. package/dist/cloudflare/core/tokens/parsers/stubs.js +0 -20
  93. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +0 -4
  94. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +0 -4
  95. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +0 -4
  96. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +0 -4
  97. package/dist/cloudflare/core/tokens/schemas.js +0 -152
  98. package/dist/cloudflare/core/tokens/transforms/color.js +0 -12
  99. package/dist/cloudflare/core/tokens/transforms/index.js +0 -29
  100. package/dist/cloudflare/core/tokens/transforms/size.js +0 -7
  101. package/dist/cloudflare/core/tokens/types.js +0 -18
  102. package/dist/cloudflare/core/tokens-tools.js +0 -1788
  103. package/dist/cloudflare/core/types/design-code.js +0 -4
  104. package/dist/cloudflare/core/types/enriched.js +0 -5
  105. package/dist/cloudflare/core/types/index.js +0 -4
  106. package/dist/cloudflare/core/variable-resolver.js +0 -85
  107. package/dist/cloudflare/core/version-tools.js +0 -1199
  108. package/dist/cloudflare/core/websocket-connector.js +0 -436
  109. package/dist/cloudflare/core/websocket-server.js +0 -1009
  110. package/dist/cloudflare/core/write-tools.js +0 -2506
  111. package/dist/cloudflare/index.js +0 -3010
@@ -1,237 +0,0 @@
1
- /**
2
- * Tailwind v3 config formatter.
3
- *
4
- * Output shape:
5
- *
6
- * // tailwind.tokens.js
7
- * module.exports = {
8
- * colors: {
9
- * primary: "#4085F2",
10
- * brand: { primary: "#FF00AA" }
11
- * },
12
- * spacing: {
13
- * md: "16px"
14
- * }
15
- * };
16
- *
17
- * Designed to be spread into `tailwind.config.js`:
18
- *
19
- * const tokens = require('./src/styles/generated/tailwind.tokens.js');
20
- * module.exports = { theme: { extend: tokens } };
21
- *
22
- * Token-to-namespace mapping mirrors Tailwind v3's theme keys:
23
- *
24
- * color/* → colors.*
25
- * spacing/* → spacing.*
26
- * font/* → fontFamily.*
27
- * text/* → fontSize.*
28
- * radius/* → borderRadius.*
29
- *
30
- * Multi-mode tokens flatten to the primary mode (Tailwind v3 doesn't have
31
- * a native multi-mode model — dark-mode variants come from `tailwindcss`'s
32
- * `darkMode: 'class'` + separate CSS variable files).
33
- */
34
- import { buildTokenIndex, resolveAliasChain } from "../alias-resolver.js";
35
- export function formatTailwindV3(doc, opts) {
36
- const warnings = [];
37
- const files = [];
38
- const splitByCollection = opts.target.splitByCollection ?? false;
39
- // Tailwind v3 config is consumed by Tailwind's build step, which needs
40
- // literal values — no runtime alias resolution. Build a full-document
41
- // token index so we can resolve alias chains at export time. Even when
42
- // splitByCollection writes one file per set, aliases may target tokens
43
- // in OTHER sets (e.g. semantic → primitives) so the index must span
44
- // every set, not just the file's own.
45
- const tokenIndex = buildTokenIndex(doc);
46
- if (splitByCollection) {
47
- for (const set of doc.sets) {
48
- files.push({
49
- path: filenameFor(opts, set),
50
- content: renderConfigFile([set], opts, tokenIndex, warnings),
51
- });
52
- }
53
- }
54
- else {
55
- files.push({
56
- path: filenameFor(opts),
57
- content: renderConfigFile(doc.sets, opts, tokenIndex, warnings),
58
- });
59
- }
60
- return { files, warnings };
61
- }
62
- function filenameFor(opts, set) {
63
- if (opts.target.filename)
64
- return opts.target.filename;
65
- if (set)
66
- return `${slugify(set.name)}.tailwind.tokens.js`;
67
- return "tailwind.tokens.js";
68
- }
69
- function slugify(s) {
70
- return s
71
- .trim()
72
- .toLowerCase()
73
- .replace(/[^a-z0-9]+/g, "-")
74
- .replace(/^-+|-+$/g, "");
75
- }
76
- function jsKey(segment) {
77
- if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(segment))
78
- return segment;
79
- return JSON.stringify(segment);
80
- }
81
- function renderConfigFile(sets, opts, tokenIndex, warnings) {
82
- const lines = [];
83
- lines.push("/**");
84
- lines.push(" * Generated by figma-console-mcp — do not edit by hand.");
85
- lines.push(" * Spread this into your tailwind.config.js theme.extend block:");
86
- lines.push(" *");
87
- lines.push(" * const tokens = require('./path/to/this-file');");
88
- lines.push(" * module.exports = { theme: { extend: tokens } };");
89
- lines.push(" */");
90
- lines.push("");
91
- // Group tokens by Tailwind theme namespace.
92
- const namespaces = {};
93
- for (const set of sets) {
94
- const primaryMode = pickPrimaryMode(set.modes);
95
- for (const token of set.tokens) {
96
- const value = token.values[primaryMode];
97
- if (!value)
98
- continue;
99
- const formattedValue = formatTokenValue(value, token, primaryMode, tokenIndex, warnings);
100
- if (formattedValue === null)
101
- continue;
102
- const { namespace, subPath } = mapToNamespace(token);
103
- if (!namespaces[namespace])
104
- namespaces[namespace] = {};
105
- writeIntoTree(namespaces[namespace], subPath, formattedValue);
106
- }
107
- }
108
- lines.push("module.exports = {");
109
- const namespaceEntries = Object.entries(namespaces).sort(([a], [b]) => a.localeCompare(b));
110
- for (let i = 0; i < namespaceEntries.length; i++) {
111
- const [ns, tree] = namespaceEntries[i];
112
- lines.push(` ${jsKey(ns)}: ${renderTree(tree, 1)}${i < namespaceEntries.length - 1 ? "," : ""}`);
113
- }
114
- lines.push("};");
115
- lines.push("");
116
- return lines.join("\n");
117
- }
118
- function pickPrimaryMode(modes) {
119
- return modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
120
- }
121
- function mapToNamespace(token) {
122
- const slug = token.path.map(slugify);
123
- if (slug.length === 0)
124
- return { namespace: "extend", subPath: [] };
125
- // First-segment aliases that should be re-namespaced to Tailwind theme keys.
126
- const NAMESPACE_MAP = {
127
- color: "colors",
128
- colors: "colors",
129
- spacing: "spacing",
130
- space: "spacing",
131
- radius: "borderRadius",
132
- "border-radius": "borderRadius",
133
- rounded: "borderRadius",
134
- font: "fontFamily",
135
- "font-family": "fontFamily",
136
- text: "fontSize",
137
- "font-size": "fontSize",
138
- "font-weight": "fontWeight",
139
- shadow: "boxShadow",
140
- "box-shadow": "boxShadow",
141
- opacity: "opacity",
142
- "z-index": "zIndex",
143
- "line-height": "lineHeight",
144
- leading: "lineHeight",
145
- tracking: "letterSpacing",
146
- "letter-spacing": "letterSpacing",
147
- };
148
- if (NAMESPACE_MAP[slug[0]]) {
149
- return { namespace: NAMESPACE_MAP[slug[0]], subPath: slug.slice(1) };
150
- }
151
- // Heuristic by type when no first-segment match.
152
- if (token.type === "color")
153
- return { namespace: "colors", subPath: slug };
154
- if (token.type === "dimension")
155
- return { namespace: "spacing", subPath: slug };
156
- if (token.type === "fontFamily")
157
- return { namespace: "fontFamily", subPath: slug };
158
- if (token.type === "fontWeight")
159
- return { namespace: "fontWeight", subPath: slug };
160
- // Anything else: dump under extend.{path}
161
- return { namespace: "extend", subPath: slug };
162
- }
163
- function writeIntoTree(tree, path, value) {
164
- if (path.length === 0)
165
- return;
166
- let cursor = tree;
167
- for (let i = 0; i < path.length - 1; i++) {
168
- const segment = path[i];
169
- const next = cursor[segment];
170
- if (!next || typeof next === "string") {
171
- const newNode = {};
172
- cursor[segment] = newNode;
173
- cursor = newNode;
174
- }
175
- else {
176
- cursor = next;
177
- }
178
- }
179
- const leafKey = path[path.length - 1];
180
- cursor[leafKey] = value;
181
- }
182
- function renderTree(tree, indent) {
183
- const ind = " ".repeat(indent);
184
- const innerInd = " ".repeat(indent + 1);
185
- const entries = [];
186
- const sortedKeys = Object.keys(tree).sort();
187
- for (const key of sortedKeys) {
188
- const value = tree[key];
189
- const k = jsKey(key);
190
- if (typeof value === "string") {
191
- entries.push(`${innerInd}${k}: ${value}`);
192
- }
193
- else {
194
- entries.push(`${innerInd}${k}: ${renderTree(value, indent + 1)}`);
195
- }
196
- }
197
- if (entries.length === 0)
198
- return "{}";
199
- return `{\n${entries.join(",\n")},\n${ind}}`;
200
- }
201
- /**
202
- * Format a token value as a JavaScript literal (quoted string for most
203
- * things). Resolves alias chains to their literal target value via the
204
- * document-wide token index — Tailwind v3 config is read at Tailwind's
205
- * build time and needs concrete values (no runtime cascade).
206
- *
207
- * Returns null only for cross-library aliases or genuinely empty values,
208
- * after emitting a warning explaining what got skipped.
209
- */
210
- function formatTokenValue(value, token, mode, tokenIndex, warnings) {
211
- let effective = value;
212
- if (value.reference) {
213
- effective = resolveAliasChain(value, mode, tokenIndex);
214
- if (!effective) {
215
- const bare = value.reference.replace(/^\{|\}$/g, "");
216
- const reason = bare.startsWith("__library:") || bare === "unknown"
217
- ? "cross-library alias"
218
- : "alias target not found in any set";
219
- warnings.push(`Skipped ${token.path.join(".")} in Tailwind v3 — ${reason}: ${value.reference}.`);
220
- return null;
221
- }
222
- }
223
- if (!effective || effective.literal === undefined || effective.literal === null) {
224
- return null;
225
- }
226
- if (typeof effective.literal === "number") {
227
- if (token.type === "dimension")
228
- return JSON.stringify(`${effective.literal}px`);
229
- return String(effective.literal);
230
- }
231
- if (typeof effective.literal === "string")
232
- return JSON.stringify(effective.literal);
233
- if (typeof effective.literal === "boolean")
234
- return String(effective.literal);
235
- // Composite: emit as JSON object literal
236
- return JSON.stringify(effective.literal);
237
- }
@@ -1,335 +0,0 @@
1
- /**
2
- * Tailwind v4 `@theme` block formatter.
3
- *
4
- * Tailwind v4 introduced CSS-first configuration: instead of a JS
5
- * `tailwind.config.js`, you write tokens directly into your CSS with an
6
- * `@theme inline { ... }` block. Tailwind processes that block to generate
7
- * utility classes (`bg-primary`, `text-foreground`, etc.) at build time.
8
- *
9
- * Output shape:
10
- *
11
- * @theme inline {
12
- * --color-primary: #4085F2;
13
- * --spacing-md: 16px;
14
- * --font-family-sans: "Inter", sans-serif;
15
- * }
16
- *
17
- * .dark {
18
- * --color-primary: #5599FF;
19
- * }
20
- *
21
- * Token-to-Tailwind-namespace mapping follows the canonical Tailwind v4
22
- * convention (see https://tailwindcss.com/docs/theme):
23
- *
24
- * color/* → --color-* (maps to bg-*, text-*, border-*, etc.)
25
- * spacing/* → --spacing-*
26
- * radius/* → --radius-* (maps to rounded-*)
27
- * font/* → --font-* (font-family namespace)
28
- * text/* → --text-* (font-size namespace)
29
- * font-weight/* → --font-weight-*
30
- * tracking/* → --tracking-*
31
- * leading/* → --leading-*
32
- * shadow/* → --shadow-*
33
- *
34
- * Tokens that don't fit a known Tailwind namespace get emitted with their
35
- * path verbatim (with prefix if configured). They're still valid CSS vars
36
- * — they just don't generate Tailwind utility classes.
37
- */
38
- import { buildTokenIndex, referenceTargetPath } from "../alias-resolver.js";
39
- export function formatTailwindV4(doc, opts) {
40
- const warnings = [];
41
- const files = [];
42
- // Whole-document index so set-qualified alias references resolve to the
43
- // target token's own path when generating var(--...) names.
44
- const tokenIndex = buildTokenIndex(doc, warnings);
45
- const splitByMode = opts.target.splitByMode ?? false;
46
- const splitByCollection = opts.target.splitByCollection ?? false;
47
- const prefix = opts.target.prefix ?? "";
48
- if (splitByMode && splitByCollection) {
49
- for (const set of doc.sets) {
50
- for (const mode of set.modes) {
51
- files.push({
52
- path: filenameFor(opts, set, mode),
53
- content: renderTailwindFile(doc.sets.filter((s) => s.name === set.name), [mode], prefix, tokenIndex, warnings),
54
- });
55
- }
56
- }
57
- }
58
- else if (splitByMode) {
59
- const allModes = new Set();
60
- for (const set of doc.sets)
61
- for (const m of set.modes)
62
- allModes.add(m);
63
- for (const mode of allModes) {
64
- files.push({
65
- path: filenameFor(opts, undefined, mode),
66
- content: renderTailwindFile(doc.sets, [mode], prefix, tokenIndex, warnings),
67
- });
68
- }
69
- }
70
- else if (splitByCollection) {
71
- for (const set of doc.sets) {
72
- files.push({
73
- path: filenameFor(opts, set),
74
- content: renderTailwindFile([set], set.modes, prefix, tokenIndex, warnings),
75
- });
76
- }
77
- }
78
- else {
79
- // Single file with everything.
80
- const allModes = new Set();
81
- for (const set of doc.sets)
82
- for (const m of set.modes)
83
- allModes.add(m);
84
- files.push({
85
- path: filenameFor(opts),
86
- content: renderTailwindFile(doc.sets, [...allModes], prefix, tokenIndex, warnings),
87
- });
88
- }
89
- return { files, warnings };
90
- }
91
- function filenameFor(opts, set, mode) {
92
- if (opts.target.filename)
93
- return opts.target.filename;
94
- const parts = [];
95
- if (set)
96
- parts.push(slugify(set.name));
97
- if (mode)
98
- parts.push(slugify(mode));
99
- if (parts.length === 0)
100
- parts.push("tailwind.theme");
101
- return `${parts.join(".")}.css`;
102
- }
103
- function slugify(s) {
104
- return s
105
- .trim()
106
- .toLowerCase()
107
- .replace(/[^a-z0-9]+/g, "-")
108
- .replace(/^-+|-+$/g, "");
109
- }
110
- /**
111
- * Render a Tailwind v4 CSS file. The "primary" mode (first in the list)
112
- * goes into `@theme inline { ... }` — this is what Tailwind reads to
113
- * generate utility classes. Additional modes go under standard CSS
114
- * selectors (`.dark`, `[data-theme="..."]`) so the user can swap modes
115
- * at runtime.
116
- */
117
- function renderTailwindFile(sets, modes, prefix, tokenIndex, warnings) {
118
- const lines = [];
119
- lines.push("/* Generated by figma-console-mcp — do not edit by hand */");
120
- lines.push("");
121
- // Decide which mode is the "primary" (lives inside @theme).
122
- const primaryMode = modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
123
- // @theme inline { ... } block for the primary mode.
124
- lines.push("@theme inline {");
125
- for (const set of sets) {
126
- if (!set.modes.includes(primaryMode))
127
- continue;
128
- for (const token of set.tokens) {
129
- const value = token.values[primaryMode];
130
- if (!value)
131
- continue;
132
- emitTailwindTokenLines(token, value, prefix, tokenIndex, lines, warnings);
133
- }
134
- }
135
- lines.push("}");
136
- lines.push("");
137
- // Mode variants under standard CSS selectors.
138
- for (const mode of modes) {
139
- if (mode === primaryMode)
140
- continue;
141
- lines.push(`${selectorFor(mode)} {`);
142
- for (const set of sets) {
143
- if (!set.modes.includes(mode))
144
- continue;
145
- for (const token of set.tokens) {
146
- const value = token.values[mode];
147
- if (!value)
148
- continue;
149
- emitTailwindTokenLines(token, value, prefix, tokenIndex, lines, warnings);
150
- }
151
- }
152
- lines.push("}");
153
- lines.push("");
154
- }
155
- return lines.join("\n");
156
- }
157
- function selectorFor(mode) {
158
- const lower = mode.toLowerCase();
159
- if (lower === "dark")
160
- return ".dark";
161
- return `[data-theme="${slugify(mode)}"]`;
162
- }
163
- /**
164
- * Emit one or more CSS custom property declarations for a token, using
165
- * Tailwind v4's namespace conventions where possible.
166
- */
167
- function emitTailwindTokenLines(token, value, prefix, tokenIndex, out, warnings) {
168
- const cssName = `--${prefix}${pathToTailwindName(token.path, token.type)}`;
169
- if (value.reference) {
170
- // Cross-library alias: skip with a comment.
171
- const bareRef = value.reference.replace(/^\{|\}$/g, "");
172
- const libMatch = bareRef.match(/^__library:(.+)$/);
173
- if (libMatch || bareRef === "unknown") {
174
- const originalId = libMatch ? libMatch[1] : "unknown";
175
- warnings.push(`Skipped ${token.path.join(".")} in Tailwind v4 — references cross-library variable ${originalId}.`);
176
- out.push(` /* ${cssName}: skipped — cross-library alias to ${originalId} */`);
177
- return;
178
- }
179
- // Local alias → var() reference using the same namespace mapping.
180
- // Resolve set-qualified references to the target token's own path.
181
- const refPath = referenceTargetPath(value.reference, tokenIndex);
182
- const targetName = pathToTailwindName(refPath, token.type);
183
- out.push(` ${cssName}: var(--${prefix}${targetName});`);
184
- return;
185
- }
186
- if (value.literal === undefined || value.literal === null) {
187
- warnings.push(`Token ${token.path.join(".")} has no value — emitting nothing.`);
188
- return;
189
- }
190
- // Composite tokens: typography expands, shadow renders as CSS string.
191
- if (token.type === "typography" && typeof value.literal === "object") {
192
- const t = value.literal;
193
- for (const subField of ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"]) {
194
- if (t[subField] !== undefined) {
195
- out.push(` ${cssName}-${kebab(subField)}: ${formatValue(t[subField], token.type)};`);
196
- }
197
- }
198
- return;
199
- }
200
- if (token.type === "shadow" && typeof value.literal === "object") {
201
- const css = renderShadow(value.literal);
202
- if (css)
203
- out.push(` ${cssName}: ${css};`);
204
- return;
205
- }
206
- out.push(` ${cssName}: ${formatValue(value.literal, token.type)};`);
207
- }
208
- /**
209
- * Map a token path to a Tailwind v4 CSS custom property name.
210
- *
211
- * Strategy:
212
- * 1. If the path's leading segment matches a Tailwind namespace
213
- * (color, spacing, radius, font, text, etc.), emit it as-is — that
214
- * tells Tailwind to generate the corresponding utility class family.
215
- * 2. Otherwise, prepend our type-based namespace heuristically.
216
- * 3. Always slugify each segment for valid CSS identifiers.
217
- *
218
- * Examples:
219
- * ["color", "primary"] → "color-primary" (bg-primary works)
220
- * ["spacing", "md"] → "spacing-md" (p-md works)
221
- * ["radius", "lg"] → "radius-lg" (rounded-lg works)
222
- * ["text", "base", "font-size"] → "text-base-font-size"
223
- * ["semantic", "bg"] (color type) → "color-semantic-bg" (heuristic prepend)
224
- */
225
- function pathToTailwindName(path, type) {
226
- // Tailwind v4's known namespaces (https://tailwindcss.com/docs/theme).
227
- const KNOWN = new Set([
228
- "color",
229
- "colors",
230
- "spacing",
231
- "radius",
232
- "rounded",
233
- "shadow",
234
- "font",
235
- "text",
236
- "font-weight",
237
- "tracking",
238
- "leading",
239
- "blur",
240
- "ease",
241
- "animate",
242
- "breakpoint",
243
- "container",
244
- "aspect",
245
- ]);
246
- // Aliases that normalize to a canonical namespace.
247
- const NORMALIZE = {
248
- colors: "color",
249
- rounded: "radius",
250
- };
251
- const slug = path.map(slugify);
252
- if (slug.length === 0)
253
- return "token";
254
- // First-segment match: easy case, normalize plural → singular and emit.
255
- if (KNOWN.has(slug[0])) {
256
- const head = NORMALIZE[slug[0]] ?? slug[0];
257
- return [head, ...slug.slice(1)].join("-");
258
- }
259
- // Determine the target Tailwind namespace from token type.
260
- const ns = type === "color"
261
- ? "color"
262
- : type === "dimension" || type === "number"
263
- ? "spacing"
264
- : type === "fontFamily"
265
- ? "font"
266
- : type === "fontWeight"
267
- ? "font-weight"
268
- : null;
269
- if (!ns)
270
- return slug.join("-");
271
- // Dedup: if the path already contains the target namespace (e.g. a path
272
- // like `theme.color.header.background` for a color token would otherwise
273
- // produce `color-theme-color-header-background`), strip the duplicate
274
- // segment so the result reads cleanly. Match both the canonical name
275
- // and any alias that normalizes to it.
276
- const dupAliases = new Set([ns, ...Object.entries(NORMALIZE)
277
- .filter(([, target]) => target === ns)
278
- .map(([alias]) => alias)]);
279
- const dedupedTail = slug.filter((s) => !dupAliases.has(s));
280
- return [ns, ...dedupedTail].join("-");
281
- }
282
- function kebab(s) {
283
- return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
284
- }
285
- function formatValue(value, type) {
286
- if (typeof value === "number") {
287
- if (type === "dimension")
288
- return `${value}px`;
289
- return String(value);
290
- }
291
- if (typeof value === "string") {
292
- if (type === "color")
293
- return value;
294
- if (type === "fontFamily" || type === "string") {
295
- return needsQuoting(value) ? JSON.stringify(value) : value;
296
- }
297
- return value;
298
- }
299
- if (typeof value === "boolean")
300
- return String(value);
301
- return JSON.stringify(value);
302
- }
303
- function needsQuoting(s) {
304
- if (/^["']/.test(s))
305
- return false;
306
- if (/^[\d.]+([a-z%]+)?$/.test(s))
307
- return false;
308
- if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(s))
309
- return false;
310
- return true;
311
- }
312
- function renderShadow(shadow) {
313
- if (Array.isArray(shadow)) {
314
- return shadow.map(renderShadow).filter(Boolean).join(", ");
315
- }
316
- if (!shadow || typeof shadow !== "object")
317
- return null;
318
- const s = shadow;
319
- const inset = s.inset ? "inset " : "";
320
- const x = withPx(s.offsetX);
321
- const y = withPx(s.offsetY);
322
- const blur = withPx(s.blur);
323
- const spread = s.spread !== undefined ? ` ${withPx(s.spread)}` : "";
324
- const color = s.color;
325
- if (!x || !y || !blur || typeof color !== "string")
326
- return null;
327
- return `${inset}${x} ${y} ${blur}${spread} ${color}`;
328
- }
329
- function withPx(v) {
330
- if (typeof v === "number")
331
- return `${v}px`;
332
- if (typeof v === "string")
333
- return v;
334
- return "";
335
- }