Skip to content
~sami-ullah

Concept project · runs in your browserTypeScript · React.js · Next.js · Tailwind CSS

Design-to-Code Landing Page with a Spec Overlay Demo

A landing page for a fictional clinic software product, built from a written design spec: tokens, a type scale, a 12/8/4 column grid and fixed spacing steps. Switch the frame between phone, tablet and desktop, turn on the spec overlay and grid like a design handoff, and click any element to see its measurements and which token it uses.

Read the case study →

Interactive demo

samiullah.tech/lab/figma-landing-page
measuring…

New · two-way SMS reminders

Your clinic's back office, on autopilot.

Acme Flow books appointments, sends reminders, and turns visits into paid invoices, so your front desk can look after patients instead of paperwork.

No card needed · cancel any time

Connects toGoogle CalendarOutlookStripeQuickBooksTwilio SMS

Features

Everything the front desk does, in one place

  • Booking that fills gaps

    Patients pick a free slot online. Cancellations offer the slot to the waiting list automatically.

  • Reminders that get read

    SMS and email reminders 48 and 2 hours before, with a one-tap confirm or reschedule link.

  • Invoices in one click

    Turn a finished appointment into an invoice with the right treatment codes and a payment link.

  • One inbox

    Calls, emails and messages for every location land in one shared queue the whole desk can see.

  • Reports without spreadsheets

    No-show rate, revenue per chair and outstanding invoices, updated every night.

  • Built for privacy

    Role-based access, an audit log of who opened what, and data stored in your region.

Live in an afternoon

  1. 1

    Import

    Upload your patient list and treatments from a CSV export.

  2. 2

    Connect

    Link your calendar and payment account. Two clicks each.

  3. 3

    Switch on

    Choose which reminders to send and share your booking link.

“We stopped phoning patients the day before. Confirmations come back by text and the gaps fill themselves.”
Practice manager, Acme Dental (fictional)

Simple pricing

  • Starter

    For a single practice getting organised.

    $16/ month, billed yearly

    • 1 location
    • Online booking
    • Email reminders
    • Basic invoices
    Choose Starter
  • Most popular

    Team

    For busy clinics with a front desk team.

    $41/ month, billed yearly

    • Up to 3 locations
    • SMS + email reminders
    • Payment links
    • Roles and permissions
    • Priority support
    Choose Team
  • Group

    For groups that need reporting across sites.

    $108/ month, billed yearly

    • Unlimited locations
    • Group reporting
    • SSO
    • Audit log
    • Onboarding call
    Choose Group

Questions

Yes. Upload a CSV export from your current system and map the columns. Nothing is overwritten until you confirm the preview.

Try it with your own calendar

Get the setup guide and a trial link by email. One message, no newsletter.

The page uses container queries, so these frame sizes reflow it exactly as a phone, tablet or desktop window would. Hover outlined elements for their spec; tinted bands show section padding.

## How it works

tokens.ts holds colours, type, spacing, radii and the grid. The page gets the colours and radii as CSS custom properties, so a token change updates every element that uses it, and the inspector can map a computed colour back to its token name.

Layout uses container queries instead of media queries. The device buttons change the width of the frame, and the page reflows exactly as it would on that screen, without resizing your browser.

The spec overlay is plain CSS scoped to the frame: elements with a data-spec attribute get a dashed outline and show their spec on hover, and sections with data-pad tint their vertical padding. Inspect mode reads getComputedStyle() for whatever you click or tab to.

## View source

The code running in the window above, exactly as deployed.

app/lab/figma-landing-page/tokens.ts103 lines
/**
 * Design tokens for the "Acme Flow" landing page (a fictional product), written the way they would be exported
 * from Figma variables. The page reads them as CSS custom properties, and the spec overlay and inspector map
 * computed styles back to these names, so the code and the "design file" share one vocabulary.
 */

export const COLORS = {
  "ink-900": "#0f172a",
  "ink-600": "#475569",
  "ink-400": "#94a3b8",
  "line": "#e2e8f0",
  "surface": "#ffffff",
  "surface-2": "#f8fafc",
  "brand-700": "#4338ca",
  "brand-500": "#6366f1",
  "brand-50": "#eef2ff",
  "mint-700": "#047857",
  "amber-500": "#f59e0b",
  "night": "#0b1020",
} as const;
export type ColorToken = keyof typeof COLORS;

/** [size px, line-height px, weight] — desktop values; display and h2 step down on small frames. */
export const TYPE = {
  display: { size: 56, line: 60, weight: 800, mobile: 36 },
  h2: { size: 36, line: 42, weight: 700, mobile: 28 },
  h3: { size: 20, line: 28, weight: 600, mobile: 20 },
  lead: { size: 18, line: 28, weight: 400, mobile: 17 },
  body: { size: 16, line: 24, weight: 400, mobile: 16 },
  small: { size: 14, line: 20, weight: 500, mobile: 14 },
  caption: { size: 12, line: 16, weight: 600, mobile: 12 },
} as const;

export const SPACE = [4, 8, 12, 16, 24, 32, 48, 64, 96] as const;
export const RADIUS = { sm: 6, md: 10, lg: 16, pill: 999 } as const;

export const GRID = {
  desktop: { min: 1024, columns: 12, gutter: 24, margin: 32, max: 1120 },
  tablet: { min: 768, columns: 8, gutter: 24, margin: 32, max: 768 },
  mobile: { min: 0, columns: 4, gutter: 16, margin: 16, max: 480 },
} as const;
export type Breakpoint = keyof typeof GRID;

export const breakpointFor = (width: number): Breakpoint => (width >= GRID.desktop.min ? "desktop" : width >= GRID.tablet.min ? "tablet" : "mobile");

/** Tokens as CSS custom properties, applied to the landing page root. */
export function cssVars(): Record<string, string> {
  const vars: Record<string, string> = {};
  for (const [k, v] of Object.entries(COLORS)) vars[`--af-${k}`] = v;
  for (const [k, r] of Object.entries(RADIUS)) vars[`--af-r-${k}`] = `${r}px`;
  return vars;
}

/* ------------------------------------------------------------------ */
/* Colour maths for the handoff panel                                  */
/* ------------------------------------------------------------------ */

export function hexToRgb(hex: string): [number, number, number] {
  const h = hex.replace("#", "");
  const n = parseInt(h.length === 3 ? h.replace(/./g, "$&$&") : h, 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}

/** "rgb(15, 23, 42)" -> "#0f172a" (alpha ignored). */
export function rgbToHex(rgb: string): string | null {
  const m = /rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(rgb);
  if (!m) return null;
  return "#" + [m[1], m[2], m[3]].map((x) => Number(x).toString(16).padStart(2, "0")).join("");
}

export function tokenForHex(hex: string | null): ColorToken | null {
  if (!hex) return null;
  const found = Object.entries(COLORS).find(([, v]) => v.toLowerCase() === hex.toLowerCase());
  return found ? (found[0] as ColorToken) : null;
}

/** WCAG 2.x relative luminance and contrast ratio. */
export function luminance(hex: string) {
  const [r, g, b] = hexToRgb(hex).map((c) => {
    const s = c / 255;
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  }) as [number, number, number];
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

export function contrast(a: string, b: string) {
  const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number];
  return (hi + 0.05) / (lo + 0.05);
}

/** The text/background pairs the page actually uses, checked against WCAG AA. */
export const PAIRS: { fg: ColorToken; bg: ColorToken; use: string; large?: boolean }[] = [
  { fg: "ink-900", bg: "surface", use: "Headings, body" },
  { fg: "ink-600", bg: "surface", use: "Secondary text" },
  { fg: "ink-600", bg: "surface-2", use: "Text on tinted sections" },
  { fg: "surface", bg: "brand-700", use: "Primary button" },
  { fg: "brand-700", bg: "brand-50", use: "Eyebrow pill" },
  { fg: "mint-700", bg: "surface", use: "Status chips, check marks" },
  { fg: "surface", bg: "night", use: "CTA band" },
  { fg: "ink-400", bg: "night", use: "Footer text" },
  { fg: "surface", bg: "brand-500", use: "Large text only", large: true },
];
app/lab/figma-landing-page/Landing.tsx419 lines
"use client";

import { useId, useState } from "react";
import { cssVars } from "./tokens";

/**
 * The landing page for "Acme Flow", a fictional back-office tool for small clinics.
 * Layout uses container queries (@3xl = tablet frame, @5xl = desktop frame) so the device switcher in the demo
 * can resize the frame without resizing the browser. `data-spec` / `data-pad` attributes feed the spec overlay.
 */

const PLANS = [
  { name: "Starter", monthly: 19, blurb: "For a single practice getting organised.", features: ["1 location", "Online booking", "Email reminders", "Basic invoices"] },
  { name: "Team", monthly: 49, blurb: "For busy clinics with a front desk team.", features: ["Up to 3 locations", "SMS + email reminders", "Payment links", "Roles and permissions", "Priority support"], featured: true },
  { name: "Group", monthly: 129, blurb: "For groups that need reporting across sites.", features: ["Unlimited locations", "Group reporting", "SSO", "Audit log", "Onboarding call"] },
];

const FEATURES = [
  { title: "Booking that fills gaps", body: "Patients pick a free slot online. Cancellations offer the slot to the waiting list automatically.", icon: "M4 6h16M4 12h10M4 18h7" },
  { title: "Reminders that get read", body: "SMS and email reminders 48 and 2 hours before, with a one-tap confirm or reschedule link.", icon: "M12 3v10l4 3M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0" },
  { title: "Invoices in one click", body: "Turn a finished appointment into an invoice with the right treatment codes and a payment link.", icon: "M6 3h12v18l-3-2-3 2-3-2-3 2zM9 8h6M9 12h6" },
  { title: "One inbox", body: "Calls, emails and messages for every location land in one shared queue the whole desk can see.", icon: "M3 7l9 6 9-6M3 7v10h18V7" },
  { title: "Reports without spreadsheets", body: "No-show rate, revenue per chair and outstanding invoices, updated every night.", icon: "M4 20V10M10 20V4M16 20v-7M22 20H2" },
  { title: "Built for privacy", body: "Role-based access, an audit log of who opened what, and data stored in your region.", icon: "M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z" },
];

const FAQ = [
  { q: "Can I import my existing patients?", a: "Yes. Upload a CSV export from your current system and map the columns. Nothing is overwritten until you confirm the preview." },
  { q: "Do patients need to create an account?", a: "No. Booking and confirming work through a secure link. Patients can create an account later if they want to see their history." },
  { q: "What happens after the trial?", a: "You choose a plan or your workspace pauses. Nothing is deleted for 30 days, and you can export everything at any time." },
  { q: "Is there a contract?", a: "Monthly plans can be cancelled any time. Yearly plans are paid up front and cost two months less." },
];

function Icon({ d }: { d: string }) {
  return (
    <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d={d} />
    </svg>
  );
}

const btnPrimary =
  "inline-flex h-11 items-center justify-center rounded-[var(--af-r-md)] bg-[var(--af-brand-700)] px-5 text-[15px] font-semibold text-white hover:bg-[#3730a3] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--af-brand-700)]";
const btnGhost =
  "inline-flex h-11 items-center justify-center rounded-[var(--af-r-md)] border border-[var(--af-line)] bg-white px-5 text-[15px] font-semibold text-[var(--af-ink-900)] hover:border-[var(--af-ink-400)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--af-brand-700)]";
const wrap = "mx-auto w-full max-w-[1120px] px-4 @3xl:px-8";

export function Landing() {
  const [menu, setMenu] = useState(false);
  const [yearly, setYearly] = useState(true);
  const [open, setOpen] = useState<number | null>(0);
  const [email, setEmail] = useState("");
  const [formState, setFormState] = useState<{ status: "idle" | "error" | "done"; msg: string }>({ status: "idle", msg: "" });
  const id = useId();

  return (
    <div style={cssVars()} className="@container bg-[var(--af-surface)] font-sans text-[16px] leading-6 text-[var(--af-ink-900)] antialiased">
      {/* ---------------- nav ---------------- */}
      <header className="border-b border-[var(--af-line)]" data-spec="nav · h 64 · line">
        <div className={`${wrap} flex h-16 items-center gap-6`}>
          <a href="#af-top" className="flex items-center gap-2 text-[17px] font-bold tracking-tight" data-spec="logo · 17/700">
            <span className="grid size-7 place-items-center rounded-[var(--af-r-sm)] bg-[var(--af-brand-700)] text-[13px] text-white" aria-hidden="true">
              ◆
            </span>
            Acme Flow
          </a>
          <nav aria-label="Main" className="ml-4 hidden gap-6 text-[14px] font-medium text-[var(--af-ink-600)] @3xl:flex">
            <a href="#af-features" className="hover:text-[var(--af-ink-900)]">
              Product
            </a>
            <a href="#af-pricing" className="hover:text-[var(--af-ink-900)]">
              Pricing
            </a>
            <a href="#af-faq" className="hover:text-[var(--af-ink-900)]">
              FAQ
            </a>
          </nav>
          <div className="ml-auto hidden items-center gap-3 @3xl:flex">
            <a href="#af-cta" className="text-[14px] font-medium text-[var(--af-ink-600)] hover:text-[var(--af-ink-900)]">
              Sign in
            </a>
            <a href="#af-cta" className={btnPrimary + " h-9 px-4 text-[14px]"} data-spec="button/sm · h 36 · brand-700">
              Start free
            </a>
          </div>
          <button
            type="button"
            className="ml-auto grid size-10 place-items-center rounded-[var(--af-r-md)] border border-[var(--af-line)] @3xl:hidden"
            aria-expanded={menu}
            aria-controls={`${id}-menu`}
            onClick={() => setMenu((m) => !m)}
          >
            <span className="sr-only">{menu ? "Close menu" : "Open menu"}</span>
            <svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth={2} aria-hidden="true">
              <path d={menu ? "M6 6l12 12M18 6L6 18" : "M4 7h16M4 12h16M4 17h16"} />
            </svg>
          </button>
        </div>
        <nav id={`${id}-menu`} aria-label="Mobile" hidden={!menu} className="border-t border-[var(--af-line)] px-4 py-3 @3xl:hidden">
          <ul className="space-y-1 text-[15px] font-medium">
            {[
              ["Product", "#af-features"],
              ["Pricing", "#af-pricing"],
              ["FAQ", "#af-faq"],
              ["Sign in", "#af-cta"],
            ].map(([l, h]) => (
              <li key={l}>
                <a href={h} onClick={() => setMenu(false)} className="block rounded-[var(--af-r-sm)] px-2 py-2 hover:bg-[var(--af-surface-2)]">
                  {l}
                </a>
              </li>
            ))}
          </ul>
          <a href="#af-cta" onClick={() => setMenu(false)} className={btnPrimary + " mt-2 w-full"}>
            Start free
          </a>
        </nav>
      </header>

      {/* ---------------- hero ---------------- */}
      <section id="af-top" className="relative overflow-hidden" data-pad="64 / 96">
        <div className={`${wrap} grid items-center gap-12 py-16 @5xl:grid-cols-12 @5xl:py-24`}>
          <div className="@5xl:col-span-6">
            <p className="inline-flex rounded-[var(--af-r-pill)] bg-[var(--af-brand-50)] px-3 py-1 text-[12px] leading-4 font-semibold text-[var(--af-brand-700)]" data-spec="eyebrow · caption 12/16 · brand-700 on brand-50">
              New · two-way SMS reminders
            </p>
            <h2 className="mt-5 text-[36px] leading-[40px] font-extrabold tracking-[-0.02em] @3xl:text-[48px] @3xl:leading-[52px] @5xl:text-[56px] @5xl:leading-[60px]" data-spec="display · 56/60 · 800 · ink-900 (36/40 mobile)">
              Your clinic&apos;s back office, on autopilot.
            </h2>
            <p className="mt-5 max-w-[34rem] text-[17px] leading-7 text-[var(--af-ink-600)] @3xl:text-[18px]" data-spec="lead · 18/28 · ink-600">
              Acme Flow books appointments, sends reminders, and turns visits into paid invoices, so your front desk can look after patients instead of
              paperwork.
            </p>
            <div className="mt-8 flex flex-col gap-3 @md:flex-row" data-spec="gap 12">
              <a href="#af-cta" className={btnPrimary} data-spec="button/lg · h 44 · r 10 · brand-700">
                Start 14-day trial
              </a>
              <a href="#af-features" className={btnGhost}>
                See how it works
              </a>
            </div>
            <p className="mt-4 text-[14px] leading-5 text-[var(--af-ink-600)]">No card needed · cancel any time</p>
          </div>

          {/* product mock, drawn in HTML so it stays sharp and themeable */}
          <div className="@5xl:col-span-6" aria-hidden="true">
            <div className="rounded-[var(--af-r-lg)] border border-[var(--af-line)] bg-[var(--af-surface-2)] p-3 shadow-[0_24px_60px_-20px_rgb(15_23_42/0.25)]" data-spec="card · r 16 · shadow-xl">
              <div className="rounded-[var(--af-r-md)] border border-[var(--af-line)] bg-white">
                <div className="flex items-center justify-between border-b border-[var(--af-line)] px-4 py-3">
                  <p className="text-[14px] font-semibold">Today · Acme Dental</p>
                  <span className="rounded-[var(--af-r-pill)] bg-[#ecfdf5] px-2 py-0.5 text-[12px] font-semibold text-[var(--af-mint-700)]">18 booked</span>
                </div>
                <ul className="divide-y divide-[var(--af-line)] text-[13px]">
                  {[
                    ["09:00", "Check-up", "Confirmed", "mint"],
                    ["09:30", "Cleaning", "Confirmed", "mint"],
                    ["10:15", "Filling", "Reminder sent", "amber"],
                    ["11:00", "Free slot", "Offered to waiting list", "brand"],
                    ["11:45", "Whitening", "Invoice #1042 paid", "mint"],
                  ].map(([t, what, status, tone]) => (
                    <li key={t} className="flex items-center gap-3 px-4 py-2.5">
                      <span className="w-11 font-mono text-[var(--af-ink-600)]">{t}</span>
                      <span className="font-medium">{what}</span>
                      <span
                        className={`ml-auto truncate rounded-[var(--af-r-pill)] px-2 py-0.5 text-[11px] font-semibold ${
                          tone === "mint" ? "bg-[#ecfdf5] text-[var(--af-mint-700)]" : tone === "amber" ? "bg-[#fffbeb] text-[#b45309]" : "bg-[var(--af-brand-50)] text-[var(--af-brand-700)]"
                        }`}
                      >
                        {status}
                      </span>
                    </li>
                  ))}
                </ul>
              </div>
              <div className="mt-3 grid grid-cols-3 gap-3">
                {[
                  ["No-shows", "↓"],
                  ["Unpaid", "3"],
                  ["Messages", "7"],
                ].map(([k, v]) => (
                  <div key={k} className="rounded-[var(--af-r-md)] border border-[var(--af-line)] bg-white px-3 py-2">
                    <p className="text-[11px] text-[var(--af-ink-600)]">{k}</p>
                    <p className="text-[18px] font-bold">{v}</p>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </section>

      {/* ---------------- integrations strip ---------------- */}
      <section className="border-y border-[var(--af-line)] bg-[var(--af-surface-2)]" aria-label="Works with your tools">
        <div className={`${wrap} flex flex-wrap items-center justify-center gap-x-8 gap-y-2 py-6 text-[14px] font-semibold text-[var(--af-ink-600)]`} data-spec="strip · py 24 · small 14/600">
          <span className="font-medium">Connects to</span>
          {["Google Calendar", "Outlook", "Stripe", "QuickBooks", "Twilio SMS"].map((t) => (
            <span key={t}>{t}</span>
          ))}
        </div>
      </section>

      {/* ---------------- features ---------------- */}
      <section id="af-features" data-pad="64 / 96">
        <div className={`${wrap} py-16 @5xl:py-24`}>
          <div className="max-w-[40rem]">
            <p className="text-[12px] leading-4 font-semibold tracking-[0.08em] text-[var(--af-brand-700)] uppercase">Features</p>
            <h2 className="mt-3 text-[28px] leading-[34px] font-bold tracking-[-0.01em] @3xl:text-[36px] @3xl:leading-[42px]" data-spec="h2 · 36/42 · 700 (28/34 mobile)">
              Everything the front desk does, in one place
            </h2>
          </div>
          <ul className="mt-12 grid gap-6 @3xl:grid-cols-2 @5xl:grid-cols-3" data-spec="grid · 3 col · gap 24">
            {FEATURES.map((f) => (
              <li key={f.title} className="rounded-[var(--af-r-lg)] border border-[var(--af-line)] p-6" data-spec="card · p 24 · r 16">
                <span className="grid size-10 place-items-center rounded-[var(--af-r-md)] bg-[var(--af-brand-50)] text-[var(--af-brand-700)]">
                  <Icon d={f.icon} />
                </span>
                <h3 className="mt-4 text-[20px] leading-7 font-semibold">{f.title}</h3>
                <p className="mt-2 text-[16px] leading-6 text-[var(--af-ink-600)]">{f.body}</p>
              </li>
            ))}
          </ul>
        </div>
      </section>

      {/* ---------------- steps + quote ---------------- */}
      <section className="bg-[var(--af-surface-2)]" data-pad="64 / 96">
        <div className={`${wrap} grid gap-12 py-16 @5xl:grid-cols-12 @5xl:py-24`}>
          <div className="@5xl:col-span-7">
            <h2 className="text-[28px] leading-[34px] font-bold @3xl:text-[36px] @3xl:leading-[42px]">Live in an afternoon</h2>
            <ol className="mt-8 space-y-6">
              {[
                ["Import", "Upload your patient list and treatments from a CSV export."],
                ["Connect", "Link your calendar and payment account. Two clicks each."],
                ["Switch on", "Choose which reminders to send and share your booking link."],
              ].map(([t, b], i) => (
                <li key={t} className="flex gap-4">
                  <span className="grid size-9 shrink-0 place-items-center rounded-full bg-[var(--af-brand-700)] text-[14px] font-bold text-white">{i + 1}</span>
                  <div>
                    <h3 className="text-[18px] leading-7 font-semibold">{t}</h3>
                    <p className="text-[var(--af-ink-600)]">{b}</p>
                  </div>
                </li>
              ))}
            </ol>
          </div>
          <figure className="self-center rounded-[var(--af-r-lg)] border border-[var(--af-line)] bg-white p-8 @5xl:col-span-5" data-spec="quote card · p 32">
            <blockquote className="text-[18px] leading-7 font-medium">“We stopped phoning patients the day before. Confirmations come back by text and the gaps fill themselves.”</blockquote>
            <figcaption className="mt-4 text-[14px] text-[var(--af-ink-600)]">Practice manager, Acme Dental (fictional)</figcaption>
          </figure>
        </div>
      </section>

      {/* ---------------- pricing ---------------- */}
      <section id="af-pricing" data-pad="64 / 96">
        <div className={`${wrap} py-16 @5xl:py-24`}>
          <div className="flex flex-col items-start gap-6 @3xl:flex-row @3xl:items-end @3xl:justify-between">
            <h2 className="text-[28px] leading-[34px] font-bold @3xl:text-[36px] @3xl:leading-[42px]">Simple pricing</h2>
            <div role="radiogroup" aria-label="Billing period" className="inline-flex rounded-[var(--af-r-pill)] border border-[var(--af-line)] p-1 text-[14px] font-semibold" data-spec="segmented · r pill">
              {[
                [false, "Monthly"],
                [true, "Yearly · 2 months free"],
              ].map(([v, l]) => (
                <button
                  key={String(v)}
                  type="button"
                  role="radio"
                  aria-checked={yearly === v}
                  tabIndex={yearly === v ? 0 : -1}
                  onClick={() => setYearly(v as boolean)}
                  onKeyDown={(e) => {
                    if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key)) {
                      e.preventDefault();
                      setYearly((y) => !y);
                      (e.currentTarget.parentElement?.querySelector(`[aria-checked="false"]`) as HTMLElement | null)?.focus();
                    }
                  }}
                  className={`rounded-[var(--af-r-pill)] px-4 py-1.5 ${yearly === v ? "bg-[var(--af-ink-900)] text-white" : "text-[var(--af-ink-600)]"}`}
                >
                  {l as string}
                </button>
              ))}
            </div>
          </div>
          <ul className="mt-10 grid gap-6 @5xl:grid-cols-3">
            {PLANS.map((p) => {
              const price = yearly ? Math.round((p.monthly * 10) / 12) : p.monthly;
              return (
                <li
                  key={p.name}
                  className={`relative flex flex-col rounded-[var(--af-r-lg)] border p-6 ${p.featured ? "border-[var(--af-brand-700)] shadow-[0_12px_40px_-16px_rgb(67_56_202/0.5)]" : "border-[var(--af-line)]"}`}
                  data-spec={p.featured ? "plan card · featured · border brand-700" : "plan card · p 24"}
                >
                  {p.featured && <span className="absolute -top-3 left-6 rounded-[var(--af-r-pill)] bg-[var(--af-brand-700)] px-2.5 py-0.5 text-[12px] font-semibold text-white">Most popular</span>}
                  <h3 className="text-[18px] font-semibold">{p.name}</h3>
                  <p className="mt-1 text-[14px] text-[var(--af-ink-600)]">{p.blurb}</p>
                  <p className="mt-6 flex items-end gap-1">
                    <span className="text-[40px] leading-[44px] font-extrabold tracking-tight">${price}</span>
                    <span className="pb-1 text-[14px] text-[var(--af-ink-600)]">/ month{yearly ? ", billed yearly" : ""}</span>
                  </p>
                  <ul className="mt-6 flex-1 space-y-2 text-[15px]">
                    {p.features.map((f) => (
                      <li key={f} className="flex gap-2">
                        <span className="text-[var(--af-mint-700)]" aria-hidden="true">
                          ✓
                        </span>
                        {f}
                      </li>
                    ))}
                  </ul>
                  <a href="#af-cta" className={`mt-8 ${p.featured ? btnPrimary : btnGhost}`}>
                    Choose {p.name}
                  </a>
                </li>
              );
            })}
          </ul>
        </div>
      </section>

      {/* ---------------- FAQ ---------------- */}
      <section id="af-faq" className="bg-[var(--af-surface-2)]" data-pad="64 / 96">
        <div className={`${wrap} grid gap-10 py-16 @5xl:grid-cols-12 @5xl:py-24`}>
          <h2 className="text-[28px] leading-[34px] font-bold @3xl:text-[36px] @3xl:leading-[42px] @5xl:col-span-4">Questions</h2>
          <div className="divide-y divide-[var(--af-line)] rounded-[var(--af-r-lg)] border border-[var(--af-line)] bg-white @5xl:col-span-8">
            {FAQ.map((f, i) => {
              const isOpen = open === i;
              return (
                <div key={f.q}>
                  <h3>
                    <button
                      type="button"
                      id={`${id}-q${i}`}
                      aria-expanded={isOpen}
                      aria-controls={`${id}-a${i}`}
                      onClick={() => setOpen(isOpen ? null : i)}
                      className="flex w-full items-center justify-between gap-4 px-5 py-4 text-left text-[16px] font-semibold focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--af-brand-700)]"
                    >
                      {f.q}
                      <span aria-hidden="true" className={`text-[var(--af-ink-400)] transition-transform motion-reduce:transition-none ${isOpen ? "rotate-45" : ""}`}>
                        +
                      </span>
                    </button>
                  </h3>
                  <div id={`${id}-a${i}`} role="region" aria-labelledby={`${id}-q${i}`} hidden={!isOpen} className="px-5 pb-4 text-[var(--af-ink-600)]">
                    {f.a}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </section>

      {/* ---------------- CTA ---------------- */}
      <section id="af-cta" className="bg-[var(--af-night)] text-white" data-pad="64">
        <div className={`${wrap} grid gap-8 py-16 @5xl:grid-cols-12 @5xl:items-center`}>
          <div className="@5xl:col-span-6">
            <h2 className="text-[28px] leading-[34px] font-bold @3xl:text-[36px] @3xl:leading-[42px]">Try it with your own calendar</h2>
            <p className="mt-3 text-[var(--af-ink-400)]">Get the setup guide and a trial link by email. One message, no newsletter.</p>
          </div>
          <form
            noValidate
            className="@5xl:col-span-6"
            onSubmit={(e) => {
              e.preventDefault();
              const v = email.trim();
              if (!v) return setFormState({ status: "error", msg: "Enter your work email." });
              if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) return setFormState({ status: "error", msg: "That email does not look right. Check for typos." });
              setFormState({ status: "done", msg: `Thanks. In the real product a setup guide would go to ${v}. Nothing was sent or stored.` });
              setEmail("");
            }}
          >
            <label htmlFor={`${id}-email`} className="text-[14px] font-semibold">
              Work email
            </label>
            <div className="mt-2 flex flex-col gap-3 @md:flex-row">
              <input
                id={`${id}-email`}
                type="email"
                autoComplete="email"
                value={email}
                onChange={(e) => {
                  setEmail(e.target.value);
                  if (formState.status === "error") setFormState({ status: "idle", msg: "" });
                }}
                aria-invalid={formState.status === "error"}
                aria-describedby={formState.msg ? `${id}-email-msg` : undefined}
                placeholder="you@clinic.example"
                className="h-11 flex-1 rounded-[var(--af-r-md)] border border-white/20 bg-white/10 px-4 text-[15px] text-white placeholder:text-[var(--af-ink-400)] focus:border-white focus:outline-none"
              />
              <button type="submit" className="h-11 rounded-[var(--af-r-md)] bg-white px-5 text-[15px] font-semibold text-[var(--af-ink-900)] hover:bg-[var(--af-brand-50)]">
                Send me the guide
              </button>
            </div>
            <p id={`${id}-email-msg`} role="status" className={`mt-2 min-h-5 text-[14px] ${formState.status === "error" ? "text-[#fca5a5]" : "text-[#6ee7b7]"}`}>
              {formState.msg}
            </p>
          </form>
        </div>
      </section>

      <footer className="bg-[var(--af-night)] text-[14px] text-[var(--af-ink-400)]">
        <div className={`${wrap} flex flex-col gap-4 border-t border-white/10 py-8 @3xl:flex-row @3xl:items-center @3xl:justify-between`}>
          <p>© 2026 Acme Flow. A fictional product for a design-to-code demo.</p>
          <ul className="flex gap-6">
            {["Privacy", "Terms", "Status"].map((l) => (
              <li key={l}>
                <a href="#af-top" className="hover:text-white">
                  {l}
                </a>
              </li>
            ))}
          </ul>
        </div>
      </footer>
    </div>
  );
}
app/lab/figma-landing-page/Demo.tsx367 lines
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { Landing } from "./Landing";
import { COLORS, GRID, PAIRS, RADIUS, SPACE, TYPE, breakpointFor, contrast, rgbToHex, tokenForHex, type ColorToken } from "./tokens";

const KEY = "lab:figma-landing-page:v1";
const FRAMES = [
  { id: "mobile", label: "Mobile", width: 375 },
  { id: "tablet", label: "Tablet", width: 768 },
  { id: "desktop", label: "Desktop", width: 1280 },
  { id: "fit", label: "Fit", width: null },
] as const;
type FrameId = (typeof FRAMES)[number]["id"];
type Settings = { frame: FrameId; spec: boolean; grid: boolean; inspect: boolean };
const DEFAULTS: Settings = { frame: "fit", spec: true, grid: false, inspect: false };

type Picked = {
  tag: string;
  box: { x: number; y: number; w: number; h: number };
  rows: [string, string, ColorToken | null][];
  spec: string | null;
};

/** Spec overlay styles, scoped to the frame. Lives here because globals.css is shared by the whole site. */
const SPEC_CSS = `
.af-spec [data-spec]{outline:1px dashed rgb(219 39 119 / .65);outline-offset:2px;position:relative}
.af-spec [data-spec]:hover::after,.af-spec [data-spec]:focus-within::after{content:attr(data-spec);position:absolute;left:0;top:-22px;z-index:40;background:#db2777;color:#fff;font:600 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace;padding:4px 6px;border-radius:4px;white-space:nowrap;pointer-events:none}
.af-spec [data-pad]{position:relative;--pad:64px;background-image:linear-gradient(to bottom,rgb(219 39 119 / .10) var(--pad),transparent var(--pad),transparent calc(100% - var(--pad)),rgb(219 39 119 / .10) calc(100% - var(--pad)))}
@container (min-width: 64rem){.af-spec [data-pad]{--pad:96px}}
.af-spec [data-pad="64"]{--pad:64px}
.af-spec [data-pad]::before{content:"padding-y " attr(data-pad);position:absolute;right:8px;top:6px;z-index:5;font:600 10px ui-monospace,SFMono-Regular,Menlo,monospace;color:#db2777}
.af-inspect *{cursor:crosshair !important}
`;

export default function Demo() {
  const [s, setS] = useState<Settings>(DEFAULTS);
  const [ready, setReady] = useState(false);
  const [width, setWidth] = useState(0);
  const [picked, setPicked] = useState<Picked | null>(null);
  const [panel, setPanel] = useState<"tokens" | "inspect" | "contrast">("tokens");
  const frame = useRef<HTMLDivElement>(null);
  const selected = useRef<HTMLElement | null>(null);

  useEffect(() => {
    try {
      const raw = localStorage.getItem(KEY);
      if (raw) setS({ ...DEFAULTS, ...(JSON.parse(raw) as Partial<Settings>) });
    } catch {
      /* ignore */
    }
    setReady(true);
  }, []);
  useEffect(() => {
    if (!ready) return;
    try {
      localStorage.setItem(KEY, JSON.stringify(s));
    } catch {
      /* ignore */
    }
  }, [s, ready]);

  // Measure the frame so the readout and grid overlay match what is rendered.
  useEffect(() => {
    const el = frame.current;
    if (!el) return;
    const ro = new ResizeObserver(([e]) => setWidth(Math.round(e!.contentRect.width)));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const measure = useCallback((el: HTMLElement) => {
    const root = frame.current;
    if (!root) return;
    const r = el.getBoundingClientRect();
    const fr = root.getBoundingClientRect();
    const cs = getComputedStyle(el);
    const color = rgbToHex(cs.color);
    const bg = rgbToHex(cs.backgroundColor);
    const px = (v: string) => v.replace(/\.\d+px/g, "px");
    const rows: Picked["rows"] = [
      ["size", `${Math.round(r.width)} × ${Math.round(r.height)}`, null],
      ["font", `${px(cs.fontSize)} / ${px(cs.lineHeight)} · ${cs.fontWeight}`, null],
      ["color", color ?? cs.color, tokenForHex(color)],
    ];
    if (cs.backgroundColor !== "rgba(0, 0, 0, 0)") rows.push(["background", bg ?? cs.backgroundColor, tokenForHex(bg)]);
    rows.push(["padding", px(cs.padding), null]);
    if (cs.borderRadius !== "0px") rows.push(["radius", px(cs.borderRadius), null]);
    if (cs.gap && cs.gap !== "normal") rows.push(["gap", px(cs.gap), null]);
    if (color && bg && cs.backgroundColor !== "rgba(0, 0, 0, 0)") rows.push(["contrast", `${contrast(color, bg).toFixed(2)} : 1`, null]);
    setPicked({
      tag: el.tagName.toLowerCase() + (el.getAttribute("role") ? `[role=${el.getAttribute("role")}]` : ""),
      box: { x: r.left - fr.left + root.scrollLeft, y: r.top - fr.top + root.scrollTop, w: r.width, h: r.height },
      rows,
      spec: el.closest("[data-spec]")?.getAttribute("data-spec") ?? null,
    });
  }, []);

  // Keep the selection box in place when the frame size changes.
  useEffect(() => {
    if (selected.current && frame.current?.contains(selected.current)) measure(selected.current);
  }, [width, s.frame, measure]);

  function pick(e: React.SyntheticEvent) {
    if (!s.inspect) return;
    const el = e.target as HTMLElement;
    if (e.type === "click") {
      e.preventDefault();
      e.stopPropagation();
    }
    selected.current = el;
    measure(el);
    setPanel("inspect");
  }

  const f = FRAMES.find((x) => x.id === s.frame)!;
  const bp = breakpointFor(width || 1280);
  const g = GRID[bp];
  const toggle = (k: "spec" | "grid" | "inspect") => setS((v) => ({ ...v, [k]: !v[k] }));
  const chip = (on: boolean) => `rounded-md border px-2.5 py-1.5 font-mono text-xs ${on ? "border-accent bg-accent-soft text-fg" : "border-border text-fg-muted hover:text-fg"}`;

  return (
    <div>
      <style>{SPEC_CSS}</style>
      {/* ---------- toolbar ---------- */}
      <div className="flex flex-wrap items-center gap-2 border-b border-border p-3">
        <div role="group" aria-label="Frame width" className="inline-flex overflow-hidden rounded-md border border-border">
          {FRAMES.map((x) => (
            <button
              key={x.id}
              type="button"
              aria-pressed={s.frame === x.id}
              onClick={() => setS({ ...s, frame: x.id })}
              className={`px-2.5 py-1.5 font-mono text-xs ${s.frame === x.id ? "bg-accent text-accent-fg" : "hover:bg-bg-muted"}`}
            >
              {x.label}
              {x.width ? <span className="hidden sm:inline"> {x.width}</span> : null}
            </button>
          ))}
        </div>
        <button type="button" aria-pressed={s.spec} onClick={() => toggle("spec")} className={chip(s.spec)}>
          spec overlay
        </button>
        <button type="button" aria-pressed={s.grid} onClick={() => toggle("grid")} className={chip(s.grid)}>
          {g.columns}-col grid
        </button>
        <button type="button" aria-pressed={s.inspect} onClick={() => toggle("inspect")} className={chip(s.inspect)}>
          inspect
        </button>
        <span className="font-mono text-xs text-fg-muted" aria-live="polite">
          {width ? `${width}px · ${bp}` : "measuring…"}
        </span>
        <button
          type="button"
          onClick={() => {
            setS(DEFAULTS);
            setPicked(null);
            selected.current = null;
            setPanel("tokens");
          }}
          className="ml-auto rounded-md border border-border px-2.5 py-1.5 font-mono text-xs hover:border-danger hover:text-danger"
        >
          reset demo
        </button>
      </div>

      <div className="grid xl:grid-cols-[minmax(0,1fr)_19rem]">
        {/* ---------- frame ---------- */}
        <div className="min-w-0 overflow-x-auto bg-bg-muted p-3 sm:p-5">
          <div
            ref={frame}
            className={`relative mx-auto overflow-hidden rounded-lg border border-border shadow-xl ${s.spec ? "af-spec" : ""} ${s.inspect ? "af-inspect" : ""}`}
            style={{ width: f.width ?? "100%", maxWidth: f.width ? undefined : "100%" }}
            onClickCapture={pick}
            onFocusCapture={pick}
          >
            <Landing />
            {s.grid && (
              <div className="pointer-events-none absolute inset-0 z-30" aria-hidden="true">
                <div
                  className="mx-auto grid h-full"
                  style={{ maxWidth: g.max === 480 ? "100%" : 1120, paddingInline: g.margin, gridTemplateColumns: `repeat(${g.columns}, 1fr)`, columnGap: g.gutter }}
                >
                  {Array.from({ length: g.columns }, (_, i) => (
                    <div key={i} className="h-full bg-[#ec4899]/10 outline outline-1 outline-[#ec4899]/30" />
                  ))}
                </div>
              </div>
            )}
            {s.inspect && picked && (
              <div
                className="pointer-events-none absolute z-40 outline-2 outline-[#2563eb] outline-solid"
                style={{ left: picked.box.x, top: picked.box.y, width: picked.box.w, height: picked.box.h }}
                aria-hidden="true"
              >
                <span className="absolute -top-5 left-0 rounded bg-[#2563eb] px-1.5 font-mono text-[10px] whitespace-nowrap text-white">
                  {Math.round(picked.box.w)} × {Math.round(picked.box.h)}
                </span>
              </div>
            )}
          </div>
          <p className="mx-auto mt-2 max-w-3xl text-center text-xs text-fg-muted">
            The page uses container queries, so these frame sizes reflow it exactly as a phone, tablet or desktop window would.
            {s.spec && " Hover outlined elements for their spec; tinted bands show section padding."}
            {s.inspect && " Inspect is on: click or tab to any element."}
          </p>
        </div>

        {/* ---------- handoff panel ---------- */}
        <aside aria-label="Design handoff" className="border-t border-border bg-bg-elev xl:border-t-0 xl:border-l">
          <div role="tablist" aria-label="Handoff" className="flex border-b border-border">
            {(["tokens", "inspect", "contrast"] as const).map((id) => (
              <button
                key={id}
                role="tab"
                id={`af-tab-${id}`}
                aria-selected={panel === id}
                aria-controls="af-panel"
                tabIndex={panel === id ? 0 : -1}
                onClick={() => setPanel(id)}
                onKeyDown={(e) => {
                  const ids = ["tokens", "inspect", "contrast"] as const;
                  if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
                  const next = ids[(ids.indexOf(id) + (e.key === "ArrowRight" ? 1 : 2)) % 3]!;
                  setPanel(next);
                  document.getElementById(`af-tab-${next}`)?.focus();
                }}
                className={`flex-1 border-b-2 px-3 py-2.5 font-mono text-xs ${panel === id ? "border-accent text-fg" : "border-transparent text-fg-muted hover:text-fg"}`}
              >
                {id}
              </button>
            ))}
          </div>
          <div id="af-panel" role="tabpanel" aria-labelledby={`af-tab-${panel}`} className="max-h-[52rem] space-y-5 overflow-y-auto p-4 text-sm">
            {panel === "tokens" && (
              <>
                <section>
                  <h3 className="font-mono text-xs font-semibold text-fg-muted">color</h3>
                  <ul className="mt-2 grid grid-cols-2 gap-2">
                    {(Object.entries(COLORS) as [ColorToken, string][]).map(([k, v]) => (
                      <li key={k} className="flex items-center gap-2">
                        <span className="size-6 shrink-0 rounded border border-border" style={{ background: v }} aria-hidden="true" />
                        <span className="min-w-0 leading-tight">
                          <span className="block truncate font-mono text-[11px]">{k}</span>
                          <span className="font-mono text-[10px] text-fg-muted">{v}</span>
                        </span>
                      </li>
                    ))}
                  </ul>
                </section>
                <section>
                  <h3 className="font-mono text-xs font-semibold text-fg-muted">type scale</h3>
                  <table className="mt-2 w-full font-mono text-[11px]">
                    <thead className="text-fg-muted">
                      <tr>
                        <th scope="col" className="text-left font-normal">
                          token
                        </th>
                        <th scope="col" className="text-right font-normal">
                          desktop
                        </th>
                        <th scope="col" className="text-right font-normal">
                          mobile
                        </th>
                      </tr>
                    </thead>
                    <tbody>
                      {Object.entries(TYPE).map(([k, t]) => (
                        <tr key={k}>
                          <td>{k}</td>
                          <td className="text-right">
                            {t.size}/{t.line} · {t.weight}
                          </td>
                          <td className="text-right text-fg-muted">{t.mobile}</td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </section>
                <section>
                  <h3 className="font-mono text-xs font-semibold text-fg-muted">spacing (px)</h3>
                  <ul className="mt-2 space-y-1">
                    {SPACE.map((n) => (
                      <li key={n} className="flex items-center gap-2 font-mono text-[11px]">
                        <span className="w-6 text-right">{n}</span>
                        <span className="h-2 rounded-sm bg-accent-3/60" style={{ width: n * 1.5 }} aria-hidden="true" />
                      </li>
                    ))}
                  </ul>
                </section>
                <section>
                  <h3 className="font-mono text-xs font-semibold text-fg-muted">radius · grid</h3>
                  <p className="mt-2 font-mono text-[11px]">
                    {Object.entries(RADIUS)
                      .map(([k, v]) => `${k} ${v === 999 ? "pill" : v}`)
                      .join(" · ")}
                  </p>
                  <ul className="mt-2 space-y-1 font-mono text-[11px] text-fg-muted">
                    {Object.entries(GRID).map(([k, v]) => (
                      <li key={k} className={k === bp ? "text-fg" : ""}>
                        {k} ≥{v.min}: {v.columns} col · gutter {v.gutter} · margin {v.margin}
                      </li>
                    ))}
                  </ul>
                </section>
              </>
            )}
            {panel === "inspect" &&
              (picked ? (
                <section aria-live="polite">
                  <p className="font-mono text-xs">
                    <span className="text-accent">{picked.tag}</span>
                  </p>
                  {picked.spec && <p className="mt-1 rounded bg-bg-muted px-2 py-1 font-mono text-[11px]">spec: {picked.spec}</p>}
                  <dl className="mt-3 space-y-1.5 font-mono text-[11px]">
                    {picked.rows.map(([k, v, token]) => (
                      <div key={k} className="flex justify-between gap-3 border-b border-border pb-1">
                        <dt className="text-fg-muted">{k}</dt>
                        <dd className="text-right">
                          {token && <span className="mr-1 inline-block size-2.5 rounded-sm align-middle" style={{ background: COLORS[token] }} aria-hidden="true" />}
                          {v}
                          {token && <span className="ml-1 text-accent">{token}</span>}
                          {k === "color" && !token && <span className="ml-1 text-warn-fg">(not a token)</span>}
                        </dd>
                      </div>
                    ))}
                  </dl>
                </section>
              ) : (
                <p className="text-fg-muted">
                  Turn on <strong>inspect</strong> and click any element in the page to read its size, type, colours (mapped back to tokens), padding and contrast.
                </p>
              ))}
            {panel === "contrast" && (
              <section>
                <p className="text-xs text-fg-muted">Every text/background pair the page uses, against WCAG 2.2 AA (4.5:1, or 3:1 for large text).</p>
                <ul className="mt-3 space-y-2">
                  {PAIRS.map((p) => {
                    const ratio = contrast(COLORS[p.fg], COLORS[p.bg]);
                    const pass = ratio >= (p.large ? 3 : 4.5);
                    return (
                      <li key={p.fg + p.bg} className="flex items-center gap-2">
                        <span className="grid h-8 w-12 shrink-0 place-items-center rounded border border-border text-sm font-bold" style={{ background: COLORS[p.bg], color: COLORS[p.fg] }} aria-hidden="true">
                          Aa
                        </span>
                        <span className="min-w-0 flex-1 leading-tight">
                          <span className="block truncate font-mono text-[11px]">
                            {p.fg} on {p.bg}
                          </span>
                          <span className="text-[11px] text-fg-muted">{p.use}</span>
                        </span>
                        <span className={`font-mono text-[11px] ${pass ? "text-accent" : "text-danger"}`}>
                          {ratio.toFixed(2)} {pass ? "AA ✓" : "fail"}
                        </span>
                      </li>
                    );
                  })}
                </ul>
              </section>
            )}
          </div>
        </aside>
      </div>
    </div>
  );
}