@zephytiju/prism-timeline-board
v0.1.0
Published
Platform Prism timeline-board micro-UI (component id "timeline-board"): GeoVision v7 timeline-fluxboard — a vertical spine with time ticks at event instants, alternating left/right event cards with kind-colored borders and connector lines on a dot-grid ca
Downloads
133
Readme
PrismTimelineBoardMicroUI
Platform Prism timeline-board micro-UI. Component id: timeline-board.
Published to npm as @zephytiju/prism-timeline-board.
The board renders a subject's event history as the CONFIRMED GeoVision v7 timeline-fluxboard — a vertical
board (dot-grid canvas) with a central spine carrying a tick dot at each event's instant, event cards
alternating left/right of the spine with kind-colored border-lefts, mono "time · title" lines and
"entity · kind" sub-lines, thin connector lines from the spine to every card, and a pulsing LIVE
indicator at the spine's head when live is enabled. The viewport carries no custom chrome: panning and
zooming use the default React Flow interactions, and React Flow's own attribution stays in its default
small bottom-right corner. The whole board is deterministic: every card's y comes from its event's instant
inside the composition-configured window (defaultWindow ISO instants — never a hardcoded band), and
sides alternate in chronological order. It is a pure consumer: the board subscribes to the global
search-results.selected-entity Prism channel (an EntityReference), and on every selection change fetches
the bounded event projection through the generated ITimelineEvent client bound to the host Lattice
transport. Loading skeleton, empty (no selection / no events in window) and error + retry states are all
first-class. Composed applications (for example Guanlan) consume it as-is; the component is platform-owned.
Design notes — timeline-fluxboard
The layout is the section marked timeline · fluxboard vertical in the CONFIRMED design
(nexus-geovision-v7-standalone.html, ~line 626). Element-by-element mapping:
| Design element (class) | Implementation |
| --- | --- |
| .fluxboard.tl-board board shell | read-only ReactFlow canvas (nodesDraggable={false}, pan/zoom active) on the @xyflow/react foundation, @xyflow/react/dist/style.css imported by the component |
| .fb-canvas dot grid | <Background variant={BackgroundVariant.Dots} gap={15} size={1.4}> colored through the line token at half presence |
| .tl-spine central vertical spine | a tall non-interactive custom node centered at flow x=0, mint gradient via tokens; tick dots are placed on it at each event's instant |
| .tl-tick time ticks | rendered on the spine node at top = liveInset + fraction × span, where fraction is the event instant's position inside the configured window (deep-filled dot with mint ring, exactly the design recipe) |
| .tl-live LIVE indicator | badge at the spine's head with a pulsing dot (token-only @keyframes), shown when config.live (default) |
| .fb-card.tl-card event cards | custom nodes alternating left (flow x < 0) / right (x > 0) of the spine in chronological order; kind-colored 2px border-left, mono "HH:mm · SUMMARY" line in the kind token color, "ENTITY · KIND" sub-line in muted; the selected card gets the mint .fb-card.sel ring |
| .tl-conn connector lines | straight, 1.5px, token-colored edges from invisible anchor nodes pinned ON the spine at each event's y to the facing side of its card — no arrowheads |
| viewport operations | NO custom chrome — the DEFAULT React Flow pan/zoom interactions (the component imports @xyflow/react/dist/style.css, which also positions nodes and keeps the attribution small, bottom-right) |
| ed-title header | title + subtitle (locale-bundled defaults, composition-overridable) on the left; subject pill, event count and the "−06H LIVE"-style window badge (derived from defaultWindow span + live) on the right — all from component configuration |
The deterministic layout is exported as buildTimelineGraph(events, window, subject, options) together with
TIMELINE_SPINE_GEOMETRY and fractionInWindow, so hosts (and tests) can reason about exact positions. No
minimap — the prototype's .tl-board has none.
Configuration keys
| Prop | Meaning |
| --- | --- |
| locale | UI locale for the component-fixed strings: "en" \| "zh-CN" (default "en") — see i18n |
| title | Panel header title — composition override; defaults to the locale bundle's title (TIMELINE / 时间线) |
| subtitle | Panel header mono subtitle — composition override; defaults to the locale bundle's subtitle (SUBJECT EVENT SEQUENCE / 实体事件序列) |
| defaultWindow | { start, end } — the timeline window as ISO-8601 instants (required; composition-authored, forwarded verbatim with every fetch and used to place the spine ticks and cards) |
| kinds | Optional interface-declared event-kind restriction forwarded with every request (TimelineEventTimelineRequest.kinds) |
| live | LIVE mode (default true): shows the pulsing LIVE indicator at the spine's head and the "… LIVE" suffix on the header window badge |
Channel contract
| Direction | Kind | Id | Payload |
| --- | --- | --- | --- |
| consumes | state | search-results.selected-entity | EntityReference \| null — selection change triggers a refetch; null returns the board to the empty state |
The board publishes nothing: no channels, no Prism events, no audit records. Busy/error are local component state, so the board never re-renders anyone else and no host contract is implied beyond the consumed selection. The one channel id is a string literal at its single call-site so the build-time channel-graph scanner can derive the edge.
Lattice binding
createTimelineEventClient(useLatticeTransport()) from @zephytiju/lattice-common-interfaces — every fetch goes
through the per-method route interfaces/ITimelineEvent/timeline with full server-side validation. No URLs,
credentials, or generic invokes. The request is exactly { subjects: [selected], window: defaultWindow, kinds? }
(kinds omitted when not configured); stale responses are discarded via a fetch sequence guard so a rapid
re-selection never renders the wrong subject's board.
Audit rule
Timeline viewing is NOT audit-worthy. This component emits NO audit event and no Prism event of any kind.
Internationalization (i18n)
The component ships en and zh-CN locale bundles — src/locales/en.json / src/locales/zh-CN.json —
and every component-fixed UI string is resolved from them (the header title/subtitle defaults, the
empty-selection notice, the loading label, the error title, RETRY, the no-events notice, the EVENTS count
suffix and the LIVE label). The component renders no hardcoded copy.
{
"timeline-board": {
"title": "TIMELINE",
"subtitle": "SUBJECT EVENT SEQUENCE",
"emptySelection": "no subject selected — awaiting search-results selection",
"loading": "loading timeline…",
"errorTitle": "TIMELINE ERROR",
"retry": "RETRY",
"noEvents": "no events in window",
"eventsLabel": "EVENTS",
"liveLabel": "LIVE"
}
}locale?: "en" | "zh-CN"prop (default"en") selects the string table per instance — it is a per-instance prop, so two instances in one host may render differing languages.- Composition-authored strings override the locale defaults.
title/subtitledefault to the locale bundle's strings (so an un-configured header follows the UI language); a host with its own localized header passes its own strings per locale. - Locale bundles are namespaced under the component id (
"timeline-board") so a composer can deep-merge every component's bundle into ONE UI language bundle without collisions:
import { locales as timelineBoardLocales } from "@zephytiju/prism-timeline-board";
// timelineBoardLocales.en -> { "timeline-board": { … } }
// timelineBoardLocales["zh-CN"] -> { "timeline-board": { … } }
const uiBundle = deepMerge(hostStrings, timelineBoardLocales.en, queryBoxLocales.en);The parsed bundles are exported from the package entry (locales, en, zhCN, stringsForLocale), and the
raw JSONs are also served by the ./locales/* exports subpath
(e.g. @zephytiju/prism-timeline-board/locales/zh-CN.json); files ships both dist and locales.
Theme
No palette is hardcoded. Every color resolves to SEMANTIC theme tokens (ok, accent, threat, warn,
signal, card, card-dark, input, border, line, text, muted, deep, panel) consumed as CSS
variables (translucent variants via color-mix on the same tokens — never raw hex or rgba), plus
--mantine-font-family-monospace for the mono typography — the palette is supplied entirely by the host's
MantineProvider. Event-kind border-lefts map onto those tokens through a curated kind map
(movement/transit → accent, observation/sensor → ok, logistics/alert → warn, engagement/strike →
threat, meeting/comms → signal; exported helper kindTokenFor), and unknown kinds fall back to a stable
position on the same token cycle — never a raw color. The local demo ships TWO themes, both defined in
src/demo.tsx: geovisionTheme (dark), mapping each semantic token onto the exact :root variables of the
CONFIRMED GeoVision v7 design, and the contrasting latticeLightTheme (light), mapping the SAME semantic token
keys onto a different palette — the component is skinned purely through the surrounding MantineProvider.
Dependencies
@xyflow/react ^12 is a runtime dependency of the package (the fluxboard foundation; its stylesheet is
imported by the component, so bundler hosts need no extra imports). React, react-dom, @mantine/core,
@zephytiju/prism-react and @zephytiju/lattice-common-interfaces stay peer dependencies supplied by the
host.
Source layout
src/ is strictly two parts:
- Component source (what the package compiles):
TimelineBoard.tsx(the board orchestrator; it imports@xyflow/react/dist/style.cssso nodes are positioned and the default attribution renders bottom-right),chrome/BoardHeader.tsx(the panel header),graph.ts(the deterministic graph builder),kindTokens.ts(kind → semantic token mapping),nodes/(the spine / anchor / card node views and the node-type registry),states/BoardStates.tsx(the skeleton / empty / no-events / error states),time.ts,tokens.ts,useTimelineFetch.ts(the sequence-guarded fetch engine),index.ts(public entry),src/locales/(en.json,zh-CN.json,index.ts— the i18n string bundles and their resolver),src/globals.d.ts(CSS side-effect import declaration) andsrc/shims/node-crypto.ts(browser-build shim for the digest import — build infrastructure). There is notypes.ts: the board publishes no channel payloads, so it declares no wire types of its own — it consumesEntityReference/TimeWindow/TimelineEventItemfrom@zephytiju/lattice-common-interfaces. - Demo: exactly ONE file,
src/demo.tsx— the two host themes (GeoVision v7 + Lattice Light), the mock host action executor (createDemoExecutor, answeringinterfaces/ITimelineEvent/timeline) and all demo test data (sample subjects, the seeded −06H window/kind configuration, the sample event set), the SELECT SUBJECT control that plays the search-results publisher cycling sampleEntityReferences onto the global channel viawriteChannel(including a*-FAILsubject to exercise the error path), the channel monitor, and the demo page rendering multipleTimelineBoardinstances side by side behind a global EN | 中文 language switcher (plus per-instance switches).
The npm package ships dist (compiled component + type declarations + locale JSONs) and the top-level
locales/ directory (the raw JSON bundles, served by the ./locales/* exports subpath); no demo code is
published. scripts/copy-locales.mjs copies the JSON bundles into both locations during npm run build.
Local development
npm install
npm run typecheck
npm test
npm run dev
npm run shot-demonpm install pulls the platform peers (@zephytiju/prism-react,
@zephytiju/lattice-common-interfaces) from the npm registry, along with the host-side peer dependencies
(react, react-dom, @mantine/core) and the @xyflow/react foundation. When consuming the published
package, install it directly (npm install @zephytiju/prism-timeline-board) and provide those peer
dependencies in the host application.
The demo (npm run dev, entry src/demo.tsx) renders TWO TimelineBoard instances side by side, each inside
its own scoped MantineProvider with a different theme (GeoVision v7 dark left, Lattice Light right), behind
an EN | 中文 segmented control — the GLOBAL switch sets the locale prop of BOTH instances at once, and each
instance carries its own per-instance control so the two hosts can render DIFFERING locales simultaneously.
The SELECT SUBJECT control publishes the global search-results.selected-entity channel (both boards consume
it and refetch through the mock executor); select NET-9902-FAIL to exercise the error path and RETRY, and
CLEAR SELECTION to return both boards to the empty state. npm run shot-demo boots the vite dev server, drives
both instances in headless Chrome (sets the LEFT instance to en and the RIGHT instance to zh-CN, publishes
the seeded subject selection) and captures the language switcher plus both instances — vertical spines, ticks,
alternating cards with connectors and the locale-default titles — in one shot to
/tmp/guanlan-review/demo-timeline-board.png.
Design record
https://qcnwge0wy4s0.feishu.cn/wiki/K40nwA5TZiUE7Nk5hK6cE0W8nTe — §7 (channel contracts) and §8 (Lattice
transport and interface clients). Visual reference: CONFIRMED GeoVision v7 design
(nexus-geovision-v7-standalone.html, the timeline · fluxboard vertical section).
