Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ function computePanelWidth(ev: PointerEvent): number {
return Math.min(Math.max(newWidth, PANEL_WIDTH.MIN), maxWidth)
}

/**
* Applies the panel width per frame. The `--panel-width` CSS variable alone
* sizes `.panel-container`, so no React work happens during the drag.
*/
function applyPanelWidth(width: number): void {
document.documentElement.style.setProperty('--panel-width', `${width}px`)
/** The `.panel-container` element sizes itself from `--panel-width`. */
function getPanelContainer(): HTMLElement | null {
return document.querySelector<HTMLElement>('.panel-container')
}

/**
* Handles panel drag-resize with zero React renders during the drag.
* The final width is committed to the store (one re-render + one
* localStorage write) when the drag ends.
* Handles panel drag-resize with zero React renders during the drag. The
* `--panel-width` variable is written to `.panel-container` (a scoped style
* recalc) rather than `:root` (a whole-document recalc), and the final width
* is committed to the store (one re-render + one localStorage write) when the
* drag ends.
*
* @returns Pointer-down handler for the resize handle
*/
Expand All @@ -33,8 +32,9 @@ export function usePanelResize() {

return useDragResize({
cursor: 'ew-resize',
cssVar: '--panel-width',
getTarget: getPanelContainer,
compute: computePanelWidth,
apply: applyPanelWidth,
commit: setPanelWidth,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,25 @@ import { useDragResize } from '@/hooks/use-drag-resize'
import { OUTPUT_PANEL_WIDTH, TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants'
import { useTerminalStore } from '@/stores/terminal'

/**
* Applies the output panel width per frame. The `--output-panel-width` CSS
* variable alone sizes the output panel and its sibling logs column, so no
* React work happens during the drag.
*/
function applyOutputPanelWidth(width: number): void {
document.documentElement.style.setProperty('--output-panel-width', `${width}px`)
}

/**
* Handles the terminal output panel drag-resize with zero React renders
* during the drag. The terminal element is captured once on drag start and
* its rect is re-read per frame (rAF-aligned, before the CSS-var write), so
* the clamp stays correct even if the terminal resizes mid-drag. The final
* width is committed to the store (one re-render + one localStorage write)
* when the drag ends.
* during the drag. `--output-panel-width` is written to `.terminal-container`
* (which both the output panel and its sibling logs column inherit from) — a
* scoped style recalc rather than a whole-document one on `:root`. The
* terminal rect is re-read per frame (rAF-aligned, before the write), so the
* clamp stays correct even if the terminal resizes mid-drag. The final width
* is committed to the store (one re-render + one localStorage write) when the
* drag ends.
*
* @returns Pointer-down handler for the resize handle
*/
export function useOutputPanelResize() {
const setOutputPanelWidth = useTerminalStore((s) => s.setOutputPanelWidth)
const terminalElRef = useRef<Element | null>(null)
const terminalElRef = useRef<HTMLElement | null>(null)

const captureTerminalElement = useCallback(() => {
terminalElRef.current = document.querySelector('[aria-label="Terminal"]')
return terminalElRef.current !== null
const getTerminalElement = useCallback(() => {
terminalElRef.current = document.querySelector<HTMLElement>('.terminal-container')
return terminalElRef.current
}, [])

const computeOutputPanelWidth = useCallback((ev: PointerEvent) => {
Expand All @@ -42,9 +35,9 @@ export function useOutputPanelResize() {

return useDragResize({
cursor: 'ew-resize',
cssVar: '--output-panel-width',
getTarget: getTerminalElement,
Comment thread
waleedlatif1 marked this conversation as resolved.
compute: computeOutputPanelWidth,
apply: applyOutputPanelWidth,
commit: setOutputPanelWidth,
onStart: captureTerminalElement,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,33 @@ function computeTerminalHeight(ev: PointerEvent): number {
return Math.min(Math.max(newHeight, TERMINAL_HEIGHT.MIN), maxHeight)
}

/** The `.terminal-container` element sizes itself from `--terminal-height`. */
function getTerminalContainer(): HTMLElement | null {
return document.querySelector<HTMLElement>('.terminal-container')
}

/**
* Applies the terminal height per frame. The `--terminal-height` CSS
* variable alone sizes `.terminal-container`, so no React work happens on
* ordinary frames. The store is committed mid-drag only when the height
* crosses the expanded threshold, so `isExpanded` subscribers (header
* chevron, auto-open logic) still flip live during the drag.
* Updates the store height mid-drag only when it crosses the expanded
* threshold, so `isExpanded` subscribers (header chevron, auto-open logic)
* still flip live during the drag. Writes store state directly rather than
* calling `setTerminalHeight` so it does not also write `--terminal-height` to
* `:root` (a whole-document recalc) — the scoped CSS-var write handled by
* {@link useDragResize} already drives the visual, and the final value is
* persisted through `setTerminalHeight` on release.
*/
function applyTerminalHeight(height: number): void {
document.documentElement.style.setProperty('--terminal-height', `${height}px`)

const store = useTerminalStore.getState()
const wasExpanded = store.terminalHeight > TERMINAL_CONFIG.NEAR_MIN_THRESHOLD
function syncExpandedThreshold(height: number): void {
const wasExpanded =
useTerminalStore.getState().terminalHeight > TERMINAL_CONFIG.NEAR_MIN_THRESHOLD
const nowExpanded = height > TERMINAL_CONFIG.NEAR_MIN_THRESHOLD
if (wasExpanded !== nowExpanded) store.setTerminalHeight(height)
if (wasExpanded !== nowExpanded) useTerminalStore.setState({ terminalHeight: height })
}

/**
* Handles terminal drag-resize with zero React renders during the drag
* (except at expanded-threshold crossings). The final height is committed
* to the store (one re-render + one localStorage write) when the drag ends.
* (except at expanded-threshold crossings). The `--terminal-height` variable
* is written to `.terminal-container` (a scoped style recalc) rather than
* `:root` (a whole-document recalc), and the final height is committed to the
* store (one re-render + one localStorage write) when the drag ends.
*
* @returns Pointer-down handler for the resize handle
*/
Expand All @@ -41,8 +48,10 @@ export function useTerminalResize() {

return useDragResize({
cursor: 'ns-resize',
cssVar: '--terminal-height',
getTarget: getTerminalContainer,
compute: computeTerminalHeight,
apply: applyTerminalHeight,
commit: setTerminalHeight,
onApply: syncExpandedThreshold,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -1255,10 +1255,15 @@ export const Terminal = memo(function Terminal() {
/**
* Adjust output panel width on resize.
* Closes the output panel if there's not enough space for the minimum width.
* The clamp compares against the live `--output-panel-width` CSS variable
* (the visual width — during a drag it runs ahead of the store, which only
* commits on release) so a resize mid-drag can never stomp the drag with a
* stale store value; the store value is the fallback before any write.
*
* An active output-panel drag owns clamping — its own compute clamps every
* frame against the live terminal rect, and its scoped inline override on
* `.terminal-container` (present only while that drag runs) would mask a
* store-driven `:root` write here anyway — so this skips while it is active.
* Otherwise the width is read from the committed `--output-panel-width` on
* `:root`, which the store setter writes synchronously and is therefore
* fresher than the React store value (a render behind the commit), falling
* back to the store value before any commit exists.
*/
useEffect(() => {
const el = terminalRef.current
Expand All @@ -1267,6 +1272,8 @@ export const Terminal = memo(function Terminal() {
const handleResize = () => {
if (!selectedEntry) return

if (el.style.getPropertyValue('--output-panel-width')) return

const maxWidth = el.getBoundingClientRect().width - TERMINAL_CONFIG.BLOCK_COLUMN_WIDTH_PX

if (maxWidth < MIN_OUTPUT_PANEL_WIDTH_PX) {
Expand All @@ -1275,10 +1282,10 @@ export const Terminal = memo(function Terminal() {
return
}

const liveWidth = Number.parseFloat(
const committed = Number.parseFloat(
document.documentElement.style.getPropertyValue('--output-panel-width')
)
const currentWidth = Number.isNaN(liveWidth) ? outputPanelWidth : liveWidth
const currentWidth = Number.isNaN(committed) ? outputPanelWidth : committed
if (currentWidth > maxWidth) {
setOutputPanelWidth(Math.max(maxWidth, MIN_OUTPUT_PANEL_WIDTH_PX))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,29 @@ interface UseFloatBoundarySyncProps {
const CONTENT_WINDOW_GAP = 8

/**
* Hook to synchronize floats position with layout boundary changes.
* Keeps the float within bounds when sidebar, panel, or terminal resize.
* Uses requestAnimationFrame for smooth real-time updates
* Layout dimensions a float must stay clear of. During a resize drag the live
* value is an inline override on the consuming element (a scoped style recalc);
* at rest it lives on `:root` (committed via the store / pre-hydration script).
* Each entry pairs the element the drag writes to with its variable so the
* float tracks the drag live and re-clamps at rest.
*/
const BOUNDARY_DIMENSIONS = [
{ selector: '.sidebar-shell-outer', cssVar: '--sidebar-width' },
{ selector: '.panel-container', cssVar: '--panel-width' },
{ selector: '.terminal-container', cssVar: '--terminal-height' },
] as const

/** Reads a boundary dimension, preferring the drag's scoped inline override. */
function readBoundaryDimension(selector: string, cssVar: string): number {
const inline = document.querySelector<HTMLElement>(selector)?.style.getPropertyValue(cssVar)
const value = inline || getComputedStyle(document.documentElement).getPropertyValue(cssVar)
return Number.parseInt(value || '0')
}

/**
* Hook to synchronize a float's position with layout boundary changes.
* Keeps the float within bounds when the sidebar, panel, or terminal resize.
* Uses requestAnimationFrame for smooth real-time updates.
*/
export function useFloatBoundarySync({
isOpen,
Expand All @@ -30,15 +50,9 @@ export function useFloatBoundarySync({
positionRef.current = position

const checkAndUpdatePosition = useCallback(() => {
const sidebarWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width') || '0'
)
const panelWidth = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--panel-width') || '0'
)
const terminalHeight = Number.parseInt(
getComputedStyle(document.documentElement).getPropertyValue('--terminal-height') || '0'
)
const sidebarWidth = readBoundaryDimension('.sidebar-shell-outer', '--sidebar-width')
const panelWidth = readBoundaryDimension('.panel-container', '--panel-width')
const terminalHeight = readBoundaryDimension('.terminal-container', '--terminal-height')

const prev = previousDimensionsRef.current
if (
Expand Down Expand Up @@ -83,11 +97,17 @@ export function useFloatBoundarySync({

window.addEventListener('resize', handleResize)

/**
* Watch both `:root` (at-rest commits, the pre-hydration script) and each
* container the resize hooks write to mid-drag, so the float re-clamps live
* throughout a drag rather than only on release.
*/
const observer = new MutationObserver(handleResize)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style'],
})
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] })
for (const { selector } of BOUNDARY_DIMENSIONS) {
const el = document.querySelector(selector)
if (el) observer.observe(el, { attributes: true, attributeFilter: ['style'] })
}

checkAndUpdatePosition()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,55 @@ import { useSidebarStore } from '@/stores/sidebar/store'
* add `is-resizing` class directly to the DOM (no React
* round-trip, so the CSS width transition is suppressed from the
* very first frame)
* pointermove → write to --sidebar-width inside a requestAnimationFrame
* callback (aligns work with the browser paint cycle)
* pointermove → write --sidebar-width to `.sidebar-shell-outer` (the element
* that sizes the rail) inside a requestAnimationFrame callback.
* Scoping the variable to that subtree keeps the style recalc
* local; writing it to `:root` instead forces a whole-document
* recalc (~150x slower on a large canvas).
* pointerup → cancel any pending RAF, tear down, persist final width to
* Zustand once (one React re-render to save to localStorage)
* Zustand once (writes the authoritative `:root` value for
* on-demand readers), then drop the scoped override
*
* The drag is torn down by `pointerup`, `pointercancel`, or window `blur`, so an
* interrupted gesture (release outside the window, alt-tab, context menu, the OS
* stealing focus) can never leave the `is-resizing` / `sidebar-resizing` classes
* stuck — which would otherwise freeze the sidebar at a tiny width with the
* collapse transition permanently disabled. A single-flight guard prevents
* stacking listeners across rapid presses, and an unmount cleanup tears down a
* drag still in flight when the sidebar unmounts (e.g. route change).
* stacking listeners across rapid presses, and unmounting mid-drag finalizes it
* the same way a release does — persisting the last width and dropping the
* scoped override — which matters because `.sidebar-shell-outer` lives in the
* workspace chrome and outlives the sidebar, so a stranded override would
* otherwise win over the committed `:root` value.
*/
export function useSidebarResize() {
const setSidebarWidth = useSidebarStore((s) => s.setSidebarWidth)
const cleanupRef = useRef<(() => void) | null>(null)
const teardownRef = useRef<(() => void) | null>(null)

const handlePointerDown = useCallback(
(e: React.PointerEvent<HTMLElement>) => {
if (cleanupRef.current) return
if (teardownRef.current) return

const handle = e.currentTarget
const pointerId = e.pointerId
const sidebar = document.querySelector<HTMLElement>('.sidebar-container')
const shell = document.querySelector<HTMLElement>('.sidebar-shell-outer')
const target = shell ?? document.documentElement
sidebar?.classList.add('is-resizing')
document.documentElement.classList.add('sidebar-resizing')
document.body.style.cursor = 'ew-resize'
document.body.style.userSelect = 'none'
handle.setPointerCapture?.(pointerId)

let rafId: number | null = null
let lastWidth: number | null = null

const onPointerMove = (ev: PointerEvent) => {
const max = Math.max(SIDEBAR_WIDTH.MIN, window.innerWidth * SIDEBAR_WIDTH.MAX_PERCENTAGE)
const clamped = Math.min(Math.max(ev.clientX, SIDEBAR_WIDTH.MIN), max)
lastWidth = clamped
if (rafId !== null) cancelAnimationFrame(rafId)
rafId = requestAnimationFrame(() => {
const max = Math.max(SIDEBAR_WIDTH.MIN, window.innerWidth * SIDEBAR_WIDTH.MAX_PERCENTAGE)
const clamped = Math.min(Math.max(ev.clientX, SIDEBAR_WIDTH.MIN), max)
document.documentElement.style.setProperty('--sidebar-width', `${clamped}px`)
target.style.setProperty('--sidebar-width', `${clamped}px`)
rafId = null
})
}
Expand All @@ -68,17 +79,18 @@ export function useSidebarResize() {
document.removeEventListener('pointerup', endDrag)
document.removeEventListener('pointercancel', endDrag)
window.removeEventListener('blur', endDrag)
cleanupRef.current = null
teardownRef.current = null
}

function endDrag() {
cleanup()
const raw = document.documentElement.style.getPropertyValue('--sidebar-width')
const finalWidth = Number.parseFloat(raw)
if (!Number.isNaN(finalWidth)) setSidebarWidth(finalWidth)
if (lastWidth !== null) {
setSidebarWidth(lastWidth)
Comment thread
waleedlatif1 marked this conversation as resolved.
if (target !== document.documentElement) target.style.removeProperty('--sidebar-width')
}
Comment thread
waleedlatif1 marked this conversation as resolved.
}

cleanupRef.current = cleanup
teardownRef.current = endDrag
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', endDrag)
document.addEventListener('pointercancel', endDrag)
Expand All @@ -87,7 +99,7 @@ export function useSidebarResize() {
[setSidebarWidth]
)

useEffect(() => () => cleanupRef.current?.(), [])
useEffect(() => () => teardownRef.current?.(), [])

return { handlePointerDown }
}
Loading
Loading