@vvedantb/ui 0.39.1 → 0.40.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 +52 -1
- package/package.json +1 -1
- package/src/ai-elements/activity-tasks.tsx +14 -10
- package/src/client.ts +3 -0
- package/src/index.ts +3 -0
- package/src/marketing/ProductHero.tsx +129 -0
- package/src/marketing/SiteFooter.tsx +2 -1
- package/src/marketing/marketing-classes.ts +3 -0
- package/src/marketing/projectV.ts +2 -0
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 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. |
|
|
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`; `ProductHero` and `PROJECT_V_HOME_URL` in the next release), 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
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). |
|
|
@@ -954,6 +954,57 @@ links under a separator. Set `app` for the Project V footer (0.37.0):
|
|
|
954
954
|
|
|
955
955
|
**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.
|
|
956
956
|
|
|
957
|
+
### `ProductHero`
|
|
958
|
+
|
|
959
|
+
One hero for every product landing page: copy first, then a concept visual on mobile, side by side from `lg`.
|
|
960
|
+
It sits in `landingShellClass` width. The title is the only `h1` and uses the `PageTitle` contract
|
|
961
|
+
(`pageTitleClass`: Instrument Sans, weight 600, `leading-tight`, `tracking-tight`) at `text-4xl sm:text-5xl lg:text-6xl`.
|
|
962
|
+
Actions are pill `Button`s. The only motion `ProductHero` adds is the `userCount` dot pulse, which stops under `prefers-reduced-motion: reduce`.
|
|
963
|
+
|
|
964
|
+
```tsx
|
|
965
|
+
<ProductHero
|
|
966
|
+
title="Make a bot. Keep the chat going."
|
|
967
|
+
tagline="Each bot has its own ongoing chat and its own memory. vibot is your default bot. It sends each message to the bot that should answer."
|
|
968
|
+
primaryCta={{ label: "Try vibot free", href: "/sign-up" }}
|
|
969
|
+
signIn={{ href: "/sign-in" }}
|
|
970
|
+
visual={<OrbitRings />}
|
|
971
|
+
visualLabel="Rings orbiting a bot"
|
|
972
|
+
userCount={userCount} // undefined while loading
|
|
973
|
+
/>
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
Velth has no Sign in, so it sets no `signIn`:
|
|
977
|
+
|
|
978
|
+
```tsx
|
|
979
|
+
const V_STORE_DOWNLOAD_URL = "https://projectv.uk/download/v-store";
|
|
980
|
+
|
|
981
|
+
<ProductHero
|
|
982
|
+
title="Your health, your control"
|
|
983
|
+
tagline="..."
|
|
984
|
+
primaryCta={{ label: "Try the demo", href: "#demo" }}
|
|
985
|
+
secondaryCta={{ label: "Download on V Store", href: V_STORE_DOWNLOAD_URL, rel: "nofollow" }}
|
|
986
|
+
visual={<OrbitRings />}
|
|
987
|
+
/>
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
#### Feeding `userCount`
|
|
991
|
+
|
|
992
|
+
The package does no fetching. The app calls a Convex action that wraps the Clerk backend SDK `clerk.users.getCount()` (it needs `CLERK_SECRET_KEY` on the server), once on mount or cached server-side. Pass `undefined` while loading and the positive number when it resolves. The row stays hidden for `undefined`, `null` and values of 0 or less, so a loading `0` never flashes "0+ people".
|
|
993
|
+
|
|
994
|
+
| Prop | |
|
|
995
|
+
| --- | --- |
|
|
996
|
+
| `title`, `tagline` | Nodes. |
|
|
997
|
+
| `primaryCta` | `{ label, href?, rel?, target?, onClick?, variant? }`. With `href` it renders a plain `a` (no framework link), and `rel` and `target` pass through to it. Otherwise it renders a `button`. Variant default `"default"`. |
|
|
998
|
+
| `secondaryCta` | Same shape. Optional. Variant default `"outline"`. |
|
|
999
|
+
| `signIn` | `{ href?, onClick? }`. Renders exactly one "Sign in" control (`secondary` variant). Omit it for none. Do not also put a Sign in in the page nav. |
|
|
1000
|
+
| `visual` | Node. An animated concept visual in the style of https://projectv.uk (ring, orbit, particles). **Not** an app replica or screenshot. If it animates, it must respect `prefers-reduced-motion`. |
|
|
1001
|
+
| `visualLabel` | Gives the visual region `role="img"` and this name. Without it the region is `aria-hidden`. |
|
|
1002
|
+
| `userCount` | Optional number. Shows "Free to use · {n}+ people already here" under the actions, with a green dot that pulses unless `prefers-reduced-motion: reduce`. `n` is formatted (`1284` is "1,284"). Hidden when `undefined`, `null` or 0 or less. See [Feeding `userCount`](#feeding-usercount). |
|
|
1003
|
+
| `showProductOf` | Default `true`: "a product of Project V" linking to `PROJECT_V_HOME_URL` (`https://projectv.uk`, new tab). The link target is fixed: no prop changes it. `false` hides the whole line. The text names no app, unlike `SiteFooter`. |
|
|
1004
|
+
| `className` | Added to the section. |
|
|
1005
|
+
|
|
1006
|
+
`SiteFooter` uses the same `PROJECT_V_HOME_URL`. A runnable playground is in `examples/product-hero/`.
|
|
1007
|
+
|
|
957
1008
|
### `PageTitle` and `PageHeader`
|
|
958
1009
|
|
|
959
1010
|
`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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React 19 UI for web apps: one main JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), 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",
|
|
@@ -109,14 +109,21 @@ const MAX_VISIBLE_ROWS = 8;
|
|
|
109
109
|
/** Max nested child rows shown under a subtask before "+N more". */
|
|
110
110
|
const MAX_VISIBLE_CHILDREN = 8;
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Both layouts show mid-turn reply text in the timeline, in the order it was
|
|
114
|
+
* written, so a live turn never stacks its first words below newer steps.
|
|
115
|
+
*/
|
|
116
|
+
const HIDDEN_TYPES = new Set<ActivityStep["type"]>(["thinking"]);
|
|
113
117
|
|
|
114
|
-
/**
|
|
115
|
-
const
|
|
118
|
+
/** A subagent's own text belongs to its child thread view, not under its row. */
|
|
119
|
+
const HIDDEN_CHILD_TYPES = new Set<ActivityStep["type"]>([
|
|
120
|
+
"thinking",
|
|
121
|
+
"response",
|
|
122
|
+
]);
|
|
116
123
|
|
|
117
124
|
/**
|
|
118
|
-
* `folded` (Codex-style): a settled turn collapses behind "Worked for …"
|
|
119
|
-
* long timelines cap behind "Show N more"
|
|
125
|
+
* `folded` (Codex-style): a settled turn collapses behind "Worked for …" and
|
|
126
|
+
* long timelines cap behind "Show N more".
|
|
120
127
|
* `inline` (Claude-style): every step and the text between them stay open.
|
|
121
128
|
*/
|
|
122
129
|
export type ActivityLayout = "folded" | "inline";
|
|
@@ -396,10 +403,8 @@ function ActivityStepRow({
|
|
|
396
403
|
if (step.type === "reasoning") return <ActivityReasoningLine step={step} />;
|
|
397
404
|
|
|
398
405
|
const Icon = iconForStep(step);
|
|
399
|
-
// A subagent's own text (`response` children) belongs to its child thread
|
|
400
|
-
// view, not the parent's timeline.
|
|
401
406
|
const childRows = (children ?? []).filter(
|
|
402
|
-
(child) => !
|
|
407
|
+
(child) => !HIDDEN_CHILD_TYPES.has(child.step.type),
|
|
403
408
|
);
|
|
404
409
|
const openSubagent =
|
|
405
410
|
step.type === "subtask" ? (
|
|
@@ -693,10 +698,9 @@ export const ActivityTasks = memo(
|
|
|
693
698
|
}: ActivityTasksProps) => {
|
|
694
699
|
const merge = useCn();
|
|
695
700
|
const inline = layout === "inline";
|
|
696
|
-
const hiddenTypes = inline ? INLINE_HIDDEN_TYPES : HIDDEN_TYPES;
|
|
697
701
|
const rows = buildActivityRows(steps).filter(
|
|
698
702
|
(row) =>
|
|
699
|
-
!
|
|
703
|
+
!HIDDEN_TYPES.has(row.step.type) &&
|
|
700
704
|
// A subagent's text whose Agent row is missing still belongs to its
|
|
701
705
|
// child thread, never to the parent's inline reply.
|
|
702
706
|
!(row.step.type === "response" && row.step.parentToolUseId),
|
package/src/client.ts
CHANGED
|
@@ -326,6 +326,9 @@ export type {
|
|
|
326
326
|
SiteFooterProps,
|
|
327
327
|
SiteFooterSocial,
|
|
328
328
|
} from "./marketing/SiteFooter";
|
|
329
|
+
export { ProductHero } from "./marketing/ProductHero";
|
|
330
|
+
export type { ProductHeroAction, ProductHeroProps } from "./marketing/ProductHero";
|
|
331
|
+
export { PROJECT_V_HOME_URL } from "./marketing/projectV";
|
|
329
332
|
export { PricingModal, V_PRO_APP_BENEFITS } from "./marketing/PricingModal";
|
|
330
333
|
export type {
|
|
331
334
|
PricingModalLegacyProps,
|
package/src/index.ts
CHANGED
|
@@ -326,6 +326,9 @@ export type {
|
|
|
326
326
|
SiteFooterProps,
|
|
327
327
|
SiteFooterSocial,
|
|
328
328
|
} from "./marketing/SiteFooter";
|
|
329
|
+
export { ProductHero } from "./marketing/ProductHero";
|
|
330
|
+
export type { ProductHeroAction, ProductHeroProps } from "./marketing/ProductHero";
|
|
331
|
+
export { PROJECT_V_HOME_URL } from "./marketing/projectV";
|
|
329
332
|
export { PricingModal, V_PRO_APP_BENEFITS } from "./marketing/PricingModal";
|
|
330
333
|
export type {
|
|
331
334
|
PricingModalLegacyProps,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { ComponentProps, MouseEventHandler, ReactNode } from "react";
|
|
2
|
+
import { Button } from "../ui/button";
|
|
3
|
+
import { pageTitleClass } from "../ui/page-title";
|
|
4
|
+
import { useMediaQuery } from "../hooks/use-media-query";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
import { landingShellClass, productHeroSocialProofClass } from "./marketing-classes";
|
|
7
|
+
import { PROJECT_V_HOME_URL } from "./projectV";
|
|
8
|
+
|
|
9
|
+
export type ProductHeroAction = {
|
|
10
|
+
label: string;
|
|
11
|
+
/** Renders an `a`. Without it the action renders a `button`. */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** Passed to the `a`. Only used with `href`. */
|
|
14
|
+
rel?: string;
|
|
15
|
+
/** Passed to the `a`. Only used with `href`. */
|
|
16
|
+
target?: string;
|
|
17
|
+
onClick?: MouseEventHandler;
|
|
18
|
+
/** Default: `"default"` for the primary action and `"outline"` for the secondary action. */
|
|
19
|
+
variant?: ComponentProps<typeof Button>["variant"];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ProductHeroProps = {
|
|
23
|
+
/** The only `h1` of the hero. Instrument Sans, weight 600, `leading-tight`, `tracking-tight` (`pageTitleClass`), at a display scale. */
|
|
24
|
+
title: ReactNode;
|
|
25
|
+
/** Short product description under the title. */
|
|
26
|
+
tagline: ReactNode;
|
|
27
|
+
primaryCta: ProductHeroAction;
|
|
28
|
+
secondaryCta?: ProductHeroAction;
|
|
29
|
+
/**
|
|
30
|
+
* Renders exactly one "Sign in" control. Omit it for none. The hero never renders two,
|
|
31
|
+
* so leave the page nav without its own Sign in. Set `href` or `onClick`.
|
|
32
|
+
*/
|
|
33
|
+
signIn?: { href?: string; onClick?: MouseEventHandler };
|
|
34
|
+
/**
|
|
35
|
+
* An animated concept visual in the style of https://projectv.uk: a glowing ring, orbit
|
|
36
|
+
* or particle field that conveys the idea of the product.
|
|
37
|
+
*
|
|
38
|
+
* It must NOT be an app replica, a screenshot or a mock of the product UI. Those age as
|
|
39
|
+
* the app changes and are the drift this component removes.
|
|
40
|
+
*
|
|
41
|
+
* If the visual animates, it must respect `prefers-reduced-motion` itself.
|
|
42
|
+
*/
|
|
43
|
+
visual: ReactNode;
|
|
44
|
+
/** Gives the visual region `role="img"` and this accessible name. Without it the region is decorative (`aria-hidden`). */
|
|
45
|
+
visualLabel?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Live user count for social proof under the actions: "Free to use · 1,284+ people already here",
|
|
48
|
+
* with a pulsing green dot (static under `prefers-reduced-motion: reduce`).
|
|
49
|
+
*
|
|
50
|
+
* The package does no fetching. The app calls a Convex action that wraps the Clerk backend SDK
|
|
51
|
+
* `clerk.users.getCount()` (needs `CLERK_SECRET_KEY`), once on mount or cached server-side.
|
|
52
|
+
* Pass `undefined` while loading and the positive number once it resolves. The row stays hidden
|
|
53
|
+
* for `undefined`, `null` and any value of 0 or less, so a loading `0` never shows "0+ people".
|
|
54
|
+
*/
|
|
55
|
+
userCount?: number | null;
|
|
56
|
+
/** Default `true`. Renders "a product of Project V", always linked to `PROJECT_V_HOME_URL`. The link target cannot be changed; `false` hides the whole line. */
|
|
57
|
+
showProductOf?: boolean;
|
|
58
|
+
className?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Same family, weight, leading and tracking as `PageTitle`; only the size changes with the viewport.
|
|
62
|
+
// Joined as plain strings: `cn` would drop `leading-tight` because a `text-*` size class conflicts with it.
|
|
63
|
+
const titleClass = `text-balance text-4xl sm:text-5xl lg:text-6xl ${pageTitleClass.replace(/\btext-xl\s*/, "")}`;
|
|
64
|
+
|
|
65
|
+
function UserCountProof({ count }: { count: number }) {
|
|
66
|
+
const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
|
|
67
|
+
return (
|
|
68
|
+
<p className={productHeroSocialProofClass} data-testid="user-count">
|
|
69
|
+
<span data-testid="user-count-dot" aria-hidden="true" className={cn("size-2 rounded-full bg-success", !reducedMotion && "animate-pulse")} />
|
|
70
|
+
<span>Free to use · {new Intl.NumberFormat("en-GB").format(count)}+ people already here</span>
|
|
71
|
+
</p>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ActionControl({ action, variant, size = "lg" }: { action: ProductHeroAction; variant: NonNullable<ProductHeroAction["variant"]>; size?: ComponentProps<typeof Button>["size"] }) {
|
|
76
|
+
const v = action.variant ?? variant;
|
|
77
|
+
if (action.href !== undefined) {
|
|
78
|
+
return (
|
|
79
|
+
<Button asChild variant={v} size={size}>
|
|
80
|
+
<a href={action.href} rel={action.rel} target={action.target} onClick={action.onClick}>
|
|
81
|
+
{action.label}
|
|
82
|
+
</a>
|
|
83
|
+
</Button>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return (
|
|
87
|
+
<Button type="button" variant={v} size={size} onClick={action.onClick}>
|
|
88
|
+
{action.label}
|
|
89
|
+
</Button>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Shared hero for a product landing page. Copy first, then the visual on mobile; side by
|
|
95
|
+
* side from `lg`. The title, calls to action, single Sign in and product-of line are fixed
|
|
96
|
+
* by the component so every product renders the same hero.
|
|
97
|
+
*/
|
|
98
|
+
export function ProductHero({ title, tagline, primaryCta, secondaryCta, signIn, visual, visualLabel, userCount, showProductOf = true, className }: ProductHeroProps) {
|
|
99
|
+
return (
|
|
100
|
+
<section className={cn(landingShellClass, "py-12 sm:py-16 lg:py-24", className)}>
|
|
101
|
+
<div className="grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
|
|
102
|
+
<div className="flex flex-col items-start text-left">
|
|
103
|
+
<h1 className={titleClass}>{title}</h1>
|
|
104
|
+
<p className="mt-4 max-w-xl text-pretty text-base leading-relaxed text-muted-foreground sm:mt-5 sm:text-lg">{tagline}</p>
|
|
105
|
+
<div className="mt-8 flex flex-wrap items-center gap-3">
|
|
106
|
+
<ActionControl action={primaryCta} variant="default" />
|
|
107
|
+
{secondaryCta ? <ActionControl action={secondaryCta} variant="outline" /> : null}
|
|
108
|
+
{signIn ? <ActionControl action={{ label: "Sign in", ...signIn }} variant="secondary" /> : null}
|
|
109
|
+
</div>
|
|
110
|
+
{userCount != null && userCount > 0 ? <UserCountProof count={userCount} /> : null}
|
|
111
|
+
{showProductOf ? (
|
|
112
|
+
<p className="mt-8 text-sm text-muted-foreground">
|
|
113
|
+
a product of{" "}
|
|
114
|
+
<a href={PROJECT_V_HOME_URL} target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">
|
|
115
|
+
Project V
|
|
116
|
+
</a>
|
|
117
|
+
</p>
|
|
118
|
+
) : null}
|
|
119
|
+
</div>
|
|
120
|
+
<div
|
|
121
|
+
className="relative flex min-h-64 items-center justify-center overflow-hidden lg:min-h-96"
|
|
122
|
+
{...(visualLabel ? { role: "img", "aria-label": visualLabel } : { "aria-hidden": true })}
|
|
123
|
+
>
|
|
124
|
+
{visual}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</section>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -3,6 +3,7 @@ import { IconBrandGithub, IconBrandInstagram, IconBrandX } from "@tabler/icons-r
|
|
|
3
3
|
import { APP_ICONS, AppWordmark, projectVAppsColumn, type AppName, type ProductAppName } from "../brand";
|
|
4
4
|
import { Button } from "../ui/button";
|
|
5
5
|
import { Separator } from "../ui/separator";
|
|
6
|
+
import { PROJECT_V_HOME_URL } from "./projectV";
|
|
6
7
|
import { siteFooterClasses, type SiteFooterVariant } from "./marketing-classes";
|
|
7
8
|
|
|
8
9
|
export type SiteFooterLink = {
|
|
@@ -178,7 +179,7 @@ export function SiteFooter(props: SiteFooterProps) {
|
|
|
178
179
|
{showProductOf ? (
|
|
179
180
|
<p className={c.rights}>
|
|
180
181
|
{props.app} is a product of{" "}
|
|
181
|
-
<a href=
|
|
182
|
+
<a href={PROJECT_V_HOME_URL} target="_blank" rel="noopener noreferrer" className="underline underline-offset-2">
|
|
182
183
|
Project V
|
|
183
184
|
</a>
|
|
184
185
|
.
|
|
@@ -125,3 +125,6 @@ export const pricingMultiClasses = {
|
|
|
125
125
|
switchIdle: "text-muted-foreground",
|
|
126
126
|
},
|
|
127
127
|
} as const;
|
|
128
|
+
|
|
129
|
+
/** Social proof row under the `ProductHero` actions. */
|
|
130
|
+
export const productHeroSocialProofClass = "mt-6 flex items-center gap-2 text-sm text-muted"; // check-tokens-allow: text-muted
|