Skip to content

fix(folders): rail counts sit flush right; the ⋯ menu takes their place on hover - #18

Merged
resure merged 2 commits into
resure:mainfrom
ykamendrovskiy:rail-flush-counts
Aug 18, 2026
Merged

fix(folders): rail counts sit flush right; the ⋯ menu takes their place on hover#18
resure merged 2 commits into
resure:mainfrom
ykamendrovskiy:rail-flush-counts

Conversation

@ykamendrovskiy

@ykamendrovskiy ykamendrovskiy commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

The folder rail reserves a fixed 24px trailing slot for the ⋯ menu on every row, so the note counts sit ~30px in from the rail's edge even when no menu is showing, and a long name's ellipsis stops at that empty box rather than at the digits. The rail reads airier than it needs to be, and the count column floats away from the edge it's visually anchored to.

This PR removes the reserved gutter:

  • Counts sit flush at the row padding — the trailing slot is exactly as wide as its count (nothing when there is none), so the digits line up at the rail's edge and a long name's ellipsis runs to a plain flex-gap from them.
  • The ⋯ menu takes the count's place when it shows — parked out of flow (invisible, click-through) at rest, it re-enters the flow to reveal: the slot re-sizes to the 24px button and the name re-flows to make the room. It reveals on hover ((hover: hover)-guarded, as before), on the selected row (held permanently, as the selected note-list item holds its actions), on the row's own focus (keyboard nav), while the row's menu is open, and while the ⋯ itself holds keyboard focus — Tab steps from a focused row into it, matching the note list's ⋯.
  • All Notes needs no spacer anymore — its badge is simply the row's last flex item, which lands at the same inset as every other count, and (having no menu) it never swaps out.

On revisiting 48dae41

I'm aware the fixed slot came in deliberately with 48dae41 ("Counts: stay visible on hover … no longer replaced by the ⋯ menu"), and this brings the take-the-count's-place reveal back, so let me argue the trade honestly. What 48dae41 fixed stays fixed: counts align across all rows, including All Notes — better than the pre-48dae41 layout, which reserved no slot there at all. What returns: the hovered row swaps its count for the menu, one row at a time, under the pointer that's about to use it — and the selected row holds its ⋯ permanently for cross-pane consistency, so its count yields for good. That last bit is the most debatable part (the one count you never see is the folder you're in — flagged in a code comment, challenge welcome); every unselected, unhovered row keeps its count visible. In exchange the numbers stop floating ~30px off the edge and long names keep their ellipsis next to the digits. Happy to adjust or close if you'd rather keep the reserved column.

Details

  • The parked state is position: absolute; opacity: 0; pointer-events: none — never display: none: the ⋯ is Tab-reachable now (its tabIndex={-1} is gone, matching the note list), and a Tab target that loses its box mid-hand-over gets dropped by the browser, stranding focus on body. Parked, it stays focusable and reveals the moment it holds keyboard focus.
  • The button's own reveal leg is :focus-visible, not :focus: closing the menu can strand pointer-given focus on the ⋯ — e.g. the panel divider's pointerdown calls preventDefault(), suppressing the focus move that would blur it — and a plain-focus reveal would then pin the swap after the pointer leaves (found while dragging the divider with a menu open). A stranded button just stays parked: invisible, inert, yielding on the next focus move.
  • The revealed button's right edge sits at the 8px row padding, clear of the panel divider's 4px hit strip; a small pull-in keeps the optical name-to-⋯ distance matching the name-to-count one.
  • Geometry verified live: counts at one inset on every row; the 24×24 s button vertically centered on its 30px row; the name re-ellipsizes on reveal and returns on leave; menu anchoring unchanged.
  • npm run lint / format:check / typecheck / test all green (972/972; tests added for the menu-open mark and the tab order).

🤖 Generated with Claude Code

@ykamendrovskiy ykamendrovskiy changed the title fix(folders): rail counts sit flush right; the ⋯ menu reveals beside them fix(folders): rail counts sit flush right; the ⋯ menu takes their place on hover Aug 11, 2026
@ykamendrovskiy
ykamendrovskiy force-pushed the rail-flush-counts branch 4 times, most recently from fb27a54 to 6006089 Compare August 12, 2026 09:16
…lace on hover

The trailing ⋯ slot reserved a 24px column beside every count, holding
the numbers ~30px off the rail's edge even with no menu showing, and a
long name's ellipsis stopped at that box rather than at the digits. The
slot is now exactly as wide as its count — digits sit flush at the row
padding, a plain flex-gap from the name — and on hover/focus the menu
takes the count's place: parked out of flow (invisible, click-through)
at rest, it re-enters the flow to reveal, so the slot re-sizes to the
24px button and the name re-flows to make the room. The All Notes badge
lines up as the row's last flex item, so the spacer is gone entirely.

The selected row holds its ⋯ permanently, as the selected note-list item
holds its actions — at the price of its count (a debatable trade, noted
in a comment). Beyond that, the reveal listens to the row's own focus,
this row's menu being open (a row_menu-open modifier), or keyboard focus
on the ⋯ itself — Tab steps from a focused row into its menu button, as
in the note list. The parked state is never display: none (a Tab target
that loses its box mid-hand-over gets dropped, stranding focus on body),
and the button leg uses :focus-visible, not :focus: closing the menu can
strand pointer-given focus on the button (the panel divider's
pointerdown preventDefault suppresses the blurring focus move), which
must not pin the reveal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@resure resure left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*ai generated

LGTM. It took me a while to convince myself the position static/absolute dance is safe, notes below.

The one thing I'd actually change: use position: relative instead of static in the two reveal rules. .g-button draws its hover wash, press effect and focus ring on abspos ::before/::after with inset: 0, so with the button un-positioned those anchor to .folder-rail__trailing instead — 19px wide and shifted by the -5px margin, i.e. a clipped wash and a misplaced Tab ring. The only reason it renders correctly today is that .g-button happens to carry transform: scale(1), which makes it a containing block anyway. I'd rather not depend on uikit keeping that transform around. relative with no offsets behaves exactly like static in flex layout (still in flow, the slot still resizes), and it's what .g-button sets for itself anyway.

On the selected-row-loses-its-count question — fine by me, keep it. The selected folder is the one whose contents are already listed in the middle pane, so its count is the least useful of the bunch, and consistency with the note list's held actions is worth more. Worth noting the row:focus leg means j/k browsing hides the count under the cursor too. Same trade, symmetric with hover, I just want to make sure that part is deliberate.

Things I checked and am fine with:

  • tab parity with the note list is real (its ⋯ has no tabIndex either), and this actually closes a hole: the rail menu was completely unreachable by keyboard before, Pin had no keyboard path at all. Extra tab stops per row, but that ship sailed with the note list.
  • the focus-visible-not-focus choice on the button: the stranded-pointer-focus reasoning holds, and the worst case (Enter on an invisibly focused button) just opens the menu, which reveals it via _menu-open. Self-correcting.
  • :has() — this file already uses it for the rename row, so no new browser baseline.
  • touch: the hover guard is intact, and the parked button is pointer-events: none, so first taps still hit the row.
  • menu anchoring unchanged (the anchor was already e.currentTarget), and _menu-open covers the pointer-wanders-off case the old :focus-within never did.

Nit, not blocking: an empty folder's zero-width trailing slot still costs one 6px flex gap, so its name ellipsizes 6px earlier than All Notes' does when that one has no count (it renders no trailing element at all). Invisible in practice, and still better than the 24px gutter.

Ran the suite (972 green) and typecheck on the branch myself. The two new tests are the right ones — the tab hand-over and the _menu-open lifecycle are exactly the bits that would regress.

.g-button paints its hover wash, press effect and focus ring on abspos
::before/::after with inset: 0, so an un-positioned button anchors those
to .folder-rail__trailing — 19px wide and shifted by the -5px pull-in —
i.e. a clipped wash and a misplaced Tab ring. It renders correctly today
only because uikit's own transform: scale(1) on .g-button makes it a
containing block anyway; not worth depending on. Offset-less relative is
identical to static in flex flow (the slot still re-sizes), and it's
what .g-button sets for itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ykamendrovskiy

Copy link
Copy Markdown
Contributor Author

Done in 1155575relative in both reveal rules, with the why parked in the comment block above them.

I reproduced your concern on the stand before changing anything: force static back and strip uikit's transform, and the ::before/::after box collapses to the 19px slot; with relative it stays on the 24×24 button whether the transform is there or not. Good catch — I'd been leaning on that scale(1) without knowing it.

The row:focus leg is deliberate: the focused row is the keyboard's pointer, so it makes the same trade hover makes — and it means the ⋯ that Tab is about to step into is already visible. The empty-folder 6px gap I'm also leaving as is, agreed it's invisible in practice.

Thanks for the careful read.

@resure
resure merged commit b576aac into resure:main Aug 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants