mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 20:14:01 +01:00
tokens.md 260 -> 219 + operability.md 69
nielsen-audit.md 258 -> 154 + shipped-record.md 122
README.md 257 -> 172 + operability-patterns.md 95
Worked the requested order rather than cutting straight to a split.
(a) Automate: the categorical ramp's four constraints were restated in prose
with the measured margins hand-copied (ΔE 34.6/26.0/25.6/27.4, etc).
ramp_check.py prints every one of those numbers and fails if any
regresses, so the prose now points at the command instead of duplicating
its output. The two consequences the numbers do not state — why the ramp
is mid-toned, and why lightness falls monotonically — stay in prose.
(b) Dedup: measured, not assumed. Zero paragraphs are shared between the
three files; README already delegates the rules to tokens.md and shows
only per-stack code. Nothing to remove.
(c/d) Split on meaning: tokens.md is "frozen values", so the behavioural
requirements that *use* those tokens moved to operability.md, with the
per-stack code for them in operability-patterns.md. nielsen-audit.md is a
live grid, so completed phases moved to shipped-record.md — following the
existing phase3-record.md convention, not a new one.
The focus-ring token row had to stay in tokens.md: palette_check.py parses
that file, and moving the row made the check fail with "MISSING md:focus-ring"
— a token it cannot see is a token nothing compares across the four stacks.
The rationale lives in operability.md and the row links to it.
palette_check and ramp_check both at 0 failing checks; every relative link
between the docs resolves.
`bash scripts/check_file_length.sh --all` now exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
96 lines
4.3 KiB
Markdown
96 lines
4.3 KiB
Markdown
# Pointer-free + small-screen: per-stack patterns
|
|
|
|
Split out of [`README.md`](README.md), which covers the palette and the
|
|
per-stack *theming* pattern. This file is the other half: how to satisfy the
|
|
operability requirements in [`operability.md`](operability.md) in Flutter, on
|
|
the web, and in Python/Tkinter.
|
|
|
|
The rules themselves are in [`tokens.md`](tokens.md) (*Pointer-free
|
|
operability*, *Screen size*). This is how to implement them per stack. Both are
|
|
machine-checked — a static lint in pre-commit plus a render test in each repo's
|
|
suite — so treat these as the shape the checks expect.
|
|
|
|
### Flutter
|
|
|
|
```dart
|
|
// Reach: never a bare GestureDetector for an action.
|
|
InkWell(onTap: select, child: cell) // focusable + Enter/Space
|
|
// A destructive action needs a focusable control, not just onLongPress:
|
|
IconButton(icon: Icon(Icons.delete), onPressed: confirmDelete)
|
|
|
|
// App-level accelerators. On the Chrome --app desktop surface, avoid chords
|
|
// Chrome reserves (Ctrl+N/T/W/Shift+N) — they never reach Flutter.
|
|
Shortcuts(shortcuts: {
|
|
SingleActivator(LogicalKeyboardKey.keyS, control: true): SaveIntent(),
|
|
SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),
|
|
}, child: Actions(actions: {...}, child: child));
|
|
|
|
// Size: budget height, clamp width.
|
|
final short = MediaQuery.sizeOf(context).height < 700; // 768 minus chrome
|
|
ConstrainedBox(constraints: BoxConstraints(maxWidth: kProseMaxWidth), child: prose);
|
|
```
|
|
|
|
Any screen whose content can grow needs a `SingleChildScrollView`/`ListView` —
|
|
a `Column` with one `Expanded` child does not scroll, it just collapses the
|
|
flexible child until it is unusable. Bottom sheets need `isScrollControlled:
|
|
true` plus an inner scroll view.
|
|
|
|
Note on **Flutter web** (todo-app and diet-guard's desktop surface are the web
|
|
build in a Chrome `--app` window, not the Linux embedder): the semantics tree is
|
|
not built unless assistive tech is detected, so `Semantics` is not a substitute
|
|
for real focusability — use focusable widgets, and treat `Semantics` as
|
|
additive.
|
|
|
|
### Web
|
|
|
|
`:focus-visible` must be styled everywhere, and never removed:
|
|
|
|
```css
|
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
/* Never `outline: none` without an equally visible replacement. */
|
|
```
|
|
|
|
Verify at the required viewports with no horizontal scroll and no *inner*
|
|
scrollbar: `1366x768` and `1024x600` at minimum. Prefer a compact baseline that
|
|
grows, over a desktop layout that must shrink.
|
|
|
|
### Python/Tkinter
|
|
|
|
Font sizes are **negative** (see `tokens.md` — positive means points and
|
|
inflates everything ~37%). The focus ring and both scales live on
|
|
`gatelock.LockConfig` so all four lockers inherit them:
|
|
|
|
```python
|
|
# focused ring — NOT highlightbackground, which is the *unfocused* ring
|
|
widget.configure(highlightcolor=cfg.focus_ring, highlightthickness=2)
|
|
widget.configure(font=(cfg.font_family, -cfg.type_body)) # negative == pixels
|
|
```
|
|
|
|
Tk specifics that are easy to get wrong, all verified against Tk 8.6's own
|
|
class bindings:
|
|
|
|
- `tk.Button` binds `<space>` on X11 but **not** `<Return>`; `Entry`/`Spinbox`
|
|
bind `<Return>` to nothing. Bind `<Return>` yourself for submit.
|
|
- `tk.Text` traps `<Tab>` (inserts a literal tab, refocuses itself) and makes
|
|
`<Shift-Tab>` a no-op. Rebind both to move focus, or the box is a dead end.
|
|
- `tk.Canvas` has no class key bindings, so a Canvas scroll viewport is not even
|
|
a focus stop. Bind `<Prior>`/`<Next>`/arrows, **and** add a `<FocusIn>` handler
|
|
on children that scrolls the focused child into view — clipping does not
|
|
unmap children, so focus walks into invisible fields by default.
|
|
- `OptionMenu`/`Menubutton` default to `takefocus=0`. On lock surfaces they are
|
|
banned anyway (a posted menu steals the grab); use radio buttons.
|
|
- `ttk.Notebook` needs an explicit `enable_traversal()` for `Ctrl+Tab` /
|
|
`Ctrl+PageDown` / `Alt`+mnemonic.
|
|
- Verify the focus ring by *walking* it (`tk_focusNext`), not by reading code.
|
|
|
|
Measure the height rather than eyeballing it — this is how the meal-gate
|
|
overflow was found, and it is what the render half of the gate does:
|
|
|
|
```bash
|
|
xvfb-run -s "-screen 0 1366x768x24" python3 -m your_pkg.tests.measure_layout
|
|
# assert frame.winfo_reqheight() <= available_pane_height
|
|
```
|
|
|
|
Measure both empty **and** fully populated (a day of meals logged, history rows
|
|
present) — the empty case is not the worst case.
|