@vvedantb/ui 0.22.0 → 0.23.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 CHANGED
@@ -4,7 +4,7 @@ Shared React 19 web UI. Source-exported: no build step.
4
4
 
5
5
  | Import | What it is |
6
6
  | --- | --- |
7
- | `@vvedantb/ui` | The only JavaScript entry. Primitives (Radix-based), AI elements, audio (`playChime`, `useReplyChime`, `playTone`), `toast` and `SonnerToaster`, the composer (`InputArea`, deprecated 0.3 `Composer`), shell (`SidebarHeader`, `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. |
7
+ | `@vvedantb/ui` | The only 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/tokens` | `tokens.css`: colour, radius, shadow, ease and font tokens for Tailwind v4. CSS. |
9
9
  | `@vvedantb/ui/tokens/tokens.css` | Same file as `@vvedantb/ui/tokens`. CSS. |
10
10
  | `@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`. |
@@ -179,6 +179,17 @@ function MentionComposer({ draft, setDraft, send }: Props) {
179
179
 
180
180
  Fixture: `composer/examples/` (see its README). It has a `tokens.css` page and a `compat-shadcn-rgb.css` page, each with light and dark.
181
181
 
182
+ ## Sidebar upgrade card
183
+
184
+ ```tsx
185
+ import { SidebarUpgradeCard } from "@vvedantb/ui";
186
+
187
+ {!isPro && <SidebarUpgradeCard onUpgrade={() => setPricingOpen(true)} />}
188
+ {!isPro && <SidebarUpgradeCard collapsed onUpgrade={() => setPricingOpen(true)} />}
189
+ ```
190
+
191
+ Shows the Project V mark, "Upgrade to Pro" and an "Upgrade" pill. `collapsed` renders an icon-only round button labelled "Upgrade to Pro" for narrow rails. The card does not check the plan, so the app hides it for Pro users.
192
+
182
193
  ## Sessions hover card
183
194
 
184
195
  ```tsx
@@ -865,6 +876,24 @@ links under a separator.
865
876
  | `legalLinks` | `{ href, label }[]`, open in a new tab. |
866
877
  | `variant` | `"muted"` (default, `text-muted`), `"muted-foreground"`, or `"touch"` (`text-muted-foreground`, `max-sm:hit-target` links, safe-area bottom padding). |
867
878
 
879
+ ### `AppMark`
880
+
881
+ Family app icons: `app` (`projectv`, `vibot`, `vhub`, `vmem`, `verve`, `velth`),
882
+ `variant`, `size`, `title`.
883
+
884
+ - `variant="mark"` (default) is the plain logo in `currentColor`. It is cropped
885
+ tight to the glyph (stroke marks include half the stroke width) with no
886
+ padding. `size` is the rendered height; the width follows the glyph's aspect
887
+ ratio, so it sits level with a wordmark. A string `size` (for example `"2rem"`)
888
+ sets both width and height to that value. `APP_MARK_VIEWBOX` holds the tight
889
+ viewBox per app; `appMarkSvg()` and `APP_MARK_SVG` use it.
890
+ - `variant="tile"` and `"tile-light"` keep the equalised, padded 1024 grid on a
891
+ rounded tile, always square. `appTileSvg`, `appAdaptiveSvg`, `appExtension128Svg`,
892
+ `APP_TILE_SVG`, `APP_TILE_LIGHT_SVG` and favicons are unchanged.
893
+
894
+ Use tiles only where the app shows on its background (app and store icons,
895
+ favicons). Use the plain mark wherever the logo has no background.
896
+
868
897
  ### `PricingModal`
869
898
 
870
899
  Upgrade dialog with two layouts. Without `plans` it is the single-plan dialog
@@ -877,7 +906,7 @@ default) and `renderCheckout`.
877
906
 
878
907
  #### Multi-plan (0.22.0)
879
908
 
880
- Header: `apps` (an `AppName[]`, shown as `AppMark` tiles at the top left),
909
+ Header: `apps` (an `AppName[]`, shown as plain `AppMark` logos, no tile, at the top left),
881
910
  `headline` (centre), a Monthly / Annual switch and a close button (top right).
882
911
  Then one card per entry of `plans` (2 to 4 fit) and one `finePrint` line. Cards
883
912
  stack on narrow screens.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "license": "MIT",
5
5
  "description": "Shared React 19 UI for web apps: one 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), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
6
6
  "type": "module",
@@ -0,0 +1,52 @@
1
+ import { AppMark } from "../../src/brand/AppMark";
2
+ import { Button } from "../../src/ui/button";
3
+ import { cn } from "../../src/utils/cn";
4
+
5
+ export type SidebarUpgradeCardProps = {
6
+ /** Opens the app's `PricingModal`. */
7
+ onUpgrade: () => void;
8
+ /** Narrow rail: an icon-only "Upgrade to Pro" button instead of the full card. Default `false`. */
9
+ collapsed?: boolean;
10
+ className?: string;
11
+ };
12
+
13
+ const pillClass = "bg-foreground text-background hover:bg-foreground/85";
14
+
15
+ /**
16
+ * Upgrade prompt for the sidebar: Project V mark, "Upgrade to Pro" and an "Upgrade" pill. It does not check
17
+ * the plan; the app hides it when the user is already on Pro. The mark uses a numeric `size` (its height) so
18
+ * the width follows the aspect ratio.
19
+ */
20
+ export function SidebarUpgradeCard({ onUpgrade, collapsed = false, className }: SidebarUpgradeCardProps) {
21
+ if (collapsed) {
22
+ return (
23
+ <Button
24
+ type="button"
25
+ size="icon"
26
+ aria-label="Upgrade to Pro"
27
+ title="Upgrade to Pro"
28
+ onClick={onUpgrade}
29
+ className={cn("rounded-full", pillClass, className)}
30
+ >
31
+ <AppMark app="projectv" size={16} />
32
+ </Button>
33
+ );
34
+ }
35
+ return (
36
+ <div
37
+ data-slot="sidebar-upgrade-card"
38
+ className={cn(
39
+ "flex items-center justify-between gap-3 rounded-2xl bg-surface-secondary px-3 py-2.5 text-foreground",
40
+ className,
41
+ )}
42
+ >
43
+ <div className="flex min-w-0 items-center gap-2 text-sm font-semibold">
44
+ <AppMark app="projectv" size={14} className="shrink-0" />
45
+ <span className="truncate">Upgrade to Pro</span>
46
+ </div>
47
+ <Button type="button" size="sm" onClick={onUpgrade} className={cn("shrink-0 rounded-full", pillClass)}>
48
+ Upgrade
49
+ </Button>
50
+ </div>
51
+ );
52
+ }
@@ -1,5 +1,7 @@
1
1
  export { SidebarHeader } from "./SidebarHeader.js";
2
2
  export type { SidebarHeaderProps } from "./SidebarHeader.js";
3
+ export { SidebarUpgradeCard } from "./SidebarUpgradeCard.js";
4
+ export type { SidebarUpgradeCardProps } from "./SidebarUpgradeCard.js";
3
5
  export { PillTabs } from "./PillTabs.js";
4
6
  export type { PillTabItem, PillTabsProps } from "./PillTabs.js";
5
7
  export { cn } from "./cn.js";
@@ -1,12 +1,16 @@
1
1
  import { useId, type SVGProps } from "react";
2
- import { APP_MARKS, MARK_VIEWBOX, TILE_RADIUS, markLinecap, markStrokeWidth, type AppMarkVariant, type AppName } from "./svg";
2
+ import { APP_MARKS, APP_MARK_VIEWBOX, MARK_VIEWBOX, TILE_RADIUS, markAspect, markLinecap, markStrokeWidth, type AppMarkVariant, type AppName } from "./svg";
3
3
 
4
4
  export type AppMarkProps = Omit<SVGProps<SVGSVGElement>, "children" | "height" | "width" | "viewBox"> & {
5
5
  /** Which family app's mark to draw. */
6
6
  app: AppName;
7
- /** `"mark"` (default): glyph in `currentColor`. `"tile"`: white glyph on a black rounded tile. `"tile-light"`: black glyph on a white tile. */
7
+ /** `"mark"` (default): plain logo in `currentColor`, cropped tight to the glyph with no padding. `"tile"`: white glyph on a black rounded tile. `"tile-light"`: black glyph on a white tile. */
8
8
  variant?: AppMarkVariant;
9
- /** Width and height. Default 24. */
9
+ /**
10
+ * Default 24. For `"mark"` it sets the rendered height and the width follows the glyph's aspect ratio
11
+ * (a number gives an exact width; a string sets both to the same value and the glyph is centred).
12
+ * Tiles are always square, `size` by `size`.
13
+ */
10
14
  size?: number | string;
11
15
  /** Accessible name. Without it the mark is `aria-hidden`. */
12
16
  title?: string;
@@ -17,10 +21,12 @@ const TILE_TONES = {
17
21
  "tile-light": { bg: "#fff", fg: "#000" },
18
22
  } as const;
19
23
 
20
- /** One of the six family app marks on the 1024 grid. */
24
+ /** One of the six family app marks. The plain mark is cropped tight; tiles use the padded 1024 grid. */
21
25
  export function AppMark({ app, variant = "mark", size = 24, title, ...rest }: AppMarkProps) {
22
26
  const titleId = useId();
23
27
  const { kind, paths } = APP_MARKS[app];
28
+ const isMark = variant === "mark";
29
+ const width = isMark && typeof size === "number" ? Math.round(size * markAspect(app) * 100) / 100 : size;
24
30
  const tone = variant === "mark" ? undefined : TILE_TONES[variant];
25
31
  const color = tone?.fg ?? "currentColor";
26
32
  const glyphProps =
@@ -38,9 +44,10 @@ export function AppMark({ app, variant = "mark", size = 24, title, ...rest }: Ap
38
44
  <svg
39
45
  {...rest}
40
46
  xmlns="http://www.w3.org/2000/svg"
41
- width={size}
47
+ width={width}
42
48
  height={size}
43
- viewBox={MARK_VIEWBOX}
49
+ viewBox={isMark ? APP_MARK_VIEWBOX[app] : MARK_VIEWBOX}
50
+ preserveAspectRatio="xMidYMid meet"
44
51
  data-app={app}
45
52
  data-variant={variant}
46
53
  role={title ? "img" : undefined}
@@ -2,6 +2,7 @@ export { AppMark } from "./AppMark";
2
2
  export type { AppMarkProps } from "./AppMark";
3
3
  export {
4
4
  APP_MARKS,
5
+ APP_MARK_VIEWBOX,
5
6
  APP_MARK_SVG,
6
7
  APP_NAMES,
7
8
  APP_TILE_LIGHT_SVG,
@@ -10,7 +11,10 @@ export {
10
11
  appExtension128Svg,
11
12
  appMarkSvg,
12
13
  appTileSvg,
14
+ markAspect,
15
+ markBBox,
13
16
  markLinecap,
14
17
  markStrokeWidth,
18
+ pathBBox,
15
19
  } from "./svg";
16
20
  export type { AppMarkVariant, AppName, MarkShape } from "./svg";
package/src/brand/svg.ts CHANGED
@@ -5,7 +5,7 @@ export const APP_NAMES = ["projectv", "vibot", "vhub", "vmem", "verve", "velth"]
5
5
 
6
6
  export type AppName = (typeof APP_NAMES)[number];
7
7
 
8
- /** `mark`: glyph only, follows `currentColor`. `tile`: white glyph on a black tile. `tile-light`: black glyph on a white tile. */
8
+ /** `mark`: glyph only, cropped tight, follows `currentColor`. `tile`: white glyph on a black tile. `tile-light`: black glyph on a white tile. */
9
9
  export type AppMarkVariant = "mark" | "tile" | "tile-light";
10
10
 
11
11
  export const MARK_VIEWBOX = "0 0 1024 1024";
@@ -79,6 +79,121 @@ export const APP_MARKS: Record<AppName, MarkShape> = {
79
79
  },
80
80
  };
81
81
 
82
+ type Point = [number, number];
83
+ type Box = [minX: number, minY: number, maxX: number, maxY: number];
84
+
85
+ /** Roots in (0, 1) of the derivative of a Bezier with 1D control values `v` (quadratic or cubic). */
86
+ function bezierExtrema(v: number[]): number[] {
87
+ const roots: number[] = [];
88
+ if (v.length === 3) {
89
+ const d = v[0]! - 2 * v[1]! + v[2]!;
90
+ if (d !== 0) roots.push((v[0]! - v[1]!) / d);
91
+ } else {
92
+ const a = -v[0]! + 3 * v[1]! - 3 * v[2]! + v[3]!;
93
+ const b = 2 * (v[0]! - 2 * v[1]! + v[2]!);
94
+ const c = v[1]! - v[0]!;
95
+ if (Math.abs(a) < 1e-12) {
96
+ if (b !== 0) roots.push(-c / b);
97
+ } else {
98
+ const disc = b * b - 4 * a * c;
99
+ if (disc >= 0) roots.push((-b + Math.sqrt(disc)) / (2 * a), (-b - Math.sqrt(disc)) / (2 * a));
100
+ }
101
+ }
102
+ return roots.filter((t) => t > 0 && t < 1);
103
+ }
104
+
105
+ function bezierAt(v: number[], t: number): number {
106
+ const u = 1 - t;
107
+ const [p0, p1, p2, p3] = v as [number, number, number, number];
108
+ return v.length === 3 ? u * u * p0 + 2 * u * t * p1 + t * t * p2 : u ** 3 * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t ** 3 * p3;
109
+ }
110
+
111
+ /** Extreme points of an SVG elliptical arc (endpoint form, no rotation), including both ends. */
112
+ function arcPoints(from: Point, [rx0, ry0, , large, sweep, x, y]: [number, number, number, number, number, number, number]): Point[] {
113
+ const [x0, y0] = from;
114
+ let rx = Math.abs(rx0);
115
+ let ry = Math.abs(ry0);
116
+ const dx = (x0 - x) / 2;
117
+ const dy = (y0 - y) / 2;
118
+ const lambda = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry);
119
+ if (lambda > 1) {
120
+ rx *= Math.sqrt(lambda);
121
+ ry *= Math.sqrt(lambda);
122
+ }
123
+ const num = rx * rx * ry * ry - rx * rx * dy * dy - ry * ry * dx * dx;
124
+ const den = rx * rx * dy * dy + ry * ry * dx * dx;
125
+ const k = (large === sweep ? -1 : 1) * Math.sqrt(Math.max(0, num / den));
126
+ const cx = k * ((rx * dy) / ry) + (x0 + x) / 2;
127
+ const cy = k * (-(ry * dx) / rx) + (y0 + y) / 2;
128
+ const a0 = Math.atan2((y0 - cy) / ry, (x0 - cx) / rx);
129
+ let delta = Math.atan2((y - cy) / ry, (x - cx) / rx) - a0;
130
+ if (sweep && delta < 0) delta += 2 * Math.PI;
131
+ if (!sweep && delta > 0) delta -= 2 * Math.PI;
132
+ const points: Point[] = [from, [x, y]];
133
+ for (let q = -6; q <= 6; q++) {
134
+ const a = (q * Math.PI) / 2;
135
+ const t = (a - a0) / delta;
136
+ if (t > 0 && t < 1) points.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
137
+ }
138
+ return points;
139
+ }
140
+
141
+ /** Exact bounding box of absolute `M`, `L`, `Q`, `C` and `A` path data (the only commands the marks use). Returns [minX, minY, maxX, maxY]. */
142
+ export function pathBBox(d: string): Box {
143
+ const tokens = d.match(/[MLQCA]|-?\d*\.?\d+(?:e-?\d+)?/g) ?? [];
144
+ const points: Point[] = [];
145
+ let cmd = "";
146
+ let cur: Point = [0, 0];
147
+ const argc: Record<string, number> = { M: 2, L: 2, Q: 4, C: 6, A: 7 };
148
+ for (let i = 0; i < tokens.length; ) {
149
+ if (/[A-Z]/.test(tokens[i]!)) cmd = tokens[i++]!;
150
+ const n = argc[cmd];
151
+ if (!n) throw new Error(`Unsupported path command: ${cmd}`);
152
+ const a = tokens.slice(i, i + n).map(Number);
153
+ i += n;
154
+ const end: Point = [a[n - 2]!, a[n - 1]!];
155
+ if (cmd === "Q" || cmd === "C") {
156
+ const xs = [cur[0], ...a.filter((_, j) => j % 2 === 0)];
157
+ const ys = [cur[1], ...a.filter((_, j) => j % 2 === 1)];
158
+ for (const t of new Set([...bezierExtrema(xs), ...bezierExtrema(ys)])) points.push([bezierAt(xs, t), bezierAt(ys, t)]);
159
+ } else if (cmd === "A") {
160
+ points.push(...arcPoints(cur, a as Parameters<typeof arcPoints>[1]));
161
+ }
162
+ points.push(end);
163
+ cur = end;
164
+ }
165
+ return [Math.min(...points.map((p) => p[0])), Math.min(...points.map((p) => p[1])), Math.max(...points.map((p) => p[0])), Math.max(...points.map((p) => p[1]))];
166
+ }
167
+
168
+ /** Tight box of a mark: union of its paths, grown by half the stroke width for stroke marks. Returns [minX, minY, maxX, maxY]. */
169
+ export function markBBox(app: AppName): Box {
170
+ const { kind, paths } = APP_MARKS[app];
171
+ const boxes = paths.map(pathBBox);
172
+ const pad = kind === "stroke" ? markStrokeWidth(app) / 2 : 0;
173
+ return [Math.min(...boxes.map((b) => b[0])) - pad, Math.min(...boxes.map((b) => b[1])) - pad, Math.max(...boxes.map((b) => b[2])) + pad, Math.max(...boxes.map((b) => b[3])) + pad];
174
+ }
175
+
176
+
177
+ /**
178
+ * Tight viewBox per app (`minX minY width height` on the 1024 grid): the glyph box grown by half the
179
+ * stroke width for stroke marks, rounded outward to whole units. Checked against `markBBox` in tests.
180
+ * Used by the plain logo (`variant="mark"`, `appMarkSvg`). Tiles keep `MARK_VIEWBOX`.
181
+ */
182
+ export const APP_MARK_VIEWBOX: Record<AppName, string> = {
183
+ projectv: "172 377 686 295",
184
+ vibot: "295 314 494 398",
185
+ vhub: "186 338 582 348",
186
+ vmem: "255 259 513 503",
187
+ verve: "260 319 504 413",
188
+ velth: "273 276 478 482",
189
+ };
190
+
191
+ /** Width over height of a mark's tight viewBox. */
192
+ export function markAspect(app: AppName): number {
193
+ const [, , w = 1, h = 1] = APP_MARK_VIEWBOX[app].split(" ").map(Number);
194
+ return w / h;
195
+ }
196
+
82
197
  const NS = 'xmlns="http://www.w3.org/2000/svg"';
83
198
 
84
199
  /** Attributes that paint the glyph in `color`. */
@@ -90,9 +205,9 @@ function pathElements(app: AppName): string {
90
205
  return APP_MARKS[app].paths.map((d) => `<path d="${d}"/>`).join("");
91
206
  }
92
207
 
93
- /** Glyph only, painted with `color` (default `currentColor`). */
208
+ /** Glyph only, cropped tight with no padding, painted with `color` (default `currentColor`). */
94
209
  export function appMarkSvg(app: AppName, color = "currentColor"): string {
95
- return `<svg ${NS} viewBox="${MARK_VIEWBOX}" ${markPaintAttrs(app, color)}>${pathElements(app)}</svg>\n`;
210
+ return `<svg ${NS} viewBox="${APP_MARK_VIEWBOX[app]}" ${markPaintAttrs(app, color)}>${pathElements(app)}</svg>\n`;
96
211
  }
97
212
 
98
213
  /** Tile with a glyph. `radius` 0 gives a full-bleed square (apple-touch, maskable). */
package/src/index.ts CHANGED
@@ -181,6 +181,8 @@ export { toast } from "sonner";
181
181
  // use the root `cn` (clsx + tailwind-merge).
182
182
  export { SidebarHeader } from "../shell/src/SidebarHeader";
183
183
  export type { SidebarHeaderProps } from "../shell/src/SidebarHeader";
184
+ export { SidebarUpgradeCard } from "../shell/src/SidebarUpgradeCard";
185
+ export type { SidebarUpgradeCardProps } from "../shell/src/SidebarUpgradeCard";
184
186
  export { PillTabs } from "../shell/src/PillTabs";
185
187
  export type { PillTabItem, PillTabsProps } from "../shell/src/PillTabs";
186
188
  export { SessionsHoverCard } from "../shell/src/SessionsHoverCard";
@@ -293,7 +293,7 @@ function PricingModalMulti({
293
293
 
294
294
  <div className="grid grid-cols-[1fr_auto] items-center gap-x-4 gap-y-3 px-4 pb-4 pt-5 sm:grid-cols-[1fr_auto_1fr] sm:px-6">
295
295
  <div className="flex items-center gap-2 text-foreground" data-testid="pricing-apps">
296
- {apps?.map((app) => <AppMark key={app} app={app} variant="tile" size={24} className="rounded-md" />)}
296
+ {apps?.map((app) => <AppMark key={app} app={app} size={20} />)}
297
297
  </div>
298
298
  <div className="flex items-center justify-end gap-2 sm:order-last">
299
299
  <div role="group" aria-label="Billing period" className="inline-flex rounded-full bg-surface-tertiary p-0.5">
@@ -351,7 +351,14 @@ function PricingModalMulti({
351
351
  emphasised ? "ring-1 ring-foreground/25" : "ring-1 ring-separator"
352
352
  }`}
353
353
  >
354
- <h3 className="text-base text-foreground">{plan.name}</h3>
354
+ {emphasised ? (
355
+ <div className="flex items-center gap-2 text-base text-foreground">
356
+ <AppMark app="projectv" size={16} className="shrink-0" />
357
+ <h3>{plan.name}</h3>
358
+ </div>
359
+ ) : (
360
+ <h3 className="text-base text-foreground">{plan.name}</h3>
361
+ )}
355
362
  <p className="mt-4 flex items-baseline gap-1">
356
363
  <span className="text-5xl font-medium tabular-nums leading-none tracking-tight text-foreground">
357
364
  {plan.price[period]}
@@ -375,9 +382,9 @@ function PricingModalMulti({
375
382
  benefit.muted ? c.suffix : "text-foreground"
376
383
  }`}
377
384
  >
378
- <span className="mt-0.5 flex size-4 shrink-0 items-center justify-center">
385
+ <span className="mt-0.5 flex h-4 w-5 shrink-0 items-center justify-center">
379
386
  {benefit.icon && benefit.icon !== "check" ? (
380
- <AppMark app={benefit.icon} size={16} />
387
+ <AppMark app={benefit.icon} size={12} className="max-w-full" />
381
388
  ) : (
382
389
  <IconCheck className="size-4" strokeWidth={2} />
383
390
  )}