Files
gods-eye-view/docs/VUE-MIGRATION.md
T
Sameh Khamis a2bca0fb22 feat(ui): render the Cockpit instruments, route and vision selector from a store
The Cockpit controller no longer writes the HUD's instruments, the flight
plan card, the visor's destination arrow or the vision selector. It keeps
them as plain state, starting from cockpitInstrumentState.js, and
publishes copies to the gev-cockpit store: the instruments on every HUD
update, the route only when it changed, the vision selector when the mode
changes. CockpitView renders the callsign, aircraft meta and emergency
mark, speed and altitude rims and ticks, compass tape, clock, position,
HUD layer, destination arrow and vision selector; a new CockpitRoute
component renders the flight plan card inside the briefing card.

The odometer readouts become CockpitRollingValue components. Their
throttle and digit roll move unchanged from setCockpitRollingValue into
cockpitRollingValue.js: every accepted reading restamps the throttle, a
change inside it is dropped, and each accepted change draws fresh glyphs
so the roll animation restarts. A rim tick with no reading still hides
where it stood, and a hidden arrow keeps its last angle and label.

In a dev build the HUD's Vue patch costs about 0.5 ms per update (1.4 ms
at 4x CPU throttle), against 0.4 ms (2.1 ms) for the DOM writes it
replaces, at about 10 updates a second.
2026-10-31 14:36:32 -07:00

173 lines
10 KiB
Markdown

# Vue migration
The application UI moves from static templates plus imperative DOM wiring to
Vue 3 components with Pinia stores. The migration keeps the experience the
same: element ids, classes, the global stylesheet, translations, keyboard
shortcuts, voice actions, share links and the phone layout all behave as
before. The one intended change is the panel rail described below.
## Goals
- Views are Vue single-file components; state that several views read lives in
Pinia stores rather than in DOM classes.
- The owners listed in [UI-OWNERSHIP.md](UI-OWNERSHIP.md) keep their
responsibilities. A component replaces DOM construction; it does not add a
second owner of the same state.
- Public methods used by layers, scene playback, voice and host applications
(`setPanelCollapsed`, `setContextMode`, `setStyle`, share and location
subscriptions) keep their signatures.
- Ids and classes used by browser QA and host applications stay stable until a
host can use an explicit extension point instead.
## The panel rail
Each desktop sidebar gets a floating icon rail that lists every panel available
on that side. Selecting an icon opens that panel; selecting it again closes it.
One panel is open per side at a time, so the rail replaces the accordion
stacking. Panels that are not available in the current mode (for example
Weather before a weather layer is active) are shown disabled or omitted as the
panel's own availability rule dictates. The rail hides with the rest of the
chrome in Cockpit, scene playback, recording and the clean view.
Phones keep the existing bottom tab bar and sheets; the rail is desktop only.
Voice and keyboard openings go through the same panel store, so they follow the
one-panel rule too. Share links continue to record which panels are open.
## Component kit
`src/ui/kit/` holds the generic components (`UiRail`, `UiPanel`, `UiButton`,
`UiChip`, `UiTooltip` and so on). Names, props and events follow Halfpixel's
`ui-vue` kit so the two can converge later, but the components are written for
this repository and styled by its existing stylesheet and custom properties.
They contain no application state.
## Phases
1. **Foundation.** Vue, Pinia and the Vite Vue plugin join the browser build
(`build/vite.js`, so host applications receive them too). One Vue runtime is
created by the shell (`src/ui/vueRuntime.js`) with one Pinia instance that
every view shares; disposing the shell unmounts them. Stores are added
when a view needs one, mirroring state an owner already publishes, so
there is never a second owner of the same state.
2. **Rail.** `PanelChrome` enforces one panel per side; the panel store
mirrors each side's panels and the rail renders from it. Done.
3. **Panels.** Each template and its wiring become a component, keeping ids,
classes, `data-i18n` text and CSS. A converted template is listed in
`build/applicationViews.js`; its `gev:template` marker then expands to a
`.gev-view` host (`display: contents`), and `mountApplicationViews()`
(`src/ui/views/index.js`) renders the view into it before the page is
translated and wired. `createApplication()` calls it too, so host pages
need no change. Views belong to the page and are never unmounted. The
loading screen stays static so it paints before any script runs. Markup
tests read `renderApplicationHtml()` from `src/testSupport`, which renders
the views on the server. Templates compile with Vue's `preserve`
whitespace, which keeps the whitespace between elements as a single
space, so a view renders as its template did; no view markup sits under a
`white-space: pre` rule. Application views (`src/ui/views/*View.vue`) are
kept out of Prettier; other components are formatted with strict
whitespace sensitivity, so their source spacing is what renders. The
markup has moved, unchanged:
every template except the loading screen is a view.
Next each view takes over its wiring: markup an owner builds with
`createElement` becomes component markup that renders from a Pinia store
the owner publishes to, and user actions come back to the owner through
the store, so the owner keeps every decision. Static markup that the
translation pass rewrites (`data-i18n*`) stays outside `v-if` and
`v-for`, because re-rendering it would bring back the English; component
text uses `t()` instead. Shared builders become components first and
keep their imperative entry points until their owners are components:
the rail cards, card blocks and timeline (`src/ui/rail/`) render through
Vue's synchronous `render()` behind `createRailCards()`,
`createRailCardBlocks()` and `createRailTimeline()`. Order: Scenes (done: `SceneOptions` and
`SceneShotList` over the `gev-scenes` store), Weather (done: the owner
renders `WeatherReadout` from the view it computes), Recent imagery
(done: `RecentImageryReadout` and its day cards, the same way),
Street level (done: `StreetLevelPanel` renders the `gev-street-level`
store that `StreetLevelControls` publishes to; the controls keep their
listeners, EXPAND, focus and resizing), CCTV (done: `CctvPanel` renders
the whole panel from the `gev-cctv` store, including the camera picker,
the inline pose editor, the frame preview, source badge and typed
summary; `CctvControls` keeps its listeners, the frame requests and the
live video surface), Layers (done: `LayerPanel` presents each row and
renders `LayerRows` synchronously; keyed chips and list items keep focus
through the refresh their own click triggers, and clicks re-read the live
descriptor before acting), Display (in progress: `DisplayPanel` renders
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
`gev-display` store, which VisualSettings publishes to, and the 3D
aircraft group, the Draw controls and the imported file list, which
AircraftDisplay, the draw tool and the file import publish beside it; the
file import restores keyboard focus once the list has rendered; the
style parameter rows render through Vue behind `createStyleParameters()`,
as a render function so the portable `./ui/input` entry needs no
component compiler; the other groups follow one owner at a time),
Context and Radio (in progress: `RadioPanel` renders the directory row,
station card, transport, volume and playback line from the `gev-radio`
store that RadioControls publishes to, and `SdrCard` the local RTL-SDR
card that LocalSdrControls publishes beside it; fields a person edits take
their value through `v-field-value`, which never writes a focused field;
the tuner and the Cockpit radio controls keep their owners' element
bindings; `ContextView` renders the mode switch and the nearby sites
search from the `gev-context` store, which ContextControls reaches only
through its `publish` option, so `./ui/context` needs no Vue;
`AwarenessPanel` renders the Contacts panel that the awareness layer
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 (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 map source chips and status badge render
what `createMapSourceControls()` publishes beside it, so `./ui/maps`
needs no Vue; the location tray's city and landmark pills, mini status
and search error render what `LocationControls` publishes to the
`gev-location` store, and the search field keeps its listeners), the
phone shell (`PhoneTabBar` renders the tabs and open sheet the shell
publishes to the `gev-display` store, teleporting the tab bar and sheet
backdrop to the end of the page from the first publish until the shell
is destroyed; the shell takes their clicks from the document), then
Cockpit (in progress: `CockpitBriefing` renders the briefing card's
pages, cycle toggle, regional news and local conditions, and
`CockpitContext` the Contact card's scope, navigation, counts, nearest
contact and weather toggle, `CockpitView` the instruments, destination
arrow and vision selector, and `CockpitRoute` the flight plan card, from
the `gev-cockpit` store that the Cockpit controller publishes to,
starting from `cockpitBriefPages.js`, `cockpitContextState.js` and
`cockpitInstrumentState.js`, which both read; the odometer readouts
(`CockpitRollingValue`) keep their throttle and roll in
`cockpitRollingValue.js`. The instruments publish on every HUD update
(about 10 Hz); in a dev build their Vue patch costs about 0.5 ms, close
to the DOM writes it replaced. The controls keep the controller's
listeners, and the cards' visibility, collapse and layout stay with the
controller. The signal rows and the radio controls follow; the signal
rows become a
component that keeps their focus-safe reorder, since a keyed Vue list
would move the focused row and drop focus several times a second).
Layers that write into the right rail directly gain a slot instead.
4. **Cleanup.** The build-time template expansion and the attribute
translation pass are retired once nothing uses them. Hosts register panels
and rail items through an explicit API instead of editing markup.
Markup-string tests are replaced by component tests.
## Verification
- Unit tests: stores and components run under `npm test`. Single-file
components are compiled by a test loader; component tests render with
`@vue/test-utils` and `happy-dom`. `npm test` registers the loader; to run
one file that reaches a component (including anything importing the
shell), use `node --import=./build/registerVue.js --test <file>`.
- Never pass happy-dom nodes to `assert.equal` or `deepEqual`: a failure
prints both operands, and a node reaches the whole page and window, which
runs the process out of memory. Compare nodes with `assertSameNode()`
(`src/testSupport/domAssert.mjs`).
- Panels lifted out of a view keep its verbatim markup in
`src/ui/views/panels/`, which Prettier leaves alone like the views.
- Visual parity: before/after screenshots of every panel at desktop and phone
widths, in the default and Cyber themes, with the rail as the only expected
difference.
- Browser QA: the selector-based `scripts/qa-*.mjs` suites keep passing.
- `npm run format:check`, `npm run check:boundaries`, `npm run i18n` and
`npm run build`.