@horon-ai/react 0.1.1 → 0.1.3
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 +2 -2
- package/package.json +2 -2
- package/src/components/agent-activity.tsx +75 -7
- package/src/components/agent-lane.tsx +32 -3
- package/src/components/agent-lanes-timeline.tsx +85 -7
- package/src/components/agent-panel.tsx +46 -4
- package/src/components/agent-steering-panel.tsx +53 -3
- package/src/components/approval-card.tsx +34 -2
- package/src/components/avatar.tsx +20 -4
- package/src/components/badge.tsx +24 -3
- package/src/components/banner.tsx +19 -3
- package/src/components/bot-instructions-panel.tsx +86 -7
- package/src/components/bot-list-row.tsx +13 -1
- package/src/components/bot-memory-panel.tsx +58 -3
- package/src/components/bottom-sheet.tsx +61 -7
- package/src/components/breadcrumb-header.tsx +32 -2
- package/src/components/breadcrumb.tsx +31 -1
- package/src/components/button.tsx +46 -1
- package/src/components/calendar.tsx +76 -6
- package/src/components/card.tsx +30 -1
- package/src/components/chat-bubble.tsx +32 -3
- package/src/components/chat-controls.tsx +60 -7
- package/src/components/checkbox.tsx +22 -1
- package/src/components/chip.tsx +29 -2
- package/src/components/choice-card.tsx +25 -1
- package/src/components/code-panel.tsx +28 -3
- package/src/components/combobox.tsx +52 -2
- package/src/components/command-palette.tsx +65 -1
- package/src/components/composer.tsx +28 -1
- package/src/components/data-table.tsx +198 -16
- package/src/components/date-field.tsx +31 -2
- package/src/components/dialog.tsx +45 -3
- package/src/components/directory-card.tsx +23 -2
- package/src/components/document-extract-panel.tsx +83 -5
- package/src/components/empty-state.tsx +21 -2
- package/src/components/field.tsx +49 -3
- package/src/components/file-drop.tsx +39 -4
- package/src/components/filter-bar.tsx +38 -1
- package/src/components/flue-background.tsx +88 -4
- package/src/components/horon-icon.tsx +50 -3
- package/src/components/horon-wordmark.tsx +26 -1
- package/src/components/icon-button.tsx +38 -1
- package/src/components/input-group.tsx +57 -6
- package/src/components/input.tsx +48 -2
- package/src/components/integration-row.tsx +43 -2
- package/src/components/kbd.tsx +28 -2
- package/src/components/knowledge-base-panel.tsx +40 -2
- package/src/components/liquid-glass.tsx +16 -0
- package/src/components/list-detail.tsx +89 -7
- package/src/components/marketplace-card.tsx +40 -4
- package/src/components/mcp-server-row.tsx +49 -3
- package/src/components/menu.tsx +83 -3
- package/src/components/message-extras.tsx +50 -4
- package/src/components/modal.tsx +71 -4
- package/src/components/orchestrator-panel.tsx +48 -4
- package/src/components/page-header.tsx +26 -3
- package/src/components/plan-card.tsx +28 -1
- package/src/components/plan-step.tsx +41 -1
- package/src/components/plugin-row.tsx +61 -3
- package/src/components/progress.tsx +32 -2
- package/src/components/radio-group.tsx +55 -2
- package/src/components/research-run-panel.tsx +73 -3
- package/src/components/schedule.tsx +163 -12
- package/src/components/segmented-control.tsx +57 -2
- package/src/components/select.tsx +60 -0
- package/src/components/settings-list.tsx +66 -8
- package/src/components/sidebar.tsx +214 -23
- package/src/components/skeleton.tsx +61 -4
- package/src/components/slider.tsx +30 -0
- package/src/components/source-row.tsx +43 -4
- package/src/components/spinner.tsx +29 -2
- package/src/components/split-button.tsx +37 -3
- package/src/components/switch.tsx +31 -1
- package/src/components/tab-bar.tsx +47 -2
- package/src/components/tabs.tsx +59 -5
- package/src/components/task-row.tsx +49 -3
- package/src/components/textarea-field.tsx +36 -2
- package/src/components/textarea.tsx +35 -2
- package/src/components/toast.tsx +52 -3
- package/src/components/tool-call.tsx +87 -4
- package/src/components/tool-calls-header.tsx +32 -1
- package/src/components/tool-permissions.tsx +50 -2
- package/src/components/tooltip.tsx +33 -1
- package/src/components/top-bar.tsx +35 -2
- package/src/components/usage-meter.tsx +28 -2
- package/src/components/workspace-menu.tsx +46 -1
- package/src/hooks/use-active-indicator.ts +44 -1
- package/src/hooks/use-color-scheme.ts +24 -1
- package/src/hooks/use-copy-to-clipboard.ts +23 -0
- package/src/hooks/use-device.ts +29 -1
- package/src/hooks/use-hotkey.ts +21 -0
- package/src/hooks/use-media-query.ts +18 -0
- package/src/hooks/use-platform.ts +30 -1
- package/src/hooks/use-reduced-motion.ts +18 -1
- package/src/index.ts +31 -0
- package/src/lib/cn.ts +13 -1
- package/src/lib/cursor-ghost.ts +16 -6
- package/src/lib/platform.ts +71 -4
|
@@ -3,26 +3,41 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Overall state of an indexed source: `ready` when every stage finished, `indexing` while the
|
|
8
|
+
* pipeline runs, and `failed` when a stage broke and Retry is offered.
|
|
9
|
+
*/
|
|
6
10
|
export type SourceStatus = "ready" | "indexing" | "failed";
|
|
7
11
|
|
|
12
|
+
/** Props of {@link SourceRow}: the source's identity, its pipeline state and the Retry action. */
|
|
8
13
|
export type SourceRowProps = {
|
|
14
|
+
/** Drives the default stage states, the detail color and which trailing control appears. */
|
|
9
15
|
status: SourceStatus;
|
|
10
16
|
/** 16px source icon on the 32px tile. */
|
|
11
17
|
icon: ReactNode;
|
|
18
|
+
/** Source name, one truncated line at `label-md`. */
|
|
12
19
|
name: ReactNode;
|
|
13
20
|
/** Pages and chunks, progress, or the error. */
|
|
14
21
|
detail: ReactNode;
|
|
15
|
-
/** 0-100 while indexing. */
|
|
22
|
+
/** 0-100 while indexing. @default 0 */
|
|
16
23
|
progress?: number;
|
|
24
|
+
/** Called by the Retry button, which only appears when `status` is `failed`. */
|
|
17
25
|
onRetry?: () => void;
|
|
18
|
-
/** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. */
|
|
26
|
+
/** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. @default "card" */
|
|
19
27
|
variant?: "card" | "list";
|
|
20
|
-
/** Pipeline stage states; defaults to Parse, Chunk, Embed, Index derived from `status`. */
|
|
28
|
+
/** Pipeline stage states; defaults to Parse, Chunk, Embed, Index derived from `status`. @default `defaultStages[status]`: `ready` is all `done`, `indexing` is `done, done, active, todo`, `failed` is `done, done, failed, todo` */
|
|
21
29
|
stages?: SourceStages;
|
|
30
|
+
/** Extra classes on the row, which owns the radius, border or divider. */
|
|
22
31
|
className?: string;
|
|
23
32
|
};
|
|
24
33
|
|
|
34
|
+
/**
|
|
35
|
+
* One stage of the parse → chunk → embed → index pipeline: `done` is a 35% ink bar, `active`
|
|
36
|
+
* shows the live progress fill, `todo` is 8% ink and `failed` takes the red accent.
|
|
37
|
+
*/
|
|
25
38
|
export type SourceStage = "done" | "active" | "todo" | "failed";
|
|
39
|
+
|
|
40
|
+
/** The four pipeline stages in display order: parse, chunk, embed, index. */
|
|
26
41
|
export type SourceStages = [SourceStage, SourceStage, SourceStage, SourceStage];
|
|
27
42
|
const stageNames = ["parse", "chunk", "embed", "index"] as const;
|
|
28
43
|
|
|
@@ -36,7 +51,31 @@ const stageFill = { done: "bg-overlay-ink-35", todo: "bg-overlay-ink-8", failed:
|
|
|
36
51
|
const stage = (state: keyof typeof stageFill) =>
|
|
37
52
|
cn("h-[3px] w-6 overflow-clip rounded-full transition-colors duration-150 sm:w-11", stageFill[state]);
|
|
38
53
|
|
|
39
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Figma `Source row` (Knowledge base): a flat row with a four-stage pipeline (parse, chunk, embed, index).
|
|
56
|
+
*
|
|
57
|
+
* The four 24px stage bars (44px from `sm` up) show the pipeline; the `active` bar fills to
|
|
58
|
+
* `progress`. The trailing slot is state-specific: a check when ready, a live percentage while
|
|
59
|
+
* indexing, and a Retry button when failed, where the detail line turns danger red with a warning icon.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { FilePdf } from "@phosphor-icons/react";
|
|
64
|
+
* import { SourceRow } from "@horon-ai/react";
|
|
65
|
+
*
|
|
66
|
+
* function IndexingSource() {
|
|
67
|
+
* return (
|
|
68
|
+
* <SourceRow
|
|
69
|
+
* status="indexing"
|
|
70
|
+
* icon={<FilePdf />}
|
|
71
|
+
* name="Q4 launch brief.pdf"
|
|
72
|
+
* detail="42 pages · embedding"
|
|
73
|
+
* progress={68}
|
|
74
|
+
* />
|
|
75
|
+
* );
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
40
79
|
export function SourceRow({
|
|
41
80
|
status,
|
|
42
81
|
icon,
|
|
@@ -1,17 +1,44 @@
|
|
|
1
1
|
import { cn } from "../lib/cn";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Named spinner rungs, in 14/16/24/32px (`xs`, `sm`, `md`, `lg`); a raw number is also
|
|
5
|
+
* accepted for one-off sizes.
|
|
6
|
+
*/
|
|
3
7
|
export type SpinnerSize = "xs" | "sm" | "md" | "lg";
|
|
4
8
|
|
|
9
|
+
/** Props of {@link Spinner}: a rung or pixel size, the accessible label, and extra classes. */
|
|
5
10
|
export type SpinnerProps = {
|
|
6
|
-
/** Named size, or a pixel size for the one-off spinners in agent rows (12, 15, 20). */
|
|
11
|
+
/** Named size, or a pixel size for the one-off spinners in agent rows (12, 15, 20). @default "sm" */
|
|
7
12
|
size?: SpinnerSize | number;
|
|
13
|
+
/** Accessible name announced while spinning (`role="status"`). @default "Loading" */
|
|
8
14
|
label?: string;
|
|
15
|
+
/** Extra classes on the `<svg>`; `animate-spin` and `shrink-0` are always applied. */
|
|
9
16
|
className?: string;
|
|
10
17
|
};
|
|
11
18
|
|
|
12
19
|
const sizeClass: Record<SpinnerSize, string> = { xs: "size-3.5", sm: "size-4", md: "size-6", lg: "size-8" };
|
|
13
20
|
|
|
14
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Figma `Spinner` (Loading 109:733): a 10% ink ring with a quarter ink arc, 11% stroke.
|
|
23
|
+
*
|
|
24
|
+
* Sizes are fixed pixel rungs (`xs` 14px, `sm` 16px, `md` 24px, `lg` 32px) or a raw number;
|
|
25
|
+
* it never shrinks, spins for as long as it is mounted, and announces `label` through
|
|
26
|
+
* `role="status"`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import { Spinner } from "@horon-ai/react";
|
|
31
|
+
*
|
|
32
|
+
* function Saving() {
|
|
33
|
+
* return (
|
|
34
|
+
* <span className="flex items-center gap-2 text-text-secondary">
|
|
35
|
+
* <Spinner size="sm" label="Saving" />
|
|
36
|
+
* Saving
|
|
37
|
+
* </span>
|
|
38
|
+
* );
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
15
42
|
export function Spinner({ size = "sm", label = "Loading", className }: SpinnerProps) {
|
|
16
43
|
return (
|
|
17
44
|
<svg
|
|
@@ -5,21 +5,55 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import { Menu, MenuContent, MenuTrigger } from "./menu";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Props of {@link SplitButton}: the Base UI button props (`onClick`, `disabled`, `type`, plus
|
|
10
|
+
* aria attributes) with the label, icon and menu the split pill adds.
|
|
11
|
+
*/
|
|
8
12
|
export type SplitButtonProps = Omit<ComponentProps<typeof BaseButton>, "className" | "children"> & {
|
|
9
|
-
/** Primary action label. */
|
|
13
|
+
/** Primary action label, rendered at `label-md` inside the left half. */
|
|
10
14
|
children: ReactNode;
|
|
11
15
|
/** 16px leading icon. */
|
|
12
16
|
icon?: ReactNode;
|
|
13
|
-
/** Menu items (`MenuItem`, `MenuSeparator`) opened by the caret. */
|
|
17
|
+
/** Menu items (`MenuItem`, `MenuSeparator`) opened by the caret. Without it the pill is a single full-round button. */
|
|
14
18
|
menu?: ReactNode;
|
|
19
|
+
/** Accessible name of the caret trigger. @default "More actions" */
|
|
15
20
|
menuLabel?: string;
|
|
21
|
+
/** Extra classes on the pill container, which owns the fill, the press scale and the disabled dim. */
|
|
16
22
|
className?: string;
|
|
17
23
|
};
|
|
18
24
|
|
|
19
25
|
const halfClass =
|
|
20
26
|
"relative inline-flex h-full cursor-pointer items-center select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand data-disabled:cursor-not-allowed";
|
|
21
27
|
|
|
22
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Paper `Split button` (1VGK-0): one primary pill on `Button`'s 32px rung, two hit areas split by a 1x18 bar.
|
|
30
|
+
*
|
|
31
|
+
* The container owns the primary fill, the 0.97 press scale and the 40% disabled dim, so both halves
|
|
32
|
+
* stay one pill; the caret opens the menu aligned to the end and rotates while the popup is open.
|
|
33
|
+
* Without `menu` there is no divider or caret and the button keeps a full round shape.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* import { MenuItem, SplitButton } from "@horon-ai/react";
|
|
38
|
+
*
|
|
39
|
+
* function Publish() {
|
|
40
|
+
* return (
|
|
41
|
+
* <SplitButton
|
|
42
|
+
* menuLabel="Publish options"
|
|
43
|
+
* onClick={() => publishNow()}
|
|
44
|
+
* menu={
|
|
45
|
+
* <>
|
|
46
|
+
* <MenuItem onClick={() => schedulePublish()}>Schedule</MenuItem>
|
|
47
|
+
* <MenuItem onClick={() => saveDraft()}>Save as draft</MenuItem>
|
|
48
|
+
* </>
|
|
49
|
+
* }
|
|
50
|
+
* >
|
|
51
|
+
* Publish
|
|
52
|
+
* </SplitButton>
|
|
53
|
+
* );
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
23
57
|
export function SplitButton({
|
|
24
58
|
children,
|
|
25
59
|
icon,
|
|
@@ -4,12 +4,19 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
|
|
7
|
-
/** `compact
|
|
7
|
+
/** Shared control rungs plus `compact`, the 40x24 Paper bot-memory switch that keeps its own rung. */
|
|
8
8
|
export type SwitchSize = ControlSize | "compact";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Props of {@link Switch}: Base UI's switch root props (`checked`, `defaultChecked`,
|
|
12
|
+
* `onCheckedChange`, `disabled`, `name`) plus the rung and the optional inline label.
|
|
13
|
+
*/
|
|
10
14
|
export type SwitchProps = Omit<ComponentProps<typeof BaseSwitch.Root>, "className"> & {
|
|
15
|
+
/** Track and thumb rung: `sm` 32x20, `md` 44x26, `lg` 52x30, `compact` 40x24. @default "md" */
|
|
11
16
|
size?: SwitchSize;
|
|
17
|
+
/** When set, the switch sits at the end of a label row and the whole row toggles it. */
|
|
12
18
|
label?: ReactNode;
|
|
19
|
+
/** Extra classes, applied to the label row when `label` is set and to the switch itself otherwise. */
|
|
13
20
|
className?: string;
|
|
14
21
|
};
|
|
15
22
|
|
|
@@ -38,6 +45,29 @@ const labelClass: Record<SwitchSize, string> = {
|
|
|
38
45
|
* Switch: MD 44x26 and SM 32x20 tracks with a white thumb 2px in; LG is 52x30; COMPACT 40x24 with an
|
|
39
46
|
* 18px thumb 3px in (Paper bot memory). The thumb stays white on the grey track in both themes and
|
|
40
47
|
* takes the inverse of the ink fill when checked, so the checked state reads the same in dark.
|
|
48
|
+
*
|
|
49
|
+
* Renders Base UI's switch button (`role="switch"`, `aria-checked`) and is controlled or
|
|
50
|
+
* uncontrolled the same way; the thumb springs across in 220ms and the track repaints on hover,
|
|
51
|
+
* checked and disabled states. With `label` the control becomes a `<label>` row whose text
|
|
52
|
+
* toggles it, so the switch does not need its own accessible name.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* import { useState } from "react";
|
|
57
|
+
* import { Switch } from "@horon-ai/react";
|
|
58
|
+
*
|
|
59
|
+
* function MemoryToggle() {
|
|
60
|
+
* const [remember, setRemember] = useState(true);
|
|
61
|
+
* return (
|
|
62
|
+
* <Switch
|
|
63
|
+
* label="Remember this bot's memory"
|
|
64
|
+
* size="compact"
|
|
65
|
+
* checked={remember}
|
|
66
|
+
* onCheckedChange={setRemember}
|
|
67
|
+
* />
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
41
71
|
*/
|
|
42
72
|
export function Switch({ size = "md", label, className, ...props }: SwitchProps) {
|
|
43
73
|
const control = (
|
|
@@ -5,17 +5,34 @@ import type { ControlSize } from "../lib/size";
|
|
|
5
5
|
import { useActiveIndicator } from "../hooks/use-active-indicator";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/** One destination in a {@link TabBar}: its stable value, the accessible name, and the icon shown in the tab. */
|
|
9
|
+
export type TabBarItem = {
|
|
10
|
+
/** Identifies the tab; compared against the bar's `value`. */
|
|
11
|
+
value: string;
|
|
12
|
+
/** Accessible name of the icon-only button. */
|
|
13
|
+
label: string;
|
|
14
|
+
/** The tab icon; sized by the bar (20/22/24px per rung). */
|
|
15
|
+
icon: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Tab bar rungs: `sm` 56x40, `md` 64x48 (the Figma size) and `lg` 72x56. */
|
|
9
19
|
export type TabBarSize = ControlSize;
|
|
10
20
|
|
|
21
|
+
/** Props of {@link TabBar}: the destinations, the selected value, and the track rung and surface. */
|
|
11
22
|
export type TabBarProps = {
|
|
23
|
+
/** Destinations in display order; each keeps its fixed tap size and scrolls if the row overflows. */
|
|
12
24
|
items: TabBarItem[];
|
|
25
|
+
/** Currently selected item's `value`; the control is fully controlled. */
|
|
13
26
|
value: string;
|
|
27
|
+
/** Called with the clicked item's `value`; the bar itself does not change `value`. */
|
|
14
28
|
onValueChange?: (value: string) => void;
|
|
15
|
-
/** `md` is the 64x48 Figma tab; `sm` is 56x40 and `lg` 72x56. */
|
|
29
|
+
/** `md` is the 64x48 Figma tab; `sm` is 56x40 and `lg` 72x56. @default "md" */
|
|
16
30
|
size?: TabBarSize;
|
|
31
|
+
/** `glass` (default) adds the liquid glass layer inside the pill; `flat` keeps the glass fill but omits that layer. @default "glass" */
|
|
17
32
|
surface?: "glass" | "flat";
|
|
33
|
+
/** Accessible name of the `<nav>` landmark. @default "Main" */
|
|
18
34
|
"aria-label"?: string;
|
|
35
|
+
/** Extra classes on the scrolling track. */
|
|
19
36
|
className?: string;
|
|
20
37
|
};
|
|
21
38
|
|
|
@@ -35,6 +52,34 @@ const tabClass: Record<TabBarSize, string> = {
|
|
|
35
52
|
* Figma `Tab bar` (63:441): the mobile bottom navigation, a regular-glass pill
|
|
36
53
|
* with icon-only 64x48 tabs and 22px icons; the current tab is a 10% ink pill
|
|
37
54
|
* that slides to the tab you pick.
|
|
55
|
+
*
|
|
56
|
+
* The bar is controlled: it reports clicks through `onValueChange` and marks the matching
|
|
57
|
+
* item with `aria-current="page"`. Tabs keep their fixed tap size, so an over-wide row scrolls
|
|
58
|
+
* horizontally instead of shrinking; the moving pill is measured from the current tab and
|
|
59
|
+
* honours `prefers-reduced-motion`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { useState } from "react";
|
|
64
|
+
* import { House, MagnifyingGlass, User } from "@phosphor-icons/react";
|
|
65
|
+
* import { TabBar } from "@horon-ai/react";
|
|
66
|
+
*
|
|
67
|
+
* function MobileNav() {
|
|
68
|
+
* const [tab, setTab] = useState("home");
|
|
69
|
+
* return (
|
|
70
|
+
* <TabBar
|
|
71
|
+
* aria-label="Sections"
|
|
72
|
+
* value={tab}
|
|
73
|
+
* onValueChange={setTab}
|
|
74
|
+
* items={[
|
|
75
|
+
* { value: "home", label: "Home", icon: <House /> },
|
|
76
|
+
* { value: "search", label: "Search", icon: <MagnifyingGlass /> },
|
|
77
|
+
* { value: "profile", label: "Profile", icon: <User /> },
|
|
78
|
+
* ]}
|
|
79
|
+
* />
|
|
80
|
+
* );
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
38
83
|
*/
|
|
39
84
|
export function TabBar({
|
|
40
85
|
items,
|
package/src/components/tabs.tsx
CHANGED
|
@@ -5,23 +5,61 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/** Tab rungs, shared with `ControlSize`: `sm` 26px, `md` 30px and `lg` 36px tab height. */
|
|
8
9
|
export type TabsSize = ControlSize;
|
|
9
10
|
|
|
10
11
|
/** `Tabs` owns the size; every `TabsTab` inside reads it so one list stays on one rung. */
|
|
11
12
|
const TabsSizeContext = createContext<ControlSize>("md");
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Shape of a tab list: `pill` is the tray of rounded pill tabs, `underline` is a bare
|
|
16
|
+
* text row with a 2px bar that slides under the active tab.
|
|
17
|
+
*/
|
|
13
18
|
export type TabsListVariant = "pill" | "underline";
|
|
14
19
|
|
|
15
20
|
/** `TabsList` owns the variant; its `TabsTab`s read it to drop the pill shape on the underline list. */
|
|
16
21
|
const TabsVariantContext = createContext<TabsListVariant>("pill");
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
/** Drops the native `className` slot and re-adds it as optional, so every part takes extra classes. */
|
|
24
|
+
type WithClass<T> = Omit<T, "className"> & {
|
|
25
|
+
/** Extra classes appended after the part's own classes, so conflicting utilities win. */
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
19
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The root of a tab set (Figma `Tabs` example 62:353): it stacks the list and panels in a
|
|
31
|
+
* 16px-gap column, caps at its container, and provides the size rung every `TabsTab` inside
|
|
32
|
+
* reads. It is controlled or uncontrolled like Base UI's tabs (`value`/`defaultValue` plus
|
|
33
|
+
* `onValueChange`), and panels are matched to tabs by `value`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* import { Tabs, TabsList, TabsPanel, TabsTab } from "@horon-ai/react";
|
|
38
|
+
*
|
|
39
|
+
* function SettingsTabs() {
|
|
40
|
+
* return (
|
|
41
|
+
* <Tabs defaultValue="general">
|
|
42
|
+
* <TabsList variant="underline">
|
|
43
|
+
* <TabsTab value="general">General</TabsTab>
|
|
44
|
+
* <TabsTab value="billing" count={3}>
|
|
45
|
+
* Billing
|
|
46
|
+
* </TabsTab>
|
|
47
|
+
* </TabsList>
|
|
48
|
+
* <TabsPanel value="general">General settings</TabsPanel>
|
|
49
|
+
* <TabsPanel value="billing">Three open invoices</TabsPanel>
|
|
50
|
+
* </Tabs>
|
|
51
|
+
* );
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
20
55
|
export function Tabs({
|
|
21
56
|
size = "md",
|
|
22
57
|
className,
|
|
23
58
|
...props
|
|
24
|
-
}: WithClass<ComponentProps<typeof BaseTabs.Root>> & {
|
|
59
|
+
}: WithClass<ComponentProps<typeof BaseTabs.Root>> & {
|
|
60
|
+
/** Rung for this tab set: tab heights of 26px (`sm`), 30px (`md`) or 36px (`lg`). @default "md" */
|
|
61
|
+
size?: TabsSize;
|
|
62
|
+
}) {
|
|
25
63
|
return (
|
|
26
64
|
<TabsSizeContext.Provider value={size}>
|
|
27
65
|
<BaseTabs.Root className={cn("flex max-w-full min-w-0 flex-col gap-4", className)} {...props} />
|
|
@@ -44,6 +82,8 @@ const listClass: Record<TabsSize, string> = {
|
|
|
44
82
|
*
|
|
45
83
|
* `variant="underline"` is the document variant: no tray, a full-width 1px default border under
|
|
46
84
|
* the tabs, and a 2px primary bar that slides under the active tab. `surface` does not apply to it.
|
|
85
|
+
*
|
|
86
|
+
* Part of {@link Tabs}; it also provides the variant every `TabsTab` inside reads.
|
|
47
87
|
*/
|
|
48
88
|
export function TabsList({
|
|
49
89
|
variant = "pill",
|
|
@@ -52,9 +92,9 @@ export function TabsList({
|
|
|
52
92
|
children,
|
|
53
93
|
...props
|
|
54
94
|
}: WithClass<ComponentProps<typeof BaseTabs.List>> & {
|
|
55
|
-
/** `pill` (default) is the tray of pill tabs; `underline` is a text row with a sliding underline. */
|
|
95
|
+
/** `pill` (default) is the tray of pill tabs; `underline` is a text row with a sliding underline. @default "pill" */
|
|
56
96
|
variant?: TabsListVariant;
|
|
57
|
-
/** `solid` (default) is an opaque surface tray with a default border; `glass` is the regular glass tray. */
|
|
97
|
+
/** `solid` (default) is an opaque surface tray with a default border; `glass` is the regular glass tray. @default "solid" */
|
|
58
98
|
surface?: "solid" | "glass";
|
|
59
99
|
}) {
|
|
60
100
|
const size = useContext(TabsSizeContext);
|
|
@@ -112,7 +152,14 @@ const countClass: Record<TabsSize, string> = {
|
|
|
112
152
|
lg: "h-6 px-2",
|
|
113
153
|
};
|
|
114
154
|
|
|
115
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* Figma `Tabs` (62:335): a pill tab; the list's indicator draws the selection under the active tab.
|
|
157
|
+
*
|
|
158
|
+
* Part of {@link Tabs}. The tab takes its height, text and icon size from the surrounding
|
|
159
|
+
* `Tabs`; on an underline list it drops the pill radius and side padding. Base UI marks it
|
|
160
|
+
* `role="tab"` with `aria-selected`, and hover or the active state moves the label to the
|
|
161
|
+
* primary text color.
|
|
162
|
+
*/
|
|
116
163
|
export function TabsTab({
|
|
117
164
|
className,
|
|
118
165
|
icon,
|
|
@@ -157,6 +204,13 @@ export function TabsTab({
|
|
|
157
204
|
);
|
|
158
205
|
}
|
|
159
206
|
|
|
207
|
+
/**
|
|
208
|
+
* The content region of one tab, rendered by Base UI with `role="tabpanel"` and matched to a
|
|
209
|
+
* `TabsTab` by `value`. The outline is suppressed, and a panel swap slides the content in 4px
|
|
210
|
+
* over the shared 200ms swap move; a change in content height is not animated.
|
|
211
|
+
*
|
|
212
|
+
* Part of {@link Tabs}.
|
|
213
|
+
*/
|
|
160
214
|
export function TabsPanel({ className, ...props }: WithClass<ComponentProps<typeof BaseTabs.Panel>>) {
|
|
161
215
|
return <BaseTabs.Panel className={cn("animate-swap-move outline-none", className)} {...props} />;
|
|
162
216
|
}
|
|
@@ -4,26 +4,46 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { Spinner } from "./spinner";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Task lifecycle states a {@link TaskRow} renders: `running` shows a spinner and Cancel,
|
|
9
|
+
* `review` a check and Review, `failed` a warning and Retry, `done` a muted check and Archive.
|
|
10
|
+
*/
|
|
7
11
|
export type TaskStatus = "running" | "review" | "failed" | "done";
|
|
8
12
|
|
|
13
|
+
/** Props of {@link TaskRow}: the task's state and texts plus the actions offered for that state. */
|
|
9
14
|
export type TaskRowProps = {
|
|
15
|
+
/** Drives the status icon, the message color and which actions appear. */
|
|
10
16
|
status: TaskStatus;
|
|
17
|
+
/** Task name, one truncated line at `label-lg`. */
|
|
11
18
|
title: ReactNode;
|
|
12
19
|
/** Elapsed time, such as "1m 56s". */
|
|
13
20
|
elapsed?: ReactNode;
|
|
14
21
|
/** Bot initial and name. */
|
|
15
|
-
bot: {
|
|
22
|
+
bot: {
|
|
23
|
+
/** One or two characters shown in the 18px tile; falls back to the status color when `tileClassName` is absent. */
|
|
24
|
+
initial: string;
|
|
25
|
+
/** Bot name after the tile, kept on one line. */
|
|
26
|
+
name: ReactNode;
|
|
27
|
+
/** Overrides the status-driven tile fill; the text is forced to `text-inverse` when set. */
|
|
28
|
+
tileClassName?: string;
|
|
29
|
+
};
|
|
30
|
+
/** Project shown after the bot name, separated by a dot and a folder icon. */
|
|
16
31
|
project?: ReactNode;
|
|
17
32
|
/** Status line: the current step, the result or the error. */
|
|
18
33
|
message: ReactNode;
|
|
19
34
|
/** Caption after the message, such as "2 drafts · 1 calendar change". */
|
|
20
35
|
detail?: ReactNode;
|
|
36
|
+
/** Called by the Cancel pill; shown only while `running`. */
|
|
21
37
|
onCancel?: () => void;
|
|
38
|
+
/** Called by the Retry pill; shown only when `failed`. */
|
|
22
39
|
onRetry?: () => void;
|
|
40
|
+
/** Called by the Review pill; shown only when `review`. */
|
|
23
41
|
onReview?: () => void;
|
|
42
|
+
/** Called by the archive icon button; shown when `review` or `done`. */
|
|
24
43
|
onArchive?: () => void;
|
|
25
|
-
/** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. */
|
|
44
|
+
/** `card` is a standalone subtle row; `list` is transparent with a 1px divider, for stacking inside a list. @default "card" */
|
|
26
45
|
variant?: "card" | "list";
|
|
46
|
+
/** Extra classes on the row container, which owns the radius and divider. */
|
|
27
47
|
className?: string;
|
|
28
48
|
};
|
|
29
49
|
|
|
@@ -37,7 +57,33 @@ const botTile: Record<TaskStatus, string> = {
|
|
|
37
57
|
const pill =
|
|
38
58
|
"motion-press flex h-7.5 shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full px-3 text-label-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-text-brand";
|
|
39
59
|
|
|
40
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Figma `Task row` (Agent tasks 112:939): a flat subtle row, 16px radius, with Running / Review / Failed / Done.
|
|
62
|
+
*
|
|
63
|
+
* The status drives the icon, the message color and the one primary action offered: Cancel while
|
|
64
|
+
* running, Retry when failed, Review when waiting for a person, and Archive once review or done.
|
|
65
|
+
* Every state change animates in with the shared pop/fade motion.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* import { TaskRow } from "@horon-ai/react";
|
|
70
|
+
*
|
|
71
|
+
* function RunningTask() {
|
|
72
|
+
* return (
|
|
73
|
+
* <TaskRow
|
|
74
|
+
* status="running"
|
|
75
|
+
* title="Draft the Q4 launch note"
|
|
76
|
+
* elapsed="1m 56s"
|
|
77
|
+
* bot={{ initial: "H", name: "Horon" }}
|
|
78
|
+
* project="Launch"
|
|
79
|
+
* message="Writing the second draft"
|
|
80
|
+
* detail="2 drafts · 1 calendar change"
|
|
81
|
+
* onCancel={() => cancelTask("launch-note")}
|
|
82
|
+
* />
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
41
87
|
export function TaskRow({
|
|
42
88
|
status,
|
|
43
89
|
title,
|
|
@@ -5,20 +5,29 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
import { fieldBoxClass, fieldTextClass, type InputSurface } from "./input";
|
|
7
7
|
|
|
8
|
+
/** Field rungs, shared with `ControlSize`: `sm` 88px, `md` 112px and `lg` 136px boxes. */
|
|
8
9
|
export type TextareaFieldSize = ControlSize;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Props of {@link TextareaField}: the native textarea attributes plus a label row, a description
|
|
13
|
+
* or error footer, an optional counter and the surface.
|
|
14
|
+
*/
|
|
10
15
|
export type TextareaFieldProps = Omit<ComponentProps<"textarea">, "className"> & {
|
|
16
|
+
/** Field label, one line at the rung's label size. */
|
|
11
17
|
label: ReactNode;
|
|
12
18
|
/** Right side of the label row, such as "Optional". */
|
|
13
19
|
hint?: ReactNode;
|
|
20
|
+
/** Help text in the footer; replaced by `error` when one is set. */
|
|
14
21
|
description?: ReactNode;
|
|
15
22
|
/** Error message; replaces the description and turns the border and footer red. */
|
|
16
23
|
error?: ReactNode;
|
|
17
24
|
/** Shows a "count / max" counter when set. */
|
|
18
25
|
maxLength?: number;
|
|
26
|
+
/** Box height, text and footer sizes: 88/112/136px. @default "md" */
|
|
19
27
|
size?: TextareaFieldSize;
|
|
20
|
-
/** Solid (default) or glass, as on `Input`. */
|
|
28
|
+
/** Solid (default) or glass, as on `Input`. @default "solid" */
|
|
21
29
|
surface?: InputSurface;
|
|
30
|
+
/** Extra classes on the field root, which is capped at 360px by default. */
|
|
22
31
|
className?: string;
|
|
23
32
|
};
|
|
24
33
|
|
|
@@ -47,7 +56,32 @@ const boxClass: Record<TextareaFieldSize, string> = {
|
|
|
47
56
|
lg: "h-34 px-5 py-3.5",
|
|
48
57
|
};
|
|
49
58
|
|
|
50
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Figma `Textarea field` (60:466): label row, glass or solid box, footer with help text and counter.
|
|
61
|
+
*
|
|
62
|
+
* Built on Base UI's field: the label is linked to the textarea, `disabled` dims the whole field
|
|
63
|
+
* to 60%, and `error` marks the field invalid, swaps the description for the error and paints the
|
|
64
|
+
* border and footer with the danger color. The counter follows the live length, including a
|
|
65
|
+
* controlled `value` that changes without an input event.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* import { TextareaField } from "@horon-ai/react";
|
|
70
|
+
*
|
|
71
|
+
* function PromptField() {
|
|
72
|
+
* return (
|
|
73
|
+
* <TextareaField
|
|
74
|
+
* label="Instructions"
|
|
75
|
+
* hint="Optional"
|
|
76
|
+
* description="Tell the bot how to answer."
|
|
77
|
+
* maxLength={280}
|
|
78
|
+
* size="md"
|
|
79
|
+
* placeholder="Be concise and cite sources."
|
|
80
|
+
* />
|
|
81
|
+
* );
|
|
82
|
+
* }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
51
85
|
export function TextareaField({
|
|
52
86
|
label,
|
|
53
87
|
hint,
|
|
@@ -5,14 +5,20 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
import { fieldBoxClass, fieldTextClass, type InputSurface } from "./input";
|
|
7
7
|
|
|
8
|
+
/** Textarea rungs, shared with `ControlSize`: `sm` 88px, `md` 112px and `lg` 136px minimum heights. */
|
|
8
9
|
export type TextareaSize = ControlSize;
|
|
9
10
|
|
|
11
|
+
/** Props of {@link Textarea}: the native textarea attributes plus the rung, invalid state and surface. */
|
|
10
12
|
export type TextareaProps = Omit<ComponentProps<"textarea">, "className"> & {
|
|
13
|
+
/** Minimum box height, padding and text rung: 88/112/136px. @default "md" */
|
|
11
14
|
size?: TextareaSize;
|
|
15
|
+
/** Marks the box invalid (`data-invalid`) and the textarea `aria-invalid`, painting the danger border. */
|
|
12
16
|
invalid?: boolean;
|
|
13
|
-
/** Solid (default) or glass, as on `Input`. */
|
|
17
|
+
/** Solid (default) or glass, as on `Input`. @default "solid" */
|
|
14
18
|
surface?: InputSurface;
|
|
19
|
+
/** Extra classes on the box that wraps the textarea. */
|
|
15
20
|
className?: string;
|
|
21
|
+
/** Classes for the native `<textarea>` itself, such as a fixed height or font. */
|
|
16
22
|
textareaClassName?: string;
|
|
17
23
|
};
|
|
18
24
|
|
|
@@ -22,7 +28,34 @@ const sizeClass: Record<TextareaSize, string> = {
|
|
|
22
28
|
lg: "min-h-34 px-5 py-3.5",
|
|
23
29
|
};
|
|
24
30
|
|
|
25
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Figma `Textarea` (58:137): an 88/112/136px glass or solid box with 12/14/20px padding, the same field rungs as `Input`.
|
|
33
|
+
*
|
|
34
|
+
* A bare field box: no label or footer (use {@link TextareaField} for those). The box draws the
|
|
35
|
+
* `:focus-within` ring and the invalid border; the textarea itself fills it, starts at three rows
|
|
36
|
+
* and is not user-resizable, so the rung keeps its height.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* import { useState } from "react";
|
|
41
|
+
* import { Textarea } from "@horon-ai/react";
|
|
42
|
+
*
|
|
43
|
+
* function Notes() {
|
|
44
|
+
* const [notes, setNotes] = useState("");
|
|
45
|
+
* return (
|
|
46
|
+
* <Textarea
|
|
47
|
+
* aria-label="Notes"
|
|
48
|
+
* size="md"
|
|
49
|
+
* surface="solid"
|
|
50
|
+
* rows={4}
|
|
51
|
+
* placeholder="Add a note"
|
|
52
|
+
* value={notes}
|
|
53
|
+
* onChange={(event) => setNotes(event.target.value)}
|
|
54
|
+
* />
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
26
59
|
export function Textarea({
|
|
27
60
|
size = "md",
|
|
28
61
|
invalid,
|