feat(ui): render application templates as Vue views mounted before wiring

Templates listed in build/applicationViews.js expand to mount hosts, and
mountApplicationViews() renders them before translation and wiring. Markup
tests read the page through a server-side render helper, the i18n scanner
reads view templates, and Vue preserves whitespace so views keep the exact
DOM. No template is converted yet.
This commit is contained in:
Sameh Khamis committed 2026-10-30 17:23:45 -07:00
1 parent b7e7058a5c
commit 9d24945143
32 files changed
+243 -63

No files matched your search

+3
View File
@@ -1,3 +1,4 @@
import { mountApplicationViews } from '../ui/views/index.js';
const START_ORDER = ['scene', 'controls', 'data', 'tools'];
// Controls cancel restoration while the data manager and viewer still exist.
const STOP_ORDER = ['tools', 'controls', 'data', 'scene'];
@@ -15,6 +16,8 @@ export function createApplication({
createData,
createTools,
}) {
// A host's page needs its views before any phase reads the markup.
mountApplicationViews();
const factories = {
scene: createScene,
controls: createControls,
+2 -2
View File
@@ -2,7 +2,7 @@ import {
readShellSource,
shellMethod,
} from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readLayerSource } from './testSupport/readLayerSource.mjs';
import { PanelLayoutController } from './ui/panelLayoutController.js';
import { readShellElements } from './ui/shellElements.js';
@@ -46,7 +46,7 @@ import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const html = expandApplicationHtml(
const html = await renderApplicationHtml(
fs.readFileSync(path.join(ROOT, 'index.html'), 'utf8'),
);
const ui = readShellSource();
+2 -2
View File
@@ -1,4 +1,4 @@
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
import { _selectContextMode } from './ui/contextTransactions.js';
import { _syncContextModeButtons } from './ui/contextPresentation.js';
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const css = readStylesheet(new URL('../style.css', import.meta.url));
test('Contacts and Space Missions both participate in the ordinary Tab sequence', () => {
+2 -2
View File
@@ -1,4 +1,4 @@
import { expandApplicationHtml } from '../../build/application-html.js';
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -19,7 +19,7 @@ import {
} from './detectionPolicy.js';
import { KEYHOLE_OUTSIDE_OPACITY_DEFAULT } from '../celestialRing.js';
const indexHtml = expandApplicationHtml(fs.readFileSync(new URL('../../index.html', import.meta.url), 'utf8'));
const indexHtml = await renderApplicationHtml(fs.readFileSync(new URL('../../index.html', import.meta.url), 'utf8'));
test('side aircraft brackets stay readable without changing zero-opacity intent', () => {
assert.equal(detectionBracketAlpha('AIR', 0), 0);
+2 -2
View File
@@ -1,6 +1,6 @@
import { encodeParams, decodeParams } from '../layers/streetLevel/params.js';
import { readShellSource } from '../testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../../build/application-html.js';
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -734,7 +734,7 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an
/this\.(?:flightState\.)?_models3dMode = 'proximity';/,
'ui.js: and believes the mode is proximity',
);
const html = expandApplicationHtml(
const html = await renderApplicationHtml(
await readFile(new URL('../../index.html', import.meta.url), 'utf8'),
);
assert.match(
+5 -5
View File
@@ -1,4 +1,4 @@
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
import test from 'node:test';
@@ -797,8 +797,8 @@ test('the decision table is written down where the next editor will read it', ()
// ── Markup, startup ordering, accessibility ─────────────────────────────────
test('markup, startup ordering and accessibility remain pinned', () => {
const html = expandApplicationHtml(
test('markup, startup ordering and accessibility remain pinned', async () => {
const html = await renderApplicationHtml(
fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
);
const startup = fs.readFileSync(
@@ -915,8 +915,8 @@ test('markup, startup ordering and accessibility remain pinned', () => {
assert.match(choicesBlock, /overflow-y: auto;/);
});
test('the card closes without a keyboard: the ESC hint is a button, kept on phones', () => {
const html = expandApplicationHtml(
test('the card closes without a keyboard: the ESC hint is a button, kept on phones', async () => {
const html = await renderApplicationHtml(
fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
);
const module = fs.readFileSync(
+2 -2
View File
@@ -1,9 +1,9 @@
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const parameters = readFileSync(new URL('./ui/styleParameters.js', import.meta.url), 'utf8');
// Focused markup guards; actual computed names are checked in Chromium.
+3 -3
View File
@@ -1,6 +1,6 @@
import { ShareRestoration } from './ui/shareRestoration.js';
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -202,9 +202,9 @@ test('replacement, repetition, and hidden-tab elapsed time use the newest fixed
'elapsed wall time while hidden expires the notice instead of replaying it');
});
test('universal notice lifecycle clears on dispose and uses the one top-center live region', () => {
test('universal notice lifecycle clears on dispose and uses the one top-center live region', async () => {
const ui = readShellSource();
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const disposeStart = ui.indexOf(' async dispose() {');
const disposeEnd = ui.indexOf('\n }\n', disposeStart);
const dispose = ui.slice(disposeStart, disposeEnd);
+2 -2
View File
@@ -1,5 +1,5 @@
import { shellMethod } from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { createHoverDisclosure, collapsePanelOnEscape } from './ui/panelDisclosure.js';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs';
import { msg, t } from './i18n/index.js';
// Exercise the installed event routes and central close method, without WebGL.
const markup = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const markup = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const locationMarkup = markup.slice(markup.indexOf('<div id="location-bar"'), markup.indexOf('<div id="left-panel-stack"'));
const locationToggleMarkup = locationMarkup.match(/<button\b([^>]*\bid="location-bar-toggle"[^>]*)>([\s\S]*?)<\/button>/);
const locationToggleAttributes = Object.fromEntries(
+3 -3
View File
@@ -1,5 +1,5 @@
import { readShellSource } from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
// MAP STACK chip row — the dropdown's replacement control surface.
//
@@ -482,8 +482,8 @@ test('the keyboard focus ring survives on the ACTIVE chip', () => {
);
});
test('the Visual Presets tray owns Map Source and the retired left panel is absent', () => {
const html = expandApplicationHtml(
test('the Visual Presets tray owns Map Source and the retired left panel is absent', async () => {
const html = await renderApplicationHtml(
readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
);
const ui = readShellSource();
+3 -3
View File
@@ -1,5 +1,5 @@
import { createNamedMarkers } from '../layers/installations/namedMarkers.js';
import { expandApplicationHtml } from '../../build/application-html.js';
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -447,7 +447,7 @@ test('destroying before the first init leaves pre-init buffering intact', () =>
env.cleanup();
});
test('lifecycle is idempotent and teardown removes listeners, observers, and DOM', () => {
test('lifecycle is idempotent and teardown removes listeners, observers, and DOM', async () => {
const env = installMockEnvironment();
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
initWorldOverlay(env.viewer);
@@ -465,7 +465,7 @@ test('lifecycle is idempotent and teardown removes listeners, observers, and DOM
assert.deepEqual(root.children, [canvas],
'the overlay root carries only the shared card canvas');
assert.doesNotMatch(
expandApplicationHtml(readFileSync(new URL('../../index.html', import.meta.url), 'utf8')),
await renderApplicationHtml(readFileSync(new URL('../../index.html', import.meta.url), 'utf8')),
/world-overlay-detection-surface/,
'the surface is runtime host-owned, not static markup',
);
+5 -5
View File
@@ -1,5 +1,5 @@
import { readShellSource } from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
@@ -264,8 +264,8 @@ test('expanded left panels integrate their headers with the container shell', ()
);
});
test('Map Source uses five compact tiles in the bottom Visual Presets tray', () => {
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
test('Map Source uses five compact tiles in the bottom Visual Presets tray', async () => {
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const css = readStylesheet(new URL('../style.css', import.meta.url));
assert.doesNotMatch(html, /id="stack-panel"/);
@@ -277,8 +277,8 @@ test('Map Source uses five compact tiles in the bottom Visual Presets tray', ()
);
});
test('expanded right panels highlight the title divider without changing collapsed launchers', () => {
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
test('expanded right panels highlight the title divider without changing collapsed launchers', async () => {
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const css = readStylesheet(new URL('../style.css', import.meta.url));
assert.match(
+2 -2
View File
@@ -2,7 +2,7 @@ import { readRealtimeSource } from './testSupport/readRealtimeSource.mjs';
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
import { realtimeInstructions } from '../server/providers/openai/instructions.js';
import { readShellSource } from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readLayerSource } from './testSupport/readLayerSource.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
import { readFileSync as readRadioSource } from 'node:fs';
@@ -14,7 +14,7 @@ import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const ui = readShellSource();
const radio = ['playback', 'interaction'].map(name =>
readFileSync(new URL(`./layers/radio/${name}.js`, import.meta.url), 'utf8')
+2 -2
View File
@@ -1,5 +1,5 @@
import { readShellSource } from './testSupport/readShellSource.mjs';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
// src/reasonableDefaults.test.mjs
//
// What the console looks like the FIRST time it opens — before any share link,
@@ -46,7 +46,7 @@ import { ShareLinkManager } from './sharelink.js';
// Follow the UI wiring and its extracted preset definitions.
const uiSource = readShellSource()
+ '\n' + fs.readFileSync(new URL('./ui/visualPresets.js', import.meta.url), 'utf8');
const indexHtml = expandApplicationHtml(fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const indexHtml = await renderApplicationHtml(fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
const shareSource = fs.readFileSync(new URL('./sharelink.js', import.meta.url), 'utf8');
/** Slice ui.js between two literal anchors, so a pin reads one method, not the file. */
+3
View File
@@ -8,6 +8,7 @@ import { createStandaloneData } from './data.js';
import { createStandaloneTools } from './tools.js';
import { resolveLocale, applyDocumentLanguage } from '../i18n/locale.js';
import { applyDocumentTranslations, setLocale } from '../i18n/index.js';
import { mountApplicationViews } from '../ui/views/index.js';
import { createGoogleTokenSource } from '../maps/googleTokens.js';
// The existing controls and layer catalog contain page-scoped state.
@@ -33,6 +34,8 @@ export function createStandaloneApplication({
const activeLocale = resolveLocale();
setLocale(activeLocale);
applyDocumentLanguage(document, activeLocale);
// Views render the page's markup; translation and wiring read it.
mountApplicationViews(document);
applyDocumentTranslations(document);
const loadingScreen = document.getElementById('loading-screen');
const loaderStatus = loadingScreen.querySelector('.loader-status');
+2 -2
View File
@@ -3,13 +3,13 @@ import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { expandApplicationHtml } from '../build/application-html.js';
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
import { readStylesheet } from './testSupport/readStylesheet.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8');
const html = expandApplicationHtml(read('index.html'));
const html = await renderApplicationHtml(read('index.html'));
const css = readStylesheet(path.join(ROOT, 'style.css'));
const panelCss = read('src/ui/styles/street-level.css');
const panelChrome = read('src/ui/panelChrome.js');
+40
View File
@@ -0,0 +1,40 @@
import { readFileSync } from 'node:fs';
import { createPinia } from 'pinia';
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { expandApplicationHtml } from '../../build/application-html.js';
import { APPLICATION_VIEWS } from '../ui/views/index.js';
const HOST = /<div class="gev-view" data-gev-view="([^"]+)"><\/div>/g;
/**
* Expand application markup as the browser shows it once views are mounted:
* static templates inline, and each view rendered into its host. Markup tests
* read this instead of the raw template expansion.
* @param {string} html Document with `gev:template` markers.
* @returns {Promise<string>}
*/
export async function renderApplicationHtml(html) {
let output = expandApplicationHtml(html);
for (const [host, name] of [...output.matchAll(HOST)]) {
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.
.replace(/<!--\[-->|<!--\]-->|<!---->/g, '')
.replace(/ ([\w:.-]+)=""/g, ' $1');
output = output.replace(
host,
`<div class="gev-view" data-gev-view="${name}">${markup}</div>`,
);
}
return output;
}
/** The standalone page's markup with every view rendered. */
export function readApplicationHtml() {
return renderApplicationHtml(
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
);
}
+7 -1
View File
@@ -1,6 +1,7 @@
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.
@@ -11,7 +12,11 @@ export function compileVueModule(source, filename) {
if (errors.length) throw errors[0];
const id = filename;
if (descriptor.scriptSetup) {
return compileScript(descriptor, { id, inlineTemplate: true }).content;
return compileScript(descriptor, {
id,
inlineTemplate: true,
templateOptions: { compilerOptions: VUE_COMPILER_OPTIONS },
}).content;
}
const script = descriptor.script
? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content
@@ -22,6 +27,7 @@ export function compileVueModule(source, filename) {
source: descriptor.template.content,
filename,
id,
compilerOptions: VUE_COMPILER_OPTIONS,
});
if (compiled.errors.length) throw compiled.errors[0];
template = compiled.code;
+24 -5
View File
@@ -1,11 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { expandApplicationHtml, APPLICATION_TEMPLATES } from '../../build/application-html.js';
import {
expandApplicationHtml,
APPLICATION_TEMPLATES,
} from '../../build/application-html.js';
import { APPLICATION_VIEW_NAMES } from '../../build/applicationViews.js';
import { APPLICATION_VIEWS } from '../ui/views/index.js';
import { readApplicationHtml } from '../testSupport/applicationHtml.mjs';
test('the standalone document expands every component once and preserves unique element ids', () => {
test('the standalone document expands every component once and preserves unique element ids', async () => {
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
const html = expandApplicationHtml(source);
const html = await readApplicationHtml();
assert.equal([...source.matchAll(/gev:template /g)].length, APPLICATION_TEMPLATES.length);
assert.doesNotMatch(html, /gev:template/);
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map(match => match[1]);
@@ -15,8 +21,21 @@ test('the standalone document expands every component once and preserves unique
});
test('component selection includes only requested markup and refuses filesystem traversal', () => {
const html = expandApplicationHtml('<!-- gev:template welcome -->\n');
assert.match(html, /id="first-run-launcher"/);
const html = expandApplicationHtml('<!-- gev:template hud-loading -->\n');
assert.match(html, /id="loading-screen"/);
assert.doesNotMatch(html, /id="cesiumContainer"/);
assert.throws(() => expandApplicationHtml('<!-- gev:template ../../.env -->'), /Unknown application template/);
});
test('views expand to mount hosts, the loading screen stays static, and every view is registered', () => {
assert.deepEqual(Object.keys(APPLICATION_VIEWS).sort(), [...APPLICATION_VIEW_NAMES].sort());
for (const name of APPLICATION_VIEW_NAMES) {
assert.ok(APPLICATION_TEMPLATES.includes(name), name);
assert.equal(
expandApplicationHtml(`<!-- gev:template ${name} -->\n`),
`<div class="gev-view" data-gev-view="${name}"></div>\n`,
);
}
// It paints before any script runs.
assert.ok(!APPLICATION_VIEW_NAMES.includes('hud-loading'));
});
+2 -2
View File
@@ -52,7 +52,7 @@ import {
} from '../i18n/index.js';
import { applyLocaleChoice, mountLocalePicker } from './localePicker.js';
import { PanelNavigation } from './panelNavigation.js';
import { createUiRuntime } from './vueRuntime.js';
import { applicationPinia, createUiRuntime } from './vueRuntime.js';
/**
* Central UI orchestrator for the God's Eye View application.
@@ -136,7 +136,7 @@ export class StyleManager extends ShellFacade {
this._mobileShell?.sharedCollapsed(panelId),
});
// Vue views share this runtime and its stores; dispose unmounts them.
this._ui = createUiRuntime();
this._ui = createUiRuntime({ pinia: applicationPinia() });
this._feedback = new ShellFeedback({
readLayers: () => this._dataManager?.getAll?.() || [],
});
+2 -2
View File
@@ -2,11 +2,11 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { expandApplicationHtml } from '../../build/application-html.js';
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
import { createBrowserViteConfig } from '../../build/vite.js';
import { R820T_GAIN_STEPS_DB } from '../sdr/gain.js';
const html = expandApplicationHtml(
const html = await renderApplicationHtml(
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
);
const css = readFileSync(
+6
View File
@@ -98,3 +98,9 @@
font: 600 9px/14px var(--font-mono);
text-align: center;
}
/* Application view hosts (src/ui/views): the view's own elements lay out as
children of the page, as the static markup they replace did. */
.gev-view {
display: contents;
}
+27
View File
@@ -0,0 +1,27 @@
import { createApp } from 'vue';
import { applicationPinia } from '../vueRuntime.js';
/**
* 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({});
const mounted = new WeakSet();
/**
* Render every application view into its host. Views belong to the page, as
* their static markup did, so they are never unmounted. Call before anything
* reads or translates the page; repeated calls are harmless.
* @param {Document} [doc] Page document.
*/
export function mountApplicationViews(doc = globalThis.document) {
for (const host of doc?.querySelectorAll?.('[data-gev-view]') ?? []) {
if (mounted.has(host)) continue;
const name = host.getAttribute('data-gev-view');
const view = APPLICATION_VIEWS[name];
if (!view) throw new Error(`Unknown application view: ${name}`);
createApp(view).use(applicationPinia()).mount(host);
mounted.add(host);
}
}
+16 -4
View File
@@ -1,13 +1,25 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
let pagePinia = null;
/**
* Create the application's Vue runtime: one Pinia instance shared by every
* view the shell mounts, and the mounts themselves for teardown.
* The page's Pinia instance, shared by the application views and the shell,
* so a view and a shell control read the same stores.
* @returns {import('pinia').Pinia}
*/
export function applicationPinia() {
pagePinia ??= createPinia();
return pagePinia;
}
/**
* Create a Vue runtime: one Pinia instance shared by every view it mounts,
* and the mounts themselves for teardown.
* @param {{pinia?: import('pinia').Pinia}} [options]
* @returns {{pinia: import('pinia').Pinia, mount: Function, destroy: Function}}
*/
export function createUiRuntime() {
const pinia = createPinia();
export function createUiRuntime({ pinia = createPinia() } = {}) {
const apps = new Set();
return {
pinia,