Skip to content

feat(client): draggable usage panel with persisted position (closes #16) - #17

Open
rezon-aki wants to merge 1 commit into
Max-Samson:mainfrom
rezon-aki:pr/panel-drag
Open

rezon-aki wants to merge 1 commit into
Max-Samson:mainfrom
rezon-aki:pr/panel-drag

Conversation

@rezon-aki

Copy link
Copy Markdown
Contributor

Summary

Implements the behavior proposed in #16: the panel can now be dragged out of the way of the conversation.

  • A handle at the top of the panel moves it with a pointer drag; the offset is stored in localStorage and survives a reload.
  • Double-clicking the "Usage" button resets the panel right above the button.
  • The offset is clamped against the same fixed-position containing block that is used for the anchor (fix(client): 面板锚点按最近包含块换算(修复皮肤下偏到屏幕右侧) #14), so themed docks using transform / backdrop-filter are handled correctly; the panel always keeps at least 48px of itself on screen.
  • The movement is layered on top of the existing transform: translate(); the default anchor position and the automatic follow behavior are unchanged.

Only src/, both CHANGELOGs and tests are touched.

Verification

  • npm run verify — 50/50 (the 46 existing tests plus 4 new cases in tests/panel-position.test.mjs)
  • npm pack --dry-run
  • Clean-profile DSH installation tested when bundle, inject, slot, or route behavior changed
  • No API keys, account data, private sessions, logs, or generated lib/ files included
  • CHANGELOG.md updated for user-visible changes

Verified in a live DSH web profile (this branch link-installed, page refreshed): the drag handle, the position surviving a reload, and the double-click reset all behave as described. Happy to adjust the interaction shape (for example the automatic-avoidance alternative from the issue) if you prefer it.

- 面板顶部新增把手:pointer 拖动移动(localStorage 记忆),双击「用量」按钮复位。
- 偏移只做 transform 相对位移,边界夹取复用锚点同一个 containingBlock(),
  面板四条边至少保留 48px 在包含块内(皮肤 transform/backdrop-filter 下同样正确)。
- 新增 src/client/panel-position.ts(无 DOM 依赖的读取/夹取纯函数)与
  tests/panel-position.test.mjs;CHANGELOG 中英 [未发布] 段;
  LOCAL_PATCHES.md 记录本次并入上游 1.1.7 与补丁重放。

@Max-Samson Max-Samson 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.

Reviewed at d44f73f. The drag feature is narrowly scoped, and local verification passes all 50 tests; however, the new interaction has four reproducible correctness issues below. Please address them before merging.

Verification: isolated headless Chromium running the actual UsageIndicator and plugin CSS, with data hooks and panel contents mocked. This is component-level verification, not a full live DSH installation. npm pack --dry-run also passes.


/** 读取已持久化的偏移;缺失、坏 JSON、非有限数值一律回 {0,0}。 */
export function readPanelOffset(
storage: Pick<Storage, 'getItem'> | undefined = typeof localStorage === 'undefined' ? undefined : localStorage,

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.

[P1] Access localStorage inside the guarded function body

When browser policy denies storage access, the window.localStorage getter itself throws SecurityError. The default argument is evaluated before the function body, so the try/catch below cannot handle it. UsageIndicator calls readPanelOffset() during its useState initializer; in the Chromium reproduction the indicator does not render at all. The existing currency storage reads guard this access inside try, so this is a new regression.

Please move acquisition of the default storage into the try block and add a regression test for a throwing localStorage getter (not only a missing global or a throwing getItem).

right: rect.right - offsetRef.current.x,
bottom: rect.bottom - offsetRef.current.y,
}
final = clampPanelOffset(next, base, dragBounds())

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.

[P2] Re-clamp restored offsets when the panel opens or its bounds change

This clamp runs only through moveDrag. Restored positions, window resize, and anchor changes can therefore leave the panel entirely outside the viewport. Reproduction: at a 1200px viewport drag right and save offset {x:900,y:0}, then shrink to 400px. The panel's left edge becomes 908px, and reloading/reopening retains that off-screen position. The user cannot reach the panel or handle without knowing the double-click reset gesture.

Please validate the restored offset after the panel can be measured and re-clamp when the anchor/bounds change. Add resize and narrow-window restoration coverage.

}, [expanded])

// 卸载时兜底恢复:拖动中途组件被卸载不该把整页文字设成不可选。
useEffect(() => () => { document.body.style.userSelect = '' }, [])

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.

[P2] End the drag when the popover closes, not only when the indicator unmounts

Start dragging, press Escape while holding the pointer, then release. Escape removes the popover/handle but leaves UsageIndicator mounted, so neither endDrag nor this component-unmount cleanup restores the body style. Chromium confirms the panel is gone while document.body.style.userSelect remains 'none'; conversation text remains unselectable.

Please clear active drag state and restore the previous body userSelect value when the popover closes or pointer capture is lost, as well as on component unmount. Add an Escape-during-drag regression test.

const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max)
const minX = bounds.left - panel.right + minVisible
const maxX = bounds.right - panel.left - minVisible
const minY = bounds.top - panel.bottom + minVisible

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.

[P2] Keep the only drag handle reachable at the upper boundary

The vertical minimum allows the panel's top to move above the viewport as long as its bottom 48px remain visible. The handle is sticky inside the popover's scroll container, not to the viewport, so it moves off-screen with the panel. In Chromium, dragging upward leaves the panel at y=-371 and the handle at y=-366; after releasing the pointer the panel cannot be dragged back. Only the reset gesture recovers it.

Please constrain the handle/header to remain within the visible bounds, rather than only retaining the panel's bottom edge, and cover release/re-drag at the upper boundary.

This branch has not been deployed

No deployments
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