@horon-ai/react 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +168 -2
  2. package/package.json +50 -5
  3. package/src/assets/sidebar-photo.jpg +0 -0
  4. package/src/components/agent-activity.tsx +202 -0
  5. package/src/components/agent-lane.tsx +80 -0
  6. package/src/components/agent-lanes-timeline.tsx +312 -0
  7. package/src/components/agent-panel.tsx +121 -0
  8. package/src/components/agent-steering-panel.tsx +274 -0
  9. package/src/components/approval-card.tsx +89 -0
  10. package/src/components/avatar.tsx +104 -0
  11. package/src/components/badge.tsx +63 -0
  12. package/src/components/banner.tsx +103 -0
  13. package/src/components/bot-instructions-panel.tsx +365 -0
  14. package/src/components/bot-list-row.tsx +42 -0
  15. package/src/components/bot-memory-panel.tsx +229 -0
  16. package/src/components/bottom-sheet.tsx +140 -0
  17. package/src/components/breadcrumb-header.tsx +87 -0
  18. package/src/components/breadcrumb.tsx +86 -0
  19. package/src/components/button.tsx +146 -0
  20. package/src/components/calendar.tsx +410 -0
  21. package/src/components/card.tsx +79 -0
  22. package/src/components/chat-bubble.tsx +36 -0
  23. package/src/components/chat-controls.tsx +118 -0
  24. package/src/components/checkbox.tsx +73 -0
  25. package/src/components/chip.tsx +81 -0
  26. package/src/components/choice-card.tsx +55 -0
  27. package/src/components/code-panel.tsx +27 -0
  28. package/src/components/combobox.tsx +312 -0
  29. package/src/components/command-palette.tsx +191 -0
  30. package/src/components/composer.tsx +124 -0
  31. package/src/components/data-table.tsx +429 -0
  32. package/src/components/date-field.tsx +100 -0
  33. package/src/components/dialog.tsx +93 -0
  34. package/src/components/directory-card.tsx +59 -0
  35. package/src/components/document-extract-panel.tsx +398 -0
  36. package/src/components/empty-state.tsx +29 -0
  37. package/src/components/field.tsx +78 -0
  38. package/src/components/file-drop.tsx +89 -0
  39. package/src/components/filter-bar.tsx +83 -0
  40. package/src/components/flue-background.tsx +402 -0
  41. package/src/components/horon-icon.tsx +280 -0
  42. package/src/components/horon-wordmark.tsx +26 -0
  43. package/src/components/icon-button.tsx +61 -0
  44. package/src/components/input-group.tsx +112 -0
  45. package/src/components/input.tsx +72 -0
  46. package/src/components/integration-row.tsx +103 -0
  47. package/src/components/kbd.tsx +35 -0
  48. package/src/components/knowledge-base-panel.tsx +185 -0
  49. package/src/components/liquid-glass.tsx +24 -0
  50. package/src/components/list-detail.tsx +275 -0
  51. package/src/components/marketplace-card.tsx +97 -0
  52. package/src/components/mcp-server-row.tsx +122 -0
  53. package/src/components/menu.tsx +165 -0
  54. package/src/components/message-extras.tsx +156 -0
  55. package/src/components/modal.tsx +108 -0
  56. package/src/components/orchestrator-panel.tsx +175 -0
  57. package/src/components/page-header.tsx +48 -0
  58. package/src/components/plan-card.tsx +70 -0
  59. package/src/components/plan-step.tsx +111 -0
  60. package/src/components/plugin-row.tsx +128 -0
  61. package/src/components/progress.tsx +21 -0
  62. package/src/components/radio-group.tsx +84 -0
  63. package/src/components/research-run-panel.tsx +232 -0
  64. package/src/components/schedule.tsx +332 -0
  65. package/src/components/segmented-control.tsx +136 -0
  66. package/src/components/select.tsx +154 -0
  67. package/src/components/settings-list.tsx +99 -0
  68. package/src/components/sidebar.tsx +542 -0
  69. package/src/components/skeleton.tsx +75 -0
  70. package/src/components/slider.tsx +29 -0
  71. package/src/components/source-row.tsx +112 -0
  72. package/src/components/spinner.tsx +29 -0
  73. package/src/components/split-button.tsx +72 -0
  74. package/src/components/switch.tsx +79 -0
  75. package/src/components/tab-bar.tsx +90 -0
  76. package/src/components/tabs.tsx +162 -0
  77. package/src/components/task-row.tsx +159 -0
  78. package/src/components/textarea-field.tsx +127 -0
  79. package/src/components/textarea.tsx +47 -0
  80. package/src/components/toast.tsx +128 -0
  81. package/src/components/tool-call.tsx +224 -0
  82. package/src/components/tool-calls-header.tsx +65 -0
  83. package/src/components/tool-permissions.tsx +105 -0
  84. package/src/components/tooltip.tsx +33 -0
  85. package/src/components/top-bar.tsx +65 -0
  86. package/src/components/usage-meter.tsx +47 -0
  87. package/src/components/workspace-menu.tsx +77 -0
  88. package/src/index.ts +86 -0
  89. package/src/lib/cn.ts +25 -0
  90. package/src/lib/cursor-ghost.ts +244 -0
  91. package/src/lib/size.ts +10 -0
  92. package/src/lib/use-active-indicator.ts +60 -0
  93. package/src/styles/globals.css +564 -0
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Cursor Ghost: copies of the arrow trail behind the real one as a motion smear.
3
+ *
4
+ * The native cursor is never hidden or replaced. The ghosts live in one fixed,
5
+ * `pointer-events: none`, `aria-hidden` layer on top of the page, so they never
6
+ * take clicks, hovers or focus. The effect only mounts on a fine hover pointer
7
+ * without `prefers-reduced-motion: reduce`, and re-checks both on change.
8
+ *
9
+ * Importing `@horon-ai/react` (or `@horon-ai/react/cursor-ghost`) mounts it once
10
+ * per page. Put `data-horon-cursor="off"` on `<html>` before that import to opt
11
+ * out, then call `mountCursorGhost()` yourself for custom options.
12
+ */
13
+
14
+ export type CursorGhostOptions = {
15
+ /** Number of arrow copies. */
16
+ ghosts?: number;
17
+ /** Arrow height in CSS px. */
18
+ size?: number;
19
+ /** Lerp factor of the first copy toward the pointer, per frame. */
20
+ headFollow?: number;
21
+ /** Blur spread of the last copy in CSS px. */
22
+ smear?: number;
23
+ /** Arrow fill. Blended with `difference`, so white reads dark on light pages. */
24
+ color?: string;
25
+ };
26
+
27
+ const DEFAULTS: Required<CursorGhostOptions> = { ghosts: 5, size: 26, headFollow: 0.16, smear: 14, color: "#FFFFFF" };
28
+
29
+ const ENABLE_QUERY = "(hover: hover) and (pointer: fine)";
30
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
31
+ const INTERACTIVE = "a, button, input";
32
+ const OPT_OUT_ATTRIBUTE = "data-horon-cursor";
33
+ const AUTO_MOUNT_KEY = Symbol.for("horon.cursorGhost");
34
+
35
+ /** Arrow drawn in a 24-unit box; its tip sits at (3, 2). */
36
+ const ARROW_PATH = "M3 2v18.2l5-4.7 3.4 7.6 3.2-1.4-3.3-7.5H18Z";
37
+ const ARROW_TIP = { x: 3, y: 2 };
38
+ const ARROW_BOX = 24;
39
+
40
+ /** Each copy follows the one ahead of it more loosely than the last. */
41
+ const FOLLOW_DECAY = 0.78;
42
+ const HEAD_OPACITY = 0.55;
43
+ /** Trail strength while the pointer is over `a, button, input`. */
44
+ const INTERACTIVE_INTENSITY = 0.5;
45
+ const SETTLE_EPSILON = 0.05;
46
+
47
+ const SVG_NS = "http://www.w3.org/2000/svg";
48
+
49
+ function startEffect({ ghosts, size, headFollow, smear, color }: Required<CursorGhostOptions>): () => void {
50
+ const count = Math.max(1, Math.round(ghosts));
51
+ const scale = size / ARROW_BOX;
52
+ const tipX = ARROW_TIP.x * scale;
53
+ const tipY = ARROW_TIP.y * scale;
54
+ const fadeDistance = size * 0.5;
55
+
56
+ const layer = document.createElement("div");
57
+ layer.setAttribute("aria-hidden", "true");
58
+ layer.setAttribute("data-horon-cursor-ghost", "");
59
+ Object.assign(layer.style, {
60
+ position: "fixed",
61
+ inset: "0",
62
+ pointerEvents: "none",
63
+ zIndex: "2147483647",
64
+ overflow: "hidden",
65
+ mixBlendMode: "difference",
66
+ contain: "strict",
67
+ visibility: "hidden",
68
+ });
69
+
70
+ const nodes: SVGSVGElement[] = [];
71
+ const follow = new Float32Array(count);
72
+ const baseOpacity = new Float32Array(count);
73
+ const xs = new Float32Array(count);
74
+ const ys = new Float32Array(count);
75
+
76
+ for (let i = 0; i < count; i += 1) {
77
+ const progress = (i + 1) / count;
78
+ follow[i] = headFollow * FOLLOW_DECAY ** i;
79
+ baseOpacity[i] = HEAD_OPACITY * (1 - i / count);
80
+
81
+ const svg = document.createElementNS(SVG_NS, "svg");
82
+ svg.setAttribute("viewBox", `0 0 ${ARROW_BOX} ${ARROW_BOX}`);
83
+ svg.setAttribute("width", String(size));
84
+ svg.setAttribute("height", String(size));
85
+ const path = document.createElementNS(SVG_NS, "path");
86
+ path.setAttribute("d", ARROW_PATH);
87
+ path.setAttribute("fill", color);
88
+ svg.append(path);
89
+ Object.assign(svg.style, {
90
+ position: "absolute",
91
+ left: "0",
92
+ top: "0",
93
+ overflow: "visible",
94
+ opacity: "0",
95
+ // CSS blur takes a standard deviation; half the smear spreads it about `smear` px.
96
+ filter: `blur(${((smear * progress) / 2).toFixed(2)}px)`,
97
+ willChange: "transform, opacity",
98
+ });
99
+ nodes.push(svg);
100
+ }
101
+ // Paint the most blurred copy first so the sharpest one sits on top.
102
+ layer.append(...nodes.toReversed());
103
+ document.body.append(layer);
104
+
105
+ let pointerX = 0;
106
+ let pointerY = 0;
107
+ let present = false;
108
+ let intensity = 1;
109
+ let targetIntensity = 1;
110
+ let frame = 0;
111
+
112
+ const tick = () => {
113
+ frame = 0;
114
+ intensity += (targetIntensity - intensity) * 0.2;
115
+ let leaderX = pointerX;
116
+ let leaderY = pointerY;
117
+ let moving = Math.abs(targetIntensity - intensity) > 0.01;
118
+
119
+ for (let i = 0; i < count; i += 1) {
120
+ // The weight, position and node of one copy, read once per frame. Every
121
+ // array here is allocated at `count`, so the fallbacks are dead: they let
122
+ // the reads type under `noUncheckedIndexedAccess`.
123
+ const rate = follow[i] ?? 0;
124
+ const node = nodes[i];
125
+ let x = xs[i] ?? leaderX;
126
+ let y = ys[i] ?? leaderY;
127
+ const dx = leaderX - x;
128
+ const dy = leaderY - y;
129
+ if (Math.abs(dx) > SETTLE_EPSILON || Math.abs(dy) > SETTLE_EPSILON) {
130
+ x += dx * rate;
131
+ y += dy * rate;
132
+ moving = true;
133
+ } else {
134
+ x = leaderX;
135
+ y = leaderY;
136
+ }
137
+ xs[i] = x;
138
+ ys[i] = y;
139
+ if (!node) continue;
140
+ const distance = Math.hypot(pointerX - x, pointerY - y);
141
+ node.style.transform = `translate3d(${(x - tipX).toFixed(2)}px, ${(y - tipY).toFixed(2)}px, 0)`;
142
+ node.style.opacity = ((baseOpacity[i] ?? 0) * Math.min(1, distance / fadeDistance) * intensity).toFixed(3);
143
+ leaderX = x;
144
+ leaderY = y;
145
+ }
146
+
147
+ if (moving) frame = requestAnimationFrame(tick);
148
+ };
149
+
150
+ const wake = () => {
151
+ if (!frame) frame = requestAnimationFrame(tick);
152
+ };
153
+
154
+ const onPointerMove = (event: PointerEvent) => {
155
+ if (event.pointerType === "touch") return;
156
+ pointerX = event.clientX;
157
+ pointerY = event.clientY;
158
+ if (!present) {
159
+ // Start every copy at the pointer instead of sweeping in from the corner.
160
+ xs.fill(pointerX);
161
+ ys.fill(pointerY);
162
+ present = true;
163
+ layer.style.visibility = "visible";
164
+ }
165
+ const target = event.target;
166
+ targetIntensity = target instanceof Element && target.closest(INTERACTIVE) ? INTERACTIVE_INTENSITY : 1;
167
+ wake();
168
+ };
169
+
170
+ const onPointerOut = (event: PointerEvent) => {
171
+ if (event.relatedTarget) return;
172
+ present = false;
173
+ layer.style.visibility = "hidden";
174
+ };
175
+
176
+ const onBlur = () => {
177
+ present = false;
178
+ layer.style.visibility = "hidden";
179
+ };
180
+
181
+ window.addEventListener("pointermove", onPointerMove, { passive: true });
182
+ document.addEventListener("pointerout", onPointerOut, { passive: true });
183
+ window.addEventListener("blur", onBlur);
184
+
185
+ return () => {
186
+ if (frame) cancelAnimationFrame(frame);
187
+ frame = 0;
188
+ window.removeEventListener("pointermove", onPointerMove);
189
+ document.removeEventListener("pointerout", onPointerOut);
190
+ window.removeEventListener("blur", onBlur);
191
+ layer.remove();
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Mounts the Cursor Ghost trail and returns its teardown. The trail appears and
197
+ * disappears on its own as the pointer type or the reduced-motion setting changes.
198
+ */
199
+ export function mountCursorGhost(options: CursorGhostOptions = {}): () => void {
200
+ if (typeof window === "undefined" || typeof document === "undefined" || !window.matchMedia) return () => {};
201
+ const config = { ...DEFAULTS, ...options };
202
+ const enable = window.matchMedia(ENABLE_QUERY);
203
+ const reducedMotion = window.matchMedia(REDUCED_MOTION_QUERY);
204
+ let stop: (() => void) | null = null;
205
+
206
+ const sync = () => {
207
+ const wanted = enable.matches && !reducedMotion.matches;
208
+ if (wanted && !stop) stop = startEffect(config);
209
+ else if (!wanted && stop) {
210
+ stop();
211
+ stop = null;
212
+ }
213
+ };
214
+
215
+ let started = false;
216
+ const begin = () => {
217
+ started = true;
218
+ sync();
219
+ enable.addEventListener("change", sync);
220
+ reducedMotion.addEventListener("change", sync);
221
+ };
222
+ if (document.body) begin();
223
+ else document.addEventListener("DOMContentLoaded", begin, { once: true });
224
+
225
+ return () => {
226
+ document.removeEventListener("DOMContentLoaded", begin);
227
+ if (!started) return;
228
+ enable.removeEventListener("change", sync);
229
+ reducedMotion.removeEventListener("change", sync);
230
+ stop?.();
231
+ stop = null;
232
+ };
233
+ }
234
+
235
+ function autoMount() {
236
+ if (typeof window === "undefined" || typeof document === "undefined") return;
237
+ const registry = globalThis as typeof globalThis & { [AUTO_MOUNT_KEY]?: true };
238
+ if (registry[AUTO_MOUNT_KEY]) return;
239
+ if (document.documentElement.getAttribute(OPT_OUT_ATTRIBUTE) === "off") return;
240
+ registry[AUTO_MOUNT_KEY] = true;
241
+ mountCursorGhost();
242
+ }
243
+
244
+ autoMount();
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The size scale every sized control shares. `md` is the default and the
3
+ * original Figma/Paper size; `sm` and `lg` step one rung down and up:
4
+ *
5
+ * - control height: sm 32px, md 40px, lg 48px (h-8 / h-10 / h-12)
6
+ * - control text: sm `label-md` / `body-sm`, md `label-lg` / `body-md`, lg `label-lg` / `body-lg`
7
+ * - icon: sm 16px, md 16-20px, lg 20-24px
8
+ * - selection marks (checkbox, radio): sm 16px, md 20px, lg 24px
9
+ */
10
+ export type ControlSize = "sm" | "md" | "lg";
@@ -0,0 +1,60 @@
1
+ import { type RefObject, useEffect, useLayoutEffect, useState } from "react";
2
+
3
+ export type IndicatorRect = { left: number; top: number; width: number; height: number };
4
+
5
+ /**
6
+ * Measures the active item inside a positioned track so one indicator can slide
7
+ * between items. `selector` finds the active item (for example `[data-pressed]`
8
+ * or `[aria-current="page"]`); the rect follows attribute changes and resizes.
9
+ * `animate` turns true one frame after the first placement, so the indicator
10
+ * lands in place instead of sliding in from 0,0.
11
+ */
12
+ export function useActiveIndicator(track: RefObject<HTMLElement | null>, selector: string) {
13
+ const [rect, setRect] = useState<IndicatorRect | null>(null);
14
+ const [animate, setAnimate] = useState(false);
15
+
16
+ useLayoutEffect(() => {
17
+ const element = track.current;
18
+ if (!element) return undefined;
19
+ const measure = () => {
20
+ const active = element.querySelector<HTMLElement>(selector);
21
+ if (!active) {
22
+ setRect(null);
23
+ return;
24
+ }
25
+ const next = {
26
+ left: active.offsetLeft,
27
+ top: active.offsetTop,
28
+ width: active.offsetWidth,
29
+ height: active.offsetHeight,
30
+ };
31
+ setRect((previous) =>
32
+ previous &&
33
+ previous.left === next.left &&
34
+ previous.top === next.top &&
35
+ previous.width === next.width &&
36
+ previous.height === next.height
37
+ ? previous
38
+ : next,
39
+ );
40
+ };
41
+ measure();
42
+ const resize = new ResizeObserver(measure);
43
+ resize.observe(element);
44
+ for (const child of Array.from(element.children)) resize.observe(child);
45
+ const mutation = new MutationObserver(measure);
46
+ mutation.observe(element, { attributes: true, subtree: true, childList: true });
47
+ return () => {
48
+ resize.disconnect();
49
+ mutation.disconnect();
50
+ };
51
+ }, [track, selector]);
52
+
53
+ useEffect(() => {
54
+ if (!rect || animate) return undefined;
55
+ const frame = requestAnimationFrame(() => setAnimate(true));
56
+ return () => cancelAnimationFrame(frame);
57
+ }, [rect, animate]);
58
+
59
+ return { rect, animate };
60
+ }