@usefragments/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,293 @@
1
+ import type {
2
+ FragmentDefinition,
3
+ FragmentDefinitionV2,
4
+ CompiledFragment,
5
+ FragmentComponent,
6
+ BlockDefinition,
7
+ CompiledBlock,
8
+ AIMetadata,
9
+ FragmentGenerated,
10
+ } from "./types.js";
11
+ import type {
12
+ GovernedFragmentDefinition,
13
+ ResolvedGovernedFragmentDefinition,
14
+ } from "./governance.js";
15
+ import {
16
+ fragmentDefinitionSchema,
17
+ fragmentDefinitionV2Schema,
18
+ governedFragmentDefinitionSchema,
19
+ blockDefinitionSchema,
20
+ } from "./schema.js";
21
+ import { resolveComponentGovernance } from "./governance.js";
22
+
23
+ function isGovernedDefinition<T>(
24
+ def: FragmentDefinition<T> | FragmentDefinitionV2<T> | GovernedFragmentDefinition<T>
25
+ ): def is GovernedFragmentDefinition<T> {
26
+ return "govern" in def || "governance" in def;
27
+ }
28
+
29
+ /**
30
+ * Check if a definition uses v2 field names.
31
+ * Detects `guidance` or `examples` (v2) vs `usage` or `variants` (v1).
32
+ */
33
+ function isV2Definition<T>(
34
+ def: FragmentDefinition<T> | FragmentDefinitionV2<T> | GovernedFragmentDefinition<T>
35
+ ): def is FragmentDefinitionV2<T> {
36
+ return !isGovernedDefinition(def) && ("guidance" in def || "examples" in def);
37
+ }
38
+
39
+ /**
40
+ * Normalize a v2 definition to v1 shape for downstream compatibility.
41
+ * The build pipeline, compiler, and validators all work with v1 internally.
42
+ */
43
+ export function normalizeToV1<T>(def: FragmentDefinitionV2<T>): FragmentDefinition<T> {
44
+ // Map composition → ai (rename fields)
45
+ let ai: AIMetadata | undefined;
46
+ if (def.composition) {
47
+ ai = {
48
+ compositionPattern: def.composition.pattern,
49
+ subComponents: def.composition.subComponents,
50
+ requiredChildren: def.composition.requiredChildren,
51
+ commonPatterns: def.composition.commonPatterns,
52
+ };
53
+ }
54
+
55
+ // Map _provenance → _generated (narrow source type, drop extended fields)
56
+ let generated: FragmentGenerated | undefined;
57
+ if (def._provenance) {
58
+ generated = {
59
+ source: def._provenance.source === "scan" ? "ai" : def._provenance.source,
60
+ sourceFile: def._provenance.sourceFile,
61
+ confidence: def._provenance.confidence,
62
+ timestamp: def._provenance.timestamp,
63
+ };
64
+ }
65
+
66
+ return {
67
+ component: def.component,
68
+ meta: def.meta,
69
+ usage: def.guidance,
70
+ props: def.props,
71
+ relations: def.relations,
72
+ variants: def.examples,
73
+ contract: def.contract,
74
+ ai,
75
+ _generated: generated,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Define a fragment for a component.
81
+ *
82
+ * This is the main API for creating fragment documentation.
83
+ * It provides runtime validation and type safety.
84
+ *
85
+ * Accepts both v1 and v2 shapes:
86
+ * - v1: `{ usage, variants, ai, _generated }`
87
+ * - v2: `{ guidance, examples, composition, _provenance }`
88
+ *
89
+ * @example v1
90
+ * ```tsx
91
+ * import { defineFragment } from '@usefragments/core';
92
+ * import { Button } from './Button';
93
+ *
94
+ * export default defineFragment({
95
+ * component: Button,
96
+ * meta: {
97
+ * name: 'Button',
98
+ * description: 'Primary action trigger',
99
+ * category: 'actions',
100
+ * },
101
+ * usage: {
102
+ * when: ['User needs to trigger an action'],
103
+ * whenNot: ['Navigation without side effects'],
104
+ * },
105
+ * props: {
106
+ * variant: {
107
+ * type: 'enum',
108
+ * values: ['primary', 'secondary'],
109
+ * default: 'primary',
110
+ * description: 'Visual style',
111
+ * },
112
+ * },
113
+ * variants: [
114
+ * {
115
+ * name: 'Default',
116
+ * description: 'Default button',
117
+ * render: () => <Button>Click me</Button>,
118
+ * },
119
+ * ],
120
+ * });
121
+ * ```
122
+ *
123
+ * @example v2
124
+ * ```tsx
125
+ * import { defineFragment } from '@usefragments/core';
126
+ * import { Card } from './Card';
127
+ *
128
+ * export default defineFragment({
129
+ * component: Card,
130
+ * meta: { name: 'Card', description: 'Content container', category: 'layout' },
131
+ * guidance: {
132
+ * when: ['Grouping related content'],
133
+ * whenNot: ['Full-page layouts'],
134
+ * },
135
+ * props: { ... },
136
+ * examples: [{ name: 'Default', description: 'Basic card', render: () => <Card>...</Card> }],
137
+ * composition: { pattern: 'compound', subComponents: ['Header', 'Body', 'Footer'] },
138
+ * });
139
+ * ```
140
+ */
141
+ export function defineFragment<TProps>(
142
+ definition: GovernedFragmentDefinition<TProps>
143
+ ): ResolvedGovernedFragmentDefinition<TProps>;
144
+ export function defineFragment<TProps>(
145
+ definition: FragmentDefinition<TProps>
146
+ ): FragmentDefinition<TProps>;
147
+ export function defineFragment<TProps>(
148
+ definition: FragmentDefinitionV2<TProps>
149
+ ): FragmentDefinitionV2<TProps>;
150
+ export function defineFragment<TProps>(
151
+ definition:
152
+ | FragmentDefinition<TProps>
153
+ | FragmentDefinitionV2<TProps>
154
+ | GovernedFragmentDefinition<TProps>
155
+ ):
156
+ | FragmentDefinition<TProps>
157
+ | FragmentDefinitionV2<TProps>
158
+ | ResolvedGovernedFragmentDefinition<TProps> {
159
+ // Validate at runtime in development
160
+ if (process.env.NODE_ENV !== "production") {
161
+ const governed = isGovernedDefinition(definition);
162
+ const v2 = isV2Definition(definition);
163
+ const schema = governed
164
+ ? governedFragmentDefinitionSchema
165
+ : v2
166
+ ? fragmentDefinitionV2Schema
167
+ : fragmentDefinitionSchema;
168
+ const result = schema.safeParse(definition);
169
+ if (!result.success) {
170
+ const name = definition.meta?.name || "unknown";
171
+ const errors = result.error.errors
172
+ .map((e) => ` - ${e.path.join(".")}: ${e.message}`)
173
+ .join("\n");
174
+ throw new Error(`Invalid fragment definition for "${name}":\n${errors}`);
175
+ }
176
+ }
177
+
178
+ if (isGovernedDefinition(definition)) {
179
+ const governance = resolveComponentGovernance(definition);
180
+ return {
181
+ ...definition,
182
+ governance,
183
+ };
184
+ }
185
+
186
+ return definition;
187
+ }
188
+
189
+ /**
190
+ * Compile a fragment definition to JSON-serializable format.
191
+ * Used for generating fragments.json for AI consumption.
192
+ *
193
+ * Accepts both v1 and v2 shapes. V2 is normalized to v1 internally.
194
+ */
195
+ export function compileFragment(
196
+ definition: FragmentDefinition | FragmentDefinitionV2 | GovernedFragmentDefinition,
197
+ filePath: string
198
+ ): CompiledFragment {
199
+ // Normalize v2 → v1 if needed
200
+ const v1 = isGovernedDefinition(definition)
201
+ ? {
202
+ component: definition.component,
203
+ meta: definition.meta,
204
+ usage: definition.guidance,
205
+ props: definition.props ?? {},
206
+ relations: definition.relations,
207
+ variants: definition.examples ?? [],
208
+ contract: definition.contract,
209
+ _generated: definition._provenance
210
+ ? {
211
+ source:
212
+ definition._provenance.source === "scan" ? "ai" : definition._provenance.source,
213
+ sourceFile: definition._provenance.sourceFile,
214
+ confidence: definition._provenance.confidence,
215
+ timestamp: definition._provenance.timestamp,
216
+ }
217
+ : undefined,
218
+ }
219
+ : isV2Definition(definition)
220
+ ? normalizeToV1(definition)
221
+ : definition;
222
+
223
+ return {
224
+ filePath,
225
+ meta: v1.meta,
226
+ ...(isGovernedDefinition(definition) && { guidance: definition.guidance }),
227
+ usage: v1.usage,
228
+ props: v1.props,
229
+ ...(isGovernedDefinition(definition) && {
230
+ governance: resolveComponentGovernance(definition),
231
+ }),
232
+ relations: v1.relations,
233
+ variants: v1.variants.map((v) => ({
234
+ name: v.name,
235
+ description: v.description,
236
+ code: v.code,
237
+ figma: v.figma,
238
+ })),
239
+ contract: v1.contract,
240
+ ai: v1.ai,
241
+ _generated: v1._generated,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Define a composition block.
247
+ *
248
+ * Blocks are pure data describing how design system components
249
+ * wire together for common use cases.
250
+ */
251
+ export function defineBlock(definition: BlockDefinition): BlockDefinition {
252
+ if (process.env.NODE_ENV !== "production") {
253
+ const result = blockDefinitionSchema.safeParse(definition);
254
+ if (!result.success) {
255
+ const errors = result.error.errors
256
+ .map((e) => ` - ${e.path.join(".")}: ${e.message}`)
257
+ .join("\n");
258
+ throw new Error(`Invalid block definition for "${definition.name || "unknown"}":\n${errors}`);
259
+ }
260
+ }
261
+
262
+ return definition;
263
+ }
264
+
265
+ /**
266
+ * @deprecated Use defineBlock instead
267
+ */
268
+ export const defineRecipe = defineBlock;
269
+
270
+ /**
271
+ * Compile a block definition to JSON-serializable format.
272
+ */
273
+ export function compileBlock(definition: BlockDefinition, filePath: string): CompiledBlock {
274
+ return {
275
+ filePath,
276
+ name: definition.name,
277
+ description: definition.description,
278
+ category: definition.category,
279
+ components: definition.components,
280
+ code: definition.code,
281
+ tags: definition.tags,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * @deprecated Use compileBlock instead
287
+ */
288
+ export const compileRecipe = compileBlock;
289
+
290
+ /**
291
+ * Type helper for extracting props type from a component
292
+ */
293
+ export type InferProps<T> = T extends FragmentComponent<infer P> ? P : never;
@@ -0,0 +1,324 @@
1
+ /**
2
+ * DTCG Output Generators — generate CSS, SCSS, Tailwind, and Figma output
3
+ * from a DTCG token source file.
4
+ *
5
+ * Makes DTCG the single source of truth with everything else as derived output.
6
+ */
7
+
8
+ import type { DTCGTokenFile } from './dtcg.js';
9
+ import { parseColor, parseDtcgTokens } from './tokens/index.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Internal: parse a DTCG file to flat tokens
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function parseToFlatTokens(
16
+ tokens: DTCGTokenFile,
17
+ prefix?: string,
18
+ ): Array<{ cssName: string; cssValue: string; category: string; description?: string }> {
19
+ // Use parseDTCGFile to get consistent output
20
+ const parsed = parseDtcgTokens(JSON.stringify(tokens), 'tokens.tokens.json');
21
+ const effectivePrefix = prefix ? `--${prefix.replace(/^--/, '').replace(/-$/, '')}-` : parsed.prefix;
22
+
23
+ const result: Array<{ cssName: string; cssValue: string; category: string; description?: string }> = [];
24
+
25
+ for (const [category, categoryTokens] of Object.entries(parsed.categories)) {
26
+ for (const token of categoryTokens) {
27
+ // Re-prefix if a custom prefix was requested
28
+ let cssName = token.name;
29
+ if (prefix && token.name.startsWith(parsed.prefix)) {
30
+ cssName = effectivePrefix + token.name.slice(parsed.prefix.length);
31
+ }
32
+
33
+ result.push({
34
+ cssName,
35
+ cssValue: token.value ?? '',
36
+ category,
37
+ description: token.description,
38
+ });
39
+ }
40
+ }
41
+
42
+ return result;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // CSS Custom Properties Generator
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export interface CSSGeneratorOptions {
50
+ /** Token name prefix (e.g., 'ds' → '--ds-*') */
51
+ prefix?: string;
52
+ /** CSS selector to wrap variables in (default: ':root') */
53
+ selector?: string;
54
+ }
55
+
56
+ /**
57
+ * Generate CSS custom properties from a DTCG token file.
58
+ */
59
+ export function generateCSSCustomProperties(
60
+ tokens: DTCGTokenFile,
61
+ options?: CSSGeneratorOptions,
62
+ ): string {
63
+ const selector = options?.selector ?? ':root';
64
+ const flatTokens = parseToFlatTokens(tokens, options?.prefix);
65
+
66
+ const lines: string[] = [];
67
+ lines.push(`${selector} {`);
68
+
69
+ // Group by category for readability
70
+ const grouped = new Map<string, typeof flatTokens>();
71
+ for (const token of flatTokens) {
72
+ const group = grouped.get(token.category) ?? [];
73
+ group.push(token);
74
+ grouped.set(token.category, group);
75
+ }
76
+
77
+ for (const [category, categoryTokens] of grouped) {
78
+ lines.push(` /* ${category} */`);
79
+ for (const token of categoryTokens) {
80
+ if (token.description) {
81
+ lines.push(` /* ${token.description} */`);
82
+ }
83
+ lines.push(` ${token.cssName}: ${token.cssValue};`);
84
+ }
85
+ lines.push('');
86
+ }
87
+
88
+ lines.push('}');
89
+ return lines.join('\n');
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // SCSS Variables Generator
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export interface SCSSGeneratorOptions {
97
+ /** Token name prefix (e.g., 'ds' → '$ds-*') */
98
+ prefix?: string;
99
+ }
100
+
101
+ /**
102
+ * Generate SCSS variables from a DTCG token file.
103
+ */
104
+ export function generateSCSSVariables(
105
+ tokens: DTCGTokenFile,
106
+ options?: SCSSGeneratorOptions,
107
+ ): string {
108
+ const flatTokens = parseToFlatTokens(tokens, options?.prefix);
109
+
110
+ const lines: string[] = [];
111
+ lines.push('// Auto-generated from DTCG token file');
112
+ lines.push('// Do not edit directly — modify the .tokens.json source');
113
+ lines.push('');
114
+
115
+ // Group by category for readability
116
+ const grouped = new Map<string, typeof flatTokens>();
117
+ for (const token of flatTokens) {
118
+ const group = grouped.get(token.category) ?? [];
119
+ group.push(token);
120
+ grouped.set(token.category, group);
121
+ }
122
+
123
+ for (const [category, categoryTokens] of grouped) {
124
+ lines.push(`// ${category}`);
125
+ for (const token of categoryTokens) {
126
+ // Convert CSS variable name to SCSS variable name
127
+ const scssName = token.cssName.replace(/^--/, '$');
128
+ lines.push(`${scssName}: ${token.cssValue} !default;`);
129
+ }
130
+ lines.push('');
131
+ }
132
+
133
+ return lines.join('\n');
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Tailwind Config Generator
138
+ // ---------------------------------------------------------------------------
139
+
140
+ /**
141
+ * Generate a Tailwind CSS configuration object from a DTCG token file.
142
+ * Compatible with Tailwind v3 and v4.
143
+ */
144
+ export function generateTailwindConfig(
145
+ tokens: DTCGTokenFile,
146
+ ): Record<string, unknown> {
147
+ const flatTokens = parseToFlatTokens(tokens);
148
+
149
+ const theme: Record<string, Record<string, string>> = {};
150
+
151
+ for (const token of flatTokens) {
152
+ const tailwindKey = categoryToTailwindKey(token.category);
153
+ if (!tailwindKey) continue;
154
+
155
+ if (!theme[tailwindKey]) {
156
+ theme[tailwindKey] = {};
157
+ }
158
+
159
+ // Convert CSS name to Tailwind token key
160
+ // e.g., "--ds-color-brand-primary" → "brand-primary"
161
+ const parts = token.cssName.replace(/^--[\w]+-/, '').split('-');
162
+ // Remove the category prefix if it matches
163
+ const tokenKey = parts.join('-') || token.cssName;
164
+
165
+ theme[tailwindKey][tokenKey] = `var(${token.cssName})`;
166
+ }
167
+
168
+ return {
169
+ theme: {
170
+ extend: theme,
171
+ },
172
+ };
173
+ }
174
+
175
+ function categoryToTailwindKey(category: string): string | undefined {
176
+ switch (category) {
177
+ case 'colors':
178
+ case 'surfaces':
179
+ case 'text':
180
+ return 'colors';
181
+ case 'spacing':
182
+ return 'spacing';
183
+ case 'radius':
184
+ return 'borderRadius';
185
+ case 'typography':
186
+ return 'fontFamily';
187
+ case 'shadows':
188
+ return 'boxShadow';
189
+ case 'borders':
190
+ return 'borderWidth';
191
+ case 'transitions':
192
+ return 'transitionDuration';
193
+ case 'z-index':
194
+ return 'zIndex';
195
+ default:
196
+ return undefined;
197
+ }
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Figma Variables Generator
202
+ // ---------------------------------------------------------------------------
203
+
204
+ export interface FigmaVariable {
205
+ name: string;
206
+ type: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN';
207
+ value: unknown;
208
+ description?: string;
209
+ scopes?: string[];
210
+ }
211
+
212
+ export interface FigmaVariableCollection {
213
+ name: string;
214
+ modes: Array<{ name: string; modeId: string }>;
215
+ variables: FigmaVariable[];
216
+ }
217
+
218
+ /**
219
+ * Generate Figma Variables REST API-compatible payload from a DTCG token file.
220
+ */
221
+ export function generateFigmaVariables(
222
+ tokens: DTCGTokenFile,
223
+ ): FigmaVariableCollection[] {
224
+ const flatTokens = parseToFlatTokens(tokens);
225
+
226
+ // Group by category into collections
227
+ const collections = new Map<string, FigmaVariable[]>();
228
+
229
+ for (const token of flatTokens) {
230
+ const collectionName = categoryToFigmaCollection(token.category);
231
+ const vars = collections.get(collectionName) ?? [];
232
+
233
+ vars.push({
234
+ name: token.cssName.replace(/^--/, '').replace(/-/g, '/'),
235
+ type: categoryToFigmaType(token.category),
236
+ value: parseFigmaValue(token.cssValue, token.category),
237
+ description: token.description,
238
+ scopes: categoryToFigmaScopes(token.category),
239
+ });
240
+
241
+ collections.set(collectionName, vars);
242
+ }
243
+
244
+ return Array.from(collections.entries()).map(([name, variables]) => ({
245
+ name,
246
+ modes: [{ name: 'Default', modeId: 'default' }],
247
+ variables,
248
+ }));
249
+ }
250
+
251
+ function categoryToFigmaCollection(category: string): string {
252
+ switch (category) {
253
+ case 'colors':
254
+ case 'surfaces':
255
+ case 'text':
256
+ return 'Colors';
257
+ case 'spacing':
258
+ case 'radius':
259
+ return 'Dimensions';
260
+ case 'typography':
261
+ return 'Typography';
262
+ case 'shadows':
263
+ return 'Effects';
264
+ default:
265
+ return 'Other';
266
+ }
267
+ }
268
+
269
+ function categoryToFigmaType(category: string): 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN' {
270
+ switch (category) {
271
+ case 'colors':
272
+ case 'surfaces':
273
+ case 'text':
274
+ return 'COLOR';
275
+ case 'spacing':
276
+ case 'radius':
277
+ case 'z-index':
278
+ return 'FLOAT';
279
+ default:
280
+ return 'STRING';
281
+ }
282
+ }
283
+
284
+ function categoryToFigmaScopes(category: string): string[] {
285
+ switch (category) {
286
+ case 'colors':
287
+ return ['ALL_FILLS', 'STROKE_COLOR'];
288
+ case 'surfaces':
289
+ return ['FRAME_FILL', 'SHAPE_FILL'];
290
+ case 'text':
291
+ return ['TEXT_FILL'];
292
+ case 'spacing':
293
+ return ['GAP', 'WIDTH_HEIGHT'];
294
+ case 'radius':
295
+ return ['CORNER_RADIUS'];
296
+ default:
297
+ return ['ALL_SCOPES'];
298
+ }
299
+ }
300
+
301
+ function parseFigmaValue(cssValue: string, category: string): unknown {
302
+ // For colors, try to parse hex to Figma RGBA object
303
+ if (category === 'colors' || category === 'surfaces' || category === 'text') {
304
+ const color = parseColor(cssValue);
305
+ if (color) {
306
+ return {
307
+ r: color.r / 255,
308
+ g: color.g / 255,
309
+ b: color.b / 255,
310
+ a: color.a ?? 1,
311
+ };
312
+ }
313
+ }
314
+
315
+ // For dimensions, extract numeric value
316
+ if (category === 'spacing' || category === 'radius') {
317
+ const numMatch = cssValue.match(/^(\d+(?:\.\d+)?)/);
318
+ if (numMatch) {
319
+ return parseFloat(numMatch[1]);
320
+ }
321
+ }
322
+
323
+ return cssValue;
324
+ }
@@ -0,0 +1 @@
1
+ export { isDTCGFile, parseDtcgTokens as parseDTCGFile } from "./tokens/index.js";