@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,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;
|