import type { DesignSystem } from '../design-system'
import { resolveConfig, type ConfigFile } from './config/resolve-config'
import type { ResolvedConfig } from './config/types'
function resolveThemeValue(value: unknown, subValue: string | null = null): string | null {
if (
Array.isArray(value) &&
value.length === 2 &&
typeof value[1] === 'object' &&
typeof value[1] !== null
) {
return subValue ? (value[1][subValue] ?? null) : value[0]
} else if (Array.isArray(value) && subValue === null) {
return value.join(', ')
} else if (typeof value === 'string' && subValue === null) {
return value
}
return null
}
export function applyConfigToTheme(designSystem: DesignSystem, configs: ConfigFile[]) {
let theme = resolveConfig(designSystem, configs).theme
for (let [path, value] of themeableValues(theme)) {
let name = keyPathToCssProperty(path)
designSystem.theme.add(`--${name}`, value as any, {
isInline: true,
isReference: true,
isDefault: true,
})
}
if (Object.hasOwn(theme, 'fontFamily')) {
let options = {
isInline: true,
isReference: false,
isDefault: true,
}
{
let fontFamily = resolveThemeValue(theme.fontFamily.sans)
if (fontFamily && designSystem.theme.hasDefault('--font-family-sans')) {
designSystem.theme.add('--default-font-family', fontFamily, options)
designSystem.theme.add(
'--default-font-feature-settings',
resolveThemeValue(theme.fontFamily.sans, 'fontFeatureSettings') ?? 'normal',
options,
)
designSystem.theme.add(
'--default-font-variation-settings',
resolveThemeValue(theme.fontFamily.sans, 'fontVariationSettings') ?? 'normal',
options,
)
}
}
{
let fontFamily = resolveThemeValue(theme.fontFamily.mono)
if (fontFamily && designSystem.theme.hasDefault('--font-family-mono')) {
designSystem.theme.add('--default-mono-font-family', 'theme(fontFamily.mono)', options)
designSystem.theme.add(
'--default-mono-font-feature-settings',
resolveThemeValue(theme.fontFamily.mono, 'fontFeatureSettings') ?? 'normal',
options,
)
designSystem.theme.add(
'--default-mono-font-variation-settings',
resolveThemeValue(theme.fontFamily.mono, 'fontVariationSettings') ?? 'normal',
options,
)
}
}
}
return theme
}
function themeableValues(config: ResolvedConfig['theme']): [string[], unknown][] {
let toAdd: [string[], unknown][] = []
walk(config as any, [], (value, path) => {
if (isValidThemePrimitive(value)) {
toAdd.push([path, value])
return WalkAction.Skip
}
if (isValidThemeTuple(value)) {
toAdd.push([path, value[0]])
for (let key of Reflect.ownKeys(value[1]) as string[]) {
toAdd.push([[...path, `-${key}`], value[1][key]])
}
return WalkAction.Skip
}
if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
toAdd.push([path, value.join(', ')])
return WalkAction.Skip
}
})
return toAdd
}
function keyPathToCssProperty(path: string[]) {
if (path[0] === 'colors') {
path[0] = 'color'
}
return (
path
.map((path) => (path === '1' ? '' : path))
.map((part) =>
part
.replaceAll('.', '_')
.replace(/([a-z])([A-Z])/g, (_, a, b) => `${a}-${b.toLowerCase()}`),
)
.filter((part, index) => part !== 'DEFAULT' || index !== path.length - 1)
.join('-')
)
}
function isValidThemePrimitive(value: unknown) {
return typeof value === 'number' || typeof value === 'string'
}
function isValidThemeTuple(value: unknown): value is [string, Record<string, string | number>] {
if (!Array.isArray(value)) return false
if (value.length !== 2) return false
if (typeof value[0] !== 'string' && typeof value[0] !== 'number') return false
if (value[1] === undefined || value[1] === null) return false
if (typeof value[1] !== 'object') return false
for (let key of Reflect.ownKeys(value[1])) {
if (typeof key !== 'string') return false
if (typeof value[1][key] !== 'string' && typeof value[1][key] !== 'number') return false
}
return true
}
enum WalkAction {
Continue,
Skip,
Stop,
}
function walk(
obj: Record<string, unknown>,
path: string[] = [],
callback: (value: unknown, path: string[]) => WalkAction | void,
) {
for (let key of Reflect.ownKeys(obj) as string[]) {
let value = obj[key]
if (value === undefined || value === null) {
continue
}
let keyPath = [...path, key]
let result = callback(value, keyPath) ?? WalkAction.Continue
if (result === WalkAction.Skip) continue
if (result === WalkAction.Stop) break
if (!Array.isArray(value) && typeof value !== 'object') continue
walk(value as any, keyPath, callback)
}
}