The widget¶
dist/compass-map.js registers the <compass-map> custom element. Svelte
compiles it as a custom element, so the UI lives in a shadow root and the host
page's CSS does not reach it. The stylesheets in src/styles/ are imported
?inline and injected into that root.
Data¶
The widget ships no ontology: init(apiurl) and every query after it call the
FastAPI backend. Editorial changes reach the map after a regenerate and a
reload, without a frontend rebuild.
| Call | Returns |
|---|---|
GET /api/v1/filters |
Filter dimensions, labels and options per language |
GET /api/v1/entities |
Pins as GeoJSON |
GET /api/v1/entities/facets |
Per-option result counts |
GET /api/v1/stories/count |
Story count and link URL |
The filter panel comes from the API, except for DIM_IDS in
src/lib/schema.ts, which selects and orders the dimensions to draw (see
Frontend configuration).
Host organisation in the counts¶
compass:HostOrganization (OceanCare) is in the backend's ALWAYS_ON_CLASSES:
its pins show whatever the filters say. The facet query leaves them out, and
shownFacets in CompassMap adds hostCount (pins matching isHost in
src/lib/pins.ts) to every count. The result tally (resultCount) counts
what /entities returns, which already includes them.
Layout¶
| Path | Holds |
|---|---|
src/components/ |
CompassMap (the element), Stage + Basemap (the map), Sidebar, FilterAccordion, FilterRows, ActivePills, DetailPane, and smaller parts |
src/lib/ |
Projection and camera, pins, bathymetry, labels, palette, i18n, URL state, mobile sheet |
src/engine/ |
API client and its types |
src/styles/ |
One stylesheet per UI region |
Desktop and mobile¶
One component tree, two layouts, split at 860 px (isMobile in
src/lib/sheet.ts).
- Desktop: filters in a rail beside the map, active filters as removable pills above them. An opened pin replaces the rail with its detail pane.
- Mobile: the rail is a bottom sheet with stops
dock,halfandfull; active filters float as chips over the map. An entry opens athalfand the pin moves to the centre of the visible map; closing returns todock. Picking a filter fromfulldrops the sheet tohalf. A gesture on the sheet is classified as drag or scroll on its first move, so the host page does not scroll under it.
The URL carries the active filters, or ?pin= while an entry is open.
Texts¶
- Interface strings (buttons, captions, errors, screen-reader text):
src/lib/i18n.ts, oneenand onedeobject with the same keys. Placeholders like{n}are filled byfmt; keys ending inOneare the singular. Needs a rebuild. - Data texts (dimension names, option labels, pin names, descriptions,
locations): the
_en/_decolumns of the use-case'ssource-data.ods. Edit, runjust data::generate, reload the API. An empty_decell falls back to English.
Design¶
The UI follows the OceanCare website's design system: Cabin, five colours, square corners except the donate CTA. Map-specific parts the system does not cover (chrome, switches, chips, dense type under 18 px) are extensions, with the reason in a comment where each is defined. Two rules:
- A cerulean fill marks a choice (a ticked filter row, a group header's selection count); a bare cerulean numeral is a quantity (per-option result counts).
- Text actions in the sidebar are links, not filled buttons.
Commands¶
just frontend # dev server (runs the standalone check first)
just dev-up # API and widget together
just check::frontend-standalone # svelte-check and the no-third-party-hosts gate
just lint # ESLint + Prettier, and ruff for the Python side
just test # vitest, plus the backend and generator suites
npm run build in src/frontend writes the bundle. The no-third-party-hosts
gate also runs in the CI Widget job.