figura-cli 0.16.1 → 0.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figura-cli",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "Command-line client for the Figura visualization SaaS",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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_generate_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, Bash(figura *), Bash(bunx figura-cli *)'
5
+ allowed-tools: 'mcp__figura__figura_whoami, mcp__figura__figura_fig_scaffold, mcp__figura__figura_generate_fig, mcp__figura__figura_create_fig, mcp__figura__figura_revise_fig, mcp__figura__figura_publish_fig, mcp__figura__figura_commit_fig, Bash(figura *), Bash(bunx figura-cli *)'
6
6
  ---
7
7
 
8
8
  # /fig — figure it out before you build it
@@ -102,16 +102,26 @@ so you hand it the studied structure in the brief (see step 3, "Server render").
102
102
  re-verify. If the response has no `binding` field (older CLI/MCP), confirm
103
103
  the team slug matches this project with the user before proceeding.
104
104
 
105
- 2. **Default — build it yourself, with YOUR Claude.** Fetch the team's design
106
- system `bunx figura-cli brand --json` and hand-build the fig's HTML against
107
- it, then land it with **`figura_create_fig`**, setting
108
- `area`, `orientation` (`portrait` / `landscape` / `both`), `mode` (`single` /
109
- `compare` / `variations` — default to `compare` when Existing-first turned up a
110
- baseline), `title`, and a real description. This uses the
111
- caller's own Claude (the model running this skill) no extra cost, full control
112
- for precise broken-vs-proposed diptychs and applies the team's brand from the
113
- fetched design system. **Follow that design system verbatim; never invent brand
114
- values.**
105
+ 2. **Default — build it yourself, with YOUR Claude.** Fetch the **canonical
106
+ authoring scaffold****`figura_fig_scaffold`** (CLI:
107
+ `bunx figura-cli fig scaffold --platform <mobile|web> [--device <id>] [--area <str>]`)
108
+ and hand-build the fig's HTML against it, then land it with
109
+ **`figura_create_fig`**, setting `area`, `orientation` (`portrait` /
110
+ `landscape` / `both`), `mode` (`single` / `compare` / `variations` default
111
+ to `compare` when Existing-first turned up a baseline), `title`, and a real
112
+ description. This uses the caller's own Claude (the model running this skill)
113
+ no extra cost, full control for precise broken-vs-proposed diptychs.
114
+
115
+ The scaffold IS the source of truth: it's the **exact system prompt Figura
116
+ injects when it renders a fig server-side** — the design rules, exemplars,
117
+ accent hints, your team's composed brand, and the component/device kit
118
+ context, all in one. Follow it verbatim; never invent brand values or restate
119
+ design rules from memory. Match `--platform` to the target (a web fig's
120
+ scaffold composes the team's *web* brand surface, a mobile fig's the *mobile*
121
+ one). **Fallback if the scaffold fetch fails** (older engine/CLI, or the
122
+ `figura_fig_scaffold` tool is unavailable): fetch just the brand with
123
+ `bunx figura-cli brand --json` and author against that — the scaffold is the
124
+ preferred path, the raw brand is the degraded one.
115
125
 
116
126
  **Landing IS hosting — leave `publish` and `commit` unset.** Creating a fig
117
127
  always persists it to Figura (there is no separate "save it for real" step);
@@ -332,8 +342,9 @@ So when you **hand-build** a fig (the default path), do your part of the contrac
332
342
  - **MCP unavailable** (headless / cron, or no interactive auth): fall back to the
333
343
  CLI — `bunx figura-cli fig create` / `fig publish`, with `fig revise` /
334
344
  `fig revisions` for versions, `fig commit` / `fig uncommit` (or `--commit` on
335
- the final create/revise) for the team library, and
336
- `bunx figura-cli brand --json` for the brand.
345
+ the final create/revise) for the team library, `bunx figura-cli fig scaffold`
346
+ for the authoring scaffold, and `bunx figura-cli brand --json` for the raw
347
+ brand (the scaffold's fallback).
337
348
  - **`figura_create_fig`/`figura_revise_fig` itself fails** (no network, API down,
338
349
  auth expired): the fig never entered Figura, so there is no `id`/`viewUrl` to
339
350
  open. Only THEN write the HTML to a local scratch file (project-scoped —
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: figura-brand
3
- description: "Introduce a repo to Figura — scan the codebase for its design system (design tokens, brand docs, app name) and set the team's Figura brand profile so generated figs are on-brand. Use when asked to import or sync a brand, set up Figura for a project, or 'introduce my repo to Figura'."
3
+ description: "Introduce a repo to Figura — scan the codebase for its design system (design tokens, brand docs, app name) and set the team's Figura brand profile (STRUCTURED) so generated figs are on-brand. Use when asked to import or sync a brand, set up Figura for a project, 'introduce my repo to Figura', or upgrade an existing prose brand to structured."
4
4
  argument-hint: '[path to the repo or design-system directory]'
5
- allowed-tools: 'Read, Grep, Glob, Bash(figura *), Bash(bunx figura-cli *)'
5
+ allowed-tools: 'Read, Grep, Glob, mcp__figura__figura_whoami, mcp__figura__figura_brand_schema, mcp__figura__figura_set_brand, Bash(figura *), Bash(bunx figura-cli *)'
6
6
  ---
7
7
 
8
8
  # Figura Brand — introduce a repo to Figura
@@ -12,6 +12,18 @@ profile** from it, so every fig the `/fig` skill / generator produces is on-bran
12
12
  You (Claude) do the synthesis — read the code and docs, then push the result. No
13
13
  figura.so UI.
14
14
 
15
+ Author a **STRUCTURED brand** — a `{ identity, platforms }` object (colors, type,
16
+ voice, per-platform surfaces) — not freeform prose. Structured is addressable,
17
+ platform-partitioned, and versioned; prose is the fallback for when structured
18
+ synthesis genuinely isn't possible.
19
+
20
+ **The shape authority is the SERVER schema, not this doc.** Fetch it and author
21
+ against it so the brand validates first try:
22
+ `figura brand schema` (CLI) or the `figura_brand_schema` MCP tool → the JSON Schema
23
+ `PUT /brand` validates. `reference/brand-format.md` is a short orientation +
24
+ worked example only; the fetched schema is the source of truth for field
25
+ names/shape/caps.
26
+
15
27
  `$ARGUMENTS` is an optional path to focus on (a repo root or a design-system
16
28
  directory). Default to the current repo.
17
29
 
@@ -33,6 +45,9 @@ here first. The brand is **team-wide and overwrites** the current one — never
33
45
  push without showing the user what you're about to set and getting an explicit
34
46
  OK.
35
47
 
48
+ > **Upgrading an existing prose brand instead of a fresh import?** Jump to
49
+ > [Upgrade a legacy prose brand to structured](#upgrade-a-legacy-prose-brand-to-structured).
50
+
36
51
  ## Workflow
37
52
 
38
53
  ### 1. Scan the repo for design signals
@@ -47,72 +62,145 @@ Hunt across whatever stack this repo uses — do not assume one framework:
47
62
  - **Brand voice + positioning** — `BRAND.md`, `STRATEGY.md`, design-system READMEs, marketing copy, the landing page hero. Capture the tone and any explicit voice lines / taglines.
48
63
  - **Identity** — app/brand name + one-line description: `package.json` (name/description), `Info.plist` (`CFBundleName`), app-store copy, the README's first paragraph.
49
64
  - **Role/usage notes** — inline comments next to tokens often state the role
50
- (e.g. `// primary brand accent; record button`). Carry those into the tokens block.
65
+ (e.g. `// primary brand accent; record button`). Carry those into `color.role`/`meaning`.
66
+ - **Signature components** — the product's distinctive UI pieces (a scrubber, a
67
+ reaction button, a dense data table), from component files — become
68
+ per-platform `components[]`.
51
69
  - **Product areas** — the app's own top-level sections, e.g. a tab bar, a
52
70
  sidebar's nav items, or the router's top-level route folders (`app/(tabs)/*`,
53
- `src/routes/*`, `src/screens/*`). These become `surfaces` — THIS product's
54
- real areas ("Feed", "Checkout"), never a generic placeholder list and never
55
- another product's areas.
71
+ `src/routes/*`, `src/screens/*`). These become each platform's `surfaces[]` —
72
+ THIS product's real areas ("Feed", "Checkout"), never a generic placeholder
73
+ list and never another product's.
74
+ - **Which platform(s)** — detect the target platform(s) so you only emit the
75
+ surfaces that apply:
76
+ - `mobile` — an iOS/Android/React-Native/Expo app (`*.xcodeproj`, `Info.plist`,
77
+ `android/`, `app.json`/Expo, SF Symbols usage, safe-area/tab-bar tokens).
78
+ - `web` — a web app / site (`index.html`, a web router, CSS media queries,
79
+ hover states, a `tailwind.config`).
80
+ - `desktop` — an Electron/Tauri/native-desktop shell.
81
+ - A product can target more than one — emit a `platforms` key for each.
56
82
 
57
83
  Use `Glob`/`Grep` to locate, `Read` to extract real values. Use the REAL hexes,
58
84
  fonts, and sizes from the repo — never invent them.
59
85
 
60
- ### 2. Synthesize the brand profile
61
-
62
- Produce four fields (see `reference/brand-format.md` for the exact tokensBlock shape):
63
-
64
- - **name** the product/brand name.
65
- - **appDescription** one clause: what it is + who it's for.
66
- - **tokensBlock** freeform plain text the fig generator reads verbatim:
67
- - **Colors** — the real palette, each `#HEX — name — role` (role from the repo's comments/usage).
68
- - **Brand gradient** — if the repo defines one, state it exactly (`linear-gradient(…)`).
69
- - **Type** — display / body / mono faces (real font names + fallbacks) and the size scale.
70
- - **Spacing / radius** the base unit + scale.
71
- - **Icon language** SF Symbols / inline SVG / icon set the product uses; ban emoji if the brand does.
72
- - **Voice** 3–5 short brand voice lines + a one-line tone rule, inferred from the docs/copy.
73
- - **Conventions** any hard rules you found ("never use X", aspect ratios, chrome shape).
74
- - **surfaces** — THIS product's own key areas/sections, short labels (e.g.
75
- `["Feed", "Profile", "Settings"]`), read from the real navigation/routes you
76
- found in step 1. Drives the Studio Area picker. 3–10 is typical; skip
77
- (leave unset) if the repo genuinely has no discoverable navigation — never
78
- guess or reuse a generic/other-product list. Capped at 24 areas / 40
79
- characters each.
86
+ ### 2. Synthesize the STRUCTURED brand
87
+
88
+ Fetch the schema (`figura brand schema` / `figura_brand_schema`) and produce a
89
+ `{ identity, platforms }` object plus a `name`, authoring against that schema
90
+ EXACTLY (strict keys, hex 3/4/6/8 digits, no blank array entries, only
91
+ `mobile`/`web`/`desktop` platform keys) so it validates first try.
92
+ `reference/brand-format.md` is a quick orientation + one worked example.
93
+
94
+ - **name** — the product/brand name (still required on the write).
95
+ - **identity** — the SHARED brand: `colors` (real palette, each
96
+ `{ name, hex, role?, meaning? }`), `gradient` (if any), `type`
97
+ (`{ families, ramp, rules }`), `spacing`, `radius`, `voice`
98
+ (`{ nouns, promise, lines, tone }`), `forbidden` (hard guardrails as
99
+ `[{ id?, rule }]`).
100
+ - **platforms** — a map with a key ONLY for each platform you detected in step 1.
101
+ Each surface: `components` (signature components as `[{ name, spec }]`), `icons`
102
+ (the platform's icon language), `layout` (its frame/shell law), `surfaces` (this
103
+ product's real areas), `bans` (platform-specific don'ts). Use the per-platform
104
+ **`tokensBlock`** escape-hatch ONLY for guidance that genuinely doesn't fit a
105
+ structured field — prefer a structured field.
80
106
 
81
107
  This synthesis IS the value — enrich the raw tokens with the roles, voice, and
82
108
  conventions you read from the repo. Keep it grounded in what's actually there.
83
-
84
- ### 3. Confirm, then push
85
-
86
- Show the user the synthesized `name`, `appDescription`, `surfaces`, and the full
87
- `tokensBlock`, and confirm the target team (`figura whoami`). On approval:
88
-
89
- - **MCP (preferred):** call `figura_set_brand` with `{ name, appDescription, tokensBlock, surfaces }`.
90
- - **CLI fallback:** pipe the tokens block over stdin, pass surfaces as a flag
109
+ `appDescription` (one clause: what it is + who it's for) is optional and sent
110
+ alongside.
111
+
112
+ **Also write a `proseSource`** a short human-readable description (a few brief
113
+ paragraphs) of this brand: what a person would type to describe its colors, type,
114
+ voice, and conventions. This is NOT a generation input; it's the editable source
115
+ the web brand editor's "Author from a description" textarea seeds from. Send it on
116
+ the push (next step) so a scan-authored brand still has a populated editable
117
+ source, not a blank textarea. Ground it in the same signals you read from the
118
+ repo — it should read as the prose a founder would write, not a dump of the JSON.
119
+
120
+ ### 3. Confirm, then push (structured)
121
+
122
+ Show the user the `name`, `appDescription`, and the synthesized `{ identity,
123
+ platforms }` (summarize the palette/type/voice + which platforms + their
124
+ surfaces), and confirm the target team (`figura whoami`). On approval:
125
+
126
+ Always send `proseSource` (the prose description from step 2) alongside the
127
+ structured write so the web editor's textarea is seeded:
128
+
129
+ - **MCP (preferred):** call `figura_set_brand` with `{ name, appDescription,
130
+ identity, platforms, proseSource }` (the `identity`/`platforms` objects directly).
131
+ - **CLI fallback:** write the `{ identity, platforms }` JSON to a file and the prose
132
+ description to a second file, then pass `--structured` + `--prose-source`.
133
+ `--name` is still required:
91
134
  ```bash
92
- cat <<'TOKENS' | figura brand set --name "<name>" --description "<one clause>" --tokens - --surfaces "Feed,Profile,Settings"
93
- the tokensBlock
94
- TOKENS
135
+ figura brand set --name "<name>" --description "<one clause>" \
136
+ --structured @brand.json --prose-source @brand-notes.txt
95
137
  ```
96
- (Use `bunx figura-cli` in place of `figura` if it isn't on PATH.)
138
+ `brand.json` is the `{ "identity": {…}, "platforms": {…} }` object;
139
+ `brand-notes.txt` is the prose description. Pass both as **@files** — a single
140
+ command can't read stdin twice, so `--structured -` and `--prose-source` can't
141
+ share stdin. (Use `bunx figura-cli` in place of `figura` if it isn't on PATH.)
97
142
 
98
- A user can always add/rename/remove/reorder areas later on the brand page — this
99
- is a starting point, not a one-shot lock-in.
143
+ A user can always add/rename/remove areas or edit tokens later — this is a
144
+ starting point, not a one-shot lock-in.
100
145
 
101
146
  ### 4. Verify
102
147
 
103
- `figura brand --json` (or the `figura_whoami` → `figura brand` read) confirm
104
- `isCustom: true` and that the saved tokens AND `surfaces` match what you set.
105
- The response also carries `platformProfile` the engine auto-detects mobile /
106
- web / both from the tokens you just wrote (breakpoints, font stacks,
107
- safe-area/tab-bar tokens, hover
108
- states deterministic, no LLM) every time you save, so nothing extra to do
109
- here. It only ever changes on its own until the team explicitly overrides it
110
- on the brand page (Studio then defaults its device frame off that value —
111
- iPhone for mobile, browser for web). Then tell the user the brand is set and
112
- suggest trying `/fig <a screen>` to see it render on-brand.
148
+ `figura brand --json` (or `figura brand --json --platform <mobile|web|desktop>`)
149
+ → confirm `isCustom: true`, that the response carries the structured
150
+ `{ identity, platforms }` you set, and that the composed `tokensBlock`/`surfaces`
151
+ reflect them. (For a legacy prose fallback you'll see the prose `tokensBlock`
152
+ instead.) The response also carries `platformProfile` — mobile/web/both, which the
153
+ team can override later on the brand page (Studio defaults its device frame off
154
+ it). Then tell the user the brand is set and suggest `/fig <a screen>` to see it
155
+ render on-brand.
156
+
157
+ ## Upgrade a legacy prose brand to structured
158
+
159
+ If the team already has a legacy PROSE brand (from an earlier onboarding), convert
160
+ it to structured through the **server transform** (the single transform mechanism —
161
+ same one the web brand editor uses; do NOT hand-parse the prose yourself). It's
162
+ reversible.
163
+
164
+ 1. **Fetch the current brand** — `figura brand --json`. For a legacy brand this
165
+ returns the prose `tokensBlock` (+ `surfaces`, `forbidden`).
166
+ 2. **Transform it on the server** — pipe that prose to `figura brand structure`:
167
+ ```bash
168
+ figura brand --json | jq -r '.tokensBlock' | figura brand structure -
169
+ ```
170
+ (or `figura brand structure --prose @notes.txt`). The server GLM returns
171
+ schema-valid `{ identity, platforms }` on stdout (it does NOT save). On a
172
+ transform failure it errors with a clear message — edit the prose and retry.
173
+ 3. **Confirm the mapping with the user** — show the before (prose) → after
174
+ (structured `{ identity, platforms }`) and get an explicit OK. Flag anything the
175
+ transform dropped or that the prose didn't clearly specify.
176
+ 4. **Push structured** — save the reviewed JSON, AND pass the original prose that
177
+ was transformed as `--prose-source` (that prose IS the editable source, so the
178
+ web editor seeds from it):
179
+ `figura brand set --name <name> --structured @file.json --prose-source @notes.txt`.
180
+ (`notes.txt` = the original prose from step 2; if you piped `.tokensBlock`, write
181
+ it to a file first so you can pass it here.) Over MCP, send the same original
182
+ prose as `proseSource` on `figura_set_brand`.
183
+
184
+ `brand set --structured` **snapshots a version first** (#707), so the upgrade is
185
+ reversible: every brand write appends to the team's version history, and the team
186
+ can restore the pre-upgrade prose version from the brand page's version history on
187
+ figura.so if the mapping was wrong. Reassure the user of this before pushing.
188
+
189
+ ## Prose fallback (legacy — only if structured synthesis isn't possible)
190
+
191
+ If the repo's design system genuinely can't be structured (e.g. it's a single
192
+ freeform brand doc with no extractable tokens), fall back to a legacy PROSE brand
193
+ so onboarding never hard-fails: send `tokensBlock` (freeform plain text) +
194
+ `surfaces` (the product's areas) instead of `identity`/`platforms`.
195
+
196
+ - **MCP:** `figura_set_brand` with `{ name, appDescription, tokensBlock, surfaces }`.
197
+ - **CLI:** `cat tokens.txt | figura brand set --name "<name>" --tokens - --surfaces "Feed,Settings"`.
198
+
199
+ A prose brand still renders; it's just not addressable. Prefer structured whenever
200
+ you can extract real tokens.
113
201
 
114
202
  ## Output
115
203
 
116
204
  Report the brand you set (name + a one-line summary of the palette/type/voice you
117
- captured, plus the surfaces list) and confirm it's live for the team. Don't
118
- paste the entire tokensBlock again unless asked.
205
+ captured, the platforms + their surfaces) and confirm it's live for the team.
206
+ Don't paste the entire object again unless asked.
@@ -1,80 +1,74 @@
1
- # tokensBlock format
2
-
3
- The brand profile's `tokensBlock` is **freeform plain text** injected verbatim
4
- into the fig-generation prompt. It is NOT JSON, NOT markdown just clear,
5
- sectioned plain text with concrete values. Lead with hard rules; be terse.
6
-
7
- Aim for these sections (skip any the repo genuinely doesn't have; never invent
8
- values):
9
-
10
- ```
11
- <NAME> BRAND GROUND-TRUTH (locked these are NOT suggestions):
12
-
13
- WHAT <NAME> IS
14
- - One or two lines: what the product is, the platform, the core metaphor.
15
- - The pitch / tagline if there is one.
16
-
17
- COLORS (use ONLY these never invent hues)
18
- - <#HEX> <Name> <role/usage, from the repo's comments or how it's used>.
19
- - one line per color. Note any opacity variants used over media.
20
-
21
- THE BRAND GRADIENT (only if the repo defines one)
22
- - EXACTLY one: linear-gradient(<deg>, <#HEX>, <#HEX>). Where it's allowed.
23
-
24
- TYPE
25
- - Display: <Font> stack '<Font>', <fallbacks>. Where it's used + weight rule.
26
- - Body: <Font> — stack <fallbacks>. UI text.
27
- - Mono: <Font>stack <fallbacks>. Numerics / code.
28
- - Scale: <the real size ramp, e.g. 12 / 14 / 16 / 20 / 24 / 36 / 48>.
29
-
30
- SPACING / RADIUS
31
- - Base unit + scale (e.g. 4-based: 4 8 12 16 24 32 …). Radius tokens (sm/md/lg/full).
32
-
33
- ICON LANGUAGE
34
- - The icon set the product uses (SF Symbols names / inline stroke SVG / a named set).
35
- - Ban emoji if the brand bans it.
36
-
37
- VOICE (3–5 lines, the brand's register)
38
- - Short example lines in the brand's actual tone (pull/adapt from brand docs/copy).
39
- - One tone rule (e.g. "terse, confident, never salesy").
40
-
41
- CONVENTIONS (hard rules found in the repo)
42
- - e.g. aspect ratios, chrome shape (pill/capsule), "never use <X>", dark-first, etc.
1
+ # Structured brand format — orientation
2
+
3
+ The brand profile is a **structured JSON object** `{ identity, platforms }`. The
4
+ **authoritative shape is the live JSON Schema**, not this file: fetch it with
5
+ `figura brand schema` (CLI) or the `figura_brand_schema` MCP tool and author
6
+ against THAT (it's derived server-side from the zod schema `PUT /brand` validates,
7
+ so it can't drift). This page is only orientation + one worked example.
8
+
9
+ Two halves:
10
+ - **`identity`** — the SHARED brand (same on every platform): `colors`
11
+ (`[{ name, hex, role?, meaning? }]`), `gradient?`, `type?`
12
+ (`{ families?: { slot: fontName }, ramp?: number[], rules? }`), `spacing?`,
13
+ `radius?`, `voice?` (`{ nouns?, promise?, lines?, tone? }`), `forbidden?`
14
+ (`[{ id?, rule }]`).
15
+ - **`platforms`** a map keyed by `mobile` / `web` / `desktop`. Include ONLY the
16
+ platforms the product targets. Each surface: `components?` (`[{ name, spec }]`),
17
+ `icons?`, `layout?`, `surfaces?` (this product's real areas), `bans?`,
18
+ `tokensBlock?` (the prose ESCAPE HATCH for guidance that genuinely doesn't fit a
19
+ structured field use sparingly).
20
+
21
+ Validation the schema enforces (a violation is a 400 at `PUT /brand`): **strict
22
+ keys** (an unknown/misspelled key is rejected); **hex** = `#` + exactly 3/4/6/8
23
+ digits; **no blank** array entries; platform keys exactly `mobile`/`web`/`desktop`;
24
+ caps on array lengths + string lengths (the fetched schema carries the exact
25
+ numbers). Author against the fetched schema, not these prose notes.
26
+
27
+ ## Worked examplea web/desktop brand ("Ledgerline")
28
+
29
+ ```json
30
+ {
31
+ "identity": {
32
+ "colors": [
33
+ { "name": "Fern", "hex": "#1F6F54", "role": "accent", "meaning": "primary actions; gradient start" },
34
+ { "name": "Brass", "hex": "#C9A227", "role": "accent", "meaning": "highlights + paid states; gradient end" },
35
+ { "name": "Bone", "hex": "#F4F2EC", "role": "surface", "meaning": "light surfaces + text on dark" },
36
+ { "name": "Ink", "hex": "#101312", "role": "ground", "meaning": "near-black base background" }
37
+ ],
38
+ "gradient": "linear-gradient(90deg, #1F6F54, #C9A227)",
39
+ "type": {
40
+ "families": { "display": "Fraunces", "body": "Inter", "mono": "JetBrains Mono" },
41
+ "ramp": [12, 14, 16, 18, 20, 24, 30, 36, 48],
42
+ "rules": "Fraunces semibold at display size; Inter 400/600 for UI; mono for amounts + ids."
43
+ },
44
+ "spacing": [4, 8, 12, 16, 20, 24, 32, 40, 48, 64],
45
+ "radius": [4, 8, 12, 999],
46
+ "voice": {
47
+ "nouns": ["ledger", "balance", "month-end"],
48
+ "promise": "Books, balanced.",
49
+ "lines": ["Every cent, accounted.", "Close the month, not your eyes."],
50
+ "tone": "Terse, dry, precise — never salesy."
51
+ },
52
+ "forbidden": [
53
+ { "id": "no-emoji", "rule": "Never use emoji; icons are inline stroke SVG." },
54
+ { "id": "squared", "rule": "Chrome is squared (radius 8), never pill." }
55
+ ]
56
+ },
57
+ "platforms": {
58
+ "web": {
59
+ "components": [
60
+ { "name": "dataTable", "spec": "Dense zebra-striped table; right-aligned numeric columns in mono; sticky header." },
61
+ { "name": "primaryButton", "spec": "Solid Fern fill, Bone text, radius 8, 40px tall." }
62
+ ],
63
+ "icons": "Inline stroke SVG (receipt, arrow-right, check-circle). currentColor, ~1.6 width.",
64
+ "layout": "Desktop-first two-column app shell: left nav rail + dense content. Tables are the hero.",
65
+ "surfaces": ["Invoices", "Clients", "Reports", "Settings"],
66
+ "bans": ["No pill buttons", "No hero illustrations — numbers first"]
67
+ }
68
+ }
69
+ }
43
70
  ```
44
71
 
45
- ## Worked example (fictional brand "Ledgerline" abridged)
46
-
47
- ```
48
- LEDGERLINE BRAND GROUND-TRUTH (locked):
49
-
50
- WHAT LEDGERLINE IS
51
- - A calm B2B invoicing web app. Dense tables, quiet confidence, desktop-first.
52
- - Numbers first. Tagline: "Books, balanced."
53
-
54
- COLORS (use ONLY these)
55
- - #1F6F54 Fern — primary brand accent; primary actions; gradient start.
56
- - #C9A227 Brass — secondary accent; highlights + paid states; gradient end.
57
- - #F4F2EC Bone — light surfaces + foreground on dark (a warm off-white).
58
- - #101312 Ink — near-black base background.
59
-
60
- THE BRAND GRADIENT
61
- - EXACTLY one: linear-gradient(90deg, #1F6F54, #C9A227). Wordmark/hero accents only.
62
-
63
- TYPE
64
- - Display: Fraunces — stack 'Fraunces', Georgia, serif. Semibold at display size.
65
- - Body: Inter — stack Inter, -apple-system, 'SF Pro Text', system-ui, sans-serif.
66
- - Mono: JetBrains Mono — amounts, invoice ids, dates.
67
- - Scale: 12 / 14 / 16 / 18 / 20 / 24 / 30 / 36 / 48.
68
-
69
- SPACING / RADIUS
70
- - 4-based scale: 4 8 12 16 20 24 32 40 48 64. Radius: sm 4, md 8, lg 12, full 999.
71
-
72
- ICON LANGUAGE
73
- - Inline stroke SVG (receipt, arrow-right, check-circle). NEVER emoji.
74
-
75
- VOICE
76
- - "Every cent, accounted." "Close the month, not your eyes." Terse, dry, precise — never salesy.
77
-
78
- CONVENTIONS
79
- - Tables are the hero. Chrome is squared (radius md, never pill). Light-first with a dark ledger mode.
80
- ```
72
+ A brand can hold more than one platform at once (a product shipping iOS + web sets
73
+ both `platforms.mobile` and `platforms.web`, sharing one `identity`). Emit a
74
+ platform key ONLY for a platform the product actually targets.