A map, a data table, a charts rail and a detail drawer behave as one dashboard
without importing each other, and nothing said how. This records the arrangement
that exists on development today; every claim in it was checked against the code
rather than remembered.
One composable provides five keys and every panel injects what it needs, with
null and {} defaults that are load-bearing: a panel rendered outside a shell must
degrade rather than throw, because a thrown setup() removes the subtree with an
empty error and no build failure.
The part worth writing down is that the bus carries two different kinds of
traffic through one generic on/emit pair. Four STATE events belong to
useSelectionBus and are emitted only by it. Seven UI events are passed through
the same bus by panels with no composable behind them. Reading the second kind as
if it were the first is how a maintainer concludes a contract is dead.
That mistake is named in the file because it has already been made twice while
auditing this kit: two panels listen for 'select' and no panel emits it, which
looks like a broken wire until you notice the emitter is the composable at
useSelectionBus.js:202. Grepping components for a bus's traffic cannot see the
bus itself.
It also records that selection is deliberately NOT reactive -- selectionSet is a
plain Set and selectionVersion is the reactive handle -- and that the charts rail
participates in filtering only, so selecting a pin does not highlight its slice.
That last one is the current contract, not an oversight: whether selection should
reach the charts is a product question and it is open.
The components stay in plugin-designs. A library component with one consumer is a
relocation rather than a promotion, and this repo has already had to move such
modules back; they graduate when a second dashboard consumes them.
|
||
|---|---|---|
| .. | ||
| README.md | ||
The dashboard kit contract
Four panels — a map, a data table, a charts rail and a detail drawer — behave as one dashboard
without importing each other. This file is the contract they share. It is documentation of what
exists today, verified against development, not a proposal.
The components still live in plugin-designs/research-dashboard/src/components/dashboards/. They
are deliberately not in library/ yet: a library component with a single consumer is a
relocation, not a promotion, and the repo has already had to move such modules back. They graduate
when a second dashboard consumes them.
The shape
One composable provides; every panel injects. Nothing is passed down as props, and no panel imports another.
plugin-designs/research-dashboard/src/composables/useDashboardShell.js provides five keys:
| key | what it is |
|---|---|
selectionBus |
the shared event bus and selection/filter state (library/composables/useSelectionBus.js) |
dashboardData |
the row store the panels read |
dashboardConfig |
per-profile configuration — labels, colours, which column is the photo |
dataStore |
the underlying data source |
searchSourceId |
identifies whose search a result belongs to |
A panel takes what it needs and tolerates absence:
const dash = inject('dashboardData', null)
const bus = inject('selectionBus', null)
const cfg = inject('dashboardConfig', {})
The null defaults are load-bearing. A panel rendered outside a shell must degrade, not throw — a
thrown setup() removes the subtree with an empty error and no build failure, which is expensive to
diagnose.
The bus carries two different things
on(event, cb) and emit(event, payload) are generic: any string is a valid event. That makes
the bus easy to extend and easy to misread, so the distinction matters.
State events — owned by useSelectionBus, emitted only by it, in response to its own mutators:
| event | payload | emitted when |
|---|---|---|
filter |
filterSet |
setFilter, clearFilters, toggleFilterValue |
select |
selectionSet |
toggleSelect, selectMany, deselectMany, setSelection, clearSelection |
search |
the term | setSearch |
hover |
the key, or null |
setHover |
UI events — passed through the same bus by panels, with no composable behind them:
focusRow, mapFocus, toggleTable, toggleRail, toggleFacet, resetLayout, layoutResizing.
★ Do not look for a state event's emitter in the panels. Two panels listen for select and no
panel emits it; the emitter is useSelectionBus.js:202. Grepping components for the bus's traffic
cannot see the bus itself, and reading that absence as a dead contract is a mistake worth naming
here because it has already been made twice while auditing this kit.
Selection does not travel by emit
selectionSet is a plain Set<slug>, deliberately not reactive — a per-row toggleSelect()
loop firing a synchronous emit('select') per row is why. selectionVersion is the reactive handle:
watch that, read the Set.
So a panel reacting to selection watches selectionVersion (or subscribes to select) and then
reads bus.selectionSet. The table writes into it; the map frames its camera off the full row
store rather than the filtered rows, so a record selected and then filtered out of scope still shows.
What each panel does with it today
| panel | reads | writes |
|---|---|---|
| data table | selectionSet, filter |
selectionSet, toggleTable |
| map panel | select, focusRow, toggleTable, layoutResizing |
focusRow, mapFocus |
| detail drawer | select, focusRow |
— |
| charts rail | filterSet via isFilterActive / rowMatchesFilters |
setFilter |
★ The charts rail participates in filtering only. Clicking a slice narrows the map; selecting a
pin does not highlight its slice, because the rail neither emits nor listens for select. That
is the current contract, not an oversight to fix casually — whether selection should reach the
charts is a product decision, and it is open.
Adding a panel
Inject the three keys with null/{} defaults, subscribe in setup, and unsubscribe on unmount —
on() returns its own unsubscribe function. Read state from bus, never from another panel. If a
new cross-panel signal is needed, prefer a state event with a mutator on the composable over a
new ad-hoc UI event: the ad-hoc ones are invisible to anyone reading useSelectionBus.js, which is
where a maintainer looks first.