@lotics/ui 43.4.0 → 43.5.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/AGENTS.md +3 -2
- package/MIGRATION.md +93 -0
- package/docs/catalog.md +128 -26
- package/docs/composition.md +380 -9
- package/docs/data_entry.md +25 -0
- package/docs/reviewing.md +477 -0
- package/docs/templates.md +9 -6
- package/examples/tpl_attendance.tsx +0 -1
- package/examples/tpl_item_list.tsx +109 -54
- package/examples/tpl_record.tsx +89 -4
- package/package.json +7 -3
- package/src/avatar.tsx +29 -29
- package/src/avatar.web.tsx +32 -31
- package/src/avatar_props.ts +66 -0
- package/src/avatar_tone.ts +79 -0
- package/src/button.tsx +36 -4
- package/src/checkbox.tsx +4 -1
- package/src/choice_list.tsx +5 -3
- package/src/color_tokens.ts +15 -14
- package/src/composer.tsx +3 -3
- package/src/control_surface.ts +37 -9
- package/src/copy_button.tsx +8 -1
- package/src/counter.tsx +1 -1
- package/src/data_grid.tsx +6 -3
- package/src/date_calendar.tsx +4 -2
- package/src/date_range_filter_field.tsx +5 -1
- package/src/date_segments_field.tsx +2 -2
- package/src/file_drop_target.web.tsx +2 -2
- package/src/file_dropzone.tsx +12 -6
- package/src/file_rows.tsx +22 -2
- package/src/file_thumbnail.tsx +19 -3
- package/src/font_family.ts +1 -1
- package/src/font_family.web.ts +1 -1
- package/src/funnel.tsx +1 -1
- package/src/icon_button.tsx +5 -2
- package/src/index.css +22 -16
- package/src/inline_edit.tsx +9 -10
- package/src/inline_files.tsx +6 -0
- package/src/json_panel.tsx +1 -1
- package/src/kpi_card.tsx +1 -1
- package/src/locale.tsx +1 -1
- package/src/markdown.css +5 -1
- package/src/metric.tsx +12 -18
- package/src/number_input.tsx +2 -2
- package/src/option_picker.tsx +57 -0
- package/src/picker.tsx +2 -2
- package/src/pressable_row.tsx +15 -5
- package/src/progress_bar.tsx +1 -1
- package/src/radio_picker.tsx +2 -1
- package/src/search_input.tsx +12 -11
- package/src/sort_header.tsx +7 -3
- package/src/stacked_progress_bar.tsx +1 -1
- package/src/step_progress.tsx +5 -2
- package/src/switch.tsx +11 -6
- package/src/table.tsx +148 -16
- package/src/table_fit.ts +12 -0
- package/src/tabs.tsx +13 -1
- package/src/text.css +50 -18
- package/src/text.tsx +37 -40
- package/src/text_input_field.tsx +2 -2
- package/src/text_utils.ts +48 -6
- package/src/theme.ts +13 -0
- package/src/theme.web.ts +49 -0
- package/src/theme_vars.ts +113 -0
- package/src/type_ramp.ts +100 -0
- package/src/theme.tsx +0 -24
- package/src/theme.web.tsx +0 -79
- package/src/theme_context.ts +0 -107
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Reviewing a screen you built
|
|
2
|
+
|
|
3
|
+
The other docs say what good looks like. This one says how to find out whether you achieved it.
|
|
4
|
+
|
|
5
|
+
That gap is the reason this file exists. An agent cannot see the screen it just wrote any better
|
|
6
|
+
than it can see a bug it just introduced — "it looks fine" is the same claim as "it should work",
|
|
7
|
+
made with the same evidence. Every rule in [composition.md](./composition.md) was written the last
|
|
8
|
+
time it was broken, by someone who had read it. So the discipline is not *know the rules*, it is
|
|
9
|
+
**render it, measure it, and let the numbers say whether the rules held**.
|
|
10
|
+
|
|
11
|
+
Everything here is about a screen **that already renders**. Building one starts at
|
|
12
|
+
[catalog.md](./catalog.md) (which component) and [templates.md](./templates.md) (which shape).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The method
|
|
17
|
+
|
|
18
|
+
**"Looks off" is a hypothesis, not a finding.** An impression cannot be compared across a screen
|
|
19
|
+
or re-checked after a fix. Extract computed values and let the table show the defect. Every
|
|
20
|
+
finding cites numbers.
|
|
21
|
+
|
|
22
|
+
1. **Render it** — `lotics app dev` for an app, a preview route for a screen. Drive it with
|
|
23
|
+
browser automation that can evaluate JS in the page.
|
|
24
|
+
2. **Run the two gates**, then the probes.
|
|
25
|
+
3. **Fix, then re-measure.** A design fix is verified the way it was found.
|
|
26
|
+
4. **Screenshot too.** Numbers say *what* is wrong; a picture says *that* something is, and some
|
|
27
|
+
defects live only in the picture — a mark floating over a tile that never loaded measures
|
|
28
|
+
perfectly and is obviously broken on sight. Take it even when the numbers are clean.
|
|
29
|
+
|
|
30
|
+
**Map raw CSS back to the ladder as you go.** A finding reads as `xs/500/muted`, not
|
|
31
|
+
`12px/500/rgb(82,82,91)`, so the treatment classes are visible at a glance. The rungs are in
|
|
32
|
+
[`type_ramp`](../src/type_ramp.ts); above 768px `lg`/`xl`/`xxl`/`xxxl` are 20/24/32/48 and below
|
|
33
|
+
it 18/22/28/32, so the same rung reports different pixels at the two widths — resolve against the
|
|
34
|
+
width you are measuring.
|
|
35
|
+
|
|
36
|
+
**Widths: measure at 1280 and 375.** Treatment bugs are width-independent on a screen that merely
|
|
37
|
+
REFLOWS — but a screen that FORKS (`useScreenSize().small`, a container query, a different
|
|
38
|
+
component per bucket) is a second screen running code the other width never executes, so a plain
|
|
39
|
+
defect can sit there forever while every measurement at the other width passes. Audit each fork as
|
|
40
|
+
its own screen.
|
|
41
|
+
|
|
42
|
+
**Read the width the component actually RECEIVES, never the viewport you set.** A gallery or docs
|
|
43
|
+
shell wraps it in its own nav, so "1280" may be rendering at 1060 and every conclusion is wrong in
|
|
44
|
+
the same direction. The kit measures the CONTAINER, not the screen — see AGENTS.md, Iron rules.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Two gates, before any probe
|
|
49
|
+
|
|
50
|
+
A perfectly-treated element that should not exist still measures clean, and a wrong control cannot
|
|
51
|
+
be treated into a right one. Both of these are cheap and both outrank everything below.
|
|
52
|
+
|
|
53
|
+
### 1. SUBTRACT
|
|
54
|
+
|
|
55
|
+
Per element, ask what QUESTION it answers and who is asking. A value nobody compares across rows
|
|
56
|
+
is not a column; a figure the summary already totals does not need repeating per row; a fact
|
|
57
|
+
derivable from its neighbour is a second copy that can disagree. Cut it, then re-ask whether the
|
|
58
|
+
screen got worse.
|
|
59
|
+
|
|
60
|
+
*The tell that you are subtracting too little:* reaching for a smaller size, a tighter gap, a
|
|
61
|
+
truncation or an abbreviation — each makes the excess harder to read instead of removing it.
|
|
62
|
+
"Cramped" is almost always COUNT, which no width fixes.
|
|
63
|
+
|
|
64
|
+
*Then count FACTS, not elements:* three well-made blocks all reporting that someone was absent is
|
|
65
|
+
one fact three times. → [composition.md](./composition.md) §"Touch & whitespace" (the trim test).
|
|
66
|
+
|
|
67
|
+
### 2. RIGHT CONTROL
|
|
68
|
+
|
|
69
|
+
Name each control's SPECIES and justify it — against the ladder in
|
|
70
|
+
[data_entry.md](./data_entry.md) §"Choosing a CHOICE control" (by option count, not taste), then
|
|
71
|
+
against the `tpl_*` that already solved this shape.
|
|
72
|
+
|
|
73
|
+
A boolean is a switch, not a popover holding one checkbox. An exclusive choice belongs only where
|
|
74
|
+
the states really are exclusive — *independent capabilities that happen to be exclusive today*
|
|
75
|
+
bake today's combinations into the type. And check the species is not furniture from another
|
|
76
|
+
ALTITUDE: a list-row disclosure nested in a list row, a section header serving as a field label.
|
|
77
|
+
Each reviews clean in isolation and reads as clutter in place.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## The probes
|
|
82
|
+
|
|
83
|
+
Each names what to COLLECT and the SIGNATURE — what the numbers look like when it is wrong. The
|
|
84
|
+
rule behind it lives in the area doc named beside it; this file never restates one.
|
|
85
|
+
|
|
86
|
+
### 1. Type inventory
|
|
87
|
+
**Collect** every text node as `rung / weight / ink`, with a sample of what wears each.
|
|
88
|
+
|
|
89
|
+
- **Range** = largest ÷ smallest. Under ~2× reads flat *at any amount of colour* — scale is the
|
|
90
|
+
one hierarchy device colour cannot substitute for. A register with no page band measures ~1.33×.
|
|
91
|
+
- **Weight is a font FAMILY here, not an axis.** Semibold is a separate, genuinely heavier file,
|
|
92
|
+
and the heading ramp is semibold at every rung — so semibold on a row subject, a value or a
|
|
93
|
+
label renders content at heading weight. State steps up in INK, never into the heading weight.
|
|
94
|
+
- **Same shape → same treatment.** Group every element by its visual SHAPE (label-over-value,
|
|
95
|
+
figure+caption, icon+row, chip) and check each group has ONE treatment. A reader parses shape
|
|
96
|
+
before meaning, so an unexplained difference reads as an accident however principled the reason.
|
|
97
|
+
- **Singletons.** A rung/weight/ink combination appearing ONCE either wants siblings or wants to
|
|
98
|
+
join an existing class. Pick the sibling set by STRUCTURAL ROLE, not visual resemblance.
|
|
99
|
+
- **Pairs.** For every primary+supporting pair, divide. A spread in the RATIOS is the finding, not
|
|
100
|
+
a spread in the sizes.
|
|
101
|
+
- **A label sharing a row with a flexible value: measure the LABEL across rows whose values differ
|
|
102
|
+
in LENGTH.** Whichever of the two cannot shrink forces the other to, so a value floored at its
|
|
103
|
+
longest word leaves the label absorbing every pixel and collapsing to a one-character column
|
|
104
|
+
many times taller than the row it names. Invisible wherever the values happen to fit — usually
|
|
105
|
+
the width you audited. The signature is the label's box CHANGING SHAPE row to row: collect width
|
|
106
|
+
and height per row and treat a spread as the finding, never the average.
|
|
107
|
+
- **Shrunk composites.** Where a component is sized by its container rather than its own scale,
|
|
108
|
+
measure the type INSIDE it, not the box — a badge dropped into a cell can scale its own label to
|
|
109
|
+
five pixels, internally consistent and invisible to every treatment probe. The fix is rarely a
|
|
110
|
+
bigger box; ask what the thing IS on that surface. But separate unreadable TYPE from unreadable
|
|
111
|
+
MARK before ruling: a badge's label never reaches the ladder at any row-scale size, so 5px
|
|
112
|
+
proves only that you measured a mark.
|
|
113
|
+
|
|
114
|
+
→ [composition.md](./composition.md) §"Typography", §"The heading ramp".
|
|
115
|
+
|
|
116
|
+
### 2. Ground, border and radius across siblings
|
|
117
|
+
**Collect** `background-color`, `border`, `border-radius` and height for every control in a group.
|
|
118
|
+
|
|
119
|
+
- **A singleton ground is a finding**, like a singleton type treatment: four buttons at `h=40`
|
|
120
|
+
with identical type, three with a ground and the destructive one transparent. Readers parse
|
|
121
|
+
box-vs-no-box before colour.
|
|
122
|
+
- **A SELECTED control needs a GROUND, not a heavier outline** — among white pills a 1px border
|
|
123
|
+
change is noise.
|
|
124
|
+
- **Tally chromatic grounds.** Disciplined is one or two hues plus greys. A hue with a single
|
|
125
|
+
member, from a family used nowhere else, is a library default nobody chose — the call site
|
|
126
|
+
usually mentions no colour at all.
|
|
127
|
+
- **A control riding a hoverable surface must be DARKER than it** — and hover BOTH to see it,
|
|
128
|
+
since reaching the control means crossing its host. Equal washes vanish exactly when pointed at;
|
|
129
|
+
a LIGHTER one reads as a hole rather than a target. Neither is visible at rest or when hovering
|
|
130
|
+
the control alone. `ROW_CONTROL_HOVER` / `ROW_CONTROL_PRESS` in `control_surface` are a step
|
|
131
|
+
past the row's own wash for exactly this; a hand-picked grey is the finding however plausible.
|
|
132
|
+
- **A pressable that does not NAVIGATE must not wear navigation ink.** Blue underlined text
|
|
133
|
+
promises a destination; a control opening a modal, a peek or an inline expansion is not one.
|
|
134
|
+
- **A semantic token passed where a raw value is expected fails SILENTLY and typechecks.** A prop
|
|
135
|
+
typed `color?: string` takes a CSS colour, not a design token, so `color="muted"` is an invalid
|
|
136
|
+
colour and the element falls back to full-strength ink while the compiler stays quiet. Diff the
|
|
137
|
+
computed `color`/`stroke` against the token you INTENDED, using neighbouring text that carries
|
|
138
|
+
the real token as the control.
|
|
139
|
+
- **A control drawing NOTHING at rest is text**, not a quiet control. Collect every pressable's
|
|
140
|
+
RESTING `background-color`, `border-width` and `text-decoration`: all three empty means the only
|
|
141
|
+
affordance is the hover wash, which is invisible to keyboard and touch. It measures perfectly
|
|
142
|
+
and has a real focus ring; what fails is that nobody presses it.
|
|
143
|
+
|
|
144
|
+
→ [composition.md](./composition.md) §"Color discipline", §"Buttons & action labels", §"Every number is a door", §"Hover grammar".
|
|
145
|
+
|
|
146
|
+
### 3. Rules — ask what each one SEPARATES
|
|
147
|
+
**Collect** every border/divider with weight, colour and width.
|
|
148
|
+
|
|
149
|
+
The test is not how big the break is. A rule between two rows divides **like from like** —
|
|
150
|
+
whitespace says that just as well, which is why a register can drop every internal rule and read
|
|
151
|
+
calmer. A rule under a column band divides **chrome from content**, and whitespace cannot say
|
|
152
|
+
that, because whitespace is already what separates the rows. Delete the like-from-like ones
|
|
153
|
+
however important the break feels. Backwards produces both failure modes: a grid of equal lines
|
|
154
|
+
that reads as a spreadsheet, and a lone survivor that becomes the loudest thing on a quiet surface.
|
|
155
|
+
|
|
156
|
+
Also: **the most important break must never be tighter than a routine one** — a hand-rolled rule
|
|
157
|
+
beside a `SectionStack` is the usual cause.
|
|
158
|
+
|
|
159
|
+
→ [composition.md](./composition.md) §"The register's rhythm".
|
|
160
|
+
|
|
161
|
+
### 4. Gaps — measure RATIOS, not sizes
|
|
162
|
+
**Collect** the gap WITHIN a group and BETWEEN groups, and divide. Near 1 is the defect however
|
|
163
|
+
reasonable each number looks alone — 10px inside and 12px between measures fine twice and still
|
|
164
|
+
reads as one cluster. One `gap` on a container makes every child a peer; wrap the group, then
|
|
165
|
+
space the groups. Worst where both items are small grey glyphs on one line, because nothing but
|
|
166
|
+
spacing distinguishes them.
|
|
167
|
+
|
|
168
|
+
→ [composition.md](./composition.md) §"Touch & whitespace".
|
|
169
|
+
|
|
170
|
+
### 5. State diff
|
|
171
|
+
**Collect** each control at rest, hover, focus, pressed and editing, then diff the boxes.
|
|
172
|
+
|
|
173
|
+
- A control is meant to change **paint** across its states and never **geometry**. So
|
|
174
|
+
`border`/`background`/`box-shadow` differing is the design; the box or the text moving is the
|
|
175
|
+
defect. Text that shifts, a border that thickens, a ring drawn twice, a tint on one path only —
|
|
176
|
+
two pieces of code own one property and happened to agree at rest. Every other probe reads a
|
|
177
|
+
RESTING screen, so all of them pass on a control that jumps 8px on focus. **Fix by giving the
|
|
178
|
+
property ONE owner** — a shared surface helper, a single frame kept across the mode swap — never
|
|
179
|
+
by nudging the second owner into agreement.
|
|
180
|
+
- **A themeable control must derive every state from its token.** Signature: correct at rest, a
|
|
181
|
+
fixed neighbouring shade under the pointer. Watch for an opaque overlay hiding a themed ground.
|
|
182
|
+
- **A prop's EFFECT must match its claim** — render each value and diff what actually moved
|
|
183
|
+
against what the prop says it changes. The fix is in the component, never the caller.
|
|
184
|
+
- **Measure a component against ITSELF under each optional prop** that should not change size — a
|
|
185
|
+
file row measured 37px static and 49px with an `onPress`, because only the pressable variant
|
|
186
|
+
carried the padding its wash needed. Same for a slot that may be empty.
|
|
187
|
+
|
|
188
|
+
→ [composition.md](./composition.md) §"Typography" (the `size` law), §"Hover grammar".
|
|
189
|
+
|
|
190
|
+
### 6. Alignment and drift
|
|
191
|
+
- **One left edge — the whole page, then the whole APP.** Headings, detail labels, prose, a
|
|
192
|
+
table's first column, a footer's totals, a dialog's title. Then measure the screen this one
|
|
193
|
+
OPENS: a list and its record are one reading column seen twice, and both must derive from ONE
|
|
194
|
+
constant rather than happening to match today. The offender is nearly always a row that padded
|
|
195
|
+
for its own wash instead of bleeding it — fix the row, then delete the compensators, which
|
|
196
|
+
become double-corrections the moment the row is right. Two exceptions are legitimate: a
|
|
197
|
+
self-contained overlay has nothing to align with, and an indent something VISIBLE occupies (a
|
|
198
|
+
checkbox, an ordinal, a mark) is explained.
|
|
199
|
+
- **A CENTRED child hides its own drift.** Centring puts a child's top at `(container − child)/2`,
|
|
200
|
+
so a child whose size varies with DATA moves its own first line between instances while the
|
|
201
|
+
container measures identical every time. It takes a container with SLACK to bite. Wherever a
|
|
202
|
+
container centres repeated children (table cells, list rows, toolbar controls, grid tiles),
|
|
203
|
+
collect the child's FIRST-line offset per instance; a constant container with a spread of
|
|
204
|
+
offsets is the signature. Fix by bounding the child that varies — never with a cross-axis knob
|
|
205
|
+
(it moves the content and leaves the chrome), and never by resizing the CONTAINER, which changes
|
|
206
|
+
both terms and leaves the spread.
|
|
207
|
+
- **A slot holding another component's part must be sized by that component's constant** — a
|
|
208
|
+
24px mark in an 18px column sits 3px outside the surface.
|
|
209
|
+
- **A row beat belongs to the PAGE** — collect every row primitive's height on one screen before
|
|
210
|
+
calling any of them right.
|
|
211
|
+
|
|
212
|
+
→ [composition.md](./composition.md) §"Canvas & content column", §"No dead rows", §"The register's own craft".
|
|
213
|
+
|
|
214
|
+
### 7. Surface walk — open it, press it, read it
|
|
215
|
+
A resting register is the cheapest thing to screenshot and the least likely to hold the defect.
|
|
216
|
+
|
|
217
|
+
- **Open what the row opens** and measure where the actionable field LANDS. An UNBOUNDED section
|
|
218
|
+
(a feed, a thread) above a bounded one buries it, and the burial deepens with use.
|
|
219
|
+
- **Press every control** and look at the state you land in. Commonest miss: a value rendering as
|
|
220
|
+
a coloured chip in a register and as bare grey text in its own EDITOR.
|
|
221
|
+
- **Diff a promoted field against where it came from.** Promoting means MOVING; a copy left behind
|
|
222
|
+
gives one field two editors.
|
|
223
|
+
- **Read every string in its SETTLED state**, not the streaming one.
|
|
224
|
+
- **Every state designed** — skeleton mirroring the final layout, empty, empty-SEARCH, error — and
|
|
225
|
+
no layout shift between them. → [catalog.md](./catalog.md) §"Status / feedback".
|
|
226
|
+
- **A comment stating a PRIORITY between two regions is a claim to VERIFY.** "When the two
|
|
227
|
+
compete, X gives way" is testable, and a region whose width is a CONSTANT cannot give way —
|
|
228
|
+
whatever sits beside it absorbs every squeeze, silently and in the opposite direction to the
|
|
229
|
+
stated intent. Find the rule in prose, then find the number implementing it.
|
|
230
|
+
- **Read the document OUTLINE**, and again AFTER fixing the type. Content from stored markdown
|
|
231
|
+
carries the tags ITS writer chose, so a heading inside a value announces as a peer of the page's
|
|
232
|
+
sections; sizing it down fixes what the screen SHOWS and nothing about what it ANNOUNCES.
|
|
233
|
+
|
|
234
|
+
### 8. Data probe — count what is really there
|
|
235
|
+
- **Query the table before choosing a surface.** "Show thumbnails" is wrong for a corpus of nine
|
|
236
|
+
files and zero images. Images are identified by CONTENT (a grid), documents by NAME (a list) —
|
|
237
|
+
follow the majority.
|
|
238
|
+
- **Read the stored value before blaming the renderer.** A grey generic file badge can be correct
|
|
239
|
+
(the stored mime is `application/octet-stream`); grey status pills can be correct (the field has
|
|
240
|
+
no option colours). The fix is often config or a write path, not CSS.
|
|
241
|
+
- **The obvious column may be the empty one.** Reading only the field the composer writes ships a
|
|
242
|
+
feature empty on every row and correct in code review.
|
|
243
|
+
- **A fixture can fail in the generous direction too**: an image tile pointed at a dead URL renders
|
|
244
|
+
nothing, so the chip designed to survive a photo was never tested against one.
|
|
245
|
+
- **Grep rendered text for internal identifiers — first probe, every audit** —
|
|
246
|
+
`/\b(opt|fld|tbl|rec|wfl|mbr|app|wsp)_[A-Za-z0-9]{6,}/`. It renders, measures, typechecks and
|
|
247
|
+
passes every test, because the value IS a valid string, and no other probe looks for it. The
|
|
248
|
+
usual cause is one field serving two consumers: a select decoded to its option KEY because a
|
|
249
|
+
picker's `value` needs the key, then the same variable printed at the reader. **Fix by decoding
|
|
250
|
+
BOTH** — the key for the control, the label for the sentence — never with a local key→label map,
|
|
251
|
+
which is a second copy of the field's own options and drifts the first time one is renamed. The
|
|
252
|
+
kit's `optionPicker` exists for this; see [data_entry.md](./data_entry.md).
|
|
253
|
+
- **For every value, name its AUTHOR — then check that author can change it.** No measurement
|
|
254
|
+
finds a MISSING affordance. Prose a person typed is always editable; a transcript or a file
|
|
255
|
+
never is.
|
|
256
|
+
- **Ask of every value: could this exist in the data?** Name the column. No answer means the block
|
|
257
|
+
goes.
|
|
258
|
+
|
|
259
|
+
### 9. The absence pass — what catches "bland"
|
|
260
|
+
Every probe above reads an element that EXISTS. None fires on a screen where nothing is wrong and
|
|
261
|
+
nothing is right, which is the screen reported as bland. Defect-removal converges on *inoffensive*;
|
|
262
|
+
this converges on *distinctive*.
|
|
263
|
+
|
|
264
|
+
- **Type range** (probe 1) — under 2× is flat regardless of colour.
|
|
265
|
+
- **Chromatic count** — one or two hues used many times reads calm; zero reads like a spreadsheet;
|
|
266
|
+
ten used once each reads cheap.
|
|
267
|
+
- **Identity marks** — on a screen whose subject is an entity, count them. Zero is the finding,
|
|
268
|
+
and so is N identical: a mark that looks the same on every row carries nothing, and it is the
|
|
269
|
+
largest, brightest thing there.
|
|
270
|
+
- **What does the screen DECIDE?** A register listing every row at equal weight has no point of
|
|
271
|
+
view. Usually the real answer when treatment fixes keep not landing.
|
|
272
|
+
- **What is inherently visual in THIS data?** Where there is no photography — most work software —
|
|
273
|
+
the data is the imagery: identity marks, status colour, file-type badges, sparklines, proportion
|
|
274
|
+
bars, tabular figures.
|
|
275
|
+
- **What makes this belong to THIS product?** Default typeface, default greys, default accent,
|
|
276
|
+
default control shapes is a well-built generic screen, and no polish changes that. The fix is a
|
|
277
|
+
theme, not another treatment pass.
|
|
278
|
+
|
|
279
|
+
→ [composition.md](./composition.md) §"Identity marks", §"Character comes from the DATA", §"Where the accent goes".
|
|
280
|
+
|
|
281
|
+
### 10. Microcopy
|
|
282
|
+
- **No punctuation doing a word's job.** ` · ` is banned outright — it claims a relationship while
|
|
283
|
+
refusing to name it, and it is the loudest templated-metadata tell there is. If a mark cannot be
|
|
284
|
+
read aloud as the relation it stands for, write the relation or split the line.
|
|
285
|
+
- **One job per element.** A label labels, an example demonstrates, a caption qualifies. Nothing
|
|
286
|
+
does double duty.
|
|
287
|
+
- **A shortened element still has to justify existing** — trimming makes it pass every length
|
|
288
|
+
probe, so re-ask gate 1 on the trimmed thing.
|
|
289
|
+
- **Structure must encode something true.** A rule, an eyebrow, a number, a badge, a colour each
|
|
290
|
+
assert that something is separate / labelled / ordered / status. A false assertion is decoration
|
|
291
|
+
that costs more than it gives.
|
|
292
|
+
|
|
293
|
+
→ [composition.md](./composition.md) §"Microcopy".
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Reviewing an AI surface
|
|
298
|
+
|
|
299
|
+
An AI feature proposes and a person decides. These ask whether that is decidable. The full
|
|
300
|
+
contract is [ai_patterns.md](./ai_patterns.md); these are the ones a rendered screen can be
|
|
301
|
+
measured against.
|
|
302
|
+
|
|
303
|
+
- **Show the WHOLE thing with the change marked in place.** A proposal in its own card is
|
|
304
|
+
context-free; a figure is judgeable only beside the others it sits with. `DiffValue`/`DiffMark`
|
|
305
|
+
drop into any container.
|
|
306
|
+
- **Both values stay on screen** — the old one is the only evidence the correction is the right
|
|
307
|
+
SIZE.
|
|
308
|
+
- **UNKNOWN is not zero.** An unanswered figure must not share its treatment with an answered one,
|
|
309
|
+
and a removal's placeholder must not share the labels' muted tone.
|
|
310
|
+
- **A value the system supplied must say where it came from** — what evidence, from which source,
|
|
311
|
+
and that a model produced it. Two failure shapes: a filled control with no adjacent explanation,
|
|
312
|
+
and an explanation restating the value instead of the EVIDENCE for it. Corollary: deterministic
|
|
313
|
+
and probabilistic answers must not look alike.
|
|
314
|
+
- **Nothing auto-applies**, the commit names the outcome and count, and the count is re-checked
|
|
315
|
+
against the surface after every block you add.
|
|
316
|
+
- **Never pre-select a conflict candidate** — that is a decision the system made and attributed to
|
|
317
|
+
the person.
|
|
318
|
+
- **A change that is not to the VALUE must diff the thing that did change** — a document refiled
|
|
319
|
+
under a new type is the same bytes; put the diff on the classification.
|
|
320
|
+
- **A mark over an arbitrary ground needs its own ground** — a tint that reads on white is a smudge
|
|
321
|
+
on a photo, and the fixture has to contain the photo.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Extraction snippets
|
|
326
|
+
|
|
327
|
+
Run each at 1280 and 375. The tables ARE the findings.
|
|
328
|
+
|
|
329
|
+
### Type inventory — every run of language, in document order
|
|
330
|
+
|
|
331
|
+
Walks TEXT NODES rather than elements, so a run-in label and its sentence are separate rows
|
|
332
|
+
instead of one blended string.
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
() => {
|
|
336
|
+
const SZ = { '12px':'xs','14px':'sm','16px':'md','18px':'lg','20px':'lg','22px':'xl','24px':'xl','28px':'xxl','32px':'xxl/xxxl','48px':'xxxl' };
|
|
337
|
+
const INK = { 'rgb(24, 24, 27)':'default','rgb(82, 82, 91)':'muted','rgb(113, 113, 122)':'zinc-500','rgb(161, 161, 170)':'zinc-400','rgb(255, 255, 255)':'inverted' };
|
|
338
|
+
const out = [];
|
|
339
|
+
const walk = (n) => {
|
|
340
|
+
if (n.nodeType === 3 && n.textContent.trim()) {
|
|
341
|
+
const cs = getComputedStyle(n.parentElement);
|
|
342
|
+
const rg = document.createRange(); rg.selectNode(n); const r = rg.getBoundingClientRect();
|
|
343
|
+
if (r.height) out.push({
|
|
344
|
+
t: n.textContent.trim().slice(0, 34),
|
|
345
|
+
y: Math.round(r.y), x: Math.round(r.x),
|
|
346
|
+
rung: SZ[cs.fontSize] || cs.fontSize, w: cs.fontWeight,
|
|
347
|
+
ink: INK[cs.color] || cs.color,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
n.childNodes.forEach(walk);
|
|
351
|
+
};
|
|
352
|
+
walk(document.body);
|
|
353
|
+
return out.sort((a, b) => a.y - b.y);
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Then **count the treatments**: collapse to `rung/weight/ink` and tally. A combination with one
|
|
358
|
+
member is a singleton. Two combinations doing the same job is the inconsistency you were sent to
|
|
359
|
+
find.
|
|
360
|
+
|
|
361
|
+
**Skip `SCRIPT`/`STYLE`/`TITLE`/`META`/`LINK`/`NOSCRIPT` and check `checkVisibility()`** in any
|
|
362
|
+
variant that walks ELEMENTS rather than text nodes. Those tags hold text nodes the reader never
|
|
363
|
+
sees, and counting them produces confident nonsense — a page "with eight nodes in Times New Roman"
|
|
364
|
+
that has none, or a clean tally on a screen with an untracked component still on it.
|
|
365
|
+
|
|
366
|
+
### Separator beats — is the biggest boundary the widest?
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
() => {
|
|
370
|
+
const rules = [...new Set([...document.querySelectorAll('div')]
|
|
371
|
+
.filter(e => { const r = e.getBoundingClientRect(); return r.height <= 1.5 && r.width > 200; })
|
|
372
|
+
.map(e => Math.round(e.getBoundingClientRect().y)))].sort((a, b) => a - b);
|
|
373
|
+
const blocks = [...document.querySelectorAll('*')]
|
|
374
|
+
.filter(e => e.children.length === 0 && e.textContent.trim())
|
|
375
|
+
.map(e => { const r = e.getBoundingClientRect(); return { y: Math.round(r.y), b: Math.round(r.bottom) }; });
|
|
376
|
+
return rules.map(y => ({
|
|
377
|
+
rule: y,
|
|
378
|
+
above: y - Math.max(...blocks.filter(o => o.b <= y).map(o => o.b), -Infinity),
|
|
379
|
+
below: Math.min(...blocks.filter(o => o.y >= y).map(o => o.y), Infinity) - y,
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Unequal `above`/`below` on one rule, or a rule with a visibly tighter beat than its neighbours, is
|
|
385
|
+
the finding. A hand-rolled `Divider` sitting in a parent's uniform `gap` next to a `SectionStack`
|
|
386
|
+
is the usual cause.
|
|
387
|
+
|
|
388
|
+
### Clipping, overflow, collision
|
|
389
|
+
|
|
390
|
+
```js
|
|
391
|
+
() => ({
|
|
392
|
+
pageOverflowsX: document.documentElement.scrollWidth > window.innerWidth,
|
|
393
|
+
clipped: [...document.querySelectorAll('*')]
|
|
394
|
+
.filter(e => e.scrollWidth > e.clientWidth + 1 && getComputedStyle(e).overflow !== 'visible')
|
|
395
|
+
.map(e => ({ t: e.textContent.trim().slice(0, 40), w: e.clientWidth, needs: e.scrollWidth,
|
|
396
|
+
ellipsis: getComputedStyle(e).textOverflow })),
|
|
397
|
+
})
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
`ellipsis: "clip"` on a clipped element means the text ends mid-word with nothing marking the cut.
|
|
401
|
+
Note the RN-Web trap: once a `Text` contains a nested pressable, `numberOfLines` degrades to bare
|
|
402
|
+
`overflow: hidden` and loses the ellipsis.
|
|
403
|
+
|
|
404
|
+
### Grouping — is one `gap` flattening the hierarchy?
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
(sel) => [...document.querySelector(sel).children].map(c => {
|
|
408
|
+
const r = c.getBoundingClientRect();
|
|
409
|
+
return { t: c.textContent.trim().slice(0, 40), y: Math.round(r.y), h: Math.round(r.height) };
|
|
410
|
+
})
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
List a stack's direct children and ask which BELONG together. If the answer is not "all of them",
|
|
414
|
+
one gap is serving several relationships and the screen will read as blended.
|
|
415
|
+
|
|
416
|
+
### Interaction states — does the control move when you touch it?
|
|
417
|
+
|
|
418
|
+
Snapshot the SAME selector four times — at rest, hovered, focused, and while editing — then diff.
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
(sel) => [...document.querySelectorAll(sel)].map(e => {
|
|
422
|
+
const r = e.getBoundingClientRect(), cs = getComputedStyle(e);
|
|
423
|
+
const inner = e.querySelector('input, textarea, [contenteditable]') || e;
|
|
424
|
+
const ir = inner.getBoundingClientRect();
|
|
425
|
+
return {
|
|
426
|
+
box: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
|
|
427
|
+
text: [Math.round(ir.x), Math.round(ir.y)],
|
|
428
|
+
border: `${cs.borderTopWidth} ${cs.borderTopColor}`,
|
|
429
|
+
bg: cs.backgroundColor,
|
|
430
|
+
shadow: cs.boxShadow,
|
|
431
|
+
};
|
|
432
|
+
})
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`border`/`bg`/`shadow` differing is the design; `box`/`text` differing is the defect. The usual
|
|
436
|
+
cause is two components spending the same inset or the same padding — a wrapper AND the input both
|
|
437
|
+
paying `CONTROL_TEXT_INSET`, a focus ring owned by both a shell and a nested pressable.
|
|
438
|
+
|
|
439
|
+
RN-Web trap: a frame REBUILT on the mode swap (view node unmounted, edit node mounted) never
|
|
440
|
+
receives `mouseenter`, because the pointer is already stationary over it — so hover state silently
|
|
441
|
+
dies at the moment of the swap and reads as a flash. Keep one node across the swap rather than
|
|
442
|
+
restoring the state by hand.
|
|
443
|
+
|
|
444
|
+
Driving the kit's own anatomies — a `PressDoor` row, a portalled overlay, a custom pointer drag —
|
|
445
|
+
has three gotchas of its own: [testing.md](./testing.md).
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## What measurement cannot see
|
|
450
|
+
|
|
451
|
+
A screenshot shows *that* something is wrong when every number looks defensible. But it will not
|
|
452
|
+
show you a disclosure you did not expand, or copy that only lies in its terminal state (a settled
|
|
453
|
+
row still saying "Loading…" measures perfectly). Nor a flattering fixture — compare the preview
|
|
454
|
+
data's SHAPE against what the real source emits before trusting any of it.
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## When you find something
|
|
459
|
+
|
|
460
|
+
- **Fix the CLASS, not the instance — and grep by the JOB, not the styling.** Two controls with
|
|
461
|
+
different labels, different call sites and no shared string can be the same species doing the
|
|
462
|
+
same job. Name the job in one phrase ("names a column", "commits the row", "opens a peek") and
|
|
463
|
+
search for everything else doing it. Fixing the tab in front of you is what leaves the same
|
|
464
|
+
field rendering two ways on two tabs — the inconsistency users actually report.
|
|
465
|
+
- **Three fixes in one spot means the ARRANGEMENT is the defect.** Count repairs, not their
|
|
466
|
+
difficulty; the third is the signal they were never independent.
|
|
467
|
+
- **Prefer changing the SHAPE over changing the weight.** Restructuring states a difference;
|
|
468
|
+
nudging a weight whispers it, and a whisper reads as a slip.
|
|
469
|
+
- **When the kit is the offender, fix the kit.** An app-local override is a fork, and a component
|
|
470
|
+
built beside one that nearly fits is a fork that drifts. Kit fixes land in `src/` plus the owning
|
|
471
|
+
`docs/<area>.md`, the `AGENTS.md` index line, and a version bump — docs reach apps only on
|
|
472
|
+
publish.
|
|
473
|
+
- **Never resolve a finding with "the docs say X."** Resolve it with what makes the screen better,
|
|
474
|
+
then push the answer back into the docs. A written rule cannot report that it produces a bad
|
|
475
|
+
screen, so a person has to. Where the docs ARE load-bearing is IDENTITY: the fonts, the type
|
|
476
|
+
ramp, the `Icon` set, one accent per screen — inventing a palette or a typeface is the defect,
|
|
477
|
+
not the craft.
|
package/docs/templates.md
CHANGED
|
@@ -197,8 +197,11 @@ The one work-execution list shape; it subsumes approvals, dispatch, batch-buildi
|
|
|
197
197
|
screens — register, per-row action, gated selection, and act-on-many in one. The page:
|
|
198
198
|
|
|
199
199
|
- **One toolbar row** — search + a status `Select` + facet `FilterChip`s LEFT, the New CTA
|
|
200
|
-
RIGHT; a light `SummaryLine` of the filtered view
|
|
201
|
-
- **A sortable
|
|
200
|
+
RIGHT; then a light `SummaryLine` of the filtered view BELOW it, above the rows — the counts are what the toolbar just produced, so they read after it, not before.
|
|
201
|
+
- **A sortable `Table`** — rows separated by their own height and the hover wash, with the one
|
|
202
|
+
hairline under the column band. It sets no row height: the register's rhythm is the kit's,
|
|
203
|
+
and a template that overrode it would teach the override. → [composition.md](./composition.md)
|
|
204
|
+
§"The register's rhythm". The record's SUBJECT is the first column —
|
|
202
205
|
who or what the record is FOR, because that is what a reader scans for and what they say out
|
|
203
206
|
loud ("the Harbor Freight one", never "RC-2026-0041"). The KEY is how the SYSTEM refers to the
|
|
204
207
|
record, so it rides the identity cell's supporting line, muted, where the other supporting
|
|
@@ -208,14 +211,14 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
208
211
|
itself the same way, or the two disagree about what a record is called. Every row carries a
|
|
209
212
|
leading checkbox; a row that can't take the bulk action gets a
|
|
210
213
|
**disabled** checkbox (the same gating as any blocked line). A row's status is
|
|
211
|
-
`Badge variant="dot"` — never the tonal `Badge` (that's the drawer/header twin; → [composition
|
|
212
|
-
|
|
214
|
+
`Badge variant="dot"` — never the tonal `Badge` (that's the drawer/header twin; → [composition.md](./composition.md)
|
|
215
|
+
§"`Badge` is for STATUS only") — and the SAME dot vocabulary
|
|
213
216
|
drives the status facet. A cell that carries supporting detail is a HIERARCHY, not two peers:
|
|
214
217
|
the identity on top (`size="sm"`, `weight="medium"` when it IS the row's name), its supporting
|
|
215
218
|
value beneath as `size="xs" color="muted"`, `gap: 2` — and a category / type / attribute goes
|
|
216
219
|
on THAT line, never a second chip beside the name (which reads as its peer, or louder when
|
|
217
|
-
colored, inverting what the row is scanned by; → [composition
|
|
218
|
-
only
|
|
220
|
+
colored, inverting what the row is scanned by; → [composition.md](./composition.md)
|
|
221
|
+
§"`Badge` is for STATUS only"). Every value in a row is SELECTABLE, so a reader copies any of them
|
|
219
222
|
without chrome; the supporting phone additionally carries a **`CopyButton`** as the worked
|
|
220
223
|
example of a verb riding its value — the one value copied often enough to earn a control, and
|
|
221
224
|
in its own cell rather than the trailing gutter, which two values in a cell give nothing to
|