Markdown across every repo had accumulated a dozen ad-hoc names (refactor_claude_todo.md x23, DESIGN_AUDIT_TODO.md, NEXT_SESSION, HANDOFF, PLAN, SPEC) with nothing distinguishing a live task from a finished record. An audit found docs asserting work was pending that shipped months earlier, and one asserting completion while the work was open. Four namespaces now: README.md, CLAUDE*.md, DOCS*.md, TODO*.md. Task files carry the literal "REMOVE ME AFTER FINISH" and are deleted when the work lands. Enforced bidirectionally -- a TODO without the marker fails, AND any file carrying the marker that is not named TODO*.md fails. Without the reverse rule a task can be renamed into the permanent namespace and quietly live forever, which is the failure being fixed. md_naming/ mirrors the file_length topology and imports its tables (EXCLUDED_DIRS, THIRD_PARTY_REPOS, VENDORED_ANYWHERE, GENERATED_PATTERN) rather than restating them, so the two gates cannot disagree about the same file. 71 tests, 100% branch coverage. stale.py warns on TODOs past 90 days but never fails: no machine can prove a task is finished, so the honest mechanism is a nudge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
4.3 KiB
Pointer-free + small-screen: per-stack patterns
Split out of 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 in Flutter, on
the web, and in Python/Tkinter.
The rules themselves are in 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
// 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:
: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:
# 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.Buttonbinds<space>on X11 but not<Return>;Entry/Spinboxbind<Return>to nothing. Bind<Return>yourself for submit.tk.Texttraps<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.Canvashas 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/Menubuttondefault totakefocus=0. On lock surfaces they are banned anyway (a posted menu steals the grab); use radio buttons.ttk.Notebookneeds an explicitenable_traversal()forCtrl+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:
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.