Use it elsewhere

Codex

Spin up a Flatpay-native Next.js project from one Codex turn — terminal CLI or ChatGPT cloud agent, same rules. The template scaffolds Next 16 + Tailwind v4 + React 19, wires `@flatpay-dk/ui` and the v4 preset, and writes an `AGENTS.md` at the repo root so every subsequent run inherits the system rules without re-pasting.

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.