@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.
@@ -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
+ }