diff --git a/packages/diagram-client/src/diagram-client.css b/packages/diagram-client/src/diagram-client.css index e875b71..c94b383 100644 --- a/packages/diagram-client/src/diagram-client.css +++ b/packages/diagram-client/src/diagram-client.css @@ -3283,12 +3283,15 @@ details[open] > summary .agent-chat-expand-arrow { z-index: 1002; } +/* Opens beside its button in the floating stack (bottom right), growing + upward; hierarchy-outline.ts `panelPlacement` sets right/bottom/max-height + from the button's position. These are where it opens before it is measured. */ .workflow-hierarchy-outline { position: fixed; - top: 78px; - left: 8px; - bottom: 48px; - z-index: 30; + right: 60px; + bottom: calc(var(--workflow-wf-nav-height, 0px) + 16px); + max-height: 70vh; + z-index: 1002; width: 300px; display: flex; flex-direction: column; @@ -3339,6 +3342,7 @@ details[open] > summary .agent-chat-expand-arrow { .workflow-hierarchy-outline-body { flex: 1; + min-height: 0; /* scrolls inside the panel's max-height */ overflow: auto; padding-bottom: 6px; } diff --git a/packages/diagram-client/src/hierarchy-outline.ts b/packages/diagram-client/src/hierarchy-outline.ts index f383fe8..f0897f4 100644 --- a/packages/diagram-client/src/hierarchy-outline.ts +++ b/packages/diagram-client/src/hierarchy-outline.ts @@ -150,6 +150,7 @@ export class HierarchyOutlinePanel { const body = document.getElementById(BODY_ID); body?.addEventListener('click', event => this.onRowClick(event, false)); body?.addEventListener('dblclick', event => this.onRowClick(event, true)); + window.addEventListener('resize', () => this.placeNearButton()); document.addEventListener('keydown', event => { const target = event.target as HTMLElement | null; const typing = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable); @@ -176,6 +177,8 @@ export class HierarchyOutlinePanel { if (panel) { panel.hidden = !available || !this.open; } + // The button may have moved (the stack grows and shrinks): follow it. + requestAnimationFrame(() => this.placeNearButton()); } private setOpen(open: boolean): void { @@ -187,10 +190,28 @@ export class HierarchyOutlinePanel { } document.getElementById(TOGGLE_ID)?.classList.toggle('active', open); if (open) { + this.placeNearButton(); this.render(); } } + /** Open the panel beside its button, wherever the button stack put it. */ + private placeNearButton(): void { + const panel = document.getElementById(PANEL_ID); + const toggle = document.getElementById(TOGGLE_ID); + if (!panel || !toggle || panel.hidden || toggle.hidden) { + return; + } + const rect = toggle.getBoundingClientRect(); + if (rect.width === 0 && rect.height === 0) { + return; + } + const place = panelPlacement(rect, { width: window.innerWidth, height: window.innerHeight }); + panel.style.right = `${place.right}px`; + panel.style.bottom = `${place.bottom}px`; + panel.style.maxHeight = `${place.maxHeight}px`; + } + // ── Rows ──────────────────────────────────────────────────────────── private render(): void { @@ -362,6 +383,25 @@ export class HierarchyOutlinePanel { } } +/** Space between the button and the panel, and kept from the window's top edge. */ +const PANEL_GAP_PX = 8; + +/** + * Where the panel opens: just left of its button, bottom-aligned with it, and + * growing upward -- as tall as the room above the button allows. In the + * coordinates `position: fixed` takes (`right`/`bottom` from the window edges). + */ +export function panelPlacement( + button: { left: number; bottom: number }, + window: { width: number; height: number } +): { right: number; bottom: number; maxHeight: number } { + return { + right: Math.max(PANEL_GAP_PX, window.width - button.left + PANEL_GAP_PX), + bottom: Math.max(PANEL_GAP_PX, window.height - button.bottom), + maxHeight: Math.max(160, button.bottom - PANEL_GAP_PX * 2) + }; +} + /** * Put the hierarchy button in the floating button stack, directly under the * chat button. The stack is a reversed column -- what comes first in it sits diff --git a/packages/diagram-client/test/outline-button-and-viewport.test.ts b/packages/diagram-client/test/outline-button-and-viewport.test.ts index 969b777..82b1dd3 100644 --- a/packages/diagram-client/test/outline-button-and-viewport.test.ts +++ b/packages/diagram-client/test/outline-button-and-viewport.test.ts @@ -4,7 +4,7 @@ * instead of to the default scroll, which put the graph off-center. */ import { describe, expect, it } from 'vitest'; -import { placeInButtonStack } from '../src/hierarchy-outline'; +import { panelPlacement, placeInButtonStack } from '../src/hierarchy-outline'; import { modelViewportKey, settleViewport } from '../src/viewport-preserving-set-model-command'; /** A minimal element tree: enough for parent/sibling bookkeeping. */ @@ -112,3 +112,19 @@ describe('the viewport across a model change', () => { expect({ zoom: refreshed.zoom, scroll: refreshed.scroll }).toEqual({ zoom: 1.2, scroll: { x: 5, y: 6 } }); }); }); + +describe('the hierarchy panel', () => { + it('opens just left of its button, bottom-aligned with it, growing upward', () => { + // A 1200x800 window; the button 32px wide, 14px from the right edge, + // its bottom 16px above the window's. + const place = panelPlacement({ left: 1154, bottom: 784 }, { width: 1200, height: 800 }); + + expect(place.right).toBe(1200 - 1154 + 8); + expect(place.bottom).toBe(16); + expect(place.maxHeight).toBe(784 - 16); + }); + + it('keeps a usable height when the button sits low in a short window', () => { + expect(panelPlacement({ left: 400, bottom: 100 }, { width: 500, height: 120 }).maxHeight).toBe(160); + }); +});