diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba80fe27..7b28633ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,29 @@ rendering, so maintainer notes stay out of the user-facing window. No emoji from 1.64 on (owner rule: no emoji in the UI); the app strips them from older entries. --> +## 1.66.0 — Menus and panels, polished + +### Sidebars and the dock +- **Handles stay out of the way**: when the bottom dock leaves no room above it, the sidebar handles go behind it and come back when it hides; each handle keeps its place in proportion when the window is resized. +- **Docked windows' side panes**: a closed pane's handle sits flush against the window edge, the main area keeps its scrollbar clear of it, and **Ctrl+B** / **Ctrl+Alt+B** open and close the panes of the docked window under the mouse or holding the focus. **Switch sidebar side** moved into the handles' right-click menu. +- **Tabs keep their names**: sidebar tabs never shrink; when they do not fit, a **...** button lists them all and the mouse wheel scrolls the strip. A menu row that moves a window reads **Move here**. + +### Properties and Configure Scene +- **No pin, no lock**: Properties follows your selection and keeps showing the last object after you click away (it closes when that object is deleted); its header always stays at the top. +- **Configure Scene moves on its own**: drag its tab to the other sidebar and Properties stays where it is. + +### Windows +- **Dock and Pop out in every floating header**: one icon-only **Dock** button (and **Pop out** where a window can pop out) in the Node editor, Timeline, Explorer, Objects, Chat, Free assets and the other dockable windows. Docked, a window closes from its dock tab; Code docked has no header row at all. + +### Main menu and reports +- **Layouts** is the first row of **Window**; **What's new** is the version line at the bottom, with a dot when there is something new. The menu is solid to its bottom edge, and **Help** is a grid with a bug icon on **Report a bug**. +- **One Report a bug view**: the moment report and the bug report are one form. It names the seconds it holds ("the last 16 s"), and with no account it sends through the anonymous channel. + ## 1.65.1 — Safer loading - **Objects and materials from other people and from files are checked before they load.** Anything that does not pass loads like a missing file, and the rest of the scene is unchanged. + ## 1.65.0 — New menus and panels ### Chat and the AI assistant diff --git a/perf/boot-budget.json b/perf/boot-budget.json index c26744bea..a1205029b 100644 --- a/perf/boot-budget.json +++ b/perf/boot-budget.json @@ -4,12 +4,12 @@ "pages": { "play": { "html": "play.html", - "maxJsGzBytes": 2097152, + "maxJsGzBytes": 2162688, "maxFirstFrameMs": 20000 }, "editor": { "html": "index.html", - "maxJsGzBytes": 3407872, + "maxJsGzBytes": 3538944, "maxFirstFrameMs": 32000 } }, @@ -21,6 +21,13 @@ "editor": "3042.2 KB measured (3133 KB at v1.56.0) -> line 3328 KB, the looser line", "note": "first lines: the play entry's boot (SP1) and the editor's, gzip -9 initial JS", "firstFrame": "throttled (slow 4G + 4x CPU, median of 5 on the Deck): play 14641 ms, editor 23647 ms -> loose lines 20000 / 32000 ms (x1.35: a shared runner is noisy; the byte lines are the precise gate)" + }, + { + "date": "2026-11-11", + "lane": "uir-polish (1.66.0)", + "play": "1.63.0 1987.1 KB -> 1.64.0 2066.7 (+79.6, over) -> 1.65.0 2073.0 (+6.3) -> 1.65.1 2082.9 (+9.9) -> 1.66 2083.6 (+0.7) -> line 2112 KB", + "editor": "1.63.0 3229.4 KB -> 1.64.0 3383.8 (+154.4, over) -> 1.65.0 3410.0 (+26.2) -> 1.65.1 3421.9 (+11.9) -> 1.66 3424.2 (+2.3) -> line 3456 KB", + "note": "main CI was red on this step from 1.64.0 to 1.65.1 (numbers from each tag's ci.yml run). The line catches up with what shipped and leaves ~30 KB of room; it is NOT a new allowance: the 1.64/1.65 growth (levels, the new menus and panels, safer loading) belongs in lazy chunks, a master-plan card, and the lines come back down when that lands" } ], "throttle": { diff --git a/src/App.svelte b/src/App.svelte index e33d5dc49..93c9f5931 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -55,7 +55,6 @@ // 26-A: the desktop Statistics window + the sampler that feeds it and the status-line // budget meter. `renderer.info` had exactly one reader before this (the VR plate). import StatsOverlay from './components/menu/StatsOverlay.svelte' - import MomentReport from './components/menu/MomentReport.svelte' import ProblemReport from './components/menu/ProblemReport.svelte' // 37 R20 import { startSceneMetrics, budgetSummary } from './lib/sceneBudget' import { startPerfRecorder } from './lib/perf/recorder' @@ -553,7 +552,6 @@ import { startMusicToolbox } from './lib/musicToolbox' {/if} - {#if $assetBrowserOpen} diff --git a/src/components/ContextMenuItems.svelte b/src/components/ContextMenuItems.svelte index 6fb8f96df..7b8cb9168 100644 --- a/src/components/ContextMenuItems.svelte +++ b/src/components/ContextMenuItems.svelte @@ -12,6 +12,8 @@ // { section: 'Edit' } a quiet uppercase section label // icon: 'copy' lucide kebab name (ui/Icon.svelte) // hint: 'Ctrl+D' dimmed right-aligned shortcut hint + // hoverHint: 'Move here' (uir-polish F3) replaces `hint` while the row is hovered or + // keyboard-highlighted — what a press will DO, where `hint` says where it is // 16-P3 adds `checked: true` — the ACTIVE choice of a group (bold + accent), // which replaced the old '● ' label prefix. // @@ -285,7 +287,11 @@ {#if rowIcon(item)}{/if} {/if} {item.label} - {#if item.hint} + {#if item.hint && item.hoverHint} + + {item.hint} + {item.hoverHint} + {:else if item.hint} {item.hint} {/if} {#if item.rowActions} @@ -437,4 +443,16 @@ font-size: var(--fs-badge); color: var(--text-faint); } + .ctx-hint-hover { + display: none; + color: var(--accent-text); + } + .ctx-row:hover .ctx-hint-rest, + .ctx-row.ctx-active .ctx-hint-rest { + display: none; + } + .ctx-row:hover .ctx-hint-hover, + .ctx-row.ctx-active .ctx-hint-hover { + display: inline; + } diff --git a/src/components/Flow.svelte b/src/components/Flow.svelte index 941ecfa10..de9bdcbdf 100644 --- a/src/components/Flow.svelte +++ b/src/components/Flow.svelte @@ -15,6 +15,7 @@ import DockTabs from './DockTabs.svelte'; import WindowChrome from './ui/WindowChrome.svelte'; import Icon from './ui/Icon.svelte'; + import HeadDockPop from './ui/HeadDockPop.svelte'; // uir-polish G2/G3 import { dragWindow } from '$lib/dragWindow'; import { focusStack } from '$lib/windowFocus'; import { tabbable, resizeGroup, tabGroups } from '$lib/windowTabs'; @@ -254,7 +255,7 @@ > {#snippet actions()} - + setDocked(true)} dockId="flow-dock" /> {/snippet}
diff --git a/src/components/Menu.svelte b/src/components/Menu.svelte index 5534917d3..bdb60cf44 100644 --- a/src/components/Menu.svelte +++ b/src/components/Menu.svelte @@ -12,6 +12,7 @@ import BudgetChip from './menu/BudgetChip.svelte'; // 46 B1 import Inspector from './menu/Inspector.svelte'; import SidebarStrips from './menu/SidebarStrips.svelte'; // 42 I2/I3 + import { inspectorDual } from '$lib/sidebars'; // uir-polish E5 import SidebarHandles from './menu/SidebarHandles.svelte'; // showhide-v8: the viewport sidebar handles import MenuSidebar from './menu/Sidebar.svelte'; import TouchTools from './menu/TouchTools.svelte'; @@ -86,7 +87,7 @@ -{#if !$panelAway.inspector}{/if} +{#if !$panelAway.inspector}{#if $inspectorDual}{/if}{/if} diff --git a/src/components/editors/AnimationWindow.svelte b/src/components/editors/AnimationWindow.svelte index 649b1a173..de189e22a 100644 --- a/src/components/editors/AnimationWindow.svelte +++ b/src/components/editors/AnimationWindow.svelte @@ -37,6 +37,7 @@ import { openApplyClip } from '$lib/characterUi.js'; import ClipToolsPanel from './ClipToolsPanel.svelte'; // 57 M3/M8 import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import PaneHandle from '../ui/PaneHandle.svelte'; // showhide-v8: the one pane-handle pattern import { readPanelOpen, writePanelOpen } from '$lib/ui/handheldPanels.js'; import { minimalScroll } from '$lib/ui/minimalScroll.js'; @@ -2848,7 +2849,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} /> {/snippet} {#if compact && target}
{@render paneSwitch()}
{/if} diff --git a/src/components/editors/AssetBrowser.svelte b/src/components/editors/AssetBrowser.svelte index 18837715f..2ac7711c5 100644 --- a/src/components/editors/AssetBrowser.svelte +++ b/src/components/editors/AssetBrowser.svelte @@ -8,6 +8,8 @@ // every download lands through $lib/cc0/cc0Import — the Explorer's own import entry for files, // the environment picker's write for a sky, the material preset library for a material. import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2 + import { dockTab } from '$lib/dockMenu'; import Button from '../ui/Button.svelte'; import Segmented from '../ui/Segmented.svelte'; import Tabs from '../ui/Tabs.svelte'; @@ -390,8 +392,10 @@ aria-label="Refresh" onclick={refresh}>
{:else} @@ -873,12 +867,11 @@ @@ -887,7 +880,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} onpopout={popOut} popId="code-ws-popout" /> {/snippet}
{@render main()}
diff --git a/src/components/editors/Explorer.svelte b/src/components/editors/Explorer.svelte index 38a60f6e9..b01dbcba9 100644 --- a/src/components/editors/Explorer.svelte +++ b/src/components/editors/Explorer.svelte @@ -4,6 +4,7 @@ import { showChoice, showConfirm } from '$lib/confirmDialog.js'; import { safeHtml } from '$lib/sandbox/sanitize.js'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import EmptyState from '../ui/EmptyState.svelte'; import { FILE_TEMPLATES } from '$lib/codeFilesCore.js'; import { isViewer } from '$lib/objectPermissions'; @@ -9005,7 +9006,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} dockId="explorer-dock" /> {/snippet}
diff --git a/src/components/editors/FilePreviewWindow.svelte b/src/components/editors/FilePreviewWindow.svelte index 51abd08b9..385c91820 100644 --- a/src/components/editors/FilePreviewWindow.svelte +++ b/src/components/editors/FilePreviewWindow.svelte @@ -856,7 +856,7 @@ onclick={() => openPreviewCog.set(cogOpen ? null : winId)} >
{#if cogOpen && !editing} diff --git a/src/components/editors/HudEditor.svelte b/src/components/editors/HudEditor.svelte index 4195fe391..b06faf5b1 100644 --- a/src/components/editors/HudEditor.svelte +++ b/src/components/editors/HudEditor.svelte @@ -24,6 +24,7 @@ // and it belongs on this side of the seam. import { untrack } from 'svelte'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import { hudEditorClose, showToast } from '../../stores/appStore.js'; import { hudDocs, hudRuntime, hudSelection, hudScreenOverride, HUD_ANCHORS, HUD_SCENE_KEY, @@ -1495,7 +1496,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} /> {/snippet}
diff --git a/src/components/editors/Profiler.svelte b/src/components/editors/Profiler.svelte index d15a37462..5ab512ff6 100644 --- a/src/components/editors/Profiler.svelte +++ b/src/components/editors/Profiler.svelte @@ -17,6 +17,7 @@ import { onMount, untrack } from 'svelte'; import { get } from 'svelte/store'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import { profilerLiveOpen } from '$lib/perf/liveSink'; import DockTabs from '../DockTabs.svelte'; import WindowChrome from '../ui/WindowChrome.svelte'; @@ -1016,7 +1017,7 @@ {#if !myGroup}{@render controls()}{:else}{/if} {/snippet} {#snippet actions()} - + setDocked(true)} /> {/snippet} {#if myGroup} diff --git a/src/components/editors/ShaderEditor.svelte b/src/components/editors/ShaderEditor.svelte index b1874782e..b3e8a5384 100644 --- a/src/components/editors/ShaderEditor.svelte +++ b/src/components/editors/ShaderEditor.svelte @@ -18,6 +18,7 @@ import { untrack } from 'svelte'; import { get } from 'svelte/store'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import { minimalScroll } from '$lib/ui/minimalScroll.js'; import { SvelteFlow, @@ -1128,7 +1129,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} dockId="shader-dock" /> {@render editorActions()} {/snippet} diff --git a/src/components/editors/TimelineEditor.svelte b/src/components/editors/TimelineEditor.svelte index 845f1b187..45481d9f5 100644 --- a/src/components/editors/TimelineEditor.svelte +++ b/src/components/editors/TimelineEditor.svelte @@ -28,6 +28,7 @@ import { untrack } from 'svelte'; import { get } from 'svelte/store'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import Button from '../ui/Button.svelte'; import Toggle from '../ui/Toggle.svelte'; import EmptyState from '../ui/EmptyState.svelte'; @@ -1411,12 +1412,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} /> {/snippet}
diff --git a/src/components/editors/UvEditor.svelte b/src/components/editors/UvEditor.svelte index eb0570ec2..a8b71d5a6 100644 --- a/src/components/editors/UvEditor.svelte +++ b/src/components/editors/UvEditor.svelte @@ -14,6 +14,7 @@ // DOWN in canvas space, so every mapping flips Y. import { onMount, untrack } from 'svelte'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2/G3 import ScrollStrip from '../ui/ScrollStrip.svelte'; import { selectedObject, selectedObjects, objectsGroup, globalScene } from '../../stores/sceneStore'; import { uvEditorClose, showToast } from '../../stores/appStore.js'; @@ -2067,7 +2068,7 @@ {/snippet} {#snippet actions()} - + setDocked(true)} /> {/snippet}
diff --git a/src/components/menu/Chat.svelte b/src/components/menu/Chat.svelte index c50e9c433..9a17e2615 100644 --- a/src/components/menu/Chat.svelte +++ b/src/components/menu/Chat.svelte @@ -11,6 +11,8 @@ import WindowChrome from '../ui/WindowChrome.svelte'; import Button from '../ui/Button.svelte'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2 + import { dockTab } from '$lib/dockMenu'; import ChatSession from './ChatSession.svelte'; import ChatAssistant from './ChatAssistant.svelte'; import ChatHistory from './ChatHistory.svelte'; @@ -232,6 +234,7 @@ onclick={startNewChat}> {/if} + dockTab('chat')} /> {/snippet} -{#if !$inspectorClose} +{#if pane === 'scene' || !$inspectorClose}
#drawer-label' }} use:fieldClip={nativeFieldSpec} - id="inspector" + id={pane === 'scene' ? 'inspector-scene' : 'inspector'} + data-inspector-pane={pane} data-key-scope="panel" role="region" - aria-label="Properties" + aria-label={kind === 'scene' ? 'Configure Scene' : 'Properties'} > inspectorClose.set(true)} - > - {#snippet end()} - - - - {#if sheetMode && !popped && !$phoneShellActive && ($inspectorKind === 'scene' || ($inspectorKind !== 'file' && $selectedObject))} - - {/if} - {/snippet} - - {#if $inspectorKind === 'file'} + /> + {#if kind === 'file'} {#if inspectedItem} @@ -1962,7 +1976,7 @@ {:else}

The file was removed.

{/if} - {:else if $inspectorKind === 'scene'} + {:else if kind === 'scene'}
@@ -3311,7 +3325,7 @@ diff --git a/src/components/menu/MeshGenWindow.svelte b/src/components/menu/MeshGenWindow.svelte index f75ca8fb4..c2375ffc7 100644 --- a/src/components/menu/MeshGenWindow.svelte +++ b/src/components/menu/MeshGenWindow.svelte @@ -19,6 +19,8 @@ import { bottomDockable } from '$lib/bottomDockDrop'; import { safeStorage } from '$lib/safeStorage'; import Icon from '../ui/Icon.svelte'; + import HeadDockPop from '../ui/HeadDockPop.svelte'; // uir-polish G2 + import { dockTab } from '$lib/dockMenu'; import Button from '../ui/Button.svelte'; import Tabs from '../ui/Tabs.svelte'; import { meshProviders, meshActiveProvider, setMeshActiveProvider, meshTakesImage } from '$lib/ai/meshProviders'; @@ -235,7 +237,8 @@
diff --git a/src/components/menu/MomentReport.svelte b/src/components/menu/MomentReport.svelte deleted file mode 100644 index a4e476f9c..000000000 --- a/src/components/menu/MomentReport.svelte +++ /dev/null @@ -1,144 +0,0 @@ - - -{#if $momentDraft} - - -{/if} - - diff --git a/src/components/menu/ProblemReport.svelte b/src/components/menu/ProblemReport.svelte index 96a8d0ed2..ae6bd5f5a 100644 --- a/src/components/menu/ProblemReport.svelte +++ b/src/components/menu/ProblemReport.svelte @@ -1,19 +1,24 @@ {#if filter || badge} -
+
{#if filter} @@ -44,7 +37,6 @@
{/if} {#if badge}{/if} - {#if popped && panel}{/if}
{/if} @@ -59,10 +51,6 @@ background: var(--surface-2); border-bottom: 1px solid var(--border); } - /* 42 I1: unstuck — the header is the first thing in the scroller and scrolls away with it */ - .ins-head-flow { - position: relative; - } .ins-filter { display: flex; align-items: center; diff --git a/src/components/menu/inspector/InspectorLock.svelte b/src/components/menu/inspector/InspectorLock.svelte deleted file mode 100644 index fb5e81b6a..000000000 --- a/src/components/menu/inspector/InspectorLock.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - - diff --git a/src/components/menu/inspector/InspectorPin.svelte b/src/components/menu/inspector/InspectorPin.svelte deleted file mode 100644 index 391db1d70..000000000 --- a/src/components/menu/inspector/InspectorPin.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - - - diff --git a/src/components/menu/settings/InterfaceSettings.svelte b/src/components/menu/settings/InterfaceSettings.svelte index 2db5bd3bc..fed4cdad9 100644 --- a/src/components/menu/settings/InterfaceSettings.svelte +++ b/src/components/menu/settings/InterfaceSettings.svelte @@ -74,7 +74,6 @@ import SidebarHandlesSettings from './SidebarHandlesSettings.svelte'; // showhide-v8 (R3) import PresenceSettings from './PresenceSettings.svelte'; // uir-people import PopOutWindowSettings from './PopOutWindowSettings.svelte'; // 70 W3/W5 - import PanelHeaderSettings from './PanelHeaderSettings.svelte'; // 42 I1 import ChromeButtonsSettings from './ChromeButtonsSettings.svelte'; // panel-gestures item 5 import PhoneBarSettings from './PhoneBarSettings.svelte'; // 42 I5 import { gameSoundVolume } from '$lib/gameSfx'; @@ -326,7 +325,6 @@ - diff --git a/src/components/menu/settings/PanelHeaderSettings.svelte b/src/components/menu/settings/PanelHeaderSettings.svelte deleted file mode 100644 index 0783254ef..000000000 --- a/src/components/menu/settings/PanelHeaderSettings.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - setAllStickyHeads(mixed ? true : v)} /> - diff --git a/src/components/phone/PhoneShell.svelte b/src/components/phone/PhoneShell.svelte index 0f1ab733a..d90b699f4 100644 --- a/src/components/phone/PhoneShell.svelte +++ b/src/components/phone/PhoneShell.svelte @@ -23,7 +23,6 @@ import Icon from '../ui/Icon.svelte'; import { connectionStatus, connectionLabel } from '$lib/connectionStatus'; import SheetGrip from '../ui/SheetGrip.svelte'; - import InspectorLock from '../menu/inspector/InspectorLock.svelte'; // 78-int PL14 import { stripScroll } from '$lib/ui/stripScroll.js'; import { minimalScroll } from '$lib/ui/minimalScroll.js'; import { @@ -910,14 +909,7 @@
- - {#snippet end()} - - {#if $phoneDockShown === 'properties' || $phoneDockShown === 'scene'} - - {/if} - {/snippet} - +
{/if} diff --git a/src/components/shared/WindowShell.svelte b/src/components/shared/WindowShell.svelte index ce448c8ff..a5ce4fb5a 100644 --- a/src/components/shared/WindowShell.svelte +++ b/src/components/shared/WindowShell.svelte @@ -5,6 +5,7 @@ import { readPanelOpen, writePanelOpen, handheld } from '$lib/ui/handheldPanels.js' import { uiScale } from '$lib/uiScale' import PaneHandle from '../ui/PaneHandle.svelte' + import ContextMenu from '../ContextMenu.svelte' import Icon from '../ui/Icon.svelte' import { paneLocks } from '$lib/ui/paneLocks.js' import { edgeFade } from '$lib/ui/edgeFade.js' @@ -249,9 +250,66 @@ // the handles sit BELOW the topbar (owner: "placed lower, covering no controls") let topbarH = $state(0) const handleTop = $derived(Math.round(topbarH + 8)) + + // uir-polish B1 (owner 2026-11-11): a COLLAPSED pane's handle sits flush against the window's edge. A docked window + // (and a floating one) wraps this shell in its frame's padding, which left the tab floating a few px in from the + // edge with a dark strip beside it: the gap to the frame is MEASURED and the closed handle is drawn across it + // (the root clips with a margin as wide as that gap instead of at its own box). + const FRAME = '.tp-dock-panel, .tp-window, [data-pd-host], [data-docked]' + let rootEl = $state(null) + let gap = $state({ left: 0, right: 0 }) + function measureGap() { + const el = rootEl + const frame = el?.parentElement?.closest(FRAME) as HTMLElement | null + if (!el || !frame) return + const r = el.getBoundingClientRect() + const f = frame.getBoundingClientRect() + const cap = (v: number) => Math.max(0, Math.min(12, Math.round(v))) + const next = { left: cap(r.left - f.left - frame.clientLeft), right: cap(f.right - r.right - (frame.offsetWidth - frame.clientWidth - frame.clientLeft)) } + if (next.left !== gap.left || next.right !== gap.right) gap = next + } + $effect(() => { + void rootW + void narrow + const raf = requestAnimationFrame(measureGap) + return () => cancelAnimationFrame(raf) + }) + const clipMargin = $derived(Math.max(gap.left, gap.right)) + // B4: the handle on the RIGHT edge owns that edge — the main area's scroller ends where the handle's tab starts, so + // a vertical scrollbar is never under it (a closed pane's tab sits on the frame edge, across the measured gap) + const TAB_ROOM = 16 + const rightPaneOpen = $derived(side === 'right' ? primaryOpen && !hidePrimary : secondaryOpen) + const rightHandle = $derived(side === 'right' ? !hidePrimary : secondaryModes.length > 0) + // (an OPEN pane's handle hangs from the pane's inner edge into the main area: the same room keeps its scrollbar clear) + // (not on a touch handheld: its scrollbars overlay the content, and a stacked layout has no handle on that edge) + const mainRoomRight = $derived(narrow || touchHandheld || !rightHandle ? 0 : Math.max(0, TAB_ROOM - (rightPaneOpen ? 0 : gap.right))) + /** where a handle is drawn on its edge: the panel's inner edge when the pane shows, else across the frame gap */ + const handleAt = (edge: 'left' | 'right', open: boolean, width: number) => (open ? width * k : -gap[edge]) + + // uir-polish B6 (owner 2026-11-11): "Switch sidebar side" lives in the sidebars' CONTEXT MENU (each pane handle's, + // and the secondary pane's header) instead of a button in that header + const switchItem = $derived({ + label: 'Switch sidebar side', + icon: 'arrow-left-right', + tooltip: `Move ${primaryLabel} to the ${side === 'left' ? 'right' : 'left'} and the other pane to the ${side}`, + action: switchSide + }) + let headMenu = $state<{ x: number; y: number } | null>(null) + function onHeadMenu(e: MouseEvent) { + e.preventDefault() + e.stopPropagation() + headMenu = { x: e.clientX, y: e.clientY } + } -
+
{#if narrow}
@@ -296,7 +354,7 @@ {#if topbar}
{@render topbar()}
{/if} -
{@render main?.()}
+
{@render main?.()}
@@ -306,7 +364,8 @@ style={narrow ? 'order: 1' : `order: ${secondaryPanelOrder}; width: ${secondaryEff * k}px`} > {#if !narrow} -
+ +
{#each secondaryModes as m (m.key)} {/each}
-
{/if} @@ -352,9 +410,10 @@ open={primaryOpen} label={primaryLabel} windowKey={key} + extraItems={secondaryModes.length ? [switchItem] : []} onclick={togglePrimary} data-ws-primary-toggle - style="top: {handleTop}px; {side === 'left' ? 'left' : 'right'}: {primaryOpen ? primaryWidth * k : 0}px" + style="top: {handleTop}px; {side === 'left' ? 'left' : 'right'}: {handleAt(side, primaryOpen, primaryWidth)}px" /> {/if} {#if secondaryModes.length && !narrow} @@ -364,12 +423,22 @@ label={secondaryModes.length > 1 ? secondaryModes.map((m) => m.label).join(' / ') : (activeMode?.label ?? 'the panel')} role={iconOf(activeMode)} windowKey={key} + extraItems={hidePrimary ? [] : [switchItem]} onclick={toggleSecondary} data-ws-secondary-toggle - style="top: {handleTop}px; {secondarySide === 'left' ? 'left' : 'right'}: {secondaryOpen ? secondaryEff * k : 0}px" + style="top: {handleTop}px; {secondarySide === 'left' ? 'left' : 'right'}: {handleAt(secondarySide, secondaryOpen, secondaryEff)}px" /> {/if}
+{#if headMenu} + (headMenu = null)} + /> +{/if}