@servicetitan/hammer-token 2.5.0 → 3.0.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.
Files changed (147) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +332 -0
  3. package/build/web/core/component-variables.scss +1088 -131
  4. package/build/web/core/component.d.ts +558 -0
  5. package/build/web/core/component.js +6685 -249
  6. package/build/web/core/component.scss +557 -69
  7. package/build/web/core/css-utils/a2-border.css +47 -45
  8. package/build/web/core/css-utils/a2-color.css +443 -227
  9. package/build/web/core/css-utils/a2-font.css +0 -2
  10. package/build/web/core/css-utils/a2-spacing.css +476 -478
  11. package/build/web/core/css-utils/a2-utils.css +992 -772
  12. package/build/web/core/css-utils/border.css +47 -45
  13. package/build/web/core/css-utils/color.css +443 -227
  14. package/build/web/core/css-utils/font.css +0 -2
  15. package/build/web/core/css-utils/spacing.css +476 -478
  16. package/build/web/core/css-utils/utils.css +992 -772
  17. package/build/web/core/index.d.ts +6 -0
  18. package/build/web/core/index.js +1 -1
  19. package/build/web/core/primitive-variables.scss +148 -65
  20. package/build/web/core/primitive.d.ts +209 -0
  21. package/build/web/core/primitive.js +779 -61
  22. package/build/web/core/primitive.scss +207 -124
  23. package/build/web/core/semantic-variables.scss +363 -239
  24. package/build/web/core/semantic.d.ts +221 -0
  25. package/build/web/core/semantic.js +1613 -347
  26. package/build/web/core/semantic.scss +219 -137
  27. package/build/web/index.d.ts +3 -4
  28. package/build/web/index.js +0 -1
  29. package/build/web/types.d.ts +17 -0
  30. package/config.js +121 -497
  31. package/eslint.config.mjs +11 -1
  32. package/package.json +15 -5
  33. package/src/global/primitive/breakpoint.tokens.json +54 -0
  34. package/src/global/primitive/color.tokens.json +1092 -0
  35. package/src/global/primitive/duration.tokens.json +44 -0
  36. package/src/global/primitive/font.tokens.json +151 -0
  37. package/src/global/primitive/radius.tokens.json +94 -0
  38. package/src/global/primitive/size.tokens.json +174 -0
  39. package/src/global/primitive/transition.tokens.json +32 -0
  40. package/src/theme/core/background.tokens.json +1312 -0
  41. package/src/theme/core/border.tokens.json +192 -0
  42. package/src/theme/core/chart.tokens.json +982 -0
  43. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  44. package/src/theme/core/component/alert.tokens.json +261 -0
  45. package/src/theme/core/component/announcement.tokens.json +460 -0
  46. package/src/theme/core/component/avatar.tokens.json +137 -0
  47. package/src/theme/core/component/badge.tokens.json +42 -0
  48. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  49. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  50. package/src/theme/core/component/button.tokens.json +941 -0
  51. package/src/theme/core/component/calendar.tokens.json +391 -0
  52. package/src/theme/core/component/card.tokens.json +107 -0
  53. package/src/theme/core/component/checkbox.tokens.json +631 -0
  54. package/src/theme/core/component/chip.tokens.json +169 -0
  55. package/src/theme/core/component/combobox.tokens.json +269 -0
  56. package/src/theme/core/component/details.tokens.json +152 -0
  57. package/src/theme/core/component/dialog.tokens.json +87 -0
  58. package/src/theme/core/component/divider.tokens.json +23 -0
  59. package/src/theme/core/component/dnd.tokens.json +208 -0
  60. package/src/theme/core/component/drawer.tokens.json +61 -0
  61. package/src/theme/core/component/drilldown.tokens.json +61 -0
  62. package/src/theme/core/component/edit-card.tokens.json +381 -0
  63. package/src/theme/core/component/field-label.tokens.json +42 -0
  64. package/src/theme/core/component/field-message.tokens.json +74 -0
  65. package/src/theme/core/component/icon.tokens.json +42 -0
  66. package/src/theme/core/component/link.tokens.json +108 -0
  67. package/src/theme/core/component/list-view.tokens.json +82 -0
  68. package/src/theme/core/component/listbox.tokens.json +283 -0
  69. package/src/theme/core/component/menu.tokens.json +230 -0
  70. package/src/theme/core/component/overflow.tokens.json +84 -0
  71. package/src/theme/core/component/page.tokens.json +377 -0
  72. package/src/theme/core/component/pagination.tokens.json +63 -0
  73. package/src/theme/core/component/popover.tokens.json +122 -0
  74. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  75. package/src/theme/core/component/radio.tokens.json +631 -0
  76. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  77. package/src/theme/core/component/select-card.tokens.json +943 -0
  78. package/src/theme/core/component/side-nav.tokens.json +349 -0
  79. package/src/theme/core/component/skeleton.tokens.json +42 -0
  80. package/src/theme/core/component/spinner.tokens.json +96 -0
  81. package/src/theme/core/component/status-icon.tokens.json +164 -0
  82. package/src/theme/core/component/stepper.tokens.json +484 -0
  83. package/src/theme/core/component/switch.tokens.json +285 -0
  84. package/src/theme/core/component/tab.tokens.json +192 -0
  85. package/src/theme/core/component/text-field.tokens.json +160 -0
  86. package/src/theme/core/component/text.tokens.json +59 -0
  87. package/src/theme/core/component/toast.tokens.json +343 -0
  88. package/src/theme/core/component/toolbar.tokens.json +114 -0
  89. package/src/theme/core/component/tooltip.tokens.json +61 -0
  90. package/src/theme/core/focus.tokens.json +56 -0
  91. package/src/theme/core/foreground.tokens.json +416 -0
  92. package/src/theme/core/gradient.tokens.json +41 -0
  93. package/src/theme/core/opacity.tokens.json +25 -0
  94. package/src/theme/core/shadow.tokens.json +81 -0
  95. package/src/theme/core/status.tokens.json +74 -0
  96. package/src/theme/core/typography.tokens.json +163 -0
  97. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  98. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  99. package/src/utils/__tests__/sd-formats.test.js +950 -0
  100. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  101. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  102. package/src/utils/copy-css-utils-cli.js +13 -1
  103. package/src/utils/css-utils-format-utils.js +105 -176
  104. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  105. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  106. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  107. package/src/utils/figma/auth.js +355 -0
  108. package/src/utils/figma/constants.js +22 -0
  109. package/src/utils/figma/errors.js +80 -0
  110. package/src/utils/figma/figma-api.js +1069 -0
  111. package/src/utils/figma/get-token.js +348 -0
  112. package/src/utils/figma/sync-components.js +909 -0
  113. package/src/utils/figma/sync-main.js +692 -0
  114. package/src/utils/figma/sync-orchestration.js +683 -0
  115. package/src/utils/figma/sync-primitives.js +230 -0
  116. package/src/utils/figma/sync-semantic.js +1056 -0
  117. package/src/utils/figma/token-conversion.js +340 -0
  118. package/src/utils/figma/token-parsing.js +186 -0
  119. package/src/utils/figma/token-resolution.js +569 -0
  120. package/src/utils/figma/utils.js +199 -0
  121. package/src/utils/sd-build-configs.js +305 -0
  122. package/src/utils/sd-formats.js +965 -0
  123. package/src/utils/sd-transforms.js +165 -0
  124. package/src/utils/token-helpers.js +848 -0
  125. package/tsconfig.json +18 -0
  126. package/vitest.config.js +17 -0
  127. package/.turbo/turbo-build.log +0 -37
  128. package/build/web/core/raw.js +0 -229
  129. package/src/global/primitive/breakpoint.js +0 -19
  130. package/src/global/primitive/color.js +0 -231
  131. package/src/global/primitive/duration.js +0 -16
  132. package/src/global/primitive/font.js +0 -60
  133. package/src/global/primitive/radius.js +0 -31
  134. package/src/global/primitive/size.js +0 -55
  135. package/src/global/primitive/transition.js +0 -16
  136. package/src/theme/core/background.js +0 -170
  137. package/src/theme/core/border.js +0 -103
  138. package/src/theme/core/charts.js +0 -439
  139. package/src/theme/core/component/button.js +0 -708
  140. package/src/theme/core/component/checkbox.js +0 -405
  141. package/src/theme/core/focus.js +0 -35
  142. package/src/theme/core/foreground.js +0 -148
  143. package/src/theme/core/overlay.js +0 -137
  144. package/src/theme/core/shadow.js +0 -29
  145. package/src/theme/core/status.js +0 -49
  146. package/src/theme/core/typography.js +0 -82
  147. package/type/types.ts +0 -341
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Token Conversion Utilities
5
+ *
6
+ * Functions for converting token values to Figma variable format.
7
+ */
8
+
9
+ /**
10
+ * Convert color value to Figma format
11
+ * Figma expects RGBA object: { r: 0-1, g: 0-1, b: 0-1, a: 0-1 }
12
+ * @param {any} colorValue - Color value to convert
13
+ * @returns {Object|null} Figma color object or null if conversion fails
14
+ */
15
+ function convertColorValue(colorValue) {
16
+ if (typeof colorValue !== "string") {
17
+ // Handle composite color objects
18
+ if (colorValue && typeof colorValue === "object" && colorValue.color) {
19
+ const baseColor = convertColorValue(colorValue.color);
20
+ if (baseColor === null) {
21
+ return null;
22
+ }
23
+ return {
24
+ ...baseColor,
25
+ a: colorValue.alpha ?? baseColor.a,
26
+ };
27
+ }
28
+ return null;
29
+ }
30
+
31
+ // Handle rgba/rgb strings
32
+ const rgbaMatch = colorValue.match(
33
+ /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
34
+ );
35
+ if (rgbaMatch) {
36
+ return {
37
+ r: parseInt(rgbaMatch[1]) / 255,
38
+ g: parseInt(rgbaMatch[2]) / 255,
39
+ b: parseInt(rgbaMatch[3]) / 255,
40
+ a: rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1,
41
+ };
42
+ }
43
+
44
+ // Handle hex colors
45
+ const hexMatch = colorValue.match(/^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/);
46
+ if (hexMatch) {
47
+ const r = parseInt(hexMatch[1].substring(0, 2), 16) / 255;
48
+ const g = parseInt(hexMatch[1].substring(2, 4), 16) / 255;
49
+ const b = parseInt(hexMatch[1].substring(4, 6), 16) / 255;
50
+ const a = hexMatch[2] ? parseInt(hexMatch[2], 16) / 255 : 1;
51
+ return { r, g, b, a };
52
+ }
53
+
54
+ // Handle 3-digit hex
55
+ const hex3Match = colorValue.match(/^#([0-9a-fA-F]{3})$/);
56
+ if (hex3Match) {
57
+ const hex = hex3Match[1];
58
+ const r = parseInt(hex[0] + hex[0], 16) / 255;
59
+ const g = parseInt(hex[1] + hex[1], 16) / 255;
60
+ const b = parseInt(hex[2] + hex[2], 16) / 255;
61
+ return { r, g, b, a: 1 };
62
+ }
63
+
64
+ // Handle transparent
65
+ if (colorValue === "transparent") {
66
+ return { r: 0, g: 0, b: 0, a: 0 };
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Convert a CSS font-family value to a single font name for Figma.
74
+ * Figma expects one font name; CSS allows a comma-separated stack with generic fallbacks.
75
+ * e.g. "'Nunito Sans', sans-serif" -> "Nunito Sans", "'Sofia Pro', SofiaPro, sans-serif" -> "Sofia Pro"
76
+ * @param {string} cssFontFamily - CSS font-family string (may be quoted, comma-separated)
77
+ * @returns {string} Single font name with quotes stripped
78
+ */
79
+ function cssFontFamilyToFigmaFontName(cssFontFamily) {
80
+ if (typeof cssFontFamily !== "string") return cssFontFamily;
81
+ const first = cssFontFamily.split(",")[0].trim();
82
+ const unquoted = first.replace(/^["']|["']$/g, "").trim();
83
+ return unquoted || first;
84
+ }
85
+
86
+ /** Base font size for rem-to-px conversion (1rem = 16px) */
87
+ const REM_BASE_PX = 16;
88
+
89
+ /** Figma does not support percentage for dimensions; 100% (circular radius) is the only outlier we map, to 5000px. */
90
+ const CIRCULAR_RADIUS_PX = 5000;
91
+
92
+ /**
93
+ * Convert a numeric dimension and unit to pixels (as a number).
94
+ * rem is converted to px using REM_BASE_PX; px is returned as-is.
95
+ * Only 100% is converted (to 5000px); other percentages are not supported by Figma.
96
+ * @param {number} num - Numeric value
97
+ * @param {string} [unit] - Unit (e.g. "rem", "px", "%")
98
+ * @returns {number|null} Value in pixels, or null if unit is % and num is not 100
99
+ */
100
+ function dimensionToPx(num, unit) {
101
+ if (unit === "rem") {
102
+ return num * REM_BASE_PX;
103
+ }
104
+ if (unit === "%") {
105
+ return num === 100 ? CIRCULAR_RADIUS_PX : null;
106
+ }
107
+ return num;
108
+ }
109
+
110
+ /**
111
+ * Convert dimension value to Figma number format (pixels).
112
+ * Figma variables support FLOAT type for dimensions; we store px values.
113
+ * rem units are converted to px (e.g. 2rem → 32).
114
+ * @param {any} dimensionValue - Dimension value to convert (object with value/unit, or string like "2rem"/"32px")
115
+ * @returns {number|null} Numeric value in pixels, or null if conversion fails
116
+ */
117
+ function convertDimensionValue(dimensionValue) {
118
+ if (
119
+ typeof dimensionValue === "object" &&
120
+ dimensionValue.value !== undefined
121
+ ) {
122
+ const num = Number(dimensionValue.value);
123
+ if (Number.isNaN(num)) return null;
124
+ const unit = dimensionValue.unit ?? "px";
125
+ return dimensionToPx(num, unit);
126
+ }
127
+ if (typeof dimensionValue === "string") {
128
+ // Match number and optional unit: "16px", "1.5rem", "24", "100%" (Figma has no %; 100% → 5000px)
129
+ const match = dimensionValue.match(/^([\d.]+)\s*(px|rem|%)?$/);
130
+ if (match) {
131
+ const num = parseFloat(match[1]);
132
+ const unit = match[2] ?? "px";
133
+ return dimensionToPx(num, unit);
134
+ }
135
+ }
136
+ if (typeof dimensionValue === "number") {
137
+ return dimensionValue;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Convert line-height from decimal (1 = 100%) to Figma percentage string ("100%").
144
+ * @param {number|string} value - Decimal line-height (e.g. 1.5 = 150%)
145
+ * @returns {string} Percentage string for Figma (e.g. "150%")
146
+ */
147
+ function lineHeightToFigmaPercent(value) {
148
+ const num = typeof value === "number" ? value : parseFloat(value);
149
+ if (Number.isNaN(num)) return value;
150
+ const pct = num * 100;
151
+ return `${Number.isInteger(pct) ? pct : Number(pct.toFixed(2))}%`;
152
+ }
153
+
154
+ /**
155
+ * Check if options indicate a line-height token (path or variable name).
156
+ * @param {{ tokenPath?: string, variableName?: string }} [options]
157
+ * @returns {boolean}
158
+ */
159
+ function isLineHeightContext(options) {
160
+ if (!options) return false;
161
+ const path = options.tokenPath ?? options.variableName ?? "";
162
+ return typeof path === "string" && path.includes("line-height");
163
+ }
164
+
165
+ /**
166
+ * Check if options indicate a font-family token (path or variable name).
167
+ * Used so we only collapse comma-separated font stacks to a single name for Figma;
168
+ * other comma-containing strings (e.g. cubic-bezier) are left intact.
169
+ * @param {{ tokenPath?: string, variableName?: string }} [options]
170
+ * @returns {boolean}
171
+ */
172
+ function isFontFamilyContext(options) {
173
+ if (!options) return false;
174
+ const path = options.tokenPath ?? options.variableName ?? "";
175
+ return (
176
+ typeof path === "string" &&
177
+ (path.includes("font/family") || path.includes("font-family"))
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Convert token value to Figma variable value based on type
183
+ * @param {string} tokenType - Token type (color, dimension, etc.)
184
+ * @param {any} value - Value to convert
185
+ * @param {{ tokenPath?: string, variableName?: string, scopes?: string[] }} [options] - Optional token path or variable name for context (e.g. line-height → %)
186
+ * @returns {any} Converted value for Figma
187
+ */
188
+ function convertTokenValueToFigma(tokenType, value, options) {
189
+ switch (tokenType) {
190
+ case "color":
191
+ return convertColorValue(value);
192
+ case "dimension":
193
+ case "duration":
194
+ // If the value is a string with spaces (e.g., "0rem 0rem 0rem"),
195
+ // treat it as a string since Figma doesn't support space-separated dimension values
196
+ if (typeof value === "string" && value.includes(" ")) {
197
+ return value;
198
+ }
199
+ // Opacity scope: Figma expects 0–100 (percentage), tokens use 0–1 (decimal)
200
+ if (
201
+ Array.isArray(options?.scopes) &&
202
+ options.scopes.includes("OPACITY")
203
+ ) {
204
+ const num = typeof value === "number" ? value : parseFloat(value);
205
+ if (!Number.isNaN(num)) return num * 100;
206
+ }
207
+ return convertDimensionValue(value);
208
+ case "number":
209
+ // Line-height: tokens use decimal (1 = 100%); Figma expects % string (e.g. "150%")
210
+ if (isLineHeightContext(options)) {
211
+ return lineHeightToFigmaPercent(value);
212
+ }
213
+ return typeof value === "number" ? value : parseFloat(value);
214
+ case "string":
215
+ // Figma font family expects a single font name; CSS uses comma-separated stacks.
216
+ // Only collapse to first name when we're in a font-family context so we don't
217
+ // truncate other comma-containing strings (e.g. cubic-bezier(.4, 0, .2, 1)).
218
+ if (
219
+ typeof value === "string" &&
220
+ isFontFamilyContext(options) &&
221
+ (value.includes(",") || /^["'].*["']$/.test(value.trim()))
222
+ ) {
223
+ return cssFontFamilyToFigmaFontName(value);
224
+ }
225
+ return value;
226
+ default:
227
+ // For unknown types, try to extract value from objects with a value property
228
+ // This handles cases where tokens have structured values like {value: 200, unit: "ms"}
229
+ if (
230
+ typeof value === "object" &&
231
+ value !== null &&
232
+ value.value !== undefined
233
+ ) {
234
+ return convertDimensionValue(value);
235
+ }
236
+ // If it's a string with spaces, return as string
237
+ if (typeof value === "string" && value.includes(" ")) {
238
+ return value;
239
+ }
240
+ return value;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Get Figma variable type from token type
246
+ * @param {string} tokenType - Token type
247
+ * @returns {string} Figma variable type (COLOR, FLOAT, or STRING)
248
+ */
249
+ function getFigmaVariableType(tokenType) {
250
+ switch (tokenType) {
251
+ case "color":
252
+ return "COLOR";
253
+ case "dimension":
254
+ case "duration":
255
+ case "number":
256
+ return "FLOAT";
257
+ case "string":
258
+ return "STRING";
259
+ default:
260
+ return "STRING";
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Check if a token path is a shadow size token
266
+ * @param {string} tokenPath - Token path
267
+ * @returns {boolean} True if token is a shadow size token
268
+ */
269
+ function isShadowSizeToken(tokenPath) {
270
+ return tokenPath.startsWith("shadow/size/");
271
+ }
272
+
273
+ /**
274
+ * Parse a shadow size value string to extract x, y, blur references
275
+ * Returns {x, y, blur} or null if parsing fails
276
+ * Example: "{size.0} {size.0} {size.0}" -> {x: "size.0", y: "size.0", blur: "size.0"}
277
+ * @param {string} value - Shadow size value string
278
+ * @returns {Object|null} Parsed shadow components or null
279
+ */
280
+ function parseShadowSizeValue(value) {
281
+ if (typeof value !== "string") return null;
282
+
283
+ // Match pattern like "{ref1} {ref2} {ref3}" (three space-separated references)
284
+ const match = value.match(/^\{([^}]+)\}\s+\{([^}]+)\}\s+\{([^}]+)\}$/);
285
+ if (!match) return null;
286
+
287
+ return {
288
+ x: match[1], // e.g., "size.0"
289
+ y: match[2], // e.g., "size.0"
290
+ blur: match[3], // e.g., "size.0"
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Check if a token type is a gradient type
296
+ * @param {string} tokenType - Token type
297
+ * @returns {boolean}
298
+ */
299
+ function isGradientToken(tokenType) {
300
+ return tokenType === "gradient";
301
+ }
302
+
303
+ /**
304
+ * Parse gradient stops from a gradient token, extracting per-stop light/dark color refs.
305
+ * Returns null if the token value is not a resolved gradient object.
306
+ * Component tokens reference a semantic gradient token, so the raw $value may be a
307
+ * reference string — pass the actual gradient object (already resolved) as gradientValue.
308
+ *
309
+ * @param {Object} gradientValue - Gradient $value object { type, angle, stops[] }
310
+ * @returns {Array<{lightRef: string, darkRef: string}>|null}
311
+ */
312
+ function parseGradientStops(gradientValue) {
313
+ if (
314
+ !gradientValue ||
315
+ typeof gradientValue !== "object" ||
316
+ !Array.isArray(gradientValue.stops)
317
+ ) {
318
+ return null;
319
+ }
320
+
321
+ return gradientValue.stops.map((stop) => {
322
+ const lightRef =
323
+ stop.color?.$extensions?.appearance?.light?.$value ?? stop.color?.$value;
324
+ const darkRef =
325
+ stop.color?.$extensions?.appearance?.dark?.$value ?? stop.color?.$value;
326
+ return { lightRef, darkRef };
327
+ });
328
+ }
329
+
330
+ module.exports = {
331
+ convertColorValue,
332
+ convertDimensionValue,
333
+ cssFontFamilyToFigmaFontName,
334
+ convertTokenValueToFigma,
335
+ getFigmaVariableType,
336
+ isShadowSizeToken,
337
+ parseShadowSizeValue,
338
+ isGradientToken,
339
+ parseGradientStops,
340
+ };
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Token Parsing Utilities
5
+ *
6
+ * Functions for loading, parsing, and processing token files.
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ // Configuration
13
+ const TOKEN_SRC_DIR = path.join(__dirname, "../../");
14
+ const THEME_DIR = path.join(TOKEN_SRC_DIR, "theme");
15
+
16
+ /**
17
+ * Load all token JSON files recursively
18
+ * @param {string} dir - Directory to search
19
+ * @param {Array<string>} fileList - Accumulated file list
20
+ * @returns {Array<string>} Array of file paths
21
+ */
22
+ function loadTokenFiles(dir, fileList = []) {
23
+ const files = fs.readdirSync(dir);
24
+
25
+ for (const file of files) {
26
+ const filePath = path.join(dir, file);
27
+ const stat = fs.statSync(filePath);
28
+
29
+ if (stat.isDirectory()) {
30
+ loadTokenFiles(filePath, fileList);
31
+ } else if (file.endsWith(".tokens.json")) {
32
+ fileList.push(filePath);
33
+ }
34
+ }
35
+
36
+ return fileList;
37
+ }
38
+
39
+ /**
40
+ * Parse a token file and return the token object
41
+ * @param {string} filePath - Path to token file
42
+ * @returns {Object} Parsed token object
43
+ */
44
+ function parseTokenFile(filePath) {
45
+ try {
46
+ const content = fs.readFileSync(filePath, "utf8");
47
+ return JSON.parse(content);
48
+ } catch (error) {
49
+ throw new Error(`Failed to parse token file ${filePath}: ${error.message}`);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Detect all themes in src/theme/ directory
55
+ * @returns {Array<string>} Array of theme names (e.g., ["core", "test"])
56
+ */
57
+ function detectThemes() {
58
+ if (!fs.existsSync(THEME_DIR)) {
59
+ return ["core"]; // Default to core if theme directory doesn't exist
60
+ }
61
+
62
+ const themes = [];
63
+ const entries = fs.readdirSync(THEME_DIR, { withFileTypes: true });
64
+
65
+ for (const entry of entries) {
66
+ if (entry.isDirectory()) {
67
+ themes.push(entry.name);
68
+ }
69
+ }
70
+
71
+ // Ensure "core" is first if it exists
72
+ if (themes.includes("core")) {
73
+ themes.sort((a, b) => {
74
+ if (a === "core") return -1;
75
+ if (b === "core") return 1;
76
+ return a.localeCompare(b);
77
+ });
78
+ }
79
+
80
+ return themes.length > 0 ? themes : ["core"];
81
+ }
82
+
83
+ /**
84
+ * Load token files for a specific theme
85
+ * @param {string} themeName - Theme name (e.g., "core", "test")
86
+ * @returns {Array<string>} Array of file paths
87
+ */
88
+ function loadThemeTokenFiles(themeName) {
89
+ const themePath = path.join(THEME_DIR, themeName);
90
+ if (!fs.existsSync(themePath)) {
91
+ return [];
92
+ }
93
+
94
+ // Also include global/primitive tokens (they're shared across themes)
95
+ // TOKEN_SRC_DIR is already packages/hammer-token/src/, so we don't need to add "src/" again
96
+ const globalPrimitivePath = path.join(TOKEN_SRC_DIR, "global/primitive");
97
+ const files = [];
98
+
99
+ // Load primitive tokens (shared)
100
+ if (fs.existsSync(globalPrimitivePath)) {
101
+ loadTokenFiles(globalPrimitivePath, files);
102
+ }
103
+
104
+ // Load theme-specific tokens
105
+ loadTokenFiles(themePath, files);
106
+
107
+ return files;
108
+ }
109
+
110
+ /**
111
+ * Flatten token tree to a list of tokens with their paths
112
+ * @param {Object} obj - Token object to flatten
113
+ * @param {Array} path - Current path in the token tree
114
+ * @param {Array} tokens - Accumulated tokens array
115
+ * @param {string} sourceFilePath - Path to the source file (for determining token type)
116
+ * @returns {Array} Array of token objects with path information
117
+ */
118
+ function flattenTokenTree(obj, path = [], tokens = [], sourceFilePath = "") {
119
+ for (const [key, value] of Object.entries(obj)) {
120
+ if (key === "$schema" || key === "$type") {
121
+ continue;
122
+ }
123
+
124
+ const currentPath = [...path, key];
125
+
126
+ if (value && typeof value === "object" && !Array.isArray(value)) {
127
+ if (value.$type !== undefined || value.$value !== undefined) {
128
+ tokens.push({
129
+ path: currentPath,
130
+ token: value,
131
+ sourceFilePath,
132
+ });
133
+ } else {
134
+ flattenTokenTree(value, currentPath, tokens, sourceFilePath);
135
+ }
136
+ }
137
+ }
138
+
139
+ return tokens;
140
+ }
141
+
142
+ /**
143
+ * Convert token path to Figma variable name (path-based with slashes)
144
+ * @param {Array<string>} tokenPath - Token path array
145
+ * @returns {string} Variable name with slashes
146
+ */
147
+ function pathToVariableName(tokenPath) {
148
+ return tokenPath.join("/");
149
+ }
150
+
151
+ /**
152
+ * Determine if a token is a primitive token based on its source file path
153
+ * @param {string} sourceFilePath - Path to the source token file
154
+ * @returns {boolean} True if the token is a primitive token
155
+ */
156
+ function isPrimitiveToken(sourceFilePath) {
157
+ if (!sourceFilePath) return false;
158
+ // Primitive tokens are in src/global/primitive/ directory
159
+ // Normalize path separators for cross-platform compatibility
160
+ const normalizedPath = sourceFilePath.replace(/\\/g, "/");
161
+ return normalizedPath.includes("/primitive/");
162
+ }
163
+
164
+ /**
165
+ * Determine if a token is a component token based on its source file path
166
+ * @param {string} sourceFilePath - Path to the source token file
167
+ * @returns {boolean} True if the token is a component token
168
+ */
169
+ function isComponentToken(sourceFilePath) {
170
+ if (!sourceFilePath) return false;
171
+ // Component tokens are in src/theme/core/component/ directory
172
+ // Normalize path separators for cross-platform compatibility
173
+ const normalizedPath = sourceFilePath.replace(/\\/g, "/");
174
+ return normalizedPath.includes("/component/");
175
+ }
176
+
177
+ module.exports = {
178
+ loadTokenFiles,
179
+ parseTokenFile,
180
+ detectThemes,
181
+ loadThemeTokenFiles,
182
+ flattenTokenTree,
183
+ pathToVariableName,
184
+ isPrimitiveToken,
185
+ isComponentToken,
186
+ };