Two weeks ago I wrote about why I threw away three weeks of work on our Communications redesign. The part of that post I did not finish telling: the prototype I kept working on is quietly becoming a design system.
Over the last two weeks, I have been pushing commits to a feature branch of
Billabex’s in-progress design system, Lingot, to bring it in line with the
Billabex workspace prototype: explore/workspace-ui/workspace.html, 6,175
lines, one static HTML file. 109 commits so far. Not merged. Not released. Not
wired into the production app. Lingot is not live.
What exists today is a branch, a working Storybook running on Chromatic, and a lot of code that needs one more pass before it is ready to ship.
This post is the full process: the foundation Quentin laid down before I touched a line of code, the audit docs I generated from the prototype, the loop I used to pixel-match components inside Claude Code, and the honest parts: where it hurt, what I still have to fix, and why the branch is nowhere near clean.
Branch: github.com/billabex/lingot/tree/gillou/design-system-update.
The foundation: what Quentin built before I showed up
This is the part of the story that is easy to skip, and it is the reason any of the rest worked.
On March 11, Quentin spent a day initializing Lingot. Eight commits across one afternoon. What he set up is not the glamorous story we used to hear, but it is the difference between “I tried to build a design system” and “I built a design system”:
- A four-layer token architecture. Primitives (
tokens/primitive.ts) → Semantics (tokens/semantic.ts) → Panda preset → Component recipes. Every component reads from semantic tokens (bg.muted,text.primary), which read from primitives (colors.neutral[200]). You change a design decision in one place. - Tokens as objects, not strings. Quentin’s call. Instead of
colors.neutral[700] = '#1c1917', every token is a rich frozen object:
bg.default.hex // '#ffffff'
bg.default.rgba(0.5) // 'rgba(255, 255, 255, 0.5)'
spacing.md.px // '8px'
spacing.md.rem // '0.5rem'
shadows.sm.css // '0px 2px 8px 0px rgba(28, 28, 26, 0.08)'
- Sounds minor. It is actually the single feature that lets you write consumer code without memorizing unit conversions.
- A Figma-sync path (
tokens.json, W3C DTCG format), regenerated bypnpm --filter @billabex/ui-tokens build:figmaand verified in CI. The design system and Figma cannot drift silently. - Release plumbing. Changesets. Commitlint with a whitelisted scope list (
ui-tokens,ui-preset,ui-components,docs,repo,ci,deps,release). Husky pre-commit runningtsc --noEmit+viteston affected packages only. GitHub Actions CI. Chromatic visual snapshots. Abillabex-devops[bot]that opens the “version packages” PR after every changeset. - A 450-line
AGENTS.mdthat explains the architecture, file layout, units policy, ESM-only constraint, and every convention I would otherwise have had to invent or violate.
None of this is design work. All of it is the scaffolding that makes design work
tractable inside a Claude Code session. When I later told Claude “add a
size='xs' variant to Badge”, the agent knew exactly which recipe file to edit,
which semantic token to reference, what the test file should be named, and which
commit prefix to use. That did not happen by accident. It happened because
Quentin wrote the rules down.
Tip: if you are starting a design system solo with an AI pair-programmer, do
not let the AI bootstrap the repo. Have your most senior engineer spend one day
on the scaffolding. Tokens-as-objects, a documented token flow, commitlint, and
an AGENTS.md pay for themselves in the first week.
Why we didn’t start from shadcn
The obvious question: three packages, monorepo plumbing, a whole design system, for a three-person SaaS?
Because we already had something better than any off-the-shelf library: a prototype that worked. 6,175 lines of hand-written HTML and CSS, designed pixel by pixel against real customer screens, validated in user tests, with all the density, warmth, and French-finance-specific patterns baked in.
Starting from shadcn would have meant fighting every deviation. Starting from the prototype meant we only had to pay for the conversion. The prototype was the spec. The design system just had to codify it.
Step 1: Turn the prototype into a working spec
Before writing a single component, I asked Claude Code to audit the prototype against the state of Lingot. The output is four markdown files still living in the repo:
docs/prototype-analysis/
00-overview.md layout regions, IA, visual language
01-token-audit.md every color, spacing, radius, shadow, type scale
02-component-inventory.md 29 patterns mapped to Lingot
03-interaction-patterns.md
04-sprint-backlog.md ordered Update / Create / Remove backlog
These are not pretty docs. They are working tables with one row per decision. Example from the token audit:
Colors primitives: 23 values, 23 matched (1:1)
Spacing: 8 steps, all matched + 1 unused (5xl)
Radius: 6 values, all matched
Shadow: 3 match, 1 gap (popover dual-shadow)
Typography sizes: 8 in prototype, 3 missing in Lingot (10, 11, 18)
Typography family: Outfit in prototype, Nunito in Lingot - BLOCKER
That last line set the first real sprint item. The whole type scale ended up following.
The component inventory did the same for structure. 29 patterns in the prototype mapped to Lingot: 14 already existed, 10 needed updates, 11 were missing, 3 were candidates for removal (stepper, toggle, one form of breadcrumb - none of them appeared anywhere in 6,175 lines).
This audit is the single highest-leverage thing I did. Without it, I would have rebuilt components from memory. With it, every implementation decision was a reference back to a line number in the prototype.
Tip: when you ask an AI to audit a prototype, do not ask for “a summary”. Ask for a table with one row per prototype pattern, each row containing: prototype selector + line number, matching component in your system, gap description, and a verdict from a fixed four-value vocabulary (exists / update / create / remove). Vague audits get you vague commits.
Step 2: Sprint backlog as working artifact
Same folder, one file: 04-sprint-backlog.md. Ordered Update / Create / Remove
items, each with a rough effort size and a one-line rationale. Example row:
U2 list-item Add title-row + trailing status, meta-row, L
multi-line, left-accent border, avatar slot.
Consumers: task-item, comm-thread-item, msg-row.
Three surfaces share 80% of this spec.
This is the first thing a Claude Code session reads when I open a Lingot window.
It is also the first thing I update at the end of a session. Commits close out
rows; new rows open when a user test exposes a pattern I missed. When a row is
done, I strike it through with ~~Un~~ and a one-line “Shipped.” note, so the
file becomes a running record.
Tip: kill the Notion board. A markdown backlog colocated with the code, updated by the same agent that writes the code, beats any external tracker by a full order of magnitude when you work in AI-assisted sessions. The agent can read its own todo list.
Step 3: The per-component loop that actually worked
Every component update followed the same pattern. No exceptions.
- Open the prototype. Find the pattern. Copy its exact CSS (selector, all declarations, any
:hover/.activemodifiers). - Open Storybook. Add a story that renders the target pattern at the same size. This is the visual target.
- Ask Claude Code to implement the component, passing it the prototype CSS and the story. Constrain it: “use semantic tokens, not hex; use
remfor layout,pxfor hairlines; match the prototype byte for byte where possible.” - Diff Storybook against the prototype in two browser tabs, side by side. Note every mismatch. Feed the notes back to the agent. Repeat until the diff is boring.
- Commit with a pixel-precise message.
refactor(ui-components): pixel-match Onboarding spacing to prototype.fix(ui-tokens): register missing 3xs = 2px spacing primitive.feat(ui-tokens): add spacing.6xl = 80px.
These are real commits from last week, in order.
Six weeks of that produced 109 commits on the branch. The commit log is the
audit trail for every design decision. I can git blame any token and get back
to the prototype line number that justified it.
Tip: treat the commit log as the one piece of documentation that never rots.
Make the subject line describe the observable change in the UI.
refactor(ui-components): shrink Onboarding trust-strip text to caption.xs tells
future-me exactly what happened, without opening the diff. refactor: cleanup
tells me nothing and I wrote it myself, sorry.
How much time this actually took
Honest numbers from my Claude Code session logs, April 13 to April 21:
- Seven full-day sessions, each 3 to 5 hours of active agent time. Total: roughly 30 hours of Claude Code.
- Spread across seven working days over two weeks, alternating with my normal founder work.
- Zero outside contractors. No designers. One prototype, one AI, one repo.
For comparison: the prototype itself (the 6,175-line HTML file it was derived from) took me about three weeks earlier in the quarter. So the ratio is roughly 1:1.3. If you already have a strong prototype, codifying it into a real design system is not a six-month project. It is a two-week sprint with an AI pair.
What lives on the branch today
On
https://github.com/billabex/lingot/tree/gillou/design-system-update,
109 commits ahead of main, not merged:
@billabex/ui-tokenscarries an Outfit type scale, three new typography sizes (10, 11, 18), two new spacing primitives (3xs=2px, 6xl=80px), and tone variants on Avatar/Card/Chip.@billabex/ui-componentsholds 37 components across 6 families. New on the branch: Bubble, Avatar, Chip (absorbing the old FilterButton), NotificationBadge, StatusDot, ListPagination. Heavily updated: ListItem, Table, Button variants, Modal sizes, Section headers.- Storybook on Chromatic renders a story for every component variant on the branch. PR previews are automatic.
- Changesets are queued for the semver bumps when the branch is finally merged.
Before you open the branch, the caveat:
The code is not as clean as the architecture pretends it is. Despite the
token discipline, despite the AGENTS.md rules, despite the per-commit
pixel-matching, there are rough edges everywhere. A handful of components still
carry style={{ marginTop: 2 }} inline because the spacing token for 2px arrived
late. A few stories re-use magic numbers instead of tokens. There are three
TODO(refactor) comments I did not get to. The MessageComposer API moved twice
and the old prop names are still supported under aliases. The Bubble component
has four props that should probably be two.
I am sharing the branch anyway. A branch that sits unpublished waiting for private cleanup is worse than one that is open, honest about its state, and tightened in review.
What I got wrong
The Outfit swap was not a one-line change. In the audit doc I wrote “change
primitive.ts:135”. Reality: it touched every component, every story, every
Chromatic snapshot. A full day of visual regressions I had not planned for. If
you change a typography primitive, plan a full day to chase the ripples.
I underestimated the Bubble component by a factor of three. The prototype had three parallel bubble implementations (task Conversation tab, Comms thread view, and a third variant in discussions). Unifying them forced decisions about sides, tones, status chips, and attachment layouts that the prototype had quietly left undecided. “Add Bubble” was the longest ticket on the backlog and it was sized M by Claude Code.
I built Onboarding View too early. I added an entire Onboarding template to the component library because I wanted to see tokens used end-to-end. The template now bends the library around a single screen’s needs. I should have waited for two or three screens to see what is actually shared.
I am about to publish Lingot before the production Billabex app consumes it. The contract is still shifting under me on the branch. If I had to replay this, I would hold the release until the first real consumer app has a month of use, because every API decision made in a vacuum gets renegotiated in production.
Why this matters for a 3-person SaaS
The conventional wisdom says a design system is overhead you earn once you have 20 engineers and three product teams. I think that is backwards when you are AI-accelerated.
A design system is the artifact that lets one person build consistent UI across a product that would otherwise drift into chaos. The cost is no longer “six months of a design-infra team.” The cost is a senior engineer’s day of scaffolding, a prototype as ground truth, and two weeks of Claude Code sessions with a working backlog, followed by the unglamorous stretch of merging, wiring it in, and cleaning up.
Quentin’s half-day on the foundation made the 30 hours of my sessions tractable. The prototype made the 30 hours purposeful. AI made the 30 hours fit between customer calls.
Next up, and I want to be honest about this: the branch goes to Quentin for review. He will push back on API choices, spot token abuses I did not see, and flag the places where I worked around the architecture instead of through it. Some components will be rewritten. Some will be rejected. The ones that survive will land in the production app, where real usage will renegotiate every call I made in Storybook isolation.
I do not expect this branch to be consumed as-is. Low likelihood, honestly. Between Quentin’s review, the integration work, and the first month of production use, I expect a lot of trade-offs and a lot of commits before Lingot is fully live. That is the point. The work of the last two weeks was not to finish a design system. It was to produce something concrete enough that the next round of arguments has artifacts to argue over.
A future post will cover what survived the review, what did not, and what production use changed. The story is in the negotiation, not the branch.
Lingot is an in-progress, public repo: github.com/billabex/lingot. The work is happening on the gillou/design-system-update branch, which is not merged and not released. Storybook is on Chromatic (linked in the README). Honest feedback welcome, especially on the bits I admitted are rough.