mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
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:
10 files changed
+846
-32
No files matched your search
@@ -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
@@ -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
|
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`
|
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
|
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
|
Visual, so Visual leaves out its own presets and chips there. In Cockpit,
|
||||||
groups Cockpit's Display popover takes
|
the groups that Cockpit's Display popover takes leave the panel as before and
|
||||||
leave the panel as before and keep their own styling there.
|
keep their own styling there.
|
||||||
|
|
||||||
Scope has an Opacity slider (`#scope-opacity-slider`, 50–100%, default 100%;
|
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
|
`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
|
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.
|
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
|
## 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,
|
Tool answers that can be shown in God's Eye View include a view: camera, layers,
|
||||||
|
|||||||
@@ -1461,5 +1461,6 @@
|
|||||||
"src/data/placementProvider.test.mjs",
|
"src/data/placementProvider.test.mjs",
|
||||||
"src/layers/vessels/placement.test.mjs",
|
"src/layers/vessels/placement.test.mjs",
|
||||||
"src/ui/styles/visual-panel.css",
|
"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
@@ -26,10 +26,7 @@ import {
|
|||||||
setSignalCollapsed,
|
setSignalCollapsed,
|
||||||
} from './ui/cockpitLayout.js';
|
} from './ui/cockpitLayout.js';
|
||||||
import { createBriefState } from './ui/cockpitBriefPages.js';
|
import { createBriefState } from './ui/cockpitBriefPages.js';
|
||||||
import {
|
import { publishBriefView, setBriefAutoRotate } from './ui/cockpitBriefing.js';
|
||||||
publishBriefView,
|
|
||||||
setBriefAutoRotate,
|
|
||||||
} from './ui/cockpitBriefing.js';
|
|
||||||
import { CockpitDisplayPortal } from './ui/cockpitDisplayPortal.js';
|
import { CockpitDisplayPortal } from './ui/cockpitDisplayPortal.js';
|
||||||
import { _handleContextLayerChange } from './ui/contextLayerChanges.js';
|
import { _handleContextLayerChange } from './ui/contextLayerChanges.js';
|
||||||
import { clearSelectedLayers } from './ui/contextActions.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"/,
|
/data-cockpit-launcher="radio"[\s\S]*?id="cockpit-radio-toggle-btn"/,
|
||||||
);
|
);
|
||||||
const cockpitRadio = html.match(
|
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.ok(cockpitRadio, 'Cockpit compact Radio controls are missing');
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(cockpitRadio[0], /radio-filter|radio-tuner/);
|
||||||
cockpitRadio[0],
|
|
||||||
/radio-filter|radio-tuner/,
|
|
||||||
);
|
|
||||||
assert.match(
|
assert.match(
|
||||||
css,
|
css,
|
||||||
/\.cockpit-utility-controls[\s\S]*?top:\s*var\(\s*--cockpit-utility-top/,
|
/\.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, /onReset: \(\) => this\.resetToGlobeView\(\)/);
|
||||||
assert.match(ui, /this\._connectGlobeReset\?\.\(this\._locationControls\)/);
|
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.
|
// Both reset buttons ask the one reset owner.
|
||||||
const globeActions = fs.readFileSync(
|
const globeActions = fs.readFileSync(
|
||||||
path.join(ROOT, 'src', 'ui', 'views', 'panels', 'GlobeActions.vue'),
|
path.join(ROOT, 'src', 'ui', 'views', 'panels', 'GlobeActions.vue'),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
assert.match(globeActions, /id="reset-globe-view"[^>]*@click="act\('reset'\)"/);
|
assert.match(
|
||||||
|
globeActions,
|
||||||
|
/id="reset-globe-view"[^>]*@click="act\('reset'\)"/,
|
||||||
|
);
|
||||||
const cockpitView = readRadioSource(
|
const cockpitView = readRadioSource(
|
||||||
new URL('./ui/views/CockpitView.vue', import.meta.url),
|
new URL('./ui/views/CockpitView.vue', import.meta.url),
|
||||||
'utf8',
|
'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(
|
assert.match(
|
||||||
cockpitEnter.toString(),
|
cockpitEnter.toString(),
|
||||||
/Object\.assign\(this\.presence, \{\s*active: true,/,
|
/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>/);
|
const match = html.match(/<aside id="cockpit-context"[\s\S]*?<\/aside>/);
|
||||||
assert.ok(match, 'cockpit Contact summary is missing');
|
assert.ok(match, 'cockpit Contact summary is missing');
|
||||||
assert.match(match[0], /aria-label="Contact cockpit summary"/);
|
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], /aria-label="Contact navigation"/);
|
||||||
assert.match(
|
assert.match(
|
||||||
match[0],
|
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 previous = globalThis.document;
|
||||||
const panels = {};
|
const panels = {};
|
||||||
globalThis.document = { getElementById: (id) => panels[id] ?? null };
|
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 {
|
try {
|
||||||
const otherOpen = (id) => PanelChrome.prototype._otherCockpitLeftPanelOpen(id);
|
const otherOpen = (id) =>
|
||||||
|
PanelChrome.prototype._otherCockpitLeftPanelOpen(id);
|
||||||
panels['data-panel'] = panel('collapsed');
|
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');
|
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();
|
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 {
|
} finally {
|
||||||
globalThis.document = previous;
|
globalThis.document = previous;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ export const COCKPIT_UTILITY_REC_GAP_PX = 12;
|
|||||||
|
|
||||||
export const COCKPIT_UTILITY_SIGNAL_GAP_PX = 8;
|
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;
|
export const COCKPIT_UTILITY_LAUNCHER_MIN_HEIGHT_PX = 50;
|
||||||
|
|
||||||
|
|||||||
@@ -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(cockpit.utilityLayout.top, '120.00px');
|
||||||
assert.equal(strip.classList.contains('layout-primary-only'), false);
|
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
@@ -27,10 +27,18 @@ export function bindRadioControls() {
|
|||||||
this.store?.publishUtility({ ...this._cockpitDisclosure });
|
this.store?.publishUtility({ ...this._cockpitDisclosure });
|
||||||
if (displayOpen) this.actions.revealStyleParameters();
|
if (displayOpen) this.actions.revealStyleParameters();
|
||||||
if (!expanded && returnFocus) {
|
if (!expanded && returnFocus) {
|
||||||
(kind === 'display'
|
// The desktop cockpit hides the launchers' toggles behind its rail, so
|
||||||
? this._cockpitDisplayToggleBtn
|
// focus goes to whichever of the two is shown.
|
||||||
: this._cockpitRadioToggleBtn
|
const toggle =
|
||||||
)?.focus({ preventScroll: true });
|
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 (
|
if (
|
||||||
!displayOpen &&
|
!displayOpen &&
|
||||||
@@ -444,6 +452,16 @@ export function bindRadioControls() {
|
|||||||
),
|
),
|
||||||
toggleCockpitRadio: () =>
|
toggleCockpitRadio: () =>
|
||||||
setCockpitDisclosure('radio', !this._cockpitDisclosureOpen('radio')),
|
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
|
* @param {string} filter
|
||||||
* @returns {string} The station tag the filter shows afterwards.
|
* @returns {string} The station tag the filter shows afterwards.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,10 +9,12 @@ import { useCockpitStore } from '../../stores/cockpit.js';
|
|||||||
import { useDisplayStore } from '../../stores/display.js';
|
import { useDisplayStore } from '../../stores/display.js';
|
||||||
import UiButton from '../../kit/UiButton.vue';
|
import UiButton from '../../kit/UiButton.vue';
|
||||||
|
|
||||||
// The Cockpit's Display and Radio launchers and their popovers. RadioControls
|
// The Cockpit's Display and Radio launchers and their popovers, and the
|
||||||
// presents which popover is open and the compact Radio controls; until it
|
// desktop look's rail, whose icons stay at the edge and open one panel at a
|
||||||
// does, both are closed and the controls show their initial state. The
|
// time as the main page's rails do. RadioControls presents which popover is
|
||||||
// controls call RadioControls through the store. The Display popover's slots
|
// 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
|
// are static: the Display panel teleports its real controls into them while
|
||||||
// the Cockpit display portal publishes them.
|
// the Cockpit display portal publishes them.
|
||||||
const store = useRadioStore();
|
const store = useRadioStore();
|
||||||
@@ -52,6 +54,7 @@ const radioLabel = computed(() =>
|
|||||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
||||||
<span>{{ t('DISPLAY') }}</span>
|
<span>{{ t('DISPLAY') }}</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></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>
|
<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>
|
||||||
<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">
|
<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">
|
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
||||||
<span>{{ t('RADIO') }}</span>
|
<span>{{ t('RADIO') }}</span>
|
||||||
<span class="cockpit-utility-divider" aria-hidden="true"></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>
|
<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>
|
||||||
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" :aria-label="t('Cockpit compact Radio controls')" :hidden="!open.radio">
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
@import './src/ui/styles/left-tools.css';
|
@import './src/ui/styles/left-tools.css';
|
||||||
@import './src/ui/styles/panel-edge.css';
|
@import './src/ui/styles/panel-edge.css';
|
||||||
@import './src/ui/styles/visual-panel.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/ai-chat.css';
|
||||||
@import './src/ui/styles/scope-off-shadows.css';
|
@import './src/ui/styles/scope-off-shadows.css';
|
||||||
@import './src/ui/styles/cyber.css';
|
@import './src/ui/styles/cyber.css';
|
||||||
Reference in new issue
Block a user