@zephytiju/prism-main-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -0
- package/dist/CommentsPane.d.ts +32 -0
- package/dist/CommentsPane.d.ts.map +1 -0
- package/dist/CommentsPane.js +85 -0
- package/dist/MainEditor.d.ts +59 -0
- package/dist/MainEditor.d.ts.map +1 -0
- package/dist/MainEditor.js +102 -0
- package/dist/TitleBar.d.ts +28 -0
- package/dist/TitleBar.d.ts.map +1 -0
- package/dist/TitleBar.js +55 -0
- package/dist/blocks/AssessmentBlockView.d.ts +21 -0
- package/dist/blocks/AssessmentBlockView.d.ts.map +1 -0
- package/dist/blocks/AssessmentBlockView.js +34 -0
- package/dist/blocks/BlockRow.d.ts +33 -0
- package/dist/blocks/BlockRow.d.ts.map +1 -0
- package/dist/blocks/BlockRow.js +56 -0
- package/dist/blocks/BlockStack.d.ts +27 -0
- package/dist/blocks/BlockStack.d.ts.map +1 -0
- package/dist/blocks/BlockStack.js +75 -0
- package/dist/blocks/DragMenu.d.ts +22 -0
- package/dist/blocks/DragMenu.d.ts.map +1 -0
- package/dist/blocks/DragMenu.js +219 -0
- package/dist/blocks/EmbeddedBlockChrome.d.ts +34 -0
- package/dist/blocks/EmbeddedBlockChrome.d.ts.map +1 -0
- package/dist/blocks/EmbeddedBlockChrome.js +81 -0
- package/dist/blocks/EvidenceTableView.d.ts +25 -0
- package/dist/blocks/EvidenceTableView.d.ts.map +1 -0
- package/dist/blocks/EvidenceTableView.js +30 -0
- package/dist/blocks/GeovisionEmbedView.d.ts +28 -0
- package/dist/blocks/GeovisionEmbedView.d.ts.map +1 -0
- package/dist/blocks/GeovisionEmbedView.js +33 -0
- package/dist/blocks/InsertMenu.d.ts +22 -0
- package/dist/blocks/InsertMenu.d.ts.map +1 -0
- package/dist/blocks/InsertMenu.js +59 -0
- package/dist/blocks/InsertRail.d.ts +28 -0
- package/dist/blocks/InsertRail.d.ts.map +1 -0
- package/dist/blocks/InsertRail.js +29 -0
- package/dist/blocks/NarrativeBlockView.d.ts +22 -0
- package/dist/blocks/NarrativeBlockView.d.ts.map +1 -0
- package/dist/blocks/NarrativeBlockView.js +44 -0
- package/dist/blocks/NativeTableView.d.ts +12 -0
- package/dist/blocks/NativeTableView.d.ts.map +1 -0
- package/dist/blocks/NativeTableView.js +36 -0
- package/dist/blocks/SavedViewDropdown.d.ts +17 -0
- package/dist/blocks/SavedViewDropdown.d.ts.map +1 -0
- package/dist/blocks/SavedViewDropdown.js +85 -0
- package/dist/blocks/SelectionToolbar.d.ts +25 -0
- package/dist/blocks/SelectionToolbar.d.ts.map +1 -0
- package/dist/blocks/SelectionToolbar.js +175 -0
- package/dist/blocks/menuStyles.d.ts +32 -0
- package/dist/blocks/menuStyles.d.ts.map +1 -0
- package/dist/blocks/menuStyles.js +32 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/locales/en.json +37 -0
- package/dist/locales/index.d.ts +105 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/index.js +27 -0
- package/dist/locales/zh-CN.json +37 -0
- package/dist/tokens.d.ts +42 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +43 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/locales/en.json +37 -0
- package/locales/zh-CN.json +37 -0
- package/package.json +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# PrismMainEditorMicroUI
|
|
2
|
+
|
|
3
|
+
Platform Prism main-editor micro-UI. Component id: `main-editor`.
|
|
4
|
+
Published to npm as [`@zephytiju/prism-main-editor`](https://www.npmjs.com/package/@zephytiju/prism-main-editor).
|
|
5
|
+
|
|
6
|
+
The dossier editor axiom component (design decision D9): the single editor component owning the
|
|
7
|
+
title bar — breadcrumb + autosave stamp, `DRAFT / V07` state chip, `COMMENTS` toggle (active state
|
|
8
|
+
amber) driving the in-editor comments pane, `SHARE`, `•••` overflow — absorbed from the retired
|
|
9
|
+
Title Bar component, plus the typed block stack with `+` / drag insert rows and the expandable
|
|
10
|
+
block-anchored comments pane. Block surfaces follow the D10 taxonomy: narrative,
|
|
11
|
+
analyst-judgment, and dossier-native table blocks render as plain document elements — no outlining
|
|
12
|
+
box, no card chrome, no kind pill; the analyst-judgment highlight is an accent bar + tint only;
|
|
13
|
+
source links are ordinary inline hyperlinks embedded inside paragraph text (accent-colored,
|
|
14
|
+
underlined, preceded by the ↗ glyph) — never standalone link blocks; native tables render with row
|
|
15
|
+
rules like Feishu native tables. Only file-preview blocks are embedded, and they HOST the REAL
|
|
16
|
+
published micro-UI packages — no synthetic editor-side previews exist:
|
|
17
|
+
|
|
18
|
+
- the **geovision-embed** block hosts [`@zephytiju/prism-geovision-embed-block`](https://www.npmjs.com/package/@zephytiju/prism-geovision-embed-block)
|
|
19
|
+
(>= 0.2.0), which renders the full D10 chrome (kind tag `GEOVISION`, `SRC FILE · {file}` chip,
|
|
20
|
+
`VIEW ▾` variant dropdown fed by the render package's `describeViews(sourceFileRef)` through the
|
|
21
|
+
host Lattice transport, ↗ open arrow) and itself hosts the published render dispatcher
|
|
22
|
+
(`@zephytiju/prism-geovision-embed` 0.2.0) in its body slot, rendering exactly the ONE variant
|
|
23
|
+
the current viewId (`${fileRef}#${previewKind}`) selects;
|
|
24
|
+
- the **evidence-table** block hosts [`@zephytiju/prism-evidence-table`](https://www.npmjs.com/package/@zephytiju/prism-evidence-table)
|
|
25
|
+
(>= 0.1.0), which renders its own card chrome and the previewed saved view's
|
|
26
|
+
`IEvidenceSource`-shaped rows (source / observation / confidence / audit locator).
|
|
27
|
+
|
|
28
|
+
All data arrives IDossierDoc-shaped over the component channels (or explicit props); add, reorder,
|
|
29
|
+
and remove are dossier ACTIONS the editor emits as events for the host to apply. Composed
|
|
30
|
+
applications (for example Guanlan) consume it as-is; the component is platform-owned.
|
|
31
|
+
|
|
32
|
+
## Configuration keys
|
|
33
|
+
|
|
34
|
+
| Prop | Meaning |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `locale` | UI locale for the component-fixed strings: `"en" \| "zh-CN"` (default `"en"`) — see [i18n](#internationalization-i18n) |
|
|
37
|
+
| `variant` | `"editor"` (default; insert + drag controls) or `"readonly"` (published view — controls hidden) |
|
|
38
|
+
| `enabledBlockKinds` | Block kinds the insert menu offers (default: all five — `narrative`, `assessment`, `geovision-embed`, `evidence-table`, `native-table`) |
|
|
39
|
+
| `showInsertControls` | Whether the `+` insert controls render in the editor variant (default `true`; drag handles follow `variant`) |
|
|
40
|
+
| `titleFormat` | `(dossier) => string` workspace title line (default: the dossier `title`) |
|
|
41
|
+
| `titleBarActions` | Title-bar actions to enable: `{ comments?, share?, overflow? }` (all default `true`; the title bar is component configuration — Prism has no composed title node) |
|
|
42
|
+
| `dossier` | Explicit `MainEditorDossier` override; defaults to the `main-editor.dossier` channel |
|
|
43
|
+
| `commentThreads` | Explicit threads override; defaults to the `main-editor.comment-threads` channel |
|
|
44
|
+
| `height` | Editor surface height in px (default `900`) |
|
|
45
|
+
|
|
46
|
+
## Channel contract
|
|
47
|
+
|
|
48
|
+
| Direction | Kind | Id | Payload |
|
|
49
|
+
| --- | --- | --- | --- |
|
|
50
|
+
| 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 |
|
|
51
|
+
| consumes | state | `main-editor.comment-threads` | `readonly MainEditorCommentThread[] \| null` — ICommentThread-shaped comments widened with `blockId` anchoring and open/done status |
|
|
52
|
+
| consumes | state | `main-editor.comments-open` | `boolean` — the pane state channel (D6); `null` defaults to the prototype's open pane |
|
|
53
|
+
| consumes | state | `main-editor.focused-block` | `{ blockId } \| null` — derived for the focus wash |
|
|
54
|
+
| publishes | state | `main-editor.comments-open` | `boolean` — the title-bar COMMENTS toggle (setter-only publish; amber-active when open) |
|
|
55
|
+
| publishes | state | `main-editor.focused-block` | `{ blockId }` — clicking a block or a thread card (block review mode) |
|
|
56
|
+
| emits | event | `main-editor.insert-block-requested` | `{ afterBlockId: string \| null, kind }` — the `+` block-kind menu; a dossier action (IDossierDoc) the host applies |
|
|
57
|
+
| emits | event | `main-editor.reorder-block-requested` | `{ blockId, direction: "up" \| "down" }` — the ⠿ drag menu; a dossier action |
|
|
58
|
+
| emits | event | `main-editor.remove-block-requested` | `{ blockId }` — the ⠿ drag menu; a dossier action |
|
|
59
|
+
| 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 |
|
|
60
|
+
| emits | event | `main-editor.share-requested` | `{ docId }` — the SHARE sharing intent (never grants authorization; the host performs the share and records the IAuditChain entry) |
|
|
61
|
+
|
|
62
|
+
The variant state of the embedded blocks is owned by the HOSTED components through their own
|
|
63
|
+
file-level channels — `geovision-embed.selected-view` (`{ sourceFileRef, viewId }`, view ids
|
|
64
|
+
`${fileRef}#${previewKind}`) and `evidence-table.selected-view` (`{ sourceFileRef, viewId }`) —
|
|
65
|
+
so every embed of the same source file re-previews the picked variant (D10). The editor's former
|
|
66
|
+
per-block `main-editor.selected-view` channel was REMOVED in the real-components migration: it
|
|
67
|
+
conflicted with the components' file-level semantics; the editor no longer reimplements embedded
|
|
68
|
+
view selection at all.
|
|
69
|
+
|
|
70
|
+
Channel ids are string literals at every call-site so the build-time channel-graph scanner can
|
|
71
|
+
derive the graph. Channel reads use `usePrismStateValue`; every publication is setter-only (or an
|
|
72
|
+
event), so publishers never re-render from their own writes. Block focus follows the EntityCard
|
|
73
|
+
pattern — each item owns its binding, publishes on change, and derives its current value from the
|
|
74
|
+
same channel, so every host bound to the channel stays in sync (D6 state passing).
|
|
75
|
+
|
|
76
|
+
## Audit rule
|
|
77
|
+
|
|
78
|
+
Routine dossier reads and interactions are NOT audit-worthy: this component emits NO audit event
|
|
79
|
+
for them. The only audit-adjacent emission is the `main-editor.share-requested` intent — sharing is
|
|
80
|
+
the audit-worthy action, and the recording host (not this component) appends the IAuditChain entry.
|
|
81
|
+
|
|
82
|
+
## Internationalization (i18n)
|
|
83
|
+
|
|
84
|
+
The component ships `en` and `zh-CN` locale bundles — `src/locales/en.json` / `src/locales/zh-CN.json` —
|
|
85
|
+
and every component-fixed UI string is resolved from them (the `DOSSIER` doc prefix, `AUTOSAVED {time}`,
|
|
86
|
+
the `COMMENTS · {count}` toggle and `{count} OPEN COMMENTS` chip lines, SHARE, the `INSERT BLOCK` /
|
|
87
|
+
`BLOCK ACTIONS` menus, every block-kind label, the comments pane copy, and the
|
|
88
|
+
empty state). The embedded-block chrome copy (kind tags, `SRC FILE · {file}` chips,
|
|
89
|
+
`VIEW · {variant}` pills, `SAVED VIEWS · {file}` menus, ↗ arrow titles) lives in the HOSTED
|
|
90
|
+
packages' own locale bundles and localizes through the `locale` prop the editor forwards. The
|
|
91
|
+
component renders no hardcoded copy.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"main-editor": {
|
|
96
|
+
"docPrefix": "DOSSIER",
|
|
97
|
+
"autosavedAt": "AUTOSAVED {time}",
|
|
98
|
+
"commentsButtonWithCount": "COMMENTS · {count}",
|
|
99
|
+
"openCommentsCount": "{count} OPEN COMMENTS",
|
|
100
|
+
"share": "SHARE",
|
|
101
|
+
"kindAssessment": "ANALYST JUDGMENT",
|
|
102
|
+
"openThreads": "{count} OPEN",
|
|
103
|
+
"threadBlockMeta": "BLOCK {block} • {status}"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `locale?: "en" | "zh-CN"` prop (default `"en"`) selects the string table per instance.
|
|
109
|
+
- `{placeholder}` templates (`{count}`, `{time}`, `{file}`, `{variant}`, `{block}`, `{status}`) use
|
|
110
|
+
plain substitution via `formatMessage`, exported from the package entry.
|
|
111
|
+
- **Composition-authored strings are localized by the composer; component-fixed strings live in the
|
|
112
|
+
locale JSONs.** Breadcrumb segments, the doc meta line, block content, saved-view labels, and
|
|
113
|
+
thread titles are data (IDossierDoc/ICommentThread-shaped), so a localized dossier supplies its
|
|
114
|
+
own translated content per locale.
|
|
115
|
+
- Locale bundles are namespaced under the component id (`"main-editor"`) so a composer can
|
|
116
|
+
deep-merge every component's bundle into ONE UI language bundle without collisions:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { locales as mainEditorLocales } from "@zephytiju/prism-main-editor";
|
|
120
|
+
// mainEditorLocales["zh-CN"] -> { "main-editor": { … } }
|
|
121
|
+
const uiBundle = deepMerge(hostStrings, mainEditorLocales["zh-CN"]);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The parsed bundles are exported from the package entry (`locales`, `en`, `zhCN`,
|
|
125
|
+
`stringsForLocale`), and the raw JSONs are also served by the `./locales/*` exports subpath
|
|
126
|
+
(e.g. `@zephytiju/prism-main-editor/locales/zh-CN.json`); `files` ships both `dist` and `locales`.
|
|
127
|
+
|
|
128
|
+
## Theme
|
|
129
|
+
|
|
130
|
+
No palette is hardcoded. Every color resolves to SEMANTIC theme tokens (`ok`, `accent`, `threat`,
|
|
131
|
+
`warn`, `signal`, `card`, `card-dark`, `input`, `border`, `line`, `text`, `muted`, `deep`, `panel`)
|
|
132
|
+
consumed as CSS variables, plus `--mantine-font-family-monospace` for the mono typography — the
|
|
133
|
+
palette is supplied entirely by the host's `MantineProvider`. The prototype's mint/blue/amber/red/
|
|
134
|
+
purple accents map onto `ok` / `accent` / `warn` / `threat` / `signal` respectively; translucent
|
|
135
|
+
surfaces (the amber highlight tint, the comments-pane backdrop, the focus wash) derive from the
|
|
136
|
+
same tokens through `color-mix`. The local demo ships TWO themes, both defined in `src/demo.tsx`:
|
|
137
|
+
`geovisionTheme` (dark), mapping each semantic token onto the exact `:root` variables of the v9
|
|
138
|
+
design prototype, and the contrasting `latticeLightTheme` (light), mapping the SAME semantic token
|
|
139
|
+
keys onto a different palette — the component is skinned purely through the surrounding
|
|
140
|
+
`MantineProvider`.
|
|
141
|
+
|
|
142
|
+
## Source layout
|
|
143
|
+
|
|
144
|
+
`src/` is strictly two parts:
|
|
145
|
+
|
|
146
|
+
- Component source (what the package compiles): `MainEditor.tsx` (orchestrator — channel wiring,
|
|
147
|
+
empty state, layout), `TitleBar.tsx` (the absorbed title bar, D9), `CommentsPane.tsx` (the
|
|
148
|
+
expandable in-editor pane), `blocks/` (`BlockStack.tsx` — workspace header, insert rows, +/⠿
|
|
149
|
+
menus, per-kind dispatch; `NarrativeBlockView.tsx`, `AssessmentBlockView.tsx`,
|
|
150
|
+
`GeovisionEmbedView.tsx` — HOSTS the real `@zephytiju/prism-geovision-embed-block`;
|
|
151
|
+
`EvidenceTableView.tsx` — HOSTS the real `@zephytiju/prism-evidence-table`;
|
|
152
|
+
`NativeTableView.tsx` — the D10 block surfaces; `InsertMenu.tsx`, `InsertRail.tsx`,
|
|
153
|
+
`DragMenu.tsx`, `SelectionToolbar.tsx`, `menuStyles.ts` — the D11 affordances), `types.ts` (the
|
|
154
|
+
IDossierDoc/ICommentThread-shaped data model + event payloads), `tokens.ts`
|
|
155
|
+
(semantic-token shorthands), `index.ts` (public entry), and `src/locales/` (`en.json`,
|
|
156
|
+
`zh-CN.json`, `index.ts` — the i18n string bundles, their resolver, and the `{placeholder}`
|
|
157
|
+
interpolation helper). The embedded item templates are the hosting seams for the real published
|
|
158
|
+
packages — the editor renders NO synthetic embed previews.
|
|
159
|
+
- Demo: exactly ONE file, `src/demo.tsx` — the two host themes (GeoVision v9 + Lattice Light), all
|
|
160
|
+
demo test data (the Redwater mock dossier + threads mirroring the v9 prototype; the demo
|
|
161
|
+
geovision-view file built through the bundle's own `saveGeovisionView`; the evidence saved views
|
|
162
|
+
with their rows), the channel seeding (including the two file-level selected-view channels the
|
|
163
|
+
hosted components own), the mock Lattice action executor serving the demo file (the
|
|
164
|
+
standards-compliant host pattern — the hosted block's `describeViews()` and the REAL render
|
|
165
|
+
dispatcher run end to end against it), the mock dossier-action applier (insert/reorder/remove/
|
|
166
|
+
duplicate — the host side of the action contract), the channel monitors + action log, and the
|
|
167
|
+
demo page rendering TWO `MainEditor` instances behind a global EN | 中文 language switcher (plus
|
|
168
|
+
per-instance switches).
|
|
169
|
+
|
|
170
|
+
The npm package ships `dist` (compiled component + type declarations + locale JSONs) and the
|
|
171
|
+
top-level `locales/` directory (the raw JSON bundles, served by the `./locales/*` exports
|
|
172
|
+
subpath); no demo code is published. `scripts/copy-locales.mjs` copies the JSON bundles into both
|
|
173
|
+
locations during `npm run build`.
|
|
174
|
+
|
|
175
|
+
## Dependencies and build order
|
|
176
|
+
|
|
177
|
+
The two embedded-block packages are **peerDependencies AND devDependencies**:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
"peerDependencies": {
|
|
181
|
+
"@zephytiju/prism-evidence-table": ">=0.1.0",
|
|
182
|
+
"@zephytiju/prism-geovision-embed-block": ">=0.2.0",
|
|
183
|
+
"@zephytiju/prism-react": ">=0.2.0", …
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
While both consumed packages are UNPUBLISHED local builds, the devDependencies resolve them as
|
|
188
|
+
`file:` links into the sibling checkouts — so the required BUILD ORDER is:
|
|
189
|
+
|
|
190
|
+
1. `npm run build` in `../PrismGeovisionEmbedBlockMicroUI` (ships `dist` + `locales`),
|
|
191
|
+
2. `npm run build` in `../PrismEvidenceTableMicroUI` (ships `dist` + `locales`),
|
|
192
|
+
3. only then `npm install` / `npm run build` / `npm test` here — the `file:` links and the
|
|
193
|
+
`resolve.dedupe` in `vite.config.ts` / `vitest.config.ts` (react, react-dom, @mantine/core,
|
|
194
|
+
@zephytiju/prism-react, @zephytiju/prism-geovision-embed) keep the linked packages sharing ONE
|
|
195
|
+
prism-react channel store / action executor and ONE React instance with the editor.
|
|
196
|
+
|
|
197
|
+
The devDependencies additionally install the hosted render stack's registry peers
|
|
198
|
+
(`@zephytiju/prism-geovision-embed` 0.2.0 with its cesium/resium deps, `@zephytiju/prism-relation-fluxboard`,
|
|
199
|
+
`@zephytiju/prism-stats-panel`, `@zephytiju/prism-timeline-board`, `@xyflow/react`,
|
|
200
|
+
`@zephytiju/lattice-common-bundle`) so the demo and tests exercise the REAL dispatcher end to
|
|
201
|
+
end. `@zephytiju/prism-react` is pinned `>=0.2.0` because the hosted stack resolves the source
|
|
202
|
+
file through the 0.2.0 transport seam (`setPrismActionExecutor` / `useLatticeTransport`).
|
|
203
|
+
|
|
204
|
+
## Local development
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
npm install
|
|
208
|
+
npm run typecheck
|
|
209
|
+
npm test
|
|
210
|
+
npm run dev
|
|
211
|
+
npm run shot-demo
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`npm install` pulls the platform peers (`@zephytiju/prism-react`,
|
|
215
|
+
`@zephytiju/lattice-common-interfaces`) from the npm registry, along with the host-side peer
|
|
216
|
+
dependencies (`react`, `react-dom`, `@mantine/core`) and — through the `file:` links — the two
|
|
217
|
+
locally built embedded-block packages (see the build order above). When consuming the published
|
|
218
|
+
package, install it directly (`npm install @zephytiju/prism-main-editor`) and provide the peer
|
|
219
|
+
dependencies (including the two embedded-block packages once published) in the host application.
|
|
220
|
+
|
|
221
|
+
The demo (`npm run dev`, entry `src/demo.tsx`) plays the host: at module scope it installs the
|
|
222
|
+
mock Lattice action executor serving the demo geovision-view file, then seeds the Redwater mock
|
|
223
|
+
dossier, its threads, and the two file-level selected-view channels onto the GLOBAL channels, and
|
|
224
|
+
renders TWO `MainEditor` instances, each inside its own MantineProvider with a different theme
|
|
225
|
+
(GeoVision v9 dark, Lattice Light). The geovision-embed block row hosts the REAL
|
|
226
|
+
`@zephytiju/prism-geovision-embed-block`: its VIEW ▾ list comes from the REAL `describeViews()`
|
|
227
|
+
call over the demo executor (one saved view per previewKind — `${fileRef}#map / #relational /
|
|
228
|
+
#timeline / #stats`), and its body renders the REAL published dispatcher — exactly the ONE
|
|
229
|
+
variant the current viewId selects (the demo opts the map variant into `engine="svg"` for
|
|
230
|
+
deterministic headless capture). The evidence-table block row hosts the REAL
|
|
231
|
+
`@zephytiju/prism-evidence-table` with the demo saved views' rows. The COMMENTS toggle drives
|
|
232
|
+
the pane through the shared `main-editor.comments-open` channel, so toggling it in EITHER
|
|
233
|
+
instance opens the pane in BOTH. The `+` insert control opens the block-kind menu and emits
|
|
234
|
+
`insert-block-requested`; the ⠿ drag menu emits reorder/remove — the mock host applies these
|
|
235
|
+
dossier actions against its projection and republishes, exactly how a real host routes them
|
|
236
|
+
through IDossierDoc actions. The hosted blocks' ↗ arrows emit their open intents, which the
|
|
237
|
+
editor re-emits as `main-editor.open-view-requested`. The channel monitors and action log below
|
|
238
|
+
the instances show all of this live. `npm run shot-demo` boots the vite dev server, drives the
|
|
239
|
+
demo in headless Chrome, and captures the evidence states to `/tmp/guanlan-review/`:
|
|
240
|
+
`demo-main-editor-real-embeds-dark-en-v4.png`,
|
|
241
|
+
`demo-main-editor-real-embeds-variant-switch-v4.png`,
|
|
242
|
+
`demo-main-editor-real-embeds-light-zh-cn-v4.png`, and
|
|
243
|
+
`demo-main-editor-full-page.png`.
|
|
244
|
+
|
|
245
|
+
## Design record
|
|
246
|
+
|
|
247
|
+
https://qcnwge0wy4s0.feishu.cn/wiki/B87YwS2PriaGvlkAcOtctKm8nsd — decisions D1–D10 (esp. D3
|
|
248
|
+
runtime blocks, D5 component tiers, D6 state passing, D8 channel contracts, D9 title-bar
|
|
249
|
+
absorption, D10 block surface taxonomy). Component doc:
|
|
250
|
+
https://qcnwge0wy4s0.feishu.cn/wiki/UqqYwbzkViRiprkNpkRcKt3tnFf. Visual reference: the
|
|
251
|
+
authoritative `dossier-editor-standalone-v9.html` prototype attached to the design doc
|
|
252
|
+
(`.ed-head` / `.nblock` / `.hl` / `.block` / `.vsel` / `.cpane`).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { MainEditorLocale } from "./locales/index.js";
|
|
2
|
+
import type { MainEditorCommentThread } from "./types.js";
|
|
3
|
+
export interface CommentsPaneProps {
|
|
4
|
+
readonly threads: readonly MainEditorCommentThread[];
|
|
5
|
+
readonly open: boolean;
|
|
6
|
+
readonly locale: MainEditorLocale;
|
|
7
|
+
/** Ordered block ids of the rendered stack — threads anchor by "BLOCK {ordinal}". */
|
|
8
|
+
readonly blockOrder: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* D11 alignment: content-space top (px) of each rendered block row, measured
|
|
11
|
+
* by the editor — every thread card positions at its anchor block's top
|
|
12
|
+
* (minus the workspace scroll offset the rail mirrors).
|
|
13
|
+
*/
|
|
14
|
+
readonly blockAnchors: Readonly<Record<string, number>>;
|
|
15
|
+
/** Current workspace scroll offset the rail mirrors (px). */
|
|
16
|
+
readonly scrollTop: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The block-anchored comments rail — expandable INSIDE the editor body (D6)
|
|
20
|
+
* and ALIGNED to the anchored blocks (D11): each card sits at the vertical
|
|
21
|
+
* position of its anchor block (the workspace's block-row tops minus the
|
|
22
|
+
* scroll offset the rail mirrors), and multiple threads on one block stack
|
|
23
|
+
* directly below each other — Feishu-style. The title-bar COMMENTS toggle
|
|
24
|
+
* drives it through the shared "main-editor.comments-open" channel state
|
|
25
|
+
* (D6), not a direct reference, so any peer bound to the same channel stays
|
|
26
|
+
* in sync. Closed, the pane slides out with the prototype's translateX(100%)
|
|
27
|
+
* transition; open-thread cards render amber; clicking a thread publishes
|
|
28
|
+
* "main-editor.focused-block". Thread entries are ICommentThread-shaped
|
|
29
|
+
* bounded comments.
|
|
30
|
+
*/
|
|
31
|
+
export declare function CommentsPane({ threads, open, locale, blockOrder, blockAnchors, scrollTop, }: CommentsPaneProps): import("react").JSX.Element;
|
|
32
|
+
//# sourceMappingURL=CommentsPane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommentsPane.d.ts","sourceRoot":"","sources":["../src/CommentsPane.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,uBAAuB,EAA0B,MAAM,YAAY,CAAC;AAGlF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IACrD,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAUD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,IAAI,EACJ,MAAM,EACN,UAAU,EACV,YAAY,EACZ,SAAS,GACV,EAAE,iBAAiB,+BAsFnB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Group, Stack, Text } from "@mantine/core";
|
|
3
|
+
import { usePrismStateSetter } from "@zephytiju/prism-react";
|
|
4
|
+
import { formatMessage, stringsForLocale } from "./locales/index.js";
|
|
5
|
+
import { BORDER, CARD, DEEP, MONO, MUTED, PANE_BACKDROP, WARN, pad2 } from "./tokens.js";
|
|
6
|
+
/** Thread-card geometry (px): fixed-height cards with a fixed stacking gap. */
|
|
7
|
+
const CARD_HEIGHT = 56;
|
|
8
|
+
const CARD_GAP = 8;
|
|
9
|
+
/** The pane-header height (px) the anchor tops compensate for. */
|
|
10
|
+
const HEADER_HEIGHT = 56;
|
|
11
|
+
/** Cards never slide under the pane header. */
|
|
12
|
+
const CARD_MIN_TOP = 8;
|
|
13
|
+
/**
|
|
14
|
+
* The block-anchored comments rail — expandable INSIDE the editor body (D6)
|
|
15
|
+
* and ALIGNED to the anchored blocks (D11): each card sits at the vertical
|
|
16
|
+
* position of its anchor block (the workspace's block-row tops minus the
|
|
17
|
+
* scroll offset the rail mirrors), and multiple threads on one block stack
|
|
18
|
+
* directly below each other — Feishu-style. The title-bar COMMENTS toggle
|
|
19
|
+
* drives it through the shared "main-editor.comments-open" channel state
|
|
20
|
+
* (D6), not a direct reference, so any peer bound to the same channel stays
|
|
21
|
+
* in sync. Closed, the pane slides out with the prototype's translateX(100%)
|
|
22
|
+
* transition; open-thread cards render amber; clicking a thread publishes
|
|
23
|
+
* "main-editor.focused-block". Thread entries are ICommentThread-shaped
|
|
24
|
+
* bounded comments.
|
|
25
|
+
*/
|
|
26
|
+
export function CommentsPane({ threads, open, locale, blockOrder, blockAnchors, scrollTop, }) {
|
|
27
|
+
const strings = stringsForLocale(locale);
|
|
28
|
+
const publishFocusedBlock = usePrismStateSetter("main-editor.focused-block");
|
|
29
|
+
const openCount = threads.filter((thread) => thread.status === "open").length;
|
|
30
|
+
// Stack index per thread: its ordinal among the threads sharing the block.
|
|
31
|
+
const seen = new Map();
|
|
32
|
+
const stackIndex = threads.map((thread) => {
|
|
33
|
+
const index = seen.get(thread.blockId) ?? 0;
|
|
34
|
+
seen.set(thread.blockId, index + 1);
|
|
35
|
+
return index;
|
|
36
|
+
});
|
|
37
|
+
return (_jsxs(Box, { w: 340, style: {
|
|
38
|
+
position: "absolute",
|
|
39
|
+
right: 0,
|
|
40
|
+
top: 0,
|
|
41
|
+
bottom: 0,
|
|
42
|
+
background: PANE_BACKDROP,
|
|
43
|
+
borderLeft: `1px solid var(--mantine-color-line-filled)`,
|
|
44
|
+
transform: open ? "translateX(0)" : "translateX(100%)",
|
|
45
|
+
transition: "transform .25s ease",
|
|
46
|
+
zIndex: 5,
|
|
47
|
+
...(open ? {} : { pointerEvents: "none" }),
|
|
48
|
+
}, "data-testid": "main-editor-comments-pane", "aria-hidden": !open, children: [_jsxs(Group, { justify: "space-between", align: "center", px: 14, h: 56, wrap: "nowrap", style: { background: CARD, borderBottom: `1px solid ${BORDER}` }, "data-testid": "main-editor-comments-head", children: [_jsxs(Stack, { gap: 3, children: [_jsx(Text, { fz: 12, fw: 600, c: "text", children: strings.commentsTitle }), _jsx(Text, { ff: MONO, fz: 7, style: { color: MUTED, letterSpacing: "0.05em" }, children: strings.commentsSubtitle })] }), _jsx(Text, { ff: MONO, fz: 7, fw: 500, c: "warn", "data-testid": "main-editor-comments-open-count", children: formatMessage(strings.openThreads, { count: openCount }) })] }), _jsx(Box, { style: { position: "relative", height: "calc(100% - 56px)", overflow: "hidden" }, children: threads.map((thread, index) => {
|
|
49
|
+
const anchorTop = blockAnchors[thread.blockId] ?? 0;
|
|
50
|
+
// The track begins below the pane header; compensating HEADER_HEIGHT
|
|
51
|
+
// puts the card at the anchor block's own viewport position.
|
|
52
|
+
const top = Math.max(anchorTop - HEADER_HEIGHT - scrollTop + stackIndex[index] * (CARD_HEIGHT + CARD_GAP), CARD_MIN_TOP);
|
|
53
|
+
return (_jsx(Box, { style: {
|
|
54
|
+
position: "absolute",
|
|
55
|
+
top,
|
|
56
|
+
left: 14,
|
|
57
|
+
right: 14,
|
|
58
|
+
height: CARD_HEIGHT,
|
|
59
|
+
transition: "top .12s ease",
|
|
60
|
+
}, "data-testid": `main-editor-thread-${thread.id}`, "data-anchor-block": thread.blockId, "data-stack-index": stackIndex[index], onClick: () => {
|
|
61
|
+
publishFocusedBlock({ blockId: thread.blockId });
|
|
62
|
+
}, children: _jsx(ThreadCard, { thread: thread, locale: locale, ordinal: blockOrder.indexOf(thread.blockId) + 1 }) }, thread.id));
|
|
63
|
+
}) })] }));
|
|
64
|
+
}
|
|
65
|
+
function ThreadCard({ thread, locale, ordinal, }) {
|
|
66
|
+
const strings = stringsForLocale(locale);
|
|
67
|
+
const status = thread.status === "open" ? strings.statusOpen : strings.statusDone;
|
|
68
|
+
return (_jsxs(Group, { gap: 12, align: "center", wrap: "nowrap", px: 10, h: CARD_HEIGHT, style: {
|
|
69
|
+
background: CARD,
|
|
70
|
+
border: `1px solid ${thread.status === "open" ? WARN : BORDER}`,
|
|
71
|
+
borderRadius: 6,
|
|
72
|
+
cursor: "pointer",
|
|
73
|
+
}, children: [_jsx(Box, { w: 28, h: 28, style: {
|
|
74
|
+
borderRadius: 14,
|
|
75
|
+
background: thread.status === "open" ? WARN : BORDER,
|
|
76
|
+
color: DEEP,
|
|
77
|
+
fontFamily: MONO,
|
|
78
|
+
fontSize: 9,
|
|
79
|
+
fontWeight: 500,
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
justifyContent: "center",
|
|
83
|
+
flexShrink: 0,
|
|
84
|
+
}, children: String(thread.comments.length).padStart(2, "0") }), _jsxs(Stack, { gap: 2, miw: 0, children: [_jsx(Text, { fz: 10, fw: 500, c: "text", style: { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: thread.quote === undefined ? thread.title : `“${thread.quote}”` }), _jsx(Text, { ff: MONO, fz: 7, style: { color: MUTED, letterSpacing: "0.02em" }, children: formatMessage(strings.threadBlockMeta, { block: pad2(ordinal), status }) })] })] }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MainEditorLocale } from "./locales/index.js";
|
|
2
|
+
import type { MainEditorBlockKind, MainEditorCommentThread, MainEditorDossier } from "./types.js";
|
|
3
|
+
export interface MainEditorProps {
|
|
4
|
+
/** Dossier projection override; defaults to the "main-editor.dossier" channel. */
|
|
5
|
+
readonly dossier?: MainEditorDossier;
|
|
6
|
+
/** Comment threads override; defaults to the "main-editor.comment-threads" channel. */
|
|
7
|
+
readonly commentThreads?: readonly MainEditorCommentThread[];
|
|
8
|
+
/** UI locale for the component-fixed strings (default "en"). */
|
|
9
|
+
readonly locale?: MainEditorLocale;
|
|
10
|
+
/** editor — insert + drag controls (default); readonly — published view. */
|
|
11
|
+
readonly variant?: "editor" | "readonly";
|
|
12
|
+
/** Block kinds the insert menu offers (default: all five D10 kinds). */
|
|
13
|
+
readonly enabledBlockKinds?: readonly MainEditorBlockKind[];
|
|
14
|
+
/** Whether insert controls render in the editor variant (default true). */
|
|
15
|
+
readonly showInsertControls?: boolean;
|
|
16
|
+
/** Workspace title line (defaults to the dossier title; titleFormat config key). */
|
|
17
|
+
readonly titleFormat?: (dossier: MainEditorDossier) => string;
|
|
18
|
+
/** Title-bar actions to enable (all default true; titleBarActions config key). */
|
|
19
|
+
readonly titleBarActions?: Readonly<{
|
|
20
|
+
comments?: boolean;
|
|
21
|
+
share?: boolean;
|
|
22
|
+
overflow?: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/** Editor surface height (default 900). */
|
|
25
|
+
readonly height?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Platform Prism main-editor micro-UI (component id "main-editor") — the
|
|
29
|
+
* single dossier editor axiom component (D9): the title bar (breadcrumb +
|
|
30
|
+
* autosave stamp, DRAFT/V07 state chip, amber-active COMMENTS toggle, SHARE
|
|
31
|
+
* sharing intent, overflow) absorbed from the retired Title Bar component,
|
|
32
|
+
* the typed block stack with hover-revealed +/⠿ insert gutters (D11), and
|
|
33
|
+
* the block-ALIGNED comments rail (D11 — each card sits at its anchor
|
|
34
|
+
* block's vertical position and follows the workspace scroll), all rendered
|
|
35
|
+
* from IDossierDoc-shaped data on the component channels.
|
|
36
|
+
*
|
|
37
|
+
* Channel discipline: the dossier + threads arrive on
|
|
38
|
+
* "main-editor.dossier" / "main-editor.comment-threads"; the COMMENTS toggle
|
|
39
|
+
* drives the in-editor pane through the "main-editor.comments-open" channel
|
|
40
|
+
* (D6 state passing); block focus publishes through "main-editor.focused-
|
|
41
|
+
* block"; the HOSTED embedded blocks own their variant state through their
|
|
42
|
+
* component channels ("geovision-embed.selected-view" /
|
|
43
|
+
* "evidence-table.selected-view") and their ↗ open intents are re-emitted as
|
|
44
|
+
* "main-editor.open-view-requested" events; add/reorder/remove/duplicate are
|
|
45
|
+
* dossier ACTIONS emitted as events for the host to apply (D3 — blocks are
|
|
46
|
+
* added at app runtime through dossier actions, never composition mutation);
|
|
47
|
+
* comment anchors arrive as "main-editor.comment-requested" events the host
|
|
48
|
+
* turns into threads. No Lattice calls are made from the editor's own render
|
|
49
|
+
* path (the hosted geovision block's describeViews/render resolve through the
|
|
50
|
+
* HOST-supplied transport), so no audit events exist for routine reads; the
|
|
51
|
+
* only audit-adjacent emission is the SHARE intent, which the host (not this
|
|
52
|
+
* component) records on IAuditChain.
|
|
53
|
+
*
|
|
54
|
+
* No palette is hardcoded: every color resolves to semantic theme tokens
|
|
55
|
+
* (ok / threat / warn / signal / accent / card / card-dark / input / border /
|
|
56
|
+
* line / text / muted / deep / panel) supplied by the host's MantineProvider.
|
|
57
|
+
*/
|
|
58
|
+
export declare function MainEditor({ dossier: dossierProp, commentThreads: threadsProp, locale, variant, enabledBlockKinds, showInsertControls, titleFormat, titleBarActions, height, }: MainEditorProps): import("react").JSX.Element;
|
|
59
|
+
//# sourceMappingURL=MainEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MainEditor.d.ts","sourceRoot":"","sources":["../src/MainEditor.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAK3D,OAAO,KAAK,EACV,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IACrC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC7D,gEAAgE;IAChE,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IACzC,wEAAwE;IACxE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAC5D,2EAA2E;IAC3E,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,oFAAoF;IACpF,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,MAAM,CAAC;IAC9D,kFAAkF;IAClF,QAAQ,CAAC,eAAe,CAAC,EAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACjG,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EAAE,WAAW,EACpB,cAAc,EAAE,WAAW,EAC3B,MAAa,EACb,OAAkB,EAClB,iBAAiB,EACjB,kBAAyB,EACzB,WAAW,EACX,eAAe,EACf,MAAY,GACb,EAAE,eAAe,+BA8HjB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Card, Stack, Text } from "@mantine/core";
|
|
4
|
+
import { usePrismStateValue } from "@zephytiju/prism-react";
|
|
5
|
+
import { stringsForLocale } from "./locales/index.js";
|
|
6
|
+
import { TitleBar } from "./TitleBar.js";
|
|
7
|
+
import { BlockStack } from "./blocks/BlockStack.js";
|
|
8
|
+
import { SelectionToolbar } from "./blocks/SelectionToolbar.js";
|
|
9
|
+
import { CommentsPane } from "./CommentsPane.js";
|
|
10
|
+
import { DEEP } from "./tokens.js";
|
|
11
|
+
/**
|
|
12
|
+
* Platform Prism main-editor micro-UI (component id "main-editor") — the
|
|
13
|
+
* single dossier editor axiom component (D9): the title bar (breadcrumb +
|
|
14
|
+
* autosave stamp, DRAFT/V07 state chip, amber-active COMMENTS toggle, SHARE
|
|
15
|
+
* sharing intent, overflow) absorbed from the retired Title Bar component,
|
|
16
|
+
* the typed block stack with hover-revealed +/⠿ insert gutters (D11), and
|
|
17
|
+
* the block-ALIGNED comments rail (D11 — each card sits at its anchor
|
|
18
|
+
* block's vertical position and follows the workspace scroll), all rendered
|
|
19
|
+
* from IDossierDoc-shaped data on the component channels.
|
|
20
|
+
*
|
|
21
|
+
* Channel discipline: the dossier + threads arrive on
|
|
22
|
+
* "main-editor.dossier" / "main-editor.comment-threads"; the COMMENTS toggle
|
|
23
|
+
* drives the in-editor pane through the "main-editor.comments-open" channel
|
|
24
|
+
* (D6 state passing); block focus publishes through "main-editor.focused-
|
|
25
|
+
* block"; the HOSTED embedded blocks own their variant state through their
|
|
26
|
+
* component channels ("geovision-embed.selected-view" /
|
|
27
|
+
* "evidence-table.selected-view") and their ↗ open intents are re-emitted as
|
|
28
|
+
* "main-editor.open-view-requested" events; add/reorder/remove/duplicate are
|
|
29
|
+
* dossier ACTIONS emitted as events for the host to apply (D3 — blocks are
|
|
30
|
+
* added at app runtime through dossier actions, never composition mutation);
|
|
31
|
+
* comment anchors arrive as "main-editor.comment-requested" events the host
|
|
32
|
+
* turns into threads. No Lattice calls are made from the editor's own render
|
|
33
|
+
* path (the hosted geovision block's describeViews/render resolve through the
|
|
34
|
+
* HOST-supplied transport), so no audit events exist for routine reads; the
|
|
35
|
+
* only audit-adjacent emission is the SHARE intent, which the host (not this
|
|
36
|
+
* component) records on IAuditChain.
|
|
37
|
+
*
|
|
38
|
+
* No palette is hardcoded: every color resolves to semantic theme tokens
|
|
39
|
+
* (ok / threat / warn / signal / accent / card / card-dark / input / border /
|
|
40
|
+
* line / text / muted / deep / panel) supplied by the host's MantineProvider.
|
|
41
|
+
*/
|
|
42
|
+
export function MainEditor({ dossier: dossierProp, commentThreads: threadsProp, locale = "en", variant = "editor", enabledBlockKinds, showInsertControls = true, titleFormat, titleBarActions, height = 900, }) {
|
|
43
|
+
const strings = stringsForLocale(locale);
|
|
44
|
+
const channelDossier = usePrismStateValue("main-editor.dossier");
|
|
45
|
+
const channelThreads = usePrismStateValue("main-editor.comment-threads");
|
|
46
|
+
// The pane state arrives through the channel (D6): the title-bar toggle
|
|
47
|
+
// publishes it; null defaults to the prototype's open pane.
|
|
48
|
+
const commentsOpenChannel = usePrismStateValue("main-editor.comments-open");
|
|
49
|
+
const commentsOpen = commentsOpenChannel ?? true;
|
|
50
|
+
const focusedBlock = usePrismStateValue("main-editor.focused-block");
|
|
51
|
+
const dossier = dossierProp ?? channelDossier;
|
|
52
|
+
const threads = threadsProp ?? channelThreads;
|
|
53
|
+
// D11 alignment state: the workspace scroll offset the rail mirrors, plus
|
|
54
|
+
// the measured content-space tops of every block row.
|
|
55
|
+
const workspaceRef = useRef(null);
|
|
56
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
57
|
+
const [blockAnchors, setBlockAnchors] = useState({});
|
|
58
|
+
const measureAnchors = useCallback(() => {
|
|
59
|
+
const workspace = workspaceRef.current;
|
|
60
|
+
if (workspace === null) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const portTop = workspace.getBoundingClientRect().top;
|
|
64
|
+
const next = {};
|
|
65
|
+
workspace.querySelectorAll("[data-block-row-id]").forEach((row) => {
|
|
66
|
+
const id = row.getAttribute("data-block-row-id");
|
|
67
|
+
if (id !== null) {
|
|
68
|
+
next[id] = Math.max(row.getBoundingClientRect().top - portTop + workspace.scrollTop, 0);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
setBlockAnchors(next);
|
|
72
|
+
}, []);
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
if (dossier === null || dossier === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Measure after layout settles, then once more after fonts/async paint.
|
|
78
|
+
const raf = requestAnimationFrame(measureAnchors);
|
|
79
|
+
const later = window.setTimeout(measureAnchors, 300);
|
|
80
|
+
return () => {
|
|
81
|
+
cancelAnimationFrame(raf);
|
|
82
|
+
window.clearTimeout(later);
|
|
83
|
+
};
|
|
84
|
+
}, [dossier, threads, locale, variant, measureAnchors]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const onResize = () => {
|
|
87
|
+
measureAnchors();
|
|
88
|
+
};
|
|
89
|
+
window.addEventListener("resize", onResize);
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("resize", onResize);
|
|
92
|
+
};
|
|
93
|
+
}, [measureAnchors]);
|
|
94
|
+
if (dossier === null || dossier === undefined) {
|
|
95
|
+
return (_jsx(Card, { withBorder: true, "data-testid": "main-editor-empty", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { fw: 500, c: "text", children: strings.emptyTitle }), _jsx(Text, { size: "sm", c: "muted", children: strings.emptyHint })] }) }));
|
|
96
|
+
}
|
|
97
|
+
const openCommentCount = (threads ?? []).filter((thread) => thread.status === "open").length;
|
|
98
|
+
const shownDossier = titleFormat === undefined ? dossier : { ...dossier, title: titleFormat(dossier) };
|
|
99
|
+
return (_jsxs(Stack, { gap: 0, miw: 0, style: { background: DEEP, borderRadius: 6, overflow: "hidden", height }, "data-testid": "main-editor", children: [_jsx(TitleBar, { dossier: shownDossier, openCommentCount: openCommentCount, commentsOpen: commentsOpen, locale: locale, actions: titleBarActions }), _jsxs(Box, { style: { position: "relative", flex: 1, overflow: "hidden" }, "data-testid": "main-editor-body", children: [_jsx(Box, { ref: workspaceRef, h: "100%", px: 24, pt: 22, style: { overflowY: "auto", background: "var(--mantine-color-deep-filled)" }, "data-testid": "main-editor-workspace", onScroll: (event) => {
|
|
100
|
+
setScrollTop(event.currentTarget.scrollTop);
|
|
101
|
+
}, children: _jsx(Box, { w: 944, children: _jsx(BlockStack, { dossier: shownDossier, locale: locale, variant: variant, enabledBlockKinds: enabledBlockKinds ?? [], showInsertControls: showInsertControls, focusedBlockId: focusedBlock?.blockId ?? null }) }) }), _jsx(CommentsPane, { threads: threads ?? [], open: commentsOpen, locale: locale, blockOrder: shownDossier.blocks.map((block) => block.id), blockAnchors: blockAnchors, scrollTop: scrollTop }), variant === "editor" ? (_jsx(SelectionToolbar, { locale: locale, containerRef: workspaceRef })) : null] })] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MainEditorDossier } from "./types.js";
|
|
2
|
+
import type { MainEditorLocale } from "./locales/index.js";
|
|
3
|
+
export interface TitleBarProps {
|
|
4
|
+
readonly dossier: MainEditorDossier;
|
|
5
|
+
/** Open thread count from the comment threads (drives the chip + toggle label). */
|
|
6
|
+
readonly openCommentCount: number;
|
|
7
|
+
/** Whether the in-editor comments pane is open (drives the amber-active state). */
|
|
8
|
+
readonly commentsOpen: boolean;
|
|
9
|
+
readonly locale: MainEditorLocale;
|
|
10
|
+
/** Configuration: title-bar actions to enable (all default true). */
|
|
11
|
+
readonly actions?: Readonly<{
|
|
12
|
+
comments?: boolean;
|
|
13
|
+
share?: boolean;
|
|
14
|
+
overflow?: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The dossier editor title bar, absorbed into the Main Editor axiom component
|
|
19
|
+
* (D9): breadcrumb + autosave stamp on the left; the DRAFT/V07 state chip,
|
|
20
|
+
* the COMMENTS toggle (active state amber) that drives the in-editor pane
|
|
21
|
+
* through the "main-editor.comments-open" channel (D6 state passing, not a
|
|
22
|
+
* direct reference), SHARE (a sharing intent the host audits via IAuditChain
|
|
23
|
+
* — the component never audits routine reads), and the ••• overflow on the
|
|
24
|
+
* right. Publishes through setter-only bindings, so the title bar never
|
|
25
|
+
* re-renders from its own publications.
|
|
26
|
+
*/
|
|
27
|
+
export declare function TitleBar({ dossier, openCommentCount, commentsOpen, locale, actions }: TitleBarProps): import("react").JSX.Element;
|
|
28
|
+
//# sourceMappingURL=TitleBar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TitleBar.d.ts","sourceRoot":"","sources":["../src/TitleBar.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAA0B,MAAM,YAAY,CAAC;AAC5E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,mFAAmF;IACnF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,mFAAmF;IACnF,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC1F;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,aAAa,+BAuGnG"}
|
package/dist/TitleBar.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Group, Stack, Text, UnstyledButton } from "@mantine/core";
|
|
3
|
+
import { emitPrismEvent, usePrismStateSetter } from "@zephytiju/prism-react";
|
|
4
|
+
import { formatMessage, stringsForLocale } from "./locales/index.js";
|
|
5
|
+
import { ACCENT, BORDER, CARD, CARD_DARK, LINE, MONO, MUTED, TEXT, WARN } from "./tokens.js";
|
|
6
|
+
const btnBase = {
|
|
7
|
+
height: 34,
|
|
8
|
+
background: CARD,
|
|
9
|
+
border: `1px solid ${BORDER}`,
|
|
10
|
+
borderRadius: 5,
|
|
11
|
+
display: "inline-flex",
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
justifyContent: "center",
|
|
14
|
+
fontFamily: MONO,
|
|
15
|
+
fontSize: 9,
|
|
16
|
+
fontWeight: 500,
|
|
17
|
+
color: TEXT,
|
|
18
|
+
flexShrink: 0,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The dossier editor title bar, absorbed into the Main Editor axiom component
|
|
22
|
+
* (D9): breadcrumb + autosave stamp on the left; the DRAFT/V07 state chip,
|
|
23
|
+
* the COMMENTS toggle (active state amber) that drives the in-editor pane
|
|
24
|
+
* through the "main-editor.comments-open" channel (D6 state passing, not a
|
|
25
|
+
* direct reference), SHARE (a sharing intent the host audits via IAuditChain
|
|
26
|
+
* — the component never audits routine reads), and the ••• overflow on the
|
|
27
|
+
* right. Publishes through setter-only bindings, so the title bar never
|
|
28
|
+
* re-renders from its own publications.
|
|
29
|
+
*/
|
|
30
|
+
export function TitleBar({ dossier, openCommentCount, commentsOpen, locale, actions }) {
|
|
31
|
+
const strings = stringsForLocale(locale);
|
|
32
|
+
const enabled = { comments: true, share: true, overflow: true, ...actions };
|
|
33
|
+
// Setter-only publication: the toggle never re-renders from its own writes;
|
|
34
|
+
// the open label derives from the commentsOpen prop.
|
|
35
|
+
const publishCommentsOpen = usePrismStateSetter("main-editor.comments-open");
|
|
36
|
+
const commentsLabel = commentsOpen
|
|
37
|
+
? formatMessage(strings.commentsButtonWithCount, { count: openCommentCount })
|
|
38
|
+
: strings.commentsButton;
|
|
39
|
+
return (_jsxs(Group, { justify: "space-between", align: "center", gap: 12, wrap: "nowrap", px: "14px 18px", h: 58, style: { background: CARD_DARK, borderBottom: `1px solid ${LINE}`, flexShrink: 0 }, "data-testid": "main-editor-title-bar", children: [_jsxs(Stack, { gap: 3, miw: 0, style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { ff: MONO, fz: 10, fw: 500, style: { color: TEXT, letterSpacing: "0.04em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, "data-testid": "main-editor-breadcrumb", children: dossier.breadcrumb.join(" / ") }), _jsx(Text, { ff: MONO, fz: 8, style: { color: "var(--mantine-color-ok-filled)", letterSpacing: "0.03em" }, "data-testid": "main-editor-autosave", children: `${dossier.docCode} • ${formatMessage(strings.autosavedAt, { time: dossier.autosavedAt })}` })] }), _jsxs(Group, { gap: 10, wrap: "nowrap", children: [enabled.comments ? (_jsxs(UnstyledButton, { type: "button", style: {
|
|
40
|
+
...btnBase,
|
|
41
|
+
width: 148,
|
|
42
|
+
gap: 8,
|
|
43
|
+
...(commentsOpen
|
|
44
|
+
? { border: `1px solid ${WARN}`, color: WARN }
|
|
45
|
+
: {}),
|
|
46
|
+
}, title: "Expand / collapse comments in the editor body", "data-testid": "main-editor-comments-toggle", "aria-pressed": commentsOpen, onClick: () => {
|
|
47
|
+
publishCommentsOpen(!commentsOpen);
|
|
48
|
+
}, children: [_jsx("span", { "aria-hidden": "true", children: "\u2750" }), commentsLabel] })) : null, _jsxs(Box, { w: 132, h: 34, px: 10, style: { background: CARD, border: `1px solid ${BORDER}`, borderRadius: 5, flexShrink: 0 }, "data-testid": "main-editor-state-chip", children: [_jsx(Text, { ff: MONO, fz: 8, fw: 500, c: "accent", style: { paddingTop: 5, letterSpacing: "0.03em" }, "data-testid": "main-editor-state-version", children: `${dossier.draftState} / ${dossier.version}` }), _jsx(Text, { ff: MONO, fz: 7, style: { color: MUTED, letterSpacing: "0.02em" }, "data-testid": "main-editor-open-comments", children: formatMessage(strings.openCommentsCount, { count: openCommentCount }) })] }), enabled.share ? (_jsxs(UnstyledButton, { type: "button", style: { ...btnBase, width: 100, gap: 8, borderColor: ACCENT, color: ACCENT }, "data-testid": "main-editor-share", onClick: () => {
|
|
49
|
+
// Sharing intent (D8): an action request that never grants
|
|
50
|
+
// authorization; the host performs the share and appends the
|
|
51
|
+
// IAuditChain entry. The component audits nothing itself.
|
|
52
|
+
const request = { docId: dossier.id };
|
|
53
|
+
emitPrismEvent("main-editor.share-requested", request);
|
|
54
|
+
}, children: [_jsx("span", { "aria-hidden": "true", children: "\u2197" }), strings.share] })) : null, enabled.overflow ? (_jsx(UnstyledButton, { type: "button", style: { ...btnBase, width: 46, fontFamily: "inherit", fontSize: 14, fontWeight: 600 }, title: strings.overflowTitle, "data-testid": "main-editor-overflow", children: _jsx("span", { "aria-hidden": "true", children: "\u2022\u2022\u2022" }) })) : null] })] }));
|
|
55
|
+
}
|