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.
- package/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +585 -2
- package/dist/Editor-CiJY5R4v.mjs +26587 -0
- package/dist/Editor-DT7RkRXf.d.mts +32 -0
- package/dist/LoaderCircle-AGVFE4IP.mjs +19 -0
- package/dist/Skeleton-D922Ths5.mjs +13 -0
- package/dist/constants-D4FTL0ph.mjs +81 -0
- package/dist/fonts/OFL.txt +162 -0
- package/dist/fonts/inter-300-c.woff2 +0 -0
- package/dist/fonts/inter-300-l.woff2 +0 -0
- package/dist/fonts/inter-300i-c.woff2 +0 -0
- package/dist/fonts/inter-300i-l.woff2 +0 -0
- package/dist/fonts/inter-400-c.woff2 +0 -0
- package/dist/fonts/inter-400-l.woff2 +0 -0
- package/dist/fonts/inter-400i-c.woff2 +0 -0
- package/dist/fonts/inter-400i-l.woff2 +0 -0
- package/dist/fonts/inter-600-c.woff2 +0 -0
- package/dist/fonts/inter-600-l.woff2 +0 -0
- package/dist/fonts/inter-600i-c.woff2 +0 -0
- package/dist/fonts/inter-600i-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300i-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-300i-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400i-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400i-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600-l.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600i-c.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-600i-l.woff2 +0 -0
- package/dist/fonts/lora-400-c.woff2 +0 -0
- package/dist/fonts/lora-400-l.woff2 +0 -0
- package/dist/fonts/lora-400i-c.woff2 +0 -0
- package/dist/fonts/lora-400i-l.woff2 +0 -0
- package/dist/fonts/lora-600-c.woff2 +0 -0
- package/dist/fonts/lora-600-l.woff2 +0 -0
- package/dist/fonts/lora-600i-c.woff2 +0 -0
- package/dist/fonts/lora-600i-l.woff2 +0 -0
- package/dist/fonts/lora-700-c.woff2 +0 -0
- package/dist/fonts/lora-700-l.woff2 +0 -0
- package/dist/fonts/lora-700i-c.woff2 +0 -0
- package/dist/fonts/lora-700i-l.woff2 +0 -0
- package/dist/fonts/open-sans-300-c.woff2 +0 -0
- package/dist/fonts/open-sans-300-l.woff2 +0 -0
- package/dist/fonts/open-sans-300i-c.woff2 +0 -0
- package/dist/fonts/open-sans-300i-l.woff2 +0 -0
- package/dist/fonts/open-sans-400-c.woff2 +0 -0
- package/dist/fonts/open-sans-400-l.woff2 +0 -0
- package/dist/fonts/open-sans-400i-c.woff2 +0 -0
- package/dist/fonts/open-sans-400i-l.woff2 +0 -0
- package/dist/fonts/open-sans-600-c.woff2 +0 -0
- package/dist/fonts/open-sans-600-l.woff2 +0 -0
- package/dist/fonts/open-sans-600i-c.woff2 +0 -0
- package/dist/fonts/open-sans-600i-l.woff2 +0 -0
- package/dist/fonts/pt-sans-400-c.woff2 +0 -0
- package/dist/fonts/pt-sans-400-l.woff2 +0 -0
- package/dist/fonts/pt-sans-400i-c.woff2 +0 -0
- package/dist/fonts/pt-sans-400i-l.woff2 +0 -0
- package/dist/fonts/pt-sans-700-c.woff2 +0 -0
- package/dist/fonts/pt-sans-700-l.woff2 +0 -0
- package/dist/fonts/pt-sans-700i-c.woff2 +0 -0
- package/dist/fonts/pt-sans-700i-l.woff2 +0 -0
- package/dist/fonts/pt-serif-400-c.woff2 +0 -0
- package/dist/fonts/pt-serif-400-l.woff2 +0 -0
- package/dist/fonts/pt-serif-400i-c.woff2 +0 -0
- package/dist/fonts/pt-serif-400i-l.woff2 +0 -0
- package/dist/fonts/pt-serif-700-c.woff2 +0 -0
- package/dist/fonts/pt-serif-700-l.woff2 +0 -0
- package/dist/fonts/pt-serif-700i-c.woff2 +0 -0
- package/dist/fonts/pt-serif-700i-l.woff2 +0 -0
- package/dist/index-D6_ysSmQ.d.mts +1075 -0
- package/dist/index-YJcgf8Hh.d.mts +39 -0
- package/dist/index.d.mts +388 -0
- package/dist/index.mjs +641 -0
- package/dist/mermaid/index.d.mts +10 -0
- package/dist/mermaid/index.mjs +252 -0
- package/dist/motion/index.d.mts +8 -0
- package/dist/motion/index.mjs +1009 -0
- package/dist/pdf/index.d.mts +5 -0
- package/dist/pdf/index.mjs +164 -0
- package/dist/pdf/jbig2.wasm +0 -0
- package/dist/pdf/jbig2_nowasm_fallback.js +15 -0
- package/dist/pdf/pdf.worker.min.mjs +28 -0
- package/dist/styles.css +4562 -0
- package/package.json +99 -4
- package/theme.css +128 -0
package/README.md
CHANGED
|
@@ -1,3 +1,586 @@
|
|
|
1
|
-
#
|
|
1
|
+
# docblocks-react-ui
|
|
2
2
|
|
|
3
|
-
|
|
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.
|