From pilotso11-skills
Read a locked sample plate and produce an implementation spec — a buildable decomposition of every visual element into its constructor call, layout coords, typography role, state binding, and asset source. Pair with `screen-comparison` (decomposition is the BEFORE; comparison is the AFTER). Use before opening a screen port PR, before briefing a subagent on a new screen, or when "I need to build this plate" is the task. Produces a markdown spec the developer (or `developer-task-executor`) builds against directly.
How this skill is triggered — by the user, by Claude, or both
Slash command
/pilotso11-skills:screen-decompositionThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are an implementation-plan author. Given a locked sample plate, you produce the **buildable spec** — the brief a developer (or subagent) can construct the screen against without going back to the image to make decisions.
You are an implementation-plan author. Given a locked sample plate, you produce the buildable spec — the brief a developer (or subagent) can construct the screen against without going back to the image to make decisions.
Note on examples. This skill is dense with concrete code patterns — Pixi.js calls, token names (
palette.ox,slots.S0), helper functions (drawFleur,makeButton), asset filenames (nbframe_color.svg,slim_frame.png), MCP names (vinsim-image), and awindow.__vinsim.storePlaywright hook — drawn from the project this skill was authored in. The structure of the decomposition (frame strategy → regions → backdrop → card chrome → typography → controls → state) is general; substitute your stack's equivalents where examples show specifics.
This is the inverse of screen-comparison:
screen-comparison audits a built screen against the plate (after).screen-decomposition produces the build plan from the plate (before).A good decomposition closes the gap that creates the failures screen-comparison exists to catch: ambiguous "card on linen" without per-card chrome spec, "buttons" without variants, "backdrop" without screening strategy, "title" without font role.
docs/redesign/art-kit/plates/samples/<screen>__LOCKED.{jpg,png}ScreenId enum, e.g. "warehouse".app/src/screens/<screen>.ts for diff context.Read. Note every visible element — chrome, copy, typography, character, ornament, button — before writing.nbframe_color.svg screen-wide on this plate, or scoped to the central card? This drives every other decision (where titles can sit, how the backdrop interacts).This step exists because the taxonomy walk is category-driven and naturally reads screens semantically. Spatially implicit structure — column proportions, gutter widths, baseline grids, panel anchoring — is systematically skipped unless it is forced as its own pass. Do this pass first, in isolation, before categorising anything.
Slot names (S0, S1, CARD) are organisational labels only. They do not carry dimensional information. A developer handed S0 without a width % has to guess. Always record both.
Describe the screen as a series of regions and proportions only. No semantic labels during this pass — name regions by position and role, not by content. Use approximate percentages of screen width/height.
Required measurements:
x offset %, width %, y offset %, height %Example skeleton output (abbreviated):
PLATE SKELETON
- Left column: x=0%, w=34%, full height
- Right column: x=36%, w=64%, full height
- Gutter: ~2% between columns
- Header band: y=0%, h=11%
- Hero card: within right column, y=14%, h=72%
- Footer strip: y=90%, h=10%
- Vertical rhythm (list rows): ~9% height each, ~1.5% gap
- Primary alignment axis: left edge at x=5% (both columns)
This skeleton feeds directly into the Layout Regions table in the output template. Every region row must carry both its slot label and its measured % values. A row with a slot name but no measurements is incomplete.
/kit/nbframe_color.svg with data:{resolution:3} and addChild last.Record the choice up-front; it constrains where titles can sit (clear of frame ornaments) and whether backdrops bleed to edges.
(Already locked above — record choice + rationale here.)
slots.S0 / slots.S1 / slots.S2 as labels. Record each column's measured x %, w %, and y-range from the skeleton.CARD_X / CARD_Y / CARD_W / CARD_H / CARD_R constants matching the plate's measured proportions, not estimated values.screen.w × screen.h, dialog floats above — record dialog bbox as % of screen.Rule: every constant in this section must be derivable from the skeleton measurements. If a value cannot be grounded in a skeleton measurement, flag it as an estimate and note which element to re-measure.
For each screen:
palette.linen ground (Bench, Cellar instrument register).Graphics primitives. Use only when the plate's atmosphere is minimal or stylised geometric.vinsim-image) with a project-style prompt anchored on a known style reference (illustrative: <STYLE_UUID>:GENERATED). Matte + vectorise with filter_speckle=12 + color_precision=3 (more aggressive for backgrounds where micro-detail reads as noise). Sanitise. Load with data:{resolution:2} and add as the first child of staticLayer.ColorMatrixFilter (saturation × 0.7) + an alpha 0.15-0.2 linen overlay rect between the backdrop and the dialog so the dialog dominates.Specify the choice + the prompt (if generated) + the screening pass.
For every floating card / dialog / sub-card, specify:
roundRect fill (linen / linen2 / inst).+7,+9 offset, 0x000000 @ 0.30.2-2.8 px stroke, palette.ox.4-8 px margin, 0.9-1.4 px gilt stroke at 0.40-0.60 alpha.drawFleur glyphs at the card corners if the plate shows them.palette.ox, white-cream text.BADGE_BG-style dark fill, top stripe coloured to denote grade.For every text element, specify the helper + size + fill + extras:
titleText(s, { size: 36-44, fill: palette.ox }). Block-caps plate → uiText(s, { size: 26-36, bold: true, track: 4, fill: palette.ox }).uiText(s, { size: 11-12, fill: palette.ox, track: 3-4 }).uiText(s, { size: 11-13, fill: palette.ink }).lblText(s, { size: 19-22, fill: palette.ox, italic: true }).spkText(s, { size: 12-14, italic: true, fill: palette.ink }).numericText(s, { size: 18-44, bold: true, fill: palette.gold | palette.ox }). Anchor "end" for right-aligned scores."middle" for centred; "end" for right-aligned. Default left.slots.S0.y0 + 30 minimum on screen-wide-frame screens to clear the gilt grape-corner ornament. This matches the "Titles anchored at slots.S0.y0 get clipped — move down ~30 px" rule in screen-comparison's common-slips list; keep these two numbers in sync.Decide each control's variant:
Button.primary — oxblood pill, gilt inner stroke, gpale caps text. Size: 200-280 × 44-54.Button.secondary — outlined ox stroke, ox text, no fill. Size: ~120 × 32-40.Button.waxSeal — circular oxblood medallion with gilt double-rule, "CONFIRM" caps inside, optional fleur left.eventMode = "static" + cursor "ns-resize" or "ew-resize".Until the shared Button lands (DS-1), spec the geometry inline but note "TODO: migrate to Button." so the cross-screen sweep is trackable.
roundRect linen2 + gilt stroke + small triangle pointing at speaker + spkText italic body inside.roundRect linen2 + circle portrait + name caption + score numeral + italic pull-quote./kit/leaf{1..4}.svg (white-fill, Pixi-tintable to lot colour).For each icon: where it sits, size, tint, and whether to draw or import.
Pose registry — map game-state to pose SVG:
alma.svg (= alma_03b-compose-right, journal + pen, right-facing). Crop via texture.frame to bbox (0.32, 0.07, 0.50, 0.88).mentor_elder.svg (right-facing wine glass), placed inside the central window plate.alma.svg + mentor_elder.svg, two figures conversing in foreground.alma_02-decide.svg (at desk) + mentor_elder.svg portrait inset.alma_04, MIXED → alma.svg, QUIET_REGRET → alma_05.alma_07-study.svg.alma_08-bottling.svg.alma_08-bottling.svg (foreground winemaker).proto_f_young / proto_m_young / proto_f_elder / proto_m_elder.For each: load via Assets.load<Texture>, crop via texture.frame = new Rectangle(...) to the measured figure bbox, size by targetH (preserving ratio), position. Never mirror via scale.x = -1 (uncanny — wine glass in the wrong hand). If the plate wants the other direction, regenerate the pose.
For each dynamic element, declare:
AppState field drives the value (vintage.lots[i].pct, estate.cashTier, vintage.thesis.targetId, etc.).store setter the interaction calls (store.setLotPct, store.setHarvestStance, store.navigate, store.confirmConception, etc.).*State machine value should gate the input (vintage.benchState === "AIMING", vintage.conceptionState !== "COMMITTED").subscribe callback refreshes (per-row pct text, balance numeral, etc.).eventMode, cursor, accessible = true, accessibleTitle, accessibleHint, tabIndex.test-results/<NN>-<screen>.png, and asserts getState().currentScreen === "<screen>".The outer fence below is four backticks so the inner ts and markdown
fences nest cleanly.
# <Screen> — implementation spec (decomposed from <plate path>)
## 1. Frame strategy
<screen-wide ornate | card-only ornate | none>
**Rationale:** <one sentence>
## 2. Spatial skeleton
<region table with % measurements — populated before any slot labels are assigned>
| Region | x % | w % | y % | h % | Notes |
|---|---|---|---|---|---|
| Left column | 0% | 34% | 0% | 100% | |
| Right column | 36% | 64% | 0% | 100% | |
| Gutter | 34% | 2% | — | — | between columns |
| Header band | 0% | 100% | 0% | 11% | |
| … | | | | | |
Vertical rhythm unit: <~9% row height, ~1.5% gap>
Primary alignment axis: <left edge at x=5%>
## 3. Layout regions (slot labels + measured values)
| Region | Slot label | x % | w % | y % | h % | Contents |
|---|---|---|---|---|---|---|
| Left column | S0 | 0% | 34% | 4% | 92% | figure, stat plaques |
| Right column | S1 | 36% | 64% | 4% | 92% | card, buttons |
| … | | | | | | |
*Every constant below derives from the skeleton above. Estimates are flagged.*
## 4. Backdrop
- Type: <none | programmatic Graphics | generated SVG>
- (If generated) Prompt: `…`
- Screening: <none | ColorMatrix sat 0.7 + linen overlay alpha 0.18>
## 5. Card chrome
For each card:
- Outer rect: …
- Drop shadow: …
- Outer border: …
- Inner inset: …
- Corner fleurs: <yes/no>
- Header band: …
## 6. Asset manifest
| Asset | Source | Notes |
|---|---|---|
| `/kit/nbframe_color.svg` | reuse | data.resolution=3 if used screen-wide |
| `/kit/<bg>.svg` | generate | $0.04 cost; prompt above |
| `/kit/poses/<alma_xx>.svg` | reuse | crop bbox … |
| `/kit/mentor_elder.svg` | reuse | … |
| (others) | … | … |
## 7. Element table — every text + control + icon + tile
| # | Element | Helper | Geometry | State binding | Notes |
|---|---|---|---|---|---|
| 1 | Title "<COPY>" | `uiText / titleText` (size, fill, anchor, track) | (x, y) | static / `<store field>` | … |
| 2 | Caption "<COPY>" | … | … | … | … |
| 3 | Lot row (×4) | composite | rowL, ry, … | `vintage.lots[i].pct` ↔ `setLotPct` | … |
| … | | | | | |
Group by static (built once in `buildStatic`) vs dynamic (built in `buildDynamic` and updated in `refresh`).
## 8. Controls
| Control | Variant | Position | Action | Guard |
|---|---|---|---|---|
| Primary CTA | Button.primary | … | `store.<setter>` then `navigate("<screen>")` | `<machine>State === "<value>"` |
| Secondary | Button.secondary | … | … | … |
| Slider × N | track+handle | … | `store.setLotPct(id, t*100)` | `benchState === "AIMING"` |
## 9. Build sequence
1. `bg` rect — `palette.linen`
2. `staticLayer` add to root
3. `dynamicLayer` add to root
4. `buildStatic()` — title, captions, card chrome, region headers, rules
5. `buildDynamic()` — text placeholders, rows, plaques, buttons (state-bound)
6. `void loadKit()` — async: frame + figures + generated backdrops
7. `unsubscribe = store.subscribe(refresh)`
8. `refresh()` — initial state apply
## 10. State bindings (summary)
- Reads: …
- Writes: …
- Guards: …
## 11. Accessibility
| Element | accessibleTitle | accessibleHint | tabIndex |
|---|---|---|---|
| … | … | … | … |
## 12. Smoke test
```ts
test("<Screen>: renders <key element>", async ({ page }) => {
await page.setViewportSize({ width: 1344, height: 768 });
await page.goto("/");
await waitForHook(page);
await page.evaluate(() => {
window.__APP_STORE__.<setup>();
window.__APP_STORE__.navigate("<screen>");
});
// Wait for a screen-specific anchor element instead of a fixed
// timeout — keeps the test deterministic and avoids the flake
// class created by hardcoded waits. Pick a Pixi `accessibleTitle`,
// a DOM marker, or a screen-state assertion that uniquely
// identifies the screen having mounted.
await page.waitForSelector("<UNIQUE_SCREEN_SELECTOR>");
expect(
await page.evaluate(() => window.__APP_STORE__.getState().currentScreen),
).toBe("<screen>");
await page.screenshot({ path: "test-results/<NN>-<screen>.png" });
});
```
## 13. File deliverables
- New: `app/src/screens/<screen>.ts`
- Edit: `app/src/screens/router.ts` (add import + case)
- Edit: `app/tests/e2e/smoke.spec.ts` (add the test above)
- (If generating) New: `docs/redesign/art-kit/plates/backdrops/<screen>_bg/*` + `app/public/kit/<screen>_bg.svg`
- (If new pose) New: `docs/redesign/art-kit/poses/*` + `app/public/kit/poses/*.svg`
## 14. Definition of done
- `npx tsc --noEmit` clean
- `npx playwright test --reporter=list` — all green
- `screen-comparison <screen>` returns **A- or better** (use this for self-check before claiming done — A- is the production-ready gate defined in the screen-comparison rubric)
- Committed in the style your repo uses (conventional commits / your team's prefix scheme / etc.)
- Published per your repo's branching + PR workflow (direct push, feature-branch PR, trunk-based, etc.) — substitute the project's actual flow here
app/public/kit/ and app/public/kit/poses/ first. Generate (cost $0.04 / image) only when the plate's scene cannot be assembled from existing pieces and matters for atmosphere (Grape Market village, Char Select dusk vineyard, future Hub estate).S0, S1, CARD are organisational identifiers. They carry no width or height information on their own. Every constant in the layout spec must be traceable back to a measured % from the spatial skeleton. If a value can't be grounded in a measurement, flag it explicitly as an estimate.app/src/screens/<new-screen>.ts.developer-task-executor) for a port — the decomposition becomes the brief verbatim.The output of this skill is the brief and the acceptance criteria. Pair with screen-comparison post-build: decomposition forward, comparison backward. Together they close the visual-fidelity loop without leaving room for impressionistic shortcuts.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub pilotso11/pilotso11-skills