01 · The prompt
One prompt, every project
Codex reads project rules from an AGENTS.md at the repo root in both surfaces — the local CLI and the ChatGPT cloud agent — so once the file exists, every turn in either place inherits the same rules. The template below installs the system, writes the rules file, and runs the project you describe in one paste. Edit only the section under == Project ==.
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";
@source "../../node_modules/@flatpay-dk/ui/dist/**/*.js";
html, body {
background: var(--background);
color: var(--foreground);
}
@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>.
5. Write AGENTS.md at the repo root with the system rules below.
Codex re-reads this file on every turn in both the CLI and
the ChatGPT cloud agent, so the rules persist across surfaces
without me re-pasting them. If an AGENTS.md already exists,
append a "## Flatpay design system" section instead of
overwriting.
== System rules ==
(write into AGENTS.md verbatim, under the "## Flatpay design
system" section)
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 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, 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.
• 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 (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).
• Spacing: 4-pt scale via Tailwind utilities (gap-1 → gap-24).
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.
• Extend second. If a canonical component covers 90% of the
job but a prop is missing, extend the existing component
(additive API, default to current behaviour) and contribute
the change back upstream via a PR + changeset against
FLATPAY-DK/lab-ui. Don't fork inline.
• Author new only when the pattern doesn't exist AND the
brief is primitive enough to be reusable. Build it from
@flatpay-dk/ui primitives + token utilities. If three
pages reach for it, upstream it.
• Never copy a canonical component's source into the app to
"tweak" it — that fragments the system. Use className
overrides or asChild slots, or extend upstream.
Don't import @flatpay-dk/ui/tailwind-v3 (the legacy v3 preset
for Lovable / Vite). Don't reach for 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.]On AGENTS.md persistence
AGENTS.md is the cross-tool agent rules file. Codex reads it from the repo root on every turn — both the local CLI (npm i -g @openai/codex) and the ChatGPT cloud agent — so the same project behaves consistently regardless of which surface you’re driving from. If you’re also using Claude Code in the same repo, keep the Flatpay rules in CLAUDE.md and have AGENTS.md reference it (or duplicate the section) so neither tool drifts.
02 · Example
What goes in the project slot
The body Codex 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.
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 work 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/..., confirm AGENTS.md actually has the system-rules section and ask Codex to re-read it.
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 the generated output against the canonical install.
- ExtendContributing — when a Codex session produces a component you want to keep, this is how it lands back in
@flatpay-dk/ui. - ReferenceAGENTS.md spec — the cross-tool agent rules file convention that Codex, Claude Code, and other agentic tools read.