@fragments-sdk/cli 0.2.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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,652 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { FigmaClient, FigmaError } from "../../figma.js";
3
+ import type {
4
+ FigmaColor,
5
+ FigmaDesignProperties,
6
+ FigmaFill,
7
+ FigmaStroke,
8
+ FigmaEffect,
9
+ FigmaTypography,
10
+ } from "../../figma.js";
11
+
12
+ // Create a client instance for testing (no API calls needed for these tests)
13
+ const client = new FigmaClient({ accessToken: "test-token" });
14
+
15
+ describe("FigmaClient", () => {
16
+ describe("colorToCSS", () => {
17
+ it("converts fully opaque color to hex", () => {
18
+ const color: FigmaColor = { r: 1, g: 0, b: 0, a: 1 };
19
+ expect(client.colorToCSS(color)).toBe("#ff0000");
20
+ });
21
+
22
+ it("converts fully opaque white to hex", () => {
23
+ const color: FigmaColor = { r: 1, g: 1, b: 1, a: 1 };
24
+ expect(client.colorToCSS(color)).toBe("#ffffff");
25
+ });
26
+
27
+ it("converts fully opaque black to hex", () => {
28
+ const color: FigmaColor = { r: 0, g: 0, b: 0, a: 1 };
29
+ expect(client.colorToCSS(color)).toBe("#000000");
30
+ });
31
+
32
+ it("converts semi-transparent color to rgba", () => {
33
+ const color: FigmaColor = { r: 1, g: 0, b: 0, a: 0.5 };
34
+ expect(client.colorToCSS(color)).toBe("rgba(255, 0, 0, 0.50)");
35
+ });
36
+
37
+ it("converts fully transparent color to rgba", () => {
38
+ const color: FigmaColor = { r: 0, g: 0, b: 0, a: 0 };
39
+ expect(client.colorToCSS(color)).toBe("rgba(0, 0, 0, 0.00)");
40
+ });
41
+
42
+ it("handles fractional RGB values correctly", () => {
43
+ const color: FigmaColor = { r: 0.2, g: 0.4, b: 0.6, a: 1 };
44
+ expect(client.colorToCSS(color)).toBe("#336699");
45
+ });
46
+
47
+ it("applies opacity parameter to color alpha", () => {
48
+ const color: FigmaColor = { r: 1, g: 0, b: 0, a: 1 };
49
+ expect(client.colorToCSS(color, 0.5)).toBe("rgba(255, 0, 0, 0.50)");
50
+ });
51
+
52
+ it("multiplies opacity parameter with color alpha", () => {
53
+ const color: FigmaColor = { r: 1, g: 0, b: 0, a: 0.5 };
54
+ expect(client.colorToCSS(color, 0.5)).toBe("rgba(255, 0, 0, 0.25)");
55
+ });
56
+
57
+ it("handles common design colors", () => {
58
+ // Figma blue
59
+ const blue: FigmaColor = { r: 0.2, g: 0.6, b: 1, a: 1 };
60
+ expect(client.colorToCSS(blue)).toBe("#3399ff");
61
+
62
+ // Primary green
63
+ const green: FigmaColor = { r: 0, g: 0.8, b: 0.4, a: 1 };
64
+ expect(client.colorToCSS(green)).toBe("#00cc66");
65
+ });
66
+ });
67
+
68
+ describe("convertToCSS", () => {
69
+ describe("background color", () => {
70
+ it("converts solid fill to backgroundColor", () => {
71
+ const props: FigmaDesignProperties = {
72
+ nodeId: "1:2",
73
+ name: "Button",
74
+ type: "FRAME",
75
+ fills: [{ type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 } }],
76
+ };
77
+ const css = client.convertToCSS(props);
78
+ expect(css.backgroundColor).toBe("#ff0000");
79
+ });
80
+
81
+ it("ignores invisible fills", () => {
82
+ const props: FigmaDesignProperties = {
83
+ nodeId: "1:2",
84
+ name: "Button",
85
+ type: "FRAME",
86
+ fills: [
87
+ { type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, visible: false },
88
+ ],
89
+ };
90
+ const css = client.convertToCSS(props);
91
+ expect(css.backgroundColor).toBeUndefined();
92
+ });
93
+
94
+ it("applies fill opacity", () => {
95
+ const props: FigmaDesignProperties = {
96
+ nodeId: "1:2",
97
+ name: "Button",
98
+ type: "FRAME",
99
+ fills: [
100
+ { type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 0.5 },
101
+ ],
102
+ };
103
+ const css = client.convertToCSS(props);
104
+ expect(css.backgroundColor).toBe("rgba(255, 0, 0, 0.50)");
105
+ });
106
+
107
+ it("skips gradient fills", () => {
108
+ const props: FigmaDesignProperties = {
109
+ nodeId: "1:2",
110
+ name: "Button",
111
+ type: "FRAME",
112
+ fills: [{ type: "GRADIENT_LINEAR" }],
113
+ };
114
+ const css = client.convertToCSS(props);
115
+ expect(css.backgroundColor).toBeUndefined();
116
+ });
117
+ });
118
+
119
+ describe("border", () => {
120
+ it("converts stroke to borderColor", () => {
121
+ const props: FigmaDesignProperties = {
122
+ nodeId: "1:2",
123
+ name: "Button",
124
+ type: "FRAME",
125
+ strokes: [{ type: "SOLID", color: { r: 0, g: 0, b: 0, a: 1 } }],
126
+ strokeWeight: 2,
127
+ };
128
+ const css = client.convertToCSS(props);
129
+ expect(css.borderColor).toBe("#000000");
130
+ expect(css.borderWidth).toBe("2px");
131
+ });
132
+
133
+ it("ignores invisible strokes", () => {
134
+ const props: FigmaDesignProperties = {
135
+ nodeId: "1:2",
136
+ name: "Button",
137
+ type: "FRAME",
138
+ strokes: [
139
+ {
140
+ type: "SOLID",
141
+ color: { r: 0, g: 0, b: 0, a: 1 },
142
+ visible: false,
143
+ },
144
+ ],
145
+ };
146
+ const css = client.convertToCSS(props);
147
+ expect(css.borderColor).toBeUndefined();
148
+ });
149
+ });
150
+
151
+ describe("border radius", () => {
152
+ it("converts uniform cornerRadius", () => {
153
+ const props: FigmaDesignProperties = {
154
+ nodeId: "1:2",
155
+ name: "Button",
156
+ type: "FRAME",
157
+ cornerRadius: 8,
158
+ };
159
+ const css = client.convertToCSS(props);
160
+ expect(css.borderRadius).toBe("8px");
161
+ });
162
+
163
+ it("converts individual corner radii", () => {
164
+ const props: FigmaDesignProperties = {
165
+ nodeId: "1:2",
166
+ name: "Button",
167
+ type: "FRAME",
168
+ topLeftRadius: 4,
169
+ topRightRadius: 8,
170
+ bottomRightRadius: 12,
171
+ bottomLeftRadius: 16,
172
+ };
173
+ const css = client.convertToCSS(props);
174
+ expect(css.borderRadius).toBe("4px 8px 12px 16px");
175
+ });
176
+
177
+ it("handles mixed undefined corner radii", () => {
178
+ const props: FigmaDesignProperties = {
179
+ nodeId: "1:2",
180
+ name: "Button",
181
+ type: "FRAME",
182
+ topLeftRadius: 4,
183
+ };
184
+ const css = client.convertToCSS(props);
185
+ expect(css.borderRadius).toBe("4px 0px 0px 0px");
186
+ });
187
+ });
188
+
189
+ describe("box shadow", () => {
190
+ it("converts drop shadow to boxShadow", () => {
191
+ const props: FigmaDesignProperties = {
192
+ nodeId: "1:2",
193
+ name: "Card",
194
+ type: "FRAME",
195
+ effects: [
196
+ {
197
+ type: "DROP_SHADOW",
198
+ color: { r: 0, g: 0, b: 0, a: 0.25 },
199
+ offset: { x: 0, y: 4 },
200
+ radius: 8,
201
+ spread: 0,
202
+ },
203
+ ],
204
+ };
205
+ const css = client.convertToCSS(props);
206
+ expect(css.boxShadow).toBe("0px 4px 8px 0px rgba(0, 0, 0, 0.25)");
207
+ });
208
+
209
+ it("combines multiple shadows", () => {
210
+ const props: FigmaDesignProperties = {
211
+ nodeId: "1:2",
212
+ name: "Card",
213
+ type: "FRAME",
214
+ effects: [
215
+ {
216
+ type: "DROP_SHADOW",
217
+ color: { r: 0, g: 0, b: 0, a: 0.1 },
218
+ offset: { x: 0, y: 2 },
219
+ radius: 4,
220
+ spread: 0,
221
+ },
222
+ {
223
+ type: "DROP_SHADOW",
224
+ color: { r: 0, g: 0, b: 0, a: 0.2 },
225
+ offset: { x: 0, y: 8 },
226
+ radius: 16,
227
+ spread: 0,
228
+ },
229
+ ],
230
+ };
231
+ const css = client.convertToCSS(props);
232
+ // Should have two shadows separated by ", " after the rgba()
233
+ expect(css.boxShadow).toMatch(/rgba\([^)]+\), 0px 8px/);
234
+ // Count occurrences of "px rgba" to verify two shadows
235
+ const matches = css.boxShadow?.match(/px rgba/g);
236
+ expect(matches).toHaveLength(2);
237
+ });
238
+
239
+ it("ignores invisible effects", () => {
240
+ const props: FigmaDesignProperties = {
241
+ nodeId: "1:2",
242
+ name: "Card",
243
+ type: "FRAME",
244
+ effects: [
245
+ {
246
+ type: "DROP_SHADOW",
247
+ color: { r: 0, g: 0, b: 0, a: 0.25 },
248
+ offset: { x: 0, y: 4 },
249
+ radius: 8,
250
+ visible: false,
251
+ },
252
+ ],
253
+ };
254
+ const css = client.convertToCSS(props);
255
+ expect(css.boxShadow).toBeUndefined();
256
+ });
257
+
258
+ it("ignores blur effects", () => {
259
+ const props: FigmaDesignProperties = {
260
+ nodeId: "1:2",
261
+ name: "Card",
262
+ type: "FRAME",
263
+ effects: [{ type: "LAYER_BLUR", radius: 8 }],
264
+ };
265
+ const css = client.convertToCSS(props);
266
+ expect(css.boxShadow).toBeUndefined();
267
+ });
268
+ });
269
+
270
+ describe("typography", () => {
271
+ it("converts font family and size", () => {
272
+ const props: FigmaDesignProperties = {
273
+ nodeId: "1:2",
274
+ name: "Text",
275
+ type: "TEXT",
276
+ typography: {
277
+ fontFamily: "Inter",
278
+ fontStyle: "Regular",
279
+ fontSize: 16,
280
+ },
281
+ };
282
+ const css = client.convertToCSS(props);
283
+ expect(css.fontFamily).toBe("Inter");
284
+ expect(css.fontSize).toBe("16px");
285
+ expect(css.fontWeight).toBe("400");
286
+ });
287
+
288
+ it("converts font weight from style name", () => {
289
+ const weights: Array<[string, string]> = [
290
+ ["Thin", "100"],
291
+ ["ExtraLight", "200"],
292
+ ["Light", "300"],
293
+ ["Regular", "400"],
294
+ ["Medium", "500"],
295
+ ["SemiBold", "600"],
296
+ ["Bold", "700"],
297
+ ["ExtraBold", "800"],
298
+ ["Black", "900"],
299
+ ];
300
+
301
+ for (const [style, weight] of weights) {
302
+ const props: FigmaDesignProperties = {
303
+ nodeId: "1:2",
304
+ name: "Text",
305
+ type: "TEXT",
306
+ typography: {
307
+ fontFamily: "Inter",
308
+ fontStyle: style,
309
+ fontSize: 16,
310
+ },
311
+ };
312
+ const css = client.convertToCSS(props);
313
+ expect(css.fontWeight).toBe(weight);
314
+ }
315
+ });
316
+
317
+ it("defaults to 400 for unknown font style", () => {
318
+ const props: FigmaDesignProperties = {
319
+ nodeId: "1:2",
320
+ name: "Text",
321
+ type: "TEXT",
322
+ typography: {
323
+ fontFamily: "Inter",
324
+ fontStyle: "Custom",
325
+ fontSize: 16,
326
+ },
327
+ };
328
+ const css = client.convertToCSS(props);
329
+ expect(css.fontWeight).toBe("400");
330
+ });
331
+
332
+ it("converts pixel line height", () => {
333
+ const props: FigmaDesignProperties = {
334
+ nodeId: "1:2",
335
+ name: "Text",
336
+ type: "TEXT",
337
+ typography: {
338
+ fontFamily: "Inter",
339
+ fontStyle: "Regular",
340
+ fontSize: 16,
341
+ lineHeight: { value: 24, unit: "PIXELS" },
342
+ },
343
+ };
344
+ const css = client.convertToCSS(props);
345
+ expect(css.lineHeight).toBe("24px");
346
+ });
347
+
348
+ it("converts percent line height", () => {
349
+ const props: FigmaDesignProperties = {
350
+ nodeId: "1:2",
351
+ name: "Text",
352
+ type: "TEXT",
353
+ typography: {
354
+ fontFamily: "Inter",
355
+ fontStyle: "Regular",
356
+ fontSize: 16,
357
+ lineHeight: { value: 150, unit: "PERCENT" },
358
+ },
359
+ };
360
+ const css = client.convertToCSS(props);
361
+ expect(css.lineHeight).toBe("150%");
362
+ });
363
+
364
+ it("converts letter spacing", () => {
365
+ const props: FigmaDesignProperties = {
366
+ nodeId: "1:2",
367
+ name: "Text",
368
+ type: "TEXT",
369
+ typography: {
370
+ fontFamily: "Inter",
371
+ fontStyle: "Regular",
372
+ fontSize: 16,
373
+ letterSpacing: -0.5,
374
+ },
375
+ };
376
+ const css = client.convertToCSS(props);
377
+ expect(css.letterSpacing).toBe("-0.5px");
378
+ });
379
+
380
+ it("converts text alignment", () => {
381
+ const alignments: Array<
382
+ ["LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED", string]
383
+ > = [
384
+ ["LEFT", "left"],
385
+ ["CENTER", "center"],
386
+ ["RIGHT", "right"],
387
+ ["JUSTIFIED", "justified"],
388
+ ];
389
+
390
+ for (const [figmaAlign, cssAlign] of alignments) {
391
+ const props: FigmaDesignProperties = {
392
+ nodeId: "1:2",
393
+ name: "Text",
394
+ type: "TEXT",
395
+ typography: {
396
+ fontFamily: "Inter",
397
+ fontStyle: "Regular",
398
+ fontSize: 16,
399
+ textAlignHorizontal: figmaAlign,
400
+ },
401
+ };
402
+ const css = client.convertToCSS(props);
403
+ expect(css.textAlign).toBe(cssAlign);
404
+ }
405
+ });
406
+ });
407
+
408
+ describe("padding", () => {
409
+ it("converts uniform padding", () => {
410
+ const props: FigmaDesignProperties = {
411
+ nodeId: "1:2",
412
+ name: "Button",
413
+ type: "FRAME",
414
+ padding: { top: 16, right: 16, bottom: 16, left: 16 },
415
+ };
416
+ const css = client.convertToCSS(props);
417
+ expect(css.padding).toBe("16px");
418
+ });
419
+
420
+ it("converts symmetric padding (vertical/horizontal)", () => {
421
+ const props: FigmaDesignProperties = {
422
+ nodeId: "1:2",
423
+ name: "Button",
424
+ type: "FRAME",
425
+ padding: { top: 8, right: 16, bottom: 8, left: 16 },
426
+ };
427
+ const css = client.convertToCSS(props);
428
+ expect(css.padding).toBe("8px 16px");
429
+ });
430
+
431
+ it("converts asymmetric padding", () => {
432
+ const props: FigmaDesignProperties = {
433
+ nodeId: "1:2",
434
+ name: "Button",
435
+ type: "FRAME",
436
+ padding: { top: 4, right: 8, bottom: 12, left: 16 },
437
+ };
438
+ const css = client.convertToCSS(props);
439
+ expect(css.padding).toBe("4px 8px 12px 16px");
440
+ });
441
+
442
+ it("handles missing padding values", () => {
443
+ const props: FigmaDesignProperties = {
444
+ nodeId: "1:2",
445
+ name: "Button",
446
+ type: "FRAME",
447
+ padding: { top: 8 },
448
+ };
449
+ const css = client.convertToCSS(props);
450
+ expect(css.padding).toBe("8px 0px 0px 0px");
451
+ });
452
+ });
453
+
454
+ describe("gap", () => {
455
+ it("converts itemSpacing to gap", () => {
456
+ const props: FigmaDesignProperties = {
457
+ nodeId: "1:2",
458
+ name: "Stack",
459
+ type: "FRAME",
460
+ itemSpacing: 16,
461
+ };
462
+ const css = client.convertToCSS(props);
463
+ expect(css.gap).toBe("16px");
464
+ });
465
+ });
466
+
467
+ describe("opacity", () => {
468
+ it("converts opacity when not 1", () => {
469
+ const props: FigmaDesignProperties = {
470
+ nodeId: "1:2",
471
+ name: "Overlay",
472
+ type: "FRAME",
473
+ opacity: 0.75,
474
+ };
475
+ const css = client.convertToCSS(props);
476
+ expect(css.opacity).toBe("0.75");
477
+ });
478
+
479
+ it("does not include opacity when 1", () => {
480
+ const props: FigmaDesignProperties = {
481
+ nodeId: "1:2",
482
+ name: "Overlay",
483
+ type: "FRAME",
484
+ opacity: 1,
485
+ };
486
+ const css = client.convertToCSS(props);
487
+ expect(css.opacity).toBeUndefined();
488
+ });
489
+ });
490
+
491
+ describe("dimensions", () => {
492
+ it("converts width and height", () => {
493
+ const props: FigmaDesignProperties = {
494
+ nodeId: "1:2",
495
+ name: "Box",
496
+ type: "FRAME",
497
+ width: 200,
498
+ height: 100,
499
+ };
500
+ const css = client.convertToCSS(props);
501
+ expect(css.width).toBe("200px");
502
+ expect(css.height).toBe("100px");
503
+ });
504
+
505
+ it("rounds dimensions to whole pixels", () => {
506
+ const props: FigmaDesignProperties = {
507
+ nodeId: "1:2",
508
+ name: "Box",
509
+ type: "FRAME",
510
+ width: 199.7,
511
+ height: 100.2,
512
+ };
513
+ const css = client.convertToCSS(props);
514
+ expect(css.width).toBe("200px");
515
+ expect(css.height).toBe("100px");
516
+ });
517
+ });
518
+ });
519
+
520
+ describe("parseVariantName", () => {
521
+ it("parses single property", () => {
522
+ const result = client.parseVariantName("Size=Medium");
523
+ expect(result).toEqual({ Size: "Medium" });
524
+ });
525
+
526
+ it("parses multiple properties", () => {
527
+ const result = client.parseVariantName("State=Primary, Size=Medium");
528
+ expect(result).toEqual({ State: "Primary", Size: "Medium" });
529
+ });
530
+
531
+ it("handles extra spaces", () => {
532
+ const result = client.parseVariantName(
533
+ "State = Primary , Size = Medium"
534
+ );
535
+ expect(result).toEqual({ State: "Primary", Size: "Medium" });
536
+ });
537
+
538
+ it("handles boolean-like values", () => {
539
+ const result = client.parseVariantName("Disabled=true, Loading=false");
540
+ expect(result).toEqual({ Disabled: "true", Loading: "false" });
541
+ });
542
+
543
+ it("handles complex variant names", () => {
544
+ const result = client.parseVariantName(
545
+ "Type=Primary, Size=Large, State=Hover, Disabled=false"
546
+ );
547
+ expect(result).toEqual({
548
+ Type: "Primary",
549
+ Size: "Large",
550
+ State: "Hover",
551
+ Disabled: "false",
552
+ });
553
+ });
554
+
555
+ it("returns empty object for invalid format", () => {
556
+ const result = client.parseVariantName("InvalidFormat");
557
+ expect(result).toEqual({});
558
+ });
559
+ });
560
+
561
+ describe("parseUrl", () => {
562
+ it("parses file URL with node-id", () => {
563
+ const result = client.parseUrl(
564
+ "https://figma.com/file/abc123/MyDesign?node-id=1:2"
565
+ );
566
+ expect(result).toEqual({ fileKey: "abc123", nodeId: "1:2" });
567
+ });
568
+
569
+ it("parses design URL format", () => {
570
+ const result = client.parseUrl(
571
+ "https://figma.com/design/xyz789/Components?node-id=10:20"
572
+ );
573
+ expect(result).toEqual({ fileKey: "xyz789", nodeId: "10:20" });
574
+ });
575
+
576
+ it("handles URL-encoded node IDs", () => {
577
+ const result = client.parseUrl(
578
+ "https://figma.com/file/abc123/MyDesign?node-id=1%3A2"
579
+ );
580
+ expect(result).toEqual({ fileKey: "abc123", nodeId: "1:2" });
581
+ });
582
+
583
+ it("handles www prefix", () => {
584
+ const result = client.parseUrl(
585
+ "https://www.figma.com/file/abc123/MyDesign?node-id=1:2"
586
+ );
587
+ expect(result).toEqual({ fileKey: "abc123", nodeId: "1:2" });
588
+ });
589
+
590
+ it("handles dash-style node IDs", () => {
591
+ const result = client.parseUrl(
592
+ "https://figma.com/design/abc123/MyDesign?node-id=1-2"
593
+ );
594
+ expect(result).toEqual({ fileKey: "abc123", nodeId: "1-2" });
595
+ });
596
+
597
+ it("throws FigmaError for invalid URL", () => {
598
+ expect(() => client.parseUrl("https://example.com/invalid")).toThrow(
599
+ FigmaError
600
+ );
601
+ });
602
+
603
+ it("throws FigmaError for URL without node-id", () => {
604
+ expect(() =>
605
+ client.parseUrl("https://figma.com/file/abc123/MyDesign")
606
+ ).toThrow(FigmaError);
607
+ });
608
+ });
609
+
610
+ describe("parseFileUrl", () => {
611
+ it("parses file URL without node-id", () => {
612
+ const result = client.parseFileUrl(
613
+ "https://figma.com/file/abc123/MyDesign"
614
+ );
615
+ expect(result).toEqual({ fileKey: "abc123", nodeId: undefined });
616
+ });
617
+
618
+ it("parses file URL with node-id", () => {
619
+ const result = client.parseFileUrl(
620
+ "https://figma.com/file/abc123/MyDesign?node-id=1:2"
621
+ );
622
+ expect(result).toEqual({ fileKey: "abc123", nodeId: "1:2" });
623
+ });
624
+
625
+ it("throws FigmaError for invalid URL", () => {
626
+ expect(() =>
627
+ client.parseFileUrl("https://example.com/invalid")
628
+ ).toThrow(FigmaError);
629
+ });
630
+ });
631
+
632
+ describe("buildNodeUrl", () => {
633
+ it("builds URL with file name", () => {
634
+ const url = client.buildNodeUrl("abc123", "1:2", "My Design");
635
+ expect(url).toBe(
636
+ "https://www.figma.com/design/abc123/My-Design?node-id=1%3A2"
637
+ );
638
+ });
639
+
640
+ it("builds URL without file name", () => {
641
+ const url = client.buildNodeUrl("abc123", "1:2");
642
+ expect(url).toBe(
643
+ "https://www.figma.com/design/abc123/Design?node-id=1%3A2"
644
+ );
645
+ });
646
+
647
+ it("URL-encodes the node ID", () => {
648
+ const url = client.buildNodeUrl("abc123", "10:20", "Test");
649
+ expect(url).toContain("node-id=10%3A20");
650
+ });
651
+ });
652
+ });