Skip to content

Restyle the theme rail and source its icons from config - #323

Open
sandesh-sp wants to merge 5 commits into
developmentfrom
fix/layer-filter-theme-styling
Open

Restyle the theme rail and source its icons from config#323
sandesh-sp wants to merge 5 commits into
developmentfrom
fix/layer-filter-theme-styling

Conversation

@sandesh-sp

Copy link
Copy Markdown
Collaborator

Styling

The rail painted itself from the primary ramp (--theme-color-primary-dark). It now uses the theme's darkest surface, --theme-color-ink — which matches the Disasters Portal design (Figma node 9564:23148 is a 66×940 rect filled --color/hds/carbon/90, #17171b, horizon's ink).

Categories and the collapse chevron sit in --theme-color-base-lighter (#e3e3e3) and lift to white on hover, so pointing at a category previews the selected treatment. The selected category keeps the inversion: white fill, ink text and icon.

Two values stayed literals rather than tokens: the label's 10px/1.6. Horizon's smallest type-scale step resolves to 12px and line-height-2 to 1.2, so tokenizing them would have changed the rendering on a 66px-wide rail. Noted in a comment.

Icons come from config, not the bundle

The three theme SVGs are gone from the plugin. Each category names its own icon in the mission config from one of three sources, chosen per row:

Source Field Notes
upload iconUpload File uploaded through the Configure page
link iconUrl Hosted elsewhere
mdi iconMdi Material Design Icons name

variables.themes becomes an objectarray, so each row carries its own upload field with a thumbnail. This reuses the existing type: "upload" component and POST /api/upload — the same path the Card tool uses — so there is no new component type and no backend change. Files land in Missions/<mission>/LayerFilterThemes/uploads/, and the stored mission-relative path resolves against app:getMissionPath at render time.

Configs written against the earlier flat icon string still resolve — path-like values as images, bare words as MDI names — so existing missions keep rendering while they migrate.

Uploaded images are drawn as CSS masks

Not <img>, and not inlined. This buys two things at once:

  1. The icon takes its color from the item, so one uploaded file covers both rail states rather than needing a light and a dark variant.
  2. An uploaded SVG is never parsed as a document, so script embedded in one cannot run. This matters: the upload path does not sanitize SVG, and the safety argument on record (API/Backend/Upload/validate.js:8) holds only for <img src>.

The tradeoff is that icons render as single-color silhouettes — fine for glyph-style icons, wrong for a multi-color logo.

Matches the existing repo idiom (AOIComponent.scss, layer-legend.scss).

Collapse chevron

A chevron above the categories opens and closes the neighbouring panel, rotating a half-turn between states (dropped under prefers-reduced-motion). It targets the panel named by the new togglePanelId var, or the sole other collapsible panel in the rail's region, and hides when neither resolves.

It drives the panel through panels:getAll / panels:toggleCollapsed, added in #322. This branch does not depend on that one to build or pass — the wrappers return null when the handlers are absent, so the chevron simply hides itself until #322 lands.

Testing

tests/unit/layerFilterThemesRail.spec.js extended to cover all three icon sources, the fallback when a chosen source is left empty, the warning when nothing falls back, and legacy flat-icon configs.

Full unit suite at this branch tip: 79 files, 1306 tests passing, tsc --noEmit clean, production build compiles.

Not verified

None of this has been checked in a running browser — no live confirmation of the rail's appearance, the chevron collapsing a panel, or an uploaded icon rendering at size. Worth a smoke test on the DisastersTool mission, which is the one configured with this plugin.

Paint the rail from the theme's darkest surface (--theme-color-ink) rather
than the primary ramp, and set the categories and the collapse chevron in
--theme-color-base-lighter, lifting to white on hover so pointing at a
category previews the selected treatment. The selected category keeps the
inversion: white fill, ink text and icon.

Theme icons are no longer bundled with the plugin. Each category names its
own icon in the mission config, from one of three sources: a file uploaded
through the Configure page, a link to one hosted elsewhere, or a Material
Design Icons name. The themes editor becomes an objectarray so each row
carries its own upload field, and configs written against the earlier flat
`icon` string still resolve.

Uploaded and linked images are painted as CSS masks filled with the item's
own color. One file therefore covers both rail states, and an uploaded SVG
is never parsed as a document, so script embedded in one cannot run -- the
upload path does not sanitize SVG.

A chevron above the categories opens and closes the neighbouring panel,
rotating a half-turn between the two states. It drives the panel named by
`togglePanelId`, or the sole other collapsible panel in the rail's region,
and stays hidden when neither resolves or when core registers no panel
handlers.
sandesh-sp added a commit that referenced this pull request Aug 27, 2026
## Why

Panels and plugins can only be driven from inside core. `PanelManager_`
and `ToolControllerModern_` are reachable by direct import, which a
sandboxed plugin cannot do, so a plugin that wants to collapse the panel
it lives in — or reveal a sibling plugin — has no way to read the state
or act on it.

The state owners were also awkward to drive even from inside core. They
signalled refusal by throwing, which does not survive a bus boundary,
and panels offered only `togglePanelCollapsed`: a toggle acts on
whatever it finds rather than on what the caller asked for, so a control
working from a listing it read a moment ago flips the wrong way, and a
retry undoes itself.

## What

**Results instead of exceptions.** `setPanelState` returns `{ ok: true,
state, changed }` or `{ ok: false, reason }`. `canSetState` exposes the
same constraints, so a control can decide whether to draw itself at all
rather than discovering the answer by being clicked. `showPanel`
replaces the toggle and names the state it resolves to.

**Plugin lifecycle as states.** `ToolControllerModern_`'s separate
loaded and hidden flags become one of `unloaded`, `hidden`, `visible`,
reachable through `setPluginState`. Asking for `visible` on an unloaded
plugin loads it.

**Eight handlers on the bus** plugins already use for everything else:

| | |
|---|---|
| `panels:getAll` / `plugins:getAll` | the current listing |
| `panels:setState` / `plugins:setState` | the primitive |
| `panels:show` / `panels:hide` | sugar, resolving a target state |
| `plugins:show` / `plugins:hide` | same |

Handlers register at module load and stay registered. An absent layout
is reported as a `layout-inactive` **result**, not a missing handler, so
a caller sees one failure vocabulary whichever layer refused. The
managers are read through getters rather than captured at registration,
since the UI clears them on teardown.

**Two events.** `panels:changed` and `plugins:changed` carry the same
listing the `getAll` handlers return, so a subscriber never has to
re-request. `panels:changed` replaces `mmgis-panel-layout-changed`.
Payloads are frozen projections holding no core references, so they
survive a structured clone — which is what a future sandbox bridge will
have to do to them.

Matching typed wrappers land in
`src/essence/Tools/_shared/adapters/mmgisAPI.ts`, the one place the bus
name strings appear.

## Scope

Mostly additive, with two breaking changes worth knowing about before
merge:

- `PanelManager_.togglePanelCollapsed` is removed, from both the class
and the interface. A toggle acts on whatever it finds, which is the
problem this PR exists to fix; `showPanel` and `setPanelState` name the
state they resolve to instead.
- `mmgis-panel-layout-changed` is renamed to `panels:changed`.

Everything else is additive. The existing `mmgisAPI.showPanel` /
`hidePanel` / `togglePanel` methods and the `core:*` command events all
still work — they are reimplemented on the new primitives here and
removed in #325.

## Defects fixed along the way

Two, both in the state owners this PR reworks:

- `panels:show` on an already-visible panel resolved a target state and
so could *shrink* it. Every stock layout gives the left panel an
`iconified` default, so showing an expanded panel iconified it. Show now
only ever lifts a panel out of `collapsed`.
- A failed plugin transition reported `not-found`, pointing a reader at
the plugin id — the one thing already known to be good, since the id was
resolved before the transition was attempted. It reports
`transition-failed`.

## Testing

Provider specs for all eight handlers (refusal vocabulary, malformed
payloads, absent layout, the float-position restriction),
`PanelManager_` command specs, plugin state-transition specs, and the
typed wrappers.

**1458 tests passing, `tsc --noEmit` clean.** Every commit in the stack
was verified green in isolation, not just the tip.

## Related

Second of a three-part stack: #324 → **this** → #325.

Note for #323: this replaces the API that branch was written against,
and the break lands when **this** PR merges — not #325.
`panels:toggleCollapsed` is not registered (toggle was dropped
deliberately, see above), `collapsible` is not on the listing, and the
event is now `panels:changed`. #323 needs a follow-up to move onto
`panels:show` / `panels:hide` driven off the `state` it already reads.
The rail asked for 'panels:toggleCollapsed' and followed a
'mmgis-panel-layout-changed' event, neither of which core provides: the request
resolved to nothing and the listing never refreshed, so the chevron sat inert.
It now reads the layout through panels:getAll, follows panels:changed, and names
the direction it wants — panels:show on a collapsed panel, panels:hide
otherwise — so the command matches the state the rail is drawing and a refusal
is reported with the reason core gives.

The client's older panel wrappers went with it. They were the last callers of a
handler nothing registers, and their getter collided with the one the bus
migration introduced, which left the module unparseable and took twelve test
files down with it.

Two ids identify a tool and the rail was using the wrong one: a panel lists the
`js` id a mission config declares, while tool:getVars is keyed by the lowercased
name. Matching on the latter meant the rail never found the panel it lives in,
so the chevron only ever appeared for a mission naming a panel outright.

The listing arrives from two places, and the request can resolve after a
broadcast has landed. The seed yields to anything the subscription has already
delivered, so a layout that moves during boot is not overwritten with the
listing that predates it.
The three categories stood in with Material Design glyphs. The drawn set ships
under public/images/layerfilterthemes/ and the demo profile points each theme at
one.

Only the dark variant is needed. The rail paints an image icon as a mask filled
with the item's own color, so one file covers both the dark rail and the white
fill of the selected entry.

The artwork is mission configuration, not plugin knowledge, so it rides in the
profile's overrides. The manifest's own defaults stay on icon names any mission
can render.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant