@adea-ai/ui 0.10.8

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 (40) hide show
  1. package/README.md +5 -0
  2. package/components.json +19 -0
  3. package/package.json +56 -0
  4. package/src/components/account-drawer.tsx +133 -0
  5. package/src/components/on-screen-controls.tsx +156 -0
  6. package/src/components/prop-catalog.tsx +583 -0
  7. package/src/components/scene-settings.tsx +202 -0
  8. package/src/components/theme-provider.tsx +30 -0
  9. package/src/components/theme-toggle.tsx +73 -0
  10. package/src/components/ui/badge.tsx +49 -0
  11. package/src/components/ui/button.tsx +60 -0
  12. package/src/components/ui/card.tsx +18 -0
  13. package/src/components/ui/dialog.tsx +129 -0
  14. package/src/components/ui/drawer.tsx +209 -0
  15. package/src/components/ui/dropdown-menu.tsx +258 -0
  16. package/src/components/ui/empty.tsx +94 -0
  17. package/src/components/ui/field.tsx +224 -0
  18. package/src/components/ui/input.tsx +20 -0
  19. package/src/components/ui/label.tsx +20 -0
  20. package/src/components/ui/radio-group.tsx +38 -0
  21. package/src/components/ui/separator.tsx +21 -0
  22. package/src/components/ui/skeleton.tsx +13 -0
  23. package/src/components/ui/spinner.tsx +16 -0
  24. package/src/components/ui/switch.tsx +32 -0
  25. package/src/components/ui/tabs.tsx +75 -0
  26. package/src/components/ui/toggle-group.tsx +87 -0
  27. package/src/components/ui/toggle.tsx +45 -0
  28. package/src/components/ui/tooltip.tsx +56 -0
  29. package/src/components/version-dialog.tsx +366 -0
  30. package/src/components/workspace-brand.tsx +18 -0
  31. package/src/components/workspace-logo.tsx +17 -0
  32. package/src/index.tsx +55 -0
  33. package/src/lib/utils.ts +6 -0
  34. package/src/lib/version-notes.ts +30 -0
  35. package/src/styles/auth-shell.css +67 -0
  36. package/src/styles/conventional-workspace.css +2846 -0
  37. package/src/styles/globals.css +93 -0
  38. package/src/styles/theme.css +94 -0
  39. package/src/styles/workspace-shell.css +145 -0
  40. package/tsconfig.json +10 -0
@@ -0,0 +1,366 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import {
5
+ Check,
6
+ Download,
7
+ ExternalLink,
8
+ FileText,
9
+ LoaderCircle,
10
+ RefreshCw,
11
+ Sparkles,
12
+ } from "lucide-react";
13
+
14
+ import { formatReleaseDate, plainTextFromMarkdown } from "#lib/version-notes";
15
+ import { Button } from "#components/ui/button";
16
+ import {
17
+ Dialog,
18
+ DialogClose,
19
+ DialogContent,
20
+ DialogDescription,
21
+ DialogFooter,
22
+ DialogHeader,
23
+ DialogTitle,
24
+ DialogTrigger,
25
+ } from "#components/ui/dialog";
26
+
27
+ export type SharedDesktopUpdate = Readonly<{
28
+ available_version: string | null;
29
+ changelog: string;
30
+ current_version: string;
31
+ downloaded_bytes: number;
32
+ error: string | null;
33
+ github_url: string;
34
+ phase:
35
+ | "idle"
36
+ | "checking"
37
+ | "current"
38
+ | "available"
39
+ | "downloading"
40
+ | "installing"
41
+ | "installed"
42
+ | "failed";
43
+ release_date: string | null;
44
+ release_notes: string | null;
45
+ restart_required: boolean;
46
+ total_bytes: number | null;
47
+ }>;
48
+
49
+ export type VersionDialogAdapter = Readonly<{
50
+ check(): Promise<SharedDesktopUpdate>;
51
+ getStatus(): Promise<SharedDesktopUpdate>;
52
+ install(expectedVersion: string): Promise<SharedDesktopUpdate>;
53
+ isDesktopRuntime(): boolean;
54
+ }>;
55
+
56
+ function errorMessage(caught: unknown, fallback: string): string {
57
+ if (caught instanceof Error && caught.message) return caught.message;
58
+ if (typeof caught === "string" && caught) return caught;
59
+ return fallback;
60
+ }
61
+
62
+ function phaseLabel(update: SharedDesktopUpdate | null, fallbackVersion: string): string {
63
+ if (!update) return `Agent HQ v${fallbackVersion}`;
64
+ if (update.phase === "checking") return "Checking for updates…";
65
+ if (update.phase === "available" && update.available_version) {
66
+ return `Update v${update.available_version} available`;
67
+ }
68
+ if (update.phase === "downloading" || update.phase === "installing") {
69
+ return "Installing update…";
70
+ }
71
+ if (update.phase === "failed") return `Version ${update.current_version} · Retry`;
72
+ return `Agent HQ v${update.current_version || fallbackVersion}`;
73
+ }
74
+
75
+ function isUpdateBusy(update: SharedDesktopUpdate | null): boolean {
76
+ return (
77
+ update?.phase === "checking" ||
78
+ update?.phase === "downloading" ||
79
+ update?.phase === "installing"
80
+ );
81
+ }
82
+
83
+ export function VersionDialog({
84
+ adapter,
85
+ fallbackVersion = "0.1.0",
86
+ onOpenChange,
87
+ open: controlledOpen,
88
+ }: Readonly<{
89
+ adapter: VersionDialogAdapter;
90
+ fallbackVersion?: string;
91
+ onOpenChange?: (open: boolean) => void;
92
+ open?: boolean;
93
+ }>) {
94
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
95
+ const open = controlledOpen ?? uncontrolledOpen;
96
+ const setOpen = useCallback(
97
+ (nextOpen: boolean) => {
98
+ if (controlledOpen === undefined) setUncontrolledOpen(nextOpen);
99
+ onOpenChange?.(nextOpen);
100
+ },
101
+ [controlledOpen, onOpenChange]
102
+ );
103
+ const [desktopRuntime, setDesktopRuntime] = useState(false);
104
+ const [update, setUpdate] = useState<SharedDesktopUpdate | null>(null);
105
+ const [busy, setBusy] = useState(false);
106
+ const [error, setError] = useState("");
107
+
108
+ useEffect(() => setDesktopRuntime(adapter.isDesktopRuntime()), [adapter]);
109
+
110
+ const loadCurrentStatus = useCallback(async () => {
111
+ if (!desktopRuntime) return;
112
+ try {
113
+ setUpdate(await adapter.getStatus());
114
+ } catch (caught) {
115
+ setError(errorMessage(caught, "Version status is unavailable"));
116
+ }
117
+ }, [adapter, desktopRuntime]);
118
+
119
+ useEffect(() => {
120
+ if (desktopRuntime) void loadCurrentStatus();
121
+ }, [desktopRuntime, loadCurrentStatus]);
122
+
123
+ const checkForUpdates = useCallback(async () => {
124
+ if (!desktopRuntime) {
125
+ setError("Update checks are available from the desktop app.");
126
+ return;
127
+ }
128
+ setBusy(true);
129
+ setError("");
130
+ try {
131
+ setUpdate(await adapter.check());
132
+ } catch (caught) {
133
+ setError(errorMessage(caught, "Could not check for updates"));
134
+ await loadCurrentStatus();
135
+ } finally {
136
+ setBusy(false);
137
+ }
138
+ }, [adapter, desktopRuntime, loadCurrentStatus]);
139
+
140
+ useEffect(() => {
141
+ if (!open || !desktopRuntime) return;
142
+ let active = true;
143
+ setError("");
144
+ setBusy(true);
145
+ void (async () => {
146
+ try {
147
+ const current = await adapter.getStatus();
148
+ if (!active) return;
149
+ setUpdate(current);
150
+ const checked = await adapter.check();
151
+ if (active) setUpdate(checked);
152
+ } catch (caught) {
153
+ if (active) setError(errorMessage(caught, "Could not check for updates"));
154
+ } finally {
155
+ if (active) setBusy(false);
156
+ }
157
+ })();
158
+ return () => {
159
+ active = false;
160
+ };
161
+ }, [adapter, desktopRuntime, open]);
162
+
163
+ const install = async () => {
164
+ const version = update?.available_version;
165
+ if (!version) return;
166
+ setBusy(true);
167
+ setError("");
168
+ try {
169
+ setUpdate(await adapter.install(version));
170
+ } catch (caught) {
171
+ setError(errorMessage(caught, "Update installation failed"));
172
+ await loadCurrentStatus();
173
+ } finally {
174
+ setBusy(false);
175
+ }
176
+ };
177
+
178
+ const currentChangelog = useMemo(
179
+ () => plainTextFromMarkdown(update?.changelog || "Changelog is loading…"),
180
+ [update?.changelog]
181
+ );
182
+ const releaseNotes = update?.release_notes ? plainTextFromMarkdown(update.release_notes) : "";
183
+ const busyFromSnapshot = isUpdateBusy(update);
184
+
185
+ return (
186
+ <Dialog open={open} onOpenChange={setOpen}>
187
+ {controlledOpen === undefined ? (
188
+ <DialogTrigger
189
+ render={
190
+ <Button
191
+ type="button"
192
+ variant="ghost"
193
+ size="sm"
194
+ aria-label="Open version and updates dialog"
195
+ aria-haspopup="dialog"
196
+ onClick={() => setOpen(true)}
197
+ />
198
+ }
199
+ >
200
+ {update?.phase === "available" ? (
201
+ <Sparkles aria-hidden="true" />
202
+ ) : (
203
+ <FileText aria-hidden="true" />
204
+ )}
205
+ {phaseLabel(update, fallbackVersion)}
206
+ </DialogTrigger>
207
+ ) : null}
208
+
209
+ <DialogContent className="max-w-3xl">
210
+ <DialogHeader>
211
+ <div className="flex items-center gap-3">
212
+ <span className="flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
213
+ <Sparkles className="size-5" aria-hidden="true" />
214
+ </span>
215
+ <div>
216
+ <DialogTitle>Version & updates</DialogTitle>
217
+ <DialogDescription>
218
+ Keep Agent HQ current and review what changed in each release.
219
+ </DialogDescription>
220
+ </div>
221
+ </div>
222
+ </DialogHeader>
223
+
224
+ <div className="min-h-0 space-y-5 overflow-y-auto p-6">
225
+ <section className="rounded-xl border bg-background/45 p-4" aria-label="Version status">
226
+ <div className="flex flex-wrap items-start justify-between gap-4">
227
+ <div className="space-y-1">
228
+ <p className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
229
+ Installed version
230
+ </p>
231
+ <p className="text-xl font-semibold tracking-tight">
232
+ v{update?.current_version || fallbackVersion}
233
+ </p>
234
+ <p className="text-sm text-muted-foreground">
235
+ {update?.phase === "current"
236
+ ? "You are running the latest desktop release."
237
+ : update?.phase === "available" && update.available_version
238
+ ? `A newer desktop release, v${update.available_version}, is ready.`
239
+ : desktopRuntime
240
+ ? "Check the release channel for the latest signed build."
241
+ : "Open this dialog inside the desktop app to check for updates."}
242
+ </p>
243
+ </div>
244
+ <Button
245
+ type="button"
246
+ variant="outline"
247
+ size="sm"
248
+ disabled={!desktopRuntime || busy || busyFromSnapshot}
249
+ onClick={() => void checkForUpdates()}
250
+ >
251
+ {busy || busyFromSnapshot ? (
252
+ <LoaderCircle className="animate-spin" aria-hidden="true" />
253
+ ) : (
254
+ <RefreshCw aria-hidden="true" />
255
+ )}
256
+ Check latest version
257
+ </Button>
258
+ </div>
259
+ </section>
260
+
261
+ {update?.phase === "available" && update.available_version ? (
262
+ <section
263
+ className="rounded-xl border border-primary/35 bg-primary/8 p-4"
264
+ aria-label="Available update"
265
+ >
266
+ <div className="flex flex-wrap items-start justify-between gap-4">
267
+ <div className="space-y-1">
268
+ <p className="flex items-center gap-2 text-sm font-semibold">
269
+ <Sparkles className="size-4 text-primary" aria-hidden="true" />
270
+ Version {update.available_version} is ready
271
+ </p>
272
+ <p className="text-sm text-muted-foreground">
273
+ The signed installer will be verified before Agent HQ restarts.
274
+ </p>
275
+ {formatReleaseDate(update.release_date) ? (
276
+ <p className="text-xs text-muted-foreground">
277
+ Released {formatReleaseDate(update.release_date)}
278
+ </p>
279
+ ) : null}
280
+ </div>
281
+ <Button
282
+ type="button"
283
+ size="sm"
284
+ disabled={busy || busyFromSnapshot}
285
+ onClick={() => void install()}
286
+ >
287
+ {busy || busyFromSnapshot ? (
288
+ <LoaderCircle className="animate-spin" aria-hidden="true" />
289
+ ) : (
290
+ <Download aria-hidden="true" />
291
+ )}
292
+ Install and restart
293
+ </Button>
294
+ </div>
295
+ </section>
296
+ ) : null}
297
+
298
+ {update?.phase === "current" ? (
299
+ <p
300
+ className="flex items-center gap-2 text-sm text-emerald-700 dark:text-emerald-300"
301
+ role="status"
302
+ >
303
+ <Check className="size-4" aria-hidden="true" />
304
+ Agent HQ is up to date.
305
+ </p>
306
+ ) : null}
307
+ {error ? (
308
+ <p
309
+ className="rounded-lg border border-destructive/35 bg-destructive/8 px-3 py-2 text-sm text-destructive"
310
+ role="alert"
311
+ >
312
+ {error}
313
+ </p>
314
+ ) : null}
315
+
316
+ {releaseNotes ? (
317
+ <section className="space-y-2" aria-labelledby="agent-hq-release-notes">
318
+ <div>
319
+ <h2 id="agent-hq-release-notes" className="text-sm font-semibold">
320
+ What changed in this release
321
+ </h2>
322
+ <p className="text-xs text-muted-foreground">
323
+ Release notes are shown as readable text.
324
+ </p>
325
+ </div>
326
+ <div className="max-h-52 overflow-y-auto whitespace-pre-wrap rounded-xl border bg-background/45 p-4 font-mono text-xs leading-5 text-muted-foreground">
327
+ {releaseNotes}
328
+ </div>
329
+ </section>
330
+ ) : null}
331
+
332
+ <section className="space-y-2" aria-labelledby="agent-hq-changelog">
333
+ <div>
334
+ <h2 id="agent-hq-changelog" className="text-sm font-semibold">
335
+ Installed changelog
336
+ </h2>
337
+ <p className="text-xs text-muted-foreground">
338
+ A plain-text history of the installed channel.
339
+ </p>
340
+ </div>
341
+ <div className="max-h-72 overflow-y-auto whitespace-pre-wrap rounded-xl border bg-background/45 p-4 font-mono text-xs leading-5 text-muted-foreground">
342
+ {currentChangelog}
343
+ </div>
344
+ </section>
345
+ </div>
346
+
347
+ <DialogFooter>
348
+ {update?.github_url ? (
349
+ <Button
350
+ type="button"
351
+ variant="ghost"
352
+ size="sm"
353
+ onClick={() => window.open(update.github_url, "_blank", "noopener,noreferrer")}
354
+ >
355
+ <ExternalLink aria-hidden="true" />
356
+ View releases
357
+ </Button>
358
+ ) : null}
359
+ <DialogClose render={<Button type="button" variant="outline" size="sm" />}>
360
+ Close
361
+ </DialogClose>
362
+ </DialogFooter>
363
+ </DialogContent>
364
+ </Dialog>
365
+ );
366
+ }
@@ -0,0 +1,18 @@
1
+ export interface WorkspaceBrandProps {
2
+ title: string;
3
+ eyebrow?: string;
4
+ }
5
+
6
+ export function WorkspaceBrand({ title, eyebrow = "AGENT OPERATIONS" }: WorkspaceBrandProps) {
7
+ return (
8
+ <div className="workspace-brand">
9
+ <div className="workspace-brand__mark" aria-hidden="true">
10
+ <span>HQ</span>
11
+ </div>
12
+ <div>
13
+ <p className="workspace-eyebrow">{eyebrow}</p>
14
+ <h1>{title}</h1>
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ /** The canonical Agent HQ mark used by web, desktop, and dialogs. */
4
+ export function WorkspaceLogo(props: SVGProps<SVGSVGElement>) {
5
+ return (
6
+ <svg viewBox="0 0 64 64" role="img" aria-label="Agent HQ" {...props}>
7
+ <rect width="64" height="64" rx="16" fill="#111827" />
8
+ <path d="M10 16h7v13h9V16h7v32h-7V36h-9v12h-7z" fill="#f8fafc" />
9
+ <path
10
+ d="M45 16c-7.18 0-13 6-13 16s5.82 16 13 16c7.18 0 13-6 13-16s-5.82-16-13-16Zm0 7c3.42 0 6 3.46 6 9s-2.58 9-6 9-6-3.46-6-9 2.58-9 6-9Z"
11
+ fill="#f8fafc"
12
+ fillRule="evenodd"
13
+ />
14
+ <path d="m45 35 11 11-4 4-11-11z" fill="#f8fafc" />
15
+ </svg>
16
+ );
17
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,55 @@
1
+ export { Button, buttonVariants } from "./components/ui/button";
2
+ export { Switch } from "./components/ui/switch";
3
+ export { Toggle } from "./components/ui/toggle";
4
+ export { ToggleGroup, ToggleGroupItem } from "./components/ui/toggle-group";
5
+ export { RadioGroup, RadioGroupItem } from "./components/ui/radio-group";
6
+ export { ThemeToggle } from "./components/theme-toggle";
7
+ export { ThemeProvider } from "./components/theme-provider";
8
+ export {
9
+ Drawer,
10
+ DrawerClose,
11
+ DrawerContent,
12
+ DrawerDescription,
13
+ DrawerFooter,
14
+ DrawerHeader,
15
+ DrawerOverlay,
16
+ DrawerPortal,
17
+ DrawerSwipeHandle,
18
+ DrawerTitle,
19
+ DrawerTrigger,
20
+ } from "./components/ui/drawer";
21
+ export { AccountDrawer, type AccountDrawerProps } from "./components/account-drawer";
22
+ export { WorkspaceBrand, type WorkspaceBrandProps } from "./components/workspace-brand";
23
+ export { WorkspaceLogo } from "./components/workspace-logo";
24
+ export { OnScreenControls } from "./components/on-screen-controls";
25
+ export { SceneSettings, type SceneSettingsProps } from "./components/scene-settings";
26
+ export {
27
+ VersionDialog,
28
+ type SharedDesktopUpdate,
29
+ type VersionDialogAdapter,
30
+ } from "./components/version-dialog";
31
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/ui/tooltip";
32
+ export { Card, CardContent } from "./components/ui/card";
33
+ export { Spinner } from "./components/ui/spinner";
34
+ export {
35
+ Dialog,
36
+ DialogClose,
37
+ DialogContent,
38
+ DialogDescription,
39
+ DialogFooter,
40
+ DialogHeader,
41
+ DialogOverlay,
42
+ DialogPortal,
43
+ DialogTitle,
44
+ DialogTrigger,
45
+ } from "./components/ui/dialog";
46
+ export {
47
+ ModelThumbnail,
48
+ PropCatalog,
49
+ defaultPropCatalogCategories,
50
+ getSharedLoader,
51
+ type PropCatalogCategory,
52
+ type PropCatalogItem,
53
+ type PropCatalogProps,
54
+ } from "./components/prop-catalog";
55
+ export { cn } from "./lib/utils";
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,30 @@
1
+ export function plainTextFromMarkdown(markdown: string): string {
2
+ return markdown
3
+ .replace(/\r\n/g, "\n")
4
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
5
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
6
+ .replace(/^[ \t]*#{1,6}[ \t]*/gm, "")
7
+ .replace(/^[ \t]*>[ \t]?/gm, "")
8
+ .replace(/^[ \t]*---+[ \t]*$/gm, "")
9
+ .replace(/^[ \t]*[-*+][ \t]+/gm, "• ")
10
+ .replace(/^[ \t]*\d+\.[ \t]+/gm, "")
11
+ .replace(/```[^\n]*\n?/g, "")
12
+ .replace(/`([^`]+)`/g, "$1")
13
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
14
+ .replace(/__([^_]+)__/g, "$1")
15
+ .replace(/\*([^*]+)\*/g, "$1")
16
+ .replace(/_([^_]+)_/g, "$1")
17
+ .replace(/\n{3,}/g, "\n\n")
18
+ .trim();
19
+ }
20
+
21
+ export function formatReleaseDate(value: string | null): string | null {
22
+ if (!value) return null;
23
+ const date = new Date(value);
24
+ if (Number.isNaN(date.getTime())) return value;
25
+ return new Intl.DateTimeFormat(undefined, {
26
+ day: "numeric",
27
+ month: "short",
28
+ year: "numeric",
29
+ }).format(date);
30
+ }
@@ -0,0 +1,67 @@
1
+ .auth-shell {
2
+ --auth-background: var(--hq-shell-background);
3
+ --auth-surface: var(--hq-shell-surface);
4
+ --auth-surface-strong: var(--hq-shell-surface-strong);
5
+ --auth-border: var(--hq-shell-border);
6
+ --auth-border-subtle: var(--hq-shell-border-subtle);
7
+ --auth-foreground: var(--hq-shell-foreground);
8
+ --auth-muted: var(--hq-shell-muted);
9
+ --auth-muted-strong: var(--hq-shell-muted);
10
+ --auth-accent: var(--hq-shell-accent);
11
+ --auth-accent-hover: var(--hq-shell-accent-strong);
12
+ --auth-accent-foreground: var(--hq-shell-accent-foreground);
13
+
14
+ display: grid;
15
+ min-height: 100svh;
16
+ place-items: center;
17
+ padding: 1.5rem;
18
+ background:
19
+ linear-gradient(90deg, rgb(255 255 255 / 4%) 1px, transparent 1px) 0 0 / 4rem 4rem,
20
+ var(--auth-background);
21
+ color: var(--auth-foreground);
22
+ }
23
+
24
+ .auth-panel {
25
+ width: min(100%, 40rem);
26
+ padding: clamp(1.5rem, 5vw, 3.5rem);
27
+ border: 1px solid var(--auth-border);
28
+ border-radius: 0.75rem;
29
+ background: var(--auth-surface);
30
+ box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 24%);
31
+ }
32
+
33
+ .auth-eyebrow {
34
+ margin: 0 0 0.75rem;
35
+ color: var(--auth-accent);
36
+ font-size: 0.75rem;
37
+ font-weight: 800;
38
+ letter-spacing: 0.14em;
39
+ text-transform: uppercase;
40
+ }
41
+
42
+ .auth-title {
43
+ max-width: 13ch;
44
+ margin: 0;
45
+ font-size: clamp(2rem, 7vw, 3.5rem);
46
+ line-height: 1;
47
+ letter-spacing: -0.04em;
48
+ }
49
+
50
+ .auth-introduction {
51
+ max-width: 52ch;
52
+ margin: 1.5rem 0;
53
+ color: var(--auth-muted);
54
+ line-height: 1.6;
55
+ }
56
+
57
+ @media (max-width: 30rem) {
58
+ .auth-shell {
59
+ padding: 0;
60
+ }
61
+
62
+ .auth-panel {
63
+ min-height: 100svh;
64
+ border: 0;
65
+ border-radius: 0;
66
+ }
67
+ }