Add hover explainer panel to create menu - #5440
Conversation
Hovering (or keyboard-focusing) a row in the sidebar's create menu now opens a secondary panel beside it explaining what that block is for, with the block's "open in a new split" shortcut when it has one. The panel is not a Kobalte submenu: a SubTrigger can't also be a selectable item, and every row here has to stay clickable. It's a portaled, pointer-inert panel anchored to the active row with Floating UI, so it can't intercept clicks or trip the menu's outside-interaction dismissal, and it tracks arrow-key navigation as well as the pointer. Suppressed on touch devices, where there is nothing to hover. Copy lives in its own module rather than on CREATABLE_BLOCKS, which is shared with the command palette, mobile dock, and global hotkey table — all of which show only the terse `description`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dssuns1YheqzqZuhWyBSHk
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe change adds descriptive content for supported create-menu items. The sidebar tracks the active row and its DOM element during pointer and keyboard interaction. On non-touch devices, the menu renders a portaled explainer with the item icon, title, description, and optional alternate-hotkey guidance. Floating UI positions the panel beside the active row and adjusts it within viewport bounds. 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/web/src/features/command/sidebar/sidebar-create-menu.tsx (2)
67-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace the state-synchronization effect with a wrapped setter.
This
createEffectonly synchronizes local state fromopen(). Route every close path through asetMenuOpenhelper that clears the focused row before it callssetOpen(false). Verify thathandleOpenChange, the hotkey interceptor, andonSelectuse that helper.As per coding guidelines, “In SolidJS, avoid
createEffectexcept for synchronizing with external or imperative systems; use derived signals or wrapped setters for derived state and updates.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/features/command/sidebar/sidebar-create-menu.tsx` around lines 67 - 69, Remove the createEffect that watches open() and centralize close-state synchronization in a setMenuOpen helper that clears the focused row before calling setOpen(false). Update handleOpenChange, the hotkey interceptor, and onSelect to route their close behavior through setMenuOpen, preserving existing open behavior.Source: Coding guidelines
51-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace these cheap
createMemocalls with derived accessors.Neither lookup needs referential stability or performs expensive work.
apps/web/src/features/command/sidebar/sidebar-create-menu.tsx#L51-L51: useconst focusedBlock = () => blocks()[focusedIndex()];.apps/web/src/features/command/sidebar/create-menu-explainer.tsx#L50-L53: use a direct accessor that readsprops.block?.hotkeyTokenandCREATE_MENU_EXPLAINERS.As per coding guidelines, “Use
createMemoonly when referential stability is needed or derivation is expensive; do not memoize cheap derivations.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/features/command/sidebar/sidebar-create-menu.tsx` at line 51, The cheap createMemo derivations should be replaced with direct accessors. In apps/web/src/features/command/sidebar/sidebar-create-menu.tsx lines 51-51, update focusedBlock to read blocks()[focusedIndex()] directly; in apps/web/src/features/command/sidebar/create-menu-explainer.tsx lines 50-53, replace the memoized derivation with an accessor that reads props.block?.hotkeyToken and CREATE_MENU_EXPLAINERS.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/src/features/command/sidebar/create-menu-explainer.tsx`:
- Around line 55-75: Update the createEffect positioning flow to clear
placedPanel when props.anchor changes and invalidate any in-flight
computePosition result during cleanup. Return and invoke the autoUpdate stopper
before each rerun, and ensure stale promise callbacks cannot update el or
setPlacedPanel after cleanup; preserve the existing positioning options and
placement behavior.
---
Nitpick comments:
In `@apps/web/src/features/command/sidebar/sidebar-create-menu.tsx`:
- Around line 67-69: Remove the createEffect that watches open() and centralize
close-state synchronization in a setMenuOpen helper that clears the focused row
before calling setOpen(false). Update handleOpenChange, the hotkey interceptor,
and onSelect to route their close behavior through setMenuOpen, preserving
existing open behavior.
- Line 51: The cheap createMemo derivations should be replaced with direct
accessors. In apps/web/src/features/command/sidebar/sidebar-create-menu.tsx
lines 51-51, update focusedBlock to read blocks()[focusedIndex()] directly; in
apps/web/src/features/command/sidebar/create-menu-explainer.tsx lines 50-53,
replace the memoized derivation with an accessor that reads
props.block?.hotkeyToken and CREATE_MENU_EXPLAINERS.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: da59323e-5975-4825-89ac-fb047b08e72a
📒 Files selected for processing (3)
apps/web/src/features/command/sidebar/create-menu-explainer.tsxapps/web/src/features/command/sidebar/create-menu-explainers.tsapps/web/src/features/command/sidebar/sidebar-create-menu.tsx
| createEffect(() => { | ||
| const anchor = props.anchor; | ||
| const el = panel(); | ||
| if (!anchor || !el) return; | ||
|
|
||
| onCleanup( | ||
| autoUpdate(anchor, el, () => { | ||
| void computePosition(anchor, el, { | ||
| strategy: 'fixed', | ||
| placement: 'right-start', | ||
| middleware: [ | ||
| offset(ANCHOR_GAP_PX), | ||
| flip({ fallbackPlacements: ['left-start'] }), | ||
| shift({ padding: VIEWPORT_PADDING_PX }), | ||
| ], | ||
| }).then(({ x, y }) => { | ||
| el.style.translate = `${x}px ${y}px`; | ||
| setPlacedPanel(el); | ||
| }); | ||
| }) | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'create-menu-explainer\.tsx$' . || true
echo "== file excerpt =="
if [ -f apps/web/src/features/command/sidebar/create-menu-explainer.tsx ]; then
cat -n apps/web/src/features/command/sidebar/create-menu-explainer.tsx | sed -n '1,180p'
fi
echo "== imports/usages =="
rg -n "\b(setPlacedPanel|placedPanel|autoUpdate|computePosition|createMemo|createEffect)\b" apps/web/src/features/command/sidebar apps/web/src -g '*.ts' -g '*.tsx' || trueRepository: macro-inc/macro
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== targeted file excerpt =="
if [ -f apps/web/src/features/command/sidebar/create-menu-explainer.tsx ]; then
cat -n apps/web/src/features/command/sidebar/create-menu-explainer.tsx | sed -n '1,140p'
fi
echo "== signal and placement usages in target file =="
rg -n "\b(placedPanel|setPlacedPanel|placed|props\.anchor|createMenuExplainer|autoUpdate|computePosition|useId)\b" apps/web/src/features/command/sidebar/create-menu-explainer.tsx || true
echo "== related wrapper usages =="
rg -n -A 8 -B 8 "createMenuExplainer|MenuExplainer|explainer" apps/web/src/features/command/sidebar apps/web/src/features/command -g '*.tsx' -g '*.ts' || trueRepository: macro-inc/macro
Length of output: 41701
Reset placement and cancel stale position updates when the anchor changes.
placedPanel stays set for the same portaled panel after props.anchor changes, so the panel can remain visible at its previous position until the second computePosition promise settles. If an earlier update resolves after cleanup, it can also overwrite the new position.
Return the autoUpdate stopper and invalidate it/stale promises on cleanup before each new positioning run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/features/command/sidebar/create-menu-explainer.tsx` around lines
55 - 75, Update the createEffect positioning flow to clear placedPanel when
props.anchor changes and invalidate any in-flight computePosition result during
cleanup. Return and invoke the autoUpdate stopper before each rerun, and ensure
stale promise callbacks cannot update el or setPlacedPanel after cleanup;
preserve the existing positioning options and placement behavior.
Summary
Adds a secondary informational panel to the create menu that displays when users hover over or keyboard-navigate to menu items. The panel provides detailed descriptions of what each creatable block does, helping users understand the purpose of each option.
Key Changes
CreateMenuExplainer: A portaled panel that anchors to the currently focused/hovered menu row and displays detailed copy about that block. Uses Floating UI for intelligent positioning with fallback placement and viewport padding.CREATE_MENU_EXPLAINERSconstant mapping hotkey tokens to title and body copy for each creatable block type (email, chat, note, task, snippet, message, channel, canvas, project, code).SidebarCreateMenu:Implementation Details
pointer-events-noneto prevent interfering with menu interactionshttps://claude.ai/code/session_01Dssuns1YheqzqZuhWyBSHk