@vvedantb/ui 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -3
- package/app/README.md +100 -0
- package/app/src/confirm/ConfirmSkipHint.tsx +1 -1
- package/app/src/settings/SettingsEmptyState.tsx +2 -2
- package/app/src/settings/SettingsField.tsx +1 -1
- package/app/src/settings/SettingsSection.tsx +1 -1
- package/app/src/settings/SettingsToggleRow.tsx +1 -1
- package/package.json +16 -10
- package/shell/src/PillTabs.tsx +1 -1
- package/shell/src/SessionsHoverCard.tsx +148 -0
- package/shell/src/index.ts +5 -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 +2 -2
- 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 +1 -1
- package/src/audio/index.ts +3 -0
- package/src/audio/playChime.ts +76 -0
- package/src/audio/useReplyChime.ts +62 -0
- package/src/index.ts +2 -0
- package/src/ui/_menu-classes.alt.ts +2 -2
- package/src/ui/_menu-classes.ts +2 -2
- package/src/ui/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.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.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-icons.tsx +94 -0
- package/src/ui/sonner.tsx +167 -34
- 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/toaster/Fixture.tsx +53 -0
- package/tokens/examples/toaster/README.md +18 -0
- package/tokens/examples/toaster/compat.html +12 -0
- package/tokens/examples/toaster/main-compat.tsx +5 -0
- package/tokens/examples/toaster/main-tokens.tsx +5 -0
- package/tokens/examples/toaster/package.json +14 -0
- package/tokens/examples/toaster/styles-compat.css +82 -0
- package/tokens/examples/toaster/styles-tokens.css +16 -0
- package/tokens/examples/toaster/tokens.html +12 -0
- package/tokens/examples/toaster/tsconfig.json +14 -0
- package/tokens/examples/toaster/vite.config.ts +12 -0
- package/tokens/src/css/compat-shadcn-rgb.css +82 -0
- package/tokens/src/css/utilities.css +5 -0
- package/tokens/src/tailwind-preset.mjs +6 -0
- package/tokens/src/tokens.css +15 -0
package/README.md
CHANGED
|
@@ -4,12 +4,13 @@ Shared React 19 web UI. Source-exported: no build step.
|
|
|
4
4
|
|
|
5
5
|
| Import | What it is |
|
|
6
6
|
| --- | --- |
|
|
7
|
-
| `@vvedantb/ui` | Primitives (Radix-based)
|
|
7
|
+
| `@vvedantb/ui` | Primitives (Radix-based), AI elements, `playChime` and `useReplyChime`. |
|
|
8
8
|
| `@vvedantb/ui/cn` | `cn` class helper (clsx + tailwind-merge). |
|
|
9
|
-
| `@vvedantb/ui/shell` | Router-free `SidebarHeader` and `
|
|
9
|
+
| `@vvedantb/ui/shell` | Router-free `SidebarHeader`, `PillTabs` and `SessionsHoverCard`. |
|
|
10
10
|
| `@vvedantb/ui/app` | Alt-click confirm skip, update watcher, settings chrome, `useStableAuth`. See `app/README.md` in the repository. |
|
|
11
11
|
| `@vvedantb/ui/tokens` | `tokens.css`: colour, radius, shadow, ease and font tokens for Tailwind v4. |
|
|
12
|
-
| `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `animations`. |
|
|
12
|
+
| `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `animations`, `compat-shadcn-rgb`. |
|
|
13
|
+
| `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | Same file as `tokens/css/compat-shadcn-rgb.css`. |
|
|
13
14
|
| `@vvedantb/ui/tokens/tailwind-preset` | Tailwind preset for apps that load a JS config with `@config`. |
|
|
14
15
|
| `@vvedantb/ui/tokens/native` | JS colours, radii and spacing. No CSS, no React DOM. |
|
|
15
16
|
|
|
@@ -19,6 +20,91 @@ Shared React 19 web UI. Source-exported: no build step.
|
|
|
19
20
|
@source "../node_modules/@vvedantb/ui/src/**/*.{ts,tsx}";
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
## Reply chime
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { playChime, useReplyChime } from "@vvedantb/ui";
|
|
27
|
+
|
|
28
|
+
useReplyChime({ enabled: soundOn, isStreaming, threadId });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- `playChime()`: soft rising two-note chime (WebAudio). One shared `AudioContext`, created on first use. It is a no-op on the server and when autoplay blocks audio. Calls less than 1 second apart play once.
|
|
32
|
+
- `useReplyChime`: chimes once when `isStreaming` goes from `true` to `false` for the same `threadId`. It is silent on the first sight of a thread, when `threadId` changes, and when `threadId` is null or undefined. While `enabled` is `false` it still tracks the edge, so enabling mid-turn does not replay a finish.
|
|
33
|
+
|
|
34
|
+
## Sessions hover card
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { SessionsHoverCard } from "@vvedantb/ui/shell";
|
|
38
|
+
|
|
39
|
+
<SessionsHoverCard
|
|
40
|
+
header="Session title"
|
|
41
|
+
loadContent={() => fetchPreview(id)}
|
|
42
|
+
footer="Updated today"
|
|
43
|
+
>
|
|
44
|
+
<button type="button">Row</button>
|
|
45
|
+
</SessionsHoverCard>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`children` is the trigger. Defaults: `openDelay` 400 ms, `closeDelay` 150 ms, `side="right"`, `align="start"`, `sideOffset` 8, width and padding `w-64 p-3` (override with `className`). `body` is a node or a function of `{ open }`. `loadContent` runs only while the card is open. Control the state with `open`, `defaultOpen` and `onOpenChange`. Fixture: `shell/examples/sessions-hover-card/`.
|
|
49
|
+
|
|
50
|
+
## Toaster
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { toast } from "sonner";
|
|
54
|
+
import { SonnerToaster } from "@vvedantb/ui";
|
|
55
|
+
|
|
56
|
+
<SonnerToaster
|
|
57
|
+
theme={theme}
|
|
58
|
+
toastRadius="surface"
|
|
59
|
+
toastIcons="filled24"
|
|
60
|
+
closeButtonStyle="hoverChip"
|
|
61
|
+
sonnerVars
|
|
62
|
+
descriptionMuted
|
|
63
|
+
/>;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
With no new props the toaster looks as it did in 0.2.1. Caller props merge with the package defaults instead of replacing them:
|
|
67
|
+
|
|
68
|
+
- `toastOptions.classNames`: per key, the package classes come first and the caller classes follow, so callers can override utilities. Other `toastOptions` fields (such as `duration`) take the caller value.
|
|
69
|
+
- `icons`: merged per key. With `toastIcons="filled24"` the caller overrides single icons and keeps the rest.
|
|
70
|
+
- `style`: merged over the package style. `className` is appended.
|
|
71
|
+
|
|
72
|
+
Opt-in props (all default to the current look):
|
|
73
|
+
|
|
74
|
+
| Prop | Values | Effect |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `toastRadius` | `"lg"` (default), `"surface"` | `rounded-lg` or `rounded-surface` (from `tokens/css/utilities.css`). |
|
|
77
|
+
| `toastIcons` | `"default"`, `"filled24"` | Sonner icons or 24px filled success, info, warning and error icons, a matching loader and a 14px close cross. |
|
|
78
|
+
| `closeButtonStyle` | `"default"`, `"hoverChip"` | Text-style close button or a round chip at the top-right, shown on hover and always on small screens. It turns on `closeButton` unless you set it. |
|
|
79
|
+
| `sonnerVars` | `false` (default), `true` | Sets `--normal-bg`, `--normal-border`, `--normal-text`, `--normal-bg-hover` and `--normal-border-hover` from `--color-card`, `--color-border`, `--color-foreground` and `--color-muted`, and `font-family: inherit`. |
|
|
80
|
+
| `descriptionMuted` | `false` (default), `true` | Description uses `text-muted-foreground!`, which beats sonner's dark-theme rule. |
|
|
81
|
+
|
|
82
|
+
Helpers: `createToasterClassNames`, `mergeToastClassNames`, `mergeToastOptions`, `mergeToasterStyle`, `filledToastIcons`. Fixture: `tokens/examples/toaster/`.
|
|
83
|
+
|
|
84
|
+
## Colour names in components
|
|
85
|
+
|
|
86
|
+
Components use `text-muted-foreground`, `primary` and `--color-*` theme variables. They no longer use `text-muted`, `*-accent` or raw `var(--muted)` reads, which clash with shadcn names. `tokens.css` still defines `--color-muted` and `--color-accent`. `pnpm --filter @vvedantb/ui check:tokens` fails if a component brings the old names back (it also runs in `build`, `test` and `verify`).
|
|
87
|
+
|
|
88
|
+
## Using an existing shadcn RGB theme
|
|
89
|
+
|
|
90
|
+
Use this when the app defines shadcn-style variables as space-separated channels (`--background: 244 245 246`) and reads them as `rgb(var(--background) / 0.5)`.
|
|
91
|
+
|
|
92
|
+
1. Import `compat-shadcn-rgb.css` **instead of** `tokens.css`. Never import both.
|
|
93
|
+
2. The app keeps `--background`, `--foreground`, `--card`, `--popover`, `--primary`, `--secondary`, `--muted`, `--accent`, `--destructive`, `--border`, `--input`, `--ring` (each with its `-foreground` where shadcn has one) and `--radius`. The compat file does not define them. It only references them, so the app can change `--primary`, `--ring`, `--radius` or `--font-sans` at runtime.
|
|
94
|
+
3. Add the `@source` line for the package sources.
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
@import "tailwindcss";
|
|
98
|
+
@import "@vvedantb/ui/tokens/css/compat-shadcn-rgb.css";
|
|
99
|
+
@source "../node_modules/@vvedantb/ui/src/**/*.{ts,tsx}";
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Shared CSS that is safe without `tokens.css`: `compat-shadcn-rgb.css`, `dialog.css`, `utilities.css`, `scrollbar-thin.css` (reads `--scrollbar`, which the compat file defines), `modal.css` and `tabs.css` (both need the app motion variables listed in `tokens/README.md`), and `animations.css` (not with the Tailwind preset). `base.css` and `tabs-pills.css` read raw `--accent`, `--foreground`, `--muted`, `--segment` and `--default`; skip them unless the app defines those as full colour values.
|
|
103
|
+
|
|
104
|
+
The compat file includes a fallback `glass-panel-strong` (`rgb(var(--card) / 0.95)` with a 12px blur) for toasts and dialogs. Declare your own `@utility glass-panel-strong` after the import to override it.
|
|
105
|
+
|
|
106
|
+
A fixture that renders the main components against sample light and dark RGB variables is in `tokens/examples/compat-shadcn-rgb/`.
|
|
107
|
+
|
|
22
108
|
`@clerk/clerk-react`, `@tanstack/react-hotkeys` and `zod` are optional peers. Install them only if you import `@vvedantb/ui/app`.
|
|
23
109
|
|
|
24
110
|
Full documentation: the repository `README.md`. Upgrading from `@vvedantb/tokens`, `@vvedantb/shell` or `@vvedantb/app-kit`: `MIGRATION.md`.
|
package/app/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# `@vvedantb/ui/app`
|
|
2
|
+
|
|
3
|
+
Shared web React helpers that were duplicated across apps. Source-exported as the `./app` subpath of `@vvedantb/ui`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @vvedantb/ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Peers
|
|
12
|
+
|
|
13
|
+
Required: `react` and `react-dom` (>=19). Optional peers of `@vvedantb/ui`, needed only for this subpath: `@clerk/clerk-react` (`useStableAuth`), `@tanstack/react-hotkeys` and `zod`.
|
|
14
|
+
|
|
15
|
+
`sonner` is not a dependency. The update watcher takes the toast API as an option. This subpath uses a local class join (`cn`), not the `cn` from `@vvedantb/ui`. Tailwind must scan `node_modules/@vvedantb/ui/app/src/**/*.{ts,tsx}` so the class names are generated, and the app needs the `@vvedantb/ui/tokens` names (`bg-surface-card`, `divide-separator`, `text-muted-foreground` and so on).
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
@source "../node_modules/@vvedantb/ui/app/src/**/*.{ts,tsx}";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Confirm skip (Alt-click)
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { ConfirmSkipHint, skipConfirmTitle, useRequestConfirm } from "@vvedantb/ui/app";
|
|
25
|
+
|
|
26
|
+
const { request } = useRequestConfirm();
|
|
27
|
+
|
|
28
|
+
<DropdownMenuItem
|
|
29
|
+
title={skipConfirmTitle("Delete")}
|
|
30
|
+
onSelect={(event) => request(() => setOpen(true), () => remove(), event)}
|
|
31
|
+
>
|
|
32
|
+
Delete <ConfirmSkipHint />
|
|
33
|
+
</DropdownMenuItem>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `useRequestConfirm` does not subscribe to Alt. It reads `getAltHeld()` when the request happens, so the menu owner does not re-render when Alt goes down. It returns `{ request }` only.
|
|
37
|
+
- `ConfirmSkipHint` stays mounted and gains a `hidden` class when Alt is not held, so an open Radix menu keeps a stable child tree.
|
|
38
|
+
- The Alt store resets when the last subscriber leaves, clears on blur and tab hide, and cancels a bare Alt keydown only inside `[role='menu']` or `[data-radix-menu-content]`.
|
|
39
|
+
- Also exported: `useAltHeld`, `getAltHeld`, `createAltHeldStore`, `bindToWindow`, `shouldSkipConfirm`, `requestConfirm`.
|
|
40
|
+
|
|
41
|
+
## Update available watcher
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { toast } from "sonner";
|
|
45
|
+
import { startUpdateAvailableWatcher } from "@vvedantb/ui/app";
|
|
46
|
+
|
|
47
|
+
useEffect(
|
|
48
|
+
() =>
|
|
49
|
+
startUpdateAvailableWatcher({
|
|
50
|
+
origin: window.location.origin,
|
|
51
|
+
toast,
|
|
52
|
+
closeConvex: () => void convex.close(),
|
|
53
|
+
enabled: !import.meta.env.DEV,
|
|
54
|
+
}),
|
|
55
|
+
[],
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Options: `origin`, `toast`, `closeConvex?`, `pollMs?` (default 60000), `toastId?` (default `app-update-available`), `enabled?` (default true), `reload?` (default `window.location.reload`). On a changed asset fingerprint it shows a sticky "Update available" toast. Refresh calls `closeConvex` (errors ignored), then reloads. `fingerprintFromHtml` and `fetchDeployFingerprint` are exported too. The fingerprint looks for Vite `/assets/*.js` URLs in the index HTML.
|
|
60
|
+
|
|
61
|
+
## Settings chrome
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { SettingsPage, SettingsSection, SettingsToggleRow } from "@vvedantb/ui/app";
|
|
65
|
+
|
|
66
|
+
<SettingsPage
|
|
67
|
+
title="Notifications"
|
|
68
|
+
layout={({ title, headerRight, toolbar, tabs, fillHeight, children }) => (
|
|
69
|
+
<PageContainer title={title} rightSection={headerRight} toolbar={toolbar} tabs={tabs} noScroll={fillHeight}>
|
|
70
|
+
{children}
|
|
71
|
+
</PageContainer>
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
<SettingsSection title="Email" bodyVariant="list">
|
|
75
|
+
<SettingsToggleRow title="Weekly digest" action={<Switch />} />
|
|
76
|
+
</SettingsSection>
|
|
77
|
+
</SettingsPage>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Exports: `SettingsSection`, `SettingsField`, `SettingsStack`, `SettingsToggleRow`, `SettingsEmptyState` (icon is `ComponentType<{ size?: number; className?: string }>`), `SettingsPage`. Without `layout`, `SettingsPage` renders a plain `div` with a heading and the optional slots.
|
|
81
|
+
|
|
82
|
+
## useStableAuth
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { useStableAuth } from "@vvedantb/ui/app"; // 2000 ms debounce, wraps Clerk useAuth
|
|
86
|
+
import { createUseStableAuth } from "@vvedantb/ui/app";
|
|
87
|
+
|
|
88
|
+
const useAuth = createUseStableAuth(useClerkAuth, { debounceMs: 3000 });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
After a signed-in session, a drop to signed-out reports `isLoaded: false` for the debounce window so Convex does not cascade. State is module-level. `resetStableAuthSession()` clears it and is for tests only.
|
|
92
|
+
|
|
93
|
+
## Develop
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
pnpm --filter @vvedantb/ui typecheck
|
|
97
|
+
pnpm --filter @vvedantb/ui test
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Source is in `app/src` and tests in `app/tests`. Publish steps are in the root `README.md`.
|
|
@@ -23,7 +23,7 @@ export function ConfirmSkipHint({ className }: { className?: string }) {
|
|
|
23
23
|
return (
|
|
24
24
|
<kbd
|
|
25
25
|
className={cn(
|
|
26
|
-
"ml-auto shrink-0 rounded bg-surface-secondary px-1 py-0.5 font-mono text-[10px] text-muted",
|
|
26
|
+
"ml-auto shrink-0 rounded bg-surface-secondary px-1 py-0.5 font-mono text-[10px] text-muted-foreground",
|
|
27
27
|
!altHeld && "hidden",
|
|
28
28
|
className,
|
|
29
29
|
)}
|
|
@@ -25,10 +25,10 @@ export function SettingsEmptyState({
|
|
|
25
25
|
}: SettingsEmptyStateProps) {
|
|
26
26
|
return (
|
|
27
27
|
<div className="flex flex-col items-center justify-center px-4 py-16 text-center">
|
|
28
|
-
<Icon size={28} className="mb-5 text-muted opacity-50" />
|
|
28
|
+
<Icon size={28} className="mb-5 text-muted-foreground opacity-50" />
|
|
29
29
|
<h4 className="text-sm font-medium text-foreground">{title}</h4>
|
|
30
30
|
{description ? (
|
|
31
|
-
<p className="mt-2 max-w-sm text-xs leading-relaxed text-muted">
|
|
31
|
+
<p className="mt-2 max-w-sm text-xs leading-relaxed text-muted-foreground">
|
|
32
32
|
{description}
|
|
33
33
|
</p>
|
|
34
34
|
) : null}
|
|
@@ -33,7 +33,7 @@ export function SettingsField({
|
|
|
33
33
|
</label>
|
|
34
34
|
{children}
|
|
35
35
|
{description ? (
|
|
36
|
-
<p className="mt-1.5 text-pretty text-xs leading-relaxed text-muted">
|
|
36
|
+
<p className="mt-1.5 text-pretty text-xs leading-relaxed text-muted-foreground">
|
|
37
37
|
{description}
|
|
38
38
|
</p>
|
|
39
39
|
) : null}
|
|
@@ -60,7 +60,7 @@ export function SettingsSection({
|
|
|
60
60
|
{title}
|
|
61
61
|
</h3>
|
|
62
62
|
{description ? (
|
|
63
|
-
<p className="mt-0.5 text-pretty text-sm leading-relaxed text-muted">
|
|
63
|
+
<p className="mt-0.5 text-pretty text-sm leading-relaxed text-muted-foreground">
|
|
64
64
|
{description}
|
|
65
65
|
</p>
|
|
66
66
|
) : null}
|
|
@@ -40,7 +40,7 @@ export function SettingsToggleRow({
|
|
|
40
40
|
<p className={titleClassName}>{title}</p>
|
|
41
41
|
)}
|
|
42
42
|
{description ? (
|
|
43
|
-
<p className="mt-1 text-pretty text-xs leading-relaxed text-muted">
|
|
43
|
+
<p className="mt-1 text-pretty text-xs leading-relaxed text-muted-foreground">
|
|
44
44
|
{description}
|
|
45
45
|
</p>
|
|
46
46
|
) : null}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"license": "MIT",
|
|
5
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",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"./tokens/css/utilities.css": "./tokens/src/css/utilities.css",
|
|
45
45
|
"./tokens/css/scrollbar-thin.css": "./tokens/src/css/scrollbar-thin.css",
|
|
46
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",
|
|
47
49
|
"./tokens/css/*.css": "./tokens/src/css/*.css",
|
|
48
50
|
"./tokens/tailwind-preset": {
|
|
49
51
|
"types": "./tokens/src/tailwind-preset.d.mts",
|
|
@@ -59,11 +61,22 @@
|
|
|
59
61
|
"tokens/src",
|
|
60
62
|
"tokens/examples",
|
|
61
63
|
"README.md",
|
|
62
|
-
"LICENSE"
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"!**/*.test.ts",
|
|
66
|
+
"!**/*.test.tsx"
|
|
63
67
|
],
|
|
64
68
|
"sideEffects": [
|
|
65
69
|
"./src/ui/sonner.css"
|
|
66
70
|
],
|
|
71
|
+
"scripts": {
|
|
72
|
+
"check:tokens": "node tokens/scripts/check-component-tokens.mjs",
|
|
73
|
+
"build": "node tokens/scripts/check-exports.mjs && node tokens/scripts/check-component-tokens.mjs && tsc -p tsconfig.json",
|
|
74
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tokens/tsconfig.json",
|
|
75
|
+
"test": "node tokens/scripts/check-component-tokens.mjs && vitest run && node --test tokens/test/*.test.mjs",
|
|
76
|
+
"verify": "node tokens/scripts/check-component-tokens.mjs && node tokens/verify/run.mjs",
|
|
77
|
+
"analyze": "node tokens/verify/analyze.mjs",
|
|
78
|
+
"prepack": "node tokens/scripts/check-exports.mjs"
|
|
79
|
+
},
|
|
67
80
|
"peerDependencies": {
|
|
68
81
|
"@clerk/clerk-react": ">=5",
|
|
69
82
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
@@ -171,12 +184,5 @@
|
|
|
171
184
|
"zod": {
|
|
172
185
|
"optional": true
|
|
173
186
|
}
|
|
174
|
-
},
|
|
175
|
-
"scripts": {
|
|
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"
|
|
181
187
|
}
|
|
182
|
-
}
|
|
188
|
+
}
|
package/shell/src/PillTabs.tsx
CHANGED
|
@@ -16,7 +16,7 @@ export type PillTabsProps = {
|
|
|
16
16
|
|
|
17
17
|
const tabBase =
|
|
18
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";
|
|
19
|
+
const tabIdle = "text-muted-foreground hover:bg-default/50 hover:text-foreground";
|
|
20
20
|
const tabSelected = "bg-default text-foreground";
|
|
21
21
|
|
|
22
22
|
export function PillTabs({ tabs, value, onValueChange, className }: PillTabsProps) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
HoverCard,
|
|
5
|
+
HoverCardContent,
|
|
6
|
+
HoverCardTrigger,
|
|
7
|
+
} from "../../src/ui/hover-card";
|
|
8
|
+
import { cn } from "../../src/utils/cn";
|
|
9
|
+
|
|
10
|
+
export type SessionsHoverCardContext = { open: boolean };
|
|
11
|
+
|
|
12
|
+
export type SessionsHoverCardProps = {
|
|
13
|
+
/** The element that triggers the card, such as a list row. Rendered with `asChild`. */
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
header?: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Body slot. A function runs only while the card is open, so it can start
|
|
18
|
+
* fetches for preview data.
|
|
19
|
+
*/
|
|
20
|
+
body?: ReactNode | ((ctx: SessionsHoverCardContext) => ReactNode);
|
|
21
|
+
/** Runs each time the card opens. Its result replaces `body`. */
|
|
22
|
+
loadContent?: () => Promise<ReactNode> | ReactNode;
|
|
23
|
+
/** Shown while `loadContent` is pending. */
|
|
24
|
+
loadingFallback?: ReactNode;
|
|
25
|
+
footer?: ReactNode;
|
|
26
|
+
/** Controlled open state. */
|
|
27
|
+
open?: boolean;
|
|
28
|
+
defaultOpen?: boolean;
|
|
29
|
+
onOpenChange?: (open: boolean) => void;
|
|
30
|
+
/** Delay before opening, in milliseconds. */
|
|
31
|
+
openDelay?: number;
|
|
32
|
+
/** Delay before closing, in milliseconds. */
|
|
33
|
+
closeDelay?: number;
|
|
34
|
+
side?: "top" | "right" | "bottom" | "left";
|
|
35
|
+
align?: "start" | "center" | "end";
|
|
36
|
+
sideOffset?: number;
|
|
37
|
+
/** Merged over the default `w-64 p-3`. */
|
|
38
|
+
className?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type LoadState = { status: "loading" } | { status: "ready"; node: ReactNode };
|
|
42
|
+
|
|
43
|
+
/** Mounts only while the card is open, so `loadContent` never runs while closed. */
|
|
44
|
+
function LoadedBody({
|
|
45
|
+
loadContent,
|
|
46
|
+
fallback,
|
|
47
|
+
}: {
|
|
48
|
+
loadContent: () => Promise<ReactNode> | ReactNode;
|
|
49
|
+
fallback: ReactNode;
|
|
50
|
+
}) {
|
|
51
|
+
const [state, setState] = useState<LoadState>({ status: "loading" });
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let cancelled = false;
|
|
55
|
+
Promise.resolve()
|
|
56
|
+
.then(loadContent)
|
|
57
|
+
.then((node) => {
|
|
58
|
+
if (!cancelled) setState({ status: "ready", node });
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {
|
|
61
|
+
if (!cancelled) setState({ status: "ready", node: null });
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
};
|
|
66
|
+
// Run once per open. A new `loadContent` identity must not refetch.
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return <>{state.status === "ready" ? state.node : fallback}</>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Calls the render prop. Mounts only inside open content, never while closed. */
|
|
74
|
+
function RenderBody({
|
|
75
|
+
render,
|
|
76
|
+
open,
|
|
77
|
+
}: {
|
|
78
|
+
render: (ctx: SessionsHoverCardContext) => ReactNode;
|
|
79
|
+
open: boolean;
|
|
80
|
+
}) {
|
|
81
|
+
return <>{render({ open })}</>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hover card shell for list rows such as a sessions sidebar. It sets delay,
|
|
86
|
+
* position, width and padding, and gives header, body and footer slots. The
|
|
87
|
+
* app supplies the content.
|
|
88
|
+
*/
|
|
89
|
+
export function SessionsHoverCard({
|
|
90
|
+
children,
|
|
91
|
+
header,
|
|
92
|
+
body,
|
|
93
|
+
loadContent,
|
|
94
|
+
loadingFallback = null,
|
|
95
|
+
footer,
|
|
96
|
+
open: openProp,
|
|
97
|
+
defaultOpen = false,
|
|
98
|
+
onOpenChange,
|
|
99
|
+
openDelay = 400,
|
|
100
|
+
closeDelay = 150,
|
|
101
|
+
side = "right",
|
|
102
|
+
align = "start",
|
|
103
|
+
sideOffset = 8,
|
|
104
|
+
className,
|
|
105
|
+
}: SessionsHoverCardProps) {
|
|
106
|
+
const [openState, setOpenState] = useState(defaultOpen);
|
|
107
|
+
const open = openProp ?? openState;
|
|
108
|
+
|
|
109
|
+
const handleOpenChange = (next: boolean) => {
|
|
110
|
+
setOpenState(next);
|
|
111
|
+
onOpenChange?.(next);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const bodyNode = loadContent ? (
|
|
115
|
+
<LoadedBody loadContent={loadContent} fallback={loadingFallback} />
|
|
116
|
+
) : typeof body === "function" ? (
|
|
117
|
+
<RenderBody render={body} open={open} />
|
|
118
|
+
) : (
|
|
119
|
+
body
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<HoverCard
|
|
124
|
+
open={open}
|
|
125
|
+
onOpenChange={handleOpenChange}
|
|
126
|
+
openDelay={openDelay}
|
|
127
|
+
closeDelay={closeDelay}
|
|
128
|
+
>
|
|
129
|
+
<HoverCardTrigger asChild>{children}</HoverCardTrigger>
|
|
130
|
+
<HoverCardContent
|
|
131
|
+
side={side}
|
|
132
|
+
align={align}
|
|
133
|
+
sideOffset={sideOffset}
|
|
134
|
+
className={cn("w-64 p-3", className)}
|
|
135
|
+
>
|
|
136
|
+
{header ? (
|
|
137
|
+
<div className="mb-2 text-sm font-medium text-foreground">{header}</div>
|
|
138
|
+
) : null}
|
|
139
|
+
{bodyNode != null ? (
|
|
140
|
+
<div className="text-sm text-muted-foreground">{bodyNode}</div>
|
|
141
|
+
) : null}
|
|
142
|
+
{footer ? (
|
|
143
|
+
<div className="mt-2 text-xs text-muted-foreground">{footer}</div>
|
|
144
|
+
) : null}
|
|
145
|
+
</HoverCardContent>
|
|
146
|
+
</HoverCard>
|
|
147
|
+
);
|
|
148
|
+
}
|
package/shell/src/index.ts
CHANGED
|
@@ -4,3 +4,8 @@ export { PillTabs } from "./PillTabs.js";
|
|
|
4
4
|
export type { PillTabItem, PillTabsProps } from "./PillTabs.js";
|
|
5
5
|
export { cn } from "./cn.js";
|
|
6
6
|
export type { ClassValue } from "./cn.js";
|
|
7
|
+
export { SessionsHoverCard } from "./SessionsHoverCard.js";
|
|
8
|
+
export type {
|
|
9
|
+
SessionsHoverCardContext,
|
|
10
|
+
SessionsHoverCardProps,
|
|
11
|
+
} from "./SessionsHoverCard.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
|
)}
|
|
@@ -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
|
)}
|
|
@@ -147,7 +147,7 @@ export const InlineCitationCarouselIndex = ({
|
|
|
147
147
|
return (
|
|
148
148
|
<div
|
|
149
149
|
className={cn(
|
|
150
|
-
"flex flex-1 items-center justify-end px-3 py-1 text-muted text-xs",
|
|
150
|
+
"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs",
|
|
151
151
|
className,
|
|
152
152
|
)}
|
|
153
153
|
{...props}
|
|
@@ -178,7 +178,7 @@ export const InlineCitationCarouselPrev = ({
|
|
|
178
178
|
type="button"
|
|
179
179
|
{...props}
|
|
180
180
|
>
|
|
181
|
-
<IconArrowLeft className="size-4 text-muted" />
|
|
181
|
+
<IconArrowLeft className="size-4 text-muted-foreground" />
|
|
182
182
|
</button>
|
|
183
183
|
);
|
|
184
184
|
};
|
|
@@ -204,7 +204,7 @@ export const InlineCitationCarouselNext = ({
|
|
|
204
204
|
type="button"
|
|
205
205
|
{...props}
|
|
206
206
|
>
|
|
207
|
-
<IconArrowRight className="size-4 text-muted" />
|
|
207
|
+
<IconArrowRight className="size-4 text-muted-foreground" />
|
|
208
208
|
</button>
|
|
209
209
|
);
|
|
210
210
|
};
|
|
@@ -225,9 +225,9 @@ export const InlineCitationSource = ({
|
|
|
225
225
|
{title && (
|
|
226
226
|
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
|
227
227
|
)}
|
|
228
|
-
{url && <p className="truncate break-all text-muted text-xs">{url}</p>}
|
|
228
|
+
{url && <p className="truncate break-all text-muted-foreground text-xs">{url}</p>}
|
|
229
229
|
{description && (
|
|
230
|
-
<p className="line-clamp-3 text-muted text-sm leading-relaxed">
|
|
230
|
+
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
|
231
231
|
{description}
|
|
232
232
|
</p>
|
|
233
233
|
)}
|
|
@@ -242,7 +242,7 @@ export const InlineCitationQuote = ({
|
|
|
242
242
|
}: InlineCitationQuoteProps) => (
|
|
243
243
|
<blockquote
|
|
244
244
|
className={cn(
|
|
245
|
-
"border-l-2 border-separator pl-3 text-muted text-sm italic",
|
|
245
|
+
"border-l-2 border-separator pl-3 text-muted-foreground text-sm italic",
|
|
246
246
|
className,
|
|
247
247
|
)}
|
|
248
248
|
{...props}
|
|
@@ -34,7 +34,7 @@ export const MessageContent = ({
|
|
|
34
34
|
<div
|
|
35
35
|
className={cn(
|
|
36
36
|
"flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
|
|
37
|
-
"group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:rounded-br-md group-[.is-user]:bg-
|
|
37
|
+
"group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:rounded-br-md group-[.is-user]:bg-primary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-primary-foreground",
|
|
38
38
|
"group-[.is-assistant]:px-0 group-[.is-assistant]:text-foreground",
|
|
39
39
|
className,
|
|
40
40
|
)}
|
|
@@ -242,7 +242,7 @@ export const MessageBranchPage = ({
|
|
|
242
242
|
return (
|
|
243
243
|
<ButtonGroupText
|
|
244
244
|
className={cn(
|
|
245
|
-
"border-none bg-transparent text-muted shadow-none",
|
|
245
|
+
"border-none bg-transparent text-muted-foreground shadow-none",
|
|
246
246
|
className,
|
|
247
247
|
)}
|
|
248
248
|
{...props}
|