justskill-design 0.0.1 → 3.2.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/README.en.md +235 -0
- package/README.md +184 -11
- package/SKILL.md +435 -0
- package/canvas/app.mjs +2870 -0
- package/canvas/groups.mjs +204 -0
- package/canvas/index.html +141 -0
- package/canvas/styles.css +1032 -0
- package/canvas/tree.mjs +249 -0
- package/cordis.patch.yml +8 -0
- package/design.mjs +172 -0
- package/docs/dsh-workbench.png +0 -0
- package/dsh/client.js +864 -0
- package/dsh/index.js +354 -0
- package/dsh/scan.js +139 -0
- package/dsh/skill.js +41 -0
- package/lib/base-css.mjs +92 -0
- package/lib/detect.mjs +123 -0
- package/lib/diagnostics.mjs +382 -0
- package/lib/export-html.mjs +309 -0
- package/lib/export-svg.mjs +111 -0
- package/lib/fragment.mjs +702 -0
- package/lib/geometry.mjs +827 -0
- package/lib/icons.mjs +106 -0
- package/lib/levenshtein.mjs +133 -0
- package/lib/node/cli.mjs +387 -0
- package/lib/node/export-svg.mjs +75 -0
- package/lib/node/fs.mjs +309 -0
- package/lib/node/op-json.mjs +75 -0
- package/lib/node/raster-run.mjs +115 -0
- package/lib/node/server.mjs +1264 -0
- package/lib/node/static.mjs +206 -0
- package/lib/node/verbs/add.mjs +220 -0
- package/lib/node/verbs/export.mjs +161 -0
- package/lib/node/verbs/format.mjs +106 -0
- package/lib/node/verbs/import.mjs +126 -0
- package/lib/node/verbs/ls.mjs +98 -0
- package/lib/node/verbs/migrate.mjs +236 -0
- package/lib/node/verbs/move.mjs +76 -0
- package/lib/node/verbs/new.mjs +284 -0
- package/lib/node/verbs/remove.mjs +41 -0
- package/lib/node/verbs/rename.mjs +87 -0
- package/lib/node/verbs/serve.mjs +164 -0
- package/lib/node/verbs/set.mjs +150 -0
- package/lib/node/verbs/show.mjs +122 -0
- package/lib/node/verbs/tokens.mjs +167 -0
- package/lib/node/verbs/validate.mjs +130 -0
- package/lib/node/write-verbs.mjs +215 -0
- package/lib/normalize.mjs +288 -0
- package/lib/ops.mjs +2063 -0
- package/lib/parse.mjs +997 -0
- package/lib/raster.mjs +186 -0
- package/lib/readers/pen.mjs +1684 -0
- package/lib/receipt.mjs +203 -0
- package/lib/render.mjs +503 -0
- package/lib/serialize.mjs +441 -0
- package/lib/spans.mjs +177 -0
- package/lib/svg-path.mjs +483 -0
- package/lib/table.mjs +1047 -0
- package/lib/tokens.mjs +295 -0
- package/lib/validate.mjs +929 -0
- package/package.json +68 -6
- package/references/cookbook.md +156 -0
- package/references/images.md +171 -0
- package/references/product-flow.md +408 -0
- package/references/schema.md +90 -0
- package/references/styling.md +181 -0
- package/scripts/install.mjs +95 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# The product flow
|
|
2
|
+
|
|
3
|
+
<!-- Hand-written, like references/images.md: a playbook, not a rendering of
|
|
4
|
+
the format table. The route it expands is the table in SKILL.md. -->
|
|
5
|
+
|
|
6
|
+
`SKILL.md` has the route in one table. This file is how to walk it: which
|
|
7
|
+
request takes which lane, the questions worth asking, what to show the user at
|
|
8
|
+
each stage, the canvas handshake, and the handoff to code.
|
|
9
|
+
|
|
10
|
+
The order exists because two mistakes are expensive: designing a page before the
|
|
11
|
+
product is agreed gives a beautiful answer to the wrong question, and writing
|
|
12
|
+
code before the user has approved a screen is work done twice. Every stage
|
|
13
|
+
below removes one of those risks. None of them is a lock on the user.
|
|
14
|
+
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
1. Which request is which
|
|
18
|
+
2. Stage 1 — Idea
|
|
19
|
+
3. Stage 2 — Feature breakdown
|
|
20
|
+
4. Stage 3 — Feature confirmation
|
|
21
|
+
5. Stage 4 — Design takeover
|
|
22
|
+
6. Stage 5 — Design first
|
|
23
|
+
7. Stage 6 — Canvas
|
|
24
|
+
8. Stage 7 — UI adjustment
|
|
25
|
+
9. Stage 8 — Page sign-off
|
|
26
|
+
10. Stage 9 — Code
|
|
27
|
+
11. Keeping it soft
|
|
28
|
+
12. One conversation, end to end
|
|
29
|
+
|
|
30
|
+
## Which request is which
|
|
31
|
+
|
|
32
|
+
| The request sounds like | Lane | Start at |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| "I want an app that…", "a tool for…", "help me build…" | new product | Stage 1 |
|
|
35
|
+
| "add a feature", "change how checkout works", "let users…" | feature change | Stage 2 |
|
|
36
|
+
| "make this card rounder", "this title is too big", a pasted node id | edit in an existing design | Stage 7 |
|
|
37
|
+
| "just build it", "write the code first", "skip the design" | explicit shortcut | what they asked for; mention the screen once, then drop it |
|
|
38
|
+
| "fix this bug", an unrelated code task | not a design request | do the work; do not hijack it |
|
|
39
|
+
|
|
40
|
+
Two entry points get the route: a new product idea, and a feature to add or
|
|
41
|
+
change — including when the user never says "design", "UI" or "page", because a
|
|
42
|
+
feature is product design before it is code.
|
|
43
|
+
|
|
44
|
+
A concrete edit inside an existing design is not a product request. "Add a
|
|
45
|
+
pricing row to this page", "make that card rounder", "this headline is too
|
|
46
|
+
big": the screen and the content are already decided, so make the edit with the
|
|
47
|
+
CLI or on the canvas and skip the route. When the two are mixed — "add a
|
|
48
|
+
pricing row" inside a feature that does not exist yet — the feature part is
|
|
49
|
+
product-shaped; agree that first, then the row is an edit.
|
|
50
|
+
|
|
51
|
+
When it is genuinely unclear whether a request changes what the product does or
|
|
52
|
+
only how it looks, ask one question: "Is this a new capability, or a change to
|
|
53
|
+
the screen as it stands?" One question, not a form.
|
|
54
|
+
|
|
55
|
+
## Stage 1 — Idea
|
|
56
|
+
|
|
57
|
+
The goal is to understand the product well enough to propose screens, not to
|
|
58
|
+
interview the user. Ask at most the three questions that change the screens:
|
|
59
|
+
|
|
60
|
+
1. Who is it for, and what do they do today instead? (audience decides density,
|
|
61
|
+
tone, and how long the flow can be)
|
|
62
|
+
2. What is the one thing a user must be able to do? (the core loop decides the
|
|
63
|
+
first screen)
|
|
64
|
+
3. What is out of the first version? (scope decides how many screens exist)
|
|
65
|
+
|
|
66
|
+
Do not ask for colours, fonts, a layout preference or "do you want a
|
|
67
|
+
dashboard?". Those are design decisions, and design is your job. If the user
|
|
68
|
+
offers a brand or a reference, take it; if they do not, decide.
|
|
69
|
+
|
|
70
|
+
Then restate the product in one sentence — "A tool for freelancers to send an
|
|
71
|
+
invoice and chase it without leaving the app" — and go straight into Stage 2 in
|
|
72
|
+
the same message. Do not send the restatement alone and wait.
|
|
73
|
+
|
|
74
|
+
If the user already answered these questions in their first message, ask
|
|
75
|
+
nothing; go to Stage 2.
|
|
76
|
+
|
|
77
|
+
## Stage 2 — Feature breakdown
|
|
78
|
+
|
|
79
|
+
Propose the smallest first version that answers the core loop, in one message
|
|
80
|
+
short enough to read without scrolling. This is product design: screens are
|
|
81
|
+
chosen by the job they do, not by what would look good on the canvas.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
<Who> can <core loop> without <the thing they do today>.
|
|
85
|
+
|
|
86
|
+
Screens:
|
|
87
|
+
| Screen | Job | Key content | States |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| Home | see what needs doing | ... | empty, loading |
|
|
90
|
+
| ... | ... | ... | ... |
|
|
91
|
+
|
|
92
|
+
Data: what each screen shows, and where it comes from. Static sample data for
|
|
93
|
+
v1 is fine; say so.
|
|
94
|
+
Out of scope for v1: the features deliberately left out, and why.
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The breakdown is a proposal, not a survey. Keep it to one to five screens for a
|
|
98
|
+
first version; three is usually right for an app, one for a tool. Every screen
|
|
99
|
+
carries a job in the second column — if the job is "show the other screens", the
|
|
100
|
+
screen is navigation, not a screen. Name the states that matter: a list needs an
|
|
101
|
+
empty state, a form needs an error state, anything that loads needs a loading
|
|
102
|
+
state. Write real copy in the table, not lorem ipsum; the copy is part of the
|
|
103
|
+
product.
|
|
104
|
+
|
|
105
|
+
For a feature change on an existing product, the same table is shorter: the
|
|
106
|
+
screens the change touches, what changes on each, the new states, and what stays
|
|
107
|
+
as it is. If the change is one line of behaviour, the breakdown is three lines,
|
|
108
|
+
and you go straight to design.
|
|
109
|
+
|
|
110
|
+
## Stage 3 — Feature confirmation
|
|
111
|
+
|
|
112
|
+
Send the breakdown, then one question with two parts at most: which of these
|
|
113
|
+
must be in the first version, and what is missing. Then wait. A confirmation the
|
|
114
|
+
user already gave is not asked again, and a "you decide" is an answer: mark your
|
|
115
|
+
assumptions in one line — "I'll keep it to these three screens and the empty
|
|
116
|
+
state" — and move on.
|
|
117
|
+
|
|
118
|
+
Fold the answer back into a short agreed scope, in the chat:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
V1: open an invoice, chase it, see what is overdue.
|
|
122
|
+
Not now: settings, team accounts, exports.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
That list is the contract for the rest of the route. A screen that appears later
|
|
126
|
+
without being on it is new scope: add it deliberately, not by accident. If the
|
|
127
|
+
user wants a written brief, put this list in a short Markdown file beside the
|
|
128
|
+
design file; otherwise the chat is enough, and a stale brief file becomes a
|
|
129
|
+
third truth to maintain.
|
|
130
|
+
|
|
131
|
+
State the order in the same message, as a statement rather than a second
|
|
132
|
+
question: "After this I'll design the screens, you approve them, and then the
|
|
133
|
+
code." That is the design-first step, delivered without a round trip; Stage 5
|
|
134
|
+
below does not ask it again.
|
|
135
|
+
|
|
136
|
+
## Stage 4 — Design takeover
|
|
137
|
+
|
|
138
|
+
Announce that you are taking over the screens. Do not ask permission for a step
|
|
139
|
+
the user has already asked for; say what you are going to do and start.
|
|
140
|
+
|
|
141
|
+
Offer the page list, one line per page, and one visual direction in a sentence.
|
|
142
|
+
A direction covers the palette, the type scale, the density, the corner radius
|
|
143
|
+
and the kind of imagery — enough that the user can say "warmer" or "more
|
|
144
|
+
serious". One direction is enough. Two are only useful when the user is visibly
|
|
145
|
+
undecided, and a screen of options is not a decision; the canvas is where a
|
|
146
|
+
direction becomes real.
|
|
147
|
+
|
|
148
|
+
If the product already has a design file — the usual case for a feature change —
|
|
149
|
+
work in that file. `node design.mjs ls design.html --pages` first, then add or
|
|
150
|
+
change only the affected screens and leave the existing pages and tokens alone.
|
|
151
|
+
A second file splits the tokens, and the theme stops being one edit.
|
|
152
|
+
|
|
153
|
+
Otherwise create the file with `node design.mjs new design.html --name home
|
|
154
|
+
--title "<the product>"`, then set the tokens before the content: the brand
|
|
155
|
+
colour, the neutral surface, the spacing scale and the radius scale. A palette
|
|
156
|
+
chosen in the token block is one edit to change later; a palette typed into
|
|
157
|
+
forty elements is forty. Use `node design.mjs tokens design.html set
|
|
158
|
+
color.accent '#…'` for each token you want.
|
|
159
|
+
|
|
160
|
+
If the user has a brand — a colour, a logo, a font, an existing app — use it and
|
|
161
|
+
say so. If they do not, choose and name the choice in one sentence.
|
|
162
|
+
|
|
163
|
+
## Stage 5 — Design first
|
|
164
|
+
|
|
165
|
+
The order is normally stated at Stage 3, in the same message as the feature
|
|
166
|
+
confirmation: the screens come first, the user approves them, and then the code
|
|
167
|
+
is written. This stage is where that order becomes real — the canvas opens after
|
|
168
|
+
it — not a second gate in front of the same question.
|
|
169
|
+
|
|
170
|
+
- Already stated and answered, or the user asked for a mockup: proceed. Do not
|
|
171
|
+
repeat it as a question.
|
|
172
|
+
- Not stated yet (the features were confirmed in a different message, or the
|
|
173
|
+
order got lost): state it once, then proceed.
|
|
174
|
+
- The user says "code first": do it. Say once that the screens will be drawn
|
|
175
|
+
from the code, and keep the design file available as the place to adjust
|
|
176
|
+
appearance.
|
|
177
|
+
- The user says nothing after the stated order: proceed. Silence is not a
|
|
178
|
+
refusal.
|
|
179
|
+
|
|
180
|
+
The point is not a ceremony. It is to keep the code from being written twice,
|
|
181
|
+
and to give the user one moment where the whole product is a picture instead of
|
|
182
|
+
a task list.
|
|
183
|
+
|
|
184
|
+
## Stage 6 — Canvas
|
|
185
|
+
|
|
186
|
+
The canvas is where the user approves the work, so it opens before the first
|
|
187
|
+
screen, not after the last one. That is what "the design appears gradually"
|
|
188
|
+
requires: the user watches the screens arrive, instead of receiving a finished
|
|
189
|
+
file.
|
|
190
|
+
|
|
191
|
+
Start the service in the background — it stays in the foreground by design, so
|
|
192
|
+
run it in the background of your shell or tool — and read the URL it prints.
|
|
193
|
+
On a POSIX shell that looks like this; where the tool has a background-task
|
|
194
|
+
mode, use that instead:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
node design.mjs serve design.html > /tmp/justskill-canvas.log 2>&1 &
|
|
198
|
+
# the URL is in /tmp/justskill-canvas.log; the browser opens on its own
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`serve --json` prints one `{ url, port, file, revision }` line when the output
|
|
202
|
+
has to be parsed, and `--no-open` leaves the browser to you; with `--json`,
|
|
203
|
+
pass `--open` as well if the browser should still open.
|
|
204
|
+
|
|
205
|
+
Give the user the URL, and say what is coming first. Then build in this order:
|
|
206
|
+
|
|
207
|
+
1. The page shell: the file, the root frame, the background and the first token
|
|
208
|
+
set. Run `validate` once so the page loads clean.
|
|
209
|
+
2. One screen at a time, in the order of the user's core loop. A new page is one
|
|
210
|
+
`add` whose fragment root is a `<section data-type="page">`; leave `--in`
|
|
211
|
+
out, because a page fragment lands in `<body>`. The file holds every page, so
|
|
212
|
+
this is how the design grows without a rewrite.
|
|
213
|
+
3. Between screens, one line: "The invoice list is up — next is the invoice
|
|
214
|
+
itself." The user does not need a report, only to know what they are looking
|
|
215
|
+
at.
|
|
216
|
+
4. After each screen, `validate` the file; a screen that refuses is not a screen
|
|
217
|
+
the user can approve.
|
|
218
|
+
5. If the canvas closes (tab closed, machine slept past the idle timeout), start
|
|
219
|
+
the service again and give the user the new URL. The design file did not
|
|
220
|
+
disappear.
|
|
221
|
+
|
|
222
|
+
The canvas watches the file and shows each `add` and `set` as it lands, so the
|
|
223
|
+
design grows in front of the user. That means: build with `add` and `set`, in
|
|
224
|
+
small steps, even when one big fragment would be faster. The visible steps are
|
|
225
|
+
the point — they are also the undo steps.
|
|
226
|
+
|
|
227
|
+
When a screen needs a photograph and the user supplied none, take one from
|
|
228
|
+
`references/images.md` by subject. A solid colour is the honest placeholder when
|
|
229
|
+
no catalogue photo fits.
|
|
230
|
+
|
|
231
|
+
## Stage 7 — UI adjustment
|
|
232
|
+
|
|
233
|
+
This is the loop the canvas exists for, and it has one teachable handshake. Say
|
|
234
|
+
it once, when the canvas opens:
|
|
235
|
+
|
|
236
|
+
> Select an element on the canvas. Hover outlines the container (solid) and its
|
|
237
|
+
> direct children (dashed); a click selects the container, and a double-click
|
|
238
|
+
> steps into the dashed box under the pointer, one rung deeper. Escape goes back
|
|
239
|
+
> up. A row in the layer tree selects the same thing. With something selected, press ⌘C (Ctrl+C on
|
|
240
|
+
> Windows and Linux) — that copies a pointer to the element, not a screenshot.
|
|
241
|
+
> Paste it here and tell me what to change.
|
|
242
|
+
|
|
243
|
+
What the user pastes is generated by the canvas and looks like this:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
Use justSkill tools. Current file file:///Users/you/app/design.html. Page id: home. Selected node ids: home__invoice-total
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Read it as three things: the file to edit, the page the element lives on, and the
|
|
250
|
+
element's id. The canvas confirms the copy with a toast. When the selected node
|
|
251
|
+
is a page instead of an element, there is no `Page id:` clause: the page is the
|
|
252
|
+
selected id.
|
|
253
|
+
|
|
254
|
+
The file is written as a `file://` URL, because that is what a chat box can
|
|
255
|
+
linkify. Every command takes a plain path, so convert it first: drop the
|
|
256
|
+
`file://` scheme, percent-decode the rest (`%20` is a space, `%3A` is `:`,
|
|
257
|
+
`%2F` is `/`), and on Windows drop the leading `/` before the drive letter —
|
|
258
|
+
`file:///c%3A/app/design.html` is `c:/app/design.html`. If the path does not
|
|
259
|
+
match the file you have open, the one in the paste wins, because that is the one
|
|
260
|
+
the user is looking at.
|
|
261
|
+
|
|
262
|
+
Then, per paste:
|
|
263
|
+
|
|
264
|
+
1. Read the element: `node design.mjs show design.html home__invoice-total --json`.
|
|
265
|
+
For a parent or a group, `node design.mjs ls design.html --in home__invoice
|
|
266
|
+
--depth 2` shows what is in it.
|
|
267
|
+
2. Choose the smallest op:
|
|
268
|
+
- appearance — `set --style 'font-size:24px;font-weight:700'`
|
|
269
|
+
- text — `set --text "…"`
|
|
270
|
+
- structure — `add`, `move`, `remove`
|
|
271
|
+
- theme — `tokens set color.accent '#…'`
|
|
272
|
+
- a new name — `rename`
|
|
273
|
+
3. Apply it, read the receipt, and say what changed in one line: "Total is
|
|
274
|
+
24px and bold now." The receipt is not a report to paste; it is what tells
|
|
275
|
+
you the file holds what you asked for.
|
|
276
|
+
4. `validate` after a batch of changes, not after every single one.
|
|
277
|
+
|
|
278
|
+
The user can also drag, resize and nudge on the canvas; that goes through the
|
|
279
|
+
same write funnel and the same file. Both writers re-read first, so an edit you
|
|
280
|
+
make while the user is dragging is not lost. If the user drags, do not redo the
|
|
281
|
+
change by command; the drag already wrote it.
|
|
282
|
+
|
|
283
|
+
Some pastes will be thin:
|
|
284
|
+
|
|
285
|
+
- **A pointer and no request** ("make it better"): make one concrete proposal
|
|
286
|
+
with a reason, and apply it if the user says go. Do not ask three design
|
|
287
|
+
questions instead of making the change.
|
|
288
|
+
- **An id that no longer exists**: `ls` the parent and say which ids are there;
|
|
289
|
+
the user can click the element again. Never guess a replacement id silently.
|
|
290
|
+
- **A refusal**: read the refusal, not the user. It names what to write instead
|
|
291
|
+
(`grid` becomes a flex row, a one-sided margin becomes the shorthand,
|
|
292
|
+
`position:fixed` becomes `absolute` with `left` and `top`). Tell the user in
|
|
293
|
+
product language and make the change the refusal points to.
|
|
294
|
+
|
|
295
|
+
Never rewrite the whole file to adjust one property. One op is one undo step,
|
|
296
|
+
and the user may be dragging on the canvas while you write.
|
|
297
|
+
|
|
298
|
+
## Stage 8 — Page sign-off
|
|
299
|
+
|
|
300
|
+
Before asking for approval, check the work, not the user's patience:
|
|
301
|
+
|
|
302
|
+
- `node design.mjs validate design.html --json` — zero errors.
|
|
303
|
+
- `node design.mjs ls design.html --pages` — every screen on the agreed list is
|
|
304
|
+
there, and no screen is there that was not agreed.
|
|
305
|
+
- Every screen on the list has the states the breakdown promised; a list with an
|
|
306
|
+
empty state, a form with an error state.
|
|
307
|
+
- The copy is real sentences in the product's language, not placeholder text.
|
|
308
|
+
- Images have `alt` text that says what a person sees, and a catalogue photo
|
|
309
|
+
keeps its credit.
|
|
310
|
+
- Colours and spacing that repeat come from tokens, so the theme stays one edit.
|
|
311
|
+
- The canvas Problems badge is empty (or every entry is known and named).
|
|
312
|
+
|
|
313
|
+
Then send a short checklist and one question: are these pages approved, and is
|
|
314
|
+
anything to be changed? Offer the deliverables the user needs:
|
|
315
|
+
`node design.mjs export design.html --all -o dist` for one HTML file per page.
|
|
316
|
+
`--all` is HTML; an image is one page at a time —
|
|
317
|
+
`node design.mjs export design.html --page home --format png -o home.png`, and
|
|
318
|
+
the same with `svg` when the user needs a vector file.
|
|
319
|
+
|
|
320
|
+
A "looks good" that is followed by "now code it" is approval. A change request
|
|
321
|
+
goes back to Stage 7; it is not a rejection of the whole route.
|
|
322
|
+
|
|
323
|
+
## Stage 9 — Code
|
|
324
|
+
|
|
325
|
+
The approved file is the spec. Read it as data rather than re-imagining it:
|
|
326
|
+
|
|
327
|
+
- `node design.mjs ls design.html --pages --json` for the page list, and
|
|
328
|
+
`node design.mjs ls design.html --in <page> --depth 2 --json` for one page's
|
|
329
|
+
element tree.
|
|
330
|
+
- `node design.mjs show design.html <id> --json` for one element's exact values.
|
|
331
|
+
- `node design.mjs export design.html --all -o dist` for one HTML file per page
|
|
332
|
+
to look at while implementing.
|
|
333
|
+
|
|
334
|
+
Implement screen by screen, in the approved order, in the user's stack. Read the
|
|
335
|
+
repository to learn the stack; if it is genuinely ambiguous, ask once rather
|
|
336
|
+
than inventing a framework:
|
|
337
|
+
|
|
338
|
+
- Map the token names (`--color-accent`, `--space-md`, `--radius-card`) onto the
|
|
339
|
+
stack's theme variables. The token block is the design's smallest API; keeping
|
|
340
|
+
it means a later colour change stays one edit.
|
|
341
|
+
- Map `data-id` onto component names, test ids or selectors. They are stable,
|
|
342
|
+
human names chosen for how the screen reads.
|
|
343
|
+
- Match the approved layout with flex and `gap`, not with margin tweaks and not
|
|
344
|
+
with a rewrite in a different visual language. The code is not the place to
|
|
345
|
+
improve the design; the design file is.
|
|
346
|
+
- After each screen, compare the running result with the design — the canvas or
|
|
347
|
+
the exported page next to the app. List the mismatches, then fix the code or
|
|
348
|
+
the design, deliberately.
|
|
349
|
+
- When implementation reveals a missing screen or state, go back to Stage 6 and
|
|
350
|
+
design it first. A state invented only in code is a state the user never
|
|
351
|
+
approved.
|
|
352
|
+
- Keep the design file in the repository and commit it with the code. It is the
|
|
353
|
+
reference for the next feature request.
|
|
354
|
+
|
|
355
|
+
A later feature request returns to Stage 2. Change the design first, get the
|
|
356
|
+
screen approved, then change the code — the file stays the truth for as long as
|
|
357
|
+
the product lives.
|
|
358
|
+
|
|
359
|
+
## Keeping it soft
|
|
360
|
+
|
|
361
|
+
The route is a default, not a gate. The ways it stops being soft are worth
|
|
362
|
+
naming:
|
|
363
|
+
|
|
364
|
+
- **Opening with a questionnaire.** Eight questions before anything happens is
|
|
365
|
+
not product design; ask the two or three that change the screens.
|
|
366
|
+
- **Asking permission to begin.** "Shall I break down the features?" is a step
|
|
367
|
+
the user already asked for. Do it, then let them redirect.
|
|
368
|
+
- **Re-asking a confirmation.** Once the user has answered, the next question is
|
|
369
|
+
about the work, not the answer.
|
|
370
|
+
- **Building ten screens for a three-screen first version.** Scope is set at
|
|
371
|
+
Stage 3 and stays set until someone changes it on purpose.
|
|
372
|
+
- **Opening the canvas after the design is done.** The user then reviews a
|
|
373
|
+
finished surprise instead of watching the product appear.
|
|
374
|
+
- **Writing the whole file in one rewrite.** It removes the gradual reveal and
|
|
375
|
+
the per-step undo the canvas gives the user.
|
|
376
|
+
- **Forcing the route on a bug fix or a one-line edit.** An edit is an edit.
|
|
377
|
+
- **A wall of CSS in the chat.** The user is reading a product, not a diff. One
|
|
378
|
+
line per change is enough.
|
|
379
|
+
|
|
380
|
+
Match the user's pace. A user who moves fast can approve three stages in one
|
|
381
|
+
message; a user who thinks slowly gets smaller steps. The route is there to be
|
|
382
|
+
walked, not recited.
|
|
383
|
+
|
|
384
|
+
## One conversation, end to end
|
|
385
|
+
|
|
386
|
+
A compressed version of the route, to show the size of each step:
|
|
387
|
+
|
|
388
|
+
> **User:** I want a small tool for freelancers to manage invoices. Create,
|
|
389
|
+
> chase, see what got paid.
|
|
390
|
+
>
|
|
391
|
+
> **Agent:** A tool where a freelancer sends an invoice, chases it, and sees
|
|
392
|
+
> what is overdue — without leaving the app. First version: Home (what needs
|
|
393
|
+
> doing today), Invoice (one invoice and its status), Chase (the reminder).
|
|
394
|
+
> Empty and loading states for the list; sample data, no backend yet. Out of
|
|
395
|
+
> scope: settings, teams, exports. Is that the first version, or is something
|
|
396
|
+
> missing?
|
|
397
|
+
>
|
|
398
|
+
> **User:** That's it. Design first, then code.
|
|
399
|
+
>
|
|
400
|
+
> **Agent:** I'm taking the screens now — the three above, one direction: warm
|
|
401
|
+
> neutrals, one accent for money, a 16px type scale, 12px cards. Screens first,
|
|
402
|
+
> your approval, then the code. Opening the canvas.
|
|
403
|
+
|
|
404
|
+
The agent creates the file, starts the canvas, and builds page by page. The user
|
|
405
|
+
selects a total, presses ⌘C, pastes the pointer, and says "make this bigger and
|
|
406
|
+
bold". The agent runs `show`, applies one `set`, and says what changed. When the
|
|
407
|
+
screens validate clean it asks for approval, exports the pages, and implements
|
|
408
|
+
them with the file as the spec.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Element reference
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/gen-references.mjs from lib/table.mjs. Do not edit. -->
|
|
4
|
+
|
|
5
|
+
## The file
|
|
6
|
+
|
|
7
|
+
A design file is valid, self-contained HTML. It carries `<meta name="design-schema" content="3.0">`
|
|
8
|
+
and exactly two `<style>` blocks, both in `<head>`:
|
|
9
|
+
|
|
10
|
+
- the token block: `:root{ --color-bg:#0b0d12 }`
|
|
11
|
+
- the page-frame block: `section[data-type="page"]{ margin:0 0 24px }`
|
|
12
|
+
|
|
13
|
+
Everything else is inline in a `style` attribute. One file holds many pages; a page is
|
|
14
|
+
`<section data-type="page">`.
|
|
15
|
+
|
|
16
|
+
## Every element
|
|
17
|
+
|
|
18
|
+
| Attribute | Required | Rule |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `data-id` | yes | Unique in the whole file. Matches `^[A-Za-z_][A-Za-z0-9_-]{0,63}$`. |
|
|
21
|
+
| `data-type` | yes | Must match the tag. See the table below. |
|
|
22
|
+
|
|
23
|
+
Optional on any element:
|
|
24
|
+
|
|
25
|
+
| Attribute | Rule |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `data-name` | A display name. For people, and for the layer tree. |
|
|
28
|
+
| `data-hidden` | `"true"` hides the element while keeping it in the file. |
|
|
29
|
+
| `data-locked` | `"true"` stops the canvas moving it. It produces no CSS. |
|
|
30
|
+
|
|
31
|
+
## The 6 types
|
|
32
|
+
|
|
33
|
+
| `data-type` | Tag | Required attributes | May contain elements |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `page` | `<section>` | — | yes |
|
|
36
|
+
| `frame` | `<div>` | — | yes |
|
|
37
|
+
| `text` | `<p>` `<h1>` `<h2>` `<h3>` `<h4>` `<h5>` `<h6>` `<span>` | — | no |
|
|
38
|
+
| `image` | `<img>` | `src`, `alt` | no |
|
|
39
|
+
| `shape` | `<div>` | `data-kind` | no |
|
|
40
|
+
| `icon` | `<svg>` | `data-icon`, `data-icon-library` | no |
|
|
41
|
+
|
|
42
|
+
Additional attributes by type:
|
|
43
|
+
|
|
44
|
+
| `data-type` | Attribute | Rule |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `page` | `data-canvas` | `"x,y"`, the position on the canvas. |
|
|
47
|
+
| `shape` | `data-kind` | One of: `rect`, `ellipse`. |
|
|
48
|
+
| `icon` | `data-icon` | The glyph name. |
|
|
49
|
+
| `icon` | `data-icon-library` | One of: `lucide`. |
|
|
50
|
+
| `image` | `data-credit` | Attribution. Never rendered. |
|
|
51
|
+
| `icon` | `viewBox` | The grid the geometry was drawn on, normally `0 0 24 24`. Required as soon as the icon has geometry: without it the SVG has no coordinate system, so a browser clips the geometry instead of scaling it. |
|
|
52
|
+
| `icon` | `fill`, `stroke`, `stroke-width` | Presentation attributes on the `<svg>` element itself, which the base stroke rule reads. |
|
|
53
|
+
| `icon` | `data-fill` | Opts out of the base stroke rule, so the geometry's own `fill` and `stroke` win. |
|
|
54
|
+
|
|
55
|
+
## A tag maps to a default type
|
|
56
|
+
|
|
57
|
+
Useful when the file omits `data-type`, which is an error, so the message can name the
|
|
58
|
+
type the tag implied.
|
|
59
|
+
|
|
60
|
+
| Tag | Default `data-type` |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `<section>` | `page` |
|
|
63
|
+
| `<div>` | `frame` |
|
|
64
|
+
| `<p>` | `text` |
|
|
65
|
+
| `<h1>` | `text` |
|
|
66
|
+
| `<h2>` | `text` |
|
|
67
|
+
| `<h3>` | `text` |
|
|
68
|
+
| `<h4>` | `text` |
|
|
69
|
+
| `<h5>` | `text` |
|
|
70
|
+
| `<h6>` | `text` |
|
|
71
|
+
| `<span>` | `text` |
|
|
72
|
+
| `<img>` | `image` |
|
|
73
|
+
| `<svg>` | `icon` |
|
|
74
|
+
|
|
75
|
+
## Icons
|
|
76
|
+
|
|
77
|
+
The built-in set is `lucide` with 67 glyphs. An `icon` element holds its own geometry,
|
|
78
|
+
so it needs no network request and no icon font:
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<svg data-id="home__icon" data-type="icon" data-icon="shield-check" data-icon-library="lucide"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
style="width:24px;height:24px;color:var(--color-ink)">
|
|
84
|
+
<path d="M12 2l8 4v6c0 5-8 10-8 10S4 17 4 12V6z"></path>
|
|
85
|
+
</svg>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Everything inside an `<svg>` is the opaque layer: kept byte for byte, never indexed, never
|
|
89
|
+
validated as a design element. An id inside an `<svg>` is an error, because nothing can
|
|
90
|
+
address it.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Styling reference
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/gen-references.mjs from lib/table.mjs. Do not edit. -->
|
|
4
|
+
|
|
5
|
+
Every element styles itself with a `style` attribute. These 47 properties are the
|
|
6
|
+
whole set. A property outside this list is refused by name, with the replacement named
|
|
7
|
+
when there is one.
|
|
8
|
+
|
|
9
|
+
Values are written in the canonical form, and a write reports every value it
|
|
10
|
+
rewrote. The short version: six or eight digit lower case hex, `px` lengths with zero
|
|
11
|
+
written `0`, `gap` and never `margin`, and longhand font properties.
|
|
12
|
+
|
|
13
|
+
## The properties
|
|
14
|
+
|
|
15
|
+
| Property | Accepted values |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `display` | flex \| block \| none |
|
|
18
|
+
| `flex-direction` | row \| column |
|
|
19
|
+
| `flex-wrap` | wrap \| nowrap |
|
|
20
|
+
| `align-items` | flex-start \| center \| flex-end \| stretch \| baseline |
|
|
21
|
+
| `justify-content` | flex-start \| center \| flex-end \| space-between \| space-around |
|
|
22
|
+
| `gap` | 12px \| var(--space-md) |
|
|
23
|
+
| `padding` | 16px \| 16px 24px \| 16px 24px 8px 24px |
|
|
24
|
+
| `position` | absolute \| relative |
|
|
25
|
+
| `left` | -8px \| 0 \| var(--space-sm) |
|
|
26
|
+
| `top` | -8px \| 0 \| var(--space-sm) |
|
|
27
|
+
| `width` | 236px \| 100% \| 50% \| fit-content \| 100vh \| 100vw |
|
|
28
|
+
| `height` | 844px \| 100% \| 50% \| fit-content \| 100vh \| 100vw |
|
|
29
|
+
| `min-width` | 0 \| 200px |
|
|
30
|
+
| `max-width` | 480px \| 100% |
|
|
31
|
+
| `min-height` | 0 \| 44px |
|
|
32
|
+
| `max-height` | 100% \| 640px |
|
|
33
|
+
| `flex` | 1 1 0% \| 0 0 auto \| 1 1 auto |
|
|
34
|
+
| `flex-shrink` | 0 \| 1 |
|
|
35
|
+
| `align-self` | flex-start \| center \| flex-end \| stretch |
|
|
36
|
+
| `margin` | 0 \| -8px \| 16px 0 |
|
|
37
|
+
| `background-color` | #ffffff \| #ffffffee \| transparent \| var(--color-bg) |
|
|
38
|
+
| `border` | 1px solid #e5e7eb \| var(--border-hairline) |
|
|
39
|
+
| `border-width` | 1px \| 1px 0 1px 0 |
|
|
40
|
+
| `border-style` | solid \| dashed \| dotted |
|
|
41
|
+
| `border-color` | #e5e7eb \| var(--color-line) |
|
|
42
|
+
| `border-radius` | 12px \| 50% \| var(--radius-lg) |
|
|
43
|
+
| `box-shadow` | 0 6px 16px #0000001f \| 0 1px 2px #0000001a, 0 4px 8px #00000014 |
|
|
44
|
+
| `opacity` | 0 \| 0.5 \| 1 |
|
|
45
|
+
| `backdrop-filter` | blur(20px) \| blur(20px) saturate(1.4) |
|
|
46
|
+
| `transform` | rotate(-1.5deg) \| none |
|
|
47
|
+
| `overflow` | hidden \| visible \| auto \| scroll |
|
|
48
|
+
| `outline` | 1px solid #b8325e |
|
|
49
|
+
| `outline-offset` | -2px \| 2px |
|
|
50
|
+
| `font-family` | Inter, system-ui, sans-serif \| var(--font-body) |
|
|
51
|
+
| `font-size` | 14px \| var(--size-body) |
|
|
52
|
+
| `font-weight` | 400 \| 500 \| 600 \| 700 \| bold |
|
|
53
|
+
| `font-style` | normal \| italic |
|
|
54
|
+
| `line-height` | 1.5 \| 20px |
|
|
55
|
+
| `letter-spacing` | 0.02em \| 1px |
|
|
56
|
+
| `color` | #111827 \| var(--color-ink) |
|
|
57
|
+
| `text-align` | left \| center \| right |
|
|
58
|
+
| `text-transform` | uppercase \| lowercase \| capitalize \| none |
|
|
59
|
+
| `white-space` | normal \| nowrap \| pre-wrap \| pre-line |
|
|
60
|
+
| `object-fit` | cover \| contain \| fill |
|
|
61
|
+
| `background-image` | url("photo.png") \| linear-gradient(180deg, #00000000, #000000b3) \| var(--img-hero) |
|
|
62
|
+
| `background-size` | cover \| contain \| auto \| 26px 26px |
|
|
63
|
+
| `background-position` | center \| top \| bottom \| left \| right |
|
|
64
|
+
|
|
65
|
+
## Value forms
|
|
66
|
+
|
|
67
|
+
| Property | Kind | Notes |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `display` | enum | One of: `flex`, `block`, `none`. |
|
|
70
|
+
| `flex-direction` | enum | One of: `row`, `column`. |
|
|
71
|
+
| `flex-wrap` | enum | One of: `wrap`, `nowrap`. |
|
|
72
|
+
| `align-items` | keyword-set | One of: `flex-start`, `center`, `flex-end`, `stretch`, `baseline`. |
|
|
73
|
+
| `justify-content` | keyword-set | One of: `flex-start`, `center`, `flex-end`, `space-between`, `space-around`. |
|
|
74
|
+
| `gap` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
75
|
+
| `padding` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
76
|
+
| `position` | enum | One of: `absolute`, `relative`. |
|
|
77
|
+
| `left` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
78
|
+
| `top` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
79
|
+
| `width` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
80
|
+
| `height` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
81
|
+
| `min-width` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
82
|
+
| `max-width` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
83
|
+
| `min-height` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
84
|
+
| `max-height` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
85
|
+
| `flex` | flex | `1 1 0%` fills the main axis. A bare number becomes `n 1 0%`. |
|
|
86
|
+
| `flex-shrink` | number | Between 0 and 10. |
|
|
87
|
+
| `align-self` | keyword-set | One of: `flex-start`, `center`, `flex-end`, `stretch`. |
|
|
88
|
+
| `margin` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
89
|
+
| `background-color` | color | Six or eight digit hex, lower case. `transparent` is allowed. `rgb()` and `rgba()` are rewritten to hex. |
|
|
90
|
+
| `border` | border | Width, style, colour, in that order however they were written. |
|
|
91
|
+
| `border-width` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
92
|
+
| `border-style` | keyword-set | One of: `solid`, `dashed`, `dotted`. |
|
|
93
|
+
| `border-color` | color | Six or eight digit hex, lower case. `transparent` is allowed. `rgb()` and `rgba()` are rewritten to hex. |
|
|
94
|
+
| `border-radius` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
95
|
+
| `box-shadow` | shadow | `x y blur [spread] colour`, one or more separated by commas. Inner shadows are refused. |
|
|
96
|
+
| `opacity` | number | Between 0 and 1. |
|
|
97
|
+
| `backdrop-filter` | backdrop | `blur(<length>)` and, optionally, `saturate(<number>)`, in that order. `none` is allowed. |
|
|
98
|
+
| `transform` | transform | `rotate(<deg>)` or `none`. The angle is a literal number of degrees; `translate()` and `scale()` are refused, and so is a `var()`. |
|
|
99
|
+
| `overflow` | enum | One of: `hidden`, `visible`, `auto`, `scroll`. |
|
|
100
|
+
| `outline` | outline | — |
|
|
101
|
+
| `outline-offset` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
102
|
+
| `font-family` | text | Taken as written. |
|
|
103
|
+
| `font-size` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
104
|
+
| `font-weight` | font-weight | `400`, `500`, `600`, `700`. The words `normal`, `medium`, `semibold` and `bold` are rewritten to numbers. |
|
|
105
|
+
| `font-style` | enum | One of: `normal`, `italic`. |
|
|
106
|
+
| `line-height` | line-height | A ratio such as `1.5`, or a length. |
|
|
107
|
+
| `letter-spacing` | length | `px`, `%`, `em`, `rem`, `vh`, `vw`, or a keyword such as `auto` and `fit-content`. Zero is `0`. |
|
|
108
|
+
| `color` | color | Six or eight digit hex, lower case. `transparent` is allowed. `rgb()` and `rgba()` are rewritten to hex. |
|
|
109
|
+
| `text-align` | enum | One of: `left`, `center`, `right`. |
|
|
110
|
+
| `text-transform` | enum | One of: `uppercase`, `lowercase`, `capitalize`, `none`. |
|
|
111
|
+
| `white-space` | enum | One of: `normal`, `nowrap`, `pre-wrap`, `pre-line`. |
|
|
112
|
+
| `object-fit` | enum | One of: `cover`, `contain`, `fill`. |
|
|
113
|
+
| `background-image` | image | `url("...")`, `linear-gradient(...)` or `radial-gradient(...)`. The url argument is always quoted, so a `;` inside it cannot split the declaration. |
|
|
114
|
+
| `background-size` | size | `cover`, `contain`, `auto`, or one or two lengths such as `26px 26px`. A trailing `auto` is dropped. |
|
|
115
|
+
| `background-position` | keyword-set | One of: `center`, `top`, `bottom`, `left`, `right`. |
|
|
116
|
+
|
|
117
|
+
## Aliases
|
|
118
|
+
|
|
119
|
+
An alias is accepted and rewritten. The file ends up holding the target.
|
|
120
|
+
|
|
121
|
+
| Written | Becomes |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `background` | `background-color` |
|
|
124
|
+
| `background-colour` | `background-color` |
|
|
125
|
+
| `bg-color` | `background-color` |
|
|
126
|
+
| `-webkit-backdrop-filter` | `backdrop-filter` |
|
|
127
|
+
| `-webkit-transform` | `transform` |
|
|
128
|
+
|
|
129
|
+
## Refused properties
|
|
130
|
+
|
|
131
|
+
Each one is refused with the reason below, so a refusal tells you what to write
|
|
132
|
+
instead.
|
|
133
|
+
|
|
134
|
+
| Property | Reason | Write instead |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `animation` | No animation. A design file is a still. | — |
|
|
137
|
+
| `border-bottom` | Use border-width with four values for one edge. | `border-width: 0 0 1px 0` |
|
|
138
|
+
| `border-left` | Use border-width with four values for one edge. | `border-width: 0 0 0 1px` |
|
|
139
|
+
| `border-right` | Use border-width with four values for one edge. | `border-width: 0 1px 0 0` |
|
|
140
|
+
| `border-top` | Use border-width with four values for one edge. | `border-width: 1px 0 0 0` |
|
|
141
|
+
| `cursor` | The format describes appearance, not interaction. A cursor is behavior. | — |
|
|
142
|
+
| `font` | Write the longhand properties. | `font-family, font-size, font-weight and line-height` |
|
|
143
|
+
| `grid-gap` | The property is named gap. | `gap` |
|
|
144
|
+
| `grid-template-columns` | The format has no grid. Flex covers the same layouts. | `display:flex with flex:1 on the children` |
|
|
145
|
+
| `grid-template-rows` | The format has no grid. Flex covers the same layouts. | `display:flex with flex:1 on the children` |
|
|
146
|
+
| `margin-bottom` | Write the shorthand: margin: 0 0 8px 0. | `gap on the parent, or margin on this element` |
|
|
147
|
+
| `margin-left` | Write the shorthand: margin: 0 0 0 8px. | `gap on the parent, or margin on this element` |
|
|
148
|
+
| `margin-right` | Write the shorthand: margin: 0 8px 0 0. | `gap on the parent, or margin on this element` |
|
|
149
|
+
| `margin-top` | A one-sided margin is a second spelling of the same intent. | `margin with four values` |
|
|
150
|
+
| `rotate` | A rotation is written as transform: rotate(), so there is one spelling, not two. | `transform: rotate(<deg>)` |
|
|
151
|
+
| `text-decoration` | Not in the format. Use a border-width on the bottom edge for an underline. | — |
|
|
152
|
+
| `transition` | No animation. A design file is a still. | — |
|
|
153
|
+
| `z-index` | Paint order follows document order. Move the element instead. | `document order` |
|
|
154
|
+
|
|
155
|
+
## Custom properties
|
|
156
|
+
|
|
157
|
+
A declaration whose name starts with `--` is always accepted. That is how a design
|
|
158
|
+
carries a local value without a token for it.
|
|
159
|
+
|
|
160
|
+
## Tokens
|
|
161
|
+
|
|
162
|
+
Tokens are declared once, in the `<head>`:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<style>:root{ --color-bg:#0b0d12; --space-md:16px }</style>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The group is the text before the first hyphen, and it must be one of: `color`, `font`, `space`, `radius`, `size`, `shadow`, `border`.
|
|
169
|
+
|
|
170
|
+
| Group | Unit a number carries |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `color` | none (a colour, a font stack, a shadow) |
|
|
173
|
+
| `font` | none (a colour, a font stack, a shadow) |
|
|
174
|
+
| `space` | px |
|
|
175
|
+
| `radius` | px |
|
|
176
|
+
| `size` | px |
|
|
177
|
+
| `shadow` | none (a colour, a font stack, a shadow) |
|
|
178
|
+
| `border` | none (a colour, a font stack, a shadow) |
|
|
179
|
+
|
|
180
|
+
Reference a token with `var(--name)`. `export --resolve-tokens` bakes the values in
|
|
181
|
+
instead, so the deliverable needs no token block.
|