@horon-ai/react 0.1.1 → 0.1.2
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/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,44 +3,87 @@ import { type CSSProperties, useEffect, useRef } from "react";
|
|
|
3
3
|
import { useColorScheme } from "../hooks/use-color-scheme";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Every knob the fragment shader reads. Each key becomes a uniform named `u` plus the capitalised key
|
|
8
|
+
* (`speed` → `uSpeed`), and a hex string is uploaded as a `vec3`, so add a field here and a matching
|
|
9
|
+
* uniform in the shader or it silently does nothing. `cursor` and `maxDpr` are engine keys instead: they
|
|
10
|
+
* never become uniforms and are skipped when uniforms are bound.
|
|
11
|
+
*/
|
|
6
12
|
export type FlueConfig = {
|
|
13
|
+
/** Flat ground color, hex. Painted on the wrapper, so it is also the fallback when WebGL2 is unavailable. */
|
|
7
14
|
bgColor: string;
|
|
15
|
+
/** Faintest ramp stop, a near-ground tint that only the thinnest smoke reaches. */
|
|
8
16
|
colorA: string;
|
|
17
|
+
/** Second ramp stop: the first visible blue of the plume. */
|
|
9
18
|
colorB: string;
|
|
19
|
+
/** Third ramp stop: the dense body of the plume. */
|
|
10
20
|
colorC: string;
|
|
21
|
+
/** Densest ramp stop, also the glow color at high density. */
|
|
11
22
|
colorD: string;
|
|
23
|
+
/** Zoom of the base noise field; larger values make the smoke finer and busier. */
|
|
12
24
|
scale: number;
|
|
25
|
+
/** Time multiplier, so the drift speed follows it. Larger values scroll faster. */
|
|
13
26
|
speed: number;
|
|
27
|
+
/** Vertical advection added to the curl field per second, the internal flow of the plume. */
|
|
14
28
|
flow: number;
|
|
29
|
+
/** Gain of each fbm octave (0..1); lower values keep the coarse shape dominant. */
|
|
15
30
|
roughness: number;
|
|
31
|
+
/** Frequency step between fbm octaves; larger values add detail at smaller and smaller scale. */
|
|
16
32
|
lacunarity: number;
|
|
33
|
+
/** Zoom of the density field: larger values make individual wisps smaller. */
|
|
17
34
|
smokeScale: number;
|
|
35
|
+
/** Amount of curl-noise displacement; higher values swirl the smoke more. */
|
|
18
36
|
curl: number;
|
|
37
|
+
/** Zoom of the curl potential field, so the swirls themselves change size. */
|
|
19
38
|
curlScale: number;
|
|
39
|
+
/** Upward drift of the smoke in uv per second. */
|
|
20
40
|
rise: number;
|
|
41
|
+
/** Vertical frequency multiplier for the density field; larger values compress the wisps vertically. */
|
|
21
42
|
slender: number;
|
|
43
|
+
/** Half-width (uv) of the vertical corridor the plume is allowed to fill. */
|
|
22
44
|
width: number;
|
|
45
|
+
/** How much the corridor widens from the bottom to the top of the frame. */
|
|
23
46
|
spread: number;
|
|
47
|
+
/** How far the pointer's x pushes the top of the plume sideways. */
|
|
24
48
|
lean: number;
|
|
49
|
+
/** Overall density multiplier before the threshold. */
|
|
25
50
|
density: number;
|
|
51
|
+
/** Gain around `midpoint`: higher values give a harder edge between ground and smoke. */
|
|
26
52
|
contrast: number;
|
|
53
|
+
/** Threshold (0..1) where smoke starts to show; higher values leave more of the ground bare. */
|
|
27
54
|
midpoint: number;
|
|
55
|
+
/** Strength of the `colorD` bloom on the densest part, added as `pow(f, 4)`. */
|
|
28
56
|
glow: number;
|
|
57
|
+
/** Smoothstep width, over the density value, at which the ground gives way to the ramp; 0 floors at 0.01. */
|
|
29
58
|
sink: number;
|
|
59
|
+
/** Film-grain strength; 0 turns grain off. */
|
|
30
60
|
grain: number;
|
|
61
|
+
/** Grain refresh multiplier (24 steps per second at 1); 0 holds one static grain field. */
|
|
31
62
|
grainAnim: number;
|
|
63
|
+
/** Triangle-dither amplitude added to every channel to stop gradient banding. */
|
|
32
64
|
dither: number;
|
|
65
|
+
/** Corner darkening; multiplied by the squared uv distance from the centre. */
|
|
33
66
|
vignette: number;
|
|
34
67
|
/** 1 lets the smoke follow the real pointer; 0 (default) ignores it and a slow virtual pointer sways the smoke. */
|
|
35
68
|
cursor: number;
|
|
69
|
+
/** Radius (uv) of the pointer trail's gaussian falloff. */
|
|
36
70
|
pointerRadius: number;
|
|
71
|
+
/** How strongly the pointer's velocity drags the smoke along, through the trail. */
|
|
37
72
|
draft: number;
|
|
73
|
+
/** How much the pointer trail brightens the density, added as `trail * 0.30 * this`. */
|
|
38
74
|
pointerStrength: number;
|
|
75
|
+
/** How far the whole field shifts against the pointer, in uv. */
|
|
39
76
|
parallax: number;
|
|
77
|
+
/** Device-pixel-ratio cap for the canvas; 1 halves the pixel count on a 2x screen and keeps the fill cheap. */
|
|
40
78
|
maxDpr: number;
|
|
41
79
|
};
|
|
42
80
|
|
|
43
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The Horon sky palette for Flue. Recolour through these values only, never the shader.
|
|
83
|
+
*
|
|
84
|
+
* It is a complete `FlueConfig`, so it doubles as the base every variant tunes on top of, and its
|
|
85
|
+
* `cursor: 0` / `maxDpr: 1` keep the default cheap: a still-ish plume at 1x pixel density.
|
|
86
|
+
*/
|
|
44
87
|
export const FLUE_SKY: FlueConfig = {
|
|
45
88
|
bgColor: "#eef3f8",
|
|
46
89
|
colorA: "#e3ecf6",
|
|
@@ -77,7 +120,12 @@ export const FLUE_SKY: FlueConfig = {
|
|
|
77
120
|
maxDpr: 1,
|
|
78
121
|
};
|
|
79
122
|
|
|
80
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* The night palette for dark mode: near-black ground and blue smoke. Same motion as `FLUE_SKY`.
|
|
125
|
+
*
|
|
126
|
+
* Only the five colors and `glow` differ; every other number is inherited, so light and dark read as the
|
|
127
|
+
* same plume in two lights.
|
|
128
|
+
*/
|
|
81
129
|
export const FLUE_NIGHT: FlueConfig = {
|
|
82
130
|
...FLUE_SKY,
|
|
83
131
|
bgColor: "#050608",
|
|
@@ -91,9 +139,18 @@ export const FLUE_NIGHT: FlueConfig = {
|
|
|
91
139
|
/** Where the smoke is drawn; each place tunes the motion and colour on top of `FLUE_SKY` / `FLUE_NIGHT`. */
|
|
92
140
|
export type FlueVariant = "default" | "sidebar" | "auth";
|
|
93
141
|
|
|
94
|
-
|
|
142
|
+
/** One variant's overrides: values layered over the base palette per scheme, plus the tint laid on top. */
|
|
143
|
+
type FlueTuning = {
|
|
144
|
+
/** Overrides applied on top of `FLUE_SKY` in light mode. */
|
|
145
|
+
light: Partial<FlueConfig>;
|
|
146
|
+
/** Overrides applied on top of `FLUE_NIGHT` in dark mode. */
|
|
147
|
+
dark: Partial<FlueConfig>;
|
|
148
|
+
/** Classes for the tint div over the smoke; `""` adds none, and it yields to `tintClassName`. */
|
|
149
|
+
tint: string;
|
|
150
|
+
};
|
|
95
151
|
|
|
96
152
|
/**
|
|
153
|
+
* - `default`: no motion overrides, under a regular glass tint in light mode and none in dark.
|
|
97
154
|
* - `sidebar`: a narrow, tall column under the sidebar's glass. The glass blurs the detail away, so the smoke
|
|
98
155
|
* is calmer and larger, kept low in the column so the nav labels at the top sit on the quiet part. The canvas
|
|
99
156
|
* paints above the sidebar's own fill, so in light mode the variant lays the thick glass (80% white) over the
|
|
@@ -188,10 +245,13 @@ function activate(gl: WebGL2RenderingContext, program: WebGLProgram): void {
|
|
|
188
245
|
gl[USE_PROGRAM](program);
|
|
189
246
|
}
|
|
190
247
|
|
|
248
|
+
/** Props of {@link FlueBackground}: placement, which variant, and per-instance shader overrides. */
|
|
191
249
|
export type FlueBackgroundProps = {
|
|
250
|
+
/** Extra classes for the wrapper, which is already `absolute inset-0 -z-10 overflow-hidden`. */
|
|
192
251
|
className?: string;
|
|
252
|
+
/** Inline styles for the wrapper; `backgroundColor` is set from the resolved `bgColor` before these apply. */
|
|
193
253
|
style?: CSSProperties;
|
|
194
|
-
/** `sidebar` under the sidebar glass, `auth` behind the sign-in screen, `default` otherwise. */
|
|
254
|
+
/** `sidebar` under the sidebar glass, `auth` behind the sign-in screen, `default` otherwise. @default "default" */
|
|
195
255
|
variant?: FlueVariant;
|
|
196
256
|
/** Overrides on top of the variant's `FLUE_SKY` (light) or `FLUE_NIGHT` (dark) tuning. */
|
|
197
257
|
config?: Partial<FlueConfig>;
|
|
@@ -204,6 +264,30 @@ export type FlueBackgroundProps = {
|
|
|
204
264
|
* It fills its positioned parent behind the content (the parent needs `isolate`) and falls
|
|
205
265
|
* back to the flat `bgColor` when WebGL2 is missing. Reduced motion draws a single still frame.
|
|
206
266
|
* Light mode paints the white-and-sky `FLUE_SKY`; dark mode paints the black-and-blue `FLUE_NIGHT`.
|
|
267
|
+
*
|
|
268
|
+
* The wrapper is `absolute inset-0 -z-10`, `pointer-events-none` and `aria-hidden`, with a tint div over the
|
|
269
|
+
* canvas, so it never intercepts input and the content above it stays readable. Settings resolve as palette +
|
|
270
|
+
* variant tuning + `config`; the effect is keyed by their JSON, so changing `config` rebuilds the program.
|
|
271
|
+
* Drawing pauses while the canvas is off-screen (`IntersectionObserver`) or the tab is hidden, and the
|
|
272
|
+
* device-pixel ratio is capped by `maxDpr`. `cursor: 0` (the default) swaps the real pointer for a slow
|
|
273
|
+
* virtual one that wanders on two incommensurate sines, so a pointerless panel still breathes; `cursor: 1`
|
|
274
|
+
* follows the real pointer. A context lost
|
|
275
|
+
* by a previous run leaves the flat color instead of throwing, and cleanup frees this run's GL objects
|
|
276
|
+
* without dropping the shared context.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```tsx
|
|
280
|
+
* import { FlueBackground } from "@horon-ai/react";
|
|
281
|
+
*
|
|
282
|
+
* function Sidebar() {
|
|
283
|
+
* return (
|
|
284
|
+
* <div className="relative isolate min-h-96 overflow-hidden rounded-xl bg-bg-default">
|
|
285
|
+
* <FlueBackground variant="sidebar" config={{ speed: 0.5 }} />
|
|
286
|
+
* <nav className="relative p-6 text-text-primary">Navigation</nav>
|
|
287
|
+
* </div>
|
|
288
|
+
* );
|
|
289
|
+
* }
|
|
290
|
+
* ```
|
|
207
291
|
*/
|
|
208
292
|
export function FlueBackground({ className, style, variant = "default", config, tintClassName }: FlueBackgroundProps) {
|
|
209
293
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
@@ -81,12 +81,33 @@ import {
|
|
|
81
81
|
} from "@phosphor-icons/react";
|
|
82
82
|
import type { ComponentProps } from "react";
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
/** Props for the Horon mark: any svg attribute plus a size like a Phosphor icon. */
|
|
85
|
+
export type HoronIconProps = Omit<ComponentProps<"svg">, "children"> & {
|
|
86
|
+
/** Width and height together: a px number or a CSS length. @default "1em" */
|
|
87
|
+
size?: number | string;
|
|
88
|
+
};
|
|
85
89
|
|
|
86
90
|
/**
|
|
87
91
|
* Figma `icon/horon` (256:9116): the line falcon eye that stands in for Horon in
|
|
88
92
|
* navigation. Phosphor has no equivalent, so the path is copied from the Tab bar
|
|
89
93
|
* (63:441) export. It takes `currentColor` and sizes like a Phosphor icon.
|
|
94
|
+
*
|
|
95
|
+
* The svg is `aria-hidden` by default and its 22-unit viewBox scales with `size`, so it drops into any place
|
|
96
|
+
* a Phosphor glyph fits. The default `1em` makes it follow the font size, which is what a row of mixed icons
|
|
97
|
+
* needs.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```tsx
|
|
101
|
+
* import { HoronIcon } from "@horon-ai/react";
|
|
102
|
+
*
|
|
103
|
+
* function BrandMark() {
|
|
104
|
+
* return (
|
|
105
|
+
* <span className="flex size-10 items-center justify-center rounded-full bg-bg-subtle text-text-primary">
|
|
106
|
+
* <HoronIcon size={22} />
|
|
107
|
+
* </span>
|
|
108
|
+
* );
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
90
111
|
*/
|
|
91
112
|
export function HoronIcon({ size = "1em", ...props }: HoronIconProps) {
|
|
92
113
|
return (
|
|
@@ -100,6 +121,9 @@ export function HoronIcon({ size = "1em", ...props }: HoronIconProps) {
|
|
|
100
121
|
* Paper's icon contract: every icon is a component named `icon/<name>`. `iconNames` is that list in
|
|
101
122
|
* Paper order. Paper draws three sizes (16 in small controls and captions, 20 in buttons and rows,
|
|
102
123
|
* 24 in bars and empty states); 12, 14, 18 and 22 elsewhere in the code stay as they are.
|
|
124
|
+
*
|
|
125
|
+
* The tuple is the single source of the registry: `HoronIconName` derives from it and `phosphorGlyphs`
|
|
126
|
+
* must map every entry except `horon`, so a name added here fails to type-check until it has a glyph.
|
|
103
127
|
*/
|
|
104
128
|
export const iconNames = [
|
|
105
129
|
"plus",
|
|
@@ -183,6 +207,7 @@ export const iconNames = [
|
|
|
183
207
|
"plus-circle",
|
|
184
208
|
] as const;
|
|
185
209
|
|
|
210
|
+
/** Every registered icon name: a literal union derived from `iconNames`. */
|
|
186
211
|
export type HoronIconName = (typeof iconNames)[number];
|
|
187
212
|
|
|
188
213
|
const phosphorGlyphs: Record<Exclude<HoronIconName, "horon">, Icon> = {
|
|
@@ -266,13 +291,35 @@ const phosphorGlyphs: Record<Exclude<HoronIconName, "horon">, Icon> = {
|
|
|
266
291
|
"plus-circle": PlusCircle,
|
|
267
292
|
};
|
|
268
293
|
|
|
294
|
+
/** Props for `HoronGlyph`: the registry name plus the same svg attributes a Phosphor icon takes. */
|
|
269
295
|
export type HoronGlyphProps = Omit<ComponentProps<"svg">, "children" | "ref"> & {
|
|
296
|
+
/** Which registered glyph to draw; `horon` resolves to the Horon mark instead of a Phosphor icon. */
|
|
270
297
|
name: HoronIconName;
|
|
271
|
-
/** Paper sizes are 16, 20 and 24. */
|
|
298
|
+
/** Paper sizes are 16, 20 and 24. @default 20 */
|
|
272
299
|
size?: number | string;
|
|
273
300
|
};
|
|
274
301
|
|
|
275
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* Figma `icon/<name>`: resolves a registry name to its glyph. Takes `currentColor`, so the parent's text color drives it.
|
|
304
|
+
*
|
|
305
|
+
* The runtime lookup behind an icon name stored as data (a tool's icon, a settings row): `horon` returns
|
|
306
|
+
* `HoronIcon`, everything else the matching Phosphor component. Like Phosphor, the svg is `aria-hidden`;
|
|
307
|
+
* put the meaning on the surrounding button or label. Outside data-driven cases, prefer importing the
|
|
308
|
+
* Phosphor icon directly so the bundler can tree-shake it.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```tsx
|
|
312
|
+
* import { HoronGlyph, type HoronIconName } from "@horon-ai/react";
|
|
313
|
+
*
|
|
314
|
+
* function ToolIcon({ icon }: { icon: HoronIconName }) {
|
|
315
|
+
* return (
|
|
316
|
+
* <span className="text-text-secondary">
|
|
317
|
+
* <HoronGlyph name={icon} size={20} />
|
|
318
|
+
* </span>
|
|
319
|
+
* );
|
|
320
|
+
* }
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
276
323
|
export function HoronGlyph({ name, size = 20, ...props }: HoronGlyphProps) {
|
|
277
324
|
if (name === "horon") return <HoronIcon size={size} {...props} />;
|
|
278
325
|
const Glyph = phosphorGlyphs[name];
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ComponentProps } from "react";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Props of {@link HoronWordmark}: any svg attribute plus the accessible name. */
|
|
4
|
+
export type HoronWordmarkProps = Omit<ComponentProps<"svg">, "children"> & {
|
|
5
|
+
/**
|
|
6
|
+
* Accessible name of the wordmark. It becomes the `aria-label` on the `role="img"` svg, so screen readers
|
|
7
|
+
* announce it as one image rather than six paths.
|
|
8
|
+
* @default "Horon"
|
|
9
|
+
*/
|
|
10
|
+
title?: string;
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
const PATHS = [
|
|
6
14
|
"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",
|
|
@@ -14,6 +22,23 @@ const PATHS = [
|
|
|
14
22
|
/**
|
|
15
23
|
* The UI-size Horon wordmark from the Figma Sidebar (88:808): a 48.7 x 11.5
|
|
16
24
|
* vector in `currentColor`. Scale it with `height`; the width follows.
|
|
25
|
+
*
|
|
26
|
+
* Six filled paths trace the letter outlines as one drawing, painted in the current text color, so color it
|
|
27
|
+
* with the surrounding `text-*` class rather than a `fill` prop. The paths carry no individual names; only
|
|
28
|
+
* `title` gives the svg its accessible name.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import { HoronWordmark } from "@horon-ai/react";
|
|
33
|
+
*
|
|
34
|
+
* function SidebarBrand() {
|
|
35
|
+
* return (
|
|
36
|
+
* <div className="flex items-center gap-2 text-text-primary">
|
|
37
|
+
* <HoronWordmark height={12} />
|
|
38
|
+
* </div>
|
|
39
|
+
* );
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
17
42
|
*/
|
|
18
43
|
export function HoronWordmark({ title = "Horon", height = 11.5, ...props }: HoronWordmarkProps) {
|
|
19
44
|
return (
|
|
@@ -5,15 +5,31 @@ import { cn } from "../lib/cn";
|
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
import { LiquidGlass } from "./liquid-glass";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Fill and text colour of an icon button:
|
|
10
|
+
*
|
|
11
|
+
* - `primary`: solid brand fill with the inverse icon, for the one main action in a cluster.
|
|
12
|
+
* - `ghost` (default): no fill until hover, primary icon; the common toolbar and row action.
|
|
13
|
+
* - `glass`: the clear glass disc with a `LiquidGlass` layer, for floating chrome over imagery.
|
|
14
|
+
* - `solid`: bordered raised surface, for a button that sits on the page background.
|
|
15
|
+
* - `quiet`: no fill and no liquid layer, secondary icon that turns primary on hover; for dark shells
|
|
16
|
+
* and title bars where a glass disc would read as a grey bubble.
|
|
17
|
+
*/
|
|
8
18
|
export type IconButtonVariant = "primary" | "ghost" | "glass" | "solid" | "quiet";
|
|
19
|
+
/** Square footprint: `sm` 32px, `md` 40px, `lg` 48px. */
|
|
9
20
|
export type IconButtonSize = ControlSize;
|
|
10
21
|
|
|
22
|
+
/** Props of {@link IconButton}: the treatment, the footprint and the glyph. */
|
|
11
23
|
export type IconButtonProps = Omit<ComponentProps<typeof BaseButton>, "className" | "children"> & {
|
|
24
|
+
/** Treatment, from the list on `IconButtonVariant`. @default "ghost" */
|
|
12
25
|
variant?: IconButtonVariant;
|
|
26
|
+
/** Square footprint: `sm` 32px, `md` 40px, `lg` 48px, with the icon at 16/20/24px. @default "md" */
|
|
13
27
|
size?: IconButtonSize;
|
|
28
|
+
/** The glyph. It is wrapped in a positioning span; size it through the button rather than by hand. */
|
|
14
29
|
icon: ReactNode;
|
|
15
30
|
/** Accessible name; the button has no visible text. */
|
|
16
31
|
label: string;
|
|
32
|
+
/** Extra classes for the button. */
|
|
17
33
|
className?: string;
|
|
18
34
|
};
|
|
19
35
|
|
|
@@ -37,7 +53,28 @@ const variantClass: Record<IconButtonVariant, string> = {
|
|
|
37
53
|
"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
54
|
};
|
|
39
55
|
|
|
40
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Figma `Icon button` (57:167): Primary, Ghost, Glass and Solid in three sizes, plus a fill-free `quiet` variant.
|
|
58
|
+
*
|
|
59
|
+
* A round, icon-only Base UI button. `label` becomes `aria-label`, since nothing is visible; the visible
|
|
60
|
+
* disc is 32/40/48px while an invisible pseudo-element keeps a 44px touch target around it. It renders
|
|
61
|
+
* `LiquidGlass` inside the `glass` variant only, and disabled state is 40% opacity with `cursor-not-allowed`.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* import { Gear, Plus } from "@phosphor-icons/react";
|
|
66
|
+
* import { IconButton } from "@horon-ai/react";
|
|
67
|
+
*
|
|
68
|
+
* function Actions() {
|
|
69
|
+
* return (
|
|
70
|
+
* <div className="flex items-center gap-1">
|
|
71
|
+
* <IconButton icon={<Plus />} label="New agent" variant="primary" />
|
|
72
|
+
* <IconButton icon={<Gear />} label="Settings" />
|
|
73
|
+
* </div>
|
|
74
|
+
* );
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
41
78
|
export function IconButton({ variant = "ghost", size = "md", icon, label, className, ...props }: IconButtonProps) {
|
|
42
79
|
return (
|
|
43
80
|
<BaseButton
|
|
@@ -5,8 +5,10 @@ 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
|
+
/** Height rung of the group and its segments: `sm` 32px, `md` 40px, `lg` 48px. */
|
|
8
9
|
export type InputGroupSize = ControlSize;
|
|
9
10
|
|
|
11
|
+
/** Props of {@link InputGroup}: the input's own props plus the segments around it. */
|
|
10
12
|
export type InputGroupProps = Omit<ComponentProps<typeof BaseInput>, "className" | "prefix" | "size"> & {
|
|
11
13
|
/**
|
|
12
14
|
* Segment before the input: `<Select variant="segment" />` or `<Combobox variant="segment" />`
|
|
@@ -15,19 +17,35 @@ export type InputGroupProps = Omit<ComponentProps<typeof BaseInput>, "className"
|
|
|
15
17
|
prefix?: ReactNode;
|
|
16
18
|
/** Segment after the input, such as a currency select. */
|
|
17
19
|
suffix?: ReactNode;
|
|
20
|
+
/** Height and text rung: 32/40/48px, matching a standalone `Input`. @default "md" */
|
|
18
21
|
size?: InputGroupSize;
|
|
22
|
+
/** Paints the danger border on the whole group and marks the input `aria-invalid`. */
|
|
19
23
|
invalid?: boolean;
|
|
20
|
-
/** Solid (default) or glass, as on `Input`. */
|
|
24
|
+
/** Solid (default) or glass, as on `Input`. @default "solid" */
|
|
21
25
|
surface?: InputSurface;
|
|
26
|
+
/** Extra classes for the box that wraps the segments and the input. */
|
|
22
27
|
className?: string;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
/** What a segment reads from the group it sits in. */
|
|
31
|
+
type InputGroupContextValue = {
|
|
32
|
+
/** Height rung the group was given. */
|
|
33
|
+
size: InputGroupSize;
|
|
34
|
+
/** Whether the group is disabled, so segments can dim and drop their own interactivity. */
|
|
35
|
+
disabled: boolean;
|
|
36
|
+
/** Surface the group uses, so a segment and the box stay the same material. */
|
|
37
|
+
surface: InputSurface;
|
|
38
|
+
};
|
|
26
39
|
|
|
27
40
|
/** Lets a segment (select, combobox or fixed text) take the group's size, disabled state and surface. */
|
|
28
41
|
const InputGroupContext = createContext<InputGroupContextValue | null>(null);
|
|
29
42
|
|
|
30
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* The group a segment sits in, or null outside an `InputGroup`. `Select` and `Combobox` call it in their
|
|
45
|
+
* `segment` variant to pick the right height, padding and disabled look without a prop of their own.
|
|
46
|
+
*
|
|
47
|
+
* @returns The `{ size, disabled, surface }` of the enclosing group, or `null` when there is none.
|
|
48
|
+
*/
|
|
31
49
|
export function useInputGroup() {
|
|
32
50
|
return useContext(InputGroupContext);
|
|
33
51
|
}
|
|
@@ -55,7 +73,30 @@ export const inputGroupSegmentClass: Record<InputGroupSize, string> = {
|
|
|
55
73
|
const divider =
|
|
56
74
|
"relative h-full w-px shrink-0 bg-border-default group-data-invalid:bg-border-danger group-data-disabled:bg-border-default/60";
|
|
57
75
|
|
|
58
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Figma `Input group` (60:429): a glass or solid field split into a select segment and an input segment, in three sizes.
|
|
78
|
+
*
|
|
79
|
+
* One box drawn from `fieldBoxClass` holds the optional `prefix`, the input and the optional `suffix`, with a
|
|
80
|
+
* 1px hairline between them; the hairlines turn danger-red with `invalid` and dim with the group's `disabled`.
|
|
81
|
+
* The group renders its own Base UI input, so pass `placeholder`/`value`/`onChange` as props rather than
|
|
82
|
+
* children. Size, disabled state and surface go to the segments through `InputGroupContext`, which is why a
|
|
83
|
+
* `Select` or `Combobox` with `variant="segment"` matches without extra props.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* import { InputGroup, InputGroupSegment } from "@horon-ai/react";
|
|
88
|
+
*
|
|
89
|
+
* function Website() {
|
|
90
|
+
* return (
|
|
91
|
+
* <InputGroup
|
|
92
|
+
* prefix={<InputGroupSegment>https://</InputGroupSegment>}
|
|
93
|
+
* placeholder="horon.ai"
|
|
94
|
+
* size="md"
|
|
95
|
+
* />
|
|
96
|
+
* );
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
59
100
|
export function InputGroup({
|
|
60
101
|
prefix,
|
|
61
102
|
suffix,
|
|
@@ -99,8 +140,18 @@ export function InputGroup({
|
|
|
99
140
|
);
|
|
100
141
|
}
|
|
101
142
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Part of {@link InputGroup}. A fixed-text segment, such as a unit or a `https://` prefix. Takes the group's
|
|
145
|
+
* size (falling back to `md` outside a group), sits on the 4% ink tint, and dims its text when the group is
|
|
146
|
+
* disabled.
|
|
147
|
+
*/
|
|
148
|
+
// prettier-ignore
|
|
149
|
+
export function InputGroupSegment({ className, children }: {
|
|
150
|
+
/** Extra classes for the segment. */
|
|
151
|
+
className?: string;
|
|
152
|
+
/** Segment content: short text, or text plus an icon sized by the segment's rung. */
|
|
153
|
+
children: ReactNode;
|
|
154
|
+
}) {
|
|
104
155
|
const group = useInputGroup();
|
|
105
156
|
return (
|
|
106
157
|
<div
|
package/src/components/input.tsx
CHANGED
|
@@ -4,17 +4,29 @@ import type { ComponentProps, ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import type { ControlSize } from "../lib/size";
|
|
6
6
|
|
|
7
|
+
/** Height rung shared with every sized control: `sm` 32px, `md` 40px, `lg` 48px. */
|
|
7
8
|
export type InputSize = ControlSize;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where the input sits:
|
|
12
|
+
*
|
|
13
|
+
* - `glass`: on imagery or a glass panel; uses the veil field (`.veil-field`) with no liquid layer inside.
|
|
14
|
+
* - `solid`: on a raised surface (`bg-bg-surface`), the default.
|
|
15
|
+
* - `solid-on-page`: directly on the page background (`bg-bg-default`), so the box reads against it.
|
|
16
|
+
*/
|
|
8
17
|
export type InputSurface = "glass" | "solid" | "solid-on-page";
|
|
9
18
|
|
|
19
|
+
/** Props of {@link Input}: the native input attributes plus the box's size, surface and decorations. */
|
|
10
20
|
export type InputProps = Omit<ComponentProps<typeof BaseInput>, "className" | "size"> & {
|
|
21
|
+
/** Height and text rung: 32/40/48px with `body-sm/md/lg` text. @default "md" */
|
|
11
22
|
size?: InputSize;
|
|
12
|
-
/** `glass` over imagery and glass panels, `solid` on raised surfaces, `solid-on-page` directly on the page background. */
|
|
23
|
+
/** `glass` over imagery and glass panels, `solid` on raised surfaces, `solid-on-page` directly on the page background. @default "solid" */
|
|
13
24
|
surface?: InputSurface;
|
|
14
25
|
/** Leading icon, 16px (sm, md) or 20px (lg), in the secondary text color. */
|
|
15
26
|
icon?: ReactNode;
|
|
16
27
|
/** Shows the error border. Inside a `Field`, an invalid field does the same. */
|
|
17
28
|
invalid?: boolean;
|
|
29
|
+
/** Extra classes for the box that wraps the input. */
|
|
18
30
|
className?: string;
|
|
19
31
|
/** Classes for the native `<input>`. */
|
|
20
32
|
inputClassName?: string;
|
|
@@ -34,19 +46,53 @@ export const fieldTextClass: Record<ControlSize, string> = {
|
|
|
34
46
|
lg: "text-body-lg",
|
|
35
47
|
};
|
|
36
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The class set a field-shaped control composes: the box (border, radius, focus ring), the surface fill and
|
|
51
|
+
* the inner text/input. Shared by `Input` and `InputGroup` so every field-style control stays one material.
|
|
52
|
+
*/
|
|
37
53
|
export const fieldBoxClass = {
|
|
38
54
|
/** The box is the focus target's container, so the ring is drawn on `:focus-within`, not on the inner input. */
|
|
39
55
|
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
56
|
/** The veil field (globals.css `.veil-field`): no liquid layers, so hosts render no `LiquidGlass`. */
|
|
41
57
|
glass: "veil-field has-[[data-disabled]]:opacity-60 has-[:disabled]:opacity-60",
|
|
58
|
+
/** Raised surface fill with a default border; disabled or invalid states repaint both fill and border. */
|
|
42
59
|
solid:
|
|
43
60
|
"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",
|
|
61
|
+
/** The solid fill on the page background instead of a raised surface; the same disabled and invalid states. */
|
|
44
62
|
"solid-on-page":
|
|
45
63
|
"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",
|
|
64
|
+
/** The inner control: transparent, fills the remaining width, primary text, placeholder and disabled colours. */
|
|
46
65
|
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
66
|
} as const;
|
|
48
67
|
|
|
49
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Figma `Input` (58:126): SM, MD and LG, on Glass or Solid surface.
|
|
70
|
+
*
|
|
71
|
+
* A bordered box around Base UI's input: render the box, then the optional leading icon, then the native
|
|
72
|
+
* `<input>`, which takes the remaining width. Focus is drawn on the box (`:focus-within`), so the ring never
|
|
73
|
+
* detaches from the field. `invalid` marks both the box (`data-invalid`) and the input (`aria-invalid`); a
|
|
74
|
+
* `Field` that reports invalid does the same through Base UI. Every other prop (`value`, `onChange`,
|
|
75
|
+
* `placeholder`, `disabled`, `type`, …) goes to the native input, while `className`/`inputClassName` split
|
|
76
|
+
* between the box and the input itself.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* import { MagnifyingGlass } from "@phosphor-icons/react";
|
|
81
|
+
* import { Input } from "@horon-ai/react";
|
|
82
|
+
*
|
|
83
|
+
* function Search() {
|
|
84
|
+
* return (
|
|
85
|
+
* <Input
|
|
86
|
+
* type="search"
|
|
87
|
+
* size="md"
|
|
88
|
+
* icon={<MagnifyingGlass />}
|
|
89
|
+
* placeholder="Search agents"
|
|
90
|
+
* aria-label="Search agents"
|
|
91
|
+
* />
|
|
92
|
+
* );
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
50
96
|
export function Input({
|
|
51
97
|
size = "md",
|
|
52
98
|
surface = "solid",
|
|
@@ -4,21 +4,36 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { LiquidGlass } from "./liquid-glass";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Connection state of an integration:
|
|
9
|
+
*
|
|
10
|
+
* - `connected`: sky tile, a live-dot "Connected" pill and no button.
|
|
11
|
+
* - `reconnect`: dark-sky tile, a red outline on the row, danger text in `detail` and a danger action button.
|
|
12
|
+
* - `not-connected`: subtle tile, secondary text and the primary "Connect" button.
|
|
13
|
+
*/
|
|
7
14
|
export type IntegrationStatus = "connected" | "reconnect" | "not-connected";
|
|
8
15
|
|
|
16
|
+
/** Props of {@link IntegrationRow}: the service, its connection state and the one action. */
|
|
9
17
|
export type IntegrationRowProps = {
|
|
10
18
|
/** 20px icon shown in inverse on the 40px tile. */
|
|
11
19
|
icon: ReactNode;
|
|
20
|
+
/** Service name on the first line, truncated with an ellipsis. */
|
|
12
21
|
name: ReactNode;
|
|
13
22
|
/** Account and scopes, or the reason a reconnect is needed. */
|
|
14
23
|
detail?: ReactNode;
|
|
24
|
+
/** Drives the tile fill, the pill or button on the right and the row's danger border. */
|
|
15
25
|
status: IntegrationStatus;
|
|
26
|
+
/** Called when the button is pressed; connected rows have no button, so it never fires then. */
|
|
16
27
|
onAction?: () => void;
|
|
28
|
+
/** Pill text for `connected`. @default "Connected" */
|
|
17
29
|
connectedLabel?: string;
|
|
30
|
+
/** Button text for `reconnect`. @default "Reconnect" */
|
|
18
31
|
reconnectLabel?: string;
|
|
32
|
+
/** Button text for `not-connected`. @default "Connect" */
|
|
19
33
|
connectLabel?: string;
|
|
20
|
-
/** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass row. */
|
|
34
|
+
/** `solid` (default) is an opaque surface card with a default border; `glass` is the thick glass row. @default "solid" */
|
|
21
35
|
surface?: "solid" | "glass";
|
|
36
|
+
/** Extra classes for the row. */
|
|
22
37
|
className?: string;
|
|
23
38
|
};
|
|
24
39
|
|
|
@@ -28,7 +43,33 @@ const tileClass: Record<IntegrationStatus, string> = {
|
|
|
28
43
|
"not-connected": "bg-bg-subtle text-text-secondary",
|
|
29
44
|
};
|
|
30
45
|
|
|
31
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Figma `Integration row` (Integrations 110:883): 40px tile, status pill on the right, solid or thick glass.
|
|
48
|
+
*
|
|
49
|
+
* A one-line service row: 40px rounded tile with the icon, a name and an optional detail line that share a
|
|
50
|
+
* `basis-40` column so the row wraps on narrow widths, and one action slot on the right. The pill and the
|
|
51
|
+
* button are both mounted and swapped with `motion-swap`; the inactive one is `aria-hidden` and untabbable,
|
|
52
|
+
* so the row never exposes a dead control. `glass` adds a `LiquidGlass` layer and moves the reconnect outline
|
|
53
|
+
* to the glass edge.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* import { SlackLogo } from "@phosphor-icons/react";
|
|
58
|
+
* import { IntegrationRow } from "@horon-ai/react";
|
|
59
|
+
*
|
|
60
|
+
* function Slack() {
|
|
61
|
+
* return (
|
|
62
|
+
* <IntegrationRow
|
|
63
|
+
* icon={<SlackLogo />}
|
|
64
|
+
* name="Slack"
|
|
65
|
+
* detail="horon.slack.com · 4 scopes"
|
|
66
|
+
* status="reconnect"
|
|
67
|
+
* onAction={() => reconnect("slack")}
|
|
68
|
+
* />
|
|
69
|
+
* );
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
32
73
|
export function IntegrationRow({
|
|
33
74
|
icon,
|
|
34
75
|
name,
|