altitude-design 0.2.1 → 0.3.1
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.md +42 -7
- package/dist/adapters/codemirror.cjs +88 -0
- package/dist/adapters/codemirror.cjs.map +7 -0
- package/dist/adapters/codemirror.js +67 -0
- package/dist/adapters/codemirror.js.map +7 -0
- package/dist/adapters/react-flow.css +102 -0
- package/dist/adapters/tiptap.css +71 -0
- package/dist/altitude.css +3121 -244
- package/dist/altitude.iife.js +33 -5
- package/dist/altitude.no-fonts.css +3121 -244
- package/dist/chart-themes.cjs +195 -0
- package/dist/chart-themes.cjs.map +7 -0
- package/dist/chart-themes.js +165 -0
- package/dist/chart-themes.js.map +7 -0
- package/dist/cli/altitude.mjs +134 -0
- package/dist/guidance/README.md +32 -0
- package/dist/guidance/agent-setup.md +22 -0
- package/dist/guidance/app-skeleton.md +17 -0
- package/dist/guidance/evidence.json +95 -0
- package/dist/guidance/index.json +493 -0
- package/dist/guidance/llms.txt +20 -0
- package/dist/guidance/manifest.json +92 -0
- package/dist/guidance/pattern.schema.json +181 -0
- package/dist/guidance/patterns/agent-action.json +77 -0
- package/dist/guidance/patterns/agent-action.md +39 -0
- package/dist/guidance/patterns/assistant-conversation.json +77 -0
- package/dist/guidance/patterns/assistant-conversation.md +50 -0
- package/dist/guidance/patterns/async-collection.json +71 -0
- package/dist/guidance/patterns/async-collection.md +29 -0
- package/dist/guidance/patterns/audit-log.json +75 -0
- package/dist/guidance/patterns/audit-log.md +27 -0
- package/dist/guidance/patterns/auth.json +86 -0
- package/dist/guidance/patterns/auth.md +29 -0
- package/dist/guidance/patterns/billing.json +80 -0
- package/dist/guidance/patterns/billing.md +29 -0
- package/dist/guidance/patterns/consequential-action.json +76 -0
- package/dist/guidance/patterns/consequential-action.md +27 -0
- package/dist/guidance/patterns/developer-settings.json +79 -0
- package/dist/guidance/patterns/developer-settings.md +29 -0
- package/dist/guidance/patterns/edit-record.json +74 -0
- package/dist/guidance/patterns/edit-record.md +51 -0
- package/dist/guidance/patterns/import.json +77 -0
- package/dist/guidance/patterns/import.md +31 -0
- package/dist/guidance/patterns/members.json +77 -0
- package/dist/guidance/patterns/members.md +27 -0
- package/dist/guidance/patterns/onboarding.json +78 -0
- package/dist/guidance/patterns/onboarding.md +31 -0
- package/dist/guidance/patterns/review-submit.json +72 -0
- package/dist/guidance/patterns/review-submit.md +27 -0
- package/dist/guidance/patterns/screen-layout.json +79 -0
- package/dist/guidance/patterns/screen-layout.md +40 -0
- package/dist/guidance/patterns/settings.json +78 -0
- package/dist/guidance/patterns/settings.md +32 -0
- package/dist/guidance/patterns/sync.json +77 -0
- package/dist/guidance/patterns/sync.md +31 -0
- package/dist/guidance/principles.md +23 -0
- package/dist/guidance/references/build/checks/agent-action-pattern.mjs +57 -0
- package/dist/guidance/references/build/checks/agent-setup.mjs +78 -0
- package/dist/guidance/references/build/checks/assistant-conversation-pattern.mjs +49 -0
- package/dist/guidance/references/build/checks/audit-log-pattern.mjs +63 -0
- package/dist/guidance/references/build/checks/auth-pattern.mjs +67 -0
- package/dist/guidance/references/build/checks/billing-pattern.mjs +63 -0
- package/dist/guidance/references/build/checks/developer-settings-pattern.mjs +76 -0
- package/dist/guidance/references/build/checks/guidance.mjs +71 -0
- package/dist/guidance/references/build/checks/import-pattern.mjs +69 -0
- package/dist/guidance/references/build/checks/members-pattern.mjs +71 -0
- package/dist/guidance/references/build/checks/onboarding-pattern.mjs +54 -0
- package/dist/guidance/references/build/checks/pattern-types.mjs +13 -0
- package/dist/guidance/references/build/checks/pattern-workflows.mjs +226 -0
- package/dist/guidance/references/build/checks/primary-scope.mjs +49 -0
- package/dist/guidance/references/build/checks/render-tells.mjs +72 -0
- package/dist/guidance/references/build/checks/screen-layout-pattern.mjs +111 -0
- package/dist/guidance/references/build/checks/semantics.mjs +72 -0
- package/dist/guidance/references/build/checks/settings-pattern.mjs +73 -0
- package/dist/guidance/references/build/checks/sync-pattern.mjs +62 -0
- package/dist/guidance/references/build/checks/tells-lint.mjs +164 -0
- package/dist/guidance/references/build/fixtures/pattern-workflows.tsx +83 -0
- package/dist/guidance/references/build/fixtures/patterns/agent-action.jsx +60 -0
- package/dist/guidance/references/build/fixtures/patterns/app-skeleton.jsx +94 -0
- package/dist/guidance/references/build/fixtures/patterns/assistant-conversation.jsx +79 -0
- package/dist/guidance/references/build/fixtures/patterns/audit-log.jsx +81 -0
- package/dist/guidance/references/build/fixtures/patterns/auth.jsx +150 -0
- package/dist/guidance/references/build/fixtures/patterns/billing.jsx +96 -0
- package/dist/guidance/references/build/fixtures/patterns/developer-settings.jsx +95 -0
- package/dist/guidance/references/build/fixtures/patterns/import.jsx +102 -0
- package/dist/guidance/references/build/fixtures/patterns/members.jsx +106 -0
- package/dist/guidance/references/build/fixtures/patterns/onboarding.jsx +70 -0
- package/dist/guidance/references/build/fixtures/patterns/screen-layout-decision.jsx +74 -0
- package/dist/guidance/references/build/fixtures/patterns/screen-layout-editor.jsx +56 -0
- package/dist/guidance/references/build/fixtures/patterns/screen-layout-report.jsx +42 -0
- package/dist/guidance/references/build/fixtures/patterns/settings.jsx +73 -0
- package/dist/guidance/references/build/fixtures/patterns/sync.jsx +78 -0
- package/dist/guidance/references/project/components/src/pattern-async-collection.tsx +57 -0
- package/dist/guidance/references/project/components/src/pattern-consequential-action.tsx +77 -0
- package/dist/guidance/references/project/components/src/pattern-edit-record.tsx +127 -0
- package/dist/guidance/references/project/components/src/pattern-review-submit.tsx +47 -0
- package/dist/guidance/references/project/components/src/settings.tsx +79 -0
- package/dist/guidance/references/scripts/build-framework-fixture.mjs +20 -0
- package/dist/guidance/rules.json +653 -0
- package/dist/guidance/visual/Elevation.md +194 -0
- package/dist/guidance/visual/Fundamentals.md +83 -0
- package/dist/guidance/visual/Research.md +1620 -0
- package/dist/index.cjs +18795 -1925
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +18795 -1925
- package/dist/index.js.map +4 -4
- package/dist/tokens.css +84 -84
- package/dist/tokens.json +301 -128
- package/dist/types/adapter-codemirror.d.ts +13 -0
- package/dist/types/agent-fleet.d.ts +166 -0
- package/dist/types/ai-label.d.ts +30 -0
- package/dist/types/app-shell.d.ts +83 -0
- package/dist/types/area-chart.d.ts +80 -0
- package/dist/types/artifact.d.ts +65 -0
- package/dist/types/bar-chart.d.ts +83 -0
- package/dist/types/bar-list.d.ts +35 -0
- package/dist/types/breadcrumb.d.ts +62 -0
- package/dist/types/chart-brush.d.ts +35 -0
- package/dist/types/chart-themes.d.ts +229 -0
- package/dist/types/chart.d.ts +570 -0
- package/dist/types/checklist.d.ts +71 -0
- package/dist/types/code-block.d.ts +32 -0
- package/dist/types/collections.d.ts +111 -4
- package/dist/types/color-picker.d.ts +38 -0
- package/dist/types/command.d.ts +6 -3
- package/dist/types/comment-thread.d.ts +48 -0
- package/dist/types/confirmation.d.ts +60 -0
- package/dist/types/context-menu.d.ts +17 -0
- package/dist/types/controls.d.ts +100 -17
- package/dist/types/conversation.d.ts +130 -0
- package/dist/types/copy-button.d.ts +19 -0
- package/dist/types/core.d.ts +139 -10
- package/dist/types/data-grid.d.ts +76 -0
- package/dist/types/date-picker.d.ts +96 -0
- package/dist/types/diff-view.d.ts +54 -0
- package/dist/types/dot-plot.d.ts +54 -0
- package/dist/types/edit-region.d.ts +121 -0
- package/dist/types/feature-gate.d.ts +22 -0
- package/dist/types/filter-bar.d.ts +65 -0
- package/dist/types/formatted-number.d.ts +38 -0
- package/dist/types/heatmap.d.ts +64 -0
- package/dist/types/in-place.d.ts +24 -0
- package/dist/types/index.d.ts +158 -15
- package/dist/types/inline-citation.d.ts +35 -0
- package/dist/types/inline-edit.d.ts +71 -0
- package/dist/types/input-otp.d.ts +37 -0
- package/dist/types/item-group.d.ts +104 -0
- package/dist/types/line-chart.d.ts +116 -0
- package/dist/types/link.d.ts +11 -0
- package/dist/types/markdown.d.ts +88 -0
- package/dist/types/meter.d.ts +43 -0
- package/dist/types/modal-state.d.ts +62 -0
- package/dist/types/nav.d.ts +8 -1
- package/dist/types/notifications.d.ts +42 -0
- package/dist/types/number-input.d.ts +30 -0
- package/dist/types/overlays.d.ts +51 -17
- package/dist/types/page-header.d.ts +27 -0
- package/dist/types/pagination.d.ts +29 -0
- package/dist/types/palette.d.ts +64 -0
- package/dist/types/password-input.d.ts +22 -0
- package/dist/types/pattern-assistant-conversation.d.ts +55 -0
- package/dist/types/pattern-async-collection.d.ts +34 -0
- package/dist/types/pattern-consequential-action.d.ts +53 -0
- package/dist/types/pattern-edit-record.d.ts +72 -0
- package/dist/types/pattern-review-submit.d.ts +43 -0
- package/dist/types/permission-matrix.d.ts +34 -0
- package/dist/types/plan-picker.d.ts +40 -0
- package/dist/types/prompt-input.d.ts +106 -0
- package/dist/types/property-list.d.ts +67 -0
- package/dist/types/question.d.ts +64 -0
- package/dist/types/relative-time.d.ts +21 -0
- package/dist/types/removal-receipt.d.ts +1180 -0
- package/dist/types/resizable.d.ts +48 -0
- package/dist/types/roving.d.ts +44 -0
- package/dist/types/save-bar.d.ts +38 -0
- package/dist/types/saved-views.d.ts +72 -0
- package/dist/types/scatter-chart.d.ts +60 -0
- package/dist/types/schedule.d.ts +80 -0
- package/dist/types/secret-field.d.ts +33 -0
- package/dist/types/select.d.ts +44 -2
- package/dist/types/separator.d.ts +11 -0
- package/dist/types/settings.d.ts +39 -0
- package/dist/types/sheet.d.ts +1 -1
- package/dist/types/sparkline.d.ts +46 -0
- package/dist/types/split-button.d.ts +21 -0
- package/dist/types/stat.d.ts +54 -0
- package/dist/types/stepper.d.ts +6 -3
- package/dist/types/suggestions.d.ts +29 -0
- package/dist/types/suspense.d.ts +12 -1
- package/dist/types/sync-indicator.d.ts +24 -0
- package/dist/types/tag-input.d.ts +65 -0
- package/dist/types/terminal.d.ts +33 -0
- package/dist/types/thread-list.d.ts +58 -0
- package/dist/types/time-input.d.ts +33 -0
- package/dist/types/timeline.d.ts +38 -0
- package/dist/types/toggle.d.ts +46 -0
- package/dist/types/toolbar.d.ts +34 -0
- package/dist/types/tour.d.ts +23 -0
- package/dist/types/trace.d.ts +65 -0
- package/dist/types/tree.d.ts +71 -0
- package/dist/types-cjs/adapter-codemirror.d.cts +13 -0
- package/dist/types-cjs/agent-fleet.d.cts +166 -0
- package/dist/types-cjs/ai-label.d.cts +30 -0
- package/dist/types-cjs/app-shell.d.cts +83 -0
- package/dist/types-cjs/area-chart.d.cts +80 -0
- package/dist/types-cjs/artifact.d.cts +65 -0
- package/dist/types-cjs/bar-chart.d.cts +83 -0
- package/dist/types-cjs/bar-list.d.cts +35 -0
- package/dist/types-cjs/breadcrumb.d.cts +62 -0
- package/dist/types-cjs/chart-brush.d.cts +35 -0
- package/dist/types-cjs/chart-themes.d.cts +229 -0
- package/dist/types-cjs/chart.d.cts +570 -0
- package/dist/types-cjs/checklist.d.cts +71 -0
- package/dist/types-cjs/code-block.d.cts +32 -0
- package/dist/types-cjs/collections.d.cts +246 -0
- package/dist/types-cjs/color-picker.d.cts +38 -0
- package/dist/types-cjs/command.d.cts +66 -0
- package/dist/types-cjs/comment-thread.d.cts +48 -0
- package/dist/types-cjs/confirmation.d.cts +60 -0
- package/dist/types-cjs/context-menu.d.cts +17 -0
- package/dist/types-cjs/controls.d.cts +360 -0
- package/dist/types-cjs/conversation.d.cts +130 -0
- package/dist/types-cjs/copy-button.d.cts +19 -0
- package/dist/types-cjs/core.d.cts +256 -0
- package/dist/types-cjs/data-grid.d.cts +76 -0
- package/dist/types-cjs/date-picker.d.cts +96 -0
- package/dist/types-cjs/diff-view.d.cts +54 -0
- package/dist/types-cjs/dot-plot.d.cts +54 -0
- package/dist/types-cjs/edit-region.d.cts +121 -0
- package/dist/types-cjs/empty.d.cts +24 -0
- package/dist/types-cjs/feature-gate.d.cts +22 -0
- package/dist/types-cjs/filter-bar.d.cts +65 -0
- package/dist/types-cjs/formatted-number.d.cts +38 -0
- package/dist/types-cjs/heatmap.d.cts +64 -0
- package/dist/types-cjs/in-place.d.cts +24 -0
- package/dist/types-cjs/index.d.cts +154 -0
- package/dist/types-cjs/inline-citation.d.cts +35 -0
- package/dist/types-cjs/inline-edit.d.cts +71 -0
- package/dist/types-cjs/input-otp.d.cts +37 -0
- package/dist/types-cjs/item-group.d.cts +104 -0
- package/dist/types-cjs/line-chart.d.cts +116 -0
- package/dist/types-cjs/link.d.cts +11 -0
- package/dist/types-cjs/markdown.d.cts +88 -0
- package/dist/types-cjs/meter.d.cts +43 -0
- package/dist/types-cjs/modal-state.d.cts +62 -0
- package/dist/types-cjs/nav.d.cts +25 -0
- package/dist/types-cjs/notifications.d.cts +42 -0
- package/dist/types-cjs/number-input.d.cts +30 -0
- package/dist/types-cjs/overlays.d.cts +170 -0
- package/dist/types-cjs/page-header.d.cts +27 -0
- package/dist/types-cjs/pagination.d.cts +29 -0
- package/dist/types-cjs/palette.d.cts +64 -0
- package/dist/types-cjs/password-input.d.cts +22 -0
- package/dist/types-cjs/pattern-assistant-conversation.d.cts +55 -0
- package/dist/types-cjs/pattern-async-collection.d.cts +34 -0
- package/dist/types-cjs/pattern-consequential-action.d.cts +53 -0
- package/dist/types-cjs/pattern-edit-record.d.cts +72 -0
- package/dist/types-cjs/pattern-review-submit.d.cts +43 -0
- package/dist/types-cjs/permission-matrix.d.cts +34 -0
- package/dist/types-cjs/pile.d.cts +27 -0
- package/dist/types-cjs/plan-picker.d.cts +40 -0
- package/dist/types-cjs/prompt-input.d.cts +106 -0
- package/dist/types-cjs/property-list.d.cts +67 -0
- package/dist/types-cjs/question.d.cts +64 -0
- package/dist/types-cjs/relative-time.d.cts +21 -0
- package/dist/types-cjs/removal-receipt.d.cts +1180 -0
- package/dist/types-cjs/resizable.d.cts +48 -0
- package/dist/types-cjs/roving.d.cts +44 -0
- package/dist/types-cjs/save-bar.d.cts +38 -0
- package/dist/types-cjs/saved-views.d.cts +72 -0
- package/dist/types-cjs/scatter-chart.d.cts +60 -0
- package/dist/types-cjs/schedule.d.cts +80 -0
- package/dist/types-cjs/secret-field.d.cts +33 -0
- package/dist/types-cjs/select.d.cts +109 -0
- package/dist/types-cjs/separator.d.cts +11 -0
- package/dist/types-cjs/settings.d.cts +39 -0
- package/dist/types-cjs/sheet.d.cts +27 -0
- package/dist/types-cjs/sparkline.d.cts +46 -0
- package/dist/types-cjs/split-button.d.cts +21 -0
- package/dist/types-cjs/stat.d.cts +54 -0
- package/dist/types-cjs/stepper.d.cts +26 -0
- package/dist/types-cjs/suggestions.d.cts +29 -0
- package/dist/types-cjs/suspense.d.cts +85 -0
- package/dist/types-cjs/sync-indicator.d.cts +24 -0
- package/dist/types-cjs/tag-input.d.cts +65 -0
- package/dist/types-cjs/terminal.d.cts +33 -0
- package/dist/types-cjs/thread-list.d.cts +58 -0
- package/dist/types-cjs/time-input.d.cts +33 -0
- package/dist/types-cjs/timeline.d.cts +38 -0
- package/dist/types-cjs/toggle.d.cts +46 -0
- package/dist/types-cjs/toolbar.d.cts +34 -0
- package/dist/types-cjs/tour.d.cts +23 -0
- package/dist/types-cjs/trace.d.cts +65 -0
- package/dist/types-cjs/tree.d.cts +71 -0
- package/package.json +89 -9
|
@@ -0,0 +1,1620 @@
|
|
|
1
|
+
# Research
|
|
2
|
+
|
|
3
|
+
Altitude's elevation language is built on perception and HCI research, then checked against measurements of its own rendered output. This page records the evidence, the experiments, and the decisions each one forced. Read it before adding a new elevation behavior.
|
|
4
|
+
|
|
5
|
+
## What the literature says
|
|
6
|
+
|
|
7
|
+
### Depth is read fast, and shading polarity carries it
|
|
8
|
+
|
|
9
|
+
- **Light comes from above.** People resolve ambiguous shading by assuming overhead light: a disc lit at the top reads as a bump, lit at the bottom as a dent (Ramachandran, 1988; see the [PeerJ review](https://peerj.com/articles/363)). This is why a raised surface carries a light top edge and a well carries a dark one, and why Altitude never moves its light.
|
|
10
|
+
- **Shape-from-shading is processed before attention.** Visual search uses 3D scene properties such as lighting direction in parallel ([Enns & Rensink, 1990](https://www2.psych.ubc.ca/~rensink/publications/abs.90.1.html)). Search with shaded objects is most efficient for *vertical* shading gradients, and better when distractors are lit from the top ([visual-search replication, Southampton](https://eprints.soton.ac.uk/48718)). Raised-versus-sunk is therefore a candidate for a pre-attentive "can I press this?" signal, but only if the shading is actually there.
|
|
11
|
+
- **Shadows are processed pre-attentively too, but only if they're darker than the ground.** When shadow regions were made lighter than the background, or outlined, the pre-attentive effect disappeared ([Rensink & Cavanagh, 1993](https://www2.psych.ubc.ca/~rensink/publications/abs.93.2.html)). Altitude never uses light "glows" to show height, even in dark schemes.
|
|
12
|
+
- **A cast shadow's displacement is read as height.** Moving only the shadow makes a stationary object appear to rise or sink ([Kersten, Mamassian & Knill, 1997](https://kerstenlab.psych.umn.edu/demos/shadows)). Altitude's ladder increases shadow *offset and blur*, not just darkness.
|
|
13
|
+
- **Depth segregates attention.** A conjunction search that includes stereoscopic depth runs in parallel ([Nakayama & Silverman, 1986](https://doi.org/10.1038/320264A0)): separate layers really are easier to search separately. That supports keeping overlays visibly off the page.
|
|
14
|
+
|
|
15
|
+
### Depth is a weak channel for amounts
|
|
16
|
+
|
|
17
|
+
- Depth ranks 6th of 10 magnitude channels, behind position, length, angle and area ([Munzner, *Visualization Analysis & Design*](https://www.cs.ubc.ca/~tmm/courses/436V-20/slides/marks.pdf)).
|
|
18
|
+
- Blur-defined depth (Semantic Depth of Field) separates only **two to four** groups reliably; people could not judge equal blur or blur ratios, a 7px blur was too weak, and participants disliked reading blurred objects ([Kosara et al., 2002](https://www.cvast.tuwien.ac.at/sites/default/files/bibcite/168/kos_vissym2002.pdf)).
|
|
19
|
+
- Real 3D layouts didn't beat 2D for retrieval: 6.77s vs 5.98s, not significant, though people preferred the 3D one ([Cockburn & McKenzie, 2001](https://www.cs.kent.edu/~jmaletic/cs63903/papers/Cockburn01.pdf)). Spatial memory itself does help ([Data Mountain, Robertson et al., 1998](https://www.microsoft.com/en-us/research/publication/data-mountain-using-spatial-memory-for-document-management/)).
|
|
20
|
+
|
|
21
|
+
So elevation should encode **a few ordered categories**, never a continuous value, and should keep things where they are.
|
|
22
|
+
|
|
23
|
+
### Flat interfaces cost time and errors
|
|
24
|
+
|
|
25
|
+
- In an eye-tracking study, flat web pages produced more missed clickable elements (35.8% vs 26.0%) and more false alarms (28.0% vs 16.6%) than traditional designs, and flat icons took twice as long to find (15.4s vs 8.0s) ([Burmistrov et al., 2015](https://www.flatisbad.com/publications/Burmistrov-INTERACT15.pdf)).
|
|
26
|
+
- Nielsen Norman Group's eye-tracking found weak signifiers attract less attention and cause uncertainty ([Flat UI elements attract less attention](https://nngroup.com/articles/flat-ui-less-attention-cause-uncertainty); summary only, the article couldn't be retrieved).
|
|
27
|
+
|
|
28
|
+
### Change, time and attention
|
|
29
|
+
|
|
30
|
+
- People miss changes they weren't attending to ([Rensink, O'Regan & Clark, 1997](https://www2.psych.ubc.ca/~rensink/publications/abs.96.2.html)). **Phosphor** answered with an afterglow: show the new state at once and leave a fading trace that explains what changed; two studies found significant benefits ([Baudisch et al., 2006](https://www.microsoft.com/en-us/research/?p=145436)).
|
|
31
|
+
- Abrupt onsets capture attention automatically ([Yantis & Jonides, 1984](https://researchgate.net/profile/Steven-Yantis/publication/16958392_Abrupt_visual_onsets_and_selective_attention_Evidence_from_visual_search/links/0046351b7673aea58b000000/Abrupt-visual-onsets-and-selective-attention-Evidence-from-visual-search.pdf)), and people can track only about four or five moving objects at once ([Pylyshyn & Storm, 1988](https://ruccs.rutgers.edu/images/personal-zenon-pylyshyn/docs/storm88.pdf)). A rise is an attention grab; spend them deliberately.
|
|
32
|
+
- **Ephemeral adaptation**: predicted menu items shown at once, the rest fading in over 500ms, made selection faster than a static menu and than highlighting. 250ms was too short to help, misses cost time, and the benefit needed ~79% prediction accuracy ([Findlater et al., 2009](https://www.cs.ubc.ca/labs/edapt/papers/findlater2009.pdf)).
|
|
33
|
+
- Animation helps when it's congruent with the change it shows and easy to perceive; otherwise static is as good ([Tversky, Morrison & Bétrancourt, 2002](https://faculty.washington.edu/aragon/classes/hcde411/w13/readings/Tversky_AnimationFacilitate_IJHCS02.pdf)). Animated transitions cut spatial-reconstruction errors from 1.8 to 0.4 without slowing tasks ([Bederson & Boltman, 1999](https://www.cs.umd.edu/projects/hcil/jazz/learn/papers/CS-TR-3964.pdf)).
|
|
34
|
+
- Reordering animates with **FLIP**: measure where each item was (First) and where it ends up (Last), apply the difference as a transform (Invert), then animate the transform away (Play), so moved rows travel without a layout per frame ([Paul Lewis](https://aerotwist.com/blog/flip-your-animations/)). Table re-sorts, CommandPalette results and Board moves use it.
|
|
35
|
+
- Response-time limits: about 0.1s feels instant, about 1s keeps flow ([Nielsen](https://www.nngroup.com/videos/3-response-time-limits-interaction-design/)).
|
|
36
|
+
|
|
37
|
+
### Physical metaphors that carry information
|
|
38
|
+
|
|
39
|
+
- **Piles** let people organize casually; a pile's presence and size say something without opening it ([Mander, Salomon & Wong, 1992](https://hci.stanford.edu/seminar/abstracts/92-93/930602-salomon-mander-rose-wong.html)).
|
|
40
|
+
- **Read wear and edit wear**: interaction history shown as physical wear ([Hill et al., 1992](https://acawiki.org/Edit_wear_and_read_wear)).
|
|
41
|
+
- **Workspace awareness**: in shared spaces, people need to see who is working where and on what ([Gutwin & Greenberg, 2002](https://collablab.northwestern.edu/CollabolabDistro/nucmc/GutwinGreenberg_FrameworkWorkspaceAwareness.pdf)).
|
|
42
|
+
- **Design frictions**: small, deliberate obstacles that prompt a moment of reflection before consequential actions ([Cox et al., 2016](https://oro.open.ac.uk/47011)).
|
|
43
|
+
- **Illusion of completeness**: people stop scrolling when a page looks finished ([Nielsen Norman Group](https://www.nngroup.com/videos/illusion-completeness/)). Scroll shadows are a long-standing CSS answer ([Verou, 2012](https://lea.verou.me/2012/04/background-attachment-local/)).
|
|
44
|
+
- **Cast shadows carry height.** When only a ball's shadow moves, people see the ball itself rise off the floor ([Kersten, Mamassian & Knill, 1997](https://journals.sagepub.com/doi/10.1068/p260171); [demonstrations](https://kerstenlab.psych.umn.edu/demos/shadows)). Avatars use exactly this: a live face lifts off its shadow, while everyone else here rests on theirs (see E12).
|
|
45
|
+
- Apple's 2025 Liquid Glass puts controls on a functional layer floating above content and says to avoid glass on glass ([WWDC25](https://developer.apple.com/videos/play/wwdc2025/219)), the same rule as Altitude's "one summit".
|
|
46
|
+
|
|
47
|
+
### Anticipation and feedforward
|
|
48
|
+
|
|
49
|
+
- **Feedforward** tells people what an action will do before they take it, as distinct from feedback afterwards; it bridges Norman's gulf of execution ([Vermeulen et al., 2013](https://documentserver.uhasselt.be/bitstream/1942/14759/1/VermeulenLuytenVandenHovenConinx_chi2013.pdf)). Altitude's hover ghosts are feedforward: the check an unchecked box would get, the radio thumb where it would land, a tab's sheet peeking up, a closed accordion lid's ghost.
|
|
50
|
+
- Targets that **expand as the pointer approaches** are acquired faster, even when the expansion starts late in the movement ([McGuffin & Balakrishnan, 2002](https://www.dgp.toronto.edu/~ravin/papers/chi2002_expandingtargets.pdf)). Altitude's approach response changes height, not size (a size change would reflow the layout), so it claims no Fitts' law gain. It's feedforward, and since E10 it's reserved for openings: an empty field opens as you reach for it, previewing what focusing it will do. Raised things no longer respond before the pointer is on them.
|
|
51
|
+
|
|
52
|
+
### Charts
|
|
53
|
+
|
|
54
|
+
These are the sources behind [Charts](Charts.md).
|
|
55
|
+
|
|
56
|
+
- **Position and length are read best; depth, angle and area worse.** Cleveland and McGill ranked elementary perceptual tasks by accuracy, with position on a common scale and length ahead of angle, area and volume ([Cleveland & McGill, 1984](https://doi.org/10.1080/01621459.1984.10478080)); Heer and Bostock replicated the ranking with crowdsourced participants ([Heer & Bostock, 2010](https://doi.org/10.1145/1753326.1753357)). Munzner's ranking puts depth 6th of 10 magnitude channels (see "Depth is a weak channel for amounts"). So a mark's amount is its length or position, never its height, and Altitude has no pie or donut chart.
|
|
57
|
+
- **Depth cues on bars cost accuracy.** Extraneous depth on bar graphs made people's estimates of bar heights less accurate ([Zacks, Levy, Tversky & Schiano, 1998](https://doi.org/10.1037/1076-898X.4.2.119)); Hughes (2001) reports a cost of about 10% for 3D bars. Microsoft's Office data-visualization guidance says chart elements should avoid drop shadows ([learn.microsoft.com](https://learn.microsoft.com/en-us/office/dev/add-ins/design/data-visualization-guidelines)). This is why marks are flat ink, and why a hovered bar never lifts.
|
|
58
|
+
- **Dual axes mislead.** Charts with two scales on one plot are read less accurately and invite comparisons the data doesn't support ([Isenberg, Bezerianos, Dragicevic & Fekete, 2011](https://doi.org/10.1109/TVCG.2011.160)). Altitude allows one axis per plot.
|
|
59
|
+
- **Decorative animation doesn't help.** Animation helps only when its motion is the change (Tversky, Morrison & Bétrancourt, above), so charts glide between values and never grow from the baseline on first render.
|
|
60
|
+
- **Staged transitions help people follow a change.** Animated transitions between statistical graphics significantly improved object tracking and value estimation; simple staging helped and was preferred, while rescaling axes at the same time as marks moved hurt ([Heer & Robertson, 2007](https://doi.org/10.1109/TVCG.2007.70539)). So a chart rescales first and moves its marks after, and `checks/chart-motion.mjs` holds it to that frame by frame.
|
|
61
|
+
- **A chart needs a path, not just a description.** Data Navigator models a chart as a graph of nodes laid over any rendering as an accessible layer ([Elavsky, Nadolskis & Moritz, 2023](https://doi.org/10.1109/TVCG.2023.3327393)); Olli moves a screen reader from a summary, to the axes and series, to the data ([Zong et al., 2022](https://doi.org/10.1111/cgf.14519)). Blind readers valued trends and statistics over a restatement of the encoding ([Lundgard & Satyanarayan, 2021](https://doi.org/10.1109/TVCG.2021.3114770)), so a series' name says its range and which way it went, and a point's says its change from the one before. Recharts 3 makes a chart one Tab stop with arrow keys between points. `ChartContainer`'s keyboard path follows these.
|
|
62
|
+
- **Classes beat a continuous ramp for naming a value.** Choropleth practice bins a sequential or diverging scheme into a few classes (ColorBrewer offers three to nine, and most maps use five to seven) so each colour can be found in the legend ([Harrower & Brewer, 2003](https://doi.org/10.1179/000870403235002042)). `Heatmap` bins into two to seven steps of `data-seq` or `data-div` and names each step's range.
|
|
63
|
+
- **Small marks need bigger colour differences.** The colour difference people can see shrinks with mark size ([Szafir, 2018](https://doi.org/10.1109/TVCG.2017.2744359)), so heatmap cells are at least 10px, steps are spread across the whole ramp rather than adjacent, and the grid wraps rather than shrinking cells further.
|
|
64
|
+
- **Accessibility is checked, not assumed.** Chartability groups chart accessibility into heuristics a reviewer can audit quickly ([Elavsky, Bennett & Moritz, 2022](https://doi.org/10.1111/cgf.14522)); Charts' five-point check (visual, keyboard, screen reader, reduced motion, forced colours) follows it.
|
|
65
|
+
|
|
66
|
+
- **The frame follows the libraries people know.** `ChartContainer` and `ChartConfig` take shadcn/ui's chart names and its config shape (data key → label and colour), and Recharts, visx, Nivo, ECharts and Observable Plot were considered as engines. Each adds a dependency, and none exposes what Altitude needs (imprints while loading, the attention budget, hover only where a click acts, legend keys built as switches), so the scales and axes are in-house (see E24).
|
|
67
|
+
|
|
68
|
+
The frame was reviewed against Altitude's rendering in E24. The full prototype review of every part and state comes with the first chart type, `BarChart`.
|
|
69
|
+
|
|
70
|
+
### Generated interfaces converge
|
|
71
|
+
|
|
72
|
+
These are the sources behind [Distinctiveness](Distinctiveness.md), which also records the audit of Altitude against them. Most of the catalogued tells come from practitioners, not studies; the strength of each source is recorded in `tells.json`.
|
|
73
|
+
|
|
74
|
+
- **Models sample from the safe centre.** Anthropic attributes the generic look of generated frontends to "distributional convergence": safe, universally acceptable choices dominate web training data, and without direction a model picks them. It names Inter, Roboto and Arial, purple gradients on white, predictable layouts and flat backgrounds ([Rajasekaran, Wei & Bricken, 2025](https://claude.com/resources/articles/improving-frontend-design-through-skills)). UI kits used as shipped amplify it; one widely repeated account traces the purple to Tailwind's `indigo-500` ([DEV Community](https://dev.to/alanwest/why-every-ai-built-website-looks-the-same-blame-tailwinds-indigo-500-3h2p); a single source).
|
|
75
|
+
- **Structure converges more than colour.** Five generative UI tools given the same 24 prompts produced 120 interfaces whose visual similarity (UIClip embeddings) and layout similarity (tree edit distance) were closer than their colour (Earth Mover's Distance in CIELCh). The same study found a quarter of the tools' stated design rationales absent from what they built, and functional principles (system status, error recovery, user control) almost entirely missing: four of five tools scored 0.06 or less ([Imteyaz et al., 2026](https://arxiv.org/abs/2607.22928), preprint). Across 15 models and 160 briefs, structural originality was 0.59 against 0.76 for human reference designs, and the change between two domain variants of the same brief was 0.58 against 0.90, while appropriateness stayed above 90% ([Rusia et al., 2026](https://arxiv.org/abs/2610.11539), preprint by a vendor; colour, type and copy were excluded from the score by design). So the strongest tell is a layout that ignores its content, which is why Distinctiveness starts from the task (rule 2).
|
|
76
|
+
- **Frictionless generation deepens it.** A risk analysis of "vibe coding" argues that removing friction from generation increases homogenization, and proposes productive friction that helps creators push back on defaults ([Shin et al., 2026](https://arxiv.org/abs/2603.13036), preprint; abstract only read). Writing the task down before the layout is that friction.
|
|
77
|
+
- **Banned tells are replaced, not removed.** Practitioner catalogs report that with purple and Inter banned, output moves to a second set of defaults: cream with terracotta, near-black with one acid green, Space Grotesk, Geist, Instrument Serif, all-caps mono labels, one accented headline word and decorative numbering ([avoid-ai-design](https://github.com/funboy322/avoid-ai-design); [Developers Digest](https://www.developersdigest.tech/blog/ai-design-slop-and-how-to-spot-it)). For apps rather than pages: equal-weight card grids, the same sidebar on every screen, flat spacing, generic edge states and placeholder copy ([saasui.design](https://www.saasui.design/blog/saas-ui-looks-ai-generated)). All say the tells are read as a cluster. This is why `tells.json` records when each tell was last seen, and is refreshed on a schedule.
|
|
78
|
+
- **Familiar and simple win the first impression.** Screenshots of 119 real sites were rated for appeal at 17, 33 and 50 ms. Low visual complexity and high prototypicality were rated most appealing, and an unfamiliar design was judged uglier even when simple ([Tuch et al., 2012](https://research.google/pubs/pub38315/)). Escaping the generated look is not a licence for novelty: controls stay conventional (rule 1).
|
|
79
|
+
- **What it costs.** Without disclosure, participants leaned toward websites with generated images; once told, they trusted sites with photographs more, most of all government and health sites ([CHI EA '26](https://dl.acm.org/doi/full/10.1145/3772363.3798849)). Consumer surveys report rising distrust of heavy AI use by brands, but they are vendor-run and not comparable with each other.
|
|
80
|
+
|
|
81
|
+
### Generated interfaces break established rules
|
|
82
|
+
|
|
83
|
+
These are the sources behind [Fundamentals](Fundamentals.md). Where a figure comes from a paper that was read in full, it's given; where only an abstract or summary was read, that's said.
|
|
84
|
+
|
|
85
|
+
- **Function is what's left out.** In Design Theater (above), each of 24 tasks carried two implicit principles: structural (information architecture), styling (perception and WCAG) and functional (Nielsen's heuristics). Structural principles were implemented at 0.50–0.94 and styling at 0.38–0.88 across tools, functional ones at 0.38 for the best tool and 0.06 or less for the other four. The gap between stated reasoning and the build was widest on functional tasks: about a third of those rationales never reached the screen ([Imteyaz et al., 2026](https://arxiv.org/abs/2607.22928), preprint).
|
|
86
|
+
- **"Good from afar."** Nielsen Norman Group gave AI prototyping tools (Bolt, UX Pilot, Claude, Figma Make, v0, Stitch) a real redesign and evaluated the outputs heuristically against a designer's. With vague prompts the screens had poor grouping (related information split by large gaps), inconsistent margins, competing styles, poor contrast, emphasis on information the task didn't need, and patterns from the wrong domain force-fitted into a template; detailed prompts narrowed the gap but didn't close it ([NN/g, AI prototyping](https://www.nngroup.com/articles/ai-prototyping/)).
|
|
87
|
+
- **Names that say nothing.** Across 300 interfaces generated by Claude Sonnet 4, Gemini 2.5 Flash and GPT-4o, LLM judges (80–92% recall on injected faults) found 541 semantic accessibility violations that pass automated tools: generic button labels 27%, vague link text 26%, poor alt text 20%, ARIA labels 14% and generic form labels 12%, with heading mismatches the hardest to detect ([Calò, Guriţă & De Russis, CHI EA '26](https://doi.org/10.1145/3772363.3799364)). Human annotators agreed with each other only fairly (κ ≈ 0.24), so this is a judgment, not a lint.
|
|
88
|
+
- **Structure fails first.** Experts rated ChatGPT and Claude banking interfaces against WCAG 2.1. With accessibility-agnostic prompts, info and relationships (1.3.1) and alternative text (1.1.1) failed in every interface evaluated and status messages (4.1.3) in 90%; accessibility-oriented prompts helped most with focus visibility (2.4.7) and 1.3.1, but semantic structure stayed the persistent barrier ([Guriţă & Vatavu, W4A '25](https://doi.org/10.1145/3744257.3744266)). An earlier study they cite found 84% of ChatGPT-generated sites had violations, led by text resizing, contrast and semantic relationships.
|
|
89
|
+
- **Basic checks pass, ARIA doesn't.** GPT-4o and Qwen2.5-Coder produced code with fewer contrast and alt-text violations than human-written pages from popular repositories, but struggled with ARIA (labels, roles, unique ids); prompting strategies helped little, automated feedback loops more ([Suh et al., 2025](https://arxiv.org/abs/2503.15885), preprint). So Altitude's contrast tokens address a problem models already handle reasonably; names and structure are where the guidance has to work.
|
|
90
|
+
- **Deceptive patterns arrive unasked.** Of 312 ecommerce components generated by Claude, GPT-4o, Gemini and CodeLlama, 37% contained at least one deceptive pattern, with no significant difference between models or between business- and user-oriented prompts. Information hiding was most common (57% of the affected components: missing terms at sign-up, urgency with no deadline), then restriction (38%, nearly all popovers with no close button), covert pressure (29%, limited-time messages and countdowns) and asymmetry (14%, the business's choice drawn bigger, costly defaults pre-selected). Newsletter (92%), discount (88%) and membership (75%) components were worst ([Chen et al., 2025](https://arxiv.org/abs/2502.13499v1), first version, preprint; a later version extends it to 1,000 components). In a CHI study, 20 participants each had ChatGPT build a webshop: every site contained deceptive patterns, five on average and up to nine, ChatGPT gave no warning, and only four participants raised concerns ([Krauß et al., CHI '25](https://eprints.gla.ac.uk/348039); summary read). The categories follow Brignull's catalogue ([deceptive.design](https://www.deceptive.design/types)) and Mathur et al.'s taxonomy from 11,000 shopping sites ([Mathur et al., CSCW 2019](https://doi.org/10.1145/3359183)).
|
|
91
|
+
- **Looking good hides it.** People rate attractive interfaces as more usable before use ([Kurosu & Kashimura, CHI '95](https://doi.org/10.1145/223355.223680)), and the effect held after use in a replication ([Tractinsky, Katz & Ikar, 2000](https://doi.org/10.1016/S0953-5438(00)00031-X)). Polished generated screens are judged at a glance, which is why Distinctiveness rule 7 asks for every claim to be checked against the build.
|
|
92
|
+
- **Disabled buttons don't explain themselves.** The GOV.UK Design System: "Disabled buttons have poor contrast and can confuse some users, so avoid them if possible", and use them only where research shows they make the interface easier to understand ([GOV.UK, Button](https://design-system.service.gov.uk/components/button/)). Altitude disables a submit only while it's in flight.
|
|
93
|
+
- **Line length.** WCAG 1.4.8 (AAA) asks for lines of no more than 80 characters ([W3C](https://www.w3.org/WAI/WCAG22/Understanding/visual-presentation)), and typographic practice settles on 45–75 ([Bringhurst, *The Elements of Typographic Style*](https://en.wikipedia.org/wiki/The_Elements_of_Typographic_Style)). Studies of on-screen reading speed and line length are mixed ([Dyson, 2004](https://doi.org/10.1080/01449290410001715714)); the range is kept for comfort and comprehension, not speed.
|
|
94
|
+
|
|
95
|
+
### Accessibility
|
|
96
|
+
|
|
97
|
+
- In forced-colors mode (Windows High Contrast), `box-shadow` is reverted, so anything conveyed only by a shadow is lost ([Microsoft Edge team](https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/)).
|
|
98
|
+
- WCAG 2.2 **Focus Appearance** (2.4.13, AAA) asks for a focus indicator at least as large as a 2px perimeter of the control, whose pixels change by at least 3:1 between the unfocused and focused states ([W3C](https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance)). A focused field's neutral rim meets it in every scheme (E10).
|
|
99
|
+
- Keyboard and ARIA behaviour follow the WAI-ARIA Authoring Practices ([APG patterns](https://www.w3.org/WAI/ARIA/apg/patterns/)): combobox for `Combobox` and `CommandPalette`, listbox with a button trigger for `Select`, modal dialog for `Dialog`, `Sheet` and `CommandPalette`.
|
|
100
|
+
|
|
101
|
+
## Experiments
|
|
102
|
+
|
|
103
|
+
All measurements were taken from Altitude's rendered output in headless Chromium, in all four schemes, using CIEDE2000 (ΔE₀₀) for color differences. ΔE₀₀ ≈ 1 is a just-noticeable difference side by side; about 2–2.5 is noticeable at a glance.
|
|
104
|
+
|
|
105
|
+
### E1 · Can people tell adjacent levels apart?
|
|
106
|
+
|
|
107
|
+
Eight 96px tiles, one per level, on the ground. For each step up the ladder, the larger of the face-color difference and the shadow-band difference:
|
|
108
|
+
|
|
109
|
+
| Step | Daylight | Nightfall | Mesa | Alpine |
|
|
110
|
+
|---|---|---|---|---|
|
|
111
|
+
| −2 → −1 | 2.17 | 1.28 | 2.24 | 1.10 |
|
|
112
|
+
| −1 → 0 | 1.81 | 1.92 | 2.52 | 2.02 |
|
|
113
|
+
| 0 → 1 | 2.69 | 2.77 | 2.66 | 3.58 |
|
|
114
|
+
| 1 → 2 | **0.83** | 2.24 | **0.81** | 2.25 |
|
|
115
|
+
| 2 → 3 | 1.97 | 2.24 | 1.95 | 2.25 |
|
|
116
|
+
| 3 → 4 | 1.37 | 1.79 | 1.33 | 2.09 |
|
|
117
|
+
| 4 → 5 | **0.91** | 2.12 | **1.07** | 2.05 |
|
|
118
|
+
|
|
119
|
+
With a simulated low-vision filter (4px blur, contrast halved), the light schemes' 1→2 step fell to 0.33–0.52.
|
|
120
|
+
|
|
121
|
+
**Finding.** Adjacent levels are *not* reliably readable at rest, especially in light schemes, where every raised level is white and only the shadow differs. Tier boundaries are: sunk vs ground vs raised vs floating differ clearly in every scheme. This matches the 2–4 group limit for depth cues.
|
|
122
|
+
|
|
123
|
+
**Decision.** Meaning is assigned to **four tiers** (sunk, ground, raised, floating). The eight levels are kept for *motion*: hover climbs one level, press drops to ground. People read tiers and feel levels. No component may rely on a static difference between adjacent levels in the same tier.
|
|
124
|
+
|
|
125
|
+
### E2 · Does the shadow carry height?
|
|
126
|
+
|
|
127
|
+
The centroid of the shadow below each raised tile (Daylight): 1.4px at level 1, 4.7px at 2, 10.1px at 3, 17.3px at 4, 21.2px at 5. Nightfall: 2.7, 8.3, 15.9, 19.9, 20.4px.
|
|
128
|
+
|
|
129
|
+
**Finding.** Shadow displacement grows monotonically with level, the cue Kersten et al. showed is read as height. In dark schemes, levels 4 and 5 converge, so tonal lift (each level lighter) carries the difference there.
|
|
130
|
+
|
|
131
|
+
### E3 · What survives forced colors?
|
|
132
|
+
|
|
133
|
+
Rendered with `forced-colors: active`, the entire ladder disappeared: tiles, wells and shadows all became plain background.
|
|
134
|
+
|
|
135
|
+
**Decision.** In forced colors, elevation falls back to borders that encode the tiers: dashed for sunk (2px for trench), none for ground, 1px for raised, 2px for floating, double for modal. Verified rendering in both light and dark schemes. Under `prefers-contrast: more` (or `AltitudeProvider contrast="more"`), shadows become hard ink slabs whose thickness is the height, a high-contrast version of the same cast-shadow cue.
|
|
136
|
+
|
|
137
|
+
### E4 · Is "raised" actually shaded?
|
|
138
|
+
|
|
139
|
+
Top-to-bottom lightness difference (ΔL*) across the face of each surface:
|
|
140
|
+
|
|
141
|
+
| Surface | Daylight | Nightfall |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| Raised (before) | 0.0 | 0.0 |
|
|
144
|
+
| Well | −1.2 | −0.6 |
|
|
145
|
+
| Trench | −3.5 | −0.8 |
|
|
146
|
+
|
|
147
|
+
**Finding.** Wells had the dark-top polarity that reads as concave, but raised surfaces had no shading at all: they were flat faces with a drop shadow. The pre-attentive shading cue was present for "fill this" but missing for "press this".
|
|
148
|
+
|
|
149
|
+
**Decision.** Pressable controls (buttons, thumbs, keys, the selected tab, checked boxes) get a face gradient, `--alt-sheen`. Three strengths were rendered; the subtlest gives +1.9 to +4.8 ΔL*, mirroring the wells' polarity without looking glossy. Pressing removes the sheen, so the face flattens as it reaches the ground. Cards and other containers stay flat: they group, they aren't pressed.
|
|
150
|
+
|
|
151
|
+
### E5 · Do levels interpolate?
|
|
152
|
+
|
|
153
|
+
The computed `box-shadow` was sampled every frame while a surface climbed from level 1 to 5. All samples had four layers, and the ambient layer's offset, blur and spread moved continuously (4.6 → 12.2 → 19.9 → … → 48px).
|
|
154
|
+
|
|
155
|
+
**Finding.** Because every token, including flush, well and trench, uses the same four-layer structure, any level animates smoothly into any other.
|
|
156
|
+
|
|
157
|
+
### E6 · How long is an afterglow noticeable?
|
|
158
|
+
|
|
159
|
+
A card's face color was sampled after its afterglow started. The difference from its resting color (ΔE₀₀) in Daylight was 9.0 for the first second, 7.6 at 2s, 4.8 at 3s, 2.8 at 4s, 0.6 at 6s and 0 at 8s. Nightfall tracked within 1.3.
|
|
160
|
+
|
|
161
|
+
**Decision.** `dur-settle` is 8s, held at full strength for the first second, and noticeable for about four seconds: long enough to catch the eye of someone who looked away, gone before it becomes noise.
|
|
162
|
+
|
|
163
|
+
### E7 · How should a button show its height?
|
|
164
|
+
|
|
165
|
+
Three treatments were prototyped against the original (flat face, drop shadow) and rendered at rest, hover and press in light, dark and warm schemes: a **wall** (a solid edge under the face, darker than the face, as thick as the level), a **pill** with the same wall, and a **lit-edge slab** with no wall.
|
|
166
|
+
|
|
167
|
+
**Decision.** The wall. It draws height literally as displacement, the cue measured in E2 and shown to read as height, so a primary button (4px) visibly stands taller than a secondary one (2px) and a ghost button has none. Pressing has somewhere to go: the face travels the full wall height and the wall vanishes. It survives high contrast as an ink slab and needs no new colours, since the wall is derived from the face. The pill read as playful rather than precise; the lit-edge slab didn't separate primary from secondary.
|
|
168
|
+
|
|
169
|
+
### E8 · Which badge, avatar, tab and alert treatments carry their meaning?
|
|
170
|
+
|
|
171
|
+
A design review, not a measurement: each candidate was rendered in Daylight and Nightfall at 2× and judged against two questions: does it say the component's meaning in Altitude's own vocabulary, and does it suggest an affordance it doesn't have?
|
|
172
|
+
|
|
173
|
+
- **Badges.** Ten treatments: strong deboss, deboss with an inside wall, embossed label tape, sticker, rubber stamp, engraved type, inlay, severity steps, a trench socket with a raised mark, and an indicator plate with a lamp. The **lamp plate** won: it extends the hardware the system already has (keys with walls, switch tracks and thumbs) and keeps colour in a small lit bead, so plates stay calm in dense tables. The socket read as a toggle switch (a false affordance), severity steps read as signal strength (an inverted meaning), and the stamps and stickers looked like every other library's badge.
|
|
174
|
+
- **Avatars.** Keycap tiles with walls were rejected because walls mean "press it" everywhere else. The **detached shadow** was kept: it draws presence with a cue people already read as height (above), needs no outline, and survives dark schemes once the shadow core is dense enough. (Revised in E12: the detached shadow now belongs to live people only.)
|
|
175
|
+
- **Tabs.** A walled pill carried between tabs fought the smooth glide people liked. **Folder tabs** say "the sheet on top is the one you're reading" with one raised piece and no extra decoration; the glide survives as the segmented variant.
|
|
176
|
+
- **Alerts.** A flush band carried no elevation at all; a plaque set into the page looked heavy. A **sheet resting on the page** (paper edge, contact shadow) keeps alerts on the ground tier while giving them the same physicality as Pile and Drilldown, and lets several of them stack.
|
|
177
|
+
|
|
178
|
+
### E9 · Which treatments carry the seven newer components?
|
|
179
|
+
|
|
180
|
+
A design review like E8: static renders of every state in Daylight and Nightfall at 2×, judged on whether each says its meaning in Altitude's vocabulary without suggesting an affordance it lacks.
|
|
181
|
+
|
|
182
|
+
- **CommandPalette.** The query field is a real `Input` inset in the level-5 surface. Making the field the palette's top edge was rejected: with no rim it can't show empty against filled, and it read as a header bar.
|
|
183
|
+
- **Sheet.** Inset 8px from its edge rather than flush, because a flush sheet shows its height along one side only. The bottom sheet's handle is a recessed finger pull (an opening) rather than a flat pill.
|
|
184
|
+
- **HoverCard.** A level-3 card. A hover-intent "fuse" drawn on the trigger was rejected: it spends motion on every pointer pass over a paragraph of names.
|
|
185
|
+
- **Select and Combobox.** The chosen option wears the Checkbox's tick at the row's end. A mini checked box read as multi-select; a leading tick column wasted width.
|
|
186
|
+
- **Stepper.** Plates on one ground line, in the breadcrumb's vocabulary. Marker discs on a track were rejected because they read as a quantity. Completed plates had to be the ground itself: in Daylight they were nearly the same tone as the imprints of steps to come.
|
|
187
|
+
- **Table.** One slab per row, drawn from the first cell at the table's measured width; per-cell shadows showed seams between cells.
|
|
188
|
+
- **Board.** Columns open into wells only while a card is carried; a collapsed column as a `Pile` with its count reads as a quantity without needing the cards.
|
|
189
|
+
|
|
190
|
+
### E10 · Approach, lamps and field focus, after use
|
|
191
|
+
|
|
192
|
+
A review of three things that looked right in stills and didn't hold up in use. Candidates were rendered in Daylight and Nightfall at 2×, and the interactions checked in a browser.
|
|
193
|
+
|
|
194
|
+
- **Approach on raised things.** Buttons, crumbs, cards, piles, steps and board cards climbed toward an approaching pointer, and buttons, crumbs and cards leaned up to 2px after it across their face. In use, a button that shifts under a moving pointer reads as a fault, not as readiness. **Decision:** nothing raised responds before the pointer is on it, and nothing follows the pointer; hover is one discrete change. Fields keep approach, because a field doesn't move: only its depth changes, and that previews what focusing it will do.
|
|
195
|
+
- **Status lamps.** The glossy bead (a radial highlight, a lightened face in light schemes, a dark socket ring and a drop shadow) read as a separate object sitting on the plate, and its brightened colours didn't match the tone colours used everywhere else. Four candidates: the bead, a flat dot in the tone's own colour, a flat dot with a ring of its `-soft` tint, and a flat dot mixed 18% into the plate. **Decision:** the flat dot in the token itself. It matches the tone's text exactly; the tinted ring disappeared on neutral plates; the mixed dot looked washed out in Daylight. A neutral lamp is an empty ring, so lit and unlit differ in shape as well as colour. Daylight's `caution` dot is a deep amber that leans brown, because the token is tuned for 4.5:1 text; it's kept so the dot and the word agree.
|
|
196
|
+
- **Field focus.** An accent rim with an accent halo lit up every field you clicked into, so a form in use glowed more than its primary action. Candidates: 2px of `ink`, a 1px `ink` hairline with a 3px neutral halo, 2px of `ink` mixed into `line-control`, and that rim plus holding an empty field at its approached depth. Full `ink` was harsh in the dark schemes; the hairline and halo was too faint. **Decision:** 2px of `ink` mixed 80% into `line-control` (`--alt-rim-held`), with the empty field holding its depth while focused. The rim measures at least 3:1 against the resting rim in every scheme (Daylight 3.46, Nightfall 3.33, Mesa 3.05, Alpine 3.43) and at least 11:1 against the ground, so it meets Focus Appearance. Invalid fields keep a `critical` rim. Keyboard focus on everything you press or switch is still the `accent-ink` ring.
|
|
197
|
+
- **Field focus, follow-up: the rim is for the keyboard.** Even neutral, the rim appeared on every click. Focus Visible asks only that keyboard focus be visible, and names a text field's caret as one form of it ([W3C](https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html)). **Decision:** a field you click into shows its lit floor and the caret; the rim appears only when focus arrived from the keyboard, decided as focus arrives so that typing doesn't add it. `:focus-visible` can't do this, because browsers match it on text fields after a click too, so a shared listener (`useKeyFocus`) marks the field. On a filled field, clicked focus changes nothing but the caret, which is enough when you've just clicked it. The CommandPalette's field never shows the rim, since focus never leaves it.
|
|
198
|
+
|
|
199
|
+
### E11 · How should an empty region read?
|
|
200
|
+
|
|
201
|
+
`EmptyState` was rendered at page and widget size in all four schemes at 2×, under forced colours and with `contrast="more"`, and its keyboard path and announcement checked in a browser. The choices below are reasoned from the model and not yet tested against alternatives with people.
|
|
202
|
+
|
|
203
|
+
- **Sunk, not raised.** A raised card holding the message would sit at the height of content, as if "empty" were a record; an `Alert` is the register for problems. A well with a dashed rim puts it with the empty field and the armed `DropZone`, so the rule stays one rule: sunken things want content. shadcn/ui's `Empty` (media, title, description, content) set the anatomy; Altitude keeps it and adds the opening.
|
|
204
|
+
- **Deepening under the action only.** Deepening whenever a pointer crossed the region would spend motion on every pass. Deepening only while the primary action is pointed at or focused previews what pressing it does (a record lands here), which makes it feedforward.
|
|
205
|
+
- **No pulse.** Anything looping on an empty state spends the attention budget on the least important thing on screen.
|
|
206
|
+
|
|
207
|
+
### E12 · Why didn't the avatar look like the rest of the system?
|
|
208
|
+
|
|
209
|
+
A design review like E8: the avatar was rendered beside Badge, Button, Card, Stepper and Pile in all four schemes at 2×, then two directions were rendered side by side with photos, sizes from 24 to 64px, a group, and the avatar in a list row and a card. It was compared with the avatar guidance of Fluent 2, Primer, Atlassian and Carbon, and with Slack's and Teams' presence conventions.
|
|
210
|
+
|
|
211
|
+
What didn't fit, each against a rule the rest of the system already keeps:
|
|
212
|
+
|
|
213
|
+
- **Status colours as identity.** Faces were tinted from `accent`, `positive`, `caution` and `critical` by a hash of the name, so a person could wear `critical-soft` and read as an error, and in the dark schemes the tints went muddy. The README reserves those hues for status. Fluent and Flux hash names into a palette of 12–30 identity colours that has no other meaning; Altitude has no such palette, and inventing one would break "never invent an in-between colour". Badge already answered the same question (E8, E10): keep the plate calm and put colour in a small lamp.
|
|
214
|
+
- **Offline drawn as an opening.** The offline face was `surface-well` with an inset shadow: the sunk tier, which means "waiting to receive". An offline person is part of the record, so the face is now a badge plate: flush, ground tier.
|
|
215
|
+
- **Three heights one level apart.** Active hovered about 15% of its size and idle rested, both inside the raised tier, which the laws forbid when the difference is a meaning. At 24–44px the gap between them was 3–6px and the soft shadow under an active face read as a grey underline. In collaborative tools presence is coarse ([Gutwin & Greenberg](https://grouplab.cpsc.ucalgary.ca/grouplab/uploads/Publications/Publications/1995-Awareness.NZ.pdf)), and Slack itself shows only active and away, as a filled or a hollow dot.
|
|
216
|
+
- **A gloss of its own.** A radial white highlight that nothing else in the system uses.
|
|
217
|
+
- **A ragged group that lifted on hover.** Active faces stood higher than idle ones, and every face lifted under the pointer whether or not clicking did anything, against "hover is a promise".
|
|
218
|
+
|
|
219
|
+
**Decision.** Height says whether a person is here, not how busy they are. Everyone here rests on the page at level 1 with the card's own shadow; active and idle differ by the lamp's shape (filled, or an empty ring, as Badge's unlit lamp), so the difference survives colour blindness and forced colours (a common complaint about Teams' green and yellow, which differ by hue alone). The detached shadow is kept for what it says best: in "The art of the float" ([2023](https://pmc.ncbi.nlm.nih.gov/articles/PMC10434713)), a contact shadow made an object read as resting and a detached one as floating, so leaving the ground is now a categorical signal, used only by someone live and by a pressable avatar under the pointer. Faces are neutral `surface-raised`. Groups stand on one ground line with 12% overlap, because at a fifth plus the ground cut-out a later face covered the initials beneath it, and at 15% wider fallback fonts (DejaVu Sans, Verdana) still clipped "TM". Initials take the first and last words and skip punctuation and emoji, after the problems listed in ["Names are complex"](https://hey.georgie.nu/avatar-initials/) for "Mara de Quist"-style names; below 28px they drop to one letter.
|
|
220
|
+
|
|
221
|
+
**Following the other systems.** Fluent, Primer and Atlassian draw people as circles and everything else (teams, organisations, bots, agents) as squares, so `shape="square"` does the same, its corners taken from the radius scale by size. Primer and Atlassian ask for alt text only when no name is shown beside the avatar; Altitude always labels the face, and labels a group with its count and its overflow with the names it hides, because a "+2" alone tells a screen reader nothing. Carbon falls back from a photo to initials; Altitude also falls back when a photo fails to load.
|
|
222
|
+
|
|
223
|
+
### E13 · How should several fields of one record be edited in place?
|
|
224
|
+
|
|
225
|
+
A design review, not a study: `EditRegion` was prototyped in the EditRegion preview (three people with access, each row editable) and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keyboard and race paths checked in a browser (`checks/edit-region.mjs`). It was compared with a Dialog per row (the existing ActionsInPlace recipe) and with one `InlineEdit` per field.
|
|
226
|
+
|
|
227
|
+
- **In place, not modal.** NN/g's guidance on data tables notes that a modal editor hides the neighbouring rows people consult to choose a value ([Data tables: four major user tasks](https://www.nngroup.com/articles/data-tables/)), and Shneiderman's direct manipulation asks for a continuous view of the objects with rapid, reversible, incremental actions ([1983](https://doi.org/10.1109/MC.1983.1654471)). The region keeps the list visible and lifts only the record being worked on.
|
|
228
|
+
- **One write, not several.** Independent `InlineEdit`s can leave a record half-changed when the second save fails. Saving the draft as one `onSave` with a `changed` list keeps the record consistent and still lets an application send a patch.
|
|
229
|
+
- **No save on blur.** People sometimes click away from a multi-field editor expecting their work to persist. Autosave was rejected because the Actions in place guide rules it out and because a blur isn't a deliberate act; instead the draft simply stays open and nothing is lost.
|
|
230
|
+
- **Whole-record conflicts.** A per-field merge (save the fields nobody else touched) was considered and rejected for now: it hides the other writer's change from the person saving, and `InlineEdit` already blocks the whole save.
|
|
231
|
+
|
|
232
|
+
Not yet tested with people: whether the raised editor is noticed as “still unsaved” when someone scrolls away, and whether five fields is the right ceiling before a `Sheet`.
|
|
233
|
+
|
|
234
|
+
### E14 · How should a set of tags be edited?
|
|
235
|
+
A design review, not a study: `TagInput` was prototyped in its preview and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keyboard paths checked in a browser (`checks/tag-input.mjs`). It was compared with the chip and tag models of React Aria `TagGroup`, MUI `Autocomplete multiple`, Material input chips, Fluent v9 `TagPicker` and Ant Design `Select mode="tags"`.
|
|
236
|
+
- **Tags are labels, not controls.** They're part of the record, so they're Badge plates flush in the field's floor rather than raised chips; only the remove button reacts to a pointer. Material and Fluent raise chips; here that would say every tag is a button to press.
|
|
237
|
+
- **One Tab stop for the tags.** React Aria and Fluent v9 make the tag group one stop with arrow keys; a stop per tag is slow with 20 tags. The input's description and the tags' own description state the keys.
|
|
238
|
+
- **Backspace arms before it removes.** A single Backspace that deletes a tag is a common source of accidental loss, worse when removal is persisted. Arming (the tag lifts and is announced) costs one keystroke and makes the next one's effect visible first.
|
|
239
|
+
- **The limit keeps focus.** Hiding or disabling the input at `max` drops keyboard focus to the body ([allxsmith/bestax#1103](https://github.com/allxsmith/bestax/issues/1103)); a read-only input with “Limit reached” keeps it.
|
|
240
|
+
Not yet tested with people: whether arming is understood without the announcement, and whether people find Enter-to-edit on a focused tag.
|
|
241
|
+
### E15 · How should a date be chosen?
|
|
242
|
+
|
|
243
|
+
A design review, not a study: `DatePicker`, `DateRangePicker` and `Calendar` were prototyped in their preview and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with the APG grid keys, RTL and the segmented field checked in a browser (`checks/date-picker.mjs`). They were compared with the [APG date picker dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/), React Aria's `DateField`/`DatePicker`, shadcn/ui's `Calendar` and native `<input type="date">`.
|
|
244
|
+
|
|
245
|
+
- **Typed segments, not free text.** A free-text date field has to guess the order of “03/04/2026”; segments in the locale's order (from `Intl.DateTimeFormat.formatToParts`) make the order visible and work the same way in every locale, React Aria's reasoning. Native `type="date"` was rejected: its popup can't be styled into the elevation language and differs per browser.
|
|
246
|
+
- **Days are flush; the choice is raised.** A well per day (the Checkbox build) at 42 days a month reads as texture. Keeping only the RadioGroup's one raised thumb, carried between days, says “one choice” and keeps the grid quiet.
|
|
247
|
+
- **A month change cross-fades.** Sliding months sideways is common, but the Motion rules keep travel for changes of level and of hierarchy; a month is neither.
|
|
248
|
+
- **ISO strings.** `Date` carries a time and a zone and shifts days across them; `Temporal.PlainDate` is right but needs a polyfill in some 2026 browsers. A `'YYYY-MM-DD'` string is exact and dependency-free.
|
|
249
|
+
|
|
250
|
+
Not yet tested with people: whether the typed segments are discovered before the calendar button, and whether the range's “choose the end” prompt is enough.
|
|
251
|
+
|
|
252
|
+
### E16 · How should a hierarchy be changed in place?
|
|
253
|
+
|
|
254
|
+
A design review, not a study: `Tree` was prototyped as a page tree and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with the APG keys, RTL, rename, create and both carry paths checked in a browser (`checks/tree.mjs`). It was compared with the [APG tree view](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/), VS Code's explorer, Notion's sidebar, React Aria's `Tree` and outliners (Workflowy, Logseq).
|
|
255
|
+
|
|
256
|
+
- **Flat rows with levels, not nested groups.** The APG allows either; a flat list with `aria-level`, `aria-setsize` and `aria-posinset` keeps one row model for rendering, focus and carrying, and supports lazy loading and later virtualisation.
|
|
257
|
+
- **Carry by keyboard, shown in place.** The APG doesn't cover moving. Outliners use Alt+↑/↓ and indent/outdent; file managers use cut and paste. Picking up with Space (as `SortableList`) and showing the node where it will land, with its depth announced, keeps the result visible before the drop.
|
|
258
|
+
- **Into versus between.** Pointer targets split a row into thirds: before, into, after. “Into” opens the node as a trench, a receptacle, and “between” is a thin well at the target depth, so the two read differently without colour. Spring-loaded folders (open after 600ms) follow the Finder convention.
|
|
259
|
+
- **No hover-only actions.** Row buttons for “add” and “rename” would put interactive elements inside `treeitem`; instead the shortcuts are on the row and `TreeHandle` lets a toolbar offer the same actions to pointer users.
|
|
260
|
+
|
|
261
|
+
Not yet tested with people: whether ↑ entering an open node above (rather than staying at the same depth) matches expectation, and whether the toolbar is found.
|
|
262
|
+
|
|
263
|
+
### E17 · How should a list be written into?
|
|
264
|
+
|
|
265
|
+
A design review, not a study: `Checklist` was prototyped with a nested and a flat list and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keys checked in a browser (`checks/checklist.mjs`). It was compared with Workflowy, Logseq, Things, Notion to-do blocks and GitHub task lists, which agree on Enter for a sibling, Tab/Shift+Tab to indent, and Backspace to merge.
|
|
266
|
+
|
|
267
|
+
- **Commit on leaving the line.** Every outliner saves a line when you leave it; an explicit Save per line would make the list unwritable. It departs from `InlineEdit`'s rule, so it's scoped to this component, and a refused change keeps the text with Retry.
|
|
268
|
+
- **Text inputs in a list, not a tree.** The tree pattern's arrow keys would fight the caret; nested `ul`s with ordinary inputs keep text editing native and the structure readable.
|
|
269
|
+
- **No focus trap.** Tab indents, so Escape leaves the line first; then Tab moves on, as it does from the last, empty line.
|
|
270
|
+
- **Ops, not arrays.** Reporting one op per action lets an application persist and merge per item instead of replacing the whole list.
|
|
271
|
+
|
|
272
|
+
Not yet tested with people: whether Escape-then-Tab is discovered, and whether Backspace-to-merge surprises people who expect it to do nothing at the start of a line.
|
|
273
|
+
|
|
274
|
+
### E18 · How should a query be built in place?
|
|
275
|
+
|
|
276
|
+
A design review, not a study: `FilterBar` was prototyped over an issue table and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keys checked in a browser (`checks/filter-bar.mjs`). It was compared with Linear's filters, Notion database filters, GitHub's search tokens, Jira's basic search and Airtable.
|
|
277
|
+
|
|
278
|
+
- **Conditions as sentences.** “Status is In review” reads the same to everyone and is the accessible name of each condition; segment buttons say which part they change.
|
|
279
|
+
- **One Tab stop.** The [APG toolbar](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) roving model keeps a row of five conditions (twenty segments) from adding twenty Tab stops.
|
|
280
|
+
- **Filter in place.** Removing non-matching rows reflows the table and loses people's place; with `relevance`, matches rise and misses sink, so positions survive (the existing **Relevance** signal).
|
|
281
|
+
- **Free text as a token, not a search box.** A “Title contains” condition sits beside the others, so the whole query is visible and editable in one place.
|
|
282
|
+
|
|
283
|
+
Not yet tested with people: whether the field-then-value sequence for a new condition is faster than a single combined picker.
|
|
284
|
+
|
|
285
|
+
### E19 · How should time be handled directly?
|
|
286
|
+
|
|
287
|
+
A design review, not a study: `Schedule` was prototyped as a room-booking week and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with every operation checked by keyboard and by pointer in a browser (`checks/schedule.mjs`). It was compared with Google Calendar, Outlook, Notion Calendar, FullCalendar and Bryntum.
|
|
288
|
+
|
|
289
|
+
- **Direct manipulation, with a keyboard twin for each drag.** Shneiderman's direct manipulation asks for visible objects and rapid, reversible actions ([1983](https://doi.org/10.1109/MC.1983.1654471)); WCAG 2.2 SC 2.5.7 asks that anything done by dragging can be done without it. Space-to-carry (as `Board` and `SortableList`), Shift+arrows to resize and Enter on empty time to create give each drag a keyboard path.
|
|
290
|
+
- **A grid of slots.** Rows of time slots by day columns make arrow navigation predictable and give every empty slot a name (“Tuesday 27 October, 10:15”) to create into; events are buttons inside their start slot.
|
|
291
|
+
- **Time opens only while something could land.** As Board's columns, days stay flat at rest and open into wells while carrying or drawing, so the grid isn't a field of holes.
|
|
292
|
+
- **Naive local times.** A zoned type would need a library or `Temporal`; local `YYYY-MM-DDTHH:mm` strings are exact and leave the zone to the application, which already knows it.
|
|
293
|
+
|
|
294
|
+
Not yet tested with people: whether Enter-to-create on empty time is found, and whether the side-by-side overlap layout stays readable past three events.
|
|
295
|
+
|
|
296
|
+
### E20 · How should a table be edited cell by cell?
|
|
297
|
+
|
|
298
|
+
A design review, not a study: `DataGrid` was prototyped as an inventory grid and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keys checked in a browser (`checks/data-grid.mjs`). It was compared with the [APG data grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/), MUI X Data Grid, AG Grid, Glide Data Grid, Airtable and Excel.
|
|
299
|
+
|
|
300
|
+
- **A sibling, not a DataTable mode.** DataTable's README says it's “a table, not a grid”; adding cell focus and editing would change its semantics for everyone.
|
|
301
|
+
- **Enter, Tab and a click commit.** InlineEdit keeps a session open across blur, but every spreadsheet commits on Enter, Tab and clicking another cell, and fighting that makes entry unusable. Focus leaving the grid is the one case that keeps the draft open, marked unsaved, so nothing is lost or written silently.
|
|
302
|
+
- **No hover lift.** A click selects, so a lift would promise an action that doesn't happen; the header's pencil and `aria-readonly` carry editability.
|
|
303
|
+
- **Ranges are a tint.** Height is reserved for live content and writes; a selection is only a target.
|
|
304
|
+
|
|
305
|
+
Not yet tested with people: whether “Not saved yet” is noticed when someone tabs out of the grid mid-edit.
|
|
306
|
+
|
|
307
|
+
### E21 · How should a list of records be changed in place?
|
|
308
|
+
|
|
309
|
+
A design review, not a study: `ItemGroup` was prototyped with webhooks and a form builder's options and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keyboard paths checked in a browser (`checks/item-group.mjs`). It was compared with shadcn/ui's Item, GOV.UK's add-another pattern, Ant Design's `Form.List` and the Files recipe in Actions in place.
|
|
310
|
+
|
|
311
|
+
- **In place, not a dialog per operation.** Shneiderman's direct manipulation asks for continuous visibility and rapid, reversible actions ([1983](https://doi.org/10.1109/MC.1983.1654471)); NN/g finds modal editors hide the neighbours people refer to, and that one or two visible actions per row beat a hidden menu.
|
|
312
|
+
- **Undo over confirmation, when it's real.** Raskin's “never use a warning when you mean undo” supports immediate removal with recovery; but only the application knows whether it can reverse a removal, so `removal` has no default and confirmation remains for the irreversible case.
|
|
313
|
+
- **One editor at a time.** Two open editors split attention and make “which Save?” ambiguous; opening another keeps the first and returns focus to it, rather than discarding work.
|
|
314
|
+
|
|
315
|
+
Not yet tested with people: whether focus moving to the fresh add slat after adding is expected, and whether Undo without a time-out is noticed.
|
|
316
|
+
|
|
317
|
+
### E22 · How should a record's properties be edited?
|
|
318
|
+
|
|
319
|
+
A design review, not a study: `PropertyList` was prototyped as an issue sidebar and a “Check your answers” summary and rendered at 320 and 960px in all four schemes, under forced colours, reduced motion and `contrast="more"`, with its keyboard paths checked in a browser (`checks/property-list.mjs`). It was compared with GOV.UK's [summary list](https://design-system.service.gov.uk/components/summary-list/), Ant Design's Descriptions and ProDescriptions, and Linear's and Jira's issue sidebars.
|
|
320
|
+
|
|
321
|
+
- **The value is the button.** A separate “Change” link per row (GOV.UK) is clear but heavy in a sidebar; making the value plate itself the button, lifting on hover and named “Status: In review. Change status”, keeps the row quiet and the name meaningful out of context.
|
|
322
|
+
- **Read-only values don't lift.** Hover is a promise: only what opens an editor reacts, so editability is visible without a pencil per row.
|
|
323
|
+
- **Unset is an opening.** A dashed well that names what to add shows at a glance which properties are missing, as an empty field does.
|
|
324
|
+
- **Shortcuts off by default.** Linear's single-key shortcuts are fast, but WCAG 2.1.4 requires that they can be turned off or remapped.
|
|
325
|
+
|
|
326
|
+
Not yet tested with people: whether the hover-lifting plate is enough of an affordance on touch, where there's no hover (the open question in the issue).
|
|
327
|
+
|
|
328
|
+
### E23 · Can a chart palette stay clear of status, presence and the accent?
|
|
329
|
+
|
|
330
|
+
A measurement, not a study: `build/palette.mjs` (which CI reruns) searches OKLCH candidates in every scheme, simulates protanopia and deuteranopia after [Machado, Oliveira & Fernandes (2009)](https://doi.org/10.1109/TVCG.2009.113) at severity 1, and measures differences in [OKLab](https://bottosson.github.io/posts/oklab/) as ΔE×100 (not the ΔE₀₀ the other experiments use). Contrast is WCAG 2.
|
|
331
|
+
|
|
332
|
+
**A stock palette collides.** An 8-hue reference palette separates series well for colour-blind readers (worst adjacent ΔE 8.4–9.1) but lands within 15 of the reserved hues in every scheme (violet 3.6 from `presence-3` in daylight, red 7.7 from `critical` in nightfall), and in the light schemes four of its eight slots are under 3:1 on the ground. Okabe-Ito, Tableau 10, shadcn's `--chart-*` and the Recharts and ECharts defaults have the same problem: they use red, green, amber, magenta, teal and violet freely.
|
|
333
|
+
|
|
334
|
+
**The accent itself fails the gates.** `data-1` was proposed to be the accent. The shipped accents sit 9.6 (daylight), 6.0 (nightfall) from `presence-3`, 5.0 from `critical` (mesa) and 7.5 from `positive` (alpine), and both dark accents are lighter (OKLCH L 0.73 and 0.80) than a dark scheme's marks may be. So `data-1` takes the accent's *hue* at a lightness that clears them; in mesa no lightness of terracotta clears `critical` and `caution`, so it takes the nearest hue that does (a salmon, h 35 against the accent's 39). Charts' "never a series in `accent`" holds.
|
|
335
|
+
|
|
336
|
+
**Six slots need the reserved distance at 12, not 15.** With every other gate in place, the light schemes hold only four slots at 15: daylight and mesa leave about 85 candidates once 3:1 on white is required, and their status and presence hues sit at the same lightness as those candidates. Five fit only at chroma 0.24–0.30, which is neon beside the rest of the system. Daylight fails six at 14 and at 13; at 12 all four schemes hold six. Twelve is still well clear of confusion (the closest pair is 12.0, a plum `data-5` against daylight's violet `presence-3`), but it is a deliberate loosening of the issue's 15, recorded here.
|
|
337
|
+
|
|
338
|
+
**Gates added during the search.** Optimising only adjacent pairs let two slots share a hue (two greens 15 apart in lightness): slots now also keep 25° of hue and colour-blind ΔE ≥ 6 between *any* two, since a legend or a crossing line puts any pair side by side. The diverging ramp's best colour-blind pair was olive against indigo, which reads muddy at the dark pole; it's held to orange against blue (ColorBrewer's PuOr and RdBu family), which loses little (13.5 against 14.1 at the weakest step in daylight).
|
|
339
|
+
|
|
340
|
+
The chosen palette, by scheme:
|
|
341
|
+
|
|
342
|
+
| Scheme | Worst adjacent CVD ΔE | Worst CVD ΔE, any pair | Worst normal ΔE, any pair | Lowest contrast on ground or raised | Closest reserved hue | Smallest ramp step (L) | Diverging sides, worst CVD ΔE |
|
|
343
|
+
|---|---|---|---|---|---|---|---|
|
|
344
|
+
| daylight | 19.6 | 6.6 | 15.2 | 3.08:1 | `data-5` ↔ `presence-3` 12.0 | 0.089 | 13.5 |
|
|
345
|
+
| nightfall | 24.4 | 6.7 | 15.2 | 3.01:1 | `data-5` ↔ `accent` 12.3 | 0.089 | 13.6 |
|
|
346
|
+
| mesa | 22.8 | 6.8 | 15.1 | 3.00:1 | `data-1` ↔ `accent` 12.0 | 0.089 | 13.5 |
|
|
347
|
+
| alpine | 25.0 | 7.7 | 15.2 | 3.02:1 | `data-4` ↔ `presence-3` 13.0 | 0.088 | 11.6 |
|
|
348
|
+
|
|
349
|
+
Adjacent slots are now two to three times as far apart for colour-blind readers as the reference palette's (19.6–25.0 against 8.4–9.1). The cost is that any two slots can be as close as 6.6, so charts with more than a few series still need direct labels or patterns ([#72](https://github.com/richlaconte/altitude/issues/72)). `data-other` comes out near-black in the light schemes (L 0.33–0.35): every lighter neutral that holds 3:1 was within 15 of a slot. Not yet tested with people: whether the slots are told apart as well as these distances predict, in real charts and at small sizes.
|
|
350
|
+
|
|
351
|
+
### E24 · Does the chart frame sit on the ground?
|
|
352
|
+
|
|
353
|
+
A design review, not a study: `ChartContainer` was rendered with hand-drawn lines on a time scale and grouped bars on a band scale, each in a `Card`. The renders covered all four schemes at 960px, 360px at 2×, forced colours (Chromium's emulation) and `contrast="more"`.
|
|
354
|
+
|
|
355
|
+
- **Flat and flush.** The plot, gridlines and marks read as part of the card, and the bars stand on the ground line. Nothing in the frame has a level.
|
|
356
|
+
- **Ticks were tuned twice.** Monthly data first showed only every third month, because ticks were spaced for 84px labels; they're now spaced for the label's width (56px), so six months fit in a half-width card. At 360px the y axis allowed two ticks and stretched a $12K maximum to $20K; it now keeps at least three.
|
|
357
|
+
- **Forced colours keep the frame but not the series.** Tick labels and the ground line turn `CanvasText` and gridlines `GrayText`, but in Chromium's emulation the marks keep their `data-*` colours, so series aren't told apart without colour. Patterns, markers and `CanvasText` marks are #72.
|
|
358
|
+
- **Decided while building.** Charts ship in the main entry for now, because the scales and frame add about 8 KB to `bundle.js`. Tick labels use Instrument Sans with tabular figures, not JetBrains Mono, because they're labels, not values to copy. SVG only: past about 2,000 marks, aggregate or use a canvas library with Altitude's theme (#77).
|
|
359
|
+
|
|
360
|
+
Not yet tested with people: whether a generated summary is as useful as a written one.
|
|
361
|
+
|
|
362
|
+
**Without colour (#72).** Each slot has a fixed dash, marker and hatch, after ECharts' `aria.decal`, which it ships off by default. Altitude turns them on under forced colours and in print, and with the `patterns` prop anywhere else. Under Chromium's forced-colours emulation, `checks/chart-patterns.mjs` renders six hatched bars and four dashed, marked lines in `CanvasText`, all distinct; `GrayText` gridlines come out as the emulated system grey (a dark red, rgb 96 0 0). The print render keeps the colours and adds the patterns, so a greyscale printout still separates the series. Not yet checked on real Windows contrast themes.
|
|
363
|
+
|
|
364
|
+
### E25 · Does BarChart read flat, in every form?
|
|
365
|
+
|
|
366
|
+
The prototype review moved here from the Charts chapter (#48): `BarChart` was rendered as one sorted series, grouped, stacked, stacked to 100%, with negative values, and horizontal with long names. Each was in a `Card`, in all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`. `checks/bar-chart.mjs` measures the geometry.
|
|
367
|
+
|
|
368
|
+
- **Flat and standing.** Every bar meets the ground line square and is rounded only at its data end, and none has a shadow or a filter. Stacked segments and grouped neighbours are 2px apart, so parts read as parts without an outline. A hovered bar on a static chart doesn't change at all.
|
|
369
|
+
- **Negative bars read as position, not depth.** They hang below the ground line with the same flat fill, rounded at their lower end; nothing about them looks sunk.
|
|
370
|
+
- **Labels were thinned too eagerly.** Category labels were thinned by the widest label plus 12px, which hid every other month at 960px. They're now thinned by each pair of neighbours' actual widths plus 4px, and every label shows in all six charts.
|
|
371
|
+
- **Horizontal needed a budget.** Long category names took two-thirds of a card and crushed the value axis until its ticks overlapped. Names now take at most 40% of the width and are cut with an ellipsis (the full name stays in the summary), and the value ticks are counted from what's left.
|
|
372
|
+
- **Forced colours.** Hatches tell all three stacked and grouped series apart in `CanvasText`; the first series stays solid.
|
|
373
|
+
|
|
374
|
+
Not yet tested with people: whether a 2px gap is enough to separate stacked segments at a glance for colour-blind readers when patterns are off.
|
|
375
|
+
|
|
376
|
+
### E26 · Does LineChart keep to its data?
|
|
377
|
+
|
|
378
|
+
A design review like E25: `LineChart` was rendered with intraday latency (two series every 30 minutes), a year of monthly revenue on a monotone curve, daily signups with missing days, and four converging plans. Each was in a `Card`, in all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`. `checks/line-chart.mjs` measures it.
|
|
379
|
+
|
|
380
|
+
- **Gaps stay gaps.** Missing days break the line into runs; nothing is interpolated across them.
|
|
381
|
+
- **The curve doesn't invent data.** Sampled along the monotone path, the line never leaves the range of the two points it runs between.
|
|
382
|
+
- **Hour labels collided.** Time ticks were spaced for a fixed 56px, and "12:00 AM" and "6:00 AM" overlapped. Ticks are now chosen as the finest interval whose labels clear each other at the chart's width, and hours read "6 AM".
|
|
383
|
+
- **A foot that wasn't ground.** With zero off the axis (signups 40–80), the plot's foot still wore the ground line's `ink-muted` rule, which read as zero. It's now an ordinary gridline, and only zero is ground.
|
|
384
|
+
- **End labels.** Four converging lines (Free, Team, Business) put their names on top of each other; they're nudged at least 16px apart, with each name staying beside its line's end.
|
|
385
|
+
|
|
386
|
+
Not yet tested with people: whether end labels nudged away from their line's end are still read as belonging to it.
|
|
387
|
+
|
|
388
|
+
### E27 · Does the readout follow the data without drifting?
|
|
389
|
+
|
|
390
|
+
A design review: the readout was rendered open on line and bar charts in all four schemes and under forced colours, and `checks/chart-tooltip.mjs` drove it with a mouse and with touch. The surface, rise and warm-up were compared with `Tooltip`; the behaviour with shadcn/ui's `ChartTooltip`, Recharts' cursor and ECharts' axis pointer, which all follow the pointer continuously.
|
|
391
|
+
|
|
392
|
+
- **Steps, not glides.** Its position isn't transitioned: it jumps to the next x, with the crosshair, as a card in hand follows the pointer. Gliding between positions read as lag, and following every pixel (Recharts' default) would put it against E10's rule that nothing drifts across its own face.
|
|
393
|
+
- **Never over the point.** It opens on whichever side of the point has room, so it never hides the value it's reading.
|
|
394
|
+
- **Lighter than what it's read against.** The crosshair is 1px `line-control`, which holds 3:1 on every ground, and a reference line is `ink-muted`. Where they cross, the line the reader came to read against stays the stronger one (#186 brought Charts.md into line).
|
|
395
|
+
- **Only what's read changes.** Lines ring their points in the plot's own colour, an outline rather than height; bars stay exactly as they were, because hovering a static bar does nothing.
|
|
396
|
+
- **Touch.** Press and hold (250ms) opens it, so a quick swipe still scrolls the page; dragging scrubs; it stays after the finger lifts until a tap elsewhere.
|
|
397
|
+
- **Dates as categories.** A bar per week printed raw ISO strings ("2026-03-02") on the axis; date-valued categories now read as dates ("Mar 2") on the axis and in full in the readout.
|
|
398
|
+
|
|
399
|
+
Not yet tested with people: whether the 250ms hold is discoverable on touch without a hint.
|
|
400
|
+
|
|
401
|
+
### E28 · Does a legend you can toggle look like one?
|
|
402
|
+
|
|
403
|
+
A design review: `ChartLegend` was rendered static (stacked bars) and toggleable (four lines, one hidden from the start), in all four schemes, under forced colours, with `contrast="more"` and with keyboard focus. `checks/chart-legend.mjs` drove it from the keyboard and the pointer. It was compared with the legends of Recharts, ECharts and Chart.js, which all toggle on click with keys that look like plain labels: the invisible affordance Burmistrov et al. (2015) measured.
|
|
404
|
+
|
|
405
|
+
- **Keys that look like choices.** Each toggle key lifts one level under the pointer, as a ghost button does, and holds a trench like a Checkbox's, so it reads as something to press before it's pressed. A static legend has none of this: its keys sit flush.
|
|
406
|
+
- **The line swatch didn't fill.** For a line chart the shown key first drew a thin line inside the trench, which in the dark schemes left the trench looking empty. A toggle key's swatch is now always a block that the series colour pours into; the static legend keeps the line.
|
|
407
|
+
- **Toggling is one change.** Hiding Team faded it out, showing Enterprise (12–17) rescaled y from 30–50 to 0–50 with the rescale staged first, and no other series changed colour.
|
|
408
|
+
- **The last series stays.** Its key is `aria-disabled` and described "At least one series stays shown", so the chart is never empty by accident.
|
|
409
|
+
|
|
410
|
+
Decided: no "show only this series" (isolate) yet. Alt-click is undiscoverable, and an "Only" button on hover adds a control per key; it can come later if people ask for it.
|
|
411
|
+
|
|
412
|
+
### E29 · Can the data be read without a pointer?
|
|
413
|
+
|
|
414
|
+
An engineering check, not a study: `checks/chart-keyboard.mjs` walks a two-series and a one-series `LineChart`, stacked and horizontal `BarChart`s, and hand-drawn marks from the keyboard, asserting every name, level change and position.
|
|
415
|
+
|
|
416
|
+
- **One Tab stop.** Every chart has exactly one node in the tab order at each level, and Tab leaves the chart in one press, where a node per point would have made a 49-point chart 49 Tab stops.
|
|
417
|
+
- **The units were missing.** A line chart's `y={{ format }}` formatted only its axis, so its readout and its points read "185" where the axis said "185 ms". A chart type's value format now reaches every place a value is said.
|
|
418
|
+
- **Rings on the marks.** A focused point's ring lands within 1.5px of the line's marker and of a horizontal bar's end.
|
|
419
|
+
|
|
420
|
+
Not yet checked by hand with a screen reader: VoiceOver and NVDA weren't available where this was built. The names are verified as accessible names, not as speech; whether `aria-roledescription` ("data point") reads well in both is the first thing to check.
|
|
421
|
+
|
|
422
|
+
### E30 · Where does a chart's table go?
|
|
423
|
+
|
|
424
|
+
A design review: the table view was rendered on a stacked bar chart, starting on the chart and on the table, in all four schemes and at 360px; `checks/chart-table.mjs` drives it. Chartability lists a data table as a core heuristic, and asks for it to be available to everyone, so it's a visible view rather than a visually hidden table.
|
|
425
|
+
|
|
426
|
+
- **In the chart's own box.** The table scrolls inside the plot's size, so the card and the page don't move when the view changes.
|
|
427
|
+
- **Two views of one record.** The control is the segmented `Tabs` (a pill gliding between two choices), and the views cross-fade rather than slide. Both stay mounted, so the chart keeps its size, motion and keyboard place while the table is shown; the hidden one is inert, which also takes the chart's keyboard path out of reach.
|
|
428
|
+
- **Opt-in, for now.** On by default, every widget-sized chart on a dashboard grows a control; the summary and the keyboard path already reach every value. Revisit if the table proves to be what people reach for.
|
|
429
|
+
|
|
430
|
+
### E31 · How should a chart wait?
|
|
431
|
+
|
|
432
|
+
A design review: the imprint, an empty plot with an action, a refetch and all-zero data were rendered in all four schemes and under forced colours, and `checks/chart-loading.mjs` drove a chart through undefined, a slow and a quick promise, a refetch, null, a rejection and a `RevealGroup`.
|
|
433
|
+
|
|
434
|
+
- **Shaped imprints, not a flat band.** Bars of varied height pressed into the page say "a bar chart is coming" at the chart's own size, and can't be taken for data: they're `surface-well`, carry no labels, and are announced as loading. Under forced colours they're dashed, like every sunk surface.
|
|
435
|
+
- **Fade, don't rise.** `Await`'s content rises 8px out of its imprint; a chart's marks would leave the ground line doing that, so they fade in place.
|
|
436
|
+
- **A refetch keeps what someone is reading.** It's never imprinted over; the old marks drop to half strength only after `dur-grace`, so a quick refetch shows nothing at all.
|
|
437
|
+
- **Two bugs found.** A rejected promise passed while older data showed was treated as a refetch and never reached the error boundary; it now does. And all-zero data drew on an invented −1 to 1 axis with the bars floating mid-plot; it now draws on 0–1, standing on the ground line.
|
|
438
|
+
|
|
439
|
+
Not yet compared with people: whether shaped imprints are ever read as data at a glance (the issue's open question).
|
|
440
|
+
|
|
441
|
+
### E32 · Can a trend be the size of a word?
|
|
442
|
+
|
|
443
|
+
A design review: `Sparkline` was rendered beside a headline number, in running text (a line with a normal range, bars with a zero day, and one still loading), and in a `DataTable` with decorative and labelled cells, in all four schemes at 960px and 360px, under forced colours at 2× and with `contrast="more"`. `checks/sparkline.mjs` measures it. Tufte calls sparklines "data-intense, design-simple, word-sized graphics" ([Tufte, *Beautiful Evidence*, 2006](https://www.edwardtufte.com/tufte/books_be)): their scale is the text beside them, which is why it has no axes and its label says the numbers in words.
|
|
444
|
+
|
|
445
|
+
- **As tall as the text.** It takes `1lh` of the surrounding type, so in a 23px paragraph it's 23px and in a table cell it's the cell's line. The first build zeroed its own line height to tidy the SVG's baseline, which made `1lh` zero and drew nothing; the SVG is a block instead.
|
|
446
|
+
- **Forced colours lost to a tone.** A sparkline with `tone="data"` kept `data-1` under forced colours, because the tone rule was more specific than the forced one. Every tone is now `CanvasText` when forced.
|
|
447
|
+
- **Gaps show as gaps.** A team with two missing days shows a short run, a break, then the rest; a single point between gaps is a dot.
|
|
448
|
+
- **Bars stand on zero.** A zero day has no bar, and the other bars still share one baseline, so the gap reads as nothing rather than as missing.
|
|
449
|
+
|
|
450
|
+
Not yet tested with people: whether `ink-muted` is enough contrast for a trend read at a glance in a dense table, or whether `data-1` should be the default.
|
|
451
|
+
|
|
452
|
+
### E33 · Does a stat say which way is good?
|
|
453
|
+
|
|
454
|
+
A design review: `Stat` was rendered as a hero number that loads, and in a `StatGroup` of four cards (users with `good="up"`, an error rate with `good="down"` that's rising, a neutral latency in ms, and a null), in all four schemes at 960px, at 360px at 2× and under forced colours, then refreshed. `checks/stat.mjs` drives it.
|
|
455
|
+
|
|
456
|
+
- **Up isn't always good.** Tremor's `BadgeDelta` colours an increase green; for an error rate that's the wrong way round. A stat stays neutral until it's told which direction is good, and then the word, the arrow and the lamp each say it, so a reader who can't tell red from green still reads "Up 21%" and sees the arrow.
|
|
457
|
+
- **No counting up.** Count-up animations delay the number they exist to show; Tversky, Morrison and Bétrancourt's finding (above) is that animation helps only when its motion is the change. The value cross-fades and the stat gets the afterglow.
|
|
458
|
+
- **The number is the record.** The stat has no shadow or level of its own; the `Card` it sits in is raised because it's something you can work with, as the Charts chapter says of dashboard cards.
|
|
459
|
+
- **A plate, not a pill of colour.** The change reuses `Badge` (a plate set into the page with a lamp), so a good or bad change looks like every other status in Altitude, and gets a 1px border under forced colours.
|
|
460
|
+
|
|
461
|
+
Not yet tested with people: whether a neutral, ink-muted change is noticed as readily as a coloured one on a busy dashboard.
|
|
462
|
+
|
|
463
|
+
### E34 · Is a meter a reading or a task?
|
|
464
|
+
|
|
465
|
+
A design review: `Meter` was rendered with storage under and past a threshold, seats near their limit, API calls over it, a reading still loading, and two segmented meters (storage by type, seats by role), in all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`, then pushed across a threshold. `checks/meter.mjs` measures it.
|
|
466
|
+
|
|
467
|
+
- **Flush, not a trench.** The issue proposed `Progress`'s build, a raised fill in a trench. The Charts chapter, written after it, settles it the other way: a task under way is live, a reading is the record. A raised fill would also lift an amount off the ground, which is the one thing a chart may never do. So the meter is a flush track with flat ink across it.
|
|
468
|
+
- **Over the limit stays in range.** `aria-valuenow` must lie between its min and max ([WAI-ARIA 1.2, meter](https://www.w3.org/TR/wai-aria-1.2/#meter)), so a reading over the limit reports the max and says the real reading and "over limit" in `aria-valuetext`.
|
|
469
|
+
- **Segment cuts in the surface's colour.** A 2px cut showing the track was too faint to separate two segments of similar lightness; the cut is the surface's own colour (the page, or the card), as a stacked bar's gap is.
|
|
470
|
+
- **The state is a word.** Caution and critical are colours only alongside "Near limit" and "Over limit" on a Badge, so they survive forced colours, where every fill is `Highlight`.
|
|
471
|
+
|
|
472
|
+
Not yet tested with people: whether a notch above and below the track is found as readily as a labelled threshold.
|
|
473
|
+
|
|
474
|
+
### E35 · Can a list row be its own bar and still be read?
|
|
475
|
+
|
|
476
|
+
A design review: `BarList` was rendered with 21 pages folded into seven and "Other", static referrers that re-rank and filter in place, and customers that load, in all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`. `checks/bar-list.mjs` measures it, including the contrast of `ink` over every bar composited on its card.
|
|
477
|
+
|
|
478
|
+
- **Text over a bar.** A 22% tint of `data-1` keeps `ink` at 4.5:1 or better over every bar in every scheme ([WCAG 2.2, 1.4.3](https://www.w3.org/TR/WCAG22/#contrast-minimum)), so the bar can sit behind the label instead of beside it, and the label gets the full row.
|
|
479
|
+
- **The bar stopped at the value.** In the first build the bar spanned the whole row, so the longest one ran under the share column and the percentage looked like part of the bar. The bar now runs under the label and the value only.
|
|
480
|
+
- **Wrap, don't cut.** A long path wraps at its slashes and hyphens. Breaking anywhere split words mid-way at 360px ("cha / rt"), so it only breaks a word when nothing else fits.
|
|
481
|
+
- **Lifting rows, flat bars.** As in `Table`, only rows that act lift. Lifting the bar with its row would make it a raised amount, so the bar is painted on the row and moves only because the row does.
|
|
482
|
+
|
|
483
|
+
Not yet tested with people: whether bars sized against the largest (rather than the total) are misread as shares when `share` is on.
|
|
484
|
+
|
|
485
|
+
### E36 · Can an area be flat and still read as a total?
|
|
486
|
+
|
|
487
|
+
A design review: `AreaChart` was rendered as three stacked regions over two weeks (with a legend you can toggle, the table view, and a change of week), plans as shares of 100%, and a single area of storage used, in all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`. `checks/area-chart.mjs` measures it.
|
|
488
|
+
|
|
489
|
+
- **No gradient.** The faded-to-transparent fill most libraries use by default would borrow the sheen's cue, "press it" (E4). A flat fill at 28% under a 2px line reads as a volume without it.
|
|
490
|
+
- **Parts parted by the surface.** A 2px line in the plot's own colour separates bands as a stacked bar's gap does. Under forced colours each band also takes its slot's hatch.
|
|
491
|
+
- **Labels on a halo.** Nothing gates `ink` at 4.5:1 against the `data-*` fills, so band names carry a 3px halo of the plot's colour (`paint-order: stroke`), and appear only where a band is at least 20px tall.
|
|
492
|
+
- **↑ means up.** The container moved ↓ to the next series, which in a stack drawn from the bottom is the band above, so ↓ went up the screen. A stacked area turns that around, and each point says its share of the total, as the issue asked.
|
|
493
|
+
- **An area-shaped imprint.** The loading imprint was bars or a band; an area chart now imprints an area.
|
|
494
|
+
|
|
495
|
+
Not yet tested with people: how well the middle bands of a stack are read, since only the bottom band stands on a common baseline (Cleveland & McGill's position-on-a-common-scale advantage applies to it alone). This is why the README puts the steadiest part at the bottom.
|
|
496
|
+
|
|
497
|
+
### E37 · Can data that isn't yet the record be told apart without lifting it?
|
|
498
|
+
|
|
499
|
+
A design review: `ChartStatus` was rendered as monthly revenue with October so far, daily signups projected from today with a 10th–90th percentile band, regional requests with a three-week forecast, and quarterly bookings with Q3 so far and Q4 projected (and a button that closes the period), in all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`. `checks/chart-status.mjs` measures it.
|
|
500
|
+
|
|
501
|
+
- **Pattern and a word, not height.** Altitude's pending writes hover until they land, but a hovering bar misstates its value, so the Commitment signal can't carry over. Partial and projected marks lie on the ground with the record, hatched or dashed, and say "So far" or "Projected" wherever a value is named.
|
|
502
|
+
- **Partial bars lose their fill.** A hatch on the plot's colour reads as "less than solid" at a glance, which is the point: the most common dashboard misreading is a half-counted month taken for a drop. A projection also keeps its outline, so it reads as a shape still to be filled.
|
|
503
|
+
- **One path, clipped.** Splitting a monotone line into two paths at the boundary put a kink there, because each half gets its own end tangents. The line is drawn once and clipped two ways, solid and dashed.
|
|
504
|
+
- **Patterns already use the hatch.** Under forced colours `data-2`'s own hatch is also 45°, so the pending hatch made its settled and projected parts identical. While patterns are on, a slot that has a hatch keeps it in the pending stretch, faded to 45%; slot 1, which is solid, takes the pending hatch.
|
|
505
|
+
- **Bands, not error bars.** After Correll & Gleicher (VIS 2014), the uncertainty is a flat fill at 16%. Its edges count toward y, and it's named in the readout.
|
|
506
|
+
- **Trends from the record.** The generated summary reads trends and extremes from the settled records and then says what's partial or projected. Otherwise a half-counted October would become the "lowest" month.
|
|
507
|
+
|
|
508
|
+
Not yet tested with people: whether "Today" is the right word for the rule when the first pending record is a partial period, since today falls inside that period rather than at its start.
|
|
509
|
+
|
|
510
|
+
### E38 · Can a chart carry its references without crowding its data?
|
|
511
|
+
|
|
512
|
+
A design review: `ChartReference` was rendered as uptime against a normal range, a target and a critical limit, with an outage annotated; cloud spend against a caution budget and a median that moves with the week; team scores against a bar on a horizontal chart; and latency against its mean with three crowded events, two of them buttons. In all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`. `checks/chart-reference.mjs` measures it.
|
|
513
|
+
|
|
514
|
+
- **Drawn by the frame, not by each child.** If each reference drew itself, no label could know where the others were. The children are declarative, like `ChartTooltip`, and the container lays out every label and plate together: labels nudged 16px apart, plates in rows, a label under a plate stepped down below the rows.
|
|
515
|
+
- **Solid, never dashed.** Dashes already mean "projected" (E37), so a reference line is solid. Its colour is `ink-muted`, or the status tone when crossing it means good or bad, and then the label also carries the icon, so the tone is never colour alone.
|
|
516
|
+
- **Bands under, lines over.** A band is a region the data passes through, so it lies under the marks. A line is something to read the marks against, so it's drawn over them, with its label on a halo.
|
|
517
|
+
- **Computed values at the data's precision.** A mean read through a format that doesn't round printed "162.52380952380952 ms". The label rounds it to one more place than the data has.
|
|
518
|
+
- **References count toward y.** A budget above every bar was off the plot. Their values now join the extent.
|
|
519
|
+
|
|
520
|
+
Not yet tested with people: whether right-end labels are found as quickly as labels at the left, where reading starts.
|
|
521
|
+
|
|
522
|
+
### E39 · Can a live chart say what changed without moving under the reader?
|
|
523
|
+
|
|
524
|
+
A design review: `ChartLive` was rendered as an error rate over a 15-minute window that ticks every two seconds, crosses a 2% alert and can be paused, and a queue-depth bar chart where one queue grows or every queue refreshes. In all four schemes at 960px, at 360px at 2×, under forced colours and with reduced motion. `checks/chart-live.mjs` measures it.
|
|
525
|
+
|
|
526
|
+
- **An outline, not a glow.** Altitude's afterglow lifts a surface to level 2. A mark can't lift, so the trace is a 2px `ink` outline in the mark's own shape (a bar's outline, a ring on a point), decaying on the same `dur-settle` and `ease-decay`. Its value shows for the first second, so a glance gets the number too.
|
|
527
|
+
- **More than four is one change.** A full reload that glowed every bar would spend the whole attention budget at once and point at nothing. Past four marks, the caption takes the tint instead, as a table re-sort does.
|
|
528
|
+
- **A window slides, it doesn't cross-fade.** The motion engine cross-faded whenever x's values changed under a line (Motion rule 5), which made a ticking chart blink every tick. A live chart whose records overlap now glides its x domain one step, and departing points ride off the left edge, clipped to the plot.
|
|
529
|
+
- **Holding still beats pausing.** The readout used to change under the pointer on every tick. Updates now queue while the pointer or focus is in the plot, and the caption says so. The switch is still there for WCAG 2.2.2, and for anyone watching from a distance.
|
|
530
|
+
- **Announce crossings, not ticks.** A polite announcement every two seconds would make the page unusable with a screen reader, so nothing is said unless `announce` returns a sentence.
|
|
531
|
+
|
|
532
|
+
Not yet tested with people: whether an outline that holds for one second is noticed in peripheral vision as readily as the surface afterglow's tint and lift, which is what Phosphor measured (Baudisch et al. 2006).
|
|
533
|
+
|
|
534
|
+
### E40 · Can a chart filter without reshuffling what was read?
|
|
535
|
+
|
|
536
|
+
A design review: `ChartFilter` was rendered as eight services filtered by team (with and without `rescale`), daily signups with weekends receding, two regions stacked over a year shown one quarter at a time, and a `BarList` of pages filtered by kind. In all four schemes at 960px, at 360px at 2×, and under forced colours. `checks/chart-filter.mjs` measures it.
|
|
537
|
+
|
|
538
|
+
- **Contrast, not height.** Relevance raises matches everywhere else in Altitude. A bar can't rise without leaving the ground line, so misses recede to 35% instead. That's two groups, the reading Research.md recommends for relevance anyway.
|
|
539
|
+
- **Each record owns its stretch.** Dimming a line by whole segments made a miss between two matches vanish into them. Each record now owns the stretch of x to the midpoints with its neighbours, drawn as one path clipped there, so the curve keeps its shape and the miss shows exactly where it is.
|
|
540
|
+
- **Seams.** Neighbouring stretches of an area met at fractional x, and the anti-aliased clip edges left faint vertical lines. Edges on whole pixels, with crisp clip rects, remove them.
|
|
541
|
+
- **The count is the answer.** "3 of 8 services match" is text in the frame and the summary, so the result never depends on seeing the fade.
|
|
542
|
+
|
|
543
|
+
Not yet tested with people: whether 35% is enough for misses to recede under the chart's own contrast. It's the value DataTable's misses approach, but marks are much smaller than rows.
|
|
544
|
+
|
|
545
|
+
### E41 · Can a mark promise a click without lifting?
|
|
546
|
+
|
|
547
|
+
A design review: `ChartSelect` was rendered as revenue by region that cross-filters a monthly chart, and bookings by quarter that drill into their months inside a `Drilldown`. In all four schemes at 960px, at 360px at 2×, under forced colours, with a mouse, the keyboard and touch. `checks/chart-select.mjs` measures it.
|
|
548
|
+
|
|
549
|
+
- **The outline is the promise.** Elsewhere, hover lifts what a click acts on. A bar can't lift without leaving the ground line, so a mark that acts gets a 2px `ink` outline in its own shape, the pointer cursor and the action in the readout. A chart whose marks don't act gets none of them. That's the exception the Charts chapter states.
|
|
550
|
+
- **Selection is a filter.** Selecting recedes the rest to 35%, exactly as `relevance` does (E40), so one visual language covers "filtered from elsewhere" and "chosen here". The frame says "Filtered to EU", because the receded marks alone don't say what the filter is.
|
|
551
|
+
- **Escape clears before it leaves.** On a point, Escape went back up a level. With a selection, it now clears the selection first, so a keyboard user can undo without losing their place.
|
|
552
|
+
- **Focus after a drill.** `Drilldown` focused its view before the new chart had measured itself, so the chart's summary didn't exist yet. It now focuses the view at once, then moves to the summary when it appears, within a few frames.
|
|
553
|
+
|
|
554
|
+
Not yet tested with people: whether the readout's "Select to filter" line is read before the click, or only noticed after it.
|
|
555
|
+
|
|
556
|
+
### E42 · Can a grid of values be read without a value axis?
|
|
557
|
+
|
|
558
|
+
A design review: `Heatmap` was rendered as a year of commits in a calendar, requests by weekday and hour with the table view, and error-rate change by service and region on a diverging scale with a cell you can select. In all four schemes at 960px, at 360px at 2×, under forced colours, with reduced motion and with `contrast="more"`, by pointer and keyboard. `checks/heatmap.mjs` measures it.
|
|
559
|
+
|
|
560
|
+
- **Steps you can name.** A continuous ramp makes every cell a slightly different colour that no legend can name. Binning into steps (after ColorBrewer's classes) makes "4–9" something to find in the legend, say in a cell's name ("step 3 of 4") and sort by in the table.
|
|
561
|
+
- **Steps spread over the ramp.** Small cells need bigger colour differences (Szafir), so steps take ramp tokens spread from `data-seq-1` to `data-seq-7` rather than adjacent ones: four steps are 1, 3, 5 and 7.
|
|
562
|
+
- **Ranges said once, signs said in words.** Formatted ranges repeated their unit and read "−3.0 pts–−1.5 pts", the en dash looking like a minus. A range now says its unit once and uses "to" between signed numbers ("−0.5 to +0.5 pts"). The open ends say "< −1.5 pts" and "≥ +1.5 pts", or "10+" for counts.
|
|
563
|
+
- **Diverging steps on round numbers.** The first default put the steps at half-steps, which a one-decimal format rounded to the wrong edges ("−0.3 to +0.3" for ±0.25). The edges are now whole multiples of a 1–2–5 step, three to a side.
|
|
564
|
+
- **Wrap, don't shrink.** At 360px 24 hourly columns ran out of the card, because a cell can't go below 10px. Both variants now wrap into bands (00–11 above 12–23, a year into three), each naming its rows and columns.
|
|
565
|
+
- **The crosshair is an outline.** A grid has no axis for a crosshair to run along, so the cell under the readout wears a 2px `ink` outline. It's not a promise: a static grid has no pointer cursor. Charts' rule 4 says so.
|
|
566
|
+
- **Cross-fade, don't glide.** The shared engine glides values, which would walk a cell's colour through every step between its old and new value. A heatmap takes new values at once and cross-fades each cell's fill.
|
|
567
|
+
- **Patterns for the steps.** Under forced colours every fill becomes the same, so each step adds a density (none, sparse dots, dense dots, hatching, crosshatch); a diverging scale hatches its two sides in opposite directions.
|
|
568
|
+
|
|
569
|
+
The issue asked for the WAI-ARIA grid pattern. The keyboard follows it (arrows, Home/End, Ctrl+Home/End, Page Up/Down), but the cells sit in the chart's one-Tab-stop path from the summary, as every chart's points do (E29), and say their row and column in their names rather than through `rowheader` and `columnheader` cells.
|
|
570
|
+
|
|
571
|
+
Not yet tested with people: whether a calendar's Page Up and Page Down by month are found without being told, and whether a wrapped grid's second band is read as a continuation of the first.
|
|
572
|
+
|
|
573
|
+
### E43 · Can two measures be read from flat dots?
|
|
574
|
+
|
|
575
|
+
A design review: `ScatterChart` was rendered as p95 latency against payload for 30 endpoints in three services, with a trend, two labelled outliers and a legend that toggles; and spend against seats for 16 customers, selectable, with the table view, gliding to the next month. In all four schemes at 960px, at 360px at 2×, under forced colours and with `contrast="more"`, by pointer and keyboard. `checks/scatter-chart.mjs` measures it.
|
|
576
|
+
|
|
577
|
+
- **A ring, not a shadow.** Overlapping dots are kept apart by a 2px ring of the plot's own colour, the gap a stacked bar has, rather than by a stroke in a darker shade or a drop shadow, which would read as height.
|
|
578
|
+
- **8px means every shape.** Squares and triangles drawn in a circle's 8px box measured 7.2px and 7.4px. They're now sized so their own box is at least 8px.
|
|
579
|
+
- **Both axes named.** With two measures, an axis of bare numbers ("0, 50, 100") says nothing. The first build named them inside the plot, where a dot near the top sat under "p95". They're now named in room the frame leaves above and below the plot.
|
|
580
|
+
- **Labels find room.** At 360px, neither upper corner of the top-left outlier was clear of the plot's edges and the label beside it, so its label was dropped. Each label now tries six spots, upper corners first, then beside, then below. The trend's word counts as taken.
|
|
581
|
+
- **No word covers a dot.** The trend's word was set at a fixed spot above the line's end and sat on the last Media triangle (#185). It now tries above and below the line's end, then steps back along the line, then its left end, and takes the first spot clear of every dot and its ring, measured over the word's own span so a steep line doesn't cross it. Outlier labels prefer a spot clear of the dots too, falling back to one that only clears the other words in a cloud too dense for that.
|
|
582
|
+
- **Nearest, with a reach.** The pointer reads the nearest dot (its Voronoi cell) within 48px. Past that, nothing is read, rather than a far dot being named.
|
|
583
|
+
- **Enter starts at the left.** The keyboard path entered at the first record in the array, wherever it was on the plot. It now enters at the leftmost dot, and ← and → follow x.
|
|
584
|
+
- **Identity decides the motion.** A record's place in the array isn't an identity, so data without `id` cross-fades. With ids, dots glide on both axes: x joins the values the engine interpolates.
|
|
585
|
+
|
|
586
|
+
Not yet tested with people: whether ↑ and ↓ to the nearest dot above or below are predictable in a dense cloud, where "above" may land on a dot far to one side.
|
|
587
|
+
|
|
588
|
+
### E44 · Can one trench hold a range?
|
|
589
|
+
|
|
590
|
+
A design review: the range `Slider` was rendered as a price filter ($0–$200 in $5 steps, two steps apart) under the single-value sliders it extends. In all four schemes at 960px, at 360px at 2× and under forced colours, by pointer and keyboard. `checks/range-slider.mjs` measures it.
|
|
591
|
+
|
|
592
|
+
- **One trench, one fill.** Two sliders side by side read as two settings. Two thumbs in one trench, with the raised fill between them, read as one span, the build the DateRangePicker already uses.
|
|
593
|
+
- **The fill is a handle.** Dragging between the thumbs pans the window. Because it acts, it lifts to level 2 on hover and shows a grab cursor, the rule every control follows. Pressing it doesn't jump either thumb, so a pan never starts with a jump.
|
|
594
|
+
- **Only the thumb in hand floats.** The single slider lifts its thumb whenever the slider is hovered and floats it whenever it's dragged. With two, that floated both, and both bubbles showed, so neither said which value was changing. Now only the thumb under the pointer lifts and only the one held floats; a pan holds both.
|
|
595
|
+
- **A clash with possession.** The first build marked the held thumb with `data-held`, which is already the collaborator-hold attribute (**Possession**), and the thumb took its tilt and its position and jumped out of the trench. It's `data-grabbed` now.
|
|
596
|
+
- **Focus follows the press.** Pressing the track moved the nearer thumb but left focus on the page, because the browser's mousedown focused nothing after the slider had focused the thumb. The press now prevents that default, so the thumb that moved is the one the arrow keys move.
|
|
597
|
+
- **Thumbs that meet.** With `minStepsBetween={0}` the two thumbs can sit on one value, where the upper one covers the lower. The first move decides which one goes: left takes the lower thumb, right the upper.
|
|
598
|
+
- **Bounds in ARIA.** Each thumb's `aria-valuemin` and `aria-valuemax` stop at the other thumb less the gap, as the APG pattern says, so Home and End go to the thumb's own bounds, not the track's ends.
|
|
599
|
+
|
|
600
|
+
Not yet tested with people: whether the fill is found as something to drag without the hint text the preview gives, and whether two bubbles close together are read as a pair or as a clash.
|
|
601
|
+
|
|
602
|
+
### E45 · Can a brush look like something to drag?
|
|
603
|
+
|
|
604
|
+
A design review: `ChartBrush` was rendered under a year of daily active users, its window shared with a `DateRangePicker`, and under a year of weekly sign-ups as bars, keeping its own window. In all four schemes at 960px, at 360px at 2×, under forced colours and with reduced motion, by pointer and keyboard. `checks/chart-brush.mjs` measures it.
|
|
605
|
+
|
|
606
|
+
- **The range Slider, laid along the overview.** The brush is #73's range Slider: the same thumbs, the same "can't cross" rule, the same keys, and the same `useRange` logic underneath. Only the press outside differs: a slider moves the nearer thumb there, a brush centres the window there, because what you want in an overview is "show me that bit".
|
|
607
|
+
- **A line, not an area.** The issue asked for "a sparkline-sized area". From zero, a year of values that never come near zero was a flat ripple near the top of the trench. From its own low and high, an area would stand on nothing and misstate every value. So the overview is a line spanning its own low to high, as a `Sparkline`'s line is. It's there for the shape.
|
|
608
|
+
- **Bubbles below.** The grips' date bubbles first rose above the overview, as a slider's do, and covered the plot's x axis labels. They now hang below the trench, over the brush's own caption, which says the same dates. Near an end they align to it: at the start, the first build's bubble ran 5px past the chart's frame.
|
|
609
|
+
- **The records' own x values.** The window's `{ start, end }` are records' x values, not free dates, so a `DateRange` fits as it is and a brush and a picker can share one state. A date typed into the picker that isn't a record is still honoured: the chart shows the records between the two dates.
|
|
610
|
+
- **Evenly spaced by record.** The overview spaces records evenly and the grips step by a record. Over daily or weekly data that's the same as time; over irregular data the overview is distorted, but the grips never land between records.
|
|
611
|
+
- **The plot cross-fades.** The issue expected the main chart to glide. A new window means new x values under the marks, which Motion's rule 5 cross-fades rather than morphing, so a pan reads as the view changing rather than the data moving.
|
|
612
|
+
|
|
613
|
+
Not yet tested with people: whether the lens is grabbed as a whole on first sight, without the description the preview gives, and whether pressing the overview to centre the window there is ever found.
|
|
614
|
+
|
|
615
|
+
### E46 · Where does the result of removing an item go?
|
|
616
|
+
|
|
617
|
+
A measurement, then a design review ([#97](https://github.com/richlaconte/altitude/issues/97)). In the Actions in place Files example, Archive was clicked while every element's position was sampled each animation frame (`shoot.mjs` with `ACT`, Daylight, 960px). Before: the row was gone in the first frame and the rows below jumped up 77px; the recovery Alert appeared 172px below the click, under the list; and about 100ms later it grew 10px when "Archiving…" became "…is archived", moving everything under it a second time. `ItemGroup` measured the same way: its slats jumped 84px and its Undo Alert appeared under the add slot.
|
|
618
|
+
|
|
619
|
+
- **Left in place.** The result now shows in the row's own slot at the row's height. In the same measurement, nothing moves while it holds.
|
|
620
|
+
- **Then closed, visibly.** The receipt lifts off as an Alert does, and the gap glides shut over `dur-travel-3`; `checks/actions-in-place.mjs` (RR-02) asserts the next row passes through intermediate positions instead of jumping.
|
|
621
|
+
- **Undo where Archive was.** The first screenshot put View Archived under the pointer that had just clicked Archive, so a double-click would have navigated away. Undo now takes that place: a stray second click reverses instead.
|
|
622
|
+
- **A pointer over the list holds the close.** A delayed close could move the next row under a pointer reaching for it. The close waits while a mouse is over the list, capped at `dur-settle`; touch has no hover, so it doesn't wait.
|
|
623
|
+
- **`dur-linger` is 2000ms by judgement.** About four words read in roughly a second (Nielsen's 1s flow limit) plus a second to act; the top of the "second or two" asked for.
|
|
624
|
+
|
|
625
|
+
Not yet tested with people: whether the close is noticeable enough at `dur-travel-3`, and whether waiting for the pointer to leave the list reads as lag. Both are open in [the validation record](ActionsInPlaceValidation.md).
|
|
626
|
+
|
|
627
|
+
### E47 · Can a chart Altitude doesn't draw keep its rules?
|
|
628
|
+
|
|
629
|
+
A build, then a read-back ([#76](https://github.com/richlaconte/altitude/issues/76)). shadcn/ui's chart component, as published, over Recharts 3.10, set up as **Theming** says and rendered in all four schemes, each nested in a daylight page (`examples/shadcn-chart`, read by `build/checks/shadcn-chart.mjs`).
|
|
630
|
+
|
|
631
|
+
- **The palette carries over, if it's declared where the scheme is.** shadcn's configs name `var(--chart-1)` directly, so mapping Tailwind's `--color-chart-*` isn't enough. With `--chart-*` declared on `:root` only, the three nested panels drew daylight's colours (9 failures); on `:root, [data-theme]` every mark matched its panel's `data-*`.
|
|
632
|
+
- **Most rules are props.** The grid, ground line, flat marks, readout level, crosshair, keyboard path and reduced motion each map to a Recharts prop, and all read back correctly.
|
|
633
|
+
- **Two props are needed where shadcn's styles stop working.** Recharts 3.10 moved tick text out of the group `ChartContainer`'s selector targets, so ticks fell back to Recharts' grey until `tick` was passed; and the stylesheet's crosshair colour beats Recharts' `stroke` attribute, so only an inline `style` makes it `ink-muted`.
|
|
634
|
+
- **What doesn't carry is behaviour, not colour:** marks that act, legend switches, imprints and empty states, the table view, afterglows and patterns. These stay with Altitude's own charts.
|
|
635
|
+
|
|
636
|
+
### E48 · Can a chart library Altitude doesn't draw read the scheme?
|
|
637
|
+
|
|
638
|
+
A build and a read-back ([#77](https://github.com/richlaconte/altitude/issues/77)). `altitude-design/chart-themes` turns the computed tokens of an element into an ECharts theme and Chart.js defaults. It imports neither library, and its only import is React, for `useChartTheme`.
|
|
639
|
+
|
|
640
|
+
- **Read from the element, not copied.** A theme read inside a scheme or a custom accent is that scheme's. `build/checks/chart-themes.mjs` compares each scheme's theme with `tokens.json`, checks that tokens passed by name give the same object, and checks that `useChartTheme` re-reads after the provider's `scheme` and `accent` change (80 assertions).
|
|
641
|
+
- **Motion follows the provider.** `motion="reduced"` or `"full"` on `AltitudeProvider` overrides the system setting, as it does for Altitude's own components, because the theme reads `--alt-travel` rather than the media query alone.
|
|
642
|
+
- **ECharts takes the theme's `aria` without an option of its own.** Tried in Node against ECharts 6.1.0 (SSR, SVG renderer): the model's `aria.enabled` came from the theme, series used `data-1` and `data-2`, and `{ patterns: true }` rendered decal patterns. Chart.js 4.6.1 accepted the object through `Chart.defaults.set`. Neither library is a dependency, so CI doesn't repeat this.
|
|
643
|
+
- **Not vouched for:** keyboard paths and screen-reader output are the library's own. A theme can't keep the rules in **Charts** for anyone.
|
|
644
|
+
|
|
645
|
+
### E49 · Can ItemGroup leave its result where the slat was?
|
|
646
|
+
|
|
647
|
+
A measurement, as in E46, of [#100](https://github.com/richlaconte/altitude/issues/100): Daylight, 960px, the webhooks preview, with the slat below sampled every animation frame (`checks/item-group.mjs`).
|
|
648
|
+
- **Before.** At about 219ms the removed slat was gone and the slats below jumped 84px in one frame. The Undo Alert appeared under the add slot.
|
|
649
|
+
- **After.** From the click to Undo during the hold, the slat below doesn't move at all (IG-RR-03). Left alone, the receipt holds for `dur-linger`, and then the slat below passes through many positions as the gap glides shut (IG-RR-06).
|
|
650
|
+
|
|
651
|
+
Findings:
|
|
652
|
+
- **Undo moves twice, and each move is shown.** During the hold, Undo is where the slat was. After the close it's in a toast, which `ItemGroup` raises itself. The toast doesn't announce again (`announce: false`, new on `toast`), because the receipt already said “… is removed. Undo is available.”
|
|
653
|
+
- **Undo after the close glides the slot open.** Animating only the height made it jump the slat's padding (25px) before gliding. `RemovalReceipt` now opens and closes the whole box, padding, borders and margins included. Under StrictMode the opening ran twice, and the second run measured the first run's empty frame, so the slot stayed shut and then jumped. The opening is now cancelled on cleanup.
|
|
654
|
+
- **A confirmed removal glides shut** with no receipt, because there's nothing to undo and nothing to read.
|
|
655
|
+
- **Repeated words are still news.** Removing and restoring the same webhook twice left the polite status saying “Staging EU restored.” already, so the second restore was silent, and its focus repair waited for a render that never came. The status now clears before it speaks.
|
|
656
|
+
|
|
657
|
+
Not yet tested with people: whether Undo moving from the slot to a toast reads as the same Undo.
|
|
658
|
+
|
|
659
|
+
### E50 · How should a line removed while typing leave?
|
|
660
|
+
|
|
661
|
+
A design review and a measurement ([#101](https://github.com/richlaconte/altitude/issues/101)). In `Checklist`, removal happens only from the keyboard: Backspace on an empty line, or at the start of a line, which merges its text into the line above. Before, the line vanished in one frame, and a raised Undo bar was added after the list, which changed the page's height a second time.
|
|
662
|
+
|
|
663
|
+
- **No receipt.** E46's in-place receipt would sit between lines being typed, and its hold would delay the context the caret needs. Nothing was lost, so there's nothing to read. The line now glides shut over `dur-travel-2`. `checks/checklist.mjs` samples the line below every frame and sees it pass through several positions (CL-20, CL-21), with the caret already at the end of the line above (CL-23).
|
|
664
|
+
- **No bar.** Undo stays on Ctrl/Cmd+Z, and the announcement names it for the platform: “Empty item removed. Undo with Ctrl+Z.” (⌘Z on Apple). Pointer-only users of `onUndo` lose the bar, but they couldn't remove a line without a keyboard in the first place.
|
|
665
|
+
- **Found while testing.** Chromium reports `navigator.userAgentData.platform` as “macOS”, which a case-sensitive `/Mac/` test misses. Checklist's test is now case-insensitive. `InlineEdit` has the same test and the same gap.
|
|
666
|
+
- **The announcement is one at a time, the latest.** Saying the same words twice needs the status cleared first. Clearing it and speaking a frame later let a stale removal overwrite a newer “Indented under …”, so only the latest is spoken.
|
|
667
|
+
|
|
668
|
+
### E51 · How long should a save's afterglow last?
|
|
669
|
+
|
|
670
|
+
A design review ([#114](https://github.com/richlaconte/altitude/pull/114), the edit-in-place lab). `dur-settle` (8s) is sized for change detection: content that changed while someone looked elsewhere has to still be tinted when they look back (study 2 below). A save in `InlineEdit` or `EditRegion` is different: the person pressed Save and is watching the value, so the tint confirms rather than announces, and 8s of it read as lingering. Both now default to 5s, and `settleDuration` (ms) sets it per instance. The keyframes are unchanged, so the full tint still holds for the first 12% (600ms) before decaying. Other afterglows (an arriving row or card, a restored slat) keep `dur-settle`.
|
|
671
|
+
|
|
672
|
+
### E52 · Can a value be edited without moving?
|
|
673
|
+
|
|
674
|
+
A design review and a set of measurements, prototyped on a lab page ([#114](https://github.com/richlaconte/altitude/pull/114), [#115](https://github.com/richlaconte/altitude/pull/115)) before reaching the components. Before, opening `InlineEdit` swapped the value for a `Field`: its text moved 26px right and 43px down, the editor's 104px appeared in one frame, and a visible status line kept “Changes saved” under it. `EditRegion` moved its first value 30px right and 53px down (padding and labels arrived at once) and drew the finished card in its first frame, uncovered by a 220ms height animation over about 225px, which read as a box being revealed, not a record growing. Its afterglow filled an unpadded box, so it sat against the text.
|
|
675
|
+
|
|
676
|
+
- **The value lives in a slot with its control's inset and font.** `Input` and `Select` put their text 13px in (12px padding and a 1px border); the read slot does the same with a transparent border, and the line reaches 13px past its column so the text lines up with the page. Opening fades in only the well. Measured: 0px across and 1px down for a title, and that 1px is the focused well's own approach depth. No label is added above the value; it names the control for assistive technology.
|
|
677
|
+
- **One glide for every change of size.** The height animates on the real box (so what's below is pushed through the change, not jumped), at `dur-travel-3` up to 120px and a little longer per pixel beyond, capped at +140ms. It clips only at the moving bottom edge, and with `clip-path` rather than `overflow`: a `Select` opened mid-glide measured the clipped box, found no room below and opened its list off the top of the page. New parts fade in as the edge reaches them.
|
|
678
|
+
- **Nothing slides.** The prototype carried values from their read place to their field (FLIP) and grew wells sideways. In a record whose controls sat in a flex row, a growing well reflowed its neighbours every frame while they were also being carried, and the editor's contents visibly jumped about. Now each field takes its final place at once and holds it on every frame (`checks/edit-region.mjs` ER-26 compares them across frames). A field whose text lands where its value was simply appears; one that lands elsewhere fades in. Width grows only for InlineEdit's single slot, whose siblings don't depend on it.
|
|
679
|
+
- **Padding that's always there.** The editor's padding and border exist in read mode under a negative margin, so the lift is colour and shadow (ER-28: the box doesn't move), and the afterglow has room around the text.
|
|
680
|
+
- **Labelled rows reserve their height.** With `EditRegionValue reserve` the read value already has its control's height and inset, so Edit all moves nothing (ER-34: every value opens where it was) and only the actions row is added (ER-35).
|
|
681
|
+
- **Status is announced, never shown,** and clears itself after `dur-linger`. The afterglow is the visible confirmation.
|
|
682
|
+
- **Ways to open, and a sign at rest.** A pencil on hover and the field drawn on hover both move nothing (IP-06, IP-10); the field puts the caret where the click landed. Hover-only triggers need a resting sign, or people don't know to point. `hint="seam"` is drawn from the well's own edge: a hairline cut in `line-control` over the well's light lip, where the well will open. An underline would read as a link (followed, not opened), and a dashed underline as an abbreviation or a tooltip.
|
|
683
|
+
|
|
684
|
+
### E53 · What should a separator say?
|
|
685
|
+
|
|
686
|
+
A design review: `Separator` was rendered as a section divider in a card, between groups of toolbar buttons and as "or" between two ways to sign in, in all four schemes at 960px, under forced colours and with `contrast="more"`. `checks/separator.mjs` measures it.
|
|
687
|
+
|
|
688
|
+
- **Only `line`.** A divider is decorative ink on the ground, so it's 1px of `line` with no shadow. `line-control` is the edge of a control, and a toolbar separator drawn in it would sit next to button edges in the same ink; it's used only under `contrast="more"`.
|
|
689
|
+
- **A labelled separator isn't `role="separator"`.** A separator's children are presentational ([WAI-ARIA 1.2, separator](https://www.w3.org/TR/wai-aria-1.2/#separator)), so "or" inside one is not reliably read. The word is plain text and the lines are pseudo-elements, so "Continue with SSO, or, Email address" reads in order.
|
|
690
|
+
- **Decorative is opt-in.** A screen reader announcing every divider is noise, but a divider that parts unrelated content is information. Defaulting to `role="separator"` keeps the information, and `decorative` is one prop for the toolbar case.
|
|
691
|
+
|
|
692
|
+
Not yet tested with people: whether a labelled "or" is clearer than a heading above the second option.
|
|
693
|
+
|
|
694
|
+
### E54 · Should a link lift?
|
|
695
|
+
|
|
696
|
+
A design review: `Link` was rendered in prose, as an external link, and as "View all invoices →" through a router element (`asChild`), in all four schemes at 960px, under forced colours and with `contrast="more"`. `checks/link.mjs` measures it.
|
|
697
|
+
|
|
698
|
+
- **No lift.** Everything else that acts lifts one level on hover, but a link in a paragraph would have to leave its own baseline to do it, and height means "press it", which is a button's job. So a link stays on the ground, and the underline (1px → 2px) is its hover. The check confirms its box doesn't move.
|
|
699
|
+
- **Underline always.** [WCAG 1.4.1](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color) wants a link in text told apart by more than colour (or by 3:1 against the text around it, which no scheme's `accent-ink` was chosen for), so the underline is never removed, even for a standalone link.
|
|
700
|
+
- **New tabs are said.** An external link announces "(opens in a new tab)" ([WCAG G201](https://www.w3.org/WAI/WCAG22/Techniques/general/G201)) and shows an icon. The text is visually hidden, so the link reads as written.
|
|
701
|
+
|
|
702
|
+
Not yet tested with people: whether the thicker underline on hover is noticed without a colour change.
|
|
703
|
+
|
|
704
|
+
### E55 · How should a number in a sentence change?
|
|
705
|
+
|
|
706
|
+
A design review: `FormattedNumber` was rendered in every preset (currency in minor units, bytes, durations, percent, compact) in a definition list and inside a sentence whose value changes, in all four schemes at 960px, under forced colours and with reduced motion. `checks/formatted-number.mjs` measures it.
|
|
707
|
+
|
|
708
|
+
- **A tint, not the afterglow's lift.** `Stat` takes the full afterglow, a lift with `elev-2`. Inside a sentence the shadow would land on the line below, so a number takes only the tint (`accent-soft` decaying over `dur-settle`), as a live chart's "Updated" caption does.
|
|
709
|
+
- **Cross-fade over `dur-travel-2`, not `dur-hover`.** The issue proposed `dur-hover`, but that token is 1800ms (it paces a pending write's hover). `Stat`'s `dur-travel-2` keeps the two consistent.
|
|
710
|
+
- **Spoken units, not `aria-label`.** `<data>` has the generic role, which can't be named ([WAI-ARIA 1.2, generic](https://www.w3.org/TR/wai-aria-1.2/#generic)), so the words ("4.5 gigabytes") are visually hidden text and the symbol is hidden from assistive technology.
|
|
711
|
+
- **Currency in minor units by default,** as the issue's sketch has it and as payment APIs (Stripe, Adyen) send amounts. The divisor comes from the currency, so JPY isn't divided. `minorUnits={false}` takes whole units.
|
|
712
|
+
|
|
713
|
+
Not yet tested with people: whether "4.5 GB" with the exact bytes in a tooltip is enough, or tables want the exact value shown.
|
|
714
|
+
|
|
715
|
+
### E56 · Is time passing a change?
|
|
716
|
+
|
|
717
|
+
A design review: `RelativeTime` was rendered in an activity feed reaching from seconds to 40 days back (`format="auto"`) and as "Last edited" on a card that can be edited again, in all four schemes at 960px and under forced colours. `checks/relative-time.mjs` measures it with a clock it controls.
|
|
718
|
+
|
|
719
|
+
- **Ticking is not a change.** The afterglow says "this is new". If every timestamp glowed as it ticked, a feed would shimmer every few seconds, and the glow would mean nothing. Only a new moment glows.
|
|
720
|
+
- **One timer, paused while hidden.** Each instance with its own interval adds up in a long feed. One shared 15-second timer refreshes them all. It stops while the page is hidden and catches up when it's shown, so a tab left open overnight doesn't show "2 minutes ago".
|
|
721
|
+
- **"now" and "yesterday".** `numeric: "auto"` gives the locale's own words for the nearest units, and seconds under 45 are "now". A count of seconds that changes on every tick is noise.
|
|
722
|
+
- **Focusable only for its tooltip.** A tab stop on every timestamp is costly in a feed, but keyboard users must reach the exact time where it matters, so the tab stop comes with the tooltip and goes with `tooltip={false}`, which falls back to `title`.
|
|
723
|
+
|
|
724
|
+
Not yet tested with people: whether 15 seconds is fresh enough for "now" in a chat-like feed.
|
|
725
|
+
|
|
726
|
+
### E57 · How should a copy confirm itself?
|
|
727
|
+
|
|
728
|
+
A design review: `CopyButton` was rendered icon-only beside an API key, as a secondary "Copy invite link", and failing on purpose, in all four schemes at 960px and under forced colours, and pressed with the pointer and the keyboard. `checks/copy-button.mjs` measures it with a stubbed clipboard.
|
|
729
|
+
|
|
730
|
+
- **Pressed, not glowing.** The afterglow means new content arrived, and a copy adds nothing to the page. The confirmation is where the hand is: the button stays pressed for `dur-linger` with a check and the word. A toast for a two-second event would draw the eye away from the value just copied.
|
|
731
|
+
- **The name doesn't change.** Renaming a focused button makes some screen readers re-announce it as a new control. The button keeps its name (`label`, or its text as `aria-label` while it says "Copied"), and a polite status region outside the button says what happened.
|
|
732
|
+
- **Its width doesn't change.** "Copied" is shorter than "Copy invite link" and "Couldn't copy" may be longer, so every label shares one cell, the fix #116 made for HoldButton.
|
|
733
|
+
- **A failure says what to do.** Clipboard writes fail in insecure contexts and under permission policies. After the fallback (a selected hidden textarea and the copy command) fails, it announces how to copy by hand with the platform's shortcut.
|
|
734
|
+
|
|
735
|
+
Not yet tested with people: whether two seconds is long enough to notice "Copied" on an icon-only button.
|
|
736
|
+
|
|
737
|
+
### E58 · How should a latched button look when it's on?
|
|
738
|
+
|
|
739
|
+
A design review: `Toggle` and `ToggleGroup` were rendered as a formatting bar (multiple), a view switch (single), "Show archived" (secondary) and a thumbs up/down vote, in all four schemes at 960px, under forced colours and with `contrast="more"`. `checks/toggle.mjs` measures them.
|
|
740
|
+
|
|
741
|
+
- **Sunk and filled, not held at ground.** "Pressing returns to ground; loading holds it there", so an on button drawn flush at ground level reads as a button still loading, which is the ambiguity the issue raised. On is a choice, so it borrows the Switch's build: a shallow well with `accent-soft` poured in and the label in `accent-ink`. That keeps "push up, fill down" (an on toggle is filled, not pressed), and it's the one stated exception to "never style a button as sunk". The alternative, a ground-level face with an accent bar, was rejected without being built: it puts a status-like bar on a control.
|
|
742
|
+
- **Single choice is segmented Tabs.** A view switch without panels is the same choice as segmented `Tabs`, so it uses the same classes (a well and one gliding pill) and is a radio group, where arrows move and choose.
|
|
743
|
+
- **One tab stop.** A 3-button formatting group costs one Tab, not three. Focus returns to the item you left, and inside a `Toolbar` the group leaves the arrow keys to the bar (shared roving focus).
|
|
744
|
+
- **The name doesn't change.** "Bold" stays "Bold" with `aria-pressed`. Renaming to "Unbold" makes the state ambiguous ([APG, button](https://www.w3.org/WAI/ARIA/apg/patterns/button/)).
|
|
745
|
+
|
|
746
|
+
Not yet tested with people: whether a sunk, tinted toggle is read as "on" as quickly as a raised, tinted one.
|
|
747
|
+
|
|
748
|
+
### E59 · What should a toolbar do when it runs out of room?
|
|
749
|
+
|
|
750
|
+
A design review: `Toolbar` was rendered as a seven-tool formatting bar (a ToggleGroup, a group with a ToolbarToggle, and a link button) at full width and squeezed to 260px with `overflow="menu"`, in all four schemes and under forced colours. `checks/toolbar.mjs` measures it.
|
|
751
|
+
|
|
752
|
+
- **Hide from the end, into a menu.** Wrapping onto a second row moves the tools you know into new places, and a horizontally scrolling bar hides them with no sign. The menu keeps every visible tool where it was and puts the rest in one predictable place. Hidden tools don't animate as they go: a resize isn't an event.
|
|
753
|
+
- **Toggles stay toggles in the menu.** A latched tool moved into the menu becomes a `menuitemcheckbox` with a check, so its state survives the move. `DropdownMenu` gained `checked` for this.
|
|
754
|
+
- **One tab stop, shared with ToggleGroup.** The bar and any ToggleGroup in it share one roving-focus hook, so a group inside the bar adds no tab stop of its own.
|
|
755
|
+
- **No separator at the edge.** A row that ends in a divider reads as if something is missing after it, so a trailing separator goes into the menu with what follows it.
|
|
756
|
+
|
|
757
|
+
Not yet tested with people: whether "More tools" is found as readily as a wrapped second row.
|
|
758
|
+
|
|
759
|
+
### E60 · One slab or two?
|
|
760
|
+
|
|
761
|
+
A design review: `SplitButton` was rendered as a primary "Save changes", a secondary "Merge" and a secondary "Deploy" that loads, in all four schemes at 960px and under forced colours, and hovered face by face. `checks/split-button.mjs` measures it.
|
|
762
|
+
|
|
763
|
+
- **One wall, two faces.** A `Button` and a chevron `Button` placed side by side have two walls and two rounded ends, and read as two actions of equal weight. Squaring the inner corners and running one wall under both faces makes one object with a notch.
|
|
764
|
+
- **Each face lifts alone.** The faces do different things (act, open a menu), so "hover is a promise" applies to each. Lifting both together would promise that pointing at the chevron saves.
|
|
765
|
+
- **The notch on an accent slab.** `line-control` is tuned for neutral surfaces and nearly vanishes on a saturated accent, so the notch on a primary slab mixes `on-accent` into the accent. That's derived from the scheme's tokens, not a new colour.
|
|
766
|
+
- **Loading doesn't take the menu.** A held-pressed action face keeps its label, and the chevron stays usable, so "Deploy to production" is still reachable while staging deploys.
|
|
767
|
+
|
|
768
|
+
Not yet tested with people: whether Alt+Down on the action is discovered without being taught.
|
|
769
|
+
|
|
770
|
+
### E61 · Where do a number field's steppers go?
|
|
771
|
+
|
|
772
|
+
A design review: `NumberInput` was rendered for seats (1–500), days (a unit), a price in `de-DE` euros and a percentage, empty and filled, in all four schemes at 960px, under forced colours and with `contrast="more"`. `checks/number-input.mjs` types, steps and pastes into them.
|
|
773
|
+
|
|
774
|
+
- **Slabs in the end cap.** The field is an opening and the steppers are things you press, so they're raised (level 1, 1px wall) inside the well's end cap. A sunk stepper would read as another field, and "push up, fill down" says why that's wrong. At a limit a stepper lies flush and faint, as a disabled button does.
|
|
775
|
+
- **Clamp when committed, not while typing.** Clamping each keystroke turns "12" into "1" then "12" in a field whose minimum is 10. The value is clamped on blur, Enter or a step, which keeps it in range without fighting the typing.
|
|
776
|
+
- **The locale's own separators.** `<input type="number">` refuses "1,5" in German and takes "1.000" as one. Parsing uses `Intl.NumberFormat`'s own group and decimal signs, so "1.250,50" is 1250.5 in `de-DE`.
|
|
777
|
+
- **No scroll-wheel accidents.** A page scroll over a number field changes its value in some browsers. The wheel steps only when `wheel` is set and the field is focused.
|
|
778
|
+
|
|
779
|
+
Not yet tested with people: whether 15px-tall steppers are large enough on touch screens, where people type instead.
|
|
780
|
+
|
|
781
|
+
### E62 · Six inputs or one?
|
|
782
|
+
|
|
783
|
+
A design review: `InputOTP` was rendered with six digits in two groups and eight alphanumeric characters, empty, partly filled, focused by keyboard, rejected and verifying, in all four schemes at 960px and under forced colours. `checks/input-otp.mjs` types, pastes and deletes in it.
|
|
784
|
+
|
|
785
|
+
- **One input.** Hand-rolled code fields often use one input per character, which breaks SMS autofill (`autocomplete="one-time-code"` fills one field), splits a pasted code, and makes a screen reader announce six unlabelled fields. One invisible input over the row keeps all three working, and the slots are drawn from its value.
|
|
786
|
+
- **Completeness per slot.** The Input rule (an empty field shows its back wall, a filled one rises flush) applied to each character shows progress through the code without a counter.
|
|
787
|
+
- **Filter, don't refuse.** Codes arrive formatted ("123-456", "123 456"). Dropping characters that don't match the pattern accepts both, where a strict `pattern` attribute would reject the paste.
|
|
788
|
+
- **Rejection keeps the code.** The row drops to a trench and stays filled, so a single mistyped digit can be corrected rather than retyped.
|
|
789
|
+
|
|
790
|
+
Not yet tested with people: whether the staggered fill on paste is noticed, or read as lag.
|
|
791
|
+
|
|
792
|
+
### E63 · When is a time complete?
|
|
793
|
+
|
|
794
|
+
A design review: `TimeInput` was rendered on a 12-hour clock with a time zone, on a 24-hour clock empty, and with a range it was outside, in all four schemes at 960px and under forced colours. `checks/time-input.mjs` types into it.
|
|
795
|
+
|
|
796
|
+
- **Whole or not at all.** "09:––" can't be scheduled, so it isn't a value: the field stays an opening and the value stays null until hour, minute (and AM/PM) are all there. That's the DatePicker's rule, and the two fields read the same in a form.
|
|
797
|
+
- **Typed by segment, not picked.** A list of 96 quarter hours is slower than typing "930p", and the native time input renders differently in every browser and ignores the scheme. The segments reuse DatePicker's, so typing, arrows and Backspace behave identically.
|
|
798
|
+
- **The value is always 24-hour.** Whatever the clock shows, the value is "HH:mm" from 00:00 to 23:59, so an app never parses "9:30 PM".
|
|
799
|
+
- **The zone is a label, not a conversion.** Converting a wall time across zones needs a date (daylight saving), which a time of day doesn't have. The short name tells people which zone the time is in, and the app does any converting.
|
|
800
|
+
|
|
801
|
+
Not yet tested with people: whether auto-advance after one digit ("9" moves on) surprises people who meant to type "09".
|
|
802
|
+
|
|
803
|
+
### E64 · Is a password's strength a task or a reading?
|
|
804
|
+
|
|
805
|
+
A design review: `PasswordInput` was rendered for signing in and for creating a password with a strength meter and two requirements, in all four schemes at 960px and under forced colours, typed into with Caps Lock on and off. `checks/password-input.mjs` measures it.
|
|
806
|
+
|
|
807
|
+
- **A reading.** Strength bars are usually drawn as progress, which says "keep going and you'll finish". Strength is a judgement of what's there, so it's a `Meter`: flat ink in a flush track, with the word beside it. That's the Charts rule applied outside a chart.
|
|
808
|
+
- **No caution colour.** The issue proposed `caution` and `positive` fills past thresholds. Meter's thresholds mean higher is worse (quotas, load), and a fill that turns caution as strength rises would say the opposite. The word carries the meaning, and the fill stays neutral.
|
|
809
|
+
- **Announce when typing pauses.** A live region that speaks on every key talks over the typing. Strength and unmet rules are said 0.9s after the last key.
|
|
810
|
+
- **The reveal toggle keeps its name.** "Show password" with `aria-pressed`, not "Show" becoming "Hide", is the Toggle rule (E58). Focus stays on the toggle, so revealing doesn't move the caret or the reading position.
|
|
811
|
+
|
|
812
|
+
Not yet tested with people: whether "Good" is read as good enough, or as a hint to keep going.
|
|
813
|
+
|
|
814
|
+
### E65 · How should a secret shown once read?
|
|
815
|
+
|
|
816
|
+
A design review: `SecretField` was rendered as a just-created API key in a card whose Done button waits for it, and as a stored webhook secret with reveal, rotate and revoke, in all four schemes at 960px and under forced colours. `checks/secret-field.mjs` measures it.
|
|
817
|
+
|
|
818
|
+
- **Completeness, inverted.** A field normally fills when it holds a value. A new secret holds its value from the start, but the task isn't done until the person has taken it away. So it stays a sunk well with its back wall showing until they confirm, and then it fills flush and masks. The depth says "something is still owed here".
|
|
819
|
+
- **The prefix stays.** Masking `ak_live_EXAMPLE…4f2a` to "••••4f2a" loses which kind of key it is. A new secret keeps its own prefix ("ak_live_") when it masks.
|
|
820
|
+
- **Bullets aren't read.** A screen reader reading "bullet bullet bullet bullet 4 f 2 a" is noise. A masked secret is announced as "Secret key, ending in 4f2a".
|
|
821
|
+
- **Focus doesn't fall to the page.** Checking "I've saved this key" removes the checkbox, so focus moves to the sentence that replaces it rather than to the top of the document.
|
|
822
|
+
|
|
823
|
+
Not yet tested with people: whether the sunk well alone, without the checkbox, makes people copy the key before leaving.
|
|
824
|
+
|
|
825
|
+
### E66 · Is a code block a card?
|
|
826
|
+
|
|
827
|
+
A design review: `CodeBlock` was rendered as an install command in three forms, a JSON payload with two highlighted lines, line numbers and a scroll, and code streaming in with a stand-in highlighter, in all four schemes at 960px and under forced colours. `checks/code-block.mjs` measures it.
|
|
828
|
+
|
|
829
|
+
- **Flush, not raised.** Code blocks are often drawn as dark raised cards. A card is a live surface, and code you read and copy is part of the record, so it lies flush with a `line` rim. The copy button and the form switch are the only live parts.
|
|
830
|
+
- **Bring your own highlighter.** Bundling one (Shiki is several hundred KB with grammars) would dwarf the rest of the package. `syntax` takes any function, and the default token colours stay within ink and the accent, because the data palette means series.
|
|
831
|
+
- **A region only when it scrolls.** A tab stop on every code block costs keyboard users in a docs page. The scroll area becomes focusable and named only when it overflows ([WCAG 2.1.1](https://www.w3.org/WAI/WCAG22/Understanding/keyboard)).
|
|
832
|
+
- **Not copyable while writing.** Copying half a command an agent is still writing is worse than waiting, so copy is disabled while `streaming`.
|
|
833
|
+
|
|
834
|
+
Not yet tested with people: whether a dashed left rule reads as "still arriving" without the caption.
|
|
835
|
+
|
|
836
|
+
### E67 · How should a colour be chosen, and checked?
|
|
837
|
+
|
|
838
|
+
A design review: `ColorPicker` was rendered with six brand swatches, a hex field, the full picker open and the contrast check, and with four label swatches beside a primary button in the chosen accent, in all four schemes at 960px and under forced colours. `checks/color-picker.mjs` measures it.
|
|
839
|
+
|
|
840
|
+
- **Swatches are radios.** A palette is one choice among several, so it's RadioGroup's build: trenches the colour pours into, one raised white mark that travels. The issue proposed this, and it keeps "choices are switches" true for colour too.
|
|
841
|
+
- **Two verdicts, not one.** The issue's single "Text passes 4.5:1" check almost never fails: for any colour, either white or black text reaches about 4.6:1. What a brand owner needs to know is which text works, so each gets its own plate. The issue's teal, for example, fails with white text (4.3:1) and passes with dark text (4.8:1).
|
|
842
|
+
- **Names before colour.** Every swatch is named and announced. Under forced colours a swatch keeps its real colour, because the colour is the content, with a border whose weight marks the chosen one.
|
|
843
|
+
- **A hex field that fills only when whole.** "0f8a" isn't a colour, so the field stays an opening (as TimeInput and DatePicker do) and reverts to the last colour on blur.
|
|
844
|
+
|
|
845
|
+
Not yet tested with people: whether the 2D area is needed at all, or the hex field and swatches cover brand use.
|
|
846
|
+
|
|
847
|
+
### E68 · Can the staircase leave Drilldown?
|
|
848
|
+
|
|
849
|
+
A design review: `Breadcrumb` was rendered for a three-step path and a six-step path collapsed to four, through a router element, in all four schemes at 960px and under forced colours, beside `Drilldown`. `checks/breadcrumb.mjs` measures it, and `approach-and-lamps.mjs` still checks Drilldown's steps.
|
|
850
|
+
|
|
851
|
+
- **One staircase.** Drilldown's step is extracted (`CrumbStep`) and both components use it, so the two can't drift. The CSS now targets `.alt-crumb-link`, so a step can be a button (Drilldown), a link or a router's element.
|
|
852
|
+
- **No pile and no rise.** Drilldown lays a sheet on a pile as you go deeper, and its new step rises out of the ground. A route change isn't you carrying something deeper, and it happens on every navigation, so Breadcrumb re-renders without motion.
|
|
853
|
+
- **No separators.** Height already says "deeper". A chevron between steps would say it twice and cost width.
|
|
854
|
+
- **Keep the ends.** Collapsing a deep path keeps the root (where you can always go) and the nearest steps (where you usually go), with the middle in a menu.
|
|
855
|
+
|
|
856
|
+
Not yet tested with people: whether a staircase is read as a path in a route-based app as readily as it is in Drilldown, where you built it yourself.
|
|
857
|
+
|
|
858
|
+
### E69 · Should pages look like a choice?
|
|
859
|
+
|
|
860
|
+
A design review: `Pagination` was rendered with 11 pages and a summary, and in cursor mode, in all four schemes at 960px and 360px and under forced colours, and paged by pointer and keyboard. `checks/pagination.mjs` measures it.
|
|
861
|
+
|
|
862
|
+
- **A thumb, not a row of buttons.** Numbered page buttons each promise an action. The current page is a position among the others, which is RadioGroup's reading: one raised thumb in a trench. It travels on `dur-travel-1`, faster than RadioGroup's thumb, because paging is frequent.
|
|
863
|
+
- **Links in Tab order, despite the look.** Drawing them like a radio group could suggest arrow keys, but pages are navigation, often with URLs, so they keep link semantics and plain Tab order, as the issue proposed.
|
|
864
|
+
- **One page, not "…".** A gap of exactly one page shows the page: "…" would take as much room and hide where it leads.
|
|
865
|
+
- **Held while loading.** "Loading: held pressed" applied to the thumb shows that the request is in flight without a spinner, and the rows land as DataTable's do.
|
|
866
|
+
|
|
867
|
+
Not yet tested with people: whether the thumb is read as "current page" as quickly as a filled, coloured button.
|
|
868
|
+
|
|
869
|
+
### E70 · Can a header make law 1 structural?
|
|
870
|
+
|
|
871
|
+
A design review: `PageHeader` was rendered with a breadcrumb, a primary, a secondary, an overflow menu and segmented tabs, sticky inside a scrolling page, in all four schemes at 960px and 360px and under forced colours. `checks/page-header.mjs` measures it.
|
|
872
|
+
|
|
873
|
+
- **One slot.** Pages assembled by hand often end up with two or three primary buttons in the header, which breaks law 1. A `primary` prop that takes one element, and a development warning when a primary button is put among the other actions, make the law the easy path.
|
|
874
|
+
- **The primary last.** The primary sits at the end of the row, nearest the reading edge and furthest from the breadcrumb, so it's where the eye lands after the title.
|
|
875
|
+
- **Rise only while covered.** A header raised all the time would be a card on top of the page. It lies on the ground and rises one level only while content passes under it, as ScrollArea's header does, through an IntersectionObserver sentinel rather than a scroll listener.
|
|
876
|
+
- **Reach through the padding.** A sticky header's rise that stops at the page's padding looks like a floating strip. `--alt-page-pad` lets it span the page.
|
|
877
|
+
|
|
878
|
+
Not yet tested with people: whether the overflow menu between the secondary and the primary hides actions people look for.
|
|
879
|
+
|
|
880
|
+
### E71 · Is the current page a live surface?
|
|
881
|
+
|
|
882
|
+
A design review: `AppShell` was rendered with a workspace switcher, two groups (one collapsible), a count, a footer user and a page with a `PageHeader`, expanded, collapsed to a rail and as a drawer at 360px, in all four schemes and under forced colours. `checks/app-shell.mjs` measures it.
|
|
883
|
+
|
|
884
|
+
- **A raised pill, as the issue proposed.** "Selected: raised out of its well" was written for choices inside a control, and the issue asked for a comparison with a flush plate and an accent bar. The pill keeps navigation in the Tabs family (one raised mark that travels between options), and its glide shows where you went. The accent bar was not built: a coloured bar on a destination reads as status.
|
|
885
|
+
- **The frame stays on the ground.** Sidebars are often floated as raised cards. A frame that's always there is the record of where you can go, so it lies flush, divided by lines, and only what acts lifts.
|
|
886
|
+
- **The rail's tooltips go right.** Above, a tooltip covers the icon over it. Tooltip gained `side="right"`, and the rail lets its tooltips out past its edge.
|
|
887
|
+
- **One measurement path.** The pill is measured when `aria-current` moves (a MutationObserver) or the sidebar resizes, not after every render. An after-every-render measurement fed back into React's updates in this tree, and hit React's update-depth limit on navigation.
|
|
888
|
+
|
|
889
|
+
Not yet tested with people: whether a rail of icons with tooltips is learnable without the expanded labels.
|
|
890
|
+
|
|
891
|
+
### E72 · Should a detail pane lift?
|
|
892
|
+
|
|
893
|
+
A design review: `Resizable` was rendered as a list and detail at 35/65, dragged, resized by keyboard and collapsed, in all four schemes at 960px and under forced colours. `checks/resizable.mjs` measures it.
|
|
894
|
+
|
|
895
|
+
- **Panes stay on the ground.** A detail pane is often drawn as a raised panel. A pane that's always there beside its list is part of the page, and raising it would make it read as a Sheet (a task laid over the page) or a card. Only the grip, which you pick up, uses height.
|
|
896
|
+
- **The grip floats while held.** "Whatever is in a hand floats at level 3" applies to a divider you carry, so the grip rises from 1 to 3 on press, as a sortable item does.
|
|
897
|
+
- **Enter collapses.** The APG window splitter puts collapse on Enter. A collapsed pane is inert and the handle says so in words, so keyboard users can't tab into content they can't see.
|
|
898
|
+
- **Percent sizes.** Sizes are percentages of the group, so a layout saved at one window width still fits another.
|
|
899
|
+
|
|
900
|
+
Not yet tested with people: whether an 8px-wide grip is found without a hover.
|
|
901
|
+
|
|
902
|
+
### E73 · What marks the thing a context menu is about?
|
|
903
|
+
|
|
904
|
+
A design review: `ContextMenu` was rendered on rows of a file list, each with a "⋯" menu of the same actions, opened by right-click, by Shift+F10 and by a long press, in all four schemes and under forced colours. `checks/context-menu.mjs` measures it.
|
|
905
|
+
|
|
906
|
+
- **A ring, not a lift.** The thing must show it's the subject, and lifting is how Altitude usually says "this one". But a row lifted out of a table breaks the columns (the reason DataTable's details open flush). A 2px `accent-ink` outline is DataGrid's active-cell ring, and works on any surface.
|
|
907
|
+
- **One menu implementation.** DropdownMenu's panel (items, highlight, keyboard, choosing) is now a shared internal `MenuPanel`, and ContextMenu positions it at the pointer in a portal. The two can't drift.
|
|
908
|
+
- **No submenus.** The issue sketched "Move to…" with nested items. That's floating on floating, which the rules forbid, and nested menus are a known keyboard and pointer trap. A long choice opens a Dialog.
|
|
909
|
+
- **Keyboard first-class.** Shift+F10 and the Menu key open it at the thing with the first item active. The thing must be focusable for that, so the README says so.
|
|
910
|
+
|
|
911
|
+
Not yet tested with people: whether the outline is noticed when the menu covers most of the row.
|
|
912
|
+
|
|
913
|
+
### E74 · Does a page banner need a new reading?
|
|
914
|
+
|
|
915
|
+
A design review: `Alert placement="page"` was rendered as a raised "trial ends" banner with an action and a worded dismiss, and a flat "email delays" banner, across the top of a page, in all four schemes at 960px and 360px and under forced colours. `checks/alert-banner.mjs` measures it.
|
|
916
|
+
|
|
917
|
+
- **No new tier.** A banner is status at page scope, so the existing Alert rule holds: informing is a flat sheet (ground) and asking for action is raised. Only the shape changes: edge to edge, no side rounding, and only a bottom edge and contact shadow, because a sheet laid across the whole width has no sides to show.
|
|
918
|
+
- **A region, so it can be skipped.** A banner repeated on every page is in the way for screen reader users, so it's a region named by its title.
|
|
919
|
+
- **Focus goes to the page.** Dismissing removes the button that had focus. Focus moves to `main` rather than to the top of the document.
|
|
920
|
+
- **Worded dismiss.** "Remind me tomorrow" says what dismissing does, where an × would suggest the notice is gone for good.
|
|
921
|
+
|
|
922
|
+
Not yet tested with people: whether a raised banner is read as more urgent than a flat one at the very top of an app.
|
|
923
|
+
|
|
924
|
+
### E75 · How do you mark a whole app as different?
|
|
925
|
+
|
|
926
|
+
A design review: `AltitudeProvider mode` was rendered as test mode over a dashboard, with a Dialog open, in all four schemes at 960px, under forced colours and with `contrast="more"`. `checks/app-mode.mjs` measures it.
|
|
927
|
+
|
|
928
|
+
- **Words and a border, as the issue proposed.** Height means "this matters now" relative to the page, and a mode changes the page itself, so there's no ground left to rise from. The ruling: a mode is the one state carried primarily by the non-height cues (law 7), and Elevation.md states it as a rule.
|
|
929
|
+
- **A frame that can't scroll away.** Coloured strips at the top of an app scroll away and get ignored. The 2px frame is fixed to the viewport and sits above dialogs and sheets, so the mode is in view at the moment of a consequential action.
|
|
930
|
+
- **The dialog names it too.** A confirmation in test mode ("Refund this payment?") carries a "Test mode" plate, because the dialog's scrim dims the strip.
|
|
931
|
+
- **Not dismissable.** A mode you can hide is a mode you can forget, so the strip has only your way out (`action`).
|
|
932
|
+
|
|
933
|
+
Not yet tested with people: whether the frame alone, without the strip in view, is recognised as "test mode" by people new to the app.
|
|
934
|
+
|
|
935
|
+
### E76 · Can law 1 be checked?
|
|
936
|
+
|
|
937
|
+
An engineering review: two primaries on a page, one on a page plus one in a Dialog, two in separate `PrimaryScope`s, one hidden, and the check switched off, rendered in a preview page. `checks/primary-scope.mjs` counts the warnings.
|
|
938
|
+
|
|
939
|
+
- **Count what's visible, per view.** A page commonly holds primaries it isn't showing (a closed tab panel, a collapsed section). Only visible primaries count, and each Dialog, Sheet and Popover is its own view, because only one of them is in front at a time.
|
|
940
|
+
- **One warning per arrangement.** A warning that repeats on every render is ignored, so each scope warns once per set of names, and the warning names the buttons so they can be found.
|
|
941
|
+
- **Off in the design system's own build.** The IIFE bundle has no `process.env`, so the check is on only where a bundler says it's development (or with `devChecks`). That keeps the previews, which fail on any console warning, honest about real warnings.
|
|
942
|
+
- **An explicit opt-out.** Dashboards of independent cards each have a legitimate primary. `PrimaryScope` says so in the code, which is clearer than a global switch.
|
|
943
|
+
|
|
944
|
+
Not yet tested with people: whether agents reliably move the second primary into `actions` when they see the warning.
|
|
945
|
+
|
|
946
|
+
### E77 · Should a column appearing animate?
|
|
947
|
+
|
|
948
|
+
A design review: `DataTable columnsMenu` was rendered in the Table preview with the menu open, columns hidden and reordered, and compact rows, in all four schemes and under forced colours. `checks/table-columns.mjs` measures it.
|
|
949
|
+
|
|
950
|
+
- **No motion for layout.** Rows glide when they're re-sorted, because the record moved. A column appearing or leaving changes how the record is laid out, not the record, so the table re-lays out at once.
|
|
951
|
+
- **Reuse, not a new control.** The menu is a Popover with a SortableList of Checkboxes and a ToggleGroup: every part already has its elevation and keyboard model, so the menu needed none of its own.
|
|
952
|
+
- **The first column stays.** A row without its name column can't be read or acted on, so the first column in order can't be hidden, and its checkbox says so by being disabled.
|
|
953
|
+
- **Compact stays 24px.** Compact density trims row padding to 4px, which keeps 28px rows and 24px targets.
|
|
954
|
+
|
|
955
|
+
Not yet tested with people: whether the "· 3 hidden" count is enough to find hidden columns again.
|
|
956
|
+
|
|
957
|
+
### E78 · What do rows do while the next page loads?
|
|
958
|
+
|
|
959
|
+
A design review: `DataTable pagination` was rendered paging 214 invoices eight at a time and loading 46 events six at a time, mid-load and loaded, in all four schemes. `checks/table-paging.mjs` measures it.
|
|
960
|
+
|
|
961
|
+
- **Half strength, not an imprint.** Replacing the rows with skeletons on every page change throws away what people were reading and moves their eyes. Keeping them on the ground at half strength, as a refetching chart does, says "these are about to change" and keeps the layout still.
|
|
962
|
+
- **The thumb is held pressed.** "Loading: held pressed" applied to Pagination's thumb puts the in-flight state where the hand just was.
|
|
963
|
+
- **Appended rows glow, staggered.** Rows from "Load more" are new to the page, so they take the afterglow, and riseDelay keeps at most four starting at once.
|
|
964
|
+
- **The range is the announcement.** "6 more loaded, 12 of 46" says what happened and where you are, which "Loading complete" doesn't.
|
|
965
|
+
|
|
966
|
+
Not yet tested with people: whether half strength is read as "loading" without a spinner.
|
|
967
|
+
|
|
968
|
+
### E79 · Where do bulk actions go when the selection is "all matching"?
|
|
969
|
+
|
|
970
|
+
A design review: `DataTable` was rendered with 25 loaded rows of 2,341 matching, the page selected, "all matching" selected with one exception, and scrolled until the toolbar was gone, in all four schemes. `checks/table-bulk.mjs` measures it.
|
|
971
|
+
|
|
972
|
+
- **A query, not a list.** "Select all 2,341" can't mean 2,341 ids the browser has never seen. The selection becomes `{ kind: 'query', total, except }`, which an API can act on directly, the flow Gmail, Linear and Shopify's Polaris share.
|
|
973
|
+
- **Said in words.** "All 2,341 matching, except 1" says exactly what an action will touch. A count alone ("2,340") hides that it's a query.
|
|
974
|
+
- **The bar floats at level 3.** A large selection is something in your hand while you scroll, and "whatever is in a hand floats". In `auto` it floats only once the toolbar is out of view, so it never covers rows needlessly.
|
|
975
|
+
- **The offer is flush.** "Select all matching" is information with a link, not an action asking to be taken, so it's a flush `accent-soft` row, not a raised banner.
|
|
976
|
+
|
|
977
|
+
Not yet tested with people: whether "except 1" is noticed before a destructive bulk action.
|
|
978
|
+
|
|
979
|
+
### E80 · What is a view, and when is it modified?
|
|
980
|
+
|
|
981
|
+
A design review: `SavedViews` was rendered over a FilterBar and a DataTable with three views, switched, modified (a sort and a filter changed), saved, saved as new and reset, in all four schemes. `checks/saved-views.mjs` measures it.
|
|
982
|
+
|
|
983
|
+
- **A view is the combination.** Filters, their match, the sort and the columns change together, so the hook holds them as one draft per view and spreads them onto FilterBar and DataTable. Agent-built versions usually store some of these and forget the rest.
|
|
984
|
+
- **A draft per view.** Switching tabs shouldn't throw away a change you were making, so each view keeps its draft until it's saved or reset.
|
|
985
|
+
- **Folder tabs, as the issue said.** The views own the table beneath them, so the active tab and the sheet holding the filters and table are one raised piece. The actions sit beside the list, not inside it, because a tab list may hold only tabs.
|
|
986
|
+
- **Modified is a word and a dot.** It's announced once, when the view first diverges, not on every change.
|
|
987
|
+
|
|
988
|
+
Not yet tested with people: whether "Save view" over a shared view needs a confirmation.
|
|
989
|
+
|
|
990
|
+
### E81 · What does each stage of an upload look like?
|
|
991
|
+
|
|
992
|
+
A design review: `DropZone queue` was rendered with files uploading, processing, done and failed (with Retry), in all four schemes at 960px and 360px and under forced colours. `checks/dropzone-queue.mjs` drives a queue through every state.
|
|
993
|
+
|
|
994
|
+
- **Four stages, four existing readings.** Uploading is a task moving toward done, so it's `Progress`. Processing is a write the server hasn't confirmed, so it hovers as pending (after `dur-grace`, so fast steps don't flicker). Done is committed, so it lands and settles. Failed drops to a trench. No new signal was needed, which is the point of the vocabulary.
|
|
995
|
+
- **Coarse announcements.** Announcing every percent floods a screen reader, so progress is said at quarters, plus the changes of stage.
|
|
996
|
+
- **Every button names its file.** In a list of five uploads, "Retry" alone is ambiguous, so each action's name includes the file.
|
|
997
|
+
|
|
998
|
+
Not yet tested with people: whether the pending hover is read as "the server is working" rather than as a hover effect.
|
|
999
|
+
|
|
1000
|
+
### E82 · How deep should a filter go?
|
|
1001
|
+
|
|
1002
|
+
A design review: `FilterBar nesting={2}` was rendered with "Plan is Pro and any of: Country is Germany, Seats > 50" over a table filtered in place, a group added and removed, in all four schemes and under forced colours. `checks/filter-groups.mjs` measures it.
|
|
1003
|
+
|
|
1004
|
+
- **A bracket, not a card.** A group could be a raised panel, but it doesn't act: it's part of the query's record. A flush 1px `line-control` rule groups its conditions as brackets do in writing.
|
|
1005
|
+
- **One level.** Rule builders that nest without limit turn into code editors with boxes. Real segments almost never need more than "A and (B or C)", so groups go one level deep.
|
|
1006
|
+
- **Composition over a new model.** Each group is a FilterBar of its own (its conditions, its all/any, its Add filter), so the keyboard model, pickers and announcements are the ones FilterBar already has.
|
|
1007
|
+
- **The sentence.** `filterSentence` reads the whole tree ("… and any of: …"), so screen reader users and summaries get the logic, not just a count.
|
|
1008
|
+
|
|
1009
|
+
Not yet tested with people: whether "any of" groups are understood without an "or" between their conditions.
|
|
1010
|
+
|
|
1011
|
+
### E83 · Should past events be cards?
|
|
1012
|
+
|
|
1013
|
+
A design review: `Timeline` was rendered with events from today and yesterday, one linking, one with detail open, and a comment you made landing and failing, in all four schemes and under forced colours. `checks/timeline.mjs` measures it.
|
|
1014
|
+
|
|
1015
|
+
- **Flush, on an upright ground line.** Activity feeds are often lists of raised cards, which claims every past event is live. A history is the record, so its events are flush entries on the same ground line Stepper and charts stand on, turned upright.
|
|
1016
|
+
- **Only what's new or yours moves.** New events take the afterglow, staggered. An event you caused hovers as a pending write until it lands. A failure drops to a trench without the afterglow's tint, because a failure isn't news to celebrate.
|
|
1017
|
+
- **Detail as a lid.** Detail opens as a raised card (the Accordion's lid), because opening it is something you did, unlike the events themselves.
|
|
1018
|
+
- **One sentence each.** Each event is a sentence with its time, so a screen reader can read the list without the line or markers.
|
|
1019
|
+
|
|
1020
|
+
Not yet tested with people: whether day headings help or slow scanning in a busy feed.
|
|
1021
|
+
|
|
1022
|
+
### E84 · Is unread a height?
|
|
1023
|
+
|
|
1024
|
+
A design review: `NotificationCenter` was rendered with unread and read notifications over two days, one with an action, opened, marked read and receiving a new notification, in all four schemes, at 360px (a bottom Sheet) and under forced colours. `checks/notifications.mjs` measures it.
|
|
1025
|
+
|
|
1026
|
+
- **Unread is a mark.** Inboxes often keep unread items tinted or raised until they're read. That makes height mean "unread", a persistent state, which breaks "tiers carry meaning". Unread is an accent dot, weight 600 and "Unread" in the name; the item settles like anything else. Elevation.md's "States that don't rise" now points here.
|
|
1027
|
+
- **Only the count floats.** The bell is a ghost button; its count is the one thing on it that wants you, so it floats, as in a Sidebar.
|
|
1028
|
+
- **Announced while closed.** New notifications are announced, batched, from outside the panel, so they're heard without opening it.
|
|
1029
|
+
- **Opening one reads it.** Following an item marks it read and closes the panel, which is what people expect and saves a click.
|
|
1030
|
+
|
|
1031
|
+
Not yet tested with people: whether the dot alone is noticed in a long list, without the tint people are used to.
|
|
1032
|
+
|
|
1033
|
+
### E85 · How should "saved" be said?
|
|
1034
|
+
|
|
1035
|
+
A design review: `SyncIndicator` was rendered saving, saved, offline with changes waiting (the queue open), failed and in conflict, beside an autosaving textarea, in all four schemes and under forced colours. `checks/sync-indicator.mjs` measures it.
|
|
1036
|
+
|
|
1037
|
+
- **A label, not a light show.** The state is a label at document scope, so it's a Badge plate whose lamp relights on change. Nothing rises, because a state change isn't new content, and the words carry the meaning in every mode.
|
|
1038
|
+
- **One vocabulary, two scopes.** Items waiting to sync hover as pending, the commitment signal; the plate says the same thing for the whole document, so the two never disagree.
|
|
1039
|
+
- **Announce without chatter.** An autosave that announces "Saving… Saved" on every keystroke is noise. "Saved" is said at most every five seconds and "Saving" never; going offline and failures are said at once, because they change what people should do.
|
|
1040
|
+
- **The queue is a door.** Offline, the plate counts what's waiting and opens the list, so "3 changes waiting" can be checked rather than trusted.
|
|
1041
|
+
|
|
1042
|
+
Not yet tested with people: whether "Saved · 2 minutes ago" builds more trust than "Saved" alone.
|
|
1043
|
+
|
|
1044
|
+
### E86 · How should a change read without colour?
|
|
1045
|
+
|
|
1046
|
+
A design review: `DiffView` was rendered for a config file (two changed lines, an added line, unchanged runs folded) and for a subscription's properties, in all four schemes and under forced colours. `checks/diff-view.mjs` measures it, including the diff itself.
|
|
1047
|
+
|
|
1048
|
+
- **Signs and words first.** Diffs drawn in red and green alone fail colour-blind readers and the "never colour alone" rule. Every change has a sign (`+`, `−`), an element (`ins`, `del`), a strike-through for removals and "Added"/"Removed" for screen readers; the tints are a third cue, dropped under forced colours.
|
|
1049
|
+
- **Pair by block.** A block of two removed lines followed by two added lines is two changed lines, so the nth removal pairs with the nth addition for word marks. Pairing only neighbours marked the wrong words.
|
|
1050
|
+
- **Fold long runs, not single lines.** A lid for one unchanged line takes as much room as the line and hides it, so only runs of two or more fold.
|
|
1051
|
+
- **Small, built in.** An LCS line and word diff covers documents and configs. Very large inputs can pass precomputed `hunks`.
|
|
1052
|
+
|
|
1053
|
+
Not yet tested with people: whether the summary ("3 additions, 2 removals") is enough to decide whether to read on.
|
|
1054
|
+
|
|
1055
|
+
### E87 · When should a save bar rise?
|
|
1056
|
+
|
|
1057
|
+
A design review: `SaveBar` was rendered over a four-field settings form driven by `useEditRecord`, clean, dirty, saving, saved and failed, in all four schemes and under forced colours. `checks/save-bar.mjs` drives it.
|
|
1058
|
+
|
|
1059
|
+
- **After the change settles, not on the key.** A bar that rises on the first keystroke is an abrupt onset while someone is typing. It rises 600ms after the draft stops changing, which the issue proposed.
|
|
1060
|
+
- **Level 3, below sheets.** A draft is in your hand, so it floats, but at level 3, so a Sheet or Dialog opened over the page still sits above it.
|
|
1061
|
+
- **Failure keeps the draft.** The bar drops to a trench and says "Your changes are kept", because the worst outcome of a failed save is a lost draft.
|
|
1062
|
+
- **Reads the record.** It reads useEditRecord's phase and errors rather than having its own state, so it can't disagree with the form.
|
|
1063
|
+
|
|
1064
|
+
Not yet tested with people: whether 600ms feels responsive or late after a single edit.
|
|
1065
|
+
|
|
1066
|
+
### E88 · Is an inherited permission disabled?
|
|
1067
|
+
|
|
1068
|
+
A design review: `PermissionMatrix` was rendered with a locked Owner, three editable roles, inherited grants and denials, and a cell saving, in all four schemes and under forced colours. `checks/permission-matrix.mjs` drives it.
|
|
1069
|
+
|
|
1070
|
+
- **Decided, not disabled.** Agent-built matrices draw inherited and fixed grants as disabled checkboxes, which says "you can't have this". These cells are already decided, so they're stamped into the record like a done Checklist item, and they say where the decision comes from.
|
|
1071
|
+
- **Reachable read-only cells.** Skipping them in the grid would hide why a role has a permission. They're focusable, read their reason, and pressing Space says they can't be changed here.
|
|
1072
|
+
- **A ring, not a lift.** The active cell is DataGrid's ring, because a cell lifting out of its row breaks the grid.
|
|
1073
|
+
- **A saving cell hovers.** Permission changes usually go to a server one at a time, so a promise from `onChange` makes that cell hover as pending, then settle, or revert on failure.
|
|
1074
|
+
|
|
1075
|
+
Not yet tested with people: whether a stamped check is read as "inherited" without its tooltip.
|
|
1076
|
+
|
|
1077
|
+
### E89 · Should the recommended plan stand taller?
|
|
1078
|
+
|
|
1079
|
+
A design review: `PlanPicker` was rendered with four plans (one current, one recommended, one unavailable) and a period switch, chosen by pointer and keyboard, in all four schemes and under forced colours. `checks/plan-picker.mjs` measures it.
|
|
1080
|
+
|
|
1081
|
+
- **No lifted card.** Pricing pages raise and glow the plan they want sold. Height in Altitude means "matters now"; a sales preference isn't that, and a raised card would also promise to be pressable in a different way from the others. "Recommended" is a plate.
|
|
1082
|
+
- **One mark.** The plans are one choice, so they're RadioGroup's build at card size: flush cards and one raised mark that travels to the chosen card.
|
|
1083
|
+
- **Prices don't count.** FormattedNumber cross-fades prices when the period changes. Counting digits would delay the number for decoration (the Stat rule).
|
|
1084
|
+
- **The CTA lives outside.** Law 1 keeps one level-2 slab in the view, so the picker doesn't draw buttons on its cards. Your Continue button names the outcome.
|
|
1085
|
+
|
|
1086
|
+
Not yet tested with people: whether a "Recommended" plate steers as many choices as a raised card, which is the question a growth team will ask.
|
|
1087
|
+
|
|
1088
|
+
### E90 · Is a locked feature disabled?
|
|
1089
|
+
|
|
1090
|
+
A design review: `FeatureGate` was rendered on a secondary and a ghost button, one with an upgrade and one for someone who can't upgrade, opened by pointer and keyboard, in all four schemes. `checks/feature-gate.mjs` measures it.
|
|
1091
|
+
|
|
1092
|
+
- **Live, as the issue proposed.** The disabled build says nothing happens, is skipped by keyboard and some screen readers, and uses `ink-faint`. A locked feature does something (it explains), so it keeps the control's build, ink and hover lift, and adds a lock and a plan plate. Elevation.md now says so beside the disabled rule.
|
|
1093
|
+
- **The requirement is in the name.** "Export to CSV, requires Business plan" tells screen reader users what pressing it will lead to.
|
|
1094
|
+
- **One primary in the popover.** The upgrade is the popover's primary; PrimaryScope makes the popover its own view, so it doesn't count against the page's.
|
|
1095
|
+
- **Who can upgrade.** People who can't buy shouldn't get a dead-end button; they're told to ask an admin.
|
|
1096
|
+
|
|
1097
|
+
Not yet tested with people: whether locked features in a toolbar feel like advertising.
|
|
1098
|
+
|
|
1099
|
+
### E91 · What happens to a resolved thread?
|
|
1100
|
+
|
|
1101
|
+
A design review: `CommentThread` was rendered with two comments (one mentioning), someone typing, a comment posting, landing and failing, a comment deleted and restored, and the thread resolved and reopened, in all four schemes. `checks/comment-thread.mjs` drives it.
|
|
1102
|
+
|
|
1103
|
+
- **Stamped, not gone.** Resolved threads often vanish, which loses the record of a decision. A resolved thread is stamped like a done Checklist item: muted, collapsed to one line that says who resolved it, and kept where it was.
|
|
1104
|
+
- **Posting is a write.** A comment you post hovers as pending until the server confirms it, and a failure keeps its text in a trench with Retry, so a failed post is never mistaken for a sent one.
|
|
1105
|
+
- **Plain text, real listbox.** Mentions use a listbox with `aria-activedescendant` on the textarea, the combobox pattern; the body stays plain text with "@Name" plates, so it can be stored anywhere.
|
|
1106
|
+
- **Presence only for people.** The typing indicator is the one place presence colours belong: other people, live. It isn't announced.
|
|
1107
|
+
|
|
1108
|
+
Not yet tested with people: whether a collapsed resolved thread is reopened when it should be.
|
|
1109
|
+
|
|
1110
|
+
### E92 · Should a product tour dim the page?
|
|
1111
|
+
|
|
1112
|
+
A design review: `Tour` was rendered with three steps on a toolbar (a ghost button, an input, the primary), moved through by pointer and keyboard, skipped and finished, in all four schemes and under forced colours. `checks/tour.mjs` drives it.
|
|
1113
|
+
|
|
1114
|
+
- **No dimming.** Conventional tours dim everything but a cut-out spotlight. That's a level-5 summit, it blocks the thing being explained, and it would compete with any real dialog. A step is a level-3 note, so the page stays at full strength and usable.
|
|
1115
|
+
- **A ring on the target.** The target may be text or a region, not something you press, so it's ringed rather than lifted (DataGrid's active cell).
|
|
1116
|
+
- **No flying.** Moving on lifts one step off and raises the next at its target. A card sliding across the screen would carry the eye past what matters.
|
|
1117
|
+
- **Focus comes back.** The tour is a non-modal dialog: focus goes to the step, Tab can reach the target, and focus returns where it was when the tour ends.
|
|
1118
|
+
|
|
1119
|
+
Not yet tested with people: whether an undimmed tour is noticed as quickly on a busy page.
|
|
1120
|
+
|
|
1121
|
+
### E93 · Where does Stop go?
|
|
1122
|
+
|
|
1123
|
+
A design review: `PromptInput` was rendered empty, with two lines, with a file uploading and attached, mid-drag, and through submitted → streaming → ready, in all four schemes and under forced colours. `checks/prompt-input.mjs` drives it, including a synthetic IME composition.
|
|
1124
|
+
|
|
1125
|
+
- **Stop takes Send's place.** Composers that put Stop elsewhere, or swap a wide "Stop generating" button in, move the layout under the pointer just as people reach for it. Stop is the same square in the same slot, its icon cross-fading.
|
|
1126
|
+
- **IME Enter.** Enter during composition confirms the characters; sending then loses the message mid-word for Japanese, Chinese and Korean writers. `isComposing` (and keyCode 229 for older Safari) is checked before sending.
|
|
1127
|
+
- **Send stays reachable when empty.** It's flush and faint like a disabled button, but `aria-disabled` rather than `disabled`, so keyboard users find it and learn why it does nothing.
|
|
1128
|
+
- **An upload is a write.** A file being uploaded hovers as pending, and Send waits for it, so a message is never sent missing its attachment.
|
|
1129
|
+
|
|
1130
|
+
Not yet tested with people: whether the faint Send reads as "write something first" without a hint.
|
|
1131
|
+
|
|
1132
|
+
### E94 · Is a streaming answer pending?
|
|
1133
|
+
|
|
1134
|
+
A design review: `Conversation` was rendered with a settled exchange, an answer streaming in at six characters per 60ms, stopped, retried and completed, scrolled up mid-stream, in all four schemes and under forced colours. `checks/conversation.mjs` drives it.
|
|
1135
|
+
|
|
1136
|
+
- **Not pending.** A pending write hovers because something you did is in flight and might fail. A streaming answer isn't yours, and drawing it hovering would say your message hadn't been sent. It's the record still being written, so it borrows charts' "not yet the record": a dashed rule.
|
|
1137
|
+
- **The arrival signal waits.** "New content arrives tinted and lifted, then settles" can't run for the 10 seconds an answer streams. The afterglow is held until completion, which is also when the answer becomes worth reading as a whole.
|
|
1138
|
+
- **Announce once.** `role="log"` announces every change, which for streamed text is a word-by-word stutter. The log's announcements are off; the completed answer is announced once, truncated.
|
|
1139
|
+
- **Let go when they scroll up.** Following to the bottom is right until someone scrolls back to read; then it stops and offers "Jump to latest".
|
|
1140
|
+
|
|
1141
|
+
Not yet tested with people: whether screen reader users prefer sentence-by-sentence announcements for long answers.
|
|
1142
|
+
|
|
1143
|
+
### E95 · Does a proposal hover?
|
|
1144
|
+
|
|
1145
|
+
A design review: `Confirmation` was rendered as three proposals (plain, hold to approve, and a dialog for an irreversible one that fails), approved, rejected, failed and retried, in all four schemes. `checks/confirmation.mjs` drives it.
|
|
1146
|
+
|
|
1147
|
+
- **Raised, not pending.** Commitment says a pending write hovers. Without a separate state for proposals, an agent would reasonably draw "awaiting approval" hovering, which tells the person being asked that it's already happening. Elevation.md now has a Proposed row: raised, like any status that asks for action.
|
|
1148
|
+
- **Pending begins at approval.** Only then is something in flight; then the commitment signal runs as usual.
|
|
1149
|
+
- **The consequence in words.** "214 Pro customers will receive…" rather than "Run tool send_email?", and Approve names the outcome.
|
|
1150
|
+
- **No default action.** Enter never approves, and nothing is focused when a proposal arrives, so typing in the composer can't approve it by accident.
|
|
1151
|
+
|
|
1152
|
+
Not yet tested with people: whether "The assistant is asking" is needed when proposals appear inside a conversation.
|
|
1153
|
+
|
|
1154
|
+
### E96 · How does an agent's work look while it runs?
|
|
1155
|
+
|
|
1156
|
+
A design review: `Trace` was rendered running a five-step plan (a tool call, reasoning, a step that fails and is retried, a step awaiting approval), in all four schemes and under forced colours. `checks/trace.mjs` drives it.
|
|
1157
|
+
|
|
1158
|
+
- **Stepper, not cards.** Traces are usually nested cards with spinners. A run is a process, which Altitude already draws: done steps settle, the current one stands, planned ones are imprints. Using Stepper's plates means an agent run and a checkout flow read the same way.
|
|
1159
|
+
- **No spinners.** The running step is raised with a steady lamp in its mark. A spinner on every running row is motion that says nothing new.
|
|
1160
|
+
- **Failure is a trench, approval is raised.** A failed step drops down with its reason and Retry. A step waiting for approval holds a Confirmation, raised, because it asks for action (E95).
|
|
1161
|
+
- **Throttled announcements.** Agents can complete several steps per second; announcing each would bury the one that matters. Changes are coalesced; failures and approvals interrupt.
|
|
1162
|
+
|
|
1163
|
+
Not yet tested with people: whether planned imprints set expectations the agent then breaks when its plan changes.
|
|
1164
|
+
|
|
1165
|
+
### E97 · Is a suggested value a value?
|
|
1166
|
+
|
|
1167
|
+
A design review: `Input` and `Textarea` were rendered with suggestions (a whole subject for an empty field, the rest of a label being typed, a sentence appended to a reply), accepted with Tab, dismissed with Escape and typed past, in all four schemes and under forced colours. `checks/input-suggestion.mjs` drives it.
|
|
1168
|
+
|
|
1169
|
+
- **Not a value.** Putting the suggestion in the value makes it look like the person's input and fills the well, so the form says the field is done when nobody has decided. The ghost is drawn over the field and the well stays an opening until it's accepted.
|
|
1170
|
+
- **Feedforward, not new.** Altitude already shows what an action would produce as a faint ghost (the check a box would get). A suggestion is the same promise: "this is what Tab will put here."
|
|
1171
|
+
- **Tab only when there's a ghost.** Otherwise Tab must move focus, or keyboard users are trapped in the field.
|
|
1172
|
+
- **Heard before accepted.** The suggestion is the field's description, so a screen reader user hears it when they arrive, and acceptance is announced.
|
|
1173
|
+
|
|
1174
|
+
Not yet tested with people: whether Tab-to-accept collides with expectations in forms where Tab always moves on.
|
|
1175
|
+
|
|
1176
|
+
### E98 · Where does a proposed edit live?
|
|
1177
|
+
|
|
1178
|
+
A design review: `PropertyList` was rendered as a deal with three proposed changes from an assistant (one whose save fails), accepted one by one, all at once and rejected, in all four schemes. `checks/property-proposals.mjs` drives it.
|
|
1179
|
+
|
|
1180
|
+
- **In place, not elsewhere.** Proposals are usually shown in a side panel and copied over, or written straight into the record with an undo. The first loses the context of the current value; the second makes a proposal look like the record. Here each field shows both, current struck beside proposed, where the value lives.
|
|
1181
|
+
- **Raised, like any proposal.** A field with a proposal asks for action, so it's raised one level, as Confirmation (E95). It isn't pending: nothing has been sent.
|
|
1182
|
+
- **Saved through the same contract.** Accepting runs the item's own onSave, so the pending hover, failure and afterglow are InlineEdit's, and a failure keeps the proposal with Retry.
|
|
1183
|
+
- **Accept all staggers.** Several fields settling at once would glow together; riseDelay keeps at most four afterglows starting at once.
|
|
1184
|
+
|
|
1185
|
+
Not yet tested with people: whether a long list of proposals needs a "review one by one" mode.
|
|
1186
|
+
|
|
1187
|
+
### E99 · How should AI content be marked?
|
|
1188
|
+
|
|
1189
|
+
A design review: `AILabel` was rendered on a generated summary (with sources and confidence) and a reviewed, edited reply, opened by pointer and keyboard, in all four schemes and under forced colours. `checks/ai-label.mjs` measures it.
|
|
1190
|
+
|
|
1191
|
+
- **A plate, not a glow.** The common treatment is a gradient border, a glow or a shimmer. A glow lights the content from below, which breaks "the light never moves", and Carbon's own reviewers found gradient rims confused with focus. It's a Badge plate with a word.
|
|
1192
|
+
- **It explains.** Following Carbon, the label is the entry point: it opens what model made it, when, from what, and how confident it is, in words.
|
|
1193
|
+
- **Provenance persists.** Once reviewed, the label changes to "Drafted with AI" and records who reviewed it, rather than disappearing.
|
|
1194
|
+
- **No moving parts.** The icon is static; nothing animates to draw attention to AI content.
|
|
1195
|
+
|
|
1196
|
+
Not yet tested with people: whether "AI" alone is understood, or a longer word ("AI-generated") is needed in some markets.
|
|
1197
|
+
|
|
1198
|
+
### E100 · What is a citation in an answer?
|
|
1199
|
+
|
|
1200
|
+
A design review: `InlineCitation` and `Sources` were rendered in a three-sentence answer citing three sources (one unverified), reached by pointer and keyboard, in all four schemes and under forced colours. `checks/inline-citation.mjs` drives it.
|
|
1201
|
+
|
|
1202
|
+
- **A plate in the text.** Bare superscripts are hard to hit and read as "[2]". The marker is ChartAnnotation's flush plate at text size: part of the record, and it lifts only because it acts.
|
|
1203
|
+
- **Preview on focus too.** Hover-only previews leave keyboard users following links blind. The HoverCard opens on focus, and Enter follows.
|
|
1204
|
+
- **A name, not a number.** "Source 2: Billing FAQ" makes sense out of context, in a links list or read aloud.
|
|
1205
|
+
- **Unverified in words.** A caution mark alone would be colour and shape; "Unverified source" says it.
|
|
1206
|
+
|
|
1207
|
+
Not yet tested with people: whether readers check sources more often with previews than with a list alone.
|
|
1208
|
+
|
|
1209
|
+
### E101 · Do a chart's references hide the data they explain?
|
|
1210
|
+
|
|
1211
|
+
An audit of the `ChartReference` preview (#184) found five words over marks: the "DNS outage" plate on the uptime dip it annotates, "Target 99.9%" and "Normal range" across the series line, "Bar 75" over the top bar with its halo cutting a notch out of the fill, and "Average 162.5 ms" crossed by the Apr 15 peak. E38 kept words off each other but never tested them against the marks. `checks/chart-reference.mjs` now samples each line's stroke and each bar's box under every label and plate, at 960px and 360px.
|
|
1212
|
+
|
|
1213
|
+
- **Headroom for plates.** A plate is a label on the record, set above the data with a leader down to its x. The data now starts below the plates' last row, so a plate can't land on a peak. The plot gives up 26px a row (at most half its height).
|
|
1214
|
+
- **Above the bars, across a horizontal chart.** The labels' row sits above the first bar instead of on it.
|
|
1215
|
+
- **The other side, the other end.** A reference label tries its right end above the line, then below, then its left end, then steps along the line, and takes the first spot clear of every line, area and bar. A tall band's label tries its four inside corners first.
|
|
1216
|
+
- **The least covered, when nothing is clear.** On a 360px plot a label can have no clear spot. It then takes the one that covers least, with bars weighed by the area covered, so grazing a bar's top edge beats lying across it.
|
|
1217
|
+
|
|
1218
|
+
Not yet tested with people: whether a label that moves to the left end is still read as belonging to its line.
|
|
1219
|
+
|
|
1220
|
+
### E102 · Should a chart say its point before it's read?
|
|
1221
|
+
|
|
1222
|
+
A design review of `takeaway` (#187): the BarChart and LineChart previews now lead with one, in a card at 960px and 360px, in all four schemes and under forced colours. `checks/chart-takeaway.mjs` measures it.
|
|
1223
|
+
|
|
1224
|
+
- **Words at the top are what's remembered.** Titles that state the message are fixated on and recalled more, and more so at the top (Borkin et al., 2016); a headline stating the pattern shaped which pattern people remembered (Ajani et al., 2021). Before this, the only sentence with a finding was `summary`, under the plot in 13px `ink-muted`.
|
|
1225
|
+
- **Authored, never generated.** A title's slant changes what people take away, even against the chart (Kong et al., 2018). The generated screen-reader summary still says the range and trend; the takeaway sits beside it, never in place of it.
|
|
1226
|
+
- **The title becomes a label.** With a takeaway the chart is still named: the title steps down to a caption in `ink-muted`, and stays the figure's accessible name. A screen reader hears the name, then the finding, then the summary.
|
|
1227
|
+
- **Cross-faded, not glowing.** A takeaway that changes with the data fades in place like a `Stat`'s value. It doesn't take the afterglow, because the marks' changes already spend the attention budget.
|
|
1228
|
+
|
|
1229
|
+
Not yet tested with people: whether readers check a takeaway against the marks, or take it on trust.
|
|
1230
|
+
|
|
1231
|
+
### E103 · Does the frame's text rank below the data?
|
|
1232
|
+
|
|
1233
|
+
A design review of axis text (#193): every chart preview at 960px and 360px in all four schemes, under forced colours and with `contrast="more"`, with `yLabels="inline"` on the "Response time" card. `checks/chart-axis-text.mjs` measures it.
|
|
1234
|
+
|
|
1235
|
+
- **Weight before size.** Tick labels were 500 12.5px, the weight of a label on a control, so the axis read as something to press and competed with the marks. They're now 400 13px, the caption of `.alt-chart-desc`, still `ink-muted`, which holds 4.5:1 on every surface at that size. Gridlines stay `line`: visible but subtle is what Stone & Bartram measured, and there's no lighter grey to invent.
|
|
1236
|
+
- **The gutter is the cost.** In a 440px card the y labels took 56px: 15% of the plot at 960, 21% at 360. Inline labels, just above each gridline at the left (the FT and Economist convention), give it back.
|
|
1237
|
+
- **The unit once.** "250 ms" at the top, then "150" and "50": the numbers read faster without the repeated unit. Only shared symbols and spaced words are lifted out; a compact "K" is part of the number.
|
|
1238
|
+
- **Never on the data.** A label that would sit on a line, an area or a bar is left out, and its gridline stays (in the preview, 200 and 100 sit where the lines start). The readout and the table give the exact values. The ground line goes unlabelled, because it's zero, rather than sitting in the bars' way.
|
|
1239
|
+
- **Opt-in.** The default stays the gutter until people have used the inline layout.
|
|
1240
|
+
|
|
1241
|
+
Not yet tested with people: whether leaving out labels that would cover the data slows reading compared with a full gutter.
|
|
1242
|
+
|
|
1243
|
+
### E104 · How tall should a line chart be?
|
|
1244
|
+
|
|
1245
|
+
A design review of `aspect="banked"` (#194): the LineChart preview's monthly revenue at the default and banked, in a card at 960px and 360px. `checks/chart-banking.mjs` measures each line's segments as drawn.
|
|
1246
|
+
|
|
1247
|
+
- **The data chooses.** Every chart was 2:1, so a sharp rise and a gentle one filled the same box. People judge slopes best when segments average about 45° (Cleveland; Heer & Agrawala 2006 refined the criteria). The ratio is found by bisection on the mean absolute orientation, which falls as the plot widens.
|
|
1248
|
+
- **Measured.** The revenue line averaged 24.7° at the preview's 1.9 (a data area of 1.84); banked, the ratio wants to go below 1 and is held there, at 42.8°. A smooth sine banks to 45° within a degree.
|
|
1249
|
+
- **Clamped to 1–4.** A sawtooth wants a sliver and a gentle ramp a tower; neither fits a card.
|
|
1250
|
+
- **Steady.** The ratio is read from all shown series and kept while values glide, so the chart doesn't change height under the reader. New x values cross-fade as a new chart, and bank afresh then. Resizing never animates.
|
|
1251
|
+
|
|
1252
|
+
Rejected for now: multi-scale banking (a chart per frequency band). It's an analysis, not a default; small multiples could carry it later.
|
|
1253
|
+
|
|
1254
|
+
Not yet tested with people: whether readers notice that banked neighbours differ in height, and whether that reads as a difference in the data.
|
|
1255
|
+
|
|
1256
|
+
### E105 · Can a chart say what matters without raising it?
|
|
1257
|
+
|
|
1258
|
+
A design review of `focus` and `data-context` (#188): the deploys bar chart focused on the two teams its takeaway names, the stacked requests on Web, seats by plan on Team and the plan shares on Team, in all four schemes at 960px and 360px, under forced colours, with reduced motion and with `contrast="more"`. `checks/chart-focus.mjs` measures it; `build/palette.mjs` chooses and gates the token.
|
|
1259
|
+
|
|
1260
|
+
- **Nothing in a chart could matter more.** Every series wore a saturated identity colour at full strength, so the grouped, stacked and multi-line previews read as any library's defaults. Marks can't rise, so importance has to be colour and words: one hue for the story, a neutral for the context, as Knaflic and the FT do; focus shaped what people remembered (Ajani et al., 2021), and authors can't judge salience for readers (Xiong et al., 2020), so it's declared.
|
|
1261
|
+
- **A neutral of its own.** `data-other` couldn't serve: in the light schemes it's near-black and emphasises. `data-context` is the most receding neutral that holds 3:1, in the ground's hue:
|
|
1262
|
+
|
|
1263
|
+
| Scheme | `data-context` | L | C | Contrast, worst of ground and raised | ΔE from `line` | ΔE from `data-other` | Nearest slot ΔE |
|
|
1264
|
+
|---|---|---|---|---|---|---|---|
|
|
1265
|
+
| Daylight | `#898a8e` | 0.63 | 0.006 | 3.14:1 | 28.1 | 30.9 | 11.1 |
|
|
1266
|
+
| Nightfall | `#86888b` | 0.63 | 0.005 | 4.80:1 | 34.1 | 10.2 | 13.2 |
|
|
1267
|
+
| Mesa | `#8b8985` | 0.63 | 0.006 | 3.11:1 | 27.4 | 28.0 | 11.2 |
|
|
1268
|
+
| Alpine | `#848987` | 0.63 | 0.007 | 4.79:1 | 34.3 | 10.5 | 12.8 |
|
|
1269
|
+
|
|
1270
|
+
- **Exempt from the slot gate, by measurement.** As E23 found, no neutral at 3:1 is ΔE 15 from every slot (the nearest is 11.1). It's exempt because a context series is never told apart by its colour: it's unlabelled or labelled in `ink-muted`, and the words say what's highlighted.
|
|
1271
|
+
- **Dark schemes climb past `data-other`.** There the most receding neutral at 3:1 *is* `data-other` (ΔE 0.8), so `data-context` steps lighter until it's ΔE 10 clear. It recedes by having no hue rather than by darkness.
|
|
1272
|
+
- **The rule it strains.** Charts rule 5 says colour follows the entity. Focus repaints the rest, but it isn't a filter: it's fixed by the author, and every repaint cross-fades. A reader's filter (`relevance`) still recedes misses to 35% of their own colour, so the two never read alike, and under forced colours context is `GrayText` at full strength while a miss is at 35%.
|
|
1273
|
+
|
|
1274
|
+
Not yet tested with people: whether context series are still compared accurately against the focused one, or simply ignored.
|
|
1275
|
+
|
|
1276
|
+
### E106 · How should a chart show the period before?
|
|
1277
|
+
|
|
1278
|
+
A design review of `compare` (#189): seats by region against last quarter on a horizontal bar chart, daily signups against the two weeks before, and a stack against its earlier totals, in all four schemes at 960px and 360px, under forced colours and with `contrast="more"`. `checks/chart-compare.mjs` measures it.
|
|
1279
|
+
|
|
1280
|
+
- **Superpose the shape, encode the number.** Of Gleicher's three comparison designs, juxtaposing doubles the chart and a second series competes as an equal in a categorical colour. The earlier period is superposed, quieter, and the difference is explicit in words: the readout ("38 · +11%"), each point's name, the table's "vs" column and the summary's count of what rose and fell.
|
|
1281
|
+
- **Bars: the bullet graph's tick.** A 2px `ink` tick across the bar, 4px past it each side (Few, 2006). A ghost bar beside each would double the bars, and a hollow bar already means projected (E37). A drop shows as empty space between the bar's end and its tick.
|
|
1282
|
+
- **Lines: a trace, not a reference.** The open question was whether a 1.5px `ink-muted` trace could be confused with a reference line, which is 1px `ink-muted` (E38). It can't, in practice: a reference is straight, spans the plot and is named for a value ("Target 99.9%"); the trace follows the data and is named for a period. It stays `ink-muted` rather than `line-control`, because `line-control` is the crosshair's rule (E27, #186), and a trace in it would read as the pointer. Solid, because dashes mean projected.
|
|
1283
|
+
- **The tick against the hover outline.** A clickable bar's hover outline (E41) traces the bar's edge; the tick crosses it and reaches past both sides. Side by side they don't read alike.
|
|
1284
|
+
- **Stacks compare their total.** A tick per segment would stack ticks on ticks. Shares don't compare at all: every total is 100%.
|
|
1285
|
+
- **Matched by position, before any sort.** A shifted period on a time axis has different x values, so `data` is matched by position, over the whole record before windowing or sorting.
|
|
1286
|
+
|
|
1287
|
+
Not yet tested with people: whether readers prefer the tick to a delta label on each bar on small charts.
|
|
1288
|
+
|
|
1289
|
+
### E107 · Can a reading be kept?
|
|
1290
|
+
|
|
1291
|
+
A design review of `pinnable` (#190): two pinned readings on a line of issues opened, pinned by mouse, keyboard and touch, on a chart whose clicks select, and with controlled pins, in all four schemes at 960px and 360px and under forced colours. `checks/chart-pins.mjs` measures it.
|
|
1292
|
+
|
|
1293
|
+
- **Live things settle.** The readout was the only way to read a value exactly, and it left with the pointer, so comparing two meant holding one in memory. A pin lands the live readout on the record as a plate, the way an afterglow settles or a pending write lands.
|
|
1294
|
+
- **Two pins compare, in words.** Gleicher's explicit encoding: a strip under the axis between them says "+24% over 28 days" (or the change per series), and the second pin says it aloud. More pins are allowed, but only a pair compares.
|
|
1295
|
+
- **Clicks that already act (open question 1).** Where marks select or drill, a click keeps its meaning and only P pins; the readout's action line says "P to pin" while the keyboard is on the chart.
|
|
1296
|
+
- **Touch (open question 2).** A button inside the readout would need the readout to stop being `aria-hidden` decoration. Instead, while the touch readout is open, a quick tap on the plot pins the reading there, and the readout says "Tap to pin". Holding to open it never pins.
|
|
1297
|
+
- **Is a pin the record? (open question 3).** It's the reader's note on the record, which is what a plate is (Elevation, Labels). A pin icon keeps it from reading as an author's `ChartAnnotation`, and its remove button is the one part that lifts.
|
|
1298
|
+
- **Escape order.** The last pin goes first, then a selection, then the keyboard goes up a level, so Escape always undoes the most recent thing.
|
|
1299
|
+
|
|
1300
|
+
Not yet tested with people: whether readers find click-to-pin without being told, and whether two pins are enough.
|
|
1301
|
+
|
|
1302
|
+
### E108 · What moved since I was last here?
|
|
1303
|
+
|
|
1304
|
+
A design review of `since` (#191): queue depths against a morning snapshot (three changed) and an error rate against an earlier one (six changed), in all four schemes at 960px and 360px, under forced colours and with reduced motion. `checks/chart-since.mjs` measures it.
|
|
1305
|
+
|
|
1306
|
+
- **Change blindness.** People don't find a change they didn't see happen (Rensink, O'Regan & Clark, 1997); a trace of the change makes it findable (Baudisch et al., Phosphor, 2006), which is what the afterglow already does for changes that arrive while you watch. `since` gives the same trace to changes that happened while you were away.
|
|
1307
|
+
- **The comparison's mark, at the old value.** A changed bar carries `compare`'s tick (E106); a line can't carry a tick at every point, so each changed point's old place is a small hollow ring, outside the line, never on it. A "New" badge per mark would put a raised-looking plate on the data, and colouring changed marks would give colour a second job (rule 5).
|
|
1308
|
+
- **Counted, then filtered.** "3 changed since Tuesday 09:00" is a `Switch`, as `pausable`'s is: it turns on the filter language for the changed records (35%, positions held), so "show me only what changed" needs no new reading.
|
|
1309
|
+
- **Arrival spends the budget once.** Up to four changed marks glow, the `live` choreography exactly; more, and the caption glows. A changed point then says how it changed in its name instead of a bare "Changed."
|
|
1310
|
+
- **The application owns the snapshot.** What a person last saw and when is persistence, which the framework leaves to the application, as elsewhere.
|
|
1311
|
+
|
|
1312
|
+
Not decided: whether `since` should advance with `live` as the person acknowledges changes.
|
|
1313
|
+
|
|
1314
|
+
Not yet tested with people: whether the rings are found without the switch.
|
|
1315
|
+
|
|
1316
|
+
### E109 · How should a chart compare values that don't start at zero?
|
|
1317
|
+
|
|
1318
|
+
A prototype review of `DotPlot` (#192): eight services' p95 with the 10th–90th percentile, sorted, and five teams' scores from Q2 to Q3 as a dumbbell, in all four schemes at 960px and 360px, under forced colours, with reduced motion and with `contrast="more"`. `checks/dot-plot.mjs` measures it.
|
|
1319
|
+
|
|
1320
|
+
- **Bars must stand on zero.** Rule 2 is right, so a few percent between 180 and 240 ms became eight identical bars. A dot is position, so its axis can start where the data does (Cleveland & McGill's position judgements are the most accurate). With no zero, there's no ground line.
|
|
1321
|
+
- **Bars of averages mislead.** Readers judge values inside a bar likelier than values as far outside it (Newman & Scholl, 2012); dot plots reduced that error (Okan et al., 2018). The spread is the band `LineChart` already uses, after Correll & Gleicher, not error bars.
|
|
1322
|
+
- **Built from relatives.** The dots are ScatterChart's (8px, ringed in the plot's colour); the rows and label budget are horizontal BarChart's; the spread's words come from the container's band, the dumbbell's from `compare` (E106). Nothing new is invented for it.
|
|
1323
|
+
- **The dumbbell is a mode (open question 1).** It shares the rows, the keyboard path and the spread, so it's `from`/`to` on `DotPlot`, not a component.
|
|
1324
|
+
- **A hollow dot means projected elsewhere (open question 2).** A projected `LineChart` point is hollow too (E37). A `DotPlot` takes no `status`, so a dumbbell never has a projection to confuse with its "from" dot, and the link, the description and the names say which end is which. The risk stays for a reader who has just looked at a projected line; noted for testing.
|
|
1325
|
+
|
|
1326
|
+
Not yet tested with people: whether readers take a dumbbell's direction from the filled end without the words.
|
|
1327
|
+
### E110 · How large is a control on a phone?
|
|
1328
|
+
|
|
1329
|
+
A measurement (#245): `checks/phone.mjs` (#247) opened every preview at 320 and 375 CSS px as a phone. Of the interactive targets, 1,205 were under 44×44 on touch and 167 under 24×24, the WCAG 2.5.8 minimum; heights were literals, and four layout switches used five widths (768, 640, 640, 560, 480).
|
|
1330
|
+
|
|
1331
|
+
- **One setting, not a size per component.** Heights now come from `size-*` tokens that step up under `(pointer: coarse)`: 36px buttons become 44, 40px fields 48, 32px rows 44. Apple's and WCAG 2.5.5's 44px is the touch size; Parhi, Karlson & Bederson (2006) found about 35 CSS px enough for one-handed thumb targets, with accuracy worst at the edges.
|
|
1332
|
+
- **Density is the override.** `density="comfortable"` takes the touch sizes everywhere; `compact` keeps desktop sizes on touch. The hit area follows the pointer alone, so a compact face still gets a 44px target: the face is the tier's, the target is the finger's.
|
|
1333
|
+
- **Not 44px everywhere.** Desktop density is information, and a mouse doesn't need it. Hit areas alone (a 44px target on a 36px face) would overlap neighbours in a dense list, which is why the faces step up too.
|
|
1334
|
+
- **Two breakpoints.** 480 for a component's own narrow layout and 640 for page chrome. AppShell's sidebar now becomes a drawer below 640 (it was 768) and the date picker stacks below 480 (it was 560). CSS can't read a token in a media query, so the numbers are checked.
|
|
1335
|
+
|
|
1336
|
+
Measured with `checks/density.mjs`: buttons 36 → 44, small buttons 30 → 40, inputs 40 → 48, folder tabs 38 → 46 under a coarse pointer; unchanged with a mouse.
|
|
1337
|
+
|
|
1338
|
+
### E111 · Does typing zoom the page?
|
|
1339
|
+
|
|
1340
|
+
A measurement (#250): `checks/phone.mjs` found 146 text fields under 16px on touch across 68 previews: `.alt-input` text at 14px, `SecretField` at 13.5px, native selects at 12.5–13.3px. iOS Safari zooms the page when a field under 16px is focused, and leaves it zoomed.
|
|
1341
|
+
|
|
1342
|
+
- **16px under a coarse pointer, for the text you type only.** Labels stay `text-label` (14px). The rule is `max(16px, 1em)`, so a field that inherits larger text, such as an in-place title, keeps its own size.
|
|
1343
|
+
- **A platform minimum wins.** Field fonts are set at many specificities (in-place editors inherit the value's font), so the minimum is `!important` inside the coarse-pointer query.
|
|
1344
|
+
- **Never `maximum-scale=1`.** It's the common workaround, and it stops pinch zoom, which fails WCAG 1.4.4 (Resize text).
|
|
1345
|
+
|
|
1346
|
+
After: no field under 16px on touch in any preview.
|
|
1347
|
+
|
|
1348
|
+
### E112 · Does a tap leave a surface lifted?
|
|
1349
|
+
|
|
1350
|
+
A code audit (#246): all 98 `:hover` rules in `bundle.css` applied on touch screens, where a tap leaves `:hover` stuck until the next tap elsewhere, so a pressed button stayed at level 2 after doing its job. Law 4 says pressing returns a surface to the ground.
|
|
1351
|
+
|
|
1352
|
+
- **Hover is a pointer's.** Every `:hover` rule now sits inside `@media (hover: hover)`. Rules that mixed hover with `:focus-visible` were split, so keyboard focus keeps its one-level lift everywhere.
|
|
1353
|
+
- **Forced colours keep their outlines.** Under forced colours a hover is a system-colour outline with no height, so those eight rules stay bare (the issue's hairline exception).
|
|
1354
|
+
- **Linted.** `checks/hover-media.mjs` fails on a bare `:hover`, and in a browser checks that a mouse still lifts a button while a phone's tap leaves it at rest.
|
|
1355
|
+
|
|
1356
|
+
### E113 · Can a finger hit every control?
|
|
1357
|
+
|
|
1358
|
+
A measurement (#261): at 375 CSS px the phone check found 167 targets under WCAG 2.5.8's 24×24, in 58 previews: NumberInput's 24×15 steppers stacked with no gap, date segments, 20px slider thumbs, 10px brush grips, 20px tag removes, the 23px toast close, 16px citation markers, 22px badge buttons, a 1px resize handle, and the previews' own disclosures.
|
|
1359
|
+
|
|
1360
|
+
- **The face keeps its tier; the target grows.** A transparent `::after` centred on each small face reaches `size-target-min`, 24px or 44px under a coarse pointer (#245). It has no colour, border or shadow, so elevation and forced colours are untouched. The resize handle's existing hit strip now spans the target too.
|
|
1361
|
+
- **Neighbours don't share a target.** Date segments sit side by side, so theirs stops at 24px. Links and relative times in rows of text reach 24px tall, never wider.
|
|
1362
|
+
- **Steppers side by side.** Two stacked 15px steppers could never each reach 24px without overlapping. They now sit in a row, down then up, 24×24 each.
|
|
1363
|
+
- **Spacing counts.** WCAG's exception (targets whose 24px circles don't touch) is in the check, so annotation plates 26px apart pass.
|
|
1364
|
+
|
|
1365
|
+
After: 16 left, all either a `Schedule` event (#257) or a target the check finds covered by an open layer.
|
|
1366
|
+
|
|
1367
|
+
### E114 · Should a phone show keyboard shortcuts?
|
|
1368
|
+
|
|
1369
|
+
A review (#262): at 375px, menus showed "⌘D" beside "Duplicate", and CommandPalette's footer said "↑↓ Move · ↵ Choose · esc Close", on a device with no keyboard. That's noise that takes width from the labels.
|
|
1370
|
+
|
|
1371
|
+
- **Likely a keyboard, not definitely.** `useKeyboardLikely()` is false only on a touch-only device (`(hover: none) and (pointer: coarse)`) where no key has been pressed this session. A tablet with a keyboard flips it on with the first key press, for the rest of the session.
|
|
1372
|
+
- **Hints hide; shortcuts don't.** Menu shortcuts, CommandPalette's footer, chips and esc, and InlineEdit's "⌘+Enter to save" (which becomes "Save to keep it") follow it. The keys still work.
|
|
1373
|
+
- **An app's own `Kbd` stays** unless `hideOnTouch` is set, because it may be documenting shortcuts on purpose.
|
|
1374
|
+
|
|
1375
|
+
### E115 · Is a dialog's button under the keyboard?
|
|
1376
|
+
|
|
1377
|
+
A source audit (#251): Dialog, Sheet and CommandPalette sized themselves with `vh` and `position: fixed; inset: 0`, which on a phone means the largest viewport. With the on-screen keyboard open, a dialog's footer (its primary action) could sit under the keyboard. Nothing read `window.visualViewport`.
|
|
1378
|
+
|
|
1379
|
+
- **The visible viewport.** While a modal is open, `useModal` follows `visualViewport` (resize and scroll) into `--alt-vvh` and `--alt-vv-top` on its root. The roots cover just the visible area, and the dialog's height, the palette's top padding and list height derive from it, with `dvh` and then `vh` as fallbacks.
|
|
1380
|
+
- **A claim you can answer.** A dialog is level 5 and claims you (Elevation); its answer must stay in reach.
|
|
1381
|
+
- Not observed on a device: Playwright can't open a keyboard, so `checks/visible-viewport.mjs` shrinks the viewport under an open Dialog and CommandPalette and checks the root follows and the panel stays inside.
|
|
1382
|
+
|
|
1383
|
+
### E116 · Does a toast sit under the home indicator?
|
|
1384
|
+
|
|
1385
|
+
A CSS audit (#252): no rule in `bundle.css` used `safe-area-inset`. In an edge-to-edge page (`viewport-fit=cover`, any installed PWA) the Toaster, SaveBar and Sheet sat under the notch, the rounded corners or the home indicator, where a swipe goes home instead of pressing.
|
|
1386
|
+
|
|
1387
|
+
- **Offset by the inset.** Every layer fixed to an edge adds `env(safe-area-inset-*)` to its offset: Toaster, SaveBar, each side of a Sheet, and the Dialog root's padding. Without `viewport-fit=cover`, `env()` is 0 and nothing moves.
|
|
1388
|
+
- **The mode frame stays at the edge.** It frames the whole screen, so it isn't inset.
|
|
1389
|
+
- Not observed on a device (Chromium has no notch to emulate). `checks/safe-area.mjs` checks the rules, and that nothing moves without an inset.
|
|
1390
|
+
|
|
1391
|
+
### E117 · Does a menu leave the screen?
|
|
1392
|
+
|
|
1393
|
+
A measurement (#248): at 375 CSS px, DropdownMenu's "Predicted" menu pushed the page 86px sideways and HoverCard's card 26px; a tooltip at the left edge would have clipped. Panels were placed by CSS relative to their anchor, and nothing measured them. ContextMenu and Tour already clamped themselves.
|
|
1394
|
+
|
|
1395
|
+
- **Measured, then shifted.** `useFitFloating` measures a panel when it opens (and on resize) and shifts it sideways to stay 8px clear of the visible viewport's edges, composed with its own placement's `translate`. It's on Tooltip, Popover, DropdownMenu's panel, HoverCard and the Select and Combobox lists.
|
|
1396
|
+
- **Capped.** Panels are at most the viewport's width less 16px; a tooltip wraps at 320px rather than running off the screen.
|
|
1397
|
+
- **The visible width.** A phone zooms out to fit overflowing content, which stretches `clientWidth` with it, so the fit uses the visual viewport's width.
|
|
1398
|
+
|
|
1399
|
+
`checks/float-fit.mjs` opens each panel against a 360px screen's edges.
|
|
1400
|
+
|
|
1401
|
+
### E118 · Can a toast be read on a phone?
|
|
1402
|
+
|
|
1403
|
+
A source audit (#253): the stack fanned out and paused its timers on `pointerenter` and folded on `pointerleave`. Touch fires both around a tap, so on a phone the stack opened and folded with one tap and the timers never paused: WCAG 2.2.1 and 1.4.13 both ask for content a reader can hold.
|
|
1404
|
+
|
|
1405
|
+
- **A tap holds it.** On touch, a tap on the stack fans it out and pauses every timer until a tap outside; lifting the finger changes nothing. Hover still does this for a mouse, and focus for the keyboard.
|
|
1406
|
+
- **A swipe dismisses.** A horizontal swipe on a toast follows the finger and dismisses it past 64px; the toast is `touch-action: pan-y`, so vertical scrolling still passes through.
|
|
1407
|
+
|
|
1408
|
+
`checks/toaster-touch.mjs` drives it with touch events.
|
|
1409
|
+
|
|
1410
|
+
### E119 · Where should a menu open on a phone?
|
|
1411
|
+
|
|
1412
|
+
A review (#249): at 375px a Select opened a 280px list under its trigger, with 30–36px rows, possibly off the bottom of the screen; the thumb is at the bottom, the list wherever the trigger was. Native pickers open at the bottom for this reason, and AppShell and NotificationCenter already become sheets when narrow.
|
|
1413
|
+
|
|
1414
|
+
- **At the bottom edge.** `presentation="auto"` opens DropdownMenu and Select as a sheet under 640px with a coarse pointer: full width less 8px, clear of the safe areas (#252), at most 70% of the visible height (#251), 44px rows, a head with the title and Close. Close sits outside the `menu`/`listbox` role, which only holds its options.
|
|
1415
|
+
- **Still a menu (the open question).** Moving to the bottom edge doesn't make it a different choice, so it stays at level 3, the menu's, not a sheet's level 4.
|
|
1416
|
+
- **Combobox stays put.** You type into it, so its list stays under the field rather than at a bottom edge the keyboard covers; `presentation="sheet"` opts in.
|
|
1417
|
+
|
|
1418
|
+
`checks/menu-sheet.mjs` drives each by touch, and with a mouse.
|
|
1419
|
+
|
|
1420
|
+
### E120 · Does a backup code fit a phone?
|
|
1421
|
+
|
|
1422
|
+
A measurement (#266): eight 40px slots with 6px gaps are 362px, so at 375 CSS px the preview's backup code ran past the edge. Slots now give way from 40px to 28px and the gaps tighten (3–6px), so eight characters fit a 320px screen; six-digit codes keep their full size. The phone check (#247) shows no overflow.
|
|
1423
|
+
|
|
1424
|
+
### E121 · What happens to tabs that don't fit?
|
|
1425
|
+
|
|
1426
|
+
A measurement (#259): at 375 CSS px the Tabs preview was 47px wider than the screen and SavedViews 26px, so the page scrolled sideways (WCAG 1.4.10). A tab list now scrolls within itself, with no scrollbar: the chosen tab is kept in view, and an edge with more tabs past it fades out, so you can see there's more. A "More" menu (as Toolbar has) was the alternative; scrolling keeps every tab one swipe away and in its place. The folder tabs' shadow and their reach into the panel sit in the list's padding, so scrolling doesn't clip them. `checks/tabs-narrow.mjs`.
|
|
1427
|
+
|
|
1428
|
+
### E122 · Should a breadcrumb wrap?
|
|
1429
|
+
|
|
1430
|
+
A review (#263): `maxItems` collapsed the middle of a path only by count, and the trail wrapped onto more lines when narrow, so the staircase of steps broke over lines. `fit="collapse"` (default) measures the trail against its container and moves steps from the second onward behind the existing "…" menu until it fits on one line, always keeping the first step and the current page; with room again, every step comes back. Changes of width don't animate: it's layout, not a change in the record. `fit="wrap"` keeps the old behaviour. `checks/breadcrumb-fit.mjs`.
|
|
1431
|
+
|
|
1432
|
+
### E123 · What does a five-step stepper do on a phone?
|
|
1433
|
+
|
|
1434
|
+
A review (#264): at 375 CSS px the horizontal Stepper wrapped onto three lines, each ending in a connector that led nowhere, saying the opposite of the order. `orientation="auto"` lays the steps out in a row when every step fits on one line in its container, and vertically when not, measured at each width. The switch doesn't animate (resizing never does), and the list's semantics are the same either way. The default stays `horizontal`; the README recommends `auto`. `checks/stepper-auto.mjs`.
|
|
1435
|
+
|
|
1436
|
+
### E124 · Does a proposed change fit a phone?
|
|
1437
|
+
|
|
1438
|
+
A measurement (#258): at 375 CSS px a PropertyList row with a proposal kept three columns (label, change, actions), leaving the change about 35px, so "Qualified" broke as "Quali/fied" and "Proposed by…" took eleven lines. The list is now a container, and under the narrow breakpoint (480px) an inline list stacks each label over its value, and a proposal stacks its change, who proposed it and its actions. The plain list was already fine; nothing changes on wider screens.
|
|
1439
|
+
|
|
1440
|
+
### E125 · Who gives way in a row with actions?
|
|
1441
|
+
|
|
1442
|
+
A review (#267): an ItemGroup row's actions never yielded width, so at phone width Edit and Remove squeezed the title into a narrow column. The group is now a container, and under 480px of its own width the actions take their own row, lined up with the content after the media; `narrow="inline"` keeps them beside it. `ItemDescription truncate` clamps a long secondary line. The issue's third form, collapsing actions into a "…" menu, is deferred: an app's action children aren't menu items, so it needs an explicit API rather than a guess. `checks/narrow-rows.mjs` (with #258).
|
|
1443
|
+
|
|
1444
|
+
### E126 · What's a list and its detail on a phone?
|
|
1445
|
+
|
|
1446
|
+
A measurement (#265): at 375 CSS px the Resizable preview's list pane got about 80px, so "Invoice / INV-0042 / failed" took three lines, and the 1px handle had to be found with a finger. Under 480px of its own width, `narrow="stack"` (default) shows one pane at a time, the list-and-detail pattern on phones, with `narrowPanel` choosing which and `onNarrowChange` telling the app when to offer its way back; `narrow="vertical"` stacks the panes with a horizontal grip; `narrow={false}` keeps today's layout. The handle's hit area already spans the target size (#261); an older rule that shrank the vertical grip's area is gone. Pixel `minSize` is left for later. `checks/resizable-narrow.mjs`.
|
|
1447
|
+
|
|
1448
|
+
### E127 · Can a week be read on a phone?
|
|
1449
|
+
|
|
1450
|
+
A measurement (#257): at 375 CSS px the work week drew five 55px columns, so events read "Sta…" and "D", and the 55×11px slots were far under 24px. Under 480px of its own width, `narrowView="day"` (default) shows one day at a time, paged by the header's previous and next day; `narrowView={false}` keeps the view. Under a coarse pointer an hour is 96px unless `hourHeight` is set, so a 15-minute slot or event is 24px and still in proportion to its time. On touch a tap opens an event (double-click isn't reliably delivered there). A day strip, tap-to-create and press-and-hold to move are left for the touch-drag phase (#254–#256). `checks/schedule-narrow.mjs`.
|
|
1451
|
+
|
|
1452
|
+
### E128 · Squeeze a table, or let it scroll?
|
|
1453
|
+
|
|
1454
|
+
A measurement (#260): at 375 CSS px the DataGrid's five columns were each about 65px (its table is `table-layout: fixed` at 100%, which ignores a cell's minimum), so names read "Des…" and status badges "Acti"; DataTable's owners wrapped and clipped. The frame could already scroll; the columns never asked for the width. Under 480px of the frame's width (a container query), the table lays out by content at least as wide as its frame, every column keeps at least 6rem (checkbox, grip, expand and edit columns excepted), and the table scrolls inside its frame, the edge with more columns fading. Columns take `minWidth` and `sticky` (opaque ground surface over a flush 1px edge, no shadow, since a sticky column isn't live; the start fade is dropped when one marks the edge). Wide, nothing changes. The issue's `narrow="stack"` (rows as records) and `priority` are deferred: scrolling keeps the table's semantics and DataGrid's cell navigation intact, and stacking needs its own design for editing. `checks/table-narrow.mjs`.
|
|
1455
|
+
|
|
1456
|
+
### E129 · On touch, is a finger on a row a scroll or a pick-up?
|
|
1457
|
+
|
|
1458
|
+
A measurement (#256): `.alt-sort-item` was `touch-action: none` across the whole row, so on a phone every swipe that started on a sortable list reordered it and the page never scrolled. Rows are now `touch-action: manipulation`: a swipe scrolls, and a press-and-hold of 400ms without moving more than 8px picks the row up, after which the browser isn't allowed to pan (the first `touchmove` is cancelled) or open a long-press menu. The grip alone is `touch-action: none` and picks up at once, with a finger-sized hit area under a coarse pointer. A mouse drag starts as before, without a hold. The hold is a shared helper (`touchHold`), so Board and Tree take the same gesture (#254, #255). `checks/touch-drag.mjs`.
|
|
1459
|
+
|
|
1460
|
+
### E130 · Board cards by touch
|
|
1461
|
+
|
|
1462
|
+
A measurement (#254): on touch a Board card couldn't be moved at all. The board scrolls sideways, so the browser claimed the finger for panning and sent `pointercancel`, which the card treats as a drop; and the card tools only appeared on hover. Cards now take the shared press-and-hold (E129): a swipe scrolls the board, a 400ms hold picks the card up (it lifts as a mouse pick-up does, and "Picked up" is announced), and a drag then moves it between columns. The tools are always shown where there's no hover. Auto-scrolling the board while a card is carried near its edge is left for later. `checks/touch-drag.mjs`.
|
|
1463
|
+
|
|
1464
|
+
### E131 · Tree rows by touch
|
|
1465
|
+
|
|
1466
|
+
A measurement (#255): a Tree row with `onMove` couldn't be moved by touch. Its `touch-action: pan-y` gave every vertical gesture, the only direction a tree row moves, to the browser, which panned and sent `pointercancel`. Rows keep `pan-y`, so a swipe still scrolls, and take the shared press-and-hold (E129): after 400ms the row is carried as a mouse pick-up carries it, with the same targets and spring-open folders. A non-drag path for people with neither keyboard nor fine pointer (WCAG 2.5.7, a "Move to…" action) is left as its own change. `checks/touch-drag.mjs`.
|
|
1467
|
+
|
|
1468
|
+
### E132 · The phone audit, and what it changed
|
|
1469
|
+
|
|
1470
|
+
The audit behind the **Mobile** chapter (#244), at commit `f32b92b`: all 129 previews at 375 CSS px in Chromium with `isMobile` and `hasTouch`, plus touch events sent through CDP. It found 98 `:hover` rules outside `@media (hover: hover)`; 951 of 1,076 interactive targets under 44×44 CSS px, 279 under 24; 14px field text; `Board` and `Tree` not draggable by touch and `SortableList` reordering instead of scrolling; floating panels spilling off screen and overlays sized to `100vh`; nine layouts that didn't reflow; and ad hoc breakpoints at 768, 640, 560 and 480. E110–E131 record each fix. The chapter's constraints M1–M10 each name the check that holds them; M6 fails CI, and M1 and M2 warn, because a few pattern previews still overflow at 320px.
|
|
1471
|
+
|
|
1472
|
+
### E133 · Markdown without a dependency
|
|
1473
|
+
|
|
1474
|
+
Models answer in Markdown, and the answer is the largest thing on a harness's screen, but `MessageContent` took children, so every app chose its own parser and prose styles (#268). Altitude's bundle is dependency-free and ships as an IIFE, so `Markdown` is a small built-in parser (CommonMark blocks and inlines, GFM tables, task lists, strikethrough and bare links; no footnotes, math or reference links) with a `components` map for anything an app wants to render itself, the way `CodeBlock` takes `syntax`. Prior art reviewed: AI Elements' `Response` (built on Streamdown, which also patches unterminated constructs while streaming), react-markdown and markdown-it. Streaming rules, each a decision: an open fence is a streaming `CodeBlock`, since code is readable before it closes; a table shows nothing until its delimiter row arrives (before that it might not be a table) and never a row whose line hasn't ended, since its cells would shift as they arrive; unclosed emphasis, code spans and links show their words without markers, so nothing flashes as raw syntax and then jumps. Once complete, the source is read as CommonMark reads it (an unclosed `**` is literal). Raw HTML is text unless `allowHtml`; `javascript:`, `vbscript:`, `file:` and non-image `data:` URLs are refused. `checks/markdown.mjs` (35 cases).
|
|
1475
|
+
|
|
1476
|
+
### E134 · Where did the files go?
|
|
1477
|
+
|
|
1478
|
+
A person attaches a screenshot and a PDF in `PromptInput` and sends; the chips leave the composer and nothing in the thread shows what went with the message (#274), so neither someone scrolling back nor a screen reader user knows the answer was about those files. `MessageAttachments` puts them back, before the message text, in a list named "2 attachments": images as 64px thumbnails (`object-fit: cover`, a 1px rim drawn over the image so a light screenshot keeps its edge), other files as the composer's Tag plates, now settled, with type and size (`FormattedNumber` bytes). Those with `href` or `onOpen` act and are named "Open <file>"; an expired one says "No longer available" and doesn't act. They're never pending: a message isn't sent until its uploads finish. `checks/message-attachments.mjs`.
|
|
1479
|
+
|
|
1480
|
+
### E135 · Can an imprint wait without moving the light?
|
|
1481
|
+
|
|
1482
|
+
A design review (#343): three rounds of standalone prototypes, built from Altitude's tokens, compared ways for an imprint to show it's waiting and ways for content to land. `checks/loading-breath.mjs` drives the result: imprints waiting, a card from one request, a card whose parts arrive apart, a summary before its detail, a list, a later arrival, a ten-second wait, Nightfall, reduced motion, a chart, and the default left alone.
|
|
1483
|
+
|
|
1484
|
+
- **Breath, not ripples.** Ripples, ribs, a wave, a lit membrane and a bump under the surface all read as the ground moving, but they move a pattern across the screen, which costs attention and is the kind of large-area motion that troubles motion-sensitive people. A ring from a point reads as Material's touch ripple, "you pressed here". Breath changes only depth and tone.
|
|
1485
|
+
- **It keeps the light still.** The sweep moves a band of light over the imprint, which bends "the light never moves". Breath moves the ground under a fixed light, which is what Altitude means by depth.
|
|
1486
|
+
- **One clock.** Things that change in step are grouped into one object (common fate; Usher and Donnelly, *Nature*, 1998), so imprints phased from one page clock spend one place in the attention budget. Out of step, a full view shimmers. The swell down the view keeps that coherence and gives it a direction. A CSS animation starts on the frame after its imprint commits, so imprints mounted in different commits drift by a few frames, under 5% of a breath.
|
|
1487
|
+
- **A different rhythm from pending.** A waiting imprint and a pending write are in different tiers, but at the same 1.8s rhythm they look alike. Breath is 4.8s, deepening over 40% and releasing over 60%, near a resting breath.
|
|
1488
|
+
- **The back wall in Nightfall.** Nightfall's well and trench are nearly one tone, so a floor breath almost disappears there. The back wall (`lip-top`) shows in both.
|
|
1489
|
+
- **Rise past needs a surface.** A card rises a level past its rest and settles. Text, badges and rows rest on the ground and have no surface to cast a shadow, so they fill past it, as a field does when filled. A plate is kept for content arriving into a view already showing; on a first load it would make everything look new, too close to the afterglow.
|
|
1490
|
+
- **Optional parts don't wait.** A card's undefined optional parts (description, body text) still render empty once its required parts land, so "a card's parts land together" holds for required parts and for anything that suspends.
|
|
1491
|
+
|
|
1492
|
+
Not yet tested with people: whether breath reads as loading, how long the wait feels compared with the sweep, comfort for people who report motion sensitivity, and whether arrivals are noticed. Until then it's opt-in.
|
|
1493
|
+
|
|
1494
|
+
### E136 · Editing what you asked
|
|
1495
|
+
|
|
1496
|
+
Every major harness lets a person edit an earlier message and resend it, starting a new branch while keeping the old one ("2 of 2"; assistant-ui's `EditComposer` and `BranchPicker`), but Altitude only versioned regenerated answers (#275). `MessageActions onEdit` opens the person's message in place, `InlineEdit`'s pattern for multi-line text: a Textarea well inside the sheet with Cancel and "Send edit" (secondary, so the composer's Send stays the view's one primary), Escape to cancel and Ctrl/⌘+Enter to send, and IME composition left alone. Sending hands the text to the app, which starts the branch; `versions` already had `onChange`, so the same switch moves between a person's branches and announces "Showing version 1 of 2". Focus goes back to Edit on cancel and to the composer after sending, where the new answer will be followed. Cross-fading the turns below on a switch is left to the app, which owns those turns. `checks/message-edit.mjs`.
|
|
1497
|
+
|
|
1498
|
+
### E137 · Saying the next thing while the agent works
|
|
1499
|
+
|
|
1500
|
+
An agent can take minutes, and the person thinks of the next thing meanwhile; Claude Code, Cursor and AI Elements (`Queue`) let them type it and press Enter, queue it above the composer, and send it when the turn ends (#276). In Altitude, Enter did nothing while streaming. With `PromptInput onQueue`, Enter queues instead (the button stays Stop, so Escape and the button still stop the run), the composer empties and keeps focus, and queuing is announced. `ConversationQueue` lists the queue between the thread and the composer rather than in the log, where queued messages would sit below an answer they haven't been read against and scroll away from the composer that controls them (the issue's rejected alternative). A queued message is written but not dispatched, so it doesn't hover: Altitude's pending is only for a write in flight. It takes the dashed rim used for what isn't in the record yet. `checks/prompt-queue.mjs`.
|
|
1501
|
+
|
|
1502
|
+
### E138 · Going back to before that turn
|
|
1503
|
+
|
|
1504
|
+
An agent edited six files and went the wrong way; the person wants the conversation (and, where the app supports it, the files) as they were before that turn. Claude Code's rewind and AI Elements' `Checkpoint` mark such points with a Restore action, but `Conversation` had no marker between turns and nothing said what restoring would do (#277). `ConversationCheckpoint` is a divider with a word between its lines and Restore, described by the consequence the app writes. It's the consequential-action contract applied: with real recovery (`onReturn`), Restore runs at once and leaves "Return to where you were", because asking first adds nothing when the result can be undone; without it, a stationary Dialog asks with the consequence. The write's states use the existing depth signals: pending hovers on the label, success settles once, failure sinks into a trench with Retry; outcomes are announced through the conversation's live region. The group is a labelled `group` rather than a `separator`, because a separator's children are presentational and Restore must stay a button. `checks/checkpoint.mjs`.
|
|
1505
|
+
|
|
1506
|
+
### E139 · Wayfinders: starters and follow-ups
|
|
1507
|
+
|
|
1508
|
+
The Shape of AI calls them wayfinders (Suggestions, Initial CTA, Follow up), and AI Elements has `Suggestion`; apps built them as pills with no rule for how they arrive, how high they sit, or what choosing does (#272). `Suggestions` makes each one a small secondary button: an affordance at level 1, never level 2, since the composer's Send is the view's one primary. `mode="fill"` puts the value in the composer and moves focus there with the caret at the end; `mode="send"` sends it, and says so in its name ("…, sends now"). Past four, the group becomes a toolbar with one tab stop. The issue had follow-ups wait for the answer's afterglow to end before arriving, but that afterglow is `dur-settle` (8s), and a follow-up that appears eight seconds after the answer reads as missing. So they cross-fade in over `dur-onset` as soon as they're rendered under the completed answer; the "don't rise" half of the rule stands. AI-generated follow-ups don't carry an `AILabel`: they're the product's offer, and the group's name says what they are. `checks/suggestions.mjs`.
|
|
1509
|
+
|
|
1510
|
+
### E140 · Subagents in a trace
|
|
1511
|
+
|
|
1512
|
+
Agents delegate: a coding agent starts three subagents in parallel, each with steps of its own, and the person needs "3 running", a way into each, and to know when all have finished (#278). `Trace` was one flat list with one current step. A `Trace` inside a `TraceStep` is now a nested run behind an Accordion lid that summarises it ("6 steps · 1 running"), open by default only while something in it needs attention; `TraceParallel` brackets steps that run at once and counts them in words. Stepper's single current step doesn't hold in parallel, but several raised steps don't make two meanings one level apart: each is raised because it's in play, and the count is in words. `status="auto"` derives a parent, and one decision there: a failed subagent fails its parent only once the others have stopped, since "Failed" over a step that's still working would be wrong. Nested steps announce through the outer Trace's throttled region with their run's name ("Summarise (Paddle subagent)"); failures go at once. `checks/trace-nested.mjs`.
|
|
1513
|
+
|
|
1514
|
+
### E141 · Thinking, without a shimmer
|
|
1515
|
+
|
|
1516
|
+
Reasoning models think for seconds or minutes first; harnesses say so ("Thinking…"), let the person read along, then replace it with "Thought for 12s". Some providers send only a summary or redact it. `TraceReasoning` only knew the finished state, so apps reached for a spinner or a shimmer of light across the text, both motion Altitude doesn't use for waiting (#279). `status="streaming"` reads as a streaming answer does, not yet the record: "Thinking…" on the lid and a dashed rule down the open compartment, with the text following its end until the person scrolls up (Conversation's rule). The duration is said once, when known, never as a ticking counter. `status="redacted"` says what happened instead of showing an empty lid. `checks/trace-reasoning.mjs`.
|
|
1517
|
+
|
|
1518
|
+
### E142 · A process, not a code block
|
|
1519
|
+
|
|
1520
|
+
Coding agents run commands and the person watches output arrive and then reads how it ended; AI Elements has `Terminal`, and Claude Code and Cursor show the command, streaming output, stderr, exit code, duration and Stop (#273). `CodeBlock streaming` covers code being written, but a process has state code doesn't, so `Terminal` is its own component in `CodeBlock`'s frame rather than a `kind` of it. Running borrows the streaming dashed rule ("not yet the record"); following the end and "Jump to latest output" are `Conversation`'s; the exit is always said in words, with a critical rule only when non-zero. The issue gated ANSI colour on contrast: measured on `surface-ground`, `critical`, `caution` and `positive` are at least 5.31:1 in every scheme (daylight `positive` 5.31, mesa `positive` 5.48, nightfall `critical` 7.48, alpine `critical` 8.02), so red, yellow and green may keep their meaning as text. The log region isn't live, so output isn't read line by line; completion is announced once. A carriage return overwrites its line, so progress bars settle on their final state. `checks/terminal.mjs`.
|
|
1521
|
+
|
|
1522
|
+
### E143 · When the agent needs to ask
|
|
1523
|
+
|
|
1524
|
+
Agents stop to ask ("Which environment?", "Keep the column or drop it?"), and the person picks, or writes their own answer: MCP elicitation, Claude Code's AskUserQuestion, AI Elements' `Question` (#270). `Confirmation` only approves or rejects. `Question` takes Confirmation's reading: nothing has been dispatched, so it's raised, not pending, with no default and no focus taken. Options use the selection family (radio or checkbox trenches). Answered, it becomes one muted line in the record, like a resolved thread. `QuestionSet` answers several questions with one button (AskUserQuestion allows four). Building it found a bug in the selection family: radios in a group with nothing chosen match `:indeterminate`, which the checkbox's dash style also used, so every radio drew filled until one was picked; the rule is now for checkboxes only. Deferred: the issue proposed making the composer's Send secondary while a question is open (one primary per view). That changes `PromptInput` and `Confirmation` together, so it's left as its own change. `checks/question.mjs`.
|
|
1525
|
+
|
|
1526
|
+
### E144 · Approve once, for the session, or always
|
|
1527
|
+
|
|
1528
|
+
Approving `npm test` for the fifth time trains people to approve without reading, and never asking is unsafe; Claude Code's permission prompts and Cursor's allowlist offer a scope (#280). `Confirmation scopes` turns Approve into a `SplitButton` (an action with alternatives): the face approves once, the menu holds the wider scopes with their descriptions, and the scope is recorded in the resolved line and the announcement, because "allowed for this session" differs from "approved once". Scopes are ignored with `escalate`: whatever needs a hold or a dialog is consequential enough never to be pre-approved. No new elevation: the SplitButton and its menu keep their levels. `checks/confirm-scopes.mjs`.
|
|
1529
|
+
|
|
1530
|
+
### E145 · Fix the subject, then approve
|
|
1531
|
+
|
|
1532
|
+
A proposal that's nearly right ("Send campaign email" with a subject one word off) could only be approved as is or rejected and explained in the composer, which costs a turn; harnesses increasingly let the person edit the arguments and approve the edited version, telling the agent what changed (#281). `Confirmation edit` opens the fields in place inside the raised card (EditRegion's pattern: the padding is already there, so nothing jumps), focusing the first. The app keeps `consequence` and `approveLabel` following the values, so the description read with Approve is always what will happen. Escape cancels the edit, not the proposal, and Reject stays available while editing. The record says "edited and approved by you". `checks/confirm-edit.mjs`.
|
|
1533
|
+
|
|
1534
|
+
### E146 · Accept this hunk, reject that one
|
|
1535
|
+
|
|
1536
|
+
An agent changes a file and the person reviews it before it's kept: accept this hunk, reject that one, accept the rest (Cursor, Copilot Edits, Claude Code's IDE integrations), and apps embedding editing agents need the same for documents (#282). `DiffView` was read only. `review` follows `PropertyList proposed`: a hunk awaiting a decision is a proposal, so it's raised around its lines; once decided it's the record again, accepted flush with an afterglow, rejected muted and collapsed after `dur-linger` with a way to show it. Decisions are controlled by the app, which owns applying them; changes have stable ids in order (`diffChanges`). Navigation keys skip decided changes. The rejected alternative, reviewing whole files only, forces all-or-nothing on a file that's mostly right; it remains the composition for many files (an Accordion per file). Property diffs aren't reviewable yet. `checks/diff-review.mjs`.
|
|
1537
|
+
|
|
1538
|
+
### E147 · Which conversations are still working?
|
|
1539
|
+
|
|
1540
|
+
Every harness lists past conversations beside the thread, and agents that work in the background add states: still working, finished while you were away, waiting for you (#269). `Sidebar` knew destinations but not a conversation's state. `ThreadList` builds on Sidebar's parts and says each state in words in the row's name. Two height decisions: a working thread isn't pending (Conversation already says a streaming answer isn't a write in flight), so it never hovers, just a steady lamp; and a thread that needs you can't lift into a card inside a nav without breaking the list, so it's a caution plate and a word, announced once. Rename is a small field in the row rather than `InlineEdit`, whose own edit button doesn't fit a nav row; delete reuses `RemovalReceipt`. Left for later: carrying a thread to the top when it gets a message (it reorders in place), a lamp on the rail icon when collapsed, and one tab stop with arrows per group. `checks/thread-list.mjs`.
|
|
1541
|
+
|
|
1542
|
+
### E148 · A pane for what the agent makes
|
|
1543
|
+
|
|
1544
|
+
When an agent makes something substantial, harnesses show it in its own pane rather than inline (Claude's artifacts, ChatGPT's canvas, v0, AI Elements' `Artifact` and `WebPreview`) with the latest version as it's written, preview and source, earlier versions and what changed (#271). `Artifact` composes what Altitude has: the pane is a `ResizablePanel` on the ground, source is `CodeBlock`, changes are `DiffView`, the switch is segmented `Tabs`. Being written takes the streaming rule; complete settles once. The issue's open question, whether viewing an earlier version is a provider `mode` or its own strip, is answered with a strip: it's scoped to one pane and needs Return and Restore beside its words, and "a mode isn't a height" holds either way. The reference in a message opens the pane and moves focus to its heading, and Escape returns there, so keyboard users aren't stranded between panes. On a phone, `ResizablePanelGroup narrow="stack"` (#265) already gives one pane at a time. Sandboxing previews stays the app's job and isn't enforced. `checks/artifact.mjs`.
|
|
1545
|
+
|
|
1546
|
+
### E149 · @ for context, / for commands
|
|
1547
|
+
|
|
1548
|
+
Every coding harness lets the person point the agent at things ("@pricing.ts", "@Q3 report", "@Maya") and run commands ("/review", "/compact") from the composer (#283); `CommentThread` already had @people. The issue asked for a plate *in* the text, which a textarea can't hold, and replacing the composer with a contenteditable editor would cost the IME, spelling and undo behaviour PromptInput gets for free. So it follows CommentThread: the text keeps "@label", and what's chosen also shows as a plate in the composer's context row, removable from either place (Backspace right after the token removes it whole). The list is Combobox's, rising above the composer because composers sit at the bottom of the screen. The first Escape closes the list and only a second stops generating, so closing a list never kills a run. Commands open only at the start of an empty composer, so a slash in a sentence or a path stays text. `checks/prompt-mentions.mjs`.
|
|
1549
|
+
|
|
1550
|
+
### E150 · Listening without a pulse
|
|
1551
|
+
|
|
1552
|
+
People talk to assistants, on phones, with busy hands, or because a long instruction is faster said; ChatGPT, Claude and AI Elements (`SpeechInput`) put a microphone in the composer (#284). The common ways to show listening, a pulsing red dot or a bouncing waveform, are motion Altitude doesn't use, and a level meter in height would put an amount in height. `PromptInputDictate` is a latched toggle instead: Toggle's "on" with "Listening" and a steady lamp. Interim words follow the suggestion rule (a ghost in `ink-faint`, never the value, so the field stays an opening); final words land at the caret. Altitude draws the states and leaves recognition to the app (the Web Speech API's support is uneven, and many apps transcribe on a server). Escape stops listening before it stops generating, the same order as closing a mention list (#283). `checks/prompt-dictate.mjs`.
|
|
1553
|
+
|
|
1554
|
+
### E151 · The conversation around the agent
|
|
1555
|
+
|
|
1556
|
+
The agent-action contract covers what an agent does; the conversation around it is where most harness bugs live: a double Enter sends twice, Stop leaves the composer stuck, a reload shows half an answer as complete, and a rate limit or a full context window arrives as "Something went wrong" (#285). The `assistant-conversation` pattern maps each turn state onto readings that already exist (PromptInput's status, Message's streaming rule, Stopped, the queue and checkpoints) and adds `useAssistantTurn`, which holds the turn as `useConsequentialAction` holds an action: a send that can't happen twice or over a turn waiting for the person, late events from a stopped or superseded stream dropped, the partial answer kept on Stop, completion only when the stream resolves, an unfinished answer after a reload shown as interrupted, the input kept when sending fails, and each error kind in words with what to do (offline, rate limited with when, context full, refused, outage). Its contracts run in `pattern-workflows.mjs` (double submit, stale stream, reload mid-stream, rate limit); the example is checked in `assistant-conversation-pattern.mjs`. Rule `ALT-CONVO-001`. Whether the wording helps people recover faster is unmeasured.
|
|
1557
|
+
|
|
1558
|
+
### E152 · Every state of an agent run, on demand
|
|
1559
|
+
|
|
1560
|
+
Altitude's AI components each have a preview, but nobody choosing a design system for an agent product sees them working together, and the states that matter most (a proposal that's raised, not pending; a streaming answer on a dashed rule; a failed step in a trench; a queued follow-up) show for a second mid-run (#286). `site-dist/agent.html` replays one scripted run, "Add annual pricing to the pricing page", through a complete harness: ThreadList, Conversation with Markdown, attachments and Suggestions, a Trace with streaming reasoning, a tool call, parallel subagents and a Terminal, a Confirmation with scopes, a Question, a queued follow-up, a DiffView to review and a checkpoint. The run is a list of events shaped like an AI SDK stream, and a moment is the fold of the events up to it, so seeking renders a state directly and identically every time; Play steps through the moments. One file, `build/fixtures/patterns/agent-harness.jsx`, is the site page's harness, the `Patterns/AgentHarness` story, and a preview rendered in both schemes in CI, with `checks/agent-harness.mjs` visiting every moment in every scheme. The rejected alternatives were a live chat (needs a key, can't reach approval or failure on demand, and isn't deterministic) and a video (goes stale, shows no keyboard or modes). Two parts of the issue aren't done: `examples/agent-harness/` against a real model (it can't be verified here without a key and a model, so it's left rather than shipped untested), and emulated forced colours on the page (the scheme, reduced motion and contrast switches are there; forced colours is checked in CI renders). Play steps moment to moment rather than token by token.
|
|
1561
|
+
|
|
1562
|
+
### E153 · A screen's frame from its task
|
|
1563
|
+
|
|
1564
|
+
Structure is where generated interfaces converge most ("Generated interfaces converge"), and the framework offered one frame: the app skeleton's sidebar, filtered table and record in a sheet, which agents copied for every screen (tell LA5, #323). The `screen-layout` pattern asks for four things before any placement (the task in a sentence, the one thing that leads, the one primary action, what comes next) and chooses one of six frames from the task: records (the skeleton), list and detail, focused editor, guided flow, decision and report. Three new frames are working fixtures, each with its design record in its first lines: a release-note editor (no sidebar; the draft fills the column with its details beside it; saving is a status), a refund decision (the question is the title; the consequence and both choices come before the evidence; the outcome replaces the choice and takes focus) and a signups report (the takeaway heads the chart that leads, on a zero-based axis; secondary numbers follow, smaller). `checks/screen-layout-pattern.mjs` asserts what the records state: the lead is first after the page header and, for the editor and report, the largest region; at most one primary action and no navigation sidebar; and each frame's focus path (24 assertions). `render-tells.mjs` finds no unled equal set or flat stack in any of them. Rule `ALT-LAYOUT-001`; LA5 moves from exposed to watch. Rejected: a pattern per frame (the choice between frames is the point, so it lives in one document), new layout components (every frame composes existing parts), and a frame switcher in one preview (it would show frames as tabs of one app, the opposite of choosing one). Not measured: whether agents given the pattern build different frames for different tasks; that is the creative-range measure in the benchmark (#322), run with and without the pattern.
|
|
1565
|
+
|
|
1566
|
+
### E154 · How far from the generated centre
|
|
1567
|
+
|
|
1568
|
+
The benchmark (#322, `evals/distinctiveness/`) measures rather than asserts how far Altitude sits from what default tools generate, with Design Theater's three homogeneity distances (UIClip, EMD between CIELCh histograms, tree edit distance between layout trees, at 1200 × 1200) and Design Creativity Bench's creative range (one task in two product domains). Four tasks (settings, records, dashboard, record view), each in two domains, were built by Claude Code (Opus 5.5) with nothing installed, with stock shadcn/ui, and with Altitude installed and set up, plus Altitude's previews: 30 runs, 21.52 USD ([report](https://github.com/richlaconte/altitude/blob/main/evals/distinctiveness/results/2026-10-31/REPORT.md)). Building with Altitude did not move screens measurably further from the centre: on UIClip, Altitude builds sit 0.0945 from the baselines' screens of the same brief, and the baselines sit 0.0937 from each other, while repeat builds of one prompt differ by 0.021–0.031. Altitude builds are closest to Altitude's previews (0.067), because agents read the guidance and start from the matching pattern fixture: the pull is toward Altitude's look, not away from the centre, and what distinguishes it (elevation in place of borders, type weights, a lead with its takeaway) was thought to be below what a 224-pixel embedding resolves (E155 tested that and rejected it). On colour, Altitude and shadcn sit 0.90 ΔE apart, the closest pair on any measure, both a neutral ground with one accent (tell CO8 stays on watch); plain Claude chose a palette per product. Layout distance barely separates systems (repeats already differ by 0.38–0.50), so task range, the measure for tell LA5, is in Altitude's favour (0.744 against 0.727 and 0.698) but within noise, and LA5 stays on watch. Limits: the baseline is only Claude (v0 and Lovable need accounts, and their builds go under `baselines/`), one sample per cell beyond the noise pairs, previews that are compact examples with less content than a built screen, layout read from the DOM rather than OmniParser, so the scales aren't the papers'. Rejected: a combined distinctiveness score (it would hide that visual, colour and layout disagree), gating CI on the numbers (generation is stochastic), and tuning the layout tree after the first results (the method was frozen after one trial brief and one change, folding repeated siblings, before the full run).
|
|
1569
|
+
|
|
1570
|
+
### E155 · Detail isn't where the distance is
|
|
1571
|
+
|
|
1572
|
+
E154 read its result as Altitude's difference being in detail a whole-screen embedding can't resolve. The benchmark gained a tile measure to test that: each 1200-pixel screen cut into a 3 × 3 grid, each 400-pixel tile embedded with UIClip at 224 (about three times closer, near its training scale), the tiles of two screens matched with the Hungarian algorithm, as Design Creativity Bench matches segments, and the matched distances averaged. On the same captures (the other measures reproduced exactly), Altitude builds sit 0.093 from the baselines' builds of the same briefs, against the baselines' 0.099 from each other and a noise floor of 0.041–0.057: no further up close than from across the room. They are nearer to shadcn's builds (0.092) than to Altitude's own previews (0.101). Only the previews stand apart (0.133), and not only because they hold less (they fill 48% of the capture against the builds' 71–97%, but cropped to their content they still sit at 0.126 against the builds' 0.097). Nor is it raised surfaces: the report, editor and records frames have none by design, and neither do their builds. Something in the previews doesn't survive into what agents build with them; Altitude's agent builds don't differ from the default at either scale (#383). Rejected: position-matched tiles (they would charge a moved panel as a difference in look) and a finer grid (a 6 × 6 tile is mostly one control or blank ground, which every system shares).
|
|
1573
|
+
|
|
1574
|
+
### E156 · Supervising many agents, not talking to one
|
|
1575
|
+
|
|
1576
|
+
The harness (E152) covers one person and one agent run; agent products now run many at once (Claude Code's agent view, GitHub's Mission Control, Cursor's Agents window, Vibe Kanban), and supervising is a different job from conversing: a small amount of attention spread across agents, most of whom don't need you ([research](https://github.com/richlaconte/altitude/blob/main/docs/research/agent-orchestration/README.md)). Supervisory-control work on several robots or aircraft finds that operators need both a pointer to what needs them and the whole set in view (Crandall et al. 2011), with observability and directability as the interface's two duties; Endsley's levels say a list of statuses is only perception, while grouping and warnings about what comes next ("Waiting on Billing API", a file two agents are changing) are comprehension and projection; interruption research (Iqbal and Horvitz 2007) says to defer and batch. Three components follow. `AgentFleet` is the roster, grouped by need (needs you, longest wait first; working, stalled first; done, folded), each row saying its state in words with its activity, wait or run time, plan step and spend; flush, the open agent selected like a table row, working on a lamp plate that never hovers, stalled a caution plate (a suspicion, not a request), conflicts a flush strip naming the file. `AgentRequests` gathers every decision waiting into one queue of raised cards, oldest first, answerable in place with secondary and ghost buttons. `AgentLanes` is a chart of where each agent's time went, flat bars on lanes with waiting-on-you hatched, whose takeaway leads with the time agents waited on the person, the bottleneck none of the surveyed products show. `build/fixtures/patterns/agent-orchestration.jsx` scripts seven agents on one job through eight moments (planned, fanned out, two need you, overlap, stalled, failed, reviewed, all done), shown on the agent page under the harness, with `checks/agent-orchestration.mjs` and `checks/agent-fleet.mjs`. Rejected: "approve all" (it stops people reading consequences), a grid of equal agent cards (tell LA4, and it hides the one that needs you), raising rows that need you (it would duplicate the queue's card and break the list), and dependency arrows between lanes. Not measured: whether people find what needs them faster with the queue than with the roster alone.
|
|
1577
|
+
|
|
1578
|
+
### E157 · A scroller in the page hands the wheel on
|
|
1579
|
+
|
|
1580
|
+
Every scrolling region set `overscroll-behavior: contain`, so a wheel or swipe that reached the top or bottom of a CodeBlock, ScrollArea, Table, Conversation, Terminal or streaming reasoning stopped there instead of carrying on to the page: on a long page, a reader whose pointer happened to rest over a table was stuck. Containment exists to keep a layer's scroll from moving what's beneath it, which is right for layers above the page and wrong for regions on the ground, which are part of the page. Regions on the ground now chain (the browser default); the CommandPalette list, Sheet, a Select's list and a menu shown as a bottom sheet keep `contain`, since the page under a floating layer shouldn't move while you scroll it; the Tabs strip keeps `overscroll-behavior-x: contain`, which only stops a sideways swipe from going back in history. `checks/scroll-chaining.mjs` reads every scroller's computed overscroll behaviour and wheels a real ScrollArea and Table past both ends in a page taller than the window: 13 of 13 pass, against 3 of 13 before the change. Rejected: a script that forwards the wheel at an edge (the browser already does it, with momentum, once containment is off).
|
|
1581
|
+
|
|
1582
|
+
### E158 · A split button moves as one slab
|
|
1583
|
+
|
|
1584
|
+
SplitButton drew one slab with two faces, but each face lifted alone when pointed at and pressed alone when clicked, so pointing at the chevron stood it a pixel above the action with a step in the shared wall, and pressing it sank it to the ground while the action kept its 4px wall: the slab visibly split into two pieces, and the chevron read as "sliced off". A slab is one object, so it now moves as one: pointing at either face lifts both, pressing either presses both. What a click will do still has to be shown, because the faces do different things; that's now a tint on the face under the pointer (12% of the face's ink mixed into its lifted surface), which differs without changing height. `checks/split-button.mjs` asserts the faces stay level when either is hovered or pressed and that the hovered face alone is tinted (17 pass). Rejected: two separate buttons with a gap (it reads as two actions of equal weight, which is what the slab exists to avoid), and lifting the hovered face further than the other (any step between them is the break being fixed).
|
|
1585
|
+
|
|
1586
|
+
### E159 · A phone's padding stacked, one level at a time
|
|
1587
|
+
|
|
1588
|
+
On a 375px phone the agent harness set its answer 79px in from the screen's left edge: the preview page's 24px, the AppShell Inset's 24px, the example's own 12px, the conversation's 4px and the message's 14px, each reasonable alone, none aware of the others; with Trace's 40px indent to the step's text, tool calls and approvals had about 190px. Measuring every preview at 375px (the summed side padding of each text's ancestors) found the same kind of stack across the system, from three sources: no inset ever stepped down (cards 20px, the page, dialogs and sheets 24px at every width), indents built for a desktop (Trace detail 40px), and examples adding padding inside a padded page. Vertical gaps were measured the same way and were not the problem (12–20px between components; grouping, kept). Insets now ease to `space-4` below the compact breakpoint, following the viewport the way a density does, so a narrow card in a desktop grid keeps its 20px; Trace detail drops to a 12px step in a trace under 480px wide; the harness example stops padding inside the Inset, and an assistant message no longer pads its right edge, which the conversation already does. The answer in the harness went from 249px to 289px wide; 47 previews are tighter on a phone and none got wider padding. Two faults surfaced: every Markdown answer carried 12px out of its last block (the rule removing it lost to an equally specific rule declared after it), and a DiffView in review clipped "Accept all" off its header on a phone. `checks/narrow-insets.mjs` asserts the desktop values, the phone values, that what reaches through an inset still lines up, and both faults (21 pass). Rejected: percentage padding against the containing block (it tightened a 300px card on a desktop too, which the first version of the check caught), redefining `space-*` under a media query (every gap would shrink, and gaps carry grouping), and container units for the page gutter (a page header reaching through it would read a different container than the page).
|
|
1589
|
+
|
|
1590
|
+
### E160 · Agents keep the ground; the embeddings don't see it
|
|
1591
|
+
|
|
1592
|
+
#383 asked whether Altitude's elevation reaches the apps agents build with it, since E155 found the builds no further from the generated centre than the baselines. The benchmark gained an elevation measure. It records what each character of text sits on: a raised, sunk, outlined or filled surface (from how the nearest surface of at least 120 × 60 draws), or the ground, with the Altitude tier where the tokens resolve. The 2026-10-31 builds were rebuilt from their saved source with the run's own package (every earlier measure reproduced to within 0.001) and measured again ([report](https://github.com/richlaconte/altitude/blob/main/evals/distinctiveness/results/2026-10-31/REPORT.md), *Elevation*). The premise didn't hold. Agents carry each frame's elevation from the fixture: settings raised on `elev-1` (0.87 of text in the preview, 0.72 in the builds), the report on the ground (1.00 and 0.98). Overall, Altitude builds set 78% of their text on the ground, against 8% for shadcn and 16% for plain Claude. shadcn puts 92% inside outlined boxes, plain Claude 68%, Altitude 4%. Set against the generated habit of boxing everything, the Altitude builds are the furthest of any system from the centre. Yet UIClip, whole-screen or tiled, places them nearest to shadcn. The visual distances don't register a screen's surface structure, so E154's "no further from the centre" is a statement about those embeddings, not about the structure. The gap between the previews and the builds is what remains in *Proportions*: builds hold about twice the content, more densely. Nothing in the guidance or components was changed, so the Altitude condition wasn't regenerated; a rerun would have measured the same inputs. Whether people see the difference is #326's question. Rejected: counting any `box-shadow` as raised (Tailwind's rings and Altitude's field lips compute as shadows), and comparing colours as strings (Tailwind 4 computes `oklch` and `oklab`, so they go through a canvas).
|
|
1593
|
+
|
|
1594
|
+
## What was rejected
|
|
1595
|
+
|
|
1596
|
+
- **Blur for de-emphasis.** Small blur isn't detected pre-attentively, large blur is unpleasant to read, and it destroys text contrast. Relevance uses shading polarity (raised vs sunk) instead.
|
|
1597
|
+
- **Continuous elevation.** No "priority 0–100 mapped to height". Depth is ordinal: use tiers and put the number in text.
|
|
1598
|
+
- **Light glows for height in dark schemes.** Light "shadows" lose their pre-attentive depth reading.
|
|
1599
|
+
- **Moving things to adapt.** Predicted menu items arrive sooner (ephemeral adaptation) but never change position.
|
|
1600
|
+
- **Pointer-driven parallax, tilt and lean.** Motion parallax is a strong depth cue, but continuous parallax is a vestibular trigger and adds motion that isn't tied to a change of state. Lean (a face following the pointer by up to 2px) and raised things climbing toward an approaching pointer were tried and removed: see E10. Approach survives only as an opening deepening, bounded, local and removed under reduced motion.
|
|
1601
|
+
- **Glossy lamps and accent rims on fields.** See E10.
|
|
1602
|
+
- **Keycap avatars, socket badges, severity bars.** See E8.
|
|
1603
|
+
- **Hashed identity tints and presence as three heights on avatars.** See E12.
|
|
1604
|
+
- **A stock categorical palette for charts** (Okabe-Ito, Tableau 10, shadcn's chart colours): it puts series on top of status and presence hues. See E23.
|
|
1605
|
+
- **Shadowed, walled or sheened chart marks**, "because Altitude is about height". A bar's length is already its height on screen; adding Altitude's height to it costs accuracy and says "press it". See [Charts](Charts.md).
|
|
1606
|
+
- **Hovered chart marks lifting a level**, as other interactive surfaces do. A lifted bar either leaves the ground line or casts a shadow below its top edge, and both misstate its value. Clickable marks get an outline instead.
|
|
1607
|
+
- **A fixed blocklist as the answer to the generated look.** Banned defaults are replaced by the next ones (see "Generated interfaces converge"), so `tells.json` dates every tell and is refreshed on a schedule, and the rules start from the task rather than from what to avoid.
|
|
1608
|
+
- **Read wear as altitude.** Frequent use is a different idea from "now", and raising frequently used items would make positions drift. A worn-edge treatment may suit it better; not built.
|
|
1609
|
+
|
|
1610
|
+
## How to validate with people
|
|
1611
|
+
|
|
1612
|
+
Lab measurements show the cues are *present*. These studies would show they *work*:
|
|
1613
|
+
|
|
1614
|
+
1. **Clickability (after Burmistrov et al.).** Screenshots with and without `--alt-sheen`; participants mark every pressable element. Measure misses, false alarms and time per element.
|
|
1615
|
+
2. **Change detection.** Update one of twelve cards while the participant does a distractor task. Compare time-to-notice and accuracy for afterglow, a "New" badge, and nothing.
|
|
1616
|
+
3. **Tier reading.** Flash a screen for 200ms and ask which elements are inputs, buttons or overlays, in each scheme and with the low-vision filter.
|
|
1617
|
+
4. **Pending trust.** With 200ms, 800ms and 3s server latency, compare optimistic writes with pending/landed altitude against a spinner. Measure duplicate submissions and confidence ratings.
|
|
1618
|
+
5. **Hold-to-commit.** Accidental-activation rate and completion time for destructive actions, HoldButton vs a confirmation dialog.
|
|
1619
|
+
6. **Form completion.** A long form with some fields left empty: compare how often people submit with a required field missed, and how fast they find it, when empty fields are sunk wells versus all fields looking alike.
|
|
1620
|
+
7. **Made with AI? (#326).** Altitude screens mixed with default generated screens for the same tasks. Each is rated for appeal after 50ms (after Tuch et al.), then judged "made with AI or by a designer?" after 5s, with the reasons coded against `tells.json`. The [protocol](https://github.com/richlaconte/altitude/blob/main/evals/first-impression/PROTOCOL.md) is fixed, with preregistered hypotheses, a calibration set built by two designers, and the stimuli drawn from the benchmark (`evals/first-impression/`). It hasn't been run yet.
|