From ab0445149fbecd207b154d359fc34febfe4cb5b4 Mon Sep 17 00:00:00 2001 From: Endri Bezati Date: Fri, 2 Oct 2026 13:52:56 +0200 Subject: [PATCH] The hierarchy panel opens beside its button The hierarchy button moved under the chat button, at the bottom right, but its panel still opened at the top left of the editor -- the other side of the screen from what opened it. It now opens just left of the button, bottom-aligned with it, growing upward as far as the room above allows, and follows the button when the stack or the window changes (`panelPlacement`). Claude-Session: https://claude.ai/code/session_015VK7fH1c4aKbexnq2QcuKU --- .../diagram-client/src/diagram-client.css | 12 ++++-- .../diagram-client/src/hierarchy-outline.ts | 40 +++++++++++++++++++ .../test/outline-button-and-viewport.test.ts | 18 ++++++++- 3 files changed, 65 insertions(+), 5 deletions(-) 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); + }); +});