@adea-ai/ui 0.85.0 → 0.88.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 (239) hide show
  1. package/README.md +17 -1
  2. package/dist/NOTICE +34 -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/list-row/list-row.d.ts +6 -3
  7. package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
  8. package/dist/components/composites/list-row/list-row.js +69 -7
  9. package/dist/components/composites/list-row/list-row.js.map +1 -1
  10. package/dist/components/composites/settings/settings.d.ts +9 -11
  11. package/dist/components/composites/settings/settings.d.ts.map +1 -1
  12. package/dist/components/composites/settings/settings.js +48 -33
  13. package/dist/components/composites/settings/settings.js.map +1 -1
  14. package/dist/components/conversation/message-composer.js +1 -1
  15. package/dist/components/conversation/scroll-follow.d.ts.map +1 -1
  16. package/dist/components/conversation/scroll-follow.js +95 -15
  17. package/dist/components/conversation/scroll-follow.js.map +1 -1
  18. package/dist/components/layout/app-shell/index.d.ts +1 -0
  19. package/dist/components/layout/app-shell/index.d.ts.map +1 -1
  20. package/dist/components/layout/app-shell/index.js +2 -1
  21. package/dist/components/layout/app-shell/skip-link.d.ts +11 -0
  22. package/dist/components/layout/app-shell/skip-link.d.ts.map +1 -0
  23. package/dist/components/layout/app-shell/skip-link.js +44 -0
  24. package/dist/components/layout/app-shell/skip-link.js.map +1 -0
  25. package/dist/components/layout/floating-preview/floating-preview.d.ts +12 -0
  26. package/dist/components/layout/floating-preview/floating-preview.d.ts.map +1 -0
  27. package/dist/components/layout/floating-preview/floating-preview.js +283 -0
  28. package/dist/components/layout/floating-preview/floating-preview.js.map +1 -0
  29. package/dist/components/layout/floating-preview/index.d.ts +3 -0
  30. package/dist/components/layout/floating-preview/index.d.ts.map +1 -0
  31. package/dist/components/layout/floating-preview/index.js +3 -0
  32. package/dist/components/layout/floating-preview/model.d.ts +74 -0
  33. package/dist/components/layout/floating-preview/model.d.ts.map +1 -0
  34. package/dist/components/layout/floating-preview/model.js +146 -0
  35. package/dist/components/layout/floating-preview/model.js.map +1 -0
  36. package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
  37. package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
  38. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +18 -2
  39. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
  40. package/dist/components/layout/sidebar-nav/sidebar-nav.js +42 -11
  41. package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
  42. package/dist/components/theme/index.d.ts +1 -0
  43. package/dist/components/theme/index.d.ts.map +1 -1
  44. package/dist/components/theme/index.js +2 -1
  45. package/dist/components/theme/theme-mode-toggle.d.ts +10 -0
  46. package/dist/components/theme/theme-mode-toggle.d.ts.map +1 -0
  47. package/dist/components/theme/theme-mode-toggle.js +82 -0
  48. package/dist/components/theme/theme-mode-toggle.js.map +1 -0
  49. package/dist/components/ui/checkbox/checkbox.d.ts +2 -0
  50. package/dist/components/ui/checkbox/checkbox.d.ts.map +1 -1
  51. package/dist/components/ui/checkbox/checkbox.js +24 -3
  52. package/dist/components/ui/checkbox/checkbox.js.map +1 -1
  53. package/dist/components/ui/combobox/combobox.d.ts.map +1 -1
  54. package/dist/components/ui/combobox/combobox.js +41 -4
  55. package/dist/components/ui/combobox/combobox.js.map +1 -1
  56. package/dist/components/ui/combobox/index.d.ts +1 -0
  57. package/dist/components/ui/combobox/index.d.ts.map +1 -1
  58. package/dist/components/ui/combobox/index.js +2 -1
  59. package/dist/components/ui/combobox/value-combobox.d.ts +19 -0
  60. package/dist/components/ui/combobox/value-combobox.d.ts.map +1 -0
  61. package/dist/components/ui/combobox/value-combobox.js +42 -0
  62. package/dist/components/ui/combobox/value-combobox.js.map +1 -0
  63. package/dist/components/ui/empty/empty-state.d.ts +15 -0
  64. package/dist/components/ui/empty/empty-state.d.ts.map +1 -0
  65. package/dist/components/ui/empty/empty-state.js +96 -0
  66. package/dist/components/ui/empty/empty-state.js.map +1 -0
  67. package/dist/components/ui/empty/index.d.ts +1 -0
  68. package/dist/components/ui/empty/index.d.ts.map +1 -1
  69. package/dist/components/ui/empty/index.js +2 -1
  70. package/dist/components/ui/entity-icon/entity-icon.d.ts +4 -1
  71. package/dist/components/ui/entity-icon/entity-icon.d.ts.map +1 -1
  72. package/dist/components/ui/entity-icon/entity-icon.js +37 -13
  73. package/dist/components/ui/entity-icon/entity-icon.js.map +1 -1
  74. package/dist/components/ui/field/field.d.ts +36 -13
  75. package/dist/components/ui/field/field.d.ts.map +1 -1
  76. package/dist/components/ui/field/field.js +146 -24
  77. package/dist/components/ui/field/field.js.map +1 -1
  78. package/dist/components/ui/field/index.d.ts +3 -1
  79. package/dist/components/ui/field/index.d.ts.map +1 -1
  80. package/dist/components/ui/field/index.js +3 -2
  81. package/dist/components/ui/field/number-field.d.ts +19 -0
  82. package/dist/components/ui/field/number-field.d.ts.map +1 -0
  83. package/dist/components/ui/field/number-field.js +76 -0
  84. package/dist/components/ui/field/number-field.js.map +1 -0
  85. package/dist/components/ui/input/index.d.ts +1 -0
  86. package/dist/components/ui/input/index.d.ts.map +1 -1
  87. package/dist/components/ui/input/index.js +2 -1
  88. package/dist/components/ui/input/input-control.d.ts +11 -0
  89. package/dist/components/ui/input/input-control.d.ts.map +1 -0
  90. package/dist/components/ui/input/input-control.js +24 -0
  91. package/dist/components/ui/input/input-control.js.map +1 -0
  92. package/dist/components/ui/input/input.d.ts.map +1 -1
  93. package/dist/components/ui/input/input.js +56 -28
  94. package/dist/components/ui/input/input.js.map +1 -1
  95. package/dist/components/ui/input-group/index.d.ts +1 -0
  96. package/dist/components/ui/input-group/index.d.ts.map +1 -1
  97. package/dist/components/ui/input-group/index.js +2 -1
  98. package/dist/components/ui/input-group/input-group.d.ts.map +1 -1
  99. package/dist/components/ui/input-group/input-group.js +42 -5
  100. package/dist/components/ui/input-group/input-group.js.map +1 -1
  101. package/dist/components/ui/input-group/secret-input-group.d.ts +11 -0
  102. package/dist/components/ui/input-group/secret-input-group.d.ts.map +1 -0
  103. package/dist/components/ui/input-group/secret-input-group.js +67 -0
  104. package/dist/components/ui/input-group/secret-input-group.js.map +1 -0
  105. package/dist/components/ui/native-select/native-select.d.ts.map +1 -1
  106. package/dist/components/ui/native-select/native-select.js +39 -4
  107. package/dist/components/ui/native-select/native-select.js.map +1 -1
  108. package/dist/components/ui/progress/progress.d.ts.map +1 -1
  109. package/dist/components/ui/progress/progress.js +30 -18
  110. package/dist/components/ui/progress/progress.js.map +1 -1
  111. package/dist/components/ui/radio-group/radio-group.d.ts.map +1 -1
  112. package/dist/components/ui/radio-group/radio-group.js +41 -4
  113. package/dist/components/ui/radio-group/radio-group.js.map +1 -1
  114. package/dist/components/ui/switch/switch.d.ts +2 -0
  115. package/dist/components/ui/switch/switch.d.ts.map +1 -1
  116. package/dist/components/ui/switch/switch.js +24 -3
  117. package/dist/components/ui/switch/switch.js.map +1 -1
  118. package/dist/components/ui/tabs/tabs.js +1 -1
  119. package/dist/components/ui/tabs/tabs.js.map +1 -1
  120. package/dist/components/ui/text-link/index.d.ts +2 -0
  121. package/dist/components/ui/text-link/index.d.ts.map +1 -0
  122. package/dist/components/ui/text-link/index.js +2 -0
  123. package/dist/components/ui/text-link/text-link.d.ts +9 -0
  124. package/dist/components/ui/text-link/text-link.d.ts.map +1 -0
  125. package/dist/components/ui/text-link/text-link.js +30 -0
  126. package/dist/components/ui/text-link/text-link.js.map +1 -0
  127. package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
  128. package/dist/components/ui/textarea/textarea.js +41 -4
  129. package/dist/components/ui/textarea/textarea.js.map +1 -1
  130. package/dist/index.d.ts +3 -0
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +18 -7
  133. package/dist/lib/clipboard.d.ts +3 -0
  134. package/dist/lib/clipboard.d.ts.map +1 -0
  135. package/dist/lib/clipboard.js +35 -0
  136. package/dist/lib/clipboard.js.map +1 -0
  137. package/dist/lib/download.d.ts +3 -0
  138. package/dist/lib/download.d.ts.map +1 -0
  139. package/dist/lib/download.js +31 -0
  140. package/dist/lib/download.js.map +1 -0
  141. package/dist/lib/form-field.d.ts +37 -0
  142. package/dist/lib/form-field.d.ts.map +1 -0
  143. package/dist/lib/form-field.js +47 -0
  144. package/dist/lib/form-field.js.map +1 -0
  145. package/dist/r/app-shell.json +5 -0
  146. package/dist/r/combobox.json +5 -0
  147. package/dist/r/empty.json +10 -1
  148. package/dist/r/field.json +7 -0
  149. package/dist/r/floating-preview.json +30 -0
  150. package/dist/r/input-group.json +6 -0
  151. package/dist/r/input.json +5 -0
  152. package/dist/r/lib.json +15 -0
  153. package/dist/r/list-row.json +1 -0
  154. package/dist/r/registry.json +112 -1
  155. package/dist/r/src/components/composites/action-button/action-button.tsx +5 -1
  156. package/dist/r/src/components/composites/list-row/list-row.tsx +81 -8
  157. package/dist/r/src/components/composites/settings/settings.tsx +67 -37
  158. package/dist/r/src/components/conversation/scroll-follow.ts +109 -15
  159. package/dist/r/src/components/layout/app-shell/index.ts +1 -0
  160. package/dist/r/src/components/layout/app-shell/skip-link.tsx +35 -0
  161. package/dist/r/src/components/layout/floating-preview/floating-preview.tsx +260 -0
  162. package/dist/r/src/components/layout/floating-preview/index.ts +2 -0
  163. package/dist/r/src/components/layout/floating-preview/model.ts +285 -0
  164. package/dist/r/src/components/layout/sidebar-nav/index.ts +2 -0
  165. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +71 -16
  166. package/dist/r/src/components/theme/index.ts +1 -0
  167. package/dist/r/src/components/theme/theme-mode-toggle.tsx +56 -0
  168. package/dist/r/src/components/ui/checkbox/checkbox.tsx +20 -3
  169. package/dist/r/src/components/ui/combobox/combobox.tsx +24 -1
  170. package/dist/r/src/components/ui/combobox/index.ts +1 -0
  171. package/dist/r/src/components/ui/combobox/value-combobox.tsx +38 -0
  172. package/dist/r/src/components/ui/empty/empty-state.tsx +71 -0
  173. package/dist/r/src/components/ui/empty/index.ts +1 -0
  174. package/dist/r/src/components/ui/entity-icon/entity-icon.tsx +33 -2
  175. package/dist/r/src/components/ui/field/field.tsx +102 -5
  176. package/dist/r/src/components/ui/field/index.ts +5 -0
  177. package/dist/r/src/components/ui/field/number-field.tsx +71 -0
  178. package/dist/r/src/components/ui/input/index.ts +1 -0
  179. package/dist/r/src/components/ui/input/input-control.tsx +38 -0
  180. package/dist/r/src/components/ui/input/input.tsx +32 -20
  181. package/dist/r/src/components/ui/input-group/index.ts +1 -0
  182. package/dist/r/src/components/ui/input-group/input-group.tsx +25 -2
  183. package/dist/r/src/components/ui/input-group/secret-input-group.tsx +58 -0
  184. package/dist/r/src/components/ui/native-select/native-select.tsx +27 -1
  185. package/dist/r/src/components/ui/progress/progress.tsx +12 -8
  186. package/dist/r/src/components/ui/radio-group/radio-group.tsx +30 -2
  187. package/dist/r/src/components/ui/switch/switch.tsx +20 -3
  188. package/dist/r/src/components/ui/tabs/tabs.tsx +1 -1
  189. package/dist/r/src/components/ui/text-link/index.ts +2 -0
  190. package/dist/r/src/components/ui/text-link/text-link.tsx +28 -0
  191. package/dist/r/src/components/ui/textarea/textarea.tsx +24 -1
  192. package/dist/r/src/lib/clipboard.ts +41 -0
  193. package/dist/r/src/lib/download.ts +29 -0
  194. package/dist/r/src/lib/form-field.ts +93 -0
  195. package/dist/r/text-link.json +22 -0
  196. package/dist/r/theming.json +6 -0
  197. package/package.json +1 -1
  198. package/registry.json +112 -1
  199. package/src/components/composites/action-button/action-button.tsx +5 -1
  200. package/src/components/composites/list-row/list-row.tsx +81 -8
  201. package/src/components/composites/settings/settings.tsx +67 -37
  202. package/src/components/conversation/scroll-follow.ts +109 -15
  203. package/src/components/layout/app-shell/index.ts +1 -0
  204. package/src/components/layout/app-shell/skip-link.tsx +35 -0
  205. package/src/components/layout/floating-preview/floating-preview.tsx +260 -0
  206. package/src/components/layout/floating-preview/index.ts +2 -0
  207. package/src/components/layout/floating-preview/model.ts +285 -0
  208. package/src/components/layout/sidebar-nav/index.ts +2 -0
  209. package/src/components/layout/sidebar-nav/sidebar-nav.tsx +71 -16
  210. package/src/components/theme/index.ts +1 -0
  211. package/src/components/theme/theme-mode-toggle.tsx +56 -0
  212. package/src/components/ui/checkbox/checkbox.tsx +20 -3
  213. package/src/components/ui/combobox/combobox.tsx +24 -1
  214. package/src/components/ui/combobox/index.ts +1 -0
  215. package/src/components/ui/combobox/value-combobox.tsx +38 -0
  216. package/src/components/ui/empty/empty-state.tsx +71 -0
  217. package/src/components/ui/empty/index.ts +1 -0
  218. package/src/components/ui/entity-icon/entity-icon.tsx +33 -2
  219. package/src/components/ui/field/field.tsx +102 -5
  220. package/src/components/ui/field/index.ts +5 -0
  221. package/src/components/ui/field/number-field.tsx +71 -0
  222. package/src/components/ui/input/index.ts +1 -0
  223. package/src/components/ui/input/input-control.tsx +38 -0
  224. package/src/components/ui/input/input.tsx +32 -20
  225. package/src/components/ui/input-group/index.ts +1 -0
  226. package/src/components/ui/input-group/input-group.tsx +25 -2
  227. package/src/components/ui/input-group/secret-input-group.tsx +58 -0
  228. package/src/components/ui/native-select/native-select.tsx +27 -1
  229. package/src/components/ui/progress/progress.tsx +12 -8
  230. package/src/components/ui/radio-group/radio-group.tsx +30 -2
  231. package/src/components/ui/switch/switch.tsx +20 -3
  232. package/src/components/ui/tabs/tabs.tsx +1 -1
  233. package/src/components/ui/text-link/index.ts +2 -0
  234. package/src/components/ui/text-link/text-link.tsx +28 -0
  235. package/src/components/ui/textarea/textarea.tsx +24 -1
  236. package/src/index.ts +3 -0
  237. package/src/lib/clipboard.ts +41 -0
  238. package/src/lib/download.ts +29 -0
  239. 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
@@ -11,6 +11,8 @@ export {
11
11
  sidebarNavItemStateClass,
12
12
  type SidebarNavItemProps,
13
13
  type SidebarNavProps,
14
+ type SidebarNavSectionProps,
15
+ type SidebarNavSectionTriggerProps,
14
16
  type SidebarNavTitleProps,
15
17
  } from './sidebar-nav'
16
18
  export {
@@ -5,6 +5,15 @@ import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
5
5
  import { Show, createSignal, splitProps } from 'solid-js'
6
6
  import { cn } from '#lib/utils'
7
7
 
8
+ function invokeEventHandler(handler: unknown, event: Event) {
9
+ if (Array.isArray(handler)) {
10
+ const [callback, data] = handler as [(data: unknown, event: Event) => void, unknown]
11
+ callback(data, event)
12
+ } else if (typeof handler === 'function') {
13
+ ;(handler as (event: Event) => void)(event)
14
+ }
15
+ }
16
+
8
17
  /**
9
18
  * SidebarNav.
10
19
  *
@@ -96,36 +105,69 @@ export function SidebarNavFooter(props: ComponentProps<'div'>) {
96
105
  )
97
106
  }
98
107
 
108
+ /** Props forwarded to a collapsible section's disclosure button. */
109
+ export type SidebarNavSectionTriggerProps = Omit<
110
+ ComponentProps<'button'>,
111
+ 'type' | 'aria-expanded' | 'onClick' | 'onKeyDown' | 'children' | 'class' | 'style'
112
+ > & {
113
+ /** Called for Alt+ArrowUp/Down; the host owns ordering and row identity. */
114
+ onReorder?: (direction: 'up' | 'down') => void
115
+ /** Runs before the section toggle and may cancel it with `preventDefault()`. */
116
+ onClick?: ComponentProps<'button'>['onClick']
117
+ /** Runs before shared Alt+Arrow handling and may cancel it with `preventDefault()`. */
118
+ onKeyDown?: ComponentProps<'button'>['onKeyDown']
119
+ }
120
+
99
121
  /**
100
122
  * A titled, collapsible group. Collapsible because a list of projects
101
123
  * inevitably outgrows the column, and the user needs to put the ones they are
102
124
  * not using away *without* them disappearing into a menu.
103
125
  */
104
- export function SidebarNavSection(
105
- props: ComponentProps<'div'> & {
106
- label: string
107
- /** Render a disclosure control instead of a plain heading. */
108
- collapsible?: boolean
109
- defaultOpen?: boolean
110
- /** Count shown beside the label. */
111
- count?: number
112
- /** A control at the trailing edge of the heading row. */
113
- action?: JSX.Element
114
- /** Semantic heading element for a host's section hierarchy. */
115
- headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
116
- }
117
- ) {
126
+ export type SidebarNavSectionProps = ComponentProps<'div'> & {
127
+ label: string
128
+ /** Render a disclosure control instead of a plain heading. */
129
+ collapsible?: boolean
130
+ defaultOpen?: boolean
131
+ /** Controlled disclosure state. When supplied, the host owns changes. */
132
+ open?: boolean
133
+ /** Called for user-initiated disclosure changes. */
134
+ onOpenChange?: (open: boolean) => void
135
+ /** Native props and reorder callbacks for the disclosure control. */
136
+ triggerProps?: SidebarNavSectionTriggerProps
137
+ /** Count shown beside the label. */
138
+ count?: number
139
+ /** A control at the trailing edge of the heading row. */
140
+ action?: JSX.Element
141
+ /** Semantic heading element for a host's section hierarchy. */
142
+ headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
143
+ }
144
+
145
+ export function SidebarNavSection(props: SidebarNavSectionProps) {
118
146
  const [local, rest] = splitProps(props, [
119
147
  'class',
120
148
  'label',
121
149
  'collapsible',
122
150
  'defaultOpen',
151
+ 'open',
152
+ 'onOpenChange',
153
+ 'triggerProps',
123
154
  'count',
124
155
  'action',
125
156
  'headingAs',
126
157
  'children',
127
158
  ])
128
- const [open, setOpen] = createSignal(local.defaultOpen ?? true)
159
+ const [uncontrolledOpen, setUncontrolledOpen] = createSignal(local.defaultOpen ?? true)
160
+ const open = () => local.open ?? uncontrolledOpen()
161
+ const setOpen = (next: boolean) => {
162
+ if (local.open === undefined) setUncontrolledOpen(next)
163
+ local.onOpenChange?.(next)
164
+ }
165
+
166
+ const [trigger, triggerRest] = splitProps(local.triggerProps ?? {}, [
167
+ 'onClick',
168
+ 'onKeyDown',
169
+ 'onReorder',
170
+ ])
129
171
 
130
172
  const heading = (
131
173
  <>
@@ -154,9 +196,22 @@ export function SidebarNavSection(
154
196
  >
155
197
  <Show when={local.collapsible} fallback={heading}>
156
198
  <button
199
+ {...(triggerRest as ComponentProps<'button'>)}
157
200
  type="button"
158
201
  aria-expanded={open()}
159
- onClick={() => setOpen((value) => !value)}
202
+ onClick={(event) => {
203
+ invokeEventHandler(trigger.onClick, event)
204
+ if (event.defaultPrevented) return
205
+ setOpen(!open())
206
+ }}
207
+ onKeyDown={(event) => {
208
+ invokeEventHandler(trigger.onKeyDown, event)
209
+ if (event.defaultPrevented || !event.altKey) return
210
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
211
+ event.preventDefault()
212
+ event.stopPropagation()
213
+ trigger.onReorder?.(event.key === 'ArrowUp' ? 'up' : 'down')
214
+ }}
160
215
  class="flex min-w-0 flex-1 items-center gap-1.5 rounded-sm text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase outline-none transition-colors ease-out hover:text-sidebar-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle"
161
216
  >
162
217
  {heading}
@@ -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
+ }