@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.
- package/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- 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";
|