UNPKG

@use-pico/cls

Version:

Type-safe, composable styling system for React, Vue, Svelte, and vanilla JS

120 lines (100 loc) 2.96 kB
import type { Contract } from "../types/Contract"; import type { Tweak } from "../types/Tweak"; import type { What } from "../types/What"; import { dedupe } from "./dedupe"; import { filter } from "./filter"; export function deepClone<T>(value: T): T { if (value === null || typeof value !== "object") { return value; } if (Array.isArray(value)) { return value.map((item) => deepClone(item)) as unknown as T; } const result: Record<string, unknown> = {}; for (const [key, val] of Object.entries(value as Record<string, unknown>)) { result[key] = deepClone(val); } return result as T; } /** * merge(tweak1, tweak2, ...) * * Merges multiple tweak objects with specific behavior: * - Variants are replaced by default (regardless of "override" flag) * - Slots are merged by default until override is explicitly set * - Tokens are merged until some tweak has override: true, then that tweak resets and starts over * - Override flag only affects slots/tokens explicitly set, not the whole tweak */ export function tweaks<const TContract extends Contract.Any>( ...tweaks: Tweak.Tweaks<TContract>[] ): Tweak.Type<TContract> { if (!tweaks || tweaks.length === 0) { return {}; } const list = deepClone(tweaks) .flat(10) .filter((tweak): tweak is Tweak.Type<TContract> => tweak !== undefined); if (list.length === 0) { return {}; } const [root, ...rest] = list as [ Tweak.Type<TContract>, ...Tweak.Type<TContract>[], ]; return rest.reduce((acc, current) => { const override = current.override ?? false; const clear = current.clear ?? false; if (clear) { return { token: filter(current.token), slot: filter(current.slot), variant: filter(current.variant), } as Tweak.Type<TContract>; } return { token: merge(acc.token ?? {}, current.token ?? {}, override), slot: merge(acc.slot ?? {}, current.slot ?? {}, override), variant: { ...filter(acc.variant), ...filter(current.variant), }, } as Tweak.Type<TContract>; }, root); } const merge = ( acc: Record<string, What.AnyOverride<Contract.Any> | undefined>, current: Record<string, What.AnyOverride<Contract.Any> | undefined>, override: boolean, ): Record<string, What.AnyOverride<Contract.Any>> => { const result = filter({ ...acc, }); Object.entries(filter(current)).forEach(([key, value]) => { if (!value) { return; } // Check both top-level override and What-level override const hasOverride = override || value.override === true; if (hasOverride) { result[key] = value; return; } if (!result[key]) { result[key] = value; return; } if ("class" in result[key] && "class" in value) { result[key].class = [ result[key].class, value.class, ]; } if ("token" in result[key] && "token" in value) { result[key].token = dedupe([ ...result[key].token, ...value.token, ]); } }); return result as Record<string, What.AnyOverride<Contract.Any>>; };