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 ( - +{shortAddr(item.kind === "launch" ? item.launcher : item.trader)}{item.kind === "launch" && item.lp_fee === 0 ? , 0% trading fee : null}
+{shortAddr(item.kind === "launch" ? item.launcher : item.trader)}{item.kind === "launch" && item.lp_fee === 0 ? , 0% trading fee : null}
); diff --git a/app/src/components/launchpad/TokenAvatar.tsx b/app/src/components/launchpad/TokenAvatar.tsx index 04aa738b..5152b35c 100644 --- a/app/src/components/launchpad/TokenAvatar.tsx +++ b/app/src/components/launchpad/TokenAvatar.tsx @@ -1,13 +1,7 @@ "use client"; import { useState } from "react"; - -/** Deterministic hue from an address — same identity trick as the tape dots. */ -export function hueOf(addr: string): number { - let h = 0; - for (let i = 2; i < Math.min(addr.length, 18); i++) h = (h * 31 + addr.charCodeAt(i)) % 360; - return h; -} +import { addressHue } from "@/lib/launchpad/tint"; /** * Token image with a graceful fallback: a soft gradient tile with the first letter of the symbol @@ -17,7 +11,7 @@ export function hueOf(addr: string): number { export default function TokenAvatar({ token, symbol, image, size = 40, className = "" }: { chain?: string; token: string; symbol: string; image?: string | null; size?: number; className?: string }) { const [failedSrc, setFailedSrc] = useState{eyebrow}
+{icon ? : null}{eyebrow}
{title}
diff --git a/app/src/components/sections/SectionShell.module.css b/app/src/components/sections/SectionShell.module.css index 4232a531..8c4b554e 100644 --- a/app/src/components/sections/SectionShell.module.css +++ b/app/src/components/sections/SectionShell.module.css @@ -1,20 +1,25 @@ .page { max-width: 1152px; margin-inline: auto; padding: 40px 16px 32px; min-width: 0; } -.intro { padding-bottom: 32px; border-bottom: 1px solid var(--color-line); margin-bottom: 32px; } -.eyebrow { display: flex; align-items: center; gap: 9px; color: var(--color-muted); font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; } -.eyebrow > span { width: 6px; height: 6px; background: var(--color-brand); border-radius: 1px; } +.intro { margin-bottom: 36px; } +/* the page's label as a chip, like the one on the home hero: sentence case, never a tracked all-caps eyebrow */ +.eyebrow { display: inline-flex; align-items: center; gap: 7px; max-width: 100%; min-height: 28px; padding: 0 11px; border: 1px solid var(--color-line); border-radius: 999px; background: var(--color-card); color: var(--color-body); font-size: 12px; font-weight: 500; } +.eyebrowIcon { display: inline-flex; flex-shrink: 0; color: var(--color-brand); } +.eyebrowIcon svg { width: 14px; height: 14px; } .introRow { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-top: 18px; } .introCopy { min-width: 0; max-width: 740px; } -.title { color: var(--color-ink); font-size: 32px; line-height: 1.14; font-weight: 700; letter-spacing: -.04em; text-wrap: balance; overflow-wrap: anywhere; } -.description { max-width: 620px; margin-top: 16px; color: var(--color-body); font-size: 15px; line-height: 1.7; text-wrap: pretty; } +.title { color: var(--color-ink); font-size: 32px; line-height: 1.12; font-weight: 700; letter-spacing: -.04em; text-wrap: balance; overflow-wrap: anywhere; } +.description { max-width: 620px; margin-top: 14px; color: var(--color-body); font-size: 15px; line-height: 1.7; text-wrap: pretty; } .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; flex-shrink: 0; } -.action { display: inline-flex; justify-content: center; align-items: center; gap: 12px; min-height: 44px; padding: 10px 17px; border: 1px solid var(--color-line-strong); border-radius: 999px; color: var(--color-ink); font-size: 13px; font-weight: 600; transition: background-color 160ms, border-color 160ms; } -.action:hover { background: var(--color-card); border-color: var(--color-ink); } +/* the site's secondary button: a card-coloured rounded rectangle with a strong hairline */ +.action { display: inline-flex; justify-content: center; align-items: center; gap: 8px; min-height: 44px; padding: 0 18px; border: 1px solid var(--color-line-strong); border-radius: 12px; background: var(--color-card); color: var(--color-ink); font-size: 14px; font-weight: 600; white-space: nowrap; transition: background-color 160ms, border-color 160ms, transform 150ms; } +.action:hover { background: var(--color-paper); border-color: color-mix(in srgb, var(--color-ink) 40%, transparent); } +.action:active { transform: scale(.98); } +.action:disabled { opacity: .5; cursor: wait; } .textLink { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; font-size: 13px; color: var(--color-body); } .textLink:hover { color: var(--color-ink); } -.panel { min-width: 0; border: 1px solid var(--color-line); border-radius: 16px; background: var(--color-paper); overflow: hidden; } +.panel { min-width: 0; border: 1px solid var(--color-line); border-radius: 16px; background: var(--color-card); overflow: hidden; } .panelHeading { min-height: 64px; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; border-bottom: 1px solid var(--color-line); color: var(--color-ink); font-size: 14px; font-weight: 600; } .anchorSection { scroll-margin-top: 104px; } .page :is(a, button, summary, input, textarea):focus-visible { outline: 2px solid var(--color-brand); outline-offset: 3px; } -@media (min-width: 640px) { .page { padding-top: 56px; } .title { font-size: 44px; } .description { font-size: 16px; } } +@media (min-width: 640px) { .page { padding-top: 48px; } .title { font-size: 44px; } .description { font-size: 16px; } } @media (max-width: 1023px) { .introRow { align-items: flex-start; flex-direction: column; } } -@media (prefers-reduced-motion: reduce) { .action { transition: none; } } +@media (prefers-reduced-motion: reduce) { .action { transition: none; } .action:active { transform: none; } } diff --git a/app/src/components/theme.test.ts b/app/src/components/theme.test.ts index 30099aaa..3f87c52f 100644 --- a/app/src/components/theme.test.ts +++ b/app/src/components/theme.test.ts @@ -60,10 +60,12 @@ test("light is the original Clear Sky palette, unchanged, and the unclassed base assert.match(darkBlock, /color-scheme:\s*dark/); }); -test("typography is unchanged", () => { - assert.match(themeBlock, /--font-sans:\s*var\(--font-inter\)/); - assert.match(themeBlock, /--font-mono:\s*var\(--font-space-mono\)/); - assert.match(themeBlock, /--font-display:\s*var\(--font-unbounded\)/); +test("typography is one family: Geist for words and figures, Geist Mono only for addresses and hashes", () => { + assert.match(themeBlock, /--font-sans:\s*var\(--font-geist\)/); + assert.match(themeBlock, /--font-mono:\s*var\(--font-geist\)/); + assert.match(themeBlock, /--font-code:\s*var\(--font-geist-mono\)/); + assert.match(themeBlock, /--font-display:\s*var\(--font-geist\)/); + assert.match(css, /\.font-mono\s*\{\s*font-variant-numeric:\s*tabular-nums;/, "figures line up in columns"); }); test("the dark: variant resolves against a class, not a media query", () => { diff --git a/app/src/components/ui.ts b/app/src/components/ui.ts index fb45a9b4..2041cc35 100644 --- a/app/src/components/ui.ts +++ b/app/src/components/ui.ts @@ -1,18 +1,20 @@ /** Shared class strings for the "Clear Sky" design system. Presentation only. */ +// every button presses in a touch (scale 0.98) and filled ones carry a hairline top highlight; disabled ones stay still const btnBase = - "inline-flex items-center justify-center gap-1.5 rounded-xl font-semibold text-sm whitespace-nowrap transition-colors disabled:opacity-40 disabled:cursor-not-allowed select-none"; + "inline-flex items-center justify-center gap-1.5 rounded-xl font-semibold text-sm whitespace-nowrap transition-[color,background-color,border-color,box-shadow,transform,filter] duration-150 active:scale-[0.98] disabled:opacity-40 disabled:cursor-not-allowed disabled:active:scale-100 select-none motion-reduce:transition-colors motion-reduce:active:scale-100"; +const lit = "shadow-[inset_0_1px_0_rgb(255_255_255/0.18)]"; export const btn = { - primary: `${btnBase} min-h-11 px-5 bg-brand text-inverse hover:bg-brand-strong`, - primarySm: `${btnBase} min-h-9 px-3.5 text-[13px] bg-brand text-inverse hover:bg-brand-strong`, + primary: `${btnBase} ${lit} min-h-11 px-5 bg-brand text-inverse hover:bg-brand-strong hover:shadow-[inset_0_1px_0_rgb(255_255_255/0.18),0_10px_24px_-12px_var(--color-glow)]`, + primarySm: `${btnBase} ${lit} min-h-9 px-3.5 text-[13px] bg-brand text-inverse hover:bg-brand-strong`, secondary: `${btnBase} min-h-11 px-5 bg-card text-ink border border-line-strong hover:border-ink/40 hover:bg-paper`, secondarySm: `${btnBase} min-h-9 px-3.5 text-[13px] bg-card text-ink border border-line-strong hover:border-ink/40`, soft: `${btnBase} min-h-11 px-5 bg-brand-soft text-brand hover:bg-brand hover:text-inverse`, softSm: `${btnBase} min-h-9 px-3.5 text-[13px] bg-brand-soft text-brand hover:bg-brand hover:text-inverse`, - up: `${btnBase} min-h-11 px-5 bg-up text-inverse hover:brightness-110`, + up: `${btnBase} ${lit} min-h-11 px-5 bg-up text-inverse hover:brightness-110 hover:shadow-[inset_0_1px_0_rgb(255_255_255/0.18),0_10px_24px_-12px_var(--color-glow)]`, // bg-warm-ink, not bg-warm: white on #d97706 is 3.19:1, below AA for a filled control. - warm: `${btnBase} min-h-11 px-5 bg-warm-ink text-inverse hover:brightness-110`, + warm: `${btnBase} ${lit} min-h-11 px-5 bg-warm-ink text-inverse hover:brightness-110`, warmOutline: `${btnBase} min-h-11 px-5 bg-warm-soft text-warm-ink border border-warm/40 hover:border-warm`, icon: `${btnBase} h-10 w-10 rounded-xl text-ink hover:bg-paper border border-transparent hover:border-line`, }; @@ -32,4 +34,4 @@ export const helper = "mt-1.5 text-xs text-muted"; export const pill = "inline-flex items-center gap-1.5 rounded-full border border-line bg-card px-3 py-1.5 text-xs font-medium text-body whitespace-nowrap"; export const codeBlock = - "overflow-x-auto rounded-xl bg-paper border border-line px-4 py-3 font-mono text-[13px] leading-relaxed text-ink bb-scroll"; + "overflow-x-auto rounded-xl bg-paper border border-line px-4 py-3 font-code text-[13px] leading-relaxed text-ink bb-scroll"; diff --git a/app/src/components/vendor/LICENSES.md b/app/src/components/vendor/LICENSES.md index e41c8321..3745ccc8 100644 --- a/app/src/components/vendor/LICENSES.md +++ b/app/src/components/vendor/LICENSES.md @@ -14,6 +14,8 @@ The upstream [licensing guide](https://github.com/cosscom/coss/blob/main/LICENSI `animated-theme-toggler.tsx` is adapted from the [Magic UI component](https://magicui.design/docs/components/animated-theme-toggler). The upstream [license](https://github.com/magicuidesign/magicui/blob/main/LICENSE.md) is reproduced below. Openlaunch-specific changes are noted in the source. +These are adapted from Magic UI registry items under the same license, each with its local changes noted at the top of the file: `border-beam.tsx` ([border-beam](https://magicui.design/r/border-beam.json)), `marquee.tsx` ([marquee](https://magicui.design/r/marquee.json)), `flickering-grid.tsx` ([flickering-grid](https://magicui.design/r/flickering-grid.json)), `animated-shiny-text.tsx` ([animated-shiny-text](https://magicui.design/r/animated-shiny-text.json)), `magic-card.tsx` ([magic-card](https://magicui.design/r/magic-card.json)), `shine-border.tsx` ([shine-border](https://magicui.design/r/shine-border.json)) and `animated-list.tsx` ([animated-list](https://magicui.design/r/animated-list.json)). + MIT License Copyright (c) Magic UI diff --git a/app/src/components/vendor/animated-list.tsx b/app/src/components/vendor/animated-list.tsx new file mode 100644 index 00000000..4e541d02 --- /dev/null +++ b/app/src/components/vendor/animated-list.tsx @@ -0,0 +1,33 @@ +"use client"; + +// Adapted from Magic UI: https://magicui.design/r/animated-list.json (MIT, see ./LICENSES.md). +// LOCAL PATCH (openlaunch): driven by real data instead of a timed reveal. The list renders the items it is given; +// an item whose key is new springs in at the top while the rest slide down, and nothing animates on the first paint. +// Reduced motion swaps the spring for an instant change. +import type { ComponentPropsWithoutRef, ReactNode } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { cn } from "@/lib/utils"; + +export function AnimatedListItem({ children, className }: { children: ReactNode; className?: string }) { + const reduced = useReducedMotion(); + return ( +-
+