@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,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
  />
@@ -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>