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.mdcso every prompt inherits it without re-pasting.