@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.
Files changed (102) hide show
  1. package/README.md +168 -2
  2. package/package.json +50 -5
  3. package/src/assets/sidebar-photo.jpg +0 -0
  4. package/src/components/agent-activity.tsx +204 -0
  5. package/src/components/agent-lane.tsx +80 -0
  6. package/src/components/agent-lanes-timeline.tsx +312 -0
  7. package/src/components/agent-panel.tsx +125 -0
  8. package/src/components/agent-steering-panel.tsx +276 -0
  9. package/src/components/approval-card.tsx +89 -0
  10. package/src/components/avatar.tsx +104 -0
  11. package/src/components/badge.tsx +63 -0
  12. package/src/components/banner.tsx +103 -0
  13. package/src/components/bot-instructions-panel.tsx +369 -0
  14. package/src/components/bot-list-row.tsx +42 -0
  15. package/src/components/bot-memory-panel.tsx +229 -0
  16. package/src/components/bottom-sheet.tsx +140 -0
  17. package/src/components/breadcrumb-header.tsx +87 -0
  18. package/src/components/breadcrumb.tsx +86 -0
  19. package/src/components/button.tsx +146 -0
  20. package/src/components/calendar.tsx +410 -0
  21. package/src/components/card.tsx +79 -0
  22. package/src/components/chat-bubble.tsx +36 -0
  23. package/src/components/chat-controls.tsx +118 -0
  24. package/src/components/checkbox.tsx +73 -0
  25. package/src/components/chip.tsx +81 -0
  26. package/src/components/choice-card.tsx +55 -0
  27. package/src/components/code-panel.tsx +27 -0
  28. package/src/components/combobox.tsx +312 -0
  29. package/src/components/command-palette.tsx +206 -0
  30. package/src/components/composer.tsx +124 -0
  31. package/src/components/data-table.tsx +429 -0
  32. package/src/components/date-field.tsx +100 -0
  33. package/src/components/dialog.tsx +93 -0
  34. package/src/components/directory-card.tsx +59 -0
  35. package/src/components/document-extract-panel.tsx +398 -0
  36. package/src/components/empty-state.tsx +29 -0
  37. package/src/components/field.tsx +78 -0
  38. package/src/components/file-drop.tsx +89 -0
  39. package/src/components/filter-bar.tsx +83 -0
  40. package/src/components/flue-background.tsx +388 -0
  41. package/src/components/horon-icon.tsx +280 -0
  42. package/src/components/horon-wordmark.tsx +26 -0
  43. package/src/components/icon-button.tsx +61 -0
  44. package/src/components/input-group.tsx +112 -0
  45. package/src/components/input.tsx +72 -0
  46. package/src/components/integration-row.tsx +103 -0
  47. package/src/components/kbd.tsx +42 -0
  48. package/src/components/knowledge-base-panel.tsx +185 -0
  49. package/src/components/liquid-glass.tsx +24 -0
  50. package/src/components/list-detail.tsx +275 -0
  51. package/src/components/marketplace-card.tsx +97 -0
  52. package/src/components/mcp-server-row.tsx +122 -0
  53. package/src/components/menu.tsx +165 -0
  54. package/src/components/message-extras.tsx +146 -0
  55. package/src/components/modal.tsx +108 -0
  56. package/src/components/orchestrator-panel.tsx +175 -0
  57. package/src/components/page-header.tsx +48 -0
  58. package/src/components/plan-card.tsx +70 -0
  59. package/src/components/plan-step.tsx +111 -0
  60. package/src/components/plugin-row.tsx +128 -0
  61. package/src/components/progress.tsx +21 -0
  62. package/src/components/radio-group.tsx +84 -0
  63. package/src/components/research-run-panel.tsx +232 -0
  64. package/src/components/schedule.tsx +332 -0
  65. package/src/components/segmented-control.tsx +136 -0
  66. package/src/components/select.tsx +154 -0
  67. package/src/components/settings-list.tsx +99 -0
  68. package/src/components/sidebar.tsx +542 -0
  69. package/src/components/skeleton.tsx +75 -0
  70. package/src/components/slider.tsx +29 -0
  71. package/src/components/source-row.tsx +112 -0
  72. package/src/components/spinner.tsx +29 -0
  73. package/src/components/split-button.tsx +72 -0
  74. package/src/components/switch.tsx +79 -0
  75. package/src/components/tab-bar.tsx +90 -0
  76. package/src/components/tabs.tsx +162 -0
  77. package/src/components/task-row.tsx +159 -0
  78. package/src/components/textarea-field.tsx +127 -0
  79. package/src/components/textarea.tsx +47 -0
  80. package/src/components/toast.tsx +128 -0
  81. package/src/components/tool-call.tsx +215 -0
  82. package/src/components/tool-calls-header.tsx +65 -0
  83. package/src/components/tool-permissions.tsx +105 -0
  84. package/src/components/tooltip.tsx +33 -0
  85. package/src/components/top-bar.tsx +65 -0
  86. package/src/components/usage-meter.tsx +47 -0
  87. package/src/components/workspace-menu.tsx +77 -0
  88. package/src/hooks/index.ts +8 -0
  89. package/src/hooks/use-active-indicator.ts +60 -0
  90. package/src/hooks/use-color-scheme.ts +27 -0
  91. package/src/hooks/use-copy-to-clipboard.ts +32 -0
  92. package/src/hooks/use-device.ts +41 -0
  93. package/src/hooks/use-hotkey.ts +35 -0
  94. package/src/hooks/use-media-query.ts +25 -0
  95. package/src/hooks/use-platform.ts +39 -0
  96. package/src/hooks/use-reduced-motion.ts +8 -0
  97. package/src/index.ts +99 -0
  98. package/src/lib/cn.ts +25 -0
  99. package/src/lib/cursor-ghost.ts +244 -0
  100. package/src/lib/platform.ts +169 -0
  101. package/src/lib/size.ts +10 -0
  102. 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
+ }