fix(desktop): make panel handle toggles reliable
This commit is contained in:
1 parent
432b49d38b
commit
b8f8a3e88c
2 files changed
+65
-10
No files matched your search
@@ -127,8 +127,29 @@ test('workspace panels retain their open state and widths after relaunch', async
|
||||
|
||||
// The collapsed history handle remains available, and can bring its pane back.
|
||||
const restoredHistory = page.getByRole('separator', { name: 'Resize conversation history panel' })
|
||||
await restoredHistory.dblclick()
|
||||
const maxHistoryWidth = Number(await restoredHistory.getAttribute('aria-valuemax'))
|
||||
assert.ok(maxHistoryWidth >= 280, `history panel has no available width (${maxHistoryWidth})`)
|
||||
await page.getByRole('button', { name: 'Open conversation history' }).click()
|
||||
await page.waitForFunction(() => document.querySelector('.workspace-history-slot')?.getAttribute('data-open') === 'true')
|
||||
await page.waitForFunction((minimum) => {
|
||||
const slot = document.querySelector('.workspace-history-slot')
|
||||
return slot?.getAttribute('data-open') === 'true' && slot.getBoundingClientRect().width >= minimum
|
||||
}, Math.min(280, maxHistoryWidth))
|
||||
const restoredWidth = await page.locator('.workspace-history-slot').evaluate((slot) => slot.getBoundingClientRect().width)
|
||||
assert.ok(restoredWidth >= 280, `history panel reopened at ${restoredWidth}px`)
|
||||
await page.locator('button[aria-pressed="true"][aria-label="Close conversation history"]').click()
|
||||
await page.waitForFunction(() => document.querySelector('.workspace-history-slot')?.getAttribute('data-open') === 'false')
|
||||
const historyHandleBox = await restoredHistory.boundingBox()
|
||||
assert.ok(historyHandleBox, 'the collapsed history handle is visible')
|
||||
const historyHandleX = historyHandleBox.x + historyHandleBox.width / 2
|
||||
const historyHandleY = historyHandleBox.y + historyHandleBox.height / 2
|
||||
await page.mouse.click(historyHandleX, historyHandleY)
|
||||
await page.waitForTimeout(80)
|
||||
await page.mouse.click(historyHandleX, historyHandleY)
|
||||
await page.waitForFunction(() => {
|
||||
const slot = document.querySelector('.workspace-history-slot')
|
||||
return slot?.getAttribute('data-open') === 'true' && slot.getBoundingClientRect().width >= 280
|
||||
}, Math.min(280, maxHistoryWidth))
|
||||
} finally {
|
||||
await app?.close()
|
||||
profile.remove()
|
||||
|
||||
@@ -275,7 +275,8 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
const [historyResizing, setHistoryResizing] = useState(false)
|
||||
const [historyPanelHost, setHistoryPanelHost] = useState<HTMLDivElement | null>(null)
|
||||
const desktopStageRef = useRef<HTMLDivElement>(null)
|
||||
const historyDragRef = useRef<{ pointerId: number; startX: number; startWidth: number } | null>(null)
|
||||
const historyDragRef = useRef<{ pointerId: number; startX: number; startY: number; startWidth: number; dragging: boolean } | null>(null)
|
||||
const historyLastTapRef = useRef<{ at: number; x: number; y: number } | null>(null)
|
||||
const [topologyOpen, setTopologyOpen] = useState(savedLayout.topologyOpen)
|
||||
const [topologyRatio, setTopologyRatio] = useState(() => {
|
||||
const savedRatio = savedLayout.network / 100
|
||||
@@ -283,7 +284,8 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
})
|
||||
const [topologyResizing, setTopologyResizing] = useState(false)
|
||||
const [desktopStageWidth, setDesktopStageWidth] = useState(() => window.innerWidth)
|
||||
const topologyDragRef = useRef<{ pointerId: number; startX: number; startWidth: number; startRatio: number; startOpen: boolean } | null>(null)
|
||||
const topologyDragRef = useRef<{ pointerId: number; startX: number; startY: number; startWidth: number; startRatio: number; startOpen: boolean; dragging: boolean } | null>(null)
|
||||
const topologyLastTapRef = useRef<{ at: number; x: number; y: number } | null>(null)
|
||||
const [globeReady, setGlobeReady] = useState(false)
|
||||
const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null)
|
||||
useEffect(() => {
|
||||
@@ -328,16 +330,28 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
const topologyWidth = topologyAvailable && topologyOpen ? Math.min(panelAreaWidth * topologyRatio, topologyMaxWidth) : 0
|
||||
const startHistoryResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
const previousTap = historyLastTapRef.current
|
||||
const isDoubleTap = previousTap && Date.now() - previousTap.at < 450 && Math.hypot(event.clientX - previousTap.x, event.clientY - previousTap.y) < 12
|
||||
historyLastTapRef.current = isDoubleTap ? null : { at: Date.now(), x: event.clientX, y: event.clientY }
|
||||
if (isDoubleTap) {
|
||||
historyDragRef.current = null
|
||||
setHistoryResizing(false)
|
||||
setHistoryOpen((open) => !open)
|
||||
return
|
||||
}
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
const startWidth = historyOpen ? visibleHistoryWidth : 0
|
||||
historyDragRef.current = { pointerId: event.pointerId, startX: event.clientX, startWidth }
|
||||
if (!historyOpen) setHistoryWidth(0)
|
||||
historyDragRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startWidth, dragging: false }
|
||||
if (historyOpen) setHistoryOpen(true)
|
||||
setHistoryResizing(true)
|
||||
}
|
||||
const moveHistoryResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
const drag = historyDragRef.current
|
||||
if (!drag || drag.pointerId !== event.pointerId) return
|
||||
if (!drag.dragging) {
|
||||
if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) < 4) return
|
||||
drag.dragging = true
|
||||
setHistoryResizing(true)
|
||||
}
|
||||
const delta = (event.clientX - drag.startX) * (direction === 'rtl' ? -1 : 1)
|
||||
const rawWidth = drag.startWidth + delta
|
||||
const minimumWidth = Math.min(historyMinimumWidth, historyMaxWidth)
|
||||
@@ -370,8 +384,10 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
const finishHistoryResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
if (historyDragRef.current?.pointerId !== event.pointerId) return
|
||||
if (historyOpen && historyWidth < historyMinimumWidth) setHistoryWidth(historyMinimumWidth)
|
||||
const wasDragging = historyDragRef.current.dragging
|
||||
historyDragRef.current = null
|
||||
setHistoryResizing(false)
|
||||
if (!wasDragging) return
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
const keyHistoryResize = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||
@@ -397,13 +413,26 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
}
|
||||
const startTopologyResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault()
|
||||
const previousTap = topologyLastTapRef.current
|
||||
const isDoubleTap = previousTap && Date.now() - previousTap.at < 450 && Math.hypot(event.clientX - previousTap.x, event.clientY - previousTap.y) < 12
|
||||
topologyLastTapRef.current = isDoubleTap ? null : { at: Date.now(), x: event.clientX, y: event.clientY }
|
||||
if (isDoubleTap) {
|
||||
topologyDragRef.current = null
|
||||
setTopologyResizing(false)
|
||||
setTopologyOpen((open) => !open)
|
||||
return
|
||||
}
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
topologyDragRef.current = { pointerId: event.pointerId, startX: event.clientX, startWidth: topologyWidth, startRatio: topologyRatio, startOpen: topologyOpen }
|
||||
setTopologyResizing(true)
|
||||
topologyDragRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startWidth: topologyWidth, startRatio: topologyRatio, startOpen: topologyOpen, dragging: false }
|
||||
}
|
||||
const moveTopologyResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
const drag = topologyDragRef.current
|
||||
if (!drag || drag.pointerId !== event.pointerId) return
|
||||
if (!drag.dragging) {
|
||||
if (Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) < 4) return
|
||||
drag.dragging = true
|
||||
setTopologyResizing(true)
|
||||
}
|
||||
const delta = (event.clientX - drag.startX) * (direction === 'rtl' ? 1 : -1)
|
||||
const rawWidth = drag.startWidth + delta
|
||||
const minimumWidth = topologyMinimumWidth
|
||||
@@ -433,9 +462,14 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
const finishTopologyResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||
const drag = topologyDragRef.current
|
||||
if (!drag || drag.pointerId !== event.pointerId) return
|
||||
const wasDragging = drag.dragging
|
||||
if (topologyOpen && topologyWidth < topologyMinimumWidth && topologyWidth > 0) setTopologyRatio(Math.min(1, topologyMinimumWidth / Math.max(1, panelAreaWidth)))
|
||||
topologyDragRef.current = null
|
||||
setTopologyResizing(false)
|
||||
if (!wasDragging) {
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
return
|
||||
}
|
||||
if (topologyOpen) {
|
||||
const ratio = topologyWidth < topologyMinimumWidth && topologyWidth > 0 ? Math.min(1, topologyMinimumWidth / Math.max(1, panelAreaWidth)) : topologyRatio
|
||||
saveLayout({ chat: (1 - ratio) * 100, network: ratio * 100, topologyOpen: true })
|
||||
@@ -490,10 +524,10 @@ export function WorkspacePage({ snapshot, messages, direction, visible = true }:
|
||||
{visible && paneItems.length > 1 && createPortal(<nav className="workspace-mobile-dock" aria-label={messages.workspace}><LiquidGlassDock items={paneItems} label={messages.workspace} onNavigate={(id) => setMobileView(id as 'chat' | 'network')} /></nav>, document.body)}
|
||||
</> : <div ref={desktopStageRef} className="workspace-desktop-stage">
|
||||
<div data-open={historyOpen} data-resizing={historyResizing} aria-hidden={!historyOpen} style={{ flexBasis: historyOpen ? visibleHistoryWidth : 0 }} className="workspace-history-slot"><div ref={setHistoryPanelHost} className="workspace-history-host" /></div>
|
||||
<button type="button" role="separator" aria-orientation="vertical" aria-label={messages.resizeHistory} aria-valuemin={0} aria-valuemax={Math.round(historyMaxWidth)} aria-valuenow={Math.round(visibleHistoryWidth)} onPointerDown={startHistoryResize} onPointerMove={moveHistoryResize} onPointerUp={finishHistoryResize} onPointerCancel={finishHistoryResize} onKeyDown={keyHistoryResize} onDoubleClick={(event) => { event.preventDefault(); setHistoryOpen((open) => !open) }} className="workspace-history-resize-handle" />
|
||||
<button type="button" role="separator" aria-orientation="vertical" aria-label={messages.resizeHistory} aria-valuemin={0} aria-valuemax={Math.round(historyMaxWidth)} aria-valuenow={Math.round(visibleHistoryWidth)} onPointerDown={startHistoryResize} onPointerMove={moveHistoryResize} onPointerUp={finishHistoryResize} onPointerCancel={finishHistoryResize} onKeyDown={keyHistoryResize} className="workspace-history-resize-handle" />
|
||||
<div className="workspace-chat-desktop-pane">{chatPane}</div>
|
||||
{topologyAvailable && <>
|
||||
<button type="button" role="separator" aria-orientation="vertical" aria-label={messages.resizeTopology} aria-valuemin={0} aria-valuemax={Math.round(topologyMaxWidth)} aria-valuenow={Math.round(topologyWidth)} onPointerDown={startTopologyResize} onPointerMove={moveTopologyResize} onPointerUp={finishTopologyResize} onPointerCancel={finishTopologyResize} onKeyDown={keyTopologyResize} onDoubleClick={(event) => { event.preventDefault(); setTopologyOpen((open) => !open) }} className="workspace-topology-resize-handle"><span /></button>
|
||||
<button type="button" role="separator" aria-orientation="vertical" aria-label={messages.resizeTopology} aria-valuemin={0} aria-valuemax={Math.round(topologyMaxWidth)} aria-valuenow={Math.round(topologyWidth)} onPointerDown={startTopologyResize} onPointerMove={moveTopologyResize} onPointerUp={finishTopologyResize} onPointerCancel={finishTopologyResize} onKeyDown={keyTopologyResize} className="workspace-topology-resize-handle"><span /></button>
|
||||
<div data-open={topologyOpen} data-resizing={topologyResizing} aria-hidden={!topologyOpen} style={{ flexBasis: topologyWidth }} className="workspace-topology-slot">{networkPane}</div>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user