@vvedantb/ui 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -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 +65 -9
- 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/conversation.tsx +1 -1
- package/src/ai-elements/suggestion.tsx +1 -1
- package/src/index.ts +18 -18
- package/src/ui/{_menu-classes.vibot.ts → _menu-classes.alt.ts} +2 -2
- package/src/ui/_menu-classes.ts +2 -2
- 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/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 +177 -0
- package/tokens/src/tokens.css +192 -0
- /package/src/motion/{presets.vibot.ts → presets.alt.ts} +0 -0
- /package/src/ui/{accordion.verve.tsx → accordion.alt.tsx} +0 -0
- /package/src/ui/{clear-input.vibot.tsx → clear-input.alt.tsx} +0 -0
- /package/src/ui/{input-group.verve.tsx → input-group.alt.tsx} +0 -0
- /package/src/ui/{scroll-area.verve.tsx → scroll-area.alt.tsx} +0 -0
|
@@ -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">
|
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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,52 @@
|
|
|
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/*.css": "./tokens/src/css/*.css",
|
|
48
|
+
"./tokens/tailwind-preset": {
|
|
49
|
+
"types": "./tokens/src/tailwind-preset.d.mts",
|
|
50
|
+
"default": "./tokens/src/tailwind-preset.mjs"
|
|
51
|
+
},
|
|
52
|
+
"./package.json": "./package.json"
|
|
24
53
|
},
|
|
25
54
|
"types": "./src/index.ts",
|
|
26
55
|
"files": [
|
|
27
56
|
"src",
|
|
57
|
+
"shell/src",
|
|
58
|
+
"app/src",
|
|
59
|
+
"tokens/src",
|
|
60
|
+
"tokens/examples",
|
|
61
|
+
"README.md",
|
|
28
62
|
"LICENSE"
|
|
29
63
|
],
|
|
30
64
|
"sideEffects": [
|
|
31
65
|
"./src/ui/sonner.css"
|
|
32
66
|
],
|
|
33
67
|
"peerDependencies": {
|
|
68
|
+
"@clerk/clerk-react": ">=5",
|
|
34
69
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
35
70
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
36
71
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -55,7 +90,7 @@
|
|
|
55
90
|
"@streamdown/math": "*",
|
|
56
91
|
"@streamdown/mermaid": "*",
|
|
57
92
|
"@tabler/icons-react": "^3.35.0",
|
|
58
|
-
"@
|
|
93
|
+
"@tanstack/react-hotkeys": "*",
|
|
59
94
|
"ai": ">=5.0.0",
|
|
60
95
|
"class-variance-authority": "^0.7.1",
|
|
61
96
|
"clsx": "^2.1.1",
|
|
@@ -71,9 +106,11 @@
|
|
|
71
106
|
"streamdown": "*",
|
|
72
107
|
"tailwind-merge": "^2.6.0",
|
|
73
108
|
"use-stick-to-bottom": "^1.1.0",
|
|
74
|
-
"vaul": "^1.1.0"
|
|
109
|
+
"vaul": "^1.1.0",
|
|
110
|
+
"zod": "^3.25.0 || ^4.0.0"
|
|
75
111
|
},
|
|
76
112
|
"devDependencies": {
|
|
113
|
+
"@clerk/clerk-react": "^5.0.0",
|
|
77
114
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
78
115
|
"@radix-ui/react-checkbox": "^1.3.3",
|
|
79
116
|
"@radix-ui/react-collapsible": "^1.1.12",
|
|
@@ -98,6 +135,8 @@
|
|
|
98
135
|
"@streamdown/math": "*",
|
|
99
136
|
"@streamdown/mermaid": "*",
|
|
100
137
|
"@tabler/icons-react": "^3.35.0",
|
|
138
|
+
"@tailwindcss/node": "4.3.3",
|
|
139
|
+
"@tanstack/react-hotkeys": "^0.13.0",
|
|
101
140
|
"@types/react": "^19.1.0",
|
|
102
141
|
"@types/react-dom": "^19.1.0",
|
|
103
142
|
"ai": "^5.0.0",
|
|
@@ -105,6 +144,7 @@
|
|
|
105
144
|
"clsx": "^2.1.1",
|
|
106
145
|
"cmdk": "^1.1.1",
|
|
107
146
|
"embla-carousel-react": "^8.6.0",
|
|
147
|
+
"happy-dom": "^18.0.0",
|
|
108
148
|
"motion": "^12.0.0",
|
|
109
149
|
"nanoid": "^5.1.0",
|
|
110
150
|
"radix-ui": "^1.4.0",
|
|
@@ -114,13 +154,29 @@
|
|
|
114
154
|
"sonner": "^2.0.3",
|
|
115
155
|
"streamdown": "*",
|
|
116
156
|
"tailwind-merge": "^2.6.0",
|
|
157
|
+
"tailwindcss": "4.3.3",
|
|
117
158
|
"typescript": "^5.9.0",
|
|
118
159
|
"use-stick-to-bottom": "^1.1.0",
|
|
119
160
|
"vaul": "^1.1.0",
|
|
120
|
-
"
|
|
161
|
+
"vitest": "^3.2.0",
|
|
162
|
+
"zod": "^4.0.0"
|
|
163
|
+
},
|
|
164
|
+
"peerDependenciesMeta": {
|
|
165
|
+
"@clerk/clerk-react": {
|
|
166
|
+
"optional": true
|
|
167
|
+
},
|
|
168
|
+
"@tanstack/react-hotkeys": {
|
|
169
|
+
"optional": true
|
|
170
|
+
},
|
|
171
|
+
"zod": {
|
|
172
|
+
"optional": true
|
|
173
|
+
}
|
|
121
174
|
},
|
|
122
175
|
"scripts": {
|
|
123
|
-
"build": "tsc -p tsconfig.json",
|
|
124
|
-
"typecheck": "tsc -p tsconfig.json"
|
|
176
|
+
"build": "node tokens/scripts/check-exports.mjs && tsc -p tsconfig.json",
|
|
177
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tokens/tsconfig.json",
|
|
178
|
+
"test": "vitest run && node --test tokens/test/*.test.mjs",
|
|
179
|
+
"verify": "node tokens/verify/run.mjs",
|
|
180
|
+
"analyze": "node tokens/verify/analyze.mjs"
|
|
125
181
|
}
|
|
126
182
|
}
|
|
@@ -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 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";
|
|
@@ -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
|
|
@@ -6,7 +6,7 @@ import { cn } from "../utils/cn";
|
|
|
6
6
|
import { flatInteractiveClass } from "../ui/surface-classes";
|
|
7
7
|
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
|
8
8
|
import { AnimatePresence, motion } from "motion/react";
|
|
9
|
-
import { fastTransition } from "../motion/presets.
|
|
9
|
+
import { fastTransition } from "../motion/presets.alt";
|
|
10
10
|
export type SuggestionsProps = {
|
|
11
11
|
className?: string;
|
|
12
12
|
children?: ReactNode;
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Colour, radius, shadow and font tokens live in @vvedantb/tokens. Import that
|
|
1
|
+
// Colour, radius, shadow and font tokens live in @vvedantb/ui/tokens. Import that
|
|
2
2
|
// stylesheet in the app. This package does not ship a second token set.
|
|
3
3
|
export { cn } from "./utils/cn";
|
|
4
4
|
export { floatingSurfaceClass } from "./ui/_menu-classes";
|
|
@@ -55,30 +55,30 @@ export * from "./ui/sheet";
|
|
|
55
55
|
export * from "./ui/visually-hidden";
|
|
56
56
|
export * from "./ui/clearInputDissolve";
|
|
57
57
|
|
|
58
|
-
// Dual copies: richer file uses the plain name; the other keeps
|
|
58
|
+
// Dual copies: the richer file uses the plain name; the other keeps an `Alt` suffix.
|
|
59
59
|
export * from "./ui/accordion";
|
|
60
60
|
export {
|
|
61
|
-
Accordion as
|
|
62
|
-
AccordionItem as
|
|
63
|
-
AccordionTrigger as
|
|
64
|
-
AccordionContent as
|
|
65
|
-
} from "./ui/accordion.
|
|
61
|
+
Accordion as AccordionAlt,
|
|
62
|
+
AccordionItem as AccordionItemAlt,
|
|
63
|
+
AccordionTrigger as AccordionTriggerAlt,
|
|
64
|
+
AccordionContent as AccordionContentAlt,
|
|
65
|
+
} from "./ui/accordion.alt";
|
|
66
66
|
export * from "./ui/clear-input";
|
|
67
|
-
export { ClearInput as
|
|
67
|
+
export { ClearInput as ClearInputAlt } from "./ui/clear-input.alt";
|
|
68
68
|
export * from "./ui/input-group";
|
|
69
69
|
export {
|
|
70
|
-
InputGroup as
|
|
71
|
-
InputGroupAddon as
|
|
72
|
-
InputGroupButton as
|
|
73
|
-
InputGroupText as
|
|
74
|
-
InputGroupInput as
|
|
75
|
-
InputGroupTextarea as
|
|
76
|
-
} from "./ui/input-group.
|
|
70
|
+
InputGroup as InputGroupAlt,
|
|
71
|
+
InputGroupAddon as InputGroupAddonAlt,
|
|
72
|
+
InputGroupButton as InputGroupButtonAlt,
|
|
73
|
+
InputGroupText as InputGroupTextAlt,
|
|
74
|
+
InputGroupInput as InputGroupInputAlt,
|
|
75
|
+
InputGroupTextarea as InputGroupTextareaAlt,
|
|
76
|
+
} from "./ui/input-group.alt";
|
|
77
77
|
export * from "./ui/scroll-area";
|
|
78
78
|
export {
|
|
79
|
-
ScrollArea as
|
|
80
|
-
ScrollBar as
|
|
81
|
-
} from "./ui/scroll-area.
|
|
79
|
+
ScrollArea as ScrollAreaAlt,
|
|
80
|
+
ScrollBar as ScrollBarAlt,
|
|
81
|
+
} from "./ui/scroll-area.alt";
|
|
82
82
|
|
|
83
83
|
export * from "./ai-elements/artifact";
|
|
84
84
|
export * from "./ai-elements/attachments";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Floating overlay shell: tone + smooth-shadow-ring, no hairline. */
|
|
2
2
|
export const floatingSurfaceClass =
|
|
3
3
|
"rounded-lg bg-overlay text-overlay-foreground smooth-shadow-ring-lg";
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ export const floatingSurfaceAnimateClass = [
|
|
|
9
9
|
"origin-(--radix-popper-transform-origin) data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=open]:duration-200 data-[state=closed]:duration-150 data-[state=open]:ease-[cubic-bezier(0.32,0.72,0,1)] data-[state=closed]:ease-[cubic-bezier(0.32,0.72,0,1)]",
|
|
10
10
|
].join(" ");
|
|
11
11
|
|
|
12
|
-
/** Shared floating menu surfaces
|
|
12
|
+
/** Shared floating menu surfaces: popover tone + smooth-shadow-ring-lg. */
|
|
13
13
|
export const menuContentClass = [
|
|
14
14
|
"z-50 min-w-48 overflow-hidden p-1.5 outline-hidden",
|
|
15
15
|
floatingSurfaceClass,
|
package/src/ui/_menu-classes.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// floating overlay: tone + smooth-shadow-ring, never border+shadow
|
|
2
2
|
export const floatingSurfaceClass =
|
|
3
3
|
"rounded-lg bg-overlay text-overlay-foreground smooth-shadow-ring-lg max-sm:max-w-[calc(100vw-2rem)]";
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ export const floatingSurfaceAnimateClass = [
|
|
|
6
6
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
7
7
|
].join(" ");
|
|
8
8
|
|
|
9
|
-
// shared floating menu surfaces
|
|
9
|
+
// shared floating menu surfaces: popover tone + smooth-shadow-ring-lg
|
|
10
10
|
export const menuContentClass = [
|
|
11
11
|
"z-50 min-w-[12rem] overflow-hidden p-1.5 outline-none max-sm:max-h-[85dvh] max-sm:overflow-y-auto",
|
|
12
12
|
floatingSurfaceClass,
|