Group the message overflow menu into scannable sections (LC-598) #556
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-message-menu-grouping"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes LC-598.
The per-message
...overflow menu had grown to 15 flat rows and ran most of the viewport height, with no icons and no visual chunking. Every shipped feature had appended one more row.Presentation and interaction only: no endpoint,
hx-target,hx-swap, or permission gate changed.Grouping
Five sections separated by dividers: message (Copy link, Copy text, Forward); save and remind (Save, Pin, Remind, Mark unread); tools (Translate, Read aloud, Find related, Suggest reply, Require acknowledgement); edit; and a destructive group holding Report and Delete. Dividers only, no section labels - five labels would have added back most of the height the grouping saves.
Note the menu had 15 items, not the 13 in the original brief:
Suggest replyandReportwere also there, both rendering only on other people's messages. Suggest reply joined the tools cluster (it is LLM-backed like Translate); Report went to the destructive group above Delete, where the two are near-exclusive in practice.No submenu. A "More tools" submenu would buy about four rows at the cost of hover-intent timing, touch handling, and nested arrow-key logic. Grouping plus the placement fix below solves the height problem without that.
The separator is a
border-topon.lc-menu-grouprather than hand-placed.lc-menu-dividernodes, so a group that renders empty takes its rule with it instead of leaving a doubled or leading divider. Every member of the tools group is capability-gated, so the whole group collapses when the operator has no LLM and no embeddings, the browser has no speech synthesis, and the viewer is neither author nor room moderator.Every row gains an inline SVG icon, Delete in the danger color. The copy buttons previously swapped
textContentto show "Copied", which would have wiped the new icon, so the label now lives in its own span and only that is swapped.Placement (the substantive fix)
The menu is absolutely positioned inside
#messages, which isoverflow-y: auto. The scroll container - not the viewport - is therefore the clipping ancestor, and it capped the menu at the container height (~557px at 1400x900). A grouped menu is ~480px tall, so anchoring inside the container forced an internal scroll on almost every open, which is the opposite of "internal scroll as a last resort". Measuring againstwindow.innerHeightalso overcounted available space by ~138px (everything below the timeline), which let a downward menu get cut off mid-row.It is now promoted to
position: fixedon open and placed against the viewport, flipping upward when the row sits near the bottom and clamping into the horizontal edge - the same escapereactions.jsalready uses for the reaction picker. The CSS keeps a static downward fallback if the script never runs, and the menu closes on scroll or resize rather than being left stranded mid-air.Keyboard
The menu declared
role="menu"but its items carried notabindex="-1", so they all sat in the natural tab order and there was no arrow-key navigation - a pre-existing a11y bug. Items are now roving-tabindex with ArrowUp/ArrowDown/Home/End, a Tab focus trap, and Escape closing and restoring focus to the trigger, matching the account menu inpartials/sidebar_self.html. Capability-hidden rows drop out of the keyboard order via anoffsetParentcheck, so a server without an LLM does not leave dead stops in the sequence.Verified live
Driven against the mock-SSO dev stack with real Chrome over CDP, two identities (
devuserandalice):...trigger.just checkpasses.just testpasses excepttests/last_visited.rs::home_renders_welcome_when_no_cookieand::home_welcome_renders_quick_actions, which fail identically on cleanmainand are unrelated to this change.Follow-ups found while auditing (not fixed here, recorded on LC-598)
dm.rs:239computescan_deletefrom the global role only, ignoring per-room moderator overrides, so a room-level moderator sees no Delete in a DM the endpoint would allow. Divergent fromroom.rs.just verifycannot pass locally:dev/server-upruns--user $(id -u)but thelets-chat-rewrite-datavolume is root-owned, so SQLite panics opening the auth DB. The compose dev stack solves this with a chown entrypoint;verifynever got it. Its 30s health poll is also shorter than a coldcargobuild inside the container.Pingives no hint about the per-room pin cap the endpoint enforces, so the action can fail with a Conflict after the click.🤖 Generated with Claude Code
https://claude.ai/code/session_01P2Lh6DKB15inZTb5z8miu7