@horon-ai/react 0.1.0 → 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 +81 -11
- package/src/components/agent-lane.tsx +32 -3
- package/src/components/agent-lanes-timeline.tsx +85 -7
- package/src/components/agent-panel.tsx +53 -7
- package/src/components/agent-steering-panel.tsx +73 -21
- 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 +92 -9
- 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 +89 -10
- 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 +39 -2
- package/src/components/flue-background.tsx +91 -21
- 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 +37 -4
- 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 +54 -18
- 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 +58 -3
- 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 +48 -3
- 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 +91 -17
- 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/index.ts +8 -0
- package/src/{lib → hooks}/use-active-indicator.ts +44 -1
- package/src/hooks/use-color-scheme.ts +50 -0
- package/src/hooks/use-copy-to-clipboard.ts +55 -0
- package/src/hooks/use-device.ts +69 -0
- package/src/hooks/use-hotkey.ts +56 -0
- package/src/hooks/use-media-query.ts +43 -0
- package/src/hooks/use-platform.ts +68 -0
- package/src/hooks/use-reduced-motion.ts +25 -0
- package/src/index.ts +44 -0
- package/src/lib/cn.ts +13 -1
- package/src/lib/cursor-ghost.ts +16 -6
- package/src/lib/platform.ts +236 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/** The operating systems the shortcut helpers know; `unknown` is what a server render and an empty hint report. */
|
|
2
|
+
export type Os = "macos" | "windows" | "linux" | "ios" | "android" | "unknown";
|
|
3
|
+
|
|
4
|
+
/** Where the page runs: `desktop` inside the Tauri shell or Electron, `web` everywhere else including the server. */
|
|
5
|
+
export type Runtime = "web" | "desktop";
|
|
6
|
+
|
|
7
|
+
/** The parts of `navigator` that identify the OS; plain data so detection stays testable. */
|
|
8
|
+
export type PlatformInput = {
|
|
9
|
+
/** Full user agent string, the last and coarsest hint. */
|
|
10
|
+
userAgent?: string;
|
|
11
|
+
/** `navigator.platform`, still the most reliable hint in Safari and Firefox. */
|
|
12
|
+
platform?: string;
|
|
13
|
+
/** `navigator.userAgentData.platform`, the first hint when the browser exposes it. */
|
|
14
|
+
uaPlatform?: string;
|
|
15
|
+
/** Touch points, which is what separates iPadOS (a Mac hint) from a Mac. */
|
|
16
|
+
maxTouchPoints?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** The parts of `window` that tell a page from the desktop shell; plain data for the same reason. */
|
|
20
|
+
export type RuntimeWindow = {
|
|
21
|
+
/** Set by Tauri in the desktop shell. */
|
|
22
|
+
__TAURI_INTERNALS__?: unknown;
|
|
23
|
+
/** `window.navigator`, read for the Electron fragment in its user agent. */
|
|
24
|
+
navigator?: {
|
|
25
|
+
/** User agent string, tested for `electron`. */
|
|
26
|
+
userAgent?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** The parts of a keyboard event a shortcut reads; `KeyboardEvent` satisfies it as-is. */
|
|
31
|
+
export type ShortcutEvent = {
|
|
32
|
+
/** `KeyboardEvent.key`, for example `k`, `Enter` or `ArrowUp`. */
|
|
33
|
+
key?: string;
|
|
34
|
+
/** ⌘ on macOS and iOS. */
|
|
35
|
+
metaKey?: boolean;
|
|
36
|
+
/** Ctrl on Windows, Linux and Android. */
|
|
37
|
+
ctrlKey?: boolean;
|
|
38
|
+
/** ⌥ or Alt. */
|
|
39
|
+
altKey?: boolean;
|
|
40
|
+
/** ⇧ or Shift. */
|
|
41
|
+
shiftKey?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads the platform hints from the live `navigator`. `userAgentData` is not in
|
|
46
|
+
* the DOM typings yet, so the shape is spelled out here instead of cast away.
|
|
47
|
+
*
|
|
48
|
+
* Every field is copied out, and a missing `navigator` (a server render) yields an empty object, so
|
|
49
|
+
* {@link detectOs} falls through to `unknown` instead of throwing.
|
|
50
|
+
*
|
|
51
|
+
* @returns The four hints as {@link PlatformInput}; fields the browser does not expose stay `undefined`.
|
|
52
|
+
*/
|
|
53
|
+
export function readNavigator(): PlatformInput {
|
|
54
|
+
if (typeof navigator === "undefined") return {};
|
|
55
|
+
const nav = navigator as {
|
|
56
|
+
/** Full user agent string. */
|
|
57
|
+
userAgent?: string;
|
|
58
|
+
/** `navigator.platform`, such as `MacIntel`. */
|
|
59
|
+
platform?: string;
|
|
60
|
+
/** Number of touch points, which tells iPadOS from a Mac. */
|
|
61
|
+
maxTouchPoints?: number;
|
|
62
|
+
/** Chromium's hint object, absent in Safari and Firefox. */
|
|
63
|
+
userAgentData?: {
|
|
64
|
+
/** The OS hint, such as `macOS` or `Windows`. */
|
|
65
|
+
platform?: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
userAgent: nav.userAgent,
|
|
70
|
+
platform: nav.platform,
|
|
71
|
+
uaPlatform: nav.userAgentData?.platform,
|
|
72
|
+
maxTouchPoints: nav.maxTouchPoints,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const OS_HINTS = ["uaPlatform", "platform", "userAgent"] as const;
|
|
77
|
+
|
|
78
|
+
/** Classifies one hint string; `undefined` when the hint names no OS we know. */
|
|
79
|
+
function osFromHint(hint: string, maxTouchPoints: number): Os | undefined {
|
|
80
|
+
if (hint.includes("android")) return "android";
|
|
81
|
+
if (hint.includes("iphone") || hint.includes("ipad") || hint.includes("ipod") || hint.includes("ios")) return "ios";
|
|
82
|
+
// iPadOS reports a Mac platform and user agent, so touch points are what separate it from a Mac.
|
|
83
|
+
if (hint.includes("mac")) return maxTouchPoints > 1 ? "ios" : "macos";
|
|
84
|
+
if (hint.includes("win")) return "windows";
|
|
85
|
+
if (hint.includes("linux") || hint.includes("x11") || hint.includes("cros") || hint.includes("bsd")) return "linux";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Finds the OS from the first hint that names one: `userAgentData.platform`,
|
|
91
|
+
* then `navigator.platform`, then the user agent string. Empty input is
|
|
92
|
+
* `unknown`, which the shortcuts treat as the macOS/⌘ rendering.
|
|
93
|
+
*
|
|
94
|
+
* A hint containing `mac` is `ios` when `maxTouchPoints` is above 1, since iPadOS reports a Mac platform and
|
|
95
|
+
* user agent. A hint that matches nothing is skipped, so a browser that names no known OS still reports
|
|
96
|
+
* `unknown` rather than a wrong guess.
|
|
97
|
+
*
|
|
98
|
+
* @param input - The hints to read, usually from {@link readNavigator}; omitted means empty. @default {}
|
|
99
|
+
* @returns The first `Os` a hint names, or `unknown` when none does.
|
|
100
|
+
*/
|
|
101
|
+
export function detectOs(input: PlatformInput = {}): Os {
|
|
102
|
+
const maxTouchPoints = input.maxTouchPoints ?? 0;
|
|
103
|
+
for (const field of OS_HINTS) {
|
|
104
|
+
const hint = (input[field] ?? "").trim().toLowerCase();
|
|
105
|
+
if (!hint) continue;
|
|
106
|
+
const os = osFromHint(hint, maxTouchPoints);
|
|
107
|
+
if (os) return os;
|
|
108
|
+
}
|
|
109
|
+
return "unknown";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `desktop` for the Tauri shell and Electron; `web` everywhere else, including the server.
|
|
114
|
+
*
|
|
115
|
+
* The Tauri test wins over the user agent, so a shell that also advertises Electron still reports `desktop`.
|
|
116
|
+
*
|
|
117
|
+
* @param win - The window to inspect, usually `window`; `null` or omitted reports `web`. @default null
|
|
118
|
+
* @returns `desktop` inside either shell, `web` otherwise.
|
|
119
|
+
*/
|
|
120
|
+
export function detectRuntime(win: RuntimeWindow | null = null): Runtime {
|
|
121
|
+
if (!win) return "web";
|
|
122
|
+
if ("__TAURI_INTERNALS__" in win) return "desktop";
|
|
123
|
+
return /electron/i.test(win.navigator?.userAgent ?? "") ? "desktop" : "web";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const MAC_LABELS: Record<string, string> = {
|
|
127
|
+
mod: "⌘",
|
|
128
|
+
shift: "⇧",
|
|
129
|
+
alt: "⌥",
|
|
130
|
+
enter: "↵",
|
|
131
|
+
escape: "esc",
|
|
132
|
+
esc: "esc",
|
|
133
|
+
arrowup: "↑",
|
|
134
|
+
arrowdown: "↓",
|
|
135
|
+
arrowleft: "←",
|
|
136
|
+
arrowright: "→",
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const OTHER_LABELS: Record<string, string> = {
|
|
140
|
+
mod: "Ctrl",
|
|
141
|
+
shift: "Shift",
|
|
142
|
+
alt: "Alt",
|
|
143
|
+
enter: "Enter",
|
|
144
|
+
escape: "Esc",
|
|
145
|
+
esc: "Esc",
|
|
146
|
+
arrowup: "↑",
|
|
147
|
+
arrowdown: "↓",
|
|
148
|
+
arrowleft: "←",
|
|
149
|
+
arrowright: "→",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Draws a `+`-joined shortcut for one OS: `Mod+K` is `⌘K` on macOS and iOS,
|
|
154
|
+
* `Ctrl+K` on Windows and Linux. Mac families glue the parts, the rest keep `+`.
|
|
155
|
+
*
|
|
156
|
+
* `mod`, `shift`, `alt`, `enter`, `escape`/`esc` and the four arrows have labels; any other part is kept as
|
|
157
|
+
* written, so a bare `K` stays `K`. An empty string, or one made only of `+` and spaces, returns `""`.
|
|
158
|
+
*
|
|
159
|
+
* @param keys - Shortcut in the same syntax `Kbd` and {@link matchesShortcut} take, such as `Mod+Shift+Enter`.
|
|
160
|
+
* @param os - The OS to draw for; `unknown` draws the Mac symbols, the server default.
|
|
161
|
+
* @returns The label to render, for example `⌘⇧↵` or `Ctrl+Shift+Enter`.
|
|
162
|
+
*/
|
|
163
|
+
export function formatShortcut(keys: string, os: Os): string {
|
|
164
|
+
const parts = keys
|
|
165
|
+
.split("+")
|
|
166
|
+
.map((part) => part.trim())
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
if (parts.length === 0) return "";
|
|
169
|
+
// macOS, iOS and the unknown server default draw Mac symbols; everyone else spells them out.
|
|
170
|
+
const mac = os === "macos" || os === "ios" || os === "unknown";
|
|
171
|
+
const labels = mac ? MAC_LABELS : OTHER_LABELS;
|
|
172
|
+
return parts.map((part) => labels[part.toLowerCase()] ?? part).join(mac ? "" : "+");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Which physical key carries `Mod` for an OS; `either` is the server/unknown case. */
|
|
176
|
+
function modifierSlot(os: Os): "meta" | "ctrl" | "either" {
|
|
177
|
+
if (os === "macos" || os === "ios") return "meta";
|
|
178
|
+
if (os === "windows" || os === "linux" || os === "android") return "ctrl";
|
|
179
|
+
return "either";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* True when the platform's `Mod` key is down; the other side's key is not `Mod`.
|
|
184
|
+
*
|
|
185
|
+
* On macOS and iOS that means `metaKey` without `ctrlKey`; on Windows, Linux and Android `ctrlKey` without
|
|
186
|
+
* `metaKey`, so Ctrl+⌘ on a Mac is not `Mod`. With an `unknown` OS either key counts.
|
|
187
|
+
*
|
|
188
|
+
* @param event - The keyboard event or anything shaped like {@link ShortcutEvent}.
|
|
189
|
+
* @param os - The OS whose `Mod` to test, usually from {@link detectOs} or `usePlatform().os`.
|
|
190
|
+
* @returns Whether this event carries the platform's modifier.
|
|
191
|
+
*/
|
|
192
|
+
export function isModKey(event: ShortcutEvent, os: Os): boolean {
|
|
193
|
+
const slot = modifierSlot(os);
|
|
194
|
+
const meta = event.metaKey === true;
|
|
195
|
+
const ctrl = event.ctrlKey === true;
|
|
196
|
+
if (slot === "meta") return meta && !ctrl;
|
|
197
|
+
if (slot === "ctrl") return ctrl && !meta;
|
|
198
|
+
return meta || ctrl;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Modifier names that are never the final key of a shortcut. */
|
|
202
|
+
const MODIFIERS: Record<string, true> = { mod: true, shift: true, alt: true, ctrl: true, meta: true };
|
|
203
|
+
|
|
204
|
+
const KEY_ALIASES: Record<string, string> = { esc: "escape" };
|
|
205
|
+
|
|
206
|
+
function canonicalKey(value: string): string {
|
|
207
|
+
const lower = value.trim().toLowerCase();
|
|
208
|
+
return KEY_ALIASES[lower] ?? lower;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Tests a keyboard event against a shortcut string such as `Mod+K` or
|
|
213
|
+
* `Mod+Shift+Enter`. `Mod` is the platform key (⌘ on macOS/iOS, Ctrl
|
|
214
|
+
* elsewhere, either one when the OS is unknown).
|
|
215
|
+
*
|
|
216
|
+
* Matching is exact on the modifier set: Alt and Shift must be present or absent exactly as the shortcut says,
|
|
217
|
+
* and a missing base key (a string of modifiers only) never matches. `esc` and `escape` are the same key, and
|
|
218
|
+
* the comparison is case-insensitive.
|
|
219
|
+
*
|
|
220
|
+
* @param event - The keydown event or anything shaped like {@link ShortcutEvent}.
|
|
221
|
+
* @param keys - Shortcut string, with `mod` standing for the platform key, such as `Mod+K`.
|
|
222
|
+
* @param os - The OS whose `Mod` to test.
|
|
223
|
+
* @returns Whether the event is exactly this shortcut on this OS.
|
|
224
|
+
*/
|
|
225
|
+
export function matchesShortcut(event: ShortcutEvent, keys: string, os: Os): boolean {
|
|
226
|
+
const parts = keys
|
|
227
|
+
.split("+")
|
|
228
|
+
.map((part) => part.trim().toLowerCase())
|
|
229
|
+
.filter(Boolean);
|
|
230
|
+
const base = parts.find((part) => !MODIFIERS[part]);
|
|
231
|
+
if (!base) return false;
|
|
232
|
+
if ((event.altKey === true) !== parts.includes("alt")) return false;
|
|
233
|
+
if ((event.shiftKey === true) !== parts.includes("shift")) return false;
|
|
234
|
+
if (isModKey(event, os) !== parts.includes("mod")) return false;
|
|
235
|
+
return canonicalKey(event.key ?? "") === canonicalKey(base);
|
|
236
|
+
}
|