@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,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../../lib/utils.js";
3
+ /**
4
+ * The two-column sign-in shell: a form on the left, a decorative panel on the
5
+ * right.
6
+ *
7
+ * The panel is hidden below `lg` rather than stacked. On a phone the form is
8
+ * the entire task, and a full-height decorative band above it means scrolling
9
+ * past the artwork to reach the thing you came to do.
10
+ *
11
+ * The form column is the semantic `<main>` and comes first in the DOM, so a
12
+ * screen reader and a keyboard both arrive at the form immediately. The panel
13
+ * is an `aria-hidden` sibling — it holds no information a signed-out visitor
14
+ * needs, and the quote inside it is atmosphere.
15
+ *
16
+ * `min-h-svh`, not `min-h-screen`. On mobile browsers `100vh` is the viewport
17
+ * *without* the retracting toolbar, so a `100vh` sign-in page is reliably a
18
+ * little too tall and the button sits just under the fold.
19
+ */ export function AuthLayout({ brand, children, panel, aside, className }) {
20
+ return /*#__PURE__*/ _jsxs("div", {
21
+ className: cn("grid min-h-svh bg-background lg:grid-cols-2", className),
22
+ children: [
23
+ /*#__PURE__*/ _jsxs("main", {
24
+ className: "relative flex flex-col",
25
+ children: [
26
+ brand === undefined ? null : /*#__PURE__*/ _jsx("div", {
27
+ className: "flex items-center gap-2.5 p-6 text-base font-semibold tracking-tight md:p-8",
28
+ children: brand
29
+ }),
30
+ /*#__PURE__*/ _jsx("div", {
31
+ className: "flex flex-1 items-center justify-center px-6 pb-16 md:px-10",
32
+ children: /*#__PURE__*/ _jsx("div", {
33
+ className: "w-full max-w-sm",
34
+ children: children
35
+ })
36
+ })
37
+ ]
38
+ }),
39
+ /*#__PURE__*/ _jsxs("div", {
40
+ "aria-hidden": true,
41
+ className: "relative hidden overflow-hidden bg-card lg:block",
42
+ children: [
43
+ panel,
44
+ aside === undefined ? null : /*#__PURE__*/ _jsx("div", {
45
+ className: "absolute inset-0 flex items-center justify-center p-12",
46
+ children: aside
47
+ })
48
+ ]
49
+ })
50
+ ]
51
+ });
52
+ }
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from "react";
2
+ export type AuthProvider = {
3
+ /** Passed straight back to `onProvider`. Yours to define. */
4
+ readonly id: string;
5
+ /** The whole button label: "Continue with Google". */
6
+ readonly label: string;
7
+ /**
8
+ * The provider's mark, if you have it.
9
+ *
10
+ * This registry ships no third-party logos — trademarks are not ours to
11
+ * redistribute, and any app that wants real marks already has a licensed
12
+ * source for them. The slot is here; the artwork is yours.
13
+ */
14
+ readonly icon?: ReactNode;
15
+ };
16
+ export type LoginFormMode = "sign-in" | "sign-up";
17
+ export type LoginFormProps = {
18
+ /** "sign-in" (default) shows email/password. "sign-up" adds a name field. */
19
+ readonly mode?: LoginFormMode;
20
+ readonly heading?: string;
21
+ /** Social / SSO options. Empty renders none, and no divider either. */
22
+ readonly providers?: readonly AuthProvider[];
23
+ readonly onProvider?: (id: string) => void;
24
+ /** Omit to render an SSO-only page with no email fields at all. */
25
+ readonly onSubmit?: (credentials: {
26
+ readonly name?: string;
27
+ readonly email: string;
28
+ readonly password: string;
29
+ }) => void;
30
+ /** True while a sign-in or sign-up is in flight. Disables every control. */
31
+ readonly busy?: boolean;
32
+ /** A failure to show. Announced, not just drawn. */
33
+ readonly error?: string | null;
34
+ /** Below the form — a "forgot password" link, terms copy. */
35
+ readonly footer?: ReactNode;
36
+ readonly className?: string;
37
+ };
38
+ /**
39
+ * The sign-in form. It renders and reports; it never authenticates.
40
+ *
41
+ * Every outcome leaves through a callback and every state arrives as a prop, so
42
+ * the block has no idea what a session is. That is what makes it installable:
43
+ * an auth block that imports a client is a block you have to gut before you can
44
+ * use it with yours.
45
+ *
46
+ * The error is `role="alert"`. A sign-in failure that is only drawn is a
47
+ * sign-in failure some users never learn about — they submit, nothing appears
48
+ * to happen, and they submit again. It is `aria-live="assertive"` precisely
49
+ * because interrupting is correct here.
50
+ *
51
+ * `autoComplete="email"` and `"current-password"` (`"new-password"` in
52
+ * sign-up mode) are not decoration: they are what lets a password manager
53
+ * fill this form, and a form password managers cannot fill pushes people
54
+ * toward worse passwords.
55
+ *
56
+ * The submit button reads "Signing in…" / "Signing up…" while busy rather
57
+ * than swapping to a spinner. The label is the status, so it is announced by
58
+ * the same mechanism that announces the button.
59
+ */
60
+ export declare function LoginForm({ mode, heading, providers, onProvider, onSubmit, busy, error, footer, className, }: LoginFormProps): import("react").JSX.Element;
@@ -0,0 +1,151 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { cn } from "../../lib/utils.js";
4
+ import { Button } from "../../ui/button.js";
5
+ import { Input } from "../../ui/input.js";
6
+ /**
7
+ * The sign-in form. It renders and reports; it never authenticates.
8
+ *
9
+ * Every outcome leaves through a callback and every state arrives as a prop, so
10
+ * the block has no idea what a session is. That is what makes it installable:
11
+ * an auth block that imports a client is a block you have to gut before you can
12
+ * use it with yours.
13
+ *
14
+ * The error is `role="alert"`. A sign-in failure that is only drawn is a
15
+ * sign-in failure some users never learn about — they submit, nothing appears
16
+ * to happen, and they submit again. It is `aria-live="assertive"` precisely
17
+ * because interrupting is correct here.
18
+ *
19
+ * `autoComplete="email"` and `"current-password"` (`"new-password"` in
20
+ * sign-up mode) are not decoration: they are what lets a password manager
21
+ * fill this form, and a form password managers cannot fill pushes people
22
+ * toward worse passwords.
23
+ *
24
+ * The submit button reads "Signing in…" / "Signing up…" while busy rather
25
+ * than swapping to a spinner. The label is the status, so it is announced by
26
+ * the same mechanism that announces the button.
27
+ */ export function LoginForm({ mode = "sign-in", heading = "Welcome back", providers = [], onProvider, onSubmit, busy = false, error = null, footer, className }) {
28
+ const [name, setName] = useState("");
29
+ const [email, setEmail] = useState("");
30
+ const [password, setPassword] = useState("");
31
+ const isSignUp = mode === "sign-up";
32
+ return /*#__PURE__*/ _jsxs("div", {
33
+ className: cn("flex flex-col gap-6", className),
34
+ children: [
35
+ /*#__PURE__*/ _jsx("h1", {
36
+ className: "text-3xl leading-none font-semibold tracking-tight text-balance",
37
+ children: heading
38
+ }),
39
+ error === null ? null : /*#__PURE__*/ _jsx("p", {
40
+ role: "alert",
41
+ "aria-live": "assertive",
42
+ className: "rounded-md border border-destructive px-3 py-2 text-sm text-destructive",
43
+ children: error
44
+ }),
45
+ onSubmit === undefined ? null : /*#__PURE__*/ _jsxs("form", {
46
+ className: "flex flex-col gap-4",
47
+ onSubmit: (event)=>{
48
+ event.preventDefault();
49
+ onSubmit(isSignUp ? {
50
+ name,
51
+ email,
52
+ password
53
+ } : {
54
+ email,
55
+ password
56
+ });
57
+ },
58
+ children: [
59
+ !isSignUp ? null : /*#__PURE__*/ _jsxs("label", {
60
+ className: "flex flex-col gap-1.5 text-sm font-medium",
61
+ children: [
62
+ "Name",
63
+ /*#__PURE__*/ _jsx(Input, {
64
+ type: "text",
65
+ name: "name",
66
+ autoComplete: "name",
67
+ placeholder: "Ada Lovelace",
68
+ value: name,
69
+ onChange: (event)=>setName(event.target.value),
70
+ disabled: busy,
71
+ required: true,
72
+ className: "h-11"
73
+ })
74
+ ]
75
+ }),
76
+ /*#__PURE__*/ _jsxs("label", {
77
+ className: "flex flex-col gap-1.5 text-sm font-medium",
78
+ children: [
79
+ "Email",
80
+ /*#__PURE__*/ _jsx(Input, {
81
+ type: "email",
82
+ name: "email",
83
+ autoComplete: "email",
84
+ placeholder: "you@example.com",
85
+ value: email,
86
+ onChange: (event)=>setEmail(event.target.value),
87
+ disabled: busy,
88
+ required: true,
89
+ className: "h-11"
90
+ })
91
+ ]
92
+ }),
93
+ /*#__PURE__*/ _jsxs("label", {
94
+ className: "flex flex-col gap-1.5 text-sm font-medium",
95
+ children: [
96
+ "Password",
97
+ /*#__PURE__*/ _jsx(Input, {
98
+ type: "password",
99
+ name: "password",
100
+ autoComplete: isSignUp ? "new-password" : "current-password",
101
+ value: password,
102
+ onChange: (event)=>setPassword(event.target.value),
103
+ disabled: busy,
104
+ required: true,
105
+ className: "h-11"
106
+ })
107
+ ]
108
+ }),
109
+ /*#__PURE__*/ _jsx(Button, {
110
+ type: "submit",
111
+ size: "lg",
112
+ disabled: busy,
113
+ children: busy ? isSignUp ? "Signing up…" : "Signing in…" : "Continue"
114
+ })
115
+ ]
116
+ }),
117
+ onSubmit === undefined || providers.length === 0 ? null : /*#__PURE__*/ _jsxs("div", {
118
+ className: "flex items-center gap-3",
119
+ children: [
120
+ /*#__PURE__*/ _jsx("span", {
121
+ "aria-hidden": true,
122
+ className: "h-px flex-1 bg-border"
123
+ }),
124
+ /*#__PURE__*/ _jsx("span", {
125
+ className: "text-xs tracking-wider text-muted-foreground uppercase",
126
+ children: "or"
127
+ }),
128
+ /*#__PURE__*/ _jsx("span", {
129
+ "aria-hidden": true,
130
+ className: "h-px flex-1 bg-border"
131
+ })
132
+ ]
133
+ }),
134
+ providers.map((provider)=>/*#__PURE__*/ _jsxs(Button, {
135
+ type: "button",
136
+ variant: "outline",
137
+ size: "lg",
138
+ disabled: busy,
139
+ onClick: ()=>onProvider?.(provider.id),
140
+ children: [
141
+ provider.icon,
142
+ provider.label
143
+ ]
144
+ }, provider.id)),
145
+ footer === undefined ? null : /*#__PURE__*/ _jsx("div", {
146
+ className: "text-sm text-muted-foreground",
147
+ children: footer
148
+ })
149
+ ]
150
+ });
151
+ }
@@ -0,0 +1,25 @@
1
+ import type { KeyboardEvent as ReactKeyboardEvent } from "react";
2
+ export type UseCommandPaletteNavigationOptions<TItem extends {
3
+ readonly id: string;
4
+ }> = {
5
+ /** The flat, visible result order — grouping is a display concern only. */
6
+ readonly items: readonly TItem[];
7
+ readonly onSelect: (id: string) => void;
8
+ readonly onClose: () => void;
9
+ };
10
+ export type UseCommandPaletteNavigationResult = {
11
+ readonly activeId: string | undefined;
12
+ readonly setActiveId: (id: string) => void;
13
+ readonly onKeyDown: (event: ReactKeyboardEvent) => void;
14
+ };
15
+ /**
16
+ * Keyboard driver for a command palette: Up/Down move the selection and wrap
17
+ * at the ends, Enter activates it, Escape closes without selecting.
18
+ *
19
+ * Selection is held as an id rather than an index so it survives the list
20
+ * reordering that happens every keystroke (fresh search results land at
21
+ * different positions) — an index would silently point at the wrong row.
22
+ */
23
+ export declare function useCommandPaletteNavigation<TItem extends {
24
+ readonly id: string;
25
+ }>({ items, onSelect, onClose, }: UseCommandPaletteNavigationOptions<TItem>): UseCommandPaletteNavigationResult;
@@ -0,0 +1,52 @@
1
+ import { useMemo, useState } from "react";
2
+ /**
3
+ * Keyboard driver for a command palette: Up/Down move the selection and wrap
4
+ * at the ends, Enter activates it, Escape closes without selecting.
5
+ *
6
+ * Selection is held as an id rather than an index so it survives the list
7
+ * reordering that happens every keystroke (fresh search results land at
8
+ * different positions) — an index would silently point at the wrong row.
9
+ */ export function useCommandPaletteNavigation({ items, onSelect, onClose }) {
10
+ const [requestedId, setRequestedId] = useState(undefined);
11
+ const activeId = useMemo(()=>{
12
+ if (items.length === 0) return undefined;
13
+ if (requestedId !== undefined && items.some((item)=>item.id === requestedId)) return requestedId;
14
+ return items[0]?.id;
15
+ }, [
16
+ items,
17
+ requestedId
18
+ ]);
19
+ function moveBy(delta) {
20
+ if (items.length === 0) return;
21
+ const currentIndex = activeId === undefined ? -1 : items.findIndex((item)=>item.id === activeId);
22
+ const nextIndex = (currentIndex + delta + items.length) % items.length;
23
+ setRequestedId(items[nextIndex]?.id);
24
+ }
25
+ function onKeyDown(event) {
26
+ switch(event.key){
27
+ case "ArrowDown":
28
+ event.preventDefault();
29
+ moveBy(1);
30
+ return;
31
+ case "ArrowUp":
32
+ event.preventDefault();
33
+ moveBy(-1);
34
+ return;
35
+ case "Enter":
36
+ event.preventDefault();
37
+ if (activeId !== undefined) onSelect(activeId);
38
+ return;
39
+ case "Escape":
40
+ event.preventDefault();
41
+ onClose();
42
+ return;
43
+ default:
44
+ return;
45
+ }
46
+ }
47
+ return {
48
+ activeId,
49
+ setActiveId: setRequestedId,
50
+ onKeyDown
51
+ };
52
+ }
@@ -0,0 +1,28 @@
1
+ export type ControllableStateAction<T> = T | ((previous: T) => T);
2
+ export type UseControllableStateOptions<T> = {
3
+ /** Controlled value. Supply this with `onChange` to lift state to a parent. */
4
+ readonly value?: T;
5
+ /** Seeds the hook's own state when `value` is not supplied. */
6
+ readonly defaultValue: T;
7
+ readonly onChange?: (value: T) => void;
8
+ /** Identifies the caller in the dev-mode controlled/uncontrolled warning below. */
9
+ readonly name: string;
10
+ };
11
+ /**
12
+ * The controlled-or-uncontrolled resolution every prop pair shaped like
13
+ * `open`/`onOpenChange` needs: track state internally, but defer to `value`
14
+ * whenever the caller supplies one. `useDismissablePopover`, `ToolBlock` and
15
+ * `ToolNarrative` each want exactly this and previously each wrote their own
16
+ * ternary for it; this is the one place it lives now.
17
+ *
18
+ * The returned setter accepts a value or a React-style updater
19
+ * (`setOpen((prev) => !prev)`), same as `useState`.
20
+ *
21
+ * Like a native `<input>`, a caller is expected to pick controlled or
22
+ * uncontrolled once and keep it for the component's lifetime. Switching
23
+ * between them — `value` present on one render, `undefined` on the next —
24
+ * logs a `console.error` in non-production builds, mirroring React's own
25
+ * controlled/uncontrolled `<input>` warning; it does not throw, and the
26
+ * production build is silent.
27
+ */
28
+ export declare function useControllableState<T>(options: UseControllableStateOptions<T>): readonly [T, (action: ControllableStateAction<T>) => void];
@@ -0,0 +1,44 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ const isProduction = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
3
+ /**
4
+ * The controlled-or-uncontrolled resolution every prop pair shaped like
5
+ * `open`/`onOpenChange` needs: track state internally, but defer to `value`
6
+ * whenever the caller supplies one. `useDismissablePopover`, `ToolBlock` and
7
+ * `ToolNarrative` each want exactly this and previously each wrote their own
8
+ * ternary for it; this is the one place it lives now.
9
+ *
10
+ * The returned setter accepts a value or a React-style updater
11
+ * (`setOpen((prev) => !prev)`), same as `useState`.
12
+ *
13
+ * Like a native `<input>`, a caller is expected to pick controlled or
14
+ * uncontrolled once and keep it for the component's lifetime. Switching
15
+ * between them — `value` present on one render, `undefined` on the next —
16
+ * logs a `console.error` in non-production builds, mirroring React's own
17
+ * controlled/uncontrolled `<input>` warning; it does not throw, and the
18
+ * production build is silent.
19
+ */ export function useControllableState(options) {
20
+ const { value: controlledValue, defaultValue, onChange, name } = options;
21
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
22
+ const isControlled = controlledValue !== undefined;
23
+ const value = isControlled ? controlledValue : uncontrolledValue;
24
+ const wasControlledRef = useRef(isControlled);
25
+ if (!isProduction && wasControlledRef.current !== isControlled) {
26
+ const from = wasControlledRef.current ? "controlled" : "uncontrolled";
27
+ const to = isControlled ? "controlled" : "uncontrolled";
28
+ console.error(`${name} is changing from ${from} to ${to}. Components should not switch between controlled ` + "and uncontrolled across their lifetime. Decide between using a controlled or uncontrolled " + `value for the lifetime of the component (i.e. always pass a value to \`${name}\`, or never do).`);
29
+ }
30
+ wasControlledRef.current = isControlled;
31
+ const setValue = useCallback((action)=>{
32
+ const resolved = typeof action === "function" ? action(value) : action;
33
+ if (!isControlled) setUncontrolledValue(resolved);
34
+ onChange?.(resolved);
35
+ }, [
36
+ isControlled,
37
+ value,
38
+ onChange
39
+ ]);
40
+ return [
41
+ value,
42
+ setValue
43
+ ];
44
+ }
@@ -0,0 +1,9 @@
1
+ import { type RefObject } from "react";
2
+ /**
3
+ * Focuses the given element shortly after `active` turns true — the composer
4
+ * wants to autofocus once the dock has finished popping open, not while it is
5
+ * still mid-animation, so the delay is deliberate rather than a magic number.
6
+ * `delayMs` defaults to `CHAT_DOCK_AUTOFOCUS_DELAY_MS`, which is pinned `>=`
7
+ * the dock's own entrance duration so focus never lands mid-animation.
8
+ */
9
+ export declare function useDelayedAutofocus<T extends HTMLElement>(ref: RefObject<T | null>, active: boolean, delayMs?: number): void;
@@ -0,0 +1,20 @@
1
+ import { useEffect } from "react";
2
+ import { CHAT_DOCK_AUTOFOCUS_DELAY_MS } from "../ui/chat-dock-timing.js";
3
+ /**
4
+ * Focuses the given element shortly after `active` turns true — the composer
5
+ * wants to autofocus once the dock has finished popping open, not while it is
6
+ * still mid-animation, so the delay is deliberate rather than a magic number.
7
+ * `delayMs` defaults to `CHAT_DOCK_AUTOFOCUS_DELAY_MS`, which is pinned `>=`
8
+ * the dock's own entrance duration so focus never lands mid-animation.
9
+ */ export function useDelayedAutofocus(ref, active, delayMs = CHAT_DOCK_AUTOFOCUS_DELAY_MS) {
10
+ useEffect(()=>{
11
+ if (!active) return;
12
+ const timer = window.setTimeout(()=>ref.current?.focus(), delayMs);
13
+ return ()=>window.clearTimeout(timer);
14
+ // `ref` is intentionally excluded: it is a stable container, and including
15
+ // it would refocus on every render that happens to produce a new object.
16
+ }, [
17
+ active,
18
+ delayMs
19
+ ]);
20
+ }
@@ -0,0 +1,37 @@
1
+ import { type RefObject } from "react";
2
+ import { type ControllableStateAction } from "./use-controllable-state.js";
3
+ export type UseDismissablePopoverOptions = {
4
+ /** Controlled open flag. Supply this with `onOpenChange` to lift state to a parent. */
5
+ readonly open?: boolean;
6
+ readonly onOpenChange?: (open: boolean) => void;
7
+ /** Seeds the hook's own state when `open` is not supplied. */
8
+ readonly defaultOpen?: boolean;
9
+ /** Escape closes the popover and returns focus to the trigger. Default `true`. */
10
+ readonly closeOnEscape?: boolean;
11
+ };
12
+ export type UseDismissablePopoverResult<Root extends HTMLElement, Trigger extends HTMLElement> = {
13
+ readonly open: boolean;
14
+ readonly setOpen: (action: ControllableStateAction<boolean>) => void;
15
+ readonly rootRef: RefObject<Root | null>;
16
+ readonly triggerRef: RefObject<Trigger | null>;
17
+ /** Closes and returns focus to the trigger. */
18
+ readonly close: () => void;
19
+ };
20
+ /**
21
+ * The behavior every dismissable popover trigger shares: an open flag, a
22
+ * pointer-down outside `rootRef` that closes it, and (by default) Escape that
23
+ * closes it and returns focus to `triggerRef`. `NotificationsBell`,
24
+ * `TenantSelector` and `ThreadSwitcher` each hand-rolled this before it moved
25
+ * here — one hook now, not three near-identical `useEffect` pairs.
26
+ *
27
+ * Controlled when `open` is supplied (pair it with `onOpenChange`);
28
+ * uncontrolled otherwise, seeded from `defaultOpen`. A caller with its own
29
+ * Escape handling (a listbox that also closes on Tab, say) sets
30
+ * `closeOnEscape: false` and wires Escape itself.
31
+ *
32
+ * The controlled/uncontrolled resolution — including the dev-mode warning if
33
+ * a caller switches between them after mount — is `useControllableState`;
34
+ * this hook only adds the popover-specific dismissal behavior on top.
35
+ * `setOpen` takes a value or an updater, same as `useState`.
36
+ */
37
+ export declare function useDismissablePopover<Root extends HTMLElement = HTMLElement, Trigger extends HTMLElement = HTMLElement>(options?: UseDismissablePopoverOptions): UseDismissablePopoverResult<Root, Trigger>;
@@ -0,0 +1,64 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import { useControllableState } from "./use-controllable-state.js";
3
+ /**
4
+ * The behavior every dismissable popover trigger shares: an open flag, a
5
+ * pointer-down outside `rootRef` that closes it, and (by default) Escape that
6
+ * closes it and returns focus to `triggerRef`. `NotificationsBell`,
7
+ * `TenantSelector` and `ThreadSwitcher` each hand-rolled this before it moved
8
+ * here — one hook now, not three near-identical `useEffect` pairs.
9
+ *
10
+ * Controlled when `open` is supplied (pair it with `onOpenChange`);
11
+ * uncontrolled otherwise, seeded from `defaultOpen`. A caller with its own
12
+ * Escape handling (a listbox that also closes on Tab, say) sets
13
+ * `closeOnEscape: false` and wires Escape itself.
14
+ *
15
+ * The controlled/uncontrolled resolution — including the dev-mode warning if
16
+ * a caller switches between them after mount — is `useControllableState`;
17
+ * this hook only adds the popover-specific dismissal behavior on top.
18
+ * `setOpen` takes a value or an updater, same as `useState`.
19
+ */ export function useDismissablePopover(options = {}) {
20
+ const { open: controlledOpen, onOpenChange, defaultOpen = false, closeOnEscape = true } = options;
21
+ const [open, setOpen] = useControllableState({
22
+ value: controlledOpen,
23
+ defaultValue: defaultOpen,
24
+ onChange: onOpenChange,
25
+ name: "useDismissablePopover"
26
+ });
27
+ const rootRef = useRef(null);
28
+ const triggerRef = useRef(null);
29
+ const close = useCallback(()=>{
30
+ setOpen(false);
31
+ triggerRef.current?.focus();
32
+ }, [
33
+ setOpen
34
+ ]);
35
+ useEffect(()=>{
36
+ if (!open) return;
37
+ const onPointerDown = (event)=>{
38
+ if (!(event.target instanceof Node)) return;
39
+ if (rootRef.current?.contains(event.target) === false) setOpen(false);
40
+ };
41
+ const onKeyDown = (event)=>{
42
+ if (!closeOnEscape || event.key !== "Escape") return;
43
+ close();
44
+ };
45
+ document.addEventListener("pointerdown", onPointerDown);
46
+ document.addEventListener("keydown", onKeyDown);
47
+ return ()=>{
48
+ document.removeEventListener("pointerdown", onPointerDown);
49
+ document.removeEventListener("keydown", onKeyDown);
50
+ };
51
+ }, [
52
+ open,
53
+ close,
54
+ setOpen,
55
+ closeOnEscape
56
+ ]);
57
+ return {
58
+ open,
59
+ setOpen,
60
+ rootRef,
61
+ triggerRef,
62
+ close
63
+ };
64
+ }
@@ -0,0 +1,66 @@
1
+ export type UseListSelectionOptions<TId extends PropertyKey> = {
2
+ /**
3
+ * The full, currently visible row order — range selection is anchored
4
+ * against this, and it is also what a stale selection gets reconciled
5
+ * against (see `selectedIds` below). Ids must be unique: a duplicate
6
+ * makes `indexOf` resolve to whichever occurrence comes first, so a
7
+ * shift-click range would sweep rows the person never actually crossed.
8
+ */
9
+ readonly ids: readonly TId[];
10
+ };
11
+ export type ToggleModifiers = {
12
+ /** Extends the anchored range through the clicked row, like a Finder/Sheets shift-click. */
13
+ readonly shiftKey?: boolean;
14
+ };
15
+ export type UseListSelectionResult<TId extends PropertyKey> = {
16
+ /**
17
+ * Selected ids, reconciled against the `ids` this call was given — an id
18
+ * a refetch or a filter change dropped from `ids` is filtered out here
19
+ * even though the hook still remembers it internally (see below), so
20
+ * this is always an honest subset of what is actually on screen.
21
+ */
22
+ readonly selectedIds: ReadonlySet<TId>;
23
+ readonly selectedCount: number;
24
+ readonly isSelected: (id: TId) => boolean;
25
+ /**
26
+ * Toggle one row. Plain click and cmd/ctrl-click both land here — the
27
+ * distinction between "replace selection" and "add to selection" is a
28
+ * caller-side concern this hook does not have an opinion on, since it
29
+ * never clears the rest of the selection on your behalf. Pass
30
+ * `shiftKey: true` to extend the last-toggled row's anchor through `id`
31
+ * instead of toggling `id` alone.
32
+ */
33
+ readonly toggle: (id: TId, modifiers?: ToggleModifiers) => void;
34
+ readonly selectAll: () => void;
35
+ readonly clear: () => void;
36
+ };
37
+ /**
38
+ * Headless selection state for a list/table: toggle-by-id, shift-click range
39
+ * selection anchored on the last row toggled outside a range, and
40
+ * select-all/clear. Renders nothing and owns no DOM — `SelectionCheckbox` and
41
+ * `BulkActionBar` are the rendered halves of this.
42
+ *
43
+ * The anchor is the last id passed to `toggle` *without* `shiftKey` — the
44
+ * same row a Finder/Sheets shift-click ranges from. A shift-click does not
45
+ * move the anchor, so a person can shift-click several times in a row (even
46
+ * in different directions) and each click re-ranges from the same start
47
+ * rather than from wherever they last landed.
48
+ *
49
+ * **Stale-id reconciliation.** The hook keeps whatever was selected even
50
+ * after a row drops out of `ids` (a refetch, a filter change) rather than
51
+ * pruning it immediately — pruning on every `ids` change would need an
52
+ * effect, and an extra render on top of whatever caused `ids` to change in
53
+ * the first place. Instead `selectedIds`/`selectedCount`/`isSelected` are
54
+ * reconciled against the current `ids` on every read, so a stale id can
55
+ * never inflate the count or reach the caller; it simply stops counting
56
+ * once its row is gone, and starts counting again if the same id reappears
57
+ * (e.g. a filter that got cleared) with no action needed from the caller.
58
+ *
59
+ * **Stable callbacks.** `toggle`/`selectAll`/`clear` read `ids` through a
60
+ * ref rather than closing over the `ids` array directly, so their identity
61
+ * stays stable across renders even when a caller passes a freshly allocated
62
+ * array each time (e.g. an inline `rows.map(row => row.id)`) — a memoized
63
+ * row downstream that depends on `toggle` does not re-render just because
64
+ * the caller didn't bother memoizing `ids`.
65
+ */
66
+ export declare function useListSelection<TId extends PropertyKey>({ ids, }: UseListSelectionOptions<TId>): UseListSelectionResult<TId>;