From 6d65f89f89999ccd0f7d7014ec0a29daa5c4c530 Mon Sep 17 00:00:00 2001 From: peace node Date: Thu, 3 Sep 2026 18:35:41 -0400 Subject: [PATCH 1/5] feat(console): rebrand early access shell --- app/globals.css | 424 ++++++++++++++++++++-------- app/icon.svg | 12 + app/layout.tsx | 78 ++++- app/opengraph-image.tsx | 44 +++ app/robots.ts | 4 +- app/sitemap.ts | 2 +- components.json | 24 ++ components/console/CopyButton.tsx | 16 +- components/console/TabStrip.tsx | 1 - components/console/ThemeContext.tsx | 94 ++---- components/design-system/Button.tsx | 2 +- package.json | 2 + pnpm-lock.yaml | 74 +++++ 13 files changed, 568 insertions(+), 209 deletions(-) create mode 100644 app/icon.svg create mode 100644 app/opengraph-image.tsx create mode 100644 components.json diff --git a/app/globals.css b/app/globals.css index 554199e..bda6a79 100644 --- a/app/globals.css +++ b/app/globals.css @@ -18,49 +18,126 @@ --color-blue-dark: #145571; --color-blue-subtle: rgba(20, 106, 143, 0.15); - /* Legacy literal-dark tokens — used as the console's primary surface - vocabulary (`bg-dark` / `bg-dark-lighter` / `bg-dark-card` / - `bg-dark-border`, 78+ usages). Flip to light values via the - html[data-theme="light"] block below. */ - --color-dark: #121212; - --color-dark-lighter: #181818; - --color-dark-card: #1e1e1e; - --color-dark-border: #2a2a2a; - - /* Semantic tokens — values overridden directly on :root and - html[data-theme="light"]. (Initial values here are dark-theme defaults.) */ - --color-background: #121212; - --color-foreground: #ffffff; - --color-surface: #181818; - --color-card: #1e1e1e; - --color-border: rgba(255, 255, 255, 0.08); - --color-muted-foreground: rgba(255, 255, 255, 0.7); - --color-subtle-foreground: rgba(255, 255, 255, 0.5); - - --font-sans: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif; - --font-mono: var(--font-geist-mono), ui-monospace, monospace; -} - -/* ── Dark theme (default) ──────────────────────────────────────── */ + /* Legacy console utilities retained as aliases over the registry theme. */ + --color-dark: var(--background); + --color-dark-lighter: var(--muted); + --color-dark-card: var(--card); + --color-dark-border: var(--border); + + /* Semantic aliases consumed by existing components. */ + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-surface: var(--muted); + --color-card: var(--card); + --color-border: var(--border); + --color-muted-foreground: var(--muted-foreground); + --color-subtle-foreground: color-mix( + in oklch, + var(--foreground) 50%, + transparent + ); + + --font-inter: InterVariable, Inter, ui-sans-serif, system-ui, sans-serif; + --font-display: "Favorit", "Favorit Pro", var(--font-inter), sans-serif; + --font-heading: var(--font-inter), Inter, sans-serif; + --font-sans: var(--font-inter), Inter, ui-sans-serif, system-ui, sans-serif; + --font-mono: "Favorit Mono", ui-monospace, monospace; + --font-weight-book: 350; +} + +/* ── Registry dark theme (default) ──────────────────────────────── */ :root { - --color-background: #121212; - --color-foreground: #ffffff; - --color-surface: #181818; - --color-card: #1e1e1e; - --color-border: rgba(255, 255, 255, 0.08); - --color-muted-foreground: rgba(255, 255, 255, 0.7); - --color-subtle-foreground: rgba(255, 255, 255, 0.5); + --background: oklch(0 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.16 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-surface: var(--muted); + --color-card: var(--card); + --color-border: var(--border); + --color-muted-foreground: var(--muted-foreground); + --color-subtle-foreground: color-mix( + in oklch, + var(--foreground) 50%, + transparent + ); } -/* ── Light theme override ──────────────────────────────────────── */ +/* ── Registry light theme override ──────────────────────────────── */ html[data-theme="light"] { - --color-background: #ffffff; - --color-foreground: #121212; - --color-surface: #f6f7f5; - --color-card: #fafbfa; - --color-border: rgba(18, 18, 18, 0.1); - --color-muted-foreground: rgba(18, 18, 18, 0.65); - --color-subtle-foreground: rgba(18, 18, 18, 0.45); + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-surface: var(--muted); + --color-card: var(--card); + --color-border: var(--border); + --color-muted-foreground: var(--muted-foreground); + --color-subtle-foreground: color-mix( + in oklch, + var(--foreground) 45%, + transparent + ); color-scheme: light; } @@ -68,11 +145,17 @@ html[data-theme="light"] { html { scroll-behavior: smooth; color-scheme: dark; + max-width: 100%; + overflow-x: clip; + overscroll-behavior: none; } body { background-color: var(--color-background); color: var(--color-foreground); + max-width: 100%; + overflow-x: clip; + overscroll-behavior: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; transition: @@ -80,6 +163,46 @@ body { color 0.25s ease; } +* { + scrollbar-width: thin; + scrollbar-color: var(--muted-foreground) transparent; +} + +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background: var(--muted-foreground); + border-radius: 999px; +} + +.console-toaster[data-x-position="center"] { + left: 50vw !important; + right: auto !important; + width: max-content !important; + max-width: calc(100vw - 24px) !important; + transform: translateX(-50%) !important; +} + +@supports not (overflow: clip) { + html, + body { + overflow-x: hidden; + } +} + +.overflow-auto, +.overflow-x-auto, +.overflow-y-auto { + overscroll-behavior: none; +} + /* Text wrap utilities */ .text-balance { text-wrap: balance; @@ -111,29 +234,29 @@ body { --color-fg-muted: var(--color-muted-foreground); --color-fg-faint: var(--color-subtle-foreground); --color-fg-label: var(--color-subtle-foreground); - --color-fg-disabled: rgba(255, 255, 255, 0.25); + --color-fg-disabled: color-mix(in oklch, var(--foreground) 25%, transparent); /* Border ramp */ - --color-border-hairline: rgba(255, 255, 255, 0.06); - --color-border-subtle: var(--color-border); - --color-border-strong: rgba(255, 255, 255, 0.15); + --color-border-hairline: color-mix(in oklch, var(--border) 60%, transparent); + --color-border-subtle: var(--border); + --color-border-strong: var(--input); /* Surface ramp */ - --color-shell: #070707; - --color-dark-surface: #1a1a1a; - --color-surface-raised: #242424; + --color-shell: var(--background); + --color-dark-surface: var(--muted); + --color-surface-raised: var(--popover); /* State tints */ - --color-zebra: rgba(255, 255, 255, 0.025); - --color-hover: rgba(255, 255, 255, 0.04); - --color-tint: rgba(255, 255, 255, 0.06); - --color-active: rgba(255, 255, 255, 0.07); - --color-pop: rgba(255, 255, 255, 0.1); + --color-zebra: color-mix(in oklch, var(--foreground) 2.5%, transparent); + --color-hover: color-mix(in oklch, var(--foreground) 4%, transparent); + --color-tint: color-mix(in oklch, var(--foreground) 6%, transparent); + --color-active: color-mix(in oklch, var(--foreground) 7%, transparent); + --color-pop: color-mix(in oklch, var(--foreground) 10%, transparent); --color-overlay: rgba(0, 0, 0, 0.5); /* Sidebar card */ - --color-sidebar-card-bg: #181818; - --color-sidebar-card-bg-hover: #1e1e1e; + --color-sidebar-card-bg: var(--card); + --color-sidebar-card-bg-hover: var(--accent); /* Chart series — categorical identity for capabilities (table dots, sparklines, tooltips). Seven slots in a FIXED order; never cycled — an @@ -151,11 +274,15 @@ body { --color-series-other: rgba(255, 255, 255, 0.28); /* CTA */ - --color-cta: var(--color-green); - --color-cta-hover: var(--color-green-light); - --color-cta-active: var(--color-green-dark); - --color-cta-border: var(--color-green-light); - --color-cta-fg: #ffffff; + --color-cta: var(--primary); + --color-cta-hover: color-mix(in oklch, var(--primary) 86%, var(--background)); + --color-cta-active: color-mix( + in oklch, + var(--primary) 76%, + var(--background) + ); + --color-cta-border: transparent; + --color-cta-fg: var(--primary-foreground); /* Accents */ --color-warm: #f59e0b; @@ -185,12 +312,7 @@ body { --motion-easing-spring: cubic-bezier(0.32, 0.72, 0, 1); /* Chart palette */ - --chart-revenue: #40bf86; - --chart-1: #40bf86; - --chart-2: #25abd0; - --chart-3: #e5a536; - --chart-4: #d94f70; - --chart-5: #8b5cf6; + --chart-revenue: var(--chart-2); --chart-6: #06b6d4; --chart-7: #f59e0b; --chart-8: #ec4899; @@ -208,29 +330,28 @@ body { /* Light-mode console token overrides */ html[data-theme="light"] { - --color-fg-disabled: rgba(24, 24, 27, 0.25); - --color-border-hairline: rgba(24, 24, 27, 0.1); - --color-border-strong: rgba(24, 24, 27, 0.2); - - /* Flip the literal-dark surface vocabulary in light mode */ - --color-dark: #fafafa; - --color-dark-lighter: #ffffff; - --color-dark-card: #f4f4f5; - --color-dark-border: #e4e4e7; - - --color-shell: #f4f4f5; - --color-dark-surface: #f4f4f5; - --color-surface-raised: #ffffff; - - --color-zebra: rgba(24, 24, 27, 0.025); - --color-hover: rgba(24, 24, 27, 0.05); - --color-tint: rgba(24, 24, 27, 0.06); - --color-active: rgba(24, 24, 27, 0.08); - --color-pop: rgba(24, 24, 27, 0.1); + --color-fg-disabled: color-mix(in oklch, var(--foreground) 25%, transparent); + --color-border-hairline: color-mix(in oklch, var(--border) 80%, transparent); + --color-border-strong: var(--input); + + --color-dark: var(--background); + --color-dark-lighter: var(--muted); + --color-dark-card: var(--card); + --color-dark-border: var(--border); + + --color-shell: var(--background); + --color-dark-surface: var(--muted); + --color-surface-raised: var(--popover); + + --color-zebra: color-mix(in oklch, var(--foreground) 2.5%, transparent); + --color-hover: color-mix(in oklch, var(--foreground) 5%, transparent); + --color-tint: color-mix(in oklch, var(--foreground) 6%, transparent); + --color-active: color-mix(in oklch, var(--foreground) 8%, transparent); + --color-pop: color-mix(in oklch, var(--foreground) 10%, transparent); --color-overlay: rgba(24, 24, 27, 0.32); --color-sidebar-card-bg: transparent; - --color-sidebar-card-bg-hover: rgba(24, 24, 27, 0.05); + --color-sidebar-card-bg-hover: var(--accent); /* Chart series — the light-surface steps of the same seven slots. Three (aqua, yellow, magenta) sit under 3:1 on white; the breakdown table is @@ -244,11 +365,14 @@ html[data-theme="light"] { --color-series-7: #4a3aa7; --color-series-other: rgba(24, 24, 27, 0.3); - /* Light-mode CTA flips to zinc-900 (Vercel/Linear/Stripe convention) */ - --color-cta: #18181b; - --color-cta-hover: #27272a; - --color-cta-active: #09090b; - --color-cta-border: #27272a; + --color-cta: var(--primary); + --color-cta-hover: color-mix(in oklch, var(--primary) 86%, var(--background)); + --color-cta-active: color-mix( + in oklch, + var(--primary) 76%, + var(--background) + ); + --color-cta-border: transparent; /* Legibility steps for accents on light surfaces */ --color-green-bright: #1e9960; @@ -315,23 +439,15 @@ textarea:-webkit-autofill { of button fills — a page with six green buttons has no accent left. Both fills key off foreground/background, so light mode inverts for free. */ .btn-primary { - background-color: var(--color-foreground); - color: var(--color-background); + background-color: var(--primary); + color: var(--primary-foreground); border: 1px solid transparent; } .btn-primary:hover:not(:disabled) { - background-color: color-mix( - in srgb, - var(--color-foreground) 86%, - var(--color-background) - ); + background-color: color-mix(in oklch, var(--primary) 86%, var(--background)); } .btn-primary:active:not(:disabled) { - background-color: color-mix( - in srgb, - var(--color-foreground) 76%, - var(--color-background) - ); + background-color: color-mix(in oklch, var(--primary) 76%, var(--background)); } /* The outline recipe. Console chrome comes in several heights (26/30/36px), so @@ -339,27 +455,30 @@ textarea:-webkit-autofill { it with a Button size is preferred; hand-rolled sizing is fine, a hand-rolled *palette* is not, which is how two different outline looks shipped. */ .btn-outline { - border: 1px solid var(--color-border-hairline); - background-color: var(--color-dark-card); - color: var(--color-fg-strong); + border: 1px solid var(--border); + background-color: var(--secondary); + color: var(--secondary-foreground); } .btn-outline:hover:not(:disabled) { - border-color: var(--color-border-subtle); - color: var(--color-fg); + background-color: var(--accent); + color: var(--accent-foreground); } -/* Reserved for the rare action that should read as Livepeer-green rather than - neutral. Deliberately not the default — see the accent rule in CLAUDE.md. */ +/* Legacy `accent` callers now use the registry accent role. */ .btn-accent { - background-color: var(--color-cta); - color: var(--color-cta-fg); - border: 1px solid var(--color-cta-border); + background-color: var(--accent); + color: var(--accent-foreground); + border: 1px solid var(--border); } .btn-accent:hover:not(:disabled) { - background-color: var(--color-cta-hover); + background-color: var(--secondary); } .btn-accent:active:not(:disabled) { - background-color: var(--color-cta-active); + background-color: color-mix( + in oklch, + var(--secondary) 85%, + var(--foreground) + ); } /* ─── Tone palette utilities ─── */ @@ -450,3 +569,82 @@ textarea:-webkit-autofill { .animate-status-halo { animation: statusHalo 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; } + +@theme inline { + --font-inter: InterVariable, Inter, ui-sans-serif, system-ui, sans-serif; + --font-display: "Favorit", "Favorit Pro", var(--font-inter), sans-serif; + --font-heading: var(--font-inter), Inter, sans-serif; + --font-sans: var(--font-inter), Inter, ui-sans-serif, system-ui, sans-serif; + --font-mono: "Favorit Mono", ui-monospace, monospace; + --font-weight-book: 350; + --color-foreground: var(--foreground); + --color-background: var(--background); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); + --text-ui-caption: 12px; + --text-ui-caption--line-height: 16px; + --text-ui-caption--font-weight: 500; + --text-ui-caption--letter-spacing: 0; + --text-ui-body: 14px; + --text-ui-body--line-height: 20px; + --text-ui-body--font-weight: 400; + --text-ui-body--letter-spacing: 0; + --text-reading-body: 16px; + --text-reading-body--line-height: 28px; + --text-reading-body--font-weight: 400; + --text-reading-body--letter-spacing: 0; + --text-page-title: 32px; + --text-page-title--line-height: 0.98; + --text-page-title--font-weight: 300; + --text-page-title--letter-spacing: 0; + --text-display-sm: 36px; + --text-display-sm--line-height: 0.98; + --text-display-sm--font-weight: 300; + --text-display-sm--letter-spacing: 0; + --text-display-md: 48px; + --text-display-md--line-height: 0.98; + --text-display-md--font-weight: 300; + --text-display-md--letter-spacing: 0; + --text-display-lg: 60px; + --text-display-lg--line-height: 0.98; + --text-display-lg--font-weight: 300; + --text-display-lg--letter-spacing: 0; + --text-display-fluid: 64px; + --text-display-fluid--line-height: 0.98; + --text-display-fluid--font-weight: 300; + --text-display-fluid--letter-spacing: 0; +} diff --git a/app/icon.svg b/app/icon.svg new file mode 100644 index 0000000..f7b5b96 --- /dev/null +++ b/app/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/app/layout.tsx b/app/layout.tsx index 76679d7..b4db325 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,26 +1,53 @@ import type { Metadata } from "next"; -import { GeistSans } from "geist/font/sans"; -import { GeistMono } from "geist/font/mono"; +import type { CSSProperties } from "react"; +import { Toaster } from "sonner"; import "./globals.css"; +const SITE_TITLE = "Livepeer Early Access"; +const SITE_DESCRIPTION = + "Explore Livepeer AI apps, manage API access, and track usage during early access."; + export const metadata: Metadata = { metadataBase: new URL( process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` - : "https://dashboard.livepeer.org" + : "https://earlyaccess.livepeer.org" ), - title: "Livepeer Console", - description: - "Browse AI apps, manage API keys, and monitor usage on the Livepeer network.", + title: SITE_TITLE, + description: SITE_DESCRIPTION, + applicationName: SITE_TITLE, + icons: { + icon: "/icon.svg", + }, + openGraph: { + title: SITE_TITLE, + description: SITE_DESCRIPTION, + siteName: SITE_TITLE, + type: "website", + images: [ + { + url: "/opengraph-image", + width: 1200, + height: 630, + alt: SITE_TITLE, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: SITE_TITLE, + description: SITE_DESCRIPTION, + images: ["/opengraph-image"], + }, }; // FOUT prevention — runs synchronously before paint so dual-source CSS -// variables resolve to the stored theme on first render. With no stored -// preference we fall back to "system", i.e. the OS `prefers-color-scheme`. -// ThemeProvider in the (app) layout takes over after hydration. -const THEME_INIT_SCRIPT = `(function(){try{var s=localStorage.getItem('theme')||'system';var d=s==='dark'||(s!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.dataset.theme=d?'dark':'light';}catch(e){document.documentElement.dataset.theme='dark';}})();`; +// variables resolve from the OS theme immediately. ThemeProvider in the +// console layout keeps this in sync after hydration. +const THEME_INIT_SCRIPT = `(function(){try{document.documentElement.dataset.theme=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}catch(e){document.documentElement.dataset.theme='dark';}})();`; +const TOASTER_STYLE = { "--width": "max-content" } as CSSProperties; export default function RootLayout({ children, @@ -28,16 +55,37 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - + + +