@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,26 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
|
|
3
|
+
export type HoronWordmarkProps = Omit<ComponentProps<"svg">, "children"> & { title?: string };
|
|
4
|
+
|
|
5
|
+
const PATHS = [
|
|
6
|
+
"M0.0230414 11.2212H1.91244L1.92626 8.60366L1.93548 5.99076H8.47923L8.48845 8.60366L8.50228 11.2212H10.3917L10.4193 8.06449C10.447 5.32717 10.5207 2.68663 10.6267 0.585252L10.6543 0H8.47923V4.2857H1.93548V0H0L0.0230414 11.2212Z",
|
|
7
|
+
"M15.6082 2.09677C13.5483 2.48847 11.9539 4.02764 11.5852 5.98155C11.2719 7.64514 12.0184 9.49306 13.4147 10.5069C14.3778 11.2073 15.2764 11.493 16.5437 11.493C17.576 11.4977 18.2764 11.3456 19.0552 10.9585C19.5621 10.7004 19.7741 10.5484 20.2442 10.0967C20.8893 9.47923 21.3179 8.75112 21.5483 7.9032C21.6958 7.36864 21.6727 6.05528 21.5068 5.52533C20.9861 3.82487 19.7327 2.63594 17.9861 2.18432C17.4055 2.03225 16.1843 1.99078 15.6082 2.09677ZM17.4884 3.894C18.811 4.24883 19.7465 5.36404 19.8064 6.65896C19.8433 7.5161 19.5806 8.17509 18.9446 8.80181C18.5022 9.2396 18.1843 9.44697 17.6958 9.61287C15.4562 10.3686 13.1106 8.63131 13.4101 6.43777C13.5898 5.13823 14.5714 4.12902 15.94 3.84331C16.3179 3.76496 17.1013 3.79261 17.4884 3.894Z",
|
|
8
|
+
"M16.2672 5.52533C15.8571 5.65436 15.5069 5.96311 15.3686 6.32717C15.2442 6.65436 15.2903 7.16587 15.4654 7.45159C15.9078 8.16587 16.8755 8.29029 17.5115 7.71426C17.811 7.44237 17.9032 7.23039 17.9032 6.79721C17.9032 6.47002 17.8893 6.4009 17.7603 6.17509C17.4654 5.65436 16.7972 5.36404 16.2672 5.52533Z",
|
|
9
|
+
"M32.0598 2.09677C29.6819 2.54838 28.0183 4.47003 28.0091 6.77417C27.9953 9.02762 29.5852 10.9216 31.8893 11.4147C32.4745 11.5391 33.6404 11.5253 34.2856 11.3871C36.198 10.9769 37.6128 9.59905 38.0229 7.73269C38.1612 7.11518 38.1289 6.10136 37.9492 5.50689C37.4561 3.84331 36.1796 2.63594 34.4285 2.17972C33.8616 2.03225 32.6312 1.99078 32.0598 2.09677ZM34.0321 3.91704C35.3962 4.341 36.2672 5.4516 36.2672 6.76496C36.2672 7.28569 36.1888 7.64514 35.9768 8.05527C35.0598 9.86172 32.6036 10.3502 30.9584 9.05527C29.8156 8.16126 29.5299 6.57602 30.281 5.31335C30.7004 4.6175 31.5114 4.05529 32.4054 3.84791C32.8156 3.75575 33.6128 3.78801 34.0321 3.91704Z",
|
|
10
|
+
"M43.516 2.09677C42.6865 2.24884 41.9676 2.59446 41.3501 3.15667L41.1059 3.37326V2.30414H39.2764L39.2441 6.29952C39.2303 8.50228 39.2026 10.5115 39.1888 10.7742L39.1612 11.2442L40.1197 11.235L41.0828 11.2212L41.1059 8.54836C41.1335 5.66358 41.1289 5.74652 41.3962 5.2304C41.9399 4.20275 43.3823 3.58524 44.6496 3.8433C45.8201 4.08294 46.6035 4.84791 46.8063 5.9539C46.88 6.37325 46.8662 8.45158 46.774 10.023C46.751 10.4516 46.7233 10.8986 46.7187 11.0138L46.7049 11.2212L48.6865 11.2119L48.7003 8.61287C48.7141 5.75574 48.6957 5.39169 48.4745 4.72349C48.0828 3.53916 47.2717 2.74193 46.0137 2.29031C45.3224 2.04608 44.2579 1.96313 43.516 2.09677Z",
|
|
11
|
+
"M22.8663 6.76035L22.8801 11.2212L23.7925 11.235L24.7004 11.2442L24.705 9.576C24.7188 6.46081 24.728 6.08293 24.811 5.788C24.9262 5.36865 25.1059 5.07832 25.4607 4.71888C25.8432 4.33178 26.2119 4.13363 26.7511 4.02764L27.1197 3.9493L27.7833 3.32257C28.1474 2.97695 28.5391 2.60829 28.6589 2.5023L28.8709 2.30875H26.2442L25.5068 2.99538L24.7695 3.68662L24.7419 2.30414H22.8571L22.8663 6.76035Z",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The UI-size Horon wordmark from the Figma Sidebar (88:808): a 48.7 x 11.5
|
|
16
|
+
* vector in `currentColor`. Scale it with `height`; the width follows.
|
|
17
|
+
*/
|
|
18
|
+
export function HoronWordmark({ title = "Horon", height = 11.5, ...props }: HoronWordmarkProps) {
|
|
19
|
+
return (
|
|
20
|
+
<svg viewBox="0 0 48.7038 11.5" height={height} fill="currentColor" role="img" aria-label={title} {...props}>
|
|
21
|
+
{PATHS.map((d) => (
|
|
22
|
+
<path key={d} d={d} />
|
|
23
|
+
))}
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Button as BaseButton } from "@base-ui/react/button";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import type { ControlSize } from "../lib/size";
|
|
6
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
7
|
+
|
|
8
|
+
export type IconButtonVariant = "primary" | "ghost" | "glass" | "solid" | "quiet";
|
|
9
|
+
export type IconButtonSize = ControlSize;
|
|
10
|
+
|
|
11
|
+
export type IconButtonProps = Omit<ComponentProps<typeof BaseButton>, "className" | "children"> & {
|
|
12
|
+
variant?: IconButtonVariant;
|
|
13
|
+
size?: IconButtonSize;
|
|
14
|
+
icon: ReactNode;
|
|
15
|
+
/** Accessible name; the button has no visible text. */
|
|
16
|
+
label: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sizeClass: Record<IconButtonSize, string> = {
|
|
21
|
+
sm: "size-8 [&_svg]:size-4",
|
|
22
|
+
md: "size-10 [&_svg]:size-5",
|
|
23
|
+
lg: "size-12 [&_svg]:size-6",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const variantClass: Record<IconButtonVariant, string> = {
|
|
27
|
+
primary:
|
|
28
|
+
"bg-interactive-primary text-interactive-on-primary hover:not-data-disabled:bg-interactive-primary-hover active:not-data-disabled:bg-interactive-primary-pressed",
|
|
29
|
+
ghost: "text-text-primary hover:not-data-disabled:bg-overlay-ink-6 active:not-data-disabled:bg-overlay-ink-10",
|
|
30
|
+
glass:
|
|
31
|
+
"overflow-clip bg-glass-clear text-text-primary hover:not-data-disabled:bg-glass-clear-hover active:not-data-disabled:bg-glass-clear-pressed",
|
|
32
|
+
solid:
|
|
33
|
+
"border border-border-default bg-bg-surface text-text-primary hover:not-data-disabled:bg-bg-subtle active:not-data-disabled:bg-interactive-secondary-hover",
|
|
34
|
+
// No fill and no liquid layer: only the ink wash on hover/press. A glass disc is wrong on a dark
|
|
35
|
+
// shell or title bar, where `glass` reads as a grey bubble; this keeps the icon legible without one.
|
|
36
|
+
quiet:
|
|
37
|
+
"text-text-secondary hover:not-data-disabled:bg-overlay-ink-6 hover:not-data-disabled:text-text-primary active:not-data-disabled:bg-overlay-ink-10",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Figma `Icon button` (57:167): Primary, Ghost, Glass and Solid in three sizes, plus a fill-free `quiet` variant. */
|
|
41
|
+
export function IconButton({ variant = "ghost", size = "md", icon, label, className, ...props }: IconButtonProps) {
|
|
42
|
+
return (
|
|
43
|
+
<BaseButton
|
|
44
|
+
aria-label={label}
|
|
45
|
+
className={cn(
|
|
46
|
+
"relative inline-flex shrink-0 motion-press cursor-pointer items-center justify-center rounded-full select-none",
|
|
47
|
+
// Paper draws icon buttons down to 32px. The painted hit area keeps the 44px reach
|
|
48
|
+
// without moving a single pixel of the visible control.
|
|
49
|
+
"relative after:absolute after:top-1/2 after:left-1/2 after:min-h-11 after:min-w-11 after:-translate-x-1/2 after:-translate-y-1/2 after:content-['']",
|
|
50
|
+
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand data-disabled:cursor-not-allowed data-disabled:opacity-40",
|
|
51
|
+
sizeClass[size],
|
|
52
|
+
variantClass[variant],
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{variant === "glass" && <LiquidGlass />}
|
|
58
|
+
<span className="relative flex">{icon}</span>
|
|
59
|
+
</BaseButton>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Input as BaseInput } from "@base-ui/react/input";
|
|
2
|
+
import { createContext, useContext, useMemo, type ComponentProps, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import type { ControlSize } from "../lib/size";
|
|
6
|
+
import { fieldBoxClass, fieldTextClass, type InputSurface } from "./input";
|
|
7
|
+
|
|
8
|
+
export type InputGroupSize = ControlSize;
|
|
9
|
+
|
|
10
|
+
export type InputGroupProps = Omit<ComponentProps<typeof BaseInput>, "className" | "prefix" | "size"> & {
|
|
11
|
+
/**
|
|
12
|
+
* Segment before the input: `<Select variant="segment" />` or `<Combobox variant="segment" />`
|
|
13
|
+
* for a choice such as a country code, or `<InputGroupSegment>` for fixed text.
|
|
14
|
+
*/
|
|
15
|
+
prefix?: ReactNode;
|
|
16
|
+
/** Segment after the input, such as a currency select. */
|
|
17
|
+
suffix?: ReactNode;
|
|
18
|
+
size?: InputGroupSize;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
/** Solid (default) or glass, as on `Input`. */
|
|
21
|
+
surface?: InputSurface;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type InputGroupContextValue = { size: InputGroupSize; disabled: boolean; surface: InputSurface };
|
|
26
|
+
|
|
27
|
+
/** Lets a segment (select, combobox or fixed text) take the group's size, disabled state and surface. */
|
|
28
|
+
const InputGroupContext = createContext<InputGroupContextValue | null>(null);
|
|
29
|
+
|
|
30
|
+
/** The group a segment sits in, or null outside an `InputGroup`. */
|
|
31
|
+
export function useInputGroup() {
|
|
32
|
+
return useContext(InputGroupContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sizeClass: Record<InputGroupSize, string> = {
|
|
36
|
+
sm: "h-8",
|
|
37
|
+
md: "h-10",
|
|
38
|
+
lg: "h-12",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The input's own padding, the same 12/14/20px as a standalone `Input`. */
|
|
42
|
+
const inputPaddingClass: Record<InputGroupSize, string> = {
|
|
43
|
+
sm: "px-3",
|
|
44
|
+
md: "px-3.5",
|
|
45
|
+
lg: "px-5",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The tinted segment look, shared by `InputGroupSegment` and the `segment` variant of `Select` and `Combobox`. */
|
|
49
|
+
export const inputGroupSegmentClass: Record<InputGroupSize, string> = {
|
|
50
|
+
sm: "relative flex h-full shrink-0 items-center gap-1 bg-overlay-ink-4 px-3 text-body-sm text-text-primary [&_svg]:size-4 [&_svg]:text-text-secondary",
|
|
51
|
+
md: "relative flex h-full shrink-0 items-center gap-1 bg-overlay-ink-4 px-3 text-body-md text-text-primary [&_svg]:size-4 [&_svg]:text-text-secondary",
|
|
52
|
+
lg: "relative flex h-full shrink-0 items-center gap-1.5 bg-overlay-ink-4 px-4 text-body-lg text-text-primary [&_svg]:size-5 [&_svg]:text-text-secondary",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const divider =
|
|
56
|
+
"relative h-full w-px shrink-0 bg-border-default group-data-invalid:bg-border-danger group-data-disabled:bg-border-default/60";
|
|
57
|
+
|
|
58
|
+
/** Figma `Input group` (60:429): a glass or solid field split into a select segment and an input segment, in three sizes. */
|
|
59
|
+
export function InputGroup({
|
|
60
|
+
prefix,
|
|
61
|
+
suffix,
|
|
62
|
+
size = "md",
|
|
63
|
+
invalid,
|
|
64
|
+
surface = "solid",
|
|
65
|
+
className,
|
|
66
|
+
disabled = false,
|
|
67
|
+
...props
|
|
68
|
+
}: InputGroupProps) {
|
|
69
|
+
const context = useMemo(() => ({ size, disabled, surface }), [size, disabled, surface]);
|
|
70
|
+
return (
|
|
71
|
+
<InputGroupContext value={context}>
|
|
72
|
+
<div
|
|
73
|
+
className={cn(fieldBoxClass.base, "group", sizeClass[size], fieldBoxClass[surface], className)}
|
|
74
|
+
data-invalid={invalid ? "" : undefined}
|
|
75
|
+
data-disabled={disabled ? "" : undefined}
|
|
76
|
+
>
|
|
77
|
+
{prefix && (
|
|
78
|
+
<>
|
|
79
|
+
{prefix}
|
|
80
|
+
<span aria-hidden className={divider} />
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
<div className={cn("relative flex h-full min-w-0 flex-1 items-center", inputPaddingClass[size])}>
|
|
84
|
+
<BaseInput
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
aria-invalid={invalid || undefined}
|
|
87
|
+
className={cn(fieldBoxClass.text, fieldTextClass[size])}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
{suffix && (
|
|
92
|
+
<>
|
|
93
|
+
<span aria-hidden className={divider} />
|
|
94
|
+
{suffix}
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</InputGroupContext>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A fixed-text segment, such as a unit or a `https://` prefix. Takes the group's size. */
|
|
103
|
+
export function InputGroupSegment({ className, children }: { className?: string; children: ReactNode }) {
|
|
104
|
+
const group = useInputGroup();
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className={cn(inputGroupSegmentClass[group?.size ?? "md"], "group-data-disabled:text-text-disabled", className)}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Input as BaseInput } from "@base-ui/react/input";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import type { ControlSize } from "../lib/size";
|
|
6
|
+
|
|
7
|
+
export type InputSize = ControlSize;
|
|
8
|
+
export type InputSurface = "glass" | "solid" | "solid-on-page";
|
|
9
|
+
|
|
10
|
+
export type InputProps = Omit<ComponentProps<typeof BaseInput>, "className" | "size"> & {
|
|
11
|
+
size?: InputSize;
|
|
12
|
+
/** `glass` over imagery and glass panels, `solid` on raised surfaces, `solid-on-page` directly on the page background. */
|
|
13
|
+
surface?: InputSurface;
|
|
14
|
+
/** Leading icon, 16px (sm, md) or 20px (lg), in the secondary text color. */
|
|
15
|
+
icon?: ReactNode;
|
|
16
|
+
/** Shows the error border. Inside a `Field`, an invalid field does the same. */
|
|
17
|
+
invalid?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
/** Classes for the native `<input>`. */
|
|
20
|
+
inputClassName?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Height, horizontal padding and icon size shared by every field-style control. */
|
|
24
|
+
export const fieldSizeClass: Record<ControlSize, string> = {
|
|
25
|
+
sm: "h-8 gap-1.5 px-3 [&_svg]:size-4",
|
|
26
|
+
md: "h-10 gap-2 px-3.5 [&_svg]:size-4",
|
|
27
|
+
lg: "h-12 gap-2.5 px-5 [&_svg]:size-5",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Body text size that matches each control rung. */
|
|
31
|
+
export const fieldTextClass: Record<ControlSize, string> = {
|
|
32
|
+
sm: "text-body-sm",
|
|
33
|
+
md: "text-body-md",
|
|
34
|
+
lg: "text-body-lg",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const fieldBoxClass = {
|
|
38
|
+
/** The box is the focus target's container, so the ring is drawn on `:focus-within`, not on the inner input. */
|
|
39
|
+
base: "motion-colors relative flex min-w-0 max-w-full items-center overflow-clip rounded-md border focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-text-brand",
|
|
40
|
+
/** The veil field (globals.css `.veil-field`): no liquid layers, so hosts render no `LiquidGlass`. */
|
|
41
|
+
glass: "veil-field has-[[data-disabled]]:opacity-60 has-[:disabled]:opacity-60",
|
|
42
|
+
solid:
|
|
43
|
+
"border-border-default bg-bg-surface has-[[data-disabled]]:border-border-default has-[:disabled]:border-border-default has-[[data-disabled]]:bg-bg-subtle has-[:disabled]:bg-bg-subtle has-[[data-invalid]]:border-border-danger data-invalid:border-border-danger",
|
|
44
|
+
"solid-on-page":
|
|
45
|
+
"border-border-default bg-bg-default has-[[data-disabled]]:border-border-default has-[:disabled]:border-border-default has-[[data-disabled]]:bg-bg-subtle has-[:disabled]:bg-bg-subtle has-[[data-invalid]]:border-border-danger data-invalid:border-border-danger",
|
|
46
|
+
text: "min-w-0 flex-1 bg-transparent text-text-primary outline-none placeholder:text-text-placeholder disabled:cursor-not-allowed disabled:text-text-disabled",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** Figma `Input` (58:126): SM, MD and LG, on Glass or Solid surface. */
|
|
50
|
+
export function Input({
|
|
51
|
+
size = "md",
|
|
52
|
+
surface = "solid",
|
|
53
|
+
icon,
|
|
54
|
+
invalid,
|
|
55
|
+
className,
|
|
56
|
+
inputClassName,
|
|
57
|
+
...props
|
|
58
|
+
}: InputProps) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
className={cn(fieldBoxClass.base, fieldBoxClass[surface], fieldSizeClass[size], className)}
|
|
62
|
+
data-invalid={invalid ? "" : undefined}
|
|
63
|
+
>
|
|
64
|
+
{icon && <span className="relative flex shrink-0 text-text-secondary">{icon}</span>}
|
|
65
|
+
<BaseInput
|
|
66
|
+
aria-invalid={invalid || undefined}
|
|
67
|
+
className={cn("relative", fieldBoxClass.text, fieldTextClass[size], inputClassName)}
|
|
68
|
+
{...props}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Plug } from "@phosphor-icons/react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { LiquidGlass } from "./liquid-glass";
|
|
6
|
+
|
|
7
|
+
export type IntegrationStatus = "connected" | "reconnect" | "not-connected";
|
|
8
|
+
|
|
9
|
+
export type IntegrationRowProps = {
|
|
10
|
+
/** 20px icon shown in inverse on the 40px tile. */
|
|
11
|
+
icon: ReactNode;
|
|
12
|
+
name: ReactNode;
|
|
13
|
+
/** Account and scopes, or the reason a reconnect is needed. */
|
|
14
|
+
detail?: ReactNode;
|
|
15
|
+
status: IntegrationStatus;
|
|
16
|
+
onAction?: () => void;
|
|
17
|
+
connectedLabel?: string;
|
|
18
|
+
reconnectLabel?: string;
|
|
19
|
+
connectLabel?: string;
|
|
20
|
+
/** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass row. */
|
|
21
|
+
surface?: "solid" | "glass";
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const tileClass: Record<IntegrationStatus, string> = {
|
|
26
|
+
connected: "bg-sky-500 text-neutral-50",
|
|
27
|
+
reconnect: "bg-sky-700 text-neutral-50",
|
|
28
|
+
"not-connected": "bg-bg-subtle text-text-secondary",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Figma `Integration row` (Integrations 110:883): 40px tile, status pill on the right, solid or thick glass. */
|
|
32
|
+
export function IntegrationRow({
|
|
33
|
+
icon,
|
|
34
|
+
name,
|
|
35
|
+
detail,
|
|
36
|
+
status,
|
|
37
|
+
onAction,
|
|
38
|
+
connectedLabel = "Connected",
|
|
39
|
+
reconnectLabel = "Reconnect",
|
|
40
|
+
connectLabel = "Connect",
|
|
41
|
+
surface = "solid",
|
|
42
|
+
className,
|
|
43
|
+
}: IntegrationRowProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={cn(
|
|
47
|
+
"relative flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 overflow-clip rounded-2xl py-3 pr-3.5 pl-3",
|
|
48
|
+
surface === "glass" ? "bg-glass-thick" : "border border-border-default bg-bg-surface",
|
|
49
|
+
status === "reconnect" &&
|
|
50
|
+
(surface === "glass" ? "[--glass-edge:var(--color-border-danger)]" : "border-border-danger"),
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{surface === "glass" && <LiquidGlass />}
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"relative flex size-10 shrink-0 items-center justify-center rounded-[10px] motion-colors [&>svg]:size-5",
|
|
58
|
+
surface === "glass" && status === "not-connected"
|
|
59
|
+
? "bg-overlay-ink-6 text-text-secondary"
|
|
60
|
+
: tileClass[status],
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{icon}
|
|
64
|
+
</span>
|
|
65
|
+
<span className="relative flex min-w-0 grow basis-40 flex-col gap-0.5">
|
|
66
|
+
<span className="truncate text-label-md text-text-primary">{name}</span>
|
|
67
|
+
{detail && (
|
|
68
|
+
<span
|
|
69
|
+
className={cn("truncate text-caption", status === "reconnect" ? "text-text-danger" : "text-text-secondary")}
|
|
70
|
+
>
|
|
71
|
+
{detail}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</span>
|
|
75
|
+
<span className="motion-swap relative shrink-0">
|
|
76
|
+
<span
|
|
77
|
+
data-hidden={status !== "connected" || undefined}
|
|
78
|
+
aria-hidden={status !== "connected" || undefined}
|
|
79
|
+
className="flex h-7.5 items-center justify-center gap-1.5 rounded-full bg-accent-sky-500-18 px-3 text-label-sm whitespace-nowrap text-text-primary"
|
|
80
|
+
>
|
|
81
|
+
<span aria-hidden className="size-1.5 rounded-full bg-status-live" />
|
|
82
|
+
{connectedLabel}
|
|
83
|
+
</span>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={onAction}
|
|
87
|
+
data-hidden={status === "connected" || undefined}
|
|
88
|
+
aria-hidden={status === "connected" || undefined}
|
|
89
|
+
tabIndex={status === "connected" ? -1 : undefined}
|
|
90
|
+
className={cn(
|
|
91
|
+
"flex h-7.5 motion-press cursor-pointer items-center justify-center gap-1.5 rounded-full px-3 text-label-sm whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand",
|
|
92
|
+
status === "reconnect"
|
|
93
|
+
? "bg-accent-red-500-14 text-text-danger"
|
|
94
|
+
: "bg-interactive-primary text-text-inverse",
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{status === "not-connected" && <Plug aria-hidden className="size-3.5" />}
|
|
98
|
+
{status === "reconnect" ? reconnectLabel : connectLabel}
|
|
99
|
+
</button>
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import type { ControlSize } from "../lib/size";
|
|
5
|
+
|
|
6
|
+
export type KbdSize = ControlSize;
|
|
7
|
+
|
|
8
|
+
export type KbdProps = Omit<ComponentProps<"kbd">, "className"> & {
|
|
9
|
+
/** Chip size: `sm` tighter caption, `md` the Command palette chip (default), `lg` label-sm with more padding. */
|
|
10
|
+
size?: KbdSize;
|
|
11
|
+
className?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Chip rungs: 16/20/24px high, so the three sizes read apart instead of only wider padding. */
|
|
15
|
+
const sizeClass: Record<KbdSize, string> = {
|
|
16
|
+
sm: "h-4 gap-0.5 px-1 text-caption [&_svg]:size-2.5",
|
|
17
|
+
md: "h-5 gap-1 px-1.5 text-caption [&_svg]:size-3",
|
|
18
|
+
lg: "h-6 gap-1 px-2 text-label-sm [&_svg]:size-3.5",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Figma `Kbd` (Command palette 110:676): a 6% ink chip with caption text. */
|
|
22
|
+
export function Kbd({ size = "md", className, children, ...props }: KbdProps) {
|
|
23
|
+
return (
|
|
24
|
+
<kbd
|
|
25
|
+
className={cn(
|
|
26
|
+
"inline-flex shrink-0 items-center rounded-xs bg-overlay-ink-6 font-sans leading-none whitespace-nowrap text-text-secondary",
|
|
27
|
+
sizeClass[size],
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<span className="inline-flex items-center gap-[inherit] text-trim">{children}</span>
|
|
33
|
+
</kbd>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { CaretDown, Plus, SlidersHorizontal, UploadSimple } from "@phosphor-icons/react";
|
|
2
|
+
import { type ComponentProps, type DragEvent, type ReactNode, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { PanelPillTabs } from "./bot-instructions-panel";
|
|
6
|
+
import { Spinner } from "./spinner";
|
|
7
|
+
|
|
8
|
+
export type KnowledgeBaseTab = {
|
|
9
|
+
id: string;
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
count?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type KnowledgeBasePanelProps = Omit<ComponentProps<"section">, "className" | "title" | "onChange"> & {
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
/** The 12/18 line under the title: sources, chunks, bots. */
|
|
17
|
+
meta?: ReactNode;
|
|
18
|
+
tabs: KnowledgeBaseTab[];
|
|
19
|
+
value?: string;
|
|
20
|
+
defaultValue?: string;
|
|
21
|
+
onValueChange?: (id: string) => void;
|
|
22
|
+
/** Shows the spinner and "N indexing" at the right of the tabs when above zero. */
|
|
23
|
+
indexing?: number;
|
|
24
|
+
onSettings?: () => void;
|
|
25
|
+
onAddSource?: () => void;
|
|
26
|
+
/** Called with the files dropped on the dropzone or picked with Browse. Omit to hide the dropzone. */
|
|
27
|
+
onFiles?: (files: File[]) => void;
|
|
28
|
+
dropTitle?: ReactNode;
|
|
29
|
+
dropHint?: ReactNode;
|
|
30
|
+
/** Pipeline column labels shown above the list; hidden on narrow widths. */
|
|
31
|
+
stages?: string[];
|
|
32
|
+
/** The list rows, usually `SourceRow variant="list"`. */
|
|
33
|
+
children?: ReactNode;
|
|
34
|
+
className?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const defaultStages = ["Parse", "Chunk", "Embed", "Index"];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Paper `Knowledge base` (XVM-1): a 24px-radius surface panel with a header, a pill tab tray, a table head, a
|
|
41
|
+
* flat divided source list and a dashed dropzone. The selected tab pill travels in 280ms, the dropzone border and
|
|
42
|
+
* fill change in 150ms while a file is over it, and indexing progress fills are animated by the rows.
|
|
43
|
+
*/
|
|
44
|
+
export function KnowledgeBasePanel({
|
|
45
|
+
title,
|
|
46
|
+
meta,
|
|
47
|
+
tabs,
|
|
48
|
+
value,
|
|
49
|
+
defaultValue,
|
|
50
|
+
onValueChange,
|
|
51
|
+
indexing = 0,
|
|
52
|
+
onSettings,
|
|
53
|
+
onAddSource,
|
|
54
|
+
onFiles,
|
|
55
|
+
dropTitle = "Drop files here, or browse",
|
|
56
|
+
dropHint = "PDF, DOCX, Markdown or TXT · Notion, Google Drive and websites from Add source",
|
|
57
|
+
stages = defaultStages,
|
|
58
|
+
children,
|
|
59
|
+
className,
|
|
60
|
+
...props
|
|
61
|
+
}: KnowledgeBasePanelProps) {
|
|
62
|
+
const [inner, setInner] = useState(defaultValue ?? tabs[0]?.id);
|
|
63
|
+
const selectedId = value ?? inner;
|
|
64
|
+
const [dragging, setDragging] = useState(false);
|
|
65
|
+
|
|
66
|
+
const select = (id: string) => {
|
|
67
|
+
setInner(id);
|
|
68
|
+
onValueChange?.(id);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const over = (event: DragEvent) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
setDragging(true);
|
|
74
|
+
};
|
|
75
|
+
const drop = (event: DragEvent) => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
setDragging(false);
|
|
78
|
+
const files = Array.from(event.dataTransfer.files);
|
|
79
|
+
if (files.length > 0) onFiles?.(files);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<section
|
|
84
|
+
className={cn(
|
|
85
|
+
"flex w-full max-w-200 min-w-0 flex-col items-stretch gap-3 rounded-[24px] border border-border-default bg-bg-surface px-5 py-4.5",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
91
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
92
|
+
<h3 className="truncate text-label-lg text-text-primary">{title}</h3>
|
|
93
|
+
{meta && <p className="truncate text-caption text-text-secondary">{meta}</p>}
|
|
94
|
+
</div>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
aria-label="Knowledge base settings"
|
|
98
|
+
onClick={onSettings}
|
|
99
|
+
className="flex size-7.5 shrink-0 motion-press cursor-pointer items-center justify-center rounded-full bg-overlay-ink-6 text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand [&>svg]:size-4"
|
|
100
|
+
>
|
|
101
|
+
<SlidersHorizontal aria-hidden />
|
|
102
|
+
</button>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={onAddSource}
|
|
106
|
+
className="flex h-7.5 shrink-0 motion-press cursor-pointer items-center gap-1.5 rounded-full bg-overlay-ink-6 pr-2.5 pl-3 text-label-sm text-text-primary hover:bg-overlay-ink-10 focus-visible:outline-2 focus-visible:outline-text-brand"
|
|
107
|
+
>
|
|
108
|
+
<Plus aria-hidden className="size-3.5" />
|
|
109
|
+
Add source
|
|
110
|
+
<CaretDown aria-hidden className="size-3 text-text-secondary" />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
115
|
+
<PanelPillTabs
|
|
116
|
+
aria-label="Sources"
|
|
117
|
+
value={selectedId}
|
|
118
|
+
onValueChange={select}
|
|
119
|
+
items={tabs.map((tab) => ({
|
|
120
|
+
value: tab.id,
|
|
121
|
+
label: (
|
|
122
|
+
<span className="flex items-center gap-1.5">
|
|
123
|
+
{tab.label}
|
|
124
|
+
{tab.count !== undefined && <span className="text-caption text-text-secondary">{tab.count}</span>}
|
|
125
|
+
</span>
|
|
126
|
+
),
|
|
127
|
+
}))}
|
|
128
|
+
/>
|
|
129
|
+
<span className="min-w-0 flex-1" />
|
|
130
|
+
<span className="motion-reveal-x shrink-0" data-collapsed={indexing <= 0 || undefined}>
|
|
131
|
+
<span className="flex items-center gap-2 text-caption text-text-secondary tabular-nums">
|
|
132
|
+
<Spinner size={12} label="Indexing" />
|
|
133
|
+
{indexing} indexing
|
|
134
|
+
</span>
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div aria-hidden className="hidden items-center gap-3 px-2 pt-2 pb-1 text-caption text-text-secondary sm:flex">
|
|
139
|
+
<span className="min-w-0 flex-1">Source</span>
|
|
140
|
+
<span className="flex gap-1">
|
|
141
|
+
{stages.map((stage) => (
|
|
142
|
+
<span key={stage} className="flex h-4 w-11 shrink-0 items-center">
|
|
143
|
+
{stage}
|
|
144
|
+
</span>
|
|
145
|
+
))}
|
|
146
|
+
</span>
|
|
147
|
+
<span className="w-18 shrink-0" />
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="flex flex-col">{children}</div>
|
|
151
|
+
|
|
152
|
+
{onFiles && (
|
|
153
|
+
<label
|
|
154
|
+
onDragOver={over}
|
|
155
|
+
onDragEnter={over}
|
|
156
|
+
onDragLeave={(event) => {
|
|
157
|
+
const next = event.relatedTarget;
|
|
158
|
+
if (!(next instanceof Node) || !event.currentTarget.contains(next)) setDragging(false);
|
|
159
|
+
}}
|
|
160
|
+
onDrop={drop}
|
|
161
|
+
className={cn(
|
|
162
|
+
"flex cursor-pointer items-center gap-3 rounded-2xl border border-dashed px-4.5 py-4 motion-colors has-focus-visible:outline-2 has-focus-visible:outline-text-brand",
|
|
163
|
+
dragging ? "border-border-focus bg-bg-brand" : "border-overlay-ink-18 bg-transparent",
|
|
164
|
+
)}
|
|
165
|
+
>
|
|
166
|
+
<input
|
|
167
|
+
type="file"
|
|
168
|
+
multiple
|
|
169
|
+
className="sr-only"
|
|
170
|
+
onChange={(event) => {
|
|
171
|
+
const files = Array.from(event.target.files ?? []);
|
|
172
|
+
if (files.length > 0) onFiles(files);
|
|
173
|
+
event.target.value = "";
|
|
174
|
+
}}
|
|
175
|
+
/>
|
|
176
|
+
<UploadSimple aria-hidden className="size-4.5 shrink-0 text-text-secondary" />
|
|
177
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
178
|
+
<span className="text-label-md wrap-break-word text-text-primary">{dropTitle}</span>
|
|
179
|
+
<span className="text-caption wrap-break-word text-text-secondary">{dropHint}</span>
|
|
180
|
+
</span>
|
|
181
|
+
</label>
|
|
182
|
+
)}
|
|
183
|
+
</section>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The glass material from `@horon-ai/tokens` `glass.ts`, the same one Flutter's
|
|
3
|
+
* `HoronLiquidGlass` draws: the backdrop blurred and saturated, then a 1px rim
|
|
4
|
+
* that is bright at the top and faint down the sides.
|
|
5
|
+
*
|
|
6
|
+
* Render it as the first child of a `relative isolate` host with a border radius
|
|
7
|
+
* and no border of its own; the rim is the edge. `isolate` keeps the layer's
|
|
8
|
+
* `-z-10` inside the host, so it paints under the body while staying above the
|
|
9
|
+
* host's own fill. A host that cannot isolate keeps its body `relative` instead.
|
|
10
|
+
*
|
|
11
|
+
* The host keeps its fill (`glass-clear`, `glass-regular`, `glass-thick`), which
|
|
12
|
+
* is equivalent to Flutter painting the fill over the filtered backdrop because
|
|
13
|
+
* saturation is linear and keeps white. There is deliberately no `tier` prop:
|
|
14
|
+
* the fill is the host's, and a fill on the layer would have to override the
|
|
15
|
+
* `glass-fill-frost` swap that `globals.css` applies for reduced transparency
|
|
16
|
+
* and for engines without `backdrop-filter`, so the layer would lie about the
|
|
17
|
+
* surface it is drawing. Tiers belong on the host, one class, no prop.
|
|
18
|
+
*
|
|
19
|
+
* Without backdrop-filter support, or with reduced transparency requested, the
|
|
20
|
+
* fill turns to `glass-fill-frost` and the rim stays (see `globals.css`).
|
|
21
|
+
*/
|
|
22
|
+
export function LiquidGlass() {
|
|
23
|
+
return <span aria-hidden className="liquid-layers" />;
|
|
24
|
+
}
|