@servicetitan/hammer-token 2.5.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -2
- package/README.md +332 -0
- package/build/web/core/component-variables.scss +1088 -131
- package/build/web/core/component.d.ts +558 -0
- package/build/web/core/component.js +6685 -249
- package/build/web/core/component.scss +557 -69
- package/build/web/core/css-utils/a2-border.css +23 -51
- package/build/web/core/css-utils/a2-color.css +221 -233
- package/build/web/core/css-utils/a2-font.css +1 -29
- package/build/web/core/css-utils/a2-spacing.css +238 -483
- package/build/web/core/css-utils/a2-utils.css +496 -781
- package/build/web/core/css-utils/border.css +23 -51
- package/build/web/core/css-utils/color.css +221 -233
- package/build/web/core/css-utils/font.css +1 -29
- package/build/web/core/css-utils/spacing.css +238 -483
- package/build/web/core/css-utils/utils.css +496 -781
- package/build/web/core/index.d.ts +6 -0
- package/build/web/core/index.js +1 -1
- package/build/web/core/primitive-variables.scss +148 -65
- package/build/web/core/primitive.d.ts +209 -0
- package/build/web/core/primitive.js +779 -61
- package/build/web/core/primitive.scss +207 -124
- package/build/web/core/semantic-variables.scss +363 -245
- package/build/web/core/semantic.d.ts +221 -0
- package/build/web/core/semantic.js +1592 -347
- package/build/web/core/semantic.scss +219 -140
- package/build/web/index.d.ts +3 -4
- package/build/web/types.d.ts +17 -0
- package/config.js +121 -496
- package/eslint.config.mjs +11 -1
- package/package.json +15 -5
- package/src/global/primitive/breakpoint.tokens.json +54 -0
- package/src/global/primitive/color.tokens.json +1092 -0
- package/src/global/primitive/duration.tokens.json +44 -0
- package/src/global/primitive/font.tokens.json +151 -0
- package/src/global/primitive/radius.tokens.json +94 -0
- package/src/global/primitive/size.tokens.json +174 -0
- package/src/global/primitive/transition.tokens.json +32 -0
- package/src/theme/core/background.tokens.json +1312 -0
- package/src/theme/core/border.tokens.json +192 -0
- package/src/theme/core/chart.tokens.json +982 -0
- package/src/theme/core/component/ai-mark.tokens.json +20 -0
- package/src/theme/core/component/alert.tokens.json +261 -0
- package/src/theme/core/component/announcement.tokens.json +460 -0
- package/src/theme/core/component/avatar.tokens.json +137 -0
- package/src/theme/core/component/badge.tokens.json +42 -0
- package/src/theme/core/component/breadcrumb.tokens.json +42 -0
- package/src/theme/core/component/button-toggle.tokens.json +428 -0
- package/src/theme/core/component/button.tokens.json +941 -0
- package/src/theme/core/component/calendar.tokens.json +391 -0
- package/src/theme/core/component/card.tokens.json +107 -0
- package/src/theme/core/component/checkbox.tokens.json +631 -0
- package/src/theme/core/component/chip.tokens.json +169 -0
- package/src/theme/core/component/combobox.tokens.json +269 -0
- package/src/theme/core/component/details.tokens.json +152 -0
- package/src/theme/core/component/dialog.tokens.json +87 -0
- package/src/theme/core/component/divider.tokens.json +23 -0
- package/src/theme/core/component/dnd.tokens.json +208 -0
- package/src/theme/core/component/drawer.tokens.json +61 -0
- package/src/theme/core/component/drilldown.tokens.json +61 -0
- package/src/theme/core/component/edit-card.tokens.json +381 -0
- package/src/theme/core/component/field-label.tokens.json +42 -0
- package/src/theme/core/component/field-message.tokens.json +65 -0
- package/src/theme/core/component/icon.tokens.json +42 -0
- package/src/theme/core/component/link.tokens.json +108 -0
- package/src/theme/core/component/list-view.tokens.json +82 -0
- package/src/theme/core/component/listbox.tokens.json +283 -0
- package/src/theme/core/component/menu.tokens.json +230 -0
- package/src/theme/core/component/overflow.tokens.json +84 -0
- package/src/theme/core/component/page.tokens.json +377 -0
- package/src/theme/core/component/pagination.tokens.json +63 -0
- package/src/theme/core/component/popover.tokens.json +122 -0
- package/src/theme/core/component/progress-bar.tokens.json +133 -0
- package/src/theme/core/component/radio.tokens.json +631 -0
- package/src/theme/core/component/segmented-control.tokens.json +175 -0
- package/src/theme/core/component/select-card.tokens.json +943 -0
- package/src/theme/core/component/side-nav.tokens.json +349 -0
- package/src/theme/core/component/skeleton.tokens.json +42 -0
- package/src/theme/core/component/spinner.tokens.json +96 -0
- package/src/theme/core/component/status-icon.tokens.json +164 -0
- package/src/theme/core/component/stepper.tokens.json +484 -0
- package/src/theme/core/component/switch.tokens.json +285 -0
- package/src/theme/core/component/tab.tokens.json +192 -0
- package/src/theme/core/component/text-field.tokens.json +160 -0
- package/src/theme/core/component/text.tokens.json +59 -0
- package/src/theme/core/component/toast.tokens.json +343 -0
- package/src/theme/core/component/toolbar.tokens.json +114 -0
- package/src/theme/core/component/tooltip.tokens.json +61 -0
- package/src/theme/core/focus.tokens.json +56 -0
- package/src/theme/core/foreground.tokens.json +416 -0
- package/src/theme/core/gradient.tokens.json +41 -0
- package/src/theme/core/opacity.tokens.json +25 -0
- package/src/theme/core/shadow.tokens.json +81 -0
- package/src/theme/core/status.tokens.json +74 -0
- package/src/theme/core/typography.tokens.json +163 -0
- package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
- package/src/utils/__tests__/sd-build-configs.test.js +306 -0
- package/src/utils/__tests__/sd-formats.test.js +942 -0
- package/src/utils/__tests__/sd-transforms.test.js +336 -0
- package/src/utils/__tests__/token-helpers.test.js +1160 -0
- package/src/utils/copy-css-utils-cli.js +13 -1
- package/src/utils/css-utils-format-utils.js +105 -176
- package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
- package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
- package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
- package/src/utils/figma/auth.js +355 -0
- package/src/utils/figma/constants.js +22 -0
- package/src/utils/figma/errors.js +80 -0
- package/src/utils/figma/figma-api.js +1069 -0
- package/src/utils/figma/get-token.js +348 -0
- package/src/utils/figma/sync-components.js +909 -0
- package/src/utils/figma/sync-main.js +692 -0
- package/src/utils/figma/sync-orchestration.js +683 -0
- package/src/utils/figma/sync-primitives.js +230 -0
- package/src/utils/figma/sync-semantic.js +1056 -0
- package/src/utils/figma/token-conversion.js +340 -0
- package/src/utils/figma/token-parsing.js +186 -0
- package/src/utils/figma/token-resolution.js +569 -0
- package/src/utils/figma/utils.js +199 -0
- package/src/utils/sd-build-configs.js +305 -0
- package/src/utils/sd-formats.js +948 -0
- package/src/utils/sd-transforms.js +165 -0
- package/src/utils/token-helpers.js +848 -0
- package/tsconfig.json +18 -0
- package/vitest.config.js +17 -0
- package/.turbo/turbo-build.log +0 -37
- package/build/web/core/raw.js +0 -234
- package/src/global/primitive/breakpoint.js +0 -19
- package/src/global/primitive/color.js +0 -231
- package/src/global/primitive/duration.js +0 -16
- package/src/global/primitive/font.js +0 -60
- package/src/global/primitive/radius.js +0 -31
- package/src/global/primitive/size.js +0 -55
- package/src/global/primitive/transition.js +0 -16
- package/src/theme/core/background.js +0 -170
- package/src/theme/core/border.js +0 -103
- package/src/theme/core/charts.js +0 -464
- package/src/theme/core/component/button.js +0 -708
- package/src/theme/core/component/checkbox.js +0 -405
- package/src/theme/core/focus.js +0 -35
- package/src/theme/core/foreground.js +0 -148
- package/src/theme/core/overlay.js +0 -137
- package/src/theme/core/shadow.js +0 -29
- package/src/theme/core/status.js +0 -49
- package/src/theme/core/typography.js +0 -82
- package/type/types.ts +0 -344
|
@@ -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
|
+
};
|