diff --git a/changelog.d/1735-side-rails-edge.md b/changelog.d/1735-side-rails-edge.md new file mode 100644 index 000000000..1ecee150c --- /dev/null +++ b/changelog.d/1735-side-rails-edge.md @@ -0,0 +1,5 @@ +- Move Visual to the left rail, after Data Layers, Utilities and Authoring, and + lead the right column with Context; an open Context or CCTV panel takes the + column's full height. A switch in each panel header moves that side's panels + over their rail to the screen edge, and the scroll wheel at a screen edge + steps through that side's panels (manjunath22466, samehkhamis, #1735). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 4b578d178..de8d3617c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -627,6 +627,36 @@ panel that holds its row, and `set_panel_open` reaches it as `utilities-panel` or by "utilities", "draw", "drawing" or "import". On a phone it sits collapsed in the Layers sheet under Data Layers. +## Side rails and the screen edge — November 14, 2026 + +Visual (`#pp-toggles`, the display settings, rail item `palette`) is the last +panel of the left column, after Data Layers, Utilities and Authoring, and +opens from the left rail like them (`PanelLayoutController`, falling back to +the right column in a host without a left stack). Context leads the right +column, followed by CCTV, Street Level and the other right panels. An open +Context or CCTV panel takes the column's full height; the other right panels +keep the content-sized layout and its hysteresis band. With nothing open, a +rail's icons and their names show as two glass pieces side by side; once a +panel opens, the icons stay in place and a highlight slides to the open one. + +Each collapsible panel header has an edge switch (`UiPanelEdge.vue`, +`src/ui/panelEdge.js`): it moves that side's panels over their rail to the +screen edge, with the rail as a row of icons above the open panel, and back. +The choice is per side, kept in `localStorage` (`gev.panelEdge`), and applies +on the desktop shell only: phones and Cockpit keep their own layouts and do +not show the switch. + +Scrolling the wheel at a screen edge (`src/ui/railEdgeScroll.js`, from the +edge to just past the rail, at most 84 px) steps that side's rail to its next +or previous panel instead of zooming the globe, wrapping round at either end. +One gesture is one step, trackpad coasting included, with at least half a +second between steps; a step that comes too soon is held until then and is +dropped if the pointer leaves the strip. Panels, controls, dialogs and the +world timeline in the strip keep their own wheel. The strip is off on phones +and touch screens, while the rails are hidden (clean view, recording), and it +follows the rails' sides in a right-to-left language. On a phone, Visual is in +the More sheet with the dock's Visual Presets, as before. + ## God's Eye View in conversations — October 2, 2026 Tool answers that can be shown in God's Eye View include a view: camera, layers, diff --git a/index.html b/index.html index f3057f742..806a8b41b 100644 --- a/index.html +++ b/index.html @@ -26,7 +26,7 @@ with the sources: src/materialSymbolsSubset.test.mjs fails when a source names a glyph this list is missing, because a missing glyph does not draw a box — the ligature never forms and the element renders the literal word. --> - +
diff --git a/scripts/format-scope.json b/scripts/format-scope.json index c11489f1f..0fad9bc71 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1449,5 +1449,13 @@ "src/data/fleetModelInstances.test.mjs", "src/testSupport/fleetGlb.mjs", "src/testSupport/fleetModelFakes.mjs", - "scripts/measure-fleet-trail-anchors.mjs" + "scripts/measure-fleet-trail-anchors.mjs", + "src/ui/panelEdge.js", + "src/ui/railEdgeScroll.js", + "src/ui/railEdgeScroll.test.mjs", + "src/ui/kit/UiPanelEdge.vue", + "src/ui/styles/rail-edge.css", + "src/ui/styles/rail-split.css", + "src/ui/styles/left-tools.css", + "src/ui/styles/panel-edge.css" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 2242d7b2c..c0237942e 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -154,6 +154,8 @@ "src/ui/kit/UiSegmented.vue", "src/ui/rail/RailPanel.vue", "src/ui/kit/UiPanel.vue", + "src/ui/kit/UiPanelEdge.vue", + "src/ui/panelEdge.js", "src/ui/kit/UiPanelToggle.vue", "src/ui/stores/panelChrome.js", "src/ui/rail/PanelCollapseButton.vue", @@ -1136,6 +1138,7 @@ "src/styles/surveillance.js", "src/styles/thermal.js", "src/ui/applicationShell.js", + "src/ui/railEdgeScroll.js", "src/ui/localePicker.js", "src/ui/cyberSonarControls.js", "src/ui/locationNavigation.js", @@ -3087,6 +3090,7 @@ "src/styles/surveillance.js", "src/styles/thermal.js", "src/ui/applicationShell.js", + "src/ui/railEdgeScroll.js", "src/ui/mobileShell.js", "src/exclusiveSurfaces.js", "src/ui/localePicker.js", diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index f50b30469..9ebd0e384 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -4,18 +4,31 @@ import { _selectContextMode } from './ui/contextTransactions.js'; import { _initGlobalContextPanel } from './ui/contextBindings.js'; import { clearSelectedLayers } from './ui/contextActions.js'; import { readFileSync as readRadioSource } from 'node:fs'; -const radioBindings = readRadioSource(new URL('./ui/radioBindings.js', import.meta.url), 'utf8'); -const contextView = readRadioSource(new URL('./ui/views/ContextView.vue', import.meta.url), 'utf8'); +const radioBindings = readRadioSource( + new URL('./ui/radioBindings.js', import.meta.url), + 'utf8', +); +const contextView = readRadioSource( + new URL('./ui/views/ContextView.vue', import.meta.url), + 'utf8', +); import { readFileSync } from 'node:fs'; import { test } from 'node:test'; import assert from 'node:assert/strict'; -const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8')); +const html = await renderApplicationHtml( + readFileSync(new URL('../index.html', import.meta.url), 'utf8'), +); const css = readStylesheet(new URL('../style.css', import.meta.url)); test('Contacts and Space Missions both participate in the ordinary Tab sequence', () => { - for (const id of ['global-context-flights-btn', 'global-context-missions-btn']) { - const button = html.match(new RegExp(`