@vvedantb/ui 0.37.1 → 0.38.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 +32 -3
- package/package.json +1 -1
- package/shell/src/SidebarRail.tsx +30 -5
- package/shell/src/app-shell-classes.ts +3 -0
- package/shell/src/index.ts +1 -0
- package/src/client.ts +3 -0
- package/src/index.ts +3 -0
- package/src/marketing/SiteHeader.tsx +128 -0
- package/src/marketing/createLandingKit.tsx +1 -1
package/README.md
CHANGED
|
@@ -324,7 +324,7 @@ import {
|
|
|
324
324
|
rail={
|
|
325
325
|
<SidebarRail
|
|
326
326
|
showDivider={hasPanel && !(isDesktop && collapsed)}
|
|
327
|
-
top={<RailTile asChild label="Home" active={isHome}><Link to="/"
|
|
327
|
+
top={<RailTile asChild app="vmem" label="Home" active={isHome}><Link to="/" /></RailTile>}
|
|
328
328
|
bodyClassName="scrollbar-thin"
|
|
329
329
|
footerClassName="border-t border-separator"
|
|
330
330
|
footer={<SidebarAccountMenu name={name} email={email} imageUrl={avatarUrl}><DropdownMenuItem>Settings</DropdownMenuItem></SidebarAccountMenu>}
|
|
@@ -344,7 +344,8 @@ import {
|
|
|
344
344
|
- `AppShell`: `sidebar`, `children`, `panelHidden` (main offsets by the rail width instead of the sidebar width), `skipLinkLabel` (`"Skip to main content"`), `mainId` (`"main-content"`), `className`, `mainClassName`, `contentClassName`.
|
|
345
345
|
- `AppSidebar`: legacy (still supported) `rail`, `panel` and `mobile*` props; compose `Sidebar.*` instead. `rail`, `panel` (not rendered while `panelHidden`), `collapsed`, `panelHidden`, `isDesktop`, `mobileOpen`, `onMobileOpenChange`, `id` (default from `useId`), `asideProps` (`data-*` attributes for the `<aside>`), `className`, `mobileTitle`, `mobileHeaderActions` (0.5.2; trailing actions such as icon buttons, in place of the end spacer), `mobileMenuIcon` (`(open) => node`, default a menu icon), `mobileMenuLabel` (`"Open navigation menu"`), `closeLabel` (`"Close navigation"`). Swipe left or Escape closes the drawer. The drawer is `inert` while closed on mobile, and closes when `isDesktop` becomes `true`. Close it on route change in the app. The mobile header is a three-column grid, so the title and trailing actions stay on one row and a long title truncates (0.5.4).
|
|
346
346
|
- `SidebarRail`: legacy (still supported) `top`, `footer`, `bodyAsChild`, `tabs` and `*ClassName` props; compose `Sidebar.Rail` parts instead. `top`, `children` (scroll body), `footer`, `showDivider`, `bodyAsChild` (the single child, for example a tabs list, becomes the body), `className`, `topClassName`, `bodyClassName`, `footerClassName`. Other HTML attributes (`id`, `data-*`, `aria-*`) go to the root `<div>`. The top and footer wrappers render only when their slot is set.
|
|
347
|
-
- `RailTile`: `label` (`aria-label` and tooltip), `active`, `tooltip`, `showTooltip` (default `true`), `badge` (inside the tile, after the children; style it with `railBadgeClass`), `asChild`, `className`. Without `asChild` it is a native `<button type="button">` (0.5.3), so icons keep their authored size; other props go to it. With `asChild` the child (for example a router link) gets the tile classes and `aria-current="page"` when active.
|
|
347
|
+
- `RailTile`: `label` (`aria-label` and tooltip), `active`, `tooltip`, `showTooltip` (default `true`), `badge` (inside the tile, after the children; style it with `railBadgeClass`), `asChild`, `app`, `className`. Without `asChild` it is a native `<button type="button">` (0.5.3), so icons keep their authored size; other props go to it. With `asChild` the child (for example a router link) gets the tile classes and `aria-current="page"` when active.
|
|
348
|
+
- Rail app icon: the home `RailTile` icon is 26px across the apps, enforced by the tile. Pass `app="vmem"` (any `ProductAppName`) and the tile renders `APP_ICONS[app]` at `SIDEBAR_RAIL_APP_ICON_SIZE` (`26`). With `asChild` it goes inside the child (`<Link to="/" />`); without it, `children` are ignored. Tiles without `app` are unchanged: the tile renders the icon you pass, at its size. `Sidebar.MobileHeader` is the standard mobile bar: 56px plus safe-area, solid `bg-surface`, centred `text-base font-semibold tracking-tight` title, 20px menu icon.
|
|
348
349
|
- `RailDivider`, `SidebarIconTooltip` (`label`, `enabled`, `side` `"right"`, `sideOffset` 8).
|
|
349
350
|
- `SidebarPanel`: legacy (still supported) `headerTrailing`, `onClose`, `closeLabel` and `closeButtonClassName`; compose `Sidebar.Panel` parts instead. `title`, `headerTrailing`, `onClose` (adds the close button; pass it on the mobile drawer only), `closeLabel`, `closeButtonClassName`, `collapsed` (`md:hidden`), `className`, `children`.
|
|
350
351
|
- `SidebarAccountMenu`: legacy (still supported) `badge`, `triggerProps`, `contentProps` and `stackedLabel`; compose `Sidebar.Account*` parts instead. `name`, `email` (two-line label when set), `stackedLabel` (two-line layout even without `email`, default `false`), `imageUrl` (initials without it), `align` (`"center"`), `side` (`"right"`), `sideOffset` (8), `collisionPadding`, `contentClassName`, `children` (menu items). 0.5.2 adds `badge` (unread dot beside the trigger, in a `relative` wrapper around the menu, so the trigger's dimming and press scale skip it: `true` shows it, `false` fades it out, omitted renders no wrapper and no dot), `triggerProps` (spread onto the trigger `Button` after the defaults; `className` merges last) and `contentProps` (spread onto `DropdownMenuContent`, for example `fitViewport` or `aria-label`; no `className`, use `contentClassName`). `SidebarAccountMenuPlaceholder` is the tile-sized loading box.
|
|
@@ -477,7 +478,7 @@ Convention: compound parts are static members of a namespace object, `<Namespace
|
|
|
477
478
|
| `ResizeHandle` | `SidebarResizeHandle` in a resizable `AppShell`, shown on desktop while the panel shows. Place it inside `Aside`. |
|
|
478
479
|
| `Rail` | Rail column. `showDivider`. |
|
|
479
480
|
| `RailHeader` / `RailBody` / `RailFooter` | Top block, scroll body (`asChild`, `tabs` for a Radix tabs list) and bottom block. `Footer` is an alias of `RailFooter`. |
|
|
480
|
-
| `RailItem` | Rail tile with tooltip (`RailTile`): `label`, `active`, `tooltip`, `showTooltip`, `badge`, `asChild`, `tabValue`. |
|
|
481
|
+
| `RailItem` | Rail tile with tooltip (`RailTile`): `label`, `active`, `tooltip`, `showTooltip`, `badge`, `asChild`, `app`, `tabValue`. |
|
|
481
482
|
| `RailSeparator` | Short rule between rail items (`RailDivider`). |
|
|
482
483
|
| `Tooltip` | Right-side icon tooltip (`SidebarIconTooltip`). |
|
|
483
484
|
| `Panel` | Panel column. `collapsed` (`md:hidden`) and `panelHidden` (renders nothing) default to the Root values. |
|
|
@@ -896,6 +897,34 @@ Constants: `landingShellClass`, `landingPanelClass`, `landingItemVariants`, and
|
|
|
896
897
|
for the footer's `actions` slot `landingFooterLinkClass` (a text link) and
|
|
897
898
|
`landingFooterButtonClass` (a `Button variant="link"`).
|
|
898
899
|
|
|
900
|
+
### `SiteHeader`
|
|
901
|
+
|
|
902
|
+
Marketing header (0.38.0): `AppWordmark` at `SITE_HEADER_WORDMARK_SIZE` (20px, icon 28px; no size prop), up to four
|
|
903
|
+
links from `md`, one CTA and an optional GitHub button. Below `md` the links, the CTA and GitHub sit in one full-width
|
|
904
|
+
dropdown menu, in that order. It is plain markup with no motion and needs no `createLandingKit`.
|
|
905
|
+
|
|
906
|
+
```tsx
|
|
907
|
+
<SiteHeader
|
|
908
|
+
app="vmem"
|
|
909
|
+
homeLabel="vmem home"
|
|
910
|
+
links={[{ label: "Features", href: "#features" }, { label: "Pricing", href: "#pricing" }]}
|
|
911
|
+
cta={<Button size="sm" className="rounded-full">Sign in</Button>}
|
|
912
|
+
githubUrl="https://github.com/vvedantb"
|
|
913
|
+
/>
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
| Prop | |
|
|
917
|
+
| --- | --- |
|
|
918
|
+
| `app` | `vibot`, `vmem`, `verve`, `velth` or `vhub`. |
|
|
919
|
+
| `homeLabel` | `aria-label` of the home wordmark link. |
|
|
920
|
+
| `homeHref` | Default `#home`. |
|
|
921
|
+
| `links` | `{ label, href }[]`, at most 4. More logs a development warning and only the first 4 render. |
|
|
922
|
+
| `cta` | One node. Use `Button` (default variant) with `size="sm"` and `className="rounded-full"`. Shows in the bar from `md` and full width in the menu below. No second slot. |
|
|
923
|
+
| `githubUrl` | Optional. Ghost icon button from `sm`, labelled "{app} on GitHub", and the last menu item. |
|
|
924
|
+
|
|
925
|
+
The bar is `sticky top-0 z-50`, `h-14 sm:h-16`, `bg-background/80 backdrop-blur-md` with a `border-separator` bottom border.
|
|
926
|
+
`createLandingKit().LandingNav` and `LandingNavMenu` are deprecated in favour of `SiteHeader`.
|
|
927
|
+
|
|
899
928
|
### `SiteFooter`
|
|
900
929
|
|
|
901
930
|
Brand, tagline and social icon buttons; link columns; then copyright, product-of line and legal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|
|
@@ -3,7 +3,9 @@ import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
|
3
3
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
4
4
|
import { IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftExpandFilled } from "@tabler/icons-react";
|
|
5
5
|
import { cn } from "../../src/utils/cn";
|
|
6
|
-
import {
|
|
6
|
+
import { APP_ICONS } from "../../src/brand/AppIcons";
|
|
7
|
+
import type { ProductAppName } from "../../src/brand/AppWordmark";
|
|
8
|
+
import { SIDEBAR_RAIL_APP_ICON_SIZE, railTileClass, railTileStateClass } from "./app-shell-classes";
|
|
7
9
|
import { SidebarIconTooltip } from "./SidebarIconTooltip";
|
|
8
10
|
|
|
9
11
|
export type SidebarRailProps = Omit<ComponentPropsWithoutRef<"div">, "children" | "className"> & {
|
|
@@ -111,7 +113,7 @@ export function RailFooter({ className, ...props }: SidebarRailFooterProps) {
|
|
|
111
113
|
return <div {...props} className={cn("flex w-full flex-col items-center gap-1.5 py-3", className)} />;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
type SidebarRailItemBaseProps = Omit<ComponentPropsWithoutRef<"button">, "children"> & {
|
|
115
117
|
/** `aria-label`, and the tooltip unless `tooltip` is set. */
|
|
116
118
|
label: string;
|
|
117
119
|
/** Active state. With `asChild`, also sets `aria-current="page"`. */
|
|
@@ -129,9 +131,27 @@ export type SidebarRailItemProps = Omit<ComponentPropsWithoutRef<"button">, "chi
|
|
|
129
131
|
* The label is sr-only text instead of `aria-label`; `asChild` is ignored.
|
|
130
132
|
*/
|
|
131
133
|
tabValue?: string;
|
|
132
|
-
children: ReactNode;
|
|
133
134
|
};
|
|
134
135
|
|
|
136
|
+
export type SidebarRailItemProps = SidebarRailItemBaseProps &
|
|
137
|
+
(
|
|
138
|
+
| {
|
|
139
|
+
/**
|
|
140
|
+
* Product app id for the home tile. The tile renders `APP_ICONS[app]` at
|
|
141
|
+
* `SIDEBAR_RAIL_APP_ICON_SIZE` (26px), so apps cannot drift. With `asChild` the
|
|
142
|
+
* icon goes inside the child element (for example `<Link to="/" />`). Without
|
|
143
|
+
* `asChild`, `children` are ignored.
|
|
144
|
+
*/
|
|
145
|
+
app: ProductAppName;
|
|
146
|
+
children?: ReactNode;
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
app?: undefined;
|
|
150
|
+
/** The tile icon. The caller controls its size. */
|
|
151
|
+
children: ReactNode;
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
135
155
|
/** Rail tile with a right-side tooltip. Renders a native `<button>`, its child with `asChild`, or a tabs trigger with `tabValue`. */
|
|
136
156
|
export function RailItem({
|
|
137
157
|
label,
|
|
@@ -142,9 +162,13 @@ export function RailItem({
|
|
|
142
162
|
asChild = false,
|
|
143
163
|
tabValue,
|
|
144
164
|
className,
|
|
165
|
+
app,
|
|
145
166
|
children,
|
|
146
167
|
...props
|
|
147
168
|
}: SidebarRailItemProps) {
|
|
169
|
+
const AppIcon = app ? APP_ICONS[app] : null;
|
|
170
|
+
const appIcon = AppIcon ? <AppIcon size={SIDEBAR_RAIL_APP_ICON_SIZE} /> : null;
|
|
171
|
+
const content = appIcon ?? children;
|
|
148
172
|
return (
|
|
149
173
|
<SidebarIconTooltip label={tooltip ?? label} enabled={showTooltip}>
|
|
150
174
|
{tabValue !== undefined ? (
|
|
@@ -153,7 +177,7 @@ export function RailItem({
|
|
|
153
177
|
value={tabValue}
|
|
154
178
|
className={cn(railTileClass, "group relative", railTileStateClass(active), className)}
|
|
155
179
|
>
|
|
156
|
-
{
|
|
180
|
+
{content}
|
|
157
181
|
<span className="sr-only">{label}</span>
|
|
158
182
|
{badge}
|
|
159
183
|
</TabsPrimitive.Trigger>
|
|
@@ -164,6 +188,7 @@ export function RailItem({
|
|
|
164
188
|
aria-current={active ? "page" : undefined}
|
|
165
189
|
className={cn(railTileClass, "group", railTileStateClass(active), className)}
|
|
166
190
|
>
|
|
191
|
+
{appIcon}
|
|
167
192
|
<Slottable>{children}</Slottable>
|
|
168
193
|
{badge}
|
|
169
194
|
</Slot>
|
|
@@ -174,7 +199,7 @@ export function RailItem({
|
|
|
174
199
|
aria-label={label}
|
|
175
200
|
className={cn(railTileClass, "group", railTileStateClass(active), className)}
|
|
176
201
|
>
|
|
177
|
-
{
|
|
202
|
+
{content}
|
|
178
203
|
{badge}
|
|
179
204
|
</button>
|
|
180
205
|
)}
|
|
@@ -5,6 +5,9 @@ import { cn } from "../../src/utils/cn";
|
|
|
5
5
|
// accent colour names match that markup and are allow-listed for check:tokens
|
|
6
6
|
// on the marked lines only.
|
|
7
7
|
|
|
8
|
+
/** App icon size in px for the home `RailTile`. Fixed across the apps. `RailTile` applies it when you pass `app`. */
|
|
9
|
+
export const SIDEBAR_RAIL_APP_ICON_SIZE = 26;
|
|
10
|
+
|
|
8
11
|
/** Rail tile: size-11, rounded-lg, press scale. */
|
|
9
12
|
export const railTileClass =
|
|
10
13
|
"relative flex size-11 items-center justify-center rounded-lg border active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring";
|
package/shell/src/index.ts
CHANGED
package/src/client.ts
CHANGED
|
@@ -257,6 +257,7 @@ export type { SignOutDialogProps } from "../shell/src/SignOutDialog";
|
|
|
257
257
|
export {
|
|
258
258
|
railBadgeClass,
|
|
259
259
|
railTileClass,
|
|
260
|
+
SIDEBAR_RAIL_APP_ICON_SIZE,
|
|
260
261
|
railTileStateClass,
|
|
261
262
|
sidebarNavIconClass,
|
|
262
263
|
sidebarNavItemActionsClass,
|
|
@@ -313,6 +314,8 @@ export {
|
|
|
313
314
|
landingPanelClass,
|
|
314
315
|
landingShellClass,
|
|
315
316
|
} from "./marketing/marketing-classes";
|
|
317
|
+
export { SITE_HEADER_WORDMARK_SIZE, SiteHeader } from "./marketing/SiteHeader";
|
|
318
|
+
export type { SiteHeaderLink, SiteHeaderProps } from "./marketing/SiteHeader";
|
|
316
319
|
export type { PricingModalTone, SiteFooterVariant } from "./marketing/marketing-classes";
|
|
317
320
|
export { PROJECT_V_LEGAL_LINKS, PROJECT_V_SOCIALS, SITE_FOOTER_WORDMARK_SIZE, SiteFooter } from "./marketing/SiteFooter";
|
|
318
321
|
export type {
|
package/src/index.ts
CHANGED
|
@@ -257,6 +257,7 @@ export type { SignOutDialogProps } from "../shell/src/SignOutDialog";
|
|
|
257
257
|
export {
|
|
258
258
|
railBadgeClass,
|
|
259
259
|
railTileClass,
|
|
260
|
+
SIDEBAR_RAIL_APP_ICON_SIZE,
|
|
260
261
|
railTileStateClass,
|
|
261
262
|
sidebarNavIconClass,
|
|
262
263
|
sidebarNavItemActionsClass,
|
|
@@ -313,6 +314,8 @@ export {
|
|
|
313
314
|
landingPanelClass,
|
|
314
315
|
landingShellClass,
|
|
315
316
|
} from "./marketing/marketing-classes";
|
|
317
|
+
export { SITE_HEADER_WORDMARK_SIZE, SiteHeader } from "./marketing/SiteHeader";
|
|
318
|
+
export type { SiteHeaderLink, SiteHeaderProps } from "./marketing/SiteHeader";
|
|
316
319
|
export type { PricingModalTone, SiteFooterVariant } from "./marketing/marketing-classes";
|
|
317
320
|
export { PROJECT_V_LEGAL_LINKS, PROJECT_V_SOCIALS, SITE_FOOTER_WORDMARK_SIZE, SiteFooter } from "./marketing/SiteFooter";
|
|
318
321
|
export type {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
2
|
+
import { IconMenu2, IconX } from "@tabler/icons-react";
|
|
3
|
+
import { AppWordmark, type ProductAppName } from "../brand/AppWordmark";
|
|
4
|
+
import { GitHubIcon } from "../logos";
|
|
5
|
+
import { Button } from "../ui/button";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from "../ui/dropdown-menu";
|
|
13
|
+
import { cn } from "../utils/cn";
|
|
14
|
+
import { landingClasses, landingShellClass } from "./marketing-classes";
|
|
15
|
+
|
|
16
|
+
/** Wordmark text size in px. The icon is `Math.round(20 * 1.4)` = 28px. There is no size prop. */
|
|
17
|
+
export const SITE_HEADER_WORDMARK_SIZE = 20;
|
|
18
|
+
|
|
19
|
+
const MAX_LINKS = 4;
|
|
20
|
+
|
|
21
|
+
export type SiteHeaderLink = { label: string; href: string };
|
|
22
|
+
|
|
23
|
+
export type SiteHeaderProps = {
|
|
24
|
+
/** Product app id for `AppWordmark`. */
|
|
25
|
+
app: ProductAppName;
|
|
26
|
+
/** Home link `href`. Default `#home`. */
|
|
27
|
+
homeHref?: string;
|
|
28
|
+
/** `aria-label` of the home wordmark link. */
|
|
29
|
+
homeLabel: string;
|
|
30
|
+
/** Up to 4 section links, shown from `md`. More than 4 logs a development warning. */
|
|
31
|
+
links: readonly SiteHeaderLink[];
|
|
32
|
+
/**
|
|
33
|
+
* Exactly one call to action, for example the app's Clerk "Sign in" or "Open <App>" button, or "Get the app".
|
|
34
|
+
* Pass a `Button` with the default variant, `size="sm"` and `className="rounded-full"`. It shows in the bar from `md`
|
|
35
|
+
* and below `md` it sits full width in the menu.
|
|
36
|
+
*/
|
|
37
|
+
cta: ReactNode;
|
|
38
|
+
/** Optional GitHub URL. An icon button from `sm`, and the last item in the mobile menu. */
|
|
39
|
+
githubUrl?: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const focusRing =
|
|
44
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
|
|
45
|
+
|
|
46
|
+
function SiteHeaderMenu({ links, cta, githubUrl }: Pick<SiteHeaderProps, "links" | "cta" | "githubUrl">) {
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
const Icon = open ? IconX : IconMenu2;
|
|
49
|
+
return (
|
|
50
|
+
<DropdownMenu open={open} onOpenChange={setOpen}>
|
|
51
|
+
<DropdownMenuTrigger asChild>
|
|
52
|
+
<Button variant="ghost" size="icon-sm" aria-label={open ? "Close menu" : "Open menu"} className="md:hidden">
|
|
53
|
+
<Icon size={20} />
|
|
54
|
+
</Button>
|
|
55
|
+
</DropdownMenuTrigger>
|
|
56
|
+
<DropdownMenuContent
|
|
57
|
+
align="end"
|
|
58
|
+
className="max-h-[70dvh] w-[calc(100vw-2.5rem)] max-w-none overflow-y-auto sm:w-[calc(100vw-4rem)]"
|
|
59
|
+
>
|
|
60
|
+
{links.slice(0, MAX_LINKS).map((link) => (
|
|
61
|
+
<DropdownMenuItem key={link.href} asChild className="max-sm:py-2.5">
|
|
62
|
+
<a href={link.href}>{link.label}</a>
|
|
63
|
+
</DropdownMenuItem>
|
|
64
|
+
))}
|
|
65
|
+
<div data-site-header-cta="" className="p-1 [&>*]:w-full">
|
|
66
|
+
{cta}
|
|
67
|
+
</div>
|
|
68
|
+
{githubUrl ? (
|
|
69
|
+
<>
|
|
70
|
+
<DropdownMenuSeparator />
|
|
71
|
+
<DropdownMenuItem asChild className="max-sm:py-2.5">
|
|
72
|
+
<a href={githubUrl} target="_blank" rel="noreferrer">
|
|
73
|
+
<GitHubIcon size={16} />
|
|
74
|
+
GitHub
|
|
75
|
+
</a>
|
|
76
|
+
</DropdownMenuItem>
|
|
77
|
+
</>
|
|
78
|
+
) : null}
|
|
79
|
+
</DropdownMenuContent>
|
|
80
|
+
</DropdownMenu>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Marketing header for the Project V apps: sticky, blurred, `AppWordmark` at `SITE_HEADER_WORDMARK_SIZE`, up to four
|
|
86
|
+
* links, one `cta` and an optional GitHub button. Below `md` the links, the CTA and GitHub move into one dropdown menu.
|
|
87
|
+
* Plain markup with no motion, so any app can use it.
|
|
88
|
+
*/
|
|
89
|
+
export function SiteHeader({ app, homeHref = "#home", homeLabel, links, cta, githubUrl, className }: SiteHeaderProps) {
|
|
90
|
+
const tooMany = links.length > MAX_LINKS;
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (!tooMany) return;
|
|
93
|
+
const nodeEnv = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env?.NODE_ENV;
|
|
94
|
+
if (nodeEnv === "production") return;
|
|
95
|
+
console.warn(`[SiteHeader] SiteHeader supports at most ${MAX_LINKS} links. Received ${links.length}.`);
|
|
96
|
+
}, [tooMany, links.length]);
|
|
97
|
+
|
|
98
|
+
const shown = links.slice(0, MAX_LINKS);
|
|
99
|
+
return (
|
|
100
|
+
<header className={cn("sticky top-0 z-50 border-b border-separator bg-background/80 backdrop-blur-md", className)}>
|
|
101
|
+
<div className={cn(landingShellClass, "flex h-14 items-center justify-between gap-3 sm:h-16")}>
|
|
102
|
+
<a href={homeHref} className={cn("group min-w-0 shrink-0 rounded-lg", focusRing)} aria-label={homeLabel}>
|
|
103
|
+
<AppWordmark app={app} size={SITE_HEADER_WORDMARK_SIZE} />
|
|
104
|
+
</a>
|
|
105
|
+
|
|
106
|
+
<nav className="hidden items-center gap-1 md:flex" aria-label="Page">
|
|
107
|
+
{shown.map((link) => (
|
|
108
|
+
<a key={link.href} href={link.href} className={landingClasses.navLink}>
|
|
109
|
+
{link.label}
|
|
110
|
+
</a>
|
|
111
|
+
))}
|
|
112
|
+
</nav>
|
|
113
|
+
|
|
114
|
+
<div className="flex items-center gap-1.5">
|
|
115
|
+
{githubUrl ? (
|
|
116
|
+
<Button asChild variant="ghost" size="icon-sm" className="hidden sm:inline-flex">
|
|
117
|
+
<a href={githubUrl} target="_blank" rel="noreferrer" aria-label={`${app} on GitHub`}>
|
|
118
|
+
<GitHubIcon size={18} />
|
|
119
|
+
</a>
|
|
120
|
+
</Button>
|
|
121
|
+
) : null}
|
|
122
|
+
<div className="hidden md:flex">{cta}</div>
|
|
123
|
+
<SiteHeaderMenu links={shown} cta={cta} githubUrl={githubUrl} />
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</header>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -153,7 +153,7 @@ export function createLandingKit({ motion }: LandingKitOptions): LandingKit {
|
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/** Sticky blurred header that rises in on mount. */
|
|
156
|
+
/** @deprecated Use `SiteHeader`. Sticky blurred header that rises in on mount. */
|
|
157
157
|
function LandingNav({ brand, homeLabel, links, github, actions, menu }: LandingNavProps) {
|
|
158
158
|
return (
|
|
159
159
|
<motion.header
|