docblocks-react-ui 0.0.0-stage → 0.1.1

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/LICENSE +21 -0
  3. package/README.md +592 -2
  4. package/dist/Editor-CiJY5R4v.mjs +26587 -0
  5. package/dist/Editor-DT7RkRXf.d.mts +32 -0
  6. package/dist/LoaderCircle-AGVFE4IP.mjs +19 -0
  7. package/dist/Skeleton-D922Ths5.mjs +13 -0
  8. package/dist/constants-D4FTL0ph.mjs +81 -0
  9. package/dist/fonts/OFL.txt +162 -0
  10. package/dist/fonts/inter-300-c.woff2 +0 -0
  11. package/dist/fonts/inter-300-l.woff2 +0 -0
  12. package/dist/fonts/inter-300i-c.woff2 +0 -0
  13. package/dist/fonts/inter-300i-l.woff2 +0 -0
  14. package/dist/fonts/inter-400-c.woff2 +0 -0
  15. package/dist/fonts/inter-400-l.woff2 +0 -0
  16. package/dist/fonts/inter-400i-c.woff2 +0 -0
  17. package/dist/fonts/inter-400i-l.woff2 +0 -0
  18. package/dist/fonts/inter-600-c.woff2 +0 -0
  19. package/dist/fonts/inter-600-l.woff2 +0 -0
  20. package/dist/fonts/inter-600i-c.woff2 +0 -0
  21. package/dist/fonts/inter-600i-l.woff2 +0 -0
  22. package/dist/fonts/jetbrains-mono-300-c.woff2 +0 -0
  23. package/dist/fonts/jetbrains-mono-300-l.woff2 +0 -0
  24. package/dist/fonts/jetbrains-mono-300i-c.woff2 +0 -0
  25. package/dist/fonts/jetbrains-mono-300i-l.woff2 +0 -0
  26. package/dist/fonts/jetbrains-mono-400-c.woff2 +0 -0
  27. package/dist/fonts/jetbrains-mono-400-l.woff2 +0 -0
  28. package/dist/fonts/jetbrains-mono-400i-c.woff2 +0 -0
  29. package/dist/fonts/jetbrains-mono-400i-l.woff2 +0 -0
  30. package/dist/fonts/jetbrains-mono-600-c.woff2 +0 -0
  31. package/dist/fonts/jetbrains-mono-600-l.woff2 +0 -0
  32. package/dist/fonts/jetbrains-mono-600i-c.woff2 +0 -0
  33. package/dist/fonts/jetbrains-mono-600i-l.woff2 +0 -0
  34. package/dist/fonts/lora-400-c.woff2 +0 -0
  35. package/dist/fonts/lora-400-l.woff2 +0 -0
  36. package/dist/fonts/lora-400i-c.woff2 +0 -0
  37. package/dist/fonts/lora-400i-l.woff2 +0 -0
  38. package/dist/fonts/lora-600-c.woff2 +0 -0
  39. package/dist/fonts/lora-600-l.woff2 +0 -0
  40. package/dist/fonts/lora-600i-c.woff2 +0 -0
  41. package/dist/fonts/lora-600i-l.woff2 +0 -0
  42. package/dist/fonts/lora-700-c.woff2 +0 -0
  43. package/dist/fonts/lora-700-l.woff2 +0 -0
  44. package/dist/fonts/lora-700i-c.woff2 +0 -0
  45. package/dist/fonts/lora-700i-l.woff2 +0 -0
  46. package/dist/fonts/open-sans-300-c.woff2 +0 -0
  47. package/dist/fonts/open-sans-300-l.woff2 +0 -0
  48. package/dist/fonts/open-sans-300i-c.woff2 +0 -0
  49. package/dist/fonts/open-sans-300i-l.woff2 +0 -0
  50. package/dist/fonts/open-sans-400-c.woff2 +0 -0
  51. package/dist/fonts/open-sans-400-l.woff2 +0 -0
  52. package/dist/fonts/open-sans-400i-c.woff2 +0 -0
  53. package/dist/fonts/open-sans-400i-l.woff2 +0 -0
  54. package/dist/fonts/open-sans-600-c.woff2 +0 -0
  55. package/dist/fonts/open-sans-600-l.woff2 +0 -0
  56. package/dist/fonts/open-sans-600i-c.woff2 +0 -0
  57. package/dist/fonts/open-sans-600i-l.woff2 +0 -0
  58. package/dist/fonts/pt-sans-400-c.woff2 +0 -0
  59. package/dist/fonts/pt-sans-400-l.woff2 +0 -0
  60. package/dist/fonts/pt-sans-400i-c.woff2 +0 -0
  61. package/dist/fonts/pt-sans-400i-l.woff2 +0 -0
  62. package/dist/fonts/pt-sans-700-c.woff2 +0 -0
  63. package/dist/fonts/pt-sans-700-l.woff2 +0 -0
  64. package/dist/fonts/pt-sans-700i-c.woff2 +0 -0
  65. package/dist/fonts/pt-sans-700i-l.woff2 +0 -0
  66. package/dist/fonts/pt-serif-400-c.woff2 +0 -0
  67. package/dist/fonts/pt-serif-400-l.woff2 +0 -0
  68. package/dist/fonts/pt-serif-400i-c.woff2 +0 -0
  69. package/dist/fonts/pt-serif-400i-l.woff2 +0 -0
  70. package/dist/fonts/pt-serif-700-c.woff2 +0 -0
  71. package/dist/fonts/pt-serif-700-l.woff2 +0 -0
  72. package/dist/fonts/pt-serif-700i-c.woff2 +0 -0
  73. package/dist/fonts/pt-serif-700i-l.woff2 +0 -0
  74. package/dist/index-D6_ysSmQ.d.mts +1075 -0
  75. package/dist/index-YJcgf8Hh.d.mts +39 -0
  76. package/dist/index.d.mts +388 -0
  77. package/dist/index.mjs +641 -0
  78. package/dist/mermaid/index.d.mts +10 -0
  79. package/dist/mermaid/index.mjs +252 -0
  80. package/dist/motion/index.d.mts +8 -0
  81. package/dist/motion/index.mjs +1009 -0
  82. package/dist/pdf/index.d.mts +5 -0
  83. package/dist/pdf/index.mjs +164 -0
  84. package/dist/pdf/jbig2.wasm +0 -0
  85. package/dist/pdf/jbig2_nowasm_fallback.js +15 -0
  86. package/dist/pdf/pdf.worker.min.mjs +28 -0
  87. package/dist/styles.css +4562 -0
  88. package/package.json +99 -4
  89. package/theme.css +128 -0
package/README.md CHANGED
@@ -1,3 +1,593 @@
1
- # Temporary Holding Version
1
+ # docblocks-react-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React rendering and editing layer for the docblocks headless document
4
+ engine. The data model, the editing commands, and the sync live in
5
+ [`docblocks-core`](https://www.npmjs.com/package/docblocks-core); this
6
+ package renders the document and provides the editing surface
7
+ (caret/selection, keyboard, clipboard, block renderers, presence overlay).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm i docblocks-react-ui docblocks-core
13
+ ```
14
+
15
+ Peer dependencies: `docblocks-core`, `react` (19), `react-dom`. Optional
16
+ peers, used only by their subpaths: `framer-motion` (`./motion`), `mermaid`
17
+ (`./mermaid`), `pdfjs-dist` (`./pdf`). The package's own runtime dependencies
18
+ (zustand, the Radix primitives, the shared ui runtime set) are declared, so
19
+ a plain `npm i` is the whole install.
20
+
21
+ The package is ESM-only (`import`, not `require`); it stands Node
22
+ `^22.22.2 || ^24.15.0 || >=26.0.0` (the `engines` field).
23
+
24
+ ## Quick start
25
+
26
+ The minimum is a core `Doc` wrapped in an `EditorProvider` and the
27
+ stylesheet import:
28
+
29
+ ```tsx
30
+ import { DocHeader, Editor, EditorProvider } from "docblocks-react-ui";
31
+ import "docblocks-react-ui/styles.css";
32
+
33
+ import { useState } from "react";
34
+
35
+ import { createDoc } from "docblocks-core";
36
+
37
+ const doc = createDoc({ id: "my-doc-id" });
38
+
39
+ function App() {
40
+ const [view, setView] = useState<"page" | "canvas">("page");
41
+ return (
42
+ <>
43
+ <DocHeader doc={doc} view={view} onViewChange={setView} />
44
+ <EditorProvider doc={doc}>
45
+ <Editor view={view} />
46
+ </EditorProvider>
47
+ </>
48
+ );
49
+ }
50
+ ```
51
+
52
+ The `styles.css` side-effect import stands the standard bundler's ambient
53
+ `*.css` module declaration (shipped with the Vite / CRA / Next.js templates);
54
+ a bare `tsc`-only setup needs a one-line `declare module "*.css"` shim.
55
+
56
+ - `EditorProvider` owns the per-instance editor state (the store projection
57
+ of the doc, the selection view, the subscriptions). Multiple providers may
58
+ coexist, each with its own doc.
59
+ - `Editor` renders the entry list. `view: "page"` (default) is the page;
60
+ `view: "canvas"` renders the page miniature plus the root-level surfaces'
61
+ elements on the canvas.
62
+ - `DocHeader` is a consumer-placed panel rendered in the consumer's chrome
63
+ (outside the editor, so it takes the core `Doc` directly): the
64
+ page/canvas switcher, the document mode's switcher (Edit / View /
65
+ Published — consumer-controlled over the `mode` + `onModeChange` props,
66
+ rendered over the `editor`'s access level and the callback's presence;
67
+ the `Published` option is disabled while the `publishedDoc` prop is
68
+ absent), the avatar, and the title — inline-editable in the edit mode
69
+ only (the read-only modes stand the title/avatar display-only), the
70
+ title/avatar commits are one core command each (one undo step each),
71
+ the Escape cancels without a write. Over the published's stand the
72
+ header stands the snapshot's meta (the live's meta stands off — the
73
+ `viewer`'s `doc` is the snapshot itself; the no-snapshot stand renders
74
+ the neutral meta, never the live one). It
75
+ stands the standard theme tokens
76
+ (not the `--docblocks-*` ones), since it renders outside the editor root.
77
+ - The `Doc` holds the document state, and the package holds no
78
+ persistence — how to store and restore it: the Persistence section
79
+ below.
80
+
81
+ ## The consumer contract
82
+
83
+ Everything the editor needs from the consumer is injected as an
84
+ `EditorProvider` prop; every prop except `doc` is optional. The only
85
+ built-in default is the blob store (a shared in-memory stand — the
86
+ uploads resolve as data urls for the JS session, cleared on reload); the
87
+ other props degrade gracefully over their absence (the external-form
88
+ database block stands the not-connected placeholder and the `/database`
89
+ slash command is hidden without a bridge; the `sync-embed` block stands
90
+ its broken state without a resolver; the `@` stays plain text without a
91
+ mention catalog). The full props list:
92
+
93
+ | Prop | Contract |
94
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `doc` | The core `Doc` to render. Must not change after mount (a throw on change is the contract). Required for the `editor` stand; over the `viewer`'s level the doc IS the published snapshot (the security model — the reader's browser never receives the live document), and an absent stand renders the "not published yet" placeholder. |
96
+ | `provider?` | A core `SyncProvider` for the wire + presence. Without it the editor is a single-client document (local edits stand, nothing syncs). |
97
+ | `blockRenderers?` | Per-type renderer overrides (see below). |
98
+ | `blobStore?` | The blob bridge for image / pdf / file blocks (see below). |
99
+ | `blobResolver?` | Legacy view-only resolver (key → src string). Wrapped as a store: no upload, subtype `image`. Prefer `blobStore`. |
100
+ | `databaseBridge?` | The table operations behind external-form database blocks (see below). |
101
+ | `syncEmbedResolver?` | `(ref) => Doc \| null` — resolves a `sync-embed` block's ref to the master doc (see below). |
102
+ | `pdfPreview?` | A first-page preview component for `pdf`-subtype image blocks (see below). |
103
+ | `mentionProvider?` | The `@` mention picker's search catalog (see below). |
104
+ | `rootBlockId?` | Render the entry list of this block instead of the document root (the subtree view). Must not change after mount. |
105
+ | `commandPalettePlaceholder?` | The empty-block hint text (default `Type '/' for commands`). |
106
+ | `accessLevel?` | The consumer's user access level (default `editor`): `viewer` forces the `published` mode (the current state is never rendered to a reader). |
107
+ | `mode?` | The document mode (default `edit`): `view` is a read-only live document; `published` renders the `publishedDoc` snapshot read-only. |
108
+ | `publishedDoc?` | The published snapshot (a hydrated core `Doc`; must not change after mount — remount over `key` for a new snapshot). The consumer owns the publish action and the snapshot storage. |
109
+
110
+ Two rules for the optional props:
111
+
112
+ - **Stable references.** The values are read into the provider context per
113
+ render; a fresh inline object re-creates the context value on every
114
+ render and re-renders every context consumer. Module-level or memoized
115
+ values are the contract.
116
+ - **Nested inheritance.** A provider nested inside another inherits the
117
+ enclosing renderers, resolvers, and bridges when it supplies none of its
118
+ own — a sync-embed's nested editor resolves its own content against the
119
+ enclosing catalog.
120
+
121
+ ### Persistence
122
+
123
+ The package holds no persistence — the document state lives in the core
124
+ `Doc`, and the snapshot / update storage is a consumer-side concern (the
125
+ non-goal list in the core README). The canonical loop: restore the doc
126
+ from your stored state, and save it on every transaction:
127
+
128
+ ```tsx
129
+ import { useEffect, useState } from "react";
130
+
131
+ import { loadDoc } from "docblocks-core";
132
+
133
+ function useDocument(id: string) {
134
+ // Restore: loadDoc over your stored encoded state (fromJSON over a
135
+ // stored JSON snapshot).
136
+ const [doc] = useState(() => loadDoc(readStoredState(id)));
137
+ useEffect(() => {
138
+ // onChange fires for every transaction (local and remote) — encode
139
+ // the full state after each and hand it to your storage.
140
+ const off = doc.onChange(() => {
141
+ writeStoredState(id, doc.encode());
142
+ });
143
+ return off;
144
+ }, [doc]);
145
+ return doc;
146
+ }
147
+ ```
148
+
149
+ (`doc.toJSON()` / `fromJSON` is the JSON path, `doc.encode()` /
150
+ `loadDoc` the encoded path — the core README's Doc section; a consumer
151
+ that stores incremental updates can encode them with
152
+ `doc.encodeUpdate(stateVector)`.)
153
+
154
+ ### Access modes and publishing
155
+
156
+ The access level and the document mode are consumer session state (never
157
+ document data, never on the wire):
158
+
159
+ ```tsx
160
+ // The editor's stand: the live document + the publish's snapshot.
161
+ <EditorProvider doc={doc} mode={mode} accessLevel={role} publishedDoc={snapshot}>
162
+ <Editor />
163
+ </EditorProvider>
164
+
165
+ // The viewer's stand (the security model): the published snapshot is the
166
+ // only document the reader's browser receives — pass it as `doc`.
167
+ <EditorProvider doc={snapshot} accessLevel="viewer">
168
+ <Editor />
169
+ </EditorProvider>
170
+ ```
171
+
172
+ - `accessLevel: "viewer" | "editor"` (default `editor`) decides which
173
+ modes are available: a `viewer` is always in the `published` mode (the
174
+ current state is never rendered to a reader); an `editor` switches
175
+ freely.
176
+ - **The security model — the reader never receives the live document.**
177
+ The consumer hands the viewer exactly the minimum needed to render the
178
+ published state: the published snapshot as `doc` and nothing else (no
179
+ live document, no sync transport, no `publishedDoc` — over the viewer
180
+ the `doc` is the snapshot). A document that was never published is no
181
+ stand at all: the provider renders the "not published yet" placeholder
182
+ over the absent `doc`.
183
+ - `mode: "edit" | "view" | "published"` (default `edit`): `edit` is the
184
+ full editor over the live document; `view` is a read-only render of the
185
+ live document (it keeps syncing — the state stays current); `published`
186
+ renders the `publishedDoc` snapshot read-only (a nested read-only editor
187
+ over the snapshot: its own selection, no sync transport, the snapshot
188
+ stands static over the live edits).
189
+ - A published mode without a `publishedDoc` renders the
190
+ "not published yet" placeholder in place of the content (the current
191
+ state is not shown).
192
+ - Over the read-only modes the editing surfaces are off (the text inners
193
+ are not editable, the gutter / drag / hotkeys / format toolbar / the
194
+ table and database editing chrome / the canvas sessions and dock stand
195
+ off); the read-only gestures keep (scroll, the embed pan / zoom, the
196
+ canvas wheel zoom / pan). Consecutive empty text blocks render as one
197
+ over the read-only modes (the document data is untouched).
198
+ - Publishing is a consumer action: snapshot the doc state (`doc.encode()`
199
+ or `doc.toJSON()`), persist it where the reader loads it from, and
200
+ hydrate it (`loadDoc` / `fromJSON`) — for the `editor`'s `published`
201
+ mode into the `publishedDoc` prop, for the `viewer`'s stand as the `doc`
202
+ itself. The package ships no publish API and stores no publish state; the
203
+ external database form's rows live in the consumer's table (they are not
204
+ part of the snapshot — the inline form is document data and is). A new
205
+ snapshot is a remount (pass it under a changing `key`).
206
+ - The `DocHeader` stands over the viewer's stand too: pass the snapshot as
207
+ its `doc` (the viewer's switcher stays off — the `accessLevel` stands no
208
+ `mode` stand over the `viewer` level).
209
+
210
+ ### Sync and presence
211
+
212
+ The transport is a core concern: create the doc, build a provider, attach
213
+ them, and pass both to the provider:
214
+
215
+ ```tsx
216
+ import { Editor, EditorProvider } from "docblocks-react-ui";
217
+
218
+ import { useEffect, useMemo } from "react";
219
+
220
+ import { WsSyncProvider, attach, createDoc } from "docblocks-core";
221
+
222
+ const doc = createDoc({ id: "room-1" }); // or restore it (the Persistence section)
223
+
224
+ function App() {
225
+ const provider = useMemo(() => new WsSyncProvider("ws://my-server:5199/room-1"), []);
226
+ useEffect(() => {
227
+ void provider.connect();
228
+ const session = attach(doc, provider);
229
+ return () => {
230
+ session.detach(); // detach before the transport closes
231
+ provider.disconnect();
232
+ };
233
+ }, [doc, provider]);
234
+ return (
235
+ <EditorProvider doc={doc} provider={provider}>
236
+ <Editor />
237
+ </EditorProvider>
238
+ );
239
+ }
240
+ ```
241
+
242
+ - `attach` bootstraps the session (it sends the full local state first,
243
+ then routes local transactions out and remote updates in); detach it on
244
+ unmount.
245
+ - `WsSyncProvider` speaks standard y-protocols over WebSocket (the
246
+ y-websocket wire, wire-compatible with a standard y-websocket server);
247
+ `options: { token?, webSocketImpl? }`. Any object implementing the core
248
+ `SyncProvider` interface (connect / disconnect / sendUpdate / onUpdate /
249
+ getLocalStateVector / setAwareness / onAwareness) works in its place.
250
+ - Presence rides the provider's awareness channel: the editor publishes its
251
+ own selection automatically (debounced) and renders the remote peers'
252
+ carets. `usePresence()` reads the per-peer state (peer id, selection,
253
+ `updatedAt`); staleness (dropping stale peers) is a consumer concern —
254
+ the payload's `updatedAt` (Unix ms) is the only input.
255
+
256
+ ### Blobs
257
+
258
+ The document holds only an opaque blob key (`image.src`); the bytes live
259
+ where the consumer stored them. `BlobStore` (the `blobStore` prop) is the
260
+ only path key → url and file → key, in both directions:
261
+
262
+ ```ts
263
+ interface BlobStore {
264
+ resolve(key: string): Promise<{
265
+ url: string;
266
+ meta: { subtype: "image" | "pdf" | "file"; name?: string; type?: string; size?: number };
267
+ }>;
268
+ upload(file: File | Blob, meta?: Partial<BlobMeta>): Promise<string>;
269
+ remove?(key: string): Promise<void>; // optional gc
270
+ }
271
+ ```
272
+
273
+ - `resolve` rejects an unknown key — the block stands its structural
274
+ placeholder card until the promise resolves.
275
+ - The UI renders from `meta.subtype` and never guesses: `image` is an
276
+ `<img>`, `pdf` the first-page preview (the `./pdf` subpath, or the native
277
+ iframe without it), `file` the download card. The consumer decides the
278
+ subtype on both directions (`upload` may override the mime-derived
279
+ default via `meta.subtype`).
280
+ - Without `blobStore` / `blobResolver`, the editor uses a shared
281
+ in-memory store (uploads stand data urls for the JS session, cleared on
282
+ reload), so the upload path works out of the box. `createMemoryBlobStore`
283
+ and `blobSubtypeOf` are exported for consumers who want the same
284
+ behavior under their own prop.
285
+
286
+ ### Database
287
+
288
+ The `database` block has two forms. The **inline** form keeps its columns
289
+ and rows in the document. The **external** form keeps only a `tableId`
290
+ reference; the schema and the rows live in the consumer's database behind
291
+ the `DatabaseBridge` (the `databaseBridge` prop) — all methods are async,
292
+ errors reject as the structured `BridgeError` (the closed `code`
293
+ vocabulary — `BRIDGE_ERROR_CODES` — + the `message` + the optional
294
+ `details`), and the package neither swallows nor
295
+ retries:
296
+
297
+ - protocol: `describeProtocol` (the version + the capabilities + the
298
+ limits) — the client negotiates it before any use; a version /
299
+ baseline-capability mismatch stands the `protocol_mismatch`, not a
300
+ silent downgrade
301
+ - catalog: `listTables`, `createTable`, `renameTable`, `describeTable`
302
+ - schema: `createColumn`, `updateColumn`, `renameColumn` (the `name` key
303
+ of `updateColumn`), `deleteColumn`, `restoreDeletedColumn` (the
304
+ soft-delete posture)
305
+ - reads: `snapshotTable` (the sole initial load: the schema + the first
306
+ page from one read), `queryNextPage` (the opaque keyset cursor — the
307
+ cursor is signed by the server), `resolveRowDisplay` (the row-link
308
+ cells' join)
309
+ - writes: the atomic mixed `batchRows` (the ordered `ops[]` commit as one
310
+ transaction — the insert / update / remove / move / append stand the
311
+ op's kinds), `undeleteRows` (one atomic operation, independent of the
312
+ batch) — every mutation carries the `MutationContext` (the
313
+ client-issued `operationId` idempotency key + the issue time + the
314
+ optional CAS `expectedRevision` — its absence selects the LWW default)
315
+ - live feed: `subscribeTable(tableId, { afterSeq }, observer)` → the
316
+ handle — the push channel (the consumer's transport: WS / SSE /
317
+ polling); the observer stands the `commit` envelope (one transaction →
318
+ one `seq` / `prevSeq` over the append-only per-table stream) and the
319
+ `invalidate` (the table's stream re-anchors — the grid refetches over
320
+ the alive subscription); the bootstrap's replay covers the
321
+ snapshot → subscribe gap (the `afterSeq` handoff)
322
+ - reconciliation: `getOperationStatus(operationId)` — the lost-response
323
+ stand (the committed receipt or the pending)
324
+
325
+ Without a bridge, external-form database blocks render a "database not
326
+ connected" placeholder and the `/database` slash command is hidden; the
327
+ inline-form database block (columns + rows stored in the document) renders
328
+ and edits without a bridge. Supply your own `DatabaseBridge` implementation
329
+ for external-form tables. The bridge-side helpers are exported from the
330
+ package root: `BridgeError` (the structured error type) with
331
+ `toBridgeError` / `isBridgeError` / `isBridgeErrorCode`, `newMutationContext`
332
+ (the client-issued `operationId` per op), `PROTOCOL_VERSION` /
333
+ `BASELINE_CAPABILITIES` / `validateProtocolDescriptor` (the negotiation),
334
+ `isDatabaseBridge`, and `useDatabaseTable(bridge, tableId, filter?)` — the
335
+ grid controller for a consumer-owned grid over the bridge.
336
+
337
+ ### Sync-embed
338
+
339
+ A `sync-embed` block references another document by an opaque ref. The
340
+ consumer holds the master docs and resolves them:
341
+
342
+ ```tsx
343
+ const syncEmbedResolver = (ref: SyncEmbedRef): Doc | null => (ref.docId === "master-1" ? masterDoc1 : null);
344
+
345
+ <EditorProvider doc={doc} syncEmbedResolver={syncEmbedResolver}>
346
+ <Editor />
347
+ </EditorProvider>;
348
+ ```
349
+
350
+ The renderer mounts a nested `EditorProvider` over the master (subtree
351
+ mode), so the embedded content is a fully editable doc with its own
352
+ selection and undo. `null` renders the broken state; cycles are guarded by
353
+ the ancestor-doc chain (a doc cannot embed itself transitively).
354
+
355
+ ### Block renderers
356
+
357
+ `blockRenderers` maps a block type to a consumer component that replaces
358
+ the built-in renderer for that type:
359
+
360
+ ```tsx
361
+ import type { BlockRendererMap } from "docblocks-react-ui";
362
+
363
+ const blockRenderers: BlockRendererMap = {
364
+ divider: ({ id, block }) => <div data-block-id={id} data-block-type="divider" className="my-divider" />,
365
+ };
366
+
367
+ <EditorProvider doc={doc} blockRenderers={blockRenderers}>
368
+ <Editor />
369
+ </EditorProvider>;
370
+ ```
371
+
372
+ - A type without an entry keeps its built-in renderer; a type with neither
373
+ renders the placeholder (future block types keep working).
374
+ - Replacing a text-editable type replaces the whole block view, including
375
+ the contenteditable and its editing affordance — a renderer that needs
376
+ editing drives the core commands through the hooks (`useDoc`,
377
+ `useText`, `useSelectionView`, …).
378
+ - Custom renderers should stamp `data-block-id` / `data-block-type` on their
379
+ root: the drag and selection gestures key off these attributes.
380
+
381
+ ### PDF preview
382
+
383
+ `pdfPreview` is a component that renders a `pdf`-subtype image block's
384
+ resolved blob as a client-side first-page preview. The `./pdf` subpath
385
+ provides one over `pdfjs-dist` (an optional peer):
386
+
387
+ ```tsx
388
+ import { PdfFirstPage } from "docblocks-react-ui/pdf";
389
+
390
+ <EditorProvider doc={doc} pdfPreview={PdfFirstPage}>
391
+ <Editor />
392
+ </EditorProvider>;
393
+ ```
394
+
395
+ The renderer receives `{ id, url, name }` (`id` is the block id, `url` the
396
+ resolved blob url, `name` the blob's display name) and should stamp
397
+ `data-block-id={id} data-block-type="image"` on its rendered root, like the
398
+ other block renderers. Without the prop a `pdf`-subtype block stands the
399
+ native iframe viewer; a failed preview render falls back to it, so the
400
+ document always stays viewable.
401
+
402
+ ### Mentions
403
+
404
+ The `mentionProvider` prop powers the `@` mention picker: typing `@` at a
405
+ word boundary in a page text block (paragraph, heading, list item, callout,
406
+ quote — a code block keeps `@` as plain text) opens a two-section picker
407
+ (people and documents) anchored at the `@`. The provider searches its own
408
+ catalog and returns `{ id, label }` entries:
409
+
410
+ ```ts
411
+ const mentionProvider = {
412
+ searchUsers: async (query: string) => [{ id: "u1", label: "Ada Lovelace" }],
413
+ searchDocs: async (query: string) => [{ id: "d1", label: "API Reference" }],
414
+ };
415
+ ```
416
+
417
+ A pick replaces the `@query` phrase with the label and stands the `mention`
418
+ mark (the `kind` `"user"` or `"doc"`, the `id`, and the `label` snapshot) in
419
+ one transaction — one undo step. The mention renders as an atomic,
420
+ non-editable chip; copying it stands the `@label` plain text. Without the
421
+ prop the `@` is plain text (no picker). The mark is non-exclusive (it
422
+ composes with the other marks).
423
+
424
+ ### Canvas
425
+
426
+ `view: "canvas"` renders the page miniature plus the document's
427
+ root-level `surface` blocks — the shapes, the connectors, the frames,
428
+ and the canvas texts. The canvas content is document data (the `surface`
429
+ block with its child elements, each optionally carrying the `geometry`
430
+ — `x` / `y` / `w` / `h`), created in the editor (the bottom dock's
431
+ insert menu, the drag / resize / nudge / fit / draw sessions) or
432
+ programmatically through the core's block commands. The viewport's zoom
433
+ and pan are view state — they never reach the document.
434
+
435
+ ### Subtree view
436
+
437
+ `rootBlockId` renders the entry list of one block instead of the document
438
+ root (the block itself plus its own subtree) — the editor renders the
439
+ subtree as its own page:
440
+
441
+ ```tsx
442
+ <EditorProvider doc={doc} rootBlockId={someBlockId}>
443
+ <Editor />
444
+ </EditorProvider>
445
+ ```
446
+
447
+ ### Hooks
448
+
449
+ The editor state is read through the hooks over the nearest
450
+ `EditorProvider`; every hook re-renders its consumer only when the read
451
+ value actually changes (the store's reference-stability contract):
452
+
453
+ - `useDoc()` — the core `Doc`.
454
+ - `useBlock(id)` — the block snapshot (`undefined` when the block is
455
+ missing, e.g. removed by a peer).
456
+ - `useText(id)` — the block's flattened text.
457
+ - `useSelection()` — the local view selection (the caret / range / block
458
+ selection).
459
+ - `useSelectionView()` — the selection view (set the selection, read the
460
+ local state; `null` before the mount effect ran and after unmount).
461
+ - `usePresence()` — the remote peers' ephemeral state (the selection +
462
+ the `updatedAt` per peer).
463
+ - `useMeta()` — the document meta (the reference is replaced only when
464
+ the title changed).
465
+ - `useEditorContext()` — the whole provider context (the store, the
466
+ resolvers, the access stand).
467
+ - `useDatabaseTable(bridge, tableId, filter?)` — the external database
468
+ block's grid controller (a consumer-owned grid over the bridge).
469
+
470
+ ### The `Editor` props
471
+
472
+ | Prop | Contract |
473
+ | --------------- | ------------------------------------------------------------------------ |
474
+ | `view` | `"page"` (default) or `"canvas"` — the rendering the consumer switches |
475
+ | `formatPalette` | The format toolbar's color swatches (default `FORMAT_PALETTE`, exported) |
476
+
477
+ `MotionEditor` (the `./motion` subpath) accepts the same props and adds the
478
+ layout animations and drag-reorder (requires `framer-motion`).
479
+
480
+ ## Subpath exports
481
+
482
+ | Subpath | What it is |
483
+ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
484
+ | `docblocks-react-ui` | The editor, the provider, the hooks, the primitives (the entry) |
485
+ | `docblocks-react-ui/styles.css` | The compiled stylesheet: the utility layer + the token seed + the `--docblocks-*` mapping + the structural rules |
486
+ | `docblocks-react-ui/theme.css` | The pure token seed (standard tokens + the radius scale), no utility layer |
487
+ | `docblocks-react-ui/motion` | `MotionEditor` (layout animations, insert/remove, drag-reorder; peer: `framer-motion`) |
488
+ | `docblocks-react-ui/mermaid` | The `mermaid` renderer above `code { language: "mermaid" }` blocks (peer: `mermaid`) |
489
+ | `docblocks-react-ui/pdf` | `PdfFirstPage` (the first-page preview over `pdfjs-dist`; the worker is copied to `dist/pdf/` at build time) |
490
+
491
+ The base entry imports none of the optional peers, so consumers who skip a
492
+ subpath ship no extra weight.
493
+
494
+ ## Theming
495
+
496
+ The editor carries no theme values in its component code: every theme-relevant
497
+ value — colors, radius, fonts, tints — is read from a CSS custom property
498
+ (a handful of structural micro-values — the drag-item shadow, small fixed
499
+ radii — are plain CSS). The token model is two-layer:
500
+
501
+ 1. **Standard tokens (input).** The editor reads the standard shadcn tokens
502
+ — `--foreground`, `--primary`, `--primary-foreground`, `--muted`,
503
+ `--muted-foreground`, `--accent`, `--destructive`, `--border`, `--card`,
504
+ `--popover`, `--radius`, `--font-sans`, `--font-mono` — when your
505
+ application defines them on `:root` / `.dark` (your global stylesheet or
506
+ a Tailwind v4 theme). All of them are optional.
507
+ 2. **`--docblocks-*` tokens (mapping).** Each reads its standard counterpart
508
+ with the shipped default as the fallback, so an app without a theme
509
+ renders the unthemed look. The mapping is declared on the editor root
510
+ element, which is what lets the editor follow your theme in both light
511
+ and dark.
512
+
513
+ So a shadcn-based app needs no extra work: the editor follows the tokens
514
+ your app already defines, and the `dark` class on `<html>` flips it with
515
+ the rest of the app.
516
+
517
+ For apps without a theme, the shipped stylesheet carries the standard
518
+ token seed (light + the `dark` set), so the single import above renders
519
+ the editor and the primitives with the canonical shadcn palette. The
520
+ pure seed (without the utility layer) also ships as
521
+ `docblocks-react-ui/theme.css` — import it for the tokens alone, or copy
522
+ its values into your own theme.
523
+
524
+ **Fonts.** The default UI font (the editor's chrome + the page text) is the
525
+ self-hosted `Inter` — the `--docblocks-font-family` token reads `--font-sans`
526
+ first, so a `--font-sans` you define overrides it. The mono font (the code
527
+ block) — the `--docblocks-mono-font-family` token — defaults to the
528
+ system mono and reads `--font-mono` first. The package's self-hosted font set
529
+ (the UI font, the mono default, and the shape-text font options) is listed in
530
+ the `fonts` registry (`UI_FONT_ID` / `UI_FONT_STACK`, `MONO_FONT_STACK`,
531
+ `SHAPE_FONT_OPTIONS`).
532
+
533
+ The self-hosted font set (Inter, JetBrains Mono, Lora, Open Sans, PT Sans,
534
+ PT Serif) is licensed under the SIL Open Font License 1.1; the license text
535
+ ships with the fonts in `dist/fonts/OFL.txt`.
536
+
537
+ ### Design system primitives
538
+
539
+ The package's design system primitives (Button, Checkbox, Card, Alert, … —
540
+ exported from the package entry) are styled with Tailwind utility classes.
541
+ Tailwind is a build-time concern, so the package compiles the utilities
542
+ itself and ships the result inside `styles.css` (the utility layer, plain
543
+ CSS) — no Tailwind tooling is needed on the consumer side. Three notes:
544
+
545
+ - The preflight lives in `@layer base`, so any unlayered consumer CSS
546
+ wins over it (the `--docblocks-*` mapping and the structural rules own
547
+ the editor area).
548
+ - The `dark:` utilities follow the `dark` class (any ancestor, the shadcn
549
+ convention), not the `prefers-color-scheme` media query.
550
+ - The primitives reference the `z-modal` stacking utility, which is
551
+ consumer-side: define it yourself (for example
552
+ `@utility z-modal { z-index: var(--z-modal) }`); without it the popup
553
+ content simply carries no z-index.
554
+
555
+ The seed section is the package's DEFAULT for the standard tokens, not a
556
+ fixed value: if you define them yourself, declare your definitions after
557
+ the `styles.css` import (same specificity — the later declaration wins) or
558
+ on a wrapper around the editor (an element declaration beats the inherited
559
+ `:root` value for everything inside it).
560
+
561
+ ### Overriding individual values
562
+
563
+ Override a `--docblocks-*` token on a selector that matches the editor
564
+ element (or a higher-specificity wrapper), for example:
565
+
566
+ ```css
567
+ [data-slot="docblocks/editor"] {
568
+ --docblocks-radius: 10px;
569
+ --docblocks-link-color: #0b6;
570
+ }
571
+ ```
572
+
573
+ Tokens without a standard counterpart (`--docblocks-link-color`,
574
+ `--docblocks-selection-background`, the callout warning colors, the mention
575
+ chip colors, shadows, sizes, and the presence tokens) are package defaults
576
+ and override the same way.
577
+
578
+ The color palette's default swatches read the `--docblocks-palette-*`
579
+ tokens (red, orange, yellow, green, cyan, blue, violet) plus the theme's
580
+ `--background` token. Unlike the editor-scoped tokens, these stand on
581
+ `:root` / `.dark` (the seed ships a brighter light set and a more muted
582
+ dark set, so the swatch follows the theme flip). Override them on
583
+ `:root` / `.dark`, or pass the editor's `formatPalette` prop (a `string[]`
584
+ of hex or `var(…)` entries) to replace the swatches outright; a picked
585
+ swatch always commits a concrete hex to the document.
586
+
587
+ ### Structure
588
+
589
+ Structural styling hooks are `data-slot` attributes (`docblocks/editor`,
590
+ `docblocks/block`, `docblocks/block-content`, `docblocks/quote-rule`, …) plus
591
+ `data-block-type`, `data-list`, `data-depth`, and the per-state attributes — see
592
+ the stylesheet header for the full list. User content styling (run color /
593
+ highlight) is document data, not theme, and is applied inline.