tokka 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compiler/generators/css.ts +146 -0
- package/compiler/generators/figma.ts +147 -0
- package/compiler/generators/tailwind.ts +106 -0
- package/compiler/generators/typescript.ts +113 -0
- package/compiler/index.ts +45 -0
- package/compiler/loader.ts +92 -0
- package/compiler/resolver.ts +177 -0
- package/compiler/types.ts +118 -0
- package/compiler/validator.ts +194 -0
- package/components/accordion.tsx +55 -0
- package/components/alert-dialog.tsx +138 -0
- package/components/alert.tsx +58 -0
- package/components/aspect-ratio.tsx +5 -0
- package/components/avatar.tsx +47 -0
- package/components/badge.tsx +35 -0
- package/components/breadcrumb.tsx +114 -0
- package/components/button.tsx +56 -0
- package/components/calendar.tsx +63 -0
- package/components/card.tsx +74 -0
- package/components/carousel.tsx +259 -0
- package/components/chart.tsx +9 -0
- package/components/checkbox.tsx +27 -0
- package/components/collapsible.tsx +9 -0
- package/components/combobox.tsx +8 -0
- package/components/command.tsx +152 -0
- package/components/context-menu.tsx +197 -0
- package/components/data-table.tsx +9 -0
- package/components/date-picker.tsx +8 -0
- package/components/dialog.tsx +119 -0
- package/components/drawer.tsx +115 -0
- package/components/dropdown-menu.tsx +197 -0
- package/components/form.tsx +175 -0
- package/components/hover-card.tsx +26 -0
- package/components/input-otp.tsx +68 -0
- package/components/input.tsx +48 -0
- package/components/label.tsx +23 -0
- package/components/lib/utils.ts +6 -0
- package/components/menubar.tsx +233 -0
- package/components/native-select.tsx +29 -0
- package/components/navigation-menu.tsx +127 -0
- package/components/pagination.tsx +116 -0
- package/components/popover.tsx +28 -0
- package/components/progress.tsx +25 -0
- package/components/radio-group.tsx +41 -0
- package/components/resizable.tsx +42 -0
- package/components/scroll-area.tsx +45 -0
- package/components/select.tsx +157 -0
- package/components/separator.tsx +28 -0
- package/components/sheet.tsx +137 -0
- package/components/sidebar.tsx +249 -0
- package/components/skeleton.tsx +15 -0
- package/components/slider.tsx +25 -0
- package/components/sonner.tsx +25 -0
- package/components/spinner.tsx +33 -0
- package/components/switch.tsx +26 -0
- package/components/table.tsx +116 -0
- package/components/tabs.tsx +52 -0
- package/components/textarea.tsx +23 -0
- package/components/toggle-group.tsx +58 -0
- package/components/toggle.tsx +42 -0
- package/components/tooltip.tsx +27 -0
- package/package.json +5 -3
- package/systems/README.md +56 -0
- package/systems/accessible/system.json +17 -0
- package/systems/accessible/tokens/primitives.json +9 -0
- package/systems/accessible/tokens/semantics.json +11 -0
- package/systems/brutalist/system.json +17 -0
- package/systems/brutalist/tokens/primitives.json +10 -0
- package/systems/brutalist/tokens/semantics.json +12 -0
- package/systems/corporate/system.json +20 -0
- package/systems/corporate/tokens/primitives.json +60 -0
- package/systems/corporate/tokens/semantics.json +68 -0
- package/systems/dark-mode/system.json +17 -0
- package/systems/dark-mode/tokens/primitives.json +10 -0
- package/systems/dark-mode/tokens/semantics.json +11 -0
- package/systems/package.json +14 -0
- package/systems/soft-saas/system.json +20 -0
- package/systems/soft-saas/tokens/primitives.json +235 -0
- package/systems/soft-saas/tokens/semantics.json +190 -0
- package/dist/index.js +0 -434
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { type ResolvedToken, type System } from "../types.js"
|
|
2
|
+
|
|
3
|
+
export interface CSSGeneratorOptions {
|
|
4
|
+
modeSelector?: {
|
|
5
|
+
strategy: "class" | "data-attribute"
|
|
6
|
+
selectors?: {
|
|
7
|
+
light: string
|
|
8
|
+
dark: string
|
|
9
|
+
[key: string]: string
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert token ID to CSS variable name
|
|
16
|
+
* surface.brand → --surface-brand
|
|
17
|
+
* button.primary.bg → --button-primary-bg
|
|
18
|
+
*/
|
|
19
|
+
export function tokenIdToCSSVar(id: string): string {
|
|
20
|
+
return `--${id.replace(/\./g, "-")}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse color value to HSL triplet format
|
|
25
|
+
* Supports: hsl(...), oklch(...), hex, rgb(...)
|
|
26
|
+
*/
|
|
27
|
+
export function parseColorToHSLTriplet(value: string): string {
|
|
28
|
+
// If already a triplet (e.g., "220 90% 56%"), return as-is
|
|
29
|
+
if (/^\d+\s+\d+%\s+\d+%$/.test(value.trim())) {
|
|
30
|
+
return value.trim()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// If it's hsl(...), extract the triplet
|
|
34
|
+
const hslMatch = value.match(/hsl\(([^)]+)\)/)
|
|
35
|
+
if (hslMatch) {
|
|
36
|
+
return hslMatch[1].trim()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If it's oklch(...), we'll need conversion (simplified for v1)
|
|
40
|
+
const oklchMatch = value.match(/oklch\(([^)]+)\)/)
|
|
41
|
+
if (oklchMatch) {
|
|
42
|
+
// For v1, just pass through - users should provide HSL
|
|
43
|
+
// In production, would convert OKLCH to HSL
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// For other formats, pass through as-is
|
|
48
|
+
return value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate CSS variables for a mode
|
|
53
|
+
*/
|
|
54
|
+
export function generateCSSForMode(
|
|
55
|
+
tokens: ResolvedToken[],
|
|
56
|
+
mode: string | "default",
|
|
57
|
+
options: CSSGeneratorOptions = {}
|
|
58
|
+
): string {
|
|
59
|
+
const lines: string[] = []
|
|
60
|
+
|
|
61
|
+
const selector =
|
|
62
|
+
mode === "default"
|
|
63
|
+
? ":root"
|
|
64
|
+
: options.modeSelector?.strategy === "data-attribute"
|
|
65
|
+
? options.modeSelector.selectors?.[mode] || `[data-theme="${mode}"]`
|
|
66
|
+
: options.modeSelector?.selectors?.[mode] || `.${mode}`
|
|
67
|
+
|
|
68
|
+
lines.push(`${selector} {`)
|
|
69
|
+
|
|
70
|
+
for (const token of tokens) {
|
|
71
|
+
const cssVar = tokenIdToCSSVar(token.id)
|
|
72
|
+
let value: string | undefined
|
|
73
|
+
|
|
74
|
+
// Get value for this mode
|
|
75
|
+
if (mode === "default") {
|
|
76
|
+
value = token.resolvedValue || token.value
|
|
77
|
+
} else if (token.resolvedModes?.[mode]) {
|
|
78
|
+
value = token.resolvedModes[mode]
|
|
79
|
+
} else if (token.modes?.[mode]) {
|
|
80
|
+
value = token.modes[mode]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (value === undefined) continue
|
|
84
|
+
|
|
85
|
+
// Convert colors to HSL triplet format
|
|
86
|
+
if (token.type === "color") {
|
|
87
|
+
const triplet = parseColorToHSLTriplet(String(value))
|
|
88
|
+
lines.push(` ${cssVar}: ${triplet};`)
|
|
89
|
+
} else {
|
|
90
|
+
lines.push(` ${cssVar}: ${value};`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lines.push("}")
|
|
95
|
+
|
|
96
|
+
return lines.join("\n")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate complete CSS output
|
|
101
|
+
*/
|
|
102
|
+
export function generateCSS(
|
|
103
|
+
tokens: ResolvedToken[],
|
|
104
|
+
system: System,
|
|
105
|
+
options: CSSGeneratorOptions = {}
|
|
106
|
+
): string {
|
|
107
|
+
const output: string[] = []
|
|
108
|
+
|
|
109
|
+
// Header comment
|
|
110
|
+
output.push("/**")
|
|
111
|
+
output.push(` * Design tokens for ${system.name}`)
|
|
112
|
+
output.push(" * Generated by figma-base - do not edit directly")
|
|
113
|
+
output.push(" */")
|
|
114
|
+
output.push("")
|
|
115
|
+
|
|
116
|
+
// Generate default (light) mode
|
|
117
|
+
const lightMode = system.modes.includes("light") ? "light" : system.modes[0]
|
|
118
|
+
output.push(generateCSSForMode(tokens, "default", options))
|
|
119
|
+
|
|
120
|
+
// Generate other modes
|
|
121
|
+
for (const mode of system.modes) {
|
|
122
|
+
if (mode !== lightMode) {
|
|
123
|
+
output.push("")
|
|
124
|
+
output.push(generateCSSForMode(tokens, mode, options))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return output.join("\n")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate CSS output files
|
|
133
|
+
*/
|
|
134
|
+
export interface CSSOutput {
|
|
135
|
+
"tokens.css": string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function generateCSSOutput(
|
|
139
|
+
tokens: ResolvedToken[],
|
|
140
|
+
system: System,
|
|
141
|
+
options: CSSGeneratorOptions = {}
|
|
142
|
+
): CSSOutput {
|
|
143
|
+
return {
|
|
144
|
+
"tokens.css": generateCSS(tokens, system, options),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type ResolvedToken, type System } from "../types.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format a color value for Tokens Studio compatibility
|
|
5
|
+
* Converts Tailwind-style HSL (e.g., "220 90% 56%") to proper hsl() format
|
|
6
|
+
*/
|
|
7
|
+
function formatColorValue(value: string): string {
|
|
8
|
+
// Check if it's an HSL value without the hsl() wrapper (Tailwind format)
|
|
9
|
+
if (/^\d+\s+\d+%\s+\d+%$/.test(value.trim())) {
|
|
10
|
+
return `hsl(${value})`
|
|
11
|
+
}
|
|
12
|
+
// Return as-is for hex, rgb(), hsl(), or other formats
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format a token value based on its type
|
|
18
|
+
*/
|
|
19
|
+
function formatTokenValue(token: ResolvedToken, value: string): string {
|
|
20
|
+
// Don't format references
|
|
21
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Format colors
|
|
26
|
+
if (token.type === "color") {
|
|
27
|
+
return formatColorValue(value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate Tokens Studio compatible format
|
|
35
|
+
*/
|
|
36
|
+
export function generateFigmaTokens(
|
|
37
|
+
tokens: ResolvedToken[],
|
|
38
|
+
_system: System
|
|
39
|
+
): Record<string, any> {
|
|
40
|
+
const output: Record<string, any> = {}
|
|
41
|
+
|
|
42
|
+
// Group tokens by source
|
|
43
|
+
const primitiveTokens = tokens.filter((t) => t.source === "primitive")
|
|
44
|
+
const semanticTokens = tokens.filter((t) => t.source === "semantic")
|
|
45
|
+
const componentTokens = tokens.filter((t) => t.source === "component")
|
|
46
|
+
|
|
47
|
+
// Build nested structure for primitives
|
|
48
|
+
const primitiveTree: Record<string, any> = {}
|
|
49
|
+
for (const token of primitiveTokens) {
|
|
50
|
+
const parts = token.id.split(".")
|
|
51
|
+
let current = primitiveTree
|
|
52
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
53
|
+
if (!current[parts[i]]) {
|
|
54
|
+
current[parts[i]] = {}
|
|
55
|
+
}
|
|
56
|
+
current = current[parts[i]]
|
|
57
|
+
}
|
|
58
|
+
const lastPart = parts[parts.length - 1]
|
|
59
|
+
const rawValue = token.resolvedValue || token.value
|
|
60
|
+
const formattedValue = formatTokenValue(token, rawValue)
|
|
61
|
+
|
|
62
|
+
current[lastPart] = {
|
|
63
|
+
value: formattedValue,
|
|
64
|
+
type: token.type,
|
|
65
|
+
description: token.description,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
output.global = primitiveTree
|
|
69
|
+
|
|
70
|
+
// Build nested structure for semantics
|
|
71
|
+
const semanticTree: Record<string, any> = {}
|
|
72
|
+
for (const token of semanticTokens) {
|
|
73
|
+
const parts = token.id.split(".")
|
|
74
|
+
let current = semanticTree
|
|
75
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
76
|
+
if (!current[parts[i]]) {
|
|
77
|
+
current[parts[i]] = {}
|
|
78
|
+
}
|
|
79
|
+
current = current[parts[i]]
|
|
80
|
+
}
|
|
81
|
+
const lastPart = parts[parts.length - 1]
|
|
82
|
+
|
|
83
|
+
// Use reference format if token has references
|
|
84
|
+
const rawValue =
|
|
85
|
+
token.references && token.references.length > 0
|
|
86
|
+
? `{${token.references[0]}}`
|
|
87
|
+
: token.resolvedValue || token.value
|
|
88
|
+
|
|
89
|
+
const formattedValue = formatTokenValue(token, rawValue)
|
|
90
|
+
|
|
91
|
+
current[lastPart] = {
|
|
92
|
+
value: formattedValue,
|
|
93
|
+
type: token.type,
|
|
94
|
+
description: token.description,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
output.semantic = semanticTree
|
|
98
|
+
|
|
99
|
+
// Build component tokens if any
|
|
100
|
+
if (componentTokens.length > 0) {
|
|
101
|
+
const componentTree: Record<string, any> = {}
|
|
102
|
+
for (const token of componentTokens) {
|
|
103
|
+
const parts = token.id.split(".")
|
|
104
|
+
let current = componentTree
|
|
105
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
106
|
+
if (!current[parts[i]]) {
|
|
107
|
+
current[parts[i]] = {}
|
|
108
|
+
}
|
|
109
|
+
current = current[parts[i]]
|
|
110
|
+
}
|
|
111
|
+
const lastPart = parts[parts.length - 1]
|
|
112
|
+
|
|
113
|
+
const rawValue =
|
|
114
|
+
token.references && token.references.length > 0
|
|
115
|
+
? `{${token.references[0]}}`
|
|
116
|
+
: token.resolvedValue || token.value
|
|
117
|
+
|
|
118
|
+
const formattedValue = formatTokenValue(token, rawValue)
|
|
119
|
+
|
|
120
|
+
current[lastPart] = {
|
|
121
|
+
value: formattedValue,
|
|
122
|
+
type: token.type,
|
|
123
|
+
description: token.description,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
output.component = componentTree
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return output
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate Figma token export file
|
|
134
|
+
*/
|
|
135
|
+
export interface FigmaTokenOutput {
|
|
136
|
+
"tokka.tokens.json": string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function generateFigmaTokenOutput(
|
|
140
|
+
tokens: ResolvedToken[],
|
|
141
|
+
system: System
|
|
142
|
+
): FigmaTokenOutput {
|
|
143
|
+
const tokensObject = generateFigmaTokens(tokens, system)
|
|
144
|
+
return {
|
|
145
|
+
"tokka.tokens.json": JSON.stringify(tokensObject, null, 2),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ResolvedToken, type System } from "../types.js"
|
|
2
|
+
import { tokenIdToCSSVar } from "./css.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Semantic token mapping to Tailwind color names
|
|
6
|
+
* Maps semantic tokens to Tailwind's expected color scheme
|
|
7
|
+
*/
|
|
8
|
+
const SEMANTIC_TO_TAILWIND_MAPPING: Record<string, string> = {
|
|
9
|
+
"surface.brand": "primary",
|
|
10
|
+
"text.on-brand": "primary-foreground",
|
|
11
|
+
"surface.secondary": "secondary",
|
|
12
|
+
"text.on-secondary": "secondary-foreground",
|
|
13
|
+
"surface.destructive": "destructive",
|
|
14
|
+
"text.on-destructive": "destructive-foreground",
|
|
15
|
+
"surface.default": "background",
|
|
16
|
+
"text.default": "foreground",
|
|
17
|
+
"surface.card": "card",
|
|
18
|
+
"text.on-card": "card-foreground",
|
|
19
|
+
"surface.popover": "popover",
|
|
20
|
+
"text.on-popover": "popover-foreground",
|
|
21
|
+
"surface.muted": "muted",
|
|
22
|
+
"text.muted": "muted-foreground",
|
|
23
|
+
"surface.accent": "accent",
|
|
24
|
+
"text.on-accent": "accent-foreground",
|
|
25
|
+
"border.default": "border",
|
|
26
|
+
"border.input": "input",
|
|
27
|
+
"focus.ring": "ring",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate Tailwind config mapping semantic tokens to CSS vars
|
|
32
|
+
*/
|
|
33
|
+
export function generateTailwindConfig(
|
|
34
|
+
tokens: ResolvedToken[],
|
|
35
|
+
_system: System
|
|
36
|
+
): Record<string, any> {
|
|
37
|
+
const colors: Record<string, string> = {}
|
|
38
|
+
const spacing: Record<string, string> = {}
|
|
39
|
+
const borderRadius: Record<string, string> = {}
|
|
40
|
+
const boxShadow: Record<string, string> = {}
|
|
41
|
+
|
|
42
|
+
for (const token of tokens) {
|
|
43
|
+
// Only map semantic tokens to Tailwind (not primitives)
|
|
44
|
+
if (token.source !== "semantic") continue
|
|
45
|
+
|
|
46
|
+
const cssVar = tokenIdToCSSVar(token.id)
|
|
47
|
+
|
|
48
|
+
// Map semantic tokens to Tailwind color names
|
|
49
|
+
if (token.type === "color") {
|
|
50
|
+
const tailwindName = SEMANTIC_TO_TAILWIND_MAPPING[token.id]
|
|
51
|
+
if (tailwindName) {
|
|
52
|
+
colors[tailwindName] = `hsl(var(${cssVar}))`
|
|
53
|
+
} else {
|
|
54
|
+
// Also include the semantic token by its own name
|
|
55
|
+
const name = token.id.replace(/\./g, "-")
|
|
56
|
+
colors[name] = `hsl(var(${cssVar}))`
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Map spacing tokens
|
|
61
|
+
if (token.type === "dimension" && token.id.startsWith("space.")) {
|
|
62
|
+
const name = token.id.replace("space.", "")
|
|
63
|
+
spacing[name] = `var(${cssVar})`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Map radius tokens
|
|
67
|
+
if (token.type === "radius" && token.id.startsWith("radius.")) {
|
|
68
|
+
const name = token.id.replace("radius.", "")
|
|
69
|
+
borderRadius[name] = `var(${cssVar})`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Map shadow tokens
|
|
73
|
+
if (token.type === "shadow" && token.id.startsWith("shadow.")) {
|
|
74
|
+
const name = token.id.replace("shadow.", "")
|
|
75
|
+
boxShadow[name] = `var(${cssVar})`
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
theme: {
|
|
81
|
+
extend: {
|
|
82
|
+
colors,
|
|
83
|
+
spacing,
|
|
84
|
+
borderRadius,
|
|
85
|
+
boxShadow,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Generate Tailwind config file content
|
|
93
|
+
*/
|
|
94
|
+
export function generateTailwindOutput(
|
|
95
|
+
tokens: ResolvedToken[],
|
|
96
|
+
system: System
|
|
97
|
+
): string {
|
|
98
|
+
const config = generateTailwindConfig(tokens, system)
|
|
99
|
+
|
|
100
|
+
return `/**
|
|
101
|
+
* Tailwind config for ${system.name}
|
|
102
|
+
* Generated by figma-base - do not edit directly
|
|
103
|
+
*/
|
|
104
|
+
export default ${JSON.stringify(config, null, 2)}
|
|
105
|
+
`
|
|
106
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { type ResolvedToken, type System } from "../types.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate TypeScript token map with full typing
|
|
5
|
+
*/
|
|
6
|
+
export function generateTypeScript(
|
|
7
|
+
tokens: ResolvedToken[],
|
|
8
|
+
system: System
|
|
9
|
+
): string {
|
|
10
|
+
const lines: string[] = []
|
|
11
|
+
|
|
12
|
+
// Header
|
|
13
|
+
lines.push("/**")
|
|
14
|
+
lines.push(` * Token type definitions for ${system.name}`)
|
|
15
|
+
lines.push(" * Generated by figma-base - do not edit directly")
|
|
16
|
+
lines.push(" */")
|
|
17
|
+
lines.push("")
|
|
18
|
+
|
|
19
|
+
// Token type enum
|
|
20
|
+
lines.push("export type TokenType =")
|
|
21
|
+
lines.push(' | "color"')
|
|
22
|
+
lines.push(' | "number"')
|
|
23
|
+
lines.push(' | "dimension"')
|
|
24
|
+
lines.push(' | "radius"')
|
|
25
|
+
lines.push(' | "shadow"')
|
|
26
|
+
lines.push(' | "typography"')
|
|
27
|
+
lines.push(' | "motion"')
|
|
28
|
+
lines.push(' | "opacity"')
|
|
29
|
+
lines.push(' | "zIndex"')
|
|
30
|
+
lines.push("")
|
|
31
|
+
|
|
32
|
+
// Token source enum
|
|
33
|
+
lines.push("export type TokenSource = \"primitive\" | \"semantic\" | \"component\"")
|
|
34
|
+
lines.push("")
|
|
35
|
+
|
|
36
|
+
// Token interface
|
|
37
|
+
lines.push("export interface Token {")
|
|
38
|
+
lines.push(" id: string")
|
|
39
|
+
lines.push(" type: TokenType")
|
|
40
|
+
lines.push(" source: TokenSource")
|
|
41
|
+
lines.push(" description: string")
|
|
42
|
+
lines.push(" value?: string")
|
|
43
|
+
lines.push(" modes?: Record<string, string>")
|
|
44
|
+
lines.push("}")
|
|
45
|
+
lines.push("")
|
|
46
|
+
|
|
47
|
+
// All token IDs union type
|
|
48
|
+
lines.push("export type TokenId =")
|
|
49
|
+
tokens.forEach((token, index) => {
|
|
50
|
+
const isLast = index === tokens.length - 1
|
|
51
|
+
lines.push(` | "${token.id}"${isLast ? "" : ""}`)
|
|
52
|
+
})
|
|
53
|
+
lines.push("")
|
|
54
|
+
|
|
55
|
+
// Token map object
|
|
56
|
+
lines.push("export const tokens: Record<TokenId, Token> = {")
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
lines.push(` "${token.id}": {`)
|
|
59
|
+
lines.push(` id: "${token.id}",`)
|
|
60
|
+
lines.push(` type: "${token.type}",`)
|
|
61
|
+
lines.push(` source: "${token.source}",`)
|
|
62
|
+
lines.push(` description: ${JSON.stringify(token.description)},`)
|
|
63
|
+
|
|
64
|
+
if (token.resolvedValue) {
|
|
65
|
+
lines.push(` value: ${JSON.stringify(token.resolvedValue)},`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (token.resolvedModes && Object.keys(token.resolvedModes).length > 0) {
|
|
69
|
+
lines.push(` modes: {`)
|
|
70
|
+
for (const [mode, value] of Object.entries(token.resolvedModes)) {
|
|
71
|
+
lines.push(` "${mode}": ${JSON.stringify(value)},`)
|
|
72
|
+
}
|
|
73
|
+
lines.push(` },`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lines.push(` },`)
|
|
77
|
+
}
|
|
78
|
+
lines.push("} as const")
|
|
79
|
+
lines.push("")
|
|
80
|
+
|
|
81
|
+
// Helper functions
|
|
82
|
+
lines.push("export function getToken(id: TokenId): Token | undefined {")
|
|
83
|
+
lines.push(" return tokens[id]")
|
|
84
|
+
lines.push("}")
|
|
85
|
+
lines.push("")
|
|
86
|
+
|
|
87
|
+
lines.push("export function getTokensByType(type: TokenType): Token[] {")
|
|
88
|
+
lines.push(" return Object.values(tokens).filter(t => t.type === type)")
|
|
89
|
+
lines.push("}")
|
|
90
|
+
lines.push("")
|
|
91
|
+
|
|
92
|
+
lines.push("export function getTokensBySource(source: TokenSource): Token[] {")
|
|
93
|
+
lines.push(" return Object.values(tokens).filter(t => t.source === source)")
|
|
94
|
+
lines.push("}")
|
|
95
|
+
|
|
96
|
+
return lines.join("\n")
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate TypeScript output
|
|
101
|
+
*/
|
|
102
|
+
export interface TypeScriptOutput {
|
|
103
|
+
"tokens.ts": string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function generateTypeScriptOutput(
|
|
107
|
+
tokens: ResolvedToken[],
|
|
108
|
+
system: System
|
|
109
|
+
): TypeScriptOutput {
|
|
110
|
+
return {
|
|
111
|
+
"tokens.ts": generateTypeScript(tokens, system),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export * from "./types.js"
|
|
2
|
+
export * from "./loader.js"
|
|
3
|
+
export * from "./validator.js"
|
|
4
|
+
export * from "./resolver.js"
|
|
5
|
+
export * from "./generators/css.js"
|
|
6
|
+
export * from "./generators/tailwind.js"
|
|
7
|
+
export * from "./generators/typescript.js"
|
|
8
|
+
export * from "./generators/figma.js"
|
|
9
|
+
|
|
10
|
+
import { loadTokens, loadSystem, type LoadOptions } from "./loader.js"
|
|
11
|
+
import { validateTokens } from "./validator.js"
|
|
12
|
+
import { buildDependencyGraph, resolveTokens } from "./resolver.js"
|
|
13
|
+
import { type CompilationResult } from "./types.js"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Main compilation function
|
|
17
|
+
*/
|
|
18
|
+
export async function compile(options: LoadOptions): Promise<CompilationResult> {
|
|
19
|
+
// Load tokens and system
|
|
20
|
+
const loadedTokens = await loadTokens(options)
|
|
21
|
+
const system = await loadSystem(options)
|
|
22
|
+
|
|
23
|
+
// Validate tokens
|
|
24
|
+
const validationErrors = validateTokens(loadedTokens.all, system.modes, {
|
|
25
|
+
strict: false, // Tier 0 default
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
if (validationErrors.length > 0) {
|
|
29
|
+
return {
|
|
30
|
+
tokens: [],
|
|
31
|
+
errors: validationErrors,
|
|
32
|
+
warnings: [],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build dependency graph and resolve
|
|
37
|
+
const graph = buildDependencyGraph(loadedTokens.all)
|
|
38
|
+
const { resolved, errors } = resolveTokens(loadedTokens.all, graph)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
tokens: resolved,
|
|
42
|
+
errors,
|
|
43
|
+
warnings: [],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { tokenFileSchema, systemSchema, type Token, type System } from "./types.js"
|
|
4
|
+
|
|
5
|
+
export interface LoadOptions {
|
|
6
|
+
cwd: string
|
|
7
|
+
tokensDir?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LoadedTokens {
|
|
11
|
+
primitives: Token[]
|
|
12
|
+
semantics: Token[]
|
|
13
|
+
components: Token[]
|
|
14
|
+
all: Token[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load token files from a project directory
|
|
19
|
+
*/
|
|
20
|
+
export async function loadTokens(options: LoadOptions): Promise<LoadedTokens> {
|
|
21
|
+
const tokensDir = options.tokensDir || path.join(options.cwd, "tokens")
|
|
22
|
+
|
|
23
|
+
// Load primitives (required)
|
|
24
|
+
const primitivesPath = path.join(tokensDir, "primitives.json")
|
|
25
|
+
const primitivesContent = await fs.readFile(primitivesPath, "utf-8")
|
|
26
|
+
const primitivesData = tokenFileSchema.parse(JSON.parse(primitivesContent))
|
|
27
|
+
const primitives = primitivesData.tokens
|
|
28
|
+
|
|
29
|
+
// Load semantics (required)
|
|
30
|
+
const semanticsPath = path.join(tokensDir, "semantics.json")
|
|
31
|
+
const semanticsContent = await fs.readFile(semanticsPath, "utf-8")
|
|
32
|
+
const semanticsData = tokenFileSchema.parse(JSON.parse(semanticsContent))
|
|
33
|
+
const semantics = semanticsData.tokens
|
|
34
|
+
|
|
35
|
+
// Load component tokens (optional - Tier 2)
|
|
36
|
+
const components: Token[] = []
|
|
37
|
+
const componentsDir = path.join(tokensDir, "components")
|
|
38
|
+
try {
|
|
39
|
+
const componentFiles = await fs.readdir(componentsDir)
|
|
40
|
+
for (const file of componentFiles) {
|
|
41
|
+
if (file.endsWith(".json")) {
|
|
42
|
+
const componentPath = path.join(componentsDir, file)
|
|
43
|
+
const componentContent = await fs.readFile(componentPath, "utf-8")
|
|
44
|
+
const componentData = tokenFileSchema.parse(JSON.parse(componentContent))
|
|
45
|
+
components.push(...componentData.tokens)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Components directory is optional
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
primitives,
|
|
54
|
+
semantics,
|
|
55
|
+
components,
|
|
56
|
+
all: [...primitives, ...semantics, ...components],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load system metadata
|
|
62
|
+
*/
|
|
63
|
+
export async function loadSystem(options: LoadOptions): Promise<System> {
|
|
64
|
+
const systemPath = path.join(options.cwd, "system.json")
|
|
65
|
+
const systemContent = await fs.readFile(systemPath, "utf-8")
|
|
66
|
+
const system = systemSchema.parse(JSON.parse(systemContent))
|
|
67
|
+
return system
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if tokens directory exists
|
|
72
|
+
*/
|
|
73
|
+
export async function hasTokens(cwd: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
await fs.access(path.join(cwd, "tokens"))
|
|
76
|
+
return true
|
|
77
|
+
} catch {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if system.json exists
|
|
84
|
+
*/
|
|
85
|
+
export async function hasSystem(cwd: string): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
await fs.access(path.join(cwd, "system.json"))
|
|
88
|
+
return true
|
|
89
|
+
} catch {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
}
|