utils/unified-design-system/DOCS-operability.md
Krzysztof kuhy Rudnicki a10d190b65
Some checks failed
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
design_system tests / test (push) Has been cancelled
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
web_ui tests / test (push) Has been cancelled
Add the shared markdown-naming gate and migrate utils' own docs
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
2026-08-24 15:53:12 +02:00

3.6 KiB

Pointer-free operability and screen size (required)

Split out of tokens.md, which freezes the palette and the scales. These are the behavioural requirements that use those tokens — a token file should be readable as a table of values, and these are rules with reasons attached.

Per-stack implementations of everything here are in operability-patterns.md.

Pointer-free operability (required)

Every action must be reachable and activatable with the keyboard alone. No mouse, no touchpad, no touch. This is not an accessibility nicety here: several of these apps are lockers that hold a global input grab with VT switching disabled, so a control only a pointer can reach is not an inconvenience — it is a bricked machine.

Six things must hold on every surface:

  1. No pointer-only handler. Any onTap/onLongPress/<Button-1>/click handler needs a keyboard-reachable equivalent on the same action. A bare GestureDetector and an onLongPress-only destructive action are both defects. Long-press in particular has no keyboard analogue — never make it the only path to an action.
  2. Everything interactive is a focus stop. Widgets that opt out of the focus ring (takefocus=0, which Tk's Menubutton/OptionMenu default to) are unreachable. Verify by walking the real ring, not by reading the code.
  3. Focus is visible. See the focus-ring token below — the platform default is a black ring, which vanishes on ink.
  4. Focus never lands somewhere invisible. If a container scrolls, focus entering a clipped child must scroll it into view. Clipping does not remove a widget from the focus ring, so this is the default failure, not an edge case.
  5. Scrolling is keyboard-driven. A scroll container needs PageUp/PageDown and arrow keys, not just a draggable thumb and a wheel.
  6. Confirm/submit and cancel/back have accelerators. Enter submits, Escape cancels. On Tk, note Enter does not activate a tk.Button on X11 (only Space does) and is bound to nothing on Entry/Spinbox — you must bind it.

The focus-ring token

focus-ring is #B8862E (= accent), 2px at 1px offset. It is defined in tokens.md with the rest of the palette, because scripts/palette_check.py parses that file — a token it cannot see is a token nothing compares across the four stacks.

Platform defaults are wrong on this palette: Tk ships highlightcolor="#000000" with highlightthickness=1, i.e. a black ring on ink (#211D1B) — invisible. Set focus-ring explicitly. Never highlightthickness=0 on something focusable.

⚠️ On Tk, highlightbackground is the unfocused ring and highlightcolor is the focused one. Setting only highlightbackground inverts the affordance — the widget outlines when it is not focused and goes black when it is. Set highlightcolor.

Escape hatches vs. deliberate gating

Some apps intentionally resist dismissal (an alarm you must solve a challenge to silence, a meal gate you must log a meal to clear). That gating is a feature and this rule does not override it. What the rule requires is that the sanctioned paths — solve the challenge, use the budgeted escape hatch, submit the form — are fully keyboard-operable. Making a gate keyboard-reachable is not weakening it.

Screen size (required)

Moved to screen-size.md (250-line cap): the 1366x768 / 1024x600 targets, the height-budgeting rules, and why centring unscrollable content clips symmetrically. Still part of this spec — read it for any layout work.