|
|
|
|
import {
|
|
|
|
|
COLOR_ALPHA_STEPS,
|
|
|
|
|
COLOR_SCALE_STEPS,
|
|
|
|
|
COLOR_ROLES,
|
|
|
|
|
DENSITY_KEYS,
|
|
|
|
|
type ColorRoleDefinition,
|
|
|
|
|
type ColorRoleSlot,
|
|
|
|
|
type ColorRoleMap,
|
|
|
|
|
type ColorAlphaStep,
|
|
|
|
|
type ColorAlphaScale,
|
|
|
|
|
type ColorScale,
|
|
|
|
|
type ColorScales,
|
|
|
|
|
type BorderColorRoles,
|
|
|
|
|
type BorderPrimitiveSet,
|
|
|
|
|
type ContentColorRoles,
|
|
|
|
|
type DensityPrimitiveSet,
|
|
|
|
|
type EidosCssContract,
|
|
|
|
|
type EidosCssContractToken,
|
|
|
|
|
type EidosCssVariableMap,
|
|
|
|
|
type EidosCssVariableValue,
|
|
|
|
|
type EidosConfig,
|
|
|
|
|
type FontFamily,
|
|
|
|
|
type FocusColorRoles,
|
|
|
|
|
type LayoutPrimitiveSet,
|
|
|
|
|
type PrimitiveSet,
|
|
|
|
|
type SizePrimitiveSet,
|
|
|
|
|
type SurfaceColorRoles,
|
|
|
|
|
type ThemeColorSet,
|
|
|
|
|
type TypographyPrimitiveSet
|
|
|
|
|
} from './config-types'
|
|
|
|
|
import { createEidosCssContract } from './contract'
|
|
|
|
|
import { EidosCssVariableError, EidosThemeNotFoundError } from '../errors'
|
|
|
|
|
|
|
|
|
|
export interface RenderThemeCssOptions {
|
|
|
|
|
readonly selector?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RenderContractCssOptions {
|
|
|
|
|
readonly staticSelector?: string
|
|
|
|
|
readonly themeSelector?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface RenderCssVariablesOptions {
|
|
|
|
|
readonly selector?: string
|
|
|
|
|
readonly contract?: EidosCssContract | readonly EidosCssContractToken[]
|
|
|
|
|
readonly strict?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_COLOR_ROLE_SLOT_STEPS: Record<ColorRoleSlot, string> = {
|
|
|
|
|
track: '1',
|
|
|
|
|
element: '3',
|
|
|
|
|
hover: '4',
|
|
|
|
|
active: '5',
|
|
|
|
|
border: '6',
|
|
|
|
|
solid: '9',
|
|
|
|
|
solidHover: '10',
|
|
|
|
|
text: '11',
|
|
|
|
|
contrast: '12'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DEFAULT_COLOR_ALPHA_PERCENTAGES: Record<ColorAlphaStep, string> = {
|
|
|
|
|
'1': '3%',
|
|
|
|
|
'2': '5%',
|
|
|
|
|
'3': '8%',
|
|
|
|
|
'4': '12%',
|
|
|
|
|
'5': '16%',
|
|
|
|
|
'6': '22%',
|
|
|
|
|
'7': '30%',
|
|
|
|
|
'8': '40%',
|
|
|
|
|
'9': '56%',
|
|
|
|
|
'10': '68%',
|
|
|
|
|
'11': '80%',
|
|
|
|
|
'12': '92%'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const GENERIC_FONT_FAMILIES = new Set([
|
|
|
|
|
'serif',
|
|
|
|
|
'sans-serif',
|
|
|
|
|
'monospace',
|
|
|
|
|
'cursive',
|
|
|
|
|
'fantasy',
|
|
|
|
|
'system-ui',
|
|
|
|
|
'ui-serif',
|
|
|
|
|
'ui-sans-serif',
|
|
|
|
|
'ui-monospace',
|
|
|
|
|
'emoji',
|
|
|
|
|
'math',
|
|
|
|
|
'fangsong'
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
export function renderStaticCss(options: EidosConfig): string {
|
|
|
|
|
const { primitives } = options
|
|
|
|
|
const declarations: string[] = []
|
|
|
|
|
|
|
|
|
|
appendRecordDeclarations(declarations, 'space', primitives.space)
|
|
|
|
|
appendRecordDeclarations(declarations, 'control-height', primitives.controlHeight)
|
|
|
|
|
appendRecordDeclarations(declarations, 'radius', primitives.radius)
|
|
|
|
|
|
|
|
|
|
if (primitives.border) {
|
|
|
|
|
appendBorderDeclarations(declarations, primitives.border)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.focusRing) {
|
|
|
|
|
declarations.push(cssVar('focus-ring-offset', primitives.focusRing.offset))
|
|
|
|
|
declarations.push(cssVar('focus-ring-width', primitives.focusRing.width))
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(
|
|
|
|
|
'focus-ring',
|
|
|
|
|
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), ' +
|
|
|
|
|
'0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) ' +
|
|
|
|
|
'var(--focus-ring-color)'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(
|
|
|
|
|
'focus-ring-error',
|
|
|
|
|
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), ' +
|
|
|
|
|
'0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) ' +
|
|
|
|
|
'var(--focus-ring-color-error)'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.layout) {
|
|
|
|
|
appendLayoutDeclarations(declarations, primitives.layout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.density) {
|
|
|
|
|
appendDensityDeclarations(declarations, primitives.density)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.motion) {
|
|
|
|
|
appendRecordDeclarations(declarations, 'duration', primitives.motion.duration)
|
|
|
|
|
appendRecordDeclarations(declarations, 'ease', primitives.motion.ease)
|
|
|
|
|
appendRecordDeclarations(declarations, 'motion-distance', primitives.motion.distance)
|
|
|
|
|
appendRecordDeclarations(declarations, 'motion-scale', primitives.motion.scale)
|
|
|
|
|
declarations.push(cssVar('motion-stagger', primitives.motion.stagger))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.icon) {
|
|
|
|
|
appendRecordDeclarations(declarations, 'icon-size', primitives.icon.size)
|
|
|
|
|
appendRecordDeclarations(declarations, 'icon-stroke-width', primitives.icon.strokeWidth)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendRecordDeclarations(declarations, 'opacity', primitives.opacity)
|
|
|
|
|
appendRecordDeclarations(declarations, 'z-index', primitives.zIndex)
|
|
|
|
|
|
|
|
|
|
if (primitives.typography) {
|
|
|
|
|
appendTypographyDeclarations(declarations, primitives.typography)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.size) {
|
|
|
|
|
appendSizeDeclarations(declarations, primitives.size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendTransitionAliasDeclarations(declarations, options)
|
|
|
|
|
|
|
|
|
|
const blocks = [renderBlock(':root', declarations)]
|
|
|
|
|
if (primitives.density) blocks.push(renderDensityBlocks())
|
|
|
|
|
|
|
|
|
|
return blocks.join('\n\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renderContractCss(
|
|
|
|
|
options: EidosConfig,
|
|
|
|
|
renderOptions: RenderContractCssOptions = {}
|
|
|
|
|
): string {
|
|
|
|
|
const contract = createEidosCssContract(options)
|
|
|
|
|
const staticDeclarations = contract.static.map((token) => emptyCssVar(token.name))
|
|
|
|
|
const themeDeclarations = contract.theme.map((token) => emptyCssVar(token.name))
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
renderBlock(renderOptions.staticSelector ?? ':root', staticDeclarations),
|
|
|
|
|
renderBlock(renderOptions.themeSelector ?? '[data-theme]', themeDeclarations)
|
|
|
|
|
].join('\n\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renderCssVariables(
|
|
|
|
|
variables: EidosCssVariableMap,
|
|
|
|
|
renderOptions: RenderCssVariablesOptions = {}
|
|
|
|
|
): string {
|
|
|
|
|
const declarations: string[] = []
|
|
|
|
|
const strict = renderOptions.strict ?? renderOptions.contract !== undefined
|
|
|
|
|
const contractNames = renderOptions.contract
|
|
|
|
|
? getContractVariableNames(renderOptions.contract)
|
|
|
|
|
: undefined
|
|
|
|
|
const unknownNames: string[] = []
|
|
|
|
|
|
|
|
|
|
for (const [rawName, value] of Object.entries(variables)) {
|
|
|
|
|
if (value === null || value === undefined) continue
|
|
|
|
|
|
|
|
|
|
const name = normalizeCssVariableName(rawName)
|
|
|
|
|
if (!name) {
|
|
|
|
|
unknownNames.push(rawName)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (contractNames && !contractNames.has(name)) {
|
|
|
|
|
unknownNames.push(`--${name}`)
|
|
|
|
|
if (strict) continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
declarations.push(cssVar(name, assertCssVariableValue(`--${name}`, value)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (unknownNames.length && strict) {
|
|
|
|
|
throw new EidosCssVariableError(unknownNames)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return declarations.length ? renderBlock(renderOptions.selector ?? ':root', declarations) : ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function renderThemeCss(
|
|
|
|
|
options: EidosConfig,
|
|
|
|
|
themeId: string,
|
|
|
|
|
renderOptions: RenderThemeCssOptions = {}
|
|
|
|
|
): string {
|
|
|
|
|
const theme = options.themes?.[themeId]
|
|
|
|
|
if (!theme) {
|
|
|
|
|
throw new EidosThemeNotFoundError(themeId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const color = mergeThemeColor(
|
|
|
|
|
options.primitives.color?.scales,
|
|
|
|
|
options.semantics.color,
|
|
|
|
|
theme.color
|
|
|
|
|
)
|
|
|
|
|
const declarations: string[] = []
|
|
|
|
|
|
|
|
|
|
const scaleNames = new Set([...Object.keys(color.scales), ...Object.keys(color.alphaScales)])
|
|
|
|
|
|
|
|
|
|
for (const scaleName of scaleNames) {
|
|
|
|
|
const scale = color.scales[scaleName]
|
|
|
|
|
if (scale) appendColorScaleDeclarations(declarations, scaleName, scale)
|
|
|
|
|
appendColorAlphaScaleDeclarations(declarations, scaleName, color.alphaScales[scaleName])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const role of COLOR_ROLES) {
|
|
|
|
|
const definition = color.roles[role]
|
|
|
|
|
const scaleName = getColorRoleScaleName(definition)
|
|
|
|
|
|
|
|
|
|
for (const step of COLOR_SCALE_STEPS) {
|
|
|
|
|
declarations.push(cssVar(`primitive-${role}-${step}`, `var(--scale-${scaleName}-${step})`))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const step of COLOR_ALPHA_STEPS) {
|
|
|
|
|
declarations.push(cssVar(`primitive-${role}-a${step}`, `var(--scale-${scaleName}-a${step})`))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const slots = {
|
|
|
|
|
...DEFAULT_COLOR_ROLE_SLOT_STEPS,
|
|
|
|
|
...(typeof definition === 'string' ? undefined : definition.slots)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [slot, step] of Object.entries(slots)) {
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(`color-${role}-${toKebab(slot)}`, `var(--primitive-${role}-${step})`)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendThemeColorDeclarations(declarations, color)
|
|
|
|
|
|
|
|
|
|
if (theme.shadow) {
|
|
|
|
|
appendRecordDeclarations(declarations, 'shadow', theme.shadow)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return renderBlock(renderOptions.selector ?? getThemeSelector(themeId), declarations)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { EidosCssVariableError } from '../errors'
|
|
|
|
|
|
|
|
|
|
function appendBorderDeclarations(declarations: string[], border: BorderPrimitiveSet): void {
|
|
|
|
|
appendRecordDeclarations(declarations, 'border-width', border.width)
|
|
|
|
|
appendRecordDeclarations(declarations, 'border-style', border.style)
|
|
|
|
|
declarations.push(cssVar('border-width', `var(--border-width-${border.defaultWidth})`))
|
|
|
|
|
declarations.push(cssVar('border-style', `var(--border-style-${border.defaultStyle})`))
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar('border', 'var(--border-width) var(--border-style) var(--color-border-default)')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendLayoutDeclarations(declarations: string[], layout: LayoutPrimitiveSet): void {
|
|
|
|
|
appendRecordDeclarations(declarations, 'container-width', layout.containerWidth)
|
|
|
|
|
declarations.push(cssVar('container-padding-inline', layout.containerPaddingInline))
|
|
|
|
|
appendRecordDeclarations(declarations, 'content-width', layout.contentWidth)
|
|
|
|
|
appendRecordDeclarations(declarations, 'aspect-ratio', layout.aspectRatio)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendDensityDeclarations(declarations: string[], density: DensityPrimitiveSet): void {
|
|
|
|
|
appendDensityRecordDeclarations(declarations, density.scale, 'scale')
|
|
|
|
|
appendDensityRecordDeclarations(declarations, density.spaceScale, 'space-scale')
|
|
|
|
|
appendDensityRecordDeclarations(declarations, density.controlScale, 'control-scale')
|
|
|
|
|
appendDensityRecordDeclarations(declarations, density.contentScale, 'content-scale')
|
|
|
|
|
appendActiveDensityDeclarations(declarations, 'comfortable')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendDensityRecordDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
record: Record<string, string | number>,
|
|
|
|
|
suffix: string
|
|
|
|
|
): void {
|
|
|
|
|
for (const [name, value] of Object.entries(record)) {
|
|
|
|
|
declarations.push(cssVar(`density-${name}-${suffix}`, value))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDensityBlocks(): string {
|
|
|
|
|
return DENSITY_KEYS.filter((densityKey) => densityKey !== 'comfortable')
|
|
|
|
|
.map((densityKey) => {
|
|
|
|
|
const declarations: string[] = []
|
|
|
|
|
appendActiveDensityDeclarations(declarations, densityKey)
|
|
|
|
|
return renderBlock(`[data-density='${densityKey}']`, declarations)
|
|
|
|
|
})
|
|
|
|
|
.join('\n\n')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendActiveDensityDeclarations(declarations: string[], densityKey: string): void {
|
|
|
|
|
declarations.push(cssVar('density-scale', `var(--density-${densityKey}-scale)`))
|
|
|
|
|
declarations.push(cssVar('density-space-scale', `var(--density-${densityKey}-space-scale)`))
|
|
|
|
|
declarations.push(cssVar('density-control-scale', `var(--density-${densityKey}-control-scale)`))
|
|
|
|
|
declarations.push(cssVar('density-content-scale', `var(--density-${densityKey}-content-scale)`))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendTypographyDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
typography: TypographyPrimitiveSet
|
|
|
|
|
): void {
|
|
|
|
|
for (const [name, family] of Object.entries(typography.families)) {
|
|
|
|
|
declarations.push(cssVar(`font-family-${name}`, formatFontFamily(family)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [name, metric] of Object.entries(typography.sizes)) {
|
|
|
|
|
declarations.push(cssVar(`font-size-${name}`, metric.size))
|
|
|
|
|
declarations.push(cssVar(`font-line-height-${name}`, metric.lineHeight))
|
|
|
|
|
declarations.push(cssVar(`font-letter-spacing-${name}`, metric.letterSpacing))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [name, weight] of Object.entries(typography.weights)) {
|
|
|
|
|
declarations.push(cssVar(`font-weight-${name}`, weight))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendSizeDeclarations(declarations: string[], sizes: SizePrimitiveSet): void {
|
|
|
|
|
for (const [name, size] of Object.entries(sizes)) {
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(`size-${name}-control-height`, `var(--control-height-${size.controlHeight})`)
|
|
|
|
|
)
|
|
|
|
|
declarations.push(cssVar(`size-${name}-font-size`, `var(--font-size-${size.fontSize})`))
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(`size-${name}-font-line-height`, `var(--font-line-height-${size.fontSize})`)
|
|
|
|
|
)
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(`size-${name}-font-letter-spacing`, `var(--font-letter-spacing-${size.fontSize})`)
|
|
|
|
|
)
|
|
|
|
|
declarations.push(cssVar(`size-${name}-icon-size`, `var(--icon-size-${size.iconSize})`))
|
|
|
|
|
declarations.push(cssVar(`size-${name}-padding-inline`, size.paddingInline))
|
|
|
|
|
declarations.push(cssVar(`size-${name}-padding-block`, size.paddingBlock))
|
|
|
|
|
declarations.push(cssVar(`size-${name}-gap`, size.gap))
|
|
|
|
|
declarations.push(cssVar(`size-${name}-radius`, `var(--radius-${size.radius})`))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendTransitionAliasDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
options: EidosConfig
|
|
|
|
|
): void {
|
|
|
|
|
const { primitives } = options
|
|
|
|
|
|
|
|
|
|
if (primitives.controlHeight?.xxs) {
|
|
|
|
|
declarations.push(cssVar('control-height-2xs', 'var(--control-height-xxs)'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (primitives.radius?.sm) {
|
|
|
|
|
declarations.push(cssVar('radius-xs', 'var(--radius-sm)'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendFontFamilyAliases(declarations, primitives.typography?.families)
|
|
|
|
|
appendTypographyAliases(declarations, primitives.typography)
|
|
|
|
|
|
|
|
|
|
if (primitives.motion) {
|
|
|
|
|
declarations.push(cssVar('motion-spin-duration', '800ms'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasFocusColor(options)) {
|
|
|
|
|
declarations.push(cssVar('color-focus-ring', 'var(--focus-ring-color)'))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendFontFamilyAliases(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
families: TypographyPrimitiveSet['families'] | undefined
|
|
|
|
|
): void {
|
|
|
|
|
if (!families) return
|
|
|
|
|
|
|
|
|
|
if (families.primary) {
|
|
|
|
|
declarations.push(cssVar('font-sans', 'var(--font-family-primary)'))
|
|
|
|
|
declarations.push(cssVar('font-ui', 'var(--font-family-primary)'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (families.secondary) {
|
|
|
|
|
declarations.push(cssVar('font-serif', 'var(--font-family-secondary)'))
|
|
|
|
|
declarations.push(cssVar('font-prose', 'var(--font-family-secondary)'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (families.display) {
|
|
|
|
|
declarations.push(cssVar('font-heading', 'var(--font-family-display)'))
|
|
|
|
|
declarations.push(cssVar('font-display', 'var(--font-family-display)'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (families.mono) {
|
|
|
|
|
declarations.push(cssVar('font-mono', 'var(--font-family-mono)'))
|
|
|
|
|
declarations.push(cssVar('font-code', 'var(--font-family-mono)'))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendTypographyAliases(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
typography: PrimitiveSet['typography']
|
|
|
|
|
): void {
|
|
|
|
|
if (!typography) return
|
|
|
|
|
|
|
|
|
|
const textAliasMap = {
|
|
|
|
|
'1': 'xxs',
|
|
|
|
|
'2': 'xs',
|
|
|
|
|
'3': 'sm',
|
|
|
|
|
'4': 'md',
|
|
|
|
|
'5': 'lg',
|
|
|
|
|
'6': 'xl'
|
|
|
|
|
} as const
|
|
|
|
|
|
|
|
|
|
for (const [alias, size] of Object.entries(textAliasMap)) {
|
|
|
|
|
if (!typography.sizes[size]) continue
|
|
|
|
|
|
|
|
|
|
declarations.push(cssVar(`text-${alias}-size`, `var(--font-size-${size})`))
|
|
|
|
|
declarations.push(cssVar(`text-${alias}-lh`, `var(--font-line-height-${size})`))
|
|
|
|
|
declarations.push(cssVar(`text-${alias}-ls`, `var(--font-letter-spacing-${size})`))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typography.sizes.md) declarations.push(cssVar('font-size-base', 'var(--font-size-md)'))
|
|
|
|
|
if (typography.sizes.xxl) declarations.push(cssVar('font-size-2xl', 'var(--font-size-xxl)'))
|
|
|
|
|
if (typography.sizes.xxxl) declarations.push(cssVar('font-size-3xl', 'var(--font-size-xxxl)'))
|
|
|
|
|
|
|
|
|
|
if (typography.weights.regular) {
|
|
|
|
|
declarations.push(cssVar('font-weight-normal', 'var(--font-weight-regular)'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
declarations.push(cssVar('leading-ui', '1.25'))
|
|
|
|
|
declarations.push(cssVar('leading-prose', '1.6'))
|
|
|
|
|
declarations.push(cssVar('leading-text', 'var(--leading-prose)'))
|
|
|
|
|
declarations.push(cssVar('leading-heading', '1.2'))
|
|
|
|
|
declarations.push(cssVar('leading-display', '1.05'))
|
|
|
|
|
declarations.push(cssVar('tracking-badge', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-label', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-ui', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-prose', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-heading', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-display', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-tight', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-normal', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-wide', '0'))
|
|
|
|
|
declarations.push(cssVar('tracking-wider', '0'))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasFocusColor(options: EidosConfig): boolean {
|
|
|
|
|
if (options.semantics.color.focus) return true
|
|
|
|
|
|
|
|
|
|
return Object.values(options.themes ?? {}).some((theme) => theme.color?.focus)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendThemeColorDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
color: ReturnType<typeof mergeThemeColor>
|
|
|
|
|
): void {
|
|
|
|
|
if (color.surface) {
|
|
|
|
|
declarations.push(cssVar('color-surface-default', color.surface.default))
|
|
|
|
|
declarations.push(cssVar('color-surface-raised', color.surface.raised))
|
|
|
|
|
declarations.push(cssVar('color-surface-overlay', color.surface.overlay))
|
|
|
|
|
declarations.push(cssVar('color-overlay', color.surface.backdrop))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (color.content) {
|
|
|
|
|
declarations.push(cssVar('color-content-primary', color.content.primary))
|
|
|
|
|
declarations.push(cssVar('color-content-secondary', color.content.secondary))
|
|
|
|
|
declarations.push(cssVar('color-content-disabled', color.content.disabled))
|
|
|
|
|
declarations.push(cssVar('color-content-on-solid', color.content.onSolid))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (color.border) {
|
|
|
|
|
declarations.push(cssVar('color-border-subtle', color.border.subtle))
|
|
|
|
|
declarations.push(cssVar('color-border-default', color.border.default))
|
|
|
|
|
declarations.push(cssVar('color-border-strong', color.border.strong))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (color.focus) {
|
|
|
|
|
declarations.push(cssVar('focus-ring-color', color.focus.ring))
|
|
|
|
|
declarations.push(cssVar('focus-ring-color-error', color.focus.ringError))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendColorScaleDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
scaleName: string,
|
|
|
|
|
scale: ColorScale
|
|
|
|
|
): void {
|
|
|
|
|
for (const step of COLOR_SCALE_STEPS) {
|
|
|
|
|
declarations.push(cssVar(`scale-${scaleName}-${step}`, scale[step]))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendColorAlphaScaleDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
scaleName: string,
|
|
|
|
|
scale: ColorAlphaScale | undefined
|
|
|
|
|
): void {
|
|
|
|
|
for (const step of COLOR_ALPHA_STEPS) {
|
|
|
|
|
declarations.push(
|
|
|
|
|
cssVar(
|
|
|
|
|
`scale-${scaleName}-a${step}`,
|
|
|
|
|
scale?.[step] ??
|
|
|
|
|
`color-mix(in srgb, var(--scale-${scaleName}-9) ${
|
|
|
|
|
DEFAULT_COLOR_ALPHA_PERCENTAGES[step]
|
|
|
|
|
}, transparent)`
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function appendRecordDeclarations(
|
|
|
|
|
declarations: string[],
|
|
|
|
|
prefix: string,
|
|
|
|
|
record: Record<string, string | number> | undefined
|
|
|
|
|
): void {
|
|
|
|
|
if (!record) return
|
|
|
|
|
|
|
|
|
|
for (const [name, value] of Object.entries(record)) {
|
|
|
|
|
declarations.push(cssVar(`${prefix}-${name}`, value))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeThemeColor(
|
|
|
|
|
primitiveScales: ColorScales | undefined,
|
|
|
|
|
semanticColor: EidosConfig['semantics']['color'],
|
|
|
|
|
themeColor: ThemeColorSet | undefined
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
scales: {
|
|
|
|
|
...primitiveScales,
|
|
|
|
|
...themeColor?.scales
|
|
|
|
|
},
|
|
|
|
|
alphaScales: {
|
|
|
|
|
...themeColor?.alphaScales
|
|
|
|
|
},
|
|
|
|
|
roles: {
|
|
|
|
|
...semanticColor.roles,
|
|
|
|
|
...themeColor?.roles
|
|
|
|
|
} as ColorRoleMap,
|
|
|
|
|
surface:
|
|
|
|
|
semanticColor.surface || themeColor?.surface
|
|
|
|
|
? ({
|
|
|
|
|
...semanticColor.surface,
|
|
|
|
|
...themeColor?.surface
|
|
|
|
|
} as SurfaceColorRoles)
|
|
|
|
|
: undefined,
|
|
|
|
|
content:
|
|
|
|
|
semanticColor.content || themeColor?.content
|
|
|
|
|
? ({
|
|
|
|
|
...semanticColor.content,
|
|
|
|
|
...themeColor?.content
|
|
|
|
|
} as ContentColorRoles)
|
|
|
|
|
: undefined,
|
|
|
|
|
border:
|
|
|
|
|
semanticColor.border || themeColor?.border
|
|
|
|
|
? ({
|
|
|
|
|
...semanticColor.border,
|
|
|
|
|
...themeColor?.border
|
|
|
|
|
} as BorderColorRoles)
|
|
|
|
|
: undefined,
|
|
|
|
|
focus:
|
|
|
|
|
semanticColor.focus || themeColor?.focus
|
|
|
|
|
? ({
|
|
|
|
|
...semanticColor.focus,
|
|
|
|
|
...themeColor?.focus
|
|
|
|
|
} as FocusColorRoles)
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getThemeSelector(themeId: string): string {
|
|
|
|
|
if (themeId === 'base-light') {
|
|
|
|
|
return `:root,\n[data-theme='${themeId}']`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `[data-theme='${themeId}']`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getColorRoleScaleName(definition: string | ColorRoleDefinition): string {
|
|
|
|
|
if (typeof definition === 'string') return definition
|
|
|
|
|
|
|
|
|
|
return definition.scale
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cssVar(name: string, value: string | number): string {
|
|
|
|
|
return `--${name}: ${value};`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function emptyCssVar(name: string): string {
|
|
|
|
|
return `--${name}: ;`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderBlock(selector: string, declarations: string[]): string {
|
|
|
|
|
return `${selector} {\n${declarations.map((declaration) => `\t${declaration}`).join('\n')}\n}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatFontFamily(family: FontFamily): string {
|
|
|
|
|
return [family.family, ...(family.fallbacks ?? [])].map(formatFontName).join(', ')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatFontName(name: string): string {
|
|
|
|
|
if (GENERIC_FONT_FAMILIES.has(name)) return name
|
|
|
|
|
if (name.startsWith('"') || name.startsWith("'")) return name
|
|
|
|
|
if (!/\s/.test(name)) return name
|
|
|
|
|
|
|
|
|
|
return `'${name.replace(/'/g, "\\'")}'`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toKebab(value: string): string {
|
|
|
|
|
return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getContractVariableNames(
|
|
|
|
|
contract: EidosCssContract | readonly EidosCssContractToken[]
|
|
|
|
|
): Set<string> {
|
|
|
|
|
const tokens = isEidosCssContract(contract) ? [...contract.static, ...contract.theme] : contract
|
|
|
|
|
return new Set(tokens.map((token) => token.name))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isEidosCssContract(
|
|
|
|
|
contract: EidosCssContract | readonly EidosCssContractToken[]
|
|
|
|
|
): contract is EidosCssContract {
|
|
|
|
|
return !Array.isArray(contract)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeCssVariableName(name: string): string | undefined {
|
|
|
|
|
const normalized = name.startsWith('--') ? name.slice(2) : name
|
|
|
|
|
if (/^[a-zA-Z0-9_-]+$/.test(normalized)) return normalized
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertCssVariableValue(name: string, value: EidosCssVariableValue): string | number {
|
|
|
|
|
if (typeof value === 'number') return value
|
|
|
|
|
if (!/[;{}]/.test(value)) return value
|
|
|
|
|
|
|
|
|
|
throw new EidosCssVariableError([name])
|
|
|
|
|
}
|