@vvedantb/ui 0.34.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +50 -6
  2. package/package.json +10 -2
  3. package/src/ai-elements/activity-shared.tsx +1 -1
  4. package/src/ai-elements/activity-tasks.tsx +5 -19
  5. package/src/ai-elements/attachments.tsx +2 -2
  6. package/src/ai-elements/code-block.tsx +3 -1
  7. package/src/ai-elements/conversation.tsx +10 -4
  8. package/src/ai-elements/prompt-input.tsx +5 -4
  9. package/src/client.ts +3 -0
  10. package/src/icons/animated/animated-bell-icon.tsx +68 -0
  11. package/src/icons/animated/animated-counter.tsx +45 -0
  12. package/src/icons/animated/animated-key-icon.tsx +79 -0
  13. package/src/icons/animated/animated-notification-icon.tsx +202 -0
  14. package/src/icons/animated/animated-progress.tsx +74 -0
  15. package/src/icons/animated/animated-status-dot.tsx +98 -0
  16. package/src/icons/animated/index.ts +22 -0
  17. package/src/icons/animated/morphing-menu-icon.tsx +71 -0
  18. package/src/icons/animated/morphing-mic-icon.tsx +52 -0
  19. package/src/icons/animated/pulsing-rings.tsx +50 -0
  20. package/src/index.ts +3 -0
  21. package/src/logos/chrome-icon.tsx +29 -0
  22. package/src/logos/claude-icon.tsx +13 -0
  23. package/src/logos/convex-icon.tsx +21 -0
  24. package/src/logos/cursor-icon.tsx +10 -0
  25. package/src/logos/dropbox-icon.tsx +14 -0
  26. package/src/logos/figma-icon.tsx +26 -0
  27. package/src/logos/github-icon.tsx +10 -0
  28. package/src/logos/gmail-icon.tsx +34 -0
  29. package/src/logos/google-drive-icon.tsx +33 -0
  30. package/src/logos/index.ts +19 -0
  31. package/src/logos/linear-icon.tsx +13 -0
  32. package/src/logos/logo-svg.tsx +32 -0
  33. package/src/logos/notion-icon.tsx +19 -0
  34. package/src/logos/onedrive-icon.tsx +165 -0
  35. package/src/logos/sharepoint-icon.tsx +222 -0
  36. package/src/logos/slack-icon.tsx +25 -0
  37. package/src/logos/vercel-icon.tsx +10 -0
  38. package/src/ui/_menu-parts.tsx +1 -1
  39. package/src/ui/button-group.tsx +1 -1
  40. package/src/ui/clear-input.tsx +2 -2
  41. package/src/ui/command.tsx +2 -2
  42. package/src/ui/context-menu.tsx +3 -3
  43. package/src/ui/dropdown-menu.tsx +1 -1
  44. package/src/ui/hover-card.tsx +3 -3
  45. package/src/ui/input-group.tsx +4 -4
  46. package/src/ui/popover.tsx +3 -5
  47. package/src/ui/search-input.tsx +1 -1
  48. package/src/ui/select.tsx +5 -5
  49. package/src/ui/sonner.tsx +3 -0
  50. package/src/ui/tooltip.tsx +3 -3
package/README.md CHANGED
@@ -7,6 +7,8 @@ Shared React 19 web UI. Source-exported: no build step.
7
7
  | `@vvedantb/ui` | The main JavaScript entry. Primitives (Radix-based), AI elements, audio (`playChime`, `useReplyChime`, `playTone`), `toast` and `SonnerToaster`, the composer (`InputArea`, deprecated 0.3 `Composer`), shell (`SidebarHeader`, `SidebarUpgradeCard`, `PillTabs`, `SessionsHoverCard`, and the app shell: `AppShell`, `AppSidebar`, `SidebarRail`, `RailTile`, `SidebarPanel`, `SidebarAccountMenu`, `SignOutDialog` and related helpers), app helpers (confirm skip, update watcher, settings chrome, `useStableAuth`, and from 0.8.0 `createThemeProvider`, `createPageTitleContext`, `PageMotionProvider` and `formatCompactRelativeTime`), marketing pieces from 0.9.0 (`createLandingKit` and the `Landing*` parts, `SiteFooter`, `PricingModal`), animated nav icons from 0.11.0 (`MemoryIcon`, `BoardIcon`, ... and `NavIconBase`; `SettingsIcon` `variant` from 0.12.0), artifact viewers from 0.13.0 (`ArtifactFrame`, `ArtifactList`, `ArtifactViewer`, ...), date pickers from 0.14.0 (`Calendar`, `DatePicker`, `DateTimePicker`, `MonthPicker`), `cn`, and the native tokens `tokens` and `Tokens`. See `app/README.md` in the repository for the app helpers. |
8
8
  | `@vvedantb/ui/client` | The root without `@vvedantb/server`, convex or Clerk, for Chrome extensions (0.28.0). Leaves out the artifact viewers, `ConvexClerkProvider`, `useStableAuth`, `createUseStableAuth`, `resetStableAuthSession` and `useEnsureUser`. See [Chrome extensions](#chrome-extensions-0280). |
9
9
  | `@vvedantb/ui/brand` | App icons (`Icon*`, `APP_ICONS`), `AppWordmark`, deprecated `AppMark`, raw SVG strings and `FAMILY_APPS`. Also in the root and `/client`. |
10
+ | `@vvedantb/ui/logos` | Third-party brand logos (`GitHubIcon`, `GoogleDriveIcon`, `SlackIcon`, `ClaudeIcon`, `VercelIcon`, ...) and `LogoProps` (0.36.0). Also in the root and `/client`. See [Third-party logos](#third-party-logos-0360). |
11
+ | `@vvedantb/ui/icons/animated` | Motion-driven animated SVG icons (`AnimatedNotificationIcon`, `MorphingMenuIcon`, `AnimatedBellIcon`, ...) (0.36.0). Not in the root or `/client`. Needs the optional `motion` peer. See [Animated SVG icons](#animated-svg-icons-0360). |
10
12
  | `@vvedantb/ui/tokens` | `tokens.css`: colour, radius, shadow, ease and font tokens for Tailwind v4. CSS. |
11
13
  | `@vvedantb/ui/tokens/tokens.css` | Same file as `@vvedantb/ui/tokens`. CSS. |
12
14
  | `@vvedantb/ui/tokens/css/*.css` | `index`, `base`, `dialog`, `modal`, `tabs`, `tabs-pills`, `utilities`, `scrollbar-thin`, `clear-input`, `motion`, `animations`, `nav-icons`, `compat-shadcn-rgb`, `border-beam` (opt-in, for `BorderBeam`), `markdown` (opt-in, for `Markdown`). CSS. `tokens.css` imports `scrollbar-thin`, `clear-input` and `motion`. |
@@ -292,7 +294,7 @@ import {
292
294
  - `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).
293
295
  - `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.
294
296
  - `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`.
295
- - `PopoverContent` `fitViewport`: grows from the trigger (`--radix-popper-transform-origin`), defaults `collisionPadding` to 8 (your value wins) and, on small screens, caps width to `100vw - 1.5rem` and height as above.
297
+ - `PopoverContent` `fitViewport`: grows from the trigger (`--radix-popper-transform-origin`) and, on small screens, caps width to `100vw - 1.5rem` and height as above.
296
298
  - `CommandDialog`: `title` and `description` add a visually hidden dialog header. `className` merges onto the dialog content. `shouldFilter` goes to `Command`. `listLayout` is `"pinned"` (default, fixed list height) or `"auto"` (the list sizes itself and items keep your styling).
297
299
  - `restoreModalScrollLock()`: clears body `pointer-events`, and, when no dialog or alert dialog is open, the body and root `overflow` and body `padding-right` left by a modal menu.
298
300
  - `CardHeader`: `flex flex-col space-y-1.5 p-6`.
@@ -1155,6 +1157,47 @@ Props (type `IconProps`; all optional):
1155
1157
 
1156
1158
  Other SVG props (`id`, `data-*`, handlers, `ref`) go to the svg. On group hover the svg scales to 1.15 (the first six icons, spring ease) or 1.12 (the rest). `NavIconBase` is the shared svg wrapper, for icons drawn in the same style. The stylesheet has no reduced-motion override.
1157
1159
 
1160
+ ## Third-party logos (0.36.0)
1161
+
1162
+ Full-colour vendor logos, separate from the family marks in `@vvedantb/ui/brand`. Import from `@vvedantb/ui/logos` (also in the root and `/client`):
1163
+
1164
+ ```tsx
1165
+ import { GitHubIcon, GoogleDriveIcon, SlackIcon } from "@vvedantb/ui/logos";
1166
+
1167
+ <GoogleDriveIcon /> // 24px, aria-hidden
1168
+ <SlackIcon size={16} title="Slack" /> // role="img", aria-label and <title>
1169
+ <GitHubIcon className="text-foreground" /> // monochrome marks follow currentColor
1170
+ ```
1171
+
1172
+ | Logo | Colour |
1173
+ | --- | --- |
1174
+ | `ChromeIcon`, `ConvexIcon`, `DropboxIcon`, `FigmaIcon`, `GmailIcon`, `GoogleDriveIcon`, `LinearIcon`, `NotionIcon`, `OneDriveIcon`, `SharePointIcon`, `SlackIcon` | Brand colours |
1175
+ | `ClaudeIcon`, `CursorIcon`, `GitHubIcon`, `VercelIcon` | `currentColor` |
1176
+
1177
+ Props (`LogoProps`): `size` (number or CSS length, default `24`, sets width and height), `title` (accessible name: the svg becomes `role="img"` with `aria-label` and a `<title>`; without `title` or `aria-label` it is `aria-hidden`), and any other SVG prop (`className`, `style`, `data-*`, `ref`). `OneDriveIcon` and `SharePointIcon` use `useId` for their gradient ids, so several on one page do not clash. Paths are the vendors' own: do not recolour the full-colour marks. For the AI provider marks in model pickers, keep using `ProviderIcon`.
1178
+
1179
+ ## Animated SVG icons (0.36.0)
1180
+
1181
+ Motion-driven icons, on their own entry so apps that do not use them do not load them. They are not in the root or `/client`.
1182
+
1183
+ ```tsx
1184
+ import { AnimatedNotificationIcon, MorphingMenuIcon } from "@vvedantb/ui/icons/animated";
1185
+ ```
1186
+
1187
+ They need the optional `motion` peer and animate through `m.*`, so they need the app's `MotionProvider` (or its own `LazyMotion`). Without it they render their resting frame. No stylesheet. All are decorative (`aria-hidden`) except `AnimatedProgress` (`role="progressbar"`) and `AnimatedCounter` (text).
1188
+
1189
+ | Component | Props | What it does |
1190
+ | --- | --- | --- |
1191
+ | `AnimatedNotificationIcon` | `type` (`NotificationIconType`: `success`, `warning`, `error`, `info`), `size` (20), `color`, `className` | On mount the check draws itself, the warning shakes, the error pops in, the info icon rises. Colour: `--color-success`, `--color-warning`, `--color-danger`, `--color-primary` (`info`); `color` overrides. |
1192
+ | `AnimatedBellIcon` | `size` (32), `muted` (`true`), `className` | Bell draws itself, then swings every 4.5 s. `muted` adds the slash. |
1193
+ | `AnimatedKeyIcon` | `size` (48), `className` | Bolt draws itself, sparkles twinkle, soft pulse. For empty API-key states. |
1194
+ | `MorphingMenuIcon` | `isOpen`, `size` (20), `className` | Three bars morph to an X. `data-state="open"` or `"closed"`. Label the button and set its `aria-expanded`. |
1195
+ | `MorphingMicIcon` | `isListening`, `size` (32), `strokeWidth` (2), `className` | Cross-fades a microphone and a stop square. |
1196
+ | `PulsingRings` | `size`, `ringCount` (3), `color` (`var(--color-danger)`), `active` (`true`), `className` | Staggered expanding rings, absolutely centred in a `relative` parent. `active={false}` renders nothing. |
1197
+ | `AnimatedStatusDot` | `phase` (`StatusDotPhase`: `idle`, `listening`, `thinking`, `speaking`, `error`), `size` (10; svg is 3x), `className` | Ripples while listening, glow while listening or speaking. |
1198
+ | `AnimatedProgress` | `value` (0 to 100, clamped), `height` (4), `showGradient` (`true`), `label`, `className` | Spring fill in `--color-primary` on `--color-surface-secondary`, with a moving gradient and shimmer. |
1199
+ | `AnimatedCounter` | `value`, `duration` (1 s), `formatValue`, `animateOnView` (`true`), `className` | Springs from 0 to `value` once in view. `tabular-nums`. The spring runs without `LazyMotion`. |
1200
+
1158
1201
  ## Artifact viewers (0.13.0)
1159
1202
 
1160
1203
  Web viewers for artifacts: standalone HTML, Markdown, SVG, code or typed JSON (quiz, flashcards) documents with per-user state. The pure core (schemas, state limits and ops, the document wrapper, the guest bridge) lives in `@vvedantb/server` (see its `docs/artifacts.md`); install `@vvedantb/server` 0.6.1 or later next to this package. The components hold no data: rows come in through props and changes leave through callbacks, so any backend works.
@@ -1434,9 +1477,10 @@ New props and exports:
1434
1477
 
1435
1478
  - `DialogContent` `viewportAware` (default `true`): the bottom sheet on compact viewports, visual-viewport sizing, keyboard-aware scrolling and the body pointer-events fix. `false` keeps a centred dialog at every size and leaves the viewport and body alone.
1436
1479
  - `PopoverAnchor` (Radix `Popover.Anchor`).
1437
- - `HoverCardContent` and `TooltipContent` `portal`: render into `document.body`.
1438
- - `ContextMenuSubContent` `portal`.
1439
- - Config `Select.triggerSlot`: adds `data-slot="select-trigger"`, which the `ButtonGroup` selectors read.
1480
+ - `HoverCardContent` and `TooltipContent` `portal`: render into `document.body`. Default `true`; `false` renders inline.
1481
+ - `ContextMenuSubContent` `portal`. Default `true` (also `ContextMenu.subContentPortal`).
1482
+ - Config `Select.triggerSlot`: adds `data-slot="select-trigger"`, which the `ButtonGroup` selectors read. Default `true`.
1483
+ - `PopoverContent`, `DropdownMenuContent`, `ContextMenuContent` and `SelectContent` default `collisionPadding` to 8 (a prop or config value wins).
1440
1484
  - Config `Command.emptyClassNameReplaces`: a `className` on `CommandEmpty` (even `""`) replaces its classes instead of merging.
1441
1485
 
1442
1486
  `DialogTrigger`, `PopoverTrigger`, `DropdownMenuTrigger` and `ContextMenuTrigger` are now thin wrappers that read the config; `HoverCard` and `TooltipProvider` are function components that read the configured delays. Same DOM and props without config.
@@ -1541,14 +1585,14 @@ const config = {
1541
1585
 
1542
1586
  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.
1543
1587
  - `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.
1544
- - `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.
1588
+ - `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` (default `true`: `PromptInputActionMenuTrigger` is the Radix trigger; `false` nests 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.
1545
1589
  - `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`.
1546
1590
  - `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`.
1547
1591
  - `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`.
1548
1592
  - `PromptInputSpeech`, `useSpeechRecognition`, `getSpeechRecognition` (new): Web Speech API dictation; the button renders nothing without the API.
1549
1593
  - `Queue`: one family, used in one of two ways. The queued-messages widget is unchanged: `Queue` with `count`, `QueueHeader`, `QueueList` (`open`) and `QueueItem` with `text` (`model`, `personalityName`, `personaName`, `personaImage`, `onEdit`, `onRemove`). The composable parts (the AI Elements `Queue`, new): `Queue` without `count` (a plain root), `QueueSection`, `QueueSectionTrigger`, `QueueSectionLabel`, `QueueSectionContent`, `QueueList` (inside a composable `Queue`: a list capped at `max-h-48`), `QueueItem` without `text` (an `li`, takes `ref`), `QueueItemIndicator`, `QueueItemContent`, `QueueItemActions`, `QueueItemAction`. The composable parts render no `motion.*`, so they work under `<LazyMotion strict>`. Entry `Queue`: `classes.sectionTrigger`.
1550
1594
  - `Shimmer`: `mode`, `as`, `classes.base`, `classes.overlay`, `classes.band`, `classes.copy`. New prop `mode`: `"gradient"` (default) or `"mask"`, which sweeps a masked copy of the text with transforms only (no repaint, paused offscreen). `mask` needs the app to define `.shimmer-overlay`, `.shimmer-band` and `.shimmer-copy` (or the `classes` it names).
1551
- - `CodeBlock`: `plain`, `copyButtonSize`, `copyLabel` (a screen-reader label on the copy button), `copyEveryClick` (the copy button copies on every click, also while it shows the check; by default those clicks do nothing until it resets after `timeout`), `classes.container`, `classes.pre`, `classes.copyButton`, `classes.copyIcon` (a class in place of `size={14}`). New props: `plain` (no shiki: a `pre` with the code, and `children` replace that `pre` instead of sitting above it); `language` is now optional.
1595
+ - `CodeBlock`: `plain`, `copyButtonSize`, `copyLabel` (a screen-reader label on the copy button; without it the icon-only button gets `aria-label="Copy code"`), `copyEveryClick` (the copy button copies on every click, also while it shows the check; by default those clicks do nothing until it resets after `timeout`), `classes.container`, `classes.pre`, `classes.copyButton`, `classes.copyIcon` (a class in place of `size={14}`). New props: `plain` (no shiki: a `pre` with the code, and `children` replace that `pre` instead of sitting above it); `language` is now optional.
1552
1596
  - `Artifact`: `classes.root`, `classes.header`, `classes.title`, `classes.description`, `classes.actions`, `classes.action` (`ArtifactAction` and `ArtifactClose`), `classes.actionIcon`, `classes.content`, `actionSize`, `actionLabel` (`"sr-only"` names actions with a hidden span instead of `aria-label`). `ArtifactAction`: `tooltip` is optional (no tooltip, no wrapper) and `icon` also takes an icon component (`icon={IconCopy}`). New `ArtifactClose` (an action with an X, `label` default `"Close"`).
1553
1597
  - `Attachments`: `classes.item.<variant>`, `classes.preview.<variant>`, `classes.remove.<variant>` replace the variant classes of `Attachment`, `AttachmentPreview` and `AttachmentRemove`.
1554
1598
  - `FileChip` (new, no entry): a file name chip that opens the file.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
4
4
  "license": "MIT",
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.",
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), third-party brand logos (also @vvedantb/ui/logos), motion-driven animated SVG icons (@vvedantb/ui/icons/animated), 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",
7
7
  "publishConfig": {
8
8
  "access": "public"
@@ -46,6 +46,14 @@
46
46
  "types": "./src/brand/index.ts",
47
47
  "default": "./src/brand/index.ts"
48
48
  },
49
+ "./logos": {
50
+ "types": "./src/logos/index.ts",
51
+ "default": "./src/logos/index.ts"
52
+ },
53
+ "./icons/animated": {
54
+ "types": "./src/icons/animated/index.ts",
55
+ "default": "./src/icons/animated/index.ts"
56
+ },
49
57
  "./client": {
50
58
  "types": "./src/client.ts",
51
59
  "default": "./src/client.ts"
@@ -41,7 +41,7 @@ export interface ActivityMarkdownProps {
41
41
  export interface ActivityTasksConfig {
42
42
  /** Agent name in the streaming header ("<name> is thinking..."). Default "Agent"; the `name` prop wins. */
43
43
  name?: string;
44
- /** Renders reasoning and mid-turn reply text. Default: `Streamdown`. */
44
+ /** Renders reasoning and mid-turn reply text. Default: the package `Markdown` (smooth streaming). */
45
45
  markdown?: ComponentType<ActivityMarkdownProps>;
46
46
  /**
47
47
  * An image for `ActivityThinkingIcon` (for example the app's logo mark),
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { createContext, memo, useCallback, useContext, useRef, useState } from "react";
4
- import type { ComponentProps, ReactNode, Ref } from "react";
4
+ import type { ComponentProps, ComponentType, ReactNode, Ref } from "react";
5
5
  import { flushSync } from "react-dom";
6
6
 
7
7
  import { Collapsible, CollapsibleContent } from "../ui/collapsible";
@@ -14,11 +14,7 @@ import {
14
14
  IconSearch,
15
15
  IconTerminal2,
16
16
  } from "@tabler/icons-react";
17
- import { cjk } from "@streamdown/cjk";
18
- import { code } from "@streamdown/code";
19
- import { math } from "@streamdown/math";
20
- import { mermaid } from "@streamdown/mermaid";
21
- import { Streamdown } from "streamdown";
17
+ import { Markdown as PackageMarkdown } from "../markdown/Markdown";
22
18
  import { useCn, useUiConfig } from "../ui/ui-config";
23
19
  import { Spinner } from "../ui/spinner";
24
20
  import { CrossfadeIconSlot } from "../ui/crossfade-icon";
@@ -65,19 +61,9 @@ import {
65
61
  import { useStreamingDisclosure } from "./use-streaming-disclosure";
66
62
  import { RollingNumber } from "../ui/rolling-number";
67
63
 
68
- const streamdownPlugins = { cjk, code, math, mermaid };
69
-
70
- function StreamdownMarkdown({ className, mode, children }: ActivityMarkdownProps) {
71
- return (
72
- <Streamdown className={className} mode={mode} plugins={streamdownPlugins}>
73
- {children}
74
- </Streamdown>
75
- );
76
- }
77
-
78
- /** The app's Markdown renderer (`UiConfig.ActivityTasks.markdown`), or Streamdown. */
79
- function useActivityMarkdown() {
80
- return useUiConfig("ActivityTasks")?.markdown ?? StreamdownMarkdown;
64
+ /** The app's Markdown renderer (`UiConfig.ActivityTasks.markdown`), or the package `Markdown`. */
65
+ function useActivityMarkdown(): ComponentType<ActivityMarkdownProps> {
66
+ return useUiConfig("ActivityTasks")?.markdown ?? PackageMarkdown;
81
67
  }
82
68
 
83
69
  const ActivityStreamingHeader = memo(function ActivityStreamingHeader({
@@ -317,12 +317,12 @@ export const AttachmentRemove = ({
317
317
  remove === undefined && variant === "grid" && [
318
318
  "absolute top-2 right-2 size-6 rounded-full p-0",
319
319
  cn(flatPanelSubtleClass, flatElevatedSurfaceHoverClass),
320
- "opacity-0 transition-opacity group-hover:opacity-100",
320
+ "reveal-on-hover transition-opacity",
321
321
  "[&>svg]:size-3",
322
322
  ],
323
323
  remove === undefined && variant === "inline" && [
324
324
  "size-5 rounded p-0",
325
- "opacity-0 transition-opacity group-hover:opacity-100",
325
+ "reveal-on-hover transition-opacity",
326
326
  "[&>svg]:size-2.5",
327
327
  ],
328
328
  remove === undefined && variant === "list" && ["size-8 shrink-0 rounded p-0", "[&>svg]:size-4"],
@@ -101,7 +101,8 @@ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
101
101
  /**
102
102
  * `UiConfig.CodeBlock`: `plain` default, and classes that replace the
103
103
  * container, the plain-mode `pre`, the copy button and its icon. A
104
- * `copyLabel` adds a screen-reader label to the copy button.
104
+ * `copyLabel` adds a screen-reader label to the copy button. Without it, the
105
+ * icon-only button gets `aria-label="Copy code"`.
105
106
  * `copyEveryClick` makes the copy button copy again while it still shows
106
107
  * the check; by default those clicks do nothing until it resets.
107
108
  */
@@ -482,6 +483,7 @@ export const CodeBlockCopyButton = ({
482
483
  onClick={copyToClipboard}
483
484
  size={config?.copyButtonSize ?? "icon"}
484
485
  variant="ghost"
486
+ aria-label={config?.copyLabel || children !== undefined ? undefined : "Copy code"}
485
487
  {...props}
486
488
  >
487
489
  {children ?? (iconClass === undefined ? <Icon size={14} /> : <Icon className={iconClass} />)}
@@ -101,7 +101,7 @@ export const ConversationEmptyState = ({
101
101
  );
102
102
  };
103
103
  export type ConversationScrollButtonProps = ComponentProps<typeof Button> & {
104
- /** Text after the icon; also the button's `aria-label` and `title`. */
104
+ /** Text after the icon; also the button's `aria-label` and `title`. Without it, the icon-only button's `aria-label` is "Scroll to bottom". */
105
105
  label?: string;
106
106
  /** Replaces the arrow icon. */
107
107
  icon?: ReactNode;
@@ -184,7 +184,9 @@ export const ConversationScrollButton = ({
184
184
  hide();
185
185
  scrollToBottom();
186
186
  };
187
- const motionClass = classes?.scrollButtonMotion ?? "absolute bottom-4 left-0 right-0 z-10 flex justify-center";
187
+ // The full-width row lets clicks through to the messages; the button takes them.
188
+ const motionClass =
189
+ classes?.scrollButtonMotion ?? "pointer-events-none absolute bottom-4 left-0 right-0 z-10 flex justify-center";
188
190
  const presence = (
189
191
  <LazyMotion features={domAnimation}>
190
192
  <AnimatePresence initial={false}>
@@ -198,12 +200,16 @@ export const ConversationScrollButton = ({
198
200
  transition={defaults?.transition ?? fastTransition}
199
201
  >
200
202
  <Button
201
- className={merge(classes?.scrollButton ?? [flatInteractiveClass, "rounded-full"], className)}
203
+ className={merge(
204
+ classes?.scrollButton ?? [flatInteractiveClass, "rounded-full"],
205
+ "pointer-events-auto",
206
+ className,
207
+ )}
202
208
  onClick={handleScrollToBottom}
203
209
  size={size ?? defaults?.size ?? "icon"}
204
210
  type="button"
205
211
  variant="outline"
206
- {...(label ? { "aria-label": label, title: label } : {})}
212
+ {...(label ? { "aria-label": label, title: label } : { "aria-label": "Scroll to bottom" })}
207
213
  {...props}
208
214
  >
209
215
  {icon ?? <IconArrowDown className="size-4" />}
@@ -105,8 +105,9 @@ const convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {
105
105
  * (added when no `containerClassName` is given), `textarea`, `footer`,
106
106
  * `submit` and `tabItem`. `submitIcons` replace the submit
107
107
  * button's icon per status; `submitCrossfade` cross-fades between them.
108
- * `menuTriggerAsChild` makes `PromptInputActionMenuTrigger` the trigger
109
- * itself rather than a button inside the menu's trigger button.
108
+ * `menuTriggerAsChild` (default `true`) makes `PromptInputActionMenuTrigger`
109
+ * the trigger itself; `false` nests its button inside the menu's trigger
110
+ * button (invalid HTML).
110
111
  */
111
112
  export interface PromptInputConfig {
112
113
  classes?: {
@@ -823,7 +824,7 @@ export const PromptInputFooter = ({
823
824
  return (
824
825
  <InputGroupAddon
825
826
  align="block-end"
826
- className={merge(footer ?? "justify-between gap-1", className)}
827
+ className={merge(footer ?? "justify-between gap-1 max-sm:flex-wrap", className)}
827
828
  {...props}
828
829
  />
829
830
  );
@@ -895,7 +896,7 @@ export const PromptInputActionMenuTrigger = ({
895
896
  children,
896
897
  ...props
897
898
  }: PromptInputActionMenuTriggerProps) => {
898
- const asChild = useUiConfig("PromptInput")?.menuTriggerAsChild;
899
+ const asChild = useUiConfig("PromptInput")?.menuTriggerAsChild ?? true;
899
900
  const button = (
900
901
  <PromptInputButton className={className} {...props}>
901
902
  {children ?? <IconPlus className="size-4" />}
package/src/client.ts CHANGED
@@ -399,6 +399,9 @@ export { useHeldResult } from "../app/src/convex/useHeldResult";
399
399
  // Family app marks (Icon* components, deprecated AppMark and raw SVG strings for favicons).
400
400
  export * from "./brand";
401
401
 
402
+ // Third-party brand logos (`GitHubIcon`, `SlackIcon`, `ClaudeIcon`, ...). Also `@vvedantb/ui/logos`.
403
+ export * from "./logos";
404
+
402
405
  // Native tokens (JS values that match tokens.css).
403
406
  export { tokens } from "../tokens/src/native";
404
407
  export type { Tokens } from "../tokens/src/native";
@@ -0,0 +1,68 @@
1
+ import { m } from "motion/react";
2
+
3
+ export interface AnimatedBellIconProps {
4
+ /** Width and height in px. Default `32`. */
5
+ size?: number;
6
+ className?: string;
7
+ /** Draw the mute slash. Default `true` (the empty-notifications state). */
8
+ muted?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Bell that draws itself, then swings every few seconds with a moving
13
+ * clapper. For empty notification states. Uses `currentColor`. Decorative (`aria-hidden`).
14
+ */
15
+ export function AnimatedBellIcon({ size = 32, className, muted = true }: AnimatedBellIconProps) {
16
+ return (
17
+ <m.svg
18
+ width={size}
19
+ height={size}
20
+ viewBox="0 0 24 24"
21
+ fill="none"
22
+ stroke="currentColor"
23
+ strokeWidth="1.5"
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ className={className}
27
+ aria-hidden="true"
28
+ initial={{ rotate: 0 }}
29
+ animate={{ rotate: [0, 10, -10, 8, -8, 5, -5, 0] }}
30
+ transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 3 }}
31
+ style={{ transformOrigin: "top center" }}
32
+ >
33
+ {/* Body */}
34
+ <m.path
35
+ d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"
36
+ initial={{ pathLength: 0 }}
37
+ animate={{ pathLength: 1 }}
38
+ transition={{ duration: 0.6, ease: "easeOut" }}
39
+ />
40
+ {/* Bottom curve */}
41
+ <m.path
42
+ d="M13.73 21a2 2 0 0 1-3.46 0"
43
+ initial={{ pathLength: 0 }}
44
+ animate={{ pathLength: 1 }}
45
+ transition={{ duration: 0.3, delay: 0.4 }}
46
+ />
47
+ {/* Mute slash */}
48
+ {muted ? (
49
+ <m.path
50
+ d="M2 2l20 20"
51
+ data-slot="mute-slash"
52
+ initial={{ pathLength: 0 }}
53
+ animate={{ pathLength: 1 }}
54
+ transition={{ duration: 0.3, delay: 0.6 }}
55
+ />
56
+ ) : null}
57
+ {/* Clapper */}
58
+ <m.circle
59
+ cx="12"
60
+ cy="17"
61
+ r="0.5"
62
+ fill="currentColor"
63
+ animate={{ x: [0, 1, -1, 0.5, -0.5, 0] }}
64
+ transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 3 }}
65
+ />
66
+ </m.svg>
67
+ );
68
+ }
@@ -0,0 +1,45 @@
1
+ import { m, useInView, useSpring, useTransform } from "motion/react";
2
+ import { useEffect, useRef } from "react";
3
+ import { cn } from "../../utils/cn";
4
+
5
+ export interface AnimatedCounterProps {
6
+ /** Target value to count up to. */
7
+ value: number;
8
+ /** Rough animation length in seconds (scales the spring stiffness). Default `1`. */
9
+ duration?: number;
10
+ className?: string;
11
+ /** Formats the displayed number. Default: rounded, `toLocaleString()`. */
12
+ formatValue?: (value: number) => string;
13
+ /** Wait until the counter scrolls into view. Default `true`. */
14
+ animateOnView?: boolean;
15
+ }
16
+
17
+ const defaultFormat = (v: number) => Math.round(v).toLocaleString();
18
+
19
+ /**
20
+ * Number that springs up from 0 to `value` (`tabular-nums`). Changes to
21
+ * `value` spring from the current number. The spring runs without the app's
22
+ * `LazyMotion`.
23
+ */
24
+ export function AnimatedCounter({
25
+ value,
26
+ duration = 1,
27
+ className,
28
+ formatValue = defaultFormat,
29
+ animateOnView = true,
30
+ }: AnimatedCounterProps) {
31
+ const ref = useRef<HTMLSpanElement>(null);
32
+ const isInView = useInView(ref, { once: true, margin: "-50px" });
33
+ const springValue = useSpring(0, { stiffness: 100 / duration, damping: 30, restDelta: 0.01 });
34
+ const displayValue = useTransform(springValue, (v) => formatValue(v));
35
+
36
+ useEffect(() => {
37
+ if (!animateOnView || isInView) springValue.set(value);
38
+ }, [value, springValue, animateOnView, isInView]);
39
+
40
+ return (
41
+ <m.span ref={ref} className={cn("tabular-nums", className)}>
42
+ {displayValue}
43
+ </m.span>
44
+ );
45
+ }
@@ -0,0 +1,79 @@
1
+ import { m } from "motion/react";
2
+
3
+ export interface AnimatedKeyIconProps {
4
+ /** Width and height in px. Default `48`. */
5
+ size?: number;
6
+ className?: string;
7
+ }
8
+
9
+ function Sparkle({ x, y, delay, size = 3 }: { x: number; y: number; delay: number; size?: number }) {
10
+ return (
11
+ <g transform={`translate(${x}, ${y})`}>
12
+ {/* Four-pointed star */}
13
+ <m.path
14
+ d={`M0 -${size} L0 ${size} M-${size} 0 L${size} 0`}
15
+ stroke="currentColor"
16
+ strokeWidth="1"
17
+ strokeLinecap="round"
18
+ initial={{ scale: 0, opacity: 0, rotate: 0 }}
19
+ animate={{ scale: [0, 1, 0], opacity: [0, 1, 0], rotate: [0, 45, 90] }}
20
+ transition={{ duration: 1.2, delay, repeat: Infinity, repeatDelay: 2, ease: "easeInOut" }}
21
+ />
22
+ </g>
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Lightning-bolt "key" that draws itself, with twinkling sparkles and a soft
28
+ * pulse. For empty API-key states. Uses `currentColor`. Decorative (`aria-hidden`).
29
+ */
30
+ export function AnimatedKeyIcon({ size = 48, className }: AnimatedKeyIconProps) {
31
+ return (
32
+ <svg
33
+ width={size}
34
+ height={size}
35
+ viewBox="0 0 48 48"
36
+ fill="none"
37
+ stroke="currentColor"
38
+ strokeWidth="1.5"
39
+ strokeLinecap="round"
40
+ strokeLinejoin="round"
41
+ className={className}
42
+ aria-hidden="true"
43
+ >
44
+ <Sparkle x={8} y={10} delay={0} size={2.5} />
45
+ <Sparkle x={40} y={12} delay={0.4} size={2} />
46
+ <Sparkle x={36} y={38} delay={0.8} size={2.5} />
47
+ <Sparkle x={10} y={36} delay={1.2} size={2} />
48
+
49
+ {/* Bolt with a gentle pulse */}
50
+ <m.g
51
+ initial={{ scale: 1 }}
52
+ animate={{ scale: [1, 1.02, 1] }}
53
+ transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
54
+ >
55
+ <m.path
56
+ d="M26 6L14 26h8l-4 16 16-22h-9l5-14z"
57
+ strokeWidth="1.5"
58
+ fill="none"
59
+ initial={{ pathLength: 0, opacity: 0 }}
60
+ animate={{ pathLength: 1, opacity: 1 }}
61
+ transition={{ duration: 0.8, ease: "easeOut" }}
62
+ />
63
+ </m.g>
64
+
65
+ {/* Glow pulse behind */}
66
+ <m.circle
67
+ cx="24"
68
+ cy="24"
69
+ r="16"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ strokeWidth="0.5"
73
+ initial={{ scale: 0.9, opacity: 0 }}
74
+ animate={{ scale: [0.9, 1.1, 0.9], opacity: [0, 0.15, 0] }}
75
+ transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
76
+ />
77
+ </svg>
78
+ );
79
+ }