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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- 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
|
+
}
|