Skip to content

Commit 9ec7abb

Browse files
committed
fix(workflow): make notifications track panel and terminal resize live
The toast stack insets by --panel-width / --terminal-height, but a resize drag writes those to the resized subtree only (.panel-container / .terminal-container) rather than to :root, because a custom-property write on :root recalculates the whole document (~150x slower). The stack is portalled to <body>, so it shares no ancestor with either and kept reading the stale :root value — it held its pre-drag position and jumped once the drag committed, while the canvas controls, which are laid out inside the shrinking canvas, tracked the drag in realtime. useDragResize now accepts several target subtrees and writes each one, so the scoped recalc is preserved and every consumer follows the drag frame by frame. The stack is found through a new data-toast-viewport attribute. Also drops the canvas controls from bottom-4 to bottom-2: the toast clears the terminal by 8px (it anchors from the viewport, and the terminal is inset by CONTENT_WINDOW_GAP), where the controls measure from the canvas floor and so sat at twice the gap.
1 parent aa9e0c8 commit 9ec7abb

5 files changed

Lines changed: 63 additions & 13 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks/use-panel-resize.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,18 @@ function computePanelWidth(ev: PointerEvent): number {
1313
return Math.min(Math.max(newWidth, PANEL_WIDTH.MIN), maxWidth)
1414
}
1515

16-
/** The `.panel-container` element sizes itself from `--panel-width`. */
17-
function getPanelContainer(): HTMLElement | null {
18-
return document.querySelector<HTMLElement>('.panel-container')
16+
/**
17+
* Every subtree that reads `--panel-width`: the `.panel-container` the drag
18+
* resizes, and the toast stack, which insets its right edge by the same
19+
* variable but is portalled to `<body>` and so shares no ancestor with it.
20+
* Writing both keeps the notifications tracking the drag frame by frame instead
21+
* of jumping once it commits.
22+
*/
23+
function getPanelWidthConsumers(): (HTMLElement | null)[] {
24+
return [
25+
document.querySelector<HTMLElement>('.panel-container'),
26+
document.querySelector<HTMLElement>('[data-toast-viewport]'),
27+
]
1928
}
2029

2130
/**
@@ -33,7 +42,7 @@ export function usePanelResize() {
3342
return useDragResize({
3443
cursor: 'ew-resize',
3544
cssVar: '--panel-width',
36-
getTarget: getPanelContainer,
45+
getTarget: getPanelWidthConsumers,
3746
compute: computePanelWidth,
3847
commit: setPanelWidth,
3948
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks/use-terminal-resize.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,18 @@ function computeTerminalHeight(ev: PointerEvent): number {
1313
return Math.min(Math.max(newHeight, TERMINAL_HEIGHT.MIN), maxHeight)
1414
}
1515

16-
/** The `.terminal-container` element sizes itself from `--terminal-height`. */
17-
function getTerminalContainer(): HTMLElement | null {
18-
return document.querySelector<HTMLElement>('.terminal-container')
16+
/**
17+
* Every subtree that reads `--terminal-height`: the `.terminal-container` the
18+
* drag resizes, and the toast stack, which insets its bottom by the same
19+
* variable but is portalled to `<body>` and so shares no ancestor with it.
20+
* Writing both keeps the notifications tracking the drag frame by frame instead
21+
* of jumping once it commits.
22+
*/
23+
function getTerminalHeightConsumers(): (HTMLElement | null)[] {
24+
return [
25+
document.querySelector<HTMLElement>('.terminal-container'),
26+
document.querySelector<HTMLElement>('[data-toast-viewport]'),
27+
]
1928
}
2029

2130
/**
@@ -49,7 +58,7 @@ export function useTerminalResize() {
4958
return useDragResize({
5059
cursor: 'ns-resize',
5160
cssVar: '--terminal-height',
52-
getTarget: getTerminalContainer,
61+
getTarget: getTerminalHeightConsumers,
5362
compute: computeTerminalHeight,
5463
commit: setTerminalHeight,
5564
onApply: syncExpandedThreshold,

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-controls/workflow-controls.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,14 @@ export const WorkflowControls = memo(function WorkflowControls() {
9292
return (
9393
<>
9494
<div
95-
className='absolute bottom-4 left-[16px] z-10 flex h-[36px] items-center gap-0.5 rounded-lg border border-[var(--border)] bg-[var(--surface-2)] p-1'
95+
/*
96+
* `bottom-2`, not `bottom-4`, to sit the same 8px above the terminal
97+
* that the toast stack does. The toast anchors from the viewport at
98+
* `--terminal-height + 16px` while the terminal itself is inset by
99+
* CONTENT_WINDOW_GAP (8px), so its visible gap is 8 — these controls
100+
* measure from the canvas floor, where 16px read as twice the distance.
101+
*/
102+
className='absolute bottom-2 left-[16px] z-10 flex h-[36px] items-center gap-0.5 rounded-lg border border-[var(--border)] bg-[var(--surface-2)] p-1'
96103
onContextMenu={handleContextMenu}
97104
>
98105
{/* Canvas Mode Selector */}

apps/sim/hooks/use-drag-resize.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,16 @@ interface UseDragResizeOptions {
1515
* every custom-property write recalculates the whole tree (~150x slower).
1616
* Captured once on drag start; a `null` return falls back to
1717
* `document.documentElement`.
18+
*
19+
* Return an ARRAY when consumers live in sibling subtrees with no useful
20+
* common ancestor — the toast stack is portalled to `<body>`, so it shares
21+
* one only with `:root`. Writing each subtree separately keeps the scoped
22+
* recalc and, more importantly, keeps those consumers tracking the drag live;
23+
* a consumer left off this list reads the stale `:root` value and only
24+
* catches up when the drag commits. Elements that are absent (`null`) or
25+
* repeated are ignored.
1826
*/
19-
getTarget: () => HTMLElement | null
27+
getTarget: () => HTMLElement | null | (HTMLElement | null)[]
2028
/**
2129
* Maps a pointer position to the clamped target dimension, or `null` to
2230
* ignore the move. Runs at most once per animation frame (before the write,
@@ -90,7 +98,13 @@ export function useDragResize(options: UseDragResizeOptions) {
9098
const handle = e.currentTarget
9199
const pointerId = e.pointerId
92100
const { cssVar } = optionsRef.current
93-
const target = optionsRef.current.getTarget() ?? document.documentElement
101+
const resolved = optionsRef.current.getTarget()
102+
const targets = [
103+
...new Set((Array.isArray(resolved) ? resolved : [resolved]).filter((el) => el !== null)),
104+
]
105+
if (targets.length === 0) targets.push(document.documentElement)
106+
/** Liveness is judged on the primary target — the one the drag resizes. */
107+
const target = targets[0]
94108
document.body.style.cursor = optionsRef.current.cursor
95109
document.body.style.userSelect = 'none'
96110
handle.setPointerCapture?.(pointerId)
@@ -100,7 +114,7 @@ export function useDragResize(options: UseDragResizeOptions) {
100114
let lastApplied: number | null = null
101115

102116
const applyValue = (value: number) => {
103-
target.style.setProperty(cssVar, `${value}px`)
117+
for (const el of targets) el.style.setProperty(cssVar, `${value}px`)
104118
lastApplied = value
105119
optionsRef.current.onApply?.(value)
106120
}
@@ -144,7 +158,9 @@ export function useDragResize(options: UseDragResizeOptions) {
144158
}
145159
if (lastApplied !== null) {
146160
optionsRef.current.commit(lastApplied)
147-
if (target !== document.documentElement) target.style.removeProperty(cssVar)
161+
for (const el of targets) {
162+
if (el !== document.documentElement) el.style.removeProperty(cssVar)
163+
}
148164
}
149165
optionsRef.current.onEnd?.()
150166
}

packages/emcn/src/components/toast/toast.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,15 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
590590
aria-live='polite'
591591
aria-label='Notifications'
592592
data-native-surface-overlay=''
593+
/*
594+
* The stack is portalled to `<body>`, so it shares no ancestor
595+
* with the panel or terminal it insets by. A resize drag writes
596+
* `--panel-width` / `--terminal-height` to each consuming
597+
* subtree rather than to `:root`; this attribute is how it
598+
* finds this one, and without it the stack would hold the
599+
* pre-drag position until the drag commits.
600+
*/
601+
data-toast-viewport=''
593602
className='fixed z-[var(--z-toast)] m-0 list-none p-0'
594603
exit={{
595604
opacity: 0,

0 commit comments

Comments
 (0)