/** * @factory/ui — design tokens (PURE, no external imports besides @factory/shared). * * Strategy (recorded decision): * - Single source of truth: a base "semantic" token set (light + dark) plus the * 18 Notion-like block colors and 9 status colors REUSED from @factory/shared * (`BLOCK_COLORS`, `STATUS_COLORS`) — never re-declared here. * - `cssVariables(theme)` emits `:root` (light) and `[data-theme="dark"]` * (dark) CSS text with a deterministic key order: base tokens, block colors, * status colors. `cssTokensFile()` is the full `tokens.css` ready to drop * into a web bundle. * - NO component may hard-code a color: every component references `var(--token)`. * The only hex values in this package live in the token tables below. * * Notion-inspired defaults: light primary is the "ink" #37352F, dark primary is * a near-white #E9E9E7 on dark surfaces. */ import { BLOCK_COLORS, STATUS_COLORS } from "@factory/shared/constants"; /** Semantic token names shared by both themes (deterministic order). */ export const TOKEN_NAMES = [ "background", "foreground", "card", "card-foreground", "popover", "popover-foreground", "primary", "primary-foreground", "secondary", "muted", "muted-foreground", "accent", "accent-foreground", "destructive", "destructive-foreground", "border", "input", "ring", "sidebar", "editor-background", "editor-foreground", "selection", "success", "success-bg", "warning", "warning-bg", "info", "info-bg", "focus-ring", ] as const; export type TokenName = (typeof TOKEN_NAMES)[number]; /** Light theme values (Notion light defaults). */ export const LIGHT_TOKENS: Record = { background: "#FFFFFF", foreground: "#37352F", card: "#FFFFFF", "card-foreground": "#37352F", popover: "#FFFFFF", "popover-foreground": "#37352F", primary: "#37352F", "primary-foreground": "#FFFFFF", secondary: "#F1F1EF", muted: "#F7F6F3", "muted-foreground": "#787C7E", accent: "#EBEBE7", "accent-foreground": "#37352F", destructive: "#E03E3E", "destructive-foreground": "#FFFFFF", border: "#E9E9E7", input: "#D3D1CB", ring: "#337EA9", sidebar: "#F7F6F3", "editor-background": "#FFFFFF", "editor-foreground": "#37352F", selection: "#D7E7F5", success: "#448361", "success-bg": "#EDF3EC", warning: "#CB912F", "warning-bg": "#FDF3D0", info: "#337EA9", "info-bg": "#E7F0F6", "focus-ring": "rgba(51, 126, 169, 0.4)", }; /** Dark theme values (Notion dark defaults). */ export const DARK_TOKENS: Record = { background: "#191919", foreground: "#DFDEDC", card: "#202020", "card-foreground": "#DFDEDC", popover: "#252525", "popover-foreground": "#DFDEDC", primary: "#E9E9E7", "primary-foreground": "#191919", secondary: "#2F2F2F", muted: "#2A2A2A", "muted-foreground": "#9B9B98", accent: "#373737", "accent-foreground": "#DFDEDC", destructive: "#E03E3E", "destructive-foreground": "#FFFFFF", border: "#2F2F2F", input: "#444444", ring: "#529CCA", sidebar: "#1E1E1E", "editor-background": "#191919", "editor-foreground": "#DFDEDC", selection: "#3A5F7E", success: "#6FBF8E", "success-bg": "#22352B", warning: "#D9A94B", "warning-bg": "#3A3120", info: "#529CCA", "info-bg": "#233A4A", "focus-ring": "rgba(82, 156, 202, 0.45)", }; /** Block color token keys, derived from @factory/shared (deterministic order). */ export const BLOCK_COLOR_KEYS = BLOCK_COLORS.map((c) => `block-color-${c.token}`) as string[]; /** Status color token keys, derived from @factory/shared (deterministic order). */ export const STATUS_COLOR_KEYS = STATUS_COLORS.map((s) => `status-${s}`) as string[]; /** Light block-color values: the shared palette as-is (Notion is identical). */ const LIGHT_BLOCK_COLORS: Record = Object.fromEntries( BLOCK_COLORS.map((c) => [`block-color-${c.token}`, c.hex]), ); /** Dark block-color values: same hues — Notion keeps block text/bg colors shared. */ const DARK_BLOCK_COLORS: Record = { ...LIGHT_BLOCK_COLORS }; /** Light status values: block-color text hue of the same name (documented decision). */ const lightStatus = (): Record => { const out: Record = {}; for (const s of STATUS_COLORS) { out[`status-${s}`] = LIGHT_BLOCK_COLORS[`block-color-${s}`] ?? "inherit"; } return out; }; const DARK_STATUS_COLORS: Record = lightStatus(); const LIGHT_STATUS_COLORS: Record = lightStatus(); /** * The full token tables: `THEME_TOKENS.light` / `THEME_TOKENS.dark` map token * name (without the `--` prefix) to a CSS value. */ export const THEME_TOKENS: { light: Record; dark: Record } = { light: { ...LIGHT_TOKENS, ...LIGHT_BLOCK_COLORS, ...LIGHT_STATUS_COLORS }, dark: { ...DARK_TOKENS, ...DARK_BLOCK_COLORS, ...DARK_STATUS_COLORS }, }; function emitRule(selector: string, values: Record): string { const lines = Object.keys(values) .map((name) => ` --${name}: ${values[name]};`) .join("\n"); return `${selector} {\n${lines}\n}`; } /** * Render `:root { … }` (light) and `[data-theme="dark"] { … }` (dark) CSS text. * Deterministic key order (base, block, status) in both rules. */ export function cssVariables(theme: "light" | "dark" = "light"): string { if (theme === "light") { return emitRule(":root", THEME_TOKENS.light); } return emitRule('[data-theme="dark"]', THEME_TOKENS.dark); } /** Full `tokens.css` content: both themes, header comment, trailing newline. */ export function cssTokensFile(): string { return [ "/* @factory/ui tokens.css — generated by tokens.ts (do not edit by hand).", ' * Light values on :root; dark overrides on the data-theme dark selector.', " * Block/status colors are reused from @factory/shared (BLOCK_COLORS/STATUS_COLORS). */", "", cssVariables("light"), "", cssVariables("dark"), "", ].join("\n"); }