Merge pull request #1739 from bilawalsidhu/manjunath/cockpit-panels

Cockpit side panels: the main page's rails and panels in first person
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-14 16:12:27 -08:00
commit b7160492e0
10 files changed
+846 -32

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
- Give the desktop cockpit the main page's side rails: Data Layers alone on the
left, and Display and Radio as a right rail that opens one panel at a time
beside it, at the same height, with Contact and Live Signals restyled to
match (manjunath22466, samehkhamis, #1739).
+24 -3
View File
@@ -661,9 +661,9 @@ cards keep Cyber's own red skin. The presets and chips call the
same Display store actions as the dock's Visual Presets tray, which stays on
desktop and phone as before, along with Location; voice `Visual Presets`
still opens that tray. On a phone the More sheet shows that tray right above
Visual, so Visual leaves out its own presets and chips there. In Cockpit the
groups Cockpit's Display popover takes
leave the panel as before and keep their own styling there.
Visual, so Visual leaves out its own presets and chips there. In Cockpit,
the groups that Cockpit's Display popover takes leave the panel as before and
keep their own styling there.
Scope has an Opacity slider (`#scope-opacity-slider`, 50–100%, default 100%;
`SCOPE_OPACITY_MIN_PCT` in `src/ui/visualSettings.js`, since a fainter mask
@@ -723,6 +723,27 @@ 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.
## Cockpit side panels — November 14, 2026
On the desktop, the cockpit's side panels match the main page's
(`src/ui/styles/cockpit-panels.css`). The left rail shows Data Layers only:
Authoring, Utilities and Visual, with their panels, are hidden, since the
cockpit's own Display panel stands in for Visual. Display and Radio
(`CockpitUtilityControls.vue`) become a right rail of two icons with their
names beside them, and each opens one panel at a time beside the rail, with a
header and a "−" to close it. The rail's buttons are disclosures
(`aria-expanded`, `aria-controls`) and go through the `toggleCockpitUtility`
action (`src/ui/radioBindings.js`); closing a panel gives focus back to the
rail. Both rails start at the same height: the left one in CSS, and Display
and Radio through the cockpit layout pass, whose floor
(`COCKPIT_UTILITY_MIN_TOP_PX` / `_RATIO` in `src/ui/cockpitPresentation.js`)
matches it, so an open panel still stops short of the briefing. Contact and
Live Signals are shorter, in the same glass, with the briefing's rows and
weather tiles tightened; Regional News stays a briefing page. In a right-to-left language, the rail keeps its
physical side as the cockpit's other windows do. The Cyber HUD and the phone
keep their own cockpit layouts, with the Display and Radio launchers as
before, and edge mode does not apply in the cockpit.
## 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,
+2 -1
View File
@@ -1461,5 +1461,6 @@
"src/data/placementProvider.test.mjs",
"src/layers/vessels/placement.test.mjs",
"src/ui/styles/visual-panel.css",
"src/ui/styles/scope-off-shadows.css"
"src/ui/styles/scope-off-shadows.css",
"src/ui/styles/cockpit-panels.css"
]
+39 -18
View File
@@ -26,10 +26,7 @@ import {
setSignalCollapsed,
} from './ui/cockpitLayout.js';
import { createBriefState } from './ui/cockpitBriefPages.js';
import {
publishBriefView,
setBriefAutoRotate,
} from './ui/cockpitBriefing.js';
import { publishBriefView, setBriefAutoRotate } from './ui/cockpitBriefing.js';
import { CockpitDisplayPortal } from './ui/cockpitDisplayPortal.js';
import { _handleContextLayerChange } from './ui/contextLayerChanges.js';
import { clearSelectedLayers } from './ui/contextActions.js';
@@ -438,13 +435,10 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls',
/data-cockpit-launcher="radio"[\s\S]*?id="cockpit-radio-toggle-btn"/,
);
const cockpitRadio = html.match(
/id="cockpit-radio-panel"[\s\S]*?<\/div>\s*<\/div>\s*<\/aside>/,
/id="cockpit-radio-panel"[\s\S]*?<\/div>\s*<\/div>\s*<div class="cockpit-utility-rail/,
);
assert.ok(cockpitRadio, 'Cockpit compact Radio controls are missing');
assert.doesNotMatch(
cockpitRadio[0],
/radio-filter|radio-tuner/,
);
assert.doesNotMatch(cockpitRadio[0], /radio-filter|radio-tuner/);
assert.match(
css,
/\.cockpit-utility-controls[\s\S]*?top:\s*var\(\s*--cockpit-utility-top/,
@@ -811,18 +805,27 @@ test('Reset releases Contact camera ownership through its selection-preserving r
);
assert.match(ui, /onReset: \(\) => this\.resetToGlobeView\(\)/);
assert.match(ui, /this\._connectGlobeReset\?\.\(this\._locationControls\)/);
assert.match(controls, /reset\(\) \{\s*if \(!this\.destroyed\) return this\.onReset\(\);/);
assert.match(
controls,
/reset\(\) \{\s*if \(!this\.destroyed\) return this\.onReset\(\);/,
);
// Both reset buttons ask the one reset owner.
const globeActions = fs.readFileSync(
path.join(ROOT, 'src', 'ui', 'views', 'panels', 'GlobeActions.vue'),
'utf8',
);
assert.match(globeActions, /id="reset-globe-view"[^>]*@click="act\('reset'\)"/);
assert.match(
globeActions,
/id="reset-globe-view"[^>]*@click="act\('reset'\)"/,
);
const cockpitView = readRadioSource(
new URL('./ui/views/CockpitView.vue', import.meta.url),
'utf8',
);
assert.match(cockpitView, /id="cockpit-reset-globe"[^>]*@click="globe\.act\('reset'\)"/);
assert.match(
cockpitView,
/id="cockpit-reset-globe"[^>]*@click="globe\.act\('reset'\)"/,
);
assert.match(
cockpitEnter.toString(),
/Object\.assign\(this\.presence, \{\s*active: true,/,
@@ -1193,7 +1196,10 @@ test('cockpit summary presents the focused item as Contact', () => {
const match = html.match(/<aside id="cockpit-context"[\s\S]*?<\/aside>/);
assert.ok(match, 'cockpit Contact summary is missing');
assert.match(match[0], /aria-label="Contact cockpit summary"/);
assert.match(match[0], /class="cockpit-card-kicker cockpit-context-kicker">CONTACT</);
assert.match(
match[0],
/class="cockpit-card-kicker cockpit-context-kicker">CONTACT</,
);
assert.match(match[0], /aria-label="Contact navigation"/);
assert.match(
match[0],
@@ -1408,15 +1414,30 @@ test('Contact folded for one Cockpit left panel comes back only once neither is
const previous = globalThis.document;
const panels = {};
globalThis.document = { getElementById: (id) => panels[id] ?? null };
const panel = (...classes) => ({ classList: { contains: (name) => classes.includes(name) } });
const panel = (...classes) => ({
classList: { contains: (name) => classes.includes(name) },
});
try {
const otherOpen = (id) => PanelChrome.prototype._otherCockpitLeftPanelOpen(id);
const otherOpen = (id) =>
PanelChrome.prototype._otherCockpitLeftPanelOpen(id);
panels['data-panel'] = panel('collapsed');
assert.equal(otherOpen('utilities-panel'), false, 'Data Layers closed: restore');
assert.equal(
otherOpen('utilities-panel'),
false,
'Data Layers closed: restore',
);
panels['data-panel'] = panel('collapsed', 'layout-auto-collapsed');
assert.equal(otherOpen('utilities-panel'), true, 'Data Layers comes back: keep Contact folded');
assert.equal(
otherOpen('utilities-panel'),
true,
'Data Layers comes back: keep Contact folded',
);
panels['data-panel'] = panel();
assert.equal(otherOpen('utilities-panel'), true, 'Data Layers open: keep it folded');
assert.equal(
otherOpen('utilities-panel'),
true,
'Data Layers open: keep it folded',
);
} finally {
globalThis.document = previous;
}
+4 -2
View File
@@ -27,9 +27,11 @@ export const COCKPIT_UTILITY_REC_GAP_PX = 12;
export const COCKPIT_UTILITY_SIGNAL_GAP_PX = 8;
export const COCKPIT_UTILITY_MIN_TOP_PX = 96;
// Display and Radio start where the left rail does (max(206px, 22vh) in
// cockpit-panels.css), so the two rails' tops line up.
export const COCKPIT_UTILITY_MIN_TOP_PX = 206;
export const COCKPIT_UTILITY_MIN_TOP_RATIO = 0.12;
export const COCKPIT_UTILITY_MIN_TOP_RATIO = 0.22;
export const COCKPIT_UTILITY_LAUNCHER_MIN_HEIGHT_PX = 50;
+39
View File
@@ -450,3 +450,42 @@ test('the real layout pass drives the mounted controls: compact hides the collap
assert.equal(cockpit.utilityLayout.top, '120.00px');
assert.equal(strip.classList.contains('layout-primary-only'), false);
});
test('the rail opens one panel at a time and reads as a disclosure', async (t) => {
const { $, controls } = fixture(t);
const item = (kind) =>
document.querySelector(`[data-cockpit-rail-item="${kind}"]`);
item('display').click();
await nextTick();
assert.equal($('cockpit-display-panel').hidden, false);
assert.equal(item('display').getAttribute('aria-expanded'), 'true');
assert.equal(
item('display').getAttribute('aria-controls'),
'cockpit-display-panel',
);
item('radio').click();
await nextTick();
assert.equal($('cockpit-display-panel').hidden, true);
assert.equal($('cockpit-radio-panel').hidden, false);
assert.equal(item('display').getAttribute('aria-expanded'), 'false');
assert.equal(item('radio').getAttribute('aria-expanded'), 'true');
item('radio').click();
await nextTick();
assert.equal(controls._cockpitDisclosureOpen('radio'), false);
assert.equal(controls._cockpitDisclosureOpen('display'), false);
});
test('collapsing an open panel hands focus to the rail when its toggle is hidden', async (t) => {
const { $, controls } = fixture(t);
const railItem = document.querySelector('[data-cockpit-rail-item="radio"]');
// happy-dom lays nothing out: show the rail and hide the launcher's toggle
// as the desktop look does.
railItem.getClientRects = () => [{ width: 32, height: 32 }];
$('cockpit-radio-toggle-btn').getClientRects = () => [];
controls._setCockpitDisclosure('radio', true);
await nextTick();
document.querySelector('.cockpit-utility-collapse').click();
await nextTick();
assert.equal($('cockpit-radio-panel').hidden, true);
assert.equal(document.activeElement, railItem);
});
+22 -4
View File
@@ -27,10 +27,18 @@ export function bindRadioControls() {
this.store?.publishUtility({ ...this._cockpitDisclosure });
if (displayOpen) this.actions.revealStyleParameters();
if (!expanded && returnFocus) {
(kind === 'display'
? this._cockpitDisplayToggleBtn
: this._cockpitRadioToggleBtn
)?.focus({ preventScroll: true });
// The desktop cockpit hides the launchers' toggles behind its rail, so
// focus goes to whichever of the two is shown.
const toggle =
kind === 'display'
? this._cockpitDisplayToggleBtn
: this._cockpitRadioToggleBtn;
const railItem = toggle?.ownerDocument?.querySelector?.(
`[data-cockpit-rail-item="${kind}"]`,
);
(toggle?.getClientRects?.().length ? toggle : railItem || toggle)?.focus({
preventScroll: true,
});
}
if (
!displayOpen &&
@@ -444,6 +452,16 @@ export function bindRadioControls() {
),
toggleCockpitRadio: () =>
setCockpitDisclosure('radio', !this._cockpitDisclosureOpen('radio')),
/**
* Open or close the cockpit's Display or Radio panel; opening one closes
* the other.
* @param {'display'|'radio'} kind
* @param {{ returnFocus?: boolean }} [options]
*/
toggleCockpitUtility: (kind, { returnFocus = false } = {}) => {
const open = !this._cockpitDisclosureOpen(kind);
setCockpitDisclosure(kind, open, { returnFocus: !open && returnFocus });
},
/**
* @param {string} filter
* @returns {string} The station tag the filter shows afterwards.
+693
View File
@@ -0,0 +1,693 @@
/* ── Cockpit side windows in the app's panel look ───────────────────────
Contact (left), the briefing carousel (right) and the Display / Radio
controls get the same smoked glass, 16px corners and card rows as the
main panels. They keep cockpit.css's slight 3D slant toward the centre,
and stand the same height. The text glow goes; the state colours on the
top edge (uncertain, lost) stay. */
.cockpit-context-window,
.cockpit-signal-window {
border: 1px solid rgba(255, 255, 255, 0.08);
border-top-color: rgba(255, 255, 255, 0.14);
border-radius: 16px;
background: rgba(12, 12, 20, 0.72);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
text-shadow: none;
scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
/* Open, both windows share one compact size; their lists scroll. */
.cockpit-context-window:not([data-collapsed='true']),
.cockpit-signal-window:not([data-collapsed='true']) {
width: min(280px, 23vw);
height: min(27vh, 260px);
}
/* Contact's header: the buttons share the kicker's row, so the subject runs
the full width on at most two lines instead of wrapping beside them. */
.cockpit-context-window:not([data-collapsed='true'])
.cockpit-context-header
> div {
display: contents;
}
.cockpit-context-window:not([data-collapsed='true']) .cockpit-context-kicker {
grid-column: 1;
grid-row: 1;
}
.cockpit-context-window:not([data-collapsed='true']) .cockpit-context-actions {
grid-column: 2;
grid-row: 1;
}
.cockpit-context-window:not([data-collapsed='true']) #cockpit-context-subject {
display: -webkit-box;
grid-column: 1 / -1;
grid-row: 2;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
}
/* Their foot sits just above the bottom HUD readouts (MGRS / GSD), which
stand at clamp(84px, 11vh, 118px) and are about 38px tall. */
.cockpit-context-window,
.cockpit-signal-window {
bottom: calc(clamp(84px, 11vh, 118px) + 54px);
}
.cockpit-context-window[data-state='uncertain'] {
border-top-color: rgba(116, 219, 255, 0.72);
}
.cockpit-context-window[data-state='lost'] {
border-top-color: rgba(213, 168, 99, 0.72);
}
.cockpit-context-window[data-collapsed='true'],
.cockpit-signal-window[data-collapsed='true'] {
border-radius: 12px;
background: rgba(12, 12, 20, 0.66);
}
/* Headers: a quiet hairline like the panel headers. */
.cockpit-card-header {
border-bottom-color: rgba(255, 255, 255, 0.08);
}
.cockpit-card-actions button {
border-color: rgba(255, 255, 255, 0.1);
border-radius: 6px;
background: rgba(255, 255, 255, 0.04);
color: rgba(224, 236, 242, 0.82);
}
.cockpit-card-actions button:hover,
.cockpit-card-actions button:focus-visible {
border-color: rgba(0, 212, 255, 0.45);
background: rgba(0, 212, 255, 0.1);
color: #fff;
}
/* Rows and tiles inside: the card style used in Context and Visual. */
.cockpit-context-cohorts > div,
.cockpit-signal-list li,
.cockpit-news-list li,
.cockpit-local-grid > div {
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
background: rgba(255, 255, 255, 0.035);
}
/* The weather tiles stand apart as cards, with no lit grid behind them,
and are short enough to fit the shorter window. */
.cockpit-local-grid {
gap: 5px;
background: none;
}
.cockpit-local-grid > div {
min-height: 0;
gap: 2px;
padding: 6px 9px;
}
.cockpit-route-card {
border-bottom-color: rgba(255, 255, 255, 0.08);
}
/* Display / Radio: the closed launchers and the open panels match too. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control.is-expanded {
border-color: rgba(255, 255, 255, 0.08);
border-radius: 16px;
background: rgba(12, 12, 20, 0.72);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
/* ── Cockpit uses the main page's rails ───────────────────────────────
The left rail shows Data Layers only (Authoring has no place in first
person, Utilities has nothing to do there, and the cockpit's own Display
panel stands in for Visual), one panel opens beside it, and the collapsed
header strips the cockpit used instead are hidden. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left {
display: block;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left
:is(
[data-rail-item='scene-panel'],
[data-rail-item='utilities-panel'],
[data-rail-item='pp-toggles']
),
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left
.ui-tooltip:has(
> :is(
[data-rail-item='scene-panel'],
[data-rail-item='utilities-panel'],
[data-rail-item='pp-toggles']
)
) {
display: none;
}
/* With some icons hidden, the runs between them break up: with a panel
open, each icon left is its own island. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left
.ui-rail--collapsible:not(.ui-rail--expanded):has(.ui-rail__item--on)
> .ui-tooltip::before {
inset: -4px -5px;
border-width: 1px;
border-radius: 14px;
}
/* Authoring, Utilities or Visual may still be open underneath; its hidden icon must
not split the rail into islands. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left
.ui-rail:has(
:is(
[data-rail-item='scene-panel'],
[data-rail-item='utilities-panel'],
[data-rail-item='pp-toggles']
)[aria-pressed='true']
)
> .ui-tooltip::before,
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left
.ui-rail:has(
:is(
[data-rail-item='scene-panel'],
[data-rail-item='utilities-panel'],
[data-rail-item='pp-toggles']
)[aria-pressed='true']
)::before {
/* Nor may the sliding highlight stay where the hidden icon was. */
display: none;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> .gev-panel-rail--left
.ui-rail:has(
:is(
[data-rail-item='scene-panel'],
[data-rail-item='utilities-panel'],
[data-rail-item='pp-toggles']
)[aria-pressed='true']
) {
background: rgba(16, 20, 28, 0.82) !important;
border-color: rgba(255, 255, 255, 0.14) !important;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45) !important;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> [data-panel-id].collapsed,
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack
> :is(
[data-panel-id='scene-panel'],
[data-panel-id='utilities-panel'],
[data-panel-id='pp-toggles']
) {
display: none;
}
/* Both rails start at one height, just under the top HUD text, so their top
edges line up; the cockpit layout pass places Display and Radio at the
same height (COCKPIT_UTILITY_MIN_TOP_PX / _RATIO in cockpitPresentation.js). */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
#left-panel-stack {
top: max(206px, 22vh) !important;
}
/* Bottom HUD readouts: scale from their foot, and drop the three-line right
block (GSD / ALT / AIS) by one scaled line so both start level, as on the
main page. */
body.cockpit-mode #intel-hud .hud-bottom-left {
transform-origin: left bottom;
}
body.cockpit-mode #intel-hud .hud-bottom-right {
bottom: calc(clamp(84px, 11vh, 118px) - 20px);
transform-origin: right bottom;
}
/* ── Live signals rows ──────────────────────────────────────────────────
Callsign on the left, time small at the top right, the detail under it.
The status colour is a slim inset bar (cyan for the aircraft you ride,
white for nearby, amber for military / warnings), not a bent border. */
.cockpit-signal-list {
gap: 4px;
}
.cockpit-signal-list li {
position: relative;
display: block;
padding: 5px 10px 5px 12px;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: inset 2px 0 0 rgba(244, 251, 255, 0.22);
}
.cockpit-signal-list li:is(.track, .current) {
border-color: rgba(0, 212, 255, 0.22);
background: rgba(0, 212, 255, 0.06);
box-shadow: inset 2px 0 0 var(--cockpit-accent, #00d4ff);
}
.cockpit-signal-list li:is(.nearby) {
border-color: rgba(255, 255, 255, 0.06);
box-shadow: inset 2px 0 0 rgba(244, 251, 255, 0.55);
}
.cockpit-signal-list li:is(.military, .warning) {
border-color: rgba(255, 255, 255, 0.06);
box-shadow: inset 2px 0 0 #d5a863;
}
.cockpit-signal-list li.actionable:is(:hover, :focus-within) {
border-color: rgba(0, 212, 255, 0.3);
background: rgba(0, 212, 255, 0.08);
}
.cockpit-signal-list time {
position: absolute;
top: 6px;
inset-inline-end: 28px;
color: rgba(224, 236, 242, 0.4);
font: 7px/1 var(--font-mono);
letter-spacing: 0.06em;
font-variant-numeric: tabular-nums;
}
.cockpit-signal-list li > div {
gap: 1px;
}
.cockpit-signal-target {
grid-template-columns: minmax(0, 1fr) 14px;
gap: 8px;
font-size: 10.5px;
letter-spacing: 0.1em;
}
.cockpit-signal-target-rule {
display: none;
}
.cockpit-signal-target-chevron {
justify-self: end;
color: rgba(224, 236, 242, 0.5) !important;
}
.cockpit-signal-list
li.actionable:is(:hover, :focus-within)
.cockpit-signal-target-chevron {
color: #fff !important;
}
.cockpit-signal-list span:not([class]) {
color: rgba(224, 236, 242, 0.62);
font-size: 8px;
letter-spacing: 0.08em;
}
/* Keep a long callsign clear of the time at the row's end. */
.cockpit-signal-list .cockpit-signal-target-label {
margin-inline-end: 50px;
}
/* ── Briefing content fits the narrower window ────────────────────────── */
.cockpit-signal-window {
grid-template-columns: minmax(0, 1fr);
}
.cockpit-signal-window > *,
.cockpit-brief-stage,
.cockpit-brief-stage > section,
.cockpit-signal-list,
.cockpit-signal-list li {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
/* Route: a quiet heading, endpoints that truncate. */
.cockpit-route-card {
padding-inline: 4px;
}
.cockpit-route-heading {
display: flex;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
#cockpit-route-status {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cockpit-route-airport strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Header: the title on one line, the controls on the row under it. */
.cockpit-signal-window:not([data-collapsed='true']) .cockpit-signal-header {
grid-template-columns: minmax(0, 1fr);
gap: 6px;
padding-inline: 4px;
}
.cockpit-signal-window:not([data-collapsed='true']) .cockpit-brief-heading {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 2px 8px;
}
.cockpit-signal-window:not([data-collapsed='true']) .cockpit-signal-kicker {
white-space: nowrap;
}
.cockpit-signal-window:not([data-collapsed='true']) .cockpit-brief-actions {
justify-content: flex-start;
}
.cockpit-signal-window:not([data-collapsed='true']) #cockpit-signal-toggle {
margin-inline-start: auto;
}
/* ── Open Display / Radio panels: the main panels' header and size ───── */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control.is-expanded {
min-width: 295px;
box-sizing: border-box;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control.is-expanded
.cockpit-utility-launcher {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 4px 10px 8px;
color: rgba(224, 236, 242, 0.78);
font: 9px/1 var(--font-mono);
letter-spacing: 0.22em;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control.is-expanded
.cockpit-utility-divider {
flex: 0 1 22px;
height: 1px;
margin-right: auto;
background: rgba(255, 255, 255, 0.18);
box-shadow: none;
}
/* The launcher's own toggle steps aside: the rail and "−" open and close
the panel instead. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control.is-expanded
.cockpit-utility-glyph {
display: none;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-collapse {
display: grid;
flex: none;
place-items: center;
width: 22px;
height: 22px;
padding: 0;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
background: rgba(255, 255, 255, 0.03);
color: rgba(206, 232, 239, 0.75);
font: 13px/1 var(--font-mono);
cursor: pointer;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-collapse:hover,
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-collapse:focus-visible {
border-color: rgba(0, 212, 255, 0.45);
color: #fff;
outline: none;
}
/* ── Right rail as on the left: an icon column that stays, labels beside it
while nothing is open, and the open panel to its left. The launchers'
own strips give way to it. ─────────────────────────────────────────── */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-controls {
display: grid;
grid-template-columns: auto auto;
align-items: start;
justify-content: end;
column-gap: 6px;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control:not(.is-expanded) {
display: none;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-control.is-expanded {
grid-column: 1;
grid-row: 1;
margin-top: 0 !important;
}
/* Cyber and the phone keep the launchers' own toggles, so the rail and "−"
are for the desktop look only. */
.cockpit-utility-rail,
.cockpit-utility-collapse {
display: none;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail {
grid-column: 2;
grid-row: 1;
display: flex;
gap: 4px;
align-items: stretch;
pointer-events: auto;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
:is(.cockpit-utility-rail__labels, .cockpit-utility-rail__icons) {
display: flex;
flex-direction: column;
gap: 12px;
padding: 4px;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 14px;
background: rgba(16, 20, 28, 0.82);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.45),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__labels {
box-sizing: border-box;
width: 170px;
padding-inline: 14px;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__labels
> span {
display: flex;
align-items: center;
height: 32px;
color: rgba(225, 236, 242, 0.82);
font:
500 9.28px/1 Inter,
-apple-system,
system-ui,
sans-serif;
letter-spacing: 2.08px;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail.is-open
.cockpit-utility-rail__labels {
display: none;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__item {
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: var(--text-secondary, rgba(206, 232, 239, 0.75));
cursor: pointer;
transition:
color var(--transition-fast, 120ms),
background var(--transition-fast, 120ms),
border-color var(--transition-fast, 120ms);
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__item
.material-symbols-outlined {
font-size: 18px;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__item:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary, #fff);
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__item.is-on,
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail__item.is-on:hover {
border-color: rgba(0, 212, 255, 0.15);
background: rgba(0, 212, 255, 0.15);
color: #00d4ff;
}
/* With a panel open, each icon is its own island, as on the main rails. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail.is-open
.cockpit-utility-rail__icons {
border-color: transparent;
background: none;
box-shadow: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail.is-open
.cockpit-utility-rail__item {
position: relative;
}
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail.is-open
.cockpit-utility-rail__item::before {
position: absolute;
inset: -5px;
z-index: -1;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 14px;
background: rgba(16, 20, 28, 0.82);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
content: '';
pointer-events: none;
}
/* Hovering an icon lights its label too, as on the left. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail:has(.cockpit-utility-rail__item:nth-child(1):hover)
.cockpit-utility-rail__labels
> span:nth-child(1),
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-rail:has(.cockpit-utility-rail__item:nth-child(2):hover)
.cockpit-utility-rail__labels
> span:nth-child(2) {
color: var(--text-primary, #fff);
}
/* Mirror the left rail's 52px inset, clear of the HUD's vertical readouts. */
:root:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-controls {
right: 52px;
}
/* Like the cockpit's other windows (rtl.css), the rail keeps its physical
side in a right-to-left language: icons at the screen edge, the open panel
inside them. Only the text reads right to left. */
html[dir='rtl']:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
.cockpit-utility-controls {
direction: ltr;
}
html[dir='rtl']:not([data-ui-theme='cyber'])
body.cockpit-mode:not(.mobile-shell)
:is(.cockpit-utility-control.is-expanded, .cockpit-utility-rail__labels) {
direction: rtl;
}
+18 -4
View File
@@ -9,10 +9,12 @@ import { useCockpitStore } from '../../stores/cockpit.js';
import { useDisplayStore } from '../../stores/display.js';
import UiButton from '../../kit/UiButton.vue';
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
// presents which popover is open and the compact Radio controls; until it
// does, both are closed and the controls show their initial state. The
// controls call RadioControls through the store. The Display popover's slots
// The Cockpit's Display and Radio launchers and their popovers, and the
// desktop look's rail, whose icons stay at the edge and open one panel at a
// time as the main page's rails do. RadioControls presents which popover is
// open and the compact Radio controls; until it does, both are closed and the
// controls show their initial state. The controls call RadioControls through
// the store. The Display popover's slots
// are static: the Display panel teleports its real controls into them while
// the Cockpit display portal publishes them.
const store = useRadioStore();
@@ -52,6 +54,7 @@ const radioLabel = computed(() =>
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
<span>{{ t('DISPLAY') }}</span>
<span class="cockpit-utility-divider" aria-hidden="true"></span>
<button v-if="open.display" type="button" class="cockpit-utility-collapse" :aria-label="t('Collapse panel')" :title="t('Collapse panel')" @click="act('toggleCockpitUtility', 'display', { returnFocus: true })">−</button>
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel">{{ open.display ? '▶' : '◀' }}</button>
</div>
<div id="cockpit-display-panel" class="cockpit-utility-popover ui-scroll" v-class-toggle="{ 'uses-shared-display-controls': !!cockpitPortal }" role="group" :aria-label="t('Cockpit display options')" :hidden="!open.display">
@@ -65,6 +68,7 @@ const radioLabel = computed(() =>
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
<span>{{ t('RADIO') }}</span>
<span class="cockpit-utility-divider" aria-hidden="true"></span>
<button v-if="open.radio" type="button" class="cockpit-utility-collapse" :aria-label="t('Collapse panel')" :title="t('Collapse panel')" @click="act('toggleCockpitUtility', 'radio', { returnFocus: true })">−</button>
<button id="cockpit-radio-toggle-btn" @click="act('toggleCockpitRadio')" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel">{{ open.radio ? '▶' : '◀' }}</button>
</div>
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" :aria-label="t('Cockpit compact Radio controls')" :hidden="!open.radio">
@@ -82,5 +86,15 @@ const radioLabel = computed(() =>
</label>
</div>
</div>
<div class="cockpit-utility-rail" :class="{ 'is-open': open.display || open.radio }">
<span class="cockpit-utility-rail__labels" aria-hidden="true">
<span>{{ t('DISPLAY') }}</span>
<span>{{ t('RADIO') }}</span>
</span>
<span class="cockpit-utility-rail__icons">
<button type="button" class="cockpit-utility-rail__item" data-cockpit-rail-item="display" :class="{ 'is-on': open.display }" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel" @click="act('toggleCockpitUtility', 'display')"><span class="material-symbols-outlined" aria-hidden="true">tune</span></button>
<button type="button" class="cockpit-utility-rail__item" data-cockpit-rail-item="radio" :class="{ 'is-on': open.radio }" :aria-expanded="String(open.radio)" aria-controls="cockpit-radio-panel" :aria-label="radioLabel" :title="radioLabel" @click="act('toggleCockpitUtility', 'radio')"><span class="material-symbols-outlined" aria-hidden="true">radio</span></button>
</span>
</div>
</aside>
</template>
+1
View File
@@ -50,6 +50,7 @@
@import './src/ui/styles/left-tools.css';
@import './src/ui/styles/panel-edge.css';
@import './src/ui/styles/visual-panel.css';
@import './src/ui/styles/cockpit-panels.css';
@import './src/ui/styles/ai-chat.css';
@import './src/ui/styles/scope-off-shadows.css';
@import './src/ui/styles/cyber.css';