@zseven-w/pen-codegen 0.0.1
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/README.md +55 -0
- package/package.json +25 -0
- package/src/__tests__/codegen.test.ts +89 -0
- package/src/compose-generator.ts +807 -0
- package/src/css-variables-generator.ts +138 -0
- package/src/flutter-generator.ts +581 -0
- package/src/html-generator.ts +403 -0
- package/src/index.ts +26 -0
- package/src/react-generator.ts +401 -0
- package/src/react-native-generator.ts +569 -0
- package/src/svelte-generator.ts +296 -0
- package/src/swiftui-generator.ts +754 -0
- package/src/vue-generator.ts +308 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates CSS custom properties from PenDocument variables.
|
|
3
|
+
*
|
|
4
|
+
* Produces `:root { ... }` blocks with one block per theme variant.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PenDocument } from '@zseven-w/pen-types'
|
|
8
|
+
import type { VariableDefinition, ThemedValue } from '@zseven-w/pen-types'
|
|
9
|
+
|
|
10
|
+
/** Sanitise a variable name into a valid CSS custom property name. */
|
|
11
|
+
export function variableNameToCSS(name: string): string {
|
|
12
|
+
const sanitised = name
|
|
13
|
+
.replace(/^\$/, '')
|
|
14
|
+
.replace(/\s+/g, '-')
|
|
15
|
+
.replace(/[^a-zA-Z0-9_-]/g, '')
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
return `--${sanitised}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Whether a numeric variable should be output without a unit (e.g. opacity). */
|
|
21
|
+
function isUnitless(name: string): boolean {
|
|
22
|
+
const lower = name.toLowerCase()
|
|
23
|
+
return (
|
|
24
|
+
lower.includes('opacity') ||
|
|
25
|
+
lower.includes('weight') ||
|
|
26
|
+
lower.includes('scale') ||
|
|
27
|
+
lower.includes('ratio') ||
|
|
28
|
+
lower.includes('z-index') ||
|
|
29
|
+
lower.includes('line-height')
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Format a variable value as a CSS value string. */
|
|
34
|
+
function formatValue(
|
|
35
|
+
value: string | number | boolean,
|
|
36
|
+
name: string,
|
|
37
|
+
type: VariableDefinition['type'],
|
|
38
|
+
): string | null {
|
|
39
|
+
if (type === 'boolean') return null
|
|
40
|
+
if (type === 'color') return String(value)
|
|
41
|
+
if (type === 'number') {
|
|
42
|
+
if (typeof value !== 'number') return String(value)
|
|
43
|
+
return isUnitless(name) ? String(value) : `${value}px`
|
|
44
|
+
}
|
|
45
|
+
// string
|
|
46
|
+
return String(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Resolve a single themed value for a given theme context. */
|
|
50
|
+
function resolveForTheme(
|
|
51
|
+
def: VariableDefinition,
|
|
52
|
+
theme: Record<string, string>,
|
|
53
|
+
): string | number | boolean | undefined {
|
|
54
|
+
const val = def.value
|
|
55
|
+
if (!Array.isArray(val)) return val
|
|
56
|
+
const match = (val as ThemedValue[]).find((v) => {
|
|
57
|
+
if (!v.theme) return false
|
|
58
|
+
return Object.entries(theme).every(
|
|
59
|
+
([key, expected]) => v.theme?.[key] === expected,
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
return match?.value ?? (val as ThemedValue[])[0]?.value
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate CSS custom properties from a PenDocument's variables and themes.
|
|
67
|
+
*
|
|
68
|
+
* Returns a string containing `:root { ... }` blocks.
|
|
69
|
+
*/
|
|
70
|
+
export function generateCSSVariables(doc: PenDocument): string {
|
|
71
|
+
const variables = doc.variables
|
|
72
|
+
if (!variables || Object.keys(variables).length === 0) {
|
|
73
|
+
return '/* No design variables defined */\n'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const themes = doc.themes ?? {}
|
|
77
|
+
const themeAxes = Object.entries(themes)
|
|
78
|
+
|
|
79
|
+
// Build default theme (first value per axis)
|
|
80
|
+
const defaultTheme: Record<string, string> = {}
|
|
81
|
+
for (const [key, values] of themeAxes) {
|
|
82
|
+
if (values.length > 0) defaultTheme[key] = values[0]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hasThemes = themeAxes.length > 0 && themeAxes.some(([, v]) => v.length > 1)
|
|
86
|
+
|
|
87
|
+
// Generate default :root block
|
|
88
|
+
const lines: string[] = []
|
|
89
|
+
lines.push(':root {')
|
|
90
|
+
|
|
91
|
+
const varEntries = Object.entries(variables).sort(([a], [b]) => a.localeCompare(b))
|
|
92
|
+
for (const [name, def] of varEntries) {
|
|
93
|
+
const value = Array.isArray(def.value)
|
|
94
|
+
? resolveForTheme(def, defaultTheme)
|
|
95
|
+
: def.value
|
|
96
|
+
if (value === undefined) continue
|
|
97
|
+
const css = formatValue(value, name, def.type)
|
|
98
|
+
if (css === null) continue
|
|
99
|
+
lines.push(` ${variableNameToCSS(name)}: ${css};`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lines.push('}')
|
|
103
|
+
|
|
104
|
+
// Generate per-theme variant blocks
|
|
105
|
+
if (hasThemes) {
|
|
106
|
+
// For simplicity, iterate over each axis independently
|
|
107
|
+
// (e.g. mode: light/dark generates :root[data-theme="dark"] { ... })
|
|
108
|
+
for (const [axis, values] of themeAxes) {
|
|
109
|
+
// Skip the default (first) value
|
|
110
|
+
for (let i = 1; i < values.length; i++) {
|
|
111
|
+
const themeValue = values[i]
|
|
112
|
+
const themeContext = { ...defaultTheme, [axis]: themeValue }
|
|
113
|
+
|
|
114
|
+
const block: string[] = []
|
|
115
|
+
for (const [name, def] of varEntries) {
|
|
116
|
+
if (!Array.isArray(def.value)) continue
|
|
117
|
+
const resolvedForThis = resolveForTheme(def, themeContext)
|
|
118
|
+
const resolvedForDefault = resolveForTheme(def, defaultTheme)
|
|
119
|
+
// Only include if different from default
|
|
120
|
+
if (resolvedForThis === resolvedForDefault) continue
|
|
121
|
+
if (resolvedForThis === undefined) continue
|
|
122
|
+
const css = formatValue(resolvedForThis, name, def.type)
|
|
123
|
+
if (css === null) continue
|
|
124
|
+
block.push(` ${variableNameToCSS(name)}: ${css};`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (block.length > 0) {
|
|
128
|
+
lines.push('')
|
|
129
|
+
lines.push(`:root[data-theme="${themeValue}"] {`)
|
|
130
|
+
lines.push(...block)
|
|
131
|
+
lines.push('}')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return lines.join('\n') + '\n'
|
|
138
|
+
}
|