Single source of truth for every UI element. Build screens only from what's here. Each block: a live example (all states) + an HTML comment with NAME · WHEN TO USE · SNIPPET.
h1 page title · body 16px · .text-muted 14px secondary · .amount for € (tabular-nums).
Body text at 16px Inter. VAT due is €0.00.
Secondary text at 14px — metadata and subtext.
€1,300.00 / €4,021.70 / €894.01
A text link — tab to it for the focus ring.
.btn--primary main action (one per view) · plain secondary · --small toolbars/menus · --icon icon-only · --danger destructive · .is-applied green "✓ applied" confirmation JS sets for ~900ms after a money action commits, before the view moves on.
.field + .inputEvery input wrapped in .field with a .field__label. textarea.input for notes. Error = .field--error / aria-invalid.
.tx-dd · THE STYLE GUIDEPlaceholder state (nothing chosen) — is-placeholder on the button:
.icon-field.search-field.checkbox.radio.switchInstant on/off setting. Checkbox = include-in-set; switch = turn-setting-on.
.chip-groupSegmented single-select (selected = solid dark). For switching views/panels use Tabs below.
.chip-row--tabsSwitch between sections/panels of one screen/drawer. Active tab = white/raised.
.upload-box.file-dropSeveral documents on ONE question. Single fixed slot (logo, banner) = .upload-box above.
.inbox-row / .inbox-doc / .inbox-origPaperwork that has arrived and isn't filed yet. One engine — the business pile and the personal pile both mount it.
2 documents
Emailed in
Mountainview, Leopardstown, Dublin 18
We read €45.00 on 20 Jun 2024 from Vodafone. Not tied to anything in your bank yet.
.pchip / .pswitchOn every screen. Which books am I in, and switch. Injected — never hand-written into a page.
Your companies
You
.offer (personal-onboarding)Bullet Personal's sales pitch only. A thing in the offer + its price + one sentence.
Connect your own account — not your employer's, and they can never see it.
.data-table| User | Type | Access | |
|---|---|---|---|
| Suzanne | suzanne@bullethq.com | Administrator | Full access |
| Heidi | heidi@brady.ie | Member | Full access |
.txd-status / .txd-field--missingGLOBAL (31 Jul): every record detail panel wears this card under its title — returns, invoices, payslips — with linkable items that take you to the fix. New quiet variant .txd-status--wait for "filed, waiting on Revenue".
Field states — normal vs missing-highlight:
.data-grid / .view-toggle / .data-gridbarCard ⇄ Grid lens for power/bulk work. Same dataset + filters as the cards. States shown: complete (quiet tick), gaps (loud), selected, filed/locked.
| Sorted | 24 Feb | Marriott Hotel | Travel | 23% | -€280.00 | |
| No receipt | 24 Feb | Spar | General Expenses | 9% | -€24.50 | |
| Flagged | 23 Feb | Amazon Web Services | Software | 23% | +€1,200.00 | |
| Filed | 18 Feb | Riverside Joinery Ltd | Subcontractors | 13.5% | -€960.00 |
.row-card.stat-cards / .stat-card.section-titleHeading of every content section / drawer sub-group. Always the pink chip — never plain bold text. --sm for group labels; button.section-title for collapsible.
.taxmo / .taxdl12 month cards · current month marked · handled shown but quieter · red only for genuinely overdue · empty months get copy, never blank.
Nothing due. Go on your holidays.
.pill--paid/--active/--on green · --dashed "needs info" prompt · plain = neutral tag.
STATUS states, for an at-a-glance read beside a record's title: --paid green = done/nothing to do ·
--wait amber = waiting on someone else · --danger red = needs you · --muted grey = not started.
Record status (return header pill):
.todo / .todo--cardFull-width tappable row (icon + heading + chevron/check). Use for hub links, drawer action lists (Mark as sent / Pay with X / Download), and checklist items. The Figma "Button on/off" card.
Download invoice
PDF for your records
Payroll welcome
Done
Sole trader payment
Locked until the step above is done
.filter-panelRight-hand panel — filter form or a task/settings drawer. --drawer is fixed-right; the page <main> needs app__main--with-drawer. Fill the body with section-title chips + todo--card rows. Spacing (global, space + calm): every docked drawer uses the drawer tokens — --drawer-pad 32px, --drawer-pad-bottom 48px, --drawer-section-gap 24px, --drawer-row-pad 18px, --drawer-row-gap 14px. Never hand-pick drawer paddings (see CLAUDE.md "Drawer spacing"). Motion (global): slides in on open; the X slides it back out with a little wind-up bump (ui.js + filter-panel.css). Close button: data-close-href / data-close-back / nothing (hide).
.fseg / .fsumUnlinked surfaces floating records the accounts engine can't square — a deposit/transaction not linked to an invoice, or an invoice not linked to a payment. It's a normal stackable filter chip. Built as a segmented .fseg group (Invoices) / .tx-filtseg group (Transactions) with data-filter="unlinked" so the shared live-filter engine, orange section dot, removable chip and Clear all work with no extra wiring. Read the linked status per row — Invoices: data-unlinked on the row; Transactions: data-linked (absence = unlinked). TODO(backend): supply the real reconciled/linked status and stamp those attributes from it (both screens infer a demo value today — see their "Unlinked filter hook" comments). When active, show the plain-language .fsum__note line.
Unlinked (default)
Unlinked (active)
Showing unlinked items — not yet linked to an invoice or document.
Showing unlinked items — not yet linked to a payment or bank transaction.
.pr-dash / .exp-chip (candidate — promote to CSS)Live on wages-payslips.html — subsistence rate chips (fixed Revenue rates fill the amount), the mileage drawer's band-split € calc, and the one-per-day allowance guard.
.pay-saved (candidate — promote to CSS)Your €6,133 of mileage and expenses came back to you tax free. Take the same money as wages and you'd have handed Revenue another €8,141. Bullet works that out every time you run payroll.
src/js/payroll-reset.js (self-injecting)Idle + armed states. Live on every payroll/wages screen, fixed beside the logout corner.
#inv-peek-drawer)Peek drawer — browse-in-place preview for filtered lists: clicking a row opens/swaps this drawer (totals + What happened, newest first) instead of navigating, so the filter and scroll position survive. Same chrome as every drawer (back arrow, breadcrumb title, X, --drawer-* rhythm, sticky footer). The browsed row gets the yellow inset selection ring. Reference build: invoice-home-settings.html (openPeek/closePeek).
INV 1033
Kavanagh & Sons Builders · Overdue
window.openReceiptsDrawer()Per-transaction receipts manager: pink Add-documents dropzone (with a "Get the app" QR), Forward-receipt takeover (receipts@bullethq.com), and a receipts list with All/Matched/Unmatched tabs (default Unmatched) — each row a checkbox (attach to this transaction), a thumbnail (click → preview modal) and a kebab (Download / Detach / Delete). Built + opened by openReceiptsDrawer(cfg) in ui.js — never hand-build it. Wired into bank-transactions (row paperclip + detail "Add receipt").
window.startGuideTour() / window.openDemoChat()Demo customer-help assistant: the bottom-right help bubble opens a chat; typing “how do I attach a receipt?” makes it run a coach-mark walkthrough over the real bank-transactions UI — spotlight the paperclip → tick a receipt → done. Engine is generic (startGuideTour(steps)); intent matching + the receipt script live in ui.js. No backend.
window.uiConfirmActionOnly for what cannot be undone. States the consequence with real figures; the button repeats the action.
.modal-scrim / .modalA focused blocking decision (rare — prefer a drawer). Scrim over the page; Escape + × close via ui.js.
uiToast().welcome-cardWhat is payroll?
.plan-cardAll-in, unlimited users. Renews 1 July 2026.
.note-cardYour accountant gets a free guest account — it doesn't use a paid seat.
.danger-zoneYour account stays open until the end of the billing period. Export your data first.
.avatarThe demo item — marked, and never saved
Practice Supplier Demo General expenses · −€40.00The chooser — one question, several meanings
That could mean a few different things. Which one are you after?
A journey step — says what is next, then moves
The VAT lives on each line of the invoice, so that is where we are headed.
The flash the step lands on — uiHighlightField / .txd-field--flash
.answer__figsYou are owed €12,907 right now.
That is across 10 invoices. About €6,907 of it is late.
.ask-box · THE GLOBAL SEARCH BOX.ask-float · THE SAME BOX, FLOATINGHow much am I owed?
You are owed €12,907 right now.
Across 10 invoices — €6,907 of it is overdue.
what about the vat
vat
Did you mean:
.voice · THE VOICE SKIN ON THE ASK ENGINEMorning,
Peter. Ask me a question about your business.
Listening…
One second…
“How much am I owed?”
You are owed €12,907 right now.
You are owed €12,907 right now.
Not sure I caught that — did you mean one of these?
Did you mean:
I didn’t catch anything — tap the mic and try again
Bullet can’t hear you — your browser has the microphone blocked. Allow the mic in your browser settings, or type instead.
I can’t reach the listening service right now. You can type instead.
preview/feature-map.html.status-pill.needs (+ the calm empty state)Dashboard only. One card, one row per job, count leading. Rows tap through to the pre-filtered view — reuse the existing filter system, never a bespoke list.
EMPTY — the state that matters most:
Nothing needs you right now.
Your books are up to date and nothing is due. We'll put anything that comes up right here.
.ntc-*)ONE letter view — the TAX INBOX's document pane (tax-inbox.html: list → synopsis → the letter itself as a document page, .ntc-letter inside the shared .inbox-orig frame). Home's needs-you rows and the Ask box's "Read the letter" both deep-link tax-inbox.html?notice=<id> — the Home DRAWER was retired 13 Aug (Peter: bring them to the inbox; two renderings of one letter would drift). A Home row is earned by an action or a deadline; receipts/statements live in the inbox only. Feed: data/revenue-notices.js (sample) — on localhost a gitignored revenue-notices.local.js (the ros-agent's real letters) replaces it. No tax advice anywhere — what arrived, when it's due, talk to your accountant.
The drawer body (loud state — deadline inside 14 days, payment asked for):
Needs an answer by 22 Aug 2026 — 11 days away.
Check whether the payment left your bank. If it did, match it in Bullet so the books agree. If it did not, the amount below is what Revenue are waiting on.
The payment for your March–April VAT return has not reached Revenue.
Through ROS (Revenue Online Service) · by 22 Aug 2026
Received 8 Aug 2026 · Deadline 22 Aug 2026
Not sure what it means? Talk to your accountant.
.dash-aside / .dash-claim / .dash-nextDashboard Tax section. Verdict line always does the arithmetic. No gamification on the claimed figure — ever.
You're covered — €27,331 more than you owe.
You're €4,200 short. Move it across before the next return is due.
.cashchartDashboard only, and only cash. Always ship the __basis footnote — an unexplained chart is worse than no chart.
Built from your €129,199 bank balance, €9,675 of tax already filed or due, and €12,907 of unpaid invoices placed on their due dates — anything already overdue is assumed to land within a fortnight. It's cash only, straight off your bank, so the 23 transactions still to categorise don't change it. It doesn't include anything you haven't invoiced or scheduled yet.
.setup (pre-onboarded dashboard)Pre-onboarded dashboard only. Three steps, progress visible, each opening the real flow. No €0 cards anywhere on this screen.
.answer (Ask surface + weekly summary)Ask surface + weekly summary only. One container for every answer. Max two numbers, plain voice, chart/link/follow-ups all omittable.
Full — chart, "show me" link + follow-ups
You are owed €12,907 right now.
That is across 10 invoices. About €6,907 of it is late, so a quick nudge would help.
See the 5 overdue invoicesMinimal — headline + body only
Yes — you have €37,006 put by for tax.
That covers what is coming up. Keep new sales money going in there and you will stay ahead of it.
Loading — calm skeleton, no spinner
Can't answer — no apology, a ready-made question to paste elsewhere
That is not something your books can tell you.
Bullet only knows what is in your account — your money, invoices, bills and returns. For anything wider, here is a question you can paste into your own AI or a search, with your figure filled in.
Copy this and paste it anywhere
For a small business in Ireland with about €12,907 owed by customers, what should I be thinking about?
Did you mean — near-miss chips instead of a dead end
I am not sure which you mean.
Did you mean one of these?
How-to — hands off to a walkthrough instead of answering with figures
Create an invoice.
Easier to show you than tell you. I will bring you to the right screen and point at what to tap — you can stop any time.
.pr-status / .pr-return / .pr-next (candidate — promote to CSS; lives inline on wages-payslips)ONE icon family — the circle is constant, what's inside carries the state. Same r=10 geometry, stroke-1 (global icon rule), at 19px on card headers (.pr-status) and 15px in the pill row (.pr-return), so the two always agree.
The WAITING state keeps a filled dot inside the ring, not a bare outline: at 19px a stroke-1 ring is too quiet for the one thing that needs attention. Colour does the shouting. Rendered by statusMark(steps) / markHTML(state, tip) / returnsRow(steps) on wages-payslips.
.tl-receipt + chevron .tl-chev (candidate — promote to CSS; lives inline on invoice-detail).li-card (in invoice-line-item.css)Every in-app screen = .app > .sidebar (+ optional .submenu) + .app__main + .page-header + floating .app__corner. Copy the shell from an existing screen; never hand-roll nav. Canonical: src/screens/settings-company.html.
All employee wages, payslips, search and filter.
Sidebar + submenu + corners render full-screen — see any file in src/screens/.
You’ve invoiced €11,300 and spent €8,842 — you’re €2,458 ahead so far.
That’s 49% of the budget used, with 76% of the timeline gone.
Live demo: the button opens the mock OAuth moment (Connecting… → Google-ish chooser). Rows + kebab actions are on settings-connected-emails.html. Never build a second connect UI — call the shared flow.
Live on report-profit-loss.html + report-balance-sheet.html — click any figure. Never hand a report page its own drill UI — the layer is the one module, and the transactions are derived from the page's own data object so they can't disagree with the table.
← the "has a kept note" marker that sits beside a figure.
Flag off by default. Flip with
?proposals=on, then: the board ·
create ·
the customer page.