@vvedantb/ui 0.35.0 → 0.37.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 (38) hide show
  1. package/README.md +70 -8
  2. package/package.json +23 -16
  3. package/src/brand/index.ts +1 -0
  4. package/src/brand/productApps.ts +20 -0
  5. package/src/client.ts +15 -2
  6. package/src/icons/animated/animated-bell-icon.tsx +68 -0
  7. package/src/icons/animated/animated-counter.tsx +45 -0
  8. package/src/icons/animated/animated-key-icon.tsx +79 -0
  9. package/src/icons/animated/animated-notification-icon.tsx +202 -0
  10. package/src/icons/animated/animated-progress.tsx +74 -0
  11. package/src/icons/animated/animated-status-dot.tsx +98 -0
  12. package/src/icons/animated/index.ts +22 -0
  13. package/src/icons/animated/morphing-menu-icon.tsx +71 -0
  14. package/src/icons/animated/morphing-mic-icon.tsx +52 -0
  15. package/src/icons/animated/pulsing-rings.tsx +50 -0
  16. package/src/index.ts +15 -2
  17. package/src/logos/chrome-icon.tsx +29 -0
  18. package/src/logos/claude-icon.tsx +13 -0
  19. package/src/logos/convex-icon.tsx +21 -0
  20. package/src/logos/cursor-icon.tsx +10 -0
  21. package/src/logos/dropbox-icon.tsx +14 -0
  22. package/src/logos/figma-icon.tsx +26 -0
  23. package/src/logos/github-icon.tsx +10 -0
  24. package/src/logos/gmail-icon.tsx +34 -0
  25. package/src/logos/google-drive-icon.tsx +33 -0
  26. package/src/logos/index.ts +19 -0
  27. package/src/logos/linear-icon.tsx +13 -0
  28. package/src/logos/logo-svg.tsx +32 -0
  29. package/src/logos/notion-icon.tsx +19 -0
  30. package/src/logos/onedrive-icon.tsx +165 -0
  31. package/src/logos/sharepoint-icon.tsx +222 -0
  32. package/src/logos/slack-icon.tsx +25 -0
  33. package/src/logos/vercel-icon.tsx +10 -0
  34. package/src/marketing/SiteFooter.tsx +86 -22
  35. package/src/marketing/landing.tsx +35 -7
  36. package/src/ui/page-title.tsx +29 -0
  37. package/app/README.md +0 -269
  38. package/tokens/README.md +0 -122
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`. |
@@ -888,7 +890,7 @@ Plain parts, no Motion:
888
890
  | `LandingLattice` | `className` (grid columns). Cells share 1px borders. |
889
891
  | `LandingFeatureCard` | `icon` (a component, rendered `size={18} stroke={1.5}`), `title`, `description`. |
890
892
  | `LandingNavMenu` | `links`, `github` (`href`, `icon`). Overflow menu below `md`. |
891
- | `LandingFooter` | `brand`, `tagline`, `links`, `actions` (after the links), `github` (`href`, `label`, `icon`). |
893
+ | `LandingFooter` | New path (0.37.0): `app`, `tagline`, `links` (a "Navigation" column) or `columns`, `actions`, plus the `SiteFooter` options. A thin wrapper over `SiteFooter` with every Project V default. Deprecated path (no `app`): `brand`, `tagline`, `links`, `actions` (after the links), `github` (`href`, `label`, `icon`), in the old compact layout. `brand` and `github` are ignored when `app` is set. |
892
894
 
893
895
  Constants: `landingShellClass`, `landingPanelClass`, `landingItemVariants`, and
894
896
  for the footer's `actions` slot `landingFooterLinkClass` (a text link) and
@@ -896,17 +898,36 @@ for the footer's `actions` slot `landingFooterLinkClass` (a text link) and
896
898
 
897
899
  ### `SiteFooter`
898
900
 
899
- Brand, tagline and social icon buttons; link columns; then copyright and legal
900
- links under a separator.
901
+ Brand, tagline and social icon buttons; link columns; then copyright, product-of line and legal
902
+ links under a separator. Set `app` for the Project V footer (0.37.0):
903
+
904
+ ```tsx
905
+ <SiteFooter
906
+ app="vibot"
907
+ tagline="AI-powered assistant designed to automate tasks, and enhance productivity effortlessly."
908
+ columns={[{ heading: "Navigation", links: [{ href: "#features", label: "Features" }] }]}
909
+ />
910
+ ```
901
911
 
902
912
  | Prop | |
903
913
  | --- | --- |
904
- | `brand`, `tagline` | Nodes. |
905
- | `socials` | `{ href, icon, label? }[]`. `icon` is a component that takes `className`. Links open in a new tab. |
906
- | `columns` | `{ heading, links }[]`. A link is `{ href, label, external?, image?: { src, alt }, app?: AppName }`. `image` is a 20px round image before the label. `app` (0.29.0) is the 14px `APP_ICONS[app]` glyph before the label, in `currentColor` and `aria-hidden`. If both are set, `app` wins. |
907
- | `name`, `year` | "Copyright © {year} {name}". `year` defaults to the current year. |
908
- | `legalLinks` | `{ href, label }[]`, open in a new tab. |
914
+ | `app` | `vibot`, `vmem`, `verve`, `velth` or `vhub`. Brand is `AppWordmark` at `SITE_FOOTER_WORDMARK_SIZE` (16px; no size prop). Copyright name and product-of line use the lowercase id. |
915
+ | `tagline` | Node. |
916
+ | `columns` | Required. `{ heading, links }[]`. A link is `{ href, label, external?, image?: { src, alt }, app?: AppName }`. `image` is a 20px round image before the label. `app` is the 14px `APP_ICONS[app]` glyph before the label, in `currentColor` and `aria-hidden`; it wins over `image`. |
917
+ | `includeAppsColumn` | Default `true`: appends `PROJECT_V_PRODUCT_APPS` (vibot, vmem, verve, velth, vhub) unless `columns` has an "Apps" heading (case-insensitive). `projectVAppsColumn()` returns the column for composition. |
918
+ | `showProductOf` | Default `true`: "{app} is a product of Project V." linking to `https://projectv.uk`. |
919
+ | `socials` | Default `PROJECT_V_SOCIALS` (X, Instagram, GitHub). `[]` hides them. `{ href, icon, label? }[]`; links open in a new tab. |
920
+ | `legalLinks` | Default `PROJECT_V_LEGAL_LINKS` (Privacy Policy, Terms of Service). |
921
+ | `actions` | Node under the socials, for example Sign in. |
922
+ | `year` | Default: the current year. |
909
923
  | `variant` | `"muted"` (default, `text-muted`), `"muted-foreground"`, or `"touch"` (`text-muted-foreground`, `max-sm:hit-target` links, safe-area bottom padding). |
924
+ | `brand`, `name` | Optional overrides on the `app` path. |
925
+
926
+ **Deprecated (no `app`):** `brand`, `socials`, `name` and `legalLinks` are required, and the footer renders as before: the brand sits in the Michroma wrapper, with no product-of line and no Apps column. `actions`, `includeAppsColumn` and `showProductOf` do nothing. Set `app` instead.
927
+
928
+ ### `PageTitle` and `PageHeader`
929
+
930
+ `PageTitle` is an `h1` with `text-xl font-semibold leading-tight tracking-tight` (`pageTitleClass`): Instrument Sans, 20px, weight 600, never lighter. `PageHeader` is a title row: `title` in a `PageTitle`, an optional `action` slot, and no safe-area padding.
910
931
 
911
932
  ### `Icon*` app icons
912
933
 
@@ -1155,6 +1176,47 @@ Props (type `IconProps`; all optional):
1155
1176
 
1156
1177
  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
1178
 
1179
+ ## Third-party logos (0.36.0)
1180
+
1181
+ Full-colour vendor logos, separate from the family marks in `@vvedantb/ui/brand`. Import from `@vvedantb/ui/logos` (also in the root and `/client`):
1182
+
1183
+ ```tsx
1184
+ import { GitHubIcon, GoogleDriveIcon, SlackIcon } from "@vvedantb/ui/logos";
1185
+
1186
+ <GoogleDriveIcon /> // 24px, aria-hidden
1187
+ <SlackIcon size={16} title="Slack" /> // role="img", aria-label and <title>
1188
+ <GitHubIcon className="text-foreground" /> // monochrome marks follow currentColor
1189
+ ```
1190
+
1191
+ | Logo | Colour |
1192
+ | --- | --- |
1193
+ | `ChromeIcon`, `ConvexIcon`, `DropboxIcon`, `FigmaIcon`, `GmailIcon`, `GoogleDriveIcon`, `LinearIcon`, `NotionIcon`, `OneDriveIcon`, `SharePointIcon`, `SlackIcon` | Brand colours |
1194
+ | `ClaudeIcon`, `CursorIcon`, `GitHubIcon`, `VercelIcon` | `currentColor` |
1195
+
1196
+ 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`.
1197
+
1198
+ ## Animated SVG icons (0.36.0)
1199
+
1200
+ 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`.
1201
+
1202
+ ```tsx
1203
+ import { AnimatedNotificationIcon, MorphingMenuIcon } from "@vvedantb/ui/icons/animated";
1204
+ ```
1205
+
1206
+ 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).
1207
+
1208
+ | Component | Props | What it does |
1209
+ | --- | --- | --- |
1210
+ | `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. |
1211
+ | `AnimatedBellIcon` | `size` (32), `muted` (`true`), `className` | Bell draws itself, then swings every 4.5 s. `muted` adds the slash. |
1212
+ | `AnimatedKeyIcon` | `size` (48), `className` | Bolt draws itself, sparkles twinkle, soft pulse. For empty API-key states. |
1213
+ | `MorphingMenuIcon` | `isOpen`, `size` (20), `className` | Three bars morph to an X. `data-state="open"` or `"closed"`. Label the button and set its `aria-expanded`. |
1214
+ | `MorphingMicIcon` | `isListening`, `size` (32), `strokeWidth` (2), `className` | Cross-fades a microphone and a stop square. |
1215
+ | `PulsingRings` | `size`, `ringCount` (3), `color` (`var(--color-danger)`), `active` (`true`), `className` | Staggered expanding rings, absolutely centred in a `relative` parent. `active={false}` renders nothing. |
1216
+ | `AnimatedStatusDot` | `phase` (`StatusDotPhase`: `idle`, `listening`, `thinking`, `speaking`, `error`), `size` (10; svg is 3x), `className` | Ripples while listening, glow while listening or speaking. |
1217
+ | `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. |
1218
+ | `AnimatedCounter` | `value`, `duration` (1 s), `formatValue`, `animateOnView` (`true`), `className` | Springs from 0 to `value` once in view. `tabular-nums`. The spring runs without `LazyMotion`. |
1219
+
1158
1220
  ## Artifact viewers (0.13.0)
1159
1221
 
1160
1222
  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.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.35.0",
3
+ "version": "0.37.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"
@@ -73,17 +81,6 @@
73
81
  "./src/marketing/landing.css",
74
82
  "./fonts/instrument-sans.css"
75
83
  ],
76
- "scripts": {
77
- "check:tokens": "node tokens/scripts/check-component-tokens.mjs",
78
- "build": "node tokens/scripts/check-exports.mjs && node tokens/scripts/check-component-tokens.mjs && tsc -p tsconfig.json",
79
- "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.consumer.json && tsc -p tsconfig.consumer-speech.json && tsc -p tokens/tsconfig.json",
80
- "test": "node tokens/scripts/check-component-tokens.mjs && vitest run && node --test tokens/test/*.test.mjs app/tests-node/*.test.mjs",
81
- "verify": "node tokens/scripts/check-component-tokens.mjs && node tokens/verify/run.mjs",
82
- "analyze": "node tokens/verify/analyze.mjs",
83
- "brand:assets": "node scripts/build-brand-assets.mjs",
84
- "brand:sheet": "node scripts/build-brand-sheet.mjs",
85
- "prepack": "node tokens/scripts/check-exports.mjs"
86
- },
87
84
  "dependencies": {
88
85
  "@dnd-kit/core": "^6.3.1",
89
86
  "@dnd-kit/modifiers": "^9.0.0",
@@ -174,7 +171,6 @@
174
171
  "@tanstack/react-hotkeys": "^0.13.0",
175
172
  "@types/react": "^19.1.0",
176
173
  "@types/react-dom": "^19.1.0",
177
- "@vvedantb/server": "workspace:*",
178
174
  "ai": "^5.0.0",
179
175
  "class-variance-authority": "^0.7.1",
180
176
  "clsx": "^2.1.1",
@@ -198,7 +194,8 @@
198
194
  "vaul": "^1.1.0",
199
195
  "vitest": "^3.2.0",
200
196
  "zod": "^4.0.0",
201
- "zod3": "npm:zod@^3.25.76"
197
+ "zod3": "npm:zod@^3.25.76",
198
+ "@vvedantb/server": "0.14.0"
202
199
  },
203
200
  "peerDependenciesMeta": {
204
201
  "@clerk/clerk-react": {
@@ -219,5 +216,15 @@
219
216
  "@vvedantb/server": {
220
217
  "optional": true
221
218
  }
219
+ },
220
+ "scripts": {
221
+ "check:tokens": "node tokens/scripts/check-component-tokens.mjs",
222
+ "build": "node tokens/scripts/check-exports.mjs && node tokens/scripts/check-component-tokens.mjs && tsc -p tsconfig.json",
223
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.consumer.json && tsc -p tsconfig.consumer-speech.json && tsc -p tokens/tsconfig.json",
224
+ "test": "node tokens/scripts/check-component-tokens.mjs && vitest run && node --test tokens/test/*.test.mjs app/tests-node/*.test.mjs",
225
+ "verify": "node tokens/scripts/check-component-tokens.mjs && node tokens/verify/run.mjs",
226
+ "analyze": "node tokens/verify/analyze.mjs",
227
+ "brand:assets": "node scripts/build-brand-assets.mjs",
228
+ "brand:sheet": "node scripts/build-brand-sheet.mjs"
222
229
  }
223
- }
230
+ }
@@ -28,3 +28,4 @@ export { FAMILY_APPS, familyApp } from "./familyApps";
28
28
  export type { FamilyApp, FamilyAppName } from "./familyApps";
29
29
  export { V_PRO_APP_BENEFITS } from "./vProAppBenefits";
30
30
  export type { PricingPlanAppBenefit } from "./vProAppBenefits";
31
+ export { PROJECT_V_PRODUCT_APPS, PROJECT_V_PRODUCT_APP_IDS, projectVAppsColumn } from "./productApps";
@@ -0,0 +1,20 @@
1
+ import type { ProductAppName } from "./AppWordmark";
2
+
3
+ /** The five product apps in footer order (not `FAMILY_APPS` order). */
4
+ export const PROJECT_V_PRODUCT_APP_IDS: readonly ProductAppName[] = ["vibot", "vmem", "verve", "velth", "vhub"];
5
+
6
+ /** Apps column: each app links to `https://{app}.projectv.uk` in a new tab, with its glyph and lowercase id as the label. */
7
+ export function projectVAppsColumn() {
8
+ return {
9
+ heading: "Apps",
10
+ links: PROJECT_V_PRODUCT_APP_IDS.map((app) => ({
11
+ href: `https://${app}.projectv.uk`,
12
+ label: app,
13
+ external: true as const,
14
+ app,
15
+ })),
16
+ };
17
+ }
18
+
19
+ /** `projectVAppsColumn()`, built once. */
20
+ export const PROJECT_V_PRODUCT_APPS = projectVAppsColumn();
package/src/client.ts CHANGED
@@ -54,6 +54,7 @@ export * from "./ui/avatar";
54
54
  export * from "./ui/badge";
55
55
  export * from "./ui/border-beam";
56
56
  export * from "./ui/breadcrumb";
57
+ export * from "./ui/page-title";
57
58
  export * from "./ui/button";
58
59
  export * from "./ui/button-group";
59
60
  export * from "./ui/card";
@@ -285,6 +286,8 @@ export {
285
286
  } from "./marketing/landing";
286
287
  export type {
287
288
  LandingFeature,
289
+ LandingFooterAppProps,
290
+ LandingFooterLegacyProps,
288
291
  LandingFooterProps,
289
292
  LandingLatticeProps,
290
293
  LandingLink,
@@ -311,8 +314,15 @@ export {
311
314
  landingShellClass,
312
315
  } from "./marketing/marketing-classes";
313
316
  export type { PricingModalTone, SiteFooterVariant } from "./marketing/marketing-classes";
314
- export { SiteFooter } from "./marketing/SiteFooter";
315
- export type { SiteFooterLink, SiteFooterProps, SiteFooterSocial } from "./marketing/SiteFooter";
317
+ export { PROJECT_V_LEGAL_LINKS, PROJECT_V_SOCIALS, SITE_FOOTER_WORDMARK_SIZE, SiteFooter } from "./marketing/SiteFooter";
318
+ export type {
319
+ SiteFooterAppProps,
320
+ SiteFooterColumn,
321
+ SiteFooterLegacyProps,
322
+ SiteFooterLink,
323
+ SiteFooterProps,
324
+ SiteFooterSocial,
325
+ } from "./marketing/SiteFooter";
316
326
  export { PricingModal, V_PRO_APP_BENEFITS } from "./marketing/PricingModal";
317
327
  export type {
318
328
  PricingModalLegacyProps,
@@ -399,6 +409,9 @@ export { useHeldResult } from "../app/src/convex/useHeldResult";
399
409
  // Family app marks (Icon* components, deprecated AppMark and raw SVG strings for favicons).
400
410
  export * from "./brand";
401
411
 
412
+ // Third-party brand logos (`GitHubIcon`, `SlackIcon`, `ClaudeIcon`, ...). Also `@vvedantb/ui/logos`.
413
+ export * from "./logos";
414
+
402
415
  // Native tokens (JS values that match tokens.css).
403
416
  export { tokens } from "../tokens/src/native";
404
417
  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
+ }
@@ -0,0 +1,202 @@
1
+ import { m } from "motion/react";
2
+
3
+ export type NotificationIconType = "success" | "warning" | "error" | "info";
4
+
5
+ export interface AnimatedNotificationIconProps {
6
+ type: NotificationIconType;
7
+ /** Width and height in px. Default `20`. */
8
+ size?: number;
9
+ /** Stroke colour. Default: the type's token (`--color-success`, `--color-warning`, `--color-danger`, `--color-primary` for `info`). */
10
+ color?: string;
11
+ /** Class on the wrapping `span`. */
12
+ className?: string;
13
+ }
14
+
15
+ const TYPE_COLORS: Record<NotificationIconType, string> = {
16
+ success: "var(--color-success)",
17
+ warning: "var(--color-warning)",
18
+ error: "var(--color-danger)",
19
+ info: "var(--color-primary)",
20
+ };
21
+
22
+ interface PartProps {
23
+ size: number;
24
+ color: string;
25
+ }
26
+
27
+ function SuccessIcon({ size, color }: PartProps) {
28
+ return (
29
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
30
+ {/* Circle */}
31
+ <m.circle
32
+ cx="12"
33
+ cy="12"
34
+ r="10"
35
+ stroke={color}
36
+ strokeWidth="1.5"
37
+ fill="none"
38
+ initial={{ pathLength: 0, opacity: 0 }}
39
+ animate={{ pathLength: 1, opacity: 1 }}
40
+ transition={{ duration: 0.4, ease: "easeOut" }}
41
+ />
42
+ {/* Checkmark draws itself */}
43
+ <m.path
44
+ d="M8 12l2.5 2.5L16 9"
45
+ stroke={color}
46
+ strokeWidth="1.5"
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ fill="none"
50
+ initial={{ pathLength: 0 }}
51
+ animate={{ pathLength: 1 }}
52
+ transition={{ duration: 0.3, delay: 0.3, ease: "easeOut" }}
53
+ />
54
+ </svg>
55
+ );
56
+ }
57
+
58
+ function WarningIcon({ size, color }: PartProps) {
59
+ return (
60
+ <m.svg
61
+ width={size}
62
+ height={size}
63
+ viewBox="0 0 24 24"
64
+ fill="none"
65
+ aria-hidden="true"
66
+ initial={{ rotate: 0 }}
67
+ animate={{ rotate: [0, -5, 5, -3, 3, 0] }}
68
+ transition={{ duration: 0.5, delay: 0.2 }}
69
+ >
70
+ {/* Triangle */}
71
+ <m.path
72
+ d="M12 2L2 20h20L12 2z"
73
+ stroke={color}
74
+ strokeWidth="1.5"
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ fill="none"
78
+ initial={{ pathLength: 0, opacity: 0 }}
79
+ animate={{ pathLength: 1, opacity: 1 }}
80
+ transition={{ duration: 0.4, ease: "easeOut" }}
81
+ />
82
+ {/* Exclamation line */}
83
+ <m.path
84
+ d="M12 9v4"
85
+ stroke={color}
86
+ strokeWidth="1.5"
87
+ strokeLinecap="round"
88
+ initial={{ pathLength: 0 }}
89
+ animate={{ pathLength: 1 }}
90
+ transition={{ duration: 0.2, delay: 0.3 }}
91
+ />
92
+ {/* Exclamation dot */}
93
+ <m.circle
94
+ cx="12"
95
+ cy="17"
96
+ r="1"
97
+ fill={color}
98
+ initial={{ scale: 0 }}
99
+ animate={{ scale: 1 }}
100
+ transition={{ duration: 0.15, delay: 0.45, type: "spring" }}
101
+ />
102
+ </m.svg>
103
+ );
104
+ }
105
+
106
+ function ErrorIcon({ size, color }: PartProps) {
107
+ return (
108
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true">
109
+ {/* Circle */}
110
+ <m.circle
111
+ cx="12"
112
+ cy="12"
113
+ r="10"
114
+ stroke={color}
115
+ strokeWidth="1.5"
116
+ fill="none"
117
+ initial={{ scale: 0.8, opacity: 0 }}
118
+ animate={{ scale: 1, opacity: 1 }}
119
+ transition={{ duration: 0.3, ease: "easeOut" }}
120
+ />
121
+ {/* X, first stroke */}
122
+ <m.path
123
+ d="M15 9l-6 6"
124
+ stroke={color}
125
+ strokeWidth="1.5"
126
+ strokeLinecap="round"
127
+ initial={{ pathLength: 0 }}
128
+ animate={{ pathLength: 1 }}
129
+ transition={{ duration: 0.2, delay: 0.2 }}
130
+ />
131
+ {/* X, second stroke */}
132
+ <m.path
133
+ d="M9 9l6 6"
134
+ stroke={color}
135
+ strokeWidth="1.5"
136
+ strokeLinecap="round"
137
+ initial={{ pathLength: 0 }}
138
+ animate={{ pathLength: 1 }}
139
+ transition={{ duration: 0.2, delay: 0.3 }}
140
+ />
141
+ </svg>
142
+ );
143
+ }
144
+
145
+ function InfoIcon({ size, color }: PartProps) {
146
+ return (
147
+ <m.svg
148
+ width={size}
149
+ height={size}
150
+ viewBox="0 0 24 24"
151
+ fill="none"
152
+ aria-hidden="true"
153
+ initial={{ y: 2, opacity: 0 }}
154
+ animate={{ y: 0, opacity: 1 }}
155
+ transition={{ duration: 0.3, ease: "easeOut" }}
156
+ >
157
+ {/* Circle */}
158
+ <circle cx="12" cy="12" r="10" stroke={color} strokeWidth="1.5" fill="none" />
159
+ {/* Dot */}
160
+ <m.circle
161
+ cx="12"
162
+ cy="8"
163
+ r="1"
164
+ fill={color}
165
+ initial={{ scale: 0 }}
166
+ animate={{ scale: 1 }}
167
+ transition={{ duration: 0.2, delay: 0.2, type: "spring" }}
168
+ />
169
+ {/* Line */}
170
+ <m.path
171
+ d="M12 12v4"
172
+ stroke={color}
173
+ strokeWidth="1.5"
174
+ strokeLinecap="round"
175
+ initial={{ pathLength: 0 }}
176
+ animate={{ pathLength: 1 }}
177
+ transition={{ duration: 0.2, delay: 0.3 }}
178
+ />
179
+ </m.svg>
180
+ );
181
+ }
182
+
183
+ const PARTS: Record<NotificationIconType, (props: PartProps) => React.JSX.Element> = {
184
+ success: SuccessIcon,
185
+ warning: WarningIcon,
186
+ error: ErrorIcon,
187
+ info: InfoIcon,
188
+ };
189
+
190
+ /**
191
+ * Notification status icon that animates on mount: the check draws itself,
192
+ * the warning shakes, the error pops in and the info icon rises. Decorative
193
+ * (`aria-hidden`); put the message text beside it.
194
+ */
195
+ export function AnimatedNotificationIcon({ type, size = 20, color, className }: AnimatedNotificationIconProps) {
196
+ const Part = PARTS[type];
197
+ return (
198
+ <span className={className} data-type={type}>
199
+ <Part size={size} color={color ?? TYPE_COLORS[type]} />
200
+ </span>
201
+ );
202
+ }