ZenWrite Design — Builder
Design UI that looks like it was always part of ZenWrite. ZenWrite is a light-primary, distraction-free scholarly writing sanctuary: the manuscript is the hero, chrome fades while typing, and the app is split into two color spheres. This skill is the builder — it produces new UI that is correct on the first pass by reasoning down the design chain and reusing what already exists. (To review or realign existing UI, use zenwrite-ui-audit.)
First: load the chain (the contract)
Do this before writing any markup.
- In the ZenWrite repo (
docs/design/exists) — that folder is the live source of truth. Readdocs/design/README.md, then the layer file(s) for what you're building (01-tokens…05-patterns-and-layouts, and06-prompt-engineeringfor templates). Also read the@themeblock insrc/index.cssfor the current token set before choosing colors. - As a fast portable summary / outside the repo — read
references/tokens-and-chain.md.
Then keep two references open while you work:
references/build-recipes.md— copy-paste, token-correct skeletons for every layer (tile, slide-in panel, wizard, modal, page header, catalog list, status usage, editor extension). Start from these, don't invent markup.references/static-html-docs.md— token map for self-containeddocs/html/*pages outside the Vite bundle.references/engineering.md— React 19 + Tailwind v4 + a11y standards the output must also satisfy.
Non-negotiables (memorize)
- Light-primary. No global
prefers-color-schemedark mode.dark:only under an explicit.darkancestor. - Two spheres, never crossed. Content = brand-violet (Create / Edit / Organize).
Community = sky / emerald / rose (Engage & Kairos → sky, Manage → emerald, Analyze → rose).
Resolve community accents through
getViewAccent(view)insrc/lib/viewAccents.ts— never hand-rolltext-sky-700strings. - Tokens, never hex.
bg-brand-violet, notbg-[#14006a], notbg-indigo-900. Never build dynamic color strings (bg-${c}-500) — use static class maps. - The 70 / 20 / 10 rule. ~70% shared chrome (cards, primary CTAs, focus rings) stays
violet/neutral; ~20% sphere color; ~10% view accent on headers, active tabs, nav active states,
stat highlights. Primary CTAs stay
bg-brand-violeton every view, including community screens. - Deliberate type.
font-serif(Newsreader) for literary titles/body/empty-states;font-manropefor uppercase eyebrows, labels, chips, metrics;font-monoonly for keys/scores. - Chrome fades. In the editor, secondary chrome idle-fades at
duration-700. Never add persistent editor chrome or anything that hides the manuscript while writing.
Build workflow (reason down the chain)
- Place it in the chain. Decide the layer, because that decides the recipe and the file:
- pill / badge / state surface → Primitive (
src/components/, reuse if it exists) - panel / wizard / toolbar / palette → Component
- a full view / major surface → Built component (
*Screen.tsx,Editor,MediaSurface) - a cross-cutting shell / overlay / nav / idle behavior → Pattern (lives in
App.tsx)
- pill / badge / state surface → Primitive (
- Determine the sphere. Content (violet) or community (sky/emerald/rose)? If community, plan to
pull accents from
getViewAccent(view)and title viaViewPageHeader. Keep primary CTAs violet. - Reuse before you create. Grep
src/components/first. Compose these before writing new markup:StatusChip,VoiceFidelityChip,StateLayouts(Loading/Empty/Error),ViewPageHeader,BottomNav, the slide-in-panel shell, the full-screen wizard shell, the home-tile pattern, the ⌘K palette. Extract a new primitive only when a 2nd/3rd real consumer already exists — avoid premature abstraction. - Compose from the recipes. Take the matching skeleton from
references/build-recipes.mdverbatim as your skeleton, then fill it in. This guarantees the right radii (rounded-2xl), borders (border-brand-violet/10), page shell (max-w-7xl mx-auto px-6 py-8), z-index stack,animate-inmotion, and header typography (font-serif text-lg font-light italic text-brand-violet). - Make it accessible by construction. Every interactive element gets
focus-visible:ring-2 focus-visible:ring-brand-violet focus-visible:outline-noneand an accessible name (aria-labelon icon-only buttons). Overlays: ESC + backdrop close, focus trap, restore focus on close. Semantic elements (<button>for actions,<a>for nav). - Wire it up. New screens/panels get state in
App.tsx(a view-switch case or an overlay boolean), respect the idle-fade contract, and route by manuscript type where relevant (article/book/lesson/newsletter→Editor;podcast/video→MediaSurface). - Self-check + validate. Walk the build checklist below, then run
pnpm build:check(andRUN_BUILD_VALIDATION=true pnpm build:checkfor the Vite bundle). Fix TS errors listed inreports/tsc.txt— typecheck is the repo's only lint gate.
Build checklist (run on your own output before shipping)
- Correct layer + file location (
src/components/, flat); named export matches file. - Typed props interface, no
any; local UI state only, domain data via props/hooks. - Only
@themetokens — no#/[#inclassName, no raw hex instyle, no dynamic color strings. - Right sphere: content leads violet; community leads via
getViewAccent; CTAs staybg-brand-violet. - Typography by layer:
font-serifliterary,font-manropeuppercase labels,font-monokeys only. - Cards
rounded-2xl border-brand-violet/10; pagemax-w-7xl mx-auto px-6 py-8; sectionsspace-y-8. - Reused
StatusChip/StateLayouts/ViewPageHeaderinstead of re-implementing. -
focus-visiblering + accessible name on every control; ESC/backdrop/focus-trap on overlays. - Hover
transition-all duration-200; editor chrome respectsduration-700idle-fade. - Overlay z-index follows the stack (backdrop 40 · slide-in 50 · publish 60 · palette 70).
-
dark:only under a.darkancestor; no global dark hijack. - Responsive: mobile single-column,
md+layout,max-w-7xlwide cap; tap targets ≥ ~44px. - Wired in
App.tsxif it's a screen/panel;pnpm build:checkpasses.
Output
The component/screen file(s) in src/components/, any App.tsx wiring, a one-line note on which
primitives/patterns you reused (and any new primitive you extracted + why), and confirmation the
build check passed — or the exact errors if it didn't.
Guardrails
- Enforce the existing system; don't invent a new one. If the user wants a genuinely new visual direction, say so and get explicit agreement before diverging.
- Don't cross spheres (violet is not a community accent; sky/emerald/rose are not content accents), don't add hex or dynamic color strings, global dark mode, persistent editor chrome, or debug panels — these are the documented anti-patterns.
- Prefer composition over abstraction. Reuse primitives; extract only on real, existing repetition.
- When
docs/design/disagrees with the bundled reference, the repo wins — it's the live source of truth and the reference may lag.