@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.
- package/LICENSE +501 -0
- package/README.md +125 -0
- package/dist/blocks/login/auth-layout.d.ts +38 -0
- package/dist/blocks/login/auth-layout.js +52 -0
- package/dist/blocks/login/login-form.d.ts +60 -0
- package/dist/blocks/login/login-form.js +151 -0
- package/dist/hooks/use-command-palette-navigation.d.ts +25 -0
- package/dist/hooks/use-command-palette-navigation.js +52 -0
- package/dist/hooks/use-controllable-state.d.ts +28 -0
- package/dist/hooks/use-controllable-state.js +44 -0
- package/dist/hooks/use-delayed-autofocus.d.ts +9 -0
- package/dist/hooks/use-delayed-autofocus.js +20 -0
- package/dist/hooks/use-dismissable-popover.d.ts +37 -0
- package/dist/hooks/use-dismissable-popover.js +64 -0
- package/dist/hooks/use-list-selection.d.ts +66 -0
- package/dist/hooks/use-list-selection.js +147 -0
- package/dist/hooks/use-prefers-reduced-motion.d.ts +13 -0
- package/dist/hooks/use-prefers-reduced-motion.js +28 -0
- package/dist/hooks/use-scroll-current-into-view.d.ts +19 -0
- package/dist/hooks/use-scroll-current-into-view.js +33 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +131 -0
- package/dist/lib/artifact.d.ts +72 -0
- package/dist/lib/artifact.js +67 -0
- package/dist/lib/chart-geometry.d.ts +62 -0
- package/dist/lib/chart-geometry.js +86 -0
- package/dist/lib/chart-palette.d.ts +76 -0
- package/dist/lib/chart-palette.js +92 -0
- package/dist/lib/chat-message.d.ts +115 -0
- package/dist/lib/chat-message.js +41 -0
- package/dist/lib/chat-parts.d.ts +64 -0
- package/dist/lib/chat-parts.js +16 -0
- package/dist/lib/csv.d.ts +60 -0
- package/dist/lib/csv.js +108 -0
- package/dist/lib/metrics.d.ts +70 -0
- package/dist/lib/metrics.js +101 -0
- package/dist/lib/now-item.d.ts +65 -0
- package/dist/lib/now-item.js +59 -0
- package/dist/lib/relative-time.d.ts +9 -0
- package/dist/lib/relative-time.js +47 -0
- package/dist/lib/theme.d.ts +30 -0
- package/dist/lib/theme.js +77 -0
- package/dist/lib/url.d.ts +26 -0
- package/dist/lib/url.js +37 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/lib/workflow-display-flow.d.ts +83 -0
- package/dist/lib/workflow-display-flow.js +104 -0
- package/dist/lib/workflow-registry.d.ts +90 -0
- package/dist/lib/workflow-registry.js +56 -0
- package/dist/lib/workflow-run-error.d.ts +31 -0
- package/dist/lib/workflow-run-error.js +105 -0
- package/dist/lib/workflow-run-progress.d.ts +97 -0
- package/dist/lib/workflow-run-progress.js +135 -0
- package/dist/lib/workflow-run.d.ts +43 -0
- package/dist/lib/workflow-run.js +56 -0
- package/dist/styles.css +3080 -0
- package/dist/test/render-hook.d.ts +14 -0
- package/dist/test/render-hook.js +38 -0
- package/dist/theme.css +628 -0
- package/dist/ui/agent-turn.d.ts +26 -0
- package/dist/ui/agent-turn.js +60 -0
- package/dist/ui/animated-number.d.ts +25 -0
- package/dist/ui/animated-number.js +63 -0
- package/dist/ui/approval-card.d.ts +46 -0
- package/dist/ui/approval-card.js +116 -0
- package/dist/ui/artifact-body.d.ts +26 -0
- package/dist/ui/artifact-body.js +170 -0
- package/dist/ui/artifact-notice.d.ts +21 -0
- package/dist/ui/artifact-notice.js +36 -0
- package/dist/ui/avatar.d.ts +37 -0
- package/dist/ui/avatar.js +73 -0
- package/dist/ui/badge.d.ts +10 -0
- package/dist/ui/badge.js +35 -0
- package/dist/ui/bar-chart.d.ts +38 -0
- package/dist/ui/bar-chart.js +118 -0
- package/dist/ui/block-card.d.ts +29 -0
- package/dist/ui/block-card.js +66 -0
- package/dist/ui/boot-screen.d.ts +23 -0
- package/dist/ui/boot-screen.js +49 -0
- package/dist/ui/bulk-action-bar.d.ts +37 -0
- package/dist/ui/bulk-action-bar.js +64 -0
- package/dist/ui/button.d.ts +12 -0
- package/dist/ui/button.js +73 -0
- package/dist/ui/card.d.ts +6 -0
- package/dist/ui/card.js +37 -0
- package/dist/ui/category-bars.d.ts +20 -0
- package/dist/ui/category-bars.js +69 -0
- package/dist/ui/chart-frame.d.ts +54 -0
- package/dist/ui/chart-frame.js +108 -0
- package/dist/ui/chat-composer.d.ts +24 -0
- package/dist/ui/chat-composer.js +32 -0
- package/dist/ui/chat-dock-timing.d.ts +15 -0
- package/dist/ui/chat-dock-timing.js +11 -0
- package/dist/ui/chat-input.d.ts +38 -0
- package/dist/ui/chat-input.js +138 -0
- package/dist/ui/chat-panel.d.ts +20 -0
- package/dist/ui/chat-panel.js +56 -0
- package/dist/ui/chat-thread.d.ts +28 -0
- package/dist/ui/chat-thread.js +61 -0
- package/dist/ui/checkbox.d.ts +25 -0
- package/dist/ui/checkbox.js +63 -0
- package/dist/ui/command-palette.d.ts +100 -0
- package/dist/ui/command-palette.js +341 -0
- package/dist/ui/compare-body.d.ts +52 -0
- package/dist/ui/compare-body.js +140 -0
- package/dist/ui/confirm-button.d.ts +29 -0
- package/dist/ui/confirm-button.js +41 -0
- package/dist/ui/corbits-mark.d.ts +29 -0
- package/dist/ui/corbits-mark.js +33 -0
- package/dist/ui/csv-table.d.ts +22 -0
- package/dist/ui/csv-table.js +85 -0
- package/dist/ui/dialog.d.ts +37 -0
- package/dist/ui/dialog.js +105 -0
- package/dist/ui/dictation-status-line.d.ts +20 -0
- package/dist/ui/dictation-status-line.js +38 -0
- package/dist/ui/dither-canvas.d.ts +31 -0
- package/dist/ui/dither-canvas.js +169 -0
- package/dist/ui/embed-body.d.ts +30 -0
- package/dist/ui/embed-body.js +92 -0
- package/dist/ui/empty-state.d.ts +26 -0
- package/dist/ui/empty-state.js +42 -0
- package/dist/ui/file-input.d.ts +25 -0
- package/dist/ui/file-input.js +66 -0
- package/dist/ui/filter-bar.d.ts +42 -0
- package/dist/ui/filter-bar.js +95 -0
- package/dist/ui/filter-chip.d.ts +14 -0
- package/dist/ui/filter-chip.js +23 -0
- package/dist/ui/gate-block.d.ts +16 -0
- package/dist/ui/gate-block.js +37 -0
- package/dist/ui/horizontal-stepper.d.ts +13 -0
- package/dist/ui/horizontal-stepper.js +65 -0
- package/dist/ui/input.d.ts +2 -0
- package/dist/ui/input.js +14 -0
- package/dist/ui/inspector-shell.d.ts +47 -0
- package/dist/ui/inspector-shell.js +98 -0
- package/dist/ui/intake-form.d.ts +39 -0
- package/dist/ui/intake-form.js +119 -0
- package/dist/ui/kind-card-grid.d.ts +23 -0
- package/dist/ui/kind-card-grid.js +43 -0
- package/dist/ui/library-page-header.d.ts +20 -0
- package/dist/ui/library-page-header.js +43 -0
- package/dist/ui/list-detail.d.ts +28 -0
- package/dist/ui/list-detail.js +55 -0
- package/dist/ui/live-run-banner.d.ts +13 -0
- package/dist/ui/live-run-banner.js +23 -0
- package/dist/ui/live-status-line.d.ts +22 -0
- package/dist/ui/live-status-line.js +37 -0
- package/dist/ui/menu.d.ts +18 -0
- package/dist/ui/menu.js +45 -0
- package/dist/ui/message-bubble.d.ts +24 -0
- package/dist/ui/message-bubble.js +64 -0
- package/dist/ui/mic-button.d.ts +35 -0
- package/dist/ui/mic-button.js +61 -0
- package/dist/ui/mic-permission-dialog.d.ts +20 -0
- package/dist/ui/mic-permission-dialog.js +50 -0
- package/dist/ui/notifications-bell.d.ts +28 -0
- package/dist/ui/notifications-bell.js +69 -0
- package/dist/ui/now-cards.d.ts +37 -0
- package/dist/ui/now-cards.js +116 -0
- package/dist/ui/page-panel.d.ts +37 -0
- package/dist/ui/page-panel.js +23 -0
- package/dist/ui/page-shell.d.ts +21 -0
- package/dist/ui/page-shell.js +26 -0
- package/dist/ui/parts-renderer.d.ts +22 -0
- package/dist/ui/parts-renderer.js +234 -0
- package/dist/ui/profile-card.d.ts +39 -0
- package/dist/ui/profile-card.js +124 -0
- package/dist/ui/progress-checklist.d.ts +25 -0
- package/dist/ui/progress-checklist.js +70 -0
- package/dist/ui/provider-mark.d.ts +32 -0
- package/dist/ui/provider-mark.js +31 -0
- package/dist/ui/quick-reply-chips.d.ts +14 -0
- package/dist/ui/quick-reply-chips.js +24 -0
- package/dist/ui/quote-card.d.ts +39 -0
- package/dist/ui/quote-card.js +66 -0
- package/dist/ui/reasoning-block.d.ts +35 -0
- package/dist/ui/reasoning-block.js +60 -0
- package/dist/ui/research-body.d.ts +70 -0
- package/dist/ui/research-body.js +270 -0
- package/dist/ui/rich-empty-state.d.ts +35 -0
- package/dist/ui/rich-empty-state.js +65 -0
- package/dist/ui/run-now-button.d.ts +23 -0
- package/dist/ui/run-now-button.js +42 -0
- package/dist/ui/section.d.ts +20 -0
- package/dist/ui/section.js +48 -0
- package/dist/ui/select.d.ts +2 -0
- package/dist/ui/select.js +22 -0
- package/dist/ui/selection-checkbox.d.ts +42 -0
- package/dist/ui/selection-checkbox.js +54 -0
- package/dist/ui/settings-panel.d.ts +35 -0
- package/dist/ui/settings-panel.js +68 -0
- package/dist/ui/shimmer-text.d.ts +19 -0
- package/dist/ui/shimmer-text.js +20 -0
- package/dist/ui/sidebar-item-row.d.ts +40 -0
- package/dist/ui/sidebar-item-row.js +64 -0
- package/dist/ui/sidebar-panel.d.ts +25 -0
- package/dist/ui/sidebar-panel.js +56 -0
- package/dist/ui/sidebar.d.ts +46 -0
- package/dist/ui/sidebar.js +111 -0
- package/dist/ui/skeleton.d.ts +13 -0
- package/dist/ui/skeleton.js +20 -0
- package/dist/ui/sortable-table.d.ts +43 -0
- package/dist/ui/sortable-table.js +103 -0
- package/dist/ui/sparkline.d.ts +27 -0
- package/dist/ui/sparkline.js +68 -0
- package/dist/ui/stat-grid.d.ts +55 -0
- package/dist/ui/stat-grid.js +87 -0
- package/dist/ui/status-dot.d.ts +13 -0
- package/dist/ui/status-dot.js +51 -0
- package/dist/ui/step-list.d.ts +21 -0
- package/dist/ui/step-list.js +77 -0
- package/dist/ui/switch.d.ts +23 -0
- package/dist/ui/switch.js +29 -0
- package/dist/ui/table.d.ts +8 -0
- package/dist/ui/table.js +48 -0
- package/dist/ui/tabs.d.ts +44 -0
- package/dist/ui/tabs.js +101 -0
- package/dist/ui/textarea.d.ts +8 -0
- package/dist/ui/textarea.js +32 -0
- package/dist/ui/theme-provider.d.ts +36 -0
- package/dist/ui/theme-provider.js +126 -0
- package/dist/ui/theme-toggle.d.ts +11 -0
- package/dist/ui/theme-toggle.js +32 -0
- package/dist/ui/thinking-indicator.d.ts +7 -0
- package/dist/ui/thinking-indicator.js +19 -0
- package/dist/ui/thinking-label.d.ts +19 -0
- package/dist/ui/thinking-label.js +54 -0
- package/dist/ui/thinking-mark.d.ts +10 -0
- package/dist/ui/thinking-mark.js +153 -0
- package/dist/ui/time-series-chart.d.ts +55 -0
- package/dist/ui/time-series-chart.js +249 -0
- package/dist/ui/toast.d.ts +16 -0
- package/dist/ui/toast.js +41 -0
- package/dist/ui/token-mosaic.d.ts +22 -0
- package/dist/ui/token-mosaic.js +65 -0
- package/dist/ui/tool-block.d.ts +54 -0
- package/dist/ui/tool-block.js +170 -0
- package/dist/ui/tool-narrative.d.ts +22 -0
- package/dist/ui/tool-narrative.js +77 -0
- package/dist/ui/tool-picker.d.ts +44 -0
- package/dist/ui/tool-picker.js +200 -0
- package/dist/ui/tooltip.d.ts +21 -0
- package/dist/ui/tooltip.js +48 -0
- package/dist/ui/top-bar.d.ts +53 -0
- package/dist/ui/top-bar.js +98 -0
- package/dist/ui/trace-waterfall.d.ts +38 -0
- package/dist/ui/trace-waterfall.js +121 -0
- package/dist/ui/typing-indicator.d.ts +9 -0
- package/dist/ui/typing-indicator.js +25 -0
- package/dist/ui/view-toggle.d.ts +17 -0
- package/dist/ui/view-toggle.js +46 -0
- package/dist/ui/voice-waveform.d.ts +16 -0
- package/dist/ui/voice-waveform.js +23 -0
- 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>;
|