From fa18307fd9a46b093787b5f36047bb20967726fc Mon Sep 17 00:00:00 2001 From: megaproxy Date: Mon, 25 May 2026 22:26:41 +0100 Subject: [PATCH] Tidy titlebar: dropdowns for shell + layout, '+' button to spawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Collapse the inline distro buttons + PowerShell + 🔑 SSH hosts into a single 'Ubuntu ▾' dropdown (WSL distros + PowerShell sections), with 🔑 as a separate icon-only button. - Collapse the 5 preset buttons into a 'layout ▾' dropdown. - Add a '+' button next to the shell picker that spawns a new pane of the picked shell by splitting the active pane (smart orientation: splits right if wide, down if tall). Per-pane ⇥/⇣ arrows still inherit from parent — only '+' uses the titlebar selection. - Drop the 🔔 test-toast button. - Drop overflow:hidden from titlebar + pane toolbar so dropdowns aren't clipped; height lock + nowrap still prevent the reflow bug. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.css | 32 ++---- src/App.tsx | 223 +++++++++++++++++++++++++++++------- src/lib/layout/LeafPane.css | 8 +- 3 files changed, 197 insertions(+), 66 deletions(-) diff --git a/src/App.css b/src/App.css index ef7d4f3..ac45ffe 100644 --- a/src/App.css +++ b/src/App.css @@ -20,12 +20,11 @@ text would otherwise wrap (e.g. "📡 all off") would grow the titlebar, shrink .pane-wrap, and reflow every xterm. nowrap stops text-wrap inside buttons, flex-shrink:0 stops children from being - squeezed, height locks the row height, overflow:hidden clips items - that don't fit (widen the window to see them). */ + squeezed, height locks the row height. Overflow is left visible + so dropdown menus below the chips aren't clipped by the bar. */ white-space: nowrap; height: 34px; box-sizing: border-box; - overflow: hidden; } .titlebar > * { flex-shrink: 0; @@ -35,12 +34,7 @@ color: #ddd; } -.distros, .presets { - display: flex; - gap: 4px; - align-items: center; -} -.distro-btn, .preset-btn, .palette-btn { +.titlebar-chip, .palette-btn { font: inherit; font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; font-size: 11px; @@ -51,15 +45,21 @@ padding: 2px 8px; cursor: pointer; } -.distro-btn:hover, .preset-btn:hover, .palette-btn:hover { +.titlebar-chip:hover, .palette-btn:hover { background: #2a2a2a; color: #ddd; } -.distro-btn.active { - background: #1a3a5c; +.titlebar-chip.add-pane { + font-size: 14px; + line-height: 1; + padding: 2px 8px; color: #cce6ff; border-color: #2a5a8c; } +.titlebar-chip.add-pane:hover { + background: #1a3a5c; + color: #fff; +} .palette-btn.bcast-all.on { background: #4a3010; color: #f0c060; @@ -74,14 +74,6 @@ color: #80e080; border-color: #2a6a2a; } -.preset-btn { - min-width: 28px; - text-align: center; -} -.muted { - color: #666; - font-style: italic; -} .layout-info { margin-left: auto; font-family: "Cascadia Mono", "JetBrains Mono", "Consolas", monospace; diff --git a/src/App.tsx b/src/App.tsx index 5f5a87f..4c4992d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -105,6 +105,8 @@ export default function App() { const [ready, setReady] = useState(false); const [notifications, setNotifications] = useState([]); const [paletteOpen, setPaletteOpen] = useState(false); + const [defaultShellMenuOpen, setDefaultShellMenuOpen] = useState(false); + const [layoutMenuOpen, setLayoutMenuOpen] = useState(false); // ---- non-reactive lookups ----------------------------------------------- const paneIdByLeafRef = useRef>(new Map()); @@ -249,6 +251,36 @@ export default function App() { [defaultShell, notify], ); + // Titlebar "+" — spawn a fresh pane of the picked shell, regardless of + // what the active pane is running. Splits off the active pane in + // whichever orientation leaves the new sibling closer to square. + const addPane = useCallback(() => { + const container = paneWrapRef.current; + const layout = flattenLayout(treeRef.current); + const targetId = activeLeafId ?? layout.leaves[0]?.leaf.id ?? null; + if (!targetId || !container) return; + const slot = layout.leaves.find((s) => s.leaf.id === targetId); + if (!slot) return; + + const rect = container.getBoundingClientRect(); + const paneW = slot.box.width * rect.width; + const paneH = slot.box.height * rect.height; + // Split along the longer side so both halves stay close to square. + const orientation: Orientation = paneW >= paneH ? "h" : "v"; + const childW = orientation === "h" ? paneW / 2 : paneW; + const childH = orientation === "v" ? paneH / 2 : paneH; + if (childW < MIN_PANE_PX || childH < MIN_PANE_PX) { + notify( + `Pane too small to split — would create ${Math.round(childW)}×${Math.round(childH)}px (min ${MIN_PANE_PX}px)`, + ); + return; + } + + setTree((t) => + splitLeaf(t, targetId, orientation, defaultShellAsLeafProps(defaultShell)), + ); + }, [activeLeafId, defaultShell, notify]); + const close = useCallback( (leafId: NodeId) => { const paneId = paneIdByLeafRef.current.get(leafId); @@ -339,6 +371,21 @@ export default function App() { const openHostManager = useCallback(() => setHostManagerOpen(true), []); const closeHostManager = useCallback(() => setHostManagerOpen(false), []); + + // Outside-click dismissal for the titlebar dropdowns. Mirrors the + // per-pane shell-picker pattern in LeafPane.tsx. + useEffect(() => { + if (!defaultShellMenuOpen) return; + const onDocClick = () => setDefaultShellMenuOpen(false); + window.addEventListener("click", onDocClick); + return () => window.removeEventListener("click", onDocClick); + }, [defaultShellMenuOpen]); + useEffect(() => { + if (!layoutMenuOpen) return; + const onDocClick = () => setLayoutMenuOpen(false); + window.addEventListener("click", onDocClick); + return () => window.removeEventListener("click", onDocClick); + }, [layoutMenuOpen]); const saveHosts = useCallback( (next: SshHost[]) => { // Preserve hasPassword flags that aren't included in the payload from @@ -742,56 +789,155 @@ export default function App() { setPaletteOpen(false); }, []); - // Titlebar default-shell picker: WSL distros + a single PowerShell button. - // SSH never lives here — connections are always per-pane and explicit. + // Label shown on the default-shell chip — current selection at a glance. const isDefaultDistro = (d: string) => defaultShell.shellKind === "wsl" && defaultShell.distro === d; const isDefaultPowershell = defaultShell.shellKind === "powershell"; + const defaultShellLabel = + defaultShell.shellKind === "powershell" + ? "PowerShell" + : (defaultShell.distro ?? "(none)"); return (
tiletopia - - default: - {distros.length === 0 ? ( - no WSL distros - ) : ( - distros.map((d) => ( + + + {defaultShellMenuOpen && ( +
e.stopPropagation()} + > + {distros.length > 0 ? ( + <> +
WSL
+ {distros.map((d) => ( + + ))} + + ) : ( + <> +
WSL
+
(no distros)
+ + )} +
Windows
- )) +
)} - -
- - layout: - - - - - + + + + + + + {layoutMenuOpen && ( +
e.stopPropagation()} + > + + + + + +
+ )}
-