@vvedantb/ui 0.1.5 → 0.2.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.
- package/README.md +49 -0
- package/app/README.md +100 -0
- package/app/src/auth/useStableAuth.ts +69 -0
- package/app/src/cn.ts +6 -0
- package/app/src/confirm/ConfirmSkipHint.tsx +35 -0
- package/app/src/confirm/confirmSkip.ts +26 -0
- package/app/src/confirm/useAltHeld.ts +128 -0
- package/app/src/confirm/useRequestConfirm.ts +25 -0
- package/app/src/index.ts +43 -0
- package/app/src/settings/SettingsEmptyState.tsx +38 -0
- package/app/src/settings/SettingsField.tsx +42 -0
- package/app/src/settings/SettingsPage.tsx +88 -0
- package/app/src/settings/SettingsSection.tsx +92 -0
- package/app/src/settings/SettingsStack.tsx +17 -0
- package/app/src/settings/SettingsToggleRow.tsx +51 -0
- package/app/src/update/deployFingerprint.ts +27 -0
- package/app/src/update/updateWatcher.ts +100 -0
- package/package.json +71 -11
- package/shell/src/PillTabs.tsx +86 -0
- package/shell/src/SidebarHeader.tsx +21 -0
- package/shell/src/cn.ts +5 -0
- package/shell/src/index.ts +6 -0
- package/src/ai-elements/artifact.tsx +1 -1
- package/src/ai-elements/attachments.tsx +3 -3
- package/src/ai-elements/chain-of-thought.tsx +5 -5
- package/src/ai-elements/code-block.tsx +2 -2
- package/src/ai-elements/conversation.tsx +3 -3
- package/src/ai-elements/inline-citation.tsx +6 -6
- package/src/ai-elements/message.tsx +2 -2
- package/src/ai-elements/prompt-input.tsx +3 -3
- package/src/ai-elements/queue.tsx +2 -2
- package/src/ai-elements/reasoning.tsx +1 -1
- package/src/ai-elements/shimmer.tsx +5 -5
- package/src/ai-elements/sources.tsx +2 -2
- package/src/ai-elements/speech-input.tsx +1 -1
- package/src/ai-elements/suggestion.tsx +2 -2
- package/src/index.ts +18 -18
- package/src/ui/{_menu-classes.vibot.ts → _menu-classes.alt.ts} +4 -4
- package/src/ui/_menu-classes.ts +4 -4
- package/src/ui/{accordion.verve.tsx → accordion.alt.tsx} +2 -2
- package/src/ui/badge.tsx +1 -1
- package/src/ui/breadcrumb.tsx +2 -2
- package/src/ui/button.tsx +3 -3
- package/src/ui/checkbox.tsx +1 -1
- package/src/ui/{clear-input.vibot.tsx → clear-input.alt.tsx} +1 -1
- package/src/ui/clear-input.tsx +1 -1
- package/src/ui/command.tsx +5 -5
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/{input-group.verve.tsx → input-group.alt.tsx} +1 -1
- package/src/ui/input-group.tsx +2 -2
- package/src/ui/kbd.tsx +1 -1
- package/src/ui/labeled-switch-row.tsx +2 -2
- package/src/ui/progress.tsx +1 -1
- package/src/ui/radix-select.tsx +3 -3
- package/src/ui/search-input.tsx +1 -1
- package/src/ui/select.tsx +4 -4
- package/src/ui/sonner.tsx +5 -5
- package/src/ui/spinner.tsx +1 -1
- package/src/ui/switch.tsx +2 -2
- package/src/ui/table.tsx +2 -2
- package/src/ui/time-picker.tsx +3 -3
- package/tokens/README.md +98 -0
- package/tokens/examples/compat-shadcn-rgb/Fixture.tsx +45 -0
- package/tokens/examples/compat-shadcn-rgb/README.md +11 -0
- package/tokens/examples/compat-shadcn-rgb/index.html +12 -0
- package/tokens/examples/compat-shadcn-rgb/main.tsx +5 -0
- package/tokens/examples/compat-shadcn-rgb/package.json +14 -0
- package/tokens/examples/compat-shadcn-rgb/styles.css +67 -0
- package/tokens/examples/compat-shadcn-rgb/tsconfig.json +14 -0
- package/tokens/examples/compat-shadcn-rgb/vite.config.ts +11 -0
- package/tokens/examples/migration.md +303 -0
- package/tokens/src/css/animations.css +55 -0
- package/tokens/src/css/base.css +51 -0
- package/tokens/src/css/compat-shadcn-rgb.css +75 -0
- package/tokens/src/css/dialog.css +13 -0
- package/tokens/src/css/index.css +19 -0
- package/tokens/src/css/modal.css +44 -0
- package/tokens/src/css/scrollbar-thin.css +40 -0
- package/tokens/src/css/tabs-pills.css +58 -0
- package/tokens/src/css/tabs.css +57 -0
- package/tokens/src/css/utilities.css +34 -0
- package/tokens/src/native.ts +39 -0
- package/tokens/src/tailwind-preset.d.mts +25 -0
- package/tokens/src/tailwind-preset.mjs +183 -0
- package/tokens/src/tokens.css +207 -0
- /package/src/motion/{presets.vibot.ts → presets.alt.ts} +0 -0
- /package/src/ui/{scroll-area.verve.tsx → scroll-area.alt.tsx} +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../cn.js";
|
|
4
|
+
|
|
5
|
+
type SettingsSectionBodyVariant = "form" | "list" | "compact";
|
|
6
|
+
|
|
7
|
+
const BODY_VARIANT_CLASS: Record<SettingsSectionBodyVariant, string> = {
|
|
8
|
+
form: "px-4 py-5",
|
|
9
|
+
list: "divide-y divide-separator/50 p-0",
|
|
10
|
+
compact: "px-4 py-3",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface SettingsSectionProps {
|
|
14
|
+
/** Section heading. Kept short — the description carries the detail. */
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
/** Supporting copy shown under the heading. */
|
|
17
|
+
description?: ReactNode;
|
|
18
|
+
/** Control pinned to the top-right of the heading, e.g. a switch or link. */
|
|
19
|
+
action?: ReactNode;
|
|
20
|
+
/** Controls pinned to a bottom bar, e.g. a Save button. */
|
|
21
|
+
footer?: ReactNode;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Body padding contract. Prefer this over freelancing `bodyClassName` padding.
|
|
25
|
+
* - form: default labelled fields
|
|
26
|
+
* - list: edge-to-edge divided rows (`p-0`)
|
|
27
|
+
* - compact: tighter padding for dense controls
|
|
28
|
+
*/
|
|
29
|
+
bodyVariant?: SettingsSectionBodyVariant;
|
|
30
|
+
/** Applied to the body wrapper, for one-off layout (grids, etc.). */
|
|
31
|
+
bodyClassName?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The single container for a block of settings.
|
|
37
|
+
*
|
|
38
|
+
* Title and description sit on the canvas above the card so they read as a
|
|
39
|
+
* section caption. The card is only the controls (and optional footer).
|
|
40
|
+
* Shared across apps.
|
|
41
|
+
*/
|
|
42
|
+
export function SettingsSection({
|
|
43
|
+
title,
|
|
44
|
+
description,
|
|
45
|
+
action,
|
|
46
|
+
footer,
|
|
47
|
+
children,
|
|
48
|
+
bodyVariant = "form",
|
|
49
|
+
bodyClassName,
|
|
50
|
+
className,
|
|
51
|
+
}: SettingsSectionProps) {
|
|
52
|
+
const hasBody = children != null;
|
|
53
|
+
const hasCard = hasBody || footer != null;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className={cn("flex flex-col gap-2", className)}>
|
|
57
|
+
<header className="flex items-start justify-between gap-4 px-4">
|
|
58
|
+
<div className="min-w-0">
|
|
59
|
+
<h3 className="text-balance text-sm font-semibold text-foreground">
|
|
60
|
+
{title}
|
|
61
|
+
</h3>
|
|
62
|
+
{description ? (
|
|
63
|
+
<p className="mt-0.5 text-pretty text-sm leading-relaxed text-muted-foreground">
|
|
64
|
+
{description}
|
|
65
|
+
</p>
|
|
66
|
+
) : null}
|
|
67
|
+
</div>
|
|
68
|
+
{action ? <div className="shrink-0">{action}</div> : null}
|
|
69
|
+
</header>
|
|
70
|
+
|
|
71
|
+
{hasCard ? (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"rounded-lg bg-surface-card [&_input]:bg-field-background [&_textarea]:bg-field-background [&_[data-slot=select-trigger]]:bg-field-background [&_[role=combobox]]:border-border [&_[role=combobox]]:bg-field-background",
|
|
75
|
+
bodyVariant === "list" && "overflow-hidden",
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{hasBody ? (
|
|
79
|
+
<div className={cn(BODY_VARIANT_CLASS[bodyVariant], bodyClassName)}>
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
{footer ? (
|
|
84
|
+
<div className="flex items-center justify-end gap-2 rounded-b-lg bg-surface-secondary px-4 py-3">
|
|
85
|
+
{footer}
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
) : null}
|
|
90
|
+
</section>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../cn.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vertical rhythm for every settings page body — one gap, no page-local freelancing.
|
|
7
|
+
* Shared across apps.
|
|
8
|
+
*/
|
|
9
|
+
export function SettingsStack({
|
|
10
|
+
children,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}) {
|
|
16
|
+
return <div className={cn("space-y-8", className)}>{children}</div>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../cn.js";
|
|
3
|
+
|
|
4
|
+
interface SettingsToggleRowProps {
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
action: ReactNode;
|
|
8
|
+
htmlFor?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One preference row inside a `SettingsSection` with `bodyVariant="list"`.
|
|
14
|
+
* Put rows as direct children — the section owns the dividers.
|
|
15
|
+
* Shared across apps.
|
|
16
|
+
*/
|
|
17
|
+
export function SettingsToggleRow({
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
action,
|
|
21
|
+
htmlFor,
|
|
22
|
+
className,
|
|
23
|
+
}: SettingsToggleRowProps) {
|
|
24
|
+
const titleClassName =
|
|
25
|
+
"text-sm font-medium max-sm:break-words text-foreground";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex items-center justify-between gap-4 px-4 py-3",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div className="min-w-0">
|
|
35
|
+
{htmlFor ? (
|
|
36
|
+
<label htmlFor={htmlFor} className={titleClassName}>
|
|
37
|
+
{title}
|
|
38
|
+
</label>
|
|
39
|
+
) : (
|
|
40
|
+
<p className={titleClassName}>{title}</p>
|
|
41
|
+
)}
|
|
42
|
+
{description ? (
|
|
43
|
+
<p className="mt-1 text-pretty text-xs leading-relaxed text-muted-foreground">
|
|
44
|
+
{description}
|
|
45
|
+
</p>
|
|
46
|
+
) : null}
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex min-h-10 shrink-0 items-center">{action}</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build fingerprint from deployed index HTML — Vite hashed `/assets/*.js`
|
|
3
|
+
* URLs change on every production deploy, so a mismatch means a new build.
|
|
4
|
+
*/
|
|
5
|
+
export function fingerprintFromHtml(html: string): string | null {
|
|
6
|
+
const matches = html.match(/\/assets\/[^"'?\s]+\.js/g);
|
|
7
|
+
if (!matches || matches.length === 0) return null;
|
|
8
|
+
return [...new Set(matches)].sort().join("|");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Fetch the live document root and return its asset fingerprint. */
|
|
12
|
+
export async function fetchDeployFingerprint(
|
|
13
|
+
origin: string,
|
|
14
|
+
): Promise<string | null> {
|
|
15
|
+
try {
|
|
16
|
+
const url = `${origin}/?_vb=${Date.now()}`;
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
cache: "no-store",
|
|
19
|
+
headers: { Accept: "text/html" },
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) return null;
|
|
22
|
+
const html = await response.text();
|
|
23
|
+
return fingerprintFromHtml(html);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { fetchDeployFingerprint } from "./deployFingerprint.js";
|
|
2
|
+
|
|
3
|
+
export type UpdateToastOptions = {
|
|
4
|
+
id: string;
|
|
5
|
+
description: string;
|
|
6
|
+
duration: number;
|
|
7
|
+
action: { label: string; onClick: () => void };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type UpdateAvailableWatcherOptions = {
|
|
11
|
+
/** Origin to poll, usually `window.location.origin`. */
|
|
12
|
+
origin: string;
|
|
13
|
+
/** Toast API. Pass sonner's `toast`, or a fake in tests. */
|
|
14
|
+
toast: { message: (title: string, opts: UpdateToastOptions) => void };
|
|
15
|
+
/** Close the Convex socket before reload. Errors are swallowed. */
|
|
16
|
+
closeConvex?: () => void;
|
|
17
|
+
/** Poll interval in ms. Default 60000. */
|
|
18
|
+
pollMs?: number;
|
|
19
|
+
toastId?: string;
|
|
20
|
+
/** Pass `!import.meta.env.DEV`. When false, nothing starts. Default true. */
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
/** Reload hook. Default `window.location.reload()`. */
|
|
23
|
+
reload?: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_POLL_MS = 60_000;
|
|
27
|
+
const DEFAULT_TOAST_ID = "app-update-available";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Poll the live index HTML for a new Vite asset fingerprint (and re-check when
|
|
31
|
+
* the tab becomes visible). Shows a sticky "Update available" toast with a
|
|
32
|
+
* Refresh action when a newer deploy is serving. Returns a stop function.
|
|
33
|
+
*/
|
|
34
|
+
export function startUpdateAvailableWatcher(
|
|
35
|
+
options: UpdateAvailableWatcherOptions,
|
|
36
|
+
): () => void {
|
|
37
|
+
const {
|
|
38
|
+
origin,
|
|
39
|
+
toast,
|
|
40
|
+
closeConvex,
|
|
41
|
+
pollMs = DEFAULT_POLL_MS,
|
|
42
|
+
toastId = DEFAULT_TOAST_ID,
|
|
43
|
+
enabled = true,
|
|
44
|
+
reload = () => window.location.reload(),
|
|
45
|
+
} = options;
|
|
46
|
+
if (!enabled) return () => {};
|
|
47
|
+
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
let shown = false;
|
|
50
|
+
let baseline: string | null = null;
|
|
51
|
+
|
|
52
|
+
const showToast = (): void => {
|
|
53
|
+
if (shown) return;
|
|
54
|
+
shown = true;
|
|
55
|
+
toast.message("Update available", {
|
|
56
|
+
id: toastId,
|
|
57
|
+
description: "Refresh to load the latest version.",
|
|
58
|
+
duration: Number.POSITIVE_INFINITY,
|
|
59
|
+
action: {
|
|
60
|
+
label: "Refresh",
|
|
61
|
+
onClick: () => {
|
|
62
|
+
try {
|
|
63
|
+
closeConvex?.();
|
|
64
|
+
} catch {
|
|
65
|
+
// socket may already be closed during teardown
|
|
66
|
+
}
|
|
67
|
+
reload();
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const check = async (): Promise<void> => {
|
|
74
|
+
if (cancelled || shown) return;
|
|
75
|
+
const next = await fetchDeployFingerprint(origin);
|
|
76
|
+
if (cancelled || next === null) return;
|
|
77
|
+
if (baseline === null) {
|
|
78
|
+
baseline = next;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (next !== baseline) showToast();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
void check();
|
|
85
|
+
const intervalId = setInterval(() => {
|
|
86
|
+
if (document.visibilityState !== "visible") return;
|
|
87
|
+
void check();
|
|
88
|
+
}, pollMs);
|
|
89
|
+
|
|
90
|
+
const onVisibility = (): void => {
|
|
91
|
+
if (document.visibilityState === "visible") void check();
|
|
92
|
+
};
|
|
93
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
94
|
+
|
|
95
|
+
return () => {
|
|
96
|
+
cancelled = true;
|
|
97
|
+
clearInterval(intervalId);
|
|
98
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
99
|
+
};
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "Shared React 19 UI primitives
|
|
5
|
+
"description": "Shared React 19 UI for web apps: primitives, shell pieces, app helpers, design tokens, shared Tailwind v4 CSS and a Tailwind preset.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/vvedantb/
|
|
12
|
+
"url": "git+https://github.com/vvedantb/ui.git",
|
|
13
13
|
"directory": "packages/ui"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
@@ -20,17 +20,63 @@
|
|
|
20
20
|
"./cn": {
|
|
21
21
|
"types": "./src/utils/cn.ts",
|
|
22
22
|
"default": "./src/utils/cn.ts"
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"./shell": {
|
|
25
|
+
"types": "./shell/src/index.ts",
|
|
26
|
+
"default": "./shell/src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"./app": {
|
|
29
|
+
"types": "./app/src/index.ts",
|
|
30
|
+
"default": "./app/src/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"./tokens": "./tokens/src/tokens.css",
|
|
33
|
+
"./tokens/tokens.css": "./tokens/src/tokens.css",
|
|
34
|
+
"./tokens/native": {
|
|
35
|
+
"types": "./tokens/src/native.ts",
|
|
36
|
+
"default": "./tokens/src/native.ts"
|
|
37
|
+
},
|
|
38
|
+
"./tokens/css/index.css": "./tokens/src/css/index.css",
|
|
39
|
+
"./tokens/css/base.css": "./tokens/src/css/base.css",
|
|
40
|
+
"./tokens/css/dialog.css": "./tokens/src/css/dialog.css",
|
|
41
|
+
"./tokens/css/modal.css": "./tokens/src/css/modal.css",
|
|
42
|
+
"./tokens/css/tabs.css": "./tokens/src/css/tabs.css",
|
|
43
|
+
"./tokens/css/tabs-pills.css": "./tokens/src/css/tabs-pills.css",
|
|
44
|
+
"./tokens/css/utilities.css": "./tokens/src/css/utilities.css",
|
|
45
|
+
"./tokens/css/scrollbar-thin.css": "./tokens/src/css/scrollbar-thin.css",
|
|
46
|
+
"./tokens/css/animations.css": "./tokens/src/css/animations.css",
|
|
47
|
+
"./tokens/css/compat-shadcn-rgb.css": "./tokens/src/css/compat-shadcn-rgb.css",
|
|
48
|
+
"./tokens/compat/shadcn-rgb.css": "./tokens/src/css/compat-shadcn-rgb.css",
|
|
49
|
+
"./tokens/css/*.css": "./tokens/src/css/*.css",
|
|
50
|
+
"./tokens/tailwind-preset": {
|
|
51
|
+
"types": "./tokens/src/tailwind-preset.d.mts",
|
|
52
|
+
"default": "./tokens/src/tailwind-preset.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./package.json": "./package.json"
|
|
24
55
|
},
|
|
25
56
|
"types": "./src/index.ts",
|
|
26
57
|
"files": [
|
|
27
58
|
"src",
|
|
59
|
+
"shell/src",
|
|
60
|
+
"app/src",
|
|
61
|
+
"tokens/src",
|
|
62
|
+
"tokens/examples",
|
|
63
|
+
"README.md",
|
|
28
64
|
"LICENSE"
|
|
29
65
|
],
|
|
30
66
|
"sideEffects": [
|
|
31
67
|
"./src/ui/sonner.css"
|
|
32
68
|
],
|
|
69
|
+
"scripts": {
|
|
70
|
+
"check:tokens": "node tokens/scripts/check-component-tokens.mjs",
|
|
71
|
+
"build": "node tokens/scripts/check-exports.mjs && node tokens/scripts/check-component-tokens.mjs && tsc -p tsconfig.json",
|
|
72
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tokens/tsconfig.json",
|
|
73
|
+
"test": "node tokens/scripts/check-component-tokens.mjs && vitest run && node --test tokens/test/*.test.mjs",
|
|
74
|
+
"verify": "node tokens/scripts/check-component-tokens.mjs && node tokens/verify/run.mjs",
|
|
75
|
+
"analyze": "node tokens/verify/analyze.mjs",
|
|
76
|
+
"prepack": "node tokens/scripts/check-exports.mjs"
|
|
77
|
+
},
|
|
33
78
|
"peerDependencies": {
|
|
79
|
+
"@clerk/clerk-react": ">=5",
|
|
34
80
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
35
81
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
36
82
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -55,7 +101,7 @@
|
|
|
55
101
|
"@streamdown/math": "*",
|
|
56
102
|
"@streamdown/mermaid": "*",
|
|
57
103
|
"@tabler/icons-react": "^3.35.0",
|
|
58
|
-
"@
|
|
104
|
+
"@tanstack/react-hotkeys": "*",
|
|
59
105
|
"ai": ">=5.0.0",
|
|
60
106
|
"class-variance-authority": "^0.7.1",
|
|
61
107
|
"clsx": "^2.1.1",
|
|
@@ -71,9 +117,11 @@
|
|
|
71
117
|
"streamdown": "*",
|
|
72
118
|
"tailwind-merge": "^2.6.0",
|
|
73
119
|
"use-stick-to-bottom": "^1.1.0",
|
|
74
|
-
"vaul": "^1.1.0"
|
|
120
|
+
"vaul": "^1.1.0",
|
|
121
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
75
122
|
},
|
|
76
123
|
"devDependencies": {
|
|
124
|
+
"@clerk/clerk-react": "^5.0.0",
|
|
77
125
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
78
126
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
79
127
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -98,6 +146,8 @@
|
|
|
98
146
|
"@streamdown/math": "*",
|
|
99
147
|
"@streamdown/mermaid": "*",
|
|
100
148
|
"@tabler/icons-react": "^3.35.0",
|
|
149
|
+
"@tailwindcss/node": "4.3.3",
|
|
150
|
+
"@tanstack/react-hotkeys": "^0.13.0",
|
|
101
151
|
"@types/react": "^19.1.0",
|
|
102
152
|
"@types/react-dom": "^19.1.0",
|
|
103
153
|
"ai": "^5.0.0",
|
|
@@ -105,6 +155,7 @@
|
|
|
105
155
|
"clsx": "^2.1.1",
|
|
106
156
|
"cmdk": "^1.1.1",
|
|
107
157
|
"embla-carousel-react": "^8.6.0",
|
|
158
|
+
"happy-dom": "^18.0.0",
|
|
108
159
|
"motion": "^12.0.0",
|
|
109
160
|
"nanoid": "^5.1.0",
|
|
110
161
|
"radix-ui": "^1.4.0",
|
|
@@ -114,13 +165,22 @@
|
|
|
114
165
|
"sonner": "^2.0.3",
|
|
115
166
|
"streamdown": "*",
|
|
116
167
|
"tailwind-merge": "^2.6.0",
|
|
168
|
+
"tailwindcss": "4.3.3",
|
|
117
169
|
"typescript": "^5.9.0",
|
|
118
170
|
"use-stick-to-bottom": "^1.1.0",
|
|
119
171
|
"vaul": "^1.1.0",
|
|
120
|
-
"
|
|
172
|
+
"vitest": "^3.2.0",
|
|
173
|
+
"zod": "^4.0.0"
|
|
121
174
|
},
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
|
|
175
|
+
"peerDependenciesMeta": {
|
|
176
|
+
"@clerk/clerk-react": {
|
|
177
|
+
"optional": true
|
|
178
|
+
},
|
|
179
|
+
"@tanstack/react-hotkeys": {
|
|
180
|
+
"optional": true
|
|
181
|
+
},
|
|
182
|
+
"zod": {
|
|
183
|
+
"optional": true
|
|
184
|
+
}
|
|
125
185
|
}
|
|
126
|
-
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { KeyboardEvent, ReactNode } from "react";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
|
|
4
|
+
export type PillTabItem = {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type PillTabsProps = {
|
|
11
|
+
tabs: PillTabItem[];
|
|
12
|
+
value: string;
|
|
13
|
+
onValueChange?: (value: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const tabBase =
|
|
18
|
+
"inline-flex h-8 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border border-border px-3.5 py-1 text-sm font-medium transition-[color,background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
19
|
+
const tabIdle = "text-muted-foreground hover:bg-default/50 hover:text-foreground";
|
|
20
|
+
const tabSelected = "bg-default text-foreground";
|
|
21
|
+
|
|
22
|
+
export function PillTabs({ tabs, value, onValueChange, className }: PillTabsProps) {
|
|
23
|
+
// Arrow keys, Home and End move focus and selection (WAI-ARIA tabs pattern).
|
|
24
|
+
function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
|
25
|
+
const count = tabs.length;
|
|
26
|
+
if (count === 0) return;
|
|
27
|
+
const current = Math.max(
|
|
28
|
+
0,
|
|
29
|
+
tabs.findIndex((tab) => tab.value === value),
|
|
30
|
+
);
|
|
31
|
+
let next: number;
|
|
32
|
+
switch (event.key) {
|
|
33
|
+
case "ArrowRight":
|
|
34
|
+
next = (current + 1) % count;
|
|
35
|
+
break;
|
|
36
|
+
case "ArrowLeft":
|
|
37
|
+
next = (current - 1 + count) % count;
|
|
38
|
+
break;
|
|
39
|
+
case "Home":
|
|
40
|
+
next = 0;
|
|
41
|
+
break;
|
|
42
|
+
case "End":
|
|
43
|
+
next = count - 1;
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
const buttons = event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="tab"]');
|
|
50
|
+
buttons[next]?.focus();
|
|
51
|
+
onValueChange?.(tabs[next]!.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasSelection = tabs.some((tab) => tab.value === value);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
role="tablist"
|
|
59
|
+
className={cn("inline-flex items-center gap-1.5", className)}
|
|
60
|
+
onKeyDown={onKeyDown}
|
|
61
|
+
>
|
|
62
|
+
{tabs.map((tab, index) => {
|
|
63
|
+
const selected = tab.value === value;
|
|
64
|
+
const focusable = selected || (!hasSelection && index === 0);
|
|
65
|
+
return (
|
|
66
|
+
<button
|
|
67
|
+
key={tab.value}
|
|
68
|
+
type="button"
|
|
69
|
+
role="tab"
|
|
70
|
+
aria-selected={selected}
|
|
71
|
+
tabIndex={focusable ? 0 : -1}
|
|
72
|
+
className={cn(tabBase, selected ? tabSelected : tabIdle)}
|
|
73
|
+
onClick={() => onValueChange?.(tab.value)}
|
|
74
|
+
>
|
|
75
|
+
{tab.icon ? (
|
|
76
|
+
<span aria-hidden="true" className="inline-flex shrink-0">
|
|
77
|
+
{tab.icon}
|
|
78
|
+
</span>
|
|
79
|
+
) : null}
|
|
80
|
+
<span>{tab.label}</span>
|
|
81
|
+
</button>
|
|
82
|
+
);
|
|
83
|
+
})}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
|
|
4
|
+
export type SidebarHeaderProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
trailing?: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function SidebarHeader({ title, trailing, className }: SidebarHeaderProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cn("flex h-11 items-center gap-1 pl-3 pr-1", className)}>
|
|
13
|
+
<h2 className="min-w-0 flex-1 truncate text-left text-base font-semibold leading-none tracking-tight text-foreground">
|
|
14
|
+
{title}
|
|
15
|
+
</h2>
|
|
16
|
+
{trailing ? (
|
|
17
|
+
<div className="flex items-center justify-end gap-0.5">{trailing}</div>
|
|
18
|
+
) : null}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
package/shell/src/cn.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SidebarHeader } from "./SidebarHeader.js";
|
|
2
|
+
export type { SidebarHeaderProps } from "./SidebarHeader.js";
|
|
3
|
+
export { PillTabs } from "./PillTabs.js";
|
|
4
|
+
export type { PillTabItem, PillTabsProps } from "./PillTabs.js";
|
|
5
|
+
export { cn } from "./cn.js";
|
|
6
|
+
export type { ClassValue } from "./cn.js";
|
|
@@ -46,7 +46,7 @@ export const ArtifactDescription = ({
|
|
|
46
46
|
className,
|
|
47
47
|
...props
|
|
48
48
|
}: ArtifactDescriptionProps) => (
|
|
49
|
-
<p className={cn("text-xs text-muted", className)} {...props} />
|
|
49
|
+
<p className={cn("text-xs text-muted-foreground", className)} {...props} />
|
|
50
50
|
);
|
|
51
51
|
|
|
52
52
|
export type ArtifactActionsProps = ComponentProps<"div">;
|
|
@@ -212,7 +212,7 @@ export const AttachmentPreview = ({
|
|
|
212
212
|
const { data, mediaCategory, variant } = useAttachmentContext();
|
|
213
213
|
const iconSize = variant === "inline" ? "size-3" : "size-4";
|
|
214
214
|
const renderIcon = (Icon: typeof IconPhoto) => (
|
|
215
|
-
<Icon className={cn(iconSize, "text-muted")} />
|
|
215
|
+
<Icon className={cn(iconSize, "text-muted-foreground")} />
|
|
216
216
|
);
|
|
217
217
|
const renderContent = () => {
|
|
218
218
|
if (mediaCategory === "image" && data.type === "file" && data.url) {
|
|
@@ -259,7 +259,7 @@ export const AttachmentInfo = ({
|
|
|
259
259
|
<div className={cn("min-w-0 flex-1", className)} {...props}>
|
|
260
260
|
<span className="block truncate">{label}</span>
|
|
261
261
|
{showMediaType && data.mediaType && (
|
|
262
|
-
<span className="block truncate text-muted text-xs">
|
|
262
|
+
<span className="block truncate text-muted-foreground text-xs">
|
|
263
263
|
{data.mediaType}
|
|
264
264
|
</span>
|
|
265
265
|
)}
|
|
@@ -356,7 +356,7 @@ export const AttachmentEmpty = ({
|
|
|
356
356
|
}: AttachmentEmptyProps) => (
|
|
357
357
|
<div
|
|
358
358
|
className={cn(
|
|
359
|
-
"flex items-center justify-center p-4 text-muted text-sm",
|
|
359
|
+
"flex items-center justify-center p-4 text-muted-foreground text-sm",
|
|
360
360
|
className,
|
|
361
361
|
)}
|
|
362
362
|
{...props}
|
|
@@ -75,7 +75,7 @@ export function ChainOfThoughtHeader({
|
|
|
75
75
|
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
|
|
76
76
|
<CollapsibleTrigger
|
|
77
77
|
className={cn(
|
|
78
|
-
"flex w-full items-center gap-2 text-muted text-sm transition-[color,transform] duration-150 ease-out active:scale-[0.99] hover:text-foreground",
|
|
78
|
+
"flex w-full items-center gap-2 text-muted-foreground text-sm transition-[color,transform] duration-150 ease-out active:scale-[0.99] hover:text-foreground",
|
|
79
79
|
className,
|
|
80
80
|
)}
|
|
81
81
|
{...props}
|
|
@@ -102,8 +102,8 @@ export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
|
|
|
102
102
|
};
|
|
103
103
|
const stepStatusStyles = {
|
|
104
104
|
active: "text-foreground",
|
|
105
|
-
complete: "text-muted",
|
|
106
|
-
pending: "text-muted/50",
|
|
105
|
+
complete: "text-muted-foreground",
|
|
106
|
+
pending: "text-muted-foreground/50",
|
|
107
107
|
};
|
|
108
108
|
export function ChainOfThoughtStep({
|
|
109
109
|
className,
|
|
@@ -130,7 +130,7 @@ export function ChainOfThoughtStep({
|
|
|
130
130
|
</div>
|
|
131
131
|
<div className="flex-1 space-y-2 overflow-hidden">
|
|
132
132
|
<div>{label}</div>
|
|
133
|
-
{description && <div className="text-muted text-xs">{description}</div>}
|
|
133
|
+
{description && <div className="text-muted-foreground text-xs">{description}</div>}
|
|
134
134
|
{children}
|
|
135
135
|
</div>
|
|
136
136
|
</div>
|
|
@@ -206,7 +206,7 @@ export function ChainOfThoughtImage({
|
|
|
206
206
|
>
|
|
207
207
|
{children}
|
|
208
208
|
</div>
|
|
209
|
-
{caption && <p className="text-muted text-xs">{caption}</p>}
|
|
209
|
+
{caption && <p className="text-muted-foreground text-xs">{caption}</p>}
|
|
210
210
|
</div>
|
|
211
211
|
);
|
|
212
212
|
}
|
|
@@ -211,7 +211,7 @@ const LINE_NUMBER_CLASSES = cn(
|
|
|
211
211
|
"before:w-8",
|
|
212
212
|
"before:mr-4",
|
|
213
213
|
"before:text-right",
|
|
214
|
-
"before:text-muted/50",
|
|
214
|
+
"before:text-muted-foreground/50",
|
|
215
215
|
"before:font-mono",
|
|
216
216
|
"before:select-none",
|
|
217
217
|
);
|
|
@@ -285,7 +285,7 @@ export const CodeBlockHeader = ({
|
|
|
285
285
|
className={cn(
|
|
286
286
|
cn(
|
|
287
287
|
flatPanelSubtleClass,
|
|
288
|
-
"flex items-center justify-between px-3 py-2 text-muted text-xs",
|
|
288
|
+
"flex items-center justify-between px-3 py-2 text-muted-foreground text-xs",
|
|
289
289
|
),
|
|
290
290
|
className,
|
|
291
291
|
)}
|
|
@@ -6,7 +6,7 @@ import { Button } from "../ui/button";
|
|
|
6
6
|
import { cn } from "../utils/cn";
|
|
7
7
|
import { flatInteractiveClass } from "../ui/surface-classes";
|
|
8
8
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
9
|
-
import { fastTransition } from "../motion/presets.
|
|
9
|
+
import { fastTransition } from "../motion/presets.alt";
|
|
10
10
|
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|
11
11
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
|
12
12
|
<StickToBottom
|
|
@@ -51,10 +51,10 @@ export const ConversationEmptyState = ({
|
|
|
51
51
|
>
|
|
52
52
|
{children ?? (
|
|
53
53
|
<>
|
|
54
|
-
{icon && <div className="text-muted">{icon}</div>}
|
|
54
|
+
{icon && <div className="text-muted-foreground">{icon}</div>}
|
|
55
55
|
<div className="space-y-1">
|
|
56
56
|
<h3 className="font-medium text-sm">{title}</h3>
|
|
57
|
-
{description && <p className="text-muted text-sm">{description}</p>}
|
|
57
|
+
{description && <p className="text-muted-foreground text-sm">{description}</p>}
|
|
58
58
|
</div>
|
|
59
59
|
</>
|
|
60
60
|
)}
|