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