@adea-ai/ui 0.84.0 → 0.87.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 (257) hide show
  1. package/README.md +90 -1
  2. package/dist/NOTICE +60 -0
  3. package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
  4. package/dist/components/composites/action-button/action-button.js +2 -2
  5. package/dist/components/composites/action-button/action-button.js.map +1 -1
  6. package/dist/components/composites/catalog-browser/catalog-browser-group.d.ts +20 -0
  7. package/dist/components/composites/catalog-browser/catalog-browser-group.d.ts.map +1 -0
  8. package/dist/components/composites/catalog-browser/catalog-browser-group.js +195 -0
  9. package/dist/components/composites/catalog-browser/catalog-browser-group.js.map +1 -0
  10. package/dist/components/composites/catalog-browser/catalog-browser.d.ts +3 -0
  11. package/dist/components/composites/catalog-browser/catalog-browser.d.ts.map +1 -0
  12. package/dist/components/composites/catalog-browser/catalog-browser.js +378 -0
  13. package/dist/components/composites/catalog-browser/catalog-browser.js.map +1 -0
  14. package/dist/components/composites/catalog-browser/catalog-detail.d.ts +21 -0
  15. package/dist/components/composites/catalog-browser/catalog-detail.d.ts.map +1 -0
  16. package/dist/components/composites/catalog-browser/catalog-detail.js +92 -0
  17. package/dist/components/composites/catalog-browser/catalog-detail.js.map +1 -0
  18. package/dist/components/composites/catalog-browser/catalog-types.d.ts +78 -0
  19. package/dist/components/composites/catalog-browser/catalog-types.d.ts.map +1 -0
  20. package/dist/components/composites/catalog-browser/index.d.ts +4 -0
  21. package/dist/components/composites/catalog-browser/index.d.ts.map +1 -0
  22. package/dist/components/composites/catalog-browser/index.js +3 -0
  23. package/dist/components/composites/list-row/list-row.d.ts +6 -3
  24. package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
  25. package/dist/components/composites/list-row/list-row.js +69 -7
  26. package/dist/components/composites/list-row/list-row.js.map +1 -1
  27. package/dist/components/composites/settings/settings.d.ts +9 -11
  28. package/dist/components/composites/settings/settings.d.ts.map +1 -1
  29. package/dist/components/composites/settings/settings.js +48 -33
  30. package/dist/components/composites/settings/settings.js.map +1 -1
  31. package/dist/components/conversation/message-composer.js +1 -1
  32. package/dist/components/conversation/scroll-follow.d.ts.map +1 -1
  33. package/dist/components/conversation/scroll-follow.js +95 -15
  34. package/dist/components/conversation/scroll-follow.js.map +1 -1
  35. package/dist/components/layout/app-shell/index.d.ts +1 -0
  36. package/dist/components/layout/app-shell/index.d.ts.map +1 -1
  37. package/dist/components/layout/app-shell/index.js +2 -1
  38. package/dist/components/layout/app-shell/skip-link.d.ts +11 -0
  39. package/dist/components/layout/app-shell/skip-link.d.ts.map +1 -0
  40. package/dist/components/layout/app-shell/skip-link.js +44 -0
  41. package/dist/components/layout/app-shell/skip-link.js.map +1 -0
  42. package/dist/components/layout/floating-preview/floating-preview.d.ts +12 -0
  43. package/dist/components/layout/floating-preview/floating-preview.d.ts.map +1 -0
  44. package/dist/components/layout/floating-preview/floating-preview.js +283 -0
  45. package/dist/components/layout/floating-preview/floating-preview.js.map +1 -0
  46. package/dist/components/layout/floating-preview/index.d.ts +3 -0
  47. package/dist/components/layout/floating-preview/index.d.ts.map +1 -0
  48. package/dist/components/layout/floating-preview/index.js +3 -0
  49. package/dist/components/layout/floating-preview/model.d.ts +74 -0
  50. package/dist/components/layout/floating-preview/model.d.ts.map +1 -0
  51. package/dist/components/layout/floating-preview/model.js +146 -0
  52. package/dist/components/layout/floating-preview/model.js.map +1 -0
  53. package/dist/components/theme/index.d.ts +1 -0
  54. package/dist/components/theme/index.d.ts.map +1 -1
  55. package/dist/components/theme/index.js +2 -1
  56. package/dist/components/theme/theme-mode-toggle.d.ts +10 -0
  57. package/dist/components/theme/theme-mode-toggle.d.ts.map +1 -0
  58. package/dist/components/theme/theme-mode-toggle.js +82 -0
  59. package/dist/components/theme/theme-mode-toggle.js.map +1 -0
  60. package/dist/components/ui/checkbox/checkbox.d.ts +2 -0
  61. package/dist/components/ui/checkbox/checkbox.d.ts.map +1 -1
  62. package/dist/components/ui/checkbox/checkbox.js +24 -3
  63. package/dist/components/ui/checkbox/checkbox.js.map +1 -1
  64. package/dist/components/ui/combobox/combobox.d.ts.map +1 -1
  65. package/dist/components/ui/combobox/combobox.js +41 -4
  66. package/dist/components/ui/combobox/combobox.js.map +1 -1
  67. package/dist/components/ui/combobox/index.d.ts +1 -0
  68. package/dist/components/ui/combobox/index.d.ts.map +1 -1
  69. package/dist/components/ui/combobox/index.js +2 -1
  70. package/dist/components/ui/combobox/value-combobox.d.ts +19 -0
  71. package/dist/components/ui/combobox/value-combobox.d.ts.map +1 -0
  72. package/dist/components/ui/combobox/value-combobox.js +42 -0
  73. package/dist/components/ui/combobox/value-combobox.js.map +1 -0
  74. package/dist/components/ui/empty/empty-state.d.ts +15 -0
  75. package/dist/components/ui/empty/empty-state.d.ts.map +1 -0
  76. package/dist/components/ui/empty/empty-state.js +96 -0
  77. package/dist/components/ui/empty/empty-state.js.map +1 -0
  78. package/dist/components/ui/empty/index.d.ts +1 -0
  79. package/dist/components/ui/empty/index.d.ts.map +1 -1
  80. package/dist/components/ui/empty/index.js +2 -1
  81. package/dist/components/ui/entity-icon/entity-icon.d.ts +4 -1
  82. package/dist/components/ui/entity-icon/entity-icon.d.ts.map +1 -1
  83. package/dist/components/ui/entity-icon/entity-icon.js +37 -13
  84. package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
  85. package/dist/components/ui/field/field.d.ts +36 -13
  86. package/dist/components/ui/field/field.d.ts.map +1 -1
  87. package/dist/components/ui/field/field.js +146 -24
  88. package/dist/components/ui/field/field.js.map +1 -1
  89. package/dist/components/ui/field/index.d.ts +3 -1
  90. package/dist/components/ui/field/index.d.ts.map +1 -1
  91. package/dist/components/ui/field/index.js +3 -2
  92. package/dist/components/ui/field/number-field.d.ts +19 -0
  93. package/dist/components/ui/field/number-field.d.ts.map +1 -0
  94. package/dist/components/ui/field/number-field.js +76 -0
  95. package/dist/components/ui/field/number-field.js.map +1 -0
  96. package/dist/components/ui/input/index.d.ts +1 -0
  97. package/dist/components/ui/input/index.d.ts.map +1 -1
  98. package/dist/components/ui/input/index.js +2 -1
  99. package/dist/components/ui/input/input-control.d.ts +11 -0
  100. package/dist/components/ui/input/input-control.d.ts.map +1 -0
  101. package/dist/components/ui/input/input-control.js +24 -0
  102. package/dist/components/ui/input/input-control.js.map +1 -0
  103. package/dist/components/ui/input/input.d.ts.map +1 -1
  104. package/dist/components/ui/input/input.js +56 -28
  105. package/dist/components/ui/input/input.js.map +1 -1
  106. package/dist/components/ui/input-group/index.d.ts +1 -0
  107. package/dist/components/ui/input-group/index.d.ts.map +1 -1
  108. package/dist/components/ui/input-group/index.js +2 -1
  109. package/dist/components/ui/input-group/input-group.d.ts.map +1 -1
  110. package/dist/components/ui/input-group/input-group.js +42 -5
  111. package/dist/components/ui/input-group/input-group.js.map +1 -1
  112. package/dist/components/ui/input-group/secret-input-group.d.ts +11 -0
  113. package/dist/components/ui/input-group/secret-input-group.d.ts.map +1 -0
  114. package/dist/components/ui/input-group/secret-input-group.js +67 -0
  115. package/dist/components/ui/input-group/secret-input-group.js.map +1 -0
  116. package/dist/components/ui/native-select/native-select.d.ts.map +1 -1
  117. package/dist/components/ui/native-select/native-select.js +39 -4
  118. package/dist/components/ui/native-select/native-select.js.map +1 -1
  119. package/dist/components/ui/progress/progress.d.ts.map +1 -1
  120. package/dist/components/ui/progress/progress.js +30 -18
  121. package/dist/components/ui/progress/progress.js.map +1 -1
  122. package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
  123. package/dist/components/ui/radio-group/radio-group.js +41 -4
  124. package/dist/components/ui/radio-group/radio-group.js.map +1 -1
  125. package/dist/components/ui/switch/switch.d.ts +2 -0
  126. package/dist/components/ui/switch/switch.d.ts.map +1 -1
  127. package/dist/components/ui/switch/switch.js +24 -3
  128. package/dist/components/ui/switch/switch.js.map +1 -1
  129. package/dist/components/ui/tabs/tabs.js +1 -1
  130. package/dist/components/ui/tabs/tabs.js.map +1 -1
  131. package/dist/components/ui/text-link/index.d.ts +2 -0
  132. package/dist/components/ui/text-link/index.d.ts.map +1 -0
  133. package/dist/components/ui/text-link/index.js +2 -0
  134. package/dist/components/ui/text-link/text-link.d.ts +9 -0
  135. package/dist/components/ui/text-link/text-link.d.ts.map +1 -0
  136. package/dist/components/ui/text-link/text-link.js +30 -0
  137. package/dist/components/ui/text-link/text-link.js.map +1 -0
  138. package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
  139. package/dist/components/ui/textarea/textarea.js +41 -4
  140. package/dist/components/ui/textarea/textarea.js.map +1 -1
  141. package/dist/index.d.ts +4 -0
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +21 -8
  144. package/dist/lib/clipboard.d.ts +3 -0
  145. package/dist/lib/clipboard.d.ts.map +1 -0
  146. package/dist/lib/clipboard.js +35 -0
  147. package/dist/lib/clipboard.js.map +1 -0
  148. package/dist/lib/download.d.ts +3 -0
  149. package/dist/lib/download.d.ts.map +1 -0
  150. package/dist/lib/download.js +31 -0
  151. package/dist/lib/download.js.map +1 -0
  152. package/dist/lib/form-field.d.ts +37 -0
  153. package/dist/lib/form-field.d.ts.map +1 -0
  154. package/dist/lib/form-field.js +47 -0
  155. package/dist/lib/form-field.js.map +1 -0
  156. package/dist/r/app-shell.json +5 -0
  157. package/dist/r/catalog-browser.json +47 -0
  158. package/dist/r/combobox.json +5 -0
  159. package/dist/r/empty.json +10 -1
  160. package/dist/r/field.json +7 -0
  161. package/dist/r/floating-preview.json +30 -0
  162. package/dist/r/input-group.json +6 -0
  163. package/dist/r/input.json +5 -0
  164. package/dist/r/lib.json +15 -0
  165. package/dist/r/list-row.json +1 -0
  166. package/dist/r/registry.json +159 -1
  167. package/dist/r/src/components/composites/action-button/action-button.tsx +5 -1
  168. package/dist/r/src/components/composites/catalog-browser/catalog-browser-group.tsx +148 -0
  169. package/dist/r/src/components/composites/catalog-browser/catalog-browser.tsx +258 -0
  170. package/dist/r/src/components/composites/catalog-browser/catalog-detail.tsx +64 -0
  171. package/dist/r/src/components/composites/catalog-browser/catalog-types.ts +83 -0
  172. package/dist/r/src/components/composites/catalog-browser/index.ts +10 -0
  173. package/dist/r/src/components/composites/list-row/list-row.tsx +81 -8
  174. package/dist/r/src/components/composites/settings/settings.tsx +67 -37
  175. package/dist/r/src/components/conversation/scroll-follow.ts +109 -15
  176. package/dist/r/src/components/layout/app-shell/index.ts +1 -0
  177. package/dist/r/src/components/layout/app-shell/skip-link.tsx +35 -0
  178. package/dist/r/src/components/layout/floating-preview/floating-preview.tsx +260 -0
  179. package/dist/r/src/components/layout/floating-preview/index.ts +2 -0
  180. package/dist/r/src/components/layout/floating-preview/model.ts +285 -0
  181. package/dist/r/src/components/theme/index.ts +1 -0
  182. package/dist/r/src/components/theme/theme-mode-toggle.tsx +56 -0
  183. package/dist/r/src/components/ui/checkbox/checkbox.tsx +20 -3
  184. package/dist/r/src/components/ui/combobox/combobox.tsx +24 -1
  185. package/dist/r/src/components/ui/combobox/index.ts +1 -0
  186. package/dist/r/src/components/ui/combobox/value-combobox.tsx +38 -0
  187. package/dist/r/src/components/ui/empty/empty-state.tsx +71 -0
  188. package/dist/r/src/components/ui/empty/index.ts +1 -0
  189. package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +33 -2
  190. package/dist/r/src/components/ui/field/field.tsx +102 -5
  191. package/dist/r/src/components/ui/field/index.ts +5 -0
  192. package/dist/r/src/components/ui/field/number-field.tsx +71 -0
  193. package/dist/r/src/components/ui/input/index.ts +1 -0
  194. package/dist/r/src/components/ui/input/input-control.tsx +38 -0
  195. package/dist/r/src/components/ui/input/input.tsx +32 -20
  196. package/dist/r/src/components/ui/input-group/index.ts +1 -0
  197. package/dist/r/src/components/ui/input-group/input-group.tsx +25 -2
  198. package/dist/r/src/components/ui/input-group/secret-input-group.tsx +58 -0
  199. package/dist/r/src/components/ui/native-select/native-select.tsx +27 -1
  200. package/dist/r/src/components/ui/progress/progress.tsx +12 -8
  201. package/dist/r/src/components/ui/radio-group/radio-group.tsx +30 -2
  202. package/dist/r/src/components/ui/switch/switch.tsx +20 -3
  203. package/dist/r/src/components/ui/tabs/tabs.tsx +1 -1
  204. package/dist/r/src/components/ui/text-link/index.ts +2 -0
  205. package/dist/r/src/components/ui/text-link/text-link.tsx +28 -0
  206. package/dist/r/src/components/ui/textarea/textarea.tsx +24 -1
  207. package/dist/r/src/lib/clipboard.ts +41 -0
  208. package/dist/r/src/lib/download.ts +29 -0
  209. package/dist/r/src/lib/form-field.ts +93 -0
  210. package/dist/r/text-link.json +22 -0
  211. package/dist/r/theming.json +6 -0
  212. package/package.json +1 -1
  213. package/registry.json +159 -1
  214. package/src/components/composites/action-button/action-button.tsx +5 -1
  215. package/src/components/composites/catalog-browser/catalog-browser-group.tsx +148 -0
  216. package/src/components/composites/catalog-browser/catalog-browser.tsx +258 -0
  217. package/src/components/composites/catalog-browser/catalog-detail.tsx +64 -0
  218. package/src/components/composites/catalog-browser/catalog-types.ts +83 -0
  219. package/src/components/composites/catalog-browser/index.ts +10 -0
  220. package/src/components/composites/list-row/list-row.tsx +81 -8
  221. package/src/components/composites/settings/settings.tsx +67 -37
  222. package/src/components/conversation/scroll-follow.ts +109 -15
  223. package/src/components/layout/app-shell/index.ts +1 -0
  224. package/src/components/layout/app-shell/skip-link.tsx +35 -0
  225. package/src/components/layout/floating-preview/floating-preview.tsx +260 -0
  226. package/src/components/layout/floating-preview/index.ts +2 -0
  227. package/src/components/layout/floating-preview/model.ts +285 -0
  228. package/src/components/theme/index.ts +1 -0
  229. package/src/components/theme/theme-mode-toggle.tsx +56 -0
  230. package/src/components/ui/checkbox/checkbox.tsx +20 -3
  231. package/src/components/ui/combobox/combobox.tsx +24 -1
  232. package/src/components/ui/combobox/index.ts +1 -0
  233. package/src/components/ui/combobox/value-combobox.tsx +38 -0
  234. package/src/components/ui/empty/empty-state.tsx +71 -0
  235. package/src/components/ui/empty/index.ts +1 -0
  236. package/src/components/ui/entity-icon/entity-icon.tsx +33 -2
  237. package/src/components/ui/field/field.tsx +102 -5
  238. package/src/components/ui/field/index.ts +5 -0
  239. package/src/components/ui/field/number-field.tsx +71 -0
  240. package/src/components/ui/input/index.ts +1 -0
  241. package/src/components/ui/input/input-control.tsx +38 -0
  242. package/src/components/ui/input/input.tsx +32 -20
  243. package/src/components/ui/input-group/index.ts +1 -0
  244. package/src/components/ui/input-group/input-group.tsx +25 -2
  245. package/src/components/ui/input-group/secret-input-group.tsx +58 -0
  246. package/src/components/ui/native-select/native-select.tsx +27 -1
  247. package/src/components/ui/progress/progress.tsx +12 -8
  248. package/src/components/ui/radio-group/radio-group.tsx +30 -2
  249. package/src/components/ui/switch/switch.tsx +20 -3
  250. package/src/components/ui/tabs/tabs.tsx +1 -1
  251. package/src/components/ui/text-link/index.ts +2 -0
  252. package/src/components/ui/text-link/text-link.tsx +28 -0
  253. package/src/components/ui/textarea/textarea.tsx +24 -1
  254. package/src/index.ts +4 -0
  255. package/src/lib/clipboard.ts +41 -0
  256. package/src/lib/download.ts +29 -0
  257. package/src/lib/form-field.ts +93 -0
@@ -3,6 +3,7 @@ import { Check, ChevronDown, X } from 'lucide-solid'
3
3
  import type { ComponentProps } from 'solid-js'
4
4
  import { Show, splitProps } from 'solid-js'
5
5
  import { menuContentPadding, menuItem, popoverArrow } from '#lib/overlay'
6
+ import { useFormFieldControl } from '#lib/form-field'
6
7
  import { cn } from '#lib/utils'
7
8
 
8
9
  /**
@@ -43,10 +44,32 @@ export function ComboboxControl(props: ComponentProps<typeof KobalteCombobox.Con
43
44
  }
44
45
 
45
46
  export function ComboboxInput(props: ComponentProps<typeof KobalteCombobox.Input>) {
46
- const [local, rest] = splitProps(props, ['class'])
47
+ const [local, rest] = splitProps(props, [
48
+ 'class',
49
+ 'id',
50
+ 'aria-label',
51
+ 'aria-labelledby',
52
+ 'aria-describedby',
53
+ 'aria-invalid',
54
+ 'aria-errormessage',
55
+ ])
56
+ const field = useFormFieldControl('control', {
57
+ id: local.id,
58
+ 'aria-label': local['aria-label'],
59
+ 'aria-labelledby': local['aria-labelledby'],
60
+ 'aria-describedby': local['aria-describedby'],
61
+ 'aria-invalid': local['aria-invalid'],
62
+ 'aria-errormessage': local['aria-errormessage'],
63
+ })
47
64
 
48
65
  return (
49
66
  <KobalteCombobox.Input
67
+ id={field?.id ?? local.id}
68
+ aria-label={local['aria-label']}
69
+ aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
70
+ aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
71
+ aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
72
+ aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
50
73
  class={cn(
51
74
  'h-full flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground',
52
75
  'disabled:cursor-not-allowed',
@@ -8,3 +8,4 @@ export {
8
8
  ComboboxSection,
9
9
  ComboboxTrigger,
10
10
  } from './combobox'
11
+ export { ValueCombobox, type ValueComboboxProps } from './value-combobox'
@@ -0,0 +1,38 @@
1
+ import { splitProps, type ComponentProps, type JSX } from 'solid-js'
2
+ import { Combobox, ComboboxContent, ComboboxInput, ComboboxItem } from './combobox'
3
+
4
+ export type ValueComboboxChoice =
5
+ | { value: string; label: string | number; textValue?: string }
6
+ | { value: string; label: JSX.Element; textValue: string }
7
+
8
+ export type ValueComboboxProps = Omit<ComponentProps<typeof ComboboxInput>, 'value'> & {
9
+ value: string
10
+ choices: ValueComboboxChoice[]
11
+ onValueChange: (value: string) => void
12
+ }
13
+
14
+ /** A single-choice combobox for a controlled string value and labelled choices. */
15
+ export function ValueCombobox(props: ValueComboboxProps) {
16
+ const [local, rest] = splitProps(props, ['value', 'choices', 'onValueChange'])
17
+
18
+ return (
19
+ <Combobox
20
+ options={local.choices}
21
+ optionValue="value"
22
+ optionTextValue={(choice) => choice.textValue ?? textFromLabel(choice.label)}
23
+ optionLabel={(choice) => choice.textValue ?? textFromLabel(choice.label)}
24
+ value={local.choices.find((choice) => String(choice.value) === local.value) ?? null}
25
+ onChange={(option) => option && local.onValueChange(String(option.value))}
26
+ itemComponent={(itemProps) => (
27
+ <ComboboxItem item={itemProps.item}>{itemProps.item.rawValue.label}</ComboboxItem>
28
+ )}
29
+ >
30
+ <ComboboxInput {...rest} />
31
+ <ComboboxContent />
32
+ </Combobox>
33
+ )
34
+ }
35
+
36
+ function textFromLabel(label: JSX.Element) {
37
+ return typeof label === 'string' || typeof label === 'number' ? String(label) : ''
38
+ }
@@ -0,0 +1,71 @@
1
+ import { Search } from 'lucide-solid'
2
+ import type { ComponentProps, JSX } from 'solid-js'
3
+ import { Show, splitProps } from 'solid-js'
4
+ import { ActionButton } from '../../composites/action-button'
5
+ import { Spinner } from '../spinner'
6
+ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './empty'
7
+
8
+ export type EmptyStateProps = Omit<ComponentProps<'div'>, 'children' | 'role' | 'aria-live'> & {
9
+ title: string
10
+ detail: string
11
+ busy?: boolean
12
+ icon?: JSX.Element
13
+ announceAs?: 'alert' | 'status'
14
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6
15
+ action?: () => void
16
+ actionLabel?: string
17
+ actionTooltip?: string
18
+ }
19
+
20
+ /** A semantic empty, loading, or error state built from the shared empty parts. */
21
+ export function EmptyState(props: EmptyStateProps) {
22
+ const [local, rest] = splitProps(props, [
23
+ 'title',
24
+ 'detail',
25
+ 'busy',
26
+ 'icon',
27
+ 'announceAs',
28
+ 'headingLevel',
29
+ 'action',
30
+ 'actionLabel',
31
+ 'actionTooltip',
32
+ 'class',
33
+ ])
34
+
35
+ return (
36
+ <Empty
37
+ {...rest}
38
+ class={local.class}
39
+ role={local.announceAs}
40
+ aria-live={local.announceAs === 'status' ? 'polite' : undefined}
41
+ aria-busy={local.busy || undefined}
42
+ >
43
+ <EmptyHeader>
44
+ <Show when={local.busy || local.icon}>
45
+ <EmptyMedia variant="icon">
46
+ {local.busy ? (
47
+ <Spinner label={false} aria-hidden="true" />
48
+ ) : (
49
+ (local.icon ?? <Search aria-hidden="true" />)
50
+ )}
51
+ </EmptyMedia>
52
+ </Show>
53
+ <EmptyTitle role="heading" aria-level={local.headingLevel ?? 1}>
54
+ {local.title}
55
+ </EmptyTitle>
56
+ <EmptyDescription>{local.detail}</EmptyDescription>
57
+ </EmptyHeader>
58
+ <Show when={local.action}>
59
+ <EmptyContent>
60
+ <ActionButton
61
+ tooltip={local.actionTooltip ?? local.actionLabel ?? 'Try again'}
62
+ aria-label={local.actionLabel ?? 'Try again'}
63
+ onClick={local.action}
64
+ >
65
+ {local.actionLabel ?? 'Try again'}
66
+ </ActionButton>
67
+ </EmptyContent>
68
+ </Show>
69
+ </Empty>
70
+ )
71
+ }
@@ -8,3 +8,4 @@ export {
8
8
  type EmptyMediaProps,
9
9
  type EmptyProps,
10
10
  } from './empty'
11
+ export { EmptyState, type EmptyStateProps } from './empty-state'
@@ -1,4 +1,11 @@
1
- import { Show, splitProps, type Component, type ComponentProps, type JSX } from 'solid-js'
1
+ import {
2
+ createSignal,
3
+ Show,
4
+ splitProps,
5
+ type Component,
6
+ type ComponentProps,
7
+ type JSX,
8
+ } from 'solid-js'
2
9
  import { Dynamic } from 'solid-js/web'
3
10
  import { cva, type VariantProps } from '#lib/variants'
4
11
  import { cn } from '#lib/utils'
@@ -16,7 +23,8 @@ import { cn } from '#lib/utils'
16
23
  *
17
24
  * 1. **A glyph is always present.** A brand with no logo gets a monogram, not an
18
25
  * empty box. `fallback` takes the name and returns the glyph, so a caller can
19
- * choose initials, a domain letter, or a kind icon.
26
+ * choose initials, a domain letter, or a kind icon. When `src` is supplied,
27
+ * its contained image overlays that glyph and failure reveals the glyph below.
20
28
  * 2. **The tile owns its own contrast.** `tone` tints the background while
21
29
  * monogram text keeps the body foreground; vector glyphs retain the status
22
30
  * colour. A caller never picks a colour pair or ships a glyph that disappears.
@@ -67,6 +75,8 @@ export type EntityIconProps = Omit<ComponentProps<'span'>, 'children'> &
67
75
  EntityIconVariantProps & {
68
76
  /** The entity's name. Used for the accessible label and, via `fallback`, the monogram. */
69
77
  name: string
78
+ /** Optional image URL. The image is decorative; `name` remains the accessible label. */
79
+ src?: string
70
80
  /**
71
81
  * A logo or icon — a Lucide icon reference (`icon={Wrench}`) or an element
72
82
  * (`icon={<Wrench />}`). Both are accepted because both are what a caller
@@ -108,9 +118,27 @@ function Glyph(props: { glyph: Glyph }) {
108
118
  )
109
119
  }
110
120
 
121
+ function EntityIconImage(props: { src: string }) {
122
+ const [failed, setFailed] = createSignal(false)
123
+
124
+ return (
125
+ <Show when={!failed()}>
126
+ <img
127
+ data-slot="entity-icon-image"
128
+ src={props.src}
129
+ alt=""
130
+ aria-hidden="true"
131
+ class="pointer-events-none absolute inset-0 size-full object-contain"
132
+ onError={() => setFailed(true)}
133
+ />
134
+ </Show>
135
+ )
136
+ }
137
+
111
138
  export function EntityIcon(props: EntityIconProps) {
112
139
  const [local, rest] = splitProps(props, [
113
140
  'name',
141
+ 'src',
114
142
  'icon',
115
143
  'fallback',
116
144
  'badge',
@@ -146,6 +174,9 @@ export function EntityIcon(props: EntityIconProps) {
146
174
  >
147
175
  {(value) => <Glyph glyph={value()} />}
148
176
  </Show>
177
+ <Show when={local.src} keyed>
178
+ {(src) => <EntityIconImage src={src} />}
179
+ </Show>
149
180
  <Show when={local.badge}>
150
181
  <span class="absolute right-0 bottom-0 translate-x-1/4 translate-y-1/4">{local.badge}</span>
151
182
  </Show>
@@ -1,6 +1,8 @@
1
1
  import { TextField as KobalteTextField } from '@kobalte/core/text-field'
2
- import type { ComponentProps } from 'solid-js'
3
- import { Show, splitProps } from 'solid-js'
2
+ import type { ComponentProps, JSX } from 'solid-js'
3
+ import { createUniqueId, Show, splitProps } from 'solid-js'
4
+ import { Label } from '../label/label'
5
+ import { createFormFieldContext, FormFieldContext } from '#lib/form-field'
4
6
  import { cn } from '#lib/utils'
5
7
 
6
8
  /**
@@ -9,21 +11,110 @@ import { cn } from '#lib/utils'
9
11
  * The wrapper that makes a form control describable and validatable: a label,
10
12
  * a description, an error message, and the ARIA relationships between them.
11
13
  *
14
+ * This Kobalte TextField composition is for its own `FieldLabel`, `FieldInput`
15
+ * and `FieldTextArea` parts. Use `FormField` to wire the standalone shared
16
+ * controls, including checkboxes, switches and comboboxes.
17
+ *
12
18
  * It is built on Kobalte's TextField specifically so `aria-describedby`,
13
19
  * `aria-invalid` and `aria-errormessage` are wired by construction. Those three
14
20
  * attributes are the difference between "this field is required and empty" and
15
21
  * a red border that only a sighted user can perceive — and they are also the
16
22
  * first thing a hand-rolled form drops.
17
23
  *
18
- * `Field` owns the description and the error slot, so a form's controls line up
19
- * and its messages land in the same place regardless of which control is used.
20
- * Compose it with any control: `Field` wraps, `Label`/`Input`/`Select` fill it.
24
+ * `Field` owns the description and the error slot, so its controls line up and
25
+ * its messages land in the same place regardless of which TextField part is used.
21
26
  */
22
27
  export function Field(props: ComponentProps<typeof KobalteTextField>) {
23
28
  const [local, rest] = splitProps(props, ['class'])
24
29
  return <KobalteTextField class={cn('flex flex-col gap-1.5', local.class)} {...rest} />
25
30
  }
26
31
 
32
+ export type FormFieldProps = Omit<ComponentProps<'div'>, 'children' | 'ref'> & {
33
+ label: string
34
+ hint?: string
35
+ error?: string
36
+ controlId?: string
37
+ group?: boolean
38
+ children: JSX.Element
39
+ }
40
+
41
+ /**
42
+ * FormField.
43
+ *
44
+ * A shared label, help and validation wrapper for any one shared control. Its
45
+ * context is consumed by Input, Textarea, NativeSelect, Checkbox, Switch,
46
+ * RadioGroup and ComboboxInput, so the actual focusable element receives the
47
+ * generated ID and description relationships. Set `group` when the label names
48
+ * a set of controls; the wrapper then uses fieldset and legend semantics.
49
+ */
50
+ export function FormField(props: FormFieldProps) {
51
+ const [local, rest] = splitProps(props, [
52
+ 'class',
53
+ 'label',
54
+ 'hint',
55
+ 'error',
56
+ 'controlId',
57
+ 'group',
58
+ 'children',
59
+ ])
60
+ const fieldId = createUniqueId()
61
+ const labelId = `${fieldId}-label`
62
+ const descriptionId = `${fieldId}-description`
63
+ const errorId = `${fieldId}-error`
64
+ const field = createFormFieldContext({
65
+ controlId: local.controlId,
66
+ defaultControlId: fieldId,
67
+ labelId,
68
+ descriptionId,
69
+ errorId,
70
+ hint: () => local.hint,
71
+ error: () => local.error,
72
+ group: () => local.group ?? false,
73
+ })
74
+ const help = () => (
75
+ <>
76
+ <Show when={local.hint}>
77
+ <p id={descriptionId} class="text-muted-foreground text-sm">
78
+ {local.hint}
79
+ </p>
80
+ </Show>
81
+ <Show when={local.error}>
82
+ <p id={errorId} role="alert" class="text-foreground text-sm">
83
+ {local.error}
84
+ </p>
85
+ </Show>
86
+ </>
87
+ )
88
+
89
+ return (
90
+ <FormFieldContext.Provider value={field}>
91
+ <div class={cn('flex flex-col gap-1.5', local.class)} {...rest}>
92
+ {local.group ? (
93
+ <FieldSet
94
+ class="flex flex-col gap-1.5"
95
+ aria-describedby={field.describedBy()}
96
+ aria-invalid={field.invalid() || undefined}
97
+ >
98
+ <FieldLegend id={labelId} variant="label">
99
+ {local.label}
100
+ </FieldLegend>
101
+ {local.children}
102
+ {help()}
103
+ </FieldSet>
104
+ ) : (
105
+ <>
106
+ <Label id={labelId} for={field.controlId()}>
107
+ {local.label}
108
+ </Label>
109
+ {local.children}
110
+ {help()}
111
+ </>
112
+ )}
113
+ </div>
114
+ </FormFieldContext.Provider>
115
+ )
116
+ }
117
+
27
118
  export function FieldLabel(props: ComponentProps<typeof KobalteTextField.Label>) {
28
119
  const [local, rest] = splitProps(props, ['class'])
29
120
  return (
@@ -96,6 +187,12 @@ export function FieldSet(props: ComponentProps<'fieldset'>) {
96
187
  return <fieldset class={cn('flex flex-col gap-4', local.class)} {...rest} />
97
188
  }
98
189
 
190
+ /** A visual stack for related controls that does not imply fieldset semantics. */
191
+ export function FieldGroup(props: ComponentProps<'div'>) {
192
+ const [local, rest] = splitProps(props, ['class'])
193
+ return <div data-slot="field-group" class={cn('flex flex-col gap-4', local.class)} {...rest} />
194
+ }
195
+
99
196
  export function FieldLegend(props: ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
100
197
  const [local, rest] = splitProps(props, ['class', 'variant'])
101
198
  return (
@@ -1,11 +1,16 @@
1
1
  export {
2
2
  Field,
3
+ FormField,
3
4
  FieldDescription,
4
5
  FieldError,
6
+ FieldGroup,
5
7
  FieldInput,
6
8
  FieldLabel,
7
9
  FieldLegend,
8
10
  FieldSeparator,
9
11
  FieldSet,
10
12
  FieldTextArea,
13
+ type FormFieldProps,
11
14
  } from './field'
15
+ export { NumberField } from './number-field'
16
+ export type { NumberFieldProps } from './number-field'
@@ -0,0 +1,71 @@
1
+ import { createEffect, createSignal, type JSX } from 'solid-js'
2
+ import { FormField } from './field'
3
+ import { Input } from '../input/input'
4
+
5
+ export type NumberFieldProps = {
6
+ label: string
7
+ hint?: string
8
+ value: number
9
+ min: number
10
+ max: number
11
+ integer?: boolean
12
+ onChange: (value: number) => void
13
+ class?: string
14
+ onBlur?: JSX.EventHandler<HTMLInputElement, FocusEvent>
15
+ }
16
+
17
+ /**
18
+ * A controlled numeric field that keeps an editable draft while validating
19
+ * range and integer constraints. Invalid drafts are announced and restored
20
+ * to the latest controlled value when focus leaves the input.
21
+ */
22
+ export function NumberField(props: NumberFieldProps) {
23
+ const [draft, setDraft] = createSignal(String(props.value))
24
+ const [error, setError] = createSignal('')
25
+ const integer = () => props.integer ?? true
26
+
27
+ createEffect(() => {
28
+ setDraft(String(props.value))
29
+ })
30
+
31
+ const validate = (raw: string) => {
32
+ if (!raw) return `${props.label} is required.`
33
+ const next = Number(raw)
34
+ if (!Number.isFinite(next)) return `${props.label} must be a valid number.`
35
+ if (integer() && !Number.isInteger(next)) {
36
+ return `${props.label} must be a whole number.`
37
+ }
38
+ if (next < props.min || next > props.max) {
39
+ return `${props.label} must be between ${props.min} and ${props.max}.`
40
+ }
41
+ return ''
42
+ }
43
+
44
+ return (
45
+ <FormField label={props.label} hint={props.hint} error={error()} class={props.class}>
46
+ <Input
47
+ type="number"
48
+ value={draft()}
49
+ min={props.min}
50
+ max={props.max}
51
+ step={integer() ? 1 : 'any'}
52
+ required
53
+ onInput={(event) => {
54
+ const raw = event.currentTarget.value
55
+ setDraft(raw)
56
+ const nextError = validate(raw)
57
+ setError(nextError)
58
+ if (!nextError) props.onChange(Number(raw))
59
+ }}
60
+ onBlur={(event) => {
61
+ const nextError = validate(draft())
62
+ if (nextError) {
63
+ setDraft(String(props.value))
64
+ setError('')
65
+ }
66
+ props.onBlur?.(event)
67
+ }}
68
+ />
69
+ </FormField>
70
+ )
71
+ }
@@ -1 +1,2 @@
1
1
  export { Input, type InputProps } from './input'
2
+ export { InputControl, type InputControlProps } from './input-control'
@@ -0,0 +1,38 @@
1
+ import type { ComponentProps } from 'solid-js'
2
+ import { splitProps } from 'solid-js'
3
+ import { cn } from '#lib/utils'
4
+
5
+ /**
6
+ * InputControl.
7
+ *
8
+ * A styled native input with no optional suggestion-list behavior. Use this
9
+ * when a consumer needs only the control; `Input` adds native datalist
10
+ * suggestions for forms that need them.
11
+ */
12
+ export type InputControlProps = ComponentProps<'input'>
13
+
14
+ export function InputControl(props: InputControlProps) {
15
+ const [local, rest] = splitProps(props, ['class', 'type'])
16
+
17
+ return (
18
+ <input
19
+ data-slot="input"
20
+ type={local.type ?? 'text'}
21
+ class={cn(
22
+ 'h-control-md w-full min-w-0 rounded-md border border-input bg-transparent px-control-md py-1 text-sm',
23
+ 'transition-[color,box-shadow,border-color] ease-out outline-none',
24
+ 'placeholder:text-muted-foreground',
25
+ 'selection:bg-primary selection:text-primary-foreground',
26
+ 'file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
27
+ /* A pointer click on a text field is a typing affordance, so the ring
28
+ appears where the user agent already matches :focus-visible. */
29
+ 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
30
+ 'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
31
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
32
+ 'read-only:bg-surface-hover',
33
+ local.class
34
+ )}
35
+ {...rest}
36
+ />
37
+ )
38
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ComponentProps } from 'solid-js'
2
2
  import { createUniqueId, For, Show, splitProps } from 'solid-js'
3
- import { cn } from '#lib/utils'
3
+ import { useFormFieldControl } from '#lib/form-field'
4
+ import { InputControl } from './input-control'
4
5
 
5
6
  /**
6
7
  * Input.
@@ -20,31 +21,42 @@ export type InputProps = ComponentProps<'input'> & {
20
21
  }
21
22
 
22
23
  export function Input(props: InputProps) {
23
- const [local, rest] = splitProps(props, ['class', 'type', 'list', 'suggestions'])
24
+ const [local, rest] = splitProps(props, [
25
+ 'class',
26
+ 'type',
27
+ 'id',
28
+ 'list',
29
+ 'suggestions',
30
+ 'aria-label',
31
+ 'aria-labelledby',
32
+ 'aria-describedby',
33
+ 'aria-invalid',
34
+ 'aria-errormessage',
35
+ ])
36
+ const field = useFormFieldControl('control', {
37
+ id: local.id,
38
+ 'aria-label': local['aria-label'],
39
+ 'aria-labelledby': local['aria-labelledby'],
40
+ 'aria-describedby': local['aria-describedby'],
41
+ 'aria-invalid': local['aria-invalid'],
42
+ 'aria-errormessage': local['aria-errormessage'],
43
+ })
24
44
  const suggestionsId = createUniqueId()
25
45
  const listId = () => local.list ?? `input-suggestions-${suggestionsId}`
26
46
 
27
47
  return (
28
48
  <>
29
- <input
30
- data-slot="input"
31
- type={local.type ?? 'text'}
32
- list={local.suggestions ? listId() : local.list}
33
- class={cn(
34
- 'h-control-md w-full min-w-0 rounded-md border border-input bg-transparent px-control-md py-1 text-sm',
35
- 'transition-[color,box-shadow,border-color] ease-out outline-none',
36
- 'placeholder:text-muted-foreground',
37
- 'selection:bg-primary selection:text-primary-foreground',
38
- 'file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
39
- /* A pointer click on a text field is a typing affordance, so the ring
40
- appears where the user agent already matches :focus-visible. */
41
- 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
42
- 'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
43
- 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
44
- 'read-only:bg-surface-hover',
45
- local.class
46
- )}
49
+ <InputControl
47
50
  {...rest}
51
+ class={local.class}
52
+ type={local.type}
53
+ id={field?.id ?? local.id}
54
+ list={local.suggestions ? listId() : local.list}
55
+ aria-label={local['aria-label']}
56
+ aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
57
+ aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
58
+ aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
59
+ aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
48
60
  />
49
61
  <Show when={local.suggestions}>
50
62
  {(suggestions) => (
@@ -6,3 +6,4 @@ export {
6
6
  InputGroupText,
7
7
  type InputGroupAddonProps,
8
8
  } from './input-group'
9
+ export { SecretInputGroup, type SecretInputGroupProps } from './secret-input-group'
@@ -1,5 +1,6 @@
1
1
  import type { ComponentProps, JSX } from 'solid-js'
2
2
  import { splitProps } from 'solid-js'
3
+ import { useFormFieldControl } from '#lib/form-field'
3
4
  import { cn } from '#lib/utils'
4
5
 
5
6
  /**
@@ -26,7 +27,7 @@ export function InputGroup(props: ComponentProps<'div'>) {
26
27
  'transition-[color,box-shadow,border-color] ease-out',
27
28
  'focus-within:border-ring focus-within:ring-3 focus-within:ring-primary-subtle',
28
29
  'has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-3 has-[[aria-invalid=true]]:ring-destructive-subtle',
29
- 'has-[input:disabled]:pointer-events-none has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50',
30
+ 'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50',
30
31
  local.class
31
32
  )}
32
33
  {...rest}
@@ -35,11 +36,33 @@ export function InputGroup(props: ComponentProps<'div'>) {
35
36
  }
36
37
 
37
38
  export function InputGroupInput(props: ComponentProps<'input'>) {
38
- const [local, rest] = splitProps(props, ['class'])
39
+ const [local, rest] = splitProps(props, [
40
+ 'class',
41
+ 'id',
42
+ 'aria-label',
43
+ 'aria-labelledby',
44
+ 'aria-describedby',
45
+ 'aria-invalid',
46
+ 'aria-errormessage',
47
+ ])
48
+ const field = useFormFieldControl('control', {
49
+ id: local.id,
50
+ 'aria-label': local['aria-label'],
51
+ 'aria-labelledby': local['aria-labelledby'],
52
+ 'aria-describedby': local['aria-describedby'],
53
+ 'aria-invalid': local['aria-invalid'],
54
+ 'aria-errormessage': local['aria-errormessage'],
55
+ })
39
56
 
40
57
  return (
41
58
  <input
42
59
  data-slot="input-group-input"
60
+ id={field?.id ?? local.id}
61
+ aria-label={local['aria-label']}
62
+ aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
63
+ aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
64
+ aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
65
+ aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
43
66
  class={cn(
44
67
  'h-full min-w-0 flex-1 bg-transparent text-sm outline-none',
45
68
  'placeholder:text-muted-foreground',