feat(ui): render the active style in the dock and the ACTIVE STYLE corner from the store

VisualSettings no longer toggles the style buttons' active class or writes
the dock's style mini status and the ACTIVE STYLE corner. It publishes the
active style and its label to the gev-display store; CommandDockView and
SceneChromeView render them. The buttons keep their owner's listeners, and
the initial label renders through t().
This commit is contained in:
Sameh Khamis committed 2026-10-31 12:05:19 -07:00
1 parent 66a60ff67e
commit decac29fbd
11 files changed
+146 -39

No files matched your search

+4 -1
View File
@@ -115,7 +115,10 @@ They contain no application state.
publishes through `subscribePanel()`, the first layer to hand its rail
content to a view, and `MissionRoster` and `MissionDetail` the Space
Missions roster and selected mission that the launches layer publishes
the same way), the command dock,
the same way), the command dock (in progress: the style buttons, the
dock's style mini status and the ACTIVE STYLE corner render the style
VisualSettings publishes to the `gev-display` store; the buttons keep
their owner's listeners),
the phone shell, then Cockpit. Layers that write into the right rail
directly gain a slot instead.
+6 -5
View File
@@ -20,16 +20,17 @@ function locationSearchHandler() {
test('the ACTIVE STYLE indicator is written from the style name and nothing else', () => {
// A free-text location search used to write the searched CITY into the
// top-right style slot, so the corner read "ACTIVE STYLE / TOKYO".
const writes = [...ui.matchAll(/this\._styleIndicator\.textContent\s*=/g)];
// top-right style slot, so the corner read "ACTIVE STYLE / TOKYO". The
// corner renders the style VisualSettings publishes, from one writer.
const writes = [...ui.matchAll(/\.publishStyle\(/g)];
assert.equal(writes.length, 1, 'the style indicator must have exactly one writer');
assert.match(
ui.slice(writes[0].index, writes[0].index + 160),
/this\._styleIndicator\.textContent = STYLE_STATUS_LABELS\[styleName\]\s*\?\s*t\(STYLE_STATUS_LABELS\[styleName\]\)\s*:\s*styleName\.toUpperCase\(\);/,
ui.slice(writes[0].index, writes[0].index + 200),
/\.publishStyle\(\{\s*name,\s*label: STYLE_STATUS_LABELS\[name\]\s*\?\s*t\(STYLE_STATUS_LABELS\[name\]\)\s*:\s*name\.toUpperCase\(\),/,
);
const handler = locationSearchHandler();
assert.doesNotMatch(handler, /_styleIndicator/, 'location search must not touch the style indicator');
assert.doesNotMatch(handler, /publishStyle|_publishActiveStyle/, 'location search must not touch the style indicator');
assert.doesNotMatch(handler, /active-style-name/, 'location search must not touch the style indicator');
});
+1 -3
View File
@@ -213,8 +213,6 @@ export class StyleManager extends ShellFacade {
_ppToggles: this._ppToggles,
_sliderContainer: this._sliderContainer,
_sliderPanel: this._sliderPanel,
_styleIndicator: this._styleIndicator,
_styleMiniValue: this._styleMiniValue,
},
operations: {
_restorePanelState: (...args) => this._restorePanelState(...args),
@@ -586,7 +584,7 @@ export class StyleManager extends ShellFacade {
this._initRecordingOverlay();
this._startAnimationLoop();
this._startTrafficChipTicker();
this._updateStyleMiniStatus();
this._publishActiveStyle();
this._updateLocationMiniStatus();
this._shareRestoration.start();
+81
View File
@@ -0,0 +1,81 @@
// src/ui/commandDock.test.mjs — the command dock and the scene chrome render
// what VisualSettings publishes, over happy-dom.
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createPinia } from 'pinia';
import { createApp, h, nextTick } from 'vue';
import { setLocale } from '../i18n/index.js';
import { useDisplayStore } from './stores/display.js';
import CommandDockView from './views/CommandDockView.vue';
import SceneChromeView from './views/SceneChromeView.vue';
import { VisualSettings } from './visualSettings.js';
function fixture(t) {
document.body.replaceChildren();
const host = document.body.appendChild(document.createElement('div'));
const pinia = createPinia();
const app = createApp({
render: () => [h(SceneChromeView), h(CommandDockView)],
}).use(pinia);
app.mount(host);
t.after(() => app.unmount());
const store = useDisplayStore(pinia);
// VisualSettings publishes through its store; the method needs no more.
const settings = Object.assign(Object.create(VisualSettings.prototype), {
store,
activeStyle: 'normal',
});
const $ = (selector) => document.querySelector(selector);
const active = () =>
[...document.querySelectorAll('.style-btn.active')].map(
(button) => button.dataset.style,
);
return { $, settings, active };
}
test('before VisualSettings publishes, the dock shows NORMAL', (t) => {
const { $, active } = fixture(t);
assert.deepEqual(active(), ['normal']);
assert.equal($('#style-mini-value').textContent, 'NORMAL');
assert.equal($('#active-style-name').textContent, 'NORMAL');
});
test('the active style marks its button and labels the dock and the corner', async (t) => {
const { $, settings, active } = fixture(t);
settings._publishActiveStyle('thermal');
await nextTick();
assert.deepEqual(active(), ['thermal']);
assert.equal($('#style-mini-value').textContent, 'FLIR');
assert.equal($('#active-style-name').textContent, 'FLIR');
settings.activeStyle = 'unlisted';
settings._publishActiveStyle();
await nextTick();
assert.deepEqual(active(), []);
assert.equal($('#style-mini-value').textContent, 'UNLISTED');
settings._publishActiveStyle('normal');
await nextTick();
assert.deepEqual(active(), ['normal']);
});
test('the style buttons keep classes other code sets', async (t) => {
const { $, settings } = fixture(t);
const noir = $('.style-btn[data-style="noir"]');
noir.classList.add('voice-highlight');
settings._publishActiveStyle('noir');
await nextTick();
assert.equal(noir.classList.contains('voice-highlight'), true);
assert.equal(noir.classList.contains('active'), true);
});
test('the dock and the corner label the style in the UI language', async (t) => {
t.after(() => setLocale('en'));
setLocale('ru');
const { $, settings } = fixture(t);
assert.equal($('#style-mini-value').textContent, 'ОБЫЧНЫЙ');
assert.equal($('#active-style-name').textContent, 'ОБЫЧНЫЙ');
settings._publishActiveStyle('noir');
await nextTick();
assert.equal($('#style-mini-value').textContent, 'НУАР');
assert.equal($('#active-style-name').textContent, 'НУАР');
});
+1 -2
View File
@@ -77,7 +77,6 @@ function styleOwner(t, initialVariant = 'cyber') {
),
},
services: { governorRequestRender() {}, setDetectionStyle() {} },
_styleIndicator: {},
_setBloomEnabled: (value) => calls.push(['bloom', value]),
_setSharpenEnabled: (value) => calls.push(['sharpen', value]),
_applyDetectionPreset: (value) => calls.push(['detection', value]),
@@ -86,7 +85,7 @@ function styleOwner(t, initialVariant = 'cyber') {
for (const name of [
'setCelestialRingEnabled',
'_startTransition',
'_updateStyleMiniStatus',
'_publishActiveStyle',
'_updateSliderPanel',
'_updateHudButtonState',
'_syncIrBoost',
-2
View File
@@ -2,7 +2,6 @@
export function readShellElements(document = globalThis.document) {
return {
_rightPanelStack: document.getElementById('right-context-rail'),
_styleIndicator: document.getElementById('active-style-name'),
_sliderPanel: document.getElementById('param-slider-panel'),
_sliderContainer: document.getElementById('param-sliders'),
_ppToggles: document.getElementById('pp-toggles'),
@@ -132,7 +131,6 @@ export function readShellElements(document = globalThis.document) {
_locationPills: document.getElementById('location-pills'),
_poiRow: document.getElementById('poi-row'),
_locationBarDivider: document.getElementById('location-bar-divider'),
_styleMiniValue: document.getElementById('style-mini-value'),
_locationMiniCity: document.getElementById('location-mini-city'),
_locationMiniPoi: document.getElementById('location-mini-poi'),
_safeFrameOverlay: document.getElementById('safe-frame-overlay'),
+2 -2
View File
@@ -1304,8 +1304,8 @@ export class ShellFacade {
* @param {string} [styleName=this.activeStyle] - Style name to display.
* @returns {void}
*/
_updateStyleMiniStatus(styleName = this.activeStyle) {
return this._visualSettings._updateStyleMiniStatus(...arguments);
_publishActiveStyle(styleName = this.activeStyle) {
return this._visualSettings._publishActiveStyle(...arguments);
}
_initOrbit(...args) {
+12 -1
View File
@@ -5,7 +5,8 @@ import { markRaw, shallowRef } from 'vue';
* The Display panel as its view renders it. VisualSettings presents its
* groups (`_presentDisplay()`), AircraftDisplay the 3D aircraft group, the
* draw tool its controls and the file import its list, and each publishes
* here; each keeps its listeners on the controls.
* here; each keeps its listeners on the controls. VisualSettings also
* publishes the active style, which the dock and the scene chrome show.
*/
export const useDisplayStore = defineStore('gev-display', () => {
/** The presented panel; null until VisualSettings first renders. */
@@ -40,6 +41,14 @@ export const useDisplayStore = defineStore('gev-display', () => {
geoImport.value = markRaw(next);
}
/** The active visual style; null until VisualSettings first presents it. */
const style = shallowRef(null);
/** @param {{name: string, label: string}} next The active style and its label. */
function publishStyle(next) {
style.value = markRaw(next);
}
return {
view,
publish,
@@ -49,5 +58,7 @@ export const useDisplayStore = defineStore('gev-display', () => {
publishDraw,
geoImport,
publishGeoImport,
style,
publishStyle,
};
});
+18 -7
View File
@@ -1,3 +1,14 @@
<script setup>
import { storeToRefs } from 'pinia';
import { t } from '../../i18n/index.js';
import { vClassToggle } from '../rail/classToggle.js';
import { useDisplayStore } from '../stores/display.js';
// VisualSettings publishes the active style; until it does, the dock shows
// its initial markup. The style buttons keep their owner's listeners.
const { style } = storeToRefs(useDisplayStore());
</script>
<template>
<!-- Unified bottom command dock: locations, voice (injected at runtime), visual presets -->
<div id="command-dock" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
@@ -14,32 +25,32 @@
<div id="control-panel-popover" class="dock-popover-content">
<button class="dock-pin-btn" data-pin-target="control-panel" type="button" aria-label="Pin visual presets" aria-pressed="false" title="Keep visual presets open" data-i18n-aria-label data-i18n-title><img src="/pin.svg" alt="" /></button>
<div id="style-buttons" class="button-grid">
<button class="style-btn active" data-style="normal" title="Show the globe without a visual filter." data-i18n-title>
<button class="style-btn active" data-style="normal" v-class-toggle="{ ...(style && { active: style.name === 'normal' }) }" title="Show the globe without a visual filter." data-i18n-title>
<span class="btn-icon">◯</span>
<span class="btn-label" data-i18n>Normal</span>
<span class="btn-key">1</span>
</button>
<button class="style-btn" data-style="retro" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title>
<button class="style-btn" data-style="retro" v-class-toggle="{ ...(style && { active: style.name === 'retro' }) }" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title>
<span class="btn-icon">▦</span>
<span class="btn-label" data-i18n>CRT</span>
<span class="btn-key">2</span>
</button>
<button class="style-btn" data-style="surveillance" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title>
<button class="style-btn" data-style="surveillance" v-class-toggle="{ ...(style && { active: style.name === 'surveillance' }) }" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title>
<span class="btn-icon">🌙</span>
<span class="btn-label" data-i18n>NVG</span>
<span class="btn-key">3</span>
</button>
<button class="style-btn" data-style="thermal" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title>
<button class="style-btn" data-style="thermal" v-class-toggle="{ ...(style && { active: style.name === 'thermal' }) }" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title>
<span class="btn-icon">🌡️</span>
<span class="btn-label" data-i18n>FLIR</span>
<span class="btn-key">4</span>
</button>
<button class="style-btn" data-style="anime" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title>
<button class="style-btn" data-style="anime" v-class-toggle="{ ...(style && { active: style.name === 'anime' }) }" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title>
<span class="btn-icon">✦</span>
<span class="btn-label" data-i18n>Anime</span>
<span class="btn-key">5</span>
</button>
<button class="style-btn" data-style="noir" title="Apply high-contrast monochrome film-noir grading." data-i18n-title>
<button class="style-btn" data-style="noir" v-class-toggle="{ ...(style && { active: style.name === 'noir' }) }" title="Apply high-contrast monochrome film-noir grading." data-i18n-title>
<span class="btn-icon">◐</span>
<span class="btn-label" data-i18n>Noir</span>
<span class="btn-key">6</span>
@@ -55,7 +66,7 @@
</div>
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
<span class="panel-mini-label" data-i18n>Style</span>
<span id="style-mini-value" class="panel-mini-value" data-i18n>NORMAL</span>
<span id="style-mini-value" class="panel-mini-value">{{ style ? style.label : t('NORMAL') }}</span>
</div>
</div>
</div>
+10 -1
View File
@@ -1,3 +1,12 @@
<script setup>
import { storeToRefs } from 'pinia';
import { t } from '../../i18n/index.js';
import { useDisplayStore } from '../stores/display.js';
// The ACTIVE STYLE corner shows the style VisualSettings publishes.
const { style } = storeToRefs(useDisplayStore());
</script>
<template>
<div id="cesiumContainer"></div>
<div id="world-overlay-root" aria-hidden="true">
@@ -18,7 +27,7 @@
<!-- Style indicator -->
<div id="style-indicator">
<span class="indicator-label scene-text-holdout" data-i18n>ACTIVE STYLE</span>
<span class="indicator-value scene-text-holdout" id="active-style-name" data-i18n>NORMAL</span>
<span class="indicator-value scene-text-holdout" id="active-style-name">{{ style ? style.label : t('NORMAL') }}</span>
</div>
<!-- Top-center camera and share actions -->
+11 -15
View File
@@ -1517,16 +1517,8 @@ export class VisualSettings {
this._applyStylePresetDefaults(styleName);
}
// Update button UI
document.querySelectorAll('.style-btn').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.style === styleName);
});
// Update style indicator
this._styleIndicator.textContent = STYLE_STATUS_LABELS[styleName]
? t(STYLE_STATUS_LABELS[styleName])
: styleName.toUpperCase();
this._updateStyleMiniStatus(styleName);
// The style buttons, the dock's mini status and the ACTIVE STYLE corner.
this._publishActiveStyle(styleName);
// Update parameter sliders
this._updateSliderPanel(styleName, { reveal: revealParameters });
@@ -1555,11 +1547,15 @@ export class VisualSettings {
this._visualEffects.startTransition(styleName, fromValue, toValue);
}
_updateStyleMiniStatus(styleName = this.activeStyle) {
if (!this._styleMiniValue) return;
this._styleMiniValue.textContent = STYLE_STATUS_LABELS[styleName]
? t(STYLE_STATUS_LABELS[styleName])
: String(styleName || 'normal').toUpperCase();
/** Publish the active style and its label in the UI language. */
_publishActiveStyle(styleName = this.activeStyle) {
const name = String(styleName || 'normal');
this.store.publishStyle({
name,
label: STYLE_STATUS_LABELS[name]
? t(STYLE_STATUS_LABELS[name])
: name.toUpperCase(),
});
}
_updateHudButtonState() {