@adea-ai/ui 0.123.1 → 0.124.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.
- package/dist/NOTICE +18 -0
- package/dist/components/theme/appearance-panel.js +1 -1
- package/dist/components/theme/index.d.ts +1 -1
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +5 -2
- package/dist/components/theme/theme-context.d.ts +13 -0
- package/dist/components/theme/theme-context.d.ts.map +1 -0
- package/dist/components/theme/theme-context.js +41 -0
- package/dist/components/theme/theme-context.js.map +1 -0
- package/dist/components/theme/theme-document.d.ts +76 -0
- package/dist/components/theme/theme-document.d.ts.map +1 -0
- package/dist/components/theme/theme-document.js +93 -0
- package/dist/components/theme/theme-document.js.map +1 -0
- package/dist/components/theme/theme-persistence.d.ts +50 -0
- package/dist/components/theme/theme-persistence.d.ts.map +1 -0
- package/dist/components/theme/theme-persistence.js +86 -0
- package/dist/components/theme/theme-persistence.js.map +1 -0
- package/dist/components/theme/theme-provider.d.ts +43 -75
- package/dist/components/theme/theme-provider.d.ts.map +1 -1
- package/dist/components/theme/theme-provider.js +50 -116
- package/dist/components/theme/theme-provider.js.map +1 -1
- package/dist/components/theme/theme-state.d.ts +142 -0
- package/dist/components/theme/theme-state.d.ts.map +1 -0
- package/dist/components/theme/theme-state.js +104 -0
- package/dist/components/theme/theme-state.js.map +1 -0
- package/dist/components/theme/theme-toggle.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/lib/appearance-font-settings.d.ts +1 -1
- package/dist/lib/appearance-font-settings.d.ts.map +1 -1
- package/dist/lib/appearance-font-settings.js.map +1 -1
- package/dist/lib/themes.d.ts +48 -0
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +150 -36
- package/dist/lib/themes.js.map +1 -1
- package/dist/r/registry.json +20 -0
- package/dist/r/src/components/theme/index.ts +3 -0
- package/dist/r/src/components/theme/theme-context.ts +43 -0
- package/dist/r/src/components/theme/theme-document.ts +168 -0
- package/dist/r/src/components/theme/theme-persistence.ts +140 -0
- package/dist/r/src/components/theme/theme-provider.tsx +126 -253
- package/dist/r/src/components/theme/theme-state.ts +225 -0
- package/dist/r/src/lib/appearance-font-settings.ts +1 -1
- package/dist/r/src/lib/themes.ts +181 -40
- package/dist/r/theming.json +20 -0
- package/package.json +1 -1
- package/registry.json +20 -0
- package/src/components/theme/index.ts +3 -0
- package/src/components/theme/theme-context.ts +43 -0
- package/src/components/theme/theme-document.ts +168 -0
- package/src/components/theme/theme-persistence.ts +140 -0
- package/src/components/theme/theme-provider.tsx +126 -253
- package/src/components/theme/theme-state.ts +225 -0
- package/src/index.ts +3 -0
- package/src/lib/appearance-font-settings.ts +1 -1
- package/src/lib/themes.ts +181 -40
package/dist/lib/themes.js.map
CHANGED
|
@@ -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 accentRoles,\n chartSeries,\n getAccent,\n getTheme,\n hasTheme,\n shadcnDestructiveProjection,\n statusForeground,\n tint,\n primaryHover,\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 { isThemeAccentId } from './theme-accents'\n\nexport { isThemeAccentId } from './theme-accents'\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\n/** Builds the design system's view of one catalogue entry. */\nfunction toVariant(theme: AdeaThemeRecord): ThemeVariant {\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 const colors: ThemeColors = {\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 const syntax = syntaxRoles(theme)\n\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,\n terminal: {\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.\n background: theme.colors.background,\n foreground: theme.colors.foreground,\n cursor: theme.cursor,\n selection: theme.selection,\n ...theme.ansi,\n },\n editor: {\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 chart: chartSeries(theme),\n provenance: {\n source: theme.provenance.project,\n url: theme.provenance.url,\n license: theme.provenance.license,\n },\n }\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/** The catalogue, in catalogue order. */\nexport const builtinThemes: readonly ThemeVariant[] = Object.freeze(catalogue.map(toVariant))\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): Record<string, string> | undefined {\n if (accentId === 'theme') return undefined\n const { light, dark } = recordPair(pair.lightThemeId, pair.darkThemeId)\n const preset = resolveAccentPreset(accentId, light, dark)\n if (!preset) return undefined\n\n // `--primary-subtle` is deliberately not here. It is a `color-mix()` over\n // `var(--primary)`, so the theme's own declaration (`themeCssVariables`, or the\n // default block in `theme.css`) already follows the accent; and its strength is the\n // *theme's*, measured by `primarySubtleAlpha` to hold body text at 4.5:1 for every\n // accent the theme offers. Writing one here would replace that measured strength\n // with whatever this function was given, which is how an accent used to undo it.\n const roles = accentRoles(preset, appearance)\n return {\n '--primary': roles.primary,\n '--primary-foreground': roles.primaryForeground,\n '--primary-hover': roles.primaryHover,\n '--ring': roles.ring,\n }\n}\n\n/**\n * The roles a variant sets, as CSS custom properties.\n *\n * The one place that knows the mapping from a theme's roles to this system's variable\n * names. A provider applies this; nothing else repeats it. The `--terminal-*` and\n * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather\n * than separate roles, which is why they are built here and not in the catalogue.\n */\nexport function themeCssVariables(theme: ThemeVariant): Record<string, string> {\n const c = theme.colors\n const t = theme.terminal\n const e = theme.editor\n return {\n '--background': c.background,\n '--foreground': c.foreground,\n '--card': c.card,\n '--card-foreground': c.cardForeground,\n '--popover': c.popover,\n '--popover-foreground': c.popoverForeground,\n '--primary': c.primary,\n '--primary-foreground': c.primaryForeground,\n /*\n * The two tokens derived from the primary, and they are here rather than left to\n * `theme.css` because they must follow `--primary` — which this function writes\n * for *any* theme in the catalogue, not just the two defaults `theme.css` carries.\n *\n * `--primary-hover` is a resolved colour, not a `color-mix()`: a uniform hover\n * step cannot be expressed as a mix (see `primaryHover`). Left out, a theme switch\n * would keep the previous theme's hover on the new theme's primary.\n *\n * `--primary-subtle` is a mix over `--primary`, so it follows an accent on its\n * own, but its *strength* is per theme: `primarySubtleCss(record)` lowers the\n * appearance ceiling only as far as this theme needs for body text to clear 4.5:1\n * on the tint, over every surface, for every accent it offers. It has to be\n * written per theme for that reason — the appearance-wide value is not legible on\n * every palette.\n */\n '--primary-hover': primaryHover(c.primary, theme.appearance),\n '--primary-subtle': primarySubtleFor(theme),\n '--secondary': c.secondary,\n '--secondary-foreground': c.secondaryForeground,\n '--muted': c.muted,\n '--muted-foreground': c.mutedForeground,\n '--accent': c.accent,\n '--accent-foreground': c.accentForeground,\n '--destructive': c.destructive,\n '--destructive-foreground': c.destructiveForeground,\n '--destructive-subtle': c.destructiveSubtle,\n '--destructive-action': c.destructiveAction,\n '--destructive-action-foreground': c.destructiveActionForeground,\n '--success': c.success,\n '--success-foreground': c.successForeground,\n '--success-subtle': c.successSubtle,\n '--warning': c.warning,\n '--warning-foreground': c.warningForeground,\n '--warning-subtle': c.warningSubtle,\n '--info': c.info,\n '--info-foreground': c.infoForeground,\n '--info-subtle': c.infoSubtle,\n '--border': c.border,\n '--input': c.input,\n '--ring': c.ring,\n '--sidebar': c.sidebar,\n '--sidebar-foreground': c.sidebarForeground,\n '--sidebar-accent': c.sidebarAccent,\n '--sidebar-border': c.sidebarBorder,\n '--sidebar-primary': c.primary,\n '--sidebar-primary-foreground': c.primaryForeground,\n '--sidebar-ring': c.ring,\n '--sidebar-muted-foreground': c.sidebarMutedForeground,\n '--surface-sunken': c.muted,\n '--surface-hover': c.accent,\n '--surface-active': c.border,\n '--chrome': c.background,\n '--terminal-background': t.background,\n '--terminal-foreground': t.foreground,\n '--terminal-cursor': t.cursor,\n '--terminal-selection': t.selection,\n '--terminal-ansi-black': t.black,\n '--terminal-ansi-red': t.red,\n '--terminal-ansi-green': t.green,\n '--terminal-ansi-yellow': t.yellow,\n '--terminal-ansi-blue': t.blue,\n '--terminal-ansi-magenta': t.magenta,\n '--terminal-ansi-cyan': t.cyan,\n '--terminal-ansi-white': t.white,\n '--terminal-ansi-bright-black': t.brightBlack,\n '--terminal-ansi-bright-red': t.brightRed,\n '--terminal-ansi-bright-green': t.brightGreen,\n '--terminal-ansi-bright-yellow': t.brightYellow,\n '--terminal-ansi-bright-blue': t.brightBlue,\n '--terminal-ansi-bright-magenta': t.brightMagenta,\n '--terminal-ansi-bright-cyan': t.brightCyan,\n '--terminal-ansi-bright-white': t.brightWhite,\n '--editor-keyword': e.keyword,\n '--editor-string': e.string,\n '--editor-number': e.number,\n '--editor-comment': e.comment,\n '--editor-function': e.function,\n '--editor-variable': e.variable,\n '--editor-type': e.type,\n '--editor-tag': e.tag,\n '--editor-attribute': e.attribute,\n '--editor-operator': e.operator,\n '--editor-heading': e.heading,\n '--editor-link': e.link,\n '--editor-diff-add': e.diffAdd,\n '--editor-diff-delete': e.diffDelete,\n '--editor-diff-hunk': e.diffHunk,\n '--editor-search-match': e.searchMatch,\n '--chart-1': theme.chart[0] ?? c.primary,\n '--chart-2': theme.chart[1] ?? c.info,\n '--chart-3': theme.chart[2] ?? c.success,\n '--chart-4': theme.chart[3] ?? c.warning,\n '--chart-5': theme.chart[4] ?? c.destructive,\n '--chart-6': theme.chart[5] ?? c.mutedForeground,\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\nexport const defaultLightThemeId = 'adea-light'\nexport const defaultDarkThemeId = 'adea-dark'\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 ACCENTS,\n accentForeground,\n accentRoles,\n chartSeries,\n getAccent,\n getTheme,\n hasTheme,\n shadcnDestructiveProjection,\n statusForeground,\n tint,\n primaryHover,\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 { oklchToHex, parseColor } from '@adea-ai/themes/oklch'\nimport { isThemeAccentId } from './theme-accents'\n\nexport { isThemeAccentId } from './theme-accents'\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/** The catalogue, in catalogue order. */\nexport const builtinThemes: readonly ThemeVariant[] = Object.freeze(catalogue.map(lazyVariant))\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/**\n * The variant a stored id means, or the default of the same appearance.\n *\n * An id the registry does not hold — an imported theme that was deleted, a\n * catalogue entry that was renamed — must still paint *something of the appearance\n * being shown*. Falling back to whichever variant happens to be first in the\n * registry painted a dark palette under a light `color-scheme` for a light id, so\n * the fallback is the appearance's own default, then any variant of that\n * appearance, then the registry's first entry only when it holds nothing else.\n *\n * `registry` is the builtin catalogue plus whatever extras the host supplies; the\n * stored id is never rewritten, only resolved, so a host that keeps an unknown id\n * keeps resolving it if the theme comes back.\n *\n * The fallback order is translated from Zeron's `ThemeRegistry::resolve` (MIT,\n * Copyright (c) 2026 Wing); see NOTICE.\n */\nexport function resolveVariant(\n registry: readonly ThemeVariant[],\n id: string,\n appearance: ThemeAppearance\n): ThemeVariant {\n const found = registry.find((variant) => variant.id === id)\n if (found) return found\n const defaultId = appearance === 'dark' ? defaultDarkThemeId : defaultLightThemeId\n return (\n registry.find((variant) => variant.id === defaultId) ??\n registry.find((variant) => variant.appearance === appearance) ??\n registry[0]!\n )\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): Record<string, string> | undefined {\n if (accentId === 'theme') return undefined\n const { light, dark } = recordPair(pair.lightThemeId, pair.darkThemeId)\n const preset = resolveAccentPreset(accentId, light, dark)\n if (!preset) return undefined\n\n // `--primary-subtle` is deliberately not here. It is a `color-mix()` over\n // `var(--primary)`, so the theme's own declaration (`themeCssVariables`, or the\n // default block in `theme.css`) already follows the accent; and its strength is the\n // *theme's*, measured by `primarySubtleAlpha` to hold body text at 4.5:1 for every\n // accent the theme offers. Writing one here would replace that measured strength\n // with whatever this function was given, which is how an accent used to undo it.\n const roles = accentRoles(preset, appearance)\n return {\n '--primary': roles.primary,\n '--primary-foreground': roles.primaryForeground,\n '--primary-hover': roles.primaryHover,\n '--ring': roles.ring,\n }\n}\n\n/**\n * The roles a variant sets, as CSS custom properties.\n *\n * The one place that knows the mapping from a theme's roles to this system's variable\n * names. A provider applies this; nothing else repeats it. The `--terminal-*` and\n * `--editor-*` ramps are presentations of the catalogue's ANSI and syntax data rather\n * than separate roles, which is why they are built here and not in the catalogue.\n */\nexport function themeCssVariables(theme: ThemeVariant): Record<string, string> {\n const c = theme.colors\n const t = theme.terminal\n const e = theme.editor\n const derived = {\n '--background': c.background,\n '--foreground': c.foreground,\n '--card': c.card,\n '--card-foreground': c.cardForeground,\n '--popover': c.popover,\n '--popover-foreground': c.popoverForeground,\n '--primary': c.primary,\n '--primary-foreground': c.primaryForeground,\n /*\n * The two tokens derived from the primary, and they are here rather than left to\n * `theme.css` because they must follow `--primary` — which this function writes\n * for *any* theme in the catalogue, not just the two defaults `theme.css` carries.\n *\n * `--primary-hover` is a resolved colour, not a `color-mix()`: a uniform hover\n * step cannot be expressed as a mix (see `primaryHover`). Left out, a theme switch\n * would keep the previous theme's hover on the new theme's primary.\n *\n * `--primary-subtle` is a mix over `--primary`, so it follows an accent on its\n * own, but its *strength* is per theme: `primarySubtleCss(record)` lowers the\n * appearance ceiling only as far as this theme needs for body text to clear 4.5:1\n * on the tint, over every surface, for every accent it offers. It has to be\n * written per theme for that reason — the appearance-wide value is not legible on\n * every palette.\n */\n '--primary-hover': primaryHover(c.primary, theme.appearance),\n '--primary-subtle': primarySubtleFor(theme),\n '--secondary': c.secondary,\n '--secondary-foreground': c.secondaryForeground,\n '--muted': c.muted,\n '--muted-foreground': c.mutedForeground,\n '--accent': c.accent,\n '--accent-foreground': c.accentForeground,\n '--destructive': c.destructive,\n '--destructive-foreground': c.destructiveForeground,\n '--destructive-subtle': c.destructiveSubtle,\n '--destructive-action': c.destructiveAction,\n '--destructive-action-foreground': c.destructiveActionForeground,\n '--success': c.success,\n '--success-foreground': c.successForeground,\n '--success-subtle': c.successSubtle,\n '--warning': c.warning,\n '--warning-foreground': c.warningForeground,\n '--warning-subtle': c.warningSubtle,\n '--info': c.info,\n '--info-foreground': c.infoForeground,\n '--info-subtle': c.infoSubtle,\n '--border': c.border,\n '--input': c.input,\n '--ring': c.ring,\n '--sidebar': c.sidebar,\n '--sidebar-foreground': c.sidebarForeground,\n '--sidebar-accent': c.sidebarAccent,\n '--sidebar-border': c.sidebarBorder,\n '--sidebar-primary': c.primary,\n '--sidebar-primary-foreground': c.primaryForeground,\n '--sidebar-ring': c.ring,\n '--sidebar-muted-foreground': c.sidebarMutedForeground,\n '--surface-sunken': c.muted,\n '--surface-hover': c.accent,\n '--surface-active': c.border,\n '--chrome': c.background,\n '--terminal-background': t.background,\n '--terminal-foreground': t.foreground,\n '--terminal-cursor': t.cursor,\n '--terminal-selection': t.selection,\n '--terminal-ansi-black': t.black,\n '--terminal-ansi-red': t.red,\n '--terminal-ansi-green': t.green,\n '--terminal-ansi-yellow': t.yellow,\n '--terminal-ansi-blue': t.blue,\n '--terminal-ansi-magenta': t.magenta,\n '--terminal-ansi-cyan': t.cyan,\n '--terminal-ansi-white': t.white,\n '--terminal-ansi-bright-black': t.brightBlack,\n '--terminal-ansi-bright-red': t.brightRed,\n '--terminal-ansi-bright-green': t.brightGreen,\n '--terminal-ansi-bright-yellow': t.brightYellow,\n '--terminal-ansi-bright-blue': t.brightBlue,\n '--terminal-ansi-bright-magenta': t.brightMagenta,\n '--terminal-ansi-bright-cyan': t.brightCyan,\n '--terminal-ansi-bright-white': t.brightWhite,\n '--editor-keyword': e.keyword,\n '--editor-string': e.string,\n '--editor-number': e.number,\n '--editor-comment': e.comment,\n '--editor-function': e.function,\n '--editor-variable': e.variable,\n '--editor-type': e.type,\n '--editor-tag': e.tag,\n '--editor-attribute': e.attribute,\n '--editor-operator': e.operator,\n '--editor-heading': e.heading,\n '--editor-link': e.link,\n '--editor-diff-add': e.diffAdd,\n '--editor-diff-delete': e.diffDelete,\n '--editor-diff-hunk': e.diffHunk,\n '--editor-search-match': e.searchMatch,\n '--chart-1': theme.chart[0] ?? c.primary,\n '--chart-2': theme.chart[1] ?? c.info,\n '--chart-3': theme.chart[2] ?? c.success,\n '--chart-4': theme.chart[3] ?? c.warning,\n '--chart-5': theme.chart[4] ?? c.destructive,\n '--chart-6': theme.chart[5] ?? c.mutedForeground,\n }\n return theme.tokens ? { ...derived, ...theme.tokens } : derived\n}\n\n/**\n * Token values with every `oklch()` colour written as `#rrggbb`.\n *\n * For a host whose stylesheet, tests or native shell speak hex: the catalogue's\n * roles are `oklch()` strings, and this is the one conversion, gamut-mapped by the\n * catalogue's own `oklchToHex`. A value that is already hex, carries alpha\n * (`rgba()`), or is not a plain colour (`color-mix()`, `var()`) is left byte for\n * byte, because converting would either lose the alpha or be a guess. Suitable as\n * `ThemeProvider`'s `transformTokens`.\n */\nexport function tokensAsHex(tokens: Readonly<Record<string, string>>): Record<string, string> {\n const converted: Record<string, string> = {}\n for (const [name, value] of Object.entries(tokens)) {\n const trimmed = value.trim()\n // `parseOklch` discards alpha, so a translucent oklch() stays as written.\n const opaque = trimmed.startsWith('oklch(') && !trimmed.includes('/')\n const parsed = opaque ? parseColor(trimmed) : undefined\n converted[name] = parsed ? oklchToHex(parsed) : value\n }\n return converted\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\nexport const defaultLightThemeId = 'adea-light'\nexport const defaultDarkThemeId = 'adea-dark'\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/registry.json
CHANGED
|
@@ -2527,11 +2527,26 @@
|
|
|
2527
2527
|
"type": "registry:ui",
|
|
2528
2528
|
"target": "components/theming/index.ts"
|
|
2529
2529
|
},
|
|
2530
|
+
{
|
|
2531
|
+
"path": "src/components/theme/theme-context.ts",
|
|
2532
|
+
"type": "registry:ui",
|
|
2533
|
+
"target": "components/theming/theme-context.ts"
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
"path": "src/components/theme/theme-document.ts",
|
|
2537
|
+
"type": "registry:ui",
|
|
2538
|
+
"target": "components/theming/theme-document.ts"
|
|
2539
|
+
},
|
|
2530
2540
|
{
|
|
2531
2541
|
"path": "src/components/theme/theme-mode-toggle.tsx",
|
|
2532
2542
|
"type": "registry:ui",
|
|
2533
2543
|
"target": "components/theming/theme-mode-toggle.tsx"
|
|
2534
2544
|
},
|
|
2545
|
+
{
|
|
2546
|
+
"path": "src/components/theme/theme-persistence.ts",
|
|
2547
|
+
"type": "registry:ui",
|
|
2548
|
+
"target": "components/theming/theme-persistence.ts"
|
|
2549
|
+
},
|
|
2535
2550
|
{
|
|
2536
2551
|
"path": "src/components/theme/theme-picker.tsx",
|
|
2537
2552
|
"type": "registry:ui",
|
|
@@ -2542,6 +2557,11 @@
|
|
|
2542
2557
|
"type": "registry:ui",
|
|
2543
2558
|
"target": "components/theming/theme-provider.tsx"
|
|
2544
2559
|
},
|
|
2560
|
+
{
|
|
2561
|
+
"path": "src/components/theme/theme-state.ts",
|
|
2562
|
+
"type": "registry:ui",
|
|
2563
|
+
"target": "components/theming/theme-state.ts"
|
|
2564
|
+
},
|
|
2545
2565
|
{
|
|
2546
2566
|
"path": "src/components/theme/theme-toggle.tsx",
|
|
2547
2567
|
"type": "registry:ui",
|
|
@@ -11,11 +11,14 @@ export {
|
|
|
11
11
|
ThemeProvider,
|
|
12
12
|
defaultThemeSelection,
|
|
13
13
|
inlineScriptLiteral,
|
|
14
|
+
localStoragePersistence,
|
|
14
15
|
resolvedDensity,
|
|
15
16
|
themeScript,
|
|
17
|
+
useOptionalTheme,
|
|
16
18
|
useTheme,
|
|
17
19
|
type ThemeContextValue,
|
|
18
20
|
type ThemeDensity,
|
|
21
|
+
type ThemePersistence,
|
|
19
22
|
type ThemeProviderProps,
|
|
20
23
|
type ThemeSelection,
|
|
21
24
|
} from './theme-provider'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createContext, useContext, type Context } from 'solid-js'
|
|
2
|
+
import type { ThemeContextValue } from './theme-state'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where the context object lives: on the global, under a registered symbol.
|
|
6
|
+
*
|
|
7
|
+
* A context is an object identity, and a bundle can legitimately load this module
|
|
8
|
+
* twice — a dev graph where a relative import and a package subpath are distinct
|
|
9
|
+
* module records, or two copies of the package at different versions. Each copy
|
|
10
|
+
* would then call `createContext` and own its own object; a provider from one and a
|
|
11
|
+
* `useOptionalTheme()` from the other would never meet, and the second would return
|
|
12
|
+
* `undefined` as though no provider were mounted. `Symbol.for` is shared across
|
|
13
|
+
* every copy in a realm, so they all find the one context the first created.
|
|
14
|
+
*
|
|
15
|
+
* Both copies must agree on its shape. The context value only ever grows, which is
|
|
16
|
+
* why a newer provider under an older reader is safe and the reverse is a missing
|
|
17
|
+
* member, not a mismatch.
|
|
18
|
+
*/
|
|
19
|
+
const CONTEXT_KEY = Symbol.for('adea.ui.theme')
|
|
20
|
+
|
|
21
|
+
type ContextHost = { [CONTEXT_KEY]?: Context<ThemeContextValue | undefined> }
|
|
22
|
+
|
|
23
|
+
export function themeContext(): Context<ThemeContextValue | undefined> {
|
|
24
|
+
const host = globalThis as ContextHost
|
|
25
|
+
return (host[CONTEXT_KEY] ??= createContext<ThemeContextValue>())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useTheme(): ThemeContextValue {
|
|
29
|
+
const value = useContext(themeContext())
|
|
30
|
+
if (!value) throw new Error('useTheme must be called inside a <ThemeProvider>.')
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The theme context, or `undefined` outside a `ThemeProvider`.
|
|
36
|
+
*
|
|
37
|
+
* For a component that can run both ways — a shared control rendered in a harness,
|
|
38
|
+
* a story, or a host that has not mounted a provider — and would rather degrade
|
|
39
|
+
* than throw.
|
|
40
|
+
*/
|
|
41
|
+
export function useOptionalTheme(): ThemeContextValue | undefined {
|
|
42
|
+
return useContext(themeContext())
|
|
43
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyAppearanceFontSettings,
|
|
3
|
+
normalizeAppearanceEditorFontSettings,
|
|
4
|
+
} from '../../lib/appearance-font-settings'
|
|
5
|
+
import {
|
|
6
|
+
accentVariables,
|
|
7
|
+
themeCssVariables,
|
|
8
|
+
type ThemeAppearance,
|
|
9
|
+
type ThemeVariant,
|
|
10
|
+
} from '../../lib/themes'
|
|
11
|
+
import { resolvedDensity, type ThemeSelection } from './theme-state'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The document half of the theme provider: the single writer of `<html>`.
|
|
15
|
+
*
|
|
16
|
+
* Written against the few members of an element it uses, so the rules can be tested
|
|
17
|
+
* on a stand-in and the provider's effect stays a one-line call.
|
|
18
|
+
*/
|
|
19
|
+
export type ThemeRoot = Pick<
|
|
20
|
+
HTMLElement,
|
|
21
|
+
'classList' | 'dataset' | 'style' | 'removeAttribute' | 'setAttribute'
|
|
22
|
+
>
|
|
23
|
+
|
|
24
|
+
export type ThemeDocumentState = {
|
|
25
|
+
/** What the document shows: the committed selection, or a preview laid over it. */
|
|
26
|
+
selection: ThemeSelection
|
|
27
|
+
appearance: ThemeAppearance
|
|
28
|
+
variant: ThemeVariant
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ThemeDocumentOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* Rewrites the custom properties just before they are written: the variant's
|
|
34
|
+
* roles with the accent laid over them. It sees every property of the pass and
|
|
35
|
+
* must return the full map; a name it drops is not written.
|
|
36
|
+
*/
|
|
37
|
+
transformTokens?: (tokens: Record<string, string>) => Record<string, string>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The custom properties a state paints: the variant's roles, with the accent
|
|
42
|
+
* selection laid over them, then `transformTokens`.
|
|
43
|
+
*
|
|
44
|
+
* The accent is applied *here* rather than left to the `[data-accent]` blocks in
|
|
45
|
+
* `theme.css`, because the properties are written inline and an inline declaration
|
|
46
|
+
* outranks every stylesheet selector however specific it is. The blocks still exist
|
|
47
|
+
* and are still correct — they are what styles the window before this runs — but
|
|
48
|
+
* they cannot win from here, so leaving the accent to them meant the attribute was
|
|
49
|
+
* set, the rules were present, and nothing changed on screen.
|
|
50
|
+
*/
|
|
51
|
+
export function themeDocumentTokens(
|
|
52
|
+
state: ThemeDocumentState,
|
|
53
|
+
options: ThemeDocumentOptions = {}
|
|
54
|
+
): { tokens: Record<string, string>; accented: boolean } {
|
|
55
|
+
const properties = { ...themeCssVariables(state.variant) }
|
|
56
|
+
const accent = accentVariables(state.selection.accent, state.appearance, state.selection)
|
|
57
|
+
if (accent) Object.assign(properties, accent)
|
|
58
|
+
return {
|
|
59
|
+
tokens: options.transformTokens ? options.transformTokens(properties) : properties,
|
|
60
|
+
accented: accent !== undefined,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write a state onto a root and report which custom properties it owns.
|
|
66
|
+
*
|
|
67
|
+
* Pass the previous call's return as `previous` and any palette property that pass
|
|
68
|
+
* wrote and this one does not is removed. For the catalogue the two sets are the
|
|
69
|
+
* same, so nothing is ever removed; it matters for a host variant that carries
|
|
70
|
+
* `tokens` the catalogue does not, which would otherwise stay inline, beating the
|
|
71
|
+
* stylesheet, after the user switched away from it.
|
|
72
|
+
*/
|
|
73
|
+
export function applyThemeToRoot(
|
|
74
|
+
root: ThemeRoot,
|
|
75
|
+
state: ThemeDocumentState,
|
|
76
|
+
options: ThemeDocumentOptions = {},
|
|
77
|
+
previous?: ReadonlySet<string>
|
|
78
|
+
): ReadonlySet<string> {
|
|
79
|
+
const { tokens, accented } = themeDocumentTokens(state, options)
|
|
80
|
+
|
|
81
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
82
|
+
root.style.setProperty(name, value)
|
|
83
|
+
}
|
|
84
|
+
if (previous) {
|
|
85
|
+
for (const name of previous) {
|
|
86
|
+
if (!(name in tokens)) root.style.removeProperty(name)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
root.classList.toggle('dark', state.appearance === 'dark')
|
|
91
|
+
root.dataset['theme'] = state.variant.id
|
|
92
|
+
root.dataset['appearance'] = state.appearance
|
|
93
|
+
root.style.colorScheme = state.appearance
|
|
94
|
+
|
|
95
|
+
if (!accented) root.removeAttribute('data-accent')
|
|
96
|
+
else root.dataset['accent'] = state.selection.accent
|
|
97
|
+
|
|
98
|
+
applyAppearanceFontSettings(
|
|
99
|
+
root,
|
|
100
|
+
normalizeAppearanceEditorFontSettings(state.selection.fonts, state.selection.font).settings
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Density is a pure token shift, so unlike the colour axes there is nothing to
|
|
104
|
+
// compute: the attribute selects a block in `theme.css`. It is applied here for
|
|
105
|
+
// the same reason as the others — this is the one place that knows what the
|
|
106
|
+
// current selection is, and it is the only writer of these attributes.
|
|
107
|
+
if (resolvedDensity(state.selection) === 'comfortable') root.removeAttribute('data-density')
|
|
108
|
+
else root.dataset['density'] = resolvedDensity(state.selection)
|
|
109
|
+
|
|
110
|
+
return new Set(Object.keys(tokens))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What {@link applyWithoutTransitions} needs of a document. */
|
|
114
|
+
export type TransitionDocument = {
|
|
115
|
+
head: { appendChild(node: unknown): unknown }
|
|
116
|
+
createElement(tag: 'style'): {
|
|
117
|
+
textContent: string | null
|
|
118
|
+
offsetHeight?: number
|
|
119
|
+
remove(): void
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Run `apply` with every CSS transition switched off for the frame that carries
|
|
125
|
+
* it, so a theme switch cannot animate page-wide.
|
|
126
|
+
*
|
|
127
|
+
* Without this, flipping the custom properties on `<html>` starts a transition on
|
|
128
|
+
* every element that declares one, and each fades from the old palette to the new
|
|
129
|
+
* over its own duration: a theme change that looks like a rendering fault. The
|
|
130
|
+
* rule is removed two frames later, after the browser has committed the new styles
|
|
131
|
+
* with transitions off, and `restore` runs it early (the provider calls it when it
|
|
132
|
+
* is disposed mid-switch).
|
|
133
|
+
*
|
|
134
|
+
* `schedule` is the frame source; it defaults to `requestAnimationFrame` and falls
|
|
135
|
+
* back to running at once where there is none, which leaves transitions on rather
|
|
136
|
+
* than off.
|
|
137
|
+
*/
|
|
138
|
+
export function applyWithoutTransitions(
|
|
139
|
+
doc: TransitionDocument,
|
|
140
|
+
apply: () => void,
|
|
141
|
+
schedule: (callback: () => void) => void = defaultSchedule
|
|
142
|
+
): () => void {
|
|
143
|
+
const style = doc.createElement('style')
|
|
144
|
+
style.textContent = '*{transition:none !important}'
|
|
145
|
+
doc.head.appendChild(style)
|
|
146
|
+
// Reading a layout property forces the browser to commit the rule before the
|
|
147
|
+
// properties change; without it the two can be batched into one style recalc.
|
|
148
|
+
void style.offsetHeight
|
|
149
|
+
let removed = false
|
|
150
|
+
const restore = () => {
|
|
151
|
+
if (removed) return
|
|
152
|
+
removed = true
|
|
153
|
+
style.remove()
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
apply()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
restore()
|
|
159
|
+
throw error
|
|
160
|
+
}
|
|
161
|
+
schedule(() => schedule(restore))
|
|
162
|
+
return restore
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function defaultSchedule(callback: () => void): void {
|
|
166
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(callback)
|
|
167
|
+
else callback()
|
|
168
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { accentPresets } from '../../lib/tokens'
|
|
2
|
+
import { normalizeAppearanceEditorFontSettings } from '../../lib/appearance-font-settings'
|
|
3
|
+
import { defaultDarkThemeId, defaultLightThemeId, isThemeAccentId, themeById } from '../../lib/themes'
|
|
4
|
+
import { defaultThemeSelection, resolvedDensity, type ThemeSelection } from './theme-state'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where a selection is kept between sessions.
|
|
8
|
+
*
|
|
9
|
+
* The provider never touches storage itself; it asks a `ThemePersistence`. The
|
|
10
|
+
* default is {@link localStoragePersistence}, which is what the provider always did.
|
|
11
|
+
* A host that stores the preference in a different shape, under a versioned
|
|
12
|
+
* envelope, or somewhere that is not `localStorage` supplies its own and the
|
|
13
|
+
* resolution and application rules stay the shared ones.
|
|
14
|
+
*
|
|
15
|
+
* The contract is deliberately narrow, so that an implementation cannot make the
|
|
16
|
+
* provider less safe than it was:
|
|
17
|
+
*
|
|
18
|
+
* - `read` is synchronous and runs once, when the provider is created. It may
|
|
19
|
+
* repair or quarantine what it finds, and it must not throw: the provider
|
|
20
|
+
* treats a throw as "nothing stored" and starts from the defaults.
|
|
21
|
+
* - `write` runs only from `setSelection`. Never on mount, never from `preview`,
|
|
22
|
+
* never as a side effect of reading, so the first write to a store is always a
|
|
23
|
+
* user's decision. A throw is swallowed, because a full or blocked store must
|
|
24
|
+
* not break the switch.
|
|
25
|
+
*/
|
|
26
|
+
export type ThemePersistence = {
|
|
27
|
+
read(): Partial<ThemeSelection> | undefined
|
|
28
|
+
write(next: ThemeSelection): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The preference store, or nothing.
|
|
33
|
+
*
|
|
34
|
+
* Only ever called inside a `try`: where storage is blocked — a sandboxed iframe, a
|
|
35
|
+
* privacy mode, a denied site-data permission — merely *reading* the `localStorage`
|
|
36
|
+
* global throws ("Access is denied", `SecurityError`), so even the `typeof` guard
|
|
37
|
+
* has to sit inside the `try` rather than in front of it. Without a store the
|
|
38
|
+
* preference lives only in the provider's signal for the session.
|
|
39
|
+
*/
|
|
40
|
+
function preferenceStore(): Storage | undefined {
|
|
41
|
+
return typeof localStorage === 'undefined' ? undefined : localStorage
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The stored preference, validated against the catalogue. */
|
|
45
|
+
function readSelection(storageKey: string): ThemeSelection {
|
|
46
|
+
try {
|
|
47
|
+
const raw = preferenceStore()?.getItem(storageKey)
|
|
48
|
+
if (!raw) return defaultThemeSelection
|
|
49
|
+
const parsed = JSON.parse(raw) as Partial<ThemeSelection>
|
|
50
|
+
const fonts = normalizeAppearanceEditorFontSettings(parsed.fonts, parsed.font).settings
|
|
51
|
+
return {
|
|
52
|
+
appearance:
|
|
53
|
+
parsed.appearance === 'light' ||
|
|
54
|
+
parsed.appearance === 'dark' ||
|
|
55
|
+
parsed.appearance === 'system'
|
|
56
|
+
? parsed.appearance
|
|
57
|
+
: defaultThemeSelection.appearance,
|
|
58
|
+
// An unknown id would leave the document with no theme at all, so a stale
|
|
59
|
+
// preference from an older catalogue falls back rather than applying nothing.
|
|
60
|
+
lightThemeId:
|
|
61
|
+
themeById(parsed.lightThemeId ?? '')?.appearance === 'light'
|
|
62
|
+
? (parsed.lightThemeId as string)
|
|
63
|
+
: defaultLightThemeId,
|
|
64
|
+
darkThemeId:
|
|
65
|
+
themeById(parsed.darkThemeId ?? '')?.appearance === 'dark'
|
|
66
|
+
? (parsed.darkThemeId as string)
|
|
67
|
+
: defaultDarkThemeId,
|
|
68
|
+
// A theme accent is kept whenever it names a slot, even one the stored pair
|
|
69
|
+
// cannot offer: the id is role-shaped so it survives a theme switch, and an
|
|
70
|
+
// unoffered slot already resolves to the theme's own primary when applied.
|
|
71
|
+
accent:
|
|
72
|
+
typeof parsed.accent === 'string' &&
|
|
73
|
+
(accentPresets.some((preset) => preset.id === parsed.accent) ||
|
|
74
|
+
isThemeAccentId(parsed.accent))
|
|
75
|
+
? parsed.accent
|
|
76
|
+
: defaultThemeSelection.accent,
|
|
77
|
+
font: fonts.ui.family,
|
|
78
|
+
fonts,
|
|
79
|
+
// Every axis has to be read back here, or it is lost twice: the provider's
|
|
80
|
+
// document effect removes the `data-density` that `themeScript` set for the first paint, and
|
|
81
|
+
// the next `setSelection` rewrites the store without it.
|
|
82
|
+
density: resolvedDensity(parsed as ThemeSelection),
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return defaultThemeSelection
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The default persistence: one JSON document in `localStorage` under `key`.
|
|
91
|
+
*
|
|
92
|
+
* `read` is the provider's original `readSelection`, moved here unchanged — every
|
|
93
|
+
* stored id is validated against the catalogue, so a stale preference falls back to
|
|
94
|
+
* a default rather than reaching the document. `key` may be a function so a
|
|
95
|
+
* provider whose `storageKey` prop changes writes to the key it is now given.
|
|
96
|
+
*
|
|
97
|
+
* Blocked, full or absent storage is survivable in both directions: a read answers
|
|
98
|
+
* with the defaults and a write is dropped, so the preference simply lasts the
|
|
99
|
+
* session.
|
|
100
|
+
*/
|
|
101
|
+
export function localStoragePersistence(key: string | (() => string)): ThemePersistence {
|
|
102
|
+
const keyOf = () => (typeof key === 'function' ? key() : key)
|
|
103
|
+
return {
|
|
104
|
+
read: () => readSelection(keyOf()),
|
|
105
|
+
write(next) {
|
|
106
|
+
try {
|
|
107
|
+
preferenceStore()?.setItem(keyOf(), JSON.stringify(next))
|
|
108
|
+
} catch {
|
|
109
|
+
// A full, blocked or unavailable store must not break the switch: the
|
|
110
|
+
// preference simply does not survive the reload.
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* What a persistence knows, or nothing.
|
|
118
|
+
*
|
|
119
|
+
* The seam's `read` must not throw, and the provider does not rely on that: a host
|
|
120
|
+
* persistence is code the design system has not seen, and a theme provider that
|
|
121
|
+
* fails to mount because a store misbehaved has turned a lost preference into a
|
|
122
|
+
* blank application.
|
|
123
|
+
*/
|
|
124
|
+
export function readPersisted(persistence: ThemePersistence): Partial<ThemeSelection> | undefined {
|
|
125
|
+
try {
|
|
126
|
+
return persistence.read()
|
|
127
|
+
} catch {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Hand a selection to the store; a store that throws loses the preference, not the switch. */
|
|
133
|
+
export function writePersisted(persistence: ThemePersistence, next: ThemeSelection): void {
|
|
134
|
+
try {
|
|
135
|
+
persistence.write(next)
|
|
136
|
+
} catch {
|
|
137
|
+
// Same contract as a full or blocked `localStorage`: the choice holds for the
|
|
138
|
+
// session and does not survive the reload.
|
|
139
|
+
}
|
|
140
|
+
}
|