@adea-ai/ui 0.128.3 → 0.129.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"themes.js","names":["catalogue","themes"],"sources":["../../src/lib/themes.ts"],"sourcesContent":["/**\n * The theme registry, now a bridge.\n *\n * The catalogue itself lives in `@adea-ai/themes`. This module is what is left in\n * the design system: the translation between the catalogue's **canonical** role\n * names and the **shadcn** vocabulary every component here is written against, plus\n * the handful of custom properties this system declares that are presentations of\n * data the catalogue already holds — the `--terminal-*` ramp, the `--editor-*` ramp,\n * and `--chrome`.\n *\n * ## Why it is a bridge and not a copy\n *\n * This file used to hold thirteen themes as hand-written hex literal tables, with its\n * own contrast validator, its own terminal and editor palettes, and its own\n * provenance records. That is a lot of surface to maintain for one consumer of a\n * palette, and it meant every product that wanted the same themes — Adea, Cortana —\n * either imported this package or wrote its own. The catalogue moved out so that\n * there is one answer to \"what is Catppuccin Mocha\", and this file is the part that\n * genuinely belongs to the design system.\n *\n * It is deliberately thin. If you are looking for a colour, it is not here: see\n * `@adea-ai/themes`, whose README explains how the values are produced and whose\n * `NOTICE` records where every palette came from.\n *\n * ## The mapping, and the entry that is not a rename\n *\n * The package-owned `shadcnVariables` adapter maps canonical roles onto this\n * system's vocabulary. One entry will trip a reader: the catalogue's `accent`\n * becomes this system's `primary`, because in the shadcn vocabulary `--primary` is\n * the action colour — the fill of a primary button, the thing a focus ring is drawn\n * in — while `--accent` is a low-contrast hover wash. The catalogue's `accent` is\n * the former, so it goes to `--primary`, and `--accent` is filled from\n * `surfaceHover`, which is what shadcn components actually use it for.\n */\n\nimport {\n ACCENTS,\n accentForeground,\n chartSeries,\n editorRolesHex,\n getAccent,\n getTheme,\n hasTheme,\n shadcnDestructiveProjection,\n statusForeground,\n tint,\n primarySubtleCss,\n syntaxRoles,\n themes as catalogue,\n themesByAppearance,\n themeAccentPresets,\n validateCatalogue,\n type AccentPreset,\n type AdeaTheme,\n type AdeaThemeRecord,\n type ContrastFinding,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'\nimport { isThemeAccentId } from './theme-accents'\nimport { themeCssVariablesWith } from './theme-css'\nimport { defaultDarkThemeId, defaultLightThemeId } from './theme-defaults'\n\nexport { isThemeAccentId } from './theme-accents'\nexport { terminalCssVariables, tokensAsHex } from './theme-css'\nexport { defaultDarkThemeId, defaultLightThemeId, resolveVariant } from './theme-defaults'\n\nexport type ThemeAppearance = 'light' | 'dark'\n\n/**\n * The surface and text roles, in shadcn's vocabulary.\n *\n * Named to match the components below, which is why they differ from the\n * catalogue's: these are this system's variable names, and renaming them would mean\n * touching every component in the library for no gain.\n */\nexport type ThemeColors = {\n background: string\n foreground: string\n card: string\n cardForeground: string\n popover: string\n popoverForeground: string\n primary: string\n primaryForeground: string\n secondary: string\n secondaryForeground: string\n muted: string\n mutedForeground: string\n accent: string\n accentForeground: string\n destructive: string\n destructiveForeground: string\n destructiveSubtle: string\n /** Contrast-safe solid-action presentation, derived from the canonical error role. */\n destructiveAction: string\n destructiveActionForeground: string\n success: string\n successForeground: string\n successSubtle: string\n warning: string\n warningForeground: string\n warningSubtle: string\n info: string\n infoForeground: string\n infoSubtle: string\n border: string\n input: string\n ring: string\n sidebar: string\n sidebarForeground: string\n sidebarAccent: string\n sidebarBorder: string\n sidebarMutedForeground: string\n}\n\n/** The sixteen ANSI colours a terminal needs, plus its cursor and selection. */\nexport type ThemeTerminalPalette = {\n background: string\n foreground: string\n cursor: string\n selection: string\n black: string\n red: string\n green: string\n yellow: string\n blue: string\n magenta: string\n cyan: string\n white: string\n brightBlack: string\n brightRed: string\n brightGreen: string\n brightYellow: string\n brightBlue: string\n brightMagenta: string\n brightCyan: string\n brightWhite: string\n}\n\n/** The syntax roles a code view needs. */\nexport type ThemeEditorPalette = {\n keyword: string\n string: string\n number: string\n comment: string\n function: string\n variable: string\n type: string\n tag: string\n attribute: string\n operator: string\n heading: string\n link: string\n diffAdd: string\n diffDelete: string\n diffHunk: string\n searchMatch: string\n}\n\n/** Where a palette came from, and the terms it is used under. */\nexport type ThemeProvenance = {\n /** The project or person the palette belongs to. */\n source: string\n url: string\n /** An SPDX identifier. Every palette in this catalogue is permissive. */\n license: string\n}\n\nexport type ThemeVariant = {\n /** The `data-theme` value and the id a preference stores. */\n id: string\n /** Groups the variants that came from one project. */\n family: string\n familyLabel: string\n label: string\n appearance: ThemeAppearance\n /** One line, shown in a picker. */\n description: string\n colors: ThemeColors\n terminal: ThemeTerminalPalette\n editor: ThemeEditorPalette\n /** The categorical series, in order. */\n chart: readonly string[]\n provenance: ThemeProvenance\n /**\n * Custom properties written verbatim over {@link themeCssVariables}, last.\n *\n * For a variant a host built from data it does not control the derivation of —\n * an imported theme whose painted colours must stay exactly what the user\n * imported. The catalogue's own variants never carry it, so the derived roles\n * stay the only source of truth for them.\n */\n tokens?: Readonly<Record<string, string>>\n}\n\n/**\n * The shadcn-vocabulary roles of one catalogue entry.\n *\n * The expensive half of a variant: `shadcnVariables` projects the whole palette\n * (about 4 ms a theme), which is why {@link lazyVariant} defers it.\n */\nfunction colorsOf(theme: AdeaThemeRecord): ThemeColors {\n const projected = shadcnVariables(theme)\n const token = (name: string): string => {\n const value = projected[`--${name}`]\n if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)\n return value\n }\n const destructiveAction = shadcnDestructiveProjection(theme)\n return {\n background: token('background'),\n foreground: token('foreground'),\n card: token('card'),\n cardForeground: token('card-foreground'),\n popover: token('popover'),\n popoverForeground: token('popover-foreground'),\n primary: token('primary'),\n primaryForeground: token('primary-foreground'),\n secondary: token('secondary'),\n secondaryForeground: token('secondary-foreground'),\n muted: token('muted'),\n mutedForeground: token('muted-foreground'),\n accent: token('accent'),\n accentForeground: token('accent-foreground'),\n // Preserve the UI's semantic error token. The Themes projection uses its\n // contrast-safe solid-action fill for --destructive; UI keeps that fill in\n // the separate destructiveAction pair below.\n destructive: theme.colors.error,\n destructiveForeground: statusForeground(theme, 'error'),\n destructiveSubtle: tint(theme.colors.error, theme.colors.background),\n destructiveAction: destructiveAction.fill,\n destructiveActionForeground: destructiveAction.foreground,\n success: token('success'),\n successForeground: statusForeground(theme, 'success'),\n successSubtle: tint(theme.colors.success, theme.colors.background),\n warning: token('warning'),\n warningForeground: statusForeground(theme, 'warning'),\n warningSubtle: tint(theme.colors.warning, theme.colors.background),\n info: token('info'),\n infoForeground: statusForeground(theme, 'info'),\n infoSubtle: tint(theme.colors.info, theme.colors.background),\n border: token('border'),\n input: token('input'),\n ring: token('ring'),\n sidebar: token('sidebar'),\n sidebarForeground: token('sidebar-foreground'),\n sidebarAccent: token('sidebar-accent'),\n sidebarBorder: token('sidebar-border'),\n sidebarMutedForeground: token('sidebar-muted-foreground'),\n }\n}\n\n// The embedded terminal uses the theme's own canvas rather than a palette of its\n// own. The ANSI colours came from the palette that produced the canvas, so they\n// were chosen against it; a terminal on a different background would be the one\n// surface in the application not lit by the active theme.\nfunction terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {\n return {\n background: theme.colors.background,\n foreground: theme.colors.foreground,\n cursor: theme.cursor,\n selection: theme.selection,\n ...theme.ansi,\n }\n}\n\nfunction editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {\n const syntax = syntaxRoles(theme)\n return {\n keyword: syntax.keyword,\n string: syntax.string,\n number: syntax.number,\n comment: syntax.comment,\n function: syntax.function,\n variable: syntax.variable,\n type: syntax.type,\n tag: syntax.tag,\n attribute: syntax.attribute,\n operator: syntax.operator,\n heading: syntax.heading,\n link: syntax.link,\n diffAdd: syntax.diffAdd,\n diffDelete: syntax.diffDelete,\n diffHunk: syntax.diffHunk,\n searchMatch: syntax.searchMatch,\n }\n}\n\nfunction provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {\n return {\n source: theme.provenance.project,\n url: theme.provenance.url,\n license: theme.provenance.license,\n }\n}\n\n/**\n * The design system's view of one catalogue record, built in full and at once.\n *\n * For a host that holds a record the catalogue does not — an imported theme, a\n * reconstruction from stored data — and wants a variant it can hand to\n * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot\n * map, here and now, rather than on first paint. The catalogue itself goes through\n * {@link lazyVariant} instead.\n */\nexport function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {\n return {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n colors: colorsOf(theme),\n terminal: terminalOf(theme),\n editor: editorOf(theme),\n chart: chartSeries(theme),\n provenance: provenanceOf(theme),\n }\n}\n\n/** Runs `build` on first call and returns that value from then on. A throw is not cached. */\nfunction once<T>(build: () => T): () => T {\n let built = false\n let value: T\n return () => {\n if (!built) {\n value = build()\n built = true\n }\n return value\n }\n}\n\nconst lazy = <T>(build: () => T): PropertyDescriptor => ({\n get: once(build),\n enumerable: true,\n})\n\n/**\n * {@link variantFromRecord} with the four derived palettes built on first read.\n *\n * Evaluating this module used to project all 34 catalogue themes up front, about\n * 155 ms before a single one was needed, on the hydration path of every consumer.\n * A page paints one variant, and a picker's labels and appearance are plain\n * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read\n * pays for them, once. The properties stay enumerable data-shaped accessors, so\n * spreading, `Object.entries` and JSON see the same object they always did.\n */\nfunction lazyVariant(theme: AdeaThemeRecord): ThemeVariant {\n return Object.defineProperties(\n {\n id: theme.id,\n family: theme.family,\n familyLabel: theme.familyLabel,\n label: theme.label,\n appearance: theme.appearance,\n description: theme.description,\n provenance: provenanceOf(theme),\n },\n {\n colors: lazy(() => colorsOf(theme)),\n terminal: lazy(() => terminalOf(theme)),\n editor: lazy(() => editorOf(theme)),\n chart: lazy(() => chartSeries(theme)),\n }\n ) as ThemeVariant\n}\n\n/**\n * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its\n * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the\n * catalogue has nothing to measure, and gets its appearance's ceiling.\n */\nexport function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>): string {\n const record = getTheme(theme.id)\n return record ? primarySubtleCss(record) : primarySubtleCss(theme.appearance)\n}\n\n/**\n * The catalogue, in catalogue order.\n *\n * The `@__PURE__` marks let a bundler drop the whole table, and with it the catalogue,\n * from a consumer that imports only this module's catalogue-free helpers\n * (`tokensAsHex`, `isThemeAccentId`, `resolveVariant`…) and never reads it.\n */\nexport const builtinThemes: readonly ThemeVariant[] = /* @__PURE__ */ Object.freeze(\n /* @__PURE__ */ catalogue.map(lazyVariant)\n)\n\nexport function themeById(id: string): ThemeVariant | undefined {\n if (!hasTheme(id)) return undefined\n return builtinThemes.find((theme) => theme.id === id)\n}\n\n/** The variants of one appearance, which is what a picker for that mode shows. */\nexport function themesForAppearance(appearance: ThemeAppearance): readonly ThemeVariant[] {\n const ids = new Set(themesByAppearance(appearance).map((theme) => theme.id))\n return builtinThemes.filter((theme) => ids.has(theme.id))\n}\n\n/** The families in the catalogue, in catalogue order. */\nexport function themeFamilies(): { family: string; label: string; themes: ThemeVariant[] }[] {\n const families = new Map<string, { family: string; label: string; themes: ThemeVariant[] }>()\n for (const theme of builtinThemes) {\n const entry = families.get(theme.family) ?? {\n family: theme.family,\n label: theme.familyLabel,\n themes: [],\n }\n entry.themes.push(theme)\n families.set(theme.family, entry)\n }\n return [...families.values()]\n}\n\n/** The accent presets, in the order a picker should show them. */\nexport const accents = ACCENTS\n\n/**\n * The canonical catalogue records, in catalogue order.\n *\n * {@link builtinThemes} is this system's projection of the catalogue onto the\n * shadcn vocabulary, which is what a document is painted with. A *preview* of a\n * theme — the miniatures `AppearanceEditor` and `ThemeMiniature` draw — reads the\n * catalogue's own roles instead (`surfaceActive`, `borderMuted`, `textSubtle`…),\n * several of which the projection folds together. These are those records, so a\n * consumer hands the editor exactly what it takes rather than reconstructing it\n * from the projection.\n */\nexport const themeRecords: readonly AdeaThemeRecord[] = catalogue\n\nexport function themeRecordById(id: string): AdeaThemeRecord | undefined {\n return getTheme(id)\n}\n\n/** A light/dark theme pair by id, with the defaults standing in for an unknown id. */\nfunction recordPair(lightThemeId: string, darkThemeId: string) {\n const light = getTheme(lightThemeId) ?? getTheme(defaultLightThemeId)!\n const dark = getTheme(darkThemeId) ?? getTheme(defaultDarkThemeId)!\n return { light, dark }\n}\n\n/**\n * The accents the selected light/dark pair carries — offered beside the presets.\n *\n * Computed per pair rather than per theme because a theme accent is a *pair* of\n * values, like a preset: a slot is offered only when both themes can offer it.\n */\nexport function themeAccentsFor(\n lightThemeId: string,\n darkThemeId: string\n): readonly AccentPreset[] {\n const { light, dark } = recordPair(lightThemeId, darkThemeId)\n return themeAccentPresets(light, dark)\n}\n\n/**\n * An accent id resolved against a theme pair: a preset, a theme accent the pair\n * offers, or `undefined` for `theme`, an unknown id, or a slot this pair cannot\n * offer — each of which means \"use the theme's own primary\".\n *\n * Theme accent ids are role-shaped (`ansi-blue`), so a stored one survives a theme\n * switch and resolves to the new theme's blue; when the new pair cannot offer the\n * slot it resolves to nothing, the same fallback a stale preset id gets.\n */\nexport function resolveAccentPreset(\n accentId: string,\n light: AdeaTheme,\n dark: AdeaTheme\n): AccentPreset | undefined {\n if (accentId === 'theme') return undefined\n const preset = getAccent(accentId)\n if (preset) return preset\n if (!isThemeAccentId(accentId)) return undefined\n return themeAccentPresets(light, dark).find((option) => option.id === accentId)\n}\n\n/**\n * A theme with an accent laid over it, in the catalogue's own shape.\n *\n * The accent replaces `accent` and its measured label, which is all an accent\n * changes about a theme; `undefined` returns the theme untouched. This is the\n * overlay a host needs for `AppearanceEditor`'s `lightTheme`/`darkTheme`.\n */\nexport function withAccent<T extends AdeaTheme>(theme: T, accent: AccentPreset | undefined): T {\n if (!accent) return theme\n const value = theme.appearance === 'dark' ? accent.dark : accent.light\n return {\n ...theme,\n colors: { ...theme.colors, accent: value, accentForeground: accentForeground(value) },\n }\n}\n\n/**\n * The two preview themes an appearance selection resolves to, accent applied.\n *\n * The adapter between a stored selection and `AppearanceEditor`: it takes ids —\n * what a preference holds — and returns the canonical records the editor's\n * `lightTheme` and `darkTheme` props take, so a host does not map roles by hand.\n * Unknown ids fall back to the defaults and an accent the pair cannot offer falls\n * back to the theme's own, exactly as `ThemeProvider` resolves them.\n */\nexport function appearancePreviewThemes(selection: {\n lightThemeId: string\n darkThemeId: string\n accent: string\n}): { lightTheme: AdeaThemeRecord; darkTheme: AdeaThemeRecord } {\n const { light, dark } = recordPair(selection.lightThemeId, selection.darkThemeId)\n const accent = resolveAccentPreset(selection.accent, light, dark)\n return { lightTheme: withAccent(light, accent), darkTheme: withAccent(dark, accent) }\n}\n\n/**\n * The properties an accent selection overrides, as CSS custom properties.\n *\n * ## Why this exists rather than relying on the `[data-accent]` blocks\n *\n * `theme.css` carries a `[data-accent='…']` block per preset, and those blocks are\n * correct — but they are unreachable at runtime. `ThemeProvider` writes a theme's\n * whole role set onto `<html>` as *inline* custom properties, and an inline\n * declaration outranks any stylesheet selector regardless of specificity. The\n * attribute was set, the rules existed, the values were right, and every one of the\n * seven accents rendered as the same blue.\n *\n * The blocks are kept, because they are what styles the window between the document\n * arriving and the provider mounting — which is the whole reason `themeScript`\n * exists. But the authoritative value has to be applied by the same code that\n * applies the theme, from the same source, or the two can drift again. So both the\n * provider and the CSS generator call `accentRoles` out of `@adea-ai/themes`, and\n * `tests/appearance-axes.test.ts` asserts the two agree value for value.\n *\n * A theme accent (`ansi-blue`) has no block: its value depends on the theme pair,\n * so it is resolved against `pair` — the selection's light and dark theme ids —\n * and only ever applied here. Without a pair it resolves against the defaults.\n *\n * Returns `undefined` for `theme` — the variant's own primary — for an id the\n * catalogue does not have, and for a theme accent the pair does not offer, so a\n * stale stored preference falls back to the theme rather than blanking the primary.\n */\nexport function accentVariables(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): Record<string, string> | undefined {\n const roles = accentRolesForSelection(accentId, appearance, pair, options)\n return roles ? accentOverrideVariables(roles) : undefined\n}\n\n/** What an accent is held to when it is resolved against a surface. */\nexport type AccentVariableOptions = {\n /** The surface the accent must read on: the variant's `colors.background`. */\n background?: string\n /**\n * The contrast floor against `background`. Absent, a preset is painted verbatim\n * and only a custom `#rrggbb` is held to the default floor.\n */\n floor?: number\n /** The variant the accent is applied to, so the tint takes its measured strength. */\n variantId?: string\n}\n\n/**\n * An accent selection resolved to the full {@link AccentRoles}, against a theme\n * pair. {@link accentVariables} is the document half of this; an element-scoped\n * paint (`paintAccentRoles`) wants all of it, including the tint.\n */\nexport function accentRolesForSelection(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): AccentRoles | undefined {\n if (accentId === 'theme') return undefined\n const record = options.variantId ? getTheme(options.variantId) : undefined\n return resolveAccentRoles(accentId, {\n target: record ?? appearance,\n background: options.background,\n floor: options.floor,\n // Only an `ansi-*` id needs the pair's offering, and measuring it is not free.\n themeAccents: isThemeAccentId(accentId)\n ? themeAccentsFor(pair.lightThemeId, pair.darkThemeId)\n : undefined,\n })\n}\n\n/**\n * Which projection fills the `--editor-*` ramp.\n *\n * - `syntax` (the default) is the catalogue's quiet syntax roles, as `ThemeVariant.editor`\n * holds them: a comment may sit below body-text contrast, because in a code view\n * it is meant to recede.\n * - `floor` holds every editor role to 4.5:1 on the canvas (`editorRolesHex`), for a\n * host that renders code as content rather than as decoration. It is hex, where\n * `syntax` is `oklch()`.\n */\nexport type EditorRolesMode = 'syntax' | 'floor'\n\n/** Options for {@link themeCssVariables}. */\nexport type ThemeCssVariableOptions = {\n /** The `--editor-*` projection. Default `syntax`: absent, the output is unchanged. */\n editor?: EditorRolesMode\n}\n\n/**\n * True for the catalogue's own variant, not a host's copy that merely shares its id.\n *\n * Identity rather than id, because a host's imported theme may reuse a catalogue id\n * with different colours, and projecting the catalogue's record for it would paint\n * the wrong palette.\n */\nexport function isCatalogueVariant(variant: ThemeVariant): boolean {\n return themeById(variant.id) === variant\n}\n\n/**\n * The editor palette held to 4.5:1, or `fallback` for a record that cannot reach it.\n *\n * Some palettes (an imported one, most often) cannot be repaired to the floor from\n * their own ramp within the catalogue's lightness budget, and `editorRolesHex` says\n * so by throwing. A palette that paints nothing is worse than one that paints its\n * quiet roles, so the throw is the cue to fall back rather than to fail. `project` is\n * a parameter so that path can be exercised: no catalogue theme takes it.\n */\nexport function floorEditorPalette(\n record: AdeaTheme,\n fallback: ThemeEditorPalette,\n project: (theme: AdeaTheme) => Record<keyof ThemeEditorPalette, string> = editorRolesHex\n): ThemeEditorPalette {\n try {\n const roles = project(record)\n return {\n keyword: roles.keyword,\n string: roles.string,\n number: roles.number,\n comment: roles.comment,\n function: roles.function,\n variable: roles.variable,\n type: roles.type,\n tag: roles.tag,\n attribute: roles.attribute,\n operator: roles.operator,\n heading: roles.heading,\n link: roles.link,\n diffAdd: roles.diffAdd,\n diffDelete: roles.diffDelete,\n diffHunk: roles.diffHunk,\n searchMatch: roles.searchMatch,\n }\n } catch {\n return fallback\n }\n}\n\nconst floorEditorCache = new Map<string, ThemeEditorPalette>()\n\n/**\n * The editor palette a variant paints under a projection.\n *\n * `floor` applies to the catalogue's own variants, whose record the catalogue holds.\n * A variant it does not hold (a host's imported theme) has no record to project and\n * keeps its own `editor`; such a theme normally carries its roles in `tokens`, which\n * win over either projection anyway. Computed once per theme: the repair is a search.\n */\nexport function editorPaletteFor(\n theme: ThemeVariant,\n mode: EditorRolesMode = 'syntax'\n): ThemeEditorPalette {\n if (mode !== 'floor' || !isCatalogueVariant(theme)) return theme.editor\n const cached = floorEditorCache.get(theme.id)\n if (cached) return cached\n const record = getTheme(theme.id)\n const palette = record ? floorEditorPalette(record, theme.editor) : theme.editor\n floorEditorCache.set(theme.id, palette)\n return palette\n}\n\n/**\n * The roles a variant sets, as CSS custom properties.\n *\n * The mapping itself is `themeCssVariablesWith` in `theme-css.ts`; this binds it to\n * the catalogue, which measures a theme's `--primary-subtle` strength\n * ({@link primarySubtleFor}) and projects its floor editor ramp\n * ({@link editorPaletteFor}). Absent `options`, the result is what it has always\n * been; the generated `theme-variants.css` and the design-system artifact are built\n * without it.\n */\nexport function themeCssVariables(\n theme: ThemeVariant,\n options: ThemeCssVariableOptions = {}\n): Record<string, string> {\n return themeCssVariablesWith(\n {\n primarySubtle: primarySubtleFor,\n editor: (variant, mode) => editorPaletteFor(variant, mode),\n },\n theme,\n options\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Validation\n *\n * Delegated. The floors are the catalogue's, because they are what makes the\n * catalogue able to hold other people's palettes at all, and a second set of floors\n * here would be a second answer to the same question — and the one that drifts.\n * ------------------------------------------------------------------------- */\n\nexport type ThemeFinding = {\n themeId: string\n role: string\n message: string\n}\n\nfunction toFinding(finding: ContrastFinding): ThemeFinding {\n return { themeId: finding.themeId, role: finding.role, message: finding.message }\n}\n\n/** Validates one variant. The floors, and the reasoning behind them, live in the catalogue. */\nexport function validateTheme(theme: ThemeVariant): ThemeFinding[] {\n const record = catalogue.find((entry) => entry.id === theme.id)\n if (!record) {\n return [{ themeId: theme.id, role: 'id', message: `${theme.id} is not in the catalogue` }]\n }\n return validateCatalogue([record]).map(toFinding)\n}\n\n/** Validates the whole registry, including the catalogue-level invariants. */\nexport function validateThemeRegistry(\n themes: readonly ThemeVariant[] = builtinThemes\n): ThemeFinding[] {\n const records = themes\n .map((theme) => catalogue.find((entry) => entry.id === theme.id))\n .filter((record): record is AdeaThemeRecord => !!record)\n\n const orphaned = themes\n .filter((theme) => !hasTheme(theme.id))\n .map((theme) => ({\n themeId: theme.id,\n role: 'id',\n message: `${theme.id} is in the registry but not in the catalogue`,\n }))\n\n return [...orphaned, ...validateCatalogue(records).map(toFinding)]\n}\n\n/** Re-exported so a consumer measuring a colour does not need a second import. */\nexport { contrastRatio } from '@adea-ai/themes'\n"],"mappings":"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"}
1
+ {"version":3,"file":"themes.js","names":["catalogue","themes"],"sources":["../../src/lib/themes.ts"],"sourcesContent":["/**\n * The theme registry, now a bridge.\n *\n * The catalogue itself lives in `@adea-ai/themes`. This module is what is left in\n * the design system: the translation between the catalogue's **canonical** role\n * names and the **shadcn** vocabulary every component here is written against, plus\n * the handful of custom properties this system declares that are presentations of\n * data the catalogue already holds — the `--terminal-*` ramp, the `--editor-*` ramp,\n * and `--chrome`.\n *\n * ## Why it is a bridge and not a copy\n *\n * This file used to hold thirteen themes as hand-written hex literal tables, with its\n * own contrast validator, its own terminal and editor palettes, and its own\n * provenance records. That is a lot of surface to maintain for one consumer of a\n * palette, and it meant every product that wanted the same themes — Adea, Cortana —\n * either imported this package or wrote its own. The catalogue moved out so that\n * there is one answer to \"what is Catppuccin Mocha\", and this file is the part that\n * genuinely belongs to the design system.\n *\n * It is deliberately thin. If you are looking for a colour, it is not here: see\n * `@adea-ai/themes`, whose README explains how the values are produced and whose\n * `NOTICE` records where every palette came from.\n *\n * ## The mapping, and the entry that is not a rename\n *\n * The package-owned `shadcnVariables` adapter maps canonical roles onto this\n * system's vocabulary. One entry will trip a reader: the catalogue's `accent`\n * becomes this system's `primary`, because in the shadcn vocabulary `--primary` is\n * the action colour — the fill of a primary button, the thing a focus ring is drawn\n * in — while `--accent` is a low-contrast hover wash. The catalogue's `accent` is\n * the former, so it goes to `--primary`, and `--accent` is filled from\n * `surfaceHover`, which is what shadcn components actually use it for.\n */\n\nimport {\n accentForeground,\n editorRolesHex,\n getAccent,\n getTheme,\n hasTheme,\n primarySubtleCss,\n themes as catalogue,\n themesByAppearance,\n themeAccentPresets,\n validateCatalogue,\n type AccentPreset,\n type AdeaTheme,\n type AdeaThemeRecord,\n type ContrastFinding,\n} from '@adea-ai/themes'\nimport { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'\nimport { isThemeAccentId } from './theme-accents'\nimport { themeCssVariablesWith } from './theme-css'\nimport { defaultDarkThemeId, defaultLightThemeId } from './theme-defaults'\nimport { lazyVariantFromRecord } from './theme-records'\n\nexport { accents } from './accent-presets'\nexport { isThemeAccentId } from './theme-accents'\nexport { terminalCssVariables, tokensAsHex } from './theme-css'\nexport { defaultDarkThemeId, defaultLightThemeId, resolveVariant } from './theme-defaults'\nexport { variantFromRecord } from './theme-records'\n\nexport type ThemeAppearance = 'light' | 'dark'\n\n/**\n * The surface and text roles, in shadcn's vocabulary.\n *\n * Named to match the components below, which is why they differ from the\n * catalogue's: these are this system's variable names, and renaming them would mean\n * touching every component in the library for no gain.\n */\nexport type ThemeColors = {\n background: string\n foreground: string\n card: string\n cardForeground: string\n popover: string\n popoverForeground: string\n primary: string\n primaryForeground: string\n secondary: string\n secondaryForeground: string\n muted: string\n mutedForeground: string\n accent: string\n accentForeground: string\n destructive: string\n destructiveForeground: string\n destructiveSubtle: string\n /** Contrast-safe solid-action presentation, derived from the canonical error role. */\n destructiveAction: string\n destructiveActionForeground: string\n success: string\n successForeground: string\n successSubtle: string\n warning: string\n warningForeground: string\n warningSubtle: string\n info: string\n infoForeground: string\n infoSubtle: string\n border: string\n input: string\n ring: string\n sidebar: string\n sidebarForeground: string\n sidebarAccent: string\n sidebarBorder: string\n sidebarMutedForeground: string\n}\n\n/** The sixteen ANSI colours a terminal needs, plus its cursor and selection. */\nexport type ThemeTerminalPalette = {\n background: string\n foreground: string\n cursor: string\n selection: string\n black: string\n red: string\n green: string\n yellow: string\n blue: string\n magenta: string\n cyan: string\n white: string\n brightBlack: string\n brightRed: string\n brightGreen: string\n brightYellow: string\n brightBlue: string\n brightMagenta: string\n brightCyan: string\n brightWhite: string\n}\n\n/** The syntax roles a code view needs. */\nexport type ThemeEditorPalette = {\n keyword: string\n string: string\n number: string\n comment: string\n function: string\n variable: string\n type: string\n tag: string\n attribute: string\n operator: string\n heading: string\n link: string\n diffAdd: string\n diffDelete: string\n diffHunk: string\n searchMatch: string\n}\n\n/** Where a palette came from, and the terms it is used under. */\nexport type ThemeProvenance = {\n /** The project or person the palette belongs to. */\n source: string\n url: string\n /** An SPDX identifier. Every palette in this catalogue is permissive. */\n license: string\n}\n\nexport type ThemeVariant = {\n /** The `data-theme` value and the id a preference stores. */\n id: string\n /** Groups the variants that came from one project. */\n family: string\n familyLabel: string\n label: string\n appearance: ThemeAppearance\n /** One line, shown in a picker. */\n description: string\n colors: ThemeColors\n terminal: ThemeTerminalPalette\n editor: ThemeEditorPalette\n /** The categorical series, in order. */\n chart: readonly string[]\n provenance: ThemeProvenance\n /**\n * Custom properties written verbatim over {@link themeCssVariables}, last.\n *\n * For a variant a host built from data it does not control the derivation of —\n * an imported theme whose painted colours must stay exactly what the user\n * imported. The catalogue's own variants never carry it, so the derived roles\n * stay the only source of truth for them.\n */\n tokens?: Readonly<Record<string, string>>\n}\n\n/**\n * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its\n * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the\n * catalogue has nothing to measure, and gets its appearance's ceiling.\n */\nexport function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>): string {\n const record = getTheme(theme.id)\n return record ? primarySubtleCss(record) : primarySubtleCss(theme.appearance)\n}\n\n/**\n * The catalogue, in catalogue order.\n *\n * The `@__PURE__` marks let a bundler drop the whole table, and with it the catalogue,\n * from a consumer that imports only this module's catalogue-free helpers\n * (`tokensAsHex`, `isThemeAccentId`, `resolveVariant`…) and never reads it.\n */\nexport const builtinThemes: readonly ThemeVariant[] = /* @__PURE__ */ Object.freeze(\n /* @__PURE__ */ catalogue.map(lazyVariantFromRecord)\n)\n\nexport function themeById(id: string): ThemeVariant | undefined {\n if (!hasTheme(id)) return undefined\n return builtinThemes.find((theme) => theme.id === id)\n}\n\n/** The variants of one appearance, which is what a picker for that mode shows. */\nexport function themesForAppearance(appearance: ThemeAppearance): readonly ThemeVariant[] {\n const ids = new Set(themesByAppearance(appearance).map((theme) => theme.id))\n return builtinThemes.filter((theme) => ids.has(theme.id))\n}\n\n/** The families in the catalogue, in catalogue order. */\nexport function themeFamilies(): { family: string; label: string; themes: ThemeVariant[] }[] {\n const families = new Map<string, { family: string; label: string; themes: ThemeVariant[] }>()\n for (const theme of builtinThemes) {\n const entry = families.get(theme.family) ?? {\n family: theme.family,\n label: theme.familyLabel,\n themes: [],\n }\n entry.themes.push(theme)\n families.set(theme.family, entry)\n }\n return [...families.values()]\n}\n\n/**\n * The canonical catalogue records, in catalogue order.\n *\n * {@link builtinThemes} is this system's projection of the catalogue onto the\n * shadcn vocabulary, which is what a document is painted with. A *preview* of a\n * theme — the miniatures `AppearanceEditor` and `ThemeMiniature` draw — reads the\n * catalogue's own roles instead (`surfaceActive`, `borderMuted`, `textSubtle`…),\n * several of which the projection folds together. These are those records, so a\n * consumer hands the editor exactly what it takes rather than reconstructing it\n * from the projection.\n */\nexport const themeRecords: readonly AdeaThemeRecord[] = catalogue\n\nexport function themeRecordById(id: string): AdeaThemeRecord | undefined {\n return getTheme(id)\n}\n\n/** A light/dark theme pair by id, with the defaults standing in for an unknown id. */\nfunction recordPair(lightThemeId: string, darkThemeId: string) {\n const light = getTheme(lightThemeId) ?? getTheme(defaultLightThemeId)!\n const dark = getTheme(darkThemeId) ?? getTheme(defaultDarkThemeId)!\n return { light, dark }\n}\n\n/**\n * The accents the selected light/dark pair carries — offered beside the presets.\n *\n * Computed per pair rather than per theme because a theme accent is a *pair* of\n * values, like a preset: a slot is offered only when both themes can offer it.\n */\nexport function themeAccentsFor(\n lightThemeId: string,\n darkThemeId: string\n): readonly AccentPreset[] {\n const { light, dark } = recordPair(lightThemeId, darkThemeId)\n return themeAccentPresets(light, dark)\n}\n\n/**\n * An accent id resolved against a theme pair: a preset, a theme accent the pair\n * offers, or `undefined` for `theme`, an unknown id, or a slot this pair cannot\n * offer — each of which means \"use the theme's own primary\".\n *\n * Theme accent ids are role-shaped (`ansi-blue`), so a stored one survives a theme\n * switch and resolves to the new theme's blue; when the new pair cannot offer the\n * slot it resolves to nothing, the same fallback a stale preset id gets.\n */\nexport function resolveAccentPreset(\n accentId: string,\n light: AdeaTheme,\n dark: AdeaTheme\n): AccentPreset | undefined {\n if (accentId === 'theme') return undefined\n const preset = getAccent(accentId)\n if (preset) return preset\n if (!isThemeAccentId(accentId)) return undefined\n return themeAccentPresets(light, dark).find((option) => option.id === accentId)\n}\n\n/**\n * A theme with an accent laid over it, in the catalogue's own shape.\n *\n * The accent replaces `accent` and its measured label, which is all an accent\n * changes about a theme; `undefined` returns the theme untouched. This is the\n * overlay a host needs for `AppearanceEditor`'s `lightTheme`/`darkTheme`.\n */\nexport function withAccent<T extends AdeaTheme>(theme: T, accent: AccentPreset | undefined): T {\n if (!accent) return theme\n const value = theme.appearance === 'dark' ? accent.dark : accent.light\n return {\n ...theme,\n colors: { ...theme.colors, accent: value, accentForeground: accentForeground(value) },\n }\n}\n\n/**\n * The two preview themes an appearance selection resolves to, accent applied.\n *\n * The adapter between a stored selection and `AppearanceEditor`: it takes ids —\n * what a preference holds — and returns the canonical records the editor's\n * `lightTheme` and `darkTheme` props take, so a host does not map roles by hand.\n * Unknown ids fall back to the defaults and an accent the pair cannot offer falls\n * back to the theme's own, exactly as `ThemeProvider` resolves them.\n */\nexport function appearancePreviewThemes(selection: {\n lightThemeId: string\n darkThemeId: string\n accent: string\n}): { lightTheme: AdeaThemeRecord; darkTheme: AdeaThemeRecord } {\n const { light, dark } = recordPair(selection.lightThemeId, selection.darkThemeId)\n const accent = resolveAccentPreset(selection.accent, light, dark)\n return { lightTheme: withAccent(light, accent), darkTheme: withAccent(dark, accent) }\n}\n\n/**\n * The properties an accent selection overrides, as CSS custom properties.\n *\n * ## Why this exists rather than relying on the `[data-accent]` blocks\n *\n * `theme.css` carries a `[data-accent='…']` block per preset, and those blocks are\n * correct — but they are unreachable at runtime. `ThemeProvider` writes a theme's\n * whole role set onto `<html>` as *inline* custom properties, and an inline\n * declaration outranks any stylesheet selector regardless of specificity. The\n * attribute was set, the rules existed, the values were right, and every one of the\n * seven accents rendered as the same blue.\n *\n * The blocks are kept, because they are what styles the window between the document\n * arriving and the provider mounting — which is the whole reason `themeScript`\n * exists. But the authoritative value has to be applied by the same code that\n * applies the theme, from the same source, or the two can drift again. So both the\n * provider and the CSS generator call `accentRoles` out of `@adea-ai/themes`, and\n * `tests/appearance-axes.test.ts` asserts the two agree value for value.\n *\n * A theme accent (`ansi-blue`) has no block: its value depends on the theme pair,\n * so it is resolved against `pair` — the selection's light and dark theme ids —\n * and only ever applied here. Without a pair it resolves against the defaults.\n *\n * Returns `undefined` for `theme` — the variant's own primary — for an id the\n * catalogue does not have, and for a theme accent the pair does not offer, so a\n * stale stored preference falls back to the theme rather than blanking the primary.\n */\nexport function accentVariables(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): Record<string, string> | undefined {\n const roles = accentRolesForSelection(accentId, appearance, pair, options)\n return roles ? accentOverrideVariables(roles) : undefined\n}\n\n/** What an accent is held to when it is resolved against a surface. */\nexport type AccentVariableOptions = {\n /** The surface the accent must read on: the variant's `colors.background`. */\n background?: string\n /**\n * The contrast floor against `background`. Absent, a preset is painted verbatim\n * and only a custom `#rrggbb` is held to the default floor.\n */\n floor?: number\n /** The variant the accent is applied to, so the tint takes its measured strength. */\n variantId?: string\n}\n\n/**\n * An accent selection resolved to the full {@link AccentRoles}, against a theme\n * pair. {@link accentVariables} is the document half of this; an element-scoped\n * paint (`paintAccentRoles`) wants all of it, including the tint.\n */\nexport function accentRolesForSelection(\n accentId: string,\n appearance: ThemeAppearance,\n pair: { lightThemeId: string; darkThemeId: string } = {\n lightThemeId: defaultLightThemeId,\n darkThemeId: defaultDarkThemeId,\n },\n options: AccentVariableOptions = {}\n): AccentRoles | undefined {\n if (accentId === 'theme') return undefined\n const record = options.variantId ? getTheme(options.variantId) : undefined\n return resolveAccentRoles(accentId, {\n target: record ?? appearance,\n background: options.background,\n floor: options.floor,\n // Only an `ansi-*` id needs the pair's offering, and measuring it is not free.\n themeAccents: isThemeAccentId(accentId)\n ? themeAccentsFor(pair.lightThemeId, pair.darkThemeId)\n : undefined,\n })\n}\n\n/**\n * Which projection fills the `--editor-*` ramp.\n *\n * - `syntax` (the default) is the catalogue's quiet syntax roles, as `ThemeVariant.editor`\n * holds them: a comment may sit below body-text contrast, because in a code view\n * it is meant to recede.\n * - `floor` holds every editor role to 4.5:1 on the canvas (`editorRolesHex`), for a\n * host that renders code as content rather than as decoration. It is hex, where\n * `syntax` is `oklch()`.\n */\nexport type EditorRolesMode = 'syntax' | 'floor'\n\n/** Options for {@link themeCssVariables}. */\nexport type ThemeCssVariableOptions = {\n /** The `--editor-*` projection. Default `syntax`: absent, the output is unchanged. */\n editor?: EditorRolesMode\n}\n\n/**\n * True for the catalogue's own variant, not a host's copy that merely shares its id.\n *\n * Identity rather than id, because a host's imported theme may reuse a catalogue id\n * with different colours, and projecting the catalogue's record for it would paint\n * the wrong palette.\n */\nexport function isCatalogueVariant(variant: ThemeVariant): boolean {\n return themeById(variant.id) === variant\n}\n\n/**\n * The editor palette held to 4.5:1, or `fallback` for a record that cannot reach it.\n *\n * Some palettes (an imported one, most often) cannot be repaired to the floor from\n * their own ramp within the catalogue's lightness budget, and `editorRolesHex` says\n * so by throwing. A palette that paints nothing is worse than one that paints its\n * quiet roles, so the throw is the cue to fall back rather than to fail. `project` is\n * a parameter so that path can be exercised: no catalogue theme takes it.\n */\nexport function floorEditorPalette(\n record: AdeaTheme,\n fallback: ThemeEditorPalette,\n project: (theme: AdeaTheme) => Record<keyof ThemeEditorPalette, string> = editorRolesHex\n): ThemeEditorPalette {\n try {\n const roles = project(record)\n return {\n keyword: roles.keyword,\n string: roles.string,\n number: roles.number,\n comment: roles.comment,\n function: roles.function,\n variable: roles.variable,\n type: roles.type,\n tag: roles.tag,\n attribute: roles.attribute,\n operator: roles.operator,\n heading: roles.heading,\n link: roles.link,\n diffAdd: roles.diffAdd,\n diffDelete: roles.diffDelete,\n diffHunk: roles.diffHunk,\n searchMatch: roles.searchMatch,\n }\n } catch {\n return fallback\n }\n}\n\nconst floorEditorCache = new Map<string, ThemeEditorPalette>()\n\n/**\n * The editor palette a variant paints under a projection.\n *\n * `floor` applies to the catalogue's own variants, whose record the catalogue holds.\n * A variant it does not hold (a host's imported theme) has no record to project and\n * keeps its own `editor`; such a theme normally carries its roles in `tokens`, which\n * win over either projection anyway. Computed once per theme: the repair is a search.\n */\nexport function editorPaletteFor(\n theme: ThemeVariant,\n mode: EditorRolesMode = 'syntax'\n): ThemeEditorPalette {\n if (mode !== 'floor' || !isCatalogueVariant(theme)) return theme.editor\n const cached = floorEditorCache.get(theme.id)\n if (cached) return cached\n const record = getTheme(theme.id)\n const palette = record ? floorEditorPalette(record, theme.editor) : theme.editor\n floorEditorCache.set(theme.id, palette)\n return palette\n}\n\n/**\n * The roles a variant sets, as CSS custom properties.\n *\n * The mapping itself is `themeCssVariablesWith` in `theme-css.ts`; this binds it to\n * the catalogue, which measures a theme's `--primary-subtle` strength\n * ({@link primarySubtleFor}) and projects its floor editor ramp\n * ({@link editorPaletteFor}). Absent `options`, the result is what it has always\n * been; the generated `theme-variants.css` and the design-system artifact are built\n * without it.\n */\nexport function themeCssVariables(\n theme: ThemeVariant,\n options: ThemeCssVariableOptions = {}\n): Record<string, string> {\n return themeCssVariablesWith(\n {\n primarySubtle: primarySubtleFor,\n editor: (variant, mode) => editorPaletteFor(variant, mode),\n },\n theme,\n options\n )\n}\n\n/* ---------------------------------------------------------------------------\n * Validation\n *\n * Delegated. The floors are the catalogue's, because they are what makes the\n * catalogue able to hold other people's palettes at all, and a second set of floors\n * here would be a second answer to the same question — and the one that drifts.\n * ------------------------------------------------------------------------- */\n\nexport type ThemeFinding = {\n themeId: string\n role: string\n message: string\n}\n\nfunction toFinding(finding: ContrastFinding): ThemeFinding {\n return { themeId: finding.themeId, role: finding.role, message: finding.message }\n}\n\n/** Validates one variant. The floors, and the reasoning behind them, live in the catalogue. */\nexport function validateTheme(theme: ThemeVariant): ThemeFinding[] {\n const record = catalogue.find((entry) => entry.id === theme.id)\n if (!record) {\n return [{ themeId: theme.id, role: 'id', message: `${theme.id} is not in the catalogue` }]\n }\n return validateCatalogue([record]).map(toFinding)\n}\n\n/** Validates the whole registry, including the catalogue-level invariants. */\nexport function validateThemeRegistry(\n themes: readonly ThemeVariant[] = builtinThemes\n): ThemeFinding[] {\n const records = themes\n .map((theme) => catalogue.find((entry) => entry.id === theme.id))\n .filter((record): record is AdeaThemeRecord => !!record)\n\n const orphaned = themes\n .filter((theme) => !hasTheme(theme.id))\n .map((theme) => ({\n themeId: theme.id,\n role: 'id',\n message: `${theme.id} is in the registry but not in the catalogue`,\n }))\n\n return [...orphaned, ...validateCatalogue(records).map(toFinding)]\n}\n\n/** Re-exported so a consumer measuring a colour does not need a second import. */\nexport { contrastRatio } from '@adea-ai/themes'\n"],"mappings":"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"}
package/dist/r/lib.json CHANGED
@@ -110,6 +110,11 @@
110
110
  "type": "registry:lib",
111
111
  "target": "lib/theme-engine.ts"
112
112
  },
113
+ {
114
+ "path": "src/lib/theme-records.ts",
115
+ "type": "registry:lib",
116
+ "target": "lib/theme-records.ts"
117
+ },
113
118
  {
114
119
  "path": "src/lib/theme-surface.ts",
115
120
  "type": "registry:lib",
@@ -2532,6 +2532,11 @@
2532
2532
  "type": "registry:ui",
2533
2533
  "target": "components/theming/lazy.ts"
2534
2534
  },
2535
+ {
2536
+ "path": "src/components/theme/script.ts",
2537
+ "type": "registry:ui",
2538
+ "target": "components/theming/script.ts"
2539
+ },
2535
2540
  {
2536
2541
  "path": "src/components/theme/theme-catalogue-source.ts",
2537
2542
  "type": "registry:ui",
@@ -2789,6 +2794,11 @@
2789
2794
  "type": "registry:lib",
2790
2795
  "target": "lib/theme-engine.ts"
2791
2796
  },
2797
+ {
2798
+ "path": "src/lib/theme-records.ts",
2799
+ "type": "registry:lib",
2800
+ "target": "lib/theme-records.ts"
2801
+ },
2792
2802
  {
2793
2803
  "path": "src/lib/theme-surface.ts",
2794
2804
  "type": "registry:lib",
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The no-flash boot script, importable where there is no UI runtime.
3
+ *
4
+ * `themeScript` writes the document attributes before first paint. A host that
5
+ * renders its HTML at build time (a Vite `index.html` served by a desktop shell
6
+ * whose CSP allows no inline script) needs the string in Node, and the provider
7
+ * entries cannot be imported there: they load Solid's client runtime. This entry
8
+ * holds only the script and the types it takes, so it loads anywhere.
9
+ */
10
+ export { inlineScriptLiteral, themeScript, type ThemeScriptOptions } from './theme-script'
11
+ export type { ThemeStorageSchema } from './theme-persistence'
@@ -4,7 +4,8 @@ import { ACCENTS } from '@adea-ai/themes'
4
4
  * Its own module so the theme provider's start-up path (the stored-preference reader
5
5
  * and the boot script both need the preset ids) does not import the token manifest in
6
6
  * `tokens.ts`, which is 20 KB of documentation data a provider never reads.
7
- * `tokens.ts` re-exports both names, so every existing import keeps working.
7
+ * `tokens.ts` re-exports `accentPresets` and `AccentPreset`, and `themes.ts` re-exports
8
+ * `accents`, so every existing import keeps working.
8
9
  */
9
10
 
10
11
  /**
@@ -41,3 +42,13 @@ export const accentPresets: readonly AccentPreset[] = Object.freeze([
41
42
  },
42
43
  ...ACCENTS,
43
44
  ])
45
+
46
+ /**
47
+ * The six brand accent presets, in the order a picker should show them: the list
48
+ * `accentPresets` carries after its own `theme` entry.
49
+ *
50
+ * Here and not beside the theme table in `lib/themes.ts` (which re-exports it, so
51
+ * `@adea-ai/ui/lib/themes` still has it) because a bundler places a module whole, and
52
+ * a host that wants only the list should not ship the 34-theme catalogue with it.
53
+ */
54
+ export const accents = ACCENTS
@@ -0,0 +1,205 @@
1
+ /**
2
+ * One catalogue record, as this system's {@link ThemeVariant}.
3
+ *
4
+ * `variantFromRecord` and the lazy form the catalogue is built with live here, and not
5
+ * beside the 34-theme table in `lib/themes.ts`, because a bundler places a module
6
+ * whole: a host that imports `variantFromRecord` from the same module as the table
7
+ * ships the table with it. This module reads a record it is handed and imports no
8
+ * catalogue, so a start-up path that only turns a stored record into a variant
9
+ * (an imported theme, a reconstruction) stays small. `lib/themes.ts` re-exports
10
+ * `variantFromRecord`, so `@adea-ai/ui/lib/themes` keeps working; the lazy start-up
11
+ * gate (`check:theme-startup`) holds the split in place.
12
+ *
13
+ * It does import `@adea-ai/themes` for the projection helpers (`chartSeries`,
14
+ * `tint`, `shadcnVariables`…), none of which reads the table of themes.
15
+ */
16
+ import {
17
+ chartSeries,
18
+ shadcnDestructiveProjection,
19
+ statusForeground,
20
+ syntaxRoles,
21
+ tint,
22
+ type AdeaThemeRecord,
23
+ } from '@adea-ai/themes'
24
+ import { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
25
+ import type {
26
+ ThemeColors,
27
+ ThemeEditorPalette,
28
+ ThemeProvenance,
29
+ ThemeTerminalPalette,
30
+ ThemeVariant,
31
+ } from './themes'
32
+
33
+ /**
34
+ * The shadcn-vocabulary roles of one catalogue entry.
35
+ *
36
+ * The expensive half of a variant: `shadcnVariables` projects the whole palette
37
+ * (about 4 ms a theme), which is why {@link lazyVariantFromRecord} defers it.
38
+ */
39
+ function colorsOf(theme: AdeaThemeRecord): ThemeColors {
40
+ const projected = shadcnVariables(theme)
41
+ const token = (name: string): string => {
42
+ const value = projected[`--${name}`]
43
+ if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)
44
+ return value
45
+ }
46
+ const destructiveAction = shadcnDestructiveProjection(theme)
47
+ return {
48
+ background: token('background'),
49
+ foreground: token('foreground'),
50
+ card: token('card'),
51
+ cardForeground: token('card-foreground'),
52
+ popover: token('popover'),
53
+ popoverForeground: token('popover-foreground'),
54
+ primary: token('primary'),
55
+ primaryForeground: token('primary-foreground'),
56
+ secondary: token('secondary'),
57
+ secondaryForeground: token('secondary-foreground'),
58
+ muted: token('muted'),
59
+ mutedForeground: token('muted-foreground'),
60
+ accent: token('accent'),
61
+ accentForeground: token('accent-foreground'),
62
+ // Preserve the UI's semantic error token. The Themes projection uses its
63
+ // contrast-safe solid-action fill for --destructive; UI keeps that fill in
64
+ // the separate destructiveAction pair below.
65
+ destructive: theme.colors.error,
66
+ destructiveForeground: statusForeground(theme, 'error'),
67
+ destructiveSubtle: tint(theme.colors.error, theme.colors.background),
68
+ destructiveAction: destructiveAction.fill,
69
+ destructiveActionForeground: destructiveAction.foreground,
70
+ success: token('success'),
71
+ successForeground: statusForeground(theme, 'success'),
72
+ successSubtle: tint(theme.colors.success, theme.colors.background),
73
+ warning: token('warning'),
74
+ warningForeground: statusForeground(theme, 'warning'),
75
+ warningSubtle: tint(theme.colors.warning, theme.colors.background),
76
+ info: token('info'),
77
+ infoForeground: statusForeground(theme, 'info'),
78
+ infoSubtle: tint(theme.colors.info, theme.colors.background),
79
+ border: token('border'),
80
+ input: token('input'),
81
+ ring: token('ring'),
82
+ sidebar: token('sidebar'),
83
+ sidebarForeground: token('sidebar-foreground'),
84
+ sidebarAccent: token('sidebar-accent'),
85
+ sidebarBorder: token('sidebar-border'),
86
+ sidebarMutedForeground: token('sidebar-muted-foreground'),
87
+ }
88
+ }
89
+
90
+ // The embedded terminal uses the theme's own canvas rather than a palette of its
91
+ // own. The ANSI colours came from the palette that produced the canvas, so they
92
+ // were chosen against it; a terminal on a different background would be the one
93
+ // surface in the application not lit by the active theme.
94
+ function terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {
95
+ return {
96
+ background: theme.colors.background,
97
+ foreground: theme.colors.foreground,
98
+ cursor: theme.cursor,
99
+ selection: theme.selection,
100
+ ...theme.ansi,
101
+ }
102
+ }
103
+
104
+ function editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {
105
+ const syntax = syntaxRoles(theme)
106
+ return {
107
+ keyword: syntax.keyword,
108
+ string: syntax.string,
109
+ number: syntax.number,
110
+ comment: syntax.comment,
111
+ function: syntax.function,
112
+ variable: syntax.variable,
113
+ type: syntax.type,
114
+ tag: syntax.tag,
115
+ attribute: syntax.attribute,
116
+ operator: syntax.operator,
117
+ heading: syntax.heading,
118
+ link: syntax.link,
119
+ diffAdd: syntax.diffAdd,
120
+ diffDelete: syntax.diffDelete,
121
+ diffHunk: syntax.diffHunk,
122
+ searchMatch: syntax.searchMatch,
123
+ }
124
+ }
125
+
126
+ function provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {
127
+ return {
128
+ source: theme.provenance.project,
129
+ url: theme.provenance.url,
130
+ license: theme.provenance.license,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * The design system's view of one catalogue record, built in full and at once.
136
+ *
137
+ * For a host that holds a record the catalogue does not — an imported theme, a
138
+ * reconstruction from stored data — and wants a variant it can hand to
139
+ * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot
140
+ * map, here and now, rather than on first paint. The catalogue itself goes through
141
+ * {@link lazyVariantFromRecord} instead.
142
+ */
143
+ export function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
144
+ return {
145
+ id: theme.id,
146
+ family: theme.family,
147
+ familyLabel: theme.familyLabel,
148
+ label: theme.label,
149
+ appearance: theme.appearance,
150
+ description: theme.description,
151
+ colors: colorsOf(theme),
152
+ terminal: terminalOf(theme),
153
+ editor: editorOf(theme),
154
+ chart: chartSeries(theme),
155
+ provenance: provenanceOf(theme),
156
+ }
157
+ }
158
+
159
+ /** Runs `build` on first call and returns that value from then on. A throw is not cached. */
160
+ function once<T>(build: () => T): () => T {
161
+ let built = false
162
+ let value: T
163
+ return () => {
164
+ if (!built) {
165
+ value = build()
166
+ built = true
167
+ }
168
+ return value
169
+ }
170
+ }
171
+
172
+ const lazy = <T>(build: () => T): PropertyDescriptor => ({
173
+ get: once(build),
174
+ enumerable: true,
175
+ })
176
+
177
+ /**
178
+ * {@link variantFromRecord} with the four derived palettes built on first read.
179
+ *
180
+ * Evaluating this module used to project all 34 catalogue themes up front, about
181
+ * 155 ms before a single one was needed, on the hydration path of every consumer.
182
+ * A page paints one variant, and a picker's labels and appearance are plain
183
+ * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read
184
+ * pays for them, once. The properties stay enumerable data-shaped accessors, so
185
+ * spreading, `Object.entries` and JSON see the same object they always did.
186
+ */
187
+ export function lazyVariantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
188
+ return Object.defineProperties(
189
+ {
190
+ id: theme.id,
191
+ family: theme.family,
192
+ familyLabel: theme.familyLabel,
193
+ label: theme.label,
194
+ appearance: theme.appearance,
195
+ description: theme.description,
196
+ provenance: provenanceOf(theme),
197
+ },
198
+ {
199
+ colors: lazy(() => colorsOf(theme)),
200
+ terminal: lazy(() => terminalOf(theme)),
201
+ editor: lazy(() => editorOf(theme)),
202
+ chart: lazy(() => chartSeries(theme)),
203
+ }
204
+ ) as ThemeVariant
205
+ }
@@ -34,18 +34,12 @@
34
34
  */
35
35
 
36
36
  import {
37
- ACCENTS,
38
37
  accentForeground,
39
- chartSeries,
40
38
  editorRolesHex,
41
39
  getAccent,
42
40
  getTheme,
43
41
  hasTheme,
44
- shadcnDestructiveProjection,
45
- statusForeground,
46
- tint,
47
42
  primarySubtleCss,
48
- syntaxRoles,
49
43
  themes as catalogue,
50
44
  themesByAppearance,
51
45
  themeAccentPresets,
@@ -55,15 +49,17 @@ import {
55
49
  type AdeaThemeRecord,
56
50
  type ContrastFinding,
57
51
  } from '@adea-ai/themes'
58
- import { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
59
52
  import { accentOverrideVariables, resolveAccentRoles, type AccentRoles } from './accent-roles'
60
53
  import { isThemeAccentId } from './theme-accents'
61
54
  import { themeCssVariablesWith } from './theme-css'
62
55
  import { defaultDarkThemeId, defaultLightThemeId } from './theme-defaults'
56
+ import { lazyVariantFromRecord } from './theme-records'
63
57
 
58
+ export { accents } from './accent-presets'
64
59
  export { isThemeAccentId } from './theme-accents'
65
60
  export { terminalCssVariables, tokensAsHex } from './theme-css'
66
61
  export { defaultDarkThemeId, defaultLightThemeId, resolveVariant } from './theme-defaults'
62
+ export { variantFromRecord } from './theme-records'
67
63
 
68
64
  export type ThemeAppearance = 'light' | 'dark'
69
65
 
@@ -194,180 +190,6 @@ export type ThemeVariant = {
194
190
  tokens?: Readonly<Record<string, string>>
195
191
  }
196
192
 
197
- /**
198
- * The shadcn-vocabulary roles of one catalogue entry.
199
- *
200
- * The expensive half of a variant: `shadcnVariables` projects the whole palette
201
- * (about 4 ms a theme), which is why {@link lazyVariant} defers it.
202
- */
203
- function colorsOf(theme: AdeaThemeRecord): ThemeColors {
204
- const projected = shadcnVariables(theme)
205
- const token = (name: string): string => {
206
- const value = projected[`--${name}`]
207
- if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)
208
- return value
209
- }
210
- const destructiveAction = shadcnDestructiveProjection(theme)
211
- return {
212
- background: token('background'),
213
- foreground: token('foreground'),
214
- card: token('card'),
215
- cardForeground: token('card-foreground'),
216
- popover: token('popover'),
217
- popoverForeground: token('popover-foreground'),
218
- primary: token('primary'),
219
- primaryForeground: token('primary-foreground'),
220
- secondary: token('secondary'),
221
- secondaryForeground: token('secondary-foreground'),
222
- muted: token('muted'),
223
- mutedForeground: token('muted-foreground'),
224
- accent: token('accent'),
225
- accentForeground: token('accent-foreground'),
226
- // Preserve the UI's semantic error token. The Themes projection uses its
227
- // contrast-safe solid-action fill for --destructive; UI keeps that fill in
228
- // the separate destructiveAction pair below.
229
- destructive: theme.colors.error,
230
- destructiveForeground: statusForeground(theme, 'error'),
231
- destructiveSubtle: tint(theme.colors.error, theme.colors.background),
232
- destructiveAction: destructiveAction.fill,
233
- destructiveActionForeground: destructiveAction.foreground,
234
- success: token('success'),
235
- successForeground: statusForeground(theme, 'success'),
236
- successSubtle: tint(theme.colors.success, theme.colors.background),
237
- warning: token('warning'),
238
- warningForeground: statusForeground(theme, 'warning'),
239
- warningSubtle: tint(theme.colors.warning, theme.colors.background),
240
- info: token('info'),
241
- infoForeground: statusForeground(theme, 'info'),
242
- infoSubtle: tint(theme.colors.info, theme.colors.background),
243
- border: token('border'),
244
- input: token('input'),
245
- ring: token('ring'),
246
- sidebar: token('sidebar'),
247
- sidebarForeground: token('sidebar-foreground'),
248
- sidebarAccent: token('sidebar-accent'),
249
- sidebarBorder: token('sidebar-border'),
250
- sidebarMutedForeground: token('sidebar-muted-foreground'),
251
- }
252
- }
253
-
254
- // The embedded terminal uses the theme's own canvas rather than a palette of its
255
- // own. The ANSI colours came from the palette that produced the canvas, so they
256
- // were chosen against it; a terminal on a different background would be the one
257
- // surface in the application not lit by the active theme.
258
- function terminalOf(theme: AdeaThemeRecord): ThemeTerminalPalette {
259
- return {
260
- background: theme.colors.background,
261
- foreground: theme.colors.foreground,
262
- cursor: theme.cursor,
263
- selection: theme.selection,
264
- ...theme.ansi,
265
- }
266
- }
267
-
268
- function editorOf(theme: AdeaThemeRecord): ThemeEditorPalette {
269
- const syntax = syntaxRoles(theme)
270
- return {
271
- keyword: syntax.keyword,
272
- string: syntax.string,
273
- number: syntax.number,
274
- comment: syntax.comment,
275
- function: syntax.function,
276
- variable: syntax.variable,
277
- type: syntax.type,
278
- tag: syntax.tag,
279
- attribute: syntax.attribute,
280
- operator: syntax.operator,
281
- heading: syntax.heading,
282
- link: syntax.link,
283
- diffAdd: syntax.diffAdd,
284
- diffDelete: syntax.diffDelete,
285
- diffHunk: syntax.diffHunk,
286
- searchMatch: syntax.searchMatch,
287
- }
288
- }
289
-
290
- function provenanceOf(theme: AdeaThemeRecord): ThemeProvenance {
291
- return {
292
- source: theme.provenance.project,
293
- url: theme.provenance.url,
294
- license: theme.provenance.license,
295
- }
296
- }
297
-
298
- /**
299
- * The design system's view of one catalogue record, built in full and at once.
300
- *
301
- * For a host that holds a record the catalogue does not — an imported theme, a
302
- * reconstruction from stored data — and wants a variant it can hand to
303
- * `ThemeProvider`'s `themes`. It throws for a record the shadcn projection cannot
304
- * map, here and now, rather than on first paint. The catalogue itself goes through
305
- * {@link lazyVariant} instead.
306
- */
307
- export function variantFromRecord(theme: AdeaThemeRecord): ThemeVariant {
308
- return {
309
- id: theme.id,
310
- family: theme.family,
311
- familyLabel: theme.familyLabel,
312
- label: theme.label,
313
- appearance: theme.appearance,
314
- description: theme.description,
315
- colors: colorsOf(theme),
316
- terminal: terminalOf(theme),
317
- editor: editorOf(theme),
318
- chart: chartSeries(theme),
319
- provenance: provenanceOf(theme),
320
- }
321
- }
322
-
323
- /** Runs `build` on first call and returns that value from then on. A throw is not cached. */
324
- function once<T>(build: () => T): () => T {
325
- let built = false
326
- let value: T
327
- return () => {
328
- if (!built) {
329
- value = build()
330
- built = true
331
- }
332
- return value
333
- }
334
- }
335
-
336
- const lazy = <T>(build: () => T): PropertyDescriptor => ({
337
- get: once(build),
338
- enumerable: true,
339
- })
340
-
341
- /**
342
- * {@link variantFromRecord} with the four derived palettes built on first read.
343
- *
344
- * Evaluating this module used to project all 34 catalogue themes up front, about
345
- * 155 ms before a single one was needed, on the hydration path of every consumer.
346
- * A page paints one variant, and a picker's labels and appearance are plain
347
- * fields, so only a variant whose `colors`, `terminal`, `editor` or `chart` is read
348
- * pays for them, once. The properties stay enumerable data-shaped accessors, so
349
- * spreading, `Object.entries` and JSON see the same object they always did.
350
- */
351
- function lazyVariant(theme: AdeaThemeRecord): ThemeVariant {
352
- return Object.defineProperties(
353
- {
354
- id: theme.id,
355
- family: theme.family,
356
- familyLabel: theme.familyLabel,
357
- label: theme.label,
358
- appearance: theme.appearance,
359
- description: theme.description,
360
- provenance: provenanceOf(theme),
361
- },
362
- {
363
- colors: lazy(() => colorsOf(theme)),
364
- terminal: lazy(() => terminalOf(theme)),
365
- editor: lazy(() => editorOf(theme)),
366
- chart: lazy(() => chartSeries(theme)),
367
- }
368
- ) as ThemeVariant
369
- }
370
-
371
193
  /**
372
194
  * A variant's `--primary-subtle`: the tint over `var(--primary)` at the strength its
373
195
  * catalogue record measures (`primarySubtleAlpha`). A variant that is not in the
@@ -386,7 +208,7 @@ export function primarySubtleFor(theme: Pick<ThemeVariant, 'id' | 'appearance'>)
386
208
  * (`tokensAsHex`, `isThemeAccentId`, `resolveVariant`…) and never reads it.
387
209
  */
388
210
  export const builtinThemes: readonly ThemeVariant[] = /* @__PURE__ */ Object.freeze(
389
- /* @__PURE__ */ catalogue.map(lazyVariant)
211
+ /* @__PURE__ */ catalogue.map(lazyVariantFromRecord)
390
212
  )
391
213
 
392
214
  export function themeById(id: string): ThemeVariant | undefined {
@@ -415,9 +237,6 @@ export function themeFamilies(): { family: string; label: string; themes: ThemeV
415
237
  return [...families.values()]
416
238
  }
417
239
 
418
- /** The accent presets, in the order a picker should show them. */
419
- export const accents = ACCENTS
420
-
421
240
  /**
422
241
  * The canonical catalogue records, in catalogue order.
423
242
  *