Use it elsewhere

Cursor

Spin up a Flatpay-native Next.js project from a single Cursor prompt. The template scaffolds a Next 16 + Tailwind v4 + React 19 app, wires `@flatpay-dk/ui` and the Tailwind preset, and gives Cursor the rules of engagement for composing canonical components, reading tokens, extending what's there, and authoring new pieces only when they don't already exist.

01 · The prompt

One prompt, every project

Cursor builds iteratively, but the first turn sets the foundation — get the install, the wire-up, and the system rules locked in before any feature work and the rest of the session compounds. Paste the template below into a fresh project and edit only the section under == Project ==. The setup half is identical for every Flatpay-native app.

markdown

Set up the Flatpay design system in this project, then build
the project I describe at the bottom.

== Setup ==

1. Scaffold (skip if the workspace already has a Next.js app):

   pnpm create next-app@latest .
     --typescript --eslint --app --tailwind --src-dir --turbopack
     --import-alias "@/*"

   Targets Next.js 16 (App Router), React 19, Tailwind v4. Use
   pnpm; the project's lockfile assumes it.

2. Install from GitHub Packages (FLATPAY-DK org). Assumes the
   user has already run a one-time `npm login --scope=@flatpay-dk
   --registry=https://npm.pkg.github.com` with a classic PAT
   (read:packages scope, SSO-authorized for FLATPAY-DK). If the
   install 401s, point them at the Getting started page on the
   docs site; don't try to authenticate yourself.

   Drop a `.npmrc` at the project root:

   @flatpay-dk:registry=https://npm.pkg.github.com

   Then:

   pnpm add @flatpay-dk/ui @flatpay-dk/tailwind-preset

3. In src/app/globals.css, replace the default Tailwind import
   with the preset chain. Tailwind v4 is CSS-first — there is no
   tailwind.config.{ts,js}; the preset wires every token and the
   .eyebrow utility classes through @theme:

   @import "tailwindcss";
   @import "@flatpay-dk/tailwind-preset";

   /* Pull utilities used inside @flatpay-dk/ui's source so any
      class the package emits is detected by Tailwind's scanner.
      Adjust the relative depth to match your project. */
   @source "../../node_modules/@flatpay-dk/ui/dist/**/*.js";

   html, body {
     background: var(--background);
     color: var(--foreground);
   }

   /* Default border color for every element. Wrap in @layer base so
      Tailwind utility `border-{color}` rules can still override it. */
   @layer base {
     * { border-color: var(--border); }
   }

4. Wire fonts in src/app/layout.tsx via the package's named
   exports — Inter Tight + Martian Mono ship as next/font/local
   instances. Apply them on <html> via their `.variable` strings
   so the Tailwind preset's `--font-sans` and `--font-mono` are
   resolved:

   import { interTight, martianMono } from "@flatpay-dk/ui/fonts";

   export default function RootLayout({ children }: { children: React.ReactNode }) {
     return (
       <html
         lang="en"
         className={`${interTight.variable} ${martianMono.variable}`}
         suppressHydrationWarning
       >
         <body className="bg-background text-foreground antialiased">
           {children}
         </body>
       </html>
     );
   }

   Don't reach for { useFlatpayFonts } from "@flatpay-dk/ui/fonts" —
   Next 16 rejects its compiled output ("Font loader calls must
   be assigned to a const"). The named imports above are the
   canonical Next 16 pattern and work in Next 15 too.

   Toggle dark by setting [data-theme="dark"] on <html>. Headings
   that reach for `font-display` fall back to Inter Tight via
   `--font-display: var(--font-display, var(--font-sans))` —
   Founders Grotesk X is Klim-licensed and ships only on internal
   Flatpay surfaces, so don't try to install it.

== System rules ==

Use @flatpay-dk/ui as the only UI source — no shadcn,
no @/components/ui/* re-exports, no headless-ui, no Radix
imported directly. The package is the canonical layer.

Two scaffolds, used together. Get this wrong and the route
loads without the chrome.

  • NavigationPage — the app SHELL. Renders the top bar + side
    rail and gives the route a content slot. Lives in a
    layout file (e.g. src/app/(portal)/layout.tsx) so a
    section of routes shares one shell:

      // src/app/(portal)/layout.tsx
      "use client"; // sidebar mode is React state — the layout owns it
      import { useState } from "react";
      import {
        NavigationPage,
        NavigationTopBar,
        NavigationToggle,
        NavigationSideBar,
        NavigationSearch,
        NavigationItems,
      } from "@flatpay-dk/ui";

      export default function PortalLayout({ children }: { children: React.ReactNode }) {
        const [mode, setMode] = useState<"expanded" | "mini">("expanded");
        return (
          <NavigationPage
            topBar={
              <NavigationTopBar>
                <NavigationToggle
                  mode={mode}
                  onClick={() => setMode((m) => (m === "expanded" ? "mini" : "expanded"))}
                />
                {/* NavigationBrand, NavigationTopBarSpacer, utilities, NavigationLocation */}
              </NavigationTopBar>
            }
            sideBar={
              <NavigationSideBar
                mode={mode}
                activeHref={/* current path */}
                onExpandRequest={() => setMode("expanded")}
              >
                <NavigationSearch />
                <NavigationItems merchant="POS" />
              </NavigationSideBar>
            }
          >
            {children}
          </NavigationPage>
        );
      }

  • Page — the per-route SURFACE that lives inside the
    NavigationPage's content slot. Holds PageHeader +
    PageBody + optional PageStickyFooter:

      // src/app/(portal)/dashboard/page.tsx
      export default function DashboardPage() {
        return (
          <Page variant="main" width="default">
            <PageHeader title="Dashboard" description="…" actions={…} />
            <PageBody>{/* content */}</PageBody>
            <PageStickyFooter>{/* optional — same width as Page */}</PageStickyFooter>
          </Page>
        );
      }

Always wrap a route's content in <Page> — never compose a
screen directly out of <main>, raw <header>, or hand-rolled
wrappers. Page owns the H1 typography (Founders Grotesk X-
Condensed → Inter Tight fallback) and the column width.

Page variant rules:
  • variant="main" — landing surface inside a section. Lives
    inside NavigationPage. Never renders a back button (the
    side rail goes up the tree).
  • variant="subpage" — nested or detail page. backHref is
    required; the page itself renders the back affordance.
    Sits inside NavigationPage unless the route is meant to
    escape the chrome.
  • width — "default" (max-w-4xl), "narrow" (max-w-2xl),
    "full". PageStickyFooter inherits whichever you pick so
    the bar's actions land under the form's content column.

When NOT to render NavigationPage: routes that should escape
the portal chrome — sign-in, error pages, marketing landing
pages, focused checkout steps. Put those routes OUTSIDE the
section's layout-file group so they don't inherit the shell,
then render Page on its own. If the design says "no rail",
say "no rail" — don't fake it by hiding NavigationPage at
certain breakpoints.

Reach-for cheat sheet (every export ships from the root):
  • Page surface: Page (variant="main" | "subpage", width=
    "default" | "narrow" | "full"), PageHeader, PageBody,
    PageStickyFooter — the canonical scaffold for every route.
  • Actions: Button (variant="primary" | "secondary" | "tertiary"
    | "success" | "danger"), ButtonGroup, Link.
  • Status + feedback: Banner, Toast / ToastStack /
    ToastProvider / useToast, Badge, StatusBadge, Chip / ChipGroup.
  • Inputs: TextField, NumberField, Search, Combobox, Select,
    DatePicker, TimePicker, Checkbox, RadioGroup, Toggle,
    FileUpload, ChoiceList.
  • Surfaces: Card, Section, Hero, Page, Drawer, Modal, Dialog,
    Popover, Tooltip, Collapsible, FAQ, Tabs.
  • Tables + data: Table, EditableTable, MetricCard / MetricsGroup,
    SummaryBox, Chart, Filters, Pagination.
  • Navigation: NavigationPage (the canonical portal shell — top
    bar + side rail are never used independently),
    NavigationTopBar, NavigationSideBar, NavigationItems
    (merchant: "PAY" | "POS" | "Ecom", default "POS"; admin
    flag prepends the back-office block), NavigationItem,
    NavigationSubItem, NavigationLocation (variant: "chip" |
    "secondary"), NavigationSearch, NavigationBrand,
    NavigationToggle, NavigationUtility, NavigationDivider.
  • Layout primitives: StickyFooter (type: "fixed" | "floating",
    maxWidth: "full" | "7xl" | "4xl") — match its maxWidth to the
    page's content column. Floating is reserved for table-row
    bulk actions; don't reach for it elsewhere.
  • Iconography: Icon wrapper + the FlatpayLogo / FlatpayMarque
    brand marks. Pair with Material Symbols Outlined glyphs.

Tokens are how colour and spacing reach the screen. Use the
Tailwind utilities the preset wires up — never hard-coded hex,
never raw numeric Tailwind shades:

  • Surfaces: bg-background, bg-card, bg-muted,
    bg-accent-neutral-subtlest / -subtler / -subtle (the layering
    rule: subtlest opens, subtler is a card on canvas, subtle is
    an inner highlight; never open on subtle), and the categorical
    accents — bg-accent-{blurple|green|orange|pink|blue|purple|
    yellow|red|natural}-subtlest.
  • Text: text-foreground, text-muted-foreground,
    text-accent-{hue}, text-success / text-warning /
    text-destructive / text-info / text-discovery.
  • Status: bg-success / bg-warning / bg-destructive / bg-info /
    bg-discovery — pair with their *-foreground siblings.
  • Borders + outline: border, border-outline, border-outline-hover,
    ring-ring (focus). Default border is var(--border); avoid
    `border-gray-200` and friends.
  • Spacing: 4-pt scale via Tailwind utilities (gap-1 → gap-24).
    Don't author new arbitrary values when a step exists.

When to compose, when to extend, when to author new

  • Compose first. 80% of screens are existing components arranged
    via the system's spacing scale. If the brief describes a
    pattern that already has a name (Banner, Toast, Drawer,
    StickyFooter, NavigationPage), use it verbatim.
  • Extend second. If a canonical component covers 90% of the job
    but a prop is missing — a new variant, an additional slot,
    a tone — extend the existing component. Add the prop, keep
    the existing API additive (default to current behaviour),
    and contribute the change back via a PR against
    FLATPAY-DK/lab-ui (a changeset describing the patch).
    Don't fork inline; the docs site is the source of truth.
  • Author new only when the pattern doesn't exist in the system
    AND the brief is primitive enough to be reusable. Put the
    component in src/components/<name>.tsx, build it from
    @flatpay-dk/ui primitives + token utilities, and document
    its props with TSDoc. If three pages reach for it, it's a
    candidate to upstream into @flatpay-dk/ui. If it's a
    one-off page composition, leave it in the app.
  • Never copy a canonical component's source into the app to
    "tweak" it — that fragments the system. Use the existing
    component's className override or asChild prop, or extend
    upstream.

Don't import @flatpay-dk/ui/tailwind-v3 (that's the legacy
Tailwind v3 preset for Lovable / Vite projects). Don't pull in
shadcn-style `@/components/ui/*` paths. Don't try to install
Founders Grotesk X.

Docs: https://design.flatpay.dev/

== Project ==

[Replace this line with what you want built — what the screen is,
who it's for, which Flatpay components to reach for. See the
example below this template if you need a shape to copy.]

Peer-dep note

The package targets react@^18 || ^19 and next@^15 || ^16. Cursor’s default Next.js scaffold (16 / React 19) is the primary target — no upgrade needed. Older projects on React 18 + Next 15 work too; the preset and the components support both.

02 · Example

What goes in the project slot

The body Cursor actually builds from. Drop something like this into the == Project == section of the template above. Name the screen, name the audience, then point at the components you expect — concrete beats abstract. Cursor fills in the layout and wires the state.

markdown

Build the Payouts page in the merchant portal at /payouts.
This is the route a logged-in merchant lands on when they
click "Payouts" in the portal's side rail. The page shows
their payouts across a chosen date range — total collected,
surcharges, net collected, fees, Capital repayments, and
net payout per period — and exports the same ledger to PDF
or XLS for reconciliation against the merchant's accounting
software.

This is a "main" page surface — render the FULL portal
chrome. NavigationPage wraps the route via the section's
layout file (src/app/(portal)/layout.tsx) so /payouts and
every other portal route share one shell — top bar + side
rail render once and don't remount on navigation. The
/payouts route's own page.tsx wraps its content in <Page
variant="main" width="full">. The active rail item is
/payouts.

NavigationTopBar contents (left → right):
  • NavigationToggle — the 48 × 48 collapse-rail button at
    the leading edge. Its mode prop and onClick are wired
    to the same useState hook in the layout that drives
    NavigationSideBar's mode prop. Clicking flips the rail
    between "expanded" (272 px, labels visible) and "mini"
    (80 px, icon-only). Without this control there's no way
    to collapse the rail — never omit it.
  • NavigationBrand (Flatpay logo).
  • NavigationTopBarSpacer.
  • NavigationUtility ariaLabel="What's new" tone="yellow"
    (icon: campaign).
  • NavigationUtility ariaLabel="Downloads" (icon:
    file_download).
  • NavigationUtility ariaLabel="Help" (icon: help_outline).
  • NavigationUtility ariaLabel="Account" (icon:
    account_circle).
  • NavigationLocation merchant="Ristorantes Argentinos
    Streetfood ApS" location="Berlin West" (truncates with
    ellipsis when the merchant name overflows).

NavigationSideBar in expanded mode, activeHref="/payouts".
Use <NavigationItems merchant="POS" /> for the canonical
row stack — Dashboard, Transactions, Payouts (with
indicator="dot"), Reports (chevron), Devices (chevron),
Staff (chevron), Online (badge "New"), Capital (badge
"New"), Accounting, Settings (chevron), My business
(chevron). NavigationSearch sits above the list with
placeholder "Search the portal…".

Inside the Page, build:

1. PageHeader.
   • title: "Payouts".
   • description: "Track your payouts across any period
     and export the data straight to your accounting
     software for reconciliation."
   • No actions slot here — the export action lives in the
     toolbar below so it sits next to the date range it
     operates on.

2. PageBody — single column with three stacked blocks:

   a. Toolbar row (h-12, gap-3 between left-cluster items).
      Left cluster:
        • Date-range stepper — left chevron Button
          (variant="tertiary", icon-only), a 14 px
          semibold "8 Feb 2024" label centred between
          chevrons, right chevron Button. Clicking either
          chevron shifts the visible range by one calendar
          day / week / month.
        • Select with the value "Calendar Day" (granularity
          options: Calendar Day, Calendar Week, Calendar
          Month).
        • Icon-only Button (variant="tertiary") with a
          "view_column" Material Outlined glyph — opens a
          column-customizer popover that toggles each
          table column's visibility.
      Right edge:
        • Button variant="primary" with a leading
          file_download icon — label "Export". Clicking
          opens a Menu with two MenuItems, "Payouts PDF"
          and "Payouts XLS", both wired to the export
          endpoint with the current date range pre-applied.

   b. MetricsGroup with five MetricCards (responsive: 2-up
      below md, 3-up below lg, 5-up at lg+). bg-card, no
      border, no chart sparkline. Eyebrow + value, in
      order:
        • Transactions    — "5.981"
        • Total collected — "32.198,00 €"
        • Net collected   — "31.872,30 €"
        • Total fees      — "725,40 €"
        • Net payout      — "31.146,90 €"
      Values render with font-variant-numeric: tabular-nums
      slashed-zero so digits line up as the numbers update.

   c. Table (canonical Table from the package, density
      "compact", sticky header, no row selection — this is
      a read-only ledger). Columns left → right:
        • Date              — left-aligned, "2024-01-27".
        • Period            — text-muted-foreground,
          "01-22 to 01-24".
        • Total collected   — right-aligned, tabular-nums.
        • Surcharges        — right-aligned, tabular-nums.
          Header carries a help_outline glyph wrapped in a
          Tooltip: "Surcharges are extra fees charged on
          certain card schemes; passed through to the
          merchant." Empty cells render an em-dash "—" in
          text-muted-foreground.
        • Net collected     — right-aligned.
        • Fees              — right-aligned.
        • Capital repayments — right-aligned.
        • Net payout        — right-aligned, font-semibold.
      Pagination at the bottom uses the canonical
      Pagination component.

      Seed eight rows that mirror the screenshot:
        2024-01-27 | 01-22 to 01-24 | 6.615,30 € | 20,67 € |
          6.594,63 € | 96,82 € | 1.096,82 € | 6.401,18 €
        2024-01-22 | 01-18 to 01-21 | 5.337,02 € | 11,27 € |
          5.326,67 € | 85,23 € | 985,23 €  | 5.241,72 €
        2024-01-20 | 01-15 to 01-20 | 8.214,58 € | 20,67 € |
          8.012,53 € | 123,82 €| 1.123,82 € | 7.293,68 €
        2024-01-15 | 01-13 to 01-14 | 5.337,02 € | —       |
          5.326,67 € | 85,23 € | 1.085,23 € | 5.241,72 €
        2024-01-13 | 01-08 to 01-12 | 6.641,89 € | —       |
          6.641,89 € | 103,23 €| 1.103,23 € | 6.486,88 €
        2024-01-08 | 01-06 to 01-07 | 8.214,58 € | 12,23 € |
          8.012,53 € | 123,82 €| 1.123,82 € | 7.293,68 €
        2024-01-06 | 01-03 to 01-05 | 5.337,02 € | —       |
          5.326,67 € | 85,23 € | 885,23 €  | 5.241,72 €
        2024-01-02 | 12-29 to 01-02 | 6.641,89 € | —       |
          6.641,89 € | 103,23 €| 1.103,23 € | 6.486,88 €

Mobile (below md) adaptations:
  • PageHeader collapses to title + description plus a
    single trailing icon-Button (file_download glyph) that
    opens the same Payouts PDF / XLS menu.
  • Toolbar: drop the column-customizer Button; keep the
    date-range stepper and granularity Select.
  • Above the metrics, add a Search input + a Filters
    trigger (icon-only Button with a count Badge) so the
    merchant can scope a long ledger on a small screen.
  • MetricsGroup reflows to 2 columns × 3 rows; add a
    "Pending" card (e.g. payouts in flight) to fill the
    sixth slot.
  • Table collapses to two columns: Date — with the
    period range as a small caption beneath the date — on
    the left; Net payout (with a Badge for status:
    "Completed" / "Pending" / "Failed") and a chevron-
    right glyph on the right. Each row links to the row's
    detail subpage at /payouts/[id]. Use a compact
    Pagination variant: "1 of 1" with prev/next chevrons.

Wire colours through canonical tokens:
  • bg-background for the page surface; bg-card for the
    metric tiles.
  • text-foreground for headings and primary cell values,
    text-muted-foreground for secondary cells (Period,
    placeholder em-dashes), text-success-foreground for
    the Completed status Badge on mobile.
  • All numeric cells use tabular-nums slashed-zero so
    columns align across rows regardless of digit width.

No hard-coded hex. No raw <main>, <header>, or hand-rolled
wrappers. Compose through NavigationPage + Page + the
canonical components.

Two checks once the generation lands. Every UI import should come from @flatpay-dk/ui (no @/components/ui/... shadcn paths, no inline copies of canonical components), and Tailwind utilities should resolve through tokens like bg-accent-blurple-subtlest and text-foreground. If imports come back as @/components/ui/... the install or the system rules section was dropped — confirm @flatpay-dk/ui and @flatpay-dk/tailwind-preset are both in package.json and re-run with the full template.

What’s next

Beyond the wire-up

  • InstallGetting started — the same package wired into a hand-rolled Next.js project, useful as a sandbox to compare what Cursor generated against the canonical install.
  • ExtendContributing — when a Cursor session produces a component you want to keep, this is how it lands back in @flatpay-dk/ui.
  • ReferenceCursor · Rules for AI — for sessions that span multiple turns, lift the == System rules == block into .cursor/rules/flatpay.mdc so every prompt inherits it without re-pasting.