Repository navigation
Design tokens
Q203 settled the new look in tools/Lightbox.Gallery, and the owner put the
order plainly: "the most important part of actually building it, is
tokenizing." This document records how the app gets there without a step
nobody can review.
Measured on 2026-10-07, the app's views and styles carried:
| What | Literals | Distinct values |
|---|---|---|
FontSize |
458 | 7 (11, 12 and 10 cover 98%) |
Spacing |
366 | 12 (6, 4, 8 and 2 dominate) |
Margin and Padding
|
428 | 101 |
Width and Height
|
574 | many |
CornerRadius |
59 | 6 |
| Colour references | 352 | 132 keys, all StaticResource
|
868 of them are in MainWindow.axaml. A new look applied on top of that is one
more layer of numbers. Nothing would say which inset is "the panel's" and which
is a one-off, and the gallery showed that the look only holds together when
there is one answer.
-
Foundation, no visual change.
Styles/Tokens.axamlnames a type scale, a spacing scale, the density sizes, radii by role and the icon line, all at today's values. The style files read from it. EveryFontSizeand stackSpacingin the views that sits on the scale becomes a token; this is mechanical, because each value maps to exactly one token. Every brush reference becomes aDynamicResource, which is what lets a theme switch live later.TokenRatchetTestscounts the literals left in each file against.claude/quality/ratchets/literals.json. A count may fall and may never rise, so a new literal fails the build and the count only goes down. -
Views by area, no visual change. Margins, paddings, sizes and radii move
to role tokens, one area per pull request: the dockers, Configure, the
project window, then the dialogs. These need judgement rather than a script,
because
4,0is a label gap in one place and a button inset in another. - The look. The token values switch to Dark-lit with Tight corners. The gallery's treatments move into the app's styles, Studio grey becomes a theme dictionary, and Configure → Interface gets a live theme switch. Because steps 1 and 2 did the work, this is a small diff that changes every pixel.
Steps 1 and 2 change nothing on screen, and that is what makes them reviewable. A token that holds the wrong value shows as a visible difference, and there should be none.
-
Colours are
DynamicResource; sizes areStaticResource. A colour is what a theme changes while the app runs. A size changes through the interface scale (a layout transform), never by swapping a value, so it can stay resolved once. -
Only brushes move, not raw colours. A
Colorused inside a brush definition or a gradient stop is not on a control in the tree, so a dynamic reference there would not resolve. Those stay static until step 3 gives the palette theme dictionaries. -
Off-scale values stay literal and are counted.
FontSize="9"andSpacing="3"have no token on purpose. Inventing a token for an outlier gives it legitimacy; leaving it counted means step 2 has to decide about it. -
The scale is named by size; the roles come in step 2.
Space6is a number.PanelPaddingis a decision, and decisions belong with the views that make them. -
Code-behind is out of scope here.
new Thickness(…)in C# is not counted yet. The gallery's stories are the one place that does this deliberately.
| Step | Literals left | What it moved |
|---|---|---|
| 1 · foundation | 1,239 | type scale, spacing scale, density sizes, dynamic brushes |
| 2 · dockers | 912 | setting rows (label, slider gap, value floor), icon sizes, section gaps — the docker part of MainWindow.axaml and the panels it hosts (tool option pages, effects, guides, scene) |
| 2 · Configure | 865 | the settings page inset, the settings field and combo widths, the information card's corners and inset |
| 2 · windows and dialogs | 748 | the dialog inset, label column, small field and button floor, the wider gaps, card radii — the project window and 25 dialogs, panels and bars |
| 3 · values | 746 | the new look's values: Tight corners (and Fluent's own two radii), one docker label width, one gap scale (4 and 8), the 10 px floor, a 1.25 icon line |
| 3 · themes | 746 | Dark-lit and Studio grey: the chrome's brushes in two theme dictionaries, switched live from Configure |
| 3 · treatments | 682 | the gallery's treatments in the app's own styles: flat buttons, one "on" fill, v.2 tabs (panel and document), docker islands, the edge light, the icons-only canvas bar; every literal radius onto a role; code-drawn chrome (timeline, rulers, graph, curve editor, canvas surround) reading the theme |
How each area proves "no visual change": the gallery's --snapshot-app
renders the main window empty and with a document open, every docker brought to
the front one at a time, every tool's options page and every Configure page —
44 images. They are compared pixel by pixel against the previous step with the
status line masked: it carries live readings. The comparison must first catch a
deliberate one-pixel break in a token the area introduced; for the dockers,
SizeDockerLabel at 91 showed in 15 of the 44.
What the pixels do not reach, and what covers it. The snapshot shows each
docker and tool page in its default state on a fresh document. A selected bone,
an armature, a populated effect stack, a camera, a text selection, an expanded
section, a popup or a hover is never on screen. Those lines are covered by the
second half of the evidence: every changed line pairs one-for-one with its old
form, and differs only in an attribute value replaced by a token holding the same
number and the same type. A static resource resolves to that number whether or
not its element is visible. scripts/tokenpairs.py <base> checks that pairing by
script, resolving tokens on both sides, and is run on every area: 303 pairs for
the dockers and 41 for Configure, no mismatch. It was shown to catch a planted
swap of one token for another of a different value.
These are decisions rather than mechanics, so they stay literal and counted until the look sets them:
-
One docker label width. Two widths are in use today:
SizeDockerLabel(90) andSizeDockerLabelWide(100). -
One fixed field width. 56, 68 and 72 are still literal, and EffectsPanel's
rows use 86-wide labels and 52-wide values (
EffectsPanel.axamlColumnDefinitions). - Fewer icon sizes. 8, 9, 10 and 11 are all in use.
- 9 px text (EffectsPanel shelf names and warnings) is below DESIGN.md's floor of 10. It is a question, not a token.
-
ScenePanel's depth row (
*,58,90) needs a role for its diagram and field columns. -
Gaps on one scale.
GapBefore/GapAfter(4) and theirWidetwins (6), andSectionGap(8) andSectionGapWide(10), are two tiers or one. Decide which. -
Label columns. Three widths are in use:
SizeDialogLabel(110),SizeDockerLabel(90) andSizeDockerLabelWide(100). Collapse them, or write down the rule that picks one. -
Previews.
RadiusPreview(3) covers image thumbnails and previews. Do they go square, like the rest of the artwork? -
Leftovers in the windows:
Padding="8,6"(BrushLibraryWindow, VideoExportWindow),Padding="4,0"(VersionHistoryWindow's milestone badge),Margin="24,0,0,0"(PlacementChoiceDialog's indent).
Step 3 changes what the app looks like on purpose, so its evidence is the snapshots read side by side in both themes rather than a pixel diff of zero. What it chose in the gallery and has not yet built, because each needs view work rather than a style:
-
Effect sliders for opacity and flow (a track that pictures its value) and
segmented selectors for a handful of choices. Both exist only as gallery
prototypes (
tools/Lightbox.Gallery/Looks/Controls.axaml). - The effects switch. The gallery could turn the soft shadows off; the app has them on and no preference yet.
- One fixed field width, fewer icon sizes, ScenePanel's depth row and the window leftovers above are still literal and counted.
Nothing new. Tokens are compiled resources in the app's own assembly, and no document or settings value reaches them. The interface scale already normalises its one user-supplied number.
- Getting started
- Documents and projects
- Tools and strokes
- Brushes
- Colour
- Layers, selections and guides
- The timeline
- Onion skin, references and the camera
- Symbols
- Saving and recovery
- Exporting to a game engine
- AI assistance
- Keyboard, performance and what is planned
- Bones and rigging
- Effects — fire, smoke and water
- A simplified 3D space to draw in: planes, parallax first, orbit second
- Brush performance beyond 4k: what the GPU can and cannot buy us
- The add-on boundary
- Making the AI trustworthy
- What an AI request actually costs
- Bones and spline deformers for 2D — design
- Brush tips — a generated library, not a live calculation
- Cloud and SaaS: what the architecture already fits, and what it does not
- Colour mixing — paint that picks up what it is laid on
- DESIGN: Effects
- DESIGN: Fluid effects — fire, smoke and water as drawn line and fill
- Fluid and viscous media — notes for the next brush-engine pass
- Compositing on the GPU
- Decomposing the two big files: which pieces come out, and with which tool
- One memory limit for pictures
- .NET 10: what is safe, what is unverified, and what it narrows
- One picture cache: the paused canvas on tiles
- The paint path: one mark, made once, under the pen
- Pen dynamics: tilt and speed
- A pencil is graphite on the paper's tooth
- Performance: the ratchet and the map
- Playback's first loop
- Pressure curves — a shape, not an exponent
- Importing Procreate brushes
- A project as a game's asset set
- One hierarchy: folders scope everything
- Puppet pins — design and plan
- The raster checkpoint: making a painting cheap to reopen
- Scoped resources — palettes, references and everything else shared
- Sensitive topics, and the three-track flow
- Design: pose-assisted frame-by-frame animation (rigid bones + AI gap filling)
- Stroke groups: why a merge should never turn strokes into pixels
- The project window: what you do between drawings
- Reading the subject before drawing on it
- The ceiling of a swept soft mark (B349)
- Q171 — a symbol owns a layer stack
- Pillar 3 — reusable assets, as live symbols
- What a template is
- Text, and where fonts sit
- The tiled compositor, and getting it off the CPU
- Design tokens
- Transform tool design (M14)
- Why a brush stamps twice, and when it does not
- Migrating to the designed UI
- Undo stores only what changed
- Vector tooling: making the lines you already drew editable
- The smudge/blur family, and aligning brush settings with Photoshop
Generated from the repository. Edit the files under docs/, not these pages.