@zephytiju/prism-main-editor
v0.1.0
Published
Platform Prism main-editor micro-UI (component id "main-editor"): the dossier editor axiom component (D9) — title bar (breadcrumb + autosave stamp, DRAFT/V07 state chip, amber-active COMMENTS toggle, SHARE, overflow) absorbed from the retired Title Bar co
Readme
PrismMainEditorMicroUI
Platform Prism main-editor micro-UI. Component id: main-editor.
Published to npm as @zephytiju/prism-main-editor.
The dossier editor axiom component (design decision D9): the single editor component owning the
title bar — breadcrumb + autosave stamp, DRAFT / V07 state chip, COMMENTS toggle (active state
amber) driving the in-editor comments pane, SHARE, ••• overflow — absorbed from the retired
Title Bar component, plus the typed block stack with + / drag insert rows and the expandable
block-anchored comments pane. Block surfaces follow the D10 taxonomy: narrative,
analyst-judgment, and dossier-native table blocks render as plain document elements — no outlining
box, no card chrome, no kind pill; the analyst-judgment highlight is an accent bar + tint only;
source links are ordinary inline hyperlinks embedded inside paragraph text (accent-colored,
underlined, preceded by the ↗ glyph) — never standalone link blocks; native tables render with row
rules like Feishu native tables. Only file-preview blocks are embedded, and they HOST the REAL
published micro-UI packages — no synthetic editor-side previews exist:
- the geovision-embed block hosts
@zephytiju/prism-geovision-embed-block(>= 0.2.0), which renders the full D10 chrome (kind tagGEOVISION,SRC FILE · {file}chip,VIEW ▾variant dropdown fed by the render package'sdescribeViews(sourceFileRef)through the host Lattice transport, ↗ open arrow) and itself hosts the published render dispatcher (@zephytiju/prism-geovision-embed0.2.0) in its body slot, rendering exactly the ONE variant the current viewId (${fileRef}#${previewKind}) selects; - the evidence-table block hosts
@zephytiju/prism-evidence-table(>= 0.1.0), which renders its own card chrome and the previewed saved view'sIEvidenceSource-shaped rows (source / observation / confidence / audit locator).
All data arrives IDossierDoc-shaped over the component channels (or explicit props); add, reorder, and remove are dossier ACTIONS the editor emits as events for the host to apply. Composed applications (for example Guanlan) consume it as-is; the component is platform-owned.
Configuration keys
| Prop | Meaning |
| --- | --- |
| locale | UI locale for the component-fixed strings: "en" \| "zh-CN" (default "en") — see i18n |
| variant | "editor" (default; insert + drag controls) or "readonly" (published view — controls hidden) |
| enabledBlockKinds | Block kinds the insert menu offers (default: all five — narrative, assessment, geovision-embed, evidence-table, native-table) |
| showInsertControls | Whether the + insert controls render in the editor variant (default true; drag handles follow variant) |
| titleFormat | (dossier) => string workspace title line (default: the dossier title) |
| titleBarActions | Title-bar actions to enable: { comments?, share?, overflow? } (all default true; the title bar is component configuration — Prism has no composed title node) |
| dossier | Explicit MainEditorDossier override; defaults to the main-editor.dossier channel |
| commentThreads | Explicit threads override; defaults to the main-editor.comment-threads channel |
| height | Editor surface height in px (default 900) |
Channel contract
| Direction | Kind | Id | Payload |
| --- | --- | --- | --- |
| consumes | state | main-editor.dossier | MainEditorDossier \| null — IDossierDoc-shaped (id, title, blocks of DossierDocBlock) widened with breadcrumb, doc code, autosave stamp, draft state, version, doc meta, and per-kind block content |
| consumes | state | main-editor.comment-threads | readonly MainEditorCommentThread[] \| null — ICommentThread-shaped comments widened with blockId anchoring and open/done status |
| consumes | state | main-editor.comments-open | boolean — the pane state channel (D6); null defaults to the prototype's open pane |
| consumes | state | main-editor.focused-block | { blockId } \| null — derived for the focus wash |
| publishes | state | main-editor.comments-open | boolean — the title-bar COMMENTS toggle (setter-only publish; amber-active when open) |
| publishes | state | main-editor.focused-block | { blockId } — clicking a block or a thread card (block review mode) |
| emits | event | main-editor.insert-block-requested | { afterBlockId: string \| null, kind } — the + block-kind menu; a dossier action (IDossierDoc) the host applies |
| emits | event | main-editor.reorder-block-requested | { blockId, direction: "up" \| "down" } — the ⠿ drag menu; a dossier action |
| emits | event | main-editor.remove-block-requested | { blockId } — the ⠿ drag menu; a dossier action |
| emits | event | main-editor.open-view-requested | { sourceFileRef, viewId } — the ↗ open intents of the HOSTED embedded blocks, re-emitted so hosts keep ONE open-intent contract for every embedded block |
| emits | event | main-editor.share-requested | { docId } — the SHARE sharing intent (never grants authorization; the host performs the share and records the IAuditChain entry) |
The variant state of the embedded blocks is owned by the HOSTED components through their own
file-level channels — geovision-embed.selected-view ({ sourceFileRef, viewId }, view ids
${fileRef}#${previewKind}) and evidence-table.selected-view ({ sourceFileRef, viewId }) —
so every embed of the same source file re-previews the picked variant (D10). The editor's former
per-block main-editor.selected-view channel was REMOVED in the real-components migration: it
conflicted with the components' file-level semantics; the editor no longer reimplements embedded
view selection at all.
Channel ids are string literals at every call-site so the build-time channel-graph scanner can
derive the graph. Channel reads use usePrismStateValue; every publication is setter-only (or an
event), so publishers never re-render from their own writes. Block focus follows the EntityCard
pattern — each item owns its binding, publishes on change, and derives its current value from the
same channel, so every host bound to the channel stays in sync (D6 state passing).
Audit rule
Routine dossier reads and interactions are NOT audit-worthy: this component emits NO audit event
for them. The only audit-adjacent emission is the main-editor.share-requested intent — sharing is
the audit-worthy action, and the recording host (not this component) appends the IAuditChain entry.
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 DOSSIER doc prefix, AUTOSAVED {time},
the COMMENTS · {count} toggle and {count} OPEN COMMENTS chip lines, SHARE, the INSERT BLOCK /
BLOCK ACTIONS menus, every block-kind label, the comments pane copy, and the
empty state). The embedded-block chrome copy (kind tags, SRC FILE · {file} chips,
VIEW · {variant} pills, SAVED VIEWS · {file} menus, ↗ arrow titles) lives in the HOSTED
packages' own locale bundles and localizes through the locale prop the editor forwards. The
component renders no hardcoded copy.
{
"main-editor": {
"docPrefix": "DOSSIER",
"autosavedAt": "AUTOSAVED {time}",
"commentsButtonWithCount": "COMMENTS · {count}",
"openCommentsCount": "{count} OPEN COMMENTS",
"share": "SHARE",
"kindAssessment": "ANALYST JUDGMENT",
"openThreads": "{count} OPEN",
"threadBlockMeta": "BLOCK {block} • {status}"
}
}locale?: "en" | "zh-CN"prop (default"en") selects the string table per instance.{placeholder}templates ({count},{time},{file},{variant},{block},{status}) use plain substitution viaformatMessage, exported from the package entry.- Composition-authored strings are localized by the composer; component-fixed strings live in the locale JSONs. Breadcrumb segments, the doc meta line, block content, saved-view labels, and thread titles are data (IDossierDoc/ICommentThread-shaped), so a localized dossier supplies its own translated content per locale.
- Locale bundles are namespaced under the component id (
"main-editor") so a composer can deep-merge every component's bundle into ONE UI language bundle without collisions:
import { locales as mainEditorLocales } from "@zephytiju/prism-main-editor";
// mainEditorLocales["zh-CN"] -> { "main-editor": { … } }
const uiBundle = deepMerge(hostStrings, mainEditorLocales["zh-CN"]);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-main-editor/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, plus --mantine-font-family-monospace for the mono typography — the
palette is supplied entirely by the host's MantineProvider. The prototype's mint/blue/amber/red/
purple accents map onto ok / accent / warn / threat / signal respectively; translucent
surfaces (the amber highlight tint, the comments-pane backdrop, the focus wash) derive from the
same tokens through color-mix. 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 v9
design prototype, and the contrasting latticeLightTheme (light), mapping the SAME semantic token
keys onto a different palette — the component is skinned purely through the surrounding
MantineProvider.
Source layout
src/ is strictly two parts:
- Component source (what the package compiles):
MainEditor.tsx(orchestrator — channel wiring, empty state, layout),TitleBar.tsx(the absorbed title bar, D9),CommentsPane.tsx(the expandable in-editor pane),blocks/(BlockStack.tsx— workspace header, insert rows, +/⠿ menus, per-kind dispatch;NarrativeBlockView.tsx,AssessmentBlockView.tsx,GeovisionEmbedView.tsx— HOSTS the real@zephytiju/prism-geovision-embed-block;EvidenceTableView.tsx— HOSTS the real@zephytiju/prism-evidence-table;NativeTableView.tsx— the D10 block surfaces;InsertMenu.tsx,InsertRail.tsx,DragMenu.tsx,SelectionToolbar.tsx,menuStyles.ts— the D11 affordances),types.ts(the IDossierDoc/ICommentThread-shaped data model + event payloads),tokens.ts(semantic-token shorthands),index.ts(public entry), andsrc/locales/(en.json,zh-CN.json,index.ts— the i18n string bundles, their resolver, and the{placeholder}interpolation helper). The embedded item templates are the hosting seams for the real published packages — the editor renders NO synthetic embed previews. - Demo: exactly ONE file,
src/demo.tsx— the two host themes (GeoVision v9 + Lattice Light), all demo test data (the Redwater mock dossier + threads mirroring the v9 prototype; the demo geovision-view file built through the bundle's ownsaveGeovisionView; the evidence saved views with their rows), the channel seeding (including the two file-level selected-view channels the hosted components own), the mock Lattice action executor serving the demo file (the standards-compliant host pattern — the hosted block'sdescribeViews()and the REAL render dispatcher run end to end against it), the mock dossier-action applier (insert/reorder/remove/ duplicate — the host side of the action contract), the channel monitors + action log, and the demo page rendering TWOMainEditorinstances 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.
Dependencies and build order
The two embedded-block packages are peerDependencies AND devDependencies:
"peerDependencies": {
"@zephytiju/prism-evidence-table": ">=0.1.0",
"@zephytiju/prism-geovision-embed-block": ">=0.2.0",
"@zephytiju/prism-react": ">=0.2.0", …
}While both consumed packages are UNPUBLISHED local builds, the devDependencies resolve them as
file: links into the sibling checkouts — so the required BUILD ORDER is:
npm run buildin../PrismGeovisionEmbedBlockMicroUI(shipsdist+locales),npm run buildin../PrismEvidenceTableMicroUI(shipsdist+locales),- only then
npm install/npm run build/npm testhere — thefile:links and theresolve.dedupeinvite.config.ts/vitest.config.ts(react, react-dom, @mantine/core, @zephytiju/prism-react, @zephytiju/prism-geovision-embed) keep the linked packages sharing ONE prism-react channel store / action executor and ONE React instance with the editor.
The devDependencies additionally install the hosted render stack's registry peers
(@zephytiju/prism-geovision-embed 0.2.0 with its cesium/resium deps, @zephytiju/prism-relation-fluxboard,
@zephytiju/prism-stats-panel, @zephytiju/prism-timeline-board, @xyflow/react,
@zephytiju/lattice-common-bundle) so the demo and tests exercise the REAL dispatcher end to
end. @zephytiju/prism-react is pinned >=0.2.0 because the hosted stack resolves the source
file through the 0.2.0 transport seam (setPrismActionExecutor / useLatticeTransport).
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 — through the file: links — the two
locally built embedded-block packages (see the build order above). When consuming the published
package, install it directly (npm install @zephytiju/prism-main-editor) and provide the peer
dependencies (including the two embedded-block packages once published) in the host application.
The demo (npm run dev, entry src/demo.tsx) plays the host: at module scope it installs the
mock Lattice action executor serving the demo geovision-view file, then seeds the Redwater mock
dossier, its threads, and the two file-level selected-view channels onto the GLOBAL channels, and
renders TWO MainEditor instances, each inside its own MantineProvider with a different theme
(GeoVision v9 dark, Lattice Light). The geovision-embed block row hosts the REAL
@zephytiju/prism-geovision-embed-block: its VIEW ▾ list comes from the REAL describeViews()
call over the demo executor (one saved view per previewKind — ${fileRef}#map / #relational /
#timeline / #stats), and its body renders the REAL published dispatcher — exactly the ONE
variant the current viewId selects (the demo opts the map variant into engine="svg" for
deterministic headless capture). The evidence-table block row hosts the REAL
@zephytiju/prism-evidence-table with the demo saved views' rows. The COMMENTS toggle drives
the pane through the shared main-editor.comments-open channel, so toggling it in EITHER
instance opens the pane in BOTH. The + insert control opens the block-kind menu and emits
insert-block-requested; the ⠿ drag menu emits reorder/remove — the mock host applies these
dossier actions against its projection and republishes, exactly how a real host routes them
through IDossierDoc actions. The hosted blocks' ↗ arrows emit their open intents, which the
editor re-emits as main-editor.open-view-requested. The channel monitors and action log below
the instances show all of this live. npm run shot-demo boots the vite dev server, drives the
demo in headless Chrome, and captures the evidence states to /tmp/guanlan-review/:
demo-main-editor-real-embeds-dark-en-v4.png,
demo-main-editor-real-embeds-variant-switch-v4.png,
demo-main-editor-real-embeds-light-zh-cn-v4.png, and
demo-main-editor-full-page.png.
Design record
https://qcnwge0wy4s0.feishu.cn/wiki/B87YwS2PriaGvlkAcOtctKm8nsd — decisions D1–D10 (esp. D3
runtime blocks, D5 component tiers, D6 state passing, D8 channel contracts, D9 title-bar
absorption, D10 block surface taxonomy). Component doc:
https://qcnwge0wy4s0.feishu.cn/wiki/UqqYwbzkViRiprkNpkRcKt3tnFf. Visual reference: the
authoritative dossier-editor-standalone-v9.html prototype attached to the design doc
(.ed-head / .nblock / .hl / .block / .vsel / .cpane).
