@horon-ai/react 0.0.0-stage → 0.1.1

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 (102) 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 +204 -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 +125 -0
  8. package/src/components/agent-steering-panel.tsx +276 -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 +369 -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 +206 -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 +388 -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 +42 -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 +146 -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 +215 -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/hooks/index.ts +8 -0
  89. package/src/hooks/use-active-indicator.ts +60 -0
  90. package/src/hooks/use-color-scheme.ts +27 -0
  91. package/src/hooks/use-copy-to-clipboard.ts +32 -0
  92. package/src/hooks/use-device.ts +41 -0
  93. package/src/hooks/use-hotkey.ts +35 -0
  94. package/src/hooks/use-media-query.ts +25 -0
  95. package/src/hooks/use-platform.ts +39 -0
  96. package/src/hooks/use-reduced-motion.ts +8 -0
  97. package/src/index.ts +99 -0
  98. package/src/lib/cn.ts +25 -0
  99. package/src/lib/cursor-ghost.ts +244 -0
  100. package/src/lib/platform.ts +169 -0
  101. package/src/lib/size.ts +10 -0
  102. package/src/styles/globals.css +564 -0
@@ -0,0 +1,388 @@
1
+ import { type CSSProperties, useEffect, useRef } from "react";
2
+
3
+ import { useColorScheme } from "../hooks/use-color-scheme";
4
+ import { cn } from "../lib/cn";
5
+
6
+ export type FlueConfig = {
7
+ bgColor: string;
8
+ colorA: string;
9
+ colorB: string;
10
+ colorC: string;
11
+ colorD: string;
12
+ scale: number;
13
+ speed: number;
14
+ flow: number;
15
+ roughness: number;
16
+ lacunarity: number;
17
+ smokeScale: number;
18
+ curl: number;
19
+ curlScale: number;
20
+ rise: number;
21
+ slender: number;
22
+ width: number;
23
+ spread: number;
24
+ lean: number;
25
+ density: number;
26
+ contrast: number;
27
+ midpoint: number;
28
+ glow: number;
29
+ sink: number;
30
+ grain: number;
31
+ grainAnim: number;
32
+ dither: number;
33
+ vignette: number;
34
+ /** 1 lets the smoke follow the real pointer; 0 (default) ignores it and a slow virtual pointer sways the smoke. */
35
+ cursor: number;
36
+ pointerRadius: number;
37
+ draft: number;
38
+ pointerStrength: number;
39
+ parallax: number;
40
+ maxDpr: number;
41
+ };
42
+
43
+ /** The Horon sky palette for Flue. Recolour through these values only, never the shader. */
44
+ export const FLUE_SKY: FlueConfig = {
45
+ bgColor: "#eef3f8",
46
+ colorA: "#e3ecf6",
47
+ colorB: "#c3d7ea",
48
+ colorC: "#7fa3c6",
49
+ colorD: "#1d3c5e",
50
+ scale: 0.2,
51
+ speed: 0.33,
52
+ flow: 0.04,
53
+ roughness: 0.36,
54
+ lacunarity: 2.55,
55
+ smokeScale: 2.51,
56
+ curl: 0.36,
57
+ curlScale: 0.88,
58
+ rise: 0.03,
59
+ slender: 1.55,
60
+ width: 1.55,
61
+ spread: 1.11,
62
+ lean: 1.25,
63
+ density: 2.15,
64
+ contrast: 1.7,
65
+ midpoint: 0.76,
66
+ glow: 0.53,
67
+ sink: 0.2,
68
+ grain: 0.02,
69
+ grainAnim: 0,
70
+ dither: 1.1,
71
+ vignette: 0.26,
72
+ cursor: 0,
73
+ pointerRadius: 0.84,
74
+ draft: 1.85,
75
+ pointerStrength: 0.16,
76
+ parallax: 0.028,
77
+ maxDpr: 1,
78
+ };
79
+
80
+ /** The night palette for dark mode: near-black ground and blue smoke. Same motion as `FLUE_SKY`. */
81
+ export const FLUE_NIGHT: FlueConfig = {
82
+ ...FLUE_SKY,
83
+ bgColor: "#050608",
84
+ colorA: "#07090d",
85
+ colorB: "#0c1a2c",
86
+ colorC: "#1f4a78",
87
+ colorD: "#4f8fd0",
88
+ glow: 0.35,
89
+ };
90
+
91
+ /** Where the smoke is drawn; each place tunes the motion and colour on top of `FLUE_SKY` / `FLUE_NIGHT`. */
92
+ export type FlueVariant = "default" | "sidebar" | "auth";
93
+
94
+ type FlueTuning = { light: Partial<FlueConfig>; dark: Partial<FlueConfig>; tint: string };
95
+
96
+ /**
97
+ * - `sidebar`: a narrow, tall column under the sidebar's glass. The glass blurs the detail away, so the smoke
98
+ * is calmer and larger, kept low in the column so the nav labels at the top sit on the quiet part. The canvas
99
+ * paints above the sidebar's own fill, so in light mode the variant lays the thick glass (80% white) over the
100
+ * smoke itself: its dense blue band would otherwise drop secondary labels to 2:1. Dark smoke is already dark
101
+ * enough behind light text, so dark lays nothing.
102
+ * - `auth`: the whole sign-in screen behind a centred card. The plume is wider and rises from the bottom
103
+ * centre so it frames the card. The shader's vignette darkens towards black, so light mode keeps none (it
104
+ * would turn the pale sky grey) and stops the plume short of the navy end of the ramp; dark mode uses it
105
+ * to sink the corners into the night ground. No tint.
106
+ */
107
+ const FLUE_VARIANTS: Record<FlueVariant, FlueTuning> = {
108
+ default: { light: {}, dark: {}, tint: "bg-glass-regular dark:bg-transparent" },
109
+ sidebar: {
110
+ light: {
111
+ speed: 0.22,
112
+ smokeScale: 1.9,
113
+ density: 1.85,
114
+ contrast: 1.45,
115
+ midpoint: 0.8,
116
+ glow: 0.3,
117
+ vignette: 0,
118
+ },
119
+ dark: {
120
+ speed: 0.22,
121
+ smokeScale: 1.9,
122
+ density: 1.85,
123
+ contrast: 1.5,
124
+ midpoint: 0.8,
125
+ glow: 0.22,
126
+ vignette: 0,
127
+ },
128
+ tint: "bg-glass-thick dark:bg-transparent",
129
+ },
130
+ auth: {
131
+ light: { width: 1.9, spread: 1.3, density: 1.85, midpoint: 0.8, glow: 0.3, vignette: 0 },
132
+ dark: { width: 1.9, spread: 1.3, density: 2.0, midpoint: 0.76, glow: 0.45, vignette: 0.45 },
133
+ tint: "",
134
+ },
135
+ };
136
+
137
+ const ENGINE_KEYS = new Set(["cursor", "maxDpr"]);
138
+
139
+ const VERT = `#version 300 es
140
+ void main() {
141
+ vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
142
+ gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
143
+ }`;
144
+
145
+ const FRAG = `#version 300 es
146
+ precision highp float;
147
+ out vec4 fragColor;
148
+ uniform vec2 iResolution; uniform float iTime; uniform vec2 iMouse; uniform vec2 iMouseVel; uniform vec2 iMouseWake;
149
+ uniform vec3 uBgColor, uColorA, uColorB, uColorC, uColorD;
150
+ uniform float uScale, uSpeed, uFlow, uRoughness, uLacunarity;
151
+ uniform float uSmokeScale, uCurl, uCurlScale, uRise, uSlender;
152
+ uniform float uWidth, uSpread, uLean, uDensity;
153
+ uniform float uContrast, uMidpoint, uGlow, uSink;
154
+ uniform float uGrain, uDither, uVignette;
155
+ uniform float uPointerRadius, uDraft, uPointerStrength, uParallax;
156
+ uniform float uGrainAnim;
157
+ #define OCTAVES 3
158
+ #define TRAIL_TAPS 6
159
+ vec2 hash2(vec2 p){ p=vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))); return -1.0+2.0*fract(sin(p)*43758.5453123); }
160
+ float snoise(vec2 p){ const float K1=0.366025404,K2=0.211324865; vec2 i=floor(p+(p.x+p.y)*K1); vec2 a=p-i+(i.x+i.y)*K2; float m=step(a.y,a.x); vec2 o=vec2(m,1.0-m); vec2 b=a-o+K2; vec2 c=a-1.0+2.0*K2; vec3 h=max(0.5-vec3(dot(a,a),dot(b,b),dot(c,c)),0.0); vec3 n=h*h*h*h*vec3(dot(a,hash2(i)),dot(b,hash2(i+o)),dot(c,hash2(i+1.0))); return dot(n,vec3(70.0)); }
161
+ float fbm(vec2 p){ float v=0.0,amp=0.5; for(int i=0; i<OCTAVES; i++){ v+=amp*snoise(p); p*=uLacunarity; amp*=uRoughness; } return v; }
162
+ float trail(vec2 uv,float radius){ vec2 wob=vec2(snoise(uv*1.7+vec2(0.0,iTime*0.05)),snoise(uv*1.7+vec2(4.3,-iTime*0.04))); uv+=wob*radius*0.18; float a=0.0,wsum=0.0; float r2=max(1e-4,radius*radius); for(int i=0; i<TRAIL_TAPS; i++){ float k=float(i)/float(TRAIL_TAPS-1); vec2 c=mix(iMouseWake,iMouse,k); vec2 dd=uv-c; float w=mix(0.30,1.0,k); a+=w*exp(-dot(dd,dd)/r2); wsum+=w; } return a/wsum; }
163
+ float potential(vec2 p){ return snoise(p)+0.5*snoise(p*2.03+vec2(3.7,-1.9)); }
164
+ vec2 curl(vec2 p,float e){ float nx1=potential(p+vec2(0.0,e)); float nx2=potential(p-vec2(0.0,e)); float ny1=potential(p+vec2(e,0.0)); float ny2=potential(p-vec2(e,0.0)); return vec2(nx1-nx2,ny2-ny1)/(2.0*e); }
165
+ vec3 ramp4(float t){ vec3 c=mix(uColorA,uColorB,smoothstep(0.00,0.36,t)); c=mix(c,uColorC,smoothstep(0.32,0.70,t)); c=mix(c,uColorD,smoothstep(0.66,1.00,t)); return c; }
166
+ float triDither(vec2 fc){ float a=fract(sin(dot(fc,vec2(12.9898,78.233)))*43758.5453); float b=fract(sin(dot(fc+17.0,vec2(12.9898,78.233)))*43758.5453); return (a+b-1.0)/255.0; }
167
+ float houseGrain(vec2 fc){ uvec2 q=uvec2(fc)*uvec2(1597334677u,3812015801u)+uint(floor(iTime*24.0*uGrainAnim))*2654435769u; uint n=q.x^q.y; n=n*1664525u+1013904223u; n^=n>>16u; n*=2246822519u; n^=n>>13u; float a=float(n&0xffffu)/65535.0; n*=3266489917u; n^=n>>16u; float b=float(n&0xffffu)/65535.0; return a+b-1.0; }
168
+ void main(){ vec2 uv=(gl_FragCoord.xy-0.5*iResolution)/iResolution.y; float t=iTime*uSpeed; float tr=trail(uv,uPointerRadius); vec2 p=(uv-iMouse*uParallax)*uScale; vec2 sp=p+vec2(0.0,-t*uRise); vec2 c=curl(sp*uCurlScale+vec2(0.0,t*uFlow),0.14)*uCurl; c+=iMouseVel*uDraft*tr; vec2 mp=vec2(sp.x,sp.y*uSlender)*uSmokeScale+c; float dens=fbm(mp)*0.5+0.5; float h=uv.y+0.55; float lean=iMouse.x*uLean*clamp(h,0.0,1.6); float width=max(0.06,uWidth*(0.45+uSpread*clamp(h,0.0,1.8))); float xr=(uv.x-lean)/width; float corridor=exp(-xr*xr); float cool=smoothstep(-0.65,0.85,uv.y); float smoke=dens*corridor*mix(1.0,0.30,cool)*uDensity; float f=clamp((smoke-uMidpoint)*uContrast+0.5+tr*uPointerStrength*0.30,0.0,1.0); vec3 col=ramp4(f); col+=uColorD*uGlow*pow(f,4.0); col=mix(uBgColor,col,smoothstep(0.0,max(0.01,uSink),f)*0.90+0.10); col*=1.0-uVignette*dot(uv,uv); { float hgL=clamp(dot(col,vec3(0.299,0.587,0.114)),0.0,1.0); col+=houseGrain(gl_FragCoord.xy)*uGrain*mix(1.0,4.0*hgL*(1.0-hgL),0.6); } col+=triDither(gl_FragCoord.xy)*uDither; fragColor=vec4(clamp(col,0.0,1.0),1.0); }`;
169
+
170
+ function hexToVec3(hex: string): [number, number, number] {
171
+ const value = Number.parseInt(hex.slice(1), 16);
172
+ return [((value >> 16) & 255) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];
173
+ }
174
+
175
+ function compile(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader {
176
+ const shader = gl.createShader(type);
177
+ if (!shader) throw new Error("Flue: createShader failed");
178
+ gl.shaderSource(shader, source);
179
+ gl.compileShader(shader);
180
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
181
+ throw new Error(gl.getShaderInfoLog(shader) ?? "Flue: compile failed");
182
+ return shader;
183
+ }
184
+
185
+ const USE_PROGRAM = "useProgram";
186
+
187
+ function activate(gl: WebGL2RenderingContext, program: WebGLProgram): void {
188
+ gl[USE_PROGRAM](program);
189
+ }
190
+
191
+ export type FlueBackgroundProps = {
192
+ className?: string;
193
+ style?: CSSProperties;
194
+ /** `sidebar` under the sidebar glass, `auth` behind the sign-in screen, `default` otherwise. */
195
+ variant?: FlueVariant;
196
+ /** Overrides on top of the variant's `FLUE_SKY` (light) or `FLUE_NIGHT` (dark) tuning. */
197
+ config?: Partial<FlueConfig>;
198
+ /** Classes for the tint laid over the smoke; defaults to the variant's tint. */
199
+ tintClassName?: string;
200
+ };
201
+
202
+ /**
203
+ * Flue: an animated WebGL2 smoke gradient, one fullscreen triangle and one fragment shader.
204
+ * It fills its positioned parent behind the content (the parent needs `isolate`) and falls
205
+ * back to the flat `bgColor` when WebGL2 is missing. Reduced motion draws a single still frame.
206
+ * Light mode paints the white-and-sky `FLUE_SKY`; dark mode paints the black-and-blue `FLUE_NIGHT`.
207
+ */
208
+ export function FlueBackground({ className, style, variant = "default", config, tintClassName }: FlueBackgroundProps) {
209
+ const canvasRef = useRef<HTMLCanvasElement>(null);
210
+ const dark = useColorScheme() === "dark";
211
+ const tuning = FLUE_VARIANTS[variant];
212
+ const settings = { ...(dark ? FLUE_NIGHT : FLUE_SKY), ...(dark ? tuning.dark : tuning.light), ...config };
213
+ const key = JSON.stringify(settings);
214
+
215
+ useEffect(() => {
216
+ const canvas = canvasRef.current;
217
+ if (!canvas) return undefined;
218
+ const cfg: FlueConfig = JSON.parse(key);
219
+ const gl = canvas.getContext("webgl2", {
220
+ alpha: false,
221
+ antialias: false,
222
+ depth: false,
223
+ stencil: false,
224
+ powerPreference: "high-performance",
225
+ });
226
+ // StrictMode replays the effect on the same canvas. A context that a previous run lost
227
+ // cannot compile again, so fall back to the flat bgColor instead of throwing.
228
+ if (!gl || gl.isContextLost()) return undefined;
229
+
230
+ const program = gl.createProgram();
231
+ if (!program) return undefined;
232
+ const vertexShader = compile(gl, gl.VERTEX_SHADER, VERT);
233
+ const fragmentShader = compile(gl, gl.FRAGMENT_SHADER, FRAG);
234
+ gl.attachShader(program, vertexShader);
235
+ gl.attachShader(program, fragmentShader);
236
+ gl.linkProgram(program);
237
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS))
238
+ throw new Error(gl.getProgramInfoLog(program) ?? "Flue: link failed");
239
+ activate(gl, program);
240
+ const vertexArray = gl.createVertexArray();
241
+ gl.bindVertexArray(vertexArray);
242
+
243
+ const locations = new Map<string, WebGLUniformLocation | null>();
244
+ const loc = (name: string) => {
245
+ if (!locations.has(name)) locations.set(name, gl.getUniformLocation(program, name));
246
+ return locations.get(name) ?? null;
247
+ };
248
+ for (const [name, value] of Object.entries(cfg)) {
249
+ if (ENGINE_KEYS.has(name)) continue;
250
+ const uniform = `u${name.charAt(0).toUpperCase()}${name.slice(1)}`;
251
+ if (typeof value === "string") gl.uniform3f(loc(uniform), ...hexToVec3(value));
252
+ else gl.uniform1f(loc(uniform), value);
253
+ }
254
+
255
+ const resize = () => {
256
+ const dpr = Math.min(window.devicePixelRatio || 1, cfg.maxDpr);
257
+ const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
258
+ const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
259
+ if (canvas.width !== width || canvas.height !== height) {
260
+ canvas.width = width;
261
+ canvas.height = height;
262
+ }
263
+ gl.viewport(0, 0, width, height);
264
+ gl.uniform2f(loc("iResolution"), width, height);
265
+ };
266
+
267
+ const mouse = { x: 0, y: 0, ax: 0, ay: 0, wx: 0, wy: 0, tx: 0, ty: 0 };
268
+ const aim = (event: PointerEvent) => {
269
+ if (!cfg.cursor) return;
270
+ const rect = canvas.getBoundingClientRect();
271
+ if (rect.width === 0 || rect.height === 0) return;
272
+ mouse.tx = ((event.clientX - rect.left) / rect.width - 0.5) * (rect.width / rect.height);
273
+ mouse.ty = 0.5 - (event.clientY - rect.top) / rect.height;
274
+ };
275
+
276
+ let clock = 0;
277
+ const draw = () => {
278
+ gl.uniform1f(loc("iTime"), clock);
279
+ gl.uniform2f(loc("iMouse"), mouse.x, mouse.y);
280
+ gl.uniform2f(loc("iMouseVel"), mouse.ax - mouse.x, mouse.ay - mouse.y);
281
+ gl.uniform2f(loc("iMouseWake"), mouse.wx, mouse.wy);
282
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
283
+ };
284
+
285
+ const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
286
+ let still = motionQuery.matches;
287
+ let resizeQueued = 0;
288
+ const observer = new ResizeObserver(() => {
289
+ cancelAnimationFrame(resizeQueued);
290
+ resizeQueued = requestAnimationFrame(() => {
291
+ resize();
292
+ if (still) draw();
293
+ });
294
+ });
295
+ observer.observe(canvas);
296
+ resize();
297
+ clock = still ? 12 : 0;
298
+ draw();
299
+
300
+ let visible = true;
301
+ const visibility = new IntersectionObserver((entries) => {
302
+ visible = entries.some((entry) => entry.isIntersecting);
303
+ });
304
+ visibility.observe(canvas);
305
+
306
+ let raf = 0;
307
+ let previous = performance.now();
308
+ const frame = (now: number) => {
309
+ raf = requestAnimationFrame(frame);
310
+ const raw = now - previous;
311
+ previous = now;
312
+ if (!visible || document.hidden) return;
313
+ const ms = Math.min(50, Math.max(4.167, raw));
314
+ const step = ms > 36.7 ? 2.2 : ms * 0.06;
315
+ clock += ms * 0.001;
316
+ // Without the real pointer, a slow virtual one wanders across the canvas on two incommensurate
317
+ // sines, so the plume leans, sways and trails on its own the way it does under a moving cursor.
318
+ if (!cfg.cursor) {
319
+ const aspect = canvas.clientWidth / Math.max(1, canvas.clientHeight);
320
+ mouse.tx = aspect * (0.3 * Math.sin(clock * 0.23) + 0.12 * Math.sin(clock * 0.57 + 0.8));
321
+ mouse.ty = 0.22 * Math.sin(clock * 0.19 + 1.3) + 0.08 * Math.sin(clock * 0.41);
322
+ }
323
+ const kLead = 0.105 * step;
324
+ const kBody = 0.043 * step;
325
+ const kWake = 0.017 * step;
326
+ mouse.ax += (mouse.tx - mouse.ax) * kLead;
327
+ mouse.ay += (mouse.ty - mouse.ay) * kLead;
328
+ mouse.x += (mouse.ax - mouse.x) * kBody;
329
+ mouse.y += (mouse.ay - mouse.y) * kBody;
330
+ mouse.wx += (mouse.x - mouse.wx) * kWake;
331
+ mouse.wy += (mouse.y - mouse.wy) * kWake;
332
+ draw();
333
+ };
334
+ // The plume follows the OS switch while the page is open: turning on "reduce motion"
335
+ // freezes the current frame, turning it off resumes from the same clock.
336
+ const listen = () => {
337
+ if (still) {
338
+ window.removeEventListener("pointermove", aim);
339
+ window.removeEventListener("pointerdown", aim);
340
+ return;
341
+ }
342
+ if (cfg.cursor) {
343
+ window.addEventListener("pointermove", aim, { passive: true });
344
+ window.addEventListener("pointerdown", aim, { passive: true });
345
+ }
346
+ previous = performance.now();
347
+ if (!raf) raf = requestAnimationFrame(frame);
348
+ };
349
+ const onMotionChange = (event: MediaQueryListEvent) => {
350
+ still = event.matches;
351
+ if (still) {
352
+ cancelAnimationFrame(raf);
353
+ raf = 0;
354
+ }
355
+ listen();
356
+ };
357
+ motionQuery.addEventListener("change", onMotionChange);
358
+ listen();
359
+
360
+ return () => {
361
+ motionQuery.removeEventListener("change", onMotionChange);
362
+ cancelAnimationFrame(raf);
363
+ cancelAnimationFrame(resizeQueued);
364
+ observer.disconnect();
365
+ visibility.disconnect();
366
+ window.removeEventListener("pointermove", aim);
367
+ window.removeEventListener("pointerdown", aim);
368
+ // Release this run's GL objects without losing the context: the next effect run
369
+ // (StrictMode, config change) reuses the same WebGL2RenderingContext.
370
+ gl.bindVertexArray(null);
371
+ gl.deleteVertexArray(vertexArray);
372
+ gl.deleteProgram(program);
373
+ gl.deleteShader(vertexShader);
374
+ gl.deleteShader(fragmentShader);
375
+ };
376
+ }, [key]);
377
+
378
+ return (
379
+ <div
380
+ aria-hidden
381
+ className={cn("pointer-events-none absolute inset-0 -z-10 overflow-hidden", className)}
382
+ style={{ backgroundColor: settings.bgColor, ...style }}
383
+ >
384
+ <canvas ref={canvasRef} className="block size-full" />
385
+ <div className={cn("absolute inset-0", tintClassName ?? tuning.tint)} />
386
+ </div>
387
+ );
388
+ }
@@ -0,0 +1,280 @@
1
+ import {
2
+ Archive,
3
+ ArrowClockwise,
4
+ ArrowDown,
5
+ ArrowElbowDownLeft,
6
+ ArrowRight,
7
+ ArrowSquareOut,
8
+ ArrowUp,
9
+ Bell,
10
+ BookmarkSimple,
11
+ Brain,
12
+ CalendarBlank,
13
+ CaretDown,
14
+ CaretLeft,
15
+ CaretRight,
16
+ CaretUpDown,
17
+ ChartLineUp,
18
+ ChatCircle,
19
+ Check,
20
+ CheckCircle,
21
+ ClipboardText,
22
+ Clock,
23
+ Code,
24
+ Command,
25
+ Copy,
26
+ Cube,
27
+ DotsThree,
28
+ Drop,
29
+ EnvelopeSimple,
30
+ Eye,
31
+ FilePdf,
32
+ FileText,
33
+ Files,
34
+ Flask,
35
+ Folder,
36
+ Gear,
37
+ GitPullRequest,
38
+ Globe,
39
+ House,
40
+ Image,
41
+ Info,
42
+ Key,
43
+ Lightning,
44
+ List,
45
+ ListChecks,
46
+ MagnifyingGlass,
47
+ Megaphone,
48
+ Microphone,
49
+ Minus,
50
+ PaperPlaneTilt,
51
+ Paperclip,
52
+ Pause,
53
+ PenNib,
54
+ PencilSimple,
55
+ Play,
56
+ Plug,
57
+ Plus,
58
+ PlusCircle,
59
+ Question,
60
+ Repeat,
61
+ Robot,
62
+ Scan,
63
+ SignOut,
64
+ SlidersHorizontal,
65
+ Sparkle,
66
+ SquaresFour,
67
+ Stop,
68
+ ThumbsDown,
69
+ ThumbsUp,
70
+ Trash,
71
+ Tray,
72
+ UploadSimple,
73
+ User,
74
+ UserPlus,
75
+ UsersThree,
76
+ Warning,
77
+ WarningCircle,
78
+ Wrench,
79
+ X,
80
+ type Icon,
81
+ } from "@phosphor-icons/react";
82
+ import type { ComponentProps } from "react";
83
+
84
+ export type HoronIconProps = Omit<ComponentProps<"svg">, "children"> & { size?: number | string };
85
+
86
+ /**
87
+ * Figma `icon/horon` (256:9116): the line falcon eye that stands in for Horon in
88
+ * navigation. Phosphor has no equivalent, so the path is copied from the Tab bar
89
+ * (63:441) export. It takes `currentColor` and sizes like a Phosphor icon.
90
+ */
91
+ export function HoronIcon({ size = "1em", ...props }: HoronIconProps) {
92
+ return (
93
+ <svg viewBox="0 0 22 22" width={size} height={size} fill="currentColor" aria-hidden {...props}>
94
+ <path d="M20.1444 5.12948C19.6411 5.2126 19.307 5.26623 17.0453 5.62287C16.2541 5.74889 15.3486 5.89369 15.0343 5.94464C14.72 5.99559 14.1221 6.09212 13.7045 6.15915C9.0054 6.918 6.10856 7.48379 3.04688 8.23996C1.89737 8.52419 1.98968 8.44911 1.05997 9.83005C0.251141 11.026 0.255537 11.0233 0.240152 11.0984C0.198391 11.2914 0.279714 11.5194 0.415984 11.5998C0.55665 11.6802 0.653358 11.6588 1.1347 11.4255C2.09079 10.9643 2.63807 10.7096 2.65345 10.7203C2.66005 10.7257 2.73038 10.8436 2.80511 10.9804C4.99202 14.9302 7.76138 16.9386 11.0231 16.9359C13.3616 16.9359 15.2892 16.0054 17.397 13.8603C18.1245 13.1229 19.6411 11.337 19.7246 11.1225C19.8191 10.8865 19.8015 10.6157 19.6828 10.4146C19.6059 10.2832 17.7179 8.63145 16.7376 7.84042C16.5992 7.7278 16.5003 7.63127 16.5178 7.62859C16.5354 7.62322 16.8981 7.56155 17.3201 7.48647C17.7443 7.41407 18.4674 7.28804 18.9311 7.20492C19.3949 7.12447 20.1686 6.99845 20.6521 6.92337C21.1356 6.85097 21.5466 6.77857 21.5686 6.76516C21.6851 6.6874 21.7708 6.50774 21.7708 6.34417C21.7708 6.2101 21.7159 6.10016 21.4258 5.64968C21.0191 5.02222 20.951 4.99541 20.1444 5.12948ZM14.9485 8.37403C16.0079 9.18919 18.0564 10.8865 18.0564 10.9482C18.0564 10.9616 17.9047 11.1627 17.7223 11.3933C15.4409 14.2464 13.4034 15.4879 11.0121 15.4825C8.2735 15.4745 5.93053 13.7932 4.1722 10.5701C3.97219 10.2001 3.84911 9.95608 3.8601 9.94267C3.91285 9.881 6.76134 9.25086 8.00096 9.02562C8.20756 8.98808 8.50428 8.93445 8.66033 8.90495C8.81858 8.87814 8.95924 8.85401 8.97683 8.85401C8.99221 8.85401 8.93506 8.94518 8.84935 9.0578C7.69984 10.5326 8.03173 12.911 9.51971 13.8469C11.0846 14.8336 12.966 13.5841 13.1572 11.4282C13.2583 10.2913 12.7396 9.08461 11.9066 8.51883C11.8209 8.45983 11.7484 8.4062 11.7484 8.39816C11.7484 8.38475 14.2606 7.97986 14.3771 7.97181C14.4013 7.96913 14.6584 8.15147 14.9485 8.37403Z" />
95
+ </svg>
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Paper's icon contract: every icon is a component named `icon/<name>`. `iconNames` is that list in
101
+ * Paper order. Paper draws three sizes (16 in small controls and captions, 20 in buttons and rows,
102
+ * 24 in bars and empty states); 12, 14, 18 and 22 elsewhere in the code stay as they are.
103
+ */
104
+ export const iconNames = [
105
+ "plus",
106
+ "x",
107
+ "check",
108
+ "minus",
109
+ "chevron-down",
110
+ "chevron-right",
111
+ "chevron-left",
112
+ "search",
113
+ "send",
114
+ "arrow-up",
115
+ "arrow-right",
116
+ "bot",
117
+ "user",
118
+ "settings",
119
+ "bell",
120
+ "info",
121
+ "circle-alert",
122
+ "circle-check",
123
+ "triangle-alert",
124
+ "inbox",
125
+ "mail",
126
+ "calendar",
127
+ "paperclip",
128
+ "mic",
129
+ "ellipsis",
130
+ "copy",
131
+ "log-out",
132
+ "sparkles",
133
+ "eye",
134
+ "trash",
135
+ "globe",
136
+ "question",
137
+ "drop",
138
+ "list-checks",
139
+ "grid",
140
+ "key",
141
+ "external-link",
142
+ "caret-up-down",
143
+ "user-plus",
144
+ "folder",
145
+ "thumbs-up",
146
+ "arrow-clockwise",
147
+ "image",
148
+ "plug",
149
+ "thumbs-down",
150
+ "chat-circle",
151
+ "command",
152
+ "enter",
153
+ "wrench",
154
+ "clock",
155
+ "repeat",
156
+ "house",
157
+ "file-text",
158
+ "git-pull-request",
159
+ "upload",
160
+ "archive",
161
+ "stop",
162
+ "sliders",
163
+ "lightning",
164
+ "pause",
165
+ "play",
166
+ "arrow-down",
167
+ "scan",
168
+ "file-pdf",
169
+ "brain",
170
+ "bookmark-simple",
171
+ "files",
172
+ "list",
173
+ "horon",
174
+ "cube",
175
+ "code",
176
+ "clipboard-text",
177
+ "megaphone",
178
+ "pen-nib",
179
+ "chart-line-up",
180
+ "flask",
181
+ "users-three",
182
+ "pencil-simple",
183
+ "plus-circle",
184
+ ] as const;
185
+
186
+ export type HoronIconName = (typeof iconNames)[number];
187
+
188
+ const phosphorGlyphs: Record<Exclude<HoronIconName, "horon">, Icon> = {
189
+ plus: Plus,
190
+ x: X,
191
+ check: Check,
192
+ minus: Minus,
193
+ "chevron-down": CaretDown,
194
+ "chevron-right": CaretRight,
195
+ "chevron-left": CaretLeft,
196
+ search: MagnifyingGlass,
197
+ send: PaperPlaneTilt,
198
+ "arrow-up": ArrowUp,
199
+ "arrow-right": ArrowRight,
200
+ bot: Robot,
201
+ user: User,
202
+ settings: Gear,
203
+ bell: Bell,
204
+ info: Info,
205
+ "circle-alert": WarningCircle,
206
+ "circle-check": CheckCircle,
207
+ "triangle-alert": Warning,
208
+ inbox: Tray,
209
+ mail: EnvelopeSimple,
210
+ calendar: CalendarBlank,
211
+ paperclip: Paperclip,
212
+ mic: Microphone,
213
+ ellipsis: DotsThree,
214
+ copy: Copy,
215
+ "log-out": SignOut,
216
+ sparkles: Sparkle,
217
+ eye: Eye,
218
+ trash: Trash,
219
+ globe: Globe,
220
+ question: Question,
221
+ drop: Drop,
222
+ "list-checks": ListChecks,
223
+ grid: SquaresFour,
224
+ key: Key,
225
+ "external-link": ArrowSquareOut,
226
+ "caret-up-down": CaretUpDown,
227
+ "user-plus": UserPlus,
228
+ folder: Folder,
229
+ "thumbs-up": ThumbsUp,
230
+ "arrow-clockwise": ArrowClockwise,
231
+ image: Image,
232
+ plug: Plug,
233
+ "thumbs-down": ThumbsDown,
234
+ "chat-circle": ChatCircle,
235
+ command: Command,
236
+ enter: ArrowElbowDownLeft,
237
+ wrench: Wrench,
238
+ clock: Clock,
239
+ repeat: Repeat,
240
+ house: House,
241
+ "file-text": FileText,
242
+ "git-pull-request": GitPullRequest,
243
+ upload: UploadSimple,
244
+ archive: Archive,
245
+ stop: Stop,
246
+ sliders: SlidersHorizontal,
247
+ lightning: Lightning,
248
+ pause: Pause,
249
+ play: Play,
250
+ "arrow-down": ArrowDown,
251
+ scan: Scan,
252
+ "file-pdf": FilePdf,
253
+ brain: Brain,
254
+ "bookmark-simple": BookmarkSimple,
255
+ files: Files,
256
+ list: List,
257
+ cube: Cube,
258
+ code: Code,
259
+ "clipboard-text": ClipboardText,
260
+ megaphone: Megaphone,
261
+ "pen-nib": PenNib,
262
+ "chart-line-up": ChartLineUp,
263
+ flask: Flask,
264
+ "users-three": UsersThree,
265
+ "pencil-simple": PencilSimple,
266
+ "plus-circle": PlusCircle,
267
+ };
268
+
269
+ export type HoronGlyphProps = Omit<ComponentProps<"svg">, "children" | "ref"> & {
270
+ name: HoronIconName;
271
+ /** Paper sizes are 16, 20 and 24. */
272
+ size?: number | string;
273
+ };
274
+
275
+ /** Figma `icon/<name>`: resolves a registry name to its glyph. Takes `currentColor`, so the parent's text color drives it. */
276
+ export function HoronGlyph({ name, size = 20, ...props }: HoronGlyphProps) {
277
+ if (name === "horon") return <HoronIcon size={size} {...props} />;
278
+ const Glyph = phosphorGlyphs[name];
279
+ return <Glyph aria-hidden size={size} {...props} />;
280
+ }
@@ -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
+ }