feat(ui): open Display on a first desktop visit to the tabbed rail

With every panel closed nothing shows that the sideways labels are launchers, so Display starts expanded for a first-time visitor on a desktop viewport; phones keep it collapsed and a share link decides for itself. A stored choice still wins. When the rail picks its one owner, a panel the visitor or a share link opened outranks one open only by default, so a stored CCTV choice is not displaced by the Display default.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-27 18:26:46 +04:00
1 parent 575ff3678f
commit f383174f3e
6 files changed
+72 -23

No files matched your search

+5 -3
View File
@@ -3,6 +3,8 @@
an accordion of collapsed launcher cards: the open panel sits beside the an accordion of collapsed launcher cards: the open panel sits beside the
strip and at most one panel is open in every desktop HUD layout. The open strip and at most one panel is open in every desktop HUD layout. The open
tab and its panel share one surface and one outline, like a folder tab; the tab and its panel share one surface and one outline, like a folder tab; the
other tabs are plain labels. The tabs are a keyboard-navigable tablist, other tabs are plain labels. A first-time visitor on a desktop finds Display
Escape inside a panel returns focus to its tab, and saved preferences and open, so the strip reads as launchers; a stored choice still wins. The tabs
share links are unchanged. Phones keep their sheet shell (gekh, #1425). are a keyboard-navigable tablist, Escape inside a panel returns focus to its
tab, and saved preferences and share links are unchanged. Phones keep their
sheet shell (gekh, #1425).
+12 -1
View File
@@ -1524,6 +1524,15 @@ open panel's minimum height, so a short panel such as Context still reaches
its tab. A first tab level with the panel's top shares its top edge, with no its tab. A first tab level with the panel's top shares its top edge, with no
corner between them; Cyber's strip starts below the frame's upper chamfer. corner between them; Cyber's strip starts below the frame's upper chamfer.
A first-time visitor on a desktop viewport finds Display open
(`src/ui/panelPositionControls.js`): with every panel closed nothing shows
that the sideways labels are launchers, so the first panel opens to
demonstrate the strip. Phones keep it closed (their sheets present the
rail), a share link decides for itself, and a stored choice wins either way.
When the rail picks its one owner, a panel the visitor or a share link opened
outranks one open only by default, so a stored CCTV choice is not displaced
by the Display default.
## Phone layout ## Phone layout
At 720px and narrower, `src/ui/mobileShell.js` keeps the globe full screen and At 720px and narrower, `src/ui/mobileShell.js` keeps the globe full screen and
@@ -2157,7 +2166,9 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
> Missions · Environmental · Explore manually**. No layer and no optional API > Missions · Environmental · Explore manually**. No layer and no optional API
> call happens until a tile is clicked. The right-hand DISPLAY rail > call happens until a tile is clicked. The right-hand DISPLAY rail
> (`pp-toggles`) now starts **collapsed** on a first run rather than expanded — > (`pp-toggles`) now starts **collapsed** on a first run rather than expanded —
> a stored collapse state still wins, as before. > a stored collapse state still wins, as before. (Superseded 2026-10-27 by the
> right rail tabs: on a desktop viewport DISPLAY starts open again on a first
> run; see "Right rail tabs".)
> >
> **The ENVIRONMENTAL tile is quakes AND fires** — live USGS earthquakes plus > **The ENVIRONMENTAL tile is quakes AND fires** — live USGS earthquakes plus
> NASA FIRMS active fires (`layerIds: ['earthquakes', 'local-firms']`), with the > NASA FIRMS active fires (`layerIds: ['earthquakes', 'local-firms']`), with the
+8 -6
View File
@@ -981,19 +981,21 @@ test('the launcher keeps focus, restores it, and never disables the focused butt
assert.match(keyboard, /addEventListener\('keydown', onKeyDown, true\)/); assert.match(keyboard, /addEventListener\('keydown', onKeyDown, true\)/);
}); });
test('the DISPLAY rail starts collapsed on a first run, and a stored choice wins', () => { test('the DISPLAY rail starts open on a first desktop run, and a stored choice wins', () => {
const ui = fs.readFileSync( const ui = fs.readFileSync(
new URL('./ui/panelPositionControls.js', import.meta.url), new URL('./ui/panelPositionControls.js', import.meta.url),
'utf8', 'utf8',
); );
// The rail opened by default to advertise HUD / DETECT / 3D. Those default ON // The right rail is a strip of label tabs: with every panel closed nothing
// now, so it was opening to offer controls for things already happening — // shows that the labels are launchers, so DISPLAY opens on a first desktop
// while competing with the mission card for the one first impression there is. // run to demonstrate the strip. Phones keep their sheets and a share link
// decides for itself.
assert.match( assert.match(
ui, ui,
/if \(panelId === 'pp-toggles' && stored === null\) collapsed = true;/, /if \(panelId === 'pp-toggles' && stored === null\)\s+collapsed = !allowStored \|\| isPhoneViewport\(\);/,
'first run must leave DISPLAY collapsed', 'first desktop run must open DISPLAY; phones and share links keep it closed',
); );
assert.match(ui, /matchMedia\?\.\('\(max-width: 720px\)'\)/);
// The first-run default may only apply when NOTHING is stored: a visitor who // The first-run default may only apply when NOTHING is stored: a visitor who
// opened the rail keeps it open, and one who closed it keeps it closed. // opened the rail keeps it open, and one who closed it keeps it closed.
const block = ui.slice( const block = ui.slice(
+13 -9
View File
@@ -13,6 +13,10 @@ const RETIRED_PANEL_KEY_PREFIXES = [
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelPos.`, `godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelPos.`,
`godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`, `godsEyeView.${PANEL_POSITION_STORAGE_VERSION}.panelFloatHintShown`,
]; ];
/** Phones present the rail as sheets (mobileShell.js), not as a tab strip. */
const isPhoneViewport = () =>
globalThis.matchMedia?.('(max-width: 720px)')?.matches === true;
export class PanelPositionControls { export class PanelPositionControls {
constructor({ syncPanelCollapseButton, showToast }) { constructor({ syncPanelCollapseButton, showToast }) {
this._syncPanelCollapseButton = syncPanelCollapseButton; this._syncPanelCollapseButton = syncPanelCollapseButton;
@@ -66,15 +70,15 @@ export class PanelPositionControls {
// storage unavailable // storage unavailable
} }
} }
// DISPLAY starts COLLAPSED for a first-time visitor, then respects the // DISPLAY starts EXPANDED for a first-time visitor on a desktop viewport,
// user's persisted choice like every other panel. // where the right rail is a strip of label tabs (rightRailTabs.js): with
// // every panel closed nothing shows that the labels are launchers, so the
// It used to start expanded, to advertise the HUD / DETECT / 3D toggles. // first panel opens to demonstrate the strip. Phones keep it collapsed
// That reason expired when those became ON by default: the rail now opens // (their sheets present the rail), and a share link decides for itself.
// to offer controls for things already happening, while competing with the // A stored choice still wins in both directions, so anyone who closes it
// first-run mission card for the one first impression there is. A stored // keeps it closed.
// choice still wins in both directions, so anyone who opens it keeps it. if (panelId === 'pp-toggles' && stored === null)
if (panelId === 'pp-toggles' && stored === null) collapsed = true; collapsed = !allowStored || isPhoneViewport();
panelEl.classList.toggle('collapsed', collapsed); panelEl.classList.toggle('collapsed', collapsed);
// Bodies that expand themselves on first appearance must not override a // Bodies that expand themselves on first appearance must not override a
// choice the user (or a share link) already made. // choice the user (or a share link) already made.
+25
View File
@@ -295,6 +295,31 @@ test('right layout uses keyboard focus when there is no preferred panel', () =>
assert.equal(f.second.classList.contains('collapsed'), false); assert.equal(f.second.classList.contains('collapsed'), false);
}); });
test('right layout lets a stored or shared choice outrank a panel open only by default', () => {
// Display opens by default on a first desktop visit; a visitor who had
// opened CCTV keeps CCTV as the owner, whatever the rail order.
const f = fixture('right');
f.expand(f.first, 900);
f.expand(f.second, 900);
f.first.dataset.collapsedPreference = 'default';
f.second.dataset.collapsedPreference = 'stored';
f.run();
assert.equal(f.first.classList.contains('rail-owner-collapsed'), true);
assert.equal(f.second.classList.contains('collapsed'), false);
const g = fixture('right');
g.expand(g.first, 900);
g.expand(g.second, 900);
g.first.dataset.collapsedPreference = 'default';
g.second.dataset.collapsedPreference = 'default';
g.run();
assert.equal(
g.first.classList.contains('collapsed'),
false,
'all default: first wins',
);
assert.equal(g.second.classList.contains('rail-owner-collapsed'), true);
});
for (const collapsed of [true, false]) { for (const collapsed of [true, false]) {
test(`right layout ignores a hidden ${collapsed ? 'collapsed' : 'expanded'} panel`, () => { test(`right layout ignores a hidden ${collapsed ? 'collapsed' : 'expanded'} panel`, () => {
const baseline = fixture('right'); const baseline = fixture('right');
+9 -4
View File
@@ -75,10 +75,12 @@ export function layoutRightPanelRail({
(hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'), (hiddenByRail(panel) || getComputedStyle(panel).display !== 'none'),
); );
// One expanded owner: the latest explicit disclosure, else the panel that // One expanded owner: the latest explicit disclosure, else the panel that
// holds keyboard focus, else the first. Peers are presentation-collapsed // holds keyboard focus, else one the visitor or a share link chose over one
// without overwriting saved panel preferences; the marker is distinct from // open only by default (panelPositionControls.js marks each), else the
// the left rail's space-based collapse. Phones present the rail as tab // first. Peers are presentation-collapsed without overwriting saved panel
// sheets that show several panels at once, so they get every panel back. // preferences; the marker is distinct from the left rail's space-based
// collapse. Phones present the rail as tab sheets that show several panels
// at once, so they get every panel back.
const isMobile = windowRef.matchMedia('(max-width: 720px)').matches; const isMobile = windowRef.matchMedia('(max-width: 720px)').matches;
if (!isMobile) { if (!isMobile) {
const expanded = panels.filter( const expanded = panels.filter(
@@ -87,6 +89,9 @@ export function layoutRightPanelRail({
const owner = const owner =
expanded.find((panel) => panel.id === preferredPanelId) || expanded.find((panel) => panel.id === preferredPanelId) ||
expanded.find((panel) => panel.contains(documentRef?.activeElement)) || expanded.find((panel) => panel.contains(documentRef?.activeElement)) ||
expanded.find(
(panel) => panel.dataset?.collapsedPreference !== 'default',
) ||
expanded[0]; expanded[0];
for (const panel of expanded) { for (const panel of expanded) {
if (panel === owner) continue; if (panel === owner) continue;