decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Figma Token Export — DTCG & Figma REST API format generator.
3
+ * Reads all style definitions, runs derive() for each style × mode,
4
+ * classifies ~340 tokens into W3C DTCG types, and writes JSON files.
5
+ *
6
+ * @module figma-tokens
7
+ */
8
+
9
+ import { readdir, writeFile, mkdir } from 'node:fs/promises';
10
+ import { join, dirname, resolve } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const stylesDir = resolve(__dirname, '..', 'src', 'css', 'styles');
15
+
16
+ // ============================================================
17
+ // Style Discovery
18
+ // ============================================================
19
+
20
+ /**
21
+ * Dynamically import all style definitions from src/css/styles/.
22
+ * Each file must export a default-shaped object with { id, name, seed, personality, ... }.
23
+ * @returns {Promise<Object[]>} Array of style definitions
24
+ */
25
+ async function loadStyles(filter) {
26
+ const files = await readdir(stylesDir);
27
+ const jsFiles = files.filter(f => f.endsWith('.js')).sort();
28
+ const styles = [];
29
+
30
+ for (const file of jsFiles) {
31
+ const mod = await import(join(stylesDir, file));
32
+ // Each style file exports a named const (e.g. `export const auradecantism = { ... }`)
33
+ const style = Object.values(mod).find(v => v && typeof v === 'object' && v.id && v.seed);
34
+ if (!style) continue;
35
+ if (filter && filter !== 'all' && style.id !== filter) continue;
36
+ styles.push(style);
37
+ }
38
+
39
+ return styles;
40
+ }
41
+
42
+ // ============================================================
43
+ // Token Type Classification (W3C DTCG)
44
+ // ============================================================
45
+
46
+ const VAR_REF_RE = /^var\(--d-([^)]+)\)$/;
47
+ const RGBA_RE = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/;
48
+ const HEX_RE = /^#[0-9a-fA-F]{3,8}$/;
49
+ const REM_RE = /^-?[\d.]+rem$/;
50
+ const PX_RE = /^-?[\d.]+px$/;
51
+ const PERCENT_RE = /^-?[\d.]+%$/;
52
+ const DURATION_RE = /^[\d.]+m?s$/;
53
+ const CUBIC_BEZIER_RE = /^cubic-bezier\(([\d.,-\s]+)\)$/;
54
+ const BOX_SHADOW_RE = /^(?:inset\s+)?-?[\d.]+(?:px)?\s+-?[\d.]+(?:px)?\s+[\d.]+(?:px)?/;
55
+ const LINEAR_GRADIENT_RE = /^linear-gradient\(/;
56
+ const BLUR_RE = /^blur\([\d.]+px\)$/;
57
+ const FONT_FAMILY_RE = /^(system-ui|ui-monospace|[A-Z"'])/;
58
+ const NUMBER_RE = /^[\d.]+$/;
59
+
60
+ /**
61
+ * Classify a single token value into its DTCG $type.
62
+ * @param {string} name - CSS custom property name (e.g. '--d-primary')
63
+ * @param {string} value - Token value
64
+ * @returns {{ type: string, value: any }} DTCG type and normalized value
65
+ */
66
+ function classifyToken(name, value) {
67
+ if (typeof value !== 'string') {
68
+ return { type: 'string', value: String(value) };
69
+ }
70
+
71
+ // var() references → DTCG alias
72
+ const varMatch = value.match(VAR_REF_RE);
73
+ if (varMatch) {
74
+ const refPath = tokenNameToPath(varMatch[1]);
75
+ return { type: 'alias', value: `{${refPath}}` };
76
+ }
77
+
78
+ // Compound var() references (e.g. "var(--d-duration-normal) var(--d-easing-decelerate)")
79
+ if (value.includes('var(--d-') && !varMatch) {
80
+ return { type: 'string', value };
81
+ }
82
+
83
+ // Hex colors
84
+ if (HEX_RE.test(value)) {
85
+ return { type: 'color', value };
86
+ }
87
+
88
+ // rgba() colors
89
+ const rgbaMatch = value.match(RGBA_RE);
90
+ if (rgbaMatch) {
91
+ const hex = rgbaToHex(
92
+ parseInt(rgbaMatch[1]),
93
+ parseInt(rgbaMatch[2]),
94
+ parseInt(rgbaMatch[3]),
95
+ rgbaMatch[4] != null ? parseFloat(rgbaMatch[4]) : 1
96
+ );
97
+ return { type: 'color', value: hex };
98
+ }
99
+
100
+ // Box shadows (before dimension check — shadows contain px values)
101
+ if (BOX_SHADOW_RE.test(value)) {
102
+ return { type: 'shadow', value: parseShadow(value) };
103
+ }
104
+
105
+ // Linear gradients
106
+ if (LINEAR_GRADIENT_RE.test(value)) {
107
+ return { type: 'gradient', value: parseGradient(value) };
108
+ }
109
+
110
+ // Blur values
111
+ if (BLUR_RE.test(value)) {
112
+ return { type: 'string', value };
113
+ }
114
+
115
+ // Dimensions (rem/px/%)
116
+ if (REM_RE.test(value) || PX_RE.test(value) || PERCENT_RE.test(value)) {
117
+ return { type: 'dimension', value };
118
+ }
119
+
120
+ // Duration (ms/s)
121
+ if (DURATION_RE.test(value)) {
122
+ return { type: 'duration', value };
123
+ }
124
+
125
+ // Cubic bezier
126
+ const bezierMatch = value.match(CUBIC_BEZIER_RE);
127
+ if (bezierMatch) {
128
+ const parts = bezierMatch[1].split(',').map(s => parseFloat(s.trim()));
129
+ return { type: 'cubicBezier', value: parts };
130
+ }
131
+
132
+ // Font families (heuristic: starts with system-ui, ui-monospace, or quoted name)
133
+ if (FONT_FAMILY_RE.test(value) && value.includes(',')) {
134
+ return { type: 'fontFamily', value };
135
+ }
136
+
137
+ // Font weights / line heights / opacity / z-index (bare numbers)
138
+ if (NUMBER_RE.test(value)) {
139
+ if (name.includes('fw-') || name.includes('weight')) {
140
+ return { type: 'fontWeight', value: parseFloat(value) };
141
+ }
142
+ if (name.includes('lh-')) {
143
+ return { type: 'number', value: parseFloat(value) };
144
+ }
145
+ if (name.includes('opacity')) {
146
+ return { type: 'number', value: parseFloat(value) };
147
+ }
148
+ if (name.includes('-z-')) {
149
+ return { type: 'number', value: parseInt(value) };
150
+ }
151
+ return { type: 'number', value: parseFloat(value) };
152
+ }
153
+
154
+ // Letter spacing (e.g. '-0.025em')
155
+ if (/^-?[\d.]+em$/.test(value)) {
156
+ return { type: 'dimension', value };
157
+ }
158
+
159
+ // Line-height units like '75ch'
160
+ if (/^[\d.]+ch$/.test(value)) {
161
+ return { type: 'dimension', value };
162
+ }
163
+
164
+ // vh units
165
+ if (/^[\d.]+vh$/.test(value)) {
166
+ return { type: 'dimension', value };
167
+ }
168
+
169
+ // Everything else
170
+ return { type: 'string', value };
171
+ }
172
+
173
+ // ============================================================
174
+ // Value Parsers
175
+ // ============================================================
176
+
177
+ function rgbaToHex(r, g, b, a) {
178
+ const hex = '#' + [r, g, b].map(c =>
179
+ Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')
180
+ ).join('');
181
+ if (a != null && a < 1) {
182
+ return hex + Math.round(a * 255).toString(16).padStart(2, '0');
183
+ }
184
+ return hex;
185
+ }
186
+
187
+ /**
188
+ * Parse a CSS box-shadow string into DTCG shadow object(s).
189
+ * Handles comma-separated multiple shadows.
190
+ */
191
+ function parseShadow(value) {
192
+ // Split by comma but not within rgba()
193
+ const shadows = splitShadows(value);
194
+ if (shadows.length === 1) return parseSingleShadow(shadows[0]);
195
+ return shadows.map(parseSingleShadow);
196
+ }
197
+
198
+ function splitShadows(value) {
199
+ const result = [];
200
+ let depth = 0;
201
+ let current = '';
202
+ for (const ch of value) {
203
+ if (ch === '(') depth++;
204
+ else if (ch === ')') depth--;
205
+ else if (ch === ',' && depth === 0) {
206
+ result.push(current.trim());
207
+ current = '';
208
+ continue;
209
+ }
210
+ current += ch;
211
+ }
212
+ if (current.trim()) result.push(current.trim());
213
+ return result;
214
+ }
215
+
216
+ function parseSingleShadow(s) {
217
+ // Format: [inset] offsetX offsetY blur [spread] color
218
+ const inset = s.startsWith('inset');
219
+ if (inset) s = s.replace('inset', '').trim();
220
+
221
+ // Extract color — it's at the end, either hex or rgba()
222
+ let color = 'rgba(0,0,0,0.1)';
223
+ const rgbaIdx = s.indexOf('rgba(');
224
+ const rgbIdx = s.indexOf('rgb(');
225
+ if (rgbaIdx !== -1) {
226
+ color = s.slice(rgbaIdx);
227
+ s = s.slice(0, rgbaIdx).trim();
228
+ } else if (rgbIdx !== -1) {
229
+ color = s.slice(rgbIdx);
230
+ s = s.slice(0, rgbIdx).trim();
231
+ } else {
232
+ // Try hex at end
233
+ const parts = s.split(/\s+/);
234
+ if (parts.length > 0 && HEX_RE.test(parts[parts.length - 1])) {
235
+ color = parts.pop();
236
+ s = parts.join(' ');
237
+ }
238
+ }
239
+
240
+ const dims = s.split(/\s+/).filter(Boolean);
241
+ return {
242
+ offsetX: dims[0] || '0',
243
+ offsetY: dims[1] || '0',
244
+ blur: dims[2] || '0',
245
+ spread: dims[3] || '0',
246
+ color,
247
+ ...(inset ? { inset: true } : {}),
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Parse a CSS linear-gradient into DTCG gradient object.
253
+ */
254
+ function parseGradient(value) {
255
+ // Strip 'linear-gradient(' and trailing ')'
256
+ const inner = value.slice('linear-gradient('.length, -1);
257
+ const parts = splitShadows(inner); // reuse comma splitter
258
+
259
+ let angle = '180deg';
260
+ let stops = parts;
261
+
262
+ // First part might be angle
263
+ if (parts[0] && /^\d+deg$/.test(parts[0].trim())) {
264
+ angle = parts[0].trim();
265
+ stops = parts.slice(1);
266
+ }
267
+
268
+ return {
269
+ type: 'linear',
270
+ angle,
271
+ stops: stops.map(stop => {
272
+ const trimmed = stop.trim();
273
+ const lastSpace = trimmed.lastIndexOf(' ');
274
+ if (lastSpace === -1) return { color: trimmed };
275
+ const maybePos = trimmed.slice(lastSpace + 1);
276
+ if (PERCENT_RE.test(maybePos)) {
277
+ return { color: trimmed.slice(0, lastSpace), position: maybePos };
278
+ }
279
+ return { color: trimmed };
280
+ }),
281
+ };
282
+ }
283
+
284
+ // ============================================================
285
+ // Token Path Helpers
286
+ // ============================================================
287
+
288
+ /**
289
+ * Convert a CSS custom property suffix to a DTCG-style dot path.
290
+ * e.g. 'primary-fg' → 'color.primary-fg'
291
+ * 'sp-4' → 'spacing.sp-4'
292
+ * 'duration-fast' → 'motion.duration-fast'
293
+ */
294
+ function tokenNameToPath(suffix) {
295
+ // This is used for alias resolution — keep it simple
296
+ return suffix.replace(/-/g, '.');
297
+ }
298
+
299
+ /**
300
+ * Convert a full CSS custom property name to a DTCG group path.
301
+ * Groups tokens logically for the output JSON structure.
302
+ */
303
+ function tokenToGroup(name) {
304
+ // Strip --d- prefix
305
+ const key = name.replace(/^--d-/, '');
306
+
307
+ if (key.match(/^(primary|accent|tertiary|success|warning|error|info)(-|$)/)) return 'color';
308
+ if (key.match(/^(bg|fg|muted|border|ring|overlay|chrome)/)) return 'color';
309
+ if (key.match(/^surface-/)) return 'color';
310
+ if (key.match(/^(item-|selected-|disabled-|icon-)/)) return 'color';
311
+ if (key.match(/^(field-bg|field-border|field-ring|field-placeholder)/)) return 'color';
312
+ if (key.match(/^(table-|chart-(?!legend))/)) return 'color';
313
+ if (key.match(/^(text-helper|text-error)/)) return 'typography';
314
+ if (key.match(/^(selection-bg|selection-fg|selection-shadow)/)) return 'color';
315
+ if (key.match(/^(scrollbar-track|scrollbar-thumb|skeleton-bg|skeleton-shine)/)) return 'color';
316
+ if (key.match(/^(overlay-)/)) return 'color';
317
+ if (key.match(/^(gradient-)/)) return 'gradient';
318
+ if (key.match(/^(glass-blur)/)) return 'effect';
319
+ if (key.match(/^elevation-/)) return 'elevation';
320
+ if (key.match(/^(hover-|active-|focus-)/)) return 'interaction';
321
+ if (key.match(/^sp-/)) return 'spacing';
322
+ if (key.match(/^(pad|compound-|offset-|panel-|tree-|field-h|field-py|field-px|field-gap|field-text)/)) return 'spacing';
323
+ if (key.match(/^(switch-|checkbox-size|rate-|otp-|stepper-|avatar-|spinner-|progress-|slider-|badge-|carousel-|float-|backtop-|step-|timepicker-|datepicker-|colorpicker-|colorpalette-|timeline-|rangeslider-|slide-)/)) return 'spacing';
324
+ if (key.match(/^duration-/)) return 'motion';
325
+ if (key.match(/^easing-/)) return 'motion';
326
+ if (key.match(/^motion-/)) return 'motion';
327
+ if (key.match(/^radius/)) return 'radius';
328
+ if (key.match(/^(checkbox-radius)/)) return 'radius';
329
+ if (key.match(/^(field-radius)/)) return 'radius';
330
+ if (key.match(/^border-/)) return 'border';
331
+ if (key.match(/^(field-border-width)/)) return 'border';
332
+ if (key.match(/^density-/)) return 'density';
333
+ if (key.match(/^z-/)) return 'zIndex';
334
+ if (key.match(/^(font|text-|lh-|fw-|ls-|prose-)/)) return 'typography';
335
+ if (key.match(/^(content-width|sidebar-|drawer-)/)) return 'layout';
336
+ if (key.match(/^(shadow|transition)/)) return 'legacy';
337
+ if (key.match(/^(scrollbar-w)/)) return 'spacing';
338
+
339
+ return 'misc';
340
+ }
341
+
342
+ // ============================================================
343
+ // DTCG Output Builder
344
+ // ============================================================
345
+
346
+ /**
347
+ * Build a DTCG-formatted token file for a single style (both modes).
348
+ * @param {Object} style - Style definition
349
+ * @param {Function} deriveFn - The derive() function
350
+ * @param {Function} getShapeTokensFn - The getShapeTokens() function
351
+ * @returns {Object} DTCG JSON object
352
+ */
353
+ function buildDTCG(style, deriveFn, getShapeTokensFn) {
354
+ const lightTokens = deriveFn(
355
+ style.seed, style.personality, 'light',
356
+ style.typography, style.overrides?.light
357
+ );
358
+ const darkTokens = deriveFn(
359
+ style.seed, style.personality, 'dark',
360
+ style.typography, style.overrides?.dark
361
+ );
362
+
363
+ const output = {
364
+ $name: `Decantr — ${style.name}`,
365
+ $description: `Design tokens for the ${style.name} style, exported in W3C DTCG format.`,
366
+ };
367
+
368
+ // Collect all token names (union of both modes)
369
+ const allNames = new Set([...Object.keys(lightTokens), ...Object.keys(darkTokens)]);
370
+ const sorted = [...allNames].sort();
371
+
372
+ for (const name of sorted) {
373
+ const lightVal = lightTokens[name];
374
+ const darkVal = darkTokens[name];
375
+ const group = tokenToGroup(name);
376
+ const key = name.replace(/^--d-/, '');
377
+
378
+ if (!output[group]) output[group] = {};
379
+
380
+ const lightClass = lightVal != null ? classifyToken(name, lightVal) : null;
381
+ const darkClass = darkVal != null ? classifyToken(name, darkVal) : null;
382
+
383
+ const type = lightClass?.type || darkClass?.type || 'string';
384
+ const dtcgType = mapToDTCGType(type);
385
+
386
+ // If both modes have the same value, set $value directly
387
+ const sameValue = lightVal === darkVal;
388
+
389
+ const token = { $type: dtcgType };
390
+
391
+ if (sameValue && lightClass) {
392
+ token.$value = lightClass.value;
393
+ } else {
394
+ // Use $extensions.mode for per-mode values
395
+ token.$extensions = {
396
+ mode: {}
397
+ };
398
+ if (lightClass) token.$extensions.mode.light = lightClass.value;
399
+ if (darkClass) token.$extensions.mode.dark = darkClass.value;
400
+ }
401
+
402
+ output[group][key] = token;
403
+ }
404
+
405
+ return output;
406
+ }
407
+
408
+ /**
409
+ * Build a DTCG token file for shape tokens.
410
+ * @param {Function} getShapeTokensFn - The getShapeTokens() function
411
+ * @returns {Object} DTCG JSON object
412
+ */
413
+ function buildShapeDTCG(getShapeTokensFn) {
414
+ const shapes = ['sharp', 'rounded', 'pill'];
415
+ const output = {
416
+ $name: 'Decantr — Shape Tokens',
417
+ $description: 'Radius tokens for each shape preset (sharp, rounded, pill).',
418
+ radius: {},
419
+ };
420
+
421
+ // Collect all token names from all shapes
422
+ const allNames = new Set();
423
+ const shapeData = {};
424
+ for (const shape of shapes) {
425
+ const tokens = getShapeTokensFn(shape);
426
+ if (!tokens) continue;
427
+ shapeData[shape] = tokens;
428
+ for (const name of Object.keys(tokens)) allNames.add(name);
429
+ }
430
+
431
+ for (const name of [...allNames].sort()) {
432
+ const key = name.replace(/^--d-/, '');
433
+ const modes = {};
434
+ for (const shape of shapes) {
435
+ if (shapeData[shape]?.[name]) {
436
+ modes[shape] = shapeData[shape][name];
437
+ }
438
+ }
439
+
440
+ output.radius[key] = {
441
+ $type: 'dimension',
442
+ $extensions: { mode: modes },
443
+ };
444
+ }
445
+
446
+ return output;
447
+ }
448
+
449
+ /**
450
+ * Build Figma REST API payload format from DTCG tokens.
451
+ * @param {Object} dtcg - DTCG token object
452
+ * @param {string} styleId - Style identifier
453
+ * @returns {Object} Figma Variables payload
454
+ */
455
+ function buildFigmaPayload(dtcg, styleId) {
456
+ const variables = [];
457
+ const collection = {
458
+ name: dtcg.$name || `Decantr — ${styleId}`,
459
+ modes: ['Light', 'Dark'],
460
+ variables: [],
461
+ };
462
+
463
+ for (const [group, tokens] of Object.entries(dtcg)) {
464
+ if (group.startsWith('$')) continue;
465
+ if (typeof tokens !== 'object') continue;
466
+
467
+ for (const [key, token] of Object.entries(tokens)) {
468
+ if (!token.$type) continue;
469
+
470
+ const figmaType = dtcgToFigmaType(token.$type);
471
+ const variable = {
472
+ name: `${group}/${key}`,
473
+ resolvedType: figmaType,
474
+ valuesByMode: {},
475
+ };
476
+
477
+ if (token.$value != null) {
478
+ // Same value both modes
479
+ variable.valuesByMode.Light = toFigmaValue(token.$value, token.$type);
480
+ variable.valuesByMode.Dark = toFigmaValue(token.$value, token.$type);
481
+ } else if (token.$extensions?.mode) {
482
+ if (token.$extensions.mode.light != null) {
483
+ variable.valuesByMode.Light = toFigmaValue(token.$extensions.mode.light, token.$type);
484
+ }
485
+ if (token.$extensions.mode.dark != null) {
486
+ variable.valuesByMode.Dark = toFigmaValue(token.$extensions.mode.dark, token.$type);
487
+ }
488
+ }
489
+
490
+ collection.variables.push(variable);
491
+ }
492
+ }
493
+
494
+ return collection;
495
+ }
496
+
497
+ // ============================================================
498
+ // Type Mapping
499
+ // ============================================================
500
+
501
+ function mapToDTCGType(internalType) {
502
+ const map = {
503
+ color: 'color',
504
+ dimension: 'dimension',
505
+ shadow: 'shadow',
506
+ cubicBezier: 'cubicBezier',
507
+ duration: 'duration',
508
+ fontFamily: 'fontFamily',
509
+ fontWeight: 'fontWeight',
510
+ gradient: 'gradient',
511
+ number: 'number',
512
+ string: 'string',
513
+ alias: 'string', // aliases take the type of their target
514
+ };
515
+ return map[internalType] || 'string';
516
+ }
517
+
518
+ function dtcgToFigmaType(dtcgType) {
519
+ const map = {
520
+ color: 'COLOR',
521
+ dimension: 'FLOAT',
522
+ number: 'FLOAT',
523
+ fontWeight: 'FLOAT',
524
+ duration: 'FLOAT',
525
+ cubicBezier: 'STRING',
526
+ fontFamily: 'STRING',
527
+ shadow: 'STRING',
528
+ gradient: 'STRING',
529
+ string: 'STRING',
530
+ };
531
+ return map[dtcgType] || 'STRING';
532
+ }
533
+
534
+ /**
535
+ * Convert a DTCG token value to Figma-compatible value.
536
+ */
537
+ function toFigmaValue(value, type) {
538
+ if (type === 'color' && typeof value === 'string' && value.startsWith('#')) {
539
+ return hexToFigmaColor(value);
540
+ }
541
+ if (type === 'dimension' && typeof value === 'string') {
542
+ return parseDimensionToNumber(value);
543
+ }
544
+ if (type === 'number' || type === 'fontWeight') {
545
+ return typeof value === 'number' ? value : parseFloat(value);
546
+ }
547
+ if (type === 'duration' && typeof value === 'string') {
548
+ if (value.endsWith('ms')) return parseFloat(value);
549
+ if (value.endsWith('s')) return parseFloat(value) * 1000;
550
+ }
551
+ // Everything else → string
552
+ return typeof value === 'string' ? value : JSON.stringify(value);
553
+ }
554
+
555
+ function hexToFigmaColor(hex) {
556
+ hex = hex.replace('#', '');
557
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
558
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
559
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
560
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
561
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
562
+ return { r, g, b, a };
563
+ }
564
+
565
+ function parseDimensionToNumber(value) {
566
+ // Convert rem to px (base 16px)
567
+ if (value.endsWith('rem')) return parseFloat(value) * 16;
568
+ if (value.endsWith('px')) return parseFloat(value);
569
+ if (value.endsWith('em')) return parseFloat(value) * 16;
570
+ if (value.endsWith('%')) return parseFloat(value);
571
+ if (value.endsWith('ch')) return parseFloat(value) * 8; // approximate
572
+ if (value.endsWith('vh')) return parseFloat(value);
573
+ return parseFloat(value);
574
+ }
575
+
576
+ // ============================================================
577
+ // Combined Tokens (for Tokens Studio)
578
+ // ============================================================
579
+
580
+ function buildCombinedDTCG(allStyleDTCG, shapeDTCG) {
581
+ const combined = {
582
+ $name: 'Decantr — All Styles',
583
+ $description: 'Combined token file for all Decantr styles. For use with Tokens Studio.',
584
+ };
585
+
586
+ for (const { id, dtcg } of allStyleDTCG) {
587
+ combined[id] = dtcg;
588
+ }
589
+
590
+ combined.shapes = shapeDTCG;
591
+ return combined;
592
+ }
593
+
594
+ // ============================================================
595
+ // Public API
596
+ // ============================================================
597
+
598
+ /**
599
+ * Generate Figma token files in DTCG or Figma REST API format.
600
+ * @param {Object} opts
601
+ * @param {string} [opts.style='all'] - Style filter
602
+ * @param {string} [opts.format='dtcg'] - Output format: 'dtcg' or 'figma'
603
+ * @param {string} [opts.output] - Output directory (default: dist/figma/tokens/)
604
+ * @param {boolean} [opts.dryRun=false] - Print stats without writing
605
+ * @param {string} [opts.cwd] - Working directory (for output resolution)
606
+ * @returns {Promise<{ styles: string[], tokenCount: number, files: string[] }>}
607
+ */
608
+ export async function generateFigmaTokens(opts = {}) {
609
+ const {
610
+ style: styleFilter = 'all',
611
+ format = 'dtcg',
612
+ output: outputDir,
613
+ 'dry-run': dryRun = false,
614
+ dryRun: dryRunAlt = false,
615
+ cwd = process.cwd(),
616
+ } = opts;
617
+
618
+ const isDryRun = dryRun || dryRunAlt;
619
+ const outDir = outputDir ? resolve(cwd, outputDir) : join(cwd, 'dist', 'figma', 'tokens');
620
+
621
+ // Import derive engine
622
+ const { derive, getShapeTokens } = await import(
623
+ resolve(__dirname, '..', 'src', 'css', 'derive.js')
624
+ );
625
+
626
+ // Load styles
627
+ const styles = await loadStyles(styleFilter);
628
+ if (styles.length === 0) {
629
+ throw new Error(
630
+ styleFilter === 'all'
631
+ ? 'No style definitions found in src/css/styles/'
632
+ : `Style "${styleFilter}" not found`
633
+ );
634
+ }
635
+
636
+ const allStyleDTCG = [];
637
+ const files = [];
638
+ let totalTokenCount = 0;
639
+
640
+ for (const style of styles) {
641
+ const dtcg = buildDTCG(style, derive, getShapeTokens);
642
+
643
+ // Count tokens
644
+ let count = 0;
645
+ for (const [group, tokens] of Object.entries(dtcg)) {
646
+ if (group.startsWith('$')) continue;
647
+ if (typeof tokens === 'object') count += Object.keys(tokens).length;
648
+ }
649
+ totalTokenCount += count;
650
+
651
+ if (format === 'figma') {
652
+ const payload = buildFigmaPayload(dtcg, style.id);
653
+ allStyleDTCG.push({ id: style.id, dtcg, payload });
654
+ } else {
655
+ allStyleDTCG.push({ id: style.id, dtcg });
656
+ }
657
+ }
658
+
659
+ // Shape tokens
660
+ const { getShapeTokens: getShapeTokensFn } = await import(
661
+ resolve(__dirname, '..', 'src', 'css', 'derive.js')
662
+ );
663
+ const shapeDTCG = buildShapeDTCG(getShapeTokensFn);
664
+ let shapeCount = 0;
665
+ if (shapeDTCG.radius) shapeCount = Object.keys(shapeDTCG.radius).length;
666
+
667
+ if (isDryRun) {
668
+ return {
669
+ styles: styles.map(s => s.id),
670
+ tokenCount: totalTokenCount,
671
+ shapeTokenCount: shapeCount,
672
+ files: [
673
+ ...styles.map(s => `${s.id}.tokens.json`),
674
+ 'shapes.tokens.json',
675
+ 'combined.tokens.json',
676
+ ],
677
+ };
678
+ }
679
+
680
+ // Write files
681
+ await mkdir(outDir, { recursive: true });
682
+
683
+ for (const { id, dtcg, payload } of allStyleDTCG) {
684
+ if (format === 'figma') {
685
+ const filePath = join(outDir, `${id}.figma.json`);
686
+ await writeFile(filePath, JSON.stringify(payload, null, 2));
687
+ files.push(filePath);
688
+ }
689
+ // Always write DTCG
690
+ const dtcgPath = join(outDir, `${id}.tokens.json`);
691
+ await writeFile(dtcgPath, JSON.stringify(dtcg, null, 2));
692
+ files.push(dtcgPath);
693
+ }
694
+
695
+ // Write shape tokens
696
+ const shapePath = join(outDir, 'shapes.tokens.json');
697
+ await writeFile(shapePath, JSON.stringify(shapeDTCG, null, 2));
698
+ files.push(shapePath);
699
+
700
+ // Write combined file
701
+ const combined = buildCombinedDTCG(allStyleDTCG, shapeDTCG);
702
+ const combinedPath = join(outDir, 'combined.tokens.json');
703
+ await writeFile(combinedPath, JSON.stringify(combined, null, 2));
704
+ files.push(combinedPath);
705
+
706
+ return {
707
+ styles: styles.map(s => s.id),
708
+ tokenCount: totalTokenCount,
709
+ shapeTokenCount: shapeCount,
710
+ files,
711
+ };
712
+ }