@vvedantb/ui 0.30.1 → 0.31.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 +25 -8
- package/package.json +1 -1
- package/shell/src/SidebarAccountMenu.tsx +6 -2
- package/shell/src/SidebarChrome.tsx +3 -2
- package/shell/src/SidebarPanel.tsx +6 -2
- package/src/ai-elements/__snapshots__/queue.test.tsx.snap +3 -3
- package/src/ai-elements/message.tsx +4 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/use-count-up-display.ts +80 -0
- package/src/hooks/use-document-appearance.ts +46 -0
- package/src/markdown/mermaid.ts +7 -12
- package/src/ui/button.tsx +37 -9
- package/src/ui/clearInputDissolve.ts +3 -8
- package/src/ui/input-group.alt.tsx +2 -1
- package/src/ui/input-group.tsx +5 -1
package/README.md
CHANGED
|
@@ -260,6 +260,17 @@ import {
|
|
|
260
260
|
<FadePresence show={visible} variants={fadeUpSpring}>…</FadePresence>
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
+
- `Button` `shape` (0.31.0, a default change): `"pill"` (default) is `rounded-full` for every variant and size, so the icon sizes are circles. `"rounded"` keeps the radius from before 0.31.0: `rounded-lg`, and `rounded-md` on `xs` and `icon-xs`. Use `shape="rounded"` for buttons that join other edges (inside a `ButtonGroup`) or sit in square chrome. A `className` radius still wins over both. `InputGroupButton`, the message branch arrows and the sidebar `AccountTrigger`, `MobileMenuButton` and `PanelClose` already use `"rounded"`. With the default `--radius` (1rem), buttons 32px tall or less were already fully round, so the visible change is on `default`, `lg` and `icon`.
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
<Button>Save</Button> {/* pill */}
|
|
267
|
+
<Button size="icon" aria-label="Add"><IconPlus /></Button> {/* circle */}
|
|
268
|
+
<ButtonGroup>
|
|
269
|
+
<Button variant="outline" shape="rounded">Day</Button>
|
|
270
|
+
<Button variant="outline" shape="rounded">Week</Button>
|
|
271
|
+
</ButtonGroup>
|
|
272
|
+
```
|
|
273
|
+
|
|
263
274
|
- `Button`: variants `flat`, `success` and `warning` (each is `ghost` plus an override). `loading` disables the button, sets `data-loading="true"` and shows a spinner in place of `startContent`; without `loading` there is no `data-loading` attribute. `endContent` renders after the children. Both are ignored with `asChild` (`loading` still disables).
|
|
264
275
|
- `DropdownMenuContent`: `fitViewport` caps the height on small screens and scrolls. It reads the optional `--vv-height` CSS variable, which the app may set to the visual viewport height (falls back to `70dvh`). `suppressAltKey` stops a bare Alt press from moving focus. `restoreScrollLockOnClose` calls `restoreModalScrollLock` on close (now and on the next tick). `ContextMenuContent` takes `suppressAltKey` too.
|
|
265
276
|
- `DropdownMenuItem` `endContent` and `ContextMenuItem` `startContent`, `description` and `endContent`: the label moves into a growing column (`min-w-0 flex-1`), the description sits under it and `endContent` sits at the end. `DropdownMenuSubTrigger` and `ContextMenuSubTrigger` take `startContent`.
|
|
@@ -438,7 +449,7 @@ Convention: compound parts are static members of a namespace object, `<Namespace
|
|
|
438
449
|
| --- | --- |
|
|
439
450
|
| `Root` | No DOM. Holds `isDesktop`, `mobileOpen`, `onMobileOpenChange`, `collapsed`, `panelHidden` and the drawer `id`. Closes the drawer at the desktop breakpoint. The chrome parts need it. |
|
|
440
451
|
| `MobileHeader` | Fixed header below `md`, a three-column grid (menu button, title, actions). Escape closes the drawer. Keep all three children so the columns line up. |
|
|
441
|
-
| `MobileMenuButton` | Opens the drawer. `label`, `icon` (`(open) => node`). |
|
|
452
|
+
| `MobileMenuButton` | Opens the drawer. `label`, `icon` (`(open) => node`). Keeps `rounded-lg` (`Button` `shape="rounded"`); change it with `className`. |
|
|
442
453
|
| `MobileTitle` | Centred `<h1>` that truncates. Without children, an empty `<span>` keeps the middle column. |
|
|
443
454
|
| `MobileActions` | Trailing header actions. Without children, a spacer that keeps the title centred. |
|
|
444
455
|
| `Backdrop` | Drawer backdrop; a click closes the drawer. `label` (`"Close navigation"`). |
|
|
@@ -452,11 +463,11 @@ Convention: compound parts are static members of a namespace object, `<Namespace
|
|
|
452
463
|
| `Panel` | Panel column. `collapsed` (`md:hidden`) and `panelHidden` (renders nothing) default to the Root values. |
|
|
453
464
|
| `PanelHeader` | Padded header row for `PanelTitle` and `PanelActions`. |
|
|
454
465
|
| `PanelTitle` / `PanelActions` | The `<h2>` and the trailing actions. |
|
|
455
|
-
| `PanelClose` | Ghost close button. `label` (`"Close navigation"`). Without `onClick` it closes the drawer. |
|
|
466
|
+
| `PanelClose` | Ghost close button. `label` (`"Close navigation"`). Without `onClick` it closes the drawer. Keeps `rounded-lg` (`Button` `shape="rounded"`); change it with `className`. |
|
|
456
467
|
| `NavItem` | Panel nav row (`sidebarNavItemClass`). `active` (fill, `font-medium`, `aria-current="page"`), `iconOnly`, `icon` (element; gets `sidebarNavIconClass`), `asChild` for a router link. A `<button type="button">` without `asChild`. 0.7.2: `selected`, `indent`, `dragging`, `dropTarget`; see [Sidebar nav row parts (0.7.2)](#sidebar-nav-row-parts-072). |
|
|
457
468
|
| `NavItemLeading` / `NavItemLabel` / `NavItemTrailing` / `NavItemActions` | Row parts (0.7.2): leading slot, truncated label, always-visible trailing content and hover-revealed actions. `<span>`, or the child with `asChild`. |
|
|
458
469
|
| `AccountMenu` | Dropdown root. `badge` adds the unread dot (`AccountBadge`) in a `relative` wrapper. |
|
|
459
|
-
| `AccountTrigger` | Rail-tile-sized avatar button. `userName` (default `title` and `aria-label`, and the initials), `imageUrl`, plus `Button` props; native `name` goes to the button. |
|
|
470
|
+
| `AccountTrigger` | Rail-tile-sized avatar button. `userName` (default `title` and `aria-label`, and the initials), `imageUrl`, plus `Button` props except `asChild` and `shape` (it keeps the rail tile `rounded-lg`); native `name` goes to the button. |
|
|
460
471
|
| `AccountAvatar` | Avatar image or initials. `userName`, `imageUrl`. |
|
|
461
472
|
| `AccountContent` | Menu content. Defaults `align="center"`, `side="right"`, `sideOffset={8}`, `w-56`. |
|
|
462
473
|
| `AccountLabel` | Identity label: avatar, `userName`, optional `email`, `stacked`. |
|
|
@@ -1196,9 +1207,10 @@ Generic pieces lifted from an app. Components that use `m` need the app's `LazyM
|
|
|
1196
1207
|
|
|
1197
1208
|
```tsx
|
|
1198
1209
|
import {
|
|
1199
|
-
ErrorBoundary, ListEnter, useFirstPaintGate, CountPop, countLabel, UnreadDot,
|
|
1210
|
+
ErrorBoundary, ListEnter, useFirstPaintGate, CountPop, countLabel, UnreadDot, RollingNumber,
|
|
1200
1211
|
MarqueeOnHover, RelativeDateTime, ImageLightbox, ImageGalleryPreview, ScreenshotPreview, VideoPreview,
|
|
1201
|
-
useMediaQuery, useDocumentVisible, useIdleCallback, useSingleFlight, usePersistedScrollParent,
|
|
1212
|
+
useMediaQuery, useDocumentVisible, useDocumentAppearance, useIdleCallback, useSingleFlight, usePersistedScrollParent,
|
|
1213
|
+
useCountUpDisplay, parseCountable,
|
|
1202
1214
|
convexErrorMessage, userFacingErrorMessage, useHeldResult,
|
|
1203
1215
|
} from "@vvedantb/ui";
|
|
1204
1216
|
```
|
|
@@ -1206,12 +1218,16 @@ import {
|
|
|
1206
1218
|
- `ErrorBoundary` (`fallback` node or `(retry) => node`, `onError`): contains a failure to its subtree; `retry` clears the error.
|
|
1207
1219
|
- `ListEnter` (`index`, `fast`, `firstPaint`, `staggerStep`, `staggerMax`, `slide`, `as` `div | tr | li`) and `useFirstPaintGate()`: first-paint fade and 8px slide, staggered with `staggerDelay`. Pass `firstPaint={gate.current}` so recycled rows do not restagger.
|
|
1208
1220
|
- `CountPop` (`label`, `className`, `children`): pop-in and shrink-out for a live count, re-popping on each change. `countLabel(n)` gives `null`, the number or `"99+"`. `UnreadDot` (`show`): a `bg-primary` dot on `CountPop`.
|
|
1221
|
+
- `RollingNumber` (`value`, `className`): shows an already formatted string ("1m 05s", "42%", "$1.20") and rolls each digit to its new value; other characters swap at once. Columns are keyed from the right, so "9s" → "10s" mounts only the new column. The full value stays in an `sr-only` span. For numbers that tick while the user watches; `CountPop` is for a badge that changes while the user looks elsewhere.
|
|
1209
1222
|
- `MarqueeOnHover`: a single-line label that scrolls its hidden tail into view on hover (or on hover of a `group` ancestor). Import `@vvedantb/ui/tokens/css/marquee.css`; `--mq-delay` sets the dwell.
|
|
1210
1223
|
- `RelativeDateTime` (`at`, `emptyLabel` default "—", `exactFormat` default "DD/MM/YYYY HH:mm"): "15m" / "2d" with the exact time in a tooltip.
|
|
1211
1224
|
- `ImageLightbox` (`images`, `index` or `null`, `onIndexChange`, `onClose`): fullscreen viewer with arrow keys, buttons and swipe. `ImageGalleryPreview` (`images`): up to a 2×2 grid with a "+N" tile. `ScreenshotPreview` (`url`, `alt`) and `VideoPreview` (`url`).
|
|
1212
|
-
- Hooks: `useMediaQuery(query)`, `useDocumentVisible()`, `useIdleCallback(ms, fn)` (trailing debounce; `createIdleCallback` is the React-free core), `useSingleFlight(fn)` (one call at a time, latest args run next), `usePersistedScrollParent(storageKey)` (restores a container's `scrollTop` from `sessionStorage`).
|
|
1225
|
+
- Hooks: `useMediaQuery(query)`, `useDocumentVisible()`, `useDocumentAppearance()` (`"light" | "dark"` from `<html>`'s `dark` class or `data-theme`; `readDocumentAppearance` / `subscribeDocumentAppearance` are the React-free core), `useIdleCallback(ms, fn)` (trailing debounce; `createIdleCallback` is the React-free core), `useSingleFlight(fn)` (one call at a time, latest args run next), `usePersistedScrollParent(storageKey)` (restores a container's `scrollTop` from `sessionStorage`).
|
|
1226
|
+
- `useCountUpDisplay(value)`: counts a number, or a label with one number ("45%", "12h", "$1.20"), from the last value to the new one over `tokenTransition.base`, keeping the prefix, suffix and decimal places. Where `RollingNumber` rolls each digit, this counts through the numbers between. The first value counts up from 0, and the same amount again does not replay. Labels it cannot count ("1h 12m", "—") show as they are, and so does every value while `PageMotionProvider` is `disabled`. `parseCountable(value)` is the parser: `{ amount, prefix, suffix, decimals }` or `null`.
|
|
1213
1227
|
- Convex: `convexErrorMessage(error, fallback)` reads `ConvexError` data first; `userFacingErrorMessage(error, fallback)` strips the client envelope and stack. `useHeldResult(result, key)` keeps the last result while a query is skipped (`key` `null`). None of these import `convex`.
|
|
1214
1228
|
|
|
1229
|
+
Fixture for `RollingNumber`, `SendQueueIcon`, `PromptInputSubmit` `queued`, the `ActivityTasks` clock and `useCountUpDisplay`: `examples/motion-primitives/`.
|
|
1230
|
+
|
|
1215
1231
|
## Chrome extensions (0.28.0)
|
|
1216
1232
|
|
|
1217
1233
|
Extension pages (popup, side panel, options, welcome) run under the MV3 extension-page CSP, which blocks remote font stylesheets, and should not load `@vvedantb/server`. Three pieces cover them. Web apps change nothing unless they opt in.
|
|
@@ -1349,7 +1365,7 @@ const config = {
|
|
|
1349
1365
|
} satisfies UiConfig;
|
|
1350
1366
|
```
|
|
1351
1367
|
|
|
1352
|
-
- `Button`: `classes.base`, `classes.variant.<variant>`, `classes.size.<size
|
|
1368
|
+
- `Button`: `classes.base`, `classes.variant.<variant>`, `classes.size.<size>`, `classes.shape.<pill|rounded>` (0.31.0). New size `xs` (`h-7 px-2 text-xs hit-target`, 14px icons). Since 0.31.0 the radius is not in `base` or the size strings: `classes.shape` sets it, one string per shape for every size (built in: `pill` `rounded-full`; `rounded` `rounded-lg`, or `rounded-md` on `xs` and `icon-xs`).
|
|
1353
1369
|
- `Badge`: `classes.base`, `classes.variant.<variant>`.
|
|
1354
1370
|
- `Input`, `Textarea`: `classes.base`.
|
|
1355
1371
|
- `Checkbox`: `classes.root`, `classes.indicator`, `icon`. New prop `icon`: the indicator content (default a 14px check). The `CheckboxProps` type is exported.
|
|
@@ -1497,6 +1513,7 @@ const config = {
|
|
|
1497
1513
|
Outside a `StickToBottom` (for example a page outline) use `JumpRail` with `items` (`{ id, title, preview?, level? }`), a `scrollRef` or `getScrollElement`, and `targetAttribute="data-toc-id"` or `resolveTarget`. `onReveal` runs when a target is not in the DOM; `onBeforeScroll` runs just before it. `onSelect(id)` runs after a click or Enter/Space pick, once the scroll has started or `onReveal` has run, so an app can move focus to the target for keyboard users; `ChatJumpRail` accepts it too. `observeKey` re-observes when the rendered window changes. The geometry helpers (`resolveJumpRailTopPercent`, `resolveJumpRailIndexFromPointer`, `resolveJumpRailHeightStyle`, `resolveJumpRailTickWidthClass`, `resolveJumpRailTickLevelStyle`) are exported.
|
|
1498
1514
|
- `Message`: `classes.root`, `classes.user`, `classes.assistant` (the role classes of `Message`), `classes.content` (all `MessageContent` classes), `classes.branchSelector`, `classes.branchIcon` (a class in place of the arrows' `size={14}`), and `lazyActionTooltips`: a `MessageAction` mounts its tooltip on first hover or focus (focus opens it), not with the button.
|
|
1499
1515
|
- `PromptInput`: `classes.group` (the input group, default `overflow-hidden`), `classes.groupIdle` / `classes.groupDragOver` (added while no drag / an accepted drag is over the form), `classes.groupDefault` (added when no `containerClassName` is given), `classes.textarea`, `classes.footer`, `classes.submit`, `classes.tabItem`; `submitIcons.<send|submitted|streaming|error>`, `submitCrossfade` (cross-fade the submit icon on status change) and `menuTriggerAsChild` (`PromptInputActionMenuTrigger` becomes the Radix trigger instead of a button inside it). New `PromptInput` props: `dataDrop` (`{ type, onDrop }`: a non-file drag of that MIME type counts as a drag over the form and `onDrop` gets its string payload) and `inputGroupClassName` (same as `containerClassName`). The attachments context adds `replace(id, file)`; `accept` now matches extension patterns such as `.html` by file name; `useOptionalPromptInputController` is exported.
|
|
1516
|
+
- `PromptInputSubmit`: new prop `queued` (0.30.0). Set it when a send would join a queue (the agent is still working). The send icon becomes `SendQueueIcon`, an up arrow that morphs into a stack of bars while `queued` is `true`. Leave it unset to keep `submitIcons.send`. `SendQueueIcon` (`queued`, `className` default `size-4`) is exported for other send buttons. It uses `m`, so `queued` needs the app's `LazyMotion`.
|
|
1500
1517
|
- `PromptInputSettings` (new): an overflow menu with a "Model" submenu (one radio list, or a submenu per provider). Entry: `providerLabel(provider)` (default the id), `providerIcon(provider)` (default none), `classes.trigger`.
|
|
1501
1518
|
- `TraitsPanel` (new, with `isUltrathinkPrompt`, `applyUltrathinkPrefix`): reasoning, speed, context and thinking segments for a model picker; inside a `PromptInputProvider` it offers "Ultrathink" by editing the prompt. Entry: `classes.label`, `classes.empty`, `classes.option`, `classes.optionSelected`, `classes.optionIdle`, `classes.note`.
|
|
1502
1519
|
- `PromptInputSpeech`, `useSpeechRecognition`, `getSpeechRecognition` (new): Web Speech API dictation; the button renders nothing without the API.
|
|
@@ -1518,7 +1535,7 @@ New dependency (installed with the package): `katex`. `streamdown` and the `@str
|
|
|
1518
1535
|
|
|
1519
1536
|
AI surfaces that came from an app, exported from the root. Fills use `surface-secondary` (`bg-surface-secondary`, `from-surface-secondary/40`), which is the app's `--muted` under `compat-shadcn-rgb.css` and the secondary surface under `tokens.css`. They also use the shadcn colour names `bg-secondary`, `bg-popover` and `bg-card` and a few app utilities (`motion-press`, `smooth-shadow-*`, `scrollbar`, `scrollbar-none`, `text-2xs`, `text-3xs`), so they look as intended with `compat-shadcn-rgb.css` and an app that defines those utilities. They merge a caller `className` with `UiConfig.cn` when set.
|
|
1520
1537
|
|
|
1521
|
-
- `ActivityTasks` (`steps`, `isStreaming`, `name`, `startedAt`, `duration`, `layout` `folded | inline`, `onOpenFile`, `onRequestFullDetail`): an agent turn as a timeline. Tool runs fold behind one summary line, reasoning and mid-turn text render as Markdown, subtasks nest their steps, long timelines cap behind "Show N more". `OpenSubagentContext` gives subtask rows a "View" link.
|
|
1538
|
+
- `ActivityTasks` (`steps`, `isStreaming`, `name`, `startedAt`, `duration`, `layout` `folded | inline`, `onOpenFile`, `onRequestFullDetail`): an agent turn as a timeline. Tool runs fold behind one summary line, reasoning and mid-turn text render as Markdown, subtasks nest their steps, long timelines cap behind "Show N more". While `isStreaming` and before the first row, a header shows the time since `startedAt` as a `RollingNumber` clock. `OpenSubagentContext` gives subtask rows a "View" link.
|
|
1522
1539
|
- The step model: `ActivityStep`, `ActivityTodoItem`, `ActivityQuestion`, `ActivityQuestionOption`, `ActivityStepOutput`, `ActivityStepEdit`, `activityStepHasRichDetail`, `activityStepConfig` (glyph and fallback label per step type), `ActivityThinkingIcon`, `useActivitySpinnerVerb`, `getRandomActivityVerb`, `useActivityElapsedSeconds`, `formatActivityElapsed`, `useStreamingDisclosure`.
|
|
1523
1540
|
- Tool-call details show files with `FileChip` and output in a `CodeBlock` (`plain`) with its copy button, so the `CodeBlock` entry (see "App config: AI chat") styles those frames too.
|
|
1524
1541
|
- `ActivityQuestionCard` (`questions`, `answers`): the read-only record of a multiple-choice question and its answer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React 19 UI for web apps: one main JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), a client entry without @vvedantb/server, convex or Clerk for Chrome extensions, design-token CSS, shared Tailwind v4 CSS, a Tailwind preset and packaged Instrument Sans fonts.",
|
|
6
6
|
"type": "module",
|
|
@@ -238,19 +238,23 @@ export function AccountAvatar({ imageUrl, userName, className }: SidebarAccountA
|
|
|
238
238
|
);
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
export type SidebarAccountTriggerProps = Omit<ButtonProps, "asChild"> & {
|
|
241
|
+
export type SidebarAccountTriggerProps = Omit<ButtonProps, "asChild" | "shape"> & {
|
|
242
242
|
/** Default `title` and `aria-label`, and the avatar initials. Native `name` goes to the button. */
|
|
243
243
|
userName: string;
|
|
244
244
|
imageUrl?: string;
|
|
245
245
|
};
|
|
246
246
|
|
|
247
|
-
/**
|
|
247
|
+
/**
|
|
248
|
+
* Rail-tile-sized avatar button that opens the menu. Children replace the avatar.
|
|
249
|
+
* A rail tile (`rounded-lg`), so `shape="rounded"`; change the radius with `className`.
|
|
250
|
+
*/
|
|
248
251
|
export function AccountTrigger({ userName, imageUrl, className, children, ...props }: SidebarAccountTriggerProps) {
|
|
249
252
|
return (
|
|
250
253
|
<DropdownMenuTrigger asChild>
|
|
251
254
|
<Button
|
|
252
255
|
type="button"
|
|
253
256
|
variant="ghost"
|
|
257
|
+
shape="rounded"
|
|
254
258
|
title={userName}
|
|
255
259
|
aria-label={`Account menu for ${userName}`}
|
|
256
260
|
{...props}
|
|
@@ -110,14 +110,14 @@ function MobileHeader({ className, onKeyDown, ...props }: SidebarMobileHeaderPro
|
|
|
110
110
|
|
|
111
111
|
const defaultMobileMenuIcon = () => <IconMenu2 size={20} />;
|
|
112
112
|
|
|
113
|
-
export type SidebarMobileMenuButtonProps = Omit<ButtonProps, "children"> & {
|
|
113
|
+
export type SidebarMobileMenuButtonProps = Omit<ButtonProps, "children" | "shape"> & {
|
|
114
114
|
/** `aria-label`. Default `"Open navigation menu"`. */
|
|
115
115
|
label?: string;
|
|
116
116
|
/** Icon for the drawer state. Default a 20px menu icon. */
|
|
117
117
|
icon?: (open: boolean) => ReactNode;
|
|
118
118
|
};
|
|
119
119
|
|
|
120
|
-
/** Opens the drawer. */
|
|
120
|
+
/** Opens the drawer. `shape="rounded"` (`rounded-lg`); change the radius with `className`. */
|
|
121
121
|
function MobileMenuButton({
|
|
122
122
|
label = "Open navigation menu",
|
|
123
123
|
icon = defaultMobileMenuIcon,
|
|
@@ -131,6 +131,7 @@ function MobileMenuButton({
|
|
|
131
131
|
type="button"
|
|
132
132
|
variant="ghost"
|
|
133
133
|
size="icon"
|
|
134
|
+
shape="rounded"
|
|
134
135
|
{...props}
|
|
135
136
|
onClick={(event) => {
|
|
136
137
|
onClick?.(event);
|
|
@@ -120,12 +120,15 @@ export function PanelActions({ className, ...props }: SidebarPanelActionsProps)
|
|
|
120
120
|
return <div {...props} className={cn("flex items-center justify-end gap-0.5", className)} />;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
export type SidebarPanelCloseProps = ButtonProps & {
|
|
123
|
+
export type SidebarPanelCloseProps = Omit<ButtonProps, "shape"> & {
|
|
124
124
|
/** `aria-label`. Default `"Close navigation"`. */
|
|
125
125
|
label?: string;
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Ghost close button. Without `onClick` it closes the `Sidebar.Root` drawer.
|
|
130
|
+
* `shape="rounded"` (`rounded-lg`); change the radius with `className`.
|
|
131
|
+
*/
|
|
129
132
|
export function PanelClose({ label = "Close navigation", onClick, className, children, ...props }: SidebarPanelCloseProps) {
|
|
130
133
|
const sidebar = useOptionalSidebar();
|
|
131
134
|
return (
|
|
@@ -133,6 +136,7 @@ export function PanelClose({ label = "Close navigation", onClick, className, chi
|
|
|
133
136
|
type="button"
|
|
134
137
|
variant="ghost"
|
|
135
138
|
size="icon-sm"
|
|
139
|
+
shape="rounded"
|
|
136
140
|
{...props}
|
|
137
141
|
onClick={onClick ?? (() => sidebar?.onMobileOpenChange(false))}
|
|
138
142
|
aria-label={label}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`vibot Queue widget DOM > renders the closed widget 1`] = `"<div class="w-full overflow-hidden" style="height: auto; opacity: 1;"><button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">3</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out"><path d="M6 9l6 6l6 -6"></path></svg></button></div>"`;
|
|
4
4
|
|
|
5
|
-
exports[`vibot Queue widget DOM > renders the editing state, then saves the trimmed text 1`] = `"<div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex flex-col gap-2"><textarea class="flex w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2.5 sm:text-sm placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-hidden focus-visible:border-focus-border focus-visible:ring-1 focus-visible:ring-focus-ring min-h-[60px] text-sm">Draft</textarea><div class="flex justify-end gap-2"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground
|
|
5
|
+
exports[`vibot Queue widget DOM > renders the editing state, then saves the trimmed text 1`] = `"<div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex flex-col gap-2"><textarea class="flex w-full max-sm:min-w-0 rounded-field border border-border bg-field-background px-3.5 py-2.5 sm:text-sm placeholder:text-field-placeholder disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-hidden focus-visible:border-focus-border focus-visible:ring-1 focus-visible:ring-focus-ring min-h-[60px] text-sm">Draft</textarea><div class="flex justify-end gap-2"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground text-xs max-sm:hit-target rounded-full h-7 px-2"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-x"><path d="M18 6l-12 12"></path><path d="M6 6l12 12"></path></svg><span class="ml-1 text-xs">Cancel</span></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:-translate-y-0.5 hover:opacity-90 text-xs max-sm:hit-target rounded-full h-7 px-2"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-check"><path d="M5 12l5 5l10 -10"></path></svg><span class="ml-1 text-xs">Save</span></button></div></div></div>"`;
|
|
6
6
|
|
|
7
|
-
exports[`vibot Queue widget DOM > renders the editing state, then saves the trimmed text 2`] = `"<div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Draft</p><div class="flex items-center gap-1.5 shrink-0"></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground
|
|
7
|
+
exports[`vibot Queue widget DOM > renders the editing state, then saves the trimmed text 2`] = `"<div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Draft</p><div class="flex items-center gap-1.5 shrink-0"></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground text-xs max-sm:hit-target rounded-full h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary text-xs max-sm:hit-target rounded-full h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div>"`;
|
|
8
8
|
|
|
9
9
|
exports[`vibot Queue widget DOM > renders the header with a custom class 1`] = `"<button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group x"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">1</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out"><path d="M6 9l6 6l6 -6"></path></svg></button>"`;
|
|
10
10
|
|
|
11
|
-
exports[`vibot Queue widget DOM > renders the open widget 1`] = `"<div class="w-full overflow-hidden mb-2" style="height: auto; opacity: 1;"><button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">3</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out rotate-180"><path d="M6 9l6 6l6 -6"></path></svg></button><div class="overflow-hidden" style="height: auto; opacity: 1;"><div class="bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none rounded-b-2xl"><div dir="ltr" data-slot="scroll-area" class="relative max-h-[240px]" style="position: relative; --radix-scroll-area-corner-width: 0px; --radix-scroll-area-corner-height: 0px;"><style>[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}</style><div data-radix-scroll-area-viewport="" data-slot="scroll-area-viewport" class="size-full rounded-[inherit] outline-hidden" style="overflow-x: hidden; overflow-y: scroll;"><div style="min-width: 100%; display: table;"><div class="p-2 space-y-1.5"><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><img alt="Ada" class="size-5 rounded-full shrink-0" src="https://example.com/ada.png"><p class="text-sm text-foreground/90 flex-1 truncate">Fix the build</p><div class="flex items-center gap-1.5 shrink-0"><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">claude-sonnet</div><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">Terse</div></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary extra"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Plain</p><div class="flex items-center gap-1.5 shrink-0"><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">Bob</div></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Bare</p><div class="flex items-center gap-1.5 shrink-0"></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary rounded-lg text-xs max-sm:hit-target h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div></div></div></div></div></div></div></div>"`;
|
|
11
|
+
exports[`vibot Queue widget DOM > renders the open widget 1`] = `"<div class="w-full overflow-hidden mb-2" style="height: auto; opacity: 1;"><button type="button" class="flex items-center justify-between w-full px-4 py-2.5 bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-tertiary rounded-t-2xl group"><div class="flex items-center gap-2"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-stack-2 text-muted-foreground"><path d="M12 4l-8 4l8 4l8 -4l-8 -4"></path><path d="M4 12l8 4l8 -4"></path><path d="M4 16l8 4l8 -4"></path></svg><span class="text-sm font-medium text-foreground">Queued Messages</span><div class="inline-flex items-center gap-1 rounded-full border px-2.5 py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-default text-default-foreground text-xs">3</div></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-chevron-down text-muted-foreground transition-transform duration-200 ease-out rotate-180"><path d="M6 9l6 6l6 -6"></path></svg></button><div class="overflow-hidden" style="height: auto; opacity: 1;"><div class="bg-surface-secondary text-foreground border-0 shadow-none backdrop-blur-none rounded-b-2xl"><div dir="ltr" data-slot="scroll-area" class="relative max-h-[240px]" style="position: relative; --radix-scroll-area-corner-width: 0px; --radix-scroll-area-corner-height: 0px;"><style>[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}</style><div data-radix-scroll-area-viewport="" data-slot="scroll-area-viewport" class="size-full rounded-[inherit] outline-hidden" style="overflow-x: hidden; overflow-y: scroll;"><div style="min-width: 100%; display: table;"><div class="p-2 space-y-1.5"><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><img alt="Ada" class="size-5 rounded-full shrink-0" src="https://example.com/ada.png"><p class="text-sm text-foreground/90 flex-1 truncate">Fix the build</p><div class="flex items-center gap-1.5 shrink-0"><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">claude-sonnet</div><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">Terse</div></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground text-xs max-sm:hit-target rounded-full h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary text-xs max-sm:hit-target rounded-full h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary extra"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Plain</p><div class="flex items-center gap-1.5 shrink-0"><div class="inline-flex items-center gap-1 rounded-full border py-1 font-medium tracking-normal transition-colors focus:outline-none focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:ring-offset-background border-separator bg-transparent text-muted-foreground text-xs h-5 px-1.5">Bob</div></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground text-xs max-sm:hit-target rounded-full h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary text-xs max-sm:hit-target rounded-full h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div><div class="group relative p-2.5 rounded-lg transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary"><div class="flex items-center gap-2"><p class="text-sm text-foreground/90 flex-1 truncate">Bare</p><div class="flex items-center gap-1.5 shrink-0"></div><div class="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 text-muted-foreground hover:bg-surface-tertiary hover:text-foreground text-xs max-sm:hit-target rounded-full h-6 w-6 p-0"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-edit"><path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"></path><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415"></path><path d="M16 5l3 3"></path></svg></button><button class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 hover:bg-surface-tertiary text-xs max-sm:hit-target rounded-full h-6 w-6 p-0 text-danger hover:text-danger"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-trash"><path d="M4 7l16 0"></path><path d="M10 11l0 6"></path><path d="M14 11l0 6"></path><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path></svg></button></div></div></div></div></div></div></div></div></div></div>"`;
|
|
@@ -289,6 +289,8 @@ export const MessageBranchSelector = ({
|
|
|
289
289
|
);
|
|
290
290
|
};
|
|
291
291
|
export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
|
|
292
|
+
// Prev and next sit in a `ButtonGroup` with `rounded-md` inner corners, so they
|
|
293
|
+
// default to `shape="rounded"`; a pill would pair a round outer end with them.
|
|
292
294
|
export const MessageBranchPrevious = ({
|
|
293
295
|
children,
|
|
294
296
|
...props
|
|
@@ -301,6 +303,7 @@ export const MessageBranchPrevious = ({
|
|
|
301
303
|
disabled={totalBranches <= 1}
|
|
302
304
|
onClick={goToPrevious}
|
|
303
305
|
size="icon-sm"
|
|
306
|
+
shape="rounded"
|
|
304
307
|
type="button"
|
|
305
308
|
variant="ghost"
|
|
306
309
|
{...props}
|
|
@@ -327,6 +330,7 @@ export const MessageBranchNext = ({
|
|
|
327
330
|
disabled={totalBranches <= 1}
|
|
328
331
|
onClick={goToNext}
|
|
329
332
|
size="icon-sm"
|
|
333
|
+
shape="rounded"
|
|
330
334
|
type="button"
|
|
331
335
|
variant="ghost"
|
|
332
336
|
{...props}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export { useMediaQuery } from "./use-media-query";
|
|
2
2
|
export { useDocumentVisible } from "./use-document-visible";
|
|
3
|
+
export {
|
|
4
|
+
readDocumentAppearance,
|
|
5
|
+
subscribeDocumentAppearance,
|
|
6
|
+
useDocumentAppearance,
|
|
7
|
+
type DocumentAppearance,
|
|
8
|
+
} from "./use-document-appearance";
|
|
3
9
|
export { createIdleCallback, useIdleCallback } from "./use-idle-callback";
|
|
4
10
|
export { useSingleFlight } from "./use-single-flight";
|
|
5
11
|
export { usePersistedScrollParent } from "./use-persisted-scroll-parent";
|
|
12
|
+
export { parseCountable, useCountUpDisplay } from "./use-count-up-display";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { animate } from "motion/react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useDisablePageMotion } from "../../app/src/page-motion/PageMotionProvider";
|
|
4
|
+
import { motionEase, tokenTransition } from "../motion/presets";
|
|
5
|
+
|
|
6
|
+
/** Suffixes we will count; compound durations like "1h 12m" stay static. */
|
|
7
|
+
const COUNTABLE_SUFFIX = /^(?:%|h|k|m|s|ms)?$/i;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Splits a label into the number to count and the text around it. A finite
|
|
11
|
+
* number passes through; a string needs one number with an optional prefix
|
|
12
|
+
* ("$", "~") and a suffix of `%`, `h`, `k`, `m`, `s` or `ms`. Anything else
|
|
13
|
+
* (`—`, "1h 12m", "3 of 5") is `null`.
|
|
14
|
+
*/
|
|
15
|
+
export function parseCountable(value: string | number): {
|
|
16
|
+
amount: number;
|
|
17
|
+
prefix: string;
|
|
18
|
+
suffix: string;
|
|
19
|
+
decimals: number;
|
|
20
|
+
} | null {
|
|
21
|
+
if (typeof value === "number") {
|
|
22
|
+
if (!Number.isFinite(value)) return null;
|
|
23
|
+
return { amount: value, prefix: "", suffix: "", decimals: 0 };
|
|
24
|
+
}
|
|
25
|
+
const match = value.trim().match(/^(\D*?)(-?\d+(?:\.\d+)?)(\D*)$/);
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
const prefix = match[1] ?? "";
|
|
28
|
+
const raw = match[2] ?? "";
|
|
29
|
+
const suffix = (match[3] ?? "").trim();
|
|
30
|
+
if (!COUNTABLE_SUFFIX.test(suffix)) return null;
|
|
31
|
+
const amount = Number(raw);
|
|
32
|
+
if (!Number.isFinite(amount)) return null;
|
|
33
|
+
const decimals = raw.includes(".") ? (raw.split(".")[1]?.length ?? 0) : 0;
|
|
34
|
+
return { amount, prefix, suffix, decimals };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Counts from the last seen number to `value` on `--motion-base`
|
|
39
|
+
* (`tokenTransition.base`), keeping the prefix, suffix and decimal places. The
|
|
40
|
+
* first value counts up from 0. The same amount again (a refetch) is a no-op,
|
|
41
|
+
* so the number does not replay on every write. Labels `parseCountable` rejects
|
|
42
|
+
* (`—`, "1h 12m") render as they are, and so does every value while
|
|
43
|
+
* `PageMotionProvider` is `disabled`.
|
|
44
|
+
*/
|
|
45
|
+
export function useCountUpDisplay(value: string | number): string | number {
|
|
46
|
+
const parsed = parseCountable(value);
|
|
47
|
+
const [display, setDisplay] = useState(value);
|
|
48
|
+
const lastAmount = useRef<number | null>(null);
|
|
49
|
+
const skipMotion = useDisablePageMotion();
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (skipMotion) {
|
|
53
|
+
lastAmount.current = parsed?.amount ?? null;
|
|
54
|
+
setDisplay(value);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
lastAmount.current = null;
|
|
59
|
+
setDisplay(value);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (lastAmount.current === parsed.amount) {
|
|
63
|
+
setDisplay(value);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const from = lastAmount.current ?? 0;
|
|
67
|
+
lastAmount.current = parsed.amount;
|
|
68
|
+
const controls = animate(from, parsed.amount, {
|
|
69
|
+
duration: tokenTransition.base.duration,
|
|
70
|
+
ease: motionEase,
|
|
71
|
+
onUpdate: (latest) => {
|
|
72
|
+
const rounded = parsed.decimals > 0 ? latest.toFixed(parsed.decimals) : String(Math.round(latest));
|
|
73
|
+
setDisplay(`${parsed.prefix}${rounded}${parsed.suffix}`);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return () => controls.stop();
|
|
77
|
+
}, [parsed?.amount, parsed?.prefix, parsed?.suffix, parsed?.decimals, skipMotion, value]);
|
|
78
|
+
|
|
79
|
+
return display;
|
|
80
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
|
|
3
|
+
/** Light or dark, as the page currently draws itself. Neutral is dark-family. */
|
|
4
|
+
export type DocumentAppearance = "light" | "dark";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reads the appearance from `<html>`: the `dark` class (Tailwind's convention,
|
|
8
|
+
* set before paint by the app's boot script) or `data-theme="dark"`.
|
|
9
|
+
*
|
|
10
|
+
* Use this rather than a theme context when code can run outside the app's
|
|
11
|
+
* theme provider (a signed-out marketing page, a canvas effect): the class is
|
|
12
|
+
* the one source every page shares.
|
|
13
|
+
*/
|
|
14
|
+
export function readDocumentAppearance(): DocumentAppearance {
|
|
15
|
+
const root = document.documentElement;
|
|
16
|
+
return root.classList.contains("dark") ||
|
|
17
|
+
root.getAttribute("data-theme") === "dark"
|
|
18
|
+
? "dark"
|
|
19
|
+
: "light";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Calls `onChange` whenever `<html>`'s class or `data-theme` changes. */
|
|
23
|
+
export function subscribeDocumentAppearance(onChange: () => void): () => void {
|
|
24
|
+
const observer = new MutationObserver(onChange);
|
|
25
|
+
observer.observe(document.documentElement, {
|
|
26
|
+
attributes: true,
|
|
27
|
+
attributeFilter: ["class", "data-theme"],
|
|
28
|
+
});
|
|
29
|
+
return () => observer.disconnect();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readServerAppearance(): DocumentAppearance {
|
|
33
|
+
return "light";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The page's appearance, re-rendering when it changes. Server snapshot is
|
|
38
|
+
* `"light"`, matching a page with no `dark` class.
|
|
39
|
+
*/
|
|
40
|
+
export function useDocumentAppearance(): DocumentAppearance {
|
|
41
|
+
return useSyncExternalStore(
|
|
42
|
+
subscribeDocumentAppearance,
|
|
43
|
+
readDocumentAppearance,
|
|
44
|
+
readServerAppearance,
|
|
45
|
+
);
|
|
46
|
+
}
|
package/src/markdown/mermaid.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { MermaidConfig } from "@streamdown/mermaid";
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
3
|
import type { DiagramPlugin } from "streamdown";
|
|
4
|
+
import {
|
|
5
|
+
readDocumentAppearance,
|
|
6
|
+
subscribeDocumentAppearance,
|
|
7
|
+
} from "../hooks/use-document-appearance";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Mermaid for markdown, loaded on first use. `@streamdown/mermaid` imports
|
|
@@ -102,18 +106,9 @@ function subscribe(key: string, notify: () => void): () => void {
|
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
/**
|
|
109
|
+
/** Mermaid's theme for the page's current appearance (neutral is dark-family). */
|
|
106
110
|
function readTheme(): MermaidTheme {
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function subscribeTheme(notify: () => void): () => void {
|
|
111
|
-
const observer = new MutationObserver(notify);
|
|
112
|
-
observer.observe(document.documentElement, {
|
|
113
|
-
attributes: true,
|
|
114
|
-
attributeFilter: ["class"],
|
|
115
|
-
});
|
|
116
|
-
return () => observer.disconnect();
|
|
111
|
+
return readDocumentAppearance() === "dark" ? "dark" : "default";
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
/**
|
|
@@ -145,7 +140,7 @@ export function useMermaidRender(
|
|
|
145
140
|
code: string,
|
|
146
141
|
enabled: boolean,
|
|
147
142
|
): { theme: MermaidTheme; status: Entry["status"] } {
|
|
148
|
-
const theme = useSyncExternalStore(
|
|
143
|
+
const theme = useSyncExternalStore(subscribeDocumentAppearance, readTheme);
|
|
149
144
|
const key = enabled ? cacheKey(theme, code) : "";
|
|
150
145
|
const entry = useSyncExternalStore(
|
|
151
146
|
(notify) => (key === "" ? () => {} : subscribe(key, notify)),
|
package/src/ui/button.tsx
CHANGED
|
@@ -8,7 +8,7 @@ const ghostClass =
|
|
|
8
8
|
"text-muted-foreground hover:bg-surface-tertiary hover:text-foreground";
|
|
9
9
|
|
|
10
10
|
const buttonBaseClasses =
|
|
11
|
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap
|
|
11
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium tracking-normal transition-[transform,background-color,box-shadow,opacity,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 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.96] [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0";
|
|
12
12
|
|
|
13
13
|
const buttonVariantClasses = {
|
|
14
14
|
default:
|
|
@@ -26,29 +26,52 @@ const buttonVariantClasses = {
|
|
|
26
26
|
warning: `${ghostClass} bg-warning text-warning-foreground hover:bg-warning/90`,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
// No radius here: `shape` sets it.
|
|
29
30
|
const buttonSizeClasses = {
|
|
30
31
|
default: "h-10 px-4 py-2",
|
|
31
|
-
xs: "h-7
|
|
32
|
-
sm: "h-8
|
|
32
|
+
xs: "h-7 px-2 text-xs hit-target [&_svg:not([class*='size-'])]:size-3.5",
|
|
33
|
+
sm: "h-8 px-3 text-xs max-sm:hit-target",
|
|
33
34
|
lg: "h-11 px-6",
|
|
34
35
|
icon: "h-10 w-10",
|
|
35
|
-
"icon-sm": "h-8 w-8
|
|
36
|
-
"icon-xs": "h-7 w-7
|
|
36
|
+
"icon-sm": "h-8 w-8 max-sm:hit-target",
|
|
37
|
+
"icon-xs": "h-7 w-7 hit-target",
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
// `pill` (default) is fully round, so icon sizes are circles. `rounded` is
|
|
41
|
+
// the radius from before 0.31.0, for joined edges and field addons.
|
|
42
|
+
const buttonShapeClasses = {
|
|
43
|
+
pill: "rounded-full",
|
|
44
|
+
rounded: "rounded-lg",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ButtonVariant = keyof typeof buttonVariantClasses;
|
|
48
|
+
type ButtonSize = keyof typeof buttonSizeClasses;
|
|
49
|
+
type ButtonShape = keyof typeof buttonShapeClasses;
|
|
50
|
+
|
|
51
|
+
// `rounded` keeps the smaller radius the two 28px sizes had.
|
|
52
|
+
const roundedSmallSizes: ButtonSize[] = ["xs", "icon-xs"];
|
|
53
|
+
const roundedSmallClass = "rounded-md";
|
|
54
|
+
|
|
39
55
|
const buttonVariants = cva(buttonBaseClasses, {
|
|
40
56
|
variants: {
|
|
41
57
|
variant: buttonVariantClasses,
|
|
42
58
|
size: buttonSizeClasses,
|
|
59
|
+
shape: buttonShapeClasses,
|
|
43
60
|
},
|
|
61
|
+
compoundVariants: [{ shape: "rounded", size: roundedSmallSizes, class: roundedSmallClass }],
|
|
44
62
|
defaultVariants: {
|
|
45
63
|
variant: "default",
|
|
46
64
|
size: "default",
|
|
65
|
+
shape: "pill",
|
|
47
66
|
},
|
|
48
67
|
});
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
/** Built-in radius for the config path. Matches the cva variant and compound variant. */
|
|
70
|
+
function buttonShapeClass(shape: ButtonShape, size: ButtonSize): string {
|
|
71
|
+
return shape === "rounded" && roundedSmallSizes.includes(size)
|
|
72
|
+
? roundedSmallClass
|
|
73
|
+
: buttonShapeClasses[shape];
|
|
74
|
+
}
|
|
52
75
|
|
|
53
76
|
/** `UiConfig.Button`: each given class string replaces the built-in one. */
|
|
54
77
|
export interface ButtonConfig {
|
|
@@ -56,9 +79,12 @@ export interface ButtonConfig {
|
|
|
56
79
|
base?: string;
|
|
57
80
|
variant?: Partial<Record<ButtonVariant, string>>;
|
|
58
81
|
size?: Partial<Record<ButtonSize, string>>;
|
|
82
|
+
/** Radius per shape. Replaces it for every size, `xs` and `icon-xs` included. */
|
|
83
|
+
shape?: Partial<Record<ButtonShape, string>>;
|
|
59
84
|
};
|
|
60
85
|
}
|
|
61
86
|
|
|
87
|
+
/** `shape`: `"pill"` (default, `rounded-full`) or `"rounded"` (the radius from before 0.31.0). */
|
|
62
88
|
export interface ButtonProps
|
|
63
89
|
extends
|
|
64
90
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
@@ -74,7 +100,7 @@ export interface ButtonProps
|
|
|
74
100
|
|
|
75
101
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
76
102
|
(
|
|
77
|
-
{ className, variant, size, asChild = false, startContent, endContent, loading, children, ...props },
|
|
103
|
+
{ className, variant, size, shape, asChild = false, startContent, endContent, loading, children, ...props },
|
|
78
104
|
ref,
|
|
79
105
|
) => {
|
|
80
106
|
const Comp = asChild ? Slot : "button";
|
|
@@ -82,6 +108,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
82
108
|
const merge = useCn();
|
|
83
109
|
const v = variant ?? "default";
|
|
84
110
|
const s = size ?? "default";
|
|
111
|
+
const sh = shape ?? "pill";
|
|
85
112
|
return (
|
|
86
113
|
<Comp
|
|
87
114
|
className={
|
|
@@ -90,9 +117,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
90
117
|
classes.base ?? buttonBaseClasses,
|
|
91
118
|
classes.variant?.[v] ?? buttonVariantClasses[v],
|
|
92
119
|
classes.size?.[s] ?? buttonSizeClasses[s],
|
|
120
|
+
classes.shape?.[sh] ?? buttonShapeClass(sh, s),
|
|
93
121
|
className,
|
|
94
122
|
)
|
|
95
|
-
: merge(buttonVariants({ variant, size, className }))
|
|
123
|
+
: merge(buttonVariants({ variant, size, shape, className }))
|
|
96
124
|
}
|
|
97
125
|
ref={ref}
|
|
98
126
|
{...props}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** transitions.dev input clear with dissolve — see 13-input-clear-dissolve.md */
|
|
2
2
|
|
|
3
|
+
import { readDocumentAppearance } from "../hooks/use-document-appearance";
|
|
4
|
+
|
|
3
5
|
// Times come back in ms. CSS minifiers rewrite `1000ms` as `1s`, so a value
|
|
4
6
|
// in seconds is converted rather than read as 1.
|
|
5
7
|
export function readCssNumber(
|
|
@@ -53,13 +55,6 @@ export function readCssEase(root: HTMLElement, name: string): (t: number) => num
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
function isDarkTheme(root: HTMLElement): boolean {
|
|
57
|
-
return (
|
|
58
|
-
root.classList.contains("dark") ||
|
|
59
|
-
root.getAttribute("data-theme") === "dark"
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
function measureCanvas(): CanvasRenderingContext2D | null {
|
|
64
59
|
const canvas = document.createElement("canvas");
|
|
65
60
|
return canvas.getContext("2d");
|
|
@@ -93,7 +88,7 @@ function buildGlowBackground(
|
|
|
93
88
|
return "";
|
|
94
89
|
}
|
|
95
90
|
canvas.font = getComputedStyle(input).font;
|
|
96
|
-
const dark =
|
|
91
|
+
const dark = readDocumentAppearance() === "dark";
|
|
97
92
|
const rgb = dark ? "255,255,255" : "0,0,0";
|
|
98
93
|
const width = wrap.clientWidth || 280;
|
|
99
94
|
const padLeft = parseFloat(getComputedStyle(input).paddingLeft) || 12;
|
|
@@ -103,11 +103,12 @@ const inputGroupButtonVariants = cva("shrink-0 shadow-none", {
|
|
|
103
103
|
/** @deprecated Use `InputGroupButton`. Kept for existing consumers; removed in a later minor after they move. */
|
|
104
104
|
const InputGroupButtonAlt = React.forwardRef<
|
|
105
105
|
HTMLButtonElement,
|
|
106
|
-
ButtonProps & VariantProps<typeof inputGroupButtonVariants>
|
|
106
|
+
Omit<ButtonProps, "shape"> & VariantProps<typeof inputGroupButtonVariants>
|
|
107
107
|
>(({ className, size, variant = "ghost", ...props }, ref) => (
|
|
108
108
|
<Button
|
|
109
109
|
ref={ref}
|
|
110
110
|
variant={variant}
|
|
111
|
+
shape="rounded"
|
|
111
112
|
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
112
113
|
{...props}
|
|
113
114
|
/>
|
package/src/ui/input-group.tsx
CHANGED
|
@@ -131,13 +131,16 @@ const inputGroupButtonVariants = cva(inputGroupButtonBaseClasses, {
|
|
|
131
131
|
},
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
// `shape="rounded"` keeps the field-addon radius under Button's pill default.
|
|
135
|
+
// `shape` is not a prop: the size classes set `rounded-lg`, so radius changes
|
|
136
|
+
// go through `className` (or `UiConfig.InputGroup.classes.button`).
|
|
134
137
|
function InputGroupButton({
|
|
135
138
|
className,
|
|
136
139
|
type = "button",
|
|
137
140
|
variant = "ghost",
|
|
138
141
|
size = "xs",
|
|
139
142
|
...props
|
|
140
|
-
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
143
|
+
}: Omit<React.ComponentProps<typeof Button>, "size" | "shape"> &
|
|
141
144
|
VariantProps<typeof inputGroupButtonVariants>) {
|
|
142
145
|
const classes = useUiConfig("InputGroup")?.classes?.button;
|
|
143
146
|
const merge = useCn();
|
|
@@ -147,6 +150,7 @@ function InputGroupButton({
|
|
|
147
150
|
type={type}
|
|
148
151
|
data-size={size}
|
|
149
152
|
variant={variant}
|
|
153
|
+
shape="rounded"
|
|
150
154
|
className={
|
|
151
155
|
classes
|
|
152
156
|
? merge(
|