@horon-ai/react 0.0.0-stage → 0.1.1
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/README.md +168 -2
- package/package.json +50 -5
- package/src/assets/sidebar-photo.jpg +0 -0
- package/src/components/agent-activity.tsx +204 -0
- package/src/components/agent-lane.tsx +80 -0
- package/src/components/agent-lanes-timeline.tsx +312 -0
- package/src/components/agent-panel.tsx +125 -0
- package/src/components/agent-steering-panel.tsx +276 -0
- package/src/components/approval-card.tsx +89 -0
- package/src/components/avatar.tsx +104 -0
- package/src/components/badge.tsx +63 -0
- package/src/components/banner.tsx +103 -0
- package/src/components/bot-instructions-panel.tsx +369 -0
- package/src/components/bot-list-row.tsx +42 -0
- package/src/components/bot-memory-panel.tsx +229 -0
- package/src/components/bottom-sheet.tsx +140 -0
- package/src/components/breadcrumb-header.tsx +87 -0
- package/src/components/breadcrumb.tsx +86 -0
- package/src/components/button.tsx +146 -0
- package/src/components/calendar.tsx +410 -0
- package/src/components/card.tsx +79 -0
- package/src/components/chat-bubble.tsx +36 -0
- package/src/components/chat-controls.tsx +118 -0
- package/src/components/checkbox.tsx +73 -0
- package/src/components/chip.tsx +81 -0
- package/src/components/choice-card.tsx +55 -0
- package/src/components/code-panel.tsx +27 -0
- package/src/components/combobox.tsx +312 -0
- package/src/components/command-palette.tsx +206 -0
- package/src/components/composer.tsx +124 -0
- package/src/components/data-table.tsx +429 -0
- package/src/components/date-field.tsx +100 -0
- package/src/components/dialog.tsx +93 -0
- package/src/components/directory-card.tsx +59 -0
- package/src/components/document-extract-panel.tsx +398 -0
- package/src/components/empty-state.tsx +29 -0
- package/src/components/field.tsx +78 -0
- package/src/components/file-drop.tsx +89 -0
- package/src/components/filter-bar.tsx +83 -0
- package/src/components/flue-background.tsx +388 -0
- package/src/components/horon-icon.tsx +280 -0
- package/src/components/horon-wordmark.tsx +26 -0
- package/src/components/icon-button.tsx +61 -0
- package/src/components/input-group.tsx +112 -0
- package/src/components/input.tsx +72 -0
- package/src/components/integration-row.tsx +103 -0
- package/src/components/kbd.tsx +42 -0
- package/src/components/knowledge-base-panel.tsx +185 -0
- package/src/components/liquid-glass.tsx +24 -0
- package/src/components/list-detail.tsx +275 -0
- package/src/components/marketplace-card.tsx +97 -0
- package/src/components/mcp-server-row.tsx +122 -0
- package/src/components/menu.tsx +165 -0
- package/src/components/message-extras.tsx +146 -0
- package/src/components/modal.tsx +108 -0
- package/src/components/orchestrator-panel.tsx +175 -0
- package/src/components/page-header.tsx +48 -0
- package/src/components/plan-card.tsx +70 -0
- package/src/components/plan-step.tsx +111 -0
- package/src/components/plugin-row.tsx +128 -0
- package/src/components/progress.tsx +21 -0
- package/src/components/radio-group.tsx +84 -0
- package/src/components/research-run-panel.tsx +232 -0
- package/src/components/schedule.tsx +332 -0
- package/src/components/segmented-control.tsx +136 -0
- package/src/components/select.tsx +154 -0
- package/src/components/settings-list.tsx +99 -0
- package/src/components/sidebar.tsx +542 -0
- package/src/components/skeleton.tsx +75 -0
- package/src/components/slider.tsx +29 -0
- package/src/components/source-row.tsx +112 -0
- package/src/components/spinner.tsx +29 -0
- package/src/components/split-button.tsx +72 -0
- package/src/components/switch.tsx +79 -0
- package/src/components/tab-bar.tsx +90 -0
- package/src/components/tabs.tsx +162 -0
- package/src/components/task-row.tsx +159 -0
- package/src/components/textarea-field.tsx +127 -0
- package/src/components/textarea.tsx +47 -0
- package/src/components/toast.tsx +128 -0
- package/src/components/tool-call.tsx +215 -0
- package/src/components/tool-calls-header.tsx +65 -0
- package/src/components/tool-permissions.tsx +105 -0
- package/src/components/tooltip.tsx +33 -0
- package/src/components/top-bar.tsx +65 -0
- package/src/components/usage-meter.tsx +47 -0
- package/src/components/workspace-menu.tsx +77 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/use-active-indicator.ts +60 -0
- package/src/hooks/use-color-scheme.ts +27 -0
- package/src/hooks/use-copy-to-clipboard.ts +32 -0
- package/src/hooks/use-device.ts +41 -0
- package/src/hooks/use-hotkey.ts +35 -0
- package/src/hooks/use-media-query.ts +25 -0
- package/src/hooks/use-platform.ts +39 -0
- package/src/hooks/use-reduced-motion.ts +8 -0
- package/src/index.ts +99 -0
- package/src/lib/cn.ts +25 -0
- package/src/lib/cursor-ghost.ts +244 -0
- package/src/lib/platform.ts +169 -0
- package/src/lib/size.ts +10 -0
- package/src/styles/globals.css +564 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Copy, FileText, X } from "@phosphor-icons/react";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { useCopyToClipboard } from "../hooks/use-copy-to-clipboard";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { IconButton } from "./icon-button";
|
|
7
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
8
|
+
|
|
9
|
+
/** Figma `Date separator` (Message extras 109:667): caption between two 10% ink lines. */
|
|
10
|
+
export function DateSeparator({ children, className }: { children: ReactNode; className?: string }) {
|
|
11
|
+
return (
|
|
12
|
+
<div role="separator" className={cn("flex items-center gap-3", className)}>
|
|
13
|
+
<span className="h-px flex-1 bg-overlay-ink-10" />
|
|
14
|
+
<span className="shrink-0 text-caption text-text-secondary">{children}</span>
|
|
15
|
+
<span className="h-px flex-1 bg-overlay-ink-10" />
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type MessageAction = { label: string; icon: ReactNode; onClick?: () => void };
|
|
21
|
+
|
|
22
|
+
/** Figma `Message actions` (109:667): a pill of 30px icon buttons with 16px icons, solid or regular glass. */
|
|
23
|
+
export function MessageActions({
|
|
24
|
+
actions,
|
|
25
|
+
surface = "solid",
|
|
26
|
+
className,
|
|
27
|
+
}: {
|
|
28
|
+
actions: MessageAction[];
|
|
29
|
+
/** `solid` (default) is an opaque surface pill with a default border; `glass` is the regular glass pill. */
|
|
30
|
+
surface?: "solid" | "glass";
|
|
31
|
+
className?: string;
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
role="toolbar"
|
|
36
|
+
className={cn(
|
|
37
|
+
"relative flex w-fit items-center gap-0.5 overflow-clip rounded-full p-[3px]",
|
|
38
|
+
surface === "glass" ? "bg-glass-regular" : "border border-border-default bg-bg-surface",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{surface === "glass" && <LiquidGlass />}
|
|
43
|
+
{actions.map((action) => (
|
|
44
|
+
<button
|
|
45
|
+
key={action.label}
|
|
46
|
+
type="button"
|
|
47
|
+
aria-label={action.label}
|
|
48
|
+
onClick={action.onClick}
|
|
49
|
+
className="relative flex size-7.5 motion-press cursor-pointer items-center justify-center rounded-full text-text-secondary motion-colors hover:bg-overlay-ink-6 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand [&>svg]:size-4"
|
|
50
|
+
>
|
|
51
|
+
{action.icon}
|
|
52
|
+
</button>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type AttachmentProps = {
|
|
59
|
+
name: ReactNode;
|
|
60
|
+
/** Caption, such as "PDF · 2.4 MB". */
|
|
61
|
+
detail?: ReactNode;
|
|
62
|
+
/** Image thumbnail URL; files show a document icon on a sky tile. */
|
|
63
|
+
thumbnail?: string;
|
|
64
|
+
onRemove?: () => void;
|
|
65
|
+
removeLabel?: string;
|
|
66
|
+
className?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Figma `Attachment` (109:667): a flat subtle card with a 40px thumb, name, detail and remove button. */
|
|
70
|
+
export function Attachment({ name, detail, thumbnail, onRemove, removeLabel = "Remove", className }: AttachmentProps) {
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"flex w-full max-w-70 min-w-0 motion-rise items-center gap-2.5 rounded-lg bg-bg-subtle py-2 pr-2 pl-2",
|
|
75
|
+
className,
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{thumbnail ? (
|
|
79
|
+
<span
|
|
80
|
+
aria-hidden
|
|
81
|
+
className="size-10 shrink-0 rounded-[10px] bg-cover bg-center"
|
|
82
|
+
style={{ backgroundImage: `url(${JSON.stringify(thumbnail)})` }}
|
|
83
|
+
/>
|
|
84
|
+
) : (
|
|
85
|
+
<span className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-accent-sky-500-16 text-text-brand">
|
|
86
|
+
<FileText aria-hidden className="size-5" />
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
<span className="flex min-w-0 flex-1 flex-col gap-px">
|
|
90
|
+
<span className="truncate text-label-md text-text-primary">{name}</span>
|
|
91
|
+
{detail && <span className="truncate text-caption text-text-secondary">{detail}</span>}
|
|
92
|
+
</span>
|
|
93
|
+
{onRemove && (
|
|
94
|
+
<IconButton
|
|
95
|
+
size="sm"
|
|
96
|
+
icon={<X aria-hidden />}
|
|
97
|
+
label={removeLabel}
|
|
98
|
+
onClick={onRemove}
|
|
99
|
+
className="text-text-secondary"
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type CodeBlockProps = Omit<ComponentProps<"div">, "className" | "children"> & {
|
|
107
|
+
code: string;
|
|
108
|
+
language?: string;
|
|
109
|
+
copyLabel?: string;
|
|
110
|
+
copiedLabel?: string;
|
|
111
|
+
className?: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Figma `Code block` (109:667): ink surface, 14px radius, 6% header with copy, Geist Mono 13/20 code. */
|
|
115
|
+
export function CodeBlock({
|
|
116
|
+
code,
|
|
117
|
+
language,
|
|
118
|
+
copyLabel = "Copy",
|
|
119
|
+
copiedLabel = "Copied",
|
|
120
|
+
className,
|
|
121
|
+
...props
|
|
122
|
+
}: CodeBlockProps) {
|
|
123
|
+
const { copied, copy } = useCopyToClipboard(1500);
|
|
124
|
+
return (
|
|
125
|
+
<div className={cn("flex flex-col overflow-clip rounded-lg bg-surface-ink", className)} {...props}>
|
|
126
|
+
<div className="flex items-center gap-2 bg-bg-surface-6 py-2 pr-3.5 pl-3.5 text-caption text-text-on-ink">
|
|
127
|
+
<span className="min-w-0 flex-1 opacity-70">{language}</span>
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={() => copy(code)}
|
|
131
|
+
className="flex shrink-0 motion-press cursor-pointer items-center gap-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
132
|
+
>
|
|
133
|
+
<Copy aria-hidden className="size-3.5 shrink-0" />
|
|
134
|
+
<span className="min-w-10 text-left">
|
|
135
|
+
<span key={copied ? "copied" : "copy"} className="inline-block animate-fade-in">
|
|
136
|
+
{copied ? copiedLabel : copyLabel}
|
|
137
|
+
</span>
|
|
138
|
+
</span>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
<pre className="overflow-x-auto px-3.5 pt-3 pb-3.5 font-mono text-code text-text-on-ink">
|
|
142
|
+
<code>{code}</code>
|
|
143
|
+
</pre>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Dialog as BaseDialog } from "@base-ui/react/dialog";
|
|
2
|
+
import { X } from "@phosphor-icons/react";
|
|
3
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
export const Modal = BaseDialog.Root;
|
|
8
|
+
export const ModalTrigger = BaseDialog.Trigger;
|
|
9
|
+
export const ModalClose = BaseDialog.Close;
|
|
10
|
+
|
|
11
|
+
export type ModalSize = "sm" | "md" | "lg";
|
|
12
|
+
export type ModalSurface = "solid" | "glass";
|
|
13
|
+
|
|
14
|
+
type DivProps = Omit<ComponentProps<"div">, "className"> & { className?: string };
|
|
15
|
+
|
|
16
|
+
export type ModalContentProps = Omit<ComponentProps<typeof BaseDialog.Popup>, "className" | "title"> & {
|
|
17
|
+
title: ReactNode;
|
|
18
|
+
description?: ReactNode;
|
|
19
|
+
/** Width: `sm` 400px, `md` 560px (default), `lg` 720px. Never wider than the viewport minus 32px. */
|
|
20
|
+
size?: ModalSize;
|
|
21
|
+
/** `solid` (default) is the opaque page fill with a 0 24 64 shadow; `glass` is the veil (`veil-overlay`). */
|
|
22
|
+
surface?: ModalSurface;
|
|
23
|
+
/** Tile or icon before the title. */
|
|
24
|
+
leading?: ReactNode;
|
|
25
|
+
/** Label for the close button; hides it when `null`. */
|
|
26
|
+
closeLabel?: string | null;
|
|
27
|
+
/** `ModalFooter`, kept below the scrolling body. */
|
|
28
|
+
footer?: ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const widthClass: Record<ModalSize, string> = {
|
|
33
|
+
sm: "w-full max-w-100",
|
|
34
|
+
md: "w-full max-w-140",
|
|
35
|
+
lg: "w-full max-w-180",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A modal for work that needs a form or a longer read, where `Dialog` is a short confirmation. It has three
|
|
40
|
+
* fixed parts: a header (title, description and close), a body that scrolls on its own once the modal reaches
|
|
41
|
+
* the viewport height, and a footer whose actions sit at the right. 24px padding, 20px radius, 32% scrim.
|
|
42
|
+
*/
|
|
43
|
+
export function ModalContent({
|
|
44
|
+
title,
|
|
45
|
+
description,
|
|
46
|
+
size = "md",
|
|
47
|
+
surface = "solid",
|
|
48
|
+
leading,
|
|
49
|
+
closeLabel = "Close",
|
|
50
|
+
footer,
|
|
51
|
+
className,
|
|
52
|
+
children,
|
|
53
|
+
...props
|
|
54
|
+
}: ModalContentProps) {
|
|
55
|
+
return (
|
|
56
|
+
<BaseDialog.Portal>
|
|
57
|
+
<BaseDialog.Backdrop className="fixed inset-0 z-50 bg-bg-scrim/32 motion-fade backdrop-blur-[6px]" />
|
|
58
|
+
<BaseDialog.Viewport className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
59
|
+
<BaseDialog.Popup
|
|
60
|
+
className={cn(
|
|
61
|
+
// The 16px viewport padding caps `w-full`, so the popup never exceeds
|
|
62
|
+
// the screen even on the narrowest phone.
|
|
63
|
+
"relative flex max-h-[calc(100dvh-2rem)] flex-col overflow-clip rounded-xl outline-none",
|
|
64
|
+
widthClass[size],
|
|
65
|
+
surface === "solid" ? "bg-bg-surface shadow-[0_24px_64px_#0A142433]" : "veil-overlay",
|
|
66
|
+
"motion-popup duration-240 data-starting-style:translate-y-2",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
<header className={cn("flex shrink-0 items-start px-6 pt-6 pb-4", leading ? "gap-3.5" : "gap-2")}>
|
|
72
|
+
{leading}
|
|
73
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
74
|
+
<BaseDialog.Title className="text-title-md text-text-primary">{title}</BaseDialog.Title>
|
|
75
|
+
{description && (
|
|
76
|
+
<BaseDialog.Description className="text-body-md text-text-secondary">
|
|
77
|
+
{description}
|
|
78
|
+
</BaseDialog.Description>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
{closeLabel !== null && (
|
|
82
|
+
<BaseDialog.Close
|
|
83
|
+
aria-label={closeLabel}
|
|
84
|
+
className="-mt-1 -mr-2 flex size-8 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full text-text-secondary motion-colors hover:bg-overlay-ink-6 hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand [&_svg]:size-4"
|
|
85
|
+
>
|
|
86
|
+
<X />
|
|
87
|
+
</BaseDialog.Close>
|
|
88
|
+
)}
|
|
89
|
+
</header>
|
|
90
|
+
<div
|
|
91
|
+
className={cn(
|
|
92
|
+
"min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 text-body-md text-text-primary",
|
|
93
|
+
footer ? "pb-2" : "pb-6",
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
{footer}
|
|
99
|
+
</BaseDialog.Popup>
|
|
100
|
+
</BaseDialog.Viewport>
|
|
101
|
+
</BaseDialog.Portal>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The modal's action row: actions at the right, secondary first and primary last, 8px apart, 24px padding. */
|
|
106
|
+
export function ModalFooter({ className, ...props }: DivProps) {
|
|
107
|
+
return <div className={cn("flex shrink-0 items-center justify-end gap-2 px-6 pt-4 pb-6", className)} {...props} />;
|
|
108
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Stop } from "@phosphor-icons/react";
|
|
2
|
+
import { Children, isValidElement, type CSSProperties, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { SegmentedControl } from "./segmented-control";
|
|
6
|
+
import { Spinner } from "./spinner";
|
|
7
|
+
|
|
8
|
+
export type OrchestratorLead = {
|
|
9
|
+
/** One letter shown in the 22px ink tile. */
|
|
10
|
+
initial: string;
|
|
11
|
+
name: ReactNode;
|
|
12
|
+
/** Role caption next to the name, such as "Lead". */
|
|
13
|
+
role?: ReactNode;
|
|
14
|
+
/** Status text, such as "1 working". */
|
|
15
|
+
status?: ReactNode;
|
|
16
|
+
/** Shows the 13px spinner while the lead waits on its lanes. */
|
|
17
|
+
running?: boolean;
|
|
18
|
+
elapsed?: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type OrchestratorPlanItem = {
|
|
22
|
+
label: string;
|
|
23
|
+
/** Dot color of the lane that runs this step, such as "bg-sky-600". Defaults to the live blue. */
|
|
24
|
+
dotClassName?: string;
|
|
25
|
+
/** Pulses the dot while the lane works. */
|
|
26
|
+
running?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type OrchestratorSummary = {
|
|
30
|
+
title?: ReactNode;
|
|
31
|
+
/** Right-aligned count, such as "1 of 3 returned". */
|
|
32
|
+
count?: ReactNode;
|
|
33
|
+
body: ReactNode;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type OrchestratorPanelProps = {
|
|
37
|
+
title: ReactNode;
|
|
38
|
+
/** Label before the segmented picker, such as "Bots in parallel". */
|
|
39
|
+
parallelLabel?: ReactNode;
|
|
40
|
+
/** Choices of the parallel picker. */
|
|
41
|
+
parallelOptions?: number[];
|
|
42
|
+
parallelValue?: number;
|
|
43
|
+
onParallelChange?: (value: number) => void;
|
|
44
|
+
stopLabel?: ReactNode;
|
|
45
|
+
/** Renders the Stop button when set. */
|
|
46
|
+
onStop?: () => void;
|
|
47
|
+
lead: OrchestratorLead;
|
|
48
|
+
plan?: OrchestratorPlanItem[];
|
|
49
|
+
/** `AgentLane` cards; the branch connector draws one elbow per lane. */
|
|
50
|
+
children?: ReactNode;
|
|
51
|
+
summary?: OrchestratorSummary;
|
|
52
|
+
className?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const stagger = (index: number) => ({ "--i": index, animationDelay: `calc(var(--i) * 30ms)` }) as CSSProperties;
|
|
56
|
+
|
|
57
|
+
/** Figma `Orchestrator` (Agents N1): a 24px-radius solid panel, header with a parallel picker and Stop, a lead row, its plan, branching lanes and a summary. */
|
|
58
|
+
export function OrchestratorPanel({
|
|
59
|
+
title,
|
|
60
|
+
parallelLabel = "Bots in parallel",
|
|
61
|
+
parallelOptions,
|
|
62
|
+
parallelValue,
|
|
63
|
+
onParallelChange,
|
|
64
|
+
stopLabel = "Stop",
|
|
65
|
+
onStop,
|
|
66
|
+
lead,
|
|
67
|
+
plan,
|
|
68
|
+
children,
|
|
69
|
+
summary,
|
|
70
|
+
className,
|
|
71
|
+
}: OrchestratorPanelProps) {
|
|
72
|
+
const lanes = Children.toArray(children);
|
|
73
|
+
return (
|
|
74
|
+
<section
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex w-full min-w-0 flex-col overflow-clip rounded-[24px] border border-border-default bg-bg-surface",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<header className="flex flex-col gap-3 px-5 pt-4 pb-3.5">
|
|
81
|
+
<h3 className="text-label-lg text-text-primary">{title}</h3>
|
|
82
|
+
<div className="flex flex-wrap items-center gap-2.5">
|
|
83
|
+
<span className="text-body-sm text-text-secondary">{parallelLabel}</span>
|
|
84
|
+
{parallelOptions && (
|
|
85
|
+
<SegmentedControl
|
|
86
|
+
aria-label={typeof parallelLabel === "string" ? parallelLabel : undefined}
|
|
87
|
+
size="sm"
|
|
88
|
+
surface="inset"
|
|
89
|
+
value={parallelValue === undefined ? undefined : String(parallelValue)}
|
|
90
|
+
onValueChange={(next) => onParallelChange?.(Number(next))}
|
|
91
|
+
items={parallelOptions.map((option) => ({ value: String(option), label: String(option) }))}
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
<span className="min-w-0 flex-1" />
|
|
95
|
+
{onStop && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={onStop}
|
|
99
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center justify-center gap-1.5 rounded-full bg-overlay-ink-6 px-3 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
100
|
+
>
|
|
101
|
+
<Stop aria-hidden className="size-3.5" />
|
|
102
|
+
{stopLabel}
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</header>
|
|
107
|
+
<div role="separator" className="h-px bg-overlay-ink-8" />
|
|
108
|
+
<div className="flex flex-col gap-2.5 px-5 pt-4 pb-5">
|
|
109
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
110
|
+
<span className="flex size-5.5 shrink-0 items-center justify-center rounded-[5px] bg-interactive-primary text-label-sm text-text-inverse">
|
|
111
|
+
{lead.initial}
|
|
112
|
+
</span>
|
|
113
|
+
<span className="min-w-0 truncate text-label-lg text-text-primary">{lead.name}</span>
|
|
114
|
+
<span className="min-w-0 flex-1 truncate text-body-sm text-text-secondary">{lead.role ?? "Lead"}</span>
|
|
115
|
+
{lead.running && <Spinner size={13} />}
|
|
116
|
+
{lead.status && <span className="shrink-0 text-caption text-text-secondary">{lead.status}</span>}
|
|
117
|
+
{lead.elapsed && <span className="shrink-0 text-caption text-text-secondary">{lead.elapsed}</span>}
|
|
118
|
+
</div>
|
|
119
|
+
{plan && plan.length > 0 && (
|
|
120
|
+
<ul className="flex flex-col gap-1.5 pt-0.5 pb-1 pl-7.5">
|
|
121
|
+
{plan.map((item, index) => (
|
|
122
|
+
<li key={item.label} className="flex min-w-0 motion-rise items-center gap-2.5" style={stagger(index)}>
|
|
123
|
+
<span
|
|
124
|
+
aria-hidden
|
|
125
|
+
className={cn("shrink-0 rounded-full text-sky-400", item.running && "animate-live-pulse")}
|
|
126
|
+
>
|
|
127
|
+
<span className={cn("block size-1.5 rounded-full", item.dotClassName ?? "bg-status-live")} />
|
|
128
|
+
</span>
|
|
129
|
+
<span className="min-w-0 text-body-sm wrap-break-word text-text-primary">{item.label}</span>
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
)}
|
|
134
|
+
{lanes.length > 0 && (
|
|
135
|
+
<div className="flex items-start">
|
|
136
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2.5 pl-7.5">
|
|
137
|
+
{lanes.map((lane, index) => (
|
|
138
|
+
<div
|
|
139
|
+
key={isValidElement(lane) && lane.key != null ? lane.key : `lane-${index}`}
|
|
140
|
+
className="relative motion-rise"
|
|
141
|
+
style={stagger(index)}
|
|
142
|
+
>
|
|
143
|
+
<span
|
|
144
|
+
aria-hidden
|
|
145
|
+
className="absolute top-0 -left-7.5 h-5.5 w-7.5 rounded-bl-lg border-b-[1.5px] border-l-[1.5px] border-overlay-ink-14"
|
|
146
|
+
/>
|
|
147
|
+
{index < lanes.length - 1 && (
|
|
148
|
+
<span
|
|
149
|
+
aria-hidden
|
|
150
|
+
className="absolute top-5.5 -bottom-2.5 -left-7.5 border-l-[1.5px] border-overlay-ink-14"
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
{lane}
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
<div className="motion-reveal" data-collapsed={!summary || undefined}>
|
|
160
|
+
<div>
|
|
161
|
+
{summary && (
|
|
162
|
+
<div className="flex flex-col gap-1 rounded-2xl bg-overlay-ink-4 px-4 py-3" style={stagger(lanes.length)}>
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<span className="min-w-0 flex-1 text-label-md text-text-primary">{summary.title ?? "Summary"}</span>
|
|
165
|
+
{summary.count && <span className="shrink-0 text-caption text-text-secondary">{summary.count}</span>}
|
|
166
|
+
</div>
|
|
167
|
+
<p className="text-body-sm text-text-secondary">{summary.body}</p>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CaretLeft } from "@phosphor-icons/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { IconButton } from "./icon-button";
|
|
6
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
7
|
+
|
|
8
|
+
export type PageHeaderProps = {
|
|
9
|
+
title: ReactNode;
|
|
10
|
+
onBack?: () => void;
|
|
11
|
+
backLabel?: string;
|
|
12
|
+
/** Trailing 40px icon buttons, such as search. */
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
/** Content under the top row, such as a `SegmentedControl` filter. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** `solid` (default) is an opaque surface header with a default border; `glass` is the regular glass header. */
|
|
17
|
+
surface?: "solid" | "glass";
|
|
18
|
+
className?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Figma `Header with filter` (Segmented filters 88:723): a 28px-radius header, 8px padding, solid or regular glass. */
|
|
22
|
+
export function PageHeader({
|
|
23
|
+
title,
|
|
24
|
+
onBack,
|
|
25
|
+
backLabel = "Back",
|
|
26
|
+
actions,
|
|
27
|
+
children,
|
|
28
|
+
surface = "solid",
|
|
29
|
+
className,
|
|
30
|
+
}: PageHeaderProps) {
|
|
31
|
+
return (
|
|
32
|
+
<header
|
|
33
|
+
className={cn(
|
|
34
|
+
"relative flex max-w-full min-w-0 flex-col gap-1.5 overflow-clip rounded-[28px] p-2",
|
|
35
|
+
surface === "glass" ? "bg-glass-regular" : "border border-border-default bg-bg-surface",
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{surface === "glass" && <LiquidGlass />}
|
|
40
|
+
<div className="relative flex h-10 min-w-0 items-center gap-2">
|
|
41
|
+
{onBack && <IconButton icon={<CaretLeft />} label={backLabel} onClick={onBack} />}
|
|
42
|
+
<h1 className={cn("min-w-0 flex-1 truncate text-label-lg text-text-primary", !onBack && "pl-2")}>{title}</h1>
|
|
43
|
+
{actions}
|
|
44
|
+
</div>
|
|
45
|
+
{children && <div className="relative animate-swap-move">{children}</div>}
|
|
46
|
+
</header>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Collapsible } from "@base-ui/react/collapsible";
|
|
2
|
+
import { ListChecks, WarningCircle } from "@phosphor-icons/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { Spinner } from "./spinner";
|
|
7
|
+
|
|
8
|
+
export type PlanCardStatus = "running" | "failed";
|
|
9
|
+
|
|
10
|
+
export type PlanCardProps = {
|
|
11
|
+
/** Label line next to the 16px list icon, such as "Plan · 5 steps". */
|
|
12
|
+
label?: ReactNode;
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
/** `PlanStep`s, stacked without dividers. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** Footer state. Unset hides the footer; a change expands or collapses it. */
|
|
17
|
+
status?: PlanCardStatus;
|
|
18
|
+
/** Footer text, such as "Stopped · step 4 failed" or "Running · 1 of 5". */
|
|
19
|
+
statusLabel?: ReactNode;
|
|
20
|
+
elapsed?: ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Figma `Plan card` (Agents N3): a 20px-radius solid card with a label, title, the steps and a status footer. */
|
|
25
|
+
export function PlanCard({ label, title, children, status, statusLabel, elapsed, className }: PlanCardProps) {
|
|
26
|
+
return (
|
|
27
|
+
<section
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex w-full min-w-0 flex-col overflow-clip rounded-xl border border-border-default bg-bg-surface",
|
|
30
|
+
className,
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<header className="flex flex-col gap-2 px-6 pt-5 pb-3.5">
|
|
34
|
+
{label && (
|
|
35
|
+
<div className="flex min-w-0 items-center gap-2 text-body-sm text-text-secondary">
|
|
36
|
+
<ListChecks aria-hidden className="size-4 shrink-0" />
|
|
37
|
+
<span className="min-w-0 wrap-break-word">{label}</span>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
<h3 className="min-w-0 text-label-lg font-semibold wrap-break-word text-text-primary">{title}</h3>
|
|
41
|
+
</header>
|
|
42
|
+
<div className="flex flex-col px-6 pt-1.5 pb-1">{children}</div>
|
|
43
|
+
<Collapsible.Root open={status !== undefined}>
|
|
44
|
+
<Collapsible.Panel keepMounted className="motion-collapse">
|
|
45
|
+
<div className="h-px bg-overlay-ink-8" />
|
|
46
|
+
<div
|
|
47
|
+
className={cn(
|
|
48
|
+
"flex min-w-0 items-center gap-2 px-6 pt-3.5 pb-4 text-label-md motion-colors",
|
|
49
|
+
status === "failed" ? "text-text-danger" : "text-text-primary",
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{status === "running" && <Spinner size={14} />}
|
|
53
|
+
{status === "failed" && <WarningCircle aria-hidden className="size-4 shrink-0" />}
|
|
54
|
+
<span key={String(status)} className="min-w-0 flex-1 animate-fade-in wrap-break-word">
|
|
55
|
+
{statusLabel}
|
|
56
|
+
</span>
|
|
57
|
+
{elapsed && (
|
|
58
|
+
<span
|
|
59
|
+
key={elapsed ? "with-elapsed" : "without-elapsed"}
|
|
60
|
+
className="shrink-0 animate-fade-in text-caption text-text-secondary"
|
|
61
|
+
>
|
|
62
|
+
{elapsed}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</Collapsible.Panel>
|
|
67
|
+
</Collapsible.Root>
|
|
68
|
+
</section>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ArrowClockwise, Check } from "@phosphor-icons/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { Spinner } from "./spinner";
|
|
6
|
+
|
|
7
|
+
export type PlanStepStatus = "done" | "running" | "failed" | "pending";
|
|
8
|
+
|
|
9
|
+
export type PlanStepProps = {
|
|
10
|
+
status: PlanStepStatus;
|
|
11
|
+
/** Step number, shown on pending steps. */
|
|
12
|
+
index: number;
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
/** Tool id in mono, such as "stripe.query". */
|
|
16
|
+
tool?: ReactNode;
|
|
17
|
+
elapsed?: ReactNode;
|
|
18
|
+
error?: ReactNode;
|
|
19
|
+
onRetry?: () => void;
|
|
20
|
+
/** Hides the rail line under the last step. */
|
|
21
|
+
last?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Figma `Plan step` (Agent plan 191:8296): a 24px marker on a 2px rail, sky when done. */
|
|
26
|
+
export function PlanStep({
|
|
27
|
+
status,
|
|
28
|
+
index,
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
tool,
|
|
32
|
+
elapsed,
|
|
33
|
+
error,
|
|
34
|
+
onRetry,
|
|
35
|
+
last,
|
|
36
|
+
className,
|
|
37
|
+
}: PlanStepProps) {
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("flex min-w-0 items-stretch gap-3.5", className)}>
|
|
40
|
+
<div className="flex shrink-0 flex-col items-center gap-1">
|
|
41
|
+
<span
|
|
42
|
+
className={cn(
|
|
43
|
+
"flex size-6 shrink-0 items-center justify-center rounded-md text-label-sm motion-colors",
|
|
44
|
+
status === "done" && "bg-status-live text-interactive-on-primary",
|
|
45
|
+
status === "failed" && "bg-bg-danger text-text-danger",
|
|
46
|
+
status === "pending" && "border border-border-default text-text-secondary",
|
|
47
|
+
status === "running" && "bg-bg-subtle",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span key={status} className="flex animate-pop-in items-center justify-center">
|
|
51
|
+
{status === "done" && <Check aria-hidden className="size-3.5" />}
|
|
52
|
+
{status === "running" && <Spinner size={20} />}
|
|
53
|
+
{status === "failed" && "!"}
|
|
54
|
+
{status === "pending" && index}
|
|
55
|
+
</span>
|
|
56
|
+
</span>
|
|
57
|
+
{!last && (
|
|
58
|
+
<span
|
|
59
|
+
className={cn(
|
|
60
|
+
"w-0.5 flex-1 rounded-[1px] motion-colors",
|
|
61
|
+
status === "done" ? "bg-status-live" : "bg-border-default",
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<div className="flex min-w-0 flex-1 flex-col pt-0.5 pb-4.5">
|
|
67
|
+
<div className="flex items-start gap-3">
|
|
68
|
+
<p
|
|
69
|
+
className={cn(
|
|
70
|
+
"min-w-0 flex-1 text-label-lg wrap-break-word",
|
|
71
|
+
status === "pending" ? "text-text-secondary" : "text-text-primary",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{title}
|
|
75
|
+
</p>
|
|
76
|
+
{elapsed && (status === "done" || status === "running") && (
|
|
77
|
+
<span className="shrink-0 text-caption text-text-secondary">{elapsed}</span>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1.5">
|
|
81
|
+
{description && (
|
|
82
|
+
<span className="min-w-0 text-body-sm wrap-break-word text-text-secondary">{description}</span>
|
|
83
|
+
)}
|
|
84
|
+
{tool && (
|
|
85
|
+
<span className="rounded-xs bg-bg-subtle px-2 py-0.5 font-mono text-label-md text-text-secondary">
|
|
86
|
+
{tool}
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="motion-reveal" data-collapsed={status !== "failed" || undefined}>
|
|
91
|
+
<div>
|
|
92
|
+
<div className="mt-1.5 flex flex-col gap-1.5">
|
|
93
|
+
<p className="text-body-sm wrap-break-word text-text-danger">{error}</p>
|
|
94
|
+
{onRetry && (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={onRetry}
|
|
98
|
+
inert={status !== "failed"}
|
|
99
|
+
className="flex h-7.5 w-fit motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-6 px-3 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
100
|
+
>
|
|
101
|
+
<ArrowClockwise aria-hidden className="size-3.5" />
|
|
102
|
+
Retry from here
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|