Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/src/app/fonts.ts
Original file line number Diff line number Diff line change
@@ -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" });
4 changes: 2 additions & 2 deletions app/src/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -24,7 +24,7 @@ export default function GlobalError({
}, [error]);
const digest = sanitizeDigest(error.digest);
return (
<html lang="en" className={`${inter.variable} ${spaceMono.variable} ${unbounded.variable}`}>
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
<body className="min-h-screen flex flex-col">
<main aria-label={errorRegionLabel("global")} className="mx-auto max-w-3xl px-4 py-24 text-center space-y-5">
<h1 className="font-display font-bold text-7xl">500</h1>
Expand Down
85 changes: 78 additions & 7 deletions app/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<html lang="en" className={`${inter.variable} ${spaceMono.variable} ${unbounded.variable}`} suppressHydrationWarning>
<html lang="en" className={`${geist.variable} ${geistMono.variable}`} suppressHydrationWarning>
<body id="site-top" tabIndex={-1} className="min-h-screen flex flex-col">
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: siteJsonLd }} />
<ThemeProvider nonce={nonce}>
Expand Down
6 changes: 3 additions & 3 deletions app/src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default async function OgImage() {
const stat = totals && totals.launches > 0 ? `${totals.launches} launches · ${fmtUsd(totals.volume_usd, { compact: true })} traded · $0 in fees, ever` : "no platform fee, ever";
return new ImageResponse(
(
<div style={{ width: "100%", height: "100%", background: PAPER, display: "flex", flexDirection: "column", fontFamily: "Inter, sans-serif", position: "relative", overflow: "hidden", color: INK }}>
<div style={{ width: "100%", height: "100%", background: PAPER, display: "flex", flexDirection: "column", fontFamily: "Geist, sans-serif", position: "relative", overflow: "hidden", color: INK }}>
<div style={{ position: "absolute", top: -320, left: -120, width: 900, height: 640, borderRadius: "50%", background: "radial-gradient(closest-side, #EAF0FF 0%, rgba(234,240,255,0) 100%)" }} />
<div style={{ position: "absolute", right: -140, bottom: -260, width: 620, height: 620, borderRadius: "50%", border: `18px solid ${LINE}`, opacity: 0.7 }} />

Expand All @@ -48,12 +48,12 @@ export default async function OgImage() {
<span style={{ color: BLUE }}>{BRAND_TLD}</span>
</span>
</div>
<span style={{ fontSize: 18, color: MUTED, fontFamily: "Space Mono, monospace" }}>{stat}</span>
<span style={{ fontSize: 18, color: MUTED, fontFamily: "Geist, sans-serif" }}>{stat}</span>
</div>

{/* headline */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", padding: "0 64px" }}>
<div style={{ fontFamily: "Unbounded, Inter, sans-serif", fontWeight: 700, fontSize: 86, lineHeight: 1.0, letterSpacing: -3, display: "flex", flexDirection: "column" }}>
<div style={{ fontFamily: "Geist, sans-serif", fontWeight: 700, fontSize: 86, lineHeight: 1.0, letterSpacing: -3, display: "flex", flexDirection: "column" }}>
<span style={{ display: "flex" }}>
Launch a token.&nbsp;<span style={{ color: BLUE }}>Free.</span>
</span>
Expand Down
12 changes: 6 additions & 6 deletions app/src/app/t/[chain]/[token]/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
const h = hueOf(token);
return new ImageResponse(
(
<div style={{ width: "100%", height: "100%", background: PAPER, display: "flex", flexDirection: "column", fontFamily: "Inter, sans-serif", position: "relative", overflow: "hidden", color: INK }}>
<div style={{ width: "100%", height: "100%", background: PAPER, display: "flex", flexDirection: "column", fontFamily: "Geist, sans-serif", position: "relative", overflow: "hidden", color: INK }}>
<div style={{ position: "absolute", top: -320, left: -120, width: 900, height: 640, borderRadius: "50%", background: "radial-gradient(closest-side, #EAF0FF 0%, rgba(234,240,255,0) 100%)" }} />
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "40px 64px 0" }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
Expand All @@ -111,18 +111,18 @@ export default async function TokenOg({ params }: { params: Promise<{ chain: str
{logo ? (
<img src={logo} alt="" width={220} height={220} style={{ width: 220, height: 220, borderRadius: 48, objectFit: "cover", border: `1px solid ${LINE}` }} />
) : (
<div style={{ width: 220, height: 220, borderRadius: 48, display: "flex", alignItems: "center", justifyContent: "center", background: `linear-gradient(135deg, hsl(${h} 70% 55%), hsl(${(h + 40) % 360} 75% 45%))`, color: "#fff", fontFamily: "Unbounded, Inter, sans-serif", fontWeight: 700, fontSize: 110 }}>
<div style={{ width: 220, height: 220, borderRadius: 48, display: "flex", alignItems: "center", justifyContent: "center", background: `linear-gradient(135deg, hsl(${h} 70% 55%), hsl(${(h + 40) % 360} 75% 45%))`, color: "#fff", fontFamily: "Geist, sans-serif", fontWeight: 700, fontSize: 110 }}>
{card.symbol.slice(0, 1)}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "baseline", gap: 18 }}>
<span style={{ fontFamily: "Unbounded, Inter, sans-serif", fontWeight: 700, fontSize: 72, letterSpacing: -2, lineHeight: 1.05 }}>{card.title}</span>
<span style={{ fontFamily: "Space Mono, monospace", fontSize: 30, color: MUTED }}>{card.symbol}</span>
<span style={{ fontFamily: "Geist, sans-serif", fontWeight: 700, fontSize: 72, letterSpacing: -2, lineHeight: 1.05 }}>{card.title}</span>
<span style={{ fontFamily: "Geist, sans-serif", fontSize: 30, color: MUTED }}>{card.symbol}</span>
</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 18, marginTop: 18 }}>
<span style={{ fontFamily: "Unbounded, Inter, sans-serif", fontWeight: 700, fontSize: 64, letterSpacing: -2 }}>{card.mcap}</span>
<span style={{ fontFamily: "Space Mono, monospace", fontSize: 32, fontWeight: 700, color: card.up === null ? MUTED : card.up ? UP : DOWN }}>{card.change}</span>
<span style={{ fontFamily: "Geist, sans-serif", fontWeight: 700, fontSize: 64, letterSpacing: -2 }}>{card.mcap}</span>
<span style={{ fontFamily: "Geist, sans-serif", fontSize: 32, fontWeight: 700, color: card.up === null ? MUTED : card.up ? UP : DOWN }}>{card.change}</span>
<span style={{ fontSize: 22, color: MUTED }}>market cap · since launch</span>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 12, marginTop: 26 }}>
Expand Down
6 changes: 3 additions & 3 deletions app/src/components/WalletMenu.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
.trigger:hover, .trigger[data-popup-open] { border-color: var(--color-line-strong); background: var(--color-line); }
.triggerMark { position: relative; display: flex; flex-shrink: 0; }
.connectionDot { position: absolute; right: 0; bottom: 0; width: 7px; height: 7px; background: var(--color-up); border: 2px solid var(--color-card); border-radius: 50%; }
.triggerAddress { font: 400 12px var(--font-mono); font-variant-numeric: tabular-nums; }
.triggerAddress { font: 400 12px var(--font-code); font-variant-numeric: tabular-nums; }
.triggerNetwork { padding-left: 8px; border-left: 1px solid var(--color-line-strong); font: 500 10px var(--font-sans); color: var(--color-muted); }
.chevron { color: var(--color-muted); transition: transform 160ms; }
.trigger[data-popup-open] .chevron { transform: rotate(180deg); }
Expand Down Expand Up @@ -59,9 +59,9 @@
.close:hover { color: var(--color-ink); background: var(--color-card); }
.identity { display: flex; align-items: center; gap: 12px; margin-top: 8px; }
.identityText { min-width: 0; }
.account { font: 400 20px var(--font-mono); letter-spacing: -.045em; }
.account { font: 400 20px var(--font-code); letter-spacing: -.02em; }
.provider { margin-top: 4px; font-size: 12px; color: var(--color-muted); overflow-wrap: anywhere; }
.fullAddress { margin-top: 16px; font: 400 11px/1.75 var(--font-mono); color: var(--color-muted); overflow-wrap: anywhere; user-select: all; }
.fullAddress { margin-top: 16px; font: 400 11px/1.75 var(--font-code); color: var(--color-muted); overflow-wrap: anywhere; user-select: all; }
.shortcuts { display: flex; gap: 8px; margin: 12px 0 20px; }
.shortcut { display: inline-flex; justify-content: center; align-items: center; gap: 7px; flex: 1; min-height: 44px; padding: 8px; border: 1px solid var(--color-line); border-radius: 10px; background: var(--color-card); font-size: 12px; font-weight: 500; cursor: pointer; }
.shortcut:hover { border-color: var(--color-line-strong); }
Expand Down
5 changes: 3 additions & 2 deletions app/src/components/launchpad/AdminQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { useAccount, useConfig } from "wagmi";
import { useConfig } from "wagmi";
import { useHydratedAccount } from "@/lib/useHydratedAccount";
import { getWalletClient } from "wagmi/actions";
import { btn, card } from "@/components/ui";
import { buildModMessage } from "@/lib/launchpad/posts";
Expand All @@ -19,7 +20,7 @@ function nonce(): string {

/** Moderation queue: reported posts, hide/unhide with an admin-wallet signature. */
export default function AdminQueue() {
const { address } = useAccount();
const { address } = useHydratedAccount();
const config = useConfig();
const [posts, setPosts] = useState<PostRow[] | null>(null);
const [err, setErr] = useState<string | null>(null);
Expand Down
8 changes: 8 additions & 0 deletions app/src/components/launchpad/BrandMarks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** The X mark (lucide ships no brand icons). Decorative: the link around it carries the words. */
export function XMark({ className = "size-3" }: { className?: string }) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" className={className} fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
}
Loading
Loading