@adea-ai/ui 0.130.4 → 0.131.1
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/README.md +15 -0
- package/dist/components/composites/appearance-editor/accent-swatch-groups.d.ts +7 -0
- package/dist/components/composites/appearance-editor/accent-swatch-groups.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/accent-swatch-groups.js +39 -13
- package/dist/components/composites/appearance-editor/accent-swatch-groups.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-choices.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-choices.js +222 -116
- package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.d.ts.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-editor.js +6 -1
- package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -1
- package/dist/components/composites/appearance-editor/appearance-types.d.ts +22 -3
- package/dist/components/composites/appearance-editor/appearance-types.d.ts.map +1 -1
- package/dist/components/ui/accordion/accordion.d.ts.map +1 -1
- package/dist/components/ui/accordion/accordion.js +1 -1
- package/dist/components/ui/accordion/accordion.js.map +1 -1
- package/dist/components/ui/radio-group/radio-group.d.ts +3 -1
- package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group/radio-group.js +3 -1
- package/dist/components/ui/radio-group/radio-group.js.map +1 -1
- package/dist/lib/accent-presets.d.ts +7 -0
- package/dist/lib/accent-presets.d.ts.map +1 -1
- package/dist/lib/accent-presets.js +2 -2
- package/dist/lib/accent-presets.js.map +1 -1
- package/dist/lib/theme-projection.d.ts +23 -0
- package/dist/lib/theme-projection.d.ts.map +1 -0
- package/dist/lib/theme-projection.js +48 -0
- package/dist/lib/theme-projection.js.map +1 -0
- package/dist/lib/theme-records.d.ts +2 -0
- package/dist/lib/theme-records.d.ts.map +1 -1
- package/dist/lib/theme-records.js.map +1 -1
- package/dist/r/lib.json +5 -0
- package/dist/r/registry.json +5 -0
- package/dist/r/src/components/composites/appearance-editor/accent-swatch-groups.tsx +44 -10
- package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +158 -80
- package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +7 -2
- package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +22 -3
- package/dist/r/src/components/ui/accordion/accordion.tsx +5 -1
- package/dist/r/src/components/ui/radio-group/radio-group.tsx +10 -2
- package/dist/r/src/lib/accent-presets.ts +8 -0
- package/dist/r/src/lib/theme-projection.ts +52 -0
- package/dist/r/src/lib/theme-records.ts +3 -0
- package/package.json +1 -1
- package/registry.json +5 -0
- package/src/components/composites/appearance-editor/accent-swatch-groups.tsx +44 -10
- package/src/components/composites/appearance-editor/appearance-choices.tsx +158 -80
- package/src/components/composites/appearance-editor/appearance-editor.tsx +7 -2
- package/src/components/composites/appearance-editor/appearance-types.ts +22 -3
- package/src/components/ui/accordion/accordion.tsx +5 -1
- package/src/components/ui/radio-group/radio-group.tsx +10 -2
- package/src/lib/accent-presets.ts +8 -0
- package/src/lib/theme-projection.ts +52 -0
- package/src/lib/theme-records.ts +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme-records.js","names":[],"sources":["../../src/lib/theme-records.ts"],"sourcesContent":["/**\n * One catalogue record, as this system's {@link ThemeVariant}.\n *\n * `variantFromRecord` and the lazy form the catalogue is built with live here, and not\n * beside the 34-theme table in `lib/themes.ts`, because a bundler places a module\n * whole: a host that imports `variantFromRecord` from the same module as the table\n * ships the table with it. This module reads a record it is handed and imports no\n * catalogue, so a start-up path that only turns a stored record into a variant\n * (an imported theme, a reconstruction) stays small. `lib/themes.ts` re-exports\n * `variantFromRecord`, so `@adea-ai/ui/lib/themes` keeps working; the lazy start-up\n * gate (`check:theme-startup`) holds the split in place.\n *\n * It does import `@adea-ai/themes` for the projection helpers (`chartSeries`,\n * `tint`, `shadcnVariables`…), none of which reads the table of themes.\n */\nimport {\n chartSeries,\n shadcnDestructiveProjection,\n statusForeground,\n syntaxRoles,\n tint,\n type AdeaThemeRecord,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport type {\n ThemeColors,\n ThemeEditorPalette,\n ThemeProvenance,\n ThemeTerminalPalette,\n ThemeVariant,\n} from './themes'\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 lazyVariantFromRecord} 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 lazyVariantFromRecord} 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 */\nexport function lazyVariantFromRecord(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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme-records.js","names":[],"sources":["../../src/lib/theme-records.ts"],"sourcesContent":["/**\n * One catalogue record, as this system's {@link ThemeVariant}.\n *\n * `variantFromRecord` and the lazy form the catalogue is built with live here, and not\n * beside the 34-theme table in `lib/themes.ts`, because a bundler places a module\n * whole: a host that imports `variantFromRecord` from the same module as the table\n * ships the table with it. This module reads a record it is handed and imports no\n * catalogue, so a start-up path that only turns a stored record into a variant\n * (an imported theme, a reconstruction) stays small. `lib/themes.ts` re-exports\n * `variantFromRecord`, so `@adea-ai/ui/lib/themes` keeps working; the lazy start-up\n * gate (`check:theme-startup`) holds the split in place.\n *\n * It does import `@adea-ai/themes` for the projection helpers (`chartSeries`,\n * `tint`, `shadcnVariables`…), none of which reads the table of themes.\n */\nimport {\n chartSeries,\n shadcnDestructiveProjection,\n statusForeground,\n syntaxRoles,\n tint,\n type AdeaThemeRecord,\n} from '@adea-ai/themes'\nimport { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'\nimport type {\n ThemeColors,\n ThemeEditorPalette,\n ThemeProvenance,\n ThemeTerminalPalette,\n ThemeVariant,\n} from './themes'\n\n/** The catalogue's canonical theme shapes, for a host that holds records and no catalogue. */\nexport type { AdeaTheme, AdeaThemeRecord } from '@adea-ai/themes'\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 lazyVariantFromRecord} 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 lazyVariantFromRecord} 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 */\nexport function lazyVariantFromRecord(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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAS,SAAS,OAAqC;CACrD,MAAM,YAAY,gBAAgB,KAAK;CACvC,MAAM,SAAS,SAAyB;EACtC,MAAM,QAAQ,UAAU,KAAK;EAC7B,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,GAAG,MAAM,GAAG,yCAAyC,MAAM;EACvF,OAAO;CACT;CACA,MAAM,oBAAoB,4BAA4B,KAAK;CAC3D,OAAO;EACL,YAAY,MAAM,YAAY;EAC9B,YAAY,MAAM,YAAY;EAC9B,MAAM,MAAM,MAAM;EAClB,gBAAgB,MAAM,iBAAiB;EACvC,SAAS,MAAM,SAAS;EACxB,mBAAmB,MAAM,oBAAoB;EAC7C,SAAS,MAAM,SAAS;EACxB,mBAAmB,MAAM,oBAAoB;EAC7C,WAAW,MAAM,WAAW;EAC5B,qBAAqB,MAAM,sBAAsB;EACjD,OAAO,MAAM,OAAO;EACpB,iBAAiB,MAAM,kBAAkB;EACzC,QAAQ,MAAM,QAAQ;EACtB,kBAAkB,MAAM,mBAAmB;EAI3C,aAAa,MAAM,OAAO;EAC1B,uBAAuB,iBAAiB,OAAO,OAAO;EACtD,mBAAmB,KAAK,MAAM,OAAO,OAAO,MAAM,OAAO,UAAU;EACnE,mBAAmB,kBAAkB;EACrC,6BAA6B,kBAAkB;EAC/C,SAAS,MAAM,SAAS;EACxB,mBAAmB,iBAAiB,OAAO,SAAS;EACpD,eAAe,KAAK,MAAM,OAAO,SAAS,MAAM,OAAO,UAAU;EACjE,SAAS,MAAM,SAAS;EACxB,mBAAmB,iBAAiB,OAAO,SAAS;EACpD,eAAe,KAAK,MAAM,OAAO,SAAS,MAAM,OAAO,UAAU;EACjE,MAAM,MAAM,MAAM;EAClB,gBAAgB,iBAAiB,OAAO,MAAM;EAC9C,YAAY,KAAK,MAAM,OAAO,MAAM,MAAM,OAAO,UAAU;EAC3D,QAAQ,MAAM,QAAQ;EACtB,OAAO,MAAM,OAAO;EACpB,MAAM,MAAM,MAAM;EAClB,SAAS,MAAM,SAAS;EACxB,mBAAmB,MAAM,oBAAoB;EAC7C,eAAe,MAAM,gBAAgB;EACrC,eAAe,MAAM,gBAAgB;EACrC,wBAAwB,MAAM,0BAA0B;CAC1D;AACF;AAMA,SAAS,WAAW,OAA8C;CAChE,OAAO;EACL,YAAY,MAAM,OAAO;EACzB,YAAY,MAAM,OAAO;EACzB,QAAQ,MAAM;EACd,WAAW,MAAM;EACjB,GAAG,MAAM;CACX;AACF;AAEA,SAAS,SAAS,OAA4C;CAC5D,MAAM,SAAS,YAAY,KAAK;CAChC,OAAO;EACL,SAAS,OAAO;EAChB,QAAQ,OAAO;EACf,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,UAAU,OAAO;EACjB,UAAU,OAAO;EACjB,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,WAAW,OAAO;EAClB,UAAU,OAAO;EACjB,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,SAAS,OAAO;EAChB,YAAY,OAAO;EACnB,UAAU,OAAO;EACjB,aAAa,OAAO;CACtB;AACF;AAEA,SAAS,aAAa,OAAyC;CAC7D,OAAO;EACL,QAAQ,MAAM,WAAW;EACzB,KAAK,MAAM,WAAW;EACtB,SAAS,MAAM,WAAW;CAC5B;AACF;;;;;;;;;;AAWA,SAAgB,kBAAkB,OAAsC;CACtE,OAAO;EACL,IAAI,MAAM;EACV,QAAQ,MAAM;EACd,aAAa,MAAM;EACnB,OAAO,MAAM;EACb,YAAY,MAAM;EAClB,aAAa,MAAM;EACnB,QAAQ,SAAS,KAAK;EACtB,UAAU,WAAW,KAAK;EAC1B,QAAQ,SAAS,KAAK;EACtB,OAAO,YAAY,KAAK;EACxB,YAAY,aAAa,KAAK;CAChC;AACF;;AAGA,SAAS,KAAQ,OAAyB;CACxC,IAAI,QAAQ;CACZ,IAAI;CACJ,aAAa;EACX,IAAI,CAAC,OAAO;GACV,QAAQ,MAAM;GACd,QAAQ;EACV;EACA,OAAO;CACT;AACF;AAEA,IAAM,QAAW,WAAwC;CACvD,KAAK,KAAK,KAAK;CACf,YAAY;AACd;;;;;;;;;;;AAYA,SAAgB,sBAAsB,OAAsC;CAC1E,OAAO,OAAO,iBACZ;EACE,IAAI,MAAM;EACV,QAAQ,MAAM;EACd,aAAa,MAAM;EACnB,OAAO,MAAM;EACb,YAAY,MAAM;EAClB,aAAa,MAAM;EACnB,YAAY,aAAa,KAAK;CAChC,GACA;EACE,QAAQ,WAAW,SAAS,KAAK,CAAC;EAClC,UAAU,WAAW,WAAW,KAAK,CAAC;EACtC,QAAQ,WAAW,SAAS,KAAK,CAAC;EAClC,OAAO,WAAW,YAAY,KAAK,CAAC;CACtC,CACF;AACF"}
|
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-projection.ts",
|
|
115
|
+
"type": "registry:lib",
|
|
116
|
+
"target": "lib/theme-projection.ts"
|
|
117
|
+
},
|
|
113
118
|
{
|
|
114
119
|
"path": "src/lib/theme-records.ts",
|
|
115
120
|
"type": "registry:lib",
|
package/dist/r/registry.json
CHANGED
|
@@ -2794,6 +2794,11 @@
|
|
|
2794
2794
|
"type": "registry:lib",
|
|
2795
2795
|
"target": "lib/theme-engine.ts"
|
|
2796
2796
|
},
|
|
2797
|
+
{
|
|
2798
|
+
"path": "src/lib/theme-projection.ts",
|
|
2799
|
+
"type": "registry:lib",
|
|
2800
|
+
"target": "lib/theme-projection.ts"
|
|
2801
|
+
},
|
|
2797
2802
|
{
|
|
2798
2803
|
"path": "src/lib/theme-records.ts",
|
|
2799
2804
|
"type": "registry:lib",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { RadioGroup as Radio } from '@kobalte/core/radio-group'
|
|
25
25
|
import { For, Show } from 'solid-js'
|
|
26
|
+
import { cn } from '../../../lib/utils'
|
|
26
27
|
|
|
27
28
|
type AccentOption = Readonly<{
|
|
28
29
|
id: string
|
|
@@ -50,6 +51,13 @@ type AccentSwatchGroupsProps = {
|
|
|
50
51
|
* colour reads as a duplicate choice.
|
|
51
52
|
*/
|
|
52
53
|
showThemeDefault?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* `grid` — the default — wraps at four to a line. `row` keeps every swatch on
|
|
56
|
+
* one line at a smaller size, the columns sharing the width equally, so a
|
|
57
|
+
* narrow panel does not wrap them; each colour is then the fill of an SVG
|
|
58
|
+
* circle, which is where a host that asserts on swatch colours reads it.
|
|
59
|
+
*/
|
|
60
|
+
layout?: 'grid' | 'row'
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
@@ -62,11 +70,20 @@ type AccentSwatchGroupsProps = {
|
|
|
62
70
|
*/
|
|
63
71
|
export function AccentSwatchGroups(props: AccentSwatchGroupsProps) {
|
|
64
72
|
const presets = () => props.accentOptions.filter((option) => option.id !== 'theme')
|
|
73
|
+
const row = () => props.layout === 'row'
|
|
65
74
|
|
|
66
75
|
return (
|
|
67
|
-
<div
|
|
76
|
+
<div
|
|
77
|
+
class={row() ? 'grid auto-cols-fr grid-flow-col' : 'grid grid-cols-4 gap-2'}
|
|
78
|
+
data-accent-grid
|
|
79
|
+
>
|
|
68
80
|
<Show when={props.showThemeDefault ?? true}>
|
|
69
|
-
<AccentSwatch
|
|
81
|
+
<AccentSwatch
|
|
82
|
+
id="theme"
|
|
83
|
+
label="Theme default"
|
|
84
|
+
color={props.themeDefaultColor}
|
|
85
|
+
row={row()}
|
|
86
|
+
/>
|
|
70
87
|
</Show>
|
|
71
88
|
<For each={presets()}>
|
|
72
89
|
{(option) => (
|
|
@@ -74,6 +91,7 @@ export function AccentSwatchGroups(props: AccentSwatchGroupsProps) {
|
|
|
74
91
|
id={option.id}
|
|
75
92
|
label={option.label}
|
|
76
93
|
color={(props.resolvedAppearance === 'light' ? option.light : option.dark) ?? ''}
|
|
94
|
+
row={row()}
|
|
77
95
|
/>
|
|
78
96
|
)}
|
|
79
97
|
</For>
|
|
@@ -81,20 +99,36 @@ export function AccentSwatchGroups(props: AccentSwatchGroupsProps) {
|
|
|
81
99
|
)
|
|
82
100
|
}
|
|
83
101
|
|
|
84
|
-
function AccentSwatch(props: { id: string; label: string; color: string }) {
|
|
102
|
+
function AccentSwatch(props: { id: string; label: string; color: string; row: boolean }) {
|
|
85
103
|
return (
|
|
86
104
|
<Radio.Item
|
|
87
105
|
value={props.id}
|
|
88
106
|
data-accent-option={props.id}
|
|
89
|
-
class=
|
|
107
|
+
class={cn('rounded-full focus-within:ring-3 focus-within:ring-ring/50', {
|
|
108
|
+
'flex justify-center': props.row,
|
|
109
|
+
})}
|
|
90
110
|
>
|
|
91
111
|
<Radio.ItemInput />
|
|
92
|
-
<Radio.ItemLabel
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
<Radio.ItemLabel
|
|
113
|
+
class={cn(
|
|
114
|
+
'block cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary',
|
|
115
|
+
props.row ? 'size-6' : 'size-7'
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<Show
|
|
119
|
+
when={props.row}
|
|
120
|
+
fallback={
|
|
121
|
+
<span
|
|
122
|
+
class="block size-full rounded-full"
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
style={{ 'background-color': props.color }}
|
|
125
|
+
/>
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
<svg class="size-full" viewBox="0 0 16 16" aria-hidden="true">
|
|
129
|
+
<circle cx="8" cy="8" r="8" fill={props.color} />
|
|
130
|
+
</svg>
|
|
131
|
+
</Show>
|
|
98
132
|
<span class="sr-only">{props.label}</span>
|
|
99
133
|
</Radio.ItemLabel>
|
|
100
134
|
</Radio.Item>
|
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
// Pinned mode-card, accent-swatch, and glass-chip composition. Kobalte replaces
|
|
25
25
|
// the app port's manual roving focus; it does not replace the donor hierarchy.
|
|
26
26
|
import { RadioGroup as Radio } from '@kobalte/core/radio-group'
|
|
27
|
+
import { Monitor, Moon, Sun } from 'lucide-solid'
|
|
27
28
|
import { For, Show } from 'solid-js'
|
|
28
29
|
import { isThemeAccentId } from '../../../lib/theme-accents'
|
|
30
|
+
import { Button } from '../../ui/button/button'
|
|
31
|
+
import { Input } from '../../ui/input/input'
|
|
29
32
|
import { AccentSwatchGroups } from './accent-swatch-groups'
|
|
30
33
|
import type { AppearanceEditorProps } from './appearance-types'
|
|
31
34
|
import { ThemeMiniature, ThemeMiniatureSplit } from './theme-preview'
|
|
@@ -40,6 +43,8 @@ const SURFACES = [
|
|
|
40
43
|
{ value: 'frosted', label: 'Frosted' },
|
|
41
44
|
{ value: 'opaque', label: 'Opaque' },
|
|
42
45
|
] as const
|
|
46
|
+
const MODE_ICONS = { system: Monitor, light: Sun, dark: Moon } as const
|
|
47
|
+
|
|
43
48
|
export function ModeChoices(props: AppearanceEditorProps) {
|
|
44
49
|
return (
|
|
45
50
|
<Radio
|
|
@@ -54,30 +59,40 @@ export function ModeChoices(props: AppearanceEditorProps) {
|
|
|
54
59
|
class="grid grid-cols-1 gap-3 px-4 py-4 sm:grid-cols-3"
|
|
55
60
|
>
|
|
56
61
|
<For each={MODES}>
|
|
57
|
-
{(option) =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
{(option) => {
|
|
63
|
+
const Icon = MODE_ICONS[option.value]
|
|
64
|
+
return (
|
|
65
|
+
<Radio.Item
|
|
66
|
+
value={option.value}
|
|
67
|
+
data-mode={option.value}
|
|
68
|
+
class="min-w-0 rounded-lg focus-within:ring-3 focus-within:ring-ring/50"
|
|
69
|
+
>
|
|
70
|
+
<Radio.ItemInput />
|
|
71
|
+
<Radio.ItemLabel class="flex cursor-pointer flex-col gap-2 rounded-lg border p-2 text-center text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
|
|
72
|
+
<span class="block h-20 w-full">
|
|
73
|
+
<Show
|
|
74
|
+
when={option.value === 'system'}
|
|
75
|
+
fallback={
|
|
76
|
+
<ThemeMiniature
|
|
77
|
+
theme={option.value === 'light' ? props.lightTheme : props.darkTheme}
|
|
78
|
+
/>
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
<ThemeMiniatureSplit light={props.lightTheme} dark={props.darkTheme} />
|
|
82
|
+
</Show>
|
|
83
|
+
</span>
|
|
84
|
+
{/* The glyph names the axis beside the miniature, so the cards read
|
|
85
|
+
like ThemeModeToggle without losing the previews. */}
|
|
86
|
+
<Show when={props.showModeIcons} fallback={<span>{option.label}</span>}>
|
|
87
|
+
<span class="flex items-center justify-center gap-1.5">
|
|
88
|
+
<Icon aria-hidden="true" class="size-3.5" />
|
|
89
|
+
{option.label}
|
|
90
|
+
</span>
|
|
75
91
|
</Show>
|
|
76
|
-
</
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)}
|
|
92
|
+
</Radio.ItemLabel>
|
|
93
|
+
</Radio.Item>
|
|
94
|
+
)
|
|
95
|
+
}}
|
|
81
96
|
</For>
|
|
82
97
|
</Radio>
|
|
83
98
|
)
|
|
@@ -106,75 +121,138 @@ export function AccentChoices(props: AppearanceEditorProps) {
|
|
|
106
121
|
props.draft.mode === 'system' ? props.resolvedAppearance : props.draft.mode
|
|
107
122
|
const accentSelection = () => (isCustomAccent(props) ? 'custom' : props.draft.accent)
|
|
108
123
|
const showThemeDefault = () => props.showThemeDefaultAccent ?? true
|
|
124
|
+
const entryStyle = () => props.accentEntryStyle ?? 'swatches'
|
|
125
|
+
const themeDefaultColor = () =>
|
|
126
|
+
resolvedAppearance() === 'light'
|
|
127
|
+
? props.lightTheme.colors.accent
|
|
128
|
+
: props.darkTheme.colors.accent
|
|
109
129
|
// The named-chip style feeds the same resolved preset list to both entries:
|
|
110
130
|
// "Theme default" leads only when the host has not folded it into a preset.
|
|
111
131
|
const labelEntries = () => {
|
|
112
132
|
const presets = props.accentOptions.filter((option) => option.id !== 'theme')
|
|
113
133
|
return showThemeDefault() ? [{ id: 'theme', label: 'Theme default' }, ...presets] : presets
|
|
114
134
|
}
|
|
135
|
+
// The `row` style's Custom entry is a button that opens the native colour
|
|
136
|
+
// selector, so the draft only changes once a colour is chosen there. The
|
|
137
|
+
// input stays in the tree for the selector to anchor to and for the value a
|
|
138
|
+
// saved hex restores.
|
|
139
|
+
let colorPicker: HTMLInputElement | undefined
|
|
140
|
+
const openCustomColor = () => {
|
|
141
|
+
if (!colorPicker || props.saving) return
|
|
142
|
+
if (typeof colorPicker.showPicker === 'function') colorPicker.showPicker()
|
|
143
|
+
else colorPicker.click()
|
|
144
|
+
}
|
|
115
145
|
return (
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
<Show
|
|
147
|
+
when={entryStyle() === 'row'}
|
|
148
|
+
fallback={
|
|
149
|
+
<Radio
|
|
150
|
+
value={accentSelection()}
|
|
151
|
+
disabled={props.saving}
|
|
152
|
+
orientation="horizontal"
|
|
153
|
+
aria-label="Accent"
|
|
154
|
+
class="flex flex-col items-start gap-3"
|
|
155
|
+
onChange={(accent) =>
|
|
156
|
+
props.onChange({
|
|
157
|
+
accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
<Show
|
|
162
|
+
when={entryStyle() === 'labels'}
|
|
163
|
+
fallback={
|
|
164
|
+
<>
|
|
165
|
+
<AccentSwatchGroups
|
|
166
|
+
accentOptions={props.accentOptions}
|
|
167
|
+
resolvedAppearance={resolvedAppearance()}
|
|
168
|
+
showThemeDefault={showThemeDefault()}
|
|
169
|
+
themeDefaultColor={themeDefaultColor()}
|
|
170
|
+
/>
|
|
171
|
+
<div class="flex flex-col gap-1">
|
|
172
|
+
<Show when={props.showCustomAccentCaption ?? true}>
|
|
173
|
+
<span class="text-xs font-medium text-muted-foreground">Custom</span>
|
|
174
|
+
</Show>
|
|
175
|
+
<Radio.Item value="custom">
|
|
176
|
+
<Radio.ItemInput />
|
|
177
|
+
<Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
|
|
178
|
+
Custom
|
|
179
|
+
</Radio.ItemLabel>
|
|
180
|
+
</Radio.Item>
|
|
181
|
+
</div>
|
|
182
|
+
</>
|
|
183
|
+
}
|
|
184
|
+
>
|
|
185
|
+
{/* The named-chip style: one grid, every choice a labelled control,
|
|
186
|
+
including the custom colour — nothing to caption from above. */}
|
|
187
|
+
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3" data-accent-grid>
|
|
188
|
+
<For each={labelEntries()}>
|
|
189
|
+
{(option) => (
|
|
190
|
+
<Radio.Item value={option.id} class="min-w-0" data-accent-option={option.id}>
|
|
191
|
+
<Radio.ItemInput />
|
|
192
|
+
<Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
|
|
193
|
+
{option.label}
|
|
194
|
+
</Radio.ItemLabel>
|
|
195
|
+
</Radio.Item>
|
|
196
|
+
)}
|
|
197
|
+
</For>
|
|
198
|
+
<Radio.Item value="custom" class="min-w-0">
|
|
147
199
|
<Radio.ItemInput />
|
|
148
200
|
<Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
|
|
149
201
|
Custom
|
|
150
202
|
</Radio.ItemLabel>
|
|
151
203
|
</Radio.Item>
|
|
152
204
|
</div>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
</Show>
|
|
206
|
+
</Radio>
|
|
207
|
+
}
|
|
208
|
+
>
|
|
209
|
+
<div class="flex w-full min-w-0 items-center gap-2" data-accent-choices>
|
|
210
|
+
<Radio
|
|
211
|
+
value={accentSelection()}
|
|
212
|
+
disabled={props.saving}
|
|
213
|
+
orientation="horizontal"
|
|
214
|
+
aria-label="Accent"
|
|
215
|
+
class="min-w-0 flex-1"
|
|
216
|
+
onChange={(accent) => props.onChange({ accent })}
|
|
217
|
+
>
|
|
218
|
+
<AccentSwatchGroups
|
|
219
|
+
accentOptions={props.accentOptions}
|
|
220
|
+
resolvedAppearance={resolvedAppearance()}
|
|
221
|
+
showThemeDefault={showThemeDefault()}
|
|
222
|
+
themeDefaultColor={themeDefaultColor()}
|
|
223
|
+
layout="row"
|
|
224
|
+
/>
|
|
225
|
+
</Radio>
|
|
226
|
+
<Button
|
|
227
|
+
type="button"
|
|
228
|
+
variant="outline"
|
|
229
|
+
size="sm"
|
|
230
|
+
disabled={props.saving}
|
|
231
|
+
aria-pressed={isCustomAccent(props)}
|
|
232
|
+
onClick={openCustomColor}
|
|
233
|
+
>
|
|
234
|
+
Custom
|
|
235
|
+
</Button>
|
|
236
|
+
<Input
|
|
237
|
+
ref={(element) => {
|
|
238
|
+
colorPicker = element
|
|
239
|
+
}}
|
|
240
|
+
type="color"
|
|
241
|
+
// `w-px` beats the Input's own `w-full`: an absolutely positioned
|
|
242
|
+
// full-width input sized the sheet's scroll width.
|
|
243
|
+
class="sr-only w-px"
|
|
244
|
+
tabIndex={-1}
|
|
245
|
+
aria-label="Custom accent color"
|
|
246
|
+
value={
|
|
247
|
+
/^#[0-9a-f]{6}$/i.test(props.draft.accent)
|
|
248
|
+
? props.draft.accent
|
|
249
|
+
: (props.customAccentValue ?? '#000000')
|
|
250
|
+
}
|
|
251
|
+
disabled={props.saving}
|
|
252
|
+
onChange={(event) => props.onChange({ accent: event.currentTarget.value })}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
</Show>
|
|
178
256
|
)
|
|
179
257
|
}
|
|
180
258
|
|
|
@@ -199,7 +277,7 @@ export function GlassChoices(props: AppearanceEditorProps) {
|
|
|
199
277
|
class="rounded-md focus-within:ring-3 focus-within:ring-ring/50"
|
|
200
278
|
>
|
|
201
279
|
<Radio.ItemInput />
|
|
202
|
-
<Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
|
|
280
|
+
<Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50">
|
|
203
281
|
{option.label}
|
|
204
282
|
</Radio.ItemLabel>
|
|
205
283
|
</Radio.Item>
|
|
@@ -278,12 +278,17 @@ export function AppearancePopover(props: AppearancePopoverProps) {
|
|
|
278
278
|
<Sheet open={props.open} onOpenChange={(open) => (open ? props.onOpen() : props.onDismiss())}>
|
|
279
279
|
{/* The trigger is an icon-only action, so it explains itself through the
|
|
280
280
|
shared ActionButton's tooltip (keyboard focus included) rather than a
|
|
281
|
-
bare aria-label.
|
|
281
|
+
bare aria-label. `toolbar` also holds the pressed rung while Kobalte
|
|
282
|
+
marks the trigger `data-expanded`, and repeats the glyph in the
|
|
283
|
+
tooltip like the other toolbar actions. */}
|
|
282
284
|
<SheetTrigger
|
|
283
285
|
as={ActionButton}
|
|
284
|
-
variant=
|
|
286
|
+
variant={props.triggerVariant === 'toolbar' ? 'toolbar' : 'ghost'}
|
|
285
287
|
size="icon-sm"
|
|
286
288
|
tooltip="Open appearance settings"
|
|
289
|
+
tooltipIcon={
|
|
290
|
+
props.triggerVariant === 'toolbar' ? <Palette aria-hidden="true" /> : undefined
|
|
291
|
+
}
|
|
287
292
|
aria-label="Appearance settings"
|
|
288
293
|
>
|
|
289
294
|
<Palette />
|
|
@@ -67,10 +67,16 @@ export type AppearanceEditorProps = {
|
|
|
67
67
|
fontSettingsPlacement?: 'inline' | 'end'
|
|
68
68
|
/**
|
|
69
69
|
* How the accent row renders its choices. `swatches` — the default — paints
|
|
70
|
-
* each choice as a colour circle
|
|
71
|
-
* chip
|
|
70
|
+
* each choice as a colour circle in a wrapping grid, with Custom as one more
|
|
71
|
+
* chip. `labels` renders every choice as a named chip, for hosts whose accent
|
|
72
|
+
* contract is textual rather than chromatic. `row` draws the circles in ONE
|
|
73
|
+
* equally spaced line, which holds at a narrow panel width where the grid
|
|
74
|
+
* would wrap, and turns Custom into a button that opens the native colour
|
|
75
|
+
* selector: choosing a colour there is the only thing that changes the draft,
|
|
76
|
+
* so dismissing the selector leaves the accent as it was. The caption flag
|
|
77
|
+
* has no effect on `row`, which has nothing to caption.
|
|
72
78
|
*/
|
|
73
|
-
accentEntryStyle?: 'swatches' | 'labels'
|
|
79
|
+
accentEntryStyle?: 'swatches' | 'labels' | 'row'
|
|
74
80
|
/**
|
|
75
81
|
* Whether the accent row leads with a "Theme default" swatch. Default on.
|
|
76
82
|
* Hosts whose default accent already is one of the catalogue presets pass
|
|
@@ -83,10 +89,23 @@ export type AppearanceEditorProps = {
|
|
|
83
89
|
* caption reads as a second "Custom" label on the control.
|
|
84
90
|
*/
|
|
85
91
|
showCustomAccentCaption?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* Whether each mode card names its axis with a sun, moon or monitor glyph
|
|
94
|
+
* beside the label, as `ThemeModeToggle` does. Default off.
|
|
95
|
+
*/
|
|
96
|
+
showModeIcons?: boolean
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
export type AppearancePopoverProps = AppearanceEditorProps & {
|
|
89
100
|
open: boolean
|
|
90
101
|
onOpen: () => void
|
|
91
102
|
onDismiss: () => void
|
|
103
|
+
/**
|
|
104
|
+
* The trigger's treatment. `ghost` — the default — is a quiet icon action.
|
|
105
|
+
* `toolbar` is the variant for a control that opens a surface: it holds the
|
|
106
|
+
* pressed rung while the sheet is open, so the one control that owns the
|
|
107
|
+
* visible sheet never reads like its idle neighbours, and its tooltip repeats
|
|
108
|
+
* the palette glyph the other toolbar actions carry.
|
|
109
|
+
*/
|
|
110
|
+
triggerVariant?: 'ghost' | 'toolbar'
|
|
92
111
|
}
|
|
@@ -37,7 +37,11 @@ export function AccordionTrigger(props: ComponentProps<typeof KobalteAccordion.T
|
|
|
37
37
|
<KobalteAccordion.Header class="flex">
|
|
38
38
|
<KobalteAccordion.Trigger
|
|
39
39
|
class={cn(
|
|
40
|
-
|
|
40
|
+
// `group/accordion` is what the chevron's `group-data-[expanded]/accordion`
|
|
41
|
+
// reads. Kobalte puts `data-expanded` on this trigger, not on the item, so
|
|
42
|
+
// the trigger is the group; without the class the rotation selector has no
|
|
43
|
+
// ancestor to match and the chevron never turns.
|
|
44
|
+
'group/accordion flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-medium',
|
|
41
45
|
'transition-colors ease-out outline-none',
|
|
42
46
|
'hover:text-foreground',
|
|
43
47
|
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
@@ -63,7 +63,9 @@ export type RadioGroupItemProps = ComponentProps<typeof KobalteRadioGroup.Item>
|
|
|
63
63
|
* `card` draws the option as a bordered, selectable card — for a choice
|
|
64
64
|
* whose options need a sentence each (a source type, a sync budget), where a
|
|
65
65
|
* bare radio row reads as a form field rather than a decision. The whole card
|
|
66
|
-
* is the hit target, and the selected card takes the primary edge.
|
|
66
|
+
* is the hit target, and the selected card takes the primary edge. The radio,
|
|
67
|
+
* `media` and text centre on one axis; the default variant keeps the radio on
|
|
68
|
+
* the first line of its label.
|
|
67
69
|
*/
|
|
68
70
|
variant?: 'default' | 'card'
|
|
69
71
|
/** A leading icon or `EntityIcon` between the radio and the text, for `card`. */
|
|
@@ -100,8 +102,14 @@ export function RadioGroupItem(props: RadioGroupItemProps) {
|
|
|
100
102
|
data-variant={card() ? 'card' : undefined}
|
|
101
103
|
title={local.title}
|
|
102
104
|
class={cn(
|
|
103
|
-
'group/radio flex
|
|
105
|
+
'group/radio flex gap-2.5',
|
|
104
106
|
{
|
|
107
|
+
'items-start': !card(),
|
|
108
|
+
// A card centres its radio, media and text on one axis. Top alignment
|
|
109
|
+
// is right for a bare row, where the radio sits on the label's first
|
|
110
|
+
// line, but in a card the media is taller than that line, so a
|
|
111
|
+
// top-aligned radio floats above the icon and the label beside it.
|
|
112
|
+
'items-center': card(),
|
|
105
113
|
'relative rounded-lg border border-border bg-card px-3 py-2.5 transition-[color,box-shadow,border-color,background-color] ease-out hover:bg-surface-hover data-[checked]:border-primary data-[disabled]:cursor-not-allowed data-[disabled]:hover:bg-card':
|
|
106
114
|
card(),
|
|
107
115
|
'has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-3 has-[input:focus-visible]:ring-primary-subtle':
|
|
@@ -8,6 +8,14 @@ import { ACCENTS } from '@adea-ai/themes'
|
|
|
8
8
|
* `accents`, so every existing import keeps working.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A preset by id (`blue`), or `undefined` for `theme`, a theme-carried accent or an
|
|
13
|
+
* unknown id. The catalogue's own lookup, re-exported rather than copied so a host's
|
|
14
|
+
* stored-preference reader resolves an id exactly as the catalogue does without
|
|
15
|
+
* importing `lib/themes`.
|
|
16
|
+
*/
|
|
17
|
+
export { getAccent } from '@adea-ai/themes'
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* The accent axis.
|
|
13
21
|
*
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a host needs to turn a canonical theme record into the values it stores.
|
|
3
|
+
*
|
|
4
|
+
* Adea imports user themes: a file is validated, repaired toward the readable floor,
|
|
5
|
+
* and projected onto the hex values the host persists. That projection is the
|
|
6
|
+
* catalogue's own (`shadcnVariables`, the xterm adapter, the editor roles), so it is
|
|
7
|
+
* re-exported here and the host depends on this package alone for it — the pin
|
|
8
|
+
* between this package and `@adea-ai/themes` cannot drift from the host's.
|
|
9
|
+
*
|
|
10
|
+
* Its own module, with no catalogue data and no definition of its own beyond the two
|
|
11
|
+
* default records, so that:
|
|
12
|
+
*
|
|
13
|
+
* - a host's start-up path never reaches it (it is for an importer loaded on demand);
|
|
14
|
+
* - `lib/themes.ts`, which holds the 34-theme table, is not the place a host has to
|
|
15
|
+
* import from to get a record-shaped helper.
|
|
16
|
+
*/
|
|
17
|
+
import adeaDark from '@adea-ai/themes/themes/adea-dark'
|
|
18
|
+
import adeaLight from '@adea-ai/themes/themes/adea-light'
|
|
19
|
+
import {
|
|
20
|
+
themeAccentPresets,
|
|
21
|
+
type AccentPreset,
|
|
22
|
+
type AdeaTheme,
|
|
23
|
+
type AdeaThemeRecord,
|
|
24
|
+
} from '@adea-ai/themes'
|
|
25
|
+
|
|
26
|
+
export { editorRolesHex, syntaxRolesHex } from '@adea-ai/themes'
|
|
27
|
+
export { oklchToHex, parseColor as parseOklchColor } from '@adea-ai/themes/oklch'
|
|
28
|
+
export { shadcnDestructiveProjection, shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
|
|
29
|
+
export { toXtermTheme } from '@adea-ai/themes/adapters/xterm'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The default theme of an appearance as a canonical record: the roles an imported
|
|
33
|
+
* theme falls back to for anything its file does not carry.
|
|
34
|
+
*
|
|
35
|
+
* `defaultVariantFor` is the same theme as a runtime variant; a record keeps the
|
|
36
|
+
* catalogue's own roles (`surfaceActive`, `textSubtle`, the ANSI ramp) that a variant
|
|
37
|
+
* folds together.
|
|
38
|
+
*/
|
|
39
|
+
export function defaultThemeRecord(appearance: 'light' | 'dark'): AdeaThemeRecord {
|
|
40
|
+
return appearance === 'dark' ? adeaDark : adeaLight
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The accents a light/dark pair of records carries itself, beside the presets.
|
|
45
|
+
*
|
|
46
|
+
* For a host whose pair is not in the catalogue — an imported theme — where
|
|
47
|
+
* `themeAccentsFor` (which looks the pair up by id) cannot answer. A slot is offered
|
|
48
|
+
* only when both themes clear the catalogue's accent floors.
|
|
49
|
+
*/
|
|
50
|
+
export function themeAccentsForRecords(light: AdeaTheme, dark: AdeaTheme): readonly AccentPreset[] {
|
|
51
|
+
return themeAccentPresets(light, dark)
|
|
52
|
+
}
|