@cueplusplus/ui 0.1.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 (372) hide show
  1. package/dist/charts/_token.js +39 -0
  2. package/dist/charts/container.d.ts +58 -0
  3. package/dist/charts/container.js +77 -0
  4. package/dist/charts/index.d.ts +5 -0
  5. package/dist/charts/index.js +5 -0
  6. package/dist/charts/ramps.d.ts +140 -0
  7. package/dist/charts/ramps.js +615 -0
  8. package/dist/charts/theme.d.ts +112 -0
  9. package/dist/charts/theme.js +165 -0
  10. package/dist/charts/tones.d.ts +111 -0
  11. package/dist/charts/tones.js +127 -0
  12. package/dist/chat/_glyphs.js +55 -0
  13. package/dist/chat/agent-color.d.ts +84 -0
  14. package/dist/chat/agent-color.js +101 -0
  15. package/dist/chat/agent-pile.d.ts +55 -0
  16. package/dist/chat/agent-pile.js +121 -0
  17. package/dist/chat/ask-box.d.ts +88 -0
  18. package/dist/chat/ask-box.js +341 -0
  19. package/dist/chat/composer.d.ts +66 -0
  20. package/dist/chat/composer.js +144 -0
  21. package/dist/chat/delegation-card.d.ts +60 -0
  22. package/dist/chat/delegation-card.js +129 -0
  23. package/dist/chat/index.d.ts +8 -0
  24. package/dist/chat/index.js +8 -0
  25. package/dist/chat/message-list.d.ts +39 -0
  26. package/dist/chat/message-list.js +105 -0
  27. package/dist/chat/message.d.ts +47 -0
  28. package/dist/chat/message.js +108 -0
  29. package/dist/chrome/_glyphs.js +101 -0
  30. package/dist/chrome/_tabs-scroll.d.ts +62 -0
  31. package/dist/chrome/_tabs-scroll.js +68 -0
  32. package/dist/chrome/_tree.d.ts +75 -0
  33. package/dist/chrome/_tree.js +49 -0
  34. package/dist/chrome/empty-state.d.ts +37 -0
  35. package/dist/chrome/empty-state.js +57 -0
  36. package/dist/chrome/eyebrow.d.ts +29 -0
  37. package/dist/chrome/eyebrow.js +36 -0
  38. package/dist/chrome/index.d.ts +17 -0
  39. package/dist/chrome/index.js +17 -0
  40. package/dist/chrome/navigation-menu.d.ts +80 -0
  41. package/dist/chrome/navigation-menu.js +121 -0
  42. package/dist/chrome/page-shell.d.ts +38 -0
  43. package/dist/chrome/page-shell.js +66 -0
  44. package/dist/chrome/panel.d.ts +55 -0
  45. package/dist/chrome/panel.js +67 -0
  46. package/dist/chrome/row.d.ts +82 -0
  47. package/dist/chrome/row.js +106 -0
  48. package/dist/chrome/scroll-area.d.ts +39 -0
  49. package/dist/chrome/scroll-area.js +70 -0
  50. package/dist/chrome/section-header.d.ts +42 -0
  51. package/dist/chrome/section-header.js +79 -0
  52. package/dist/chrome/segmented-control.d.ts +58 -0
  53. package/dist/chrome/segmented-control.js +76 -0
  54. package/dist/chrome/status-bar.d.ts +46 -0
  55. package/dist/chrome/status-bar.js +56 -0
  56. package/dist/chrome/tabs.d.ts +80 -0
  57. package/dist/chrome/tabs.js +176 -0
  58. package/dist/chrome/title-bar.d.ts +37 -0
  59. package/dist/chrome/title-bar.js +45 -0
  60. package/dist/chrome/toolbar.d.ts +46 -0
  61. package/dist/chrome/toolbar.js +62 -0
  62. package/dist/chrome/tree.d.ts +98 -0
  63. package/dist/chrome/tree.js +248 -0
  64. package/dist/color/_checker.js +14 -0
  65. package/dist/color/_convert.d.ts +52 -0
  66. package/dist/color/_convert.js +66 -0
  67. package/dist/color/_glyphs.js +28 -0
  68. package/dist/color/_thumb.js +15 -0
  69. package/dist/color/_value.js +28 -0
  70. package/dist/color/color-area.d.ts +27 -0
  71. package/dist/color/color-area.js +40 -0
  72. package/dist/color/color-field.d.ts +32 -0
  73. package/dist/color/color-field.js +43 -0
  74. package/dist/color/color-picker.d.ts +71 -0
  75. package/dist/color/color-picker.js +178 -0
  76. package/dist/color/color-slider.d.ts +31 -0
  77. package/dist/color/color-slider.js +51 -0
  78. package/dist/color/color-swatch.d.ts +32 -0
  79. package/dist/color/color-swatch.js +44 -0
  80. package/dist/color/index.d.ts +7 -0
  81. package/dist/color/index.js +7 -0
  82. package/dist/configurator/_export.d.ts +78 -0
  83. package/dist/configurator/_export.js +261 -0
  84. package/dist/configurator/_glyphs.js +92 -0
  85. package/dist/configurator/_overrides.d.ts +247 -0
  86. package/dist/configurator/_overrides.js +514 -0
  87. package/dist/configurator/configurator.d.ts +71 -0
  88. package/dist/configurator/configurator.js +279 -0
  89. package/dist/configurator/export-dialog.d.ts +30 -0
  90. package/dist/configurator/export-dialog.js +121 -0
  91. package/dist/configurator/index.d.ts +8 -0
  92. package/dist/configurator/index.js +8 -0
  93. package/dist/configurator/panel-sections.d.ts +86 -0
  94. package/dist/configurator/panel-sections.js +316 -0
  95. package/dist/configurator/token-editor.d.ts +74 -0
  96. package/dist/configurator/token-editor.js +149 -0
  97. package/dist/configurator/use-zoom.d.ts +87 -0
  98. package/dist/configurator/use-zoom.js +113 -0
  99. package/dist/date/_glyphs.js +34 -0
  100. package/dist/date/_segments.js +55 -0
  101. package/dist/date/_trigger.js +31 -0
  102. package/dist/date/calendar.d.ts +25 -0
  103. package/dist/date/calendar.js +125 -0
  104. package/dist/date/date-field.d.ts +34 -0
  105. package/dist/date/date-field.js +41 -0
  106. package/dist/date/date-picker.d.ts +69 -0
  107. package/dist/date/date-picker.js +89 -0
  108. package/dist/date/date-range-picker.d.ts +65 -0
  109. package/dist/date/date-range-picker.js +92 -0
  110. package/dist/date/index.d.ts +6 -0
  111. package/dist/date/index.js +6 -0
  112. package/dist/date/time-field.d.ts +29 -0
  113. package/dist/date/time-field.js +36 -0
  114. package/dist/dmx/_ramp.d.ts +77 -0
  115. package/dist/dmx/_ramp.js +169 -0
  116. package/dist/dmx/_readout.js +53 -0
  117. package/dist/dmx/_universe.d.ts +105 -0
  118. package/dist/dmx/_universe.js +112 -0
  119. package/dist/dmx/channel-matrix.d.ts +81 -0
  120. package/dist/dmx/channel-matrix.js +285 -0
  121. package/dist/dmx/dmx-bar.d.ts +50 -0
  122. package/dist/dmx/dmx-bar.js +107 -0
  123. package/dist/dmx/dmx-strip.d.ts +41 -0
  124. package/dist/dmx/dmx-strip.js +196 -0
  125. package/dist/dmx/index.d.ts +7 -0
  126. package/dist/dmx/index.js +7 -0
  127. package/dist/dmx/universe-grid.d.ts +61 -0
  128. package/dist/dmx/universe-grid.js +182 -0
  129. package/dist/flow/_frame.d.ts +52 -0
  130. package/dist/flow/_frame.js +38 -0
  131. package/dist/flow/controls.d.ts +41 -0
  132. package/dist/flow/controls.js +53 -0
  133. package/dist/flow/flow.css +121 -0
  134. package/dist/flow/handles.d.ts +40 -0
  135. package/dist/flow/handles.js +61 -0
  136. package/dist/flow/index.d.ts +7 -0
  137. package/dist/flow/index.js +7 -0
  138. package/dist/flow/node-card.d.ts +71 -0
  139. package/dist/flow/node-card.js +102 -0
  140. package/dist/flow/signal-edge.d.ts +60 -0
  141. package/dist/flow/signal-edge.js +108 -0
  142. package/dist/flow/use-flow-theme.d.ts +69 -0
  143. package/dist/flow/use-flow-theme.js +109 -0
  144. package/dist/forms/_chassis.d.ts +64 -0
  145. package/dist/forms/_chassis.js +59 -0
  146. package/dist/forms/_glyphs.js +143 -0
  147. package/dist/forms/_listbox.js +55 -0
  148. package/dist/forms/autocomplete.d.ts +61 -0
  149. package/dist/forms/autocomplete.js +88 -0
  150. package/dist/forms/button-group.d.ts +47 -0
  151. package/dist/forms/button-group.js +52 -0
  152. package/dist/forms/checkbox.d.ts +51 -0
  153. package/dist/forms/checkbox.js +98 -0
  154. package/dist/forms/combobox.d.ts +73 -0
  155. package/dist/forms/combobox.js +108 -0
  156. package/dist/forms/env-var-input.d.ts +74 -0
  157. package/dist/forms/env-var-input.js +116 -0
  158. package/dist/forms/field.d.ts +89 -0
  159. package/dist/forms/field.js +102 -0
  160. package/dist/forms/file-upload.d.ts +113 -0
  161. package/dist/forms/file-upload.js +232 -0
  162. package/dist/forms/index.d.ts +26 -0
  163. package/dist/forms/index.js +26 -0
  164. package/dist/forms/input-group.d.ts +31 -0
  165. package/dist/forms/input-group.js +54 -0
  166. package/dist/forms/input.d.ts +33 -0
  167. package/dist/forms/input.js +36 -0
  168. package/dist/forms/multi-select.d.ts +62 -0
  169. package/dist/forms/multi-select.js +107 -0
  170. package/dist/forms/number-field.d.ts +38 -0
  171. package/dist/forms/number-field.js +64 -0
  172. package/dist/forms/otp-field.d.ts +54 -0
  173. package/dist/forms/otp-field.js +59 -0
  174. package/dist/forms/password-input.d.ts +46 -0
  175. package/dist/forms/password-input.js +62 -0
  176. package/dist/forms/radio-group.d.ts +50 -0
  177. package/dist/forms/radio-group.js +64 -0
  178. package/dist/forms/rating.d.ts +69 -0
  179. package/dist/forms/rating.js +103 -0
  180. package/dist/forms/scrub-input.d.ts +81 -0
  181. package/dist/forms/scrub-input.js +249 -0
  182. package/dist/forms/search-input.d.ts +54 -0
  183. package/dist/forms/search-input.js +112 -0
  184. package/dist/forms/select.d.ts +75 -0
  185. package/dist/forms/select.js +91 -0
  186. package/dist/forms/slider.d.ts +44 -0
  187. package/dist/forms/slider.js +65 -0
  188. package/dist/forms/switch.d.ts +36 -0
  189. package/dist/forms/switch.js +71 -0
  190. package/dist/forms/tags-input.d.ts +69 -0
  191. package/dist/forms/tags-input.js +148 -0
  192. package/dist/forms/textarea.d.ts +27 -0
  193. package/dist/forms/textarea.js +37 -0
  194. package/dist/forms/toggle-group.d.ts +41 -0
  195. package/dist/forms/toggle-group.js +56 -0
  196. package/dist/forms/toggle.d.ts +41 -0
  197. package/dist/forms/toggle.js +100 -0
  198. package/dist/index.d.ts +112 -0
  199. package/dist/index.js +104 -0
  200. package/dist/instruments/_ansi.js +116 -0
  201. package/dist/instruments/_glyphs.js +90 -0
  202. package/dist/instruments/_motion.js +56 -0
  203. package/dist/instruments/_rem.js +21 -0
  204. package/dist/instruments/app-window-frame.d.ts +48 -0
  205. package/dist/instruments/app-window-frame.js +76 -0
  206. package/dist/instruments/copy-button.d.ts +41 -0
  207. package/dist/instruments/copy-button.js +112 -0
  208. package/dist/instruments/data-table.d.ts +168 -0
  209. package/dist/instruments/data-table.js +212 -0
  210. package/dist/instruments/group-bar.d.ts +63 -0
  211. package/dist/instruments/group-bar.js +122 -0
  212. package/dist/instruments/index.d.ts +13 -0
  213. package/dist/instruments/index.js +13 -0
  214. package/dist/instruments/log-viewer.d.ts +74 -0
  215. package/dist/instruments/log-viewer.js +215 -0
  216. package/dist/instruments/meter.d.ts +77 -0
  217. package/dist/instruments/meter.js +96 -0
  218. package/dist/instruments/progress.d.ts +45 -0
  219. package/dist/instruments/progress.js +61 -0
  220. package/dist/instruments/seam-grid.d.ts +66 -0
  221. package/dist/instruments/seam-grid.js +78 -0
  222. package/dist/instruments/sparkline.d.ts +39 -0
  223. package/dist/instruments/sparkline.js +85 -0
  224. package/dist/instruments/stat.d.ts +35 -0
  225. package/dist/instruments/stat.js +59 -0
  226. package/dist/instruments/table.d.ts +51 -0
  227. package/dist/instruments/table.js +92 -0
  228. package/dist/instruments/terminal-frame.d.ts +89 -0
  229. package/dist/instruments/terminal-frame.js +256 -0
  230. package/dist/instruments/two-step-button.d.ts +47 -0
  231. package/dist/instruments/two-step-button.js +147 -0
  232. package/dist/instruments/usage-chart.d.ts +55 -0
  233. package/dist/instruments/usage-chart.js +235 -0
  234. package/dist/layout/_collapse.js +46 -0
  235. package/dist/layout/_glyphs.js +45 -0
  236. package/dist/layout/accordion.d.ts +52 -0
  237. package/dist/layout/accordion.js +81 -0
  238. package/dist/layout/breadcrumb.d.ts +35 -0
  239. package/dist/layout/breadcrumb.js +67 -0
  240. package/dist/layout/card.d.ts +51 -0
  241. package/dist/layout/card.js +76 -0
  242. package/dist/layout/carousel.d.ts +96 -0
  243. package/dist/layout/carousel.js +211 -0
  244. package/dist/layout/collapsible.d.ts +74 -0
  245. package/dist/layout/collapsible.js +126 -0
  246. package/dist/layout/container.d.ts +27 -0
  247. package/dist/layout/container.js +40 -0
  248. package/dist/layout/description-list.d.ts +24 -0
  249. package/dist/layout/description-list.js +37 -0
  250. package/dist/layout/grid.d.ts +40 -0
  251. package/dist/layout/grid.js +48 -0
  252. package/dist/layout/index.d.ts +15 -0
  253. package/dist/layout/index.js +15 -0
  254. package/dist/layout/item.d.ts +65 -0
  255. package/dist/layout/item.js +100 -0
  256. package/dist/layout/link.d.ts +48 -0
  257. package/dist/layout/link.js +63 -0
  258. package/dist/layout/pagination.d.ts +54 -0
  259. package/dist/layout/pagination.js +121 -0
  260. package/dist/layout/resizable.d.ts +38 -0
  261. package/dist/layout/resizable.js +68 -0
  262. package/dist/layout/sidebar.d.ts +67 -0
  263. package/dist/layout/sidebar.js +129 -0
  264. package/dist/layout/stack.d.ts +46 -0
  265. package/dist/layout/stack.js +62 -0
  266. package/dist/layout/stepper.d.ts +49 -0
  267. package/dist/layout/stepper.js +81 -0
  268. package/dist/layout/timeline.d.ts +43 -0
  269. package/dist/layout/timeline.js +63 -0
  270. package/dist/lib/aria-class.js +24 -0
  271. package/dist/lib/cn.js +18 -0
  272. package/dist/lib/compose.js +38 -0
  273. package/dist/lib/cva.d.ts +18 -0
  274. package/dist/lib/cva.js +65 -0
  275. package/dist/midi/_notes.d.ts +101 -0
  276. package/dist/midi/_notes.js +183 -0
  277. package/dist/midi/_ruler.d.ts +100 -0
  278. package/dist/midi/_ruler.js +105 -0
  279. package/dist/midi/_time.d.ts +107 -0
  280. package/dist/midi/_time.js +130 -0
  281. package/dist/midi/index.d.ts +9 -0
  282. package/dist/midi/index.js +9 -0
  283. package/dist/midi/musical-time-input.d.ts +48 -0
  284. package/dist/midi/musical-time-input.js +154 -0
  285. package/dist/midi/piano-keyboard.d.ts +50 -0
  286. package/dist/midi/piano-keyboard.js +193 -0
  287. package/dist/midi/spectrum-visualizer.d.ts +65 -0
  288. package/dist/midi/spectrum-visualizer.js +213 -0
  289. package/dist/midi/threshold-rail.d.ts +72 -0
  290. package/dist/midi/threshold-rail.js +214 -0
  291. package/dist/midi/timeline-ruler.d.ts +83 -0
  292. package/dist/midi/timeline-ruler.js +152 -0
  293. package/dist/overlays/_debounce.js +34 -0
  294. package/dist/overlays/_glyphs.js +59 -0
  295. package/dist/overlays/_surface.d.ts +70 -0
  296. package/dist/overlays/_surface.js +70 -0
  297. package/dist/overlays/_wheel.js +42 -0
  298. package/dist/overlays/alert-dialog.d.ts +67 -0
  299. package/dist/overlays/alert-dialog.js +117 -0
  300. package/dist/overlays/command-palette.d.ts +93 -0
  301. package/dist/overlays/command-palette.js +193 -0
  302. package/dist/overlays/context-menu.d.ts +64 -0
  303. package/dist/overlays/context-menu.js +78 -0
  304. package/dist/overlays/dialog.d.ts +72 -0
  305. package/dist/overlays/dialog.js +124 -0
  306. package/dist/overlays/drawer.d.ts +75 -0
  307. package/dist/overlays/drawer.js +151 -0
  308. package/dist/overlays/dropdown-menu.d.ts +79 -0
  309. package/dist/overlays/dropdown-menu.js +156 -0
  310. package/dist/overlays/hover-card.d.ts +58 -0
  311. package/dist/overlays/hover-card.js +91 -0
  312. package/dist/overlays/index.d.ts +15 -0
  313. package/dist/overlays/index.js +15 -0
  314. package/dist/overlays/info-tip.d.ts +41 -0
  315. package/dist/overlays/info-tip.js +45 -0
  316. package/dist/overlays/menubar.d.ts +72 -0
  317. package/dist/overlays/menubar.js +100 -0
  318. package/dist/overlays/popover.d.ts +66 -0
  319. package/dist/overlays/popover.js +114 -0
  320. package/dist/overlays/sheet.d.ts +78 -0
  321. package/dist/overlays/sheet.js +131 -0
  322. package/dist/overlays/toast.d.ts +106 -0
  323. package/dist/overlays/toast.js +146 -0
  324. package/dist/overlays/tooltip.d.ts +57 -0
  325. package/dist/overlays/tooltip.js +95 -0
  326. package/dist/primitives/avatar.d.ts +51 -0
  327. package/dist/primitives/avatar.js +93 -0
  328. package/dist/primitives/button.d.ts +46 -0
  329. package/dist/primitives/button.js +94 -0
  330. package/dist/primitives/chip.d.ts +47 -0
  331. package/dist/primitives/chip.js +152 -0
  332. package/dist/primitives/icon-button.d.ts +41 -0
  333. package/dist/primitives/icon-button.js +59 -0
  334. package/dist/primitives/index.d.ts +10 -0
  335. package/dist/primitives/index.js +10 -0
  336. package/dist/primitives/kbd.d.ts +17 -0
  337. package/dist/primitives/kbd.js +24 -0
  338. package/dist/primitives/separator.d.ts +25 -0
  339. package/dist/primitives/separator.js +30 -0
  340. package/dist/primitives/skeleton.d.ts +20 -0
  341. package/dist/primitives/skeleton.js +27 -0
  342. package/dist/primitives/spinner.d.ts +26 -0
  343. package/dist/primitives/spinner.js +31 -0
  344. package/dist/primitives/status-dot.d.ts +44 -0
  345. package/dist/primitives/status-dot.js +50 -0
  346. package/dist/styles.css +19 -0
  347. package/dist/system/density.d.ts +33 -0
  348. package/dist/system/density.js +49 -0
  349. package/dist/system/index.d.ts +8 -0
  350. package/dist/system/index.js +7 -0
  351. package/dist/system/portal.d.ts +51 -0
  352. package/dist/system/portal.js +122 -0
  353. package/dist/system/prepaint.d.ts +38 -0
  354. package/dist/system/prepaint.js +37 -0
  355. package/dist/system/theme-provider.d.ts +72 -0
  356. package/dist/system/theme-provider.js +252 -0
  357. package/dist/system/use-density.d.ts +33 -0
  358. package/dist/system/use-density.js +84 -0
  359. package/dist/system/use-isomorphic-layout-effect.js +15 -0
  360. package/dist/system/use-theme.d.ts +19 -0
  361. package/dist/system/use-theme.js +25 -0
  362. package/dist/theming/_presets.d.ts +11 -0
  363. package/dist/theming/_presets.js +487 -0
  364. package/dist/theming/contrast.d.ts +110 -0
  365. package/dist/theming/contrast.js +167 -0
  366. package/dist/theming/create-theme.d.ts +165 -0
  367. package/dist/theming/create-theme.js +442 -0
  368. package/dist/theming/index.d.ts +5 -0
  369. package/dist/theming/index.js +5 -0
  370. package/dist/theming/serialize.d.ts +145 -0
  371. package/dist/theming/serialize.js +165 -0
  372. package/package.json +216 -0
@@ -0,0 +1,69 @@
1
+ import * as React from "react";
2
+ //#region src/forms/rating.d.ts
3
+ /** Mark size per rung — a rating is read at a glance, so it tracks the icon ladder. */
4
+ declare const MARK_SIZES: {
5
+ readonly sm: "size-icon-sm";
6
+ readonly md: "size-icon-md";
7
+ readonly lg: "size-[calc(var(--cue-icon-md)*1.35)]";
8
+ };
9
+ /** Rung of the icon ladder a {@link Rating}'s marks sit on. */
10
+ type RatingSize = keyof typeof MARK_SIZES;
11
+ interface RatingProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue"> {
12
+ /** The rating, `0`–`count`. Use with `onValueChange` for a controlled widget. */
13
+ value?: number;
14
+ /** Initial rating for an uncontrolled widget. Defaults to `0`. */
15
+ defaultValue?: number;
16
+ /** Called with the new rating; `0` means "cleared". */
17
+ onValueChange?: (value: number) => void;
18
+ /** How many marks. Defaults to `5`. */
19
+ count?: number;
20
+ /** Rung of the icon ladder. Defaults to `"md"`. */
21
+ size?: RatingSize;
22
+ /**
23
+ * Picking the current value clears it back to `0`. Defaults to `true` — a
24
+ * rating you cannot take back is a trap, and there is no other affordance for
25
+ * "actually, no opinion".
26
+ */
27
+ allowClear?: boolean;
28
+ /** Render a static readout with no controls. */
29
+ readOnly?: boolean;
30
+ /** Grey the marks out and refuse interaction. */
31
+ disabled?: boolean;
32
+ /**
33
+ * Accessible name for the group. Also the prefix of the read-only readout —
34
+ * `"Signal quality: 4 of 5"`. Synonym of `aria-label`, which wins if both are
35
+ * given.
36
+ */
37
+ label?: string;
38
+ /** Accessible name for the group, for callers who prefer the ARIA spelling. */
39
+ "aria-label"?: string;
40
+ /** The mark component. Defaults to a filled star. */
41
+ icon?: React.ElementType;
42
+ /**
43
+ * Accessible name for mark `index` (1-based) of `count`. Defaults to
44
+ * ``(index, count) => `${index} of ${count}` ``.
45
+ */
46
+ markLabel?: (index: number, count: number) => string;
47
+ /** Extra classes for the group. */
48
+ className?: string;
49
+ }
50
+ /**
51
+ * A 0–N rating, drawn as marks.
52
+ *
53
+ * Built as a **radio group**, not a row of buttons: exactly one value is in
54
+ * effect, arrow keys move between the options, and one Tab stop covers the whole
55
+ * widget. A row of toggle buttons would put five stops in the tab order and
56
+ * announce five unrelated controls.
57
+ *
58
+ * Read-only renders as a single `img` with the value in its name — an
59
+ * un-interactive control that still takes focus is a trap, and five disabled
60
+ * radios announce five things where one sentence will do.
61
+ *
62
+ * @example
63
+ * <Rating label="Signal quality" value={score} onValueChange={setScore} />
64
+ * @example
65
+ * <Rating aria-label="Signal quality" value={4} readOnly size="sm" />
66
+ */
67
+ declare const Rating: React.ForwardRefExoticComponent<RatingProps & React.RefAttributes<HTMLDivElement>>;
68
+ //#endregion
69
+ export { Rating, RatingProps, RatingSize };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { StarGlyph } from "./_glyphs.js";
4
+ import * as React from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ //#region src/forms/rating.tsx
7
+ /** Mark size per rung — a rating is read at a glance, so it tracks the icon ladder. */
8
+ const MARK_SIZES = {
9
+ sm: "size-icon-sm",
10
+ md: "size-icon-md",
11
+ lg: "size-[calc(var(--cue-icon-md)*1.35)]"
12
+ };
13
+ const MARK_CLASSES = "flex cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-fg-subtle transition-colors duration-150 outline-none hover:text-accent focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-40 aria-checked:text-accent";
14
+ /**
15
+ * A 0–N rating, drawn as marks.
16
+ *
17
+ * Built as a **radio group**, not a row of buttons: exactly one value is in
18
+ * effect, arrow keys move between the options, and one Tab stop covers the whole
19
+ * widget. A row of toggle buttons would put five stops in the tab order and
20
+ * announce five unrelated controls.
21
+ *
22
+ * Read-only renders as a single `img` with the value in its name — an
23
+ * un-interactive control that still takes focus is a trap, and five disabled
24
+ * radios announce five things where one sentence will do.
25
+ *
26
+ * @example
27
+ * <Rating label="Signal quality" value={score} onValueChange={setScore} />
28
+ * @example
29
+ * <Rating aria-label="Signal quality" value={4} readOnly size="sm" />
30
+ */
31
+ const Rating = React.forwardRef(function Rating({ className, value: valueProp, defaultValue = 0, onValueChange, count = 5, size = "md", allowClear = true, readOnly = false, disabled = false, label, "aria-label": ariaLabel, icon: Icon = StarGlyph, markLabel = (index, total) => `${index} of ${total}`, ...elementProps }, ref) {
32
+ const isControlled = valueProp !== void 0;
33
+ const [uncontrolled, setUncontrolled] = React.useState(defaultValue);
34
+ const value = isControlled ? valueProp : uncontrolled;
35
+ const name = ariaLabel ?? label;
36
+ const commit = React.useCallback((next) => {
37
+ const clamped = Math.min(count, Math.max(0, next));
38
+ if (clamped === value) return;
39
+ if (!isControlled) setUncontrolled(clamped);
40
+ onValueChange?.(clamped);
41
+ }, [
42
+ count,
43
+ isControlled,
44
+ onValueChange,
45
+ value
46
+ ]);
47
+ const marks = Array.from({ length: count }, (_, index) => index + 1);
48
+ if (readOnly) return /* @__PURE__ */ jsx("div", {
49
+ ref,
50
+ role: "img",
51
+ "data-slot": "rating",
52
+ "aria-label": name === void 0 ? `${value} of ${count}` : `${name}: ${value} of ${count}`,
53
+ className: cn("inline-flex items-center gap-(--cue-space-1)", className),
54
+ ...elementProps,
55
+ children: marks.map((mark) => /* @__PURE__ */ jsx(Icon, {
56
+ "aria-hidden": "true",
57
+ "data-slot": "rating-mark",
58
+ "data-filled": mark <= value ? "" : void 0,
59
+ className: cn(MARK_SIZES[size], mark <= value ? "text-accent" : "text-fg-subtle")
60
+ }, mark))
61
+ });
62
+ const handleKeyDown = (event) => {
63
+ if (disabled) return;
64
+ const step = event.key === "ArrowRight" || event.key === "ArrowUp" ? 1 : event.key === "ArrowLeft" || event.key === "ArrowDown" ? -1 : 0;
65
+ let next = null;
66
+ if (step !== 0) next = Math.min(count, Math.max(1, value + step));
67
+ else if (event.key === "Home") next = 1;
68
+ else if (event.key === "End") next = count;
69
+ if (next === null) return;
70
+ event.preventDefault();
71
+ commit(next);
72
+ event.currentTarget.querySelector(`[data-slot="rating-mark"][data-index="${next}"]`)?.focus();
73
+ };
74
+ const focusable = value === 0 ? 1 : value;
75
+ return /* @__PURE__ */ jsx("div", {
76
+ ref,
77
+ role: "radiogroup",
78
+ "data-slot": "rating",
79
+ "aria-label": name,
80
+ "aria-disabled": disabled ? true : void 0,
81
+ onKeyDown: handleKeyDown,
82
+ className: cn("inline-flex items-center gap-(--cue-space-1)", className),
83
+ ...elementProps,
84
+ children: marks.map((mark) => /* @__PURE__ */ jsx("button", {
85
+ type: "button",
86
+ role: "radio",
87
+ "data-slot": "rating-mark",
88
+ "data-index": mark,
89
+ "aria-checked": mark === value,
90
+ "aria-label": markLabel(mark, count),
91
+ tabIndex: mark === focusable ? 0 : -1,
92
+ disabled,
93
+ onClick: () => commit(mark === value && allowClear ? 0 : mark),
94
+ className: MARK_CLASSES,
95
+ children: /* @__PURE__ */ jsx(Icon, {
96
+ "aria-hidden": "true",
97
+ className: cn(MARK_SIZES[size], mark <= value ? "text-accent" : void 0)
98
+ })
99
+ }, mark))
100
+ });
101
+ });
102
+ //#endregion
103
+ export { Rating };
@@ -0,0 +1,81 @@
1
+ import { FormControlSize } from "./_chassis.js";
2
+ import * as React from "react";
3
+ //#region src/forms/scrub-input.d.ts
4
+ interface ScrubInputProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "defaultValue" | "onDragStart" | "onDragEnd"> {
5
+ /** The value. Always controlled — a scrub is a stream of edits, not a state to lose. */
6
+ value: number;
7
+ /**
8
+ * Called with every new value: each drag frame, each arrow-key step, and the
9
+ * committed result of a typed edit. Not a DOM change event.
10
+ */
11
+ onChange: (value: number) => void;
12
+ /** Called only for values produced by a drag, for hosts that throttle live writes. */
13
+ onLiveDrag?: (value: number) => void;
14
+ /** Called once when a drag actually begins — after the click threshold, not on press. */
15
+ onDragStart?: () => void;
16
+ /** Called once when a drag ends. */
17
+ onDragEnd?: () => void;
18
+ /**
19
+ * Called once per finished gesture: pointer-up after a drag, each key step, a
20
+ * committed typed value. Hang undo entries, network writes and persistence
21
+ * here — `onChange` fires dozens of times per drag.
22
+ */
23
+ onCommit?: (value: number) => void;
24
+ /** Lower bound. Defaults to `-Infinity`. */
25
+ min?: number;
26
+ /** Upper bound. Defaults to `Infinity`. */
27
+ max?: number;
28
+ /** Grid the value snaps to. Defaults to `1`. */
29
+ step?: number;
30
+ /** Decimal places shown. Defaults to whatever `step` implies. */
31
+ precision?: number;
32
+ /** Suffix drawn after the number — `%`, `s`, `dB`. */
33
+ unit?: string;
34
+ /**
35
+ * The units this value can be expressed in. With more than one the suffix
36
+ * becomes a button that cycles through them and reports via `onUnitChange`.
37
+ */
38
+ units?: readonly string[];
39
+ /** Called with the next unit when the suffix is clicked. */
40
+ onUnitChange?: (unit: string) => void;
41
+ /** Rung of the control ladder. Defaults to `"md"`. */
42
+ size?: FormControlSize;
43
+ /** Refuse dragging, typing and keyboard steps. */
44
+ disabled?: boolean;
45
+ /** Accessible name for the control. */
46
+ "aria-label"?: string;
47
+ /** Id of the element naming the control. */
48
+ "aria-labelledby"?: string;
49
+ /** Extra classes for the control. */
50
+ className?: string;
51
+ }
52
+ /**
53
+ * A number you scrub: drag up to raise it, down to lower it, click to type it.
54
+ *
55
+ * The console's answer to `<input type="number">`. Native spinners are two 4px
56
+ * arrows that only appear on hover — unusable at this density and invisible on
57
+ * touch — while a slider costs 200px of width to say one number. Dragging on the
58
+ * number itself costs nothing and is the interaction every audio and lighting
59
+ * tool has converged on: pointer capture so the cursor may leave the box, Shift
60
+ * for fine adjustment, `cursor-ns-resize` to advertise the axis.
61
+ *
62
+ * Click-to-type is not a fallback, it is the other half of the control: a
63
+ * gesture is for *approximately*, typing is for *exactly*, and a value you can
64
+ * only reach by dragging is a value you cannot enter from a cue sheet.
65
+ *
66
+ * `role="spinbutton"` is what makes any of this reachable without a pointer —
67
+ * arrow keys step, Page keys jump ten, Home/End go to the bounds — and it is why
68
+ * the resting element is not an `<input>`: a spinbutton announces its value,
69
+ * where a text box would announce its editable string on every drag frame.
70
+ *
71
+ * The `onChange`/`onCommit` split is the same undo-coalescing contract as
72
+ * {@link Slider}: one drag is one undo entry, not two hundred.
73
+ *
74
+ * @example
75
+ * <ScrubInput aria-label="Intensity" value={level} onChange={setLevel} onCommit={pushUndo} />
76
+ * @example
77
+ * <ScrubInput aria-label="Fade" value={fade} onChange={setFade} step={0.1} unit="s" min={0} />
78
+ */
79
+ declare const ScrubInput: React.ForwardRefExoticComponent<ScrubInputProps & React.RefAttributes<HTMLDivElement>>;
80
+ //#endregion
81
+ export { ScrubInput, ScrubInputProps };
@@ -0,0 +1,249 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { bareControlClasses, controlGroupFocusClasses, controlVariants } from "./_chassis.js";
4
+ import * as React from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/forms/scrub-input.tsx
7
+ /**
8
+ * Pixels of vertical travel per step. Two pixels per step is the Ableton/Live
9
+ * feel the audit called out: fast enough to cross a 0–100 range in a flick,
10
+ * slow enough that a single value is reachable without the fine modifier.
11
+ */
12
+ const PIXELS_PER_STEP = 2;
13
+ /**
14
+ * How much Shift slows a drag. Quarter speed — same value grid, four times the
15
+ * travel per step, which is precision of *control* rather than sub-step values
16
+ * the caller never asked for.
17
+ */
18
+ const FINE_FACTOR = .25;
19
+ /** Travel, in px, before a press counts as a drag rather than a click. */
20
+ const DRAG_THRESHOLD_PX = 3;
21
+ /** Arrow keys move one step; Page keys move ten. */
22
+ const PAGE_FACTOR = 10;
23
+ /** Snap to the step grid, clamp to the bounds, and drop float noise. */
24
+ function quantise(raw, { min, max, step, precision }) {
25
+ const snapped = Math.round(raw / step) * step;
26
+ const clamped = Math.min(max, Math.max(min, snapped));
27
+ const factor = 10 ** precision;
28
+ return Math.round(clamped * factor) / factor;
29
+ }
30
+ /** Decimal places implied by a step, when the caller did not say. */
31
+ function precisionOf(step) {
32
+ const text = String(step);
33
+ const dot = text.indexOf(".");
34
+ return dot === -1 ? 0 : Math.min(6, text.length - dot - 1);
35
+ }
36
+ /**
37
+ * A number you scrub: drag up to raise it, down to lower it, click to type it.
38
+ *
39
+ * The console's answer to `<input type="number">`. Native spinners are two 4px
40
+ * arrows that only appear on hover — unusable at this density and invisible on
41
+ * touch — while a slider costs 200px of width to say one number. Dragging on the
42
+ * number itself costs nothing and is the interaction every audio and lighting
43
+ * tool has converged on: pointer capture so the cursor may leave the box, Shift
44
+ * for fine adjustment, `cursor-ns-resize` to advertise the axis.
45
+ *
46
+ * Click-to-type is not a fallback, it is the other half of the control: a
47
+ * gesture is for *approximately*, typing is for *exactly*, and a value you can
48
+ * only reach by dragging is a value you cannot enter from a cue sheet.
49
+ *
50
+ * `role="spinbutton"` is what makes any of this reachable without a pointer —
51
+ * arrow keys step, Page keys jump ten, Home/End go to the bounds — and it is why
52
+ * the resting element is not an `<input>`: a spinbutton announces its value,
53
+ * where a text box would announce its editable string on every drag frame.
54
+ *
55
+ * The `onChange`/`onCommit` split is the same undo-coalescing contract as
56
+ * {@link Slider}: one drag is one undo entry, not two hundred.
57
+ *
58
+ * @example
59
+ * <ScrubInput aria-label="Intensity" value={level} onChange={setLevel} onCommit={pushUndo} />
60
+ * @example
61
+ * <ScrubInput aria-label="Fade" value={fade} onChange={setFade} step={0.1} unit="s" min={0} />
62
+ */
63
+ const ScrubInput = React.forwardRef(function ScrubInput({ className, value, onChange, onLiveDrag, onDragStart, onDragEnd, onCommit, min = Number.NEGATIVE_INFINITY, max = Number.POSITIVE_INFINITY, step = 1, precision, unit, units, onUnitChange, size = "md", disabled = false, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...elementProps }, ref) {
64
+ const decimals = precision ?? precisionOf(step);
65
+ const bounds = React.useMemo(() => ({
66
+ min,
67
+ max,
68
+ step,
69
+ precision: decimals
70
+ }), [
71
+ decimals,
72
+ max,
73
+ min,
74
+ step
75
+ ]);
76
+ const [editing, setEditing] = React.useState(false);
77
+ const [draft, setDraft] = React.useState("");
78
+ const editorRef = React.useRef(null);
79
+ /** Escape has already discarded this edit; a trailing blur must not undo that. */
80
+ const abandoned = React.useRef(false);
81
+ /** Live gesture state. A ref, not state: a drag must not re-render to track itself. */
82
+ const gesture = React.useRef(null);
83
+ const display = value.toFixed(decimals);
84
+ const valueText = unit === void 0 ? display : `${display} ${unit}`;
85
+ const emit = React.useCallback((next, fromDrag) => {
86
+ if (next === value) return false;
87
+ onChange(next);
88
+ if (fromDrag) onLiveDrag?.(next);
89
+ return true;
90
+ }, [
91
+ onChange,
92
+ onLiveDrag,
93
+ value
94
+ ]);
95
+ const beginEditing = React.useCallback(() => {
96
+ abandoned.current = false;
97
+ setDraft(value.toFixed(decimals));
98
+ setEditing(true);
99
+ }, [decimals, value]);
100
+ const handlePointerDown = (event) => {
101
+ if (disabled || editing || event.button !== 0) return;
102
+ gesture.current = {
103
+ pointerId: event.pointerId,
104
+ startY: event.clientY,
105
+ startValue: value,
106
+ dragging: false,
107
+ last: value
108
+ };
109
+ event.currentTarget.setPointerCapture?.(event.pointerId);
110
+ };
111
+ const handlePointerMove = (event) => {
112
+ const active = gesture.current;
113
+ if (active === null || active.pointerId !== event.pointerId) return;
114
+ const travel = active.startY - event.clientY;
115
+ if (!active.dragging) {
116
+ if (Math.abs(travel) < DRAG_THRESHOLD_PX) return;
117
+ active.dragging = true;
118
+ onDragStart?.();
119
+ }
120
+ const scale = event.shiftKey ? FINE_FACTOR : 1;
121
+ const next = quantise(active.startValue + travel / PIXELS_PER_STEP * step * scale, bounds);
122
+ if (next === active.last) return;
123
+ active.last = next;
124
+ emit(next, true);
125
+ };
126
+ const endGesture = (event) => {
127
+ const active = gesture.current;
128
+ if (active === null || active.pointerId !== event.pointerId) return;
129
+ gesture.current = null;
130
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
131
+ if (!active.dragging) {
132
+ if (!disabled) beginEditing();
133
+ return;
134
+ }
135
+ onDragEnd?.();
136
+ onCommit?.(active.last);
137
+ };
138
+ React.useEffect(() => {
139
+ if (editing) editorRef.current?.select();
140
+ }, [editing]);
141
+ const commitDraft = React.useCallback(() => {
142
+ setEditing(false);
143
+ if (abandoned.current) return;
144
+ const parsed = Number.parseFloat(draft);
145
+ if (Number.isNaN(parsed)) return;
146
+ const next = quantise(parsed, bounds);
147
+ if (emit(next, false)) onCommit?.(next);
148
+ }, [
149
+ bounds,
150
+ draft,
151
+ emit,
152
+ onCommit
153
+ ]);
154
+ const handleKeyDown = (event) => {
155
+ if (disabled) return;
156
+ let next = null;
157
+ if (event.key === "ArrowUp") next = value + step;
158
+ else if (event.key === "ArrowDown") next = value - step;
159
+ else if (event.key === "PageUp") next = value + step * PAGE_FACTOR;
160
+ else if (event.key === "PageDown") next = value - step * PAGE_FACTOR;
161
+ else if (event.key === "Home" && Number.isFinite(min)) next = min;
162
+ else if (event.key === "End" && Number.isFinite(max)) next = max;
163
+ else if (event.key === "Enter") {
164
+ event.preventDefault();
165
+ beginEditing();
166
+ return;
167
+ }
168
+ if (next === null) return;
169
+ event.preventDefault();
170
+ const quantised = quantise(next, bounds);
171
+ if (emit(quantised, false)) onCommit?.(quantised);
172
+ };
173
+ const cycleUnit = () => {
174
+ if (units === void 0 || units.length === 0) return;
175
+ const index = unit === void 0 ? -1 : units.indexOf(unit);
176
+ onUnitChange?.(units[(index + 1) % units.length]);
177
+ };
178
+ if (editing) return /* @__PURE__ */ jsxs("div", {
179
+ ...elementProps,
180
+ ref,
181
+ "data-slot": "scrub-input-editor",
182
+ className: cn(controlVariants({ size }), "flex items-center gap-(--cue-space-2)", controlGroupFocusClasses, className),
183
+ children: [/* @__PURE__ */ jsx("input", {
184
+ ref: editorRef,
185
+ type: "text",
186
+ inputMode: "decimal",
187
+ "aria-label": ariaLabel,
188
+ "aria-labelledby": ariaLabelledBy,
189
+ value: draft,
190
+ autoFocus: true,
191
+ onChange: (event) => setDraft(event.currentTarget.value),
192
+ onBlur: commitDraft,
193
+ onKeyDown: (event) => {
194
+ if (event.key === "Enter") {
195
+ event.preventDefault();
196
+ commitDraft();
197
+ } else if (event.key === "Escape") {
198
+ event.preventDefault();
199
+ abandoned.current = true;
200
+ setEditing(false);
201
+ }
202
+ },
203
+ className: cn(bareControlClasses, "text-right tabular-nums", "[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none")
204
+ }), unit === void 0 ? null : /* @__PURE__ */ jsx("span", {
205
+ className: "shrink-0 text-fg-subtle select-none",
206
+ children: unit
207
+ })]
208
+ });
209
+ return /* @__PURE__ */ jsxs("div", {
210
+ ...elementProps,
211
+ ref,
212
+ role: "spinbutton",
213
+ "data-slot": "scrub-input",
214
+ tabIndex: disabled ? -1 : 0,
215
+ "aria-label": ariaLabel,
216
+ "aria-labelledby": ariaLabelledBy,
217
+ "aria-valuenow": value,
218
+ "aria-valuemin": Number.isFinite(min) ? min : void 0,
219
+ "aria-valuemax": Number.isFinite(max) ? max : void 0,
220
+ "aria-valuetext": valueText,
221
+ "aria-disabled": disabled ? true : void 0,
222
+ onPointerDown: handlePointerDown,
223
+ onPointerMove: handlePointerMove,
224
+ onPointerUp: endGesture,
225
+ onPointerCancel: endGesture,
226
+ onKeyDown: handleKeyDown,
227
+ className: cn(controlVariants({ size }), "flex touch-none items-center justify-between gap-(--cue-space-2) select-none", disabled ? "cursor-default opacity-40" : "cursor-ns-resize", className),
228
+ children: [/* @__PURE__ */ jsx("span", {
229
+ "data-slot": "scrub-input-value",
230
+ className: "truncate tabular-nums",
231
+ children: display
232
+ }), unit === void 0 ? null : units === void 0 || units.length < 2 ? /* @__PURE__ */ jsx("span", {
233
+ "data-slot": "scrub-input-unit",
234
+ className: "shrink-0 text-fg-subtle",
235
+ children: unit
236
+ }) : /* @__PURE__ */ jsx("button", {
237
+ type: "button",
238
+ "data-slot": "scrub-input-unit",
239
+ "aria-label": `Unit: ${unit}`,
240
+ disabled,
241
+ onPointerDown: (event) => event.stopPropagation(),
242
+ onClick: cycleUnit,
243
+ className: "shrink-0 cursor-pointer rounded-(--radius-control) border-0 bg-transparent p-0 font-mono text-[length:inherit] text-fg-subtle outline-none transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent disabled:pointer-events-none",
244
+ children: unit
245
+ })]
246
+ });
247
+ });
248
+ //#endregion
249
+ export { ScrubInput };
@@ -0,0 +1,54 @@
1
+ import { FormControlSize } from "./_chassis.js";
2
+ import { InputProps } from "./input.js";
3
+ import * as React from "react";
4
+ //#region src/forms/search-input.d.ts
5
+ interface SearchInputProps extends Omit<InputProps, "type" | "size" | "value" | "defaultValue" | "onValueChange" | "children"> {
6
+ /** Current query. Supply it with `onValueChange` for a controlled input. */
7
+ value?: string;
8
+ /** Initial query for an uncontrolled input. */
9
+ defaultValue?: string;
10
+ /** Fired on every keystroke, and with `""` when the query is cleared. */
11
+ onValueChange?: (value: string) => void;
12
+ /**
13
+ * Quiet period in ms before {@link SearchInputProps.onDebouncedChange} fires.
14
+ * Defaults to `250`. `0` fires it synchronously with every keystroke.
15
+ */
16
+ debounce?: number;
17
+ /**
18
+ * Fired once the user stops typing for `debounce` ms — the callback to hang a
19
+ * query or a filter recomputation on. Clearing flushes it immediately, because
20
+ * "show me everything again" should never wait.
21
+ */
22
+ onDebouncedChange?: (value: string) => void;
23
+ /** Fired when the query is cleared, by Escape or by the clear button. */
24
+ onClear?: () => void;
25
+ /** Leading glyph component. Defaults to a magnifier. */
26
+ icon?: React.ElementType;
27
+ /** Accessible name for the clear button. Defaults to `"Clear search"`. */
28
+ clearLabel?: string;
29
+ /** Rung of the control ladder. Defaults to `"md"`. */
30
+ size?: FormControlSize;
31
+ /** Extra classes for the group that wraps the icon, the input and the clear button. */
32
+ className?: string;
33
+ }
34
+ /**
35
+ * A filter box: leading magnifier, Escape to clear, and a debounced change
36
+ * callback for whatever the query drives.
37
+ *
38
+ * Escape-to-clear is the reason this is a component and not a recipe. It is the
39
+ * behaviour every console filter needs and the one every hand-rolled search box
40
+ * forgets, and getting it right means also stopping the key from bubbling into a
41
+ * dialog that would otherwise close underneath the user.
42
+ *
43
+ * Debouncing is deliberately separate from `onValueChange`: the input stays
44
+ * responsive on every keystroke while the expensive work waits for the typing to
45
+ * stop. Clearing flushes the debounce rather than scheduling it.
46
+ *
47
+ * @example
48
+ * <SearchInput aria-label="Filter cues" onDebouncedChange={setQuery} />
49
+ * @example
50
+ * <SearchInput value={q} onValueChange={setQ} debounce={0} size="sm" />
51
+ */
52
+ declare const SearchInput: React.ForwardRefExoticComponent<Omit<SearchInputProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
53
+ //#endregion
54
+ export { SearchInput, SearchInputProps };
@@ -0,0 +1,112 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { InputGroup } from "./input-group.js";
4
+ import { Input } from "./input.js";
5
+ import { CrossGlyph, SearchGlyph } from "./_glyphs.js";
6
+ import * as React from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/forms/search-input.tsx
9
+ const DEFAULT_DEBOUNCE_MS = 250;
10
+ /**
11
+ * A filter box: leading magnifier, Escape to clear, and a debounced change
12
+ * callback for whatever the query drives.
13
+ *
14
+ * Escape-to-clear is the reason this is a component and not a recipe. It is the
15
+ * behaviour every console filter needs and the one every hand-rolled search box
16
+ * forgets, and getting it right means also stopping the key from bubbling into a
17
+ * dialog that would otherwise close underneath the user.
18
+ *
19
+ * Debouncing is deliberately separate from `onValueChange`: the input stays
20
+ * responsive on every keystroke while the expensive work waits for the typing to
21
+ * stop. Clearing flushes the debounce rather than scheduling it.
22
+ *
23
+ * @example
24
+ * <SearchInput aria-label="Filter cues" onDebouncedChange={setQuery} />
25
+ * @example
26
+ * <SearchInput value={q} onValueChange={setQ} debounce={0} size="sm" />
27
+ */
28
+ const SearchInput = React.forwardRef(function SearchInput({ className, value, defaultValue, onValueChange, onChange, onKeyDown, debounce = DEFAULT_DEBOUNCE_MS, onDebouncedChange, onClear, icon: Icon = SearchGlyph, clearLabel = "Clear search", size, ...elementProps }, ref) {
29
+ const isControlled = value !== void 0;
30
+ const [internal, setInternal] = React.useState(defaultValue ?? "");
31
+ const current = isControlled ? value : internal;
32
+ const innerRef = React.useRef(null);
33
+ const setRefs = React.useCallback((node) => {
34
+ innerRef.current = node;
35
+ if (typeof ref === "function") ref(node);
36
+ else if (ref !== null) ref.current = node;
37
+ }, [ref]);
38
+ const debouncedRef = React.useRef(onDebouncedChange);
39
+ React.useEffect(() => {
40
+ debouncedRef.current = onDebouncedChange;
41
+ }, [onDebouncedChange]);
42
+ const lastNotified = React.useRef(current);
43
+ React.useEffect(() => {
44
+ const notify = debouncedRef.current;
45
+ if (notify === void 0 || current === lastNotified.current) return;
46
+ if (debounce <= 0) {
47
+ lastNotified.current = current;
48
+ notify(current);
49
+ return;
50
+ }
51
+ const timer = setTimeout(() => {
52
+ lastNotified.current = current;
53
+ notify(current);
54
+ }, debounce);
55
+ return () => clearTimeout(timer);
56
+ }, [current, debounce]);
57
+ const commit = React.useCallback((next) => {
58
+ if (!isControlled) setInternal(next);
59
+ onValueChange?.(next);
60
+ }, [isControlled, onValueChange]);
61
+ const clear = React.useCallback(() => {
62
+ commit("");
63
+ lastNotified.current = "";
64
+ debouncedRef.current?.("");
65
+ onClear?.();
66
+ innerRef.current?.focus();
67
+ }, [commit, onClear]);
68
+ const handleChange = React.useCallback((event) => {
69
+ onChange?.(event);
70
+ commit(event.currentTarget.value);
71
+ }, [commit, onChange]);
72
+ const handleKeyDown = React.useCallback((event) => {
73
+ onKeyDown?.(event);
74
+ if (event.defaultPrevented || event.key !== "Escape") return;
75
+ if (current === "") return;
76
+ event.preventDefault();
77
+ event.stopPropagation();
78
+ clear();
79
+ }, [
80
+ clear,
81
+ current,
82
+ onKeyDown
83
+ ]);
84
+ return /* @__PURE__ */ jsx(InputGroup, {
85
+ size,
86
+ className,
87
+ leading: /* @__PURE__ */ jsx(Icon, {
88
+ "data-slot": "search-icon",
89
+ "aria-hidden": "true",
90
+ className: "size-icon-md"
91
+ }),
92
+ trailing: current === "" ? void 0 : /* @__PURE__ */ jsx("button", {
93
+ type: "button",
94
+ "aria-label": clearLabel,
95
+ onClick: clear,
96
+ className: "flex shrink-0 cursor-pointer items-center justify-center rounded-(--radius-control) text-fg-subtle outline-none transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent",
97
+ children: /* @__PURE__ */ jsx(CrossGlyph, { className: "size-icon-sm" })
98
+ }),
99
+ children: /* @__PURE__ */ jsx(Input, {
100
+ ref: setRefs,
101
+ type: "search",
102
+ "data-slot": "search-input",
103
+ value: current,
104
+ onChange: handleChange,
105
+ onKeyDown: handleKeyDown,
106
+ className: cn("[&::-webkit-search-cancel-button]:appearance-none"),
107
+ ...elementProps
108
+ })
109
+ });
110
+ });
111
+ //#endregion
112
+ export { SearchInput };