mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 19:06:06 +02:00
tokens.css defined colours, radii and font families but not a single --text-* step, so font sizes had no baseline to align to and grew wherever they were needed: 51 distinct sizes across web + desktop, 370 written as arbitrary values, six at half a pixel (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5px). text-xs and text-sm carried nearly all UI text while the range between them — 11, 13, 15px — could only be reached with arbitrary values. Hierarchy does not come from having more sizes; past a handful, each extra size makes the hierarchy blurrier. Add ten role-named steps, each with its own line-height so leading cannot fragment the way size did, and move every product-UI call site onto them. Steps are named for what the text is for, not for a t-shirt size, because that is what keeps the scale from drifting again. Six steps deliberately keep the exact size/line-height pairs of the Tailwind defaults they replace, so the ~1,900-call-site rename moves nothing on screen. The visible changes are confined to former arbitrary values snapping to a step: 8/9/10px -> micro (11px) on badges and overlines; 17 -> 18; 22 -> 24; 30 (text-3xl) -> 36 on headings and stat numbers; 12.8px -> label (13px) on small buttons and toggles. Half-pixel sizes are gone. This supersedes #6108, which was reverted by #6116 because the sidebar group labels rendered at the inherited 16px. The cause was not the scale but cn(): `text-<x>` is ambiguous in Tailwind, and tailwind-merge resolves it against a table listing only the default sizes, so it filed every role step under text-colour and dropped whichever of `text-caption` / `text-sidebar-foreground/70` came first. Registering the steps as a font-size class group restores the real conflict groups — size beats size, colour beats colour, the two coexist — and a test pins the list against the scale, since the failure is silent in source. Hand-written CSS is covered too. The transcript kept a 12.5px body long after every Tailwind call site was on the scale, so the "no half-pixel sizes" claim was true of the classes and false of the product; the editor's prose, code and mermaid ramps had the same blind spot, and seven of their eight values already equalled a step exactly. All now reference var(--text-*). The guard test reads raw `font-size:` declarations as well as class names, exempting only the 16px iOS input-zoom workaround in base.css and the landing pages' marketing ramp. apps/mobile (own NativeWind config) and apps/docs (fumadocs' own type system) keep Tailwind's default scale and are untouched. Landing display type (rem/clamp, 2.2-6.4rem) stays on its separate ramp, as do four decorative emoji / serif-hero sizes. Verified on a running local stack: pinned sidebar rows and group labels measure 12px/16px, nav items 14px/20px — identical to pre-migration. An audit of every rendered font size across the product surfaces finds nothing off the scale; the only exceptions are avatar initials and emoji, which actor-avatar.tsx sizes proportionally to the avatar diameter by design. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
193 lines
6.4 KiB
TypeScript
193 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Link from "next/link";
|
|
import { Menu, X } from "lucide-react";
|
|
import { MulticaIcon } from "@multica/ui/components/common/multica-icon";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
import { useAuthStore } from "@multica/core/auth";
|
|
import { docsHrefForLocale, useLocale } from "../i18n";
|
|
import { useDashboardCtaHref } from "../utils/use-dashboard-cta";
|
|
import { formatStarCount, useGithubStars } from "../utils/use-github-stars";
|
|
import { GitHubMark, githubUrl, headerButtonClassName } from "./shared";
|
|
|
|
export function LandingHeader({
|
|
variant = "dark",
|
|
}: {
|
|
variant?: "dark" | "light";
|
|
}) {
|
|
const { t, locale } = useLocale();
|
|
const user = useAuthStore((s) => s.user);
|
|
const stars = useGithubStars();
|
|
const starsLabel = stars != null ? formatStarCount(stars) : null;
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const docsHref = docsHrefForLocale(locale);
|
|
const navLinks = [
|
|
{ href: "/usecases", label: t.header.useCases },
|
|
{ href: docsHref, label: t.header.docs },
|
|
{ href: "/changelog", label: t.header.changelog },
|
|
];
|
|
const ctaHref = useDashboardCtaHref();
|
|
const ctaLabel = user ? t.header.dashboard : t.header.cta;
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
"relative inset-x-0 top-0 z-30",
|
|
variant === "dark"
|
|
? "absolute bg-transparent"
|
|
: "border-b border-[#0a0d12]/8 bg-white",
|
|
)}
|
|
>
|
|
<div className="mx-auto flex h-[76px] max-w-[1320px] items-center justify-between px-4 sm:px-6 lg:px-8">
|
|
<div className="flex min-w-0 items-center gap-6 lg:gap-8">
|
|
<Link href="/" className="flex shrink-0 items-center gap-3">
|
|
<MulticaIcon
|
|
className={cn(
|
|
"size-5",
|
|
variant === "dark" ? "text-white" : "text-[#0a0d12]",
|
|
)}
|
|
noSpin
|
|
/>
|
|
<span
|
|
className={cn(
|
|
"text-title font-semibold tracking-[0.04em] lowercase sm:text-title-lg",
|
|
variant === "dark" ? "text-white/92" : "text-[#0a0d12]",
|
|
)}
|
|
>
|
|
multica
|
|
</span>
|
|
</Link>
|
|
|
|
<nav
|
|
aria-label={t.header.navigation}
|
|
className="hidden items-center gap-1 md:flex"
|
|
>
|
|
{navLinks.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
className={navLinkClassName(variant)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center gap-2 sm:gap-2.5">
|
|
<button
|
|
type="button"
|
|
aria-label={isMenuOpen ? t.header.closeMenu : t.header.openMenu}
|
|
aria-expanded={isMenuOpen}
|
|
onClick={() => setIsMenuOpen((open) => !open)}
|
|
className={cn(
|
|
headerButtonClassName("ghost", variant),
|
|
"px-3 md:hidden",
|
|
)}
|
|
>
|
|
{isMenuOpen ? (
|
|
<X className="size-4" aria-hidden />
|
|
) : (
|
|
<Menu className="size-4" aria-hidden />
|
|
)}
|
|
</button>
|
|
<Link
|
|
href={githubUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className={cn(
|
|
headerButtonClassName("ghost", variant),
|
|
"hidden lg:inline-flex",
|
|
)}
|
|
>
|
|
<GitHubMark className="size-3.5" />
|
|
{t.header.github}
|
|
{starsLabel ? <GitHubStarsBadge label={starsLabel} /> : null}
|
|
</Link>
|
|
<Link
|
|
href={ctaHref}
|
|
className={headerButtonClassName("solid", variant)}
|
|
>
|
|
{ctaLabel}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{isMenuOpen ? (
|
|
<div
|
|
className={cn(
|
|
"absolute left-4 right-4 top-[calc(100%+8px)] z-50 rounded-[14px] border p-2 shadow-[0_18px_60px_rgba(0,0,0,0.18)] backdrop-blur-xl md:hidden",
|
|
variant === "dark"
|
|
? "border-white/14 bg-[#070a10]/95 text-white"
|
|
: "border-[#0a0d12]/10 bg-white text-[#0a0d12]",
|
|
)}
|
|
>
|
|
<nav aria-label={t.header.navigation} className="flex flex-col">
|
|
{navLinks.map((link) => (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
onClick={() => setIsMenuOpen(false)}
|
|
className={mobileNavLinkClassName(variant)}
|
|
>
|
|
{link.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
<div
|
|
className={cn(
|
|
"mt-2 border-t pt-2",
|
|
variant === "dark" ? "border-white/10" : "border-[#0a0d12]/8",
|
|
)}
|
|
>
|
|
<Link
|
|
href={githubUrl}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
onClick={() => setIsMenuOpen(false)}
|
|
className={mobileNavLinkClassName(variant)}
|
|
>
|
|
<GitHubMark className="size-3.5" />
|
|
{t.header.github}
|
|
{starsLabel ? <GitHubStarsBadge label={starsLabel} /> : null}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
/** Star-count segment appended to the header's GitHub button — a faint
|
|
* divider and the compact count (e.g. "37.6k"). No star glyph: in the GitHub
|
|
* button context the number reads as the star count on its own. Inherits the
|
|
* button's text color so it adapts to both the dark and light header
|
|
* variants. */
|
|
function GitHubStarsBadge({ label }: { label: string }) {
|
|
return (
|
|
<span className="inline-flex items-center gap-1.5 tabular-nums">
|
|
<span aria-hidden className="h-3 w-px bg-current opacity-25" />
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function navLinkClassName(variant: "dark" | "light") {
|
|
return cn(
|
|
"inline-flex h-9 items-center rounded-[9px] px-3 text-label font-medium transition-colors",
|
|
variant === "dark"
|
|
? "text-white/72 hover:bg-white/8 hover:text-white"
|
|
: "text-[#0a0d12]/62 hover:bg-[#0a0d12]/5 hover:text-[#0a0d12]",
|
|
);
|
|
}
|
|
|
|
function mobileNavLinkClassName(variant: "dark" | "light") {
|
|
return cn(
|
|
"flex min-h-11 items-center gap-2 rounded-[10px] px-3 text-body font-medium transition-colors",
|
|
variant === "dark"
|
|
? "text-white/76 hover:bg-white/8 hover:text-white"
|
|
: "text-[#0a0d12]/68 hover:bg-[#0a0d12]/5 hover:text-[#0a0d12]",
|
|
);
|
|
}
|