feat: Updated marketing site
This commit is contained in:
@@ -31,7 +31,7 @@ interface Pulse {
|
||||
/*
|
||||
* Drawn from the shared tokens rather than read from them: this is canvas, so
|
||||
* the values have to be literals. They are the light-theme accent falling to
|
||||
* the app ground, with packets in the dark-theme accent — the same two blues
|
||||
* the app ground, with packets in the dark-theme accent the same two blues
|
||||
* the marketing site's hero is built on.
|
||||
*/
|
||||
const BG_TOP = "#0b2a58"; // --accent, light
|
||||
@@ -137,7 +137,7 @@ export function NetworkBackground() {
|
||||
dir: Math.random() < 0.5 ? 1 : -1,
|
||||
t: 0,
|
||||
speed: 0.18 + Math.random() * 0.22, // ~2.5-5.5s per edge
|
||||
delay: (initial ? Math.random() * 8 : Math.random() * 6),
|
||||
delay: initial ? Math.random() * 8 : Math.random() * 6,
|
||||
life: 0,
|
||||
};
|
||||
}
|
||||
@@ -290,11 +290,5 @@ export function NetworkBackground() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none fixed inset-0 -z-10 h-full w-full"
|
||||
/>
|
||||
);
|
||||
return <canvas ref={canvasRef} aria-hidden="true" className="pointer-events-none fixed inset-0 -z-10 h-full w-full" />;
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ const navItems: NavItem[] = [
|
||||
{ href: "/settings", label: "Settings", icon: <SettingsIcon />, adminOnly: true },
|
||||
];
|
||||
|
||||
/** Shared by the permanent aside and the offcanvas drawer — one copy of the nav. */
|
||||
/** Shared by the permanent aside and the offcanvas drawer one copy of the nav. */
|
||||
export function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const pathname = usePathname();
|
||||
const { user, instance, isAdmin } = useAuth();
|
||||
@@ -159,9 +159,7 @@ export function SidebarContent({ onNavigate }: { onNavigate?: () => void }) {
|
||||
<Logo className="h-8 w-8 text-logo" />
|
||||
<div className="min-w-0">
|
||||
<span className="block text-base font-extrabold leading-tight tracking-[-0.035em] text-text-primary">Vantage</span>
|
||||
{instance && (
|
||||
<span className="block truncate font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">{instance.name}</span>
|
||||
)}
|
||||
{instance && <span className="block truncate font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">{instance.name}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -252,20 +250,8 @@ export function SidebarDrawer({ open, onClose }: { open: boolean; onClose: () =>
|
||||
}, [open, onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed inset-0 z-50 lg:hidden",
|
||||
open ? "visible" : "invisible pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
onClick={onClose}
|
||||
className={clsx(
|
||||
"absolute inset-0 bg-black/60 transition-opacity duration-200",
|
||||
open ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className={clsx("fixed inset-0 z-50 lg:hidden", open ? "visible" : "invisible pointer-events-none")}>
|
||||
<div aria-hidden="true" onClick={onClose} className={clsx("absolute inset-0 bg-black/60 transition-opacity duration-200", open ? "opacity-100" : "opacity-0")} />
|
||||
<div
|
||||
ref={panelRef}
|
||||
id="app-sidebar-drawer"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Card } from "@/components/ui";
|
||||
/*
|
||||
* Every block on the settings page is one of these: icon, title, one line of
|
||||
* explanation, then the controls. Members and SSO adopted it when they moved
|
||||
* off their own page — a card that looked different would read as a different
|
||||
* off their own page a card that looked different would read as a different
|
||||
* kind of thing rather than another setting.
|
||||
*/
|
||||
export function SectionCard({
|
||||
|
||||
+22
-22
@@ -3,9 +3,9 @@ import { clsx } from "clsx";
|
||||
type Variant = "success" | "warning" | "danger" | "neutral" | "accent";
|
||||
|
||||
interface BadgeProps {
|
||||
variant?: Variant;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: Variant;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -13,31 +13,31 @@ interface BadgeProps {
|
||||
* no fill. Every badge in this app holds one short state word, which is what
|
||||
* the treatment is for.
|
||||
*
|
||||
* State is never carried by colour alone — the word itself says it, and the
|
||||
* State is never carried by colour alone the word itself says it, and the
|
||||
* three state variants add a dot so the distinction survives a monochrome
|
||||
* screen as well as it survives colour blindness.
|
||||
*/
|
||||
const variantClasses: Record<Variant, string> = {
|
||||
success: "text-success",
|
||||
warning: "text-warning",
|
||||
danger: "text-danger",
|
||||
neutral: "text-text-secondary",
|
||||
accent: "text-accent",
|
||||
success: "text-success",
|
||||
warning: "text-warning",
|
||||
danger: "text-danger",
|
||||
neutral: "text-text-secondary",
|
||||
accent: "text-accent",
|
||||
};
|
||||
|
||||
export function Badge({ variant = "neutral", children, className }: BadgeProps) {
|
||||
const hasState = variant === "success" || variant === "warning" || variant === "danger";
|
||||
const hasState = variant === "success" || variant === "warning" || variant === "danger";
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-sm border border-current px-1.5 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.08em]",
|
||||
variantClasses[variant],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{hasState && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-sm border border-current px-1.5 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.08em]",
|
||||
variantClasses[variant],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{hasState && <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+73
-94
@@ -4,7 +4,7 @@ import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
||||
/*
|
||||
* Below sm the table stops being a table: the head is hidden, each row becomes
|
||||
* a bordered card and each cell becomes a label/value pair. That lives here
|
||||
* rather than in the six pages that render tables — two markup trees per page
|
||||
* rather than in the six pages that render tables two markup trees per page
|
||||
* would drift apart on the first edit, and every one of those trees would mean
|
||||
* the same thing.
|
||||
*
|
||||
@@ -14,115 +14,94 @@ import { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from "react";
|
||||
*/
|
||||
|
||||
export function Table({ className, children, ...props }: HTMLAttributes<HTMLTableElement>) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table
|
||||
className={clsx("w-full border-collapse text-sm max-sm:block", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className={clsx("w-full border-collapse text-sm max-sm:block", className)} {...props}>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Thead({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return (
|
||||
<thead className={clsx("border-b border-border max-sm:hidden", className)} {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
return (
|
||||
<thead className={clsx("border-b border-border max-sm:hidden", className)} {...props}>
|
||||
{children}
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tbody({ className, children, ...props }: HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return (
|
||||
<tbody
|
||||
className={clsx(
|
||||
"divide-y divide-border",
|
||||
"max-sm:block max-sm:space-y-3 max-sm:divide-y-0 max-sm:p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
return (
|
||||
<tbody className={clsx("divide-y divide-border", "max-sm:block max-sm:space-y-3 max-sm:divide-y-0 max-sm:p-3", className)} {...props}>
|
||||
{children}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tr({ className, children, ...props }: HTMLAttributes<HTMLTableRowElement>) {
|
||||
return (
|
||||
<tr
|
||||
className={clsx(
|
||||
"transition-colors hover:bg-surface-2/50",
|
||||
"max-sm:block max-sm:rounded max-sm:border max-sm:border-border max-sm:bg-surface-2/40 max-sm:p-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
return (
|
||||
<tr className={clsx("transition-colors hover:bg-surface-2/50", "max-sm:block max-sm:rounded max-sm:border max-sm:border-border max-sm:bg-surface-2/40 max-sm:p-3", className)} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function Th({ className, children, ...props }: ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<th
|
||||
className={clsx(
|
||||
// site/'s keyed-label idiom: mono, small, widely tracked, dimmed.
|
||||
// A column head is a key, not prose.
|
||||
// text-secondary, not tertiary: a column head is how you navigate the
|
||||
// table, and tertiary lands under 4.5:1 at this size.
|
||||
"px-4 py-3 text-left font-mono text-[0.68rem] uppercase tracking-[0.13em] text-text-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
return (
|
||||
<th
|
||||
className={clsx(
|
||||
// site/'s keyed-label idiom: mono, small, widely tracked, dimmed.
|
||||
// A column head is a key, not prose.
|
||||
// text-secondary, not tertiary: a column head is how you navigate the
|
||||
// table, and tertiary lands under 4.5:1 at this size.
|
||||
"px-4 py-3 text-left font-mono text-[0.68rem] uppercase tracking-[0.13em] text-text-secondary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
interface TdProps extends TdHTMLAttributes<HTMLTableCellElement> {
|
||||
/**
|
||||
* The column head this cell belongs to, shown beside the value below sm
|
||||
* where the real head is hidden. Omit on a trailing action cell — an action
|
||||
* needs no key, and the button then sits alone on its own row in the card.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* The column head this cell belongs to, shown beside the value below sm
|
||||
* where the real head is hidden. Omit on a trailing action cell an action
|
||||
* needs no key, and the button then sits alone on its own row in the card.
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function Td({ className, label, children, ...props }: TdProps) {
|
||||
return (
|
||||
<td
|
||||
className={clsx(
|
||||
"px-4 py-3 text-text-primary",
|
||||
"max-sm:flex max-sm:items-start max-sm:gap-4 max-sm:px-0 max-sm:py-1.5",
|
||||
// Exactly one justify class — clsx picks it. Emitting both and relying
|
||||
// on string order would not work: Tailwind's output order decides which
|
||||
// of two same-property utilities wins, not the order in this array.
|
||||
label ? "max-sm:justify-between" : "max-sm:justify-end max-sm:pt-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{label && (
|
||||
<span className="hidden shrink-0 font-mono text-[0.68rem] uppercase leading-5 tracking-[0.13em] text-text-secondary max-sm:inline">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
{/*
|
||||
* Below sm the cell is a flex row, and a flex item's default
|
||||
* min-width:auto resolves to its min-content width. A fingerprint or a
|
||||
* target URL is a single unbreakable token, so min-content was the whole
|
||||
* string: the value refused to shrink and spilled out of the card.
|
||||
* min-w-0 lets it shrink, break-words gives the token somewhere to break.
|
||||
*
|
||||
* Above sm the wrapper is display:contents, so the desktop table renders
|
||||
* exactly as it did before. Every other class here is max-sm: scoped on
|
||||
* purpose — display:contents generates no box, but inherited properties
|
||||
* (overflow-wrap among them) would still reach the children.
|
||||
*/}
|
||||
<span className="max-sm:min-w-0 max-sm:break-words sm:contents">{children}</span>
|
||||
</td>
|
||||
);
|
||||
return (
|
||||
<td
|
||||
className={clsx(
|
||||
"px-4 py-3 text-text-primary",
|
||||
"max-sm:flex max-sm:items-start max-sm:gap-4 max-sm:px-0 max-sm:py-1.5",
|
||||
// Exactly one justify class clsx picks it. Emitting both and relying
|
||||
// on string order would not work: Tailwind's output order decides which
|
||||
// of two same-property utilities wins, not the order in this array.
|
||||
label ? "max-sm:justify-between" : "max-sm:justify-end max-sm:pt-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{label && <span className="hidden shrink-0 font-mono text-[0.68rem] uppercase leading-5 tracking-[0.13em] text-text-secondary max-sm:inline">{label}</span>}
|
||||
{/*
|
||||
* Below sm the cell is a flex row, and a flex item's default
|
||||
* min-width:auto resolves to its min-content width. A fingerprint or a
|
||||
* target URL is a single unbreakable token, so min-content was the whole
|
||||
* string: the value refused to shrink and spilled out of the card.
|
||||
* min-w-0 lets it shrink, break-words gives the token somewhere to break.
|
||||
*
|
||||
* Above sm the wrapper is display:contents, so the desktop table renders
|
||||
* exactly as it did before. Every other class here is max-sm: scoped on
|
||||
* purpose display:contents generates no box, but inherited properties
|
||||
* (overflow-wrap among them) would still reach the children.
|
||||
*/}
|
||||
<span className="max-sm:min-w-0 max-sm:break-words sm:contents">{children}</span>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user