fix(ui): unify resize handle cursor, lock and indicator (MUL-4911)#5570
Open
NevilleQingNY wants to merge 4 commits into
Open
fix(ui): unify resize handle cursor, lock and indicator (MUL-4911)#5570NevilleQingNY wants to merge 4 commits into
NevilleQingNY wants to merge 4 commits into
Conversation
The four resize surfaces each re-implemented the same three contracts and had drifted apart. Extract the strongest existing implementation (the sidebar rail) into a shared primitive and route the hand-written handles through it. - useResizeGesture owns pointer capture, the global cursor lock and a single teardown path covering pointerup, pointercancel, lostpointercapture, window blur, disabled-mid-drag, unmount and a throwing callback. It holds no React state, so the sidebar's direct gap/container width writes stay off the React commit path. - resizeHandleVariants is the only place a resize cursor is chosen: x/y/xy map to ew-resize/ns-resize/nwse-resize. Chat and DataTable had hardcoded col-resize/row-resize/nw-resize, which is the branch react-resizable-panels only takes on Safari. - base.css generalises the sidebar-only cursor lock to data-resize-axis, keeping the `*` + !important contract that beats descendants declaring their own cursor. Chat previously set document.body.style.cursor, so the cursor jumped back to a pointer when the drag crossed a button. - Chat gains a hover/active indicator and an 8px hit area (was 4px, and no hover feedback at all), plus the cancel paths it never had. - react-resizable-panels keeps owning its own cursor; only its indicator token is aligned (active foreground/15 -> foreground/25). DataTable is deliberately left out: its only consumer is in flight in PR #5454, which rewrites the same file. It gets aligned once that lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The four rrp panel dividers had their hover/active feedback swallowed: each call site painted its own border-r/border-l at the boundary and the separator drew a 1px indicator on top of it, so hover only re-tinted a line that was already there and read as no change. Hand the divider line to the separator, which is the element that owns the interaction: - ResizableHandle renders the resting rule itself (before:bg-border), so resting looks exactly as before, hover darkens to foreground/15 and a live drag to foreground/25. - inbox, chat page and the shared right sidebar drop their edge borders. - issue/project pass before:bg-transparent while the right sidebar is collapsed: no panel on the far side to divide from, and hover still reveals the grab hint. Verified in Chromium against the running app: the inbox divider now goes oklch(0.945) at rest -> foreground/15 on hover -> foreground/25 while dragging, with no neighbouring element painting a border (0px on both panels). react-resizable-panels keeps owning its cursor: a button inside a panel still computes ew-resize mid-drag. Covers the four panel-width surfaces left over from the first pass; with the sidebar rail, all five are now consistent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
The first pass extracted a primitive for the hand-written handles but left resizable.tsx hardcoding its own copy of the same tokens — it never imported resizeHandleVariants. The two matched only because the values were typed twice, which is the drift this issue exists to remove. - resizable.tsx now consumes resizeHandleVariants. `foreground/15`, `foreground/25` and the 8px grab zone each appear exactly once in the repo. - New variants carry the two real differences instead of a second copy: `cursor: "none"` for handles whose library already drives the cursor document-wide, and `indicator: "rule"` for a permanent panel divider vs `"line"` for a hover-only hint. The active state names both `data-resizing` and `data-separator=active`; only ever one matches, so one declaration serves both kinds of handle. - Grab zone is 8px everywhere: the sidebar rail drops 16px -> 8px. Its offsets are half the width, so -right-4 -> -right-2 and the offcanvas -right-2/-left-2 -> -right-1/-left-1 move with it; changing the width alone would park the rail beside the sidebar edge instead of on it. Measured in Chromium, not inferred: the rail is 8px spanning -4..+4 around the sidebar edge with cursor ew-resize, and the panel divider offers the same 8px grab zone over a 1px oklch(0.945) rule (= --border, unchanged at rest) while leaving the cursor to the library. Only the orientation flip stays local to resizable.tsx: the library resolves it at runtime from aria-orientation, where the variants take a compile-time axis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
…-4911) Howard's blocker: 60f4fd3 moved the resting rule from ::before to ::after, but issue-detail and project-detail kept hiding the collapsed-sidebar rule with `before:bg-transparent`. They were then making the (already invisible) hit area transparent while the rule stayed painted, so a collapsed right sidebar still showed a divider. The literal fix is to swap those two strings to `after:`, but that keeps the coupling that caused this: a caller naming the component's pseudo-element cannot be right for long. ResizableHandle takes `rule` instead, so which pseudo draws the line stays private. - resizable.tsx: `rule` (default true) selects the rule/line indicator. - issue-detail / project-detail: `rule={desktopSidebarVisualOpen}`; no caller names a pseudo-element any more. - The regression test asserts no resting background on any pseudo rather than on `after:` specifically — asserting the pseudo by name is exactly what let this through. Verified in Chromium on issue detail: open -> 320px sidebar with an oklch(0.945) rule; collapsed -> 0px sidebar and transparent on both pseudo-elements. Re-running that check against the pre-fix behaviour reproduces the bug (0px sidebar, rule still painted), so it is not vacuous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MUL-4911 — 统一 resize 交互的 cursor / 光标锁 / 指示线
四套 resize 实现各自重写了同样的三个契约并已漂移。本 PR 把其中最健壮的一版(sidebar rail)提取成共享原语,让手写的把手接入。
根因
react-resizable-panels按 UA 选光标:Chrome/Firefox 走ew-resize,其它浏览器才退到col-resize。Multica 的运行环境(Electron = Chromium、Web = Chrome/FF)一律命中ew-resize;但 chat 和 DataTable 硬编码的恰好是只有 Safari 才会用到的col-resize分支。同时它们的拖拽期只设document.body.style.cursor(无!important),压不过后代自己的cursor-pointer。改动
packages/ui/hooks/use-resize-gesture.ts(新)— 拥有 pointer capture、全局光标锁、以及单一 teardown(pointerup / pointercancel / lostpointercapture / window blur / disabled 中途 / unmount / 回调抛错)。不持有任何 React state,因此 sidebar 直写 gap/container width 的性能路径完全保留。packages/ui/components/ui/resize-handle.tsx(新)— 光标 token 的唯一收口:x/y/xy→ew-resize/ns-resize/nwse-resize。调用方从此写不出col-resize。packages/ui/styles/base.css— 把 sidebar 专属的光标锁泛化成data-resize-axis,保留*+!important契约。nw-resize→nwse-resize(角可双向缩放,单向箭头语义就是错的)。foreground/15→foreground/25)。不含 DataTable
唯一消费方在 PR #5454(MUL-4797)中,且该 PR 重写同一文件(+120/-56)。拿掉后本 PR 与 #5454 零文件重叠。等其合并后再对齐。
验证
pnpm typecheck/pnpm lint(0 error)/pnpm test(220 文件 2236 用例)全绿;新增 28 个用例。真实 Chromium 端到端实测(本地栈 + Playwright,一次性脚本已删除):
ew-resize/ns-resize/nwse-resize<button>的 cursorew-resize(此前会跳回 pointer —— 本 issue 的原始 bug)<html data-resize-axis>"x";松手后null(无泄漏)::after静置 / hover / 拖拽transparent→.../0.15→.../0.25blockedByHandle: falsesidebar-resize.test.tsx的cursor-ew-resize与stableConsumerRender断言未改动且通过 —— 即 sidebar 重构行为等价、且拖拽仍未进入 React state。待确认(视觉)
chat 的指示线按定案规格(1px
foreground/15)实现后,实测观感几乎不可见 —— 它正好落在窗口已有的ring-1 ring-surface-border上。机制确认生效(计算样式如上表),但视觉增益接近零。是否接受 / 内缩 / 加重,见 issue 内说明。🤖 Generated with Claude Code