@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
@@ -0,0 +1,260 @@
1
+ import { createEffect, createSignal, onCleanup, onMount, on, type JSX } from 'solid-js'
2
+ import { Move, X } from 'lucide-solid'
3
+ import { ActionButton } from '../../composites/action-button'
4
+ import { ScrollArea } from '../../ui/scroll-area'
5
+ import {
6
+ type FloatingPreviewFrame,
7
+ type FloatingPreviewResizeDirection,
8
+ type FloatingPreviewSize,
9
+ resolveFloatingPreviewFrame,
10
+ resizeFloatingPreview,
11
+ } from './model'
12
+
13
+ const directions: readonly {
14
+ direction: FloatingPreviewResizeDirection
15
+ class: string
16
+ glyph: string
17
+ }[] = [
18
+ {
19
+ direction: 'north',
20
+ class: 'absolute -top-3 right-6 left-6 h-control-xs w-auto cursor-ns-resize touch-none',
21
+ glyph: '↕',
22
+ },
23
+ {
24
+ direction: 'south',
25
+ class: 'absolute -bottom-3 right-6 left-6 h-control-xs w-auto cursor-ns-resize touch-none',
26
+ glyph: '↕',
27
+ },
28
+ {
29
+ direction: 'east',
30
+ class: 'absolute -right-3 top-6 bottom-6 w-6 h-auto cursor-ew-resize touch-none',
31
+ glyph: '↔',
32
+ },
33
+ {
34
+ direction: 'west',
35
+ class: 'absolute -left-3 top-6 bottom-6 w-6 h-auto cursor-ew-resize touch-none',
36
+ glyph: '↔',
37
+ },
38
+ {
39
+ direction: 'northwest',
40
+ class: 'absolute -top-3 -left-3 size-control-xs cursor-nwse-resize touch-none',
41
+ glyph: '↖',
42
+ },
43
+ {
44
+ direction: 'northeast',
45
+ class: 'absolute -top-3 -right-3 size-control-xs cursor-nesw-resize touch-none',
46
+ glyph: '↗',
47
+ },
48
+ {
49
+ direction: 'southwest',
50
+ class: 'absolute -bottom-3 -left-3 size-control-xs cursor-nesw-resize touch-none',
51
+ glyph: '↙',
52
+ },
53
+ {
54
+ direction: 'southeast',
55
+ class: 'absolute -bottom-3 -right-3 size-control-xs cursor-nwse-resize touch-none',
56
+ glyph: '↘',
57
+ },
58
+ ]
59
+
60
+ export type FloatingPreviewProps = {
61
+ label: string
62
+ source: FloatingPreviewSize
63
+ onClose?: () => void
64
+ actions?: JSX.Element
65
+ children: JSX.Element
66
+ }
67
+
68
+ /** A host-bounded movable preview frame. Content and actions remain caller-owned. */
69
+ export function FloatingPreview(props: FloatingPreviewProps) {
70
+ const [frame, setFrame] = createSignal<FloatingPreviewFrame>({ x: 0, y: 0, width: 1, height: 1 })
71
+ // oxlint-disable-next-line no-unassigned-vars -- assigned through Solid's ref callback.
72
+ let bounds: HTMLDivElement | undefined
73
+ let observer: ResizeObserver | undefined
74
+ let gesture:
75
+ | {
76
+ id: number
77
+ x: number
78
+ y: number
79
+ start: FloatingPreviewFrame
80
+ direction: FloatingPreviewResizeDirection | null
81
+ }
82
+ | undefined
83
+
84
+ const measure = () => {
85
+ const rect = bounds?.getBoundingClientRect()
86
+ if (!rect) return
87
+ setFrame((current) =>
88
+ resolveFloatingPreviewFrame({
89
+ width: current.width > 1 ? current.width : null,
90
+ position: current.width > 1 ? { x: current.x, y: current.y } : null,
91
+ source: props.source,
92
+ container: { width: rect.width, height: rect.height },
93
+ })
94
+ )
95
+ }
96
+ createEffect(
97
+ on(
98
+ () => [props.source.width, props.source.height] as const,
99
+ () => measure(),
100
+ { defer: true }
101
+ )
102
+ )
103
+ onMount(() => {
104
+ if (bounds) {
105
+ observer = new ResizeObserver(measure)
106
+ observer.observe(bounds)
107
+ measure()
108
+ }
109
+ })
110
+ onCleanup(() => observer?.disconnect())
111
+
112
+ function start(event: PointerEvent, direction: FloatingPreviewResizeDirection | null) {
113
+ if (event.button !== 0 || gesture) return
114
+ event.preventDefault()
115
+ event.stopPropagation()
116
+ gesture = { id: event.pointerId, x: event.clientX, y: event.clientY, start: frame(), direction }
117
+ ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
118
+ }
119
+ function move(event: PointerEvent) {
120
+ if (!gesture || gesture.id !== event.pointerId) return
121
+ const rect = bounds?.getBoundingClientRect()
122
+ if (!rect) return
123
+ const container = { width: rect.width, height: rect.height }
124
+ const delta = { x: event.clientX - gesture.x, y: event.clientY - gesture.y }
125
+ setFrame(
126
+ gesture.direction
127
+ ? resizeFloatingPreview({
128
+ start: gesture.start,
129
+ direction: gesture.direction,
130
+ delta,
131
+ source: props.source,
132
+ container,
133
+ })
134
+ : resolveFloatingPreviewFrame({
135
+ width: gesture.start.width,
136
+ position: { x: gesture.start.x + delta.x, y: gesture.start.y + delta.y },
137
+ source: props.source,
138
+ container,
139
+ })
140
+ )
141
+ }
142
+ function end(event: PointerEvent) {
143
+ if (gesture?.id === event.pointerId) gesture = undefined
144
+ }
145
+ function keyboardMove(event: KeyboardEvent) {
146
+ const delta = { x: 0, y: 0 }
147
+ if (event.key === 'ArrowLeft') delta.x = -10
148
+ else if (event.key === 'ArrowRight') delta.x = 10
149
+ else if (event.key === 'ArrowUp') delta.y = -10
150
+ else if (event.key === 'ArrowDown') delta.y = 10
151
+ else return
152
+ event.preventDefault()
153
+ const rect = bounds?.getBoundingClientRect()
154
+ if (!rect) return
155
+ const current = frame()
156
+ setFrame(
157
+ resolveFloatingPreviewFrame({
158
+ width: current.width,
159
+ position: { x: current.x + delta.x, y: current.y + delta.y },
160
+ source: props.source,
161
+ container: { width: rect.width, height: rect.height },
162
+ })
163
+ )
164
+ }
165
+ function keyboardResize(event: KeyboardEvent, direction: FloatingPreviewResizeDirection) {
166
+ if (!event.key.startsWith('Arrow')) return
167
+ event.preventDefault()
168
+ const rect = bounds?.getBoundingClientRect()
169
+ if (!rect) return
170
+ const delta = {
171
+ x: event.key === 'ArrowRight' ? 10 : event.key === 'ArrowLeft' ? -10 : 0,
172
+ y: event.key === 'ArrowDown' ? 10 : event.key === 'ArrowUp' ? -10 : 0,
173
+ }
174
+ setFrame(
175
+ resizeFloatingPreview({
176
+ start: frame(),
177
+ direction,
178
+ delta,
179
+ source: props.source,
180
+ container: { width: rect.width, height: rect.height },
181
+ })
182
+ )
183
+ }
184
+
185
+ return (
186
+ <div
187
+ ref={bounds}
188
+ class="pointer-events-none absolute inset-0 z-(--z-sticky) overflow-hidden"
189
+ aria-label={props.label}
190
+ >
191
+ <section
192
+ class="pointer-events-auto absolute flex flex-col overflow-visible rounded-lg border border-border bg-card text-card-foreground shadow-lg"
193
+ aria-label={props.label}
194
+ style={{
195
+ left: `${frame().x}px`,
196
+ top: `${frame().y}px`,
197
+ width: `${frame().width}px`,
198
+ height: `${frame().height}px`,
199
+ }}
200
+ >
201
+ <header class="flex h-control-sm items-center gap-1 border-b border-border p-1">
202
+ <ActionButton
203
+ type="button"
204
+ variant="ghost"
205
+ size="sm"
206
+ class="flex-1 cursor-move touch-none"
207
+ aria-label={`Move ${props.label}`}
208
+ tooltip={`Move ${props.label}. Use arrow keys to adjust its position.`}
209
+ onPointerDown={(event) => start(event, null)}
210
+ onPointerMove={move}
211
+ onPointerUp={end}
212
+ onPointerCancel={end}
213
+ onLostPointerCapture={end}
214
+ onKeyDown={keyboardMove}
215
+ >
216
+ <>
217
+ <Move aria-hidden="true" />
218
+ Move
219
+ </>
220
+ </ActionButton>
221
+ <div class="flex items-center gap-1">{props.actions}</div>
222
+ {props.onClose && (
223
+ <ActionButton
224
+ type="button"
225
+ variant="ghost"
226
+ size="icon-sm"
227
+ aria-label={`Close ${props.label}`}
228
+ tooltip={`Close ${props.label}.`}
229
+ onClick={props.onClose}
230
+ >
231
+ <X aria-hidden="true" />
232
+ </ActionButton>
233
+ )}
234
+ </header>
235
+ <ScrollArea role="region" aria-label={`${props.label} content`} class="flex-1" tabIndex={0}>
236
+ {props.children}
237
+ </ScrollArea>
238
+ {directions.map(({ direction, class: className, glyph }) => (
239
+ <ActionButton
240
+ type="button"
241
+ variant="ghost"
242
+ size="icon-sm"
243
+ class={className}
244
+ data-direction={direction}
245
+ aria-label={`Resize ${props.label} ${direction}`}
246
+ tooltip={`Resize ${props.label} from the ${direction} edge. Use arrow keys to resize.`}
247
+ onPointerDown={(event) => start(event, direction)}
248
+ onPointerMove={move}
249
+ onPointerUp={end}
250
+ onPointerCancel={end}
251
+ onLostPointerCapture={end}
252
+ onKeyDown={(event) => keyboardResize(event, direction)}
253
+ >
254
+ <span aria-hidden="true">{glyph}</span>
255
+ </ActionButton>
256
+ ))}
257
+ </section>
258
+ </div>
259
+ )
260
+ }
@@ -0,0 +1,2 @@
1
+ export * from './model'
2
+ export * from './floating-preview'
@@ -0,0 +1,285 @@
1
+ /*
2
+ * Copyright (c) 2026 Adea contributors
3
+ * SPDX-License-Identifier: MIT
4
+ * Geometry translated from t3code apps/web/src/components/preview/
5
+ * previewMiniPlayerLayout.ts, issue #422, revision
6
+ * 77bca8b2d76a1f42552e5eee7d277fcb1160347a (MIT). Runtime and device
7
+ * behavior are excluded. See NOTICE.
8
+ */
9
+
10
+ export const PREVIEW_MINI_PLAYER_EDGE_GAP = 12
11
+ export const PREVIEW_MINI_PLAYER_CORNER_RADIUS = 12
12
+ export interface PreviewMiniPlayerSize {
13
+ readonly width: number
14
+ readonly height: number
15
+ }
16
+ export interface PreviewMiniPlayerPosition {
17
+ readonly x: number
18
+ readonly y: number
19
+ }
20
+ export interface PreviewMiniPlayerFrame extends PreviewMiniPlayerPosition, PreviewMiniPlayerSize {}
21
+
22
+ export type BrowserViewportResizeDirection =
23
+ | 'north'
24
+ | 'south'
25
+ | 'east'
26
+ | 'west'
27
+ | 'northwest'
28
+ | 'northeast'
29
+ | 'southwest'
30
+ | 'southeast'
31
+
32
+ const PREVIEW_MINI_PLAYER_DEFAULT_BOX = { width: 320, height: 320 } as const
33
+ const PREVIEW_MINI_PLAYER_MIN_SIZE = { width: 240, height: 150 } as const
34
+
35
+ interface HorizontalSpan {
36
+ readonly left: number
37
+ readonly right: number
38
+ }
39
+
40
+ /**
41
+ * The composer stack docked to the bottom edge, in container coordinates. It
42
+ * only reserves the columns it covers, so the margins beside it stay open all
43
+ * the way down.
44
+ */
45
+ export interface PreviewMiniPlayerObstacles {
46
+ readonly composer: (HorizontalSpan & { readonly height: number }) | null
47
+ }
48
+
49
+ export const NO_PREVIEW_MINI_PLAYER_OBSTACLES: PreviewMiniPlayerObstacles = { composer: null }
50
+
51
+ const spanOf = (x: number, width: number): HorizontalSpan => ({ left: x, right: x + width })
52
+
53
+ const spansOverlap = (a: HorizontalSpan, b: HorizontalSpan) => a.left < b.right && a.right > b.left
54
+
55
+ /** The lowest row (before the edge gap) open to a player covering these columns. */
56
+ function floorFor(
57
+ span: HorizontalSpan,
58
+ container: PreviewMiniPlayerSize,
59
+ obstacles: PreviewMiniPlayerObstacles
60
+ ): number {
61
+ const { composer } = obstacles
62
+ return composer && spansOverlap(span, composer)
63
+ ? container.height - Math.max(0, composer.height)
64
+ : container.height
65
+ }
66
+
67
+ /**
68
+ * The box a stored size is fitted into. A player with a position keeps the
69
+ * rows its own columns have, so a tall frame parked beside the composer
70
+ * survives the next layout pass; without one it takes the rows above the
71
+ * composer, which every column has.
72
+ */
73
+ const availableArea = (
74
+ container: PreviewMiniPlayerSize,
75
+ obstacles: PreviewMiniPlayerObstacles,
76
+ span: HorizontalSpan | null
77
+ ): PreviewMiniPlayerSize => ({
78
+ width: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
79
+ height:
80
+ (span
81
+ ? floorFor(span, container, obstacles)
82
+ : container.height - Math.max(0, obstacles.composer?.height ?? 0)) -
83
+ PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
84
+ })
85
+
86
+ /**
87
+ * Width is the player's only free dimension; height always follows the source
88
+ * aspect ratio so the content fills the box without letterboxing. The player
89
+ * never grows past the source's own size, and a tight container wins over the
90
+ * minimum.
91
+ */
92
+ function fitPreviewMiniPlayerWidth(
93
+ desiredWidth: number,
94
+ source: PreviewMiniPlayerSize,
95
+ max: PreviewMiniPlayerSize
96
+ ): PreviewMiniPlayerSize {
97
+ const aspectRatio = source.width / source.height
98
+ const width = Math.min(
99
+ Math.max(
100
+ desiredWidth,
101
+ PREVIEW_MINI_PLAYER_MIN_SIZE.width,
102
+ PREVIEW_MINI_PLAYER_MIN_SIZE.height * aspectRatio
103
+ ),
104
+ source.width,
105
+ Math.max(1, max.width),
106
+ Math.max(1, max.height * aspectRatio)
107
+ )
108
+ return { width: Math.round(width), height: Math.round(width / aspectRatio) }
109
+ }
110
+
111
+ function defaultPreviewMiniPlayerWidth(source: PreviewMiniPlayerSize): number {
112
+ return Math.min(
113
+ PREVIEW_MINI_PLAYER_DEFAULT_BOX.width,
114
+ (PREVIEW_MINI_PLAYER_DEFAULT_BOX.height * source.width) / source.height
115
+ )
116
+ }
117
+
118
+ const clampToContainer = (
119
+ position: PreviewMiniPlayerPosition,
120
+ container: PreviewMiniPlayerSize,
121
+ player: PreviewMiniPlayerSize,
122
+ bottom = container.height
123
+ ): PreviewMiniPlayerPosition => ({
124
+ x: Math.min(
125
+ Math.max(position.x, PREVIEW_MINI_PLAYER_EDGE_GAP),
126
+ Math.max(
127
+ PREVIEW_MINI_PLAYER_EDGE_GAP,
128
+ container.width - player.width - PREVIEW_MINI_PLAYER_EDGE_GAP
129
+ )
130
+ ),
131
+ y: Math.min(
132
+ Math.max(position.y, PREVIEW_MINI_PLAYER_EDGE_GAP),
133
+ Math.max(PREVIEW_MINI_PLAYER_EDGE_GAP, bottom - player.height - PREVIEW_MINI_PLAYER_EDGE_GAP)
134
+ ),
135
+ })
136
+
137
+ const overlapsObstacle = (
138
+ position: PreviewMiniPlayerPosition,
139
+ player: PreviewMiniPlayerSize,
140
+ container: PreviewMiniPlayerSize,
141
+ obstacles: PreviewMiniPlayerObstacles
142
+ ): boolean =>
143
+ position.y + player.height > floorFor(spanOf(position.x, player.width), container, obstacles)
144
+
145
+ /**
146
+ * Keeps the player inside the container and off the composer. An overlapping
147
+ * player is pushed out along whichever side needs the smaller move, so a drag
148
+ * slides along the composer into the margin beside it instead of stopping at
149
+ * its top edge; when no side leaves it fully clear it sits above the composer.
150
+ */
151
+ export function clampPreviewMiniPlayerPosition(
152
+ position: PreviewMiniPlayerPosition,
153
+ container: PreviewMiniPlayerSize,
154
+ player: PreviewMiniPlayerSize,
155
+ obstacles: PreviewMiniPlayerObstacles = NO_PREVIEW_MINI_PLAYER_OBSTACLES
156
+ ): PreviewMiniPlayerPosition {
157
+ const inside = clampToContainer(position, container, player)
158
+ const { composer } = obstacles
159
+ if (!composer || !overlapsObstacle(inside, player, container, obstacles)) return inside
160
+ const gap = PREVIEW_MINI_PLAYER_EDGE_GAP
161
+ const above = { x: inside.x, y: container.height - composer.height - gap - player.height }
162
+ const beside = [
163
+ { x: composer.left - gap - player.width, y: inside.y },
164
+ { x: composer.right + gap, y: inside.y },
165
+ ]
166
+ let best = clampToContainer(above, container, player)
167
+ let bestDistance = Math.abs(best.y - inside.y)
168
+ for (const candidate of beside) {
169
+ const clamped = clampToContainer(candidate, container, player)
170
+ if (clamped.x !== candidate.x || overlapsObstacle(candidate, player, container, obstacles)) {
171
+ continue
172
+ }
173
+ const distance = Math.abs(candidate.x - inside.x)
174
+ if (distance < bestDistance) {
175
+ best = candidate
176
+ bestDistance = distance
177
+ }
178
+ }
179
+ return best
180
+ }
181
+
182
+ /**
183
+ * Resolves the on-screen frame from the stored width and position. Clamping
184
+ * happens here on every layout pass instead of being written back to the
185
+ * store, so a temporarily narrow container never destroys the user's chosen
186
+ * width. A player without a position sits in the top-right corner.
187
+ */
188
+ export function resolvePreviewMiniPlayerFrame(input: {
189
+ readonly width: number | null
190
+ readonly position: PreviewMiniPlayerPosition | null
191
+ readonly source: PreviewMiniPlayerSize
192
+ readonly container: PreviewMiniPlayerSize
193
+ readonly obstacles?: PreviewMiniPlayerObstacles
194
+ }): PreviewMiniPlayerFrame {
195
+ const { width, position, source, container, obstacles = NO_PREVIEW_MINI_PLAYER_OBSTACLES } = input
196
+ const size = fitPreviewMiniPlayerWidth(
197
+ width ?? defaultPreviewMiniPlayerWidth(source),
198
+ source,
199
+ availableArea(container, obstacles, position && width ? spanOf(position.x, width) : null)
200
+ )
201
+ const anchored = position ?? {
202
+ x: container.width - PREVIEW_MINI_PLAYER_EDGE_GAP - size.width,
203
+ y: PREVIEW_MINI_PLAYER_EDGE_GAP,
204
+ }
205
+ return { ...clampPreviewMiniPlayerPosition(anchored, container, size, obstacles), ...size }
206
+ }
207
+
208
+ /**
209
+ * Resizes from any edge or corner while holding the aspect ratio. The edge
210
+ * opposite the dragged one stays anchored, so growth stops at the container
211
+ * on that axis and the pointer keeps tracking the grabbed edge.
212
+ */
213
+ export function resizePreviewMiniPlayer(input: {
214
+ readonly start: PreviewMiniPlayerFrame
215
+ readonly direction: BrowserViewportResizeDirection
216
+ readonly delta: PreviewMiniPlayerPosition
217
+ readonly source: PreviewMiniPlayerSize
218
+ readonly container: PreviewMiniPlayerSize
219
+ readonly obstacles?: PreviewMiniPlayerObstacles
220
+ }): PreviewMiniPlayerFrame {
221
+ const {
222
+ start,
223
+ direction,
224
+ delta,
225
+ source,
226
+ container,
227
+ obstacles = NO_PREVIEW_MINI_PLAYER_OBSTACLES,
228
+ } = input
229
+ const east = direction.includes('east')
230
+ const west = direction.includes('west')
231
+ const north = direction.includes('north')
232
+ const south = direction.includes('south')
233
+ const right = start.x + start.width
234
+ const bottom = start.y + start.height
235
+ const floor = floorFor(spanOf(start.x, start.width), container, obstacles)
236
+ const max = {
237
+ width: west
238
+ ? right - PREVIEW_MINI_PLAYER_EDGE_GAP
239
+ : east
240
+ ? container.width - PREVIEW_MINI_PLAYER_EDGE_GAP - start.x
241
+ : container.width - PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
242
+ height: north
243
+ ? bottom - PREVIEW_MINI_PLAYER_EDGE_GAP
244
+ : south
245
+ ? floor - PREVIEW_MINI_PLAYER_EDGE_GAP - start.y
246
+ : floor - PREVIEW_MINI_PLAYER_EDGE_GAP * 2,
247
+ }
248
+ const desiredWidth = start.width + (east ? delta.x : west ? -delta.x : 0)
249
+ const desiredHeight = start.height + (south ? delta.y : north ? -delta.y : 0)
250
+ const horizontal = east || west
251
+ const vertical = north || south
252
+ const widthLeads =
253
+ horizontal && !vertical
254
+ ? true
255
+ : vertical && !horizontal
256
+ ? false
257
+ : Math.abs(desiredWidth - start.width) / start.width >=
258
+ Math.abs(desiredHeight - start.height) / start.height
259
+ const size = fitPreviewMiniPlayerWidth(
260
+ widthLeads ? desiredWidth : (desiredHeight * source.width) / source.height,
261
+ source,
262
+ max
263
+ )
264
+ const position = clampPreviewMiniPlayerPosition(
265
+ { x: west ? right - size.width : start.x, y: north ? bottom - size.height : start.y },
266
+ container,
267
+ size,
268
+ obstacles
269
+ )
270
+ return { ...position, ...size }
271
+ }
272
+
273
+ // Product-neutral names for new consumers. The PreviewMiniPlayer names stay
274
+ // exported for Adea Dev Runtime's existing browser model API.
275
+ export type FloatingPreviewSize = PreviewMiniPlayerSize
276
+ export type FloatingPreviewPosition = PreviewMiniPlayerPosition
277
+ export type FloatingPreviewFrame = PreviewMiniPlayerFrame
278
+ export type FloatingPreviewResizeDirection = BrowserViewportResizeDirection
279
+ export type FloatingPreviewObstacles = PreviewMiniPlayerObstacles
280
+ export const FLOATING_PREVIEW_EDGE_GAP = PREVIEW_MINI_PLAYER_EDGE_GAP
281
+ export const FLOATING_PREVIEW_CORNER_RADIUS = PREVIEW_MINI_PLAYER_CORNER_RADIUS
282
+ export const NO_FLOATING_PREVIEW_OBSTACLES = NO_PREVIEW_MINI_PLAYER_OBSTACLES
283
+ export const resolveFloatingPreviewFrame = resolvePreviewMiniPlayerFrame
284
+ export const resizeFloatingPreview = resizePreviewMiniPlayer
285
+ export const clampFloatingPreviewPosition = clampPreviewMiniPlayerPosition
@@ -20,3 +20,4 @@ export {
20
20
  type ThemeSelection,
21
21
  } from './theme-provider'
22
22
  export { ThemeToggle, type ThemeToggleProps } from './theme-toggle'
23
+ export { ThemeModeToggle, type ThemeModeToggleProps } from './theme-mode-toggle'
@@ -0,0 +1,56 @@
1
+ import { Monitor, Moon, Sun } from 'lucide-solid'
2
+ import type { ComponentProps } from 'solid-js'
3
+ import { For, splitProps } from 'solid-js'
4
+ import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group/toggle-group'
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
6
+
7
+ const APPEARANCES = [
8
+ { value: 'light', label: 'Light', icon: Sun },
9
+ { value: 'dark', label: 'Dark', icon: Moon },
10
+ { value: 'system', label: 'System', icon: Monitor },
11
+ ] as const
12
+
13
+ /** Controlled appearance selection for hosts that own their preference storage. */
14
+ export type ThemeModeToggleProps = Omit<
15
+ ComponentProps<typeof ToggleGroup>,
16
+ 'children' | 'value' | 'onChange' | 'multiple'
17
+ > & {
18
+ mode: 'light' | 'dark' | 'system'
19
+ onModeChange: (mode: 'light' | 'dark' | 'system') => void
20
+ withLabels?: boolean
21
+ }
22
+
23
+ export function ThemeModeToggle(props: ThemeModeToggleProps) {
24
+ const [local, rest] = splitProps(props, ['mode', 'onModeChange', 'withLabels'])
25
+ return (
26
+ <ToggleGroup
27
+ {...rest}
28
+ attached
29
+ multiple={false}
30
+ value={local.mode}
31
+ onChange={(value: string | null) => {
32
+ const option = APPEARANCES.find((candidate) => candidate.value === value)
33
+ if (option) local.onModeChange(option.value)
34
+ }}
35
+ aria-label={rest['aria-label'] ?? 'Appearance'}
36
+ >
37
+ <For each={APPEARANCES}>
38
+ {(appearance) => (
39
+ <Tooltip>
40
+ <TooltipTrigger
41
+ as={ToggleGroupItem}
42
+ value={appearance.value}
43
+ variant="outline"
44
+ size={local.withLabels ? 'sm' : 'icon-sm'}
45
+ aria-label={appearance.label}
46
+ >
47
+ <appearance.icon aria-hidden="true" />
48
+ {local.withLabels ? <span>{appearance.label}</span> : null}
49
+ </TooltipTrigger>
50
+ <TooltipContent>Use {appearance.label.toLowerCase()} appearance</TooltipContent>
51
+ </Tooltip>
52
+ )}
53
+ </For>
54
+ </ToggleGroup>
55
+ )
56
+ }
@@ -2,6 +2,7 @@ import { Checkbox as KobalteCheckbox } from '@kobalte/core/checkbox'
2
2
  import { Check, Minus } from 'lucide-solid'
3
3
  import type { ComponentProps, JSX } from 'solid-js'
4
4
  import { splitProps } from 'solid-js'
5
+ import { useFormFieldControl } from '#lib/form-field'
5
6
  import { cn } from '#lib/utils'
6
7
 
7
8
  /**
@@ -18,6 +19,8 @@ import { cn } from '#lib/utils'
18
19
  * the ring lives on the control and reads the input's focus state.
19
20
  */
20
21
  export type CheckboxProps = Omit<ComponentProps<typeof KobalteCheckbox>, 'children'> & {
22
+ /** ID for the hidden native checkbox input; useful with an external label. */
23
+ inputId?: string
21
24
  controlClass?: string
22
25
  labelClass?: string
23
26
  descriptionClass?: string
@@ -44,12 +47,24 @@ export function Checkbox(props: CheckboxProps) {
44
47
  'aria-labelledby',
45
48
  'aria-describedby',
46
49
  'aria-invalid',
50
+ 'aria-errormessage',
47
51
  'aria-busy',
52
+ 'id',
53
+ 'inputId',
48
54
  'title',
49
55
  ])
56
+ const field = useFormFieldControl('control', {
57
+ id: local.inputId ?? 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
+ })
50
64
 
51
65
  return (
52
66
  <KobalteCheckbox
67
+ id={field ? undefined : local.id}
53
68
  title={local.title}
54
69
  class={cn('group/checkbox flex items-start gap-2.5', local.class)}
55
70
  {...rest}
@@ -60,10 +75,12 @@ export function Checkbox(props: CheckboxProps) {
60
75
  * box would announce its state with no subject.
61
76
  */}
62
77
  <KobalteCheckbox.Input
78
+ id={field?.id ?? local.inputId}
63
79
  aria-label={local['aria-label']}
64
- aria-labelledby={local['aria-labelledby']}
65
- aria-describedby={local['aria-describedby']}
66
- aria-invalid={local['aria-invalid']}
80
+ aria-labelledby={field?.['aria-labelledby'] ?? local['aria-labelledby']}
81
+ aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
82
+ aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
83
+ aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
67
84
  aria-busy={local['aria-busy']}
68
85
  title={local.title}
69
86
  />