@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,206 @@
|
|
|
1
|
+
import { Autocomplete } from "@base-ui/react/autocomplete";
|
|
2
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
3
|
+
import { MagnifyingGlass, X } from "@phosphor-icons/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
import { useDevice } from "../hooks/use-device";
|
|
7
|
+
import { cn } from "../lib/cn";
|
|
8
|
+
import { Kbd } from "./kbd";
|
|
9
|
+
|
|
10
|
+
/** Close control shape for pointers that never reach a key: a 28px icon tile instead of the esc chip. */
|
|
11
|
+
const touchCloseClass =
|
|
12
|
+
"flex size-7 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full text-text-secondary hover:bg-overlay-ink-6 hover:text-text-primary focus-visible:outline-2 focus-visible:outline-text-brand [&_svg]:size-4";
|
|
13
|
+
|
|
14
|
+
export type CommandItem = {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
keywords?: string;
|
|
18
|
+
/** 28px leading tile: an initial or a 16px icon. */
|
|
19
|
+
tile?: ReactNode;
|
|
20
|
+
/** Tile fill class, such as "bg-sky-500"; defaults to the primary tile. */
|
|
21
|
+
tileClassName?: string;
|
|
22
|
+
/** Caption after the label, such as the workspace. */
|
|
23
|
+
meta?: ReactNode;
|
|
24
|
+
/** Right-side hint, usually `<Kbd>`; hidden when the device shows no keyboard hints. */
|
|
25
|
+
hint?: ReactNode;
|
|
26
|
+
onSelect?: () => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CommandGroup = { label: string; items: CommandItem[] };
|
|
30
|
+
|
|
31
|
+
export type CommandPaletteProps = {
|
|
32
|
+
open?: boolean;
|
|
33
|
+
onOpenChange?: (open: boolean) => void;
|
|
34
|
+
groups: CommandGroup[];
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
/** Shown when the query has no matches; defaults to "No results". */
|
|
37
|
+
empty?: ReactNode;
|
|
38
|
+
/** Footer key hints, such as `[["↑↓", "Move"], ["↵", "Open"]]`; hidden when the device shows no keyboard hints. */
|
|
39
|
+
hints?: [ReactNode, string][];
|
|
40
|
+
trigger?: ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* `solid` (default) is an opaque palette with a default border. `glass` puts it on the veil (`veil-overlay`).
|
|
43
|
+
*/
|
|
44
|
+
surface?: "solid" | "glass";
|
|
45
|
+
className?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Figma `Command palette` (110:676): a 560px palette, 24px radius, with a 48px search row,
|
|
50
|
+
* grouped 44px results and a key-hint footer.
|
|
51
|
+
*/
|
|
52
|
+
export function CommandPalette({
|
|
53
|
+
open,
|
|
54
|
+
onOpenChange,
|
|
55
|
+
groups,
|
|
56
|
+
placeholder = "Search",
|
|
57
|
+
empty,
|
|
58
|
+
hints,
|
|
59
|
+
trigger,
|
|
60
|
+
surface = "solid",
|
|
61
|
+
className,
|
|
62
|
+
}: CommandPaletteProps) {
|
|
63
|
+
const { keyboardHints } = useDevice();
|
|
64
|
+
return (
|
|
65
|
+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
|
|
66
|
+
{trigger}
|
|
67
|
+
<Dialog.Portal>
|
|
68
|
+
<Dialog.Backdrop className="fixed inset-0 z-50 bg-bg-scrim/32 motion-fade backdrop-blur-[6px]" />
|
|
69
|
+
<Dialog.Viewport className="fixed inset-0 z-50 flex items-start justify-center px-2 pt-24">
|
|
70
|
+
<Dialog.Popup
|
|
71
|
+
aria-label={placeholder}
|
|
72
|
+
className={cn(
|
|
73
|
+
// 560px where it fits; the viewport's 8px gutters cap `w-full` below that.
|
|
74
|
+
"relative flex w-full max-w-140 motion-popup flex-col overflow-clip rounded-[24px] p-1.5",
|
|
75
|
+
surface === "glass" ? "veil-overlay" : "border border-border-default bg-bg-surface shadow-palette",
|
|
76
|
+
"duration-240 data-starting-style:translate-y-2",
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<CommandPaletteBody
|
|
81
|
+
groups={groups}
|
|
82
|
+
placeholder={placeholder}
|
|
83
|
+
empty={empty}
|
|
84
|
+
closeControl={
|
|
85
|
+
<Dialog.Close
|
|
86
|
+
aria-label="Close"
|
|
87
|
+
className={keyboardHints ? "motion-press cursor-pointer" : touchCloseClass}
|
|
88
|
+
>
|
|
89
|
+
{keyboardHints ? <Kbd keys="Escape" /> : <X aria-hidden />}
|
|
90
|
+
</Dialog.Close>
|
|
91
|
+
}
|
|
92
|
+
onSelect={() => onOpenChange?.(false)}
|
|
93
|
+
/>
|
|
94
|
+
{keyboardHints && hints && hints.length > 0 && (
|
|
95
|
+
<div className="relative flex flex-wrap items-center gap-x-4 gap-y-1 px-3 pt-2 pb-1">
|
|
96
|
+
{hints.map(([key, label]) => (
|
|
97
|
+
<span key={label} className="flex items-center gap-1.5 text-caption text-text-secondary">
|
|
98
|
+
<Kbd>{key}</Kbd>
|
|
99
|
+
{label}
|
|
100
|
+
</span>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</Dialog.Popup>
|
|
105
|
+
</Dialog.Viewport>
|
|
106
|
+
</Dialog.Portal>
|
|
107
|
+
</Dialog.Root>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type CommandPaletteBodyProps = {
|
|
112
|
+
groups: CommandGroup[];
|
|
113
|
+
placeholder?: string;
|
|
114
|
+
empty?: ReactNode;
|
|
115
|
+
closeLabel?: string;
|
|
116
|
+
/** A ready close control; the dialog passes its own `Dialog.Close` so uncontrolled palettes still close. */
|
|
117
|
+
closeControl?: ReactNode;
|
|
118
|
+
onClose?: () => void;
|
|
119
|
+
onSelect?: () => void;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function CommandPaletteBody({
|
|
123
|
+
groups,
|
|
124
|
+
placeholder = "Search",
|
|
125
|
+
empty = "No results",
|
|
126
|
+
closeLabel = "Close",
|
|
127
|
+
closeControl,
|
|
128
|
+
onClose,
|
|
129
|
+
onSelect,
|
|
130
|
+
}: CommandPaletteBodyProps) {
|
|
131
|
+
const { keyboardHints } = useDevice();
|
|
132
|
+
return (
|
|
133
|
+
<Autocomplete.Root
|
|
134
|
+
open
|
|
135
|
+
inline
|
|
136
|
+
items={groups}
|
|
137
|
+
itemToStringValue={(item: CommandItem) => `${item.label} ${item.keywords ?? ""}`}
|
|
138
|
+
autoHighlight="always"
|
|
139
|
+
keepHighlight
|
|
140
|
+
>
|
|
141
|
+
<div className="relative flex h-12 items-center gap-2.5 px-3">
|
|
142
|
+
<span className="flex size-7 shrink-0 items-center justify-center">
|
|
143
|
+
<MagnifyingGlass aria-hidden className="size-5 text-text-secondary" />
|
|
144
|
+
</span>
|
|
145
|
+
<Autocomplete.Input
|
|
146
|
+
placeholder={placeholder}
|
|
147
|
+
className="min-w-0 flex-1 bg-transparent text-body-sm text-text-primary outline-none placeholder:text-text-secondary"
|
|
148
|
+
/>
|
|
149
|
+
{closeControl ??
|
|
150
|
+
(onClose === undefined ? null : (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label={closeLabel}
|
|
154
|
+
className={keyboardHints ? "motion-press cursor-pointer" : touchCloseClass}
|
|
155
|
+
onClick={onClose}
|
|
156
|
+
>
|
|
157
|
+
{keyboardHints ? <Kbd keys="Escape" /> : <X aria-hidden />}
|
|
158
|
+
</button>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
<div className="relative pt-1">
|
|
162
|
+
<div className="h-px bg-overlay-ink-8" />
|
|
163
|
+
</div>
|
|
164
|
+
<Autocomplete.Empty className="relative px-3 py-4 text-body-sm text-text-secondary empty:hidden">
|
|
165
|
+
{empty}
|
|
166
|
+
</Autocomplete.Empty>
|
|
167
|
+
<Autocomplete.List className="relative flex max-h-[min(60dvh,24rem)] flex-col overflow-y-auto">
|
|
168
|
+
{(group: CommandGroup) => (
|
|
169
|
+
<Autocomplete.Group key={group.label} items={group.items} className="flex flex-col pt-1">
|
|
170
|
+
<Autocomplete.GroupLabel className="px-3 pt-1 pb-1 text-caption text-text-secondary">
|
|
171
|
+
{group.label}
|
|
172
|
+
</Autocomplete.GroupLabel>
|
|
173
|
+
<Autocomplete.Collection>
|
|
174
|
+
{(item: CommandItem) => (
|
|
175
|
+
<Autocomplete.Item
|
|
176
|
+
key={item.value}
|
|
177
|
+
value={item}
|
|
178
|
+
onClick={() => {
|
|
179
|
+
item.onSelect?.();
|
|
180
|
+
onSelect?.();
|
|
181
|
+
}}
|
|
182
|
+
className="flex h-11 cursor-pointer items-center gap-2.5 rounded-md px-3 motion-colors outline-none select-none data-highlighted:bg-overlay-ink-6"
|
|
183
|
+
>
|
|
184
|
+
{item.tile && (
|
|
185
|
+
<span
|
|
186
|
+
className={cn(
|
|
187
|
+
"flex size-7 shrink-0 items-center justify-center rounded-sm text-label-sm [&>svg]:size-4",
|
|
188
|
+
item.tileClassName ?? "bg-interactive-primary text-interactive-on-primary",
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
{item.tile}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
<span className="truncate text-label-md text-text-primary">{item.label}</span>
|
|
195
|
+
{item.meta && <span className="shrink-0 text-caption text-text-secondary">{item.meta}</span>}
|
|
196
|
+
<span className="flex-1" />
|
|
197
|
+
{keyboardHints && item.hint}
|
|
198
|
+
</Autocomplete.Item>
|
|
199
|
+
)}
|
|
200
|
+
</Autocomplete.Collection>
|
|
201
|
+
</Autocomplete.Group>
|
|
202
|
+
)}
|
|
203
|
+
</Autocomplete.List>
|
|
204
|
+
</Autocomplete.Root>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ArrowUp, Microphone, Plus } from "@phosphor-icons/react";
|
|
2
|
+
import { useState, type FormEvent } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import type { ControlSize } from "../lib/size";
|
|
6
|
+
import { IconButton } from "./icon-button";
|
|
7
|
+
|
|
8
|
+
export type ComposerSize = ControlSize;
|
|
9
|
+
export type ComposerSurface = "solid" | "glass";
|
|
10
|
+
|
|
11
|
+
export type ComposerProps = {
|
|
12
|
+
size?: ComposerSize;
|
|
13
|
+
/** `solid` (default) on raised surfaces, `glass` over imagery and glass panels. */
|
|
14
|
+
surface?: ComposerSurface;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
value?: string;
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
onValueChange?: (value: string) => void;
|
|
19
|
+
onSubmit?: (value: string) => void;
|
|
20
|
+
onAttach?: () => void;
|
|
21
|
+
onVoice?: () => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sizeClass: Record<ComposerSize, string> = {
|
|
27
|
+
sm: "gap-1.5 p-1.5",
|
|
28
|
+
md: "gap-2 p-2",
|
|
29
|
+
lg: "gap-2.5 p-2.5",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const textClass: Record<ComposerSize, string> = {
|
|
33
|
+
sm: "text-body-sm",
|
|
34
|
+
md: "text-body-md",
|
|
35
|
+
lg: "text-body-lg",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Figma `Composer` (63:462): a regular-glass pill with an attach button, the
|
|
40
|
+
* message field, and a voice button that turns into the ink send button once
|
|
41
|
+
* there is text.
|
|
42
|
+
*/
|
|
43
|
+
export function Composer({
|
|
44
|
+
size = "md",
|
|
45
|
+
surface = "solid",
|
|
46
|
+
placeholder = "Message",
|
|
47
|
+
value: valueProp,
|
|
48
|
+
defaultValue = "",
|
|
49
|
+
onValueChange,
|
|
50
|
+
onSubmit,
|
|
51
|
+
onAttach,
|
|
52
|
+
onVoice,
|
|
53
|
+
disabled,
|
|
54
|
+
className,
|
|
55
|
+
}: ComposerProps) {
|
|
56
|
+
const [valueState, setValueState] = useState(defaultValue);
|
|
57
|
+
const value = valueProp ?? valueState;
|
|
58
|
+
const setValue = (next: string) => {
|
|
59
|
+
if (valueProp === undefined) setValueState(next);
|
|
60
|
+
onValueChange?.(next);
|
|
61
|
+
};
|
|
62
|
+
const submit = (event: FormEvent) => {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
if (!value.trim()) return;
|
|
65
|
+
onSubmit?.(value);
|
|
66
|
+
setValue("");
|
|
67
|
+
};
|
|
68
|
+
const canSubmit = value.trim() !== "";
|
|
69
|
+
return (
|
|
70
|
+
<form
|
|
71
|
+
onSubmit={submit}
|
|
72
|
+
className={cn(
|
|
73
|
+
"relative flex max-w-full min-w-0 items-center overflow-clip rounded-full motion-colors has-[input:focus-visible]:border-border-strong",
|
|
74
|
+
surface === "glass" ? "veil-field" : "border border-border-default bg-bg-surface",
|
|
75
|
+
sizeClass[size],
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<IconButton
|
|
80
|
+
type="button"
|
|
81
|
+
size={size}
|
|
82
|
+
icon={<Plus className="text-text-secondary" />}
|
|
83
|
+
label="Attach"
|
|
84
|
+
onClick={onAttach}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
/>
|
|
87
|
+
<input
|
|
88
|
+
value={value}
|
|
89
|
+
onChange={(event) => setValue(event.target.value)}
|
|
90
|
+
placeholder={placeholder}
|
|
91
|
+
aria-label={placeholder}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
className={cn(
|
|
94
|
+
"relative min-w-0 flex-1 bg-transparent text-text-primary outline-none placeholder:text-text-placeholder disabled:text-text-disabled",
|
|
95
|
+
textClass[size],
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
<span className="motion-swap">
|
|
99
|
+
<span data-hidden={!canSubmit || undefined} inert={!canSubmit}>
|
|
100
|
+
<IconButton
|
|
101
|
+
type="submit"
|
|
102
|
+
size={size}
|
|
103
|
+
variant="primary"
|
|
104
|
+
icon={<ArrowUp />}
|
|
105
|
+
label="Send"
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
tabIndex={canSubmit ? undefined : -1}
|
|
108
|
+
/>
|
|
109
|
+
</span>
|
|
110
|
+
<span data-hidden={canSubmit || undefined} inert={canSubmit}>
|
|
111
|
+
<IconButton
|
|
112
|
+
type="button"
|
|
113
|
+
size={size}
|
|
114
|
+
icon={<Microphone className="text-text-secondary" />}
|
|
115
|
+
label="Voice input"
|
|
116
|
+
onClick={onVoice}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
tabIndex={canSubmit ? -1 : undefined}
|
|
119
|
+
/>
|
|
120
|
+
</span>
|
|
121
|
+
</span>
|
|
122
|
+
</form>
|
|
123
|
+
);
|
|
124
|
+
}
|