@vvedantb/ui 0.1.4 → 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.
Files changed (52) hide show
  1. package/README.md +24 -0
  2. package/app/src/auth/useStableAuth.ts +69 -0
  3. package/app/src/cn.ts +6 -0
  4. package/app/src/confirm/ConfirmSkipHint.tsx +35 -0
  5. package/app/src/confirm/confirmSkip.ts +26 -0
  6. package/app/src/confirm/useAltHeld.ts +128 -0
  7. package/app/src/confirm/useRequestConfirm.ts +25 -0
  8. package/app/src/index.ts +43 -0
  9. package/app/src/settings/SettingsEmptyState.tsx +38 -0
  10. package/app/src/settings/SettingsField.tsx +42 -0
  11. package/app/src/settings/SettingsPage.tsx +88 -0
  12. package/app/src/settings/SettingsSection.tsx +92 -0
  13. package/app/src/settings/SettingsStack.tsx +17 -0
  14. package/app/src/settings/SettingsToggleRow.tsx +51 -0
  15. package/app/src/update/deployFingerprint.ts +27 -0
  16. package/app/src/update/updateWatcher.ts +100 -0
  17. package/package.json +65 -9
  18. package/shell/src/PillTabs.tsx +86 -0
  19. package/shell/src/SidebarHeader.tsx +21 -0
  20. package/shell/src/cn.ts +5 -0
  21. package/shell/src/index.ts +6 -0
  22. package/src/ai-elements/conversation.tsx +1 -1
  23. package/src/ai-elements/suggestion.tsx +1 -1
  24. package/src/index.ts +20 -18
  25. package/src/ui/{_menu-classes.vibot.ts → _menu-classes.alt.ts} +2 -2
  26. package/src/ui/_menu-classes.ts +2 -2
  27. package/src/ui/alert-dialog.tsx +83 -0
  28. package/src/ui/button.tsx +2 -1
  29. package/src/ui/confirm-dialog.tsx +61 -0
  30. package/src/ui/dialog.tsx +174 -46
  31. package/src/ui/modalTransition.ts +10 -0
  32. package/src/ui/overlay-a11y.ts +135 -0
  33. package/src/ui/sheet.tsx +73 -6
  34. package/tokens/examples/migration.md +303 -0
  35. package/tokens/src/css/animations.css +55 -0
  36. package/tokens/src/css/base.css +51 -0
  37. package/tokens/src/css/dialog.css +13 -0
  38. package/tokens/src/css/index.css +19 -0
  39. package/tokens/src/css/modal.css +44 -0
  40. package/tokens/src/css/scrollbar-thin.css +40 -0
  41. package/tokens/src/css/tabs-pills.css +58 -0
  42. package/tokens/src/css/tabs.css +57 -0
  43. package/tokens/src/css/utilities.css +34 -0
  44. package/tokens/src/native.ts +39 -0
  45. package/tokens/src/tailwind-preset.d.mts +25 -0
  46. package/tokens/src/tailwind-preset.mjs +177 -0
  47. package/tokens/src/tokens.css +192 -0
  48. /package/src/motion/{presets.vibot.ts → presets.alt.ts} +0 -0
  49. /package/src/ui/{accordion.verve.tsx → accordion.alt.tsx} +0 -0
  50. /package/src/ui/{clear-input.vibot.tsx → clear-input.alt.tsx} +0 -0
  51. /package/src/ui/{input-group.verve.tsx → input-group.alt.tsx} +0 -0
  52. /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.1.4",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
- "description": "Shared React 19 UI primitives for vmem, vibot and Verve.",
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/vv-ui.git",
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
- "@vvedantb/tokens": "*",
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
- "@vvedantb/tokens": "0.1.0"
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
+ }
@@ -0,0 +1,5 @@
1
+ export type ClassValue = string | false | null | undefined;
2
+
3
+ export function cn(...classes: ClassValue[]): string {
4
+ return classes.filter(Boolean).join(" ");
5
+ }
@@ -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.vibot";
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.vibot";
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";
@@ -16,6 +16,8 @@ export * from "./ui/collapsible";
16
16
  export * from "./ui/command";
17
17
  export * from "./ui/context-menu";
18
18
  export * from "./ui/dialog";
19
+ export * from "./ui/alert-dialog";
20
+ export * from "./ui/confirm-dialog";
19
21
  export * from "./ui/dropdown-menu";
20
22
  export * from "./ui/hover-card";
21
23
  export * from "./ui/input";
@@ -53,30 +55,30 @@ export * from "./ui/sheet";
53
55
  export * from "./ui/visually-hidden";
54
56
  export * from "./ui/clearInputDissolve";
55
57
 
56
- // Dual copies: richer file uses the plain name; the other keeps a suffix.
58
+ // Dual copies: the richer file uses the plain name; the other keeps an `Alt` suffix.
57
59
  export * from "./ui/accordion";
58
60
  export {
59
- Accordion as AccordionVerve,
60
- AccordionItem as AccordionItemVerve,
61
- AccordionTrigger as AccordionTriggerVerve,
62
- AccordionContent as AccordionContentVerve,
63
- } from "./ui/accordion.verve";
61
+ Accordion as AccordionAlt,
62
+ AccordionItem as AccordionItemAlt,
63
+ AccordionTrigger as AccordionTriggerAlt,
64
+ AccordionContent as AccordionContentAlt,
65
+ } from "./ui/accordion.alt";
64
66
  export * from "./ui/clear-input";
65
- export { ClearInput as ClearInputVibot } from "./ui/clear-input.vibot";
67
+ export { ClearInput as ClearInputAlt } from "./ui/clear-input.alt";
66
68
  export * from "./ui/input-group";
67
69
  export {
68
- InputGroup as InputGroupVerve,
69
- InputGroupAddon as InputGroupAddonVerve,
70
- InputGroupButton as InputGroupButtonVerve,
71
- InputGroupText as InputGroupTextVerve,
72
- InputGroupInput as InputGroupInputVerve,
73
- InputGroupTextarea as InputGroupTextareaVerve,
74
- } from "./ui/input-group.verve";
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";
75
77
  export * from "./ui/scroll-area";
76
78
  export {
77
- ScrollArea as ScrollAreaVerve,
78
- ScrollBar as ScrollBarVerve,
79
- } from "./ui/scroll-area.verve";
79
+ ScrollArea as ScrollAreaAlt,
80
+ ScrollBar as ScrollBarAlt,
81
+ } from "./ui/scroll-area.alt";
80
82
 
81
83
  export * from "./ai-elements/artifact";
82
84
  export * from "./ai-elements/attachments";
@@ -1,4 +1,4 @@
1
- /** Eva-aligned floating overlay shell — tone + smooth-shadow-ring, no hairline. */
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 — matches Eva's popover + smooth-shadow-ring-lg. */
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,
@@ -1,4 +1,4 @@
1
- // eva-aligned floating overlay: tone + smooth-shadow-ring, never border+shadow
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, matches eva popover + smooth-shadow-ring-lg
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,
@@ -0,0 +1,83 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils/cn";
3
+ import { Button, type ButtonProps } from "./button";
4
+ import {
5
+ Dialog,
6
+ DialogClose,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogOverlay,
12
+ DialogPortal,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from "./dialog";
16
+
17
+ const AlertDialog = Dialog;
18
+ const AlertDialogTrigger = DialogTrigger;
19
+ const AlertDialogPortal = DialogPortal;
20
+ const AlertDialogOverlay = DialogOverlay;
21
+
22
+ const AlertDialogContent = React.forwardRef<
23
+ React.ComponentRef<typeof DialogContent>,
24
+ React.ComponentPropsWithoutRef<typeof DialogContent>
25
+ >(({ hideCloseButton = true, onPointerDownOutside, onInteractOutside, ...props }, ref) => (
26
+ <DialogContent
27
+ ref={ref}
28
+ role="alertdialog"
29
+ hideCloseButton={hideCloseButton}
30
+ onPointerDownOutside={(event) => {
31
+ event.preventDefault();
32
+ onPointerDownOutside?.(event);
33
+ }}
34
+ onInteractOutside={(event) => {
35
+ event.preventDefault();
36
+ onInteractOutside?.(event);
37
+ }}
38
+ {...props}
39
+ />
40
+ ));
41
+ AlertDialogContent.displayName = "AlertDialogContent";
42
+
43
+ const AlertDialogHeader = DialogHeader;
44
+ const AlertDialogFooter = DialogFooter;
45
+ const AlertDialogTitle = DialogTitle;
46
+ const AlertDialogDescription = DialogDescription;
47
+
48
+ const AlertDialogAction = React.forwardRef<HTMLButtonElement, ButtonProps>(
49
+ ({ className, ...props }, ref) => (
50
+ <Button ref={ref} className={cn("max-sm:min-h-11", className)} {...props} />
51
+ ),
52
+ );
53
+ AlertDialogAction.displayName = "AlertDialogAction";
54
+
55
+ const AlertDialogCancel = React.forwardRef<HTMLButtonElement, ButtonProps>(
56
+ ({ className, variant = "outline", ...props }, ref) => (
57
+ <DialogClose asChild>
58
+ <Button
59
+ ref={ref}
60
+ type="button"
61
+ variant={variant}
62
+ data-alert-cancel=""
63
+ className={cn("max-sm:min-h-11 mt-2 sm:mt-0", className)}
64
+ {...props}
65
+ />
66
+ </DialogClose>
67
+ ),
68
+ );
69
+ AlertDialogCancel.displayName = "AlertDialogCancel";
70
+
71
+ export {
72
+ AlertDialog,
73
+ AlertDialogTrigger,
74
+ AlertDialogPortal,
75
+ AlertDialogOverlay,
76
+ AlertDialogContent,
77
+ AlertDialogHeader,
78
+ AlertDialogFooter,
79
+ AlertDialogTitle,
80
+ AlertDialogDescription,
81
+ AlertDialogAction,
82
+ AlertDialogCancel,
83
+ };
package/src/ui/button.tsx CHANGED
@@ -10,7 +10,8 @@ const buttonVariants = cva(
10
10
  variant: {
11
11
  default:
12
12
  "bg-accent text-accent-foreground hover:-translate-y-0.5 hover:opacity-90",
13
- destructive: "bg-danger text-danger-foreground hover:bg-danger/90",
13
+ destructive:
14
+ "bg-[oklch(47.2%_0.195_25.7)] text-white hover:bg-[oklch(42.5%_0.185_25.7)]",
14
15
  outline:
15
16
  "border border-border bg-transparent text-foreground hover:bg-default",
16
17
  secondary: "bg-default text-default-foreground hover:bg-default/78",