@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.
Files changed (93) 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 +202 -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 +121 -0
  8. package/src/components/agent-steering-panel.tsx +274 -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 +365 -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 +191 -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 +402 -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 +35 -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 +156 -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 +224 -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/index.ts +86 -0
  89. package/src/lib/cn.ts +25 -0
  90. package/src/lib/cursor-ghost.ts +244 -0
  91. package/src/lib/size.ts +10 -0
  92. package/src/lib/use-active-indicator.ts +60 -0
  93. 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
+ }