Make the rail resizable — no focus ring for a press (#4) master
* Make the rail resizable The rail was 246px and that was that — a width chosen in the comp and true for every reader, every space and every window. It is a list of project and goal *titles*, so the width that works is a property of what is in it: a space whose goals are one-line labels wants less, a space whose goals read like sentences wants more, and neither can say so. So the rail's own edge becomes a grip. It is a focusable `separator` — ARIA's window splitter, which is exactly what this is — so it works as a drag, as the arrow keys (Home and End for the limits, Enter or a double-click for the 246px it was drawn at), and it announces the width it is at. The width is furniture rather than protocol: it belongs to the browser profile, like the theme, and is remembered beside it — including in `app.html`, before first paint, because a rail that starts at 246 and jumps to what you chose is a flicker the theme already taught us to avoid. `rail.svelte.ts` owns the rules and is tested without a browser: a floor where labels stop being readable, a ceiling where a rail stops being a rail, and a pane that keeps 560px whatever was dragged or remembered. What is stored is the reader's decision and never the window's — a session spent in a narrow window draws the rail narrower without rewriting the choice, and widening the window gives it straight back. Below 860px nothing changes: a drawer over the pane is not a column beside it, so there is no edge between two things to drag and the grip is absent. Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial> * Rail grip: fix the seven review findings Seven notes on the grip, from the review of the rail-resize work. Two of them were about the gesture and are the reason a control like this wants to be seen moving rather than reasoned about. **The drag snapped to the cursor.** It set the width from `clientX - railLeft`, so pressing near either end of the 9px target and moving one pixel jumped the edge by up to 5px, and from then on the edge sat under the pointer rather than where it was picked up. It is a delta now — the X and the drawn width, both taken at `pointerdown` — which also takes the rail's own left edge out of the arithmetic, so `?frame` no longer figures in it at all. **Clicking the grip never focused it.** `preventDefault()` on `pointerdown` suppresses the compatibility `mousedown`, and focus is one of that event's default actions — so the reader most likely to want the arrow keys, the one who has just dragged the edge, was the one who could not use them. The only other way in was Tab, past every smart list and every goal row. It takes focus explicitly now and keeps the `preventDefault`, which is still what stops the gesture from beginning by selecting the label beside it. **A held arrow key wrote `localStorage` on every autorepeat.** The module argues the case against that for the drag — none of the widths in between is a decision anybody made — and thirty repeats a second is the same gesture. It settles on `keyup`, or on `blur` if focus leaves mid-repeat. **Any button and any pointer started a resize.** A right-click whose context menu swallowed the matching `pointerup` left `resizing` set, and with it a `col-resize` cursor and dead selection across the whole shell. Left button, primary pointer, or nothing happens. **The separator did not say what it separates.** ARIA's window-splitter pattern pairs the role with `aria-controls`; the pane has an id and the grip points at it. **`PANE_MIN` was enforced against the window.** It is a rule about the space the two columns share, and under `?frame` the shell is `min(1220px, 100%)` inside a 26px-padded body — up to 52px narrower, which at a 900px framed window left the pane 508px against a promised 560. The grip observes `.app` now rather than `innerWidth`, so the promise is exact whatever the frame is doing; the pre-paint script, which has no layout to measure this early, computes the same geometry from the same numbers. **The duplicated bounds had a comment and no guard.** `test/rail-bounds.test.mjs` reads `RAIL_MIN`, `RAIL_MAX` and `PANE_MIN` back out of `rail.svelte.ts`, and the frame geometry out of `app.css`, and holds `app.html` to both — so moving the ceiling in one place fails a test instead of quietly bringing back the hydration jump the script exists to prevent, for the readers who had resized. Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial> * Rail grip: no focus ring for a press The grip takes focus when it is grabbed, so that a reader who has just dragged the edge can carry on with the arrow keys instead of tabbing the length of the rail. But a scripted `focus()` is exactly the case a UA is entitled to read as keyboard-driven, so `:focus-visible` matched and the drag ended with a ring drawn down the edge of a rail somebody had just moved by pointing at it. The ring is the component's decision now. `RailResizer` keeps a `ring` flag — set from the UA's own heuristic when focus arrives (right about Tab, right about a press, wrong only about the focus the component takes itself), cleared directly after `grab` focuses, set again when an arrow, Home, End or Enter actually drives the edge. `app.css` declines the UA ring for the grip and draws it, and the accent hairline, from the `data-ring` attribute instead. `test/rail-focus-ring.test.mjs` keeps the two halves honest: the ring rule has to be keyed on the attribute the component sets, and no rule may paint the grip from `:focus-visible` alone. Co-Authored-By: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial> --------- Co-authored-by: claudebot.disnetdev.com (did:plc:n6ku5xddiuguwze3f356evla) <claudebot.disnetdev.com@noreply.radial>