figura-cli 0.24.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +249 -250
- package/dist/mcp.js +53 -53
- package/package.json +1 -1
- package/skills/fig/SKILL.md +255 -95
- package/skills/figura-brand/SKILL.md +49 -4
- package/skills/figura-brand/reference/brand-format.md +32 -7
- package/skills/skills.json +5 -5
package/package.json
CHANGED
package/skills/fig/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: fig
|
|
3
3
|
description: "Mock up a screen as an on-brand, shareable HTML fig via your Figura team — render a UI state in seconds before writing the real code. Use when asked to visualize a screen, mock a UI, or 'make a fig'. iPhone-framed by default; browser frame for web/desktop."
|
|
4
4
|
argument-hint: '<what to visualize>'
|
|
5
|
-
allowed-tools: 'mcp__figura__figura_whoami, mcp__figura__figura_fig_scaffold, mcp__figura__figura_fig_devices, mcp__figura__figura_list_figs, mcp__figura__figura_generate_fig, mcp__figura__figura_compose_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, mcp__figura__figura_request_feedback, Bash(figura *), Bash(bunx figura-cli *)'
|
|
5
|
+
allowed-tools: 'mcp__figura__figura_whoami, mcp__figura__figura_fig_scaffold, mcp__figura__figura_fig_devices, mcp__figura__figura_list_figs, mcp__figura__figura_generate_fig, mcp__figura__figura_compose_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_upload_asset, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, mcp__figura__figura_merge_figs, mcp__figura__figura_request_feedback, Bash(figura *), Bash(bunx figura-cli *)'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# /fig — figure it out before you build it
|
|
@@ -19,6 +19,28 @@ place (your team's Figura brand profile) and is injected server-side. That is th
|
|
|
19
19
|
whole point: a fig is on-brand *by construction*, and this skill can never drift
|
|
20
20
|
from your brand. To change the brand, use the `figura-brand` skill, not this one.
|
|
21
21
|
|
|
22
|
+
## Hard rules — read before anything else
|
|
23
|
+
|
|
24
|
+
1. **When you author the markup, a phone fig ⇒ `figura_compose_fig`. Always.
|
|
25
|
+
No exceptions.** The phone is the only frame the composer builds, and every
|
|
26
|
+
framed phone screen goes through it. **Web/desktop (browser frame) ⇒
|
|
27
|
+
`figura_create_fig`**, following the scaffold's `--platform web` frame
|
|
28
|
+
contract verbatim — markup only, the scaffold owns the CSS.
|
|
29
|
+
**Tablet has no authoring path** (#869): say so and stop.
|
|
30
|
+
(`figura_generate_fig` authors nothing locally — step 3 governs it.)
|
|
31
|
+
2. **The frame is Figura's, never yours.** Bezel, notch/Dynamic Island, status
|
|
32
|
+
bar, home bar, safe insets, artboard geometry and scale are server-owned.
|
|
33
|
+
You author screen CONTENT only.
|
|
34
|
+
3. **`figura_create_fig` is for web/`document`/exotic figs only.** Never for a
|
|
35
|
+
phone fig — this is a rule, not a preference. No `compose` on this engine? Say
|
|
36
|
+
so and stop; the caller upgrades. A hand-framed phone is not the fallback.
|
|
37
|
+
4. **Self-check before you land a phone fig.** Search your own markup for these;
|
|
38
|
+
any hit means you drew the frame — discard it and rebuild via
|
|
39
|
+
`figura_compose_fig`:
|
|
40
|
+
`.fig-device` wrapper · `--fig-safe-*` · notch / Dynamic Island / bezel /
|
|
41
|
+
home-bar elements · a status-bar row · a phone-shaped `border-radius`
|
|
42
|
+
container · a hand-written device width/height.
|
|
43
|
+
|
|
22
44
|
## When to use
|
|
23
45
|
|
|
24
46
|
- Asked to mock a screen, visualize a UI state, or "make a fig".
|
|
@@ -37,8 +59,22 @@ something in a vacuum — and end up rebuilding what already ships. So every tim
|
|
|
37
59
|
matching route). If a real implementation exists, **Read it** and treat it as the
|
|
38
60
|
baseline you must show.
|
|
39
61
|
- **Check the team's figs** — `figura fig list --area <area>` (or
|
|
40
|
-
`figura_list_figs`). If a fig of this screen already exists,
|
|
41
|
-
(
|
|
62
|
+
`figura_list_figs`). If a fig of this screen already exists, **iterate that
|
|
63
|
+
fig** (step 5), never a fresh duplicate.
|
|
64
|
+
|
|
65
|
+
★ **The chain is the unit — and Figura now enforces it.** A create that looks
|
|
66
|
+
like the next round of an open question is **refused** (409 `similar_chain`) and
|
|
67
|
+
the response names the chain to revise. That is not an obstacle to route around:
|
|
68
|
+
each round you land as its own fig is a card in the library nobody can walk, and
|
|
69
|
+
a reviewer who opens round 4 sees neither rounds 1–3 nor the discussion held on
|
|
70
|
+
them. Revise the named fig. `new: true` (CLI `--new`) exists for a genuinely
|
|
71
|
+
different question — not for "revising felt like more work".
|
|
72
|
+
|
|
73
|
+
**Already forked it?** `figura_merge_figs` (CLI `figura fig merge <id> --into
|
|
74
|
+
<id>`) folds the sibling back in: its versions append after the destination's
|
|
75
|
+
head, share links keep resolving, verdicts and decisions follow. Run it with
|
|
76
|
+
`dry_run` first to see the plan. Merging your own fork mid-session is normal
|
|
77
|
+
housekeeping, not an admission of anything.
|
|
42
78
|
|
|
43
79
|
If any prior design exists → build in **`mode: compare`** and render it as
|
|
44
80
|
**existing (as-is) → proposed**, grounded in the real current state. Even when the
|
|
@@ -105,7 +141,7 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
|
|
|
105
141
|
to the user verbatim (a one-line "figura-mcp is behind / below min — run …"),
|
|
106
142
|
then proceed. It's a passive nudge, shown once per session — never a blocker.
|
|
107
143
|
|
|
108
|
-
2. **Default — write the CONTENT, let Figura own the STRUCTURE.** Build
|
|
144
|
+
2. **Default — write the CONTENT, let Figura own the STRUCTURE.** Build phone
|
|
109
145
|
figs with **`figura_compose_fig`**: you author only each **screen's CONTENT**
|
|
110
146
|
(the markup *inside* the phone); Figura owns the parts that used to drift — the
|
|
111
147
|
iPhone **frame** (bezel + Dynamic Island/notch + home bar), the **safe zone**,
|
|
@@ -130,18 +166,35 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
|
|
|
130
166
|
detail in; never for a redesign argument), or `document` (NO device
|
|
131
167
|
framing — brand/overview figs).
|
|
132
168
|
- **`screens`** — an ARRAY. Each screen's **`html` is CONTENT ONLY**: the
|
|
133
|
-
markup that goes *inside* the phone
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
intent Figura needs — **`device`** (a real catalog id from
|
|
169
|
+
markup that goes *inside* the phone (hard rule 2). For every device layout
|
|
170
|
+
each screen also carries the typed intent Figura needs — **`device`** (a
|
|
171
|
+
real catalog id from
|
|
137
172
|
`figura_fig_devices`, e.g. `iphone-16`), **`orientation`**
|
|
138
173
|
(`portrait`/`landscape`), and **`safeZone`** (`respect` = Figura pads your
|
|
139
174
|
content to the device's real safe insets; `ignore` = edge-to-edge for a
|
|
140
175
|
splash/hero). (`document` screens need none of these.)
|
|
176
|
+
- **`childFig`** (instead of that screen's `html`) — compose the frame BY
|
|
177
|
+
REFERENCE: the id of an existing fig composed with `kind: 'child'`, which
|
|
178
|
+
Figura embeds in its own iframe. `single` and `compare` only. Reach for it
|
|
179
|
+
when two screens fight over the same CSS (each child's styles are sealed in
|
|
180
|
+
its own document and cannot reach the frame or the other screen), or when a
|
|
181
|
+
screen deserves its own versions, reactions and comments. Compose each child
|
|
182
|
+
first, then pass their ids, and give the screen the SAME `device` and
|
|
183
|
+
`orientation` the child was composed with — the frame draws that phone's
|
|
184
|
+
chrome around the child's own safe-area guides, so a mismatch is refused.
|
|
185
|
+
The reference is PINNED: a later revision of a child does not change a
|
|
186
|
+
composition that already exists. A composed-by-reference parent is revised
|
|
187
|
+
by **typed screens, never by HTML** — see step 5. Publishing the parent is
|
|
188
|
+
what makes its children reachable at the public link; a child fig has no
|
|
189
|
+
share link of its own. A referenced screen still takes `notes` on `compare`
|
|
190
|
+
— the rail and its pins are the parent's markup, so they never reach into
|
|
191
|
+
the child.
|
|
141
192
|
- **`sharedCss`** — ONE stylesheet every screen shares: put your brand tokens
|
|
142
193
|
(from the scaffold) and shared content classes here. Per-screen one-offs go
|
|
143
194
|
in that screen's `css`.
|
|
144
|
-
- Plus `area`, `title`, `orientation`, and a real `description
|
|
195
|
+
- Plus `area`, `title`, `orientation`, and a real `description` — the fig's
|
|
196
|
+
BRIEF, and the reviewer's only view of the thread. Contract: "The brief"
|
|
197
|
+
below.
|
|
145
198
|
|
|
146
199
|
**Small screens are the SERVER's job — with one exception that is yours.**
|
|
147
200
|
Below a catalog-derived breakpoint every multi-frame layout (`compare`,
|
|
@@ -162,11 +215,11 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
|
|
|
162
215
|
brand with `bunx figura-cli brand --json` and author your content styles
|
|
163
216
|
against that.
|
|
164
217
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
218
|
+
**The other path — `figura_create_fig`** (hard rule 3): a full `html` document
|
|
219
|
+
you hand-author, for a web/`document`/exotic fig. Fetch the scaffold for the
|
|
220
|
+
platform (`--platform web` for a browser frame) and follow its frame contract
|
|
221
|
+
verbatim; the chrome CSS is stamped at save, so author the markup and none of
|
|
222
|
+
its styles.
|
|
170
223
|
|
|
171
224
|
**Landing IS hosting — leave `publish` and `commit` unset.** Composing a fig
|
|
172
225
|
always persists it to Figura (there is no separate "save it for real" step);
|
|
@@ -220,19 +273,31 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
|
|
|
220
273
|
only the served bytes can tell a working fig from one that renders as empty
|
|
221
274
|
phones. It exits non-zero if a frame class shipped unsized — fix and re-land
|
|
222
275
|
before a human sees it. Then run
|
|
223
|
-
`figura fig open <id>` (the id
|
|
224
|
-
|
|
276
|
+
`figura fig open <id>` (the id the landing call just returned). It opens
|
|
277
|
+
**`viewUrl`** in the reviewer's browser — the actual
|
|
225
278
|
Figura render (brand chrome, CSP, favicon, responsive frame), team-scoped so
|
|
226
279
|
any signed-in teammate can open it even though nothing was published. That's
|
|
227
280
|
what gets approved, and it's the exact page that ships once published — not a
|
|
228
281
|
`file://` reconstruction that can drift from it. No display reachable (a
|
|
229
282
|
headless/CI session)? Paste the printed `viewUrl` into the conversation
|
|
230
|
-
instead
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
5. **Iterate against the SAME fig, not a fresh one.**
|
|
234
|
-
through **`figura_revise_fig`** (`id`, the updated `html`) — it
|
|
235
|
-
existing chain as a new version and returns a fresh `viewUrl`.
|
|
283
|
+
instead — there is no local-preview fallback; a fig is only viewed on
|
|
284
|
+
figura.so.
|
|
285
|
+
|
|
286
|
+
5. **Iterate against the SAME fig, not a fresh one.** On the create path, changes
|
|
287
|
+
from review go through **`figura_revise_fig`** (`id`, the updated `html`) — it
|
|
288
|
+
joins the existing chain as a new version and returns a fresh `viewUrl`.
|
|
289
|
+
**A COMPOSED fig revises by typed screens**: call `figura_revise_fig` with
|
|
290
|
+
`layout` + `screens` (the same shape compose takes) instead of `html`, and it
|
|
291
|
+
re-composes into the same chain. Every child reference is carried forward with
|
|
292
|
+
its pin, label, notes and position; an empty screen `{}` keeps a REFERENCED
|
|
293
|
+
slot exactly as it was, so changing one side of a by-reference diptych is
|
|
294
|
+
`screens: [{}, {…}]`. A slot that held INLINE `html` must have its html sent
|
|
295
|
+
again — Figura never reads content back out of a composed document. Passing
|
|
296
|
+
`html` for a composed fig is refused (422) — it would leave the references
|
|
297
|
+
behind. `figura_get_fig` returns the fig's `children` when you need to see the
|
|
298
|
+
current composition. Never hand-author the framed document to feed `revise`
|
|
299
|
+
(hard rule 1). A revision takes its own `description` — restate the arc
|
|
300
|
+
there; whoever reads v4 did not read v1. Publishing is
|
|
236
301
|
NOT inherited automatically: a revision of an already-published fig still
|
|
237
302
|
lands unpublished by default (`viewUrl` is a team-scoped view, and the
|
|
238
303
|
existing public link keeps serving the OLD version) — pass `publish: true`
|
|
@@ -255,9 +320,9 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
|
|
|
255
320
|
the team (digest-batched) — so at the end of the session, commit exactly
|
|
256
321
|
the fig the user asked for / the final agreed version, and nothing else.
|
|
257
322
|
Explorations, rejected variants, and intermediate iterations STAY drafts.
|
|
258
|
-
If the deliverable is the fig you're about to
|
|
259
|
-
`commit: true` on that final `
|
|
260
|
-
call
|
|
323
|
+
If the deliverable is the fig you're about to land anyway, prefer
|
|
324
|
+
`commit: true` on that final landing call (CLI: `--commit`) over a separate
|
|
325
|
+
call; otherwise commit it afterwards
|
|
261
326
|
with **`figura_commit_fig`** (CLI: `figura fig commit <id>`). Committed the
|
|
262
327
|
wrong one? `figura_commit_fig` with `uncommit: true` (CLI:
|
|
263
328
|
`figura fig uncommit <id>`) returns it to a draft — the share link is
|
|
@@ -269,6 +334,8 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
|
|
|
269
334
|
**`figura_request_feedback`** (CLI: `figura fig request-feedback <id> --from
|
|
270
335
|
<name|email> --note <text>`). See "Assign a reviewer" below — this step is
|
|
271
336
|
what stops a fig from being landed, committed, and never actually read.
|
|
337
|
+
The note names the decision; the fig's `description` carries the context —
|
|
338
|
+
check it against "The brief" below *before* you send the ask.
|
|
272
339
|
|
|
273
340
|
## Presenting a redesign — FOCUS first, overview second
|
|
274
341
|
|
|
@@ -280,8 +347,8 @@ per-screen content. You supply each state's CONTENT + a `label`; the composition
|
|
|
280
347
|
the focus pills, the scaled overview grid, and the no-horizontal-scroll guarantee
|
|
281
348
|
are server-owned. The design intent below still guides WHAT each state's content
|
|
282
349
|
says and which is recommended — you just no longer hand-write the toggle markup
|
|
283
|
-
or the frame scaling. (
|
|
284
|
-
|
|
350
|
+
or the frame scaling. (On the `create` path — a web fig — the same pattern is
|
|
351
|
+
hand-authored: plain CSS-only radio tabs, binary, no library.)
|
|
285
352
|
|
|
286
353
|
**★ FOCUS IS THE DEFAULT VIEW.** The fig opens on ONE full-size frame with state
|
|
287
354
|
pills — on desktop AND mobile. Overview is the *alternative*, behind the second
|
|
@@ -291,41 +358,102 @@ using it for one shipped a 4-state fig as 134px phones with ~3px annotation
|
|
|
291
358
|
chips. A Focus-only fig is **complete on its own**; add Overview only when there
|
|
292
359
|
are enough states that scanning them at once genuinely helps.
|
|
293
360
|
|
|
361
|
+
**★ THE LAST PILL IS YOUR READ — a fig that shows options must pick one.**
|
|
362
|
+
Whenever a fig carries more than one *direction* (A / B / …, or any "which way
|
|
363
|
+
do we go" set), the FINAL state is a **verdict state**, not one more variation:
|
|
364
|
+
the options weighed against each other, and one of them named as the pick.
|
|
365
|
+
Without it a reviewer gets a gallery and no argument, and the decision lands back
|
|
366
|
+
on whoever opened the link — which is the exact thing a fig exists to prevent.
|
|
367
|
+
|
|
368
|
+
The verdict state is one framed screen like any other, labelled `My read`, and
|
|
369
|
+
it carries, tersely:
|
|
370
|
+
|
|
371
|
+
- **the pick, named in the first line** — "Go with A" — never "both have merit";
|
|
372
|
+
- **one line per option** of what it wins and what it costs (`+` / `–`) — the
|
|
373
|
+
same pairs the Overview chips carry, so the two views can't disagree;
|
|
374
|
+
- **the deciding reason**: the one constraint that broke the tie (a component
|
|
375
|
+
that already ships, a cap, a rule the others violate);
|
|
376
|
+
- **what would change your mind**, when the pick is close.
|
|
377
|
+
|
|
378
|
+
Two states of the SAME direction (a before/after, one layout at two
|
|
379
|
+
orientations) need no verdict — there is nothing to weigh. Three directions and
|
|
380
|
+
no verdict pill is an unfinished fig.
|
|
381
|
+
|
|
294
382
|
- **Focus — full frame (default).** One state at a time behind state pills
|
|
295
383
|
(Today / A / B / …), each state framed identically so flipping states in place
|
|
296
|
-
reads as spot-the-difference. This is where the context lives
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
384
|
+
reads as spot-the-difference. This is where the context lives: what's broken
|
|
385
|
+
today, what each variation improves, what it trades away. Open on the first
|
|
386
|
+
pill = the honest current state.
|
|
387
|
+
- **★ Annotation NEVER goes on the design — pass `notes`, don't draw chips.**
|
|
388
|
+
A note written into a screen's `html` lands inside the phone screen, on top of
|
|
389
|
+
the layout the fig exists to let someone judge. Measured on a shipped fig
|
|
390
|
+
(`fig_wjgcksh38pix`): 3–4 absolutely-positioned chips per state, every one 100%
|
|
391
|
+
inside the screen box, together covering **12–19% of the artboard** — sitting
|
|
392
|
+
on the masonry grid and hero row that were the whole question — while the page
|
|
393
|
+
around the frame was empty black. So annotation is **data, not markup**: give
|
|
394
|
+
each screen a `notes` array on `figura_compose_fig` and Figura renders a rail
|
|
395
|
+
**beside** the frame, with only a ~20px numbered pin on the artboard (~0.3%
|
|
396
|
+
coverage). Nothing to toggle: the layout and the argument are both fully
|
|
397
|
+
visible at every viewport, and below the restructure breakpoint the rail
|
|
398
|
+
stacks under the frame automatically.
|
|
399
|
+
|
|
400
|
+
```jsonc
|
|
401
|
+
"notes": [
|
|
402
|
+
// `at` is percent on the SCREEN → a numbered pin there, note N in the rail.
|
|
403
|
+
{ "kind": "broken", "text": "Hero is score-picked, so it lands mid-row and manufactures a second full-width row (#2755).", "at": { "x": 34, "y": 52 } },
|
|
404
|
+
{ "kind": "improved", "text": "A run IS a collection — a name is always attached.", "at": { "x": 91, "y": 14 } },
|
|
405
|
+
// No `at` → a point about the state as a whole. Numbered, no pin.
|
|
406
|
+
{ "kind": "tradeoff", "text": "Costs the shipped vertical-advance gesture." }
|
|
407
|
+
]
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`kind` is `broken` | `improved` | `tradeoff` | `detail` (default `detail`), max 8
|
|
411
|
+
notes a screen, and `compose` **rejects** notes on `single`/`variations`/
|
|
412
|
+
`document` — those layouts have no rail, and silently dropping a note drops
|
|
413
|
+
the argument. If you're hand-authoring via `figura_create_fig` instead, the
|
|
414
|
+
same rule holds by hand: the rail is a **sibling of the frame**, never a
|
|
415
|
+
descendant of the clipped `.fig-screen`, and nothing but a pin sits on the
|
|
416
|
+
design.
|
|
417
|
+
- **Every direction gets the SAME treatment.** If one direction is shown in a
|
|
418
|
+
second orientation, a second state, or a second breakpoint, they ALL are.
|
|
419
|
+
A subset argues from unequal evidence and quietly favours whichever direction
|
|
420
|
+
got the extra look: one fig shipped `Today ↻` and `A ↻` pills while B and
|
|
421
|
+
Fresh had no landscape frame at all, so two directions were judged turned and
|
|
422
|
+
two were judged only upright. **Cover the orientations the product actually
|
|
423
|
+
ships** — on an app that is landscape-native, that means portrait AND
|
|
424
|
+
landscape for every direction, by default, not as an extra. Narrow to one
|
|
425
|
+
orientation only when the work itself is scoped to one: a portrait-only bug,
|
|
426
|
+
or a conversation already about the turn, doesn't need its opposite.
|
|
427
|
+
- **A pill label is plain TEXT.** `label` is a text field, not markup — write
|
|
428
|
+
the character (`·`, `↻`, `—`), never an HTML entity. The same call carries
|
|
429
|
+
each screen's `html`, where `·` is exactly right, so the habit crosses
|
|
430
|
+
over easily; the composer decodes an entity it finds in a label rather than
|
|
431
|
+
shipping it literally, but the label still means text.
|
|
303
432
|
- **Overview — side by side (alternative).** Every state at once: the honest
|
|
304
433
|
current state first, then each variation, as scaled-down frames in a grid.
|
|
305
434
|
Under each: a status chip and ONE condensed context line (`+ / –`). This view
|
|
306
435
|
is for scanning and picking a direction, not for reading detail. The grid must
|
|
307
436
|
wrap to one column before it can overflow — **a fig never scrolls
|
|
308
|
-
horizontally**, at any viewport.
|
|
309
|
-
kit's **`.fig-mini`** wrapper (see "Scaling a framed device" below), never a
|
|
310
|
-
rule on `.fig-device` and never CSS `zoom` (zoom leaks an unzoomed
|
|
311
|
-
`scrollWidth` into horizontal overflow — the exact thing this view forbids).
|
|
437
|
+
horizontally**, at any viewport. Frame scaling is the server's (hard rule 2).
|
|
312
438
|
- **In both views:** render the current state honestly from the shipped
|
|
313
439
|
implementation — **reconstructed from source** per "Reconstruct the baseline
|
|
314
440
|
from SOURCE" (no screenshot ⇒ study the component file(s) and rebuild the real
|
|
315
|
-
layout, don't approximate);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
441
|
+
layout, don't approximate); classify every note by function via its `kind`
|
|
442
|
+
(`broken` / `improved` / `tradeoff` / `detail`) rather
|
|
443
|
+
than hand-picking colors — the rail and its pins take the team's status colors
|
|
444
|
+
from the design system, and a `sharedCss` override of `--fig-note-improved` and
|
|
445
|
+
friends re-points all of them at once; and mark the recommended direction in
|
|
446
|
+
both — the pick the verdict state argues for wears its chip here too. Keep the
|
|
447
|
+
chrome around the frames minimal — the frames are the content; no legends
|
|
448
|
+
(notes label themselves), no repeated context, fluid `max-width` throughout.
|
|
321
449
|
- **Verify the SERVED fig, not your local draft.** The component kit (device
|
|
322
450
|
frame, status bar, control bar) is stamped **server-side at save** — it does
|
|
323
451
|
**not** exist in the HTML you authored locally, so a headless render of your
|
|
324
452
|
own file shows *unstyled, empty* frames whether or not the fig is actually
|
|
325
453
|
correct. A local screenshot is structurally blind to the most common way a
|
|
326
454
|
framed fig breaks. So verify the fig that actually ships:
|
|
327
|
-
1. **Land it first
|
|
328
|
-
the stamped bytes: `figura fig open <id> --
|
|
455
|
+
1. **Land it first**, then dump
|
|
456
|
+
the stamped bytes: `figura fig open <id> --print`. This is the
|
|
329
457
|
served document, kit and all.
|
|
330
458
|
2. **Assert the kit stamped — `figura fig verify <id>`.** One command, and it
|
|
331
459
|
**exits non-zero** if any frame class your markup uses shipped with no CSS
|
|
@@ -341,15 +469,26 @@ are enough states that scanning them at once genuinely helps.
|
|
|
341
469
|
repo's `.figura/figs/` (gitignored), or `~/.figura/figs/` outside a bound
|
|
342
470
|
repo, never `/tmp`/`mktemp`), and render it at a few viewport widths.
|
|
343
471
|
`document.scrollWidth` must equal the viewport in every view and state
|
|
344
|
-
(sideways scroll = broken),
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
the
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
472
|
+
(sideways scroll = broken), and **measure how much of the artboard the
|
|
473
|
+
annotation covers**: sum the intersection of every annotation element with
|
|
474
|
+
the `.fig-screen` box and divide by its area. With `notes` that number is
|
|
475
|
+
the pins alone (~0.3%); anything in the double digits means chips got drawn
|
|
476
|
+
into the design again. Also assert nothing is cut by a clipping ancestor:
|
|
477
|
+
walk each element's ancestors up to `<html>` and, wherever computed
|
|
478
|
+
`overflow` is `hidden`/`clip`, assert its rect stays inside that ancestor's
|
|
479
|
+
box — the viewport box for `<body>`/`<html>`, whose overflow propagates
|
|
480
|
+
there. Comparing to the frame's rect alone misses the vertical escape, and
|
|
481
|
+
no rect test sees a rounded clip, which is why keeping the rail a sibling
|
|
482
|
+
of the frame is the real guarantee. `figura fig open <id> --print` is where
|
|
483
|
+
those served bytes come from.
|
|
484
|
+
|
|
485
|
+
**A fig composed BY REFERENCE has no printable bytes.** Its screens are served
|
|
486
|
+
per request, so `--print` refuses (non-zero) and names the hosted URL: the
|
|
487
|
+
stored document is references, not a render. Verify it by opening that URL —
|
|
488
|
+
`figura fig open <id>` — and screenshotting the hosted page. Steps 1 and 3
|
|
489
|
+
above assume printable bytes and do not apply; `figura fig verify <id>` still
|
|
490
|
+
reports on the parent's own markup. Its `notes` rail and pins are the
|
|
491
|
+
PARENT's markup, so they render and measure the same way.
|
|
353
492
|
|
|
354
493
|
A **wipe-slider** stays the narrow tool for exactly two states of the same
|
|
355
494
|
layout with pixel-level tweaks. Never ship a squeezed side-by-side as the
|
|
@@ -395,6 +534,54 @@ Example — a GSAP-driven entrance for a mobile screen, pinned + allowlisted:
|
|
|
395
534
|
</script>
|
|
396
535
|
```
|
|
397
536
|
|
|
537
|
+
## The brief — what the reviewer reads before your fig
|
|
538
|
+
|
|
539
|
+
**`description` is the fig's BRIEF, not a caption.** Figura renders it as the
|
|
540
|
+
**Brief** panel beside the fig, above the discussion: it is the first thing the
|
|
541
|
+
person you assign in step 8 reads, and across a multi-round exercise it is the
|
|
542
|
+
only place the *thread* survives — each round lands as its own fig, and nothing
|
|
543
|
+
in the product links them. The fig argues THIS round. The brief carries what
|
|
544
|
+
happened before it.
|
|
545
|
+
|
|
546
|
+
★ **Four beats, in this order.**
|
|
547
|
+
|
|
548
|
+
1. **The objective, in the user's own words.** Quote the sentence that started
|
|
549
|
+
the exercise, verbatim — and re-quote it in every later round. It is the
|
|
550
|
+
standard the fig gets judged against, and the first thing to evaporate when a
|
|
551
|
+
round is written from memory.
|
|
552
|
+
2. **The arc — one line per prior round: what it tried, and what it settled or
|
|
553
|
+
killed.** Name each round so the reviewer can go and find it.
|
|
554
|
+
3. **What is still open** — the question this round does *not* close.
|
|
555
|
+
4. **The pick, and the decision you want back.**
|
|
556
|
+
|
|
557
|
+
★ **An abandoned direction stays in the brief, with the reason it was
|
|
558
|
+
abandoned.** "Withdrawn", "superseded", "we tried that" are deletions, not
|
|
559
|
+
summaries. A dead route is the most useful thing a reviewer can be handed: it
|
|
560
|
+
stops them re-proposing it, and it is usually where the unresolved tension is
|
|
561
|
+
still sitting. And when a round was killed by *evidence* — a real user rejected
|
|
562
|
+
it, a constraint bit, a measurement came back — that evidence outranks every
|
|
563
|
+
design argument in the brief, and has to survive into the next round's.
|
|
564
|
+
|
|
565
|
+
★ **Never write a later round's brief from session memory.** Before composing a
|
|
566
|
+
fig that continues, supersedes, or replaces an earlier one, read the earlier
|
|
567
|
+
briefs — `figura_list_revisions` for the chain you're revising, or
|
|
568
|
+
`figura_list_figs` (by `area`/`tag`) then `figura_get_fig` — and carry the
|
|
569
|
+
objective and the arc forward. A long session compresses; the brief must
|
|
570
|
+
not. The prior fig is on the server, so this costs one call and never depends on
|
|
571
|
+
what is still in your context.
|
|
572
|
+
|
|
573
|
+
**Which field is which.** `description` is the brief, on every write path
|
|
574
|
+
(`figura_compose_fig` / `figura_create_fig` / `figura_revise_fig` /
|
|
575
|
+
`figura_update_fig`), and it is carried per version — so **a revision restates
|
|
576
|
+
the arc** instead of assuming the reader saw v1. Cap: 2000 characters. `prompt`
|
|
577
|
+
is a different field: the raw brief/instruction you were handed, kept for
|
|
578
|
+
provenance. `intent` (the `figura:context` meta) is a one-line design intent for
|
|
579
|
+
search and is **truncated at 400 characters** — never park the objective there.
|
|
580
|
+
|
|
581
|
+
**The brief is not the fig.** Don't narrate what the frames already show. If the
|
|
582
|
+
brief and the fig disagree about the recommendation, one of them is stale — fix
|
|
583
|
+
it before you send the ask.
|
|
584
|
+
|
|
398
585
|
## Assign a reviewer — a proposal fig isn't done until someone's on it
|
|
399
586
|
|
|
400
587
|
**A link nobody was pointed at is a fig nobody saw.** Landing, publishing, and
|
|
@@ -422,7 +609,7 @@ private.
|
|
|
422
609
|
|
|
423
610
|
## Metadata & geometry — stamped for you
|
|
424
611
|
|
|
425
|
-
Every landed fig is **auto-stamped server-side** (
|
|
612
|
+
Every landed fig is **auto-stamped server-side** (`compose`, `create`, `generate`)
|
|
426
613
|
with two things you do **not** hand-write:
|
|
427
614
|
|
|
428
615
|
- A **device kit** `<style>` — the target's hard geometry as CSS custom
|
|
@@ -435,53 +622,26 @@ with two things you do **not** hand-write:
|
|
|
435
622
|
search and the self-improving loop. Artifact-descriptive only — no team/brand
|
|
436
623
|
ids (the block is served on the public link).
|
|
437
624
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
presentation model (frameless single vs canvas composition), when to emit or
|
|
444
|
-
omit the whole-fig `<meta name="figura:artboard">`, the `.fig-device` /
|
|
445
|
-
`data-device` frame markup (Figura stamps the bezel, Dynamic Island, and
|
|
446
|
-
safe-area insets **before** your `<style>`, so you normally leave those classes
|
|
447
|
-
alone — if you do style one, your rule wins by cascade and the frame still
|
|
448
|
-
works; to shrink a frame use the `.fig-mini` wrapper, never a hand-written
|
|
449
|
-
`.fig-device` width/height/transform), the `figura:context` / `figura:feature`
|
|
450
|
-
metas, and the valid devices all live in the canonical scaffold.
|
|
451
|
-
|
|
452
|
-
**Scaling a framed device — use `.fig-mini`, never a rule on `.fig-device`.**
|
|
453
|
-
An overview grid needs shrunken frames, and the obvious way to write that is a
|
|
454
|
-
CSS rule on the injected class. Don't: your rule wins over the kit, so the frame
|
|
455
|
-
stops matching real device geometry — and `zoom` is worse, it leaks the unzoomed
|
|
456
|
-
width into scrollable overflow. The kit ships the wrapper instead:
|
|
457
|
-
|
|
458
|
-
```html
|
|
459
|
-
<div class="fig-mini" style="--fig-scale:.34">
|
|
460
|
-
<div class="fig-device portrait" data-device="iphone-16">
|
|
461
|
-
<div class="fig-screen">…</div><div class="fig-di"></div><div class="fig-homebar"></div>
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
`.fig-mini` scales its device with `transform` and takes the **scaled** layout
|
|
467
|
-
box, so a row of minis lays out at its visual size instead of reserving
|
|
468
|
-
full-size phones. Add `landscape` to the device (or the wrapper) and the box
|
|
469
|
-
flips with it. `--fig-scale` defaults to `.34`; set it per element.
|
|
625
|
+
The rest of this section applies ONLY to the **`figura_create_fig`** path (hard
|
|
626
|
+
rule 3), where you hand-author the whole document: the exact rules are
|
|
627
|
+
**fetched, not restated here** — the presentation model, the browser frame
|
|
628
|
+
contract, the `figura:artboard` / `figura:context` / `figura:feature` metas all
|
|
629
|
+
live in the canonical scaffold.
|
|
470
630
|
|
|
471
631
|
- **`figura_fig_scaffold`** (CLI: `figura fig scaffold`) is authoritative for the
|
|
472
632
|
presentation model, the frame markup, and the output contract. Fetch it and
|
|
473
633
|
follow it verbatim (step 2 above) — do NOT restate design/format rules from
|
|
474
634
|
memory. A bundled skill can pin stale; the scaffold is always current.
|
|
475
|
-
- **`figura_fig_devices`** (CLI: `figura fig devices`) is the
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
composition's `<div class="fig-device" data-device="…">`; an unknown id
|
|
479
|
-
silently gets no frame.
|
|
635
|
+
- **`figura_fig_devices`** (CLI: `figura fig devices`) is the catalog `compose`
|
|
636
|
+
validates against — pick each screen's **`device`** id from it (an unknown id
|
|
637
|
+
is a 400).
|
|
480
638
|
|
|
481
639
|
## Notes
|
|
482
640
|
|
|
483
|
-
- **MCP unavailable** (headless / cron, or no interactive auth):
|
|
484
|
-
|
|
641
|
+
- **MCP unavailable** (headless / cron, or no interactive auth): the CLI has no
|
|
642
|
+
`compose`, so a phone fig has no CLI path — say so and stop (hard rule 3). For
|
|
643
|
+
a web/`document` fig fall back to
|
|
644
|
+
`bunx figura-cli fig create` / `fig publish`, with `fig revise` /
|
|
485
645
|
`fig revisions` for versions, `fig commit` / `fig uncommit` (or `--commit` on
|
|
486
646
|
the final create/revise) for the team library, `bunx figura-cli fig scaffold`
|
|
487
647
|
for the authoring scaffold, and `bunx figura-cli brand --json` for the raw
|
|
@@ -71,10 +71,47 @@ Hunt across whatever stack this repo uses — do not assume one framework:
|
|
|
71
71
|
`src/routes/*`, `src/screens/*`). These become each platform's `surfaces[]` —
|
|
72
72
|
THIS product's real areas ("Feed", "Checkout"), never a generic placeholder
|
|
73
73
|
list and never another product's.
|
|
74
|
+
- **Locales** — which languages the product ships, their script and direction.
|
|
75
|
+
Run `figura brand detect-locales [path]`: an offline scan returning
|
|
76
|
+
`locales` (paste-ready `identity.locales`), `evidence` (why each locale got its
|
|
77
|
+
script/direction), `needsConfirmation`, and the `signals` it fired on. Put the
|
|
78
|
+
DEFAULT language first.
|
|
79
|
+
|
|
80
|
+
Three rules, each from a real miss on a real repo (#998):
|
|
81
|
+
|
|
82
|
+
- **NEVER list a vendor directory's languages.** `node_modules`, `Pods`,
|
|
83
|
+
`dist`, `build`, `.next`, `out`, `vendor`, `DerivedData`, `.git`, `.claude`
|
|
84
|
+
are not this product. A React Native monorepo's `node_modules` alone carries
|
|
85
|
+
40+ `*.lproj` languages; listing them makes the brand claim languages the
|
|
86
|
+
product does not ship. `detect-locales` drops them itself — if you scan by
|
|
87
|
+
hand instead, exclude every one of those path segments.
|
|
88
|
+
- **Script and direction come from the message TEXT, not the tag.** A repo can
|
|
89
|
+
tag Sorani Kurdish `ku`, which any tag table calls Latin/LTR — SHA7N's
|
|
90
|
+
`locale/ku/general.json` is 4848 Arabic codepoints. Read the locale's message
|
|
91
|
+
files and judge from the characters. Arabic script plus any of ڕ ڵ ێ ۆ is
|
|
92
|
+
Sorani → emit the tag **`ckb`** (that is the tag the per-script font
|
|
93
|
+
resolver's Sorani coverage check keys on; `ku` would let a font missing those
|
|
94
|
+
letters through), not `ar` and not `ku`.
|
|
95
|
+
- **Ambiguous tags are a question, not a guess.** `ku ks pa sd az uz tk ha ber
|
|
96
|
+
mn` are each written in more than one script depending on region or
|
|
97
|
+
orthography. With message content, classify from it. Without, they arrive in
|
|
98
|
+
`needsConfirmation` — ASK the user which script that language uses in this
|
|
99
|
+
product, and do not paste the locale until they answer.
|
|
100
|
+
|
|
101
|
+
Show the user the `evidence` line for each locale alongside the JSON, so a
|
|
102
|
+
wrong call is visible rather than silent.
|
|
103
|
+
|
|
104
|
+
Manual signals if the CLI is unavailable: i18n deps in `package.json`
|
|
105
|
+
(`next-intl`, `react-i18next`, `i18next`, `@lingui/*`, `vue-i18n`), an i18n
|
|
106
|
+
config's `locales: [...]` array, `locale/*` / `locales/*.json` /
|
|
107
|
+
`messages/*.json` dirs, `dir="rtl"` in markup, iOS `*.lproj/` dirs +
|
|
108
|
+
`Localizable.strings`, Android `values-ar/strings.xml`.
|
|
74
109
|
- **Which platform(s)** — detect the target platform(s) so you only emit the
|
|
75
110
|
surfaces that apply:
|
|
76
111
|
- `mobile` — an iOS/Android/React-Native/Expo app (`*.xcodeproj`, `Info.plist`,
|
|
77
112
|
`android/`, `app.json`/Expo, SF Symbols usage, safe-area/tab-bar tokens).
|
|
113
|
+
Note WHICH OS it targets — an `android/` + Gradle + Material repo is
|
|
114
|
+
`os: "android"`, an `*.xcodeproj`/SwiftUI repo is `os: "ios"`.
|
|
78
115
|
- `web` — a web app / site (`index.html`, a web router, CSS media queries,
|
|
79
116
|
hover states, a `tailwind.config`).
|
|
80
117
|
- `desktop` — an Electron/Tauri/native-desktop shell.
|
|
@@ -93,14 +130,22 @@ EXACTLY (strict keys, hex 3/4/6/8 digits, no blank array entries, only
|
|
|
93
130
|
|
|
94
131
|
- **name** — the product/brand name (still required on the write).
|
|
95
132
|
- **identity** — the SHARED brand: `colors` (real palette, each
|
|
96
|
-
`{ name, hex, role?, meaning? }`), `gradient` (if any), `
|
|
133
|
+
`{ name, hex, role?, meaning? }`), `gradient` (if any), `signature`
|
|
134
|
+
(`{ kind: solid|gradient|iridescent, stops?: hex[] }` — solid unless the repo's
|
|
135
|
+
tokens define a gradient identity; see `reference/brand-format.md`), `type`
|
|
97
136
|
(`{ families, ramp, rules }`), `spacing`, `radius`, `voice`
|
|
98
|
-
(`{ nouns, promise, lines, tone }`), `
|
|
99
|
-
`
|
|
137
|
+
(`{ nouns, promise, lines, tone }`), `locales` (each
|
|
138
|
+
`{ tag, script, direction, fonts? }` — BCP-47 tag, ISO 15924 script,
|
|
139
|
+
`ltr`/`rtl`; FIRST entry is the default; per-locale `fonts` override the
|
|
140
|
+
shared families for that script, e.g. an Arabic display face), `forbidden`
|
|
141
|
+
(hard guardrails as `[{ id?, rule }]`).
|
|
100
142
|
- **platforms** — a map with a key ONLY for each platform you detected in step 1.
|
|
101
143
|
Each surface: `components` (signature components as `[{ name, spec }]`), `icons`
|
|
102
144
|
(the platform's icon language), `layout` (its frame/shell law), `surfaces` (this
|
|
103
|
-
product's real areas), `bans` (platform-specific don'ts).
|
|
145
|
+
product's real areas), `bans` (platform-specific don'ts). `mobile` ALSO takes
|
|
146
|
+
`os` (`"ios"` | `"android"`) — set it when the repo names its OS; it drives the
|
|
147
|
+
fig's device frame, system-bar heights, back affordance, and corner radii.
|
|
148
|
+
Omit it for an iOS/cross-platform repo (iOS is the default). Use the per-platform
|
|
104
149
|
**`tokensBlock`** escape-hatch ONLY for guidance that genuinely doesn't fit a
|
|
105
150
|
structured field — prefer a structured field.
|
|
106
151
|
|