@adea-ai/ui 0.62.0 → 0.63.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.
Files changed (49) hide show
  1. package/README.md +50 -0
  2. package/dist/NOTICE +51 -0
  3. package/dist/components/composites/appearance-editor/appearance-choices.d.ts +5 -0
  4. package/dist/components/composites/appearance-editor/appearance-choices.d.ts.map +1 -0
  5. package/dist/components/composites/appearance-editor/appearance-choices.js +211 -0
  6. package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -0
  7. package/dist/components/composites/appearance-editor/appearance-editor.d.ts +15 -0
  8. package/dist/components/composites/appearance-editor/appearance-editor.d.ts.map +1 -0
  9. package/dist/components/composites/appearance-editor/appearance-editor.js +280 -0
  10. package/dist/components/composites/appearance-editor/appearance-editor.js.map +1 -0
  11. package/dist/components/composites/appearance-editor/appearance-rows.d.ts +18 -0
  12. package/dist/components/composites/appearance-editor/appearance-rows.d.ts.map +1 -0
  13. package/dist/components/composites/appearance-editor/appearance-rows.js +101 -0
  14. package/dist/components/composites/appearance-editor/appearance-rows.js.map +1 -0
  15. package/dist/components/composites/appearance-editor/appearance-types.d.ts +42 -0
  16. package/dist/components/composites/appearance-editor/appearance-types.d.ts.map +1 -0
  17. package/dist/components/composites/appearance-editor/index.d.ts +4 -0
  18. package/dist/components/composites/appearance-editor/index.d.ts.map +1 -0
  19. package/dist/components/composites/appearance-editor/index.js +3 -0
  20. package/dist/components/composites/appearance-editor/theme-preview.d.ts +17 -0
  21. package/dist/components/composites/appearance-editor/theme-preview.d.ts.map +1 -0
  22. package/dist/components/composites/appearance-editor/theme-preview.js +158 -0
  23. package/dist/components/composites/appearance-editor/theme-preview.js.map +1 -0
  24. package/dist/components/ui/select/select.d.ts +5 -1
  25. package/dist/components/ui/select/select.d.ts.map +1 -1
  26. package/dist/components/ui/select/select.js +24 -20
  27. package/dist/components/ui/select/select.js.map +1 -1
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -1
  31. package/dist/r/appearance-editor.json +51 -0
  32. package/dist/r/registry.json +51 -0
  33. package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +173 -0
  34. package/dist/r/src/components/composites/appearance-editor/appearance-editor.tsx +239 -0
  35. package/dist/r/src/components/composites/appearance-editor/appearance-rows.tsx +122 -0
  36. package/dist/r/src/components/composites/appearance-editor/appearance-types.ts +40 -0
  37. package/dist/r/src/components/composites/appearance-editor/index.ts +12 -0
  38. package/dist/r/src/components/composites/appearance-editor/theme-preview.tsx +128 -0
  39. package/dist/r/src/components/ui/select/select.tsx +8 -3
  40. package/package.json +3 -2
  41. package/registry.json +51 -0
  42. package/src/components/composites/appearance-editor/appearance-choices.tsx +173 -0
  43. package/src/components/composites/appearance-editor/appearance-editor.tsx +239 -0
  44. package/src/components/composites/appearance-editor/appearance-rows.tsx +122 -0
  45. package/src/components/composites/appearance-editor/appearance-types.ts +40 -0
  46. package/src/components/composites/appearance-editor/index.ts +12 -0
  47. package/src/components/composites/appearance-editor/theme-preview.tsx +128 -0
  48. package/src/components/ui/select/select.tsx +8 -3
  49. package/src/index.ts +1 -0
@@ -0,0 +1,122 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2026 Wing
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ // Retains the compact divided row and palette-preview selector from the pinned
25
+ // Zeron/Adea composition; Kobalte owns option navigation and nested dismissal.
26
+ import type { AdeaTheme, AdeaThemeRecord } from '@adea-ai/themes'
27
+ import { Show, createMemo, createSignal, type JSX } from 'solid-js'
28
+ import { Palette } from 'lucide-solid'
29
+ import { cn } from '../../../lib/utils'
30
+ import {
31
+ Select,
32
+ SelectContent,
33
+ SelectItem,
34
+ SelectTrigger,
35
+ SelectValue,
36
+ } from '../../ui/select/select'
37
+ import { PalettePreview } from './theme-preview'
38
+
39
+ export function SettingsRow(props: {
40
+ title: string
41
+ icon: JSX.Element
42
+ description?: JSX.Element
43
+ stacked?: boolean
44
+ children: JSX.Element
45
+ }) {
46
+ return (
47
+ <section
48
+ class={cn('flex flex-col gap-3 px-4 py-4', { 'sm:flex-row sm:items-center': !props.stacked })}
49
+ >
50
+ <div class="flex min-w-0 flex-1 items-center gap-3">
51
+ <span
52
+ class="flex size-9 shrink-0 items-center justify-center rounded-lg border bg-foreground/5 text-muted-foreground [&_svg]:size-4"
53
+ aria-hidden="true"
54
+ >
55
+ {props.icon}
56
+ </span>
57
+ <div class="min-w-0">
58
+ <h3 class="text-sm font-medium">{props.title}</h3>
59
+ <Show when={props.description}>
60
+ <div class="mt-0.5 text-xs text-muted-foreground">{props.description}</div>
61
+ </Show>
62
+ </div>
63
+ </div>
64
+ <div class="min-w-0 shrink-0">{props.children}</div>
65
+ </section>
66
+ )
67
+ }
68
+
69
+ export function ThemeRow(props: {
70
+ appearance: 'light' | 'dark'
71
+ selectedId: string
72
+ preview: AdeaTheme
73
+ themes: readonly AdeaThemeRecord[]
74
+ disabled?: boolean
75
+ onSelect: (id: string) => void
76
+ }) {
77
+ const [portalMount, setPortalMount] = createSignal<HTMLDivElement>()
78
+ const options = createMemo(() =>
79
+ props.themes.filter((theme) => theme.appearance === props.appearance)
80
+ )
81
+ const selected = createMemo(
82
+ () =>
83
+ options().find((theme) => theme.id === props.selectedId) ??
84
+ options().find((theme) => theme.id === props.preview.id)
85
+ )
86
+ const label = () => (props.appearance === 'light' ? 'Light theme' : 'Dark theme')
87
+ return (
88
+ <SettingsRow title={label()} icon={<Palette />}>
89
+ <div ref={setPortalMount}>
90
+ <Select
91
+ modal={false}
92
+ options={options()}
93
+ value={selected()}
94
+ optionValue="id"
95
+ optionTextValue="name"
96
+ disabled={props.disabled || options().length === 0}
97
+ onChange={(theme) => {
98
+ if (theme) props.onSelect(theme.id)
99
+ }}
100
+ itemComponent={(item) => (
101
+ <SelectItem item={item.item}>
102
+ <PalettePreview theme={item.item.rawValue} />
103
+ {item.item.rawValue.name}
104
+ </SelectItem>
105
+ )}
106
+ >
107
+ <SelectTrigger size="sm" aria-label={label()} class="w-full sm:w-52">
108
+ <SelectValue>
109
+ {() => (
110
+ <>
111
+ <PalettePreview theme={props.preview} />
112
+ <span>{selected()?.name ?? props.preview.name}</span>
113
+ </>
114
+ )}
115
+ </SelectValue>
116
+ </SelectTrigger>
117
+ <SelectContent portalMount={portalMount()} />
118
+ </Select>
119
+ </div>
120
+ </SettingsRow>
121
+ )
122
+ }
@@ -0,0 +1,40 @@
1
+ import type { AccentPreset, AdeaTheme, AdeaThemeRecord } from '@adea-ai/themes'
2
+
3
+ export type AppearanceDraft = Readonly<{
4
+ mode: 'system' | 'light' | 'dark'
5
+ lightThemeId: string
6
+ darkThemeId: string
7
+ /** Theme default, a canonical preset id, or the host's custom accent draft. */
8
+ accent: string
9
+ surface: 'theme' | 'frosted' | 'opaque'
10
+ reduceTransparency: boolean
11
+ }>
12
+
13
+ export type AppearanceEditorProps = {
14
+ draft: AppearanceDraft
15
+ /** Resolved preview themes, including the host's validated accent overlay. */
16
+ lightTheme: AdeaTheme
17
+ darkTheme: AdeaTheme
18
+ resolvedAppearance: 'light' | 'dark'
19
+ themes: readonly AdeaThemeRecord[]
20
+ accentOptions: readonly AccentPreset[]
21
+ onChange: (patch: Partial<AppearanceDraft>) => void
22
+ onSave: () => void
23
+ onCancel: () => void
24
+ onReset: () => void
25
+ customAccentError?: string
26
+ customAccentValue?: string
27
+ saveDisabledReason?: string
28
+ saving?: boolean
29
+ recoveryNotice?: string
30
+ surfaceCapability: { frosted: boolean; reason?: string; themeDefaultDescription?: string }
31
+ /** Undefined means theme import is unavailable, never a working-looking action. */
32
+ onManageThemes?: () => void
33
+ class?: string
34
+ }
35
+
36
+ export type AppearancePopoverProps = AppearanceEditorProps & {
37
+ open: boolean
38
+ onOpen: () => void
39
+ onDismiss: () => void
40
+ }
@@ -0,0 +1,12 @@
1
+ export { AppearanceEditor, AppearancePopover } from './appearance-editor'
2
+ export type {
3
+ AppearanceDraft,
4
+ AppearanceEditorProps,
5
+ AppearancePopoverProps,
6
+ } from './appearance-types'
7
+ export {
8
+ ThemeMiniature,
9
+ ThemeMiniatureSplit,
10
+ PalettePreview,
11
+ type ThemeMiniatureProps,
12
+ } from './theme-preview'
@@ -0,0 +1,128 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2026 Wing
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ /*
25
+ * Substantially translated from Zeron crates/ui/src/settings/appearance.rs
26
+ * `bar`, `miniature`, `miniature_split`, and `palette_preview`, revision
27
+ * 30a9a9537c5ec96226c87f4bf349b6f77c5dfb59, via Adea's existing
28
+ * packages/ui/src/components/theme-preview.tsx adaptation.
29
+ * Retains the sidebar/pane decomposition, exact bar fractions and opacity,
30
+ * split outer corners, and three-band palette strip. GPUI and Adea's local
31
+ * ThemeVariant are replaced by Solid and the canonical AdeaTheme schema.
32
+ * Dynamic inline colors are theme data; no palette is authored here.
33
+ */
34
+ import type { AdeaTheme } from '@adea-ai/themes'
35
+ import { For } from 'solid-js'
36
+ import { cn } from '../../../lib/utils'
37
+
38
+ export type ThemeMiniatureProps = {
39
+ theme: AdeaTheme
40
+ corners?: 'all' | 'left' | 'right'
41
+ class?: string
42
+ }
43
+
44
+ function Bar(props: { fraction: number; opacity: number; color: string }) {
45
+ return (
46
+ <span
47
+ class="h-[5px] rounded-[3px]"
48
+ style={{
49
+ width: `${Math.round(props.fraction * 100)}%`,
50
+ 'background-color': props.color,
51
+ opacity: props.opacity,
52
+ }}
53
+ />
54
+ )
55
+ }
56
+
57
+ export function ThemeMiniature(props: ThemeMiniatureProps) {
58
+ return (
59
+ <span
60
+ data-theme-miniature
61
+ aria-hidden="true"
62
+ class={cn(
63
+ 'flex size-full overflow-hidden',
64
+ {
65
+ 'rounded-md': !props.corners || props.corners === 'all',
66
+ 'rounded-l-md': props.corners === 'left',
67
+ 'rounded-r-md': props.corners === 'right',
68
+ },
69
+ props.class
70
+ )}
71
+ style={{ 'background-color': props.theme.colors.surface }}
72
+ >
73
+ <span class="flex h-full w-11 shrink-0 flex-col gap-[7px] overflow-hidden px-2 pt-3.5">
74
+ <Bar fraction={0.7} opacity={0.34} color={props.theme.colors.foreground} />
75
+ <Bar fraction={1} opacity={0.22} color={props.theme.colors.foreground} />
76
+ <Bar fraction={0.85} opacity={0.22} color={props.theme.colors.foreground} />
77
+ <Bar fraction={1} opacity={0.22} color={props.theme.colors.foreground} />
78
+ </span>
79
+ <span
80
+ class="my-2 mr-2 flex min-w-0 flex-1 flex-col gap-[7px] overflow-hidden rounded-md border border-(--appearance-preview-border) p-2.5"
81
+ style={{
82
+ 'background-color': props.theme.colors.background,
83
+ '--appearance-preview-border': props.theme.colors.border,
84
+ }}
85
+ >
86
+ <Bar fraction={0.62} opacity={0.34} color={props.theme.colors.foreground} />
87
+ <Bar fraction={0.88} opacity={0.22} color={props.theme.colors.foreground} />
88
+ <Bar fraction={0.76} opacity={0.22} color={props.theme.colors.foreground} />
89
+ <Bar fraction={0.52} opacity={0.22} color={props.theme.colors.foreground} />
90
+ </span>
91
+ </span>
92
+ )
93
+ }
94
+
95
+ export function ThemeMiniatureSplit(props: { light: AdeaTheme; dark: AdeaTheme; class?: string }) {
96
+ return (
97
+ <span class={cn('flex size-full', props.class)} aria-hidden="true">
98
+ <span class="h-full w-1/2 overflow-hidden">
99
+ <ThemeMiniature theme={props.light} corners="left" />
100
+ </span>
101
+ <span class="h-full w-1/2 overflow-hidden">
102
+ <ThemeMiniature theme={props.dark} corners="right" />
103
+ </span>
104
+ </span>
105
+ )
106
+ }
107
+
108
+ export function PalettePreview(props: { theme: AdeaTheme; class?: string }) {
109
+ const bands = () => [
110
+ props.theme.colors.surface,
111
+ props.theme.colors.background,
112
+ props.theme.colors.accent,
113
+ ]
114
+ return (
115
+ <span
116
+ class={cn(
117
+ 'flex h-[18px] w-[30px] shrink-0 overflow-hidden rounded-[5px] border border-(--appearance-preview-border)',
118
+ props.class
119
+ )}
120
+ aria-hidden="true"
121
+ style={{ '--appearance-preview-border': props.theme.colors.border }}
122
+ >
123
+ <For each={bands()}>
124
+ {(band) => <span class="h-full w-1/3" style={{ 'background-color': band }} />}
125
+ </For>
126
+ </span>
127
+ )
128
+ }
@@ -129,11 +129,16 @@ export function SelectValue(props: SelectValueProps) {
129
129
  * which is what makes a Select able to render a typed option rather than a
130
130
  * string; `children` here is for content above the list, such as a filter hint.
131
131
  */
132
- export function SelectContent(props: ComponentProps<typeof KobalteSelect.Content>) {
133
- const [local, rest] = splitProps(props, ['class', 'children'])
132
+ export type SelectContentProps = ComponentProps<typeof KobalteSelect.Content> & {
133
+ /** Keep a nested menu inside its enclosing modal's accessible subtree. */
134
+ portalMount?: HTMLElement
135
+ }
136
+
137
+ export function SelectContent(props: SelectContentProps) {
138
+ const [local, rest] = splitProps(props, ['class', 'children', 'portalMount'])
134
139
 
135
140
  return (
136
- <KobalteSelect.Portal>
141
+ <KobalteSelect.Portal mount={local.portalMount}>
137
142
  <KobalteSelect.Content
138
143
  class={cn(
139
144
  'bg-popover text-popover-foreground rounded-xl border border-border shadow-lg',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -78,7 +78,8 @@
78
78
  "check:tree-shaking": "bun scripts/check-tree-shaking.ts",
79
79
  "theme:build": "bun scripts/build-default-theme.ts",
80
80
  "theme:check": "bun scripts/build-default-theme.ts --check",
81
- "check:packed-consumer": "bun scripts/check-packed-consumer.ts"
81
+ "check:packed-consumer": "bun scripts/check-packed-consumer.ts",
82
+ "check:packed-appearance": "bun scripts/check-packed-appearance.ts"
82
83
  },
83
84
  "dependencies": {
84
85
  "@adea-ai/themes": "^0.5.0",
package/registry.json CHANGED
@@ -1599,6 +1599,57 @@
1599
1599
  }
1600
1600
  ]
1601
1601
  },
1602
+ {
1603
+ "name": "appearance-editor",
1604
+ "type": "registry:ui",
1605
+ "title": "Appearance Editor",
1606
+ "description": "",
1607
+ "dependencies": [
1608
+ "@adea-ai/themes@^0.5.0",
1609
+ "@kobalte/core@^0.13.14",
1610
+ "lucide-solid@^1.48.0"
1611
+ ],
1612
+ "registryDependencies": [
1613
+ "@adea-ai/ui/button",
1614
+ "@adea-ai/ui/input",
1615
+ "@adea-ai/ui/popover",
1616
+ "@adea-ai/ui/select",
1617
+ "@adea-ai/ui/switch",
1618
+ "lib"
1619
+ ],
1620
+ "files": [
1621
+ {
1622
+ "path": "src/components/composites/appearance-editor/appearance-choices.tsx",
1623
+ "type": "registry:ui",
1624
+ "target": "components/composites/appearance-editor/appearance-choices.tsx"
1625
+ },
1626
+ {
1627
+ "path": "src/components/composites/appearance-editor/appearance-editor.tsx",
1628
+ "type": "registry:ui",
1629
+ "target": "components/composites/appearance-editor/appearance-editor.tsx"
1630
+ },
1631
+ {
1632
+ "path": "src/components/composites/appearance-editor/appearance-rows.tsx",
1633
+ "type": "registry:ui",
1634
+ "target": "components/composites/appearance-editor/appearance-rows.tsx"
1635
+ },
1636
+ {
1637
+ "path": "src/components/composites/appearance-editor/appearance-types.ts",
1638
+ "type": "registry:ui",
1639
+ "target": "components/composites/appearance-editor/appearance-types.ts"
1640
+ },
1641
+ {
1642
+ "path": "src/components/composites/appearance-editor/index.ts",
1643
+ "type": "registry:ui",
1644
+ "target": "components/composites/appearance-editor/index.ts"
1645
+ },
1646
+ {
1647
+ "path": "src/components/composites/appearance-editor/theme-preview.tsx",
1648
+ "type": "registry:ui",
1649
+ "target": "components/composites/appearance-editor/theme-preview.tsx"
1650
+ }
1651
+ ]
1652
+ },
1602
1653
  {
1603
1654
  "name": "list-row",
1604
1655
  "type": "registry:ui",
@@ -0,0 +1,173 @@
1
+ /*
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2026 Wing
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+ // Pinned mode-card, accent-swatch, and glass-chip composition. Kobalte replaces
25
+ // the app port's manual roving focus; it does not replace the donor hierarchy.
26
+ import { RadioGroup as Radio } from '@kobalte/core/radio-group'
27
+ import { For, Show } from 'solid-js'
28
+ import type { AppearanceEditorProps } from './appearance-types'
29
+ import { ThemeMiniature, ThemeMiniatureSplit } from './theme-preview'
30
+
31
+ const MODES = [
32
+ { value: 'system', label: 'System' },
33
+ { value: 'light', label: 'Light' },
34
+ { value: 'dark', label: 'Dark' },
35
+ ] as const
36
+ const SURFACES = [
37
+ { value: 'theme', label: 'Theme default' },
38
+ { value: 'frosted', label: 'Frosted' },
39
+ { value: 'opaque', label: 'Opaque' },
40
+ ] as const
41
+
42
+ export function ModeChoices(props: AppearanceEditorProps) {
43
+ return (
44
+ <Radio
45
+ value={props.draft.mode}
46
+ disabled={props.saving}
47
+ orientation="horizontal"
48
+ aria-label="Appearance mode"
49
+ onChange={(mode) => {
50
+ const option = MODES.find((candidate) => candidate.value === mode)
51
+ if (option) props.onChange({ mode: option.value })
52
+ }}
53
+ class="grid grid-cols-1 gap-3 px-4 py-4 sm:grid-cols-3"
54
+ >
55
+ <For each={MODES}>
56
+ {(option) => (
57
+ <Radio.Item
58
+ value={option.value}
59
+ data-mode={option.value}
60
+ class="min-w-0 rounded-lg focus-within:ring-3 focus-within:ring-ring/50"
61
+ >
62
+ <Radio.ItemInput />
63
+ <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">
64
+ <span class="block h-20 w-full">
65
+ <Show
66
+ when={option.value === 'system'}
67
+ fallback={
68
+ <ThemeMiniature
69
+ theme={option.value === 'light' ? props.lightTheme : props.darkTheme}
70
+ />
71
+ }
72
+ >
73
+ <ThemeMiniatureSplit light={props.lightTheme} dark={props.darkTheme} />
74
+ </Show>
75
+ </span>
76
+ <span>{option.label}</span>
77
+ </Radio.ItemLabel>
78
+ </Radio.Item>
79
+ )}
80
+ </For>
81
+ </Radio>
82
+ )
83
+ }
84
+
85
+ export function AccentChoices(props: AppearanceEditorProps) {
86
+ const custom = () =>
87
+ props.draft.accent !== 'theme' &&
88
+ !props.accentOptions.some((option) => option.id === props.draft.accent)
89
+ const accentSelection = () => (custom() ? 'custom' : props.draft.accent)
90
+ return (
91
+ <Radio
92
+ value={accentSelection()}
93
+ disabled={props.saving}
94
+ orientation="horizontal"
95
+ aria-label="Accent"
96
+ class="flex flex-wrap items-center gap-2"
97
+ onChange={(accent) =>
98
+ props.onChange({
99
+ accent: accent === 'custom' ? (props.customAccentValue ?? '') : accent,
100
+ })
101
+ }
102
+ >
103
+ <Radio.Item value="theme">
104
+ <Radio.ItemInput />
105
+ <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">
106
+ Theme default
107
+ </Radio.ItemLabel>
108
+ </Radio.Item>
109
+ <For each={props.accentOptions}>
110
+ {(option) => (
111
+ <Radio.Item
112
+ value={option.id}
113
+ class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
114
+ >
115
+ <Radio.ItemInput />
116
+ <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
117
+ <span
118
+ class="block size-full rounded-full"
119
+ aria-hidden="true"
120
+ style={{
121
+ 'background-color':
122
+ (props.draft.mode === 'system'
123
+ ? props.resolvedAppearance
124
+ : props.draft.mode) === 'light'
125
+ ? option.light
126
+ : option.dark,
127
+ }}
128
+ />
129
+ <span class="sr-only">{option.label}</span>
130
+ </Radio.ItemLabel>
131
+ </Radio.Item>
132
+ )}
133
+ </For>
134
+ <Radio.Item value="custom">
135
+ <Radio.ItemInput />
136
+ <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">
137
+ Custom
138
+ </Radio.ItemLabel>
139
+ </Radio.Item>
140
+ </Radio>
141
+ )
142
+ }
143
+
144
+ export function GlassChoices(props: AppearanceEditorProps) {
145
+ return (
146
+ <Radio
147
+ value={props.draft.surface}
148
+ disabled={props.saving}
149
+ orientation="horizontal"
150
+ aria-label="Glass"
151
+ class="flex flex-wrap gap-1"
152
+ onChange={(surface) => {
153
+ const option = SURFACES.find((candidate) => candidate.value === surface)
154
+ if (option) props.onChange({ surface: option.value })
155
+ }}
156
+ >
157
+ <For each={SURFACES}>
158
+ {(option) => (
159
+ <Radio.Item
160
+ value={option.value}
161
+ disabled={option.value === 'frosted' && !props.surfaceCapability.frosted}
162
+ class="rounded-md focus-within:ring-3 focus-within:ring-ring/50"
163
+ >
164
+ <Radio.ItemInput />
165
+ <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">
166
+ {option.label}
167
+ </Radio.ItemLabel>
168
+ </Radio.Item>
169
+ )}
170
+ </For>
171
+ </Radio>
172
+ )
173
+ }