@adea-ai/ui 0.142.4 → 0.144.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.
Files changed (98) hide show
  1. package/dist/components/composites/accent-picker/accent-picker.d.ts +28 -0
  2. package/dist/components/composites/accent-picker/accent-picker.d.ts.map +1 -0
  3. package/dist/components/composites/accent-picker/accent-picker.js +107 -0
  4. package/dist/components/composites/accent-picker/accent-picker.js.map +1 -0
  5. package/dist/components/composites/accent-picker/index.d.ts +2 -0
  6. package/dist/components/composites/accent-picker/index.d.ts.map +1 -0
  7. package/dist/components/composites/accent-picker/index.js +2 -0
  8. package/dist/components/composites/icon-picker/icon-picker.d.ts +52 -0
  9. package/dist/components/composites/icon-picker/icon-picker.d.ts.map +1 -0
  10. package/dist/components/composites/icon-picker/icon-picker.js +265 -0
  11. package/dist/components/composites/icon-picker/icon-picker.js.map +1 -0
  12. package/dist/components/composites/icon-picker/image-data-url.d.ts +42 -0
  13. package/dist/components/composites/icon-picker/image-data-url.d.ts.map +1 -0
  14. package/dist/components/composites/icon-picker/image-data-url.js +90 -0
  15. package/dist/components/composites/icon-picker/image-data-url.js.map +1 -0
  16. package/dist/components/composites/icon-picker/index.d.ts +4 -0
  17. package/dist/components/composites/icon-picker/index.d.ts.map +1 -0
  18. package/dist/components/composites/icon-picker/index.js +4 -0
  19. package/dist/components/composites/icon-picker/shell-icons.d.ts +264 -0
  20. package/dist/components/composites/icon-picker/shell-icons.d.ts.map +1 -0
  21. package/dist/components/composites/icon-picker/shell-icons.js +334 -0
  22. package/dist/components/composites/icon-picker/shell-icons.js.map +1 -0
  23. package/dist/components/composites/settings/settings-navigation.js +1 -1
  24. package/dist/components/composites/settings/settings-navigation.js.map +1 -1
  25. package/dist/components/composites/settings/settings.d.ts +8 -14
  26. package/dist/components/composites/settings/settings.d.ts.map +1 -1
  27. package/dist/components/composites/settings/settings.js +88 -37
  28. package/dist/components/composites/settings/settings.js.map +1 -1
  29. package/dist/components/composites/terminal/terminal.js +2 -2
  30. package/dist/components/composites/terminal/terminal.js.map +1 -1
  31. package/dist/components/composites/tree/tree.d.ts +6 -0
  32. package/dist/components/composites/tree/tree.d.ts.map +1 -1
  33. package/dist/components/composites/tree/tree.js +35 -14
  34. package/dist/components/composites/tree/tree.js.map +1 -1
  35. package/dist/components/composites/workspace-switcher/index.d.ts +2 -0
  36. package/dist/components/composites/workspace-switcher/index.d.ts.map +1 -0
  37. package/dist/components/composites/workspace-switcher/index.js +2 -0
  38. package/dist/components/composites/workspace-switcher/workspace-switcher.d.ts +44 -0
  39. package/dist/components/composites/workspace-switcher/workspace-switcher.d.ts.map +1 -0
  40. package/dist/components/composites/workspace-switcher/workspace-switcher.js +157 -0
  41. package/dist/components/composites/workspace-switcher/workspace-switcher.js.map +1 -0
  42. package/dist/components/layout/shell-rail/index.d.ts +2 -0
  43. package/dist/components/layout/shell-rail/index.d.ts.map +1 -0
  44. package/dist/components/layout/shell-rail/index.js +2 -0
  45. package/dist/components/layout/shell-rail/shell-rail.d.ts +36 -0
  46. package/dist/components/layout/shell-rail/shell-rail.d.ts.map +1 -0
  47. package/dist/components/layout/shell-rail/shell-rail.js +124 -0
  48. package/dist/components/layout/shell-rail/shell-rail.js.map +1 -0
  49. package/dist/components/layout/shell-top-bar/index.d.ts +2 -0
  50. package/dist/components/layout/shell-top-bar/index.d.ts.map +1 -0
  51. package/dist/components/layout/shell-top-bar/index.js +2 -0
  52. package/dist/components/layout/shell-top-bar/shell-top-bar.d.ts +68 -0
  53. package/dist/components/layout/shell-top-bar/shell-top-bar.d.ts.map +1 -0
  54. package/dist/components/layout/shell-top-bar/shell-top-bar.js +135 -0
  55. package/dist/components/layout/shell-top-bar/shell-top-bar.js.map +1 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +8 -1
  59. package/dist/r/accent-picker.json +25 -0
  60. package/dist/r/icon-picker.json +37 -0
  61. package/dist/r/registry.json +141 -1
  62. package/dist/r/settings.json +3 -1
  63. package/dist/r/shell-rail.json +27 -0
  64. package/dist/r/shell-top-bar.json +23 -0
  65. package/dist/r/src/components/composites/accent-picker/accent-picker.tsx +106 -0
  66. package/dist/r/src/components/composites/accent-picker/index.ts +1 -0
  67. package/dist/r/src/components/composites/icon-picker/icon-picker.tsx +246 -0
  68. package/dist/r/src/components/composites/icon-picker/image-data-url.ts +107 -0
  69. package/dist/r/src/components/composites/icon-picker/index.ts +25 -0
  70. package/dist/r/src/components/composites/icon-picker/shell-icons.ts +166 -0
  71. package/dist/r/src/components/composites/settings/settings-navigation.tsx +1 -1
  72. package/dist/r/src/components/composites/settings/settings.tsx +40 -5
  73. package/dist/r/src/components/composites/tree/tree.tsx +25 -0
  74. package/dist/r/src/components/composites/workspace-switcher/index.ts +5 -0
  75. package/dist/r/src/components/composites/workspace-switcher/workspace-switcher.tsx +128 -0
  76. package/dist/r/src/components/layout/shell-rail/index.ts +1 -0
  77. package/dist/r/src/components/layout/shell-rail/shell-rail.tsx +120 -0
  78. package/dist/r/src/components/layout/shell-top-bar/index.ts +1 -0
  79. package/dist/r/src/components/layout/shell-top-bar/shell-top-bar.tsx +185 -0
  80. package/dist/r/workspace-switcher.json +26 -0
  81. package/package.json +1 -1
  82. package/registry.json +141 -1
  83. package/src/components/composites/accent-picker/accent-picker.tsx +106 -0
  84. package/src/components/composites/accent-picker/index.ts +1 -0
  85. package/src/components/composites/icon-picker/icon-picker.tsx +246 -0
  86. package/src/components/composites/icon-picker/image-data-url.ts +107 -0
  87. package/src/components/composites/icon-picker/index.ts +25 -0
  88. package/src/components/composites/icon-picker/shell-icons.ts +166 -0
  89. package/src/components/composites/settings/settings-navigation.tsx +1 -1
  90. package/src/components/composites/settings/settings.tsx +40 -5
  91. package/src/components/composites/tree/tree.tsx +25 -0
  92. package/src/components/composites/workspace-switcher/index.ts +5 -0
  93. package/src/components/composites/workspace-switcher/workspace-switcher.tsx +128 -0
  94. package/src/components/layout/shell-rail/index.ts +1 -0
  95. package/src/components/layout/shell-rail/shell-rail.tsx +120 -0
  96. package/src/components/layout/shell-top-bar/index.ts +1 -0
  97. package/src/components/layout/shell-top-bar/shell-top-bar.tsx +185 -0
  98. package/src/index.ts +5 -0
@@ -1918,6 +1918,56 @@
1918
1918
  }
1919
1919
  ]
1920
1920
  },
1921
+ {
1922
+ "name": "shell-rail",
1923
+ "type": "registry:ui",
1924
+ "title": "Shell Rail",
1925
+ "description": "",
1926
+ "dependencies": [
1927
+ "lucide-solid@^1.51.0"
1928
+ ],
1929
+ "registryDependencies": [
1930
+ "@adea-ai/ui/kbd",
1931
+ "@adea-ai/ui/separator",
1932
+ "@adea-ai/ui/side-rail",
1933
+ "lib"
1934
+ ],
1935
+ "files": [
1936
+ {
1937
+ "path": "src/components/layout/shell-rail/index.ts",
1938
+ "type": "registry:ui",
1939
+ "target": "components/layout/shell-rail/index.ts"
1940
+ },
1941
+ {
1942
+ "path": "src/components/layout/shell-rail/shell-rail.tsx",
1943
+ "type": "registry:ui",
1944
+ "target": "components/layout/shell-rail/shell-rail.tsx"
1945
+ }
1946
+ ]
1947
+ },
1948
+ {
1949
+ "name": "shell-top-bar",
1950
+ "type": "registry:ui",
1951
+ "title": "Shell Top Bar",
1952
+ "description": "",
1953
+ "dependencies": [],
1954
+ "registryDependencies": [
1955
+ "@adea-ai/ui/separator",
1956
+ "lib"
1957
+ ],
1958
+ "files": [
1959
+ {
1960
+ "path": "src/components/layout/shell-top-bar/index.ts",
1961
+ "type": "registry:ui",
1962
+ "target": "components/layout/shell-top-bar/index.ts"
1963
+ },
1964
+ {
1965
+ "path": "src/components/layout/shell-top-bar/shell-top-bar.tsx",
1966
+ "type": "registry:ui",
1967
+ "target": "components/layout/shell-top-bar/shell-top-bar.tsx"
1968
+ }
1969
+ ]
1970
+ },
1921
1971
  {
1922
1972
  "name": "side-rail",
1923
1973
  "type": "registry:ui",
@@ -2143,6 +2193,31 @@
2143
2193
  }
2144
2194
  ]
2145
2195
  },
2196
+ {
2197
+ "name": "accent-picker",
2198
+ "type": "registry:ui",
2199
+ "title": "Accent Picker",
2200
+ "description": "",
2201
+ "dependencies": [
2202
+ "@kobalte/core@^0.13.14"
2203
+ ],
2204
+ "registryDependencies": [
2205
+ "@adea-ai/ui/tooltip",
2206
+ "lib"
2207
+ ],
2208
+ "files": [
2209
+ {
2210
+ "path": "src/components/composites/accent-picker/accent-picker.tsx",
2211
+ "type": "registry:ui",
2212
+ "target": "components/composites/accent-picker/accent-picker.tsx"
2213
+ },
2214
+ {
2215
+ "path": "src/components/composites/accent-picker/index.ts",
2216
+ "type": "registry:ui",
2217
+ "target": "components/composites/accent-picker/index.ts"
2218
+ }
2219
+ ]
2220
+ },
2146
2221
  {
2147
2222
  "name": "account-menu",
2148
2223
  "type": "registry:ui",
@@ -2518,6 +2593,43 @@
2518
2593
  }
2519
2594
  ]
2520
2595
  },
2596
+ {
2597
+ "name": "icon-picker",
2598
+ "type": "registry:ui",
2599
+ "title": "Icon Picker",
2600
+ "description": "",
2601
+ "dependencies": [
2602
+ "@kobalte/core@^0.13.14",
2603
+ "lucide-solid@^1.51.0"
2604
+ ],
2605
+ "registryDependencies": [
2606
+ "@adea-ai/ui/button",
2607
+ "@adea-ai/ui/popover",
2608
+ "lib"
2609
+ ],
2610
+ "files": [
2611
+ {
2612
+ "path": "src/components/composites/icon-picker/icon-picker.tsx",
2613
+ "type": "registry:ui",
2614
+ "target": "components/composites/icon-picker/icon-picker.tsx"
2615
+ },
2616
+ {
2617
+ "path": "src/components/composites/icon-picker/image-data-url.ts",
2618
+ "type": "registry:ui",
2619
+ "target": "components/composites/icon-picker/image-data-url.ts"
2620
+ },
2621
+ {
2622
+ "path": "src/components/composites/icon-picker/index.ts",
2623
+ "type": "registry:ui",
2624
+ "target": "components/composites/icon-picker/index.ts"
2625
+ },
2626
+ {
2627
+ "path": "src/components/composites/icon-picker/shell-icons.ts",
2628
+ "type": "registry:ui",
2629
+ "target": "components/composites/icon-picker/shell-icons.ts"
2630
+ }
2631
+ ]
2632
+ },
2521
2633
  {
2522
2634
  "name": "inline-citation",
2523
2635
  "type": "registry:ui",
@@ -2757,11 +2869,13 @@
2757
2869
  "title": "Settings",
2758
2870
  "description": "",
2759
2871
  "dependencies": [
2760
- "@kobalte/core@^0.13.14"
2872
+ "@kobalte/core@^0.13.14",
2873
+ "lucide-solid@^1.51.0"
2761
2874
  ],
2762
2875
  "registryDependencies": [
2763
2876
  "@adea-ai/ui/sidebar-nav",
2764
2877
  "@adea-ai/ui/tabs",
2878
+ "@adea-ai/ui/tooltip",
2765
2879
  "@adea-ai/ui/typography",
2766
2880
  "lib"
2767
2881
  ],
@@ -3144,6 +3258,32 @@
3144
3258
  }
3145
3259
  ]
3146
3260
  },
3261
+ {
3262
+ "name": "workspace-switcher",
3263
+ "type": "registry:ui",
3264
+ "title": "Workspace Switcher",
3265
+ "description": "",
3266
+ "dependencies": [
3267
+ "lucide-solid@^1.51.0"
3268
+ ],
3269
+ "registryDependencies": [
3270
+ "@adea-ai/ui/action-button",
3271
+ "@adea-ai/ui/dropdown-menu",
3272
+ "lib"
3273
+ ],
3274
+ "files": [
3275
+ {
3276
+ "path": "src/components/composites/workspace-switcher/index.ts",
3277
+ "type": "registry:ui",
3278
+ "target": "components/composites/workspace-switcher/index.ts"
3279
+ },
3280
+ {
3281
+ "path": "src/components/composites/workspace-switcher/workspace-switcher.tsx",
3282
+ "type": "registry:ui",
3283
+ "target": "components/composites/workspace-switcher/workspace-switcher.tsx"
3284
+ }
3285
+ ]
3286
+ },
3147
3287
  {
3148
3288
  "name": "conversation",
3149
3289
  "type": "registry:ui",
@@ -4,11 +4,13 @@
4
4
  "title": "Settings",
5
5
  "description": "",
6
6
  "dependencies": [
7
- "@kobalte/core@^0.13.14"
7
+ "@kobalte/core@^0.13.14",
8
+ "lucide-solid@^1.51.0"
8
9
  ],
9
10
  "registryDependencies": [
10
11
  "@adea-ai/ui/sidebar-nav",
11
12
  "@adea-ai/ui/tabs",
13
+ "@adea-ai/ui/tooltip",
12
14
  "@adea-ai/ui/typography",
13
15
  "lib"
14
16
  ],
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "shell-rail",
3
+ "type": "registry:ui",
4
+ "title": "Shell Rail",
5
+ "description": "",
6
+ "dependencies": [
7
+ "lucide-solid@^1.51.0"
8
+ ],
9
+ "registryDependencies": [
10
+ "@adea-ai/ui/kbd",
11
+ "@adea-ai/ui/separator",
12
+ "@adea-ai/ui/side-rail",
13
+ "lib"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "src/components/layout/shell-rail/index.ts",
18
+ "type": "registry:ui",
19
+ "target": "components/layout/shell-rail/index.ts"
20
+ },
21
+ {
22
+ "path": "src/components/layout/shell-rail/shell-rail.tsx",
23
+ "type": "registry:ui",
24
+ "target": "components/layout/shell-rail/shell-rail.tsx"
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "shell-top-bar",
3
+ "type": "registry:ui",
4
+ "title": "Shell Top Bar",
5
+ "description": "",
6
+ "dependencies": [],
7
+ "registryDependencies": [
8
+ "@adea-ai/ui/separator",
9
+ "lib"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "src/components/layout/shell-top-bar/index.ts",
14
+ "type": "registry:ui",
15
+ "target": "components/layout/shell-top-bar/index.ts"
16
+ },
17
+ {
18
+ "path": "src/components/layout/shell-top-bar/shell-top-bar.tsx",
19
+ "type": "registry:ui",
20
+ "target": "components/layout/shell-top-bar/shell-top-bar.tsx"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,106 @@
1
+ import { RadioGroup as Radio } from '@kobalte/core/radio-group'
2
+ import type { JSX } from 'solid-js'
3
+ import { For } from 'solid-js'
4
+ import { accents } from '../../../lib/accent-presets'
5
+ import { cn } from '../../../lib/utils'
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
7
+
8
+ /**
9
+ * AccentPicker.
10
+ *
11
+ * A row of accent swatches with a leading "none" choice, for a setting that
12
+ * overrides the accent it would otherwise inherit: a project's icon colour, a
13
+ * workspace's colour over the app's. `null` is a real value, not an empty one:
14
+ * it means "do not override", so the swatch row never has an unchecked state and
15
+ * the control can say what the inherited colour is through `inheritLabel`.
16
+ *
17
+ * The swatches are the theme's own accent presets, each carrying its light and
18
+ * dark colour and switching on the `dark` class, so a swatch is the colour the
19
+ * setting will produce in whichever appearance is in force and the picker needs
20
+ * no theme provider.
21
+ */
22
+ export type AccentPickerProps = {
23
+ /** The group's accessible name, e.g. "Icon colour". */
24
+ label: string
25
+ /** An accent preset id, or `null` for no override. */
26
+ value: string | null
27
+ onValueChange(value: string | null): void
28
+ /** What `null` means here, e.g. "Inherit workspace accent". Also the first swatch's name and tooltip. */
29
+ inheritLabel?: string
30
+ disabled?: boolean
31
+ class?: string
32
+ }
33
+
34
+ const NONE = '__none__'
35
+
36
+ export function AccentPicker(props: AccentPickerProps) {
37
+ return (
38
+ <Radio
39
+ data-slot="swatch-picker"
40
+ aria-label={props.label}
41
+ value={props.value ?? NONE}
42
+ disabled={props.disabled}
43
+ onChange={(id) => props.onValueChange(id === NONE ? null : id)}
44
+ class={cn('flex flex-wrap items-center gap-2', props.class)}
45
+ >
46
+ <SwatchItem id={NONE} label={props.inheritLabel ?? 'Inherit (default)'}>
47
+ <InheritSwatch />
48
+ </SwatchItem>
49
+ <For each={accents}>
50
+ {(accent) => (
51
+ <SwatchItem id={accent.id} label={accent.label}>
52
+ <span
53
+ aria-hidden="true"
54
+ class="block size-full rounded-full bg-(--swatch-light) dark:bg-(--swatch-dark)"
55
+ style={{
56
+ '--swatch-light': accent.light ?? 'currentColor',
57
+ '--swatch-dark': accent.dark ?? accent.light ?? 'currentColor',
58
+ }}
59
+ />
60
+ </SwatchItem>
61
+ )}
62
+ </For>
63
+ </Radio>
64
+ )
65
+ }
66
+
67
+ /**
68
+ * The "inherit" swatch: a black disc with a red slash, the universal "no
69
+ * override" mark. It is drawn rather than painted from a token so it reads the
70
+ * same in every theme and appearance.
71
+ */
72
+ function InheritSwatch() {
73
+ return (
74
+ <span
75
+ aria-hidden="true"
76
+ data-slot="swatch-inherit"
77
+ class="relative block size-full overflow-hidden rounded-full bg-(--swatch-inherit)"
78
+ style={{ '--swatch-inherit': '#000' }}
79
+ >
80
+ <span class="absolute inset-x-0 top-1/2 h-0.5 -translate-y-1/2 -rotate-45 bg-destructive" />
81
+ </span>
82
+ )
83
+ }
84
+
85
+ function SwatchItem(props: { id: string; label: string; children: JSX.Element }) {
86
+ return (
87
+ <Radio.Item value={props.id} data-accent-option={props.id} class="contents">
88
+ <Radio.ItemInput class="peer" />
89
+ <Tooltip>
90
+ <TooltipTrigger
91
+ as={Radio.ItemLabel}
92
+ class={cn(
93
+ 'flex size-7 cursor-pointer items-center justify-center rounded-full border border-border p-0.5',
94
+ 'peer-focus-visible:border-ring peer-focus-visible:ring-3 peer-focus-visible:ring-primary-subtle',
95
+ 'data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary',
96
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
97
+ )}
98
+ >
99
+ {props.children}
100
+ <span class="sr-only">{props.label}</span>
101
+ </TooltipTrigger>
102
+ <TooltipContent>{props.label}</TooltipContent>
103
+ </Tooltip>
104
+ </Radio.Item>
105
+ )
106
+ }
@@ -0,0 +1 @@
1
+ export { AccentPicker, type AccentPickerProps } from './accent-picker'
@@ -0,0 +1,246 @@
1
+ import { RadioGroup as Radio } from '@kobalte/core/radio-group'
2
+ import { ChevronDown, ImageUp, Trash2 } from 'lucide-solid'
3
+ import type { JSX } from 'solid-js'
4
+ import { For, Show, createSignal, createUniqueId, splitProps } from 'solid-js'
5
+ import { Dynamic } from 'solid-js/web'
6
+ import { cn } from '../../../lib/utils'
7
+ import { Button } from '../../ui/button'
8
+ import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover'
9
+ import {
10
+ MarkImageError,
11
+ markImageFromFile,
12
+ markImageProblemCopy,
13
+ MARK_IMAGE_MAX_SOURCE_BYTES,
14
+ } from './image-data-url'
15
+ import { resolveShellIcon, shellIconEntries, shellIconLabel } from './shell-icons'
16
+
17
+ /**
18
+ * IconPicker.
19
+ *
20
+ * The control behind "choose an icon" for a workspace or a project: a grid of
21
+ * curated glyphs, and optionally an uploaded picture in their place.
22
+ *
23
+ * It is a single-choice group. The glyph grid is a Kobalte radio group, so
24
+ * arrow keys move the choice and Tab enters and leaves the grid as one stop,
25
+ * which is the contract a 56-cell grid needs to stay usable from a keyboard. An
26
+ * uploaded image is a separate state, not a 57th cell: while `image` is set no
27
+ * glyph is checked, and choosing a glyph asks the caller to drop the image, so
28
+ * the stored mark is never both.
29
+ *
30
+ * The picker does not persist anything. The caller owns `value` and `image`
31
+ * and saves them with the rest of its form; the image arrives already resized
32
+ * to the stored square (see `markImageFromFile`).
33
+ */
34
+ export type IconPickerProps = {
35
+ /** The group's accessible name, e.g. "Workspace icon". */
36
+ label: string
37
+ /** The checked icon key; ignored while `image` is set. */
38
+ value: string | undefined
39
+ onValueChange(key: string): void
40
+ /** The key the surface treats as unset, labelled "default" in the grid. */
41
+ defaultKey?: string
42
+ /** An uploaded mark (a data URL) currently chosen in place of a glyph. */
43
+ image?: string
44
+ /**
45
+ * Called with the resized upload, or `undefined` for Remove. Omit it for a
46
+ * surface that offers glyphs only; the upload row then does not render.
47
+ */
48
+ onImageChange?(image: string | undefined): void
49
+ disabled?: boolean
50
+ class?: string
51
+ }
52
+
53
+ export function IconPicker(props: IconPickerProps) {
54
+ const inputId = createUniqueId()
55
+ const [problem, setProblem] = createSignal<string>()
56
+ const [busy, setBusy] = createSignal(false)
57
+ const [fileInput, setFileInput] = createSignal<HTMLInputElement>()
58
+
59
+ const upload = async (file: File | undefined) => {
60
+ if (!file || !props.onImageChange) return
61
+ setProblem(undefined)
62
+ setBusy(true)
63
+ try {
64
+ props.onImageChange(await markImageFromFile(file))
65
+ } catch (error) {
66
+ setProblem(
67
+ markImageProblemCopy[error instanceof MarkImageError ? error.problem : 'unreadable']
68
+ )
69
+ } finally {
70
+ setBusy(false)
71
+ }
72
+ }
73
+
74
+ return (
75
+ <div data-slot="icon-picker" class={cn('flex flex-col gap-3', props.class)}>
76
+ <Show when={props.onImageChange}>
77
+ <div class="flex items-center gap-3" data-slot="icon-picker-upload">
78
+ <span
79
+ aria-hidden="true"
80
+ class="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border bg-muted"
81
+ >
82
+ <Show
83
+ when={props.image}
84
+ fallback={<ImageUp class="size-5 text-muted-foreground" aria-hidden="true" />}
85
+ >
86
+ {(src) => <img src={src()} alt="" class="size-full object-cover" />}
87
+ </Show>
88
+ </span>
89
+ <div class="flex flex-wrap items-center gap-2">
90
+ <input
91
+ ref={setFileInput}
92
+ id={inputId}
93
+ type="file"
94
+ class="sr-only"
95
+ tabIndex={-1}
96
+ // The visible button drives this input; hide the duplicate from assistive
97
+ // technology but keep a name so the form-label rule is satisfied.
98
+ aria-hidden="true"
99
+ aria-label={props.image ? 'Replace image' : 'Upload image'}
100
+ accept="image/png,image/jpeg,image/webp,image/gif"
101
+ disabled={props.disabled}
102
+ onChange={(event) => {
103
+ const input = event.currentTarget
104
+ void upload(input.files?.[0])
105
+ // Clearing the value lets the same file be chosen again after Remove.
106
+ input.value = ''
107
+ }}
108
+ />
109
+ <Button
110
+ type="button"
111
+ variant="outline"
112
+ size="sm"
113
+ disabled={props.disabled || busy()}
114
+ onClick={() => fileInput()?.click()}
115
+ >
116
+ <ImageUp aria-hidden="true" />
117
+ {props.image ? 'Replace image' : 'Upload image'}
118
+ </Button>
119
+ <Show when={props.image}>
120
+ <Button
121
+ type="button"
122
+ variant="ghost"
123
+ size="sm"
124
+ disabled={props.disabled || busy()}
125
+ onClick={() => {
126
+ setProblem(undefined)
127
+ props.onImageChange?.(undefined)
128
+ }}
129
+ >
130
+ <Trash2 aria-hidden="true" />
131
+ Remove image
132
+ </Button>
133
+ </Show>
134
+ </div>
135
+ </div>
136
+ <Show when={problem()}>
137
+ {(message) => (
138
+ <p role="alert" class="text-sm text-destructive">
139
+ {message()}
140
+ </p>
141
+ )}
142
+ </Show>
143
+ <p class="text-xs text-muted-foreground">
144
+ PNG, JPEG, WebP or GIF up to {MARK_IMAGE_MAX_SOURCE_BYTES / 1024 / 1024} MB. It is cropped
145
+ to a square.
146
+ </p>
147
+ </Show>
148
+ <Radio
149
+ aria-label={props.label}
150
+ value={props.image ? '' : (props.value ?? '')}
151
+ disabled={props.disabled}
152
+ onChange={(key) => {
153
+ if (key) props.onValueChange(key)
154
+ }}
155
+ class="grid grid-cols-[repeat(auto-fill,minmax(2.5rem,1fr))] gap-1.5"
156
+ >
157
+ <For each={shellIconEntries}>
158
+ {(entry) => {
159
+ const Icon = resolveShellIcon(entry.key)
160
+ return (
161
+ <Radio.Item value={entry.key} class="contents">
162
+ <Radio.ItemInput class="peer" />
163
+ <Radio.ItemLabel
164
+ title={entry.key === props.defaultKey ? `${entry.label} (default)` : entry.label}
165
+ class={cn(
166
+ 'flex size-10 cursor-pointer items-center justify-center rounded-lg border border-border bg-card text-muted-foreground',
167
+ 'transition-colors ease-out',
168
+ 'hover:bg-surface-hover hover:text-foreground',
169
+ 'peer-focus-visible:border-ring peer-focus-visible:ring-3 peer-focus-visible:ring-primary-subtle',
170
+ 'data-[checked]:border-primary data-[checked]:bg-primary-subtle data-[checked]:text-primary',
171
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
172
+ )}
173
+ >
174
+ <Icon class="size-5" aria-hidden="true" />
175
+ <span class="sr-only">
176
+ {entry.label}
177
+ {entry.key === props.defaultKey ? ' (default)' : ''}
178
+ </span>
179
+ </Radio.ItemLabel>
180
+ </Radio.Item>
181
+ )
182
+ }}
183
+ </For>
184
+ </Radio>
185
+ </div>
186
+ )
187
+ }
188
+
189
+ /**
190
+ * IconPickerDropdown.
191
+ *
192
+ * The compact form of the picker for a settings row: one button showing the
193
+ * current mark, opening the full grid (and the upload row, when the host offers
194
+ * it) in a popover. Choosing a glyph applies it and closes the popover; an
195
+ * upload or Remove leaves it open so the result is visible. The host owns the
196
+ * value exactly as it does for `IconPicker`.
197
+ */
198
+ export type IconPickerDropdownProps = IconPickerProps & {
199
+ /** The mark shown on the trigger; defaults to the checked glyph or the uploaded image. */
200
+ trigger?: JSX.Element
201
+ }
202
+
203
+ export function IconPickerDropdown(props: IconPickerDropdownProps) {
204
+ const [open, setOpen] = createSignal(false)
205
+ const [local, picker] = splitProps(props, ['trigger', 'class'])
206
+ const Current = () => resolveShellIcon(props.value ?? props.defaultKey ?? '')
207
+ return (
208
+ <Popover open={open()} onOpenChange={setOpen} placement="bottom-start">
209
+ <PopoverTrigger
210
+ as={Button}
211
+ type="button"
212
+ variant="outline"
213
+ data-slot="icon-picker-trigger"
214
+ aria-label={`${props.label}: ${props.image ? 'uploaded image' : shellIconLabel(props.value ?? props.defaultKey ?? '')}`}
215
+ disabled={props.disabled}
216
+ class={cn('gap-2', local.class)}
217
+ >
218
+ <span aria-hidden="true" class="flex size-5 items-center justify-center overflow-hidden">
219
+ <Show
220
+ when={local.trigger}
221
+ fallback={
222
+ <Show
223
+ when={props.image}
224
+ fallback={<Dynamic component={Current()} class="size-4" aria-hidden="true" />}
225
+ >
226
+ {(src) => <img src={src()} alt="" class="size-full rounded-sm object-cover" />}
227
+ </Show>
228
+ }
229
+ >
230
+ {local.trigger}
231
+ </Show>
232
+ </span>
233
+ <ChevronDown class="size-4 text-muted-foreground" aria-hidden="true" />
234
+ </PopoverTrigger>
235
+ <PopoverContent class="w-[min(22rem,var(--kb-popper-available-width))]">
236
+ <IconPicker
237
+ {...picker}
238
+ onValueChange={(key) => {
239
+ picker.onValueChange(key)
240
+ setOpen(false)
241
+ }}
242
+ />
243
+ </PopoverContent>
244
+ </Popover>
245
+ )
246
+ }