@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 +31 -2
- package/package.json +1 -1
- package/shell/src/SidebarUpgradeCard.tsx +52 -0
- package/shell/src/index.ts +2 -0
- package/src/brand/AppMark.tsx +13 -6
- package/src/brand/index.ts +4 -0
- package/src/brand/svg.ts +118 -3
- package/src/index.ts +2 -0
- package/src/marketing/PricingModal.tsx +11 -4
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`
|
|
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.
|
|
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
|
+
}
|
package/shell/src/index.ts
CHANGED
|
@@ -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";
|
package/src/brand/AppMark.tsx
CHANGED
|
@@ -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):
|
|
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
|
-
/**
|
|
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
|
|
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={
|
|
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}
|
package/src/brand/index.ts
CHANGED
|
@@ -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="${
|
|
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}
|
|
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
|
-
|
|
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
|
|
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={
|
|
387
|
+
<AppMark app={benefit.icon} size={12} className="max-w-full" />
|
|
381
388
|
) : (
|
|
382
389
|
<IconCheck className="size-4" strokeWidth={2} />
|
|
383
390
|
)}
|