@mp3wizard/figma-console-mcp 1.25.1 → 1.28.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 (207) hide show
  1. package/README.md +53 -35
  2. package/dist/apps/design-system-dashboard/mcp-app.html +78 -78
  3. package/dist/apps/token-browser/mcp-app.html +60 -59
  4. package/dist/cloudflare/core/config.js +0 -8
  5. package/dist/cloudflare/core/console-monitor.js +3 -3
  6. package/dist/cloudflare/core/diagnose-tool.js +96 -0
  7. package/dist/cloudflare/core/figma-tools.js +69 -229
  8. package/dist/cloudflare/core/identity.js +96 -0
  9. package/dist/cloudflare/core/tokens/alias-resolver.js +135 -0
  10. package/dist/cloudflare/core/tokens/config.js +284 -0
  11. package/dist/cloudflare/core/tokens/figma-converter.js +195 -0
  12. package/dist/cloudflare/core/tokens/formatters/css-vars.js +329 -0
  13. package/dist/cloudflare/core/tokens/formatters/dtcg.js +300 -0
  14. package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
  15. package/dist/cloudflare/core/tokens/formatters/json.js +187 -0
  16. package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
  17. package/dist/cloudflare/core/tokens/formatters/scss.js +252 -0
  18. package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
  19. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +207 -0
  20. package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
  21. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +330 -0
  22. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +250 -0
  23. package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
  24. package/dist/cloudflare/core/tokens/index.js +15 -0
  25. package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
  26. package/dist/cloudflare/core/tokens/parsers/dtcg.js +253 -0
  27. package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
  28. package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
  29. package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
  30. package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
  31. package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
  32. package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
  33. package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
  34. package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
  35. package/dist/cloudflare/core/tokens/schemas.js +148 -0
  36. package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
  37. package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
  38. package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
  39. package/dist/cloudflare/core/tokens/types.js +18 -0
  40. package/dist/cloudflare/core/tokens-tools.js +859 -0
  41. package/dist/cloudflare/core/websocket-server.js +5 -55
  42. package/dist/cloudflare/index.js +37 -26
  43. package/dist/core/config.d.ts.map +1 -1
  44. package/dist/core/config.js +0 -8
  45. package/dist/core/config.js.map +1 -1
  46. package/dist/core/console-monitor.d.ts +2 -2
  47. package/dist/core/console-monitor.d.ts.map +1 -1
  48. package/dist/core/console-monitor.js +3 -3
  49. package/dist/core/console-monitor.js.map +1 -1
  50. package/dist/core/diagnose-tool.d.ts +33 -0
  51. package/dist/core/diagnose-tool.d.ts.map +1 -0
  52. package/dist/core/diagnose-tool.js +97 -0
  53. package/dist/core/diagnose-tool.js.map +1 -0
  54. package/dist/core/figma-connector.d.ts +1 -1
  55. package/dist/core/figma-connector.d.ts.map +1 -1
  56. package/dist/core/figma-tools.d.ts +1 -2
  57. package/dist/core/figma-tools.d.ts.map +1 -1
  58. package/dist/core/figma-tools.js +69 -229
  59. package/dist/core/figma-tools.js.map +1 -1
  60. package/dist/core/identity.d.ts +41 -0
  61. package/dist/core/identity.d.ts.map +1 -0
  62. package/dist/core/identity.js +97 -0
  63. package/dist/core/identity.js.map +1 -0
  64. package/dist/core/tokens/alias-resolver.d.ts +55 -0
  65. package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
  66. package/dist/core/tokens/alias-resolver.js +136 -0
  67. package/dist/core/tokens/alias-resolver.js.map +1 -0
  68. package/dist/core/tokens/config.d.ts +352 -0
  69. package/dist/core/tokens/config.d.ts.map +1 -0
  70. package/dist/core/tokens/config.js +285 -0
  71. package/dist/core/tokens/config.js.map +1 -0
  72. package/dist/core/tokens/figma-converter.d.ts +81 -0
  73. package/dist/core/tokens/figma-converter.d.ts.map +1 -0
  74. package/dist/core/tokens/figma-converter.js +196 -0
  75. package/dist/core/tokens/figma-converter.js.map +1 -0
  76. package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
  77. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
  78. package/dist/core/tokens/formatters/css-vars.js +330 -0
  79. package/dist/core/tokens/formatters/css-vars.js.map +1 -0
  80. package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
  81. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
  82. package/dist/core/tokens/formatters/dtcg.js +301 -0
  83. package/dist/core/tokens/formatters/dtcg.js.map +1 -0
  84. package/dist/core/tokens/formatters/index.d.ts +30 -0
  85. package/dist/core/tokens/formatters/index.d.ts.map +1 -0
  86. package/dist/core/tokens/formatters/index.js +46 -0
  87. package/dist/core/tokens/formatters/index.js.map +1 -0
  88. package/dist/core/tokens/formatters/json.d.ts +37 -0
  89. package/dist/core/tokens/formatters/json.d.ts.map +1 -0
  90. package/dist/core/tokens/formatters/json.js +188 -0
  91. package/dist/core/tokens/formatters/json.js.map +1 -0
  92. package/dist/core/tokens/formatters/less.d.ts +4 -0
  93. package/dist/core/tokens/formatters/less.d.ts.map +1 -0
  94. package/dist/core/tokens/formatters/less.js +5 -0
  95. package/dist/core/tokens/formatters/less.js.map +1 -0
  96. package/dist/core/tokens/formatters/scss.d.ts +26 -0
  97. package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
  98. package/dist/core/tokens/formatters/scss.js +253 -0
  99. package/dist/core/tokens/formatters/scss.js.map +1 -0
  100. package/dist/core/tokens/formatters/stubs.d.ts +9 -0
  101. package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
  102. package/dist/core/tokens/formatters/stubs.js +14 -0
  103. package/dist/core/tokens/formatters/stubs.js.map +1 -0
  104. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
  105. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
  106. package/dist/core/tokens/formatters/style-dictionary-v3.js +208 -0
  107. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
  108. package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
  109. package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
  110. package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
  111. package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
  112. package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
  113. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
  114. package/dist/core/tokens/formatters/tailwind-v4.js +331 -0
  115. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
  116. package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
  117. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
  118. package/dist/core/tokens/formatters/tokens-studio.js +251 -0
  119. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
  120. package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
  121. package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
  122. package/dist/core/tokens/formatters/ts-module.js +199 -0
  123. package/dist/core/tokens/formatters/ts-module.js.map +1 -0
  124. package/dist/core/tokens/index.d.ts +17 -0
  125. package/dist/core/tokens/index.d.ts.map +1 -0
  126. package/dist/core/tokens/index.js +16 -0
  127. package/dist/core/tokens/index.js.map +1 -0
  128. package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
  129. package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
  130. package/dist/core/tokens/parsers/css-vars.js +5 -0
  131. package/dist/core/tokens/parsers/css-vars.js.map +1 -0
  132. package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
  133. package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
  134. package/dist/core/tokens/parsers/dtcg.js +254 -0
  135. package/dist/core/tokens/parsers/dtcg.js.map +1 -0
  136. package/dist/core/tokens/parsers/index.d.ts +37 -0
  137. package/dist/core/tokens/parsers/index.d.ts.map +1 -0
  138. package/dist/core/tokens/parsers/index.js +139 -0
  139. package/dist/core/tokens/parsers/index.js.map +1 -0
  140. package/dist/core/tokens/parsers/json.d.ts +4 -0
  141. package/dist/core/tokens/parsers/json.d.ts.map +1 -0
  142. package/dist/core/tokens/parsers/json.js +8 -0
  143. package/dist/core/tokens/parsers/json.js.map +1 -0
  144. package/dist/core/tokens/parsers/scss.d.ts +3 -0
  145. package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
  146. package/dist/core/tokens/parsers/scss.js +5 -0
  147. package/dist/core/tokens/parsers/scss.js.map +1 -0
  148. package/dist/core/tokens/parsers/stubs.d.ts +15 -0
  149. package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
  150. package/dist/core/tokens/parsers/stubs.js +21 -0
  151. package/dist/core/tokens/parsers/stubs.js.map +1 -0
  152. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
  153. package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
  154. package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
  155. package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
  156. package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
  157. package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
  158. package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
  159. package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
  160. package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
  161. package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
  162. package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
  163. package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
  164. package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
  165. package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
  166. package/dist/core/tokens/parsers/tokens-studio.js +5 -0
  167. package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
  168. package/dist/core/tokens/schemas.d.ts +152 -0
  169. package/dist/core/tokens/schemas.d.ts.map +1 -0
  170. package/dist/core/tokens/schemas.js +149 -0
  171. package/dist/core/tokens/schemas.js.map +1 -0
  172. package/dist/core/tokens/transforms/color.d.ts +9 -0
  173. package/dist/core/tokens/transforms/color.d.ts.map +1 -0
  174. package/dist/core/tokens/transforms/color.js +13 -0
  175. package/dist/core/tokens/transforms/color.js.map +1 -0
  176. package/dist/core/tokens/transforms/index.d.ts +36 -0
  177. package/dist/core/tokens/transforms/index.d.ts.map +1 -0
  178. package/dist/core/tokens/transforms/index.js +30 -0
  179. package/dist/core/tokens/transforms/index.js.map +1 -0
  180. package/dist/core/tokens/transforms/size.d.ts +7 -0
  181. package/dist/core/tokens/transforms/size.d.ts.map +1 -0
  182. package/dist/core/tokens/transforms/size.js +8 -0
  183. package/dist/core/tokens/transforms/size.js.map +1 -0
  184. package/dist/core/tokens/types.d.ts +228 -0
  185. package/dist/core/tokens/types.d.ts.map +1 -0
  186. package/dist/core/tokens/types.js +19 -0
  187. package/dist/core/tokens/types.js.map +1 -0
  188. package/dist/core/tokens-tools.d.ts +42 -0
  189. package/dist/core/tokens-tools.d.ts.map +1 -0
  190. package/dist/core/tokens-tools.js +860 -0
  191. package/dist/core/tokens-tools.js.map +1 -0
  192. package/dist/core/types/index.d.ts +0 -8
  193. package/dist/core/types/index.d.ts.map +1 -1
  194. package/dist/core/websocket-connector.d.ts +1 -1
  195. package/dist/core/websocket-connector.d.ts.map +1 -1
  196. package/dist/core/websocket-server.d.ts +4 -3
  197. package/dist/core/websocket-server.d.ts.map +1 -1
  198. package/dist/core/websocket-server.js +5 -55
  199. package/dist/core/websocket-server.js.map +1 -1
  200. package/dist/local.d.ts +0 -12
  201. package/dist/local.d.ts.map +1 -1
  202. package/dist/local.js +959 -3406
  203. package/dist/local.js.map +1 -1
  204. package/figma-desktop-bridge/code.js +11 -63
  205. package/figma-desktop-bridge/ui.html +72 -11
  206. package/package.json +27 -12
  207. package/figma-desktop-bridge/ui-full.html +0 -1353
@@ -0,0 +1,252 @@
1
+ /**
2
+ * SCSS variables formatter.
3
+ *
4
+ * Output shape:
5
+ *
6
+ * // tokens.scss
7
+ * $ds-color-primary: #4085F2;
8
+ * $ds-spacing-md: 16px;
9
+ *
10
+ * // map for runtime mode access
11
+ * $ds-colors: (
12
+ * "primary": $ds-color-primary,
13
+ * ...
14
+ * );
15
+ *
16
+ * Modes: SCSS doesn't have CSS's runtime cascading, so multi-mode output
17
+ * either emits one file per mode (splitByMode: true) or generates SCSS
18
+ * maps keyed by mode name that consumers can `map-get` from.
19
+ *
20
+ * Composite tokens (typography, shadow) expand into multiple primitive
21
+ * variables since SCSS variables hold a single value.
22
+ */
23
+ export function formatScss(doc, opts) {
24
+ const warnings = [];
25
+ const files = [];
26
+ const splitByMode = opts.target.splitByMode ?? false;
27
+ const splitByCollection = opts.target.splitByCollection ?? false;
28
+ const prefix = opts.target.prefix ?? "";
29
+ if (splitByMode && splitByCollection) {
30
+ for (const set of doc.sets) {
31
+ for (const mode of set.modes) {
32
+ files.push({
33
+ path: filenameFor(opts, set, mode),
34
+ content: renderSingleMode([set], mode, prefix, warnings),
35
+ });
36
+ }
37
+ }
38
+ }
39
+ else if (splitByMode) {
40
+ const allModes = new Set();
41
+ for (const set of doc.sets)
42
+ for (const m of set.modes)
43
+ allModes.add(m);
44
+ for (const mode of allModes) {
45
+ const sets = doc.sets.filter((s) => s.modes.includes(mode));
46
+ files.push({
47
+ path: filenameFor(opts, undefined, mode),
48
+ content: renderSingleMode(sets, mode, prefix, warnings),
49
+ });
50
+ }
51
+ }
52
+ else if (splitByCollection) {
53
+ for (const set of doc.sets) {
54
+ files.push({
55
+ path: filenameFor(opts, set),
56
+ content: renderAllModes([set], prefix, warnings),
57
+ });
58
+ }
59
+ }
60
+ else {
61
+ files.push({
62
+ path: filenameFor(opts),
63
+ content: renderAllModes(doc.sets, prefix, warnings),
64
+ });
65
+ }
66
+ return { files, warnings };
67
+ }
68
+ function filenameFor(opts, set, mode) {
69
+ if (opts.target.filename)
70
+ return opts.target.filename;
71
+ const parts = [];
72
+ if (set)
73
+ parts.push(slugify(set.name));
74
+ if (mode)
75
+ parts.push(slugify(mode));
76
+ if (parts.length === 0)
77
+ parts.push("tokens");
78
+ return `_${parts.join(".")}.scss`;
79
+ }
80
+ function slugify(s) {
81
+ return s
82
+ .trim()
83
+ .toLowerCase()
84
+ .replace(/[^a-z0-9]+/g, "-")
85
+ .replace(/^-+|-+$/g, "");
86
+ }
87
+ function varName(path, prefix) {
88
+ return `$${prefix}${path.map(slugify).join("-")}`;
89
+ }
90
+ function renderSingleMode(sets, mode, prefix, warnings) {
91
+ const lines = [];
92
+ lines.push("// Generated by figma-console-mcp — do not edit by hand");
93
+ lines.push(`// Mode: ${mode}`);
94
+ lines.push("");
95
+ for (const set of sets) {
96
+ if (!set.modes.includes(mode))
97
+ continue;
98
+ lines.push(`// — ${set.name} —`);
99
+ for (const token of set.tokens) {
100
+ const value = token.values[mode];
101
+ if (!value)
102
+ continue;
103
+ emitSassLines(token, value, prefix, lines, warnings);
104
+ }
105
+ lines.push("");
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+ function renderAllModes(sets, prefix, warnings) {
110
+ const lines = [];
111
+ lines.push("// Generated by figma-console-mcp — do not edit by hand");
112
+ lines.push("");
113
+ // For multi-mode sets, emit a primary variable + a mode-keyed map.
114
+ // Single-mode sets emit straight variables.
115
+ for (const set of sets) {
116
+ lines.push(`// — ${set.name} —`);
117
+ const isMultiMode = set.modes.length > 1;
118
+ const primaryMode = pickPrimaryMode(set.modes);
119
+ for (const token of set.tokens) {
120
+ // Primary value as the bare variable.
121
+ const primary = token.values[primaryMode];
122
+ if (primary)
123
+ emitSassLines(token, primary, prefix, lines, warnings);
124
+ // Other modes as a map: $ds-color-primary--modes: ("Dark": #..., "Vibrant": #...)
125
+ if (isMultiMode) {
126
+ const otherModes = set.modes.filter((m) => m !== primaryMode);
127
+ const entries = [];
128
+ for (const mode of otherModes) {
129
+ const v = token.values[mode];
130
+ if (!v)
131
+ continue;
132
+ const formatted = scssValueFor(v, token, prefix, warnings);
133
+ if (formatted !== null) {
134
+ entries.push(` "${mode}": ${formatted}`);
135
+ }
136
+ }
137
+ if (entries.length > 0) {
138
+ const mapName = `${varName(token.path, prefix)}--modes`;
139
+ lines.push(`${mapName}: (`);
140
+ lines.push(entries.join(",\n"));
141
+ lines.push(`);`);
142
+ }
143
+ }
144
+ }
145
+ lines.push("");
146
+ }
147
+ return lines.join("\n");
148
+ }
149
+ function pickPrimaryMode(modes) {
150
+ return modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
151
+ }
152
+ function emitSassLines(token, value, prefix, out, warnings) {
153
+ const sassName = varName(token.path, prefix);
154
+ if (value.reference) {
155
+ const bareRef = value.reference.replace(/^\{|\}$/g, "");
156
+ const libMatch = bareRef.match(/^__library:(.+)$/);
157
+ if (libMatch || bareRef === "unknown") {
158
+ const originalId = libMatch ? libMatch[1] : "unknown";
159
+ warnings.push(`Skipped ${token.path.join(".")} in SCSS — cross-library variable ${originalId}.`);
160
+ out.push(`// ${sassName}: skipped — cross-library alias to ${originalId}`);
161
+ return;
162
+ }
163
+ const refPath = bareRef.split(".");
164
+ out.push(`${sassName}: ${varName(refPath, prefix)};`);
165
+ return;
166
+ }
167
+ if (value.literal === undefined || value.literal === null) {
168
+ warnings.push(`Token ${token.path.join(".")} has no value.`);
169
+ return;
170
+ }
171
+ if (token.type === "typography" && typeof value.literal === "object") {
172
+ const t = value.literal;
173
+ for (const sub of ["fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing"]) {
174
+ if (t[sub] !== undefined) {
175
+ out.push(`${sassName}-${kebab(sub)}: ${formatScssLiteral(t[sub], token.type)};`);
176
+ }
177
+ }
178
+ return;
179
+ }
180
+ if (token.type === "shadow" && typeof value.literal === "object") {
181
+ const css = renderShadow(value.literal);
182
+ if (css)
183
+ out.push(`${sassName}: ${css};`);
184
+ return;
185
+ }
186
+ out.push(`${sassName}: ${formatScssLiteral(value.literal, token.type)};`);
187
+ }
188
+ function scssValueFor(value, token, prefix, _warnings) {
189
+ if (value.reference) {
190
+ const bareRef = value.reference.replace(/^\{|\}$/g, "");
191
+ if (bareRef.startsWith("__library:") || bareRef === "unknown")
192
+ return null;
193
+ return varName(bareRef.split("."), prefix);
194
+ }
195
+ if (value.literal === undefined || value.literal === null)
196
+ return null;
197
+ return formatScssLiteral(value.literal, token.type);
198
+ }
199
+ function kebab(s) {
200
+ return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
201
+ }
202
+ function formatScssLiteral(value, type) {
203
+ if (typeof value === "number") {
204
+ if (type === "dimension")
205
+ return `${value}px`;
206
+ return String(value);
207
+ }
208
+ if (typeof value === "string") {
209
+ if (type === "color")
210
+ return value;
211
+ if (type === "fontFamily" || type === "string") {
212
+ return needsQuoting(value) ? JSON.stringify(value) : value;
213
+ }
214
+ return value;
215
+ }
216
+ if (typeof value === "boolean")
217
+ return String(value);
218
+ return JSON.stringify(value);
219
+ }
220
+ function needsQuoting(s) {
221
+ if (/^["']/.test(s))
222
+ return false;
223
+ if (/^[\d.]+([a-z%]+)?$/.test(s))
224
+ return false;
225
+ if (/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(s))
226
+ return false;
227
+ return true;
228
+ }
229
+ function renderShadow(shadow) {
230
+ if (Array.isArray(shadow)) {
231
+ return shadow.map(renderShadow).filter(Boolean).join(", ");
232
+ }
233
+ if (!shadow || typeof shadow !== "object")
234
+ return null;
235
+ const s = shadow;
236
+ const inset = s.inset ? "inset " : "";
237
+ const x = withPx(s.offsetX);
238
+ const y = withPx(s.offsetY);
239
+ const blur = withPx(s.blur);
240
+ const spread = s.spread !== undefined ? ` ${withPx(s.spread)}` : "";
241
+ const color = s.color;
242
+ if (!x || !y || !blur || typeof color !== "string")
243
+ return null;
244
+ return `${inset}${x} ${y} ${blur}${spread} ${color}`;
245
+ }
246
+ function withPx(v) {
247
+ if (typeof v === "number")
248
+ return `${v}px`;
249
+ if (typeof v === "string")
250
+ return v;
251
+ return "";
252
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared NotImplementedError used by formatter stubs. Most formatters are
3
+ * implemented end-to-end as of v1.28.0; this stub remains for niche targets
4
+ * (e.g. Less) that haven't drawn enough demand to warrant implementation.
5
+ */
6
+ export class FormatterNotImplementedError extends Error {
7
+ constructor(formatName) {
8
+ super(`[figma-console-mcp] The ${formatName} formatter is not implemented. ` +
9
+ `Currently supported formatters: dtcg, css-vars, tailwind-v4, tailwind-v3, scss, ts-module, json-flat, json-nested, style-dictionary-v3, tokens-studio. ` +
10
+ `If you need ${formatName} support, open an issue with your use case so we can prioritize it.`);
11
+ this.name = "FormatterNotImplementedError";
12
+ }
13
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Style Dictionary v3 source-format JSON formatter.
3
+ *
4
+ * SD v3 uses bare `value` / `type` fields (no `$` prefix, that's the DTCG
5
+ * convention SD v4 adopted). Output is structurally similar to DTCG but
6
+ * without the dollar signs, and groups have no special meta — just
7
+ * nested objects.
8
+ *
9
+ * Output shape:
10
+ *
11
+ * {
12
+ * "color": {
13
+ * "primary": {
14
+ * "value": "#4085F2",
15
+ * "type": "color",
16
+ * "comment": "Primary brand color"
17
+ * },
18
+ * "brand": {
19
+ * "blue": { "value": "#0066FF", "type": "color" }
20
+ * }
21
+ * },
22
+ * "spacing": {
23
+ * "md": { "value": "16px", "type": "size" }
24
+ * }
25
+ * }
26
+ *
27
+ * SD v3 type names differ slightly from DTCG:
28
+ *
29
+ * DTCG type → SD v3 type
30
+ * ----------------------------
31
+ * dimension → size (or spacing for spacing tokens)
32
+ * color → color
33
+ * fontFamily → string
34
+ * fontWeight → number
35
+ *
36
+ * Aliases use SD v3's `{path.to.token}` syntax (same as DTCG, which copied
37
+ * it from SD).
38
+ *
39
+ * For back-compat with cbds-components / blocks / czi-edu / eddie-design-system
40
+ * which still use SD v3's bare-key source format.
41
+ */
42
+ export function formatStyleDictionaryV3(doc, opts) {
43
+ const warnings = [];
44
+ const files = [];
45
+ const splitByMode = opts.target.splitByMode ?? false;
46
+ const splitByCollection = opts.target.splitByCollection ?? false;
47
+ if (splitByMode && splitByCollection) {
48
+ for (const set of doc.sets) {
49
+ for (const mode of set.modes) {
50
+ files.push({
51
+ path: filenameFor(opts, set, mode),
52
+ content: renderSdJson([set], [mode], warnings),
53
+ });
54
+ }
55
+ }
56
+ }
57
+ else if (splitByMode) {
58
+ const allModes = new Set();
59
+ for (const set of doc.sets)
60
+ for (const m of set.modes)
61
+ allModes.add(m);
62
+ for (const mode of allModes) {
63
+ const sets = doc.sets.filter((s) => s.modes.includes(mode));
64
+ files.push({
65
+ path: filenameFor(opts, undefined, mode),
66
+ content: renderSdJson(sets, [mode], 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: renderSdJson([set], set.modes, warnings),
75
+ });
76
+ }
77
+ }
78
+ else {
79
+ const allModes = new Set();
80
+ for (const set of doc.sets)
81
+ for (const m of set.modes)
82
+ allModes.add(m);
83
+ files.push({
84
+ path: filenameFor(opts),
85
+ content: renderSdJson(doc.sets, [...allModes], warnings),
86
+ });
87
+ }
88
+ return { files, warnings };
89
+ }
90
+ function filenameFor(opts, set, mode) {
91
+ if (opts.target.filename)
92
+ return opts.target.filename;
93
+ const parts = [];
94
+ if (set)
95
+ parts.push(slugify(set.name));
96
+ if (mode)
97
+ parts.push(slugify(mode));
98
+ if (parts.length === 0)
99
+ parts.push("tokens");
100
+ return `${parts.join(".")}.sd.json`;
101
+ }
102
+ function slugify(s) {
103
+ return s
104
+ .trim()
105
+ .toLowerCase()
106
+ .replace(/[^a-z0-9]+/g, "-")
107
+ .replace(/^-+|-+$/g, "");
108
+ }
109
+ /**
110
+ * Map DTCG type names to SD v3 conventions.
111
+ */
112
+ function sdTypeFor(token) {
113
+ const lower = token.path[0]?.toLowerCase() ?? "";
114
+ if (token.type === "dimension") {
115
+ if (lower.startsWith("spacing") || lower === "space")
116
+ return "spacing";
117
+ return "size";
118
+ }
119
+ if (token.type === "color")
120
+ return "color";
121
+ if (token.type === "fontFamily")
122
+ return "string";
123
+ if (token.type === "fontWeight")
124
+ return "number";
125
+ if (token.type === "duration")
126
+ return "time";
127
+ if (token.type === "number")
128
+ return "number";
129
+ if (token.type === "string")
130
+ return "string";
131
+ if (token.type === "boolean")
132
+ return "boolean";
133
+ // Composites / less-common types: pass through.
134
+ return token.type;
135
+ }
136
+ function renderSdJson(sets, modes, warnings) {
137
+ const tree = {};
138
+ const primaryMode = pickPrimaryMode(modes);
139
+ for (const set of sets) {
140
+ for (const token of set.tokens) {
141
+ // Filter to modes the file is supposed to cover.
142
+ const usableModes = modes.filter((m) => token.values[m]);
143
+ if (usableModes.length === 0)
144
+ continue;
145
+ // SD v3 has no native multi-mode encoding, so we pick the primary
146
+ // mode's value. If splitByMode is being used (single mode in this
147
+ // file), that primary will match the file's mode. Otherwise, the
148
+ // chosen primary is the first available.
149
+ const valueMode = usableModes.includes(primaryMode)
150
+ ? primaryMode
151
+ : usableModes[0];
152
+ const tokenValue = token.values[valueMode];
153
+ const sdValue = sdValueFor(tokenValue, token, warnings);
154
+ if (sdValue === undefined)
155
+ continue;
156
+ // Walk the path, creating nested groups.
157
+ let cursor = tree;
158
+ for (let i = 0; i < token.path.length - 1; i++) {
159
+ const segment = token.path[i];
160
+ if (!cursor[segment] || typeof cursor[segment] !== "object") {
161
+ cursor[segment] = {};
162
+ }
163
+ cursor = cursor[segment];
164
+ }
165
+ const leafKey = token.path[token.path.length - 1];
166
+ const leaf = {
167
+ value: sdValue,
168
+ type: sdTypeFor(token),
169
+ };
170
+ if (token.description)
171
+ leaf.comment = token.description;
172
+ cursor[leafKey] = leaf;
173
+ }
174
+ }
175
+ return JSON.stringify(sortKeys(tree), null, 2) + "\n";
176
+ }
177
+ function pickPrimaryMode(modes) {
178
+ return modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
179
+ }
180
+ function sdValueFor(value, token, warnings) {
181
+ if (value.reference) {
182
+ const bare = value.reference.replace(/^\{|\}$/g, "");
183
+ if (bare.startsWith("__library:") || bare === "unknown") {
184
+ warnings.push(`Skipped ${token.path.join(".")} in Style Dictionary v3 — cross-library alias unresolved.`);
185
+ return undefined;
186
+ }
187
+ // SD v3 uses the same `{path.to.token}` alias syntax as DTCG.
188
+ return `{${bare}}`;
189
+ }
190
+ if (value.literal === undefined || value.literal === null)
191
+ return undefined;
192
+ if (typeof value.literal === "number" && token.type === "dimension") {
193
+ return `${value.literal}px`;
194
+ }
195
+ return value.literal;
196
+ }
197
+ function sortKeys(node) {
198
+ if (node === null || typeof node !== "object" || Array.isArray(node)) {
199
+ return node;
200
+ }
201
+ const obj = node;
202
+ const sorted = {};
203
+ const keys = Object.keys(obj).sort();
204
+ for (const k of keys)
205
+ sorted[k] = sortKeys(obj[k]);
206
+ return sorted;
207
+ }