Skip to content

Instantly share code, notes, and snippets.

@rodacato
Last active July 24, 2026 19:06
Show Gist options
  • Select an option

  • Save rodacato/e430b1c5eb03fdd09e2c7e9f50c939cf to your computer and use it in GitHub Desktop.

Select an option

Save rodacato/e430b1c5eb03fdd09e2c7e9f50c939cf to your computer and use it in GitHub Desktop.
Designing an app in Pencil, mirrored 1:1 from the code — a portable, tool-agnostic design/audit workflow

Designing an app in Pencil, mirrored 1:1 from the code

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 .pen format and Pencil tooling also move — verify against your version.

Why mirror the code instead of designing freely

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 two rules that matter most

  1. 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.
  2. 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.

The folder

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.

Source of truth: code > production capture > legacy design

  • 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.

Which states to actually build

"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.

The kit contract — 1:1 mirror, versioned vendoring

  • 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 .pen files, so each flow installs the kit (all tokens + only the components it uses) and pins a kit-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.

Findings become numbered decisions, not silent fixes

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.

Canvas conventions

  • 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: true on 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.

Verify like an engineer

  • 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.

Using this with an assistant (optional)

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.

Bootstrapping into your project

  1. Copy this folder in as design/. Commit.
  2. Read your app's real tokens (config + stylesheets) into ui-kit.lib.pen as themed variables with semantic names. Log every disagreement as a D<n> instead of picking silently.
  3. Recreate the core components 1:1 (names, variants, states). Feature-local ones wait for their flow.
  4. 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.
  5. 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.

Design decisions — findings registry (D1–D<n>)

This is the registry the .pen files point at. Briefs in flows/*.pen and warning notes in ui-kit.lib.pen cite these by number. Keep entries after they're resolved — record the outcome instead of deleting; the reasoning is the useful part.

Status: <n> entries · <n> resolved · <n> open · <n> 🔴 high-impact · <n> 🐞 real app bugs.

How entries work. A D<n> is a finding: you log it when the design and the code disagree and the call isn't yours to make as the designer. The design is always built to match the code (source-of-truth rule) — the entry records what the code does, what it probably should do, and who decides.


Decisions to make (design calls — not mechanical)

# Decision Values in play (usage count) Recommendation
D1 <the decision> <value A> ×<n> · <value B> ×<n> ⏳ <provisional pick + why; what would confirm it>

Changes to land in code (once decisions resolve)

Once a D<n> is decided, the fix usually lives in the code, not the design — the design already shows the target. Track them here; on a big project this section grows into its own CODE_CHANGES.md work-order.

# Change (reorganize / delete / change) Sites Status
<from D1> <what to change in the code> <files> ⏳ pending / ✅ landed

Real app bugs found while designing (🐞)

Designing 1:1 against the code surfaces bugs. A design doc isn't their home, but losing them is worse — log them here until they're filed in your tracker, then link the issue.

  1. 🐞 <what's broken, where, how you found it> → <issue link once filed>

Out of scope (confirmed)

<Scales or systems the code genuinely doesn't have — don't invent them just to mirror. Record them here so nobody "fixes" the absence later.>

Start here

Two ways to use this file. If you want to understand how the workflow is organized, read the next section. If you just want to drive it — with an assistant or by hand — jump to the prompts. Either way, README.md holds the full method and the why; this file is the map and the on-ramp.

How it's organized

Each file has one job:

File What it's for
README.md the full method — the rules and the reasoning
PROMPT.md (this) the map + copy-paste prompts to run each phase
DECISIONS.md the numbered findings registry (code↔design disagreements + real bugs)
ui-kit.CHANGELOG.md versions of the shared kit
design/ (you create it) ui-kit.lib.pen + flows/<domain>.pen + _playground.pen

The work moves in four phases, and the prompts below map one-to-one to them:

  1. Bootstrap — derive the domain list from the code, mirror the real tokens into the kit.
  2. Design a flow — build the screens 1:1 against the code; every disagreement becomes a numbered D<n> in DECISIONS.md instead of a silent fix.
  3. Audit → code — turn the findings into a work-order of code changes. By now the design is the spec: you fix the code to match it, not the other way around.
  4. Fidelity check — overlay the running app on the design and close the drift.

Run them in order the first time. After that, reach for the one you need.

The prompts

Copy-paste starting points — a floor to adapt, not a script. The assistant should read README.md first; these assume it has.

1. Bootstrap the design/ system

Read README.md in this folder, then help me bootstrap a design/ system for this project using that method. Before writing anything, ask me for the path to my app's navigator/router and my token source (theme config + stylesheets).

Then: (1) derive the domain list from the navigator/router, never from an old design — list the domains and their screens, wait for my OK before creating files. (2) read my real tokens and propose the ui-kit tokens as a 1:1 mirror; where the code is ambiguous or disagrees with itself, log a numbered D<n> in DECISIONS.md instead of picking silently. (3) rules you must hold: the code is the source of truth, never invent copy, and don't dedupe or promote anything into the kit unless I ask.

2. Design (and audit) one flow

We're doing the <domain> flow. Read README.md and this flow's brief. Build its screens as a 1:1 mirror of the code for <domain> — tokens only, no hardcoded values, only strings that exist in the code.

Whenever the design can't match the code — a token the kit lacks, contradictory behavior, a state nothing in the UI reaches — stop and log a numbered D<n> (what the code does · what it probably should do · who decides), build what the code does, and cite the number in the brief. If you hit a real app bug, mark it 🐞 in DECISIONS.md.

Deliberately design the states a normal session never reaches — no data, no permission, failed request, empty period. That's where the design is missing and the code is buggy.

3. Turn the audit into a code work-order

The designs are done. Read DECISIONS.md. Give me a prioritized work-order of changes to make in the CODE so it matches the design — the design is now the spec. For each item: cite its D<n>, say whether it's a reorganize / delete / change, and list the files to touch. Put the real bugs (🐞) at the top, separated from the design-fidelity changes.

4. Fidelity check against the running app

Here's a screenshot of the rendered <screen>. Compare it against the design frame for the same screen and list every drift (spacing, color token, font, copy, missing state). For each, tell me whether the design is wrong (fix the .pen) or the code is wrong (fix the app) — default to fixing the design first, since it mirrors the code.

If your assistant drives Pencil through an editor extension

  • Make the target .pen the active tab, and tell the assistant to verify the active editor before every write. With several near-identical files open, silent wrong-file writes are the classic failure.
  • Tell it to read a node back after writing and screenshot after each change. .pen has no auto-save — save and commit often.

Nothing here is Claude- or Pencil-specific except those two notes. The method in README.md is the real payload; these prompts just start the conversation.

ui-kit.lib.pen — Changelog

Pencil can't reference components across .pen files, so each flows/*.pen vendors the kit at a pinned version — it does NOT link live. Bumping here means flows on an older version may need a re-sync.

Version markers: a kit-version variable lives in ui-kit.lib.pen; each flow records the version it vendored in kit-version-source.

Bump rules: patch = token value tweak · minor = new/changed token or component · major = breaking rename/removal. Additive bumps don't force re-syncs — being behind is fine, diverging is not. A changed token value forces every consumer to re-sync.


0.1.0 — initial

Tokens (<n>): <semantic roles + palette scales + fonts, light/dark>. Every value lifted from the code (<config / source files>) — provisional picks logged as D-entries in DECISIONS.md.

Components (<n>): <the core components mirrored 1:1, with their presets/states>.

Vendored by: flows/&lt;pilot&gt;.pen (kit-version-source 0.1.0).

Gaps surfaced by the pilot → candidates for 0.2.0

Need Real value in code Used instead Note
<missing token role> <code value> <nearest existing token> <is it a real gap or a one-off?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment