feat: resizable side panels (folder rail + note list) - #15
Conversation
ReviewNice piece of work — the architecture is the right shape for this codebase: ref-based gesture state so pointermoves never re-render the editor, live widths written straight to the CSS vars with state committing only on release, Verified locally on Four things to fix before merge, then some smaller notes. 1. Stale base — the mobile single-pane layout landed after the branch pointThe branch forks at
Gate both renders on the layout that actually has resizable columns — 2. A stray click on a divider commits a width
Fix mirrors the guard the keyboard path already has: if (last !== drag.current.startWidth) onCommit(last);3. The editor-min cap teleports the panel backwards on the first move
4. Unmount mid-drag drops the commit and desyncs DOM from stateIf the divider unmounts during a drag (⌘⇧\ closes the rail, a ⌃R switch), no Smaller notes
DocsCLAUDE.md's Docs section is explicit: a new user-facing feature gets a README feature bullet; a new/changed shortcut updates
The divider chords are widget-local rather than global, so I'd leave the TestsStrong for a UI feature: 13 unit tests over the component and both pure helpers, 4 Workspace integration tests covering commit/persist/restore/reset and rail-closed. The Verdict: rebase onto 🤖 Reviewed with Claude Code |
Drag the divider on either edge — rail↔list and list↔editor — to resize the panel to its left; widths persist per workspace. Double-click resets to the defaults. The dividers are WAI-ARIA window splitters: focusable, arrow keys step the width, Home/End jump the range. One shared 160–480 range for both panels, with a drag-time cap that always leaves the editor at least 320px. The pointerdown is canceled at the root so WebKit cannot start a text selection mid-drag (and a divider click never steals focus). When the list is dragged tight (<250px), the New button folds to its icon via a container query so the sort select keeps a readable width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Gate both dividers out of the mobile single-pane layout (the rail is a drawer there and the list fills the width, so a drag could only rewrite the desktop widths sight unseen) - Don't commit a zero-movement click (it pinned the stylesheet default into localStorage as a chosen width) - The editor-room cap now floors at the current width: it stops growth but never yanks an already-wider panel back (pointer and keyboard) - Commit a mid-drag unmount (closing the rail under a held divider left an orphaned inline var with no matching state) - Keep aria-valuenow live during a drag, written next to the width var - :focus-visible uses the dedicated focus color — Tab is the only way to reach a divider, and the quiet hover shade undersold it - Round widths at the clamp boundary (no fractional persists) - Scope container-type to the toolbar — out of the overlay's intrinsic-width math; threshold rebased to the toolbar's content box - Docs: README feature bullet, shortcuts (Panels section + Mouse), architecture (per-workspace layout keys) - Tests: stray click, cap-below-current, unmount mid-drag, live aria-valuenow, no dividers when narrow Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d1b7002 to
e840c51
Compare
|
Thanks for the thorough review — I agree on each of the numbered items. All four are addressed, plus the smaller notes; rebased onto 1. Mobile. Rebased; both dividers now gate on 2. Stray click. Guarded exactly as you suggested ( 3. Backwards cap snap. The cap now floors at the current width at gesture start — growth stops, nothing yanks back. Applied to the keyboard path too: ArrowLeft otherwise overshot its 16px step to the cap, and ArrowRight/End would shrink the panel they promise to grow. Test covers both pointer and keyboard. 4. Mid-drag unmount. The Smaller notes, in order:
Docs: README feature bullet, a Panels section + Mouse line in Tests: 5 new (stray click, cap-below-current on both input paths, mid-drag unmount, live aria-valuenow, no-dividers-when-narrow) — 969 green, typecheck/format clean. One transparency note: 🤖 Generated with Claude Code |
…op identity (#16) * fix: decouple the divider's unmount-commit from the onCommit prop identity The mid-drag-unmount cleanup added in #15 tears down the live gesture (it nulls `drag`), so having `onCommit` in its dep list made correctness depend on the caller passing a referentially stable callback. Workspace happens to pass useState setters, so this is latent — but an inline lambda anywhere would break dragging on the next parent render: a premature commit, every later pointermove ignored, and `panel-resizing` stuck on <body> (endDrag returns early on a null gesture, so `dragging` never clears) leaving the whole app in col-resize/no-select. Read onCommit through a ref and key the effect on [dragging] alone. Regression test asserts a parent re-render mid-drag leaves the gesture intact; it fails on the old code at the premature-commit assertion. Also two comment corrections and a docs catch-up: - The cleanup's "⌃R workspace switch" example was inert: App keys Workspace by workspace id, so a switch unmounts the whole thing — the .workspace element (and the inline var) goes with it, and the commit lands on an unmounting component, so nothing persists and nothing needed to. ⌘⇧\ closing the rail is the real case. - Note that the pointer path freezes its editor-room floor for the gesture while the keyboard path re-tightens per keypress, so the two disagree after shrinking past the cap and coming back. - CLAUDE.md: list the two panel-width keys with the other per-workspace layout keys, and give PanelResizer an entry in the components map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: cargo fmt src-tauri (unbreak the rust CI job) `cargo fmt --check` has been red on main since the iOS work landed: the two cfg-gated `MetadataExt` imports in `is_dataless` are out of alphabetical order, which put the rust job in a failing state and so skipped its `cargo test` and `cargo clippy` steps entirely. Pure `cargo fmt` output; the two imports are mutually exclusive by cfg, so the order carries no meaning. Locally: fmt clean, 25 Rust tests pass, clippy --all-targets -D warnings clean (macOS host — CI builds Linux, where the macOS-only paths are cfg'd out). --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Takes the "Resizable left panel" line from the README backlog — both left panels, in fact: the folder rail and the note list each get a draggable divider.
What
line-generic-activeline fades in on hover (slightly delayed, so casual mouse travel doesn't flash it), instantly while dragging or focused.nsKeylocalStorage pattern; note windows stay out of it, like the rest of their transient layout.Implementation notes
--rail-width/--sidebar-widthinline on the workspace root; React state commits only on release, so there's no re-render per pointermove. They're the same vars the stylesheet already declared, so the collapse/peek overlay keeps sliding the whole sidebar as one unit, dividers included.pointerdownis canceled at the root: WebKit otherwise starts a text selection that outlives any lateruser-select: none, and a divider click must not steal focus from wherever the user is working.calc()of the same vars): WebKit underestimates the shrink-to-fit width of the absolutely-positioned flex row and painted the peek shadow narrower than the laid-out panes. With the width pinned, the shadow also tracks live drags.parsePanelWidthclamps anything read back from localStorage — its contents are user-editable and must not be able to break the layout.Testing
PanelResizer.test.tsxplusWorkspace.test.tsxadditions: drag/commit/reset flows, keyboard steps, clamping, persistence and restore, rail-closed cases, and the pointerdown-default assertion.🤖 Generated with Claude Code