From 8c4f66f182b2bb387a0a175955e8affdae222870 Mon Sep 17 00:00:00 2001 From: Vasanth T Date: Tue, 6 Oct 2026 10:48:35 +0530 Subject: [PATCH] ui: shared foundations: Geist type, chain marks, vendor kit Geist for text and figures, Geist Mono for addresses and code, chain logos, social marks, the token tint helpers, the vendored motion pieces, the section header chip, and a hydration-safe wallet hook. --- app/src/app/fonts.ts | 10 +- app/src/app/global-error.tsx | 4 +- app/src/app/globals.css | 85 +++++++++++-- app/src/app/layout.tsx | 4 +- app/src/app/opengraph-image.tsx | 6 +- .../app/t/[chain]/[token]/opengraph-image.tsx | 12 +- app/src/components/WalletMenu.module.css | 6 +- app/src/components/launchpad/AdminQueue.tsx | 5 +- app/src/components/launchpad/BrandMarks.tsx | 8 ++ app/src/components/launchpad/ChainLogo.tsx | 44 +++++++ app/src/components/launchpad/LaunchTape.tsx | 2 +- app/src/components/launchpad/TokenAvatar.tsx | 10 +- app/src/components/sections/SectionIntro.tsx | 8 +- .../sections/SectionShell.module.css | 25 ++-- app/src/components/theme.test.ts | 10 +- app/src/components/ui.ts | 14 ++- app/src/components/vendor/LICENSES.md | 2 + app/src/components/vendor/animated-list.tsx | 33 +++++ .../components/vendor/animated-shiny-text.tsx | 21 ++++ app/src/components/vendor/border-beam.tsx | 41 +++++++ app/src/components/vendor/flickering-grid.tsx | 93 ++++++++++++++ app/src/components/vendor/magic-card.tsx | 28 +++++ app/src/components/vendor/marquee.tsx | 36 ++++++ app/src/components/vendor/shine-border.tsx | 25 ++++ app/src/components/vendor/tabs.tsx | 15 ++- app/src/components/vendor/toggle-group.tsx | 58 +++++++-- app/src/lib/launchpad/socials.test.ts | 17 +++ app/src/lib/launchpad/socials.ts | 26 ++++ app/src/lib/launchpad/tint.test.ts | 68 +++++++++++ app/src/lib/launchpad/tint.ts | 113 ++++++++++++++++++ app/src/lib/ogFonts.ts | 25 +--- app/src/lib/useHydratedAccount.ts | 24 ++++ 32 files changed, 785 insertions(+), 93 deletions(-) create mode 100644 app/src/components/launchpad/BrandMarks.tsx create mode 100644 app/src/components/launchpad/ChainLogo.tsx create mode 100644 app/src/components/vendor/animated-list.tsx create mode 100644 app/src/components/vendor/animated-shiny-text.tsx create mode 100644 app/src/components/vendor/border-beam.tsx create mode 100644 app/src/components/vendor/flickering-grid.tsx create mode 100644 app/src/components/vendor/magic-card.tsx create mode 100644 app/src/components/vendor/marquee.tsx create mode 100644 app/src/components/vendor/shine-border.tsx create mode 100644 app/src/lib/launchpad/socials.test.ts create mode 100644 app/src/lib/launchpad/socials.ts create mode 100644 app/src/lib/launchpad/tint.test.ts create mode 100644 app/src/lib/launchpad/tint.ts create mode 100644 app/src/lib/useHydratedAccount.ts diff --git a/app/src/app/fonts.ts b/app/src/app/fonts.ts index 60ba14b2..200b59d3 100644 --- a/app/src/app/fonts.ts +++ b/app/src/app/fonts.ts @@ -1,10 +1,12 @@ -import { Inter, Space_Mono, Unbounded } from "next/font/google"; +import { Geist, Geist_Mono } from "next/font/google"; /** * Font loaders shared by the root layout and global-error.tsx. * global-error replaces the root layout when active, so it cannot inherit * these variables — it imports them from here instead of duplicating them. + * + * One family for the interface, figures included (Geist, as most launchpads keep a single grotesk); Geist Mono only + * for addresses and transaction hashes, where every character must line up. */ -export const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" }); -export const spaceMono = Space_Mono({ subsets: ["latin"], weight: ["400", "700"], variable: "--font-space-mono", display: "swap" }); -export const unbounded = Unbounded({ subsets: ["latin"], weight: ["600", "700"], variable: "--font-unbounded", display: "swap" }); +export const geist = Geist({ subsets: ["latin"], variable: "--font-geist", display: "swap" }); +export const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono", display: "swap" }); diff --git a/app/src/app/global-error.tsx b/app/src/app/global-error.tsx index 57c1de95..4268c6a9 100644 --- a/app/src/app/global-error.tsx +++ b/app/src/app/global-error.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useEffect } from "react"; import "./globals.css"; -import { inter, spaceMono, unbounded } from "./fonts"; +import { geist, geistMono } from "./fonts"; import { errorBody, errorRegionLabel, sanitizeDigest } from "@/lib/error-copy"; /** @@ -24,7 +24,7 @@ export default function GlobalError({ }, [error]); const digest = sanitizeDigest(error.digest); return ( - +

500

diff --git a/app/src/app/globals.css b/app/src/app/globals.css index 11c15a93..d4fbc062 100644 --- a/app/src/app/globals.css +++ b/app/src/app/globals.css @@ -10,9 +10,13 @@ /* Chart code reads tokens at runtime; retain even tokens with no utility class. */ @theme static { - --font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - --font-mono: var(--font-space-mono), ui-monospace, SFMono-Regular, Menlo, monospace; - --font-display: var(--font-unbounded), var(--font-inter), ui-sans-serif, system-ui, sans-serif; + --font-sans: var(--font-geist), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + /* figures: every price, amount and count. The same Geist as the words, with tabular digits (see .font-mono below), + the way other launchpads set their numbers; a typewriter face for money read as a terminal, not a market. */ + --font-mono: var(--font-geist), ui-sans-serif, system-ui, sans-serif; + /* addresses and transaction hashes: a true monospace, so 0x strings line up and read character by character */ + --font-code: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace; + --font-display: var(--font-geist), ui-sans-serif, system-ui, sans-serif; /* surfaces */ --color-paper: #fafaf8; @@ -53,16 +57,69 @@ --color-chart-grid: #f1f0ee; --color-chart-vol: #cbd5e1; + /* the dim second voice of a display headline: a neutral grey (no slate cast against the warm paper) */ + --color-dim: #737373; + /* decorative glows (card edges, hover halos, beams, the hero grid): neutral on light, brand blue on dark, + so blue on light stays the action colour alone */ + --color-glow: rgb(15 23 42 / 0.3); + --color-glow-soft: rgb(15 23 42 / 0.035); + /* segmented controls (components/vendor/toggle-group.tsx): a recessed track and a raised thumb that slides */ + --color-track: #f1f1ef; + --color-thumb: #ffffff; + --shadow-card: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 16px rgba(15, 23, 42, 0.04); --shadow-card-hover: 0 2px 4px rgba(15, 23, 42, 0.06), 0 10px 28px rgba(15, 23, 42, 0.09); --shadow-sheet: 0 -8px 40px rgba(15, 23, 42, 0.12); --shadow-dialog: 0 24px 64px rgba(15, 23, 42, 0.18); + --shadow-track: inset 0 0 0 1px rgba(15, 23, 42, 0.05), inset 0 1px 2px rgba(15, 23, 42, 0.06); + --shadow-thumb: 0 0 0 1px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(15, 23, 42, 0.1), 0 3px 8px -3px rgba(15, 23, 42, 0.14); + + /* Magic UI motion (components/vendor, MIT): each one stops under reduced motion at its call site */ + --animate-marquee: marquee var(--duration) infinite linear; + --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite; + --animate-shiny-text: shiny-text 8s infinite; + --animate-shine: shine var(--duration) infinite linear; +} + +@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(calc(-100% - var(--gap))); } } +@keyframes marquee-vertical { from { transform: translateY(0); } to { transform: translateY(calc(-100% - var(--gap))); } } +@keyframes shiny-text { + 0%, 90%, 100% { background-position: calc(-100% - var(--shiny-width)) 0; } + 30%, 60% { background-position: calc(100% + var(--shiny-width)) 0; } +} +@keyframes shine { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 100%; } + to { background-position: 0% 0%; } } +/* A band of light that crosses a filled call to action now and then (and once, quickly, on hover). The host needs + relative + overflow-hidden. A shine, not a colour: white at low opacity over the button's own fill. */ +.bb-sheen::after { + content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; + background: linear-gradient(105deg, transparent 38%, rgb(255 255 255 / 0.34) 50%, transparent 62%); + transform: translateX(-100%); + animation: bb-sheen 5s linear 1.2s infinite; /* a steady 1.2s crossing every 5s */ +} +@keyframes bb-sheen { 0%, 76% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } +@media (prefers-reduced-motion: reduce) { .bb-sheen::after { display: none; } } + +/* The hero's first-paint entrance: pure CSS so the words are there before any script runs. `--d` staggers it. */ +.bb-rise { animation: bb-rise 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both; animation-delay: var(--d, 0ms); } +@keyframes bb-rise { from { opacity: 0; transform: translateY(12px); filter: blur(8px); } to { opacity: 1; transform: none; filter: none; } } +@media (prefers-reduced-motion: reduce) { .bb-rise { animation: none; } } + :root { color-scheme: light; } +/* The page column, boxed like other launchpads: 72rem by default, and 80rem on the market pages (home and token), + which hold a .bb-mid element, so the board fits four cards a row and the chart sits beside its trade column, with + margins showing on any laptop screen. The header's resting row lines up with whichever column the page uses. */ +:root { --page-max: 72rem; --page-pad: 1rem; } +:root:has(.bb-mid) { --page-max: 80rem; --page-pad: clamp(1rem, 2.5vw, 2rem); } +.bb-page { width: 100%; max-width: var(--page-max); margin-inline: auto; padding-inline: var(--page-pad); } + /* ── Dark: black ground, alpha ramp of white, near-zero chroma greys. The blue and green brighten because the light values drop under 3:1 on black. ── */ .dark { @@ -94,11 +151,18 @@ --color-holder-good-bg: #06241a; --color-chart-grid: #1a1a1a; --color-chart-vol: #333333; + --color-dim: #8c8c8c; + --color-glow: #4d8bff; + --color-glow-soft: rgb(77 139 255 / 0.09); + --color-track: #000000; + --color-thumb: #232323; --color-scrim: #000000; --shadow-card: 0 1px 2px #00000026, 0 4px 16px #00000026; --shadow-card-hover: 0 2px 4px #00000033, 0 10px 28px #00000040; --shadow-sheet: 0 -8px 40px #00000066; --shadow-dialog: 0 24px 64px #00000080; + --shadow-track: inset 0 0 0 1px #1f1f1f, inset 0 1px 2px #00000099; + --shadow-thumb: inset 0 1px 0 #ffffff17, 0 0 0 1px #ffffff0d, 0 2px 6px -1px #000000b3; } /* Black paper does not print: a class-based theme survives into the print @@ -132,11 +196,11 @@ body { overflow-x: clip; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; - font-feature-settings: "cv11", "ss01"; } -/* tabular numbers everywhere money shows */ -.tnum { +/* tabular numbers everywhere money shows; figures (font-mono) always line up in columns */ +.tnum, +.font-mono { font-variant-numeric: tabular-nums; } @@ -277,7 +341,14 @@ input[type="number"] { -moz-appearance: textfield; appearance: textfield; } @keyframes bb-row-sell { 0% { background-color: var(--color-down-soft); border-color: var(--color-down); } 100% { background-color: var(--color-card); border-color: var(--color-line); } } .bb-row-buy { animation: bb-row-buy 2.2s ease-out 1; } .bb-row-sell { animation: bb-row-sell 2.2s ease-out 1; } -@media (prefers-reduced-motion: reduce) { .bb-row-new, .bb-row-buy, .bb-row-sell { animation: none; } } +/* the card layout's version: a ring in the event's colour instead of a row fill */ +@keyframes bb-card-new { 0% { box-shadow: 0 0 0 2px var(--color-ink); } 100% { box-shadow: 0 0 0 2px transparent; } } +@keyframes bb-card-buy { 0% { box-shadow: 0 0 0 2px var(--color-up), 0 0 28px -6px var(--color-up); } 100% { box-shadow: 0 0 0 2px transparent, 0 0 28px -6px transparent; } } +@keyframes bb-card-sell { 0% { box-shadow: 0 0 0 2px var(--color-down), 0 0 28px -6px var(--color-down); } 100% { box-shadow: 0 0 0 2px transparent, 0 0 28px -6px transparent; } } +.bb-card-new { animation: bb-card-new 1.6s ease-out 1; } +.bb-card-buy { animation: bb-card-buy 2.2s ease-out 1; } +.bb-card-sell { animation: bb-card-sell 2.2s ease-out 1; } +@media (prefers-reduced-motion: reduce) { .bb-row-new, .bb-row-buy, .bb-row-sell, .bb-card-new, .bb-card-buy, .bb-card-sell { animation: none; } } /* ── loading system: route progress bar, skeleton shimmer, spinner ─────────── */ /* scaleX rather than width: the bar animates on the compositor instead of diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index 07146233..0571eeb9 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -14,7 +14,7 @@ import Footer from "@/components/Footer"; import RouteProgress from "@/components/RouteProgress"; import ThemeProvider from "@/components/ThemeProvider"; import { Suspense } from "react"; -import { inter, spaceMono, unbounded } from "./fonts"; +import { geist, geistMono } from "./fonts"; const TITLE = SITE_TITLE; const DESCRIPTION = SITE_DESCRIPTION; @@ -44,7 +44,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo // Organization + WebSite + WebApplication: the entity record search engines reconcile the brand word against. const siteJsonLd = siteJsonLdHtml({ siteUrl: SITE_URL, brand: BRAND, domain: BRAND_DOMAIN, description: SITE_DESCRIPTION, sameAs: [`https://x.com/${BRAND_X}`, BRAND_GITHUB] }); return ( - +