mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
1 parent
9d24945143
commit
6802c9c8b0
46 files changed
+258
-97
No files matched your search
@@ -62,9 +62,9 @@ test('the engine resolves manual geometry before any name resolution', () => {
|
||||
});
|
||||
|
||||
test('the Draw control markup carries the ids the tool binds to', () => {
|
||||
// The DISPLAY rail is a component template now, assembled into index.html at
|
||||
// build time — the markup is not in index.html any more.
|
||||
const html = read('src/ui/templates/display-controls.html');
|
||||
// The DISPLAY rail is a Vue view now, rendered into index.html's view host —
|
||||
// the markup is not in index.html any more.
|
||||
const html = read('src/ui/views/DisplayControlsView.vue');
|
||||
const ids = [
|
||||
'draw-toggle',
|
||||
'draw-mode-row',
|
||||
|
||||
@@ -31,6 +31,8 @@ test('importing and constructing the package is inactive without browser globals
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
// Views are single-file components; Node needs the loader hosts use.
|
||||
'--import=gods-eye-view/build/register-vue',
|
||||
'--input-type=module',
|
||||
'-e',
|
||||
`
|
||||
|
||||
Vendored
+4
-1
@@ -7,7 +7,8 @@ import { t } from '../i18n/index.js';
|
||||
/**
|
||||
* Construct the existing controls and camera presentation. `initialShareCode`
|
||||
* opens a share code (the `<code>` of `/share/<code>`) at start-up, for a host
|
||||
* whose address does not carry one.
|
||||
* whose address does not carry one. `localePicker: false` removes the language
|
||||
* picker, for a host that does not initialize Core's locale.
|
||||
*/
|
||||
export function createApplicationControls({
|
||||
scene: { viewer, mapStackController, operations },
|
||||
@@ -18,6 +19,7 @@ export function createApplicationControls({
|
||||
placeSearch,
|
||||
defer,
|
||||
initialShareCode = null,
|
||||
localePicker = true,
|
||||
}) {
|
||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||
const styleManager = new Controls(viewer, {
|
||||
@@ -34,6 +36,7 @@ export function createApplicationControls({
|
||||
placeSearch,
|
||||
// A share code to open at start-up instead of the page's address.
|
||||
initialShareCode,
|
||||
localePicker,
|
||||
});
|
||||
defer(() => styleManager.orbitController.stop());
|
||||
defer(() => styleManager.hud.destroy());
|
||||
|
||||
@@ -816,7 +816,7 @@ test('markup, startup ordering and accessibility remain pinned', async () => {
|
||||
html,
|
||||
/data-first-run-status[^>]*role="status"[^>]*aria-live="polite"/,
|
||||
);
|
||||
assert.match(html, /<input type="checkbox" data-first-run-suppress \/>/);
|
||||
assert.match(html, /<input type="checkbox" data-first-run-suppress\s*\/?>/);
|
||||
assert.match(html, /<strong data-first-run-environmental-title>/);
|
||||
// Subcopy must name BOTH feeds the tile turns on — a tile that promised only
|
||||
// half of what it does is the defect this replaced. Only the VISIBLE <small>
|
||||
|
||||
@@ -24,7 +24,7 @@ test('HUD sliders and location search have descriptive explicit names', () => {
|
||||
test('the first-run checkbox keeps its native visible label', () => {
|
||||
// The span may carry a data-i18n extraction attribute; the native wrapping
|
||||
// label contract is what this guards.
|
||||
assert.match(html, /<label\b[^>]*class="first-run-suppress"[^>]*>\s*<input type="checkbox" data-first-run-suppress \/>\s*<span\b[^>]*>Don't show this again<\/span>\s*<\/label>/);
|
||||
assert.match(html, /<label\b[^>]*class="first-run-suppress"[^>]*>\s*<input type="checkbox" data-first-run-suppress\s*\/?>\s*<span\b[^>]*>Don't show this again<\/span>\s*<\/label>/);
|
||||
});
|
||||
|
||||
test('generated style sliders use the visible parameter label as their name', () => {
|
||||
|
||||
@@ -20,9 +20,8 @@ const ICON_PROPERTY = /\bicon:\s*['"]([a-z0-9_]+)['"]/g;
|
||||
/**
|
||||
* Files that SHIP markup. Tests assert on markup, they do not render it.
|
||||
*
|
||||
* The panel markup lives in `src/ui/templates/*.html`, so HTML under `src` is
|
||||
* read as well: a glyph named only in a template is still a glyph the font has
|
||||
* to carry.
|
||||
* The panel markup lives in Vue views under `src`, so markup there is read as
|
||||
* well: a glyph named only in a view is still a glyph the font has to carry.
|
||||
*/
|
||||
function sourceFiles(directory = SRC_ROOT) {
|
||||
const files = [];
|
||||
|
||||
@@ -286,7 +286,12 @@ test('Radio volume and mission speed share the Sharpen slider visual language',
|
||||
test('Radio is nested inside Context with its own disclosure and power controls', () => {
|
||||
const contextStart = html.indexOf('id="global-context-panel"');
|
||||
const radioStart = html.indexOf('id="radio-panel"');
|
||||
const contextEnd = html.indexOf('\n </aside>', contextStart);
|
||||
// The Context panel ends with the right rail's own closing tag, after the
|
||||
// military awareness aside nested inside it.
|
||||
const contextEnd = html.indexOf(
|
||||
'</aside>',
|
||||
html.indexOf('</aside>', html.indexOf('id="military-awareness-panel"')) + 1,
|
||||
);
|
||||
assert.ok(contextStart >= 0 && radioStart > contextStart && radioStart < contextEnd);
|
||||
assert.match(html, /id="radio-panel"[^>]*data-panel-id="radio-panel"/);
|
||||
assert.match(html, /aria-label="Radio playback"/);
|
||||
|
||||
@@ -92,7 +92,7 @@ test('first run opens with Scope ON and 100% feather at every default surface',
|
||||
assert.match(indexHtml, /id="scope-feather-value"[^>]*>100%</,
|
||||
'index.html: and its readout agrees with the handle');
|
||||
|
||||
assert.match(indexHtml, /<input type="checkbox" id="scope-toggle"[^>]*\schecked\s*\/>/,
|
||||
assert.match(indexHtml, /<input type="checkbox" id="scope-toggle"[^>]*\schecked\s*\/?>/,
|
||||
'the native Scope checkbox starts checked');
|
||||
|
||||
// The link this session generates must describe the mask this session draws,
|
||||
|
||||
@@ -20,13 +20,14 @@ export async function renderApplicationHtml(html) {
|
||||
const view = APPLICATION_VIEWS[name];
|
||||
if (!view) throw new Error(`Unknown application view: ${name}`);
|
||||
const markup = (await renderToString(createSSRApp(view).use(createPinia())))
|
||||
// Hydration markers and empty-attribute values are SSR detail; the
|
||||
// DOM is the same as the template's.
|
||||
// Hydration markers, empty-attribute values and escaped apostrophes
|
||||
// are SSR detail; the DOM is the same as the template's.
|
||||
.replace(/<!--\[-->|<!--\]-->|<!---->/g, '')
|
||||
.replace(/ ([\w:.-]+)=""/g, ' $1');
|
||||
.replace(/ ([\w:.-]+)=""/g, ' $1')
|
||||
.replaceAll(''', "'");
|
||||
output = output.replace(
|
||||
host,
|
||||
`<div class="gev-view" data-gev-view="${name}">${markup}</div>`,
|
||||
() => `<div class="gev-view" data-gev-view="${name}">${markup}</div>`,
|
||||
);
|
||||
}
|
||||
return output;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { register } from 'node:module';
|
||||
|
||||
// Lets unit tests import single-file components (`node --import`).
|
||||
register('./vueLoader.mjs', import.meta.url);
|
||||
@@ -1,48 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { compileScript, compileTemplate, parse } from 'vue/compiler-sfc';
|
||||
import { VUE_COMPILER_OPTIONS } from '../../build/vueOptions.js';
|
||||
|
||||
/**
|
||||
* Compile a single-file component to an ES module for Node's test runner.
|
||||
* Styles are dropped: tests read the global stylesheet separately.
|
||||
*/
|
||||
export function compileVueModule(source, filename) {
|
||||
const { descriptor, errors } = parse(source, { filename });
|
||||
if (errors.length) throw errors[0];
|
||||
const id = filename;
|
||||
if (descriptor.scriptSetup) {
|
||||
return compileScript(descriptor, {
|
||||
id,
|
||||
inlineTemplate: true,
|
||||
templateOptions: { compilerOptions: VUE_COMPILER_OPTIONS },
|
||||
}).content;
|
||||
}
|
||||
const script = descriptor.script
|
||||
? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content
|
||||
: 'const __sfc__ = {};';
|
||||
let template = '';
|
||||
if (descriptor.template) {
|
||||
const compiled = compileTemplate({
|
||||
source: descriptor.template.content,
|
||||
filename,
|
||||
id,
|
||||
compilerOptions: VUE_COMPILER_OPTIONS,
|
||||
});
|
||||
if (compiled.errors.length) throw compiled.errors[0];
|
||||
template = compiled.code;
|
||||
}
|
||||
return `${script}\n${template}\nif (typeof render === 'function') __sfc__.render = render;\nexport default __sfc__;\n`;
|
||||
}
|
||||
|
||||
/** Node module hook: load `.vue` files as compiled modules. */
|
||||
export async function load(url, context, nextLoad) {
|
||||
if (!url.endsWith('.vue')) return nextLoad(url, context);
|
||||
const filename = fileURLToPath(url);
|
||||
const source = await readFile(filename, 'utf8');
|
||||
return {
|
||||
format: 'module',
|
||||
source: compileVueModule(source, filename),
|
||||
shortCircuit: true,
|
||||
};
|
||||
}
|
||||
@@ -78,7 +78,7 @@ test('the documented outcomes hold: default, stored open, stored shut, and a sha
|
||||
const { PanelPositionControls } =
|
||||
await import('../ui/panelPositionControls.js');
|
||||
const html = await readFile(
|
||||
path.join(root, 'src/ui/templates/layer-panels.html'),
|
||||
path.join(root, 'src/ui/views/LayerPanelsView.vue'),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(
|
||||
|
||||
@@ -208,7 +208,7 @@ test('DETECT uses the real OFF/restore button and density input to reach DENSE',
|
||||
assert.equal(await detectOn(page, pause), 'DENSE');
|
||||
assert.deepEqual(modes, ['BALANCED', 'OFF', 'DENSE']);
|
||||
const template = readFileSync(
|
||||
new URL('../ui/templates/display-controls.html', import.meta.url),
|
||||
new URL('../ui/views/DisplayControlsView.vue', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(template, /id="detection-density-slider"/);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"`));
|
||||
}
|
||||
|
||||
@@ -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"/);
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user