A portable, tool-agnostic workflow for designing (or auditing) an app's screens in
Pencil .pen files, where the design is a 1:1 mirror of the code —
not a mockup that drifts from it. Drop this folder into a project, and read it yourself or
hand it to any assistant (Claude, Codex, Gemini, or none) as the method to follow.
New here? Start with
PROMPT.md— it maps how everything is organized and gives copy-paste prompts to run each phase.
Snapshot — 2026-07-17. A point-in-time extraction of a workflow I keep refining. This gist won't track every change; treat it as a starting skeleton to fork and bend to your project, not a spec to obey. The
.penformat and Pencil tooling also move — verify against your version.
Because forcing every screen to match the code exactly turns designing into an audit. When the design can't render a value the code doesn't have, and can't invent a string the code never ships, the disagreements surface on their own: broken flows, design-token creep, three fonts where one would do, states that exist in the domain but nothing in the UI reaches. You feel that debt as "something's off" long before you can name it. This makes you name it.
- The code is the source of truth for design and layout — not old mockups, not stale screenshots. Derive the screen list from the router/navigator, the strings from the code, the tokens from the real config and stylesheets.
- Don't dedupe or promote things into the kit unless asked. Spotting a resemblance is a note, not a task — there's no live propagation, so every kit change is manual work across every consumer. Findings accumulate; you promote them in one batch, deliberately.
design/
├── ui-kit.lib.pen ← the library: tokens + components, mirrored 1:1 from the code
├── ui-kit.CHANGELOG.md ← kit versions and what each bump changed
├── flows/<domain>.pen ← one file per domain (a user journey with enough weight for one)
├── _playground.pen ← experiments, inside the system (vendors the kit like a flow)
├── DECISIONS.md ← the numbered findings registry the .pen briefs cite
└── references/ ← local-only device captures — never commit (real user data)
Why split into small per-domain files: .pen is JSON, and Git merges JSON badly. Small
files avoid collisions and cost fewer tokens when an assistant edits them. A domain earns its
own file when the journey has weight (≈3+ screens or its own state machine); otherwise it
merges into a neighbor. Past ~10 screens in one file, split.
- Derive the domain list from the navigator/tab-bar code, never from an old design — legacy files silently drop shipped domains and keep features that were never built.
- Never invent copy. Every string must exist in the code. If you can't find it, it isn't real. Invented labels are the #1 error in this kind of work.
- Captures go stale and have a blind spot: they only show states the capturing account could reach. Deliberately hunt the states a normal session never hits — no data, no permission, failed request, empty period. That's where the design is missing and where the code turns out to be buggy.
"Every screen gets Default / Loading / Empty / Error" is wrong — it manufactures busywork. Instead: find the state's owner in the code first (it may be a root gate that replaces the whole navigator, or not exist at all); check what it renders, not what the code says; and build a state only when it carries information the design lacks. A pure string swap goes in the brief, not a new frame.
- Boundary: the app's core/shared components go in the kit; feature-local components stay in their flow. Derived from the code's own layout, not from resemblance.
- Vendoring: Pencil can't reference components across
.penfiles, so each flow installs the kit (all tokens + only the components it uses) and pins akit-version-source. Kit changes bump the version + a CHANGELOG entry (patch = value tweak · minor = new token/component · major = breaking). - Being behind is fine; diverging is not. Install every token in every flow even where nothing renders differently — approximations only surface when you re-vendor and diff.
- Tokens only — zero hardcoded values in a flow. A value the kit lacks is a kit gap: log it, never hardcode it silently.
When design and code disagree and the call isn't yours, log a numbered D<n> in
DECISIONS.md: what the code does, what it probably should do, who decides. Build what the
code does, and cite the number in the flow's brief. Resolved entries stay — the reasoning is
the value. Real app bugs you find while designing get flagged (🐞) until they're filed
somewhere real; a design doc isn't their home, but losing them is worse.
This registry is the payoff. By the time the screens are done you have a ranked list of what to reorganize, delete, or change in the code to make it faithful and coherent — the design becomes the spec for the cleanup, instead of the other way around.
- One base frame size for the whole system (e.g. 360×800 Android, or 390×844 iOS); duplicate
per platform only where the pattern genuinely differs.
clip: trueon every screen frame. - Naming:
[Flow] / [Screen] / [State]. Rows run left→right, one row per journey, and each row starts with a brief frame: description, entry points, business rules, states, kit version, findings. Much of that only lives in the code — the brief is where it becomes readable. Read it before editing; update it when you learn something. - Accessibility floor: 44pt / 48dp touch targets, AA contrast through tokens.
- Bottom sheets anchor by math (
y = screenH − sheetH), never hand-picked.
- A screenshot proves the layout isn't broken; it does not prove the geometry is right. When a design encodes a spatial relationship, derive the positions from the numbers — eyeballed geometry survives screenshot review while being completely wrong.
- Re-read after writing. Batch writes can roll back incompletely, and wrong-file writes are silent.
- Fidelity loop: screenshot the rendered app, overlay it at 50% opacity on the design, fix drift in the design first, then the code.
None of this needs an AI. If you use one, PROMPT.md has copy-paste prompts for
each phase — the rules here are written to be executable by a model or by hand. Two things
worth knowing if your tool drives Pencil through an editor extension: it can usually only act
on the active tab reliably (make the target file active, and have it verify before
writing — silent wrong-file writes are the classic failure), and .pen has no auto-save,
so have it read a node back after writing and save often.
- Copy this folder in as
design/. Commit. - Read your app's real tokens (config + stylesheets) into
ui-kit.lib.penas themed variables with semantic names. Log every disagreement as aD<n>instead of picking silently. - Recreate the core components 1:1 (names, variants, states). Feature-local ones wait for their flow.
- Split flows domain by domain (domain list from the navigator). Move clean screens in, name them to the convention, swap raw elements for vendored components.
- Fill missing states per the rules above — the ones no capture ever shows first.
The audit numbers will be humbling; that's the method working, not failing. Fork it, strip what you don't need — it's a starting point, not a rulebook.