@corbits/react-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 (255) hide show
  1. package/LICENSE +501 -0
  2. package/README.md +125 -0
  3. package/dist/blocks/login/auth-layout.d.ts +38 -0
  4. package/dist/blocks/login/auth-layout.js +52 -0
  5. package/dist/blocks/login/login-form.d.ts +60 -0
  6. package/dist/blocks/login/login-form.js +151 -0
  7. package/dist/hooks/use-command-palette-navigation.d.ts +25 -0
  8. package/dist/hooks/use-command-palette-navigation.js +52 -0
  9. package/dist/hooks/use-controllable-state.d.ts +28 -0
  10. package/dist/hooks/use-controllable-state.js +44 -0
  11. package/dist/hooks/use-delayed-autofocus.d.ts +9 -0
  12. package/dist/hooks/use-delayed-autofocus.js +20 -0
  13. package/dist/hooks/use-dismissable-popover.d.ts +37 -0
  14. package/dist/hooks/use-dismissable-popover.js +64 -0
  15. package/dist/hooks/use-list-selection.d.ts +66 -0
  16. package/dist/hooks/use-list-selection.js +147 -0
  17. package/dist/hooks/use-prefers-reduced-motion.d.ts +13 -0
  18. package/dist/hooks/use-prefers-reduced-motion.js +28 -0
  19. package/dist/hooks/use-scroll-current-into-view.d.ts +19 -0
  20. package/dist/hooks/use-scroll-current-into-view.js +33 -0
  21. package/dist/index.d.ts +122 -0
  22. package/dist/index.js +131 -0
  23. package/dist/lib/artifact.d.ts +72 -0
  24. package/dist/lib/artifact.js +67 -0
  25. package/dist/lib/chart-geometry.d.ts +62 -0
  26. package/dist/lib/chart-geometry.js +86 -0
  27. package/dist/lib/chart-palette.d.ts +76 -0
  28. package/dist/lib/chart-palette.js +92 -0
  29. package/dist/lib/chat-message.d.ts +115 -0
  30. package/dist/lib/chat-message.js +41 -0
  31. package/dist/lib/chat-parts.d.ts +64 -0
  32. package/dist/lib/chat-parts.js +16 -0
  33. package/dist/lib/csv.d.ts +60 -0
  34. package/dist/lib/csv.js +108 -0
  35. package/dist/lib/metrics.d.ts +70 -0
  36. package/dist/lib/metrics.js +101 -0
  37. package/dist/lib/now-item.d.ts +65 -0
  38. package/dist/lib/now-item.js +59 -0
  39. package/dist/lib/relative-time.d.ts +9 -0
  40. package/dist/lib/relative-time.js +47 -0
  41. package/dist/lib/theme.d.ts +30 -0
  42. package/dist/lib/theme.js +77 -0
  43. package/dist/lib/url.d.ts +26 -0
  44. package/dist/lib/url.js +37 -0
  45. package/dist/lib/utils.d.ts +2 -0
  46. package/dist/lib/utils.js +5 -0
  47. package/dist/lib/workflow-display-flow.d.ts +83 -0
  48. package/dist/lib/workflow-display-flow.js +104 -0
  49. package/dist/lib/workflow-registry.d.ts +90 -0
  50. package/dist/lib/workflow-registry.js +56 -0
  51. package/dist/lib/workflow-run-error.d.ts +31 -0
  52. package/dist/lib/workflow-run-error.js +105 -0
  53. package/dist/lib/workflow-run-progress.d.ts +97 -0
  54. package/dist/lib/workflow-run-progress.js +135 -0
  55. package/dist/lib/workflow-run.d.ts +43 -0
  56. package/dist/lib/workflow-run.js +56 -0
  57. package/dist/styles.css +3080 -0
  58. package/dist/test/render-hook.d.ts +14 -0
  59. package/dist/test/render-hook.js +38 -0
  60. package/dist/theme.css +628 -0
  61. package/dist/ui/agent-turn.d.ts +26 -0
  62. package/dist/ui/agent-turn.js +60 -0
  63. package/dist/ui/animated-number.d.ts +25 -0
  64. package/dist/ui/animated-number.js +63 -0
  65. package/dist/ui/approval-card.d.ts +46 -0
  66. package/dist/ui/approval-card.js +116 -0
  67. package/dist/ui/artifact-body.d.ts +26 -0
  68. package/dist/ui/artifact-body.js +170 -0
  69. package/dist/ui/artifact-notice.d.ts +21 -0
  70. package/dist/ui/artifact-notice.js +36 -0
  71. package/dist/ui/avatar.d.ts +37 -0
  72. package/dist/ui/avatar.js +73 -0
  73. package/dist/ui/badge.d.ts +10 -0
  74. package/dist/ui/badge.js +35 -0
  75. package/dist/ui/bar-chart.d.ts +38 -0
  76. package/dist/ui/bar-chart.js +118 -0
  77. package/dist/ui/block-card.d.ts +29 -0
  78. package/dist/ui/block-card.js +66 -0
  79. package/dist/ui/boot-screen.d.ts +23 -0
  80. package/dist/ui/boot-screen.js +49 -0
  81. package/dist/ui/bulk-action-bar.d.ts +37 -0
  82. package/dist/ui/bulk-action-bar.js +64 -0
  83. package/dist/ui/button.d.ts +12 -0
  84. package/dist/ui/button.js +73 -0
  85. package/dist/ui/card.d.ts +6 -0
  86. package/dist/ui/card.js +37 -0
  87. package/dist/ui/category-bars.d.ts +20 -0
  88. package/dist/ui/category-bars.js +69 -0
  89. package/dist/ui/chart-frame.d.ts +54 -0
  90. package/dist/ui/chart-frame.js +108 -0
  91. package/dist/ui/chat-composer.d.ts +24 -0
  92. package/dist/ui/chat-composer.js +32 -0
  93. package/dist/ui/chat-dock-timing.d.ts +15 -0
  94. package/dist/ui/chat-dock-timing.js +11 -0
  95. package/dist/ui/chat-input.d.ts +38 -0
  96. package/dist/ui/chat-input.js +138 -0
  97. package/dist/ui/chat-panel.d.ts +20 -0
  98. package/dist/ui/chat-panel.js +56 -0
  99. package/dist/ui/chat-thread.d.ts +28 -0
  100. package/dist/ui/chat-thread.js +61 -0
  101. package/dist/ui/checkbox.d.ts +25 -0
  102. package/dist/ui/checkbox.js +63 -0
  103. package/dist/ui/command-palette.d.ts +100 -0
  104. package/dist/ui/command-palette.js +341 -0
  105. package/dist/ui/compare-body.d.ts +52 -0
  106. package/dist/ui/compare-body.js +140 -0
  107. package/dist/ui/confirm-button.d.ts +29 -0
  108. package/dist/ui/confirm-button.js +41 -0
  109. package/dist/ui/corbits-mark.d.ts +29 -0
  110. package/dist/ui/corbits-mark.js +33 -0
  111. package/dist/ui/csv-table.d.ts +22 -0
  112. package/dist/ui/csv-table.js +85 -0
  113. package/dist/ui/dialog.d.ts +37 -0
  114. package/dist/ui/dialog.js +105 -0
  115. package/dist/ui/dictation-status-line.d.ts +20 -0
  116. package/dist/ui/dictation-status-line.js +38 -0
  117. package/dist/ui/dither-canvas.d.ts +31 -0
  118. package/dist/ui/dither-canvas.js +169 -0
  119. package/dist/ui/embed-body.d.ts +30 -0
  120. package/dist/ui/embed-body.js +92 -0
  121. package/dist/ui/empty-state.d.ts +26 -0
  122. package/dist/ui/empty-state.js +42 -0
  123. package/dist/ui/file-input.d.ts +25 -0
  124. package/dist/ui/file-input.js +66 -0
  125. package/dist/ui/filter-bar.d.ts +42 -0
  126. package/dist/ui/filter-bar.js +95 -0
  127. package/dist/ui/filter-chip.d.ts +14 -0
  128. package/dist/ui/filter-chip.js +23 -0
  129. package/dist/ui/gate-block.d.ts +16 -0
  130. package/dist/ui/gate-block.js +37 -0
  131. package/dist/ui/horizontal-stepper.d.ts +13 -0
  132. package/dist/ui/horizontal-stepper.js +65 -0
  133. package/dist/ui/input.d.ts +2 -0
  134. package/dist/ui/input.js +14 -0
  135. package/dist/ui/inspector-shell.d.ts +47 -0
  136. package/dist/ui/inspector-shell.js +98 -0
  137. package/dist/ui/intake-form.d.ts +39 -0
  138. package/dist/ui/intake-form.js +119 -0
  139. package/dist/ui/kind-card-grid.d.ts +23 -0
  140. package/dist/ui/kind-card-grid.js +43 -0
  141. package/dist/ui/library-page-header.d.ts +20 -0
  142. package/dist/ui/library-page-header.js +43 -0
  143. package/dist/ui/list-detail.d.ts +28 -0
  144. package/dist/ui/list-detail.js +55 -0
  145. package/dist/ui/live-run-banner.d.ts +13 -0
  146. package/dist/ui/live-run-banner.js +23 -0
  147. package/dist/ui/live-status-line.d.ts +22 -0
  148. package/dist/ui/live-status-line.js +37 -0
  149. package/dist/ui/menu.d.ts +18 -0
  150. package/dist/ui/menu.js +45 -0
  151. package/dist/ui/message-bubble.d.ts +24 -0
  152. package/dist/ui/message-bubble.js +64 -0
  153. package/dist/ui/mic-button.d.ts +35 -0
  154. package/dist/ui/mic-button.js +61 -0
  155. package/dist/ui/mic-permission-dialog.d.ts +20 -0
  156. package/dist/ui/mic-permission-dialog.js +50 -0
  157. package/dist/ui/notifications-bell.d.ts +28 -0
  158. package/dist/ui/notifications-bell.js +69 -0
  159. package/dist/ui/now-cards.d.ts +37 -0
  160. package/dist/ui/now-cards.js +116 -0
  161. package/dist/ui/page-panel.d.ts +37 -0
  162. package/dist/ui/page-panel.js +23 -0
  163. package/dist/ui/page-shell.d.ts +21 -0
  164. package/dist/ui/page-shell.js +26 -0
  165. package/dist/ui/parts-renderer.d.ts +22 -0
  166. package/dist/ui/parts-renderer.js +234 -0
  167. package/dist/ui/profile-card.d.ts +39 -0
  168. package/dist/ui/profile-card.js +124 -0
  169. package/dist/ui/progress-checklist.d.ts +25 -0
  170. package/dist/ui/progress-checklist.js +70 -0
  171. package/dist/ui/provider-mark.d.ts +32 -0
  172. package/dist/ui/provider-mark.js +31 -0
  173. package/dist/ui/quick-reply-chips.d.ts +14 -0
  174. package/dist/ui/quick-reply-chips.js +24 -0
  175. package/dist/ui/quote-card.d.ts +39 -0
  176. package/dist/ui/quote-card.js +66 -0
  177. package/dist/ui/reasoning-block.d.ts +35 -0
  178. package/dist/ui/reasoning-block.js +60 -0
  179. package/dist/ui/research-body.d.ts +70 -0
  180. package/dist/ui/research-body.js +270 -0
  181. package/dist/ui/rich-empty-state.d.ts +35 -0
  182. package/dist/ui/rich-empty-state.js +65 -0
  183. package/dist/ui/run-now-button.d.ts +23 -0
  184. package/dist/ui/run-now-button.js +42 -0
  185. package/dist/ui/section.d.ts +20 -0
  186. package/dist/ui/section.js +48 -0
  187. package/dist/ui/select.d.ts +2 -0
  188. package/dist/ui/select.js +22 -0
  189. package/dist/ui/selection-checkbox.d.ts +42 -0
  190. package/dist/ui/selection-checkbox.js +54 -0
  191. package/dist/ui/settings-panel.d.ts +35 -0
  192. package/dist/ui/settings-panel.js +68 -0
  193. package/dist/ui/shimmer-text.d.ts +19 -0
  194. package/dist/ui/shimmer-text.js +20 -0
  195. package/dist/ui/sidebar-item-row.d.ts +40 -0
  196. package/dist/ui/sidebar-item-row.js +64 -0
  197. package/dist/ui/sidebar-panel.d.ts +25 -0
  198. package/dist/ui/sidebar-panel.js +56 -0
  199. package/dist/ui/sidebar.d.ts +46 -0
  200. package/dist/ui/sidebar.js +111 -0
  201. package/dist/ui/skeleton.d.ts +13 -0
  202. package/dist/ui/skeleton.js +20 -0
  203. package/dist/ui/sortable-table.d.ts +43 -0
  204. package/dist/ui/sortable-table.js +103 -0
  205. package/dist/ui/sparkline.d.ts +27 -0
  206. package/dist/ui/sparkline.js +68 -0
  207. package/dist/ui/stat-grid.d.ts +55 -0
  208. package/dist/ui/stat-grid.js +87 -0
  209. package/dist/ui/status-dot.d.ts +13 -0
  210. package/dist/ui/status-dot.js +51 -0
  211. package/dist/ui/step-list.d.ts +21 -0
  212. package/dist/ui/step-list.js +77 -0
  213. package/dist/ui/switch.d.ts +23 -0
  214. package/dist/ui/switch.js +29 -0
  215. package/dist/ui/table.d.ts +8 -0
  216. package/dist/ui/table.js +48 -0
  217. package/dist/ui/tabs.d.ts +44 -0
  218. package/dist/ui/tabs.js +101 -0
  219. package/dist/ui/textarea.d.ts +8 -0
  220. package/dist/ui/textarea.js +32 -0
  221. package/dist/ui/theme-provider.d.ts +36 -0
  222. package/dist/ui/theme-provider.js +126 -0
  223. package/dist/ui/theme-toggle.d.ts +11 -0
  224. package/dist/ui/theme-toggle.js +32 -0
  225. package/dist/ui/thinking-indicator.d.ts +7 -0
  226. package/dist/ui/thinking-indicator.js +19 -0
  227. package/dist/ui/thinking-label.d.ts +19 -0
  228. package/dist/ui/thinking-label.js +54 -0
  229. package/dist/ui/thinking-mark.d.ts +10 -0
  230. package/dist/ui/thinking-mark.js +153 -0
  231. package/dist/ui/time-series-chart.d.ts +55 -0
  232. package/dist/ui/time-series-chart.js +249 -0
  233. package/dist/ui/toast.d.ts +16 -0
  234. package/dist/ui/toast.js +41 -0
  235. package/dist/ui/token-mosaic.d.ts +22 -0
  236. package/dist/ui/token-mosaic.js +65 -0
  237. package/dist/ui/tool-block.d.ts +54 -0
  238. package/dist/ui/tool-block.js +170 -0
  239. package/dist/ui/tool-narrative.d.ts +22 -0
  240. package/dist/ui/tool-narrative.js +77 -0
  241. package/dist/ui/tool-picker.d.ts +44 -0
  242. package/dist/ui/tool-picker.js +200 -0
  243. package/dist/ui/tooltip.d.ts +21 -0
  244. package/dist/ui/tooltip.js +48 -0
  245. package/dist/ui/top-bar.d.ts +53 -0
  246. package/dist/ui/top-bar.js +98 -0
  247. package/dist/ui/trace-waterfall.d.ts +38 -0
  248. package/dist/ui/trace-waterfall.js +121 -0
  249. package/dist/ui/typing-indicator.d.ts +9 -0
  250. package/dist/ui/typing-indicator.js +25 -0
  251. package/dist/ui/view-toggle.d.ts +17 -0
  252. package/dist/ui/view-toggle.js +46 -0
  253. package/dist/ui/voice-waveform.d.ts +16 -0
  254. package/dist/ui/voice-waveform.js +23 -0
  255. package/package.json +583 -0
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import type { AgentIdentity, ChatMessage } from "../lib/chat-message.js";
3
+ export type ChatThreadProps = {
4
+ readonly messages: readonly ChatMessage[];
5
+ readonly identity: AgentIdentity;
6
+ /** Rich body for one message — a markdown renderer, if the host has one. */
7
+ readonly renderBody?: (message: ChatMessage) => ReactNode;
8
+ readonly onRetry?: (message: ChatMessage) => void;
9
+ /** Shown when there are no messages: a greeting, suggestions, anything. */
10
+ readonly empty?: ReactNode;
11
+ readonly now?: number;
12
+ readonly className?: string;
13
+ };
14
+ /**
15
+ * The transcript.
16
+ *
17
+ * Messages are a prop, not a `DataPort` collection, and that is the one place
18
+ * this registry departs from its own rule. A transcript is live, host-owned,
19
+ * append-and-mutate state — the last message changes on every token — whereas a
20
+ * `DataPort` collection is a cached, paginated, refetchable thing. The *thread
21
+ * list* is a real collection and does use the port; a streaming conversation is
22
+ * not one. The ticket asks for message data as props for the same reason.
23
+ *
24
+ * It pins to the bottom only when the user is already there. Yanking someone
25
+ * back down while they are reading earlier context is the single most common
26
+ * way chat scrolling goes wrong.
27
+ */
28
+ export declare function ChatThread({ messages, identity, renderBody, onRetry, empty, now, className, }: ChatThreadProps): import("react").JSX.Element;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { cn } from "../lib/utils.js";
4
+ import { AgentTurn } from "./agent-turn.js";
5
+ import { MessageBubble } from "./message-bubble.js";
6
+ /**
7
+ * The transcript.
8
+ *
9
+ * Messages are a prop, not a `DataPort` collection, and that is the one place
10
+ * this registry departs from its own rule. A transcript is live, host-owned,
11
+ * append-and-mutate state — the last message changes on every token — whereas a
12
+ * `DataPort` collection is a cached, paginated, refetchable thing. The *thread
13
+ * list* is a real collection and does use the port; a streaming conversation is
14
+ * not one. The ticket asks for message data as props for the same reason.
15
+ *
16
+ * It pins to the bottom only when the user is already there. Yanking someone
17
+ * back down while they are reading earlier context is the single most common
18
+ * way chat scrolling goes wrong.
19
+ */ export function ChatThread({ messages, identity, renderBody, onRetry, empty, now, className }) {
20
+ const scrollRef = useRef(null);
21
+ const pinnedRef = useRef(true);
22
+ useEffect(()=>{
23
+ const node = scrollRef.current;
24
+ if (node === null || !pinnedRef.current) return;
25
+ node.scrollTop = node.scrollHeight;
26
+ }, [
27
+ messages
28
+ ]);
29
+ if (messages.length === 0 && empty !== undefined) {
30
+ return /*#__PURE__*/ _jsx("div", {
31
+ className: cn("flex min-h-0 flex-1 items-center justify-center p-6", className),
32
+ children: empty
33
+ });
34
+ }
35
+ return /*#__PURE__*/ _jsx("div", {
36
+ ref: scrollRef,
37
+ onScroll: (event)=>{
38
+ const node = event.currentTarget;
39
+ // A few pixels of slack: sub-pixel layout means an exact comparison is
40
+ // false on plenty of screens that are, visually, at the bottom.
41
+ pinnedRef.current = node.scrollHeight - node.scrollTop - node.clientHeight < 32;
42
+ },
43
+ // `log` + polite: new turns are announced without interrupting, which is
44
+ // what an arriving reply is.
45
+ role: "log",
46
+ "aria-live": "polite",
47
+ "aria-label": "Conversation",
48
+ className: cn("flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto p-4", className),
49
+ children: messages.map((message)=>message.role === "agent" ? /*#__PURE__*/ _jsx(AgentTurn, {
50
+ message: message,
51
+ identity: identity,
52
+ now: now,
53
+ children: renderBody?.(message)
54
+ }, message.id) : /*#__PURE__*/ _jsx(MessageBubble, {
55
+ message: message,
56
+ now: now,
57
+ onRetry: onRetry === undefined ? undefined : ()=>onRetry(message),
58
+ children: renderBody?.(message)
59
+ }, message.id))
60
+ });
61
+ }
@@ -0,0 +1,25 @@
1
+ export type CheckboxProps = {
2
+ readonly checked: boolean;
3
+ readonly onCheckedChange: (checked: boolean) => void;
4
+ /**
5
+ * Row label. Omit to render just the control, with `id`/`describedBy`
6
+ * passthrough — for composing into an externally labeled row, the way
7
+ * `ToggleList` composes `Switch`.
8
+ */
9
+ readonly label?: string;
10
+ /** Explanatory text under the label, tied to the control via `aria-describedby`. Labeled-row mode only. */
11
+ readonly description?: string;
12
+ /**
13
+ * A checkbox is neither on nor off — some of its children are checked and
14
+ * some are not. There is no HTML attribute for this: it exists only as a DOM
15
+ * property, so it has to be set imperatively via a ref.
16
+ */
17
+ readonly indeterminate?: boolean;
18
+ readonly id?: string;
19
+ readonly disabled?: boolean;
20
+ readonly invalid?: boolean;
21
+ /** Id of external text describing the control, for `aria-describedby`. Bare-control mode only. */
22
+ readonly describedBy?: string;
23
+ readonly className?: string;
24
+ };
25
+ export declare function Checkbox({ checked, onCheckedChange, label, description, indeterminate, id, disabled, invalid, describedBy, className, }: CheckboxProps): import("react").JSX.Element;
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Check, Minus } from "lucide-react";
3
+ import { useEffect, useId, useRef } from "react";
4
+ import { cn } from "../lib/utils.js";
5
+ const CONTROL_CLASS = "relative mt-0.5 flex size-4 shrink-0 items-center justify-center";
6
+ export function Checkbox({ checked, onCheckedChange, label, description, indeterminate = false, id, disabled = false, invalid = false, describedBy, className }) {
7
+ const generatedId = useId();
8
+ const checkboxId = id ?? generatedId;
9
+ const descriptionId = description === undefined ? undefined : `${checkboxId}-description`;
10
+ const ref = useRef(null);
11
+ const isBare = label === undefined;
12
+ useEffect(()=>{
13
+ if (ref.current) ref.current.indeterminate = indeterminate;
14
+ }, [
15
+ indeterminate
16
+ ]);
17
+ const control = /*#__PURE__*/ _jsxs("div", {
18
+ className: cn(CONTROL_CLASS, isBare && className),
19
+ children: [
20
+ /*#__PURE__*/ _jsx("input", {
21
+ ref: ref,
22
+ type: "checkbox",
23
+ id: checkboxId,
24
+ checked: checked,
25
+ disabled: disabled,
26
+ "aria-describedby": isBare ? describedBy : descriptionId,
27
+ "aria-invalid": invalid,
28
+ onChange: (event)=>onCheckedChange(event.target.checked),
29
+ className: cn("peer size-4 shrink-0 appearance-none rounded-sm border border-input bg-card shadow-xs transition-colors", "checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary", "aria-invalid:border-destructive disabled:cursor-not-allowed disabled:opacity-50")
30
+ }),
31
+ /*#__PURE__*/ _jsx(Check, {
32
+ "aria-hidden": true,
33
+ className: "pointer-events-none absolute size-3 scale-0 text-primary-foreground transition-transform peer-checked:scale-100"
34
+ }),
35
+ /*#__PURE__*/ _jsx(Minus, {
36
+ "aria-hidden": true,
37
+ className: "pointer-events-none absolute size-3 scale-0 text-primary-foreground transition-transform peer-indeterminate:scale-100 peer-checked:scale-0"
38
+ })
39
+ ]
40
+ });
41
+ if (isBare) return control;
42
+ return /*#__PURE__*/ _jsxs("label", {
43
+ htmlFor: checkboxId,
44
+ className: cn("flex items-start gap-2", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", className),
45
+ children: [
46
+ control,
47
+ /*#__PURE__*/ _jsxs("span", {
48
+ className: "flex flex-col gap-0.5",
49
+ children: [
50
+ /*#__PURE__*/ _jsx("span", {
51
+ className: "text-sm font-medium",
52
+ children: label
53
+ }),
54
+ description === undefined ? null : /*#__PURE__*/ _jsx("span", {
55
+ id: descriptionId,
56
+ className: "text-xs leading-relaxed text-muted-foreground",
57
+ children: description
58
+ })
59
+ ]
60
+ })
61
+ ]
62
+ });
63
+ }
@@ -0,0 +1,100 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * One result row. `title` (and optional `subtitle`) are the only text ever
4
+ * rendered — a consumer that hands in a raw identifier as the title has
5
+ * nothing else to blame, but the component itself never falls back to `id`
6
+ * for display.
7
+ */
8
+ export type CommandPaletteItem = {
9
+ readonly id: string;
10
+ readonly title: string;
11
+ readonly subtitle?: string;
12
+ readonly icon?: ReactNode;
13
+ };
14
+ /** A labelled cluster of results — a static command section, an entity kind, whatever the consumer's data shape is. */
15
+ export type CommandPaletteGroup = {
16
+ readonly id: string;
17
+ readonly heading: string;
18
+ readonly items: readonly CommandPaletteItem[];
19
+ };
20
+ /**
21
+ * Everything both presentations of the palette need: the query, the results,
22
+ * and what a selection means. Neither surface knows what any of it represents
23
+ * — matching, ranking and pagination source stay with the caller.
24
+ */
25
+ export type CommandPaletteContentProps = {
26
+ readonly query: string;
27
+ readonly onQueryChange: (query: string) => void;
28
+ /** Pre-grouped, pre-ordered results. Ranking and matching are the consumer's job. */
29
+ readonly groups: readonly CommandPaletteGroup[];
30
+ readonly onSelect: (id: string) => void;
31
+ readonly loading?: boolean;
32
+ /** A message to show in place of results; presence alone marks the errored state. */
33
+ readonly error?: ReactNode;
34
+ readonly hasMore?: boolean;
35
+ readonly onLoadMore?: () => void;
36
+ readonly placeholder?: string;
37
+ /** Leading content in the input row — a scope chip, a source badge. Sits before the input, inside the same row. */
38
+ readonly inputAccessory?: ReactNode;
39
+ /** A legend or hint strip pinned under the results, e.g. keyboard shortcut hints. */
40
+ readonly footer?: ReactNode;
41
+ };
42
+ export type CommandPaletteProps = CommandPaletteContentProps & {
43
+ readonly open: boolean;
44
+ readonly onOpenChange: (open: boolean) => void;
45
+ readonly className?: string;
46
+ };
47
+ export type CommandPaletteInlineProps = CommandPaletteContentProps & {
48
+ readonly open: boolean;
49
+ readonly onOpenChange: (open: boolean) => void;
50
+ /**
51
+ * Rendered inside the field, ahead of the input, in both states — the
52
+ * control the field morphs out of and back into. It stays mounted while
53
+ * collapsed, which is what lets focus return to it on Escape.
54
+ */
55
+ readonly leading?: ReactNode;
56
+ readonly className?: string;
57
+ };
58
+ /**
59
+ * The palette as a modal dialog: a search input over consumer-supplied,
60
+ * pre-grouped results, centered over an overlay. Use it where search is a
61
+ * destination the user goes to. Where search is a control that lives in the
62
+ * chrome, reach for `CommandPaletteInline` instead — it is the same machine
63
+ * without the overlay.
64
+ *
65
+ * Motion is the dialog's own opacity-only fade (`Dialog`'s `corbits-fade-in`/
66
+ * `corbits-fade-out` keyframes), which already collapses under
67
+ * `prefers-reduced-motion` at the theme level — a palette opened many times a
68
+ * day earns no scale or position animation on top of that.
69
+ */
70
+ export declare function CommandPalette({ open, onOpenChange, query, onQueryChange, groups, onSelect, loading, error, hasMore, onLoadMore, placeholder, inputAccessory, footer, className, }: CommandPaletteProps): import("react").JSX.Element;
71
+ /**
72
+ * The palette as an inline, non-modal surface: the field sits wherever the
73
+ * consumer puts it — a top bar, a toolbar — and its results hang directly
74
+ * beneath, anchored to that field. No overlay, no focus trap, so the page
75
+ * behind stays visible and legible while the user searches it.
76
+ *
77
+ * This is the shape a magnifier that *morphs in place* needs. A dialog cannot
78
+ * be morphed into: it is somewhere else on the screen, over everything, and
79
+ * the control it opened from is behind an overlay. Because the field is the
80
+ * real input here, the collapsed control and the expanded bar are one
81
+ * continuous element rather than a button and a separate window.
82
+ *
83
+ * The `leading` slot stays mounted in both states — collapsing removes the
84
+ * input and the panel, never the control focus has to return to. Sizing and
85
+ * the morph itself are the consumer's: this component paints the panel and
86
+ * owns the keyboard and dismissal contract, and exposes `data-slot`
87
+ * hooks (`command-palette-inline`, `-field`, `-results`) for the rest.
88
+ */
89
+ export declare function CommandPaletteInline({ open, onOpenChange, query, onQueryChange, groups, onSelect, loading, error, hasMore, onLoadMore, placeholder, inputAccessory, leading, footer, className, }: CommandPaletteInlineProps): import("react").JSX.Element;
90
+ /**
91
+ * Binds ⌘K / Ctrl-K to a toggle.
92
+ *
93
+ * A hook and not built into the palette, because plenty of apps already own
94
+ * their global key handling and a second listener fighting theirs is worse
95
+ * than no listener at all. It ignores repeats from a held key and lets the
96
+ * browser keep its own ⌘K where the user is typing into a text field —
97
+ * stealing it there is how a palette becomes the thing that eats your search
98
+ * box.
99
+ */
100
+ export declare function useCommandShortcut(onToggle: () => void, enabled?: boolean): void;
@@ -0,0 +1,341 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useId, useMemo, useRef } from "react";
3
+ import { cn } from "../lib/utils.js";
4
+ import { useCommandPaletteNavigation } from "../hooks/use-command-palette-navigation.js";
5
+ import { Dialog, DialogContent, DialogTitle } from "./dialog.js";
6
+ function flatten(groups) {
7
+ return groups.flatMap((group)=>group.items);
8
+ }
9
+ function statusMessage(loading, error, hasQuery) {
10
+ if (error !== undefined) return error;
11
+ if (loading) return "Searching…";
12
+ if (hasQuery) return "No matches found.";
13
+ return "Type to search.";
14
+ }
15
+ /**
16
+ * The half of the palette that has nothing to do with how it is presented:
17
+ * the flattened result order, the keyboard driver over it, and the ids that
18
+ * wire the input to the listbox. Shared so the dialog and the inline surface
19
+ * can never drift into two different keyboard contracts.
20
+ */ function usePaletteMachine(groups, onSelect, onClose) {
21
+ const baseId = useId();
22
+ const listRef = useRef(null);
23
+ const items = useMemo(()=>flatten(groups), [
24
+ groups
25
+ ]);
26
+ const select = (id)=>{
27
+ onSelect(id);
28
+ onClose();
29
+ };
30
+ const navigation = useCommandPaletteNavigation({
31
+ items,
32
+ onSelect: select,
33
+ onClose
34
+ });
35
+ useEffect(()=>{
36
+ listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({
37
+ block: "nearest"
38
+ });
39
+ }, [
40
+ navigation.activeId
41
+ ]);
42
+ return {
43
+ baseId,
44
+ groups,
45
+ items,
46
+ activeId: navigation.activeId,
47
+ setActiveId: navigation.setActiveId,
48
+ onKeyDown: navigation.onKeyDown,
49
+ select,
50
+ optionId: (id)=>`${baseId}-option-${id}`,
51
+ listRef
52
+ };
53
+ }
54
+ function PaletteInput({ machine, inputRef, query, onQueryChange, loading, placeholder, className }) {
55
+ return /*#__PURE__*/ _jsx("input", {
56
+ ref: inputRef,
57
+ type: "text",
58
+ role: "combobox",
59
+ "data-slot": "command-palette-input",
60
+ autoComplete: "off",
61
+ spellCheck: false,
62
+ placeholder: placeholder,
63
+ "aria-label": "Search",
64
+ "aria-expanded": true,
65
+ "aria-controls": `${machine.baseId}-list`,
66
+ "aria-activedescendant": machine.activeId === undefined ? undefined : machine.optionId(machine.activeId),
67
+ "aria-autocomplete": "list",
68
+ "aria-busy": loading,
69
+ value: query,
70
+ onChange: (event)=>onQueryChange(event.target.value),
71
+ className: className
72
+ });
73
+ }
74
+ function PaletteResults({ machine, query, loading, error, className }) {
75
+ const message = statusMessage(loading, error, query.trim().length > 0);
76
+ return /*#__PURE__*/ _jsx("ul", {
77
+ ref: machine.listRef,
78
+ id: `${machine.baseId}-list`,
79
+ role: "listbox",
80
+ "aria-label": "Results",
81
+ className: className,
82
+ children: machine.items.length === 0 ? /*#__PURE__*/ _jsx("li", {
83
+ role: "presentation",
84
+ className: "px-2 py-8 text-center text-sm text-muted-foreground",
85
+ children: message
86
+ }) : machine.groups.map((group)=>group.items.length === 0 ? null : /*#__PURE__*/ _jsxs("li", {
87
+ role: "presentation",
88
+ children: [
89
+ /*#__PURE__*/ _jsx("p", {
90
+ className: "px-2 py-1.5 text-[11px] font-semibold tracking-[0.1em] text-muted-foreground uppercase",
91
+ children: group.heading
92
+ }),
93
+ /*#__PURE__*/ _jsx("ul", {
94
+ role: "presentation",
95
+ children: group.items.map((item)=>/*#__PURE__*/ _jsxs("li", {
96
+ id: machine.optionId(item.id),
97
+ role: "option",
98
+ "aria-selected": item.id === machine.activeId,
99
+ "data-active": item.id === machine.activeId,
100
+ onPointerMove: ()=>machine.setActiveId(item.id),
101
+ onClick: ()=>machine.select(item.id),
102
+ className: cn("flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-sm", item.id === machine.activeId && "bg-muted"),
103
+ children: [
104
+ item.icon === undefined ? null : /*#__PURE__*/ _jsx("span", {
105
+ className: "grid size-4 shrink-0 place-items-center text-muted-foreground [&_svg]:size-4",
106
+ "aria-hidden": true,
107
+ children: item.icon
108
+ }),
109
+ /*#__PURE__*/ _jsxs("span", {
110
+ className: "min-w-0 flex-1",
111
+ children: [
112
+ /*#__PURE__*/ _jsx("span", {
113
+ className: "block truncate",
114
+ children: item.title
115
+ }),
116
+ item.subtitle === undefined ? null : /*#__PURE__*/ _jsx("span", {
117
+ className: "block truncate text-xs text-muted-foreground",
118
+ children: item.subtitle
119
+ })
120
+ ]
121
+ })
122
+ ]
123
+ }, item.id))
124
+ })
125
+ ]
126
+ }, group.id))
127
+ });
128
+ }
129
+ function PaletteLoadMore({ hasMore, onLoadMore, loading }) {
130
+ if (!hasMore || onLoadMore === undefined) return null;
131
+ return /*#__PURE__*/ _jsx("div", {
132
+ className: "border-t border-border p-2",
133
+ children: /*#__PURE__*/ _jsx("button", {
134
+ type: "button",
135
+ "data-slot": "command-palette-load-more",
136
+ onClick: onLoadMore,
137
+ disabled: loading,
138
+ className: "w-full rounded-md px-2.5 py-2 text-center text-xs font-medium text-muted-foreground hover:bg-muted disabled:opacity-50",
139
+ children: loading ? "Loading…" : "Load more"
140
+ })
141
+ });
142
+ }
143
+ function PaletteFooter({ footer }) {
144
+ if (footer === undefined) return null;
145
+ return /*#__PURE__*/ _jsx("div", {
146
+ "data-slot": "command-palette-footer",
147
+ className: "border-t border-border px-3 py-2 text-xs text-muted-foreground",
148
+ children: footer
149
+ });
150
+ }
151
+ /**
152
+ * The palette as a modal dialog: a search input over consumer-supplied,
153
+ * pre-grouped results, centered over an overlay. Use it where search is a
154
+ * destination the user goes to. Where search is a control that lives in the
155
+ * chrome, reach for `CommandPaletteInline` instead — it is the same machine
156
+ * without the overlay.
157
+ *
158
+ * Motion is the dialog's own opacity-only fade (`Dialog`'s `corbits-fade-in`/
159
+ * `corbits-fade-out` keyframes), which already collapses under
160
+ * `prefers-reduced-motion` at the theme level — a palette opened many times a
161
+ * day earns no scale or position animation on top of that.
162
+ */ export function CommandPalette({ open, onOpenChange, query, onQueryChange, groups, onSelect, loading = false, error, hasMore = false, onLoadMore, placeholder = "Search or jump to…", inputAccessory, footer, className }) {
163
+ const inputRef = useRef(null);
164
+ const machine = usePaletteMachine(groups, onSelect, ()=>onOpenChange(false));
165
+ useEffect(()=>{
166
+ if (open) inputRef.current?.focus();
167
+ }, [
168
+ open
169
+ ]);
170
+ return /*#__PURE__*/ _jsx(Dialog, {
171
+ open: open,
172
+ onOpenChange: onOpenChange,
173
+ children: /*#__PURE__*/ _jsxs(DialogContent, {
174
+ className: cn("max-w-xl gap-0 p-0 [&_[aria-label=Close]]:hidden", className),
175
+ onKeyDown: machine.onKeyDown,
176
+ children: [
177
+ /*#__PURE__*/ _jsx(DialogTitle, {
178
+ className: "sr-only",
179
+ children: "Search"
180
+ }),
181
+ /*#__PURE__*/ _jsxs("div", {
182
+ className: "flex h-12 shrink-0 items-center gap-2 border-b border-border px-4",
183
+ children: [
184
+ inputAccessory === undefined ? null : /*#__PURE__*/ _jsx("div", {
185
+ "data-slot": "command-palette-input-accessory",
186
+ className: "shrink-0",
187
+ children: inputAccessory
188
+ }),
189
+ /*#__PURE__*/ _jsx(PaletteInput, {
190
+ machine: machine,
191
+ inputRef: inputRef,
192
+ query: query,
193
+ onQueryChange: onQueryChange,
194
+ loading: loading,
195
+ placeholder: placeholder,
196
+ className: "h-full min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
197
+ })
198
+ ]
199
+ }),
200
+ /*#__PURE__*/ _jsx(PaletteResults, {
201
+ machine: machine,
202
+ query: query,
203
+ loading: loading,
204
+ error: error,
205
+ className: "max-h-96 min-h-0 flex-1 overflow-y-auto p-2"
206
+ }),
207
+ /*#__PURE__*/ _jsx(PaletteLoadMore, {
208
+ hasMore: hasMore,
209
+ onLoadMore: onLoadMore,
210
+ loading: loading
211
+ }),
212
+ /*#__PURE__*/ _jsx(PaletteFooter, {
213
+ footer: footer
214
+ })
215
+ ]
216
+ })
217
+ });
218
+ }
219
+ /**
220
+ * The palette as an inline, non-modal surface: the field sits wherever the
221
+ * consumer puts it — a top bar, a toolbar — and its results hang directly
222
+ * beneath, anchored to that field. No overlay, no focus trap, so the page
223
+ * behind stays visible and legible while the user searches it.
224
+ *
225
+ * This is the shape a magnifier that *morphs in place* needs. A dialog cannot
226
+ * be morphed into: it is somewhere else on the screen, over everything, and
227
+ * the control it opened from is behind an overlay. Because the field is the
228
+ * real input here, the collapsed control and the expanded bar are one
229
+ * continuous element rather than a button and a separate window.
230
+ *
231
+ * The `leading` slot stays mounted in both states — collapsing removes the
232
+ * input and the panel, never the control focus has to return to. Sizing and
233
+ * the morph itself are the consumer's: this component paints the panel and
234
+ * owns the keyboard and dismissal contract, and exposes `data-slot`
235
+ * hooks (`command-palette-inline`, `-field`, `-results`) for the rest.
236
+ */ export function CommandPaletteInline({ open, onOpenChange, query, onQueryChange, groups, onSelect, loading = false, error, hasMore = false, onLoadMore, placeholder = "Search or jump to…", inputAccessory, leading, footer, className }) {
237
+ const inputRef = useRef(null);
238
+ const rootRef = useRef(null);
239
+ const machine = usePaletteMachine(groups, onSelect, ()=>onOpenChange(false));
240
+ useEffect(()=>{
241
+ if (open) inputRef.current?.focus();
242
+ }, [
243
+ open
244
+ ]);
245
+ // Without an overlay to catch it, a press anywhere else is the dismissal.
246
+ // `pointerdown` rather than `click` so the field collapses as the user
247
+ // reaches for whatever they meant to press, not after it has been pressed.
248
+ useEffect(()=>{
249
+ if (!open) return;
250
+ function onPointerDown(event) {
251
+ if (rootRef.current?.contains(event.target) === true) return;
252
+ onOpenChange(false);
253
+ }
254
+ document.addEventListener("pointerdown", onPointerDown);
255
+ return ()=>document.removeEventListener("pointerdown", onPointerDown);
256
+ }, [
257
+ open,
258
+ onOpenChange
259
+ ]);
260
+ return /*#__PURE__*/ _jsxs("div", {
261
+ ref: rootRef,
262
+ "data-slot": "command-palette-inline",
263
+ "data-open": open,
264
+ className: cn("relative", className),
265
+ children: [
266
+ /*#__PURE__*/ _jsxs("div", {
267
+ "data-slot": "command-palette-inline-field",
268
+ onKeyDown: machine.onKeyDown,
269
+ children: [
270
+ leading,
271
+ open ? /*#__PURE__*/ _jsxs(_Fragment, {
272
+ children: [
273
+ inputAccessory === undefined ? null : /*#__PURE__*/ _jsx("div", {
274
+ "data-slot": "command-palette-input-accessory",
275
+ className: "shrink-0",
276
+ children: inputAccessory
277
+ }),
278
+ /*#__PURE__*/ _jsx(PaletteInput, {
279
+ machine: machine,
280
+ inputRef: inputRef,
281
+ query: query,
282
+ onQueryChange: onQueryChange,
283
+ loading: loading,
284
+ placeholder: placeholder,
285
+ className: "min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
286
+ })
287
+ ]
288
+ }) : null
289
+ ]
290
+ }),
291
+ open ? /*#__PURE__*/ _jsxs("div", {
292
+ "data-slot": "command-palette-inline-results",
293
+ onKeyDown: machine.onKeyDown,
294
+ className: "absolute top-full right-0 z-50 mt-1 flex w-96 max-w-[80vw] flex-col overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-lg",
295
+ children: [
296
+ /*#__PURE__*/ _jsx(PaletteResults, {
297
+ machine: machine,
298
+ query: query,
299
+ loading: loading,
300
+ error: error,
301
+ className: "max-h-96 min-h-0 flex-1 overflow-y-auto p-2"
302
+ }),
303
+ /*#__PURE__*/ _jsx(PaletteLoadMore, {
304
+ hasMore: hasMore,
305
+ onLoadMore: onLoadMore,
306
+ loading: loading
307
+ }),
308
+ /*#__PURE__*/ _jsx(PaletteFooter, {
309
+ footer: footer
310
+ })
311
+ ]
312
+ }) : null
313
+ ]
314
+ });
315
+ }
316
+ /**
317
+ * Binds ⌘K / Ctrl-K to a toggle.
318
+ *
319
+ * A hook and not built into the palette, because plenty of apps already own
320
+ * their global key handling and a second listener fighting theirs is worse
321
+ * than no listener at all. It ignores repeats from a held key and lets the
322
+ * browser keep its own ⌘K where the user is typing into a text field —
323
+ * stealing it there is how a palette becomes the thing that eats your search
324
+ * box.
325
+ */ export function useCommandShortcut(onToggle, enabled = true) {
326
+ useEffect(()=>{
327
+ if (!enabled) return;
328
+ const onKeyDown = (event)=>{
329
+ if (event.key.toLowerCase() !== "k" || !(event.metaKey || event.ctrlKey) || event.repeat) return;
330
+ const target = event.target;
331
+ if (target?.isContentEditable === true || /^(input|textarea|select)$/i.test(target?.tagName ?? "")) return;
332
+ event.preventDefault();
333
+ onToggle();
334
+ };
335
+ document.addEventListener("keydown", onKeyDown);
336
+ return ()=>document.removeEventListener("keydown", onKeyDown);
337
+ }, [
338
+ onToggle,
339
+ enabled
340
+ ]);
341
+ }
@@ -0,0 +1,52 @@
1
+ export type ComparisonVariant = {
2
+ readonly id: string;
3
+ /** "A" / "B", or a real name. Shown as the column heading. */
4
+ readonly label: string;
5
+ readonly content: string;
6
+ };
7
+ export type ComparisonCriterion = {
8
+ readonly label: string;
9
+ /** Score per variant id. A missing entry renders as "—", not as zero. */
10
+ readonly scores: Readonly<Record<string, number>>;
11
+ };
12
+ export type Comparison = {
13
+ readonly variants: readonly ComparisonVariant[];
14
+ /** The variant that won, if the comparison reached a verdict. */
15
+ readonly winnerId?: string;
16
+ /** Why it won, in prose. */
17
+ readonly rationale?: string;
18
+ readonly criteria?: readonly ComparisonCriterion[];
19
+ };
20
+ /**
21
+ * Reads a `Comparison` out of a JSON string, or returns `null`.
22
+ *
23
+ * Structural, not schema-driven: the registry ships no validation dependency,
24
+ * and this only has to be strict enough that a malformed payload falls back to
25
+ * showing the raw text rather than rendering half a comparison. It checks the
26
+ * fields it will actually index and nothing else — over-validating a viewer's
27
+ * input rejects payloads it could have drawn perfectly well.
28
+ */
29
+ export declare function parseComparison(json: string): Comparison | null;
30
+ /**
31
+ * Two or more variants of the same thing, side by side, with the verdict said
32
+ * out loud.
33
+ *
34
+ * The winner is announced three ways — a text badge, a `<mark>`-free bold
35
+ * label, and an `aria-label` on the column — and only incidentally by a tinted
36
+ * border. Colour cannot be the encoding here: "which one won" is the single
37
+ * fact the surface exists to communicate, and a reader who cannot see the tint
38
+ * would otherwise get a page of undifferentiated columns.
39
+ *
40
+ * Columns collapse to a stack below `md`. Side-by-side is the whole idea, but a
41
+ * 320px viewport showing two 140px columns compares nothing; stacked variants
42
+ * with their labels intact at least remain readable.
43
+ */
44
+ export declare function CompareBody({ comparison, className, }: {
45
+ readonly comparison: Comparison;
46
+ readonly className?: string;
47
+ }): import("react").JSX.Element;
48
+ /** The JSON-string entry point, for when the payload arrives as artifact content. */
49
+ export declare function CompareBodyFromJson({ content, pending }: {
50
+ readonly content: string;
51
+ readonly pending?: boolean;
52
+ }): import("react").JSX.Element;