openpencil/src/services/codegen/css-variables-generator.ts
Kayshen-X e8f7b597bf feat(history): increase maximum undo/redo states and enhance batch handling
- Update history store to support up to 300 undo/redo states, improving user experience during complex editing sessions.
- Refactor batch handling in various components to ensure accurate history tracking and prevent unnecessary entries.
- Implement checks to avoid adding duplicate states to the undo stack, optimizing performance and memory usage.
- Enhance event handling in canvas and keyboard shortcuts to maintain consistent history state during user interactions.
2026-02-20 15:41:45 +08:00

139 lines
4.3 KiB
TypeScript

/**
* Generates CSS custom properties from PenDocument variables.
*
* Produces `:root { ... }` blocks with one block per theme variant.
*/
import type { PenDocument } from '@/types/pen'
import type { VariableDefinition, ThemedValue } from '@/types/variables'
/** Sanitise a variable name into a valid CSS custom property name. */
export function variableNameToCSS(name: string): string {
const sanitised = name
.replace(/^\$/, '')
.replace(/\s+/g, '-')
.replace(/[^a-zA-Z0-9_-]/g, '')
.toLowerCase()
return `--${sanitised}`
}
/** Whether a numeric variable should be output without a unit (e.g. opacity). */
function isUnitless(name: string): boolean {
const lower = name.toLowerCase()
return (
lower.includes('opacity') ||
lower.includes('weight') ||
lower.includes('scale') ||
lower.includes('ratio') ||
lower.includes('z-index') ||
lower.includes('line-height')
)
}
/** Format a variable value as a CSS value string. */
function formatValue(
value: string | number | boolean,
name: string,
type: VariableDefinition['type'],
): string | null {
if (type === 'boolean') return null
if (type === 'color') return String(value)
if (type === 'number') {
if (typeof value !== 'number') return String(value)
return isUnitless(name) ? String(value) : `${value}px`
}
// string
return String(value)
}
/** Resolve a single themed value for a given theme context. */
function resolveForTheme(
def: VariableDefinition,
theme: Record<string, string>,
): string | number | boolean | undefined {
const val = def.value
if (!Array.isArray(val)) return val
const match = (val as ThemedValue[]).find((v) => {
if (!v.theme) return false
return Object.entries(theme).every(
([key, expected]) => v.theme?.[key] === expected,
)
})
return match?.value ?? (val as ThemedValue[])[0]?.value
}
/**
* Generate CSS custom properties from a PenDocument's variables and themes.
*
* Returns a string containing `:root { ... }` blocks.
*/
export function generateCSSVariables(doc: PenDocument): string {
const variables = doc.variables
if (!variables || Object.keys(variables).length === 0) {
return '/* No design variables defined */\n'
}
const themes = doc.themes ?? {}
const themeAxes = Object.entries(themes)
// Build default theme (first value per axis)
const defaultTheme: Record<string, string> = {}
for (const [key, values] of themeAxes) {
if (values.length > 0) defaultTheme[key] = values[0]
}
const hasThemes = themeAxes.length > 0 && themeAxes.some(([, v]) => v.length > 1)
// Generate default :root block
const lines: string[] = []
lines.push(':root {')
const varEntries = Object.entries(variables).sort(([a], [b]) => a.localeCompare(b))
for (const [name, def] of varEntries) {
const value = Array.isArray(def.value)
? resolveForTheme(def, defaultTheme)
: def.value
if (value === undefined) continue
const css = formatValue(value, name, def.type)
if (css === null) continue
lines.push(` ${variableNameToCSS(name)}: ${css};`)
}
lines.push('}')
// Generate per-theme variant blocks
if (hasThemes) {
// For simplicity, iterate over each axis independently
// (e.g. mode: light/dark generates :root[data-theme="dark"] { ... })
for (const [axis, values] of themeAxes) {
// Skip the default (first) value
for (let i = 1; i < values.length; i++) {
const themeValue = values[i]
const themeContext = { ...defaultTheme, [axis]: themeValue }
const block: string[] = []
for (const [name, def] of varEntries) {
if (!Array.isArray(def.value)) continue
const resolvedForThis = resolveForTheme(def, themeContext)
const resolvedForDefault = resolveForTheme(def, defaultTheme)
// Only include if different from default
if (resolvedForThis === resolvedForDefault) continue
if (resolvedForThis === undefined) continue
const css = formatValue(resolvedForThis, name, def.type)
if (css === null) continue
block.push(` ${variableNameToCSS(name)}: ${css};`)
}
if (block.length > 0) {
lines.push('')
lines.push(`:root[data-theme="${themeValue}"] {`)
lines.push(...block)
lines.push('}')
}
}
}
}
return lines.join('\n') + '\n'
}