openpencil/packages/dom-css/src/export/css.ts
Danila Poyarkov fb0cb9ee5f
feat: write variable-bound properties as tokens in exported code (#895)
* feat: write variable-bound properties as tokens in exported code

HTML and Tailwind JSX export reference the design tokens a layer is bound to, var(--color-primary) or bg-primary, instead of baking in their values. A binding becomes a reference only where the stylesheet resolves it as the canvas draws it: the layer still draws the value, the token's unit fits the property, and an attribute such as data-theme="dark" can put the element in the layer's mode. Standalone HTML includes the stylesheet for the tokens it uses, compiled into Tailwind's @theme for Tailwind pages.

* refactor: share the collection variable list and tighten token helpers

Read every variable in collection order through one collectionVariables helper built on es-toolkit's compact, used by the stylesheet and the token references, and map padding sides and color channels instead of repeating them.
2026-10-05 09:15:44 +00:00

47 lines
2 KiB
TypeScript

import type { Effect, Fill, Stroke } from '@open-pencil/scene-graph'
import { colorToCSS, colorToHex } from '@open-pencil/scene-graph/color'
import type { Color } from '@open-pencil/scene-graph/primitives'
import type { DesignStyleDeclaration } from '../types'
/** Opaque colors as hex, which Tailwind matches to its palette; translucent ones as `rgba()`. */
export function cssColor(color: Color): string {
return color.a >= 1 ? colorToHex(color) : colorToCSS(color)
}
export function fillToCSS(fill: Fill | undefined): string | undefined {
if (fill?.type !== 'SOLID' || !fill.visible) return undefined
return cssColor({ ...fill.color, a: fill.opacity })
}
export function strokeColorToCSS(stroke: Stroke | undefined): string | undefined {
if (!stroke?.visible) return undefined
return cssColor({ ...stroke.color, a: stroke.opacity })
}
function shadowToCSS(effect: Effect): string {
const inset = effect.type === 'INNER_SHADOW' ? 'inset ' : ''
return `${inset}${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread}px ${cssColor(effect.color)}`
}
export function dropShadowToCSS(effect: Effect | undefined): string | undefined {
if (effect?.type !== 'DROP_SHADOW' || !effect.visible) return undefined
return shadowToCSS(effect)
}
/** Every visible shadow, in order, plus layer and background blur. */
export function effectsToCSS(effects: Effect[]): DesignStyleDeclaration {
const style: DesignStyleDeclaration = {}
const visible = effects.filter((effect) => effect.visible)
const shadows = visible.filter(
(effect) => effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW'
)
if (shadows.length > 0) style['box-shadow'] = shadows.map(shadowToCSS).join(', ')
for (const effect of visible) {
if (effect.type === 'LAYER_BLUR' || effect.type === 'FOREGROUND_BLUR')
style.filter = `blur(${effect.radius}px)`
if (effect.type === 'BACKGROUND_BLUR') style['backdrop-filter'] = `blur(${effect.radius}px)`
}
return style
}