diff --git a/.env.example b/.env.example index 0876262b2..c9700c5be 100644 --- a/.env.example +++ b/.env.example @@ -256,6 +256,8 @@ AISSTREAM_API_KEY= # CCTV_TAIWAN_ENABLED=1 # set 0 to skip Taiwan Freeway Bureau (MOTC) road cameras # CCTV_TAIWAN_MAX_SOURCES=600 # cap Taiwan freeway cameras (1-2000); no API key required # CCTV_TAIWAN_URL= # override the Freeway Bureau MOTC 2.0 CCTV.xml URL +# CCTV_A22_ENABLED=1 # set 0 to disable the Autostrada del Brennero (A22, Italy) webcams +# CCTV_A22_URL= # override the Open Data Hub A22 webcam catalog URL # CCTV_MAX_SOURCES=4000 # catalog-wide ceiling, shared round-robin across packs # CCTV_PREFER_AUSTIN=1 # prioritize Austin cameras in the registry # CCTV_FORCE_AUSTIN= # force live default sources when a source-pack JSON is configured diff --git a/.prettierignore b/.prettierignore index 1aaad8a03..2b2cc755a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,3 +20,8 @@ node_modules .vite .vite-* *.min.js + +# Application views, and panels lifted from them, hold the page markup +# verbatim; reflowing it changes the DOM +src/ui/views/*View.vue +src/ui/views/panels/ diff --git a/.prettierrc.json b/.prettierrc.json index b1e6b2baf..095bf4e98 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,5 +2,11 @@ "singleQuote": true, "tabWidth": 2, "semi": true, - "endOfLine": "lf" + "endOfLine": "lf", + "overrides": [ + { + "files": "*.vue", + "options": { "htmlWhitespaceSensitivity": "strict" } + } + ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a7fe8ab76..d8e569d3a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -115,7 +115,7 @@ The highest-leverage places to jump in: ## Architecture in one minute -- **No framework.** Vanilla JS + [CesiumJS](https://cesium.com/platform/cesiumjs/) + [Vite](https://vitejs.dev/). +- **Vue 3 + Pinia for the interface,** [CesiumJS](https://cesium.com/platform/cesiumjs/) for the globe and [Vite](https://vitejs.dev/) to build. See [Building UI](#building-ui). - **Assembly lives in `src/app/`; standalone defaults live in `src/standalone/`.** UI controllers live in `src/ui/`, layer factories in `src/layers/`, portable sources in `src/sources/`, and application operations in `src/services/`. Existing `src/data/.js` entries retain compatibility; new code belongs with its focused owner. - Sources acquire records; renderers own Cesium resources. Import `gods-eye-view/layers//source` when only a source factory is needed. Common voice controls consume the session interface; protocol adapters own connection details. - **Secrets stay server-side.** Anything needing a private key goes through a local proxy under `server/providers/`. The browser only ever sees the Google Maps key (which you restrict) and ephemeral tokens. @@ -126,9 +126,65 @@ The highest-leverage places to jump in: - ES modules, **2-space indent, single quotes, semicolons.** - JSDoc on exported/public functions. - Match the surrounding code — comment density, naming, and idiom. -- User-visible text goes through `t()`, `tn()` or `msg()`, or a `data-i18n*` attribute in markup, and `npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md). +- User-visible text goes through `t()` or `tn()`, with `msg()` marking English that is stored and translated later. Components call `t()` in their templates, so their text follows the locale; `data-i18n*` attributes are only for static page markup such as the loading screen, and `npm run i18n` rejects them in a template. `npm run i18n` must pass. See [docs/I18N.md](docs/I18N.md). - Prefer small, reviewable commits. Conventional-commit-style prefixes (`feat:`, `fix:`, `perf:`, `docs:`) are appreciated but not required. +## Building UI + +New UX elements are Vue components whose state lives in a Pinia store, and so +are existing ones when a change converts them: + +- **Views are Vue components.** Application views live in `src/ui/views/` and + the components they place in `src/ui/views/panels/`; a component that + belongs to one feature sits beside its owner (`src/ui/rail/`, + `src/ui/weather/`). A view renders what its store holds and forwards + clicks and input to the store; it holds no application logic. +- **State lives in a Pinia store** in `src/ui/stores/`. The owner presents a + plain view object and publishes it to the store; the view renders it. User + actions return to the owner through handlers it connects to the store, + which replace an earlier owner's. Where several owners answer different + actions of one store, use `createOwners()` from `src/ui/stores/owners.js`: + `act(name, ...args)` calls the newest connected owner that handles `name`, + and the owner calls `detach(token)` when it stops. +- **Portable owners stay framework-free.** An owner in a portable export + imports neither Vue nor Pinia: it takes a `publish` option or offers a + `subscribe…()`, and the application shell connects it to the store. + `npm run check:boundaries` enforces this. +- **Reuse the kit first.** `src/ui/kit/` has one component per control: + `UiButton` (including pressed toggles and pressable chips), `UiSelect`, + `UiSlider`, `UiSegmented`, `UiChip` (a label, not a control), `UiBadge`, + `UiEmpty`, `UiIcon`, `UiTooltip`, `UiPanel` and `UiRail`. Use these before + writing a control. When the kit lacks something, extend a kit component or + add one that follows the kit's names, props, events and class names (see + [Component kit](docs/VUE-MIGRATION.md#component-kit)), rather than + building a one-off. +- **No hand-written DOM binding.** Don't build elements or write text, + classes or attributes from a controller to show UI state; render it from + the store. New controls take input through the store rather than through + listeners the owner adds. To render outside the view's host (the body, the + globe container, or a slot another surface opens), use `` to + `body`, a fixed id or a container the owner publishes; don't move nodes by + hand. What stays imperative: classes on `` and ``, which sit + outside Vue (`cockpit-mode`, `ui-clean-view`); focus moves; positions a + layout pass measures in the same task that writes them; and surfaces an + owner paints or plays, such as the Cesium canvas, overlay canvases and live + video. A few existing surfaces keep listeners or bindings that + [docs/VUE-MIGRATION.md](docs/VUE-MIGRATION.md) records; keep those + contracts when you change them, and don't copy them into new UI. +- **Text** goes through `t()` or `tn()` where the template renders it. + English kept in a constant or a store is marked with `msg()` and passed to + `t()` when it renders, since a `t()` at module load would fix the English. +- **Test components, not markup strings.** Mount the component over + happy-dom with `@vue/test-utils` or `createApp` and assert what it renders + from its store (see `src/testSupport/vueDom.mjs`, `src/ui/kit/*.test.mjs` + and `src/ui/views/intelHudView.test.mjs`). Run one such file with + `node --import=./build/registerVue.js --test `. Don't pass happy-dom + nodes to `assert.equal`/`deepEqual`; compare them with `assertSameNode()` + from `src/testSupport/domAssert.mjs`. + +[docs/VUE-MIGRATION.md](docs/VUE-MIGRATION.md) records how each surface moved +and what stays imperative, and why. + ## Formatting and reusable components Run `npm run format` before submitting changes, then `npm run format:check` diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index e31d0d3f0..a4035f175 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -69,6 +69,7 @@ How to read this: | **Kartverket** (`ws.geonorge.no/hoydedata/v1/punkt`) | Terrain heights (NN2000) used as the ground prior for Statens vegvesen cameras | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — attribution REQUIRED | "© Kartverket", modified (heights over water set to 0 m, rounded to 0.1 m) | | **NYC DOT Traffic Management Center** (`webcams.nyctmc.org`) | CCTV camera catalog + frames, New York City five boroughs | NYC DOT requires developers to sign a [data-sharing agreement](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) for the camera feed. **Off by default**; enable only under your own agreement | "NYC DOT Traffic Management Center" + NYC DOT's required application disclaimer (in the Data attribution popover) | | **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" | +| **Autostrada del Brennero (A22)** (catalog `tourism.api.opendatahub.com`, frames `www.autobrennero.it/WebCamImg/`) | CCTV camera catalog + frames, A22 Brenner–Modena motorway, Italy | Public Autobrennero webcam images; camera list republished by the Open Data Hub, whose records are tagged CC0 (`ClosedData: false`, licence holder Autobrennero); Autobrennero publishes no terms of its own for the images | "Webcam images © Autostrada del Brennero S.p.A., via the Open Data Hub (NOI Techpark)" (courtesy) | | **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` | | **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. No licence is published: the City's open-data package declares none and the feed has no `license_url`. Attribution is given to the operator. | "Bike Share Toronto (Toronto Parking Authority)" | | **Bike Itaú / Tembici** (`*.publicbikesystem.net`, seven Brazilian cities, Santiago and Bogotá) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília, Curitiba, Santiago de Chile and Bogotá | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)"; "Tembici Bogotá" | @@ -176,6 +177,7 @@ The optional Local ADS-B layer and the broadcast-FM receiver use radio you recei - **Kartverket ground heights for Statens vegvesen cameras.** The camera feed's points are 2D, so the loader asks Kartverket's keyless point-height API (`ws.geonorge.no/hoydedata/v1/punkt`, CC BY 4.0) for the terrain height at each camera. Requests carry 20 points: the API accepts 50, but above ~20 it answers the extra points from contour lines (`hoydekurver`, metres to ~20 m off) or not at all. Eight requests run at a time, about 36 for the whole country: a cold lookup took 6–13 s in testing (single requests 0.6–9.5 s). Answers are matched by the coordinates the API echoes and recorded only for points that request asked about. Over water the API returns the sea-floor depth (`dybdekurver` or `Havflate`), which is replaced by 0 m, the water surface; for the ~14 bridge cameras that is below the deck, but much closer than the sea floor or the flat prior. Heights are cached in memory by position for the life of the server, and points with no height (a few ferry quays and bridges, or a point a 200 answer leaves out) for 6 h, so the 15-minute catalog refresh only asks about new or moved cameras. The first failing request (a 408, 429 or 5xx, a network error, a request over 10 s, or an unreadable or oversized body) stops the lookup, with no retries, and no new lookup starts until the backoff ends (Retry-After when sent, else 30 min, longer than the refresh; bounded to 1 min–1 h). Any other 4xx marks that request's points as having no height and the lookup carries on. The whole lookup has a 30 s deadline; a lookup cut short by it resumes on the next refresh without a backoff. The lane waits at most 12 s, counted from the start of the loader, and only when more than 10% of the cameras have no answer yet; a few new cameras or expired misses are looked up in the background. A slower lookup keeps filling the cache, and the cameras it had not reached keep the flat prior until the next 15-minute refresh (about 190 of 865 on one slow first run). Only coordinates are sent, with the same User-Agent as the other CCTV requests. `CCTV_VEGVESEN_HEIGHTS=0` turns the lookup off. The Kartverket credit, with a note that heights over water are set to 0 m and heights are rounded to 0.1 m, is shown beside the Statens vegvesen entry in the Data attribution popover. - **NYC DOT Traffic Management Center cameras.** Off by default. NYC DOT's [developer data feeds page](https://www.nyc.gov/html/dot/html/about/datafeeds.shtml) asks developers who want the camera feed to contact `tmcdot@dot.nyc.gov` and sign a data-sharing agreement. This project does not hold one that covers downstream installations, so the pack loads only when an operator who holds their own agreement sets `CCTV_NYC_ENABLED=1`; with the flag unset, empty or any other value, the server makes no request to NYC DOT. The endpoint needs no key. The camera list is the public `webcams.nyctmc.org/api/cameras` catalog (~980 cameras across the five boroughs); frames are stills at `webcams.nyctmc.org/api/cameras//image`, synthesized from a validated UUID rather than taken from the payload, and only that host is registered. Cameras the feed reports as offline are dropped. The catalog publishes no camera facing, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. Once enabled, the 250 cameras nearest the borough cores load; `CCTV_NYC_MAX_SOURCES` (8–400) changes the cap. Frames are fetched at request time and never stored or redistributed. NYC DOT asks every application that uses its data to carry its disclaimer; it is registered verbatim in the Data attribution popover. - **City of Toronto RESCU cameras.** Keyless. The camera list is the City's public `tmcearthcameras.json` on `opendata.toronto.ca` (dataset "Traffic Cameras", Transportation Services), served as JSONP and unwrapped with an anchored pattern. This is the municipal network the Ontario 511 pack does not carry: downtown arterials plus the Gardiner, DVP, Allen and Lake Shore. The list publishes no frame URL, so frame URLs are BUILT from the documented `CameraImages/loc####.jpg` convention and a strictly-digits camera number, pinning the frame proxy to the portal origin by construction; the list fetch refuses redirects and bounds the body it buffers. Frames are fetched at request time and never stored or redistributed. The dataset publishes no camera facing: the `D1`-`D4` fields are the directions of the *comparison* images (static reference stills captured in 2016 under a different path), not a bearing for the live pan-tilt-zoom frame, so headings use the shared id-hash fallback at low confidence and are corrected with the in-app calibration gizmo. A camera that is temporarily out of service still answers 200 with a byte-identical "Image Unavailable" placeholder rather than an error (about 6% of the network at any moment), so such a camera reads as healthy; the frame it shows is the agency's, not a synthesized one. The licence requires the attribution statement above. Disable with `CCTV_TORONTO_ENABLED=0`. +- **Autostrada del Brennero (A22) webcams.** Keyless. The camera list is one request to the Open Data Hub tourism API (`tourism.api.opendatahub.com/v1/WebcamInfo?source=a22`), run by NOI Techpark in South Tyrol, which republishes Autobrennero's own list of 13 motorway webcams with coordinates and kilometre markers. Frames are the operator's stills on `www.autobrennero.it/WebCamImg/km.jpg`, refreshed about once a minute, and only the exact image for each camera's kilometre is registered. The feed names no bearing, so headings use the id-hash fallback at low confidence. The road climbs from about 20 m in the Po valley to about 1,370 m at the Brenner Pass, so each camera's ground elevation comes from a per-kilometre table sampled from the Copernicus GLO-90 DEM (via Open-Meteo's elevation API); a camera not in the table uses a 200 m prior. The Open Data Hub tags every record CC0 (`ClosedData: false`, Autobrennero as licence holder), but that tag is the aggregator's metadata and Autobrennero publishes no terms of its own for the images, so they are credited to the operator as a courtesy and are not redistributed. Frames are fetched at request time and never stored. Disable with `CCTV_A22_ENABLED=0`. - **Camera frame content.** Public camera frames can contain people, vehicles and license plates, and the camera layer does nothing with them beyond putting them on the map. A successful upstream response is relayed as the provider served it: nothing in the camera pipeline blurs, redacts, enhances or restores it, and nothing there runs face, plate or object detection or otherwise recognises or classifies what is in the picture. (The ALPR layer described below maps where plate-reader cameras are, from locations contributors tagged in OpenStreetMap; it reads nothing from any camera frame.) When an upstream has no frame for a camera, what you see instead is a Google Street View still or the app's own placeholder card; the panel's source badge names which of them you are looking at, and a refresh that fails after a frame has arrived leaves the last good picture on screen rather than replacing it. The browser resamples each frame for display: onto a fixed 1920x1080 surface for the camera's projection plane in the scene, and onto a 192x108 canvas for the small ambient card. That scaling is the only change made to the picture, and the only pixels the camera layer reads back are a 64x36 downscale hashed to tell whether the picture has changed. Live streams are piped through; the still path reads the image into memory to pass it on — TxDOT wraps its JPEG in a JSON envelope, which is decoded there — and no frame is written to disk (the disk cache, `.gev-cache/`, holds feed JSON for other layers and never camera imagery). Outside that pipeline, the voice assistant is the one feature that sends imagery anywhere: when it answers a question about what is in view it may attach a downscaled screenshot of the rendered scene — including any camera plane on screen — to the request it sends to the voice model. Frames are fetched only from server-registered camera URLs — a client cannot hand the proxy a URL (see [SECURITY.md](SECURITY.md)). What the app does stays inside the line README draws: no named-person search, no face recognition, no tracking individuals. - **Transit (GTFS-Realtime).** The proxy requests only registered operator feeds, validates redirects, bounds responses, and uses conditional requests, short snapshot caching and failure backoff. All sixteen registered regions are live-enabled; each can be disabled independently. Vehicles use delayed playback between reports, with report age and waiting states shown separately. Session-local selected trails are available for every feed. MBTA additionally retains up to 15 minutes of recently observed positions in the running proxy, so available history can survive a browser reload; server restart clears it. History reads never fetch upstream, and other feeds have no proxy retention. Nothing is archived to disk or collected in the background. Operator text credits are included as a courtesy and do not imply endorsement. Heights are aligned to work with Google 3D tiles; subway markers are shown at street level because these feeds do not supply depth. The TTC feed is BusTime, the surface system: it carries buses and streetcars and no subway at all. About four in ten of its vehicles report no route at all (unassigned or deadheading), and those are shown with the feed default rather than a route-established mode. It sends no ETag or Last-Modified, so its conditional requests fall through to the ordinary snapshot cache. Three Golden Horseshoe neighbours run alongside it — MiWay, Hamilton HSR and Durham Region Transit — each bus-only and each polled only near its own region. Durham Region Transit publishes no bearing on any vehicle, so those sprites take their heading from observed movement like any other feed that omits it. Guelph, Kingston and Barrie extend the same pattern eastward and north. Hamilton, Guelph, Burlington and YRT publish route ids that are not the number on the vehicle, so those entries carry a route-name table taken from each operator’s own published GTFS; any id missing from a table renders unchanged rather than blank. A label is never invented: YRT runs a route `9101` that appears nowhere in its published routes, so it stays as the feed sends it. Burlington’s is the one that cannot be shortcut: `351` is route 1 while `3510` is route 10, so the table is a real join rather than a trimmed prefix. GO Transit / Metrolinx is deliberately absent: its REALTIME feed requires an API key issued by manual review, which the keyless admission rule excludes (its static schedule download is keyless, but carries no vehicle positions). Barrie and YRT/Viva each invite acceptance of their licence through a form, and each licence also states that using the data is itself acceptance; this deployment relies on that clause, which is recorded in both entries. Grand River Transit in Waterloo is absent for a different reason: it answers, but its feed header was months stale and it publishes no per-vehicle timestamps, so it would render vehicles that look live and are not. Ottawa’s OC Transpo falls to the same keyless admission rule as GO Transit — its realtime feed requires an API key. London Transit and Thunder Bay Transit publish vehicle positions over plain HTTP with no HTTPS endpoint available. Every other Ontario system — Brampton, Oakville, Niagara, Milton, Windsor, Greater Sudbury, UP Express and a dozen smaller operators — publishes static GTFS schedules only, with no realtime vehicle positions to register. Between the nine included and these exclusions, every Ontario realtime vehicle-position feed catalogued in the Mobility Database is accounted for. - **Radio Browser.** `/api/radio/stations` discovers official API mirrors, makes bounded and coalesced healthy/geolocated HTTPS-station queries, caches the normalized public-domain directory for 45 minutes, and may serve the last good catalog for up to seven days during an outage. Refreshes must meet minimum accepted-query and station coverage before replacing a warm catalog; schema-valid responses whose rows all fail the product's health policy do not count as successful queries. A usable partial cold catalog is explicitly `DEGRADED`, and non-array or empty successful payloads are rejected atomically. Individual structurally malformed rows are skipped rather than failing the whole query; the response carries their bounded `rejectedStationCount` (at most 10,000), which the client adds to its own rejected rows and reports as degraded coverage without discarding the accepted generation. Every directory and click-count request rejects redirects, validates all resolved addresses as globally routable (including reserved/documentation IPv4 and special/non-global IPv6 exclusions), and pins the TLS connection to a validated address. Only MP3/AAC non-HLS directory rows with public HTTPS stream targets are returned; favicons are intentionally omitted. Pressing play connects one browser audio element directly to the selected broadcaster and calls the directory's click counter through known-ID-only `POST /api/radio/click/:uuid`. GEV never proxies, caches, records, bundles, or redistributes audio. Radio Browser supplies station-level tags, not dependable current-song or upcoming-program metadata, so Radio filtering never claims either. Direct playback exposes the listener's IP address to the broadcaster, whose own stream terms apply. diff --git a/README.md b/README.md index 745bec3d8..3a4c5fc43 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ Twenty layers and map sources. **Eighteen have a keyless path.** Some offer addi | 🌍 **Earthquakes** | Global seismic activity, last 24h | USGS | 🟢 | | ◍ **Air quality** | Live AQHI on the published 1–10+ health-risk scale, with a Weather-panel card showing the band legend and the highest readings in view. **Canada only** — ECCC is the first registered provider | ECCC MSC GeoMet | 🟢 | | 🚗 **Traffic** | Simulated vehicles on OSM roads. With TomTom, live flow speeds drive the simulation and congestion colors below ~8 km; individual vehicle positions are not live observations | TomTom + OSM | 🟢 simulation · 🟡 live flow speeds | -| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | +| 📹 **CCTV Mesh** | ~4,000 public cameras (shared cap) projected _into_ the 3D space — Austin · Texas (TxDOT) · California (Caltrans) · London (TfL) · Ontario (511, free developer key) · Finland (Fintraffic) · British Columbia (DriveBC) · Estonia (Tallinn, Tarktee) · Delaware (DelDOT live video) · New South Wales (Live Traffic NSW) · Calgary · Norway (Statens vegvesen) · Toronto · King County (WA) · Washington (WSDOT) · Catalonia (Servei Català de Trànsit) · Hong Kong (Transport Department) · Taiwan (Freeway Bureau, MOTC) · Italy (A22 Brennero motorway), plus opt-in New York City (NYC DOT TMC, needs your own NYC DOT agreement), opt-in Lithuania (Via Lietuva / Eismoinfo, photo licence unconfirmed) and opt-in Florida (FL511, individual personal use only; other use needs FDOT's written consent). Positions are published; poses are estimated priors **you calibrate by dragging a gizmo on the camera itself** | City APIs | 🟢 | | 📷 **Mapped ALPR Cameras** | License-plate-reader camera locations tagged by OpenStreetMap contributors, loaded one city-sized view at a time, with **SHOW NEAREST**. Locations and tags only: no plate data, no video | OpenStreetMap (incl. DeFlock mapping) | 🟢 | | 📷 **Street Level** | Street-level photos behind one panel, one chip per provider: coverage on the streets, image cones, an embedded viewer with full-screen **EXPAND**. Mapillary, Google Street View, KartaView and Panoramax | Mapillary · Google · KartaView · Panoramax | 🟢 KartaView, Panoramax · 🟡 Mapillary token · 🔴 Google Maps key | | 📻 **Radio** | Geolocated world radio with an **analog tuner** — drag the needle across up to 750 stations and the globe flies to each broadcaster | Radio Browser / broadcasters | 🟢 | @@ -491,12 +491,12 @@ How the globe handles live data: - **Sits on the real ground.** Entity heights are aligned to work with Google 3D tiles, so aircraft park on aprons and cameras stand on street corners instead of floating. - **Caching and request budgets.** An OpenSky credit governor, a TomTom daily tile budget, and disk-cached TLEs reduce repeated requests. These controls do not replace provider quotas or billing controls. - **Server-side credentials.** Every API that touches a private key (OpenAI, AISStream, OpenSky OAuth, camera frames) is brokered through a hardened server-side proxy with SSRF protection, response caps, and sanitized errors. The only keys the browser sees are Google Maps and Cesium ion (restrict both at the provider). -- **No framework.** Vanilla JavaScript, **CesiumJS**, and **Vite** — plus **Google Photorealistic 3D Tiles** for the planet and the **OpenAI Realtime API** for voice. Fast to read, fast to hack on. +- **A small stack.** **Vue 3** and **Pinia** for the interface, **CesiumJS** for the globe and **Vite** to build it — plus **Google Photorealistic 3D Tiles** for the planet and the **OpenAI Realtime API** for voice. Fast to read, fast to hack on. ``` src/ ├── main.js # Bootstrap: Google 3D tiles, layer registration -├── ui/ # Runtime UI — panels, HUD, styles, controls +├── ui/ # Runtime UI — Vue views, Pinia stores, kit, controllers, styles ├── hud.js # Intelligence HUD + AI scene summary ├── keySetup.js # POWER UP panel — in-app provider keys (dev server only) ├── mapStackController.js # Basemap switching — Google 3D / Esri / OSM / ion stacks diff --git a/build/application-html.js b/build/application-html.js deleted file mode 100644 index b14210a56..000000000 --- a/build/application-html.js +++ /dev/null @@ -1,40 +0,0 @@ -import { readFileSync } from 'node:fs'; - -export const APPLICATION_TEMPLATES = Object.freeze([ - 'scene-chrome', - 'cockpit', - 'display-controls', - 'command-dock', - 'layer-panels', - 'context', - 'welcome', - 'provider-settings', - 'hud-loading', -]); -const allowed = new Set(APPLICATION_TEMPLATES); - -/** Expand only known component templates; markers cannot name filesystem paths. */ -export function expandApplicationHtml(html) { - return html.replace( - /^[ \t]*\r?\n?/gm, - (_, name) => { - if (!allowed.has(name)) - throw new Error(`Unknown application template: ${name}`); - return readFileSync( - new URL(`../src/ui/templates/${name}.html`, import.meta.url), - 'utf8', - ); - }, - ); -} - -/** Assemble static application markup before Vite processes scripts and assets. */ -export function applicationHtmlPlugin() { - return { - name: 'application-component-templates', - transformIndexHtml: { - order: 'pre', - handler: expandApplicationHtml, - }, - }; -} diff --git a/build/registerVue.js b/build/registerVue.js new file mode 100644 index 000000000..eb8dc4736 --- /dev/null +++ b/build/registerVue.js @@ -0,0 +1,5 @@ +import { register } from 'node:module'; + +// Lets Node import single-file components: `node --import=`. +// Hosts reach it as `/build/register-vue`. +register('./vueLoader.js', import.meta.url); diff --git a/build/vite.js b/build/vite.js index 960603814..fe90501dd 100644 --- a/build/vite.js +++ b/build/vite.js @@ -1,6 +1,7 @@ import { fileURLToPath } from 'node:url'; -import { applicationHtmlPlugin } from './application-html.js'; import cesium from 'vite-plugin-cesium'; +import vue from '@vitejs/plugin-vue'; +import { VUE_PLUGIN_OPTIONS } from './vueOptions.js'; import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js'; import { embedFramingPlugin } from './embed-framing.js'; import { panelBuildPlugin } from './panel.js'; @@ -155,7 +156,8 @@ export function createBrowserViteConfig({ // Next, so no provider route answers a Host the server does not allow. hostCheckPlugin(), cesium(), - applicationHtmlPlugin(), + // Application views are Vue single-file components. + vue(VUE_PLUGIN_OPTIONS), ...plugins, embedFramingPlugin(), panelBuildPlugin(), @@ -209,6 +211,12 @@ export function createBrowserViteConfig({ // framing + CSP hardening (one constant, no drift). preview: { headers: BROWSER_HEADERS }, define: { + // vue-i18n holds the locale and catalogs; Core formats the messages + // (src/ui/i18n.js), so the build drops vue-i18n's message compiler. + __VUE_I18N_FULL_INSTALL__: 'false', + __VUE_I18N_LEGACY_API__: 'false', + __INTLIFY_PROD_DEVTOOLS__: 'false', + __INTLIFY_DROP_MESSAGE_COMPILER__: 'true', 'import.meta.env.GOOGLE_MAPS_API_KEY': JSON.stringify(googleApiKey), 'import.meta.env.CESIUM_ION_TOKEN': JSON.stringify(cesiumToken), // Mapillary client tokens are public by design (see SECURITY.md). diff --git a/build/vueLoader.js b/build/vueLoader.js new file mode 100644 index 000000000..a549fca47 --- /dev/null +++ b/build/vueLoader.js @@ -0,0 +1,53 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { compileScript, compileTemplate, parse } from 'vue/compiler-sfc'; +import { VUE_TEMPLATE_OPTIONS } from './vueOptions.js'; + +/** + * Compile a single-file component to an ES module for Node, so unit tests (the + * application's and a host's) can import components without a bundler. + * Styles are dropped: tests read the global stylesheet separately. + */ +export function compileVueModule(source, filename) { + // The template is parsed with the SFC; parse it as the browser build does. + const { descriptor, errors } = parse(source, { + filename, + templateParseOptions: VUE_TEMPLATE_OPTIONS.compilerOptions, + }); + if (errors.length) throw errors[0]; + const id = filename; + if (descriptor.scriptSetup) { + return compileScript(descriptor, { + id, + inlineTemplate: true, + templateOptions: VUE_TEMPLATE_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, + ...VUE_TEMPLATE_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, + }; +} diff --git a/build/vueOptions.js b/build/vueOptions.js new file mode 100644 index 000000000..bd8be66fd --- /dev/null +++ b/build/vueOptions.js @@ -0,0 +1,16 @@ +/** + * Template options shared by the browser build, the boundary check and the + * unit-test loader. Whitespace between nodes is kept (as a single space) so + * an application view renders as its static markup did; Prettier formats + * single-file components with strict whitespace sensitivity to match. Asset + * URLs such as `/pin.svg` stay literal instead of becoming module imports. + */ +export const VUE_TEMPLATE_OPTIONS = Object.freeze({ + compilerOptions: Object.freeze({ whitespace: 'preserve' }), + transformAssetUrls: false, +}); + +/** `@vitejs/plugin-vue` options for application builds. */ +export const VUE_PLUGIN_OPTIONS = Object.freeze({ + template: VUE_TEMPLATE_OPTIONS, +}); diff --git a/changelog.d/1078-firms-ipv4-fallback.md b/changelog.d/1078-firms-ipv4-fallback.md new file mode 100644 index 000000000..ff4e60e1b --- /dev/null +++ b/changelog.d/1078-firms-ipv4-fallback.md @@ -0,0 +1,4 @@ +- FIRMS fire data loads on hosts where IPv6 is unreachable and IPv4 is slow to + connect: a request whose every address fails to connect retries once over + IPv4, and FIRMS warnings name the underlying connection errors instead of + only `fetch failed` (jeffblakely, #1078). diff --git a/changelog.d/1446-draw-area-units.md b/changelog.d/1446-draw-area-units.md new file mode 100644 index 000000000..ef82d6628 --- /dev/null +++ b/changelog.d/1446-draw-area-units.md @@ -0,0 +1,3 @@ +- Show a drawn area of 9,999.5 m² or more as 1.0 ha, and of 999,500 m² or + more as 1.00 km². The draw hint picked the unit before rounding, so it read + "10000 m²" and "100.0 ha" (Aditya, @Rohilalala, #1446). diff --git a/changelog.d/1466-panel-chrome-consistency.md b/changelog.d/1466-panel-chrome-consistency.md new file mode 100644 index 000000000..c7f1f28a1 --- /dev/null +++ b/changelog.d/1466-panel-chrome-consistency.md @@ -0,0 +1,5 @@ +- Give the side panels one look. Every panel title now uses the command dock's + label type in a brighter colour; a panel's edge and title light up on hover, + as the dock's trays do; and in the default theme each collapse button sits + 19px from its panel's edge (plus the scrollbar's width in an open, scrolling + panel where the system always shows scrollbars) (asturksever, #1466). diff --git a/changelog.d/1467-remove-operator-hud-layout.md b/changelog.d/1467-remove-operator-hud-layout.md new file mode 100644 index 000000000..556d551ee --- /dev/null +++ b/changelog.d/1467-remove-operator-hud-layout.md @@ -0,0 +1,3 @@ +- Remove the Operator HUD layout. Display > HUD > Layout and the HUD voice + action now offer Tactical, Minimal and Cyber; share links, scenes and saved + views that name Operator open in Tactical (asturksever, #1467). diff --git a/changelog.d/1471-context-panel-cleanup.md b/changelog.d/1471-context-panel-cleanup.md new file mode 100644 index 000000000..0d97cc397 --- /dev/null +++ b/changelog.d/1471-context-panel-cleanup.md @@ -0,0 +1,7 @@ +- Remove the Radio icon from the Context panel header, along with the compact + Radio player it opened; Radio stays in Context as its own section, and + Cockpit's compact Radio is unchanged. Context is also easier to read: the + Contacts / Space Missions modes are one segmented control, labels and + guidance are larger and higher contrast, the standby hint is plain text + instead of a dashed box, and the panel draws no glow halo. Cyber keeps its + own framed buttons and red titles (asturksever, #1471). diff --git a/changelog.d/1472-authoring-panel.md b/changelog.d/1472-authoring-panel.md new file mode 100644 index 000000000..cec54b18e --- /dev/null +++ b/changelog.d/1472-authoring-panel.md @@ -0,0 +1,4 @@ +- Rename the Scenes panel to Authoring and lay it out in steps (Scene, Shots, + Preview, Save & share) with larger, higher-contrast text, numbered shots, a + tooltip on every action and buttons that are disabled when they would do + nothing (asturksever, #1472). diff --git a/changelog.d/1485-cctv-view-thinning.md b/changelog.d/1485-cctv-view-thinning.md new file mode 100644 index 000000000..307da84ad --- /dev/null +++ b/changelog.d/1485-cctv-view-thinning.md @@ -0,0 +1,4 @@ +- Draw only the CCTV cameras in view, and thin them to one per ground cell when + zoomed out. Dragging the globe with the default 4,000-camera catalog no + longer stalls, and the camera picker lists the cameras in view with a count + of the ones outside it (samehkhamis, #1485). diff --git a/changelog.d/1491-gnss-source-window.md b/changelog.d/1491-gnss-source-window.md new file mode 100644 index 000000000..108aa50b2 --- /dev/null +++ b/changelog.d/1491-gnss-source-window.md @@ -0,0 +1,3 @@ +- Let a GNSS integrity source set the layer's evidence window (`windowMs`). A + missing or invalid value keeps the 30-minute default, and the layer row now + names the window in use instead of a fixed "30 min" (samehkhamis, #1491). diff --git a/changelog.d/1494-weather-product-display.md b/changelog.d/1494-weather-product-display.md new file mode 100644 index 000000000..3aabc5b6a --- /dev/null +++ b/changelog.d/1494-weather-product-display.md @@ -0,0 +1,4 @@ +- Let a weather source describe its own products through an optional + `display` object in each manifest (source, credit, units, coverage, info, + legend and extent). Each valid field replaces the built-in value, and + manifests without it render as before (samehkhamis, #1494). diff --git a/changelog.d/1499-vue-and-pinia.md b/changelog.d/1499-vue-and-pinia.md new file mode 100644 index 000000000..dc93aecdd --- /dev/null +++ b/changelog.d/1499-vue-and-pinia.md @@ -0,0 +1,205 @@ +- The language picker switches language in place instead of reloading the + page. Views re-render; code that publishes translated text words it again + through `onLocaleChange` (`src/i18n/index.js`), and ``/`dir` and + the page's `data-i18n` markup follow. `persistLocale` replaces + `persistLocaleAndReload`, and `registerUnsavedWork` and the switch's + unsaved-work prompt are removed, since a switch no longer loses anything + (samehkhamis, #1499). + +- A map source's `unavailableReason` may be a function, read when the reason + is shown so it follows the locale; the sources from `createDefaultMapSources` + (`./maps/defaults`) give functions. `MapSourceController` reads either form + (samehkhamis, #1499). + +- The application views translate their text with `t()` as they render, + so it follows the active locale; they no longer carry `data-i18n*` + attributes, and `npm run i18n` rejects those in a Vue template. The boot + pass (`applyDocumentTranslations`) still translates static page markup + such as the loading screen and a host's own markup (samehkhamis, #1499). + +- The page's locale lives in a vue-i18n instance installed in every + application view, and `t()`, `tn()` and the Intl formatters read it from + there, so a view that translates while rendering renders again when the + locale changes. `bindLocale` (`src/i18n/index.js`) keeps the locale in a + given ref; Core's Vite config builds vue-i18n without its message + compiler. Catalogs, messages and call sites are unchanged + (samehkhamis, #1499). + +- Cockpit's Display popover shows the Display panel's own HUD, Detection, + Parameters and 3D groups through Vue teleports: `CockpitDisplayPortal` + publishes the popover's slots (`publish`, with `afterRender` to lay out once + they have moved) instead of moving the groups itself (samehkhamis, #1499). + +- The Cockpit utility strip's measured layout renders from the `gev-cockpit` + store: the layout pass publishes `utilityLayout` (the HUD's + `--cockpit-utility-*` lengths, `primaryOnly` and `hideCollapsed`), and a host + constructing `CockpitController` passes `publish.utilityLayout` beside its + other publishers (samehkhamis, #1499). + +- The Cockpit Contact and briefing cards' collapse renders from the + `gev-cockpit` store: the controller publishes `contextCollapsed` and + `signalCollapsed` in its presence, and each card's `data-collapsed` and its + toggle's state, words and icon follow it (samehkhamis, #1499). + +- The panels' own state classes (`collapsed`, `dock-pinned`, `dock-pinned-top`, + `layout-auto-collapsed`, `accordion-collapsed`) render from the + `gev-panel-chrome` store: the shell, the rails and the accordion present each + panel's state there instead of writing its classes. `layoutLeftPanelRail` and + `layoutRightPanelRail` (`./ui/layout`) now call + `onCollapse(panel, {collapsed, auto})` and leave drawing it to the host. + Without a shell to own the panels, the voice tools' `set_panel_open` now + reports that there is no panel to open instead of writing its classes + (samehkhamis, #1499). + +- The shell's panel chrome no longer writes the panels' collapse buttons, the + dock tray toggles' names or the dock pins' pressed state: it presents each + panel's open and pinned state to the `gev-panel-chrome` store, and the + buttons, now views, call it there. `bindPanelDisclosure` (`./ui/panels`) binds + only a panel's Escape; it no longer takes `buttons` or `onChange`. `UiButton` + joins the component kit (samehkhamis, #1499). + +- `SceneControls` (`./ui/scenes`) no longer takes `elements` or writes the Scene + panel's buttons, status, progress or runtime chip: it presents them to the + `gev-scenes` store and the panel's buttons call its actions there. + `sceneElements`, `presentSceneButtons`, `presentSceneProgress` and + `presentSceneRuntime` give way to `sceneButtonsDisabled` and `scenePercent`. + It no longer stands down when the page has no Scene panel: ESC still stops a + playing scene and the runtime chip still shows (samehkhamis, #1499). + +- `initFirstRunExperience` (via `./application/chrome`) no longer writes the + launcher's classes, attributes or text, or removes the card: it presents them + to the welcome view's `gev-first-run` store (its new `store` option) and + connects the controls the view's buttons call (samehkhamis, #1499). + +- `IntelHUD` (`./ui/hud`) no longer builds or writes `#intel-hud`: it hands its + readouts, visibility, layout variant and style colors to its `publish` option, + and Core's `intel-hud` view renders them. A page places that view with + `
` instead of a bare + `#intel-hud` element. `./layers/vessels` shows the selected vessel's HUD + readout through `services.readouts.presentSelectedVessel()` instead of writing + `#hud-ais-vessel`; the application passes `src/hudReadouts.js`, which IntelHUD + follows (samehkhamis, #1499). + +- `LocationControls` (`./ui/location`) no longer builds the city and landmark + pills or writes the mini status: it presents the tray, including the search + error (`setSearchError()`), and hands it to its `publish` option, and Core's + dock renders it and calls `chooseCity()` and `choosePoi()`. Its `elements` are + now only `search`: `resetButtons` and `searchToggle` are gone, and the reset + buttons and the search toggle call its new `reset()` and `toggleSearch()`. It + presents the search field's open and searching state too (`searchExpanded`, + `searching`; a `searchExpanded` option starts it open), set through + `setSearching()`, `settleSearch()` and `closeSearch()`, and a host renders the + field's `expanded` and `searching` classes from it; `LocationSearch` takes a + `setSearching` option in place of writing the field's class, and + `NavigationController` a `settleSearch` option in place of `searchInput`. + `createOrbitIndicator()` is replaced by `showOrbit(active)`, which adds the + ORBIT indicator to the presented tray as `orbit: { active }` (null until + offered) (samehkhamis, #1499). + +- `createMapSourceControls()` (`./ui/maps`) no longer renders the map source + chips into a `container` or writes a `statusElement`: it presents the chip row + and status badge and hands them to its `publish` option, and Core's dock + renders them and calls `select()`. `renderMapStackChips`, `syncMapStackChips` + and `MAP_STACK_CHIP_CLASS` are removed (samehkhamis, #1499). + +- `./layers/launches` no longer writes the Space Missions roster or the selected + mission panel into `#space-mission-panel-host`: the layer publishes both + through `subscribePanel(listener)` and Core's Space Missions views render + them, calling the layer's panel commands (`selectMissionAt`, + `selectPreviousMission`, `selectNextMission`, `deselectMission`, + `focusSelectedLaunchSite`, `startSelectedReplay`, `toggleReplayPause`, + `cancelReplay`, `setReplaySpeed` and the `roster*` preview inputs). The + `bindMissionRosterItemKeyboardPreview`, `captureMissionRosterFocus` and + `restoreMissionRosterFocus` helpers are gone; the roster keeps focus itself + (samehkhamis, #1499). + +- `./layers/awareness` no longer writes the Contacts panel into + `#military-awareness-panel`: the layer publishes a plain panel view through + `subscribePanel(listener)` and Core's Contacts view renders it, calling the + layer's `navigatePrevious`, `focusCurrent`, `navigateNext` and `focusTarget`. + The `awarenessPanelControlKey`, `captureAwarenessPanelFocus` and + `restoreAwarenessPanelFocus` helpers are gone; the view keeps focus itself + (samehkhamis, #1499). + +- `./ui/cctv` and `./ui/radio` import Vue and Pinia: their controllers publish + the panel's state to a store that the CCTV and Radio views render, instead of + writing the panel's markup themselves (samehkhamis, #1499). + +- `./ui/display`'s `bindDisplayControls` is gone: the Display panel's controls + and the dock's style buttons call their owner through the display store's + `connect`/`act`, and its allocation and 3D model choices are the kit's + `UiSegmented`. `initDrawTool` takes `connect(actions)`, which returns their + disconnect, and no longer listens on the Draw controls; its view publishes + `color` and `label` too, and the shape choice is a `UiSegmented`. + `initGeoFileImport` takes `connect(actions)` the same way and no longer + listens on the Import button, picker or list, nor restores focus itself: the + `GeoImport` view does both, and its `publish` need not settle once the list + renders. Street Level's fit and panorama filters and Recent Imagery's mode are + `UiSegmented` too: the Street Level store's owner answers `setPano` and + `setRenderMode`, and `RecentImageryReadout` takes `onSetMode`. The Radio + panel's and Cockpit's internet radio controls and the tuner slider's events + call `RadioControls`, and the local RTL-SDR card's controls + `LocalSdrControls`, through the radio store's new `connect`/`act`; the card + labels its gain options itself. `act` returns what the answering action + returned. The Display, Radio, RTL-SDR, Cockpit, mission replay, style + parameter, rail card, observation timeline, imagery opacity, Street Level date + and Bhote Koshi sliders are the kit's `UiSlider`, a native range input that + emits numbers, and the pressed toggles are the kit's `UiButton`, which gains + `pressedClass`; so are the rail card chips, map source chips and location + pills (which now report `aria-pressed`), and the voice card's chips are the + kit's new `UiChip`. The selects are the kit's new `UiSelect`, the status + badges its new `UiBadge` and the one-line empty states its new `UiEmpty`. The + top-center clear, share, tilt, north-up and reset buttons, and the cockpit's + reset, call the shell through the new globe actions store, which also carries + the camera's tilt and compass heading. The recording safe frame renders what + `RecordingControls` publishes to the new recording store. `syncScopeControls` + (`./ui/display`), `shouldHideCollapsedRightPanels` (`./ui/layout`), + `bindClearLayersControl` (`./ui/layers`) and `createStyleParameters` + (`./ui/input`; the Display panel renders the style parameters VisualSettings + publishes to the display store) are deprecated: the Display view, the rail and + the globe actions view now own what they did. `./ui/input` imports Vue + (samehkhamis, #1499). + +- `./scenes` presents the scene sharing dialogs and the EDIT DETAILS / SHARE + SCENE bar through a store that the scene panels view renders, so a page that + omits `layer-panels` shows neither (samehkhamis, #1499). + +- `./voice/control`'s `createVoiceControl({ panel })` returns the voice + control's presenter instead of element handles: owners write it by part + (`update('detail', { text })`) and listen with `on('button', fn)`, and the + command dock renders it from a store. `createVoiceCommands`' `createControl` + and `VoiceCardControls` (`{ ui }` instead of `{ root }`) take the same + presenter; the `reset` option is gone, since a newer control replaces the + previous one (samehkhamis, #1499). + +- Pages place each view with a `
` + host and write their own loading screen; the `build/html` template expansion + and its `gev:template` markers are gone. `createApplication` takes `omitViews` + (for example `['provider-settings']`) for views an application leaves out + (samehkhamis, #1499). + +- The page's panels, dock, cockpit and dialogs are Vue views with the same + markup as before. Hosts that do not initialize Core's locale pass + `localePicker: false` to `createApplicationControls` to drop the language + picker, and Node tests that import application modules register the component + loader with `--import=/build/register-vue` (samehkhamis, #1499). + +- The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level and + Radio panels are one component, `UiPanel` (`src/ui/kit/`), with the same + markup plus the kit's `ui-panel` classes, their titles translated by `t()` + rather than `data-i18n`; `RailPanel` binds it to the panel chrome store. + `PanelCollapseButton` drops its `initial` and `initialTitle` props and takes + `startCollapsed`, so before startup presents a panel its button already shows + that state's glyph, words and `aria-expanded` (samehkhamis, #1499). + +- The Authoring, shot list and CCTV buttons and Street Level's nearest button + are the kit's `UiButton` (danger buttons in the `alert` tone, primary ones the + `primary` variant), keeping their classes; the CCTV on/off, coverage, + auto-hop, projection and adjust toggles now say their state with + `aria-pressed` (samehkhamis, #1499). + +- Each desktop sidebar has an icon rail that lists every panel on that side and + opens one with a click. One panel is open per side in every HUD layout, so the + collapsed panel headers no longer take up the sidebars. Phones keep the bottom + tab bar. The rail is the first view built with Vue and Pinia, which now ship + with the app; see docs/VUE-MIGRATION.md (samehkhamis, #1499). diff --git a/changelog.d/956-a22-brennero-webcams.md b/changelog.d/956-a22-brennero-webcams.md new file mode 100644 index 000000000..bee2545de --- /dev/null +++ b/changelog.d/956-a22-brennero-webcams.md @@ -0,0 +1,8 @@ +- Add the Autostrada del Brennero (A22, Brenner–Modena) motorway webcams as a + CCTV pack, the first Italian cameras. The 13-camera list is one keyless + request to the Open Data Hub (NOI Techpark) tourism API, which republishes + Autobrennero's own list with coordinates; frames are the operator's stills, + refreshed about once a minute and pinned to each camera's own `km.jpg`. + Each camera's ground elevation comes from a per-kilometre DEM table, since + the road climbs from the Po valley to the Brenner Pass. `CCTV_A22_ENABLED=0` + disables the pack (Jibran Tahir, @jibraaan, #956). Closes #155. diff --git a/docs/APPLICATION.md b/docs/APPLICATION.md index c5a2642e6..8848601d1 100644 --- a/docs/APPLICATION.md +++ b/docs/APPLICATION.md @@ -200,6 +200,24 @@ its pending results. Source disposal does not silently reinstate a default. `ui/composition` supplies the default engines to the shell. Applications can supply a HUD implementation or request policy through control services. The shared chrome owns the welcome/loading transition; standalone composition adds -Provider Settings. `build/html` expands an allowlist of component markers from -`src/ui/templates`; unknown names cannot read arbitrary filesystem paths. The -standalone document expands to the same markup as before this extraction. +Provider Settings. A page places each Vue view in `src/ui/views` with a host, +`
`, and writes its own loading +screen (`#loading-screen` with a `.loader-status` line) as static markup so it +paints before any script runs. `createApplication()` renders the views before +anything reads the page; `omitViews` names views the application leaves out, +such as `provider-settings`, and their hosts are removed. Leaving out +`layer-panels` also leaves out the scene sharing dialogs it draws, and leaving +out `scene-chrome` the before/after imagery dividers (the imagery still splits, +with no line or handle to move it), the status chips, the toast, the ORBIT +indicator, the FPS readout, the Scene actions panel, the data-pack attribution +cards, the wind inspection marker, the Space Missions replay vehicle, the +full-globe celestial ring, the screen-space map annotations, the visible +map target list and the Global Context compass; +leaving out `command-dock` the voice control and its pointer reticle; +leaving out `context` the Bhote Koshi reconstruction panel; and leaving out +`intel-hud` the Intel HUD overlay (`#intel-hud`), whose readouts the HUD +implementation publishes to the `gev-hud` store. An unknown +view name is an error. A host that does not initialize Core's locale passes +`localePicker: false` to `createApplicationControls` instead of editing the +markup. Node tests that import application modules register the component +loader with `node --import=/build/register-vue`. diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index efc7b86bc..f0bb9e979 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -276,7 +276,7 @@ compatible through re-exports. ### Adding a right-rail readout panel -Add the shell in `src/ui/templates/context.html` inside `#right-context-rail`: +Add the shell in `src/ui/views/ContextView.vue` inside `#right-context-rail`: a `.panel-collapsible` with a stable `id` and matching `data-panel-id`, the existing `.panel-header`, collapse button and a dedicated body. Register it with the existing panel chrome/layout owners; do not allocate rail space in @@ -297,13 +297,13 @@ compactStatus, blocks }` in place. Their full-width native button headers expose adapter owns accordion state. Generic cards do not select data or enable layers. `blocks` is an ordered array of loose, optional `{ id, type, ... }` descriptors, -rendered by `railCardBlocks.js`. There are no per-section disclosures: +rendered by `rail/RailCardBlocks.vue`. There are no per-section disclosures: - `summary`: `text`, a single summary line. -- `list`: `list: { ariaLabel, items }`, reconciled by `syncRowList` in `rowList.js`. +- `list`: `list: { ariaLabel, items }`, keyed by `rail/RailRowList.vue`. - `lines`: keyed `lines: [{ id, text, muted? }]` for details and status. - `settings`: `settings: [{ id, label, chips }]`; each labelled row has a 56 px - label column and a wrapping `syncChipGroup` from `chipGroup.js`. + label column and wrapping keyed chips from `rail/RailChips.vue`. - `legend`: `legend: { colors, labels, units?, zeroIndex?, categorical? }` for a ramp/scale (including a physical freezing anchor) or separate keyed swatches. - `actions`: keyed `actions: [{ id, label, params?, href?, onClick?, disabled?, @@ -372,12 +372,10 @@ facade's asynchronous teardown can yield. ## Ordinary action buttons -`src/ui/actionButton.js` is a native, listener-free view helper used by Scene -presentation and sharing-toolbar owners. It receives translated text and explicit -presentation/accessibility properties. Feature controllers own action dispatch, -state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS -surface and preserve IDs and layout hooks. The helper imports no application, -renderer, storage or services; it belongs to the consuming package closures. +The scene panels view and the CCTV panel draw their action buttons with the shared +`gev-action-button` classes in `src/ui/styles/action-button.css`. Feature +controllers own action dispatch, state, persistence and cleanup, and the views +preserve IDs and layout hooks. ## Display controls @@ -401,9 +399,9 @@ remain in their callers. `ui/maps` owns source-chip presentation, selection feedback and its subscription lifetime. It receives the existing controller and explicit state/action callbacks; it imports no renderer or application. Source construction and availability policy -remain with the map controller. Rebuilding controls removes their previous chip -listeners, and destruction suppresses late completions without owning or destroying -the supplied controller. +remain with the map controller. The dock renders the chips; rebuilding controls +drops their previous subscription, and destruction suppresses late completions +without owning or destroying the supplied controller. ## Layer panel @@ -741,13 +739,13 @@ DOM and platform middleware. ### Native form fields -`src/ui/formField.js` constructs native numeric inputs without listeners or -application state. Static selects and number inputs use the same `gev-field` -surface, with explicit compact and inline densities. Feature owners retain +Native numeric inputs are rendered by the views that own them; the CCTV pose +chip renders its inline input in `src/ui/cctv/CctvCalValue.vue`. Static +selects and number inputs use the same `gev-field` surface, with explicit +compact and inline densities. Feature owners retain option identity/order, bounds, parsing, draft values, input/change timing and teardown. CCTV effective-pose conversion and tuner/device operations remain -feature responsibilities. A Vue replacement can reuse this contract and CSS; -it replaces DOM construction, rather than adding a second state owner. +feature responsibilities. The shared stylesheet owns field surfaces and disabled opacity; feature and theme palettes use `--gev-field-background`, `--gev-field-border-color`, @@ -759,11 +757,9 @@ field rules separate, so Radio's palette remains an intentional exception. ### Read-only status badges -`src/ui/statusBadge.js` constructs/updates ordinary badges without listeners, -status inference or live-region policy. It consumes translated text and controlled -visibility; appearance variants share CSS rather than mapping machine states to -universal severity. CCTV owns frame/source and calibration rules and their data -attributes, while Weather owns coverage labels. The keyed rail-card owner keeps -node identity, header focus, disclosure and teardown. The shared adapter never -creates another state store or announces all card updates. A Vue view can replace -this adapter and retain these presentation inputs and styles. +Views draw read-only badges with the `gev-status-badge` classes, without +listeners, status inference or live-region policy. Appearance variants share +CSS rather than mapping machine states to universal severity. CCTV owns +frame/source and calibration rules and their data attributes, while Weather +owns coverage labels. The rail card keeps node identity, header focus, +disclosure and teardown, and does not announce every card update. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 2a404c939..35f300f5b 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -221,7 +221,7 @@ from 0–200%. OFF disables the slider while retaining its value. HUD layout changes preserve both settings; share links and scenes restore them. The historical missing-`scf` fallback remains 35%; explicit 0% keeps the hard edge. -With Scope OFF, Tactical, Operator and Minimal add text-sized black backplates +With Scope OFF, Tactical and Minimal add text-sized black backplates at 70% opacity behind telemetry, vertical metadata and scene chrome. Original HUD text color, glow and opacity are preserved. Scope OFF brightens the tagline and Active Style label with the shared secondary text color. The title plate @@ -231,6 +231,15 @@ the original HUD presentation; Cyber retains its existing skin. `node scripts/qa-scope-hud.mjs` checks native input, range, layout persistence, restoration and both Scope presentations. `QA_SCREENSHOT_DIR` saves screenshots. +## HUD layouts — October 29, 2026 + +Display > HUD > Layout offers Tactical, Minimal and Cyber; the `set_hud` voice +action accepts the same three. The Operator layout is retired. Share links +keep its enum code so older links still parse, and a link, scene or saved state +that names Operator restores the default Tactical layout. Operator was the +only layout that showed the HUD's top banner bar, so its markup and styles are +removed too. + ## Cyber HUD — September 30, 2026 Display > HUD > Layout includes Cyber, also available through the HUD voice @@ -246,8 +255,8 @@ red hover border in both map and cockpit, so clipping does not hide the highligh Cyber's map-side right rail opens one panel at a time while keeping collapsed launchers available. Display, CCTV and Context scroll their contents inside fixed headers and decorative frames. The narrow-screen rail remains scrollable to reach -each panel. Radio retains the shared nested Context player and compact -header disclosure, without relocating playback controls on theme changes. +each panel. Radio retains the shared nested Context player without relocating +playback controls on theme changes. Voice help/error popups and Location/Visual Presets pins extend beyond their Cyber frames without being clipped: the shapes decorate non-interactive @@ -466,6 +475,33 @@ not worldwide detections; units are strikes/km²/min ×10³. It is neither raw G flashes nor a live strike counter. Weather imagery orders scalar context below satellite, radar and lightning consistently across enable and history order. +A weather source can describe its own products (credit, units, coverage, +legend) through an optional `display` object in each product manifest: +`source` (≤60 characters, the row's source name), `credit` (≤200, the imagery +credit in Data attribution, shown as plain text and updated when a refreshed +manifest changes it), `units` (≤40), `coverage` (≤48, the coverage badge and +info line), `info` (≤600, the info tooltip), `legend` (2–16 `{ label, color }` +stops; label a plain decimal number or text, at most 16 characters either way, +a number reading back exactly as the legend shows it: within ±2^53−1, with +no leading zeros and no digits lost to rounding; color `#rrggbb`) and `extent` +(`{ west, south, east, north }` degrees, non-empty; west greater than east +crosses the antimeridian; used for coverage navigation and the +outside-coverage status). `units` and `legend` describe one scale and +apply only together: if either is absent or invalid, both keep the built-in +values, and the radar info heading names the units in use. Text must be +single-line, without control characters (U+0000–U+001F, U+007F–U+009F), line +or paragraph separators, or invisible and format characters: soft hyphen +(U+00AD), combining grapheme joiner (U+034F), Arabic letter mark (U+061C), +Mongolian vowel separator (U+180E), zero-width and direction marks +(U+200B–U+200F), bidi embedding and override (U+202A–U+202E), word joiner, +invisible operators and bidi isolates (U+2060–U+2069), variation selectors +(U+FE00–U+FE0F), byte-order mark (U+FEFF) and tag characters +(U+E0000–U+E007F). Each other field is validated on its own; an absent or +invalid field keeps the built-in value, so the bundled nowCOAST manifests, +which carry no `display`, look exactly as before. Accepted source +and units text is source data and is shown untranslated. The static Data +attribution entries remain the application's `credits` list. + Cyclones combine fixed NHC/CPHC status and summary GIS endpoints through a bounded same-origin provider. The status endpoint lacks browser CORS; each installation fetches keyless public sources at runtime, with no centralized ingestion service. @@ -535,7 +571,7 @@ Native ` ``` -`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated once at boot, and switching language reloads the page. A module that holds work a reload would lose (an unsaved edit, an open session) registers it with `registerUnsavedWork(() => busy ? msg('unsaved …') : null)` from `src/i18n/index.js`, and the language picker asks before reloading. +`data-i18n` replaces the element's text, so put it on the element that directly holds the text, never on a parent with child elements. `data-i18n-title`, `data-i18n-aria-label` and `data-i18n-placeholder` translate that attribute. `data-i18n-context="radio tag"` gives the element's text a context, the catalog entry `radio tag|All`, as `t('All', { context: 'radio tag' })` does in code. Markup is translated at boot and again after every language switch. + +## Switching language + +The language picker switches in place, without a reload. A view that calls `t()` while it renders follows by itself. Code that translates text and hands it on (to a store, a published view object, a layer's state, a DOM node) must word it again when the language changes: + +- **Present again.** `onLocaleChange(listener)` from `src/i18n/index.js` calls the listener after each change and returns the function that removes it; call that at teardown. The listener presents the view again from what it keeps, as `layerPanel.js` and `cockpitController.js` do. +- **Keep the English, translate when presenting.** Store `msg()` text or a function that words it, not its translation: the voice control takes `update(part, { text: () => t('MIC') })`, the scene director's status and the SDR receiver's messages keep such functions, and `imagerySplit.js` takes its titles that way. +- **Translated constants in a view** go in `computed`, not in a plain `const` set up once. + +A toast already on screen keeps its words. ## Rules @@ -66,16 +88,16 @@ An empty translation is reported, and so is one left as the English, plural form `npm run i18n` and `npm test` fail on UI text that is not marked: -- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. +- **Markup:** element text without `data-i18n`, and `title`, `aria-label` or `placeholder` without the matching `data-i18n-*`. In a Vue template, static text or a static `title`, `aria-label` or `placeholder` (rather than `t()`), and any `data-i18n*` attribute. - **Code:** string literals that reach the page without `t()`, `tn()` or `msg()`: assigned to `textContent`, `title` and the like; passed to `setAttribute('title' | 'aria-label' | …)`, `createTextNode`, `showToast` and other notice helpers; assigned to a layer's `error`, `lastError`, `loadingLabel`, `statusMessage` or `status` (a plain `error = …` too); or kept in `label:`, `title:`, `status:` and similar properties. Not reported: declarations (`let status = …`), default values, ternary branches and lower-case codes (`'auth-failed'`). The full list is `JS_SINKS` in `scripts/i18n.mjs`. Fix it by marking the text. When the text must stay as it is, say so where it lives: -| Case | How | -| --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML | -| Markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element | -| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: ` on the line or the line above | +| Case | How | +| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | +| Codes, units, product names in markup (`0.9 dB`, `ADS-B · 1090`, `OpenSky`) | `translate="no"` on the element; it covers everything inside, as in HTML | +| Page markup whose text code sets through `t()` (a status that starts as `OFF`) | `data-i18n-runtime` on the element (not in a Vue template) | +| A literal in code that is not UI copy (a product name, sample data) | `// i18n-ignore: ` on the line or the line above | Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowercase identifiers are skipped automatically, and so is `src/tools/`, whose text is written for AI clients rather than the page. @@ -84,6 +106,7 @@ Icons (`aria-hidden="true"`, Material Symbols), comments, CSS values and lowerca | File | What it holds | | ------------------------------- | ------------------------------------------------------------------------------ | | `src/i18n/index.js` | `t`, `tn`, `msg`, the Intl formatters, markup translation, switching language | +| `src/ui/i18n.js` | the page's vue-i18n instance: the active locale and the catalogs | | `src/i18n/marker.js` | `msg` alone, for modules the server or tools import (no catalogs) | | `src/i18n/locale.js` | shipped languages, their names, choosing the language at boot | | `src/i18n/locales/.json` | one catalog per language, keyed by English text | diff --git a/docs/SCENE-DOCUMENT.md b/docs/SCENE-DOCUMENT.md index f6dc66636..b96896024 100644 --- a/docs/SCENE-DOCUMENT.md +++ b/docs/SCENE-DOCUMENT.md @@ -1,6 +1,6 @@ # Scene document format -Scenes exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md) +Authoring exports a UTF-8 JSON project. Version 5 adds [data packs](DIRECTOR-DATA-PACKS.md) and is the write format. Versions 1, 2, 3, 4 and unversioned legacy files migrate on import without converting their existing shots into authored moves. Unknown versions or unsupported fields are rejected with a field path before the current project, selection or saved bytes change. diff --git a/docs/SHARE-LINK-FORMAT.md b/docs/SHARE-LINK-FORMAT.md index ea5636f39..5728d1ec3 100644 --- a/docs/SHARE-LINK-FORMAT.md +++ b/docs/SHARE-LINK-FORMAT.md @@ -107,7 +107,7 @@ probability of about 255 in 256 (many of them already fail as base64url). | 13 | thing | LEN | none | sub-message, see thing views | | 14 | `style` | enum | `normal` | normal, retro, surveillance, thermal, anime, noir, snow (retired) | | 15 | `mapStack` | enum | `photoreal` | photoreal, bing-aerial, bing-labels, esri-imagery, osm | -| 16 | `hudVariant` | enum | style | tactical, operator, minimal, cyber | +| 16 | `hudVariant` | enum | style | tactical, operator (retired), minimal, cyber | | 17 | `bloomIntensity` | uint | style | | | 18 | `bloomVersion` | uint | 2 | | | 19 | `sharpenIntensity` | uint | style | | @@ -129,6 +129,9 @@ table learns its code. A new style's settings are encoded against its own defaults, while a decoder that does not know the style uses Normal's; adding a style also extends the pinned style-defaults table. +`hudVariant` code 1 (`operator`) is retired: the app no longer offers that +layout and restores it as `tactical`. The code stays reserved. + Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3 `celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). diff --git a/docs/TRANSLATORS.md b/docs/TRANSLATORS.md index 3e5636cd9..f83316c7a 100644 --- a/docs/TRANSLATORS.md +++ b/docs/TRANSLATORS.md @@ -22,7 +22,7 @@ Every other language is one flat JSON file, `src/i18n/locales/.json`, keye A `tn()` entry is keyed by the English plural form and holds one string per `Intl.PluralRules` category of that language. A missing entry falls back to English. -The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching reloads the page. +The page picks its language from `?lang=`, then the choice stored by the language picker (the globe button in the bottom-right corner), then the browser languages, then English. `?lang=` is not stored, and copied share links leave it out. Switching applies at once, without a reload. ## The check diff --git a/docs/UI-OWNERSHIP.md b/docs/UI-OWNERSHIP.md index 8d24215c7..46d0d5a2b 100644 --- a/docs/UI-OWNERSHIP.md +++ b/docs/UI-OWNERSHIP.md @@ -11,12 +11,18 @@ scene playback and voice. It delegates; new behavior belongs with its owner. | `cockpitCoordinator.js` | Cockpit entry/rollback, readouts and the single Display portal | | `visualSettings.js` | Effects, detection overrides, visual restoration and render-loop lifetime | | `panelChrome.js` | Disclosure, docking, layout and Cockpit panel restoration | +| `panelNavigation.js` | Desktop panel rails: each side's offered panels and the open one | | `aircraftDisplay.js` | Shared commercial/military 3D display preferences and controls | | `layerBindings.js` | Data-manager attachment, selected-tracker adoption and camera-entry listeners | | `displayBindings.js` | Keyboard/display subscriptions and frame-rate monitor lifetime | | `shareRestoration.js` | Initial share/layer restoration and cancellation | | `shellFeedback.js` | Loading feedback, notices and toast lifetime | +Owners that present UI do so through Vue: they publish plain view objects to a +Pinia store in `src/ui/stores/`, views in `src/ui/views/` render them, and user +actions return through the handlers the owner connects to that store. See +[Building UI](../CONTRIBUTING.md#building-ui). + Owners receive named services, elements, operations and readers. They do not receive the application shell itself. Readers resolve a replaceable collaborator at use time; operations cross an ownership boundary without copying its state. diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md new file mode 100644 index 000000000..3dcd7f63b --- /dev/null +++ b/docs/VUE-MIGRATION.md @@ -0,0 +1,368 @@ +# 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`, `UiTooltip`, `UiIcon`, +`UiButton`, `UiPanel`, `UiSegmented`, `UiSlider`, `UiChip`, `UiSelect`, +`UiBadge` and `UiEmpty` so far, with more joining as the controls they serve +are converted. One component +serves every use of a control, with props for the differences (`UiPanel`'s +`sideways`, `glow`, `scroll`), rather than a copy per panel. The components +are styled by the existing stylesheet and custom properties; a kit class such as +`ui-button--icon` carries no Core style yet, and a converted control keeps +its own classes beside it. They contain no application state. +`UiPanel` takes a `title`, slots and `v-model:collapsed`, but its +`collapsed` drives only the button and `ui-panel--collapsed`: the root's +`collapsed` class, which hides the body, is drawn at once by `v-panel-state` +from the panel chrome store, and there is no `closable`, since Core's panels collapse rather than close. +`UiSegmented` adds `keyboard` (`none`, `arrows`, or `radio` with +Home and End), since each of Core's groups keeps its own keyboard policy, and +`itemClass`/`activeClass` for Core's segment classes. A key moves focus at +once and asks for a change only when it lands on another option. Every +segmented group in Core is a `UiSegmented`; the old `segmentedControl.js` +adapter is gone. +`UiButton` adds `pressedClass`, Core's class on a pressed toggle (most use +`active`); its pressed classes toggle through `classList`, so a class other +code adds (a voice highlight) survives a press. Every pressed toggle in the +views is a `UiButton` except Bhote Koshi's cinematic button and the +tilt-map button, whose owners still write their state into the DOM. Core's +pressable chips and pills (rail card chips, map sources, cities and places) +are `UiButton`s too; `UiChip` is a non-interactive label chip, used by +the voice card. +`UiSelect` renders the bare native select (Core's labels sit outside it or +come as an `aria-label`) with `v-model`, `options` and `disabled`; +options that carry their own attributes come through the slot, a caller's +`@change` still reaches the select, and `hold` works as in `UiSlider`. +Without `hold`, a select takes its model back after its own change renders, +so a pick its owner refuses goes back; an owner that answers later (the SDR +gain) takes `hold`. Every select in the views is a `UiSelect`. +`UiBadge` takes a `tone` and carries Core's `gev-status-badge`; its +density variants and colours stay caller classes, and it renders the CCTV +source and calibration badges and the rail card badges. `UiEmpty` takes +a `title` and `hint`, but Core's empty states are one line, so the title +is the text itself rather than a bold heading; the Scene shot list, mission +roster and imagery strip use it. The locale picker's and the mission tables' +empty rows stay a paragraph and table cells. +`UiSlider` keeps the native range input rather than drawing a +`role="slider"` track, so Core's keyboard, form behaviour and styling stay; +it takes `v-model`, `min`, `max`, `step` and `disabled`, emits a +number, and adds `hold` (never rewrite a focused handle, and take the model +back on blur) and `unstyled` (the browser's own range, as Bhote Koshi's +are, without Core's slider surface). A caller's own input, change and blur +handlers still reach the input, and a null model leaves it as it is. It is +a functional render-function component, so the portable `./ui/input` rows +use it without a component compiler. Every range in the views is a +`UiSlider` except the radio tuner's: its owner moves the handle back to the +selected station on a drag's first input without changing the slot, which +a functional component would not render again. + +Controls share one skin, so every panel reads the same. Every scrolling +surface carries the kit's `.ui-scroll`, which draws the thin +themed scrollbar from `--scroll-thumb` and `--scroll-track`; the Cyber theme +sets those two tokens, and Cockpit's HUD windows keep their accent tint. A +surface without it would draw the platform's light scrollbar, because the +page declares no dark `color-scheme`. Selects and number fields are +`.gev-field`, which draws its own chevron in place of the platform's arrow +and keeps native spinners dark; ranges are `.gev-quantitative-slider`, with +a feature supplying only its accent. + +## 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. The rail panels + are `RailPanel`s: the kit's `UiPanel` drawing Core's panel markup, bound to the `gev-panel-chrome` + store, with their titles rendered by `t()` rather than `data-i18n`. Each panel's + collapse button, the dock tray toggles and the + dock pins render the open and pinned state PanelChrome presents to the + `gev-panel-chrome` store, and their clicks call it there. Each panel's + root draws its own state from there too (`v-panel-state`: `collapsed`, + `dock-pinned`, `dock-pinned-top`, `layout-auto-collapsed`, + `accordion-collapsed`): PanelChrome, the rails and the accordion present + it rather than writing classes. The directive applies it in a synchronous + watcher, not on Vue's next render, because the rails measure a panel in + the same task that opens or closes it. PanelChrome and its preference + restore, and the phone shell, read a panel's state back from the store + (`_shownPanel`; the markup's classes before its first presentation), so + once a panel is presented a class written outside the store no longer + counts. The rails, the accordion and the hover disclosure + (`panelDisclosure.js`), which act within the same task as a layout, read + the classes the directive has just drawn. Done. +3. **Panels.** Each template and its wiring become a component, keeping ids, + classes and CSS; text a template renders with `t()` drops its `data-i18n` + marker. The page places each view with 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, removing the + hosts of any views its `omitViews` names. Views belong to the page and are + unmounted only when a later call omits them. The loading screen is static markup in the page 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. Component text uses + `t()`, which follows the locale as the view renders; the views carry no + `data-i18n*` markers, which `npm run i18n` rejects in a template. 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; the panel's buttons, status + line, progress bar and the runtime chip render the panel SceneControls + presents there, and the buttons and file chooser call its actions), 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, sharpen and the + active style's parameter sliders from the `gev-display` store, which + VisualSettings publishes to and answers the sliders through, 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 controls call DisplayBindings, the draw tool and the file import + through the store's `connect`/`act`, the newest owner of an action + answering it; `GeoImport` hands keyboard focus on before a focused row + goes, with `v-focus-safe-children`; 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 whose controls it answers + through the store's `connect`/`act` (the shared `createOwners()`; a + refused station tag gets the kept tag back from `act`), and `SdrCard` the + local RTL-SDR card that LocalSdrControls publishes beside it and answers + the same way; fields a person edits take their value through + `v-field-value`, which never writes a focused field; + `RadioTuner` renders the directory tuner, where RadioControls keeps the + drag and its slot, takes the slider's pointer, key and value events + through the store and publishes the band, readouts, slider and needle, + and the component lays out the tick tape against the dial's measured + width and lays out the snapped needle before it drops the drag class; + `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; beside + the voice control, the dock teleports each voice pointer reticle to the + body where its `PointerReticle` presents it in the `gev-pointer-reticle` + store), 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), the + imagery dividers (`ImagerySplitView`, in the scene chrome, teleports each + before/after divider that `createImagerySplit` publishes to the + `gev-imagery-split` store to the body; the split keeps the + drag, keyboard, CSS position and scene split), the status chips + (the top-center loading status and the road network and camera grid + chips render what ShellFeedback and CctvControls publish to the + `gev-status` store; `SplitFlapText` plays the departure-board flip that + `splitFlap.js` plans, writing only the label's one text node; the toast + renders what each owner's `createToast()` publishes there, on that + owner's own timer, and the title bar's FPS readout what + `createFrameRateMonitor()` publishes), the globe actions + (`GlobeActions` renders the top-center bar; its clear, share, tilt, + north-up and reset buttons, and the cockpit's reset, call the shell + through the `gev-globe-actions` store, which also carries a clear or a + reset under way and the camera's tilt and heading + that `bindCameraOrientationControls` publishes each drawn frame it + changes, which only `GlobeCompass` renders; an owner publishes only + while it answers the button, so a shell replaced by a newer one leaves + its state alone), the recording safe frame (the scene chrome renders + the frame `RecordingControls` publishes to the `gev-recording` store), + the Scene actions panel (`SceneInteractions` + teleports what the scene interactions owner publishes to the + `gev-scene-interactions` store into the viewer's container; the owner + keeps the pick handler and focuses the picked action), the data-pack + attribution cards (`DataPacks` teleports what the data-pack presentations + publish to the `gev-data-packs` store into the viewer's container and + stops a removed card's player), the wind inspection marker + (`WindInspectionMarker` teleports the marker into the wind layer's + container where `createWindInspectionMarker()`, now in + `src/layers/wind/inspectionMarker.js` so the portable `./tools` entry + needs no Vue, places it after each rendered frame; an unchanged frame + publishes nothing), the Space Missions replay vehicle + (`MissionReplayOverlay` teleports into the globe container what the + launches layer presents through `subscribeReplayOverlay()`, which the + application shell publishes to the `gev-mission-replay` store, so the + portable `./layers/launches` entry needs no Vue), the Bhote Koshi + reconstruction panel (`BhoteKoshiPanel`, last in the context rail, + renders what the event layer publishes to the `gev-bhote-koshi-panel` + store: each control's text, hidden, disabled, value and attributes as + the layer writes them, unchanged frames sending nothing; the rail takes + `bhote-event-active` while it shows), the full-globe celestial ring + (`CelestialRing` teleports into the globe container what `CelestialRing` + in `src/celestialRing.js` publishes to the `gev-celestial-ring` store and + hands its two canvases back for the owner to paint; the application shell + passes the store in, so the keyhole helpers other packages import need no + Vue), the screen-space map annotations (`ScreenAnnotations` teleports to + the body the board the screen annotation renderer publishes to the + `gev-screen-annotations` store after each rendered frame: each mark's SVG + parts as attribute records, the fading marks until they leave; the view + hands back the callout text and mark groups the renderer measures, and + the tracked-entity fade is measured once the frame is in the document; + the styles it injected now live in `src/ui/styles/annotations.css`), the + visible map target list (the scene chrome renders the buttons and status + the world overlay presents through `subscribeWorldOverlayActions()`, which + the application shell publishes to the `gev-world-overlay-actions` store + and answers with `activateWorldOverlayAction()`; the module needs no Vue + for the portable transit entry, and its card and detection canvases stay + painting surfaces it draws on, like the Cesium canvas), the Global + Context compass and contact bearings (`AwarenessDirection` teleports into + the globe container what the awareness layer presents through + `subscribeDirectionOverlay()`, which the application shell publishes to + the `gev-awareness-direction` store, so the portable `./layers/awareness` + entry needs no Vue), the Intel HUD (the `intel-hud` view renders + `#intel-hud`, its corners, edges and bottom bar from the `gev-hud` + store, which IntelHUD publishes to through its `publish` option: the + readouts, the typing summary, the REC blink, visibility, layout variant + and style colors; the vessel layer presents the selected vessel's + readout through `src/hudReadouts.js` instead of writing into the HUD), + the first-run mission launcher (the welcome view renders the card's + visibility, busy state, status line, copy and scroll fade from the + `gev-first-run` store that `initFirstRunExperience` publishes to, and its + choices, DISMISS and "Don't show this again" call the launcher's + controls; a refused save hands the tick back to the view to revert. The + launcher still reads the rendered card for its hit test, keyboard trap, + focus and overflow measurement), 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`. `CockpitSignalList` renders the live signal + rows; `v-focus-safe-children` turns Vue's move of the focused row into + moves of the rows around it, and hands focus to the shown briefing tab + before the focused row is removed, so a keyed list reorders several times + a second without dropping keyboard focus. 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 controller also + publishes which surfaces are shown: the entry button and TR-3B chip in + the Context panel, the HUD with its exit and reset buttons and briefing + card, and the Contact card. Focus moves to the exit button, and back to + the entry, only after Vue renders them. The cards' collapse is part of + that presence too: each card's `data-collapsed` and its toggle's words + and icon render from it. The controls keep the controller's listeners, + and the cards' layout stays with the controller, which reads their + visibility when it lays them out. Its layout pass publishes the utility + strip's measured fit (the HUD's anchor and height limits, whether only the + expanded control fits and which control is hidden from assistive + technology), which `CockpitView` and `CockpitUtilityControls` render; the + briefing and Contact cards' positions stay imperative, because the same + pass measures them straight after writing them. + `CockpitUtilityControls` renders the Display + and Radio launchers, which popover is open and the compact Radio + controls from the `gev-radio` store that RadioControls publishes to and + answers through. The Cockpit display portal publishes the Display + popover's slots to the `gev-display` store while Cockpit is on, and + `DisplayPanel` teleports its HUD, Detection, Parameters and 3D groups into + them and back; the portal keeps each surface's scroll position and puts + back focus the move dropped). + Layers that write into the right rail directly gain a slot instead. + +4. **Cleanup.** The build-time template expansion is retired: `index.html` + lists the view hosts and the loading screen itself. Every owner that + showed UI state through hand-written DOM writes now publishes to a store + that a view renders, apart from the imperative parts this document + records surface by surface; [Building UI](../CONTRIBUTING.md#building-ui) + now requires the pattern for new UI. The page-level markup tests read + `renderApplicationHtml()`, the real views rendered on the server, and new + tests mount components; three tests still read a `.vue` file's source + (`cyberTheme`, `panelStorageDocs`, `geoFileImport`). The views translate with `t()` + while rendering, and vue-i18n holds the locale they read; the attribute + translation pass is left for static page markup (the loading screen and a + host's own markup). An explicit API for hosts to register panels and rail items is + deferred until a host adds one: no host does today (the hosted + application's account interface is its own markup), and the first such + panel, the hosted timeline, will shape it. + +## 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 `. +- 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`. diff --git a/docs/panel-surfaces.md b/docs/panel-surfaces.md index 2d18023e7..30f1be88c 100644 --- a/docs/panel-surfaces.md +++ b/docs/panel-surfaces.md @@ -12,7 +12,7 @@ A rail panel uses an outer owner with its existing stable `id`, matching `data-panel-id`, and `panel-collapsible` class. Its first non-glow child is the surface. Keep the header and any outward popup outside the scrolling body: -```html +```vue

Example

- +
@@ -92,15 +89,27 @@ Do not copy the rail example's `.panel-collapse-btn` class into the dock: existing dock rules intentionally hide that class. The generic surface skin does not replace the dock's disclosure policy. -The existing `PanelChrome` binds `.panel-collapse-btn[data-collapse-target]` -at initialization and routes state through `setPanelCollapsed`. For additions +A panel with Core's standard header (glow, title, badges, divider, actions +and collapse button) is a `RailPanel` (`src/ui/rail/`): the kit's `UiPanel` +bound to the `gev-panel-chrome` store, with its state classes drawn by +`v-panel-state` and its button's click asking `PanelChrome` to toggle it. +A panel with its own header uses `PanelCollapseButton`, which renders the +collapse button (`.panel-collapse-btn`) from the open state `PanelChrome` +presents to that store; until then, both show their `startCollapsed` state, +which should match the markup's `collapsed` class. +Either way, the click reaches `PanelChrome`'s `setPanelCollapsed`. `PanelChrome` collects the buttons' +targets at initialization and binds each panel's Escape. For additions present at startup, keep those hooks and the existing chrome lifecycle. A -dynamically mounted panel must bind and dispose its own disclosure using -`bindPanelDisclosure` / `collapsePanelOnEscape` from `panelDisclosure.js`, with -the application owning state changes. Do not rerun the whole application chrome +dynamically mounted panel must own its disclosure: its own state, button and +Escape, bound and disposed with `bindPanelDisclosure` / +`collapsePanelOnEscape` from `panelDisclosure.js` (which bind Escape only). Do not rerun the whole application chrome initializer just to add one panel. `layoutLeftPanelRail` and `layoutRightPanelRail` accept arbitrary direct `data-panel-id` children; their -host owns layout scheduling, observations and collapse callbacks. Dynamic +host owns layout scheduling, observations and collapse callbacks. A rail +never writes a panel's classes: it calls `onCollapse(panel, {collapsed, +auto})`, and the host draws that state (the application presents it to the +panel chrome store, which each root's `v-panel-state` draws at once, so the +rail can measure the panel in the same task). Dynamic content that changes size may require an owner-scheduled layout pass or an owned `ResizeObserver`; adding CSS attributes does not register new observers. diff --git a/docs/translations/ar.md b/docs/translations/ar.md index c3fc58237..22d5e5e35 100644 --- a/docs/translations/ar.md +++ b/docs/translations/ar.md @@ -18,7 +18,7 @@ Translation sources: abiyeenzo (#915). ## Digits and direction - **Western digits (0–9)**, as in the catalog. The HUD reads like an instrument panel, and mixing ٠–٩ with Latin codes and coordinates misreads. `intlLocale('ar')` in `src/i18n/locale.js` passes `ar-u-nu-latn` to every Intl formatter, so browsers that default to Arabic-Indic digits for an Arabic region still show 0–9. -- **Right to left.** `ar` is in `RTL_LOCALES`, so the page gets ``. The title, style indicator and both rails swap sides; the dock and the corner row reverse. The intel HUD (classification block, MGRS, coordinates, GSD) stays a left-to-right instrument overlay; its translated lines read right to left inside it. Media transport (previous / play / next) and the radio dial stay left to right, as on any player. Layout rules: `src/ui/styles/rtl.css`. +- **Right to left.** `ar` is in `RTL_LOCALES`, so the page gets ``. The title, style indicator, both panel stacks and their icon rails swap sides; the dock and the corner row reverse. The intel HUD (classification block, MGRS, coordinates, GSD) stays a left-to-right instrument overlay; its translated lines read right to left inside it. FM seek and the radio dial stay left to right, as on any receiver. The rails' screen-reader names follow the mirrored layout: «Left panels» is «اللوحات اليمنى». Layout rules: `src/ui/styles/rtl.css`. - Latin runs inside Arabic sentences (product names, codes, `{placeholders}` filled with English data) are isolated by the bidi algorithm; write them as plain text, without manual direction marks. - Position counters: a translated counter says «من» («القناة {index} من {total}», «الإطار {index} من {total}»); a spaced slash between two isolated numbers swaps them in right-to-left text. Counters the code builds from bare numbers (`01 / 25`, `1 / 3`) are shown left to right by `rtl.css`. - Code stays left to right: the JSON fields of the scene editor are LTR in every language. diff --git a/docs/translations/it.md b/docs/translations/it.md index 81854d6fe..722916101 100644 --- a/docs/translations/it.md +++ b/docs/translations/it.md @@ -79,7 +79,7 @@ The `many` form is the `other` text with «di» after the number when a noun fol | briefing / panel / dock | briefing / pannello / dock | | cockpit | cockpit | | preset / visual preset | preset / preset visivo | -| Tactical / Operator / Minimal / Cyber | Tattico / Operatore / Minimale / Cyber | +| Tactical / Minimal / Cyber | Tattico / Minimale / Cyber | | Scope (circular viewport mask) / keyhole | scope / foro | | feather / fade | sfumatura / dissolvenza | | detection overlay | overlay di rilevamento | diff --git a/docs/translations/ja.md b/docs/translations/ja.md index 2253bdc45..9e612e8e4 100644 --- a/docs/translations/ja.md +++ b/docs/translations/ja.md @@ -61,7 +61,7 @@ Translation source: abiyeenzo (#913). | scene / shot | シーン / ショット | | briefing / panel / dock | ブリーフィング / パネル / ドック | | preset / visual preset | プリセット / ビジュアルプリセット | -| Tactical / Operator / Minimal / Cyber | タクティカル / オペレーター / ミニマル / サイバー | +| Tactical / Minimal / Cyber | タクティカル / ミニマル / サイバー | | Scope (circular viewport mask) / keyhole | スコープ / キーホール | | Clean UI | クリーンUI | | cockpit | コックピット | diff --git a/docs/translations/zh-CN.md b/docs/translations/zh-CN.md index e3b7bcad1..b3a3f973a 100644 --- a/docs/translations/zh-CN.md +++ b/docs/translations/zh-CN.md @@ -43,7 +43,7 @@ Translation sources: abiyeenzo (#914) and EDIT-Aeterna (#979). | briefing / panel / dock | 简报 / 面板 / 停靠栏 | | preset / visual preset | 预设 / 视觉预设 | | Display / HUD layout | 显示 / HUD 布局 | -| Tactical / Operator / Minimal / Cyber | 战术 / 操作员 / 精简 / 赛博 | +| Tactical / Minimal / Cyber | 战术 / 精简 / 赛博 | | Scope (circular viewport mask) / keyhole | 视域 | | Clean UI | 纯净界面 | | cockpit | 座舱 | diff --git a/index.html b/index.html index 86e5fd693..803bc2417 100644 --- a/index.html +++ b/index.html @@ -26,26 +26,28 @@ with the sources: src/materialSymbolsSubset.test.mjs fails when a source names a glyph this list is missing, because a missing glyph does not draw a box — the ligature never forms and the element renders the literal word. --> - + - +
+
+
+
+
+
+
+
+ +
- - - - - - - - - - - - - - - + +
+
+ +

GOD'S EYE VIEW

+

Initializing photorealistic world...

+
+
diff --git a/package-lock.json b/package-lock.json index 3db337442..dd60d3383 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,16 @@ "mapillary-js": "^4.1.2", "mgrs": "^2.1.0", "pbf": "^5.1.2", - "satellite.js": "^6.0.2" + "pinia": "^2.3.1", + "satellite.js": "^6.0.2", + "vue": "^3.5.44", + "vue-i18n": "^11.4.15" }, "devDependencies": { + "@happy-dom/global-registrator": "^20.14.6", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/test-utils": "^2.5.1", + "happy-dom": "^20.14.6", "prettier": "3.9.6", "puppeteer": "^25.16.0", "sharp": "^0.35.4", @@ -33,6 +40,52 @@ "node": ">=24.14.0 <25 || >=26 <27" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cesium/engine": { "version": "22.3.0", "resolved": "https://registry.npmjs.org/@cesium/engine/-/engine-22.3.0.tgz", @@ -537,6 +590,20 @@ "node": ">=18" } }, + "node_modules/@happy-dom/global-registrator": { + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/@happy-dom/global-registrator/-/global-registrator-20.14.6.tgz", + "integrity": "sha512-Xd2FwCdm0cAZ+EG0upp69tpxBT9jtvVxaZv1lnbgWsNaZyEuPs0PojYgvCmtQ1RRdRCYCyWTeLjn3Z0aSxDchA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "happy-dom": "^20.14.6" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1064,6 +1131,73 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@intlify/core-base": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.15.tgz", + "integrity": "sha512-yHNg7+M6VKQuj+KUO/CrtcBMm3iAspvfFe1H/eKI0TP0pn2T2srBF1gEb/TL3c8RA/BZZGOIySwtZD0i+9/hhw==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.4.15", + "@intlify/message-compiler": "11.4.15", + "@intlify/shared": "11.4.15" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.15.tgz", + "integrity": "sha512-NFV9LsP720IkR6BHNfUYBSkd/abocWimcOWAS63D3spwczy8zDvXCf9WMyhCVQ7FcYkWyECZke18x8D6UNVBFw==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.15", + "@intlify/shared": "11.4.15" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.15.tgz", + "integrity": "sha512-dV6apms3MXAG3jZ/6bAq+IW2IKDGasmjA/Aoi0JVKCStVi6GbzrPruCVbU/Djn1n41w5EiM3KdnpTuo6I1qe5w==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.4.15", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.15.tgz", + "integrity": "sha512-jlTJkM4bMdr267e1iUE2RMNz1LyoYU6IPReKjnQM6RvipEBUVwmMHbeUckDcOmR+WeungEGjaNmSQJdQzJcOfQ==", + "license": "MIT", + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, "node_modules/@jtarrio/signals": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@jtarrio/signals/-/signals-0.10.1.tgz", @@ -1115,6 +1249,13 @@ "node": ">=24.0.0" } }, + "node_modules/@one-ini/wasm": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.2.1.tgz", + "integrity": "sha512-TUqERXGNTifZ9y2g3wPxQrw3HpHv/02DsW3D90T9x0hhonrL1ZqpSmNrU2XkoIq0fP1N6gZfVQzy2Fw1ZvGBNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@puppeteer/browsers": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.6.tgz", @@ -1556,6 +1697,16 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.6.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz", + "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, "node_modules/@types/pako": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", @@ -1605,6 +1756,171 @@ "integrity": "sha512-Qu3Nn6JFuF4+sHKYl+IcX9vYiI40ogleXzFFSxoE1W94rG98o/kXs8uJ0QSfFzuwBCZWlGfUGpPkgwuuX4PchA==", "license": "MIT" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz", + "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz", + "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz", + "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.44", + "@vue/runtime-core": "3.5.44", + "@vue/shared": "3.5.44", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz", + "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.5.1.tgz", + "integrity": "sha512-V/4a99odJ7hapr0bRt35XxljGk8hefq31gSOnlKTcEq2fUeoj2qb+FcpJYfQu4qfYhW9Ug+O2+CZ3QmZ7gXDzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^2.0.0", + "vue-component-type-helpers": "^3.0.0" + }, + "peerDependencies": { + "@vue/compiler-dom": "3.x", + "@vue/server-renderer": "3.x", + "vue": "3.x" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + } + } + }, "node_modules/@zip.js/zip.js": { "version": "2.8.18", "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.18.tgz", @@ -1619,6 +1935,16 @@ "node": ">=18.0.0" } }, + "node_modules/abbrev": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz", + "integrity": "sha512-/XrFJgzQQQHpti1raDJC6m4ws6aNktmjBlhk8Fdlk7LwCEuDoieEJJY9OFHjfiFJFFRM2tK+Ky/IsfbbmlMu1w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/ansi-regex": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", @@ -1667,18 +1993,54 @@ "pnpm": ">=10.10.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/bitmap-sdf": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/bitmap-sdf/-/bitmap-sdf-1.0.4.tgz", "integrity": "sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==", "license": "MIT" }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/browser-split": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/browser-split/-/browser-split-0.0.1.tgz", "integrity": "sha512-JhvgRb2ihQhsljNda3BI8/UcRHVzrVwo3Q+P8vDtSiyobXuFpuZ9mq+MbRGMnC22CjW3RrfXdg6j6ITX8M+7Ow==", "license": "MIT" }, + "node_modules/buffer-image-size": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/buffer-image-size/-/buffer-image-size-0.6.4.tgz", + "integrity": "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -1762,6 +2124,23 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1843,6 +2222,35 @@ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", "license": "ISC" }, + "node_modules/editorconfig": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-3.0.2.tgz", + "integrity": "sha512-T0ix8GhtxyKVfUFEcvdNDt3YGqlwkFHbD4/5bgFUDgFmxhI/cSRAeJ87/Sz//Cq8Eam6JX/e23RkoFO71P7aAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.2.1", + "commander": "^14.0.3", + "minimatch": "~10.2.4", + "semver": "^7.7.4" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1880,6 +2288,18 @@ "node": ">= 0.8" } }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz", @@ -1953,7 +2373,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, "license": "MIT" }, "node_modules/etag": { @@ -2056,6 +2475,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", @@ -2079,6 +2516,25 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "license": "MIT" }, + "node_modules/happy-dom": { + "version": "20.14.6", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.14.6.tgz", + "integrity": "sha512-5DDsf4QmEIlRL6NkCv/fxSjYHHkZceL/IaYQdJc2h3G7DRTEai++VTjbfL0LB9geW2VrA3+vrjF3Efx+OTtX0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "buffer-image-size": "^0.6.4", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/hls.js": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.3.tgz", @@ -2138,6 +2594,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/is-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", @@ -2157,6 +2620,35 @@ "@types/estree": "*" } }, + "node_modules/js-beautify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz", + "integrity": "sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^3.0.2", + "glob": "^13.0.6", + "js-cookie": "^3.0.8", + "nopt": "^10.0.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/jsep": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", @@ -2216,6 +2708,16 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -2343,6 +2845,32 @@ "dom-walk": "^0.1.0" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2368,10 +2896,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "dev": true, + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -2392,6 +2919,22 @@ "integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==", "license": "MIT" }, + "node_modules/nopt": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz", + "integrity": "sha512-df3sBr/6ax9hSGuC3CspvLlbnX8cP5L5nZwXF8cGN8l0zSWR6BvzmQ6jPUKjvo6+/xdpkNvEcucBNUdBeeV13g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^5.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/nosleep.js": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/nosleep.js/-/nosleep.js-0.12.0.tgz", @@ -2427,6 +2970,23 @@ "node": ">= 0.8" } }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pbf": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", @@ -2443,7 +3003,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2459,6 +3018,28 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/polylabel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", @@ -2475,10 +3056,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "funding": [ { "type": "opencollective", @@ -2495,9 +3075,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2528,6 +3108,13 @@ "node": ">= 0.6.0" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/protobufjs": { "version": "8.8.0", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", @@ -2825,7 +3412,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2959,6 +3545,13 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -3115,6 +3708,81 @@ "rollup": "^2.25.0" } }, + "node_modules/vue": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz", + "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-sfc": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/server-renderer": "3.5.44", + "@vue/shared": "3.5.44" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.14.tgz", + "integrity": "sha512-Pq3+OnbCiPCHk0R7KSZrx+j/XXaUn2kSQ1epxq60w8CPbByLjC1EJgV51n4WY851sTXUVzsNMuitywdkdVyKjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-i18n": { + "version": "11.4.15", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.15.tgz", + "integrity": "sha512-bOoNYoEd/ta4g2EelI9HKxPyMZvTDFoykTNYujDTOGoKDRy5ehKJg8WmMDU7hWqNM5efO7fEGNRuwrBXnwHyDg==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.15", + "@intlify/devtools-types": "11.4.15", + "@intlify/shared": "11.4.15", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 22" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -3128,6 +3796,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", diff --git a/package.json b/package.json index f0b329a90..7dda05144 100644 --- a/package.json +++ b/package.json @@ -66,9 +66,16 @@ "mapillary-js": "^4.1.2", "mgrs": "^2.1.0", "pbf": "^5.1.2", - "satellite.js": "^6.0.2" + "pinia": "^2.3.1", + "satellite.js": "^6.0.2", + "vue": "^3.5.44", + "vue-i18n": "^11.4.15" }, "devDependencies": { + "@happy-dom/global-registrator": "^20.14.6", + "@vitejs/plugin-vue": "^5.2.4", + "@vue/test-utils": "^2.5.1", + "happy-dom": "^20.14.6", "prettier": "3.9.6", "puppeteer": "^25.16.0", "sharp": "^0.35.4", @@ -213,8 +220,8 @@ "./voice/actions": "./src/voice/gevActions.js", "./annotations": "./src/annotations/index.js", "./context": "./src/data/contextStore.js", - "./build/html": { - "node": "./build/application-html.js" + "./build/register-vue": { + "node": "./build/registerVue.js" }, "./application/catalog": "./src/app/catalog.js", "./standalone/catalog": "./src/standalone/catalog.js", diff --git a/scripts/check-package-boundaries.mjs b/scripts/check-package-boundaries.mjs index 28627e4ac..700dc9a1a 100644 --- a/scripts/check-package-boundaries.mjs +++ b/scripts/check-package-boundaries.mjs @@ -1,6 +1,8 @@ import { readFile, realpath } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import vue from '@vitejs/plugin-vue'; +import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js'; import { build, normalizePath } from 'vite'; /** Build each browser or Node export group and reject imports outside its declared ownership. */ @@ -99,15 +101,20 @@ export async function checkPackageBoundaries(root) { ssr: node ? { noExternal: true } : undefined, logLevel: 'silent', plugins: [ + vue(VUE_PLUGIN_OPTIONS), { name: 'check-package-ownership', moduleParsed(info) { - if (!allowed.has(info.id)) { + // Plugin helpers are virtual; a single-file component's style and + // template blocks belong to the component file itself. + if (info.id.startsWith('\0')) return; + const id = info.id.split('?')[0]; + if (!allowed.has(id)) { throw new Error( - `Package boundary ${name} imports an unowned module: ${path.relative(root, info.id)}`, + `Package boundary ${name} imports an unowned module: ${path.relative(root, id)}`, ); } - seen.add(info.id); + seen.add(id); }, }, ], diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 228bf6a98..c6ae4b493 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -180,6 +180,8 @@ "src/ui/panelDisclosure.js", "src/ui/imagerySplit.js", "src/ui/imagerySplit.test.mjs", + "src/ui/imagerySplitView.test.mjs", + "src/ui/stores/imagerySplit.js", "src/ui/surfaceKeyboard.js", "src/ui/surfaceKeyboard.test.mjs", "src/ui/panelRails.js", @@ -197,9 +199,7 @@ "scripts/qa-scope-hud.mjs", "scripts/qa-segmented-controls.mjs", "src/ui/displayControls.js", - "src/ui/segmentedControl.test.mjs", "src/ui/styles/segmented-control.css", - "src/ui/displayControls.test.mjs", "src/ui/cameraOrientationControls.js", "src/ui/cameraOrientationControls.test.mjs", "src/ui/effects.js", @@ -297,6 +297,8 @@ "src/ui/styles.js", "src/ui/shellLifecycle.test.mjs", "src/testSupport/readStylesheet.mjs", + "src/testSupport/voiceUi.mjs", + "src/testSupport/voiceView.mjs", "src/testSupport/streetLevelFakes.mjs", "src/splitFlap.js", "style.css", @@ -526,6 +528,7 @@ "scripts/qa-traffic-navigation.mjs", "src/ui/frameRateMonitor.js", "src/ui/frameRateMonitor.test.mjs", + "src/ui/frameRateView.test.mjs", "src/layers/cctv/lifecycle.js", "src/layers/cctv/controls.js", "src/layers/cctv/geometry.js", @@ -561,6 +564,7 @@ "src/data/cctvCataloniaSource.test.mjs", "src/data/cctvHk.test.mjs", "src/data/cctvVegvesen.test.mjs", + "src/data/cctvA22.test.mjs", "src/data/cctvNyc.test.mjs", "src/data/cctvNswSource.test.mjs", "src/data/cctvTxdotSource.test.mjs", @@ -649,7 +653,6 @@ "src/voice/realtimeBackend.test.mjs", "src/agent/worldPacket.test.mjs", "src/voice/layerSummary.js", - "build/application-html.js", "src/app/sources.js", "src/ui/composition.js", "src/services/application.js", @@ -702,7 +705,6 @@ "src/ui/shareRestoration.js", "src/ui/visualSettings.js", "src/ui/panelChrome.js", - "src/ui/panelChrome.radioOwnership.test.mjs", "src/testSupport/readShellSource.mjs", "src/ui/stateOwners.test.mjs", "src/layers/flights/records.js", @@ -740,6 +742,8 @@ "server/providers/openai/toolDescriptions.js", "src/voice/session.js", "src/voice/sessionCommands.js", + "src/ui/stores/pointerReticle.js", + "src/voice/pointerReticleView.test.mjs", "src/voice/realtimeSession.js", "src/voice/realtimeEvents.js", "src/voice/session.test.mjs", @@ -897,6 +901,7 @@ "scripts/qa-weather-swap.mjs", "scripts/qa-weather-teardown.mjs", "src/layers/weather/imageryHost.test.mjs", + "src/layers/weather/display.test.mjs", "src/sdr/adsbDecoder.test.mjs", "src/sdr/controller.test.mjs", "src/sdr/decoderDiagnostics.test.mjs", @@ -909,6 +914,8 @@ "src/sources/adsbRecords.test.mjs", "src/layers/localAdsb/index.test.mjs", "src/ui/localSdrControls.test.mjs", + "src/ui/sdrCardActions.test.mjs", + "src/ui/radioPanelActions.test.mjs", "src/ui/localSdrMarkup.test.mjs", "src/ui/localSdrPresentation.test.mjs", "src/data/localReceiversProxy.test.mjs", @@ -1094,7 +1101,6 @@ "server/providers/overpass/requestAdmission.test.mjs", "src/tooling/trackArrivalObservation.test.mjs", "src/ui/styles/status-badge.css", - "src/ui/statusBadge.test.mjs", "src/ui/cctvBadges.test.mjs", "scripts/qa-status-badges.mjs", "src/dockerTrustedPeers.test.mjs", @@ -1104,12 +1110,13 @@ "src/hudSummaryGate.test.mjs", "src/voice/hudPlaceLabels.test.mjs", "src/voice/panel/preferences.test.mjs", + "src/voice/panel/preferencesView.test.mjs", + "src/voice/voiceControlView.test.mjs", "src/voice/panel/host.test.mjs", "src/voice/panel/manualPanel.test.mjs", "src/voice/panel/nativeActivity.test.mjs", "src/voice/panel/inactivity.test.mjs", "src/voice/panel/actionObserver.test.mjs", - "src/voice/panel/testDom.mjs", "src/voice/realtimeCostPanel.test.mjs", "docs/SHARE-LINK-FORMAT.md", "src/share/wire.test.mjs", @@ -1118,5 +1125,60 @@ "src/testSupport/shareViewFixtures.mjs", "src/ui/shareLinks.test.mjs", "src/ui/shareButton.test.mjs", - "src/share/shareLinkPath.test.mjs" + "src/share/shareLinkPath.test.mjs", + "src/ui/kit/UiRail.test.mjs", + "src/ui/kit/UiSegmented.vue", + "src/ui/kit/UiSegmented.test.mjs", + "src/ui/kit/UiSlider.js", + "src/ui/kit/UiSlider.test.mjs", + "src/ui/kit/UiButton.vue", + "src/ui/kit/UiButton.test.mjs", + "src/ui/kit/UiChip.vue", + "src/ui/kit/UiChip.test.mjs", + "src/ui/globeActions.test.mjs", + "src/ui/safeFrameView.test.mjs", + "src/ui/kit/UiSelect.js", + "src/ui/kit/UiSelect.test.mjs", + "src/ui/kit/UiBadge.vue", + "src/ui/kit/UiBadge.test.mjs", + "src/ui/kit/UiEmpty.vue", + "src/ui/rail/RailChips.vue", + "src/ui/displayPanelActions.test.mjs", + "src/testSupport/vueDom.mjs", + "build/vueLoader.js", + "build/registerVue.js", + "src/ui/panelNavigation.test.mjs", + "src/ui/stores/owners.js", + "src/ui/stores/owners.test.mjs", + "src/ui/stores/panels.test.mjs", + "src/ui/stores/status.js", + "src/ui/statusChipsView.test.mjs", + "src/ui/statusChipOwners.test.mjs", + "src/ui/toast.js", + "src/ui/toastView.test.mjs", + "src/ui/stores/sceneInteractions.js", + "src/scenes/dataPacks/presentationView.test.mjs", + "src/ui/stores/dataPacks.js", + "src/scenes/interactionsView.test.mjs", + "src/layers/wind/inspectionMarker.js", + "src/layers/wind/inspectionMarker.test.mjs", + "src/ui/stores/windInspection.js", + "src/ui/stores/missionReplay.js", + "src/layers/launches/replayOverlay.test.mjs", + "src/ui/stores/bhoteKoshiPanel.js", + "src/data/bhoteKoshiPanelView.test.mjs", + "src/ui/stores/celestialRing.js", + "src/celestialRingView.test.mjs", + "src/ui/stores/screenAnnotations.js", + "src/ui/stores/worldOverlayActions.js", + "src/overlays/worldOverlayActionsView.test.mjs", + "src/ui/stores/awarenessDirection.js", + "src/layers/awareness/directionOverlay.test.mjs", + "src/hudReadouts.js", + "src/ui/stores/hud.js", + "src/ui/views/intelHudView.test.mjs", + "src/testSupport/hudReadouts.mjs", + "src/ui/stores/firstRun.js", + "src/firstRunLauncherView.test.mjs", + "src/firstRunCopy.js" ] diff --git a/scripts/format.mjs b/scripts/format.mjs index 25311ba29..e91cf527b 100644 --- a/scripts/format.mjs +++ b/scripts/format.mjs @@ -39,7 +39,7 @@ export async function discoverRuntimeFormatFiles(root) { const files = []; for (const name of new Set(candidates)) { if ( - !/\.[cm]?js$/.test(name) || + !/\.([cm]?js|vue)$/.test(name) || /\.test\.[cm]?js$/.test(name) || !existsSync(path.join(root, name)) ) diff --git a/scripts/i18n.mjs b/scripts/i18n.mjs index c035c6bb2..c424666a2 100644 --- a/scripts/i18n.mjs +++ b/scripts/i18n.mjs @@ -8,7 +8,7 @@ // lists it. // // Messages are the English literals passed to t(), tn() and msg() in src/, -// plus the markup opted in with data-i18n* in src/ui/templates and index.html. +// plus the markup opted in with data-i18n* in index.html and the Vue views. // It also lists UI text that is not marked at all (see findUnmarked). import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; @@ -119,10 +119,10 @@ export function extractMessages() { } } - const markup = trackedFiles('index.html', 'src/ui/templates/*.html'); + const markup = markupFiles(); const TAG = /<([a-zA-Z][\w-]*)(\s[^>]*?)\/?>([^<]*)/gs; for (const file of markup) { - const html = readFileSync(path.join(ROOT, file), 'utf8'); + const html = readMarkup(file); for (const [, tag, attrs, text] of html.matchAll(TAG)) { const where = `${file} <${tag}>`; if (/\sdata-i18n(\s|=|$)/.test(attrs)) { @@ -156,7 +156,7 @@ const SKIP_TAGS = new Set(['script', 'style', 'svg', 'code', 'pre', 'title']); /** * User-visible markup text without data-i18n*. Skipped, with everything * inside: translate="no" (text that stays as is, as in HTML), - * data-i18n-runtime (code sets this text through t()), and icons + * data-i18n-runtime (page markup whose text code sets through t()), and icons * (aria-hidden, Material Symbols). */ export function unmarkedMarkup(file, html) { @@ -404,9 +404,12 @@ export function unmarkedScript(file, original) { */ export function findUnmarked() { const found = []; - for (const file of trackedFiles('index.html', 'src/ui/templates/*.html')) + for (const file of markupFiles()) { + found.push(...unmarkedMarkup(file, readMarkup(file))); + } + for (const file of trackedFiles('src/**/*.vue')) found.push( - ...unmarkedMarkup(file, readFileSync(path.join(ROOT, file), 'utf8')), + ...markedTemplate(file, readFileSync(path.join(ROOT, file), 'utf8')), ); for (const file of sourceFiles().filter( (name) => !NOT_UI.some((pattern) => pattern.test(name)), @@ -423,6 +426,57 @@ export function findUnmarked() { }); } +/** + * data-i18n* in a Vue template: views translate with t() as they render, so + * their text follows the locale; the attributes are for static page markup. + * Reads the whole single-file component, so lines are the file's own. + */ +export function markedTemplate(file, source) { + const start = source.indexOf(''); + // Comments and attribute values are blanked, keeping every offset. + const blank = (text) => text.replace(/[^\n]/g, ' '); + const template = source + .slice(0, end === -1 ? undefined : end) + .replace(//g, blank); + const found = []; + const TAG = /<[a-zA-Z][\w-]*((?:"[^"]*"|'[^']*'|[^>"'])*)>/g; + TAG.lastIndex = start + '