| name | arta |
| description | Use during the DESIGN phase of building a new app/feature, instead of dumping a wall of text. ALWAYS brainstorm the idea into an agreed direction first (questions one at a time, like superpowers:brainstorming) — never jump straight to a spec or prototype. Then drive Claude Code to build a shared, live design canvas (.arta/state.json) that the dev watches in the Arta viewer — spec, prototype, data model, flow, plan — iterating from the dev's clicks and feedback. Trigger when the user wants to "design", "wireframe", "prototype", "sketch the data model/flow", "plan a feature visually", or says "open Arta / let's design this in the studio". |
Arta — prototype-based design loop
You are designing with a picture, not a wall of text. The dev has the Arta
viewer open (bun run dev). Everything you write to .arta/state.json
appears on their screen instantly with a cyan flash. You see what they're looking
at and read their feedback through the MCP server. Design is one tight loop.
The loop
you write state.json ──▶ viewer repaints (the dev sees it)
▲ │
│ ▼
arta_get_feedback ◀── dev clicks the prototype, leaves a note
Never describe a screen in prose when you could render it. Show, ask, adjust.
Tools (MCP server: arta)
arta_doctor — call this FIRST, before any other arta_* write, when you start
designing. It guarantees this project is in the shared registry so it actually shows
up in the viewer's switcher (without it, a design can be invisible — "I built
something but there's no project to select"), seeds a minimal state.json if there's
none yet, and scaffolds a starter .arta/proto/ (config.json, theme.css,
screens/home.tsx — real files, not an MCP write) if none exists yet, so the project
is immediately switchable AND has something on screen. Returns the project id.
Tell the dev that id / the ?project=<id> URL it returns. It never overwrites an
existing state.json or existing proto/ files.
arta_start_viewer — call this once at the very start of a session to open
the viewer for the dev. It launches the viewer that ships inside the installed
plugin (so it always matches the plugin version — no stale npx/bunx cache),
pointed at this project's .arta/. It's idempotent (re-running just returns the
URL) and installs the viewer's deps on first run. Tell the dev the URL it returns
(default http://localhost:7317/?project=<id>) — the ?project deep-link opens the
viewer straight onto this project even when one viewer hosts several; the viewer
consumes it and tidies the address bar. The dev can drop a project from the switcher
there too (the launched project stays pinned).
arta_restart_viewer — stop the running viewer and relaunch it from the
installed plugin, so it serves the latest build. Use this after the plugin
updates (the /arta update / /arta restart flow calls it): an already-open viewer
keeps serving the old assets until it's restarted. The dev no longer clears caches
or kills processes by hand.
arta_get_state — read .arta/state.json: meta/spec/plan/dataModel/api/
architecture. prototype no longer lives here — screens are real files under
.arta/proto/screens/<id>.tsx (shared components under proto/components/), read
and edited directly with your normal Read/Write/Edit tools, never through this
one. Whole state by default; in a large project pass { outline: true } for a
cheap index (which sections exist, their counts + byte sizes), then pull only what
you need with { sections: ['spec','dataModel'] } or a named getter.
arta_get_view — see what the dev is looking at right now (active tab + prototype
screen), plus runtime.gates (watch-time anti-slop findings — the dev server
scans every screen/component/theme.css on every change, same detector as
arta_design_review) and runtime.errors (compile/runtime errors from the
prototype). Check this after every screen edit — it's now where BOTH "did it
compile" and "is it clean" surface, in one call.
arta_set_state — write the whole non-prototype canvas (spec/plan/dataModel/
api/architecture). Rejects a prototype key with a pointed error — screens
are file-based now, write .arta/proto/screens/<id>.tsx directly. Use for the first
build of these sections or a full rewrite; prefer arta_patch_state for incremental
edits.
arta_patch_state — merge one top-level section (spec, dataModel, api,
architecture, plan) without resending the whole object. Top-level keys replace;
meta deep-merges, so a partial patch can't wipe what it omits. Also rejects a
prototype key — same reason as arta_set_state. Your workhorse for the
structured tabs.
arta_set_phase — record the current phase (prototype → data → flow → architecture → plan), shown in the status bar. Tabs are free routes the dev can revisit in any order, so this just marks where you're working.
arta_get_spec — read just the spec (goal, users, userStories, scope,
constraints) without pulling the whole state. Write it via arta_patch_state.
arta_get_data_model — read just the dataModel (entities + relationships,
the Data tab) on its own. Write it via arta_patch_state.
arta_get_api / arta_set_api — read/write the api section (the Flow
tab) as an OpenAPI 3 document: routes, middleware (x-middleware), and
per-operation params (path/query/header), request body, and responses. Tie a
route to the prototype screens that call it with x-screens: ["screenId"] — the
Flow graph then draws screen → API edges (which screen hits which endpoint,
through which middleware).
arta_get_architecture / arta_set_architecture — read/write the
architecture section (the Architecture tab): the C4-style nodes/edges (system
diagram), decisions (ADRs), nfrs, security notes, and stack.
arta_get_plan / arta_set_plan — read/write the plan Kanban board:
statuses (columns), milestones (swimlanes), and tasks (status id +
priority). arta_set_task adds/updates one card by milestone + title —
use it to move a card (set its status) without resending the whole plan.
arta_get_feedback — drain notes the dev left in the viewer. Check it after
every meaningful change and act on what you find. Notes may include an element
(tag/text/selector) when the dev clicked a specific element to comment on it —
use it to target the exact thing they mean.
arta_get_screenshot — get a PNG of how a screen actually renders. Use it to
check your own work visually, not just from the source — after building or
changing a screen, look at it. By default it's a real headless-Chrome render
(pixel-identical to the browser, no font/effect drift) rendered on demand, so
you do NOT need the dev to have opened the screen first; it's the screen content at
its device width (no device bezel). Pass full: true to capture the WHOLE
screen at its content length (the entire scroll in one tall image, including a
screen that scrolls inside an inner region — header + scroll-body + tabbar).
Pass engine: "client" to instead get the viewer's own capture with the
device bezel + status bar (what the dev literally sees) — handy to check the
safe-area/notch fit, though it only exists for screens the dev has opened and can
drift on some CSS. (No Chrome on the machine → it auto-falls back to the client shot.)
arta_design_review — run Arta's own deterministic anti-slop detector directly
over .arta/proto/screens/*.tsx + components/*.tsx (and theme.css) — a
JSX-aware reader, no HTML assembly step — and get craft findings ranked
error → warn → info (gradient text, side-stripe borders, stripe backgrounds,
cramped tracking, nested cards, transition:all, emoji-as-icon, italic headings,
over-rounded cards…). Offline and instant — no npx, no network. A design-quality
eye to pair with the screenshot; fix the errors first. This is the SAME detector
arta_get_view's runtime.gates runs automatically on every file change — call
this one for an on-demand pass, or when you want findings for a single screen.
arta_export — pack the WHOLE clickable prototype into a static, deployable folder
for a client demo. Writes <project>/dist/index.html (override with dir): one
self-contained build, the same faithful render as the live viewer, but without the
Arta navigator — no floating button, no sidebar, just the routed screens as the
dev built them. Returns ready-to-run deploy commands (Cloudflare Pages
npx wrangler pages deploy, Netlify, local serve). Needs the viewer running (it does
the assembling), so call arta_start_viewer first if it isn't up. The .arta/
source is never touched. Re-run after design changes to refresh it.
Screens, components, and the design system are FILES now — no MCP tool writes
them. The ten old granular per-screen / per-component / per-design-system setters
and getters are retired (ADR-0004) — this is the single biggest workflow change from
the old prototype model. Use your normal Write/Edit tools directly on:
.arta/proto/screens/<id>.tsx — one screen = one file = one route
.arta/proto/components/<name>.tsx — shared components (nav, header, cards, …)
.arta/proto/theme.css — the shared design system (Tailwind v4 tokens)
.arta/proto/lib/ — shared state/hooks
.arta/proto/config.json — prototype-level defaults (start screen, frame, chrome)
The dev server (already watching .arta/) picks up any change automatically,
recompiles, runs the slop gates, and HMR-updates the live viewer — the same
"write and it appears instantly" loop as before, just onto real source files instead
of a JSON blob. See "Storage layout" and "Screen authoring contract" below for the
exact file shapes.
Storage layout (.arta/)
The canvas is split so each piece stays small, and the prototype is real source code:
.arta/state.json meta/spec/plan/dataModel/api/architecture (no prototype)
.arta/proto/config.json prototype-level defaults: { start?, frame?, safeArea?, chrome? }
.arta/proto/theme.css the shared design system — Tailwind v4, CSS-first
.arta/proto/screens/<id>.tsx one screen = one route = one file
.arta/proto/components/<name>.tsx shared components (nav, header, cards, …)
.arta/proto/lib/ shared state/hooks — plain .ts/.tsx, no special store API
arta_get_state never returns the prototype — there is no manifest to keep in sync;
the filesystem IS the source of truth (ADR-0001: no screen registry to drift from the
files, unlike the old design where a state.json manifest could go stale against the
HTML files it pointed at). Read and edit proto/ files directly with your normal
Read/Write/Edit tools — there's no MCP tool that does it for you. The dev server
(already watching .arta/) recompiles, runs the slop gates, and HMR-updates the live
viewer on every change — the same "write and it appears instantly" loop as before.
- Screen id = file name, sanitized to
[a-z0-9_-] (checkout.tsx → screen id
checkout, route /checkout). Create a file to create a screen; delete it to
delete the screen; rename it to rename the screen (repoint config.json's start
if you renamed the start screen).
- Every screen file exports two things:
export const meta = { title?, frame?, safeArea?, chrome?, url? } — must be a
pure object literal (no imports, no spreads, no function calls). It's evaluated
in isolation, separately from the rest of the module, so the viewer rail can still
show a title/frame for a screen that currently fails to compile.
export default function ScreenName() { … } — a plain React component, the
screen body.
config.json holds prototype-level defaults that apply unless a screen's own
meta overrides them, e.g. { "start": "home", "frame": "ios", "safeArea": "#0b0b0c", "chrome": true }.
See "Screen authoring contract" below for the full authoring rules (imports,
navigation, shared state, theme.css).
Start with a brainstorm — don't build on the first message
When the dev asks to design something, do NOT immediately write the spec or build
the prototype. First brainstorm the idea into a shape you both agree on. Jumping
straight to a hi-fi prototype bakes in unexamined assumptions and wastes work — it's
the #1 complaint about this tool. Brainstorm first, the way superpowers:brainstorming
does, then build. For the shaping tools — a fast persona, a journey map that becomes your
screen list, an impact×effort scope cut — load
reference/ux-research.md; building this user's journey to
this goal (not a generic app) is also the antidote to look-alike output.
HARD-GATE: no committed spec and no hi-fi prototype build until the dev has
approved a direction. (Throwaway lo-fi sketches during the brainstorm are
fine and encouraged — see below.) Every request goes through this, even a "simple"
one — that's exactly where wrong assumptions hide. The direction can be short.
The brainstorm, in order:
- Register the project & open the viewer, then ground yourself —
arta_doctor
FIRST (registers this project so it's selectable in the switcher and hands you its
id), then arta_start_viewer (so the dev can see, and so your lo-fi sketches land
somewhere visible), then arta_get_state / arta_get_view and skim the project
(files, recent work) so you don't ask what you could read.
- Check scope. If the request is really several independent products
(chat + billing + analytics…), say so first and help split it — don't refine the
details of something that needs decomposing. Brainstorm the first piece, then build.
- Ask one question at a time. Purpose, users, success criteria, constraints.
Prefer multiple-choice ("A / B / C — which fits?") over open-ended. One question
per message — never a questionnaire.
- Propose 2–3 approaches with trade-offs; lead with your recommendation and why.
- Present the direction — a few sentences: what it is, who it's for, the key
screens, what's in / out of scope, and a one-line visual direction — a concrete
scene sentence (who/where/mood, not "modern and clean") so the look is committed up
front, not defaulted to the category template later. Ask if it looks right; revise
until it does.
- Get an explicit yes. Only then
arta_set_phase to prototype, write the
spec into the rail, and build the first real screens.
The viewer is your visual companion. This is a picture tool — so when a
question is easier shown than told (a layout choice, two nav patterns, where a
control goes), drop a quick lo-fi sketch as a real screen file — write a rough
.arta/proto/screens/<id>.tsx with placeholder boxes and real headings (a minute of
plain JSX, not a finished build) — and ask the dev to look. Keep these cheap and
disposable — they answer one question, they aren't the product, and they don't count
as "building". Decide per question: a requirements / scope / trade-off question is
text (ask in chat); a "which layout?" question is visual (sketch it). Don't route
everything through the canvas.
Phase order (prototype-based)
Brainstorm an agreed direction first (above) — the four canvas phases below
start only after the dev signs off. The phases are then deliberately
Prototype + Spec → Data → Flow → Plan. Start from something clickable, not from
a document.
- Prototype + Spec — sketch the key screens as a wireframe the dev can click
through, with the spec (goal, users, stories, scope, constraints) in the rail
beside it. Get the shape of the product right here first. Set a brand-grade
design language before the first hi-fi screen — read
design-systems.md, pick a
kit, adapt it, and set it as the foundation (see the styling guidance below).
- Data model — entities, fields (mark
pk/fk/required), relationships.
Rendered as a React Flow ER diagram.
- Flow (API) — design the HTTP API as an OpenAPI 3 document in
api:
routes (method + path), middleware, and per-operation params
(path/query/header), request body, and responses. The viewer shows it as a
React Flow graph (routes + middleware chain) with a Postman-style inspector
(Params · Headers · Body · Responses) and an Export OpenAPI 3 button.
- Architecture — the system-level design (
architecture): a C4-style diagram
(nodes/edges — services, datastores, externals, gateways, queues, caches, infra,
connected by protocol + sync/async), plus ADRs (decisions), NFRs (nfrs),
security notes, and the system stack. Shown as a React Flow diagram with
Diagram / Decisions / Security & NFRs views.
- Plan — a Kanban board (ClickUp-style): you define the columns as custom
statuses (
plan.statuses), milestones become swimlanes (rows), and tasks
are cards (each with a status id and optional priority). Plus the tech stack.
Move the phase with arta_set_phase as each is settled. Don't race ahead — let
the dev react at each step.
HARD-GATE — the prototype is the approval checkpoint. Build Prototype + Spec
first and stop there until the dev has clicked through it and explicitly approved
it. Do not start Data / Flow / Architecture / Plan on your own initiative before
that sign-off. Within the prototype, work one screen at a time — set it,
self-review it (below), ask the dev to open it (that's also what captures the
snapshot you review), fold in their reaction, then the next screen. Building every
phase in one autonomous sprint — five screens, then data, flow, architecture and plan,
all before the dev has reacted to a single screen — is the #1 way this tool
disappoints. The dev wants to shape the product on the canvas with you, not be
handed a finished pile to accept or reject. "Faster" here is slower: you'll redo the
later phases once the prototype shifts.
How to work
- On a fresh design request, brainstorm first (above) and get a direction
approved before building. Skip this only when the dev is clearly mid-loop already.
arta_get_state and arta_get_view to ground yourself.
- Make the smallest change that answers the current question. Patch one section.
- Self-review and fix it (below) BEFORE you show the dev. This is a hard gate, not
an optional polish — every time.
- Tell the dev in one line what changed and what you want them to react to
("Click Add walk-in — does that field set feel right?").
arta_get_feedback; fold their notes in; repeat.
- When a phase is solid,
arta_set_phase to the next.
Self-review — before you hand a screen back (every time)
HARD-GATE: the moment you finish building or changing screens, review your own
work and fix what you find — BEFORE you tell the dev to look. Reviewing the prototype
is your job, not the dev's: never hand back a screen you haven't looked at and expect
them to run /arta review to find your mistakes. (/arta review is a manual re-run of
exactly this — you do it proactively, every time.) The #1 way this tool disappoints is
handing over a blank placeholder screen, a header copy-pasted onto five screens, or a
screen with a dead white band — all of which one look would have caught.
Pre-emit self-critique — score before you look. Before the per-screen checks, rate the
build you just made 1–5 on six axes, and let anything < 3 trigger a revision pass
before you hand back (two passes is normal; a third means the direction is wrong, not the
pixels — rethink it). This catches "competent but generic" that the detector can't:
- Philosophy — is there a clear why, a position the design takes, or is it just a layout?
- Hierarchy — can you tell primary / secondary / tertiary in 2 seconds, or is it one flat band?
- Execution — are the details (contrast, accent footprint, spacing rhythm, focus rings, alignment) in spec, or sloppy even if the bones are right?
- Specificity — does it look like this product, or like a page that could be anyone's?
- Restraint — has everything that isn't earning its place been cut (decoration, redundant chrome, padding-for-padding)?
- Variety — does this screen share a structural fingerprint with another screen in the app? Score by structural distance, not colour. (See "Vary the screen shape" below.)
Those six axes are about whether it looks designed. Then run a usability pass — a pretty
screen can still be hard to use: skim reference/ux-heuristics.md
(Nielsen's 10 — dead clicks, ungated destructive actions, recognition over recall)
and reference/accessibility.md (WCAG A/AA — contrast, labels,
focus, semantics, Thai font-chain), and reach for
reference/cognitive-load.md when a form or flow feels heavy.
The deep, expanded scorecard for all of this is reference/critique-rubric.md.
Do this for every screen you touched:
-
Look at the pixels — arta_get_screenshot for each screen, and actually read
the image. By default it's a real headless-Chrome render of the screen content,
faithful to the browser and captured on demand — so a fresh build that nobody has
opened still has a shot, and "no snapshot yet" is no excuse to skip review. Use it
to judge layout, type, contrast, images, spacing, and overlap. Also check
arta_get_view's runtime.errors (compile/runtime problems) and runtime.gates
(craft findings, scanned automatically on every file change).
Pick the engine by the question you're asking:
- "Is the CONTENT right?" (layout, text, fonts, colour, spacing, images, dead-band,
craft, AI-slop) → the default (
engine omitted = "chrome"). This is almost
always what you want.
- "Does it FIT THE DEVICE?" (does the header / bottom CTA clear the status bar,
notch, and home indicator — the safe-area / chrome-mode check) →
engine: "client", the only shot that includes the device bezel + status-bar overlay. It
comes from the viewer's own capture, so the dev must have opened that screen first
(else ask them to, then re-shoot); it can also drift on some CSS, so trust it for the
device-fit question, not for fine content/contrast.
In one line: content → chrome (default); device-fit → client.
-
Run the craft check — arta_design_review on the screen(s). It's Arta's own
offline anti-slop detector (no npx, no network — instant), so "no detector on this
machine" is never an excuse to skip it. Findings come ranked error → warn → info:
- error = a serious tell — fix every one (gradient-text headline, coloured
side-stripe border, stripe-gradient background, cramped letter-spacing, card nested in
a card, 1px-border + wide-shadow ghost card).
- warn / info = softer (transition:all, uniform hover-scale, emoji-as-icon, italic
heading, placeholder name, mixed icon libraries, over-rounded card) — judge in context.
It's a static reader (class-, CSS-, and JSX-aware, but it doesn't render), so when a finding
looks wrong, confirm against the screenshot (step 1) before chasing it — fix what the
pixels show, not what a static read guesses. For a deeper one-off pass, /impeccable audit is available too.
-
Run this checklist — fix anything that fails, then re-check:
- No stray or empty screens. Every screen file has real content.
arta_doctor
seeds a home.tsx placeholder ("Your canvas is live"); once the real screens
exist, replace it (or delete the file) and make sure config.json's start
points at a real, built screen. A leftover placeholder = the dev clicks it and
sees a blank.
- Shared chrome is factored, never repeated. If a header / nav / tab-bar / footer
shows on 2+ screens, it's a shared component under
proto/components/ that each
screen imports and renders, NOT pasted into each screen body. Caught yourself
copying it? Move it into a component now — <NavLink> from react-router-dom
gives the active state for free (see "Share components" below), so one shared nav
fits every screen.
- The screen fills its frame — no dead band. A short / confirmation / empty-state
screen centers its content (don't top-align it and leave a white gap below); a
list / feed fills top-down. On ios/android/ipad set
safeArea to the top-edge
colour so the clock/icons contrast.
- Content clears the status-bar overlay (chrome mode). On ios/android/ipad with
the default chrome, the status bar + notch + home indicator float ON TOP — so the
top header and bottom bar/CTA must carry the safe-area padding (≈48/28px ios) or
they hide under the clock and home pill. Full-bleed media/splash →
chrome:false.
This is the one check that needs arta_get_screenshot { engine: "client" } —
only that shot draws the bezel + status-bar overlay, so it's how you actually SEE
whether the content collides with them (the default chrome shot is content-only).
- Non-Latin text renders in a real font. Thai / CJK headings set in a Latin display
face (Instrument Serif / Fraunces / Space Grotesk) fall back to a broken system face —
collided tone-marks, wrong line-height. Add the
'Noto Sans/Serif Thai' fallback (or
use a sans). The screenshot tells you instantly: garbled diacritics = wrong font.
- Craft reads — contrast ≥4.5:1, real images (no gray boxes), the accent actually
shows up in 3+ places, a real type-scale jump, no AI-slop.
-
Only once it's clean do you hand it back — one line on what to react to.
state.json shape (reference)
{
"meta": { "name": "AppName", "phase": "prototype" },
"spec": {
"goal": "one sentence",
"users": ["..."],
"userStories": ["As a …, I want … so that …"],
"scope": { "in": ["..."], "out": ["..."] },
"constraints": ["..."]
},
"dataModel": {
"entities": [
{ "name": "Entity", "fields": [
{ "name": "id", "type": "uuid", "pk": true },
{ "name": "otherId", "type": "uuid", "fk": "Entity", "required": true }
] }
],
"relationships": [ { "from": "A", "to": "B", "type": "N:1", "label": "for" } ]
},
"api": {
"info": { "title": "App API", "version": "1.0.0" },
"servers": [ { "url": "https://api.app.com/v1" } ],
"x-middleware": [ { "name": "auth", "description": "Require a Bearer token" } ],
"paths": {
"/items/{id}": {
"get": {
"summary": "Get an item",
"x-middleware": ["cors", "auth"],
"x-screens": ["itemDetail"],
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
{ "name": "expand", "in": "query", "schema": { "type": "string" }, "description": "..." },
{ "name": "Authorization", "in": "header", "required": true, "schema": { "type": "string" } }
],
"responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" } } }, "example": { "id": "x" } } } }, "404": { "description": "Not found" } }
},
"post": {
"summary": "Create",
"requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" } } }, "example": { "name": "x" } } } },
"responses": { "201": { "description": "Created" } }
}
}
}
},
"architecture": {
"stack": ["Node/Express", "Postgres", "Redis"],
"nodes": [
{ "id": "web", "name": "Web app", "kind": "client", "tech": "React", "deployment": "Vercel" },
{ "id": "api", "name": "API", "kind": "service", "tech": "Node/Express", "deployment": "AWS ECS", "description": "..." },
{ "id": "db", "name": "Primary DB", "kind": "datastore", "tech": "Postgres 16" }
],
"edges": [
{ "from": "web", "to": "api", "protocol": "REST", "mode": "sync" },
{ "from": "api", "to": "db", "protocol": "SQL", "mode": "sync" }
],
"decisions": [
{ "id": "ADR-1", "title": "Use Postgres", "status": "accepted", "context": "…", "options": ["Postgres", "Mongo"], "decision": "…", "consequences": "…" }
],
"nfrs": [ { "name": "Availability", "target": "99.9%", "note": "…" } ],
"security": [ { "boundary": "public → API", "note": "JWT auth, rate-limit at gateway" } ]
},
"plan": {
"stack": ["React", "Node"],
"statuses": [
{ "id": "backlog", "name": "Backlog", "color": "#71717a" },
{ "id": "doing", "name": "In progress", "color": "#fbbf24" },
{ "id": "done", "name": "Done", "color": "#34d399" }
],
"milestones": [
{ "name": "M1", "tasks": [ { "title": "…", "status": "doing", "priority": "high" } ] }
]
}
}
Screen authoring contract (real React, zero Arta imports)
Every screen is a plain React component that cannot tell it's running inside Arta
(ADR-0002) — the point of the whole model: what you write lifts into the real app
unmodified, no refactor later. That means:
-
Zero imports from anything Arta-owned. No @arta/* package, no useArtaStore,
no special navigate() call. Navigation, error forwarding, annotate mode, route
tracking, snapshot timing, dark-theme enforcement — everything Arta needs lives in
the Arta-owned shell that MOUNTS your screen, not in code you write.
-
Navigation is plain react-router-dom — the shell builds routes from your
files, one route per screen file. <Link to="/checkout">Checkout</Link> or
const navigate = useNavigate(); … navigate("/checkout"). A route is just
/<screen-file-id>.
-
A curated dependency set is the whole vocabulary. These are the only bare
imports available in a screen or component — all aliased/deduped to the viewer's
single copy, so a screen can never accidentally load a second React and crash hooks:
| Package | Reach for it when |
|---|
react / react-dom | always available — the base |
react-router-dom | navigation — <Link>, useNavigate, useParams |
motion | animation — screen/element transitions, gesture-driven interactions, list reorder |
lucide-react | icons — see "Icons" below |
recharts | charts, graphs, sparklines, any data visualization |
clsx | composing a conditional className string without hand-rolled string-munging |
tailwind-merge | merging/overriding Tailwind classes without specificity fights (a component that takes a className prop and needs the caller to be able to override a default) |
Nothing else resolves — there's no agent-managed package.json yet. If a screen
genuinely needs something outside this list, say so instead of reaching for it; the
curated set is deliberately the whole vocabulary so it can be taught and checked
deterministically.
-
export const meta = {...} must be a pure object literal — no imports, no
spreads, no function calls in it (see "Storage layout" above for why).
Shared state — proto/lib/
There's no mock-store API anymore — shared state across screens is just React,
authored under proto/lib/ as plain .ts/.tsx. Because screens navigate via
routes (not a shared component tree — each screen mounts fresh when the route
changes, there's no single tree wrapping the whole app), a Context.Provider around
the router is often more ceremony than something as simple as a cart count needs. The
simplest cross-screen pattern is a module-level store + useSyncExternalStore —
one canonical example, ~20 lines:
type State = { cart: number };
let state: State = { cart: 0 };
const listeners = new Set<() => void>();
const emit = () => listeners.forEach((l) => l());
export function getState() {
return state;
}
export function subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function addToCart() {
state = { ...state, cart: state.cart + 1 };
emit();
}
import { useSyncExternalStore } from "react";
import { getState, subscribe } from "./store";
export function useCart() {
return useSyncExternalStore(subscribe, getState).cart;
}
Any screen reads it with const cart = useCart() and mutates it by calling
addToCart() — both re-render on change, no provider to wrap around the router.
Reach for React Context instead only when the state is genuinely scoped to one
component subtree (a multi-step form's fields), not a cross-screen concern.
theme.css — the shared design system
.arta/proto/theme.css is Tailwind v4, CSS-first — there's no tailwind.config.js.
Never remove or restructure these three lines — every screen's Tailwind classes
depend on them:
@import "tailwindcss";
@source "./";
@custom-variant dark (&:where(.dark, .dark *));
Design tokens are Tailwind v4 @theme variables — this is the ONE design system now,
no separate tokens/CSS split:
@theme {
--color-bg: #fbfaf8;
--color-ink: #1a1a18;
--color-accent: #b3321a;
--font-display: "Fraunces", "Noto Serif Thai", serif;
--radius-md: 0.75rem;
}
Every --color-* / --font-* / --radius-* var becomes a Tailwind utility for free
(bg-bg, text-ink, bg-accent, font-display, rounded-md) — style screens with
those utilities, not raw hex or inline style. Extra rules that aren't tokens (a
.display helper class, an @apply-based component class) go in theme.css too,
outside the @theme {} block.
Dark theme is a .dark {} block overriding the same var names:
.dark {
--color-bg: #0b0b0c;
--color-ink: #fafafa;
}
The shell wires the toggle for you — put data-theme-toggle on any element (a
button, an icon) and clicking it flips .dark on <html> and persists the choice;
you write no JS for this. Style with Tailwind's dark: variant (dark:bg-zinc-950)
or the plain vars (they already read --color-bg/--color-ink and follow the
toggle either way). Only add data-theme-toggle when the design calls for a switch;
if you support dark, theme EVERY screen (test it) — a half-dark flow reads as a bug.
The viewer's Design system sub-view parses theme.css directly and shows a
light/dark toggle whenever it finds a .dark {} block, so your overrides are
inspectable there too, not only inside a screen.
What replaced the six data-* attributes
The old HTML-string screens wired ALL interactivity through six data-* attributes
on a mock store. Screens are real React now — here's the direct replacement for each:
Old mechanism (six data-* attributes on a mock store, HTML strings) | New (React, real files) |
|---|
| the navigate-to-a-screen attribute | <Link to="/screenId"> or useNavigate()("/screenId") |
| the reflect-a-store-value-as-text attribute | plain JSX: {value} |
the conditional-visibility attribute (data-show="key" / "step==2") | plain JSX: {condition && <div>…</div>} |
the increment / decrement / set attributes (data-dec, data-set) | a real onClick handler calling a function from proto/lib/store.ts |
the mock store itself (prototype.store) | proto/lib/store.ts (or similarly named) — real React state/hooks, see "Shared state" above |
data-nav="screenId" — active-nav-for-free on a shared header | <NavLink to="/screenId" className={({ isActive }) => isActive ? "…" : "…"}> from react-router-dom — React's own version of the same "free" active state |
There is no other interactivity API to learn — Link/useNavigate, plain JSX
conditionals, and ordinary event handlers are the whole vocabulary.
Icons
Import real icon components from lucide-react — no CDN, no placeholder markup:
import { ShoppingCart } from "lucide-react";
…
<ShoppingCart className="h-5 w-5 text-accent" />
Size and colour with Tailwind classes exactly like any other element (the SVG
inherits currentColor). It's the same lucide vocabulary as before, now as named
exports — check lucide.dev/icons for the exact export name; an unknown import is a
compile error you'll see immediately (not a silent blank gap).
Brand/social glyphs (GitHub, Instagram, LINE, …) aren't in lucide's core — the old
Iconify CDN fallback is gone. Use an inline <svg> with the brand's path, or factor
repeated ones into a small shared SocialIcon component under proto/components/
(e.g. <SocialIcon name="github" className="h-4 w-4" /> wrapping a lookup over
inline SVGs). Keep lucide as the default for UI glyphs; reach for a custom SVG only
for brand marks lucide doesn't have. Emoji are not icons.
Errors & gates
Compile errors and craft findings both surface through arta_get_view now — check it
after every change, same discipline as before, just one tool:
runtime.errors — console/runtime errors forwarded from the live prototype
(unchanged from before — still how you catch a broken render without waiting for
the dev to report it).
runtime.gates — the SAME anti-slop findings arta_design_review returns
(gradient text, side-stripe borders, cramped tracking, nested cards, …), now ALSO
computed automatically by the dev server on every file change (write-time gate →
watch-time gate: it fires no matter how the file changed — your Write, a git
checkout, a human edit). Ranked error → warn → info; fix every error.
A compile error (bad JSX, a typo'd import) can't be caught by the gate scanner — it
surfaces through runtime.errors once Vite tries to transform the file, so check
arta_get_view after every edit, not just the ones you're unsure about.
Building the design system & craft
Tailwind (via theme.css) and lucide-react are available in every screen. Use
them; do not hand-roll what they give you, and never use emoji as icons.
-
Pick a brand-grade design language FIRST — don't invent generic tokens. Before
building any screen, read design-systems.md (next to this file): a library of
opinionated, ready-to-adapt systems (Ink · Graphite · Clay · Mist · Signal). Pick the
one that fits the brief, swap its accent + brand to the project, and set it as the
foundation by writing the kit's tokens into .arta/proto/theme.css's @theme {}
block (plus any extra CSS the kit needs) with your normal Write/Edit tools. This
is the single biggest lever on whether the output looks designed or looks like "an
AI made a webpage." Starting from generic grays + a blue accent is the #1 tell.
design-systems.md is the dress; component-cookbook.md
is the shape & parts. Once the system is set, build each screen from the cookbook's
app archetypes (navs, page headers, data tables, forms, empty states, …) — token-driven
and slop-free — instead of re-deriving chrome that drifts into the generic look.
-
Make it look like this project, not its category — the category-reflex check. A kit
is a starting structure, never a finished skin. If you set one and only swap the brand
name, every app in that category ships the same look — the #1 reason two different briefs
come back looking identical. Three moves break that:
- Pick the colour strategy on purpose. Restrained (tinted neutrals + one accent ≤10%),
committed (one saturated colour on 30–60% of the surface), full palette (3–4 named
roles), or drenched (the surface IS the colour). Product/tool UI floors at restrained; a
brand / marketing / launch / portfolio surface earns committed-or-louder. Beige-plus-one-
accent on a bold brief is a hedge, not a decision.
- Warmth comes from the accent, not the page. "Premium / editorial / roaster / artisan"
pulls every build toward a warm-beige paper background — and that is the cream tell
(
arta_design_review reds it as cream-palette). Keep the surface a true off-white
(near-zero chroma — red and blue channels within ~4, e.g. #fbfaf8, not #f4f1ea) or
commit to a deep / drenched brand surface; carry the warmth in the accent, type, and
imagery instead. A warm page background is the convergence default, not a design decision.
- Give the project its OWN accent. The hex in each kit is an example, not a default —
choose a fresh accent hue per project (nudge the neutral / radius / type-scale too when it
helps). Two builds must not share a palette just because they share a register.
arta_design_review flags a kit's literal accent (and the cream / AI-purple defaults) left
unchanged.
- Run the reflex check before you commit the system. First-order: could someone guess
the palette + type from the category alone ("fintech → navy + gold", "AI tool → cream +
purple", "dashboard → dark + indigo")? If yes, it's the training-data reflex — rework it.
Second-order: could they guess it from the category plus the obvious anti-move ("the
AI tool that's not cream → editorial italic-serif")? Then you only dodged the first
reflex; keep going until neither is predictable. Anchor it with one concrete scene
sentence — who uses this, where, in what light/mood ("a night-shift dispatcher in a dim
ops room", never "modern and clean") — and name 2–3 ways this build must look unlike the
generic version of its category.
-
Load the deep craft reference for what you're working on. design-systems.md and
component-cookbook.md give you the kit and the parts; the reference/
library is the craft knowledge — load the one file relevant to the decision in front of
you instead of guessing. First, read the matching register reference — non-optional, it
sets opposite defaults that keep two briefs from converging:
reference/register-brand.md when design IS the
product — a marketing / landing / campaign page, a portfolio, an image-led brand. The bar
is distinctiveness; committed-or-louder colour; imagery required.
reference/register-product.md when design SERVES a
task — an app UI, dashboard, admin, settings, tool. The bar is earned familiarity;
restrained floor; every control ships all its states.
Then pull the craft file for the work: typography ·
color · layout ·
motion · interaction ·
imagery · and critique-rubric
for the deep per-screen self-review (the six axes, expanded). They're Arta-native (the five
fonts, tokens, the shared state model, the detector) — read on demand, not all at once.
- UX & usability — a pretty screen can still be hard to use. These cover what the
visual craft doesn't:
ux-heuristics (Nielsen's 10, +
persuasion) and accessibility (WCAG 2.1 A/AA — Arta has the
real markup, so focus order / labels / semantics ARE checkable) for the self-review pass;
cognitive-load when a form / flow feels heavy;
ux-research in the brainstorm to shape whose journey
you're building (the antidote to a generic app); and
ai-product-patterns when the prototype itself is an
AI product (chat, copilot, generator) — input patterns, blank-slate wayfinding, trust
signals, human-in-the-loop governors.
-
Then build every screen from those tokens. They render as the style guide in
the Prototype → Design system sub-view, and every --color-*/--font-*/--radius-*
var in theme.css's @theme {} block becomes a matching Tailwind utility
(bg-accent, font-display, rounded-md) — style screens with those, or
var(--color-accent) when you need the raw value, plus shared components under
proto/components/ — one source of truth, never ad-hoc values per screen. No raw
hex in a screen body (no style={{ color: "#1a1a1a" }}, no bg-[#f5f5f5], no
text-[#666]): every colour is a token-derived class or var(--color-*). Need a
shade the kit doesn't have (a brighter heading ink, a tint)? Add it to theme.css's
@theme {} block and reference it — don't inline the hex. Hardcoded hex in
screens is the #1 reason a build "has a design system but doesn't use it." This bites hardest
in data-dense apps: the repeated semantic palette (status / priority / severity badge
colours — the same in-progress purple, done green, warning amber on every screen) belongs in
theme.css too (--color-status-done, --color-severity-high, …). Inlining the same #hex
10+ times across screens — even a consistent one — is the classic miss; define it once as a
token, reference the var.
-
Craft, not just absence of slop — commit to ONE color strategy (restrained /
committed / drenched), pair fonts on a contrast axis (serif + sans, geometric +
humanist, or one family in many weights — the 5 preloaded families: Geist, Geist Mono,
Instrument Serif, Fraunces, Space Grotesk), keep a single spacing rhythm, and hold
radius / shadow / motion consistent across screens. Use real content (real copy,
real names, real prices) — never lorem ipsum or "Card title". Generous whitespace and
one confident accent beat many timid ones.
-
Non-Latin text (Thai, CJK, Arabic…) needs a font that covers the script. The five
display/sans faces above are Latin-only — set a Thai heading in 'Instrument Serif'
or 'Fraunces' and the Thai glyphs fall back to a system serif: tone-marks and vowels
collide, line-height goes wrong, and it reads broken. Two Thai faces are also preloaded —
'Noto Sans Thai' and 'Noto Serif Thai' — so build a fallback chain that lets
Latin and Thai each use a real face: --font-display: 'Fraunces', 'Noto Serif Thai', serif
(display) and --font-sans: 'Geist', 'Noto Sans Thai', sans-serif (body), set in
theme.css's @theme {} block. Body text already gets 'Noto Sans Thai' by default,
but any element where you override the family for a Latin display face must add the
matching Noto Thai fallback — or reserve the Latin display face for genuinely Latin
runs (a brand name, a queue number A14) and set the Thai heading in a sans. Always
confirm in the screenshot that non-Latin headings render clean.
-
Commit — timidity is the #1 reason solid structure still reads "AI-generic". Two
places agents play it too safe, and both flatten a design into "competent but generic":
- Colour: the accent has to actually show up — on the primary action and the
active/selected state and a key highlight (3+ places), not one lonely button. Tint
the neutrals toward the brand hue (a warm off-white, a cool slate); flat grays and
pure
#fff/#000 read as an unstyled browser default. "Restrained" means few colours
used confidently, not no colour — a kit's accent ≤10% still has to be visibly present.
- Type scale: make the jump obvious — the display/hero size ≥ ~2× the body and heavy;
labels small, uppercase, muted, slightly spaced. When H1, card titles, metric values and
body all collapse into one narrow size band, hierarchy dies and it looks templated. Pick
a real scale and use its extremes (e.g. a 48px+ hero number over a 11px muted label).
- Interactive state must read: a selected / active / current item needs an unmistakable
change — shift the fill (a tint), not just a border, and use a solid control (a filled
radio / check, not a ghost outline). On dark themes a border-only or faint-outline "selected"
state is nearly invisible; the dev can't tell what's chosen. Do NOT mark it with a thick
coloured
border-left bar — that's the banned side-stripe, and an active nav / sidebar /
list row is exactly where the reflex sneaks in. Tint the row's background (optionally a
1px full border or a subtle inset highlight via box-shadow: inset), never a left accent stripe.
- No hero eyebrow chip. The little pill above the headline — a tiny dot (often a pulsing
animate-ping) plus a tracked-uppercase micro-label ("● NOW IN PRIVATE BETA", "✨ NEW") —
is one of the loudest AI landing tells, and the same chip then reappears as a kicker on
every section. arta_design_review reds it (hero-eyebrow-chip). Lead with the headline;
if a status genuinely matters, state it in plain words inline — never pulse a dot for decoration.
-
Images: decide deliberately — a REAL image, or an intentional skeleton+colour. Never a
bare solid fill. A flat coloured/gray rectangle dropped where a photo belongs is one of
the loudest "an AI made this" tells; it makes every other decision read as unfinished.
Make an explicit call per image slot:
- Real image — use it whenever a photo genuinely helps. Always set width/height (or an
aspect-[…] wrapper) + object-cover so layout stays put. Pick a source that RESOLVES
(verified working June 2026):
- Unsplash
https://images.unsplash.com/photo-<id>?w=<W>&q=70&auto=format — the
preferred source. Use a real photo id you're confident exists (a guessed id 404s).
picsum.photos is dead — do NOT use it (every URL times out → blank). The old
source.unsplash.com/... random endpoint is also retired.
https://loremflickr.com/<W>/<H>/<keyword> — when you have no specific Unsplash id:
a live keyword-relevant random photo (the picsum replacement). Add ?lock=<n> to keep
it stable across reloads.
(There's no automatic broken-image fallback anymore — a dead URL shows the browser's
broken-image glyph. If you want a graceful fallback for a specific slot, add an
onError handler on the <img> that swaps to a skeleton <div>; otherwise just
use a source that resolves.)
- Skeleton + colour — when a real image isn't right (avatars, logos, an icon slot, or
the subject must match and you have no trustworthy URL), make the placeholder intentional:
a token-tinted gradient (
bg-gradient-to-br from-accent/25 to-accent/10) for a cover slot,
or a flat tinted tile (bg-ink/5) for a loading skeleton, optionally with a centred lucide
glyph or a monogram — factor either into a small shared Cover/ImageSkeleton component
under proto/components/ if you reuse it. Avatars: initials on a tinted disc beat a gray
circle every time.
Either way the box is sized and on-brand — never a lone bg-[#…] block standing in for a photo.
-
Write Tailwind utility classes in className — NOT inline style. theme.css
compiles Tailwind at build time, so utilities are the default way to style
everything: layout, spacing, colour, typography, radius, shadow, hover/focus
states. Do not reach for style={{ … }} out of habit — it makes the markup
harder to scan and the design inconsistent. Reserve inline style for the rare
genuinely-dynamic value no utility can express (an exact computed width, a one-off
gradient).
- ✅
className="flex items-center gap-3 rounded-xl bg-zinc-900 px-4 py-2 text-sm font-medium text-zinc-100"
- ❌
style={{ display: "flex", alignItems: "center", gap: 12, borderRadius: 12, background: "#18181b", padding: "8px 16px" }}
- Repeated pattern? Define a class once in
theme.css (Tailwind @apply works
there, outside the @theme {} block) or factor a shared subcomponent under
proto/components/ — don't paste the same class string onto every element.
theme.css (custom CSS / tokens) coexists with utilities; reach for it only for
what utilities can't express, not as a substitute for classes.
Avoid AI-slop — these read as "an AI made this"
Don't ship them; rework the element instead. (Run arta_design_review to catch them
automatically.)
- Side-stripe borders (a thick coloured
border-left/right as a card/alert
accent) — use a full border, a background tint, or nothing.
- Gradient text (
background-clip:text + gradient) — use one solid colour;
emphasise with weight/size.
- Glassmorphism by default (decorative blur/glass) — rare and purposeful, or none.
- Identical card grids (same icon + heading + text card repeated) and the
hero-metric template (big number, small label, gradient accent).
- A tiny uppercase tracked eyebrow above every section, or 01/02/03 numbered
section markers as default scaffolding.
- Low-contrast text — body must hit ≥4.5:1; never grey text on a coloured fill.
- Over-rounded cards (radius ≥ 24px) — cards top out ~12–16px (pills are fine).
(The slop list above + the screen-shape variety below draw on Hallmark — MIT,
github.com/Nutlope/hallmark — and the impeccable design skill. arta_design_review
encodes the deterministic subset as gates.)
Vary the screen shape — structural variety, not just colour. The biggest reason a
multi-screen build reads "templated" isn't the palette — it's that every screen has the
same shape (hero → 3 cards → CTA, or six identical vertical card-lists). Match each
screen's shape to its job, and make consecutive screens differ structurally, not just
in content. A small menu of app/marketplace shapes to pick from (name the shape before you
build the screen — it's the Variety axis of the self-critique):
- Dashboard / bento — modular tiles of varying size (a metric, a chart, a list, a
callout), rhythm from size variation — not a uniform card grid.
- Master–detail / split — list or nav rail on one side, the selected item's detail on
the other (inbox, settings, admin).
- Feed / timeline — a vertical stream of heterogeneous items (not all the same card).
- Index / browse — a filterable grid or list of entities; the catalog is the page
(marketplace, course list, search results).
- Detail / profile — one entity's hero + supporting sections (product, course, person).
- Workbench / canvas — a primary work surface with supporting panels (editor, builder).
- Stepped flow / wizard — sequential steps with a progress spine (checkout, onboarding).
- Focused / empty state — one centered purpose, content vertically centered, no dead
band (login, confirmation, zero-data).
- Table / data-dense — a real table or spec sheet when the data is the point (don't
fake it as cards).
- Marketing / landing — only when it genuinely is one; then vary the hero itself.
The headline-left / image-right split diptych is a perfectly good hero — the catch is
it's the one nearly every build reaches for first, so unrelated briefs come back sharing
one silhouette. It's not banned; just don't let it be the default. Pick the hero shape on
purpose from the full range — a full-bleed immersive hero, a stat/number-led hero, a
type-only statement, an asymmetric editorial front page, the product UI as the hero, or
the split when it genuinely fits — and don't let two builds in a row share the same one.
Reach-for component archetypes (default away from the AI-fingerprint chrome). For
ready-to-adapt, slop-free building blocks — side rail, top bar, ⌘K, page header, stat tile,
data table, browse card, 8-state form field, segmented control, empty state, status badge,
inline alert, stepper — read component-cookbook.md (next to
this file) and assemble the screen from its parts. Two pieces are where the generic look
creeps in — pick deliberately, and don't repeat the same one on every screen:
- Navigation — a side rail (icon+label) is the app default; a bottom tab bar for
mobile; a top bar (wordmark · sections · one action) for marketing/content; ⌘K /
command palette or a floating pill when it fits. Avoid the reflex "wordmark-left +
4 links centered + button-right + 1px hairline + white bg" bar on every screen.
- Footer — app screens usually need none (or a slim status strip). For marketing,
avoid the default "4 columns of links + social-icon row + tiny copyright"; a single
statement line, a newsletter-first close, or an index-style link list reads less templated.
Pick the device frame with the "frame" key in config.json (or a screen's own
meta) — e.g. {"frame": "desktop"}. Not "device"; the key is frame. Valid
values: web (browser, default), desktop (native app window), ios or android
(phone), ipad (tablet). The phone frames render the page at ~390px wide, the ipad frame at
~780px, and web/desktop at a real desktop width (~1280px), scaled to fit the
canvas — so md:/lg:/xl: utilities and your @media rules fire exactly as they
would in a browser at that width (the web preview shows the desktop nav, not the
mobile hamburger, even when the canvas pane is narrow). Write responsive CSS
accordingly. Choose the frame that matches what you're actually building — a mobile
app spec should preview in ios/android, a tablet layout in ipad, a responsive
site in web, not a phone. A web/desktop prototype can still be previewed at
phone/tablet size (the viewer wraps it in a mobile browser frame — Safari-style
chrome, not a native status bar), so make a responsive site work all the way down to
mobile widths; its safeArea colour paints that browser status bar.
Device frame & safe area (ios / android / ipad). Content is always
full-screen — it fills the whole device, edge-to-edge. There are two chrome
modes, set with chrome (per screen meta, or config.json's default); there is
no on/off toggle in the UI.
-
chrome: true (default) — status bar overlaid. The iOS-style status bar
(showing the real current time), the notch / Dynamic Island, and the home
indicator float on top of your content. Because they overlay it, you must
reserve the safe area yourself — pad the screen's top and bottom so nothing hides
under the status bar/notch or the home pill. Rough insets to bake into the top-level
padding (or a sticky header):
ios — top ≈ 48px (status bar + Dynamic Island), bottom ≈ 28px (home indicator)
android — top ≈ 32px, bottom ≈ 20px
ipad — top ≈ 32px, bottom ≈ 24px
Also set safeArea to the screen's top-edge colour (the colour directly
under the status bar) so the clock + icons auto-contrast (light on dark, dark on
light) — set it in the screen's own export const meta = { safeArea: "#0b0b0c" },
or as a prototype-wide default in config.json. A sticky/fixed top bar must
include the top inset so it sits clear of the status bar.
-
chrome: false — true edge-to-edge, no status bar. Drops the status bar,
notch, and home indicator entirely; the design owns every pixel and needs no
safe-area padding. Use for splash, login, onboarding, camera, full-bleed media, or
any screen that draws its own top/bottom bars.
Default to mode 1 for normal app screens (pad for the overlay), and switch to
chrome: false for the full-bleed cases above.
Example, end to end — a button that mutates shared state, and a header badge that
reflects it:
import { ShoppingCart } from "lucide-react";
import { useCart } from "../lib/useCart";
import { addToCart } from "../lib/store";
function AddToCartButton() {
return (
<button onClick={addToCart} className="rounded-md bg-accent px-4 py-2 text-sm font-medium text-white">
Add to cart
</button>
);
}
function CartBadge() {
const cart = useCart();
return (
<span className="relative">
<ShoppingCart className="h-5 w-5" />
{cart > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-accent px-1.5 text-xs font-semibold text-white">
{cart}
</span>
)}
</span>
);
}
Common content patterns — rails & cards
Content screens (a marketplace, a course list, a feed) lean on the same two
primitives over and over, and they're the ones agents most often get almost
right (gray cover boxes, a card grid that won't scroll, no peek). There's no opt-in
helper class for these anymore (the old .hs-rail/.hs-cover kit isn't ported —
screens style themselves directly) — plain Tailwind gets you there in a couple of
utilities:
- Horizontal rail —
overflow-x-auto snap-x snap-mandatory [scrollbar-width:none]
on the row: a sideways-scrolling strip that snaps and hides its scrollbar. Give each
child a fixed width (w-64, w-40) plus snap-start, and let the row bleed past
the container padding (-mx-5 px-5) so the next card peeks past the edge — the
affordance that says "there's more."
- Cover placeholder — a brand-tinted gradient div for an image slot
(
bg-gradient-to-br from-accent/25 to-accent/10), never a flat gray box (the
loudest slop tell). Lay a real <img className="h-full w-full object-cover"> over
it when you have one; otherwise it's a deliberate surface, not an empty rectangle.
Factor it into a shared Cover component under proto/components/ if more than one
screen needs it.
A recommended-courses rail, end to end (mirrors the marketplace home above):
import { Link } from "react-router-dom";
import { Play, Star } from "lucide-react";
function RecommendedRail() {
return (
<section className="px-5">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-lg font-semibold">คอร์สแนะนำ</h2>
<Link to="/courses" className="text-sm font-medium text-primary">ดูเพิ่ม</Link>
</div>
<div className="-mx-5 flex snap-x snap-mandatory gap-4 overflow-x-auto px-5 [scrollbar-width:none]">
<Link to="/course-detail" className="w-64 shrink-0 snap-start overflow-hidden rounded-2xl bg-white shadow-sm">
<div className="relative h-32 bg-gradient-to-br from-accent/25 to-accent/10">
<span className="absolute left-2 top-2 inline-flex items-center gap-1 rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">
<Play className="h-3 w-3" /> ออนไลน์
</span>
<span className="absolute right-2 top-2 rounded-full bg-black/70 px-2 py-0.5 text-xs font-medium text-white">-40%</span>
</div>
<div className="p-3">
<h3 className="font-semibold">แต่งหน้าออกงานมือโปร</h3>
<p className="mt-0.5 text-xs text-zinc-500">โดย ครูพิม · 12 บทเรียน</p>
<div className="mt-2 flex items-center gap-1 text-xs">
<Star className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
<span className="font-medium">4.9</span>
<span className="text-zinc-400">(320)</span>
</div>
<p className="mt-1"><span className="font-bold text-primary">฿1,490</span> <span className="text-xs text-zinc-400 line-through">฿2,490</span></p>
</div>
</Link>
{/* more cards … */}
</div>
</section>
);
}
-mx-5 px-5 lets the rail bleed to the device edges while its content keeps the page
gutter — so the next card peeks at the screen edge, not at a margin. Vary the
cards (real titles, prices, ratings — never the same card cloned); an identical card
grid is its own slop tell. Review a long stack of these end to end with
arta_get_screenshot { full: true }.
Share components — DO NOT repeat markup
Put anything that appears on more than one screen (header, footer, nav, cards) into
its own file under proto/components/, and import it into every screen that
needs it. There's no template/slot system to learn — it's ordinary React composition:
import { NavLink } from "react-router-dom";
export function Header() {
return (
<header className="flex items-center justify-between border-b border-line px-6 py-4">
<span className="font-display text-lg font-semibold">Shop</span>
<nav className="flex gap-4 text-sm">
<NavLink to="/home" className={({ isActive }) => isActive ? "font-medium text-accent" : "text-muted"}>Home</NavLink>
<NavLink to="/cart" className={({ isActive }) => isActive ? "font-medium text-accent" : "text-muted"}>Cart</NavLink>
</nav>
</header>
);
}
import { Header } from "../components/Header";
export const meta = { title: "Home" };
export default function Home() {
return (
<>
<Header />
<main>…</main>
</>
);
}
This is the rule, not a nicety: when the dev says "change the header," you edit one
component file and every screen that imports it updates — no hunting through screens,
nothing missed.
- Active nav comes for free the React way:
<NavLink> (not <Link>) from
react-router-dom calls its className/style prop with { isActive }, so one
shared nav component works for every screen without you tracking "which screen am I
on" yourself — the direct replacement for the old runtime's data-nav.
- A shared bottom-tab-bar / side-rail / footer works the same way — one
component, imported by every screen, with
<NavLink> for the active item.
- A screen that's genuinely standalone (a landing splash, an auth screen) simply
doesn't import the shared header/nav — there's no special opt-out flag needed,
it's just a component you don't render.
Workflow: build theme.css + the shared components first, then add screens that
import them. When iterating, edit the ONE shared component file — not every screen.
Rules
- Brainstorm before you build. Your first reply to a new design request is a
question, not a prototype. Get a direction approved first (above) — only lo-fi
sketches are allowed before that.
- Keep
meta valid at all times (it must always have name and phase); the
viewer shows a waiting/error state otherwise.
- Patch, don't clobber: use
arta_patch_state so untouched sections survive.
- One change, one question. The viewer is a conversation, not a deliverable.
- Read feedback before assuming you're done. The dev's clicks are the spec.
Handing off to implementation — build with subagents
Arta is for design. Once the dev approves the design and the Plan
board is filled in, stop designing and build the real thing — and build it with
superpowers:subagent-driven-development, not one long hand-coded context.
- The Plan Kanban is the implementation plan: each card is a task. Work them
in order (respect the swimlane/milestone grouping and status), one implementer
subagent per task — never parallel implementers.
- The
.arta/ artifacts are the source of truth each subagent reads — point
subagents at them instead of re-describing the work:
spec — intent, users, scope, constraints
- the prototype HTML +
designSystem — the exact UI to build (it's real HTML/CSS)
dataModel — entities, fields, relationships
api — the OpenAPI 3 routes, middleware, params, bodies, responses
- Follow that skill's loop: per-task brief → implement + test + commit → task review
(spec compliance and code quality) → fix until clean → final whole-branch review.
- Drive the board as you go: move each card with
arta_set_task (e.g. to your
doing / review / done status ids) as its subagent progresses, so the dev
watches implementation advance live on the same Kanban they designed.