diff --git a/.changeset/style-dictionary-token-output.md b/.changeset/style-dictionary-token-output.md new file mode 100644 index 0000000..44aaece --- /dev/null +++ b/.changeset/style-dictionary-token-output.md @@ -0,0 +1,5 @@ +--- +"@hebilicious/cssforge": minor +--- + +Add Style Dictionary-compatible token JSON output with CSS variable metadata, resolved values, reference paths, and CLI support through `--mode style-dictionary` and `--style-dictionary`. diff --git a/README.md b/README.md index aeb4c99..33b5a46 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,17 @@ import { cssForge } from "./.cssforge/output.ts"; export { cssForge }; ``` +5. Use the Style Dictionary-compatible token JSON in token-aware tools: + +CSS Forge can also generate token JSON where each leaf keeps the CSS variable reference +as `value`, the fully resolved value as `$resolvedValue`, and provenance metadata under +`attributes`. This is useful for tools such as Musea that scan component styles for +`var(--token)` usage while still needing resolved values for labels and swatches. + +```bash +pnpm run cssforge -- --mode style-dictionary --style-dictionary ./.cssforge/tokens.sd.json +``` + ## Configuration ### Colors @@ -1027,8 +1038,11 @@ pnpm run cssforge # Watch mode pnpm run cssforge -- --watch -# Custom paths and output -pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --mode all +# Custom paths and output +pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --style-dictionary ./dist/design-tokens.sd.json --mode all + +# Style Dictionary-compatible JSON only +pnpm run cssforge -- --mode style-dictionary --style-dictionary ./dist/design-tokens.sd.json ``` ## Programmatic Usage @@ -1036,10 +1050,13 @@ pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-token You can also use CSS Forge programmatically: ```typescript -import { generateCSS } from "jsr:@hebilicious/cssforge"; +import { generateCSS, generateStyleDictionaryJSON } from "jsr:@hebilicious/cssforge"; // Generate CSS string const css = generateCSS(config); + +// Generate Style Dictionary-compatible token JSON +const tokens = generateStyleDictionaryJSON(config); ``` ## Agentic usage diff --git a/packages/cssforge/README.md b/packages/cssforge/README.md index aeb4c99..33b5a46 100644 --- a/packages/cssforge/README.md +++ b/packages/cssforge/README.md @@ -179,6 +179,17 @@ import { cssForge } from "./.cssforge/output.ts"; export { cssForge }; ``` +5. Use the Style Dictionary-compatible token JSON in token-aware tools: + +CSS Forge can also generate token JSON where each leaf keeps the CSS variable reference +as `value`, the fully resolved value as `$resolvedValue`, and provenance metadata under +`attributes`. This is useful for tools such as Musea that scan component styles for +`var(--token)` usage while still needing resolved values for labels and swatches. + +```bash +pnpm run cssforge -- --mode style-dictionary --style-dictionary ./.cssforge/tokens.sd.json +``` + ## Configuration ### Colors @@ -1027,8 +1038,11 @@ pnpm run cssforge # Watch mode pnpm run cssforge -- --watch -# Custom paths and output -pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --mode all +# Custom paths and output +pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-tokens.css --ts ./dist/design-tokens.ts --json ./dist/design-tokens.json --style-dictionary ./dist/design-tokens.sd.json --mode all + +# Style Dictionary-compatible JSON only +pnpm run cssforge -- --mode style-dictionary --style-dictionary ./dist/design-tokens.sd.json ``` ## Programmatic Usage @@ -1036,10 +1050,13 @@ pnpm run cssforge -- --config ./foo/bar/custom-path.ts --css ./dist/design-token You can also use CSS Forge programmatically: ```typescript -import { generateCSS } from "jsr:@hebilicious/cssforge"; +import { generateCSS, generateStyleDictionaryJSON } from "jsr:@hebilicious/cssforge"; // Generate CSS string const css = generateCSS(config); + +// Generate Style Dictionary-compatible token JSON +const tokens = generateStyleDictionaryJSON(config); ``` ## Agentic usage diff --git a/packages/cssforge/src/cli.ts b/packages/cssforge/src/cli.ts index bb57706..cf7ea81 100644 --- a/packages/cssforge/src/cli.ts +++ b/packages/cssforge/src/cli.ts @@ -14,7 +14,12 @@ import type { CommandDef } from "citty"; */ import { defineCommand, runMain } from "citty"; import type { CSSForgeConfig } from "./config.ts"; -import { generateCSS, generateJSON, generateTS } from "./generator.ts"; +import { + generateCSS, + generateJSON, + generateStyleDictionaryJSON, + generateTS, +} from "./generator.ts"; const writeFileRecursive = (path: string, data: string) => fs @@ -28,11 +33,13 @@ export interface BuildOptions { /** Path to the configuration file. */ config: string; /** The output mode. */ - mode: "css" | "json" | "ts" | "all"; + mode: "css" | "json" | "ts" | "style-dictionary" | "all"; /** Path for the CSS output file. */ cssOutput: string; /** Path for the JSON output file. */ jsonOutput: string; + /** Path for the Style Dictionary-compatible JSON output file. */ + styleDictionaryOutput: string; /** Path for the TypeScript output file. */ tsOutput: string; } @@ -47,12 +54,14 @@ export async function build({ tsOutput, cssOutput, jsonOutput, + styleDictionaryOutput, mode, }: BuildOptions): Promise<{ success: boolean; error?: unknown }> { try { const absoluteconfig = resolve(process.cwd(), config); const absoluteCssOutput = resolve(process.cwd(), cssOutput); const absoluteJsonOutput = resolve(process.cwd(), jsonOutput); + const absoluteStyleDictionaryOutput = resolve(process.cwd(), styleDictionaryOutput); const absoluteTsOutput = resolve(process.cwd(), tsOutput); // Import config with cache busting @@ -75,6 +84,16 @@ export async function build({ console.log(`✔ Generated JSON written to ${jsonOutput}`); } + if (mode === "style-dictionary" || mode === "all") { + await writeFileRecursive( + absoluteStyleDictionaryOutput, + generateStyleDictionaryJSON(userConfig.default as CSSForgeConfig), + ); + console.log( + `✔ Generated Style Dictionary JSON written to ${styleDictionaryOutput}`, + ); + } + if (mode === "ts" || mode === "all") { await writeFileRecursive( absoluteTsOutput, @@ -147,7 +166,7 @@ const mainCommand = defineCommand({ }, mode: { type: "string", - description: "Output mode (css, json, ts, all)", + description: "Output mode (css, json, ts, style-dictionary, all)", alias: "m", default: "all", }, @@ -161,6 +180,11 @@ const mainCommand = defineCommand({ description: "Optional path for an output JSON file", default: "./.cssforge/output.json", }, + "style-dictionary": { + type: "string", + description: "Path for the Style Dictionary-compatible JSON output file", + default: "./.cssforge/tokens.sd.json", + }, css: { type: "string", description: "Path for the output CSS file", @@ -174,6 +198,7 @@ const mainCommand = defineCommand({ }, async run({ args }) { const { watch: shouldWatch, config, css, json, ts, mode, prefix } = args; + const styleDictionary = args["style-dictionary"]; const realPath = (p: string) => resolve(prefix, p); const settings = { mode, @@ -181,6 +206,7 @@ const mainCommand = defineCommand({ cssOutput: realPath(css), tsOutput: realPath(ts), jsonOutput: realPath(json), + styleDictionaryOutput: realPath(styleDictionary), } as BuildOptions; if (shouldWatch) { const cleanup = await watch(settings); diff --git a/packages/cssforge/src/generator.ts b/packages/cssforge/src/generator.ts index e7a934e..67bd3d0 100644 --- a/packages/cssforge/src/generator.ts +++ b/packages/cssforge/src/generator.ts @@ -1,5 +1,5 @@ import type { CSSForgeConfig } from "./config.ts"; -import type { ResolveMap } from "./lib.ts"; +import type { ResolvedToken, ResolveMap, TokenTier, TokenType } from "./lib.ts"; import { processColors } from "./modules/colors.ts"; import { processPrimitives } from "./modules/primitive.ts"; import { processSpacing } from "./modules/spacing.ts"; @@ -15,6 +15,38 @@ type ForgeValue = { [key: string]: ForgeValue | CssValue; }; +type StyleDictionaryToken = { + value: string; + type: TokenType; + description?: string; + attributes: { + cssVariable: string; + cssVariableReference: string; + resolvedValue: string; + sourcePath: string; + referencePaths?: string[]; + }; + $tier: TokenTier; + $reference?: string; + $resolvedValue: string; +}; + +type StyleDictionaryValue = { + [key: string]: StyleDictionaryValue | StyleDictionaryToken; +}; + +export interface StyleDictionaryJSONOptions { + /** + * Controls the top-level token `value`. + * + * `css-reference` is useful for tools that scan authored CSS using + * `var(--token)` values. `resolved` puts the fully resolved value in `value`. + * + * @default "css-reference" + */ + valueMode?: "css-reference" | "resolved"; +} + const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); @@ -31,6 +63,50 @@ const deepMerge = (target: ForgeValue, source: ForgeValue): ForgeValue => { return result as ForgeValue; }; +const deepMergeStyleDictionary = ( + target: StyleDictionaryValue, + source: StyleDictionaryValue, +): StyleDictionaryValue => { + const result: Record = { ...target }; + for (const [key, sourceValue] of Object.entries(source)) { + const targetValue = result[key]; + if (isRecord(targetValue) && isRecord(sourceValue)) { + result[key] = deepMergeStyleDictionary( + targetValue as StyleDictionaryValue, + sourceValue as StyleDictionaryValue, + ); + continue; + } + result[key] = sourceValue; + } + return result as StyleDictionaryValue; +}; + +const collectResolveMap = (config: Partial): ResolveMap => { + const forge = { + colors: config.colors ? processColors(config.colors) : undefined, + spacing: config.spacing ? processSpacing(config.spacing) : undefined, + typography: config.typography ? processTypography(config.typography) : undefined, + primitives: config.primitives + ? processPrimitives({ + primitives: config.primitives, + colors: config.colors, + typography: config.typography, + spacing: config.spacing, + }) + : undefined, + }; + + const resolveMap: ResolveMap = new Map(); + for (const value of Object.values(forge)) { + if (!value) continue; + for (const [path, token] of value.resolveMap.entries()) { + resolveMap.set(path, token); + } + } + return resolveMap; +}; + /** * Creates a nested object structure of design tokens from a configuration. * @param config The CSSForge configuration. @@ -88,30 +164,101 @@ export function createForgeValues(config: Partial) { }, {} as ForgeValue); } - const forge = { - colors: config.colors ? processColors(config.colors) : undefined, - spacing: config.spacing ? processSpacing(config.spacing) : undefined, - typography: config.typography ? processTypography(config.typography) : undefined, - primitives: config.primitives - ? processPrimitives({ - primitives: config.primitives, - colors: config.colors, - typography: config.typography, - spacing: config.spacing, - }) - : undefined, - }; - const jsonKeys = []; - for (const [_, value] of Object.entries(forge)) { - if (value) { - const mapArray = [...value.resolveMap.entries()]; - jsonKeys.push(...mapArray); - } - } + const jsonKeys = [...collectResolveMap(config).entries()].map( + ([path, token]) => + [ + path, + { key: token.key, value: token.value, variable: token.variable }, + ] satisfies Input, + ); const forgeValues = createForgeValuesFromKeys(jsonKeys); return forgeValues; } +const toStyleDictionaryPath = (sourcePath: string) => sourcePath.replaceAll("@", "."); + +const createNestedStyleDictionaryObject = ( + path: string[], + value: StyleDictionaryToken | StyleDictionaryValue, +): StyleDictionaryValue => { + if (path.length === 0) { + return value as StyleDictionaryValue; + } + + const [currentKey, ...remainingPath] = path; + + return { + [currentKey]: createNestedStyleDictionaryObject(remainingPath, value), + }; +}; + +const resolveTokenValue = ( + token: ResolvedToken, + tokensByCssVariable: Map, + seen: Set = new Set(), +): string => + token.value.replace(/var\(\s*(--[\w-]+)\s*\)/g, (match, cssVariable: string) => { + if (seen.has(cssVariable)) return match; + + const referencedToken = tokensByCssVariable.get(cssVariable); + if (!referencedToken) return match; + + return resolveTokenValue( + referencedToken, + tokensByCssVariable, + new Set([...seen, cssVariable]), + ); + }); + +/** + * Generates a Style Dictionary-compatible JSON string from the CSSForge configuration. + * + * The default output is optimized for CSS variable usage scanners: token leaves use + * `value: "var(--token)"` while the fully resolved value is preserved in + * `$resolvedValue` and `attributes.resolvedValue`. + */ +export function generateStyleDictionaryJSON( + config: Partial, + options: StyleDictionaryJSONOptions = {}, +): string { + const valueMode = options.valueMode ?? "css-reference"; + const resolveMap = collectResolveMap(config); + const tokensByCssVariable = new Map( + [...resolveMap.values()].map((token) => [token.key, token]), + ); + + const styleDictionaryValues = [...resolveMap.entries()].reduce( + (acc, [sourcePath, token]) => { + const resolvedValue = resolveTokenValue(token, tokensByCssVariable); + const cssVariableReference = `var(${token.key})`; + const referencePaths = token.referencePaths?.map(toStyleDictionaryPath); + const tier = token.tier ?? (referencePaths?.length ? "semantic" : "primitive"); + const styleDictionaryToken: StyleDictionaryToken = { + value: valueMode === "resolved" ? resolvedValue : cssVariableReference, + type: token.type, + attributes: { + cssVariable: token.key, + cssVariableReference, + resolvedValue, + sourcePath: token.sourcePath, + ...(referencePaths ? { referencePaths } : {}), + }, + $tier: tier, + ...(referencePaths?.[0] ? { $reference: referencePaths[0] } : {}), + $resolvedValue: resolvedValue, + }; + const nestedObject = createNestedStyleDictionaryObject( + toStyleDictionaryPath(sourcePath).split("."), + styleDictionaryToken, + ); + return deepMergeStyleDictionary(acc, nestedObject); + }, + {} as StyleDictionaryValue, + ); + + return JSON.stringify(styleDictionaryValues, null, 2); +} + /** * Generates a JSON string from the CSSForge configuration. * This can be used to create a JSON file with all the design tokens. diff --git a/packages/cssforge/src/lib.ts b/packages/cssforge/src/lib.ts index 0a0aee5..73a1b95 100644 --- a/packages/cssforge/src/lib.ts +++ b/packages/cssforge/src/lib.ts @@ -1,8 +1,42 @@ +export type TokenType = + | "color" + | "gradient" + | "spacing" + | "typography" + | "primitive" + | "component"; + +export type TokenTier = "primitive" | "semantic"; + +/** + * Metadata carried through generation so alternate outputs can preserve token + * provenance without changing the generated CSS. + */ +export interface TokenMetadata { + /** The original cssforge path used to resolve this token. */ + sourcePath: string; + /** cssforge paths referenced while composing this token. */ + referencePaths?: string[]; + /** The broad token category for Style Dictionary-compatible outputs. */ + type: TokenType; + /** Primitive tokens have no source reference; semantic tokens alias other tokens. */ + tier?: TokenTier; +} + +export interface ResolvedToken extends TokenMetadata { + /** CSS custom property name, e.g. `--theme-light-content-primary`. */ + key: string; + /** The generated CSS value. This may contain `var(...)` references. */ + value: string; + /** The full CSS declaration. */ + variable: string; +} + /** - * A map where keys are dot-separated paths and values are objects - * containing the CSS variable key, its value, and the full variable declaration. + * A map where keys are cssforge paths and values are objects containing the CSS + * variable key, generated value, full declaration, and token metadata. */ -export type ResolveMap = Map; +export type ResolveMap = Map; /** * Represents the output of a processing function, containing the generated @@ -32,6 +66,34 @@ interface ResolveVariableParams extends Modules { interface GetResolvedVariablesMapParams extends Modules { variables: Variables | undefined; } + +/** + * Normalizes historical config paths that included `value` wrapper segments. + */ +export const normalizeTokenPath = (varPath: string): string => { + const path = varPath.split("."); + const [module, ...parts] = path; + + if (!module) return varPath; + + if ( + (module === "palette" || module === "gradients" || module === "theme") && + parts[0] === "value" + ) { + return [module, ...parts.slice(1)].join("."); + } + + if (module === "spacing" && parts[0] === "custom" && parts[2] === "value") { + return [module, parts[0], parts[1], ...parts.slice(3)].join("."); + } + + if (module === "typography" && parts[0] === "weight" && parts[2] === "value") { + return [module, parts[0], parts[1], ...parts.slice(3)].join("."); + } + + return varPath; +}; + /** * Resolves a variable path to its corresponding CSS variable name. * This function is used to resolve references to other design tokens. @@ -52,7 +114,8 @@ function resolveVariable({ typography, spacing, }: ResolveVariableParams) { - const path = varPath.split("."); + const normalizedPath = normalizeTokenPath(varPath); + const path = normalizedPath.split("."); const module = path[0]; //TODO: Make this configurable const keyMap = { @@ -69,7 +132,7 @@ function resolveVariable({ case keyMap.gradients: case keyMap.theme: { if (!colors) throw new Error("The colors object must be passed."); - const result = colors.resolveMap.get(varPath); + const result = colors.resolveMap.get(normalizedPath); if (!result) { throw new Error( `The color path ${varPath} could not be resolved. Map contains ${Array.from( @@ -82,7 +145,7 @@ function resolveVariable({ case keyMap.typography_fluid: case keyMap.typography: { if (!typography) throw new Error("The typography object must be passed."); - const result = typography.resolveMap.get(varPath); + const result = typography.resolveMap.get(normalizedPath); if (!result) { throw new Error( `The typography path ${varPath} could not be resolved. Map contains ${Array.from( @@ -95,7 +158,7 @@ function resolveVariable({ case keyMap.spacing: case keyMap.spacing_fluid: { if (!spacing) throw new Error("The spacing object must be passed."); - const result = spacing.resolveMap.get(varPath); + const result = spacing.resolveMap.get(normalizedPath); if (!result) { throw new Error( `The spacing path ${varPath} could not be resolved. Map contains ${Array.from( @@ -129,6 +192,28 @@ export const getResolvedVariablesMap = ({ return resolvedMap; }; +/** + * Returns the cssforge reference paths actually used by `var(--alias)` calls in a value. + */ +export const getReferencePaths = ({ + value, + variables, +}: { + value: string; + variables: Variables | undefined; +}): string[] | undefined => { + if (!variables) return undefined; + + const referencePaths = Array.from(value.matchAll(/var\(--([\w-]+)\)/g)) + .map(([, key]) => { + const path = variables[key]; + return path ? normalizeTokenPath(path) : undefined; + }) + .filter((path): path is string => Boolean(path)); + + return referencePaths.length > 0 ? Array.from(new Set(referencePaths)) : undefined; +}; + /** * Resolves a CSS variable value using a map of variable paths. * It replaces occurrences of `var(--key)` in the value with the corresponding diff --git a/packages/cssforge/src/mod.ts b/packages/cssforge/src/mod.ts index d60f73e..68148b0 100644 --- a/packages/cssforge/src/mod.ts +++ b/packages/cssforge/src/mod.ts @@ -9,7 +9,7 @@ import type { CSSForgeConfig } from "./config.ts"; import { defineConfig } from "./config.ts"; -import { generateCSS } from "./generator.ts"; +import { generateCSS, generateStyleDictionaryJSON } from "./generator.ts"; import { processColors } from "./modules/colors.ts"; import { processPrimitives } from "./modules/primitive.ts"; import { processSpacing } from "./modules/spacing.ts"; @@ -18,6 +18,7 @@ import { processTypography } from "./modules/typography.ts"; * The main configuration object for CSSForge. */ export type { CSSForgeConfig }; +export type { StyleDictionaryJSONOptions } from "./generator.ts"; export { /** @@ -32,6 +33,12 @@ export { * @returns The generated CSS string. */ generateCSS, + /** + * Generates a Style Dictionary-compatible JSON string from a CSSForge configuration. + * @param config The CSSForge configuration. + * @returns The generated Style Dictionary-compatible JSON string. + */ + generateStyleDictionaryJSON, /** * Processes the colors section of the configuration. * @param colors The colors configuration. diff --git a/packages/cssforge/src/modules/colors.ts b/packages/cssforge/src/modules/colors.ts index 06fbb70..48c4515 100644 --- a/packages/cssforge/src/modules/colors.ts +++ b/packages/cssforge/src/modules/colors.ts @@ -2,6 +2,7 @@ import Color from "colorjs.io"; import { validateName } from "../helpers.ts"; import type { Variables } from "../lib.ts"; import { + getReferencePaths, getResolvedVariablesMap, type Output, type ResolveMap, @@ -64,11 +65,13 @@ export interface PaletteColorConfig { settings?: PaletteColorSettings; } +type PaletteColorEntry = PaletteColorConfig | ColorVariants; + /** * A palette of colors, organized by name and variants. */ export interface ColorPalette { - [key: string]: PaletteColorConfig; + [key: string]: PaletteColorEntry; } interface GradientDefinition { @@ -144,11 +147,45 @@ export interface ColorConfig { /** * A collection of themes. */ - theme?: { - [themeName: string]: ThemeConfig; - }; + theme?: + | { + [themeName: string]: ThemeConfig; + } + | { + value: { + [themeName: string]: ThemeConfig | ThemeConfig["value"]; + }; + }; } +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const isColorValueObject = (value: unknown): value is ColorValue => + isRecord(value) && + ("hex" in value || "rgb" in value || "hsl" in value || "oklch" in value); + +const getPaletteColorConfig = (entry: PaletteColorEntry): PaletteColorConfig => { + if (isRecord(entry) && isRecord(entry.value) && !isColorValueObject(entry.value)) { + return entry as unknown as PaletteColorConfig; + } + + return { value: entry as unknown as ColorVariants }; +}; + +const getThemeConfig = (theme: ColorConfig["theme"]): ColorTheme | undefined => { + if (!theme) return undefined; + const themes = "value" in theme ? theme.value : theme; + return Object.fromEntries( + Object.entries(themes).map(([themeName, themeConfig]) => [ + themeName, + "value" in themeConfig + ? themeConfig + : { value: themeConfig as ThemeConfig["value"] }, + ]), + ); +}; + /** * Gets the color string from a color value object. * @example @@ -294,9 +331,13 @@ export function processColors(colors: ColorConfig): Output { validateName(colorName); try { - const handler = conditionalBuilder(colorConfig.settings, `/* ${colorName} */`); + const normalizedColorConfig = getPaletteColorConfig(colorConfig); + const handler = conditionalBuilder( + normalizedColorConfig.settings, + `/* ${colorName} */`, + ); - for (const [variantId, colorValue] of Object.entries(colorConfig.value)) { + for (const [variantId, colorValue] of Object.entries(normalizedColorConfig.value)) { validateName(variantId); const key = `--${moduleKey}-${colorName}-${variantId}`; const value = colorValueToOklch(colorValue); @@ -308,6 +349,9 @@ export function processColors(colors: ColorConfig): Output { key, value, variable, + sourcePath: `${moduleKey}.${colorName}.${variantId}`, + type: "color", + tier: "primitive", }); } @@ -338,6 +382,7 @@ export function processColors(colors: ColorConfig): Output { }); const gradientValue = resolveValue({ map: resolvedMapForGradient, value }); + const referencePaths = getReferencePaths({ value, variables }); const key = `--${moduleKey}-${gradientName}-${variantName}`; const variable = `${key}: ${gradientValue};`; @@ -348,6 +393,10 @@ export function processColors(colors: ColorConfig): Output { variable, key, value: gradientValue, + sourcePath: `${moduleKey}.${gradientName}.${variantName}`, + ...(referencePaths ? { referencePaths } : {}), + type: "gradient", + tier: referencePaths ? "semantic" : "primitive", }); } catch (error) { console.error( @@ -362,7 +411,9 @@ export function processColors(colors: ColorConfig): Output { } } - if (colors.theme) { + const themes = getThemeConfig(colors.theme); + + if (themes) { rootOutput.push(`/* Themes */`); const moduleKey = "theme"; const palette = { @@ -370,7 +421,7 @@ export function processColors(colors: ColorConfig): Output { resolveMap, }; - for (const [themeName, themeConfig] of Object.entries(colors.theme)) { + for (const [themeName, themeConfig] of Object.entries(themes)) { validateName(themeName); const handler = conditionalBuilder( themeConfig.settings, @@ -395,6 +446,10 @@ export function processColors(colors: ColorConfig): Output { map: resolvedMap, value: variantValue, }); + const referencePaths = getReferencePaths({ + value: variantValue, + variables: colorInTheme.variables, + }); const key = variantNameOnly ? `--${variantName}` @@ -408,6 +463,10 @@ export function processColors(colors: ColorConfig): Output { key, value: resolvedValue, variable, + sourcePath: `${moduleKey}.${themeName}.${colorName}.${variantName}`, + ...(referencePaths ? { referencePaths } : {}), + type: "color", + tier: referencePaths ? "semantic" : "primitive", }); } } diff --git a/packages/cssforge/src/modules/primitive.ts b/packages/cssforge/src/modules/primitive.ts index 03338a8..3fb7677 100644 --- a/packages/cssforge/src/modules/primitive.ts +++ b/packages/cssforge/src/modules/primitive.ts @@ -1,5 +1,6 @@ import { pxToRem, validateName } from "../helpers.ts"; import { + getReferencePaths, getResolvedVariablesMap, type Output, resolveValue, @@ -113,6 +114,7 @@ export function processPrimitives(config: { : propValue; const resolvedValue = resolveValue({ map: resolvedMap, value: convertedValue }); + const referencePaths = getReferencePaths({ value: convertedValue, variables }); const key = `--${primitiveName}-${variantName}-${propName}`; const variable = `${key}: ${resolvedValue};`; @@ -121,6 +123,10 @@ export function processPrimitives(config: { key, value: resolvedValue, variable, + sourcePath: `${moduleKey}.${primitiveName}.${variantName}.${propName}`, + ...(referencePaths ? { referencePaths } : {}), + type: "component", + tier: referencePaths ? "semantic" : "primitive", }); } } catch (error) { diff --git a/packages/cssforge/src/modules/spacing.ts b/packages/cssforge/src/modules/spacing.ts index c321cab..d6991e7 100644 --- a/packages/cssforge/src/modules/spacing.ts +++ b/packages/cssforge/src/modules/spacing.ts @@ -95,6 +95,9 @@ export function processSpacing(spacing: SpacingConfig): Output { variable: cssVar, key: variableName, value: clamp, + sourcePath: `${moduleKey}.${scaleName}@${label}`, + type: "spacing", + tier: "primitive", }); } } @@ -119,6 +122,9 @@ export function processSpacing(spacing: SpacingConfig): Output { variable, key: varName, value: convertedValue, + sourcePath: `${moduleKey}.custom.${scaleName}.${scaleKey}`, + type: "spacing", + tier: "primitive", }); } } diff --git a/packages/cssforge/src/modules/typography.ts b/packages/cssforge/src/modules/typography.ts index 6c44b1f..ca0e41a 100644 --- a/packages/cssforge/src/modules/typography.ts +++ b/packages/cssforge/src/modules/typography.ts @@ -95,6 +95,9 @@ export function processTypography(config: TypographyConfig): Output { variable, key, value: clamp, + sourcePath: `${moduleKey}.${scaleName}@${resolvedLabel}`, + type: "typography", + tier: "primitive", }); } } @@ -114,6 +117,9 @@ export function processTypography(config: TypographyConfig): Output { variable, key, value: weightValue, + sourcePath: `${moduleKey}.weight.${weightName}.${token}`, + type: "typography", + tier: "primitive", }); } } diff --git a/packages/cssforge/tests/__snapshots__/colors.test.ts.snap b/packages/cssforge/tests/__snapshots__/colors.test.ts.snap index 8266dc5..f991718 100644 --- a/packages/cssforge/tests/__snapshots__/colors.test.ts.snap +++ b/packages/cssforge/tests/__snapshots__/colors.test.ts.snap @@ -13,6 +13,9 @@ exports[`processColors - converts hex to oklch 2`] = ` "palette.coral.100", { "key": "--palette-coral-100", + "sourcePath": "palette.coral.100", + "tier": "primitive", + "type": "color", "value": "oklch(73.511% 0.16799 40.24666)", "variable": "--palette-coral-100: oklch(73.511% 0.16799 40.24666);", }, @@ -21,6 +24,9 @@ exports[`processColors - converts hex to oklch 2`] = ` "palette.coral.200", { "key": "--palette-coral-200", + "sourcePath": "palette.coral.200", + "tier": "primitive", + "type": "color", "value": "oklch(69.622% 0.19552 32.32143)", "variable": "--palette-coral-200: oklch(69.622% 0.19552 32.32143);", }, @@ -47,6 +53,9 @@ exports[`processColors - generates gradient with color variables 2`] = ` "palette.coral.50", { "key": "--palette-coral-50", + "sourcePath": "palette.coral.50", + "tier": "primitive", + "type": "color", "value": "oklch(73.58% 0.16378 34.33822)", "variable": "--palette-coral-50: oklch(73.58% 0.16378 34.33822);", }, @@ -55,6 +64,9 @@ exports[`processColors - generates gradient with color variables 2`] = ` "palette.coral.90", { "key": "--palette-coral-90", + "sourcePath": "palette.coral.90", + "tier": "primitive", + "type": "color", "value": "oklch(73.341% 0.17338 44.64289)", "variable": "--palette-coral-90: oklch(73.341% 0.17338 44.64289);", }, @@ -63,6 +75,9 @@ exports[`processColors - generates gradient with color variables 2`] = ` "palette.coral.100", { "key": "--palette-coral-100", + "sourcePath": "palette.coral.100", + "tier": "primitive", + "type": "color", "value": "oklch(69.622% 0.19552 32.32143)", "variable": "--palette-coral-100: oklch(69.622% 0.19552 32.32143);", }, @@ -71,6 +86,9 @@ exports[`processColors - generates gradient with color variables 2`] = ` "palette.indigo.100", { "key": "--palette-indigo-100", + "sourcePath": "palette.indigo.100", + "tier": "primitive", + "type": "color", "value": "oklch(51.057% 0.23005 276.96564)", "variable": "--palette-indigo-100: oklch(51.057% 0.23005 276.96564);", }, @@ -79,6 +97,15 @@ exports[`processColors - generates gradient with color variables 2`] = ` "gradients.orangeGradient.primary", { "key": "--gradients-orangeGradient-primary", + "referencePaths": [ + "palette.coral.50", + "palette.coral.90", + "palette.coral.100", + "palette.indigo.100", + ], + "sourcePath": "gradients.orangeGradient.primary", + "tier": "semantic", + "type": "gradient", "value": "linear-gradient(261.78deg, var(--palette-coral-50) 33.1%, var(--palette-coral-90) 56.3%, var(--palette-coral-100) 65.78%, var(--palette-indigo-100) 84.23%)", "variable": "--gradients-orangeGradient-primary: linear-gradient(261.78deg, var(--palette-coral-50) 33.1%, var(--palette-coral-90) 56.3%, var(--palette-coral-100) 65.78%, var(--palette-indigo-100) 84.23%);", }, @@ -101,6 +128,9 @@ exports[`processColors - handles different color formats 2`] = ` "palette.brand.200", { "key": "--palette-brand-200", + "sourcePath": "palette.brand.200", + "tier": "primitive", + "type": "color", "value": "oklch(86.644% 0.29483 142.49535)", "variable": "--palette-brand-200: oklch(86.644% 0.29483 142.49535);", }, @@ -109,6 +139,9 @@ exports[`processColors - handles different color formats 2`] = ` "palette.brand.300", { "key": "--palette-brand-300", + "sourcePath": "palette.brand.300", + "tier": "primitive", + "type": "color", "value": "oklch(45.201% 0.31321 264.05202)", "variable": "--palette-brand-300: oklch(45.201% 0.31321 264.05202);", }, @@ -117,6 +150,9 @@ exports[`processColors - handles different color formats 2`] = ` "palette.brand.400", { "key": "--palette-brand-400", + "sourcePath": "palette.brand.400", + "tier": "primitive", + "type": "color", "value": "oklch(70% 0.2 270)", "variable": "--palette-brand-400: oklch(70% 0.2 270);", }, @@ -125,6 +161,9 @@ exports[`processColors - handles different color formats 2`] = ` "palette.brand.default", { "key": "--palette-brand-default", + "sourcePath": "palette.brand.default", + "tier": "primitive", + "type": "color", "value": "oklch(62.796% 0.25768 29.23388)", "variable": "--palette-brand-default: oklch(62.796% 0.25768 29.23388);", }, @@ -132,6 +171,71 @@ exports[`processColors - handles different color formats 2`] = ` ] `; +exports[`processColors - handles direct palette groups 1`] = ` +"/* Palette */ +/* basic */ +--palette-basic-white: oklch(100% 0 0); +--palette-basic-black: oklch(0% 0 0); +/* gray */ +--palette-gray-950: oklch(14.479% 0 0); +/* Themes */ +/* Theme: light */ +/* content */ +--theme-light-content-primary: var(--palette-gray-950);" +`; + +exports[`processColors - handles direct palette groups 2`] = ` +[ + [ + "palette.basic.white", + { + "key": "--palette-basic-white", + "sourcePath": "palette.basic.white", + "tier": "primitive", + "type": "color", + "value": "oklch(100% 0 0)", + "variable": "--palette-basic-white: oklch(100% 0 0);", + }, + ], + [ + "palette.basic.black", + { + "key": "--palette-basic-black", + "sourcePath": "palette.basic.black", + "tier": "primitive", + "type": "color", + "value": "oklch(0% 0 0)", + "variable": "--palette-basic-black: oklch(0% 0 0);", + }, + ], + [ + "palette.gray.950", + { + "key": "--palette-gray-950", + "sourcePath": "palette.gray.950", + "tier": "primitive", + "type": "color", + "value": "oklch(14.479% 0 0)", + "variable": "--palette-gray-950: oklch(14.479% 0 0);", + }, + ], + [ + "theme.light.content.primary", + { + "key": "--theme-light-content-primary", + "referencePaths": [ + "palette.gray.950", + ], + "sourcePath": "theme.light.content.primary", + "tier": "semantic", + "type": "color", + "value": "var(--palette-gray-950)", + "variable": "--theme-light-content-primary: var(--palette-gray-950);", + }, + ], +] +`; + exports[`processColors - handles gradients with atRule 1`] = ` "/* Palette */ /* coral */ @@ -149,6 +253,9 @@ exports[`processColors - handles gradients with atRule 2`] = ` "palette.coral.50", { "key": "--palette-coral-50", + "sourcePath": "palette.coral.50", + "tier": "primitive", + "type": "color", "value": "oklch(73.58% 0.16378 34.33822)", "variable": "--palette-coral-50: oklch(73.58% 0.16378 34.33822);", }, @@ -157,6 +264,12 @@ exports[`processColors - handles gradients with atRule 2`] = ` "gradients.orangeGradient.primary", { "key": "--gradients-orangeGradient-primary", + "referencePaths": [ + "palette.coral.50", + ], + "sourcePath": "gradients.orangeGradient.primary", + "tier": "semantic", + "type": "gradient", "value": "linear-gradient(to right, var(--palette-coral-50), var(--palette-coral-50))", "variable": "--gradients-orangeGradient-primary: linear-gradient(to right, var(--palette-coral-50), var(--palette-coral-50));", }, @@ -180,6 +293,9 @@ exports[`processColors - handles palette colors with atRule 2`] = ` "palette.background.light", { "key": "--palette-background-light", + "sourcePath": "palette.background.light", + "tier": "primitive", + "type": "color", "value": "oklch(100% 0 0)", "variable": "--palette-background-light: oklch(100% 0 0);", }, @@ -188,6 +304,9 @@ exports[`processColors - handles palette colors with atRule 2`] = ` "palette.backgroundDark.dark", { "key": "--palette-backgroundDark-dark", + "sourcePath": "palette.backgroundDark.dark", + "tier": "primitive", + "type": "color", "value": "oklch(0% 0 0)", "variable": "--palette-backgroundDark-dark: oklch(0% 0 0);", }, @@ -215,6 +334,9 @@ exports[`processColors - handles selector + atRule 2`] = ` "palette.base.primary", { "key": "--palette-base-primary", + "sourcePath": "palette.base.primary", + "tier": "primitive", + "type": "color", "value": "oklch(73.511% 0.16799 40.24666)", "variable": "--palette-base-primary: oklch(73.511% 0.16799 40.24666);", }, @@ -223,6 +345,12 @@ exports[`processColors - handles selector + atRule 2`] = ` "theme.dark.background.primary", { "key": "--theme-dark-background-primary", + "referencePaths": [ + "palette.base.primary", + ], + "sourcePath": "theme.dark.background.primary", + "tier": "semantic", + "type": "color", "value": "var(--palette-base-primary)", "variable": "--theme-dark-background-primary: var(--palette-base-primary);", }, @@ -243,6 +371,9 @@ exports[`processColors - handles string values 2`] = ` "palette.simple.white", { "key": "--palette-simple-white", + "sourcePath": "palette.simple.white", + "tier": "primitive", + "type": "color", "value": "oklch(100% 0 0)", "variable": "--palette-simple-white: oklch(100% 0 0);", }, @@ -251,6 +382,9 @@ exports[`processColors - handles string values 2`] = ` "palette.simple.black", { "key": "--palette-simple-black", + "sourcePath": "palette.simple.black", + "tier": "primitive", + "type": "color", "value": "oklch(0% 0 0)", "variable": "--palette-simple-black: oklch(0% 0 0);", }, @@ -276,6 +410,9 @@ exports[`processColors - handles theme variantNameOnly 2`] = ` "palette.simple.white", { "key": "--palette-simple-white", + "sourcePath": "palette.simple.white", + "tier": "primitive", + "type": "color", "value": "oklch(100% 0 0)", "variable": "--palette-simple-white: oklch(100% 0 0);", }, @@ -284,6 +421,9 @@ exports[`processColors - handles theme variantNameOnly 2`] = ` "palette.simple.black", { "key": "--palette-simple-black", + "sourcePath": "palette.simple.black", + "tier": "primitive", + "type": "color", "value": "oklch(0% 0 0)", "variable": "--palette-simple-black: oklch(0% 0 0);", }, @@ -292,6 +432,12 @@ exports[`processColors - handles theme variantNameOnly 2`] = ` "theme.light.background.primary", { "key": "--primary", + "referencePaths": [ + "palette.simple.white", + ], + "sourcePath": "theme.light.background.primary", + "tier": "semantic", + "type": "color", "value": "var(--palette-simple-white)", "variable": "--primary: var(--palette-simple-white);", }, @@ -300,6 +446,12 @@ exports[`processColors - handles theme variantNameOnly 2`] = ` "theme.light.background.secondary", { "key": "--secondary", + "referencePaths": [ + "palette.simple.black", + ], + "sourcePath": "theme.light.background.secondary", + "tier": "semantic", + "type": "color", "value": "var(--palette-simple-black)", "variable": "--secondary: var(--palette-simple-black);", }, @@ -325,6 +477,9 @@ exports[`processColors - handles themes 2`] = ` "palette.simple.white", { "key": "--palette-simple-white", + "sourcePath": "palette.simple.white", + "tier": "primitive", + "type": "color", "value": "oklch(100% 0 0)", "variable": "--palette-simple-white: oklch(100% 0 0);", }, @@ -333,6 +488,9 @@ exports[`processColors - handles themes 2`] = ` "palette.simple.black", { "key": "--palette-simple-black", + "sourcePath": "palette.simple.black", + "tier": "primitive", + "type": "color", "value": "oklch(0% 0 0)", "variable": "--palette-simple-black: oklch(0% 0 0);", }, @@ -341,6 +499,12 @@ exports[`processColors - handles themes 2`] = ` "theme.light.background.primary", { "key": "--theme-light-background-primary", + "referencePaths": [ + "palette.simple.white", + ], + "sourcePath": "theme.light.background.primary", + "tier": "semantic", + "type": "color", "value": "var(--palette-simple-white)", "variable": "--theme-light-background-primary: var(--palette-simple-white);", }, @@ -349,6 +513,12 @@ exports[`processColors - handles themes 2`] = ` "theme.light.background.secondary", { "key": "--theme-light-background-secondary", + "referencePaths": [ + "palette.simple.black", + ], + "sourcePath": "theme.light.background.secondary", + "tier": "semantic", + "type": "color", "value": "var(--palette-simple-black)", "variable": "--theme-light-background-secondary: var(--palette-simple-black);", }, @@ -375,6 +545,9 @@ exports[`processColors - handles themes referencing gradients 2`] = ` "palette.coral.50", { "key": "--palette-coral-50", + "sourcePath": "palette.coral.50", + "tier": "primitive", + "type": "color", "value": "oklch(73.58% 0.16378 34.33822)", "variable": "--palette-coral-50: oklch(73.58% 0.16378 34.33822);", }, @@ -383,6 +556,12 @@ exports[`processColors - handles themes referencing gradients 2`] = ` "gradients.orangeGradient.primary", { "key": "--gradients-orangeGradient-primary", + "referencePaths": [ + "palette.coral.50", + ], + "sourcePath": "gradients.orangeGradient.primary", + "tier": "semantic", + "type": "gradient", "value": "linear-gradient(to right, var(--palette-coral-50), var(--palette-coral-50))", "variable": "--gradients-orangeGradient-primary: linear-gradient(to right, var(--palette-coral-50), var(--palette-coral-50));", }, @@ -391,6 +570,12 @@ exports[`processColors - handles themes referencing gradients 2`] = ` "theme.light.background.primary", { "key": "--theme-light-background-primary", + "referencePaths": [ + "gradients.orangeGradient.primary", + ], + "sourcePath": "theme.light.background.primary", + "tier": "semantic", + "type": "color", "value": "var(--gradients-orangeGradient-primary)", "variable": "--theme-light-background-primary: var(--gradients-orangeGradient-primary);", }, @@ -416,6 +601,9 @@ exports[`processColors - handles themes with selector 2`] = ` "palette.base.primary", { "key": "--palette-base-primary", + "sourcePath": "palette.base.primary", + "tier": "primitive", + "type": "color", "value": "oklch(73.511% 0.16799 40.24666)", "variable": "--palette-base-primary: oklch(73.511% 0.16799 40.24666);", }, @@ -424,6 +612,12 @@ exports[`processColors - handles themes with selector 2`] = ` "theme.dark.background.primary", { "key": "--theme-dark-background-primary", + "referencePaths": [ + "palette.base.primary", + ], + "sourcePath": "theme.dark.background.primary", + "tier": "semantic", + "type": "color", "value": "var(--palette-base-primary)", "variable": "--theme-dark-background-primary: var(--palette-base-primary);", }, @@ -444,6 +638,9 @@ exports[`processColors - handles transparency 2`] = ` "palette.alpha.softGray1", { "key": "--palette-alpha-softGray1", + "sourcePath": "palette.alpha.softGray1", + "tier": "primitive", + "type": "color", "value": "oklch(14.48% 0 0 / 12%)", "variable": "--palette-alpha-softGray1: oklch(14.48% 0 0 / 12%);", }, @@ -452,6 +649,9 @@ exports[`processColors - handles transparency 2`] = ` "palette.alpha.softGray2", { "key": "--palette-alpha-softGray2", + "sourcePath": "palette.alpha.softGray2", + "tier": "primitive", + "type": "color", "value": "oklch(14.48% 0 0 / 24%)", "variable": "--palette-alpha-softGray2: oklch(14.48% 0 0 / 24%);", }, diff --git a/packages/cssforge/tests/__snapshots__/generator.test.ts.snap b/packages/cssforge/tests/__snapshots__/generator.test.ts.snap new file mode 100644 index 0000000..dd1ffc3 --- /dev/null +++ b/packages/cssforge/tests/__snapshots__/generator.test.ts.snap @@ -0,0 +1,112 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateStyleDictionaryJSON - preserves references and resolved values 1`] = ` +{ + "palette": { + "neutral": { + "0": { + "$resolvedValue": "oklch(100% 0 0)", + "$tier": "primitive", + "attributes": { + "cssVariable": "--palette-neutral-0", + "cssVariableReference": "var(--palette-neutral-0)", + "resolvedValue": "oklch(100% 0 0)", + "sourcePath": "palette.neutral.0", + }, + "type": "color", + "value": "var(--palette-neutral-0)", + }, + "900": { + "$resolvedValue": "oklch(17.764% 0 0)", + "$tier": "primitive", + "attributes": { + "cssVariable": "--palette-neutral-900", + "cssVariableReference": "var(--palette-neutral-900)", + "resolvedValue": "oklch(17.764% 0 0)", + "sourcePath": "palette.neutral.900", + }, + "type": "color", + "value": "var(--palette-neutral-900)", + }, + }, + }, + "primitives": { + "button": { + "default": { + "color": { + "$reference": "theme.light.content.primary", + "$resolvedValue": "oklch(17.764% 0 0)", + "$tier": "semantic", + "attributes": { + "cssVariable": "--button-default-color", + "cssVariableReference": "var(--button-default-color)", + "referencePaths": [ + "theme.light.content.primary", + ], + "resolvedValue": "oklch(17.764% 0 0)", + "sourcePath": "primitives.button.default.color", + }, + "type": "component", + "value": "var(--button-default-color)", + }, + "padding": { + "$reference": "spacing.custom.size.2", + "$resolvedValue": "0.5rem", + "$tier": "semantic", + "attributes": { + "cssVariable": "--button-default-padding", + "cssVariableReference": "var(--button-default-padding)", + "referencePaths": [ + "spacing.custom.size.2", + ], + "resolvedValue": "0.5rem", + "sourcePath": "primitives.button.default.padding", + }, + "type": "component", + "value": "var(--button-default-padding)", + }, + }, + }, + }, + "spacing": { + "custom": { + "size": { + "2": { + "$resolvedValue": "0.5rem", + "$tier": "primitive", + "attributes": { + "cssVariable": "--spacing-size-2", + "cssVariableReference": "var(--spacing-size-2)", + "resolvedValue": "0.5rem", + "sourcePath": "spacing.custom.size.2", + }, + "type": "spacing", + "value": "var(--spacing-size-2)", + }, + }, + }, + }, + "theme": { + "light": { + "content": { + "primary": { + "$reference": "palette.neutral.900", + "$resolvedValue": "oklch(17.764% 0 0)", + "$tier": "semantic", + "attributes": { + "cssVariable": "--theme-light-content-primary", + "cssVariableReference": "var(--theme-light-content-primary)", + "referencePaths": [ + "palette.neutral.900", + ], + "resolvedValue": "oklch(17.764% 0 0)", + "sourcePath": "theme.light.content.primary", + }, + "type": "color", + "value": "var(--theme-light-content-primary)", + }, + }, + }, + }, +} +`; diff --git a/packages/cssforge/tests/__snapshots__/primitive.test.ts.snap b/packages/cssforge/tests/__snapshots__/primitive.test.ts.snap index b18ba8c..21ef411 100644 --- a/packages/cssforge/tests/__snapshots__/primitive.test.ts.snap +++ b/packages/cssforge/tests/__snapshots__/primitive.test.ts.snap @@ -17,6 +17,9 @@ exports[`processPrimitives - processes button with variables 2`] = ` "primitives.button.small.width", { "key": "--button-small-width", + "sourcePath": "primitives.button.small.width", + "tier": "primitive", + "type": "component", "value": "120px", "variable": "--button-small-width: 120px;", }, @@ -25,6 +28,9 @@ exports[`processPrimitives - processes button with variables 2`] = ` "primitives.button.small.height", { "key": "--button-small-height", + "sourcePath": "primitives.button.small.height", + "tier": "primitive", + "type": "component", "value": "40px", "variable": "--button-small-height: 40px;", }, @@ -33,6 +39,12 @@ exports[`processPrimitives - processes button with variables 2`] = ` "primitives.button.small.fontSize", { "key": "--button-small-fontSize", + "referencePaths": [ + "typography_fluid.arial@m", + ], + "sourcePath": "primitives.button.small.fontSize", + "tier": "semantic", + "type": "component", "value": "var(--typography_fluid-arial-foo-m)", "variable": "--button-small-fontSize: var(--typography_fluid-arial-foo-m);", }, @@ -41,6 +53,9 @@ exports[`processPrimitives - processes button with variables 2`] = ` "primitives.button.small.radius", { "key": "--button-small-radius", + "sourcePath": "primitives.button.small.radius", + "tier": "primitive", + "type": "component", "value": "8px", "variable": "--button-small-radius: 8px;", }, @@ -49,6 +64,13 @@ exports[`processPrimitives - processes button with variables 2`] = ` "primitives.button.small.padding", { "key": "--button-small-padding", + "referencePaths": [ + "spacing.custom.size.2", + "spacing.custom.size.3", + ], + "sourcePath": "primitives.button.small.padding", + "tier": "semantic", + "type": "component", "value": "var(--spacing-size-2) var(--spacing-size-3)", "variable": "--button-small-padding: var(--spacing-size-2) var(--spacing-size-3);", }, @@ -74,6 +96,9 @@ exports[`processPrimitives - processes buttons with settings 2`] = ` "primitives.button.small.width", { "key": "--button-small-width", + "sourcePath": "primitives.button.small.width", + "tier": "primitive", + "type": "component", "value": "120px", "variable": "--button-small-width: 120px;", }, @@ -82,6 +107,9 @@ exports[`processPrimitives - processes buttons with settings 2`] = ` "primitives.button.small.height", { "key": "--button-small-height", + "sourcePath": "primitives.button.small.height", + "tier": "primitive", + "type": "component", "value": "40px", "variable": "--button-small-height: 40px;", }, @@ -90,6 +118,9 @@ exports[`processPrimitives - processes buttons with settings 2`] = ` "primitives.button.small.radius", { "key": "--button-small-radius", + "sourcePath": "primitives.button.small.radius", + "tier": "primitive", + "type": "component", "value": "8px", "variable": "--button-small-radius: 8px;", }, @@ -98,6 +129,9 @@ exports[`processPrimitives - processes buttons with settings 2`] = ` "primitives.button.big.width", { "key": "--button-big-width", + "sourcePath": "primitives.button.big.width", + "tier": "primitive", + "type": "component", "value": "15rem", "variable": "--button-big-width: 15rem;", }, @@ -106,6 +140,9 @@ exports[`processPrimitives - processes buttons with settings 2`] = ` "primitives.button.big.height", { "key": "--button-big-height", + "sourcePath": "primitives.button.big.height", + "tier": "primitive", + "type": "component", "value": "5rem", "variable": "--button-big-height: 5rem;", }, @@ -114,6 +151,9 @@ exports[`processPrimitives - processes buttons with settings 2`] = ` "primitives.button.big.radius", { "key": "--button-big-radius", + "sourcePath": "primitives.button.big.radius", + "tier": "primitive", + "type": "component", "value": "1rem", "variable": "--button-big-radius: 1rem;", }, @@ -136,6 +176,12 @@ exports[`processPrimitives - references colors, gradients, and themes 2`] = ` "primitives.card.default.background-color", { "key": "--card-default-background-color", + "referencePaths": [ + "theme.light.background.primary", + ], + "sourcePath": "primitives.card.default.background-color", + "tier": "semantic", + "type": "component", "value": "var(--theme-light-background-primary)", "variable": "--card-default-background-color: var(--theme-light-background-primary);", }, @@ -144,6 +190,12 @@ exports[`processPrimitives - references colors, gradients, and themes 2`] = ` "primitives.card.default.background-image", { "key": "--card-default-background-image", + "referencePaths": [ + "gradients.orangeGradient.primary", + ], + "sourcePath": "primitives.card.default.background-image", + "tier": "semantic", + "type": "component", "value": "var(--gradients-orangeGradient-primary)", "variable": "--card-default-background-image: var(--gradients-orangeGradient-primary);", }, @@ -152,6 +204,12 @@ exports[`processPrimitives - references colors, gradients, and themes 2`] = ` "primitives.card.default.border-color", { "key": "--card-default-border-color", + "referencePaths": [ + "palette.coral.50", + ], + "sourcePath": "primitives.card.default.border-color", + "tier": "semantic", + "type": "component", "value": "var(--palette-coral-50)", "variable": "--card-default-border-color: var(--palette-coral-50);", }, @@ -172,6 +230,13 @@ exports[`processPrimitives - uses fluid spacing references 2`] = ` "primitives.box.default.padding", { "key": "--box-default-padding", + "referencePaths": [ + "spacing_fluid.gap@s", + "spacing_fluid.gap@m", + ], + "sourcePath": "primitives.box.default.padding", + "tier": "semantic", + "type": "component", "value": "var(--spacing_fluid-gap-gs-s) var(--spacing_fluid-gap-gs-m)", "variable": "--box-default-padding: var(--spacing_fluid-gap-gs-s) var(--spacing_fluid-gap-gs-m);", }, diff --git a/packages/cssforge/tests/__snapshots__/spacing.test.ts.snap b/packages/cssforge/tests/__snapshots__/spacing.test.ts.snap index 88f149c..364d4d1 100644 --- a/packages/cssforge/tests/__snapshots__/spacing.test.ts.snap +++ b/packages/cssforge/tests/__snapshots__/spacing.test.ts.snap @@ -16,6 +16,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing_fluid.base@xs", { "key": "--spacing_fluid-base-flux-xs", + "sourcePath": "spacing_fluid.base@xs", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, 0rem + 0vw, 0rem)", "variable": "--spacing_fluid-base-flux-xs: clamp(0rem, 0rem + 0vw, 0rem);", }, @@ -24,6 +27,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing_fluid.base@s", { "key": "--spacing_fluid-base-flux-s", + "sourcePath": "spacing_fluid.base@s", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem)", "variable": "--spacing_fluid-base-flux-s: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", }, @@ -32,6 +38,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing_fluid.base@m", { "key": "--spacing_fluid-base-flux-m", + "sourcePath": "spacing_fluid.base@m", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem)", "variable": "--spacing_fluid-base-flux-m: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", }, @@ -40,6 +49,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing_fluid.base@xs-s", { "key": "--spacing_fluid-base-flux-xs-s", + "sourcePath": "spacing_fluid.base@xs-s", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, -0.5rem + 2.5vw, 1.5rem)", "variable": "--spacing_fluid-base-flux-xs-s: clamp(0rem, -0.5rem + 2.5vw, 1.5rem);", }, @@ -48,6 +60,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing_fluid.base@s-m", { "key": "--spacing_fluid-base-flux-s-m", + "sourcePath": "spacing_fluid.base@s-m", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem)", "variable": "--spacing_fluid-base-flux-s-m: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", }, @@ -56,6 +71,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing.custom.gap.1", { "key": "--spacing-gap-1", + "sourcePath": "spacing.custom.gap.1", + "tier": "primitive", + "type": "spacing", "value": "4px", "variable": "--spacing-gap-1: 4px;", }, @@ -64,6 +82,9 @@ exports[`processSpacing - combines fluid and custom spacing 2`] = ` "spacing.custom.gap.2", { "key": "--spacing-gap-2", + "sourcePath": "spacing.custom.gap.2", + "tier": "primitive", + "type": "spacing", "value": "8px", "variable": "--spacing-gap-2: 8px;", }, @@ -85,6 +106,9 @@ exports[`processSpacing - fluid without prefix falls back to scale name 2`] = ` "spacing_fluid.rhythm@xs", { "key": "--spacing_fluid-rhythm-xs", + "sourcePath": "spacing_fluid.rhythm@xs", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, 0rem + 0vw, 0rem)", "variable": "--spacing_fluid-rhythm-xs: clamp(0rem, 0rem + 0vw, 0rem);", }, @@ -93,6 +117,9 @@ exports[`processSpacing - fluid without prefix falls back to scale name 2`] = ` "spacing_fluid.rhythm@s", { "key": "--spacing_fluid-rhythm-s", + "sourcePath": "spacing_fluid.rhythm@s", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem)", "variable": "--spacing_fluid-rhythm-s: clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem);", }, @@ -101,6 +128,9 @@ exports[`processSpacing - fluid without prefix falls back to scale name 2`] = ` "spacing_fluid.rhythm@m", { "key": "--spacing_fluid-rhythm-m", + "sourcePath": "spacing_fluid.rhythm@m", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem)", "variable": "--spacing_fluid-rhythm-m: clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem);", }, @@ -109,6 +139,9 @@ exports[`processSpacing - fluid without prefix falls back to scale name 2`] = ` "spacing_fluid.rhythm@xs-s", { "key": "--spacing_fluid-rhythm-xs-s", + "sourcePath": "spacing_fluid.rhythm@xs-s", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, -0.4167rem + 2.0833vw, 1.25rem)", "variable": "--spacing_fluid-rhythm-xs-s: clamp(0rem, -0.4167rem + 2.0833vw, 1.25rem);", }, @@ -117,6 +150,9 @@ exports[`processSpacing - fluid without prefix falls back to scale name 2`] = ` "spacing_fluid.rhythm@s-m", { "key": "--spacing_fluid-rhythm-s-m", + "sourcePath": "spacing_fluid.rhythm@s-m", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem)", "variable": "--spacing_fluid-rhythm-s-m: clamp(0.125rem, -0.25rem + 1.875vw, 1.25rem);", }, @@ -137,6 +173,9 @@ exports[`processSpacing - generates correct spacing scale 2`] = ` "spacing.custom.size.1", { "key": "--spacing-size-1", + "sourcePath": "spacing.custom.size.1", + "tier": "primitive", + "type": "spacing", "value": "0.25rem", "variable": "--spacing-size-1: 0.25rem;", }, @@ -145,6 +184,9 @@ exports[`processSpacing - generates correct spacing scale 2`] = ` "spacing.custom.size.2", { "key": "--spacing-size-2", + "sourcePath": "spacing.custom.size.2", + "tier": "primitive", + "type": "spacing", "value": "0.5rem", "variable": "--spacing-size-2: 0.5rem;", }, @@ -153,6 +195,9 @@ exports[`processSpacing - generates correct spacing scale 2`] = ` "spacing.custom.size.3", { "key": "--spacing-size-3", + "sourcePath": "spacing.custom.size.3", + "tier": "primitive", + "type": "spacing", "value": "0.75rem", "variable": "--spacing-size-3: 0.75rem;", }, @@ -161,6 +206,9 @@ exports[`processSpacing - generates correct spacing scale 2`] = ` "spacing.custom.size.s", { "key": "--spacing-size-s", + "sourcePath": "spacing.custom.size.s", + "tier": "primitive", + "type": "spacing", "value": "1rem", "variable": "--spacing-size-s: 1rem;", }, @@ -185,6 +233,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@xs", { "key": "--spacing_fluid-base-foo-xs", + "sourcePath": "spacing_fluid.base@xs", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, 0rem + 0vw, 0rem)", "variable": "--spacing_fluid-base-foo-xs: clamp(0rem, 0rem + 0vw, 0rem);", }, @@ -193,6 +244,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@s", { "key": "--spacing_fluid-base-foo-s", + "sourcePath": "spacing_fluid.base@s", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem)", "variable": "--spacing_fluid-base-foo-s: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", }, @@ -201,6 +255,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@m", { "key": "--spacing_fluid-base-foo-m", + "sourcePath": "spacing_fluid.base@m", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem)", "variable": "--spacing_fluid-base-foo-m: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", }, @@ -209,6 +266,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@l", { "key": "--spacing_fluid-base-foo-l", + "sourcePath": "spacing_fluid.base@l", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.5rem, -0.3333rem + 4.1667vw, 3rem)", "variable": "--spacing_fluid-base-foo-l: clamp(0.5rem, -0.3333rem + 4.1667vw, 3rem);", }, @@ -217,6 +277,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@xs-l", { "key": "--spacing_fluid-base-foo-xs-l", + "sourcePath": "spacing_fluid.base@xs-l", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, -1rem + 5vw, 3rem)", "variable": "--spacing_fluid-base-foo-xs-l: clamp(0rem, -1rem + 5vw, 3rem);", }, @@ -225,6 +288,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@xs-s", { "key": "--spacing_fluid-base-foo-xs-s", + "sourcePath": "spacing_fluid.base@xs-s", + "tier": "primitive", + "type": "spacing", "value": "clamp(0rem, -0.5rem + 2.5vw, 1.5rem)", "variable": "--spacing_fluid-base-foo-xs-s: clamp(0rem, -0.5rem + 2.5vw, 1.5rem);", }, @@ -233,6 +299,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@s-m", { "key": "--spacing_fluid-base-foo-s-m", + "sourcePath": "spacing_fluid.base@s-m", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem)", "variable": "--spacing_fluid-base-foo-s-m: clamp(0.25rem, -0.1667rem + 2.0833vw, 1.5rem);", }, @@ -241,6 +310,9 @@ exports[`processSpacing - generates fluid spacing (prefix) 2`] = ` "spacing_fluid.base@m-l", { "key": "--spacing_fluid-base-foo-m-l", + "sourcePath": "spacing_fluid.base@m-l", + "tier": "primitive", + "type": "spacing", "value": "clamp(0.25rem, -0.6667rem + 4.5833vw, 3rem)", "variable": "--spacing_fluid-base-foo-m-l: clamp(0.25rem, -0.6667rem + 4.5833vw, 3rem);", }, @@ -261,6 +333,9 @@ exports[`processSpacing - handles settings 2`] = ` "spacing.custom.size.1", { "key": "--spacing-size-1", + "sourcePath": "spacing.custom.size.1", + "tier": "primitive", + "type": "spacing", "value": "1rem", "variable": "--spacing-size-1: 1rem;", }, @@ -269,6 +344,9 @@ exports[`processSpacing - handles settings 2`] = ` "spacing.custom.size.2", { "key": "--spacing-size-2", + "sourcePath": "spacing.custom.size.2", + "tier": "primitive", + "type": "spacing", "value": "0.5rem", "variable": "--spacing-size-2: 0.5rem;", }, @@ -277,6 +355,9 @@ exports[`processSpacing - handles settings 2`] = ` "spacing.custom.scale.md", { "key": "--spacing-scale-md", + "sourcePath": "spacing.custom.scale.md", + "tier": "primitive", + "type": "spacing", "value": "8px", "variable": "--spacing-scale-md: 8px;", }, @@ -285,6 +366,9 @@ exports[`processSpacing - handles settings 2`] = ` "spacing.custom.scale.lg", { "key": "--spacing-scale-lg", + "sourcePath": "spacing.custom.scale.lg", + "tier": "primitive", + "type": "spacing", "value": "0.75rem", "variable": "--spacing-scale-lg: 0.75rem;", }, diff --git a/packages/cssforge/tests/__snapshots__/typography.test.ts.snap b/packages/cssforge/tests/__snapshots__/typography.test.ts.snap index 12e77b3..abc46cf 100644 --- a/packages/cssforge/tests/__snapshots__/typography.test.ts.snap +++ b/packages/cssforge/tests/__snapshots__/typography.test.ts.snap @@ -19,6 +19,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@2xl", { "key": "--typography_fluid-arial-2xl", + "sourcePath": "typography_fluid.arial@2xl", + "tier": "primitive", + "type": "typography", "value": "clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem)", "variable": "--typography_fluid-arial-2xl: clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem);", }, @@ -27,6 +30,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@xl", { "key": "--typography_fluid-arial-xl", + "sourcePath": "typography_fluid.arial@xl", + "tier": "primitive", + "type": "typography", "value": "clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem)", "variable": "--typography_fluid-arial-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);", }, @@ -35,6 +41,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@l", { "key": "--typography_fluid-arial-l", + "sourcePath": "typography_fluid.arial@l", + "tier": "primitive", + "type": "typography", "value": "clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem)", "variable": "--typography_fluid-arial-l: clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem);", }, @@ -43,6 +52,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@m", { "key": "--typography_fluid-arial-m", + "sourcePath": "typography_fluid.arial@m", + "tier": "primitive", + "type": "typography", "value": "clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem)", "variable": "--typography_fluid-arial-m: clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem);", }, @@ -51,6 +63,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@s", { "key": "--typography_fluid-arial-s", + "sourcePath": "typography_fluid.arial@s", + "tier": "primitive", + "type": "typography", "value": "clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem)", "variable": "--typography_fluid-arial-s: clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem);", }, @@ -59,6 +74,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@xs", { "key": "--typography_fluid-arial-xs", + "sourcePath": "typography_fluid.arial@xs", + "tier": "primitive", + "type": "typography", "value": "clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem)", "variable": "--typography_fluid-arial-xs: clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem);", }, @@ -67,6 +85,9 @@ exports[`processTypography - can process weights 2`] = ` "typography_fluid.arial@2xs", { "key": "--typography_fluid-arial-2xs", + "sourcePath": "typography_fluid.arial@2xs", + "tier": "primitive", + "type": "typography", "value": "clamp(0.448rem, 0.4296rem + 0.0918vw, 0.512rem)", "variable": "--typography_fluid-arial-2xs: clamp(0.448rem, 0.4296rem + 0.0918vw, 0.512rem);", }, @@ -75,6 +96,9 @@ exports[`processTypography - can process weights 2`] = ` "typography.weight.arial.regular", { "key": "--typography-weight-arial-regular", + "sourcePath": "typography.weight.arial.regular", + "tier": "primitive", + "type": "typography", "value": "500", "variable": "--typography-weight-arial-regular: 500;", }, @@ -102,6 +126,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@4xl", { "key": "--typography_fluid-arial-4xl", + "sourcePath": "typography_fluid.arial@4xl", + "tier": "primitive", + "type": "typography", "value": "clamp(2.6703rem, 2.5608rem + 0.5474vw, 3.0518rem)", "variable": "--typography_fluid-arial-4xl: clamp(2.6703rem, 2.5608rem + 0.5474vw, 3.0518rem);", }, @@ -110,6 +137,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@3xl", { "key": "--typography_fluid-arial-3xl", + "sourcePath": "typography_fluid.arial@3xl", + "tier": "primitive", + "type": "typography", "value": "clamp(2.1362rem, 2.0486rem + 0.4379vw, 2.4414rem)", "variable": "--typography_fluid-arial-3xl: clamp(2.1362rem, 2.0486rem + 0.4379vw, 2.4414rem);", }, @@ -118,6 +148,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@2xl", { "key": "--typography_fluid-arial-2xl", + "sourcePath": "typography_fluid.arial@2xl", + "tier": "primitive", + "type": "typography", "value": "clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem)", "variable": "--typography_fluid-arial-2xl: clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem);", }, @@ -126,6 +159,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@xl", { "key": "--typography_fluid-arial-xl", + "sourcePath": "typography_fluid.arial@xl", + "tier": "primitive", + "type": "typography", "value": "clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem)", "variable": "--typography_fluid-arial-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);", }, @@ -134,6 +170,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@l", { "key": "--typography_fluid-arial-l", + "sourcePath": "typography_fluid.arial@l", + "tier": "primitive", + "type": "typography", "value": "clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem)", "variable": "--typography_fluid-arial-l: clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem);", }, @@ -142,6 +181,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@m", { "key": "--typography_fluid-arial-m", + "sourcePath": "typography_fluid.arial@m", + "tier": "primitive", + "type": "typography", "value": "clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem)", "variable": "--typography_fluid-arial-m: clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem);", }, @@ -150,6 +192,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@s", { "key": "--typography_fluid-arial-s", + "sourcePath": "typography_fluid.arial@s", + "tier": "primitive", + "type": "typography", "value": "clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem)", "variable": "--typography_fluid-arial-s: clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem);", }, @@ -158,6 +203,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@xs", { "key": "--typography_fluid-arial-xs", + "sourcePath": "typography_fluid.arial@xs", + "tier": "primitive", + "type": "typography", "value": "clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem)", "variable": "--typography_fluid-arial-xs: clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem);", }, @@ -166,6 +214,9 @@ exports[`processTypography - generates correct CSS variables 2`] = ` "typography_fluid.arial@2xs", { "key": "--typography_fluid-arial-2xs", + "sourcePath": "typography_fluid.arial@2xs", + "tier": "primitive", + "type": "typography", "value": "clamp(0.448rem, 0.4296rem + 0.0918vw, 0.512rem)", "variable": "--typography_fluid-arial-2xs: clamp(0.448rem, 0.4296rem + 0.0918vw, 0.512rem);", }, @@ -194,6 +245,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@h1", { "key": "--typography_fluid-arial-text-h1", + "sourcePath": "typography_fluid.arial@h1", + "tier": "primitive", + "type": "typography", "value": "clamp(4.1723rem, 4.0013rem + 0.8553vw, 4.7684rem)", "variable": "--typography_fluid-arial-text-h1: clamp(4.1723rem, 4.0013rem + 0.8553vw, 4.7684rem);", }, @@ -202,6 +256,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@h2", { "key": "--typography_fluid-arial-text-h2", + "sourcePath": "typography_fluid.arial@h2", + "tier": "primitive", + "type": "typography", "value": "clamp(3.3379rem, 3.201rem + 0.6843vw, 3.8147rem)", "variable": "--typography_fluid-arial-text-h2: clamp(3.3379rem, 3.201rem + 0.6843vw, 3.8147rem);", }, @@ -210,6 +267,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@h3", { "key": "--typography_fluid-arial-text-h3", + "sourcePath": "typography_fluid.arial@h3", + "tier": "primitive", + "type": "typography", "value": "clamp(2.6703rem, 2.5608rem + 0.5474vw, 3.0518rem)", "variable": "--typography_fluid-arial-text-h3: clamp(2.6703rem, 2.5608rem + 0.5474vw, 3.0518rem);", }, @@ -218,6 +278,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@h4", { "key": "--typography_fluid-arial-text-h4", + "sourcePath": "typography_fluid.arial@h4", + "tier": "primitive", + "type": "typography", "value": "clamp(2.1362rem, 2.0486rem + 0.4379vw, 2.4414rem)", "variable": "--typography_fluid-arial-text-h4: clamp(2.1362rem, 2.0486rem + 0.4379vw, 2.4414rem);", }, @@ -226,6 +289,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@xxl", { "key": "--typography_fluid-arial-text-xxl", + "sourcePath": "typography_fluid.arial@xxl", + "tier": "primitive", + "type": "typography", "value": "clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem)", "variable": "--typography_fluid-arial-text-xxl: clamp(1.709rem, 1.6389rem + 0.3503vw, 1.9531rem);", }, @@ -234,6 +300,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@xl", { "key": "--typography_fluid-arial-text-xl", + "sourcePath": "typography_fluid.arial@xl", + "tier": "primitive", + "type": "typography", "value": "clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem)", "variable": "--typography_fluid-arial-text-xl: clamp(1.3672rem, 1.3111rem + 0.2803vw, 1.5625rem);", }, @@ -242,6 +311,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@l", { "key": "--typography_fluid-arial-text-l", + "sourcePath": "typography_fluid.arial@l", + "tier": "primitive", + "type": "typography", "value": "clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem)", "variable": "--typography_fluid-arial-text-l: clamp(1.0938rem, 1.0489rem + 0.2242vw, 1.25rem);", }, @@ -250,6 +322,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@m", { "key": "--typography_fluid-arial-text-m", + "sourcePath": "typography_fluid.arial@m", + "tier": "primitive", + "type": "typography", "value": "clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem)", "variable": "--typography_fluid-arial-text-m: clamp(0.875rem, 0.8391rem + 0.1794vw, 1rem);", }, @@ -258,6 +333,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@s", { "key": "--typography_fluid-arial-text-s", + "sourcePath": "typography_fluid.arial@s", + "tier": "primitive", + "type": "typography", "value": "clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem)", "variable": "--typography_fluid-arial-text-s: clamp(0.7rem, 0.6713rem + 0.1435vw, 0.8rem);", }, @@ -266,6 +344,9 @@ exports[`typography - can handle custom labels and prefixes 2`] = ` "typography_fluid.arial@xs", { "key": "--typography_fluid-arial-text-xs", + "sourcePath": "typography_fluid.arial@xs", + "tier": "primitive", + "type": "typography", "value": "clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem)", "variable": "--typography_fluid-arial-text-xs: clamp(0.56rem, 0.537rem + 0.1148vw, 0.64rem);", }, diff --git a/packages/cssforge/tests/colors.test.ts b/packages/cssforge/tests/colors.test.ts index 6083779..c7800ab 100644 --- a/packages/cssforge/tests/colors.test.ts +++ b/packages/cssforge/tests/colors.test.ts @@ -95,6 +95,56 @@ Deno.test("processColors - handles string values", async (t) => { await assertSnapshot(t, Array.from(resolveMap.entries())); }); +Deno.test("processColors - handles direct palette groups", async (t) => { + const config = defineConfig({ + colors: { + palette: { + value: { + basic: { + white: { oklch: "oklch(100% 0 0)" }, + black: { oklch: "oklch(0% 0 0)" }, + }, + gray: { + 950: "oklch(14.479% 0 0)", + }, + }, + }, + theme: { + value: { + light: { + content: { + value: { + primary: "var(--1)", + }, + variables: { + 1: "palette.value.gray.950", + }, + }, + }, + }, + }, + }, + }); + + const { css, resolveMap } = processColors(config.colors); + const combined = combine(css); + + assertEquals(combined.includes("--palette-basic-white: oklch(100% 0 0);"), true); + assertEquals(combined.includes("--palette-gray-950: oklch(14.479% 0 0);"), true); + assertEquals( + combined.includes("--theme-light-content-primary: var(--palette-gray-950);"), + true, + ); + assertEquals(Array.from(resolveMap.keys()), [ + "palette.basic.white", + "palette.basic.black", + "palette.gray.950", + "theme.light.content.primary", + ]); + await assertSnapshot(t, combined); + await assertSnapshot(t, Array.from(resolveMap.entries())); +}); + Deno.test("processColors - handles themes", async (t) => { const config = defineConfig({ colors: { diff --git a/packages/cssforge/tests/generator.test.ts b/packages/cssforge/tests/generator.test.ts new file mode 100644 index 0000000..32fca8f --- /dev/null +++ b/packages/cssforge/tests/generator.test.ts @@ -0,0 +1,264 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import cliCommand, { build } from "../src/cli.ts"; +import { defineConfig } from "../src/config.ts"; +import { generateStyleDictionaryJSON } from "../src/generator.ts"; +import { assertEquals, assertSnapshot, Deno } from "./vitest-compat.ts"; + +Deno.test("generateStyleDictionaryJSON - preserves references and resolved values", async (t) => { + const config = defineConfig({ + colors: { + palette: { + value: { + neutral: { + value: { + "0": "#fff", + "900": "#111", + }, + }, + }, + }, + theme: { + light: { + value: { + content: { + value: { + primary: "var(--text)", + }, + variables: { + text: "palette.neutral.900", + }, + }, + }, + }, + }, + }, + spacing: { + custom: { + size: { + value: { + 2: "8px", + }, + }, + }, + }, + primitives: { + button: { + value: { + default: { + value: { + color: "var(--fg)", + padding: "var(--pad)", + }, + variables: { + fg: "theme.light.content.primary", + pad: "spacing.custom.size.2", + }, + }, + }, + }, + }, + }); + + const result = JSON.parse(generateStyleDictionaryJSON(config)); + + assertEquals(result.palette.neutral["900"].value, "var(--palette-neutral-900)"); + assertEquals(result.palette.neutral["900"].type, "color"); + assertEquals(result.palette.neutral["900"].$tier, "primitive"); + assertEquals( + result.theme.light.content.primary.value, + "var(--theme-light-content-primary)", + ); + assertEquals(result.theme.light.content.primary.$tier, "semantic"); + assertEquals(result.theme.light.content.primary.$reference, "palette.neutral.900"); + assertEquals(result.theme.light.content.primary.$resolvedValue, "oklch(17.764% 0 0)"); + assertEquals( + result.theme.light.content.primary.attributes.cssVariable, + "--theme-light-content-primary", + ); + assertEquals(result.primitives.button.default.color.attributes.referencePaths, [ + "theme.light.content.primary", + ]); + assertEquals( + result.primitives.button.default.color.attributes.resolvedValue, + "oklch(17.764% 0 0)", + ); + assertEquals( + result.primitives.button.default.padding.attributes.resolvedValue, + "0.5rem", + ); + + await assertSnapshot(t, result); +}); + +Deno.test("generateStyleDictionaryJSON - can use resolved values as token values", () => { + const config = defineConfig({ + spacing: { + custom: { + size: { + value: { + 2: "8px", + }, + }, + }, + }, + }); + + const result = JSON.parse( + generateStyleDictionaryJSON(config, { valueMode: "resolved" }), + ); + + assertEquals(result.spacing.custom.size["2"].value, "0.5rem"); + assertEquals( + result.spacing.custom.size["2"].attributes.cssVariableReference, + "var(--spacing-size-2)", + ); +}); + +Deno.test("generateStyleDictionaryJSON - supports legacy value-wrapper reference paths", () => { + const config = defineConfig({ + colors: { + palette: { + value: { + cosmicGold: { + 50: "oklch(91.642% 0.15696 98.188)", + 90: "oklch(73.683% 0.13788 73.881)", + }, + gray: { + 950: "oklch(14.479% 0 0)", + }, + }, + }, + gradients: { + value: { + goldGradient: { + value: { + primary: { + value: "linear-gradient(90deg, var(--1), var(--2))", + variables: { + 1: "palette.value.cosmicGold.50", + 2: "palette.value.cosmicGold.90", + }, + }, + }, + }, + }, + }, + theme: { + value: { + light: { + content: { + value: { + primary: "var(--1)", + }, + variables: { + 1: "palette.value.gray.950", + }, + }, + }, + }, + }, + }, + spacing: { + custom: { + size: { + value: { + 2: "8px", + }, + }, + }, + }, + typography: { + weight: { + lexend: { + value: { + regular: "400", + }, + }, + }, + }, + primitives: { + button: { + value: { + default: { + value: { + backgroundImage: "var(--grad)", + color: "var(--fg)", + fontWeight: "var(--weight)", + padding: "var(--size)", + }, + variables: { + grad: "gradients.value.goldGradient.primary", + fg: "theme.value.light.content.primary", + weight: "typography.weight.lexend.value.regular", + size: "spacing.custom.size.value.2", + }, + }, + }, + }, + }, + }); + + const result = JSON.parse(generateStyleDictionaryJSON(config)); + + assertEquals(result.gradients.goldGradient.primary.$reference, "palette.cosmicGold.50"); + assertEquals(result.gradients.goldGradient.primary.attributes.referencePaths, [ + "palette.cosmicGold.50", + "palette.cosmicGold.90", + ]); + assertEquals(result.theme.light.content.primary.$reference, "palette.gray.950"); + assertEquals( + result.primitives.button.default.fontWeight.attributes.resolvedValue, + "400", + ); + assertEquals( + result.primitives.button.default.padding.attributes.resolvedValue, + "0.5rem", + ); +}); + +Deno.test("cli - exposes documented style-dictionary flag", () => { + assertEquals("style-dictionary" in (cliCommand.args ?? {}), true); + assertEquals("styleDictionary" in (cliCommand.args ?? {}), false); +}); + +Deno.test("build - writes Style Dictionary output", async () => { + const tempDir = await mkdtemp(join(tmpdir(), "cssforge-style-dictionary-")); + + try { + const configPath = join(tempDir, "cssforge.config.ts"); + const styleDictionaryOutput = join(tempDir, "tokens.sd.json"); + await writeFile( + configPath, + `export default { + spacing: { + custom: { + size: { + value: { + 2: "8px", + }, + }, + }, + }, + };`, + "utf8", + ); + + const result = await build({ + config: configPath, + mode: "style-dictionary", + cssOutput: join(tempDir, "output.css"), + jsonOutput: join(tempDir, "output.json"), + tsOutput: join(tempDir, "output.ts"), + styleDictionaryOutput, + }); + + const output = JSON.parse(await readFile(styleDictionaryOutput, "utf8")); + + assertEquals(result.success, true); + assertEquals(output.spacing.custom.size["2"].value, "var(--spacing-size-2)"); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } +});