@horon-ai/react 0.0.0-stage → 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/README.md +168 -2
- package/package.json +50 -5
- package/src/assets/sidebar-photo.jpg +0 -0
- package/src/components/agent-activity.tsx +202 -0
- package/src/components/agent-lane.tsx +80 -0
- package/src/components/agent-lanes-timeline.tsx +312 -0
- package/src/components/agent-panel.tsx +121 -0
- package/src/components/agent-steering-panel.tsx +274 -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 +365 -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 +191 -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 +402 -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 +35 -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 +156 -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 +224 -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/index.ts +86 -0
- package/src/lib/cn.ts +25 -0
- package/src/lib/cursor-ghost.ts +244 -0
- package/src/lib/size.ts +10 -0
- package/src/lib/use-active-indicator.ts +60 -0
- package/src/styles/globals.css +564 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Collapsible } from "@base-ui/react/collapsible";
|
|
2
|
+
import { CaretDown } from "@phosphor-icons/react";
|
|
3
|
+
import { Children, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
export type ToolCallsHeaderProps = {
|
|
8
|
+
/** Up to three 14px tool icons, drawn as overlapping 28px chips. */
|
|
9
|
+
icons?: ReactNode;
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
/** Elapsed time, such as "4.0s". */
|
|
12
|
+
elapsed?: ReactNode;
|
|
13
|
+
/** `ToolCall` cards, stacked 8px apart under the header. */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
defaultOpen?: boolean;
|
|
16
|
+
open?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Figma `Tool-calls header` (Agents N11): overlapping tool chips, "Using tools…" and a caret that fold the ToolCall cards below. */
|
|
22
|
+
export function ToolCallsHeader({
|
|
23
|
+
icons,
|
|
24
|
+
title = "Using tools…",
|
|
25
|
+
elapsed,
|
|
26
|
+
children,
|
|
27
|
+
defaultOpen = true,
|
|
28
|
+
open,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
className,
|
|
31
|
+
}: ToolCallsHeaderProps) {
|
|
32
|
+
const chips = Children.toArray(icons).slice(0, 3);
|
|
33
|
+
return (
|
|
34
|
+
<Collapsible.Root
|
|
35
|
+
defaultOpen={defaultOpen}
|
|
36
|
+
open={open}
|
|
37
|
+
onOpenChange={onOpenChange}
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex w-full min-w-0 flex-col rounded-xl border border-border-default bg-bg-surface p-3",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<Collapsible.Trigger className="group flex w-full min-w-0 cursor-pointer items-center gap-2.5 pb-1 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand">
|
|
44
|
+
{chips.length > 0 && (
|
|
45
|
+
<span className="flex shrink-0 items-center">
|
|
46
|
+
{Children.map(chips, (icon) => (
|
|
47
|
+
<span className="-ml-2 flex size-7 shrink-0 items-center justify-center rounded-[10px] bg-bg-subtle text-text-secondary ring-2 ring-bg-surface first:ml-0 [&>svg]:size-3.5">
|
|
48
|
+
{icon}
|
|
49
|
+
</span>
|
|
50
|
+
))}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
<span className="min-w-0 truncate text-label-md text-text-primary">{title}</span>
|
|
54
|
+
{elapsed && <span className="shrink-0 text-label-md text-text-secondary">· {elapsed}</span>}
|
|
55
|
+
<CaretDown
|
|
56
|
+
aria-hidden
|
|
57
|
+
className="ml-auto size-4 shrink-0 text-text-secondary transition-transform duration-200 ease-out-quint group-data-panel-open:rotate-180"
|
|
58
|
+
/>
|
|
59
|
+
</Collapsible.Trigger>
|
|
60
|
+
<Collapsible.Panel className="motion-collapse">
|
|
61
|
+
<div className="flex min-w-0 flex-col gap-2 pt-1">{children}</div>
|
|
62
|
+
</Collapsible.Panel>
|
|
63
|
+
</Collapsible.Root>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type ComponentProps, type ReactNode, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { SegmentedControl } from "./segmented-control";
|
|
5
|
+
|
|
6
|
+
export type ToolPermission = "allow" | "ask" | "off";
|
|
7
|
+
|
|
8
|
+
export type ToolPermissionItem = {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: ReactNode;
|
|
11
|
+
/** Starting choice for the uncontrolled component; defaults to `ask`. */
|
|
12
|
+
defaultValue?: ToolPermission;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ToolPermissionsProps = Omit<ComponentProps<"div">, "className" | "title" | "onChange"> & {
|
|
16
|
+
title: ReactNode;
|
|
17
|
+
tools: ToolPermissionItem[];
|
|
18
|
+
/** Controlled choices keyed by tool name. */
|
|
19
|
+
values?: Record<string, ToolPermission>;
|
|
20
|
+
onValuesChange?: (values: Record<string, ToolPermission>) => void;
|
|
21
|
+
/** Label of the text button that sets every tool to Allow. */
|
|
22
|
+
allowAllLabel?: ReactNode;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const options: { value: ToolPermission; label: string }[] = [
|
|
27
|
+
{ value: "allow", label: "Allow" },
|
|
28
|
+
{ value: "ask", label: "Ask" },
|
|
29
|
+
{ value: "off", label: "Off" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Paper `Tool permissions` (1VT5-0): one solid card, 16px radius with a default border. A subtle header
|
|
34
|
+
* carries the group title, a count pill and an "Allow all" text button; below it one divided row per tool
|
|
35
|
+
* with a mono name, a description and a small solid Allow / Ask / Off segmented control. 16px side padding
|
|
36
|
+
* on every row so the names, the header title and the controls line up.
|
|
37
|
+
*/
|
|
38
|
+
export function ToolPermissions({
|
|
39
|
+
title,
|
|
40
|
+
tools,
|
|
41
|
+
values,
|
|
42
|
+
onValuesChange,
|
|
43
|
+
allowAllLabel = "Allow all",
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: ToolPermissionsProps) {
|
|
47
|
+
const [inner, setInner] = useState<Record<string, ToolPermission>>(() =>
|
|
48
|
+
Object.fromEntries(tools.map((tool) => [tool.name, tool.defaultValue ?? "ask"])),
|
|
49
|
+
);
|
|
50
|
+
const current = values ?? inner;
|
|
51
|
+
const update = (next: Record<string, ToolPermission>) => {
|
|
52
|
+
setInner(next);
|
|
53
|
+
onValuesChange?.(next);
|
|
54
|
+
};
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
className={cn(
|
|
58
|
+
"flex w-full max-w-130 min-w-0 flex-col overflow-clip rounded-2xl border border-border-default bg-bg-surface",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<div className="flex h-11 items-center gap-2 border-b border-border-default bg-bg-subtle px-4">
|
|
64
|
+
<span className="min-w-0 truncate text-label-md font-semibold text-text-primary">{title}</span>
|
|
65
|
+
<span className="inline-flex h-5 shrink-0 items-center rounded-full bg-overlay-ink-6 px-1.5 text-label-sm text-text-secondary">
|
|
66
|
+
<span className="text-trim">{tools.length}</span>
|
|
67
|
+
</span>
|
|
68
|
+
<span className="min-w-0 flex-1" />
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className="shrink-0 cursor-pointer rounded-sm text-label-sm whitespace-nowrap text-text-secondary motion-colors hover:text-text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand"
|
|
72
|
+
onClick={() => update(Object.fromEntries(tools.map((tool) => [tool.name, "allow" as const])))}
|
|
73
|
+
>
|
|
74
|
+
{allowAllLabel}
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
{tools.map((tool) => (
|
|
78
|
+
<div
|
|
79
|
+
key={tool.name}
|
|
80
|
+
className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-border-default px-4 py-3 last:border-b-0"
|
|
81
|
+
>
|
|
82
|
+
<div className="flex min-w-0 grow basis-40 flex-col gap-1">
|
|
83
|
+
<span className="truncate font-mono text-code font-medium text-text-primary">{tool.name}</span>
|
|
84
|
+
{tool.description && (
|
|
85
|
+
<span className="text-caption wrap-break-word text-text-secondary">{tool.description}</span>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
<SegmentedControl
|
|
89
|
+
aria-label={`${tool.name} permission`}
|
|
90
|
+
size="sm"
|
|
91
|
+
surface="solid"
|
|
92
|
+
fill={false}
|
|
93
|
+
className="shrink-0"
|
|
94
|
+
items={options}
|
|
95
|
+
value={current[tool.name] ?? "ask"}
|
|
96
|
+
onValueChange={(value) => {
|
|
97
|
+
const next = options.find((option) => option.value === value);
|
|
98
|
+
if (next) update({ ...current, [tool.name]: next.value });
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type TooltipProps = {
|
|
5
|
+
/** The element that shows the tooltip on hover and focus. */
|
|
6
|
+
children: ReactElement;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
9
|
+
delay?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Figma `Tooltip` (62:282): an ink bubble, 10/6px padding, 8px radius, with a 10x6 arrow. */
|
|
13
|
+
export function Tooltip({ children, label, side = "top", delay }: TooltipProps) {
|
|
14
|
+
return (
|
|
15
|
+
<BaseTooltip.Root>
|
|
16
|
+
<BaseTooltip.Trigger delay={delay} render={children} />
|
|
17
|
+
<BaseTooltip.Portal>
|
|
18
|
+
<BaseTooltip.Positioner side={side} sideOffset={6} className="z-50">
|
|
19
|
+
<BaseTooltip.Popup className="flex max-w-64 motion-popup rounded-sm bg-interactive-primary px-2.5 py-1.5 text-label-sm text-balance text-interactive-on-primary duration-150">
|
|
20
|
+
{label}
|
|
21
|
+
<BaseTooltip.Arrow className="flex data-[side=bottom]:-top-1.5 data-[side=bottom]:rotate-180 data-[side=left]:-right-2 data-[side=left]:-rotate-90 data-[side=right]:-left-2 data-[side=right]:rotate-90 data-[side=top]:-bottom-1.5">
|
|
22
|
+
<svg width="10" height="6" viewBox="0 0 10 6" aria-hidden className="fill-interactive-primary">
|
|
23
|
+
<path d="M0.67 1.5h8.66L5 6z" />
|
|
24
|
+
</svg>
|
|
25
|
+
</BaseTooltip.Arrow>
|
|
26
|
+
</BaseTooltip.Popup>
|
|
27
|
+
</BaseTooltip.Positioner>
|
|
28
|
+
</BaseTooltip.Portal>
|
|
29
|
+
</BaseTooltip.Root>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const TooltipProvider = BaseTooltip.Provider;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CaretLeft, DotsThree } from "@phosphor-icons/react";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { Avatar } from "./avatar";
|
|
6
|
+
import { IconButton } from "./icon-button";
|
|
7
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
8
|
+
|
|
9
|
+
export type TopBarProps = Omit<ComponentProps<"header">, "className" | "title"> & {
|
|
10
|
+
title: ReactNode;
|
|
11
|
+
subtitle?: ReactNode;
|
|
12
|
+
/** Shows the live status dot on the avatar. */
|
|
13
|
+
live?: boolean;
|
|
14
|
+
onBack?: () => void;
|
|
15
|
+
onMore?: () => void;
|
|
16
|
+
backLabel?: string;
|
|
17
|
+
moreLabel?: string;
|
|
18
|
+
/** `solid` (default) is an opaque surface bar with a default border; `glass` is the regular glass bar. */
|
|
19
|
+
surface?: "solid" | "glass";
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Paper `Top bar`: back button, bot avatar with a flush live dot, title and subtitle, on solid surface or regular glass. */
|
|
24
|
+
export function TopBar({
|
|
25
|
+
title,
|
|
26
|
+
subtitle,
|
|
27
|
+
live,
|
|
28
|
+
onBack,
|
|
29
|
+
onMore,
|
|
30
|
+
backLabel = "Back",
|
|
31
|
+
moreLabel = "More",
|
|
32
|
+
surface = "solid",
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: TopBarProps) {
|
|
36
|
+
return (
|
|
37
|
+
<header
|
|
38
|
+
className={cn(
|
|
39
|
+
"relative z-10 flex items-center gap-2 overflow-clip rounded-full p-2",
|
|
40
|
+
surface === "glass" ? "bg-glass-regular" : "border border-border-default bg-bg-surface",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{surface === "glass" && <LiquidGlass />}
|
|
46
|
+
{onBack && <IconButton icon={<CaretLeft />} label={backLabel} onClick={onBack} />}
|
|
47
|
+
<span className="relative inline-flex shrink-0">
|
|
48
|
+
<Avatar type="bot" size="md" />
|
|
49
|
+
{live && (
|
|
50
|
+
<span aria-hidden className="absolute right-0 bottom-0 flex animate-pop-in">
|
|
51
|
+
<span className="size-[9px] animate-live-pulse rounded-full bg-status-live text-status-live" />
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</span>
|
|
55
|
+
<div
|
|
56
|
+
key={typeof title === "string" ? `${title}|${typeof subtitle === "string" ? subtitle : ""}` : undefined}
|
|
57
|
+
className="relative flex min-w-0 flex-1 animate-fade-in flex-col overflow-clip"
|
|
58
|
+
>
|
|
59
|
+
<span className="truncate text-label-lg text-text-primary">{title}</span>
|
|
60
|
+
{subtitle && <span className="truncate text-caption text-text-secondary">{subtitle}</span>}
|
|
61
|
+
</div>
|
|
62
|
+
{onMore && <IconButton icon={<DotsThree weight="bold" />} label={moreLabel} onClick={onMore} />}
|
|
63
|
+
</header>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Meter } from "@base-ui/react/meter";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
6
|
+
|
|
7
|
+
export type UsageMeterProps = {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
/** 0-100. */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Left caption, such as "6,420 of 10,000 tasks". */
|
|
12
|
+
detail?: ReactNode;
|
|
13
|
+
/** Right caption, such as "Resets Nov 1". */
|
|
14
|
+
note?: ReactNode;
|
|
15
|
+
/** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass card. */
|
|
16
|
+
surface?: "solid" | "glass";
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Figma `Usage meter` (in Loading 109:733): a card with a 6px sky fill, solid or thick glass. */
|
|
21
|
+
export function UsageMeter({ label, value, detail, note, surface = "solid", className }: UsageMeterProps) {
|
|
22
|
+
return (
|
|
23
|
+
<Meter.Root
|
|
24
|
+
value={value}
|
|
25
|
+
className={cn(
|
|
26
|
+
"relative flex w-full max-w-75 min-w-0 flex-col gap-2 overflow-clip rounded-xl p-4",
|
|
27
|
+
surface === "glass" ? "bg-glass-thick" : "border border-border-default bg-bg-surface",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{surface === "glass" && <LiquidGlass />}
|
|
32
|
+
<div className="relative flex w-full items-center gap-2 text-label-md">
|
|
33
|
+
<Meter.Label className="min-w-0 flex-1 text-text-primary">{label}</Meter.Label>
|
|
34
|
+
<Meter.Value className="shrink-0 text-text-secondary tabular-nums" />
|
|
35
|
+
</div>
|
|
36
|
+
<Meter.Track className="relative h-1.5 w-full overflow-clip rounded-full bg-overlay-ink-8">
|
|
37
|
+
<Meter.Indicator className="h-full rounded-full bg-sky-500 transition-[width] duration-400 ease-out-quint" />
|
|
38
|
+
</Meter.Track>
|
|
39
|
+
{(detail || note) && (
|
|
40
|
+
<div className="relative flex w-full items-center gap-2 text-caption text-text-secondary">
|
|
41
|
+
<span className="min-w-0 flex-1">{detail}</span>
|
|
42
|
+
{note && <span className="shrink-0 whitespace-nowrap">{note}</span>}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</Meter.Root>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Check, GearSix, PlusCircle } from "@phosphor-icons/react";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { Menu, MenuContent, MenuItem, MenuLabel, MenuSeparator, MenuTile, MenuTrigger } from "./menu";
|
|
6
|
+
|
|
7
|
+
export type WorkspaceMenuItem = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
/** Caption under the name, such as "12 members". */
|
|
11
|
+
meta?: ReactNode;
|
|
12
|
+
/** Tile initial; defaults to the first letter of the name. */
|
|
13
|
+
initial?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type WorkspaceMenuProps = {
|
|
17
|
+
workspaces: WorkspaceMenuItem[];
|
|
18
|
+
value: string;
|
|
19
|
+
onValueChange?: (id: string) => void;
|
|
20
|
+
onCreate?: () => void;
|
|
21
|
+
onSettings?: () => void;
|
|
22
|
+
/** The element that opens the menu, such as the sidebar workspace button. */
|
|
23
|
+
trigger: ComponentProps<typeof MenuTrigger>["render"];
|
|
24
|
+
triggerChildren?: ReactNode;
|
|
25
|
+
triggerClassName?: string;
|
|
26
|
+
label?: ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Paper `Workspace menu`: solid 256px menu listing workspaces with a check on the current one. */
|
|
31
|
+
export function WorkspaceMenu({
|
|
32
|
+
workspaces,
|
|
33
|
+
value,
|
|
34
|
+
onValueChange,
|
|
35
|
+
onCreate,
|
|
36
|
+
onSettings,
|
|
37
|
+
trigger,
|
|
38
|
+
triggerChildren,
|
|
39
|
+
triggerClassName,
|
|
40
|
+
label = "Workspaces",
|
|
41
|
+
className,
|
|
42
|
+
}: WorkspaceMenuProps) {
|
|
43
|
+
return (
|
|
44
|
+
<Menu>
|
|
45
|
+
<MenuTrigger render={trigger} className={triggerClassName}>
|
|
46
|
+
{triggerChildren}
|
|
47
|
+
</MenuTrigger>
|
|
48
|
+
<MenuContent surface="solid" side="bottom" align="start" sideOffset={7} className={className}>
|
|
49
|
+
<MenuLabel>{label}</MenuLabel>
|
|
50
|
+
{workspaces.map((workspace) => (
|
|
51
|
+
<MenuItem
|
|
52
|
+
key={workspace.id}
|
|
53
|
+
icon={<MenuTile>{workspace.initial ?? workspace.name.charAt(0).toUpperCase()}</MenuTile>}
|
|
54
|
+
description={workspace.meta}
|
|
55
|
+
selected={workspace.id === value}
|
|
56
|
+
trailing={
|
|
57
|
+
<Check
|
|
58
|
+
weight="bold"
|
|
59
|
+
className={cn("transition-opacity duration-150", workspace.id === value ? "opacity-100" : "opacity-0")}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
onClick={() => onValueChange?.(workspace.id)}
|
|
63
|
+
>
|
|
64
|
+
{workspace.name}
|
|
65
|
+
</MenuItem>
|
|
66
|
+
))}
|
|
67
|
+
<MenuSeparator />
|
|
68
|
+
<MenuItem icon={<PlusCircle />} onClick={onCreate}>
|
|
69
|
+
New workspace
|
|
70
|
+
</MenuItem>
|
|
71
|
+
<MenuItem icon={<GearSix />} onClick={onSettings}>
|
|
72
|
+
Workspace settings
|
|
73
|
+
</MenuItem>
|
|
74
|
+
</MenuContent>
|
|
75
|
+
</Menu>
|
|
76
|
+
);
|
|
77
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export * from "./components/agent-activity";
|
|
2
|
+
export * from "./components/agent-lane";
|
|
3
|
+
export * from "./components/agent-lanes-timeline";
|
|
4
|
+
export * from "./components/agent-panel";
|
|
5
|
+
export * from "./components/agent-steering-panel";
|
|
6
|
+
export * from "./components/approval-card";
|
|
7
|
+
export * from "./components/avatar";
|
|
8
|
+
export * from "./components/badge";
|
|
9
|
+
export * from "./components/banner";
|
|
10
|
+
export * from "./components/bot-instructions-panel";
|
|
11
|
+
export * from "./components/bot-list-row";
|
|
12
|
+
export * from "./components/bot-memory-panel";
|
|
13
|
+
export * from "./components/bottom-sheet";
|
|
14
|
+
export * from "./components/breadcrumb";
|
|
15
|
+
export * from "./components/breadcrumb-header";
|
|
16
|
+
export * from "./components/button";
|
|
17
|
+
export * from "./components/calendar";
|
|
18
|
+
export * from "./components/card";
|
|
19
|
+
export * from "./components/chat-bubble";
|
|
20
|
+
export * from "./components/chat-controls";
|
|
21
|
+
export * from "./components/checkbox";
|
|
22
|
+
export * from "./components/chip";
|
|
23
|
+
export * from "./components/choice-card";
|
|
24
|
+
export * from "./components/code-panel";
|
|
25
|
+
export * from "./components/command-palette";
|
|
26
|
+
export * from "./components/combobox";
|
|
27
|
+
export * from "./components/composer";
|
|
28
|
+
export * from "./components/data-table";
|
|
29
|
+
export * from "./components/date-field";
|
|
30
|
+
export * from "./components/dialog";
|
|
31
|
+
export * from "./components/directory-card";
|
|
32
|
+
export * from "./components/document-extract-panel";
|
|
33
|
+
export * from "./components/empty-state";
|
|
34
|
+
export * from "./components/field";
|
|
35
|
+
export * from "./components/file-drop";
|
|
36
|
+
export * from "./components/filter-bar";
|
|
37
|
+
export * from "./components/flue-background";
|
|
38
|
+
export * from "./components/horon-icon";
|
|
39
|
+
export * from "./components/horon-wordmark";
|
|
40
|
+
export * from "./components/icon-button";
|
|
41
|
+
export * from "./components/input";
|
|
42
|
+
export * from "./components/input-group";
|
|
43
|
+
export * from "./components/integration-row";
|
|
44
|
+
export * from "./components/kbd";
|
|
45
|
+
export * from "./components/knowledge-base-panel";
|
|
46
|
+
export * from "./components/liquid-glass";
|
|
47
|
+
export * from "./components/list-detail";
|
|
48
|
+
export * from "./components/marketplace-card";
|
|
49
|
+
export * from "./components/mcp-server-row";
|
|
50
|
+
export * from "./components/menu";
|
|
51
|
+
export * from "./components/modal";
|
|
52
|
+
export * from "./components/message-extras";
|
|
53
|
+
export * from "./components/orchestrator-panel";
|
|
54
|
+
export * from "./components/page-header";
|
|
55
|
+
export * from "./components/plan-card";
|
|
56
|
+
export * from "./components/plan-step";
|
|
57
|
+
export * from "./components/plugin-row";
|
|
58
|
+
export * from "./components/progress";
|
|
59
|
+
export * from "./components/radio-group";
|
|
60
|
+
export * from "./components/research-run-panel";
|
|
61
|
+
export * from "./components/schedule";
|
|
62
|
+
export * from "./components/segmented-control";
|
|
63
|
+
export * from "./components/select";
|
|
64
|
+
export * from "./components/settings-list";
|
|
65
|
+
export * from "./components/slider";
|
|
66
|
+
export * from "./components/sidebar";
|
|
67
|
+
export * from "./components/skeleton";
|
|
68
|
+
export * from "./components/source-row";
|
|
69
|
+
export * from "./components/spinner";
|
|
70
|
+
export * from "./components/split-button";
|
|
71
|
+
export * from "./components/switch";
|
|
72
|
+
export * from "./components/tab-bar";
|
|
73
|
+
export * from "./components/tabs";
|
|
74
|
+
export * from "./components/task-row";
|
|
75
|
+
export * from "./components/textarea";
|
|
76
|
+
export * from "./components/textarea-field";
|
|
77
|
+
export * from "./components/toast";
|
|
78
|
+
export * from "./components/tool-call";
|
|
79
|
+
export * from "./components/tool-calls-header";
|
|
80
|
+
export * from "./components/tool-permissions";
|
|
81
|
+
export * from "./components/tooltip";
|
|
82
|
+
export * from "./components/top-bar";
|
|
83
|
+
export * from "./components/usage-meter";
|
|
84
|
+
export * from "./components/workspace-menu";
|
|
85
|
+
export { cn, type ClassValue } from "./lib/cn";
|
|
86
|
+
export { mountCursorGhost, type CursorGhostOptions } from "./lib/cursor-ghost";
|
package/src/lib/cn.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { lightColors, primitives, radius, shadow, textStyles } from "@horon-ai/tokens";
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
export type ClassValue = string | false | null | undefined;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* tailwind-merge that knows the Horon theme, so `text-label-md` (font size) and
|
|
8
|
+
* `text-text-primary` (color) are not mistaken for each other, and a caller's
|
|
9
|
+
* `w-full` or `rounded-full` replaces the component default instead of fighting it.
|
|
10
|
+
*/
|
|
11
|
+
const merge = extendTailwindMerge({
|
|
12
|
+
extend: {
|
|
13
|
+
theme: {
|
|
14
|
+
color: [...Object.keys(lightColors), ...Object.keys(primitives)],
|
|
15
|
+
text: Object.keys(textStyles),
|
|
16
|
+
radius: Object.keys(radius),
|
|
17
|
+
shadow: Object.keys(shadow),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Joins class names, skipping empty values; later classes win over conflicting earlier ones. */
|
|
23
|
+
export function cn(...values: ClassValue[]): string {
|
|
24
|
+
return merge(values.filter(Boolean).join(" "));
|
|
25
|
+
}
|