feat(ui): render every application template except the loading screen as a Vue view

The scene chrome, cockpit, Display controls, command dock, layer panels,
Context rail, welcome card and Provider Settings move verbatim into
src/ui/views and mount into their hosts before the page is translated and
wired. Each view renders the same elements, attributes and text as its
template; Vue keeps whitespace between elements as one space, and no view
markup sits under a white-space: pre rule.

Template asset URLs stay literal, view files are left out of Prettier so
the markup is not reflowed, and the Node component loader moves to build/
and is exported as build/register-vue for host test runners. Hosts that do
not initialize Core's locale pass localePicker: false instead of editing
the markup.
This commit is contained in:
Sameh Khamis committed 2026-10-30 17:47:52 -07:00
1 parent 9d24945143
commit 6802c9c8b0
46 files changed
+258 -97

No files matched your search

+8
View File
@@ -77,6 +77,8 @@ export class StyleManager extends ShellFacade {
/**
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
* @param {object} [options]
* @param {boolean} [options.localePicker=true] - false removes the language
* picker, for a host that does not initialize Core's locale.
*/
constructor(
viewer,
@@ -86,9 +88,11 @@ export class StyleManager extends ShellFacade {
services,
requestServices,
initialShareCode = null,
localePicker = true,
} = {},
) {
super();
this._localePickerEnabled = localePicker;
const {
IntelHUD,
ShareLinkManager,
@@ -763,6 +767,10 @@ export class StyleManager extends ShellFacade {
];
const root = globalThis.document?.getElementById?.('locale-picker');
if (!root) return;
if (this._localePickerEnabled === false) {
root.remove();
return;
}
this._localePicker = mountLocalePicker({
root,
current: getLocale(),
+1 -1
View File
@@ -82,7 +82,7 @@ test('real runner and facade use visual settings, preserve visibility and return
});
test('native sonar sliders represent every accepted integer without browser snapping', () => {
const html = readFileSync(new URL('./templates/display-controls.html', import.meta.url), 'utf8');
const html = readFileSync(new URL('./views/DisplayControlsView.vue', import.meta.url), 'utf8');
for (const name of ['rings', 'range', 'intensity', 'opacity', 'sector']) {
assert.match(html, new RegExp(`id="cyber-sonar-${name}"[^>]*step="1"`));
}
+4 -4
View File
@@ -17,9 +17,9 @@ import { VisualSettings } from './visualSettings.js';
import { STYLES } from './visualPresets.js';
test('CCTV and Context keep their headers outside Cyber-only scroll bodies', () => {
for (const file of ['layer-panels.html', 'context.html']) {
for (const file of ['LayerPanelsView.vue', 'ContextView.vue']) {
const markup = readFileSync(
new URL(`./templates/${file}`, import.meta.url),
new URL(`./views/${file}`, import.meta.url),
'utf8',
);
assert.match(markup, /class="cyber-panel-body" data-rail-scroller/);
@@ -647,7 +647,7 @@ test('Cyber visual defaults update the style, thermal palette and render state',
});
test('the Display selector, voice schema and final stylesheet expose Cyber', () => {
const display = read('./templates/display-controls.html');
const display = read('./views/DisplayControlsView.vue');
const stylesheet = read('../../style.css');
const rightRail = read('./rightPanelRail.js');
const hud = read('../hud.js');
@@ -877,7 +877,7 @@ test('Cyber panels retain clean edges without the added footer band', () => {
test('Cyber keeps Sonar on/off without the removed CSS effect switch or dimmer', () => {
const template = readFileSync(
new URL('./templates/display-controls.html', import.meta.url),
new URL('./views/DisplayControlsView.vue', import.meta.url),
'utf8',
);
assert.match(template, /id="cyber-sonar-toggle"/);
+1 -1
View File
@@ -439,7 +439,7 @@ test('the application composition wires the import and owns its teardown', () =>
);
assert.ok(tools.indexOf('defer(() => geoFileImport?.destroy());') > importAt);
const markup = fs.readFileSync(
new URL('./templates/display-controls.html', import.meta.url),
new URL('./views/DisplayControlsView.vue', import.meta.url),
'utf8',
);
for (const id of [
+18
View File
@@ -136,3 +136,21 @@ test('the language already active neither asks nor reloads', () => {
view.restore();
}
});
test('a host without Core locales removes the picker instead of mounting it', () => {
const previous = globalThis.document;
let removed = 0;
const root = { remove: () => removed++ };
globalThis.document = {
getElementById: (id) => (id === 'locale-picker' ? root : null),
};
const shell = { services: {}, _localePickerEnabled: false };
try {
StyleManager.prototype._initLocaleSelector.call(shell);
assert.equal(removed, 1);
assert.equal(shell._localePicker, undefined);
} finally {
for (const unregister of shell._unsavedWorkChecks) unregister();
globalThis.document = previous;
}
});
+2 -2
View File
@@ -11,8 +11,8 @@ const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8');
test('every scroller the rail measuring pass lifts restores its scroll offset', () => {
const css = read('./styles/layers.css');
const templates = [
read('./templates/context.html'),
read('./templates/layer-panels.html'),
read('./views/ContextView.vue'),
read('./views/LayerPanelsView.vue'),
].join('\n');
const lifted = [
...css.matchAll(
+1 -1
View File
@@ -190,7 +190,7 @@ class FakeElement {
}
}
/** The static rail panel skeleton from templates/context.html. */
/** The static rail panel skeleton from views/ContextView.vue. */
function fakeDocument() {
const document = {
documentElement: { clientWidth: 1200 },
+1 -1
View File
@@ -988,7 +988,7 @@ test('a note under SINCE says what date it compares, only while one is given', (
assert.equal(note().hidden, true);
// The panel's own markup has it, read out with the slider.
const template = fs.readFileSync(
new URL('./templates/layer-panels.html', import.meta.url),
new URL('./views/LayerPanelsView.vue', import.meta.url),
'utf8',
);
assert.match(template, /<small id="sl-since-note"[^>]* hidden>/);
@@ -1,3 +1,4 @@
<template>
<!-- Aircraft first-person view. It can be entered from the visible view switcher. -->
<section id="cockpit-hud" aria-label="Aircraft cockpit view" hidden data-i18n-aria-label>
<!-- One optical coordinate system: every targeting mark shares this center.
@@ -326,3 +327,4 @@
<span data-i18n>EXIT COCKPIT</span>
</button>
</nav>
</template>
@@ -1,3 +1,4 @@
<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>
<!-- Control panel -->
@@ -91,3 +92,4 @@
</div>
</div>
</div>
</template>
@@ -1,3 +1,4 @@
<template>
<!-- Global Context is deliberately independent from the left layer accordion. -->
<aside id="right-context-rail">
<div id="weather-panel" class="panel-collapsible" data-panel-id="weather-panel" hidden>
@@ -214,3 +215,4 @@
</aside>
<div id="scene-runtime"></div>
</template>
@@ -1,3 +1,4 @@
<template>
<!-- Post-processing toggles (top-right) -->
<div id="pp-toggles" class="panel-collapsible collapsed" data-panel-id="pp-toggles">
<div class="compact pp-header-row">
@@ -184,3 +185,4 @@
</div>
</div>
<button id="clean-view-exit" title="Return UI controls" data-i18n-title data-i18n>EXIT CLEAN VIEW</button>
</template>
@@ -1,3 +1,4 @@
<template>
<div id="left-panel-stack">
<!-- Data Layer Toggle Panel -->
<div id="data-panel" class="panel-collapsible collapsed active" data-panel-id="data-panel">
@@ -185,3 +186,4 @@
</div>
</div>
</template>
@@ -1,3 +1,4 @@
<template>
<!-- POWER UP — in-app key setup. Dev-server only: src/keySetup.js removes
both the chip and this dialog outright when /api/setup/status is not
there to answer (prod builds, LAN visitors). Rows are rendered from
@@ -28,3 +29,4 @@
</div>
<p class="key-setup-note" data-key-setup-status role="status" aria-live="polite" data-i18n>The Google Maps key buys the photorealistic planet — everything else stacks on top.</p>
</aside>
</template>
@@ -1,3 +1,4 @@
<template>
<div id="cesiumContainer"></div>
<div id="world-overlay-root" aria-hidden="true">
<canvas id="world-overlay-canvas"></canvas>
@@ -59,3 +60,4 @@
<div id="safe-frame-overlay">
<div id="safe-frame-box"></div>
</div>
</template>
@@ -1,3 +1,4 @@
<template>
<!-- Explicit mission choice on a fresh session. Live feeds are not auto-enabled
until the visitor asks; shared links skip this surface entirely. It returns
every session until "Don't show this again" is ticked — see
@@ -56,3 +57,4 @@
a live region that also holds layout makes the row jump as it speaks. -->
<p class="first-run-note" data-first-run-status role="status" aria-live="polite" data-i18n>Tip: the GEV MIC button in the dock lets you talk to the map.</p>
</aside>
</template>
+18 -1
View File
@@ -1,11 +1,28 @@
import { createApp } from 'vue';
import { applicationPinia } from '../vueRuntime.js';
import WelcomeView from './WelcomeView.vue';
import ContextView from './ContextView.vue';
import LayerPanelsView from './LayerPanelsView.vue';
import CommandDockView from './CommandDockView.vue';
import DisplayControlsView from './DisplayControlsView.vue';
import CockpitView from './CockpitView.vue';
import SceneChromeView from './SceneChromeView.vue';
import ProviderSettingsView from './ProviderSettingsView.vue';
/**
* Application views by template name (build/applicationViews.js lists the
* same names). Each renders the markup its static template held.
*/
export const APPLICATION_VIEWS = Object.freeze({});
export const APPLICATION_VIEWS = Object.freeze({
'provider-settings': ProviderSettingsView,
'scene-chrome': SceneChromeView,
cockpit: CockpitView,
'display-controls': DisplayControlsView,
'command-dock': CommandDockView,
'layer-panels': LayerPanelsView,
context: ContextView,
welcome: WelcomeView,
});
const mounted = new WeakSet();