01 · The prompt
One prompt, every project
Lovable starts a project from a single prompt — there’s no second turn before it scaffolds. The template below packs install, Tailwind v3 wire-up, system rules, and a slot for what you want built into one paste. The setup half is identical for every Flatpay project; only the section under == Project == changes. New Lovable projects scaffold Vite + React 18 + Tailwind v3, which is a first-class target for @flatpay-dk/ui — no Next, no React 19, no upgrade pressure.
Auth into GitHub Packages is the unsolved bit
@flatpay-dk/ui now lives on GitHub Packages, private to FLATPAY-DK. The install needs a token, and Lovable’s sandbox runs the install on its own infra — you can’t npm login there the way you do locally. If Lovable supports a per-project .npmrc with a registry token via env var, drop one in; otherwise the install step in the prompt below will 401 and the design system won’t install. The Tailwind preset / token wiring parts of the prompt are still useful to give Lovable so it stops scaffolding shadcn defaults — they just won’t actually compile until install works. We’re tracking this; ping in #flatpay-lab if you find a working path.
markdown
Set up the Flatpay design system in this project, then build
the project I describe at the bottom.
== Setup ==
1. Install from GitHub Packages (FLATPAY-DK org). Drop a
`.npmrc` at the project root with this single line:
@flatpay-dk:registry=https://npm.pkg.github.com
Then install:
npm install @flatpay-dk/ui
If Lovable's sandbox can't reach a private GitHub Packages
registry without auth, this step will 401. See the auth note
on the docs page; the rest of this prompt is correct regardless.
2. Replace tailwind.config.ts with the file below verbatim. Do
NOT keep the shadcn-style theme.extend.colors / borderRadius
blocks Lovable scaffolds by default — those redeclare every
token as hsl(var(--border)) / var(--radius), which the Flatpay
preset doesn't define, so every utility (bg-background,
text-foreground, border-border, bg-card, bg-primary, rounded-
lg, etc.) compiles against missing variables and resolves to
nothing. The preset must be the single source of truth:
import type { Config } from "tailwindcss";
import flatpay from "@flatpay-dk/ui/tailwind-v3";
import animate from "tailwindcss-animate";
export default {
darkMode: ["class", '[data-theme="dark"]'],
presets: [flatpay],
content: [
"./index.html",
"./src/**/*.{ts,tsx}",
"./node_modules/@flatpay-dk/ui/dist/**/*.js",
],
theme: {
container: { center: true, padding: "2rem" },
extend: {
keyframes: {
"accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" } },
"accordion-up": { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" } },
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [animate],
} satisfies Config;
If the existing config has a theme.extend.colors block (it
will if Lovable scaffolded shadcn primitives), DELETE it
entirely — same with theme.extend.borderRadius. Keep
container, the keyframes/animation pair (still used by any
leftover shadcn accordion), and the tailwindcss-animate
plugin. Everything else flows through the preset.
3. In src/index.css, after the Tailwind directives, import the
token CSS and the @font-face entry:
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "@flatpay-dk/ui/styles"; /* token CSS variables */
@import "@flatpay-dk/ui/fonts/css"; /* Inter Tight + Martian Mono */
After this, every Tailwind utility resolves through Flatpay's
tokens. Inter Tight + Martian Mono load on :root. Toggle dark
mode by setting [data-theme="dark"] on <html>.
== System rules ==
Use @flatpay-dk/ui as the only UI component source — no shadcn,
no @/components/ui/* re-exports. Available components include
Button, Banner, Toast, Card, Chip, ChipGroup, Avatar, Badge,
Combobox, Select, Search, DatePicker, Drawer, Modal, Menu (with
MenuItemCheckbox / MenuItemRadio / MenuTrigger / MenuContent),
Section, Page, PageHeader, PageBody, PageStickyFooter,
StickyFooter, Chart, and more.
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 single
wrapping component (e.g. src/components/PortalShell.tsx)
that every product route renders inside, so the chrome
isn't re-mounted between navigations:
// src/components/PortalShell.tsx
import { useState } from "react";
import {
NavigationPage,
NavigationTopBar,
NavigationToggle,
NavigationSideBar,
NavigationSearch,
NavigationItems,
} from "@flatpay-dk/ui";
export function PortalShell({ activeHref, children }: { activeHref: string; 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={activeHref}
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/pages/Dashboard.tsx
export default function DashboardPage() {
return (
<PortalShell activeHref="/dashboard">
<Page variant="main" width="default">
<PageHeader title="Dashboard" description="…" actions={…} />
<PageBody>{/* content */}</PageBody>
<PageStickyFooter>{/* optional — same width as Page */}</PageStickyFooter>
</Page>
</PortalShell>
);
}
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 and the column width.
Page variant rules:
• variant="main" — landing surface inside a section. Lives
inside NavigationPage. Never renders a back button (the
side nav 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. Render Page on its own there,
without wrapping in PortalShell. If the design says "no
rail", say "no rail" — don't fake it by hiding NavigationPage
at certain breakpoints.
Reach for:
• Page + PageHeader + PageBody (+ PageStickyFooter) — the
canonical scaffold for every route. Always.
• Button (variant="primary" | "secondary" | "tertiary" |
"success" | "danger") for actions.
• Banner / Toast for inline + transient feedback.
• Card for content tiles, Chip for tag-style filters.
• Drawer (side="bottom" size="lg") for mobile filters.
• Menu for popover dropdowns (never raw <ul>).
• Section for grouped content with optional accent surfaces.
Colours come from token utilities — bg-accent-{blurple|green|
orange|pink|blue|purple|yellow|red|natural}-subtlest and
text-foreground / text-muted-foreground / text-accent-{hue}.
No hard-coded hex.
Don't import @flatpay-dk/tailwind-preset (that's the Tailwind v4
variant for Next). Don't import { useFlatpayFonts } from
"@flatpay-dk/ui/fonts" (that's a next/font/local helper, Next
only). Don't try to install Founders Grotesk X (Klim license, not
in the public package — headings fall back to Inter Tight via
--font-display: var(--font-display, var(--font-sans))).
Naming collisions: the package exports StatusBadge. If the
project also defines a local StatusBadge for table cells (or
similar), rename the local component (e.g. RowStatus) so it
doesn't shadow the canonical one in import resolution + stack
traces. Flatpay's StatusBadge already forwards refs, so prefer
it for any status pill that lives inside a Table cell.
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 react-dom@^18 || ^19. Lovable’s default scaffold (React 18) is fully supported — no upgrade required. If the install warns about an unmet peer, prompt Lovable to ensure the project is on React 18 or later, then continue.
If everything looks unstyled, the shadcn shim is winning
Lovable’s default scaffold ships a tailwind.config.ts with a theme.extend.colors block that re-declares every shadcn token as hsl(var(--border)), hsl(var(--background)), etc. Tailwind merges extend on top ofpresets, so those redeclarations shadow every Flatpay token. The Flatpay preset doesn’t expose its colours as raw HSL channels — there is no --border, --primary, etc. — so utilities resolve against missing variables and the page renders unstyled (no surfaces, no borders, default browser fonts inheriting through). Same for theme.extend.borderRadius referencing a --radius that Flatpay doesn’t define. Delete both blocks.The full config in step 2 is already shadcn-shim-free.
02 · Example
What goes in the project slot
The body Lovable 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. Lovable will fill in the rest.
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. Wrap the route in <PortalShell activeHref="/payouts">
(the NavigationPage shell defined under "Two scaffolds"
above) so the top bar + side rail render around the page.
Inside the shell, wrap the page's content in <Page
variant="main" width="full">.
The PortalShell composes:
• NavigationTopBar (left → right): NavigationToggle (the
48 × 48 collapse-rail button at the leading edge — its
mode prop and onClick are wired to the same useState in
PortalShell that drives NavigationSideBar's mode prop,
flipping the rail between "expanded" and "mini"; never
omit it), NavigationBrand, 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 with
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 PortalShell + Page + the canonical
components.When the generation lands, two checks: every UI import should come from @flatpay-dk/ui (no @/components/ui/... shadcn paths), 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 is 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, if you want a sandbox outside Lovable to compare against.
- ExtendContributing — once a Lovable prompt produces something you want to keep, this is how it lands back in the system.
- ReferenceLovable · Design systems docs — Lovable’s own guidance on pulling external design systems into a project.