docblocks-react-ui 0.2.0 → 0.3.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.
- package/CHANGELOG.md +39 -0
- package/README.md +65 -1
- package/dist/{Editor-YAlRhGNW.mjs → Editor-BvKqUGXH.mjs} +391 -259
- package/dist/{index-DOKS86aH.d.mts → index-DG0lJy_t.d.mts} +93 -2
- package/dist/index.d.mts +12 -4
- package/dist/index.mjs +16 -12
- package/dist/mermaid/index.mjs +1 -1
- package/dist/pdf/index.d.mts +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# docblocks-react-ui
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- bb5df92: The standalone `DatabaseTable` now anchors the package's `--docblocks-*`
|
|
8
|
+
token scope over its own root (the `data-slot="docblocks/editor"` stand).
|
|
9
|
+
The token mapping is declared on the editor root over the shipped CSS,
|
|
10
|
+
and the standalone table renders outside any document — without the
|
|
11
|
+
scope the grid's token-driven utilities (the borders, the paddings, the
|
|
12
|
+
row's heights) stood unresolved over the consumer's page. The
|
|
13
|
+
`readOnly` stand sets the root's `data-readonly` attribute (the page
|
|
14
|
+
mode's `user-select`'s suppression parity).
|
|
15
|
+
|
|
16
|
+
## 0.3.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 5f5ab54: The presence selection's visibility over the room stands grant-gated (the
|
|
21
|
+
privacy stand): the `EditorProvider`'s additive `presence` prop carries
|
|
22
|
+
the local user's identity (`userId` / the optional `name`) and the
|
|
23
|
+
optional `PresenceCrypto` cipher; the new `usePresenceSharing()` hook
|
|
24
|
+
drives the local grant — the session-state default is **off** (the
|
|
25
|
+
selection does not publish, only the light identity frame the roster
|
|
26
|
+
needs), and the consumer flips it to `everyone` (the plaintext wire
|
|
27
|
+
shape, the legacy stand) or to a targeted user-id list (the selection
|
|
28
|
+
publishes as opaque ciphertext entries, one per grant member — the
|
|
29
|
+
entries carry no target id, a recipient opens only the entry addressed
|
|
30
|
+
to it, an unaddressed peer sees the sharer's identity but never the
|
|
31
|
+
selection). The library orchestrates the publish gating, the receive's
|
|
32
|
+
decrypt, and the per-peer sequence guard over the async decrypt; the
|
|
33
|
+
cryptography and the sharing's UI are the consumer's stand (the package
|
|
34
|
+
ships no built-in control). A provider without the `presence` prop keeps
|
|
35
|
+
the legacy stand (the anonymous plaintext selection over the room).
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- Updated dependencies [5971d30]
|
|
40
|
+
- docblocks-core@0.1.3
|
|
41
|
+
|
|
3
42
|
## 0.2.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -109,6 +109,7 @@ mention catalog). The full props list:
|
|
|
109
109
|
| `accessLevel?` | The consumer's user access level (default `editor`): `viewer` forces the `published` mode (the current state is never rendered to a reader). |
|
|
110
110
|
| `mode?` | The document mode (default `edit`): `view` is a read-only live document; `published` renders the `publishedDoc` snapshot read-only. |
|
|
111
111
|
| `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. |
|
|
112
|
+
| `presence?` | The local user's presence identity + the selection-sharing cipher (see Presence sharing below). Without the prop the editor keeps the legacy stand (the anonymous plaintext selection, always shared over the room). |
|
|
112
113
|
|
|
113
114
|
Two rules for the optional props:
|
|
114
115
|
|
|
@@ -256,6 +257,62 @@ function App() {
|
|
|
256
257
|
`updatedAt`); staleness (dropping stale peers) is a consumer concern —
|
|
257
258
|
the payload's `updatedAt` (Unix ms) is the only input.
|
|
258
259
|
|
|
260
|
+
### Presence sharing
|
|
261
|
+
|
|
262
|
+
The selection's visibility over the room is grant-gated (the privacy
|
|
263
|
+
stand): with the `presence` prop the grant defaults to **off** — the
|
|
264
|
+
selection does not publish, only the light identity frame (the roster). The
|
|
265
|
+
consumer flips the grant through the hook (session state — not document
|
|
266
|
+
data, not on the wire; it lives until the page reload or the provider's
|
|
267
|
+
unmount):
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
import { usePresenceSharing } from "docblocks-react-ui";
|
|
271
|
+
|
|
272
|
+
function ShareControl() {
|
|
273
|
+
const { sharing, setSharing, peers } = usePresenceSharing();
|
|
274
|
+
// sharing: "off" | "everyone" | string[] (the local grant — the full
|
|
275
|
+
// information stands over the sharer's pane: what is shared, and to whom)
|
|
276
|
+
// setSharing(mode) — a no-op over the read-only modes; a targeted grant
|
|
277
|
+
// requires the presence prop's userId + crypto (the degraded stand
|
|
278
|
+
// stands a console warn, no crash)
|
|
279
|
+
// peers: { peerId, userId?, name?, updatedAt, selectionVisible }[] —
|
|
280
|
+
// the live roster (the identity frames are always published)
|
|
281
|
+
return <button onClick={() => setSharing(sharing === "off" ? "everyone" : "off")}>{sharing}</button>;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The grant's modes:
|
|
286
|
+
|
|
287
|
+
- **`off`** — the selection never publishes (the identity frame only).
|
|
288
|
+
- **`everyone`** — the plaintext `selection` (the legacy wire shape — the
|
|
289
|
+
peers of any version render it).
|
|
290
|
+
- **`[userId, …]`** (targeted) — the selection publishes as opaque
|
|
291
|
+
ciphertext entries (`selections`), one per grant member, produced by the
|
|
292
|
+
consumer's `PresenceCrypto` cipher. The entries carry **no target id**:
|
|
293
|
+
a recipient tries each entry with its own `decrypt`; an unaddressed peer
|
|
294
|
+
sees the sharer's identity in the roster but never the selection (and
|
|
295
|
+
never learns the audience).
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
interface PresenceCrypto {
|
|
299
|
+
/** One opaque entry per grant member (grant order; no target id inside). */
|
|
300
|
+
encrypt(selection: PresenceSelection, grant: string[]): Promise<string[]>;
|
|
301
|
+
/** Null when the entry is not addressed to the local user (or broken). */
|
|
302
|
+
decrypt(entry: string): Promise<PresenceSelection | null>;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The library orchestrates (publish gating, the debounce, the per-peer
|
|
307
|
+
sequence guard over the async decrypt, the roster) and renders the
|
|
308
|
+
accepted selections through the existing overlay; the cryptography and
|
|
309
|
+
the keys are the consumer's, and the sharing's UI (the toggle, the target
|
|
310
|
+
picker, the indicator) is the consumer's stand over the hook — the package
|
|
311
|
+
ships no built-in control. Without `crypto` the targeted mode degrades
|
|
312
|
+
(the setter stands a no-op); `off` / `everyone` stand regardless. A
|
|
313
|
+
provider **without** the `presence` prop keeps the legacy stand (the
|
|
314
|
+
anonymous plaintext selection over the room — the 0.2.0 behavior).
|
|
315
|
+
|
|
259
316
|
### Blobs
|
|
260
317
|
|
|
261
318
|
The document holds only an opaque blob key (`image.src`); the bytes live
|
|
@@ -372,7 +429,12 @@ function TablesSection({ bridge }: { bridge: DatabaseBridge }) {
|
|
|
372
429
|
the optional `view` prop at mount); `onViewChange(patch)` receives one
|
|
373
430
|
patch per change for the consumer's persistence (the controlled mode
|
|
374
431
|
stands absent — the state is never external). `readOnly` gates the
|
|
375
|
-
editing's paths (the read-only block's parity).
|
|
432
|
+
editing's paths (the read-only block's parity). The component is
|
|
433
|
+
self-contained: it wraps itself in the package's i18n scope and anchors
|
|
434
|
+
the `--docblocks-*` token scope over its own root, so it themes
|
|
435
|
+
standalone over the consumer's page (the `dark` class over an ancestor
|
|
436
|
+
follows the shipped dark set — the package's `styles.css` import is the
|
|
437
|
+
only requirement).
|
|
376
438
|
- `useTableList(bridge)` — `{ tables, loading, error, refresh, create,
|
|
377
439
|
rename }`: the `listTables` catalog (the system tables stand out), the
|
|
378
440
|
`createTable` / `renameTable` mutations (the fresh `operationId` per op;
|
|
@@ -525,6 +587,8 @@ value actually changes (the store's reference-stability contract):
|
|
|
525
587
|
local state; `null` before the mount effect ran and after unmount).
|
|
526
588
|
- `usePresence()` — the remote peers' ephemeral state (the selection +
|
|
527
589
|
the `updatedAt` per peer).
|
|
590
|
+
- `usePresenceSharing()` — the selection-sharing grant (the session state
|
|
591
|
+
- the setter + the live roster; see Presence sharing).
|
|
528
592
|
- `useMeta()` — the document meta (the reference is replaced only when
|
|
529
593
|
the title changed).
|
|
530
594
|
- `useEditorContext()` — the whole provider context (the store, the
|
|
@@ -1116,6 +1116,45 @@ function usePresence() {
|
|
|
1116
1116
|
const { store } = useEditorContext();
|
|
1117
1117
|
return useStore(store, (state) => state.presence);
|
|
1118
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* The presence sharing's session state (per provider instance): the local
|
|
1121
|
+
* grant (the default "off" — the privacy stand), the grant's setter (a
|
|
1122
|
+
* no-op over the read-only stand), and the live roster of the connected
|
|
1123
|
+
* peers (the identity frames are always present, the selection only over
|
|
1124
|
+
* the grant it stands shared).
|
|
1125
|
+
*/
|
|
1126
|
+
function usePresenceSharing() {
|
|
1127
|
+
const { store, viewMode, presence: presenceConfig } = useEditorContext();
|
|
1128
|
+
const sharing = useStore(store, (state) => state.sharing);
|
|
1129
|
+
const setSharing = useStore(store, (state) => state.setSharing);
|
|
1130
|
+
const presence = useStore(store, (state) => state.presence);
|
|
1131
|
+
const peers = useMemo(() => presence.map((peer) => ({
|
|
1132
|
+
peerId: peer.peerId,
|
|
1133
|
+
userId: peer.userId,
|
|
1134
|
+
name: peer.name,
|
|
1135
|
+
updatedAt: peer.updatedAt,
|
|
1136
|
+
selectionVisible: peer.selection !== null
|
|
1137
|
+
})), [presence]);
|
|
1138
|
+
const setSharingRef = useRef(setSharing);
|
|
1139
|
+
setSharingRef.current = setSharing;
|
|
1140
|
+
return useMemo(() => ({
|
|
1141
|
+
sharing,
|
|
1142
|
+
setSharing: (mode) => {
|
|
1143
|
+
if (viewMode) return;
|
|
1144
|
+
if (Array.isArray(mode) && (presenceConfig === null || presenceConfig.userId === void 0 || presenceConfig.crypto === void 0)) {
|
|
1145
|
+
console.warn("docblocks-react-ui: the targeted presence sharing requires the presence prop's userId and crypto — the grant stands off.");
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
setSharingRef.current(mode);
|
|
1149
|
+
},
|
|
1150
|
+
peers
|
|
1151
|
+
}), [
|
|
1152
|
+
sharing,
|
|
1153
|
+
viewMode,
|
|
1154
|
+
peers,
|
|
1155
|
+
presenceConfig
|
|
1156
|
+
]);
|
|
1157
|
+
}
|
|
1119
1158
|
/** The document meta; the reference is replaced only when the title changed. */
|
|
1120
1159
|
function useMeta() {
|
|
1121
1160
|
const { store } = useEditorContext();
|
|
@@ -11410,6 +11449,293 @@ function useSlashMenu(rootRef) {
|
|
|
11410
11449
|
};
|
|
11411
11450
|
}
|
|
11412
11451
|
//#endregion
|
|
11452
|
+
//#region src/presence/utils.ts
|
|
11453
|
+
/** Deterministic hue (0–359) from the peer id (pure function, no peer state). */
|
|
11454
|
+
function peerHue(peerId) {
|
|
11455
|
+
let hash = 0;
|
|
11456
|
+
for (let i = 0; i < peerId.length; i++) hash = hash * 31 + peerId.charCodeAt(i) >>> 0;
|
|
11457
|
+
return hash % 360;
|
|
11458
|
+
}
|
|
11459
|
+
/** The caret endpoints a peer's wire selection draws; a range draws range boxes instead. */
|
|
11460
|
+
function caretPointsOf(selection) {
|
|
11461
|
+
if (selection === null) return [];
|
|
11462
|
+
if ("blockId" in selection) return [{
|
|
11463
|
+
blockId: selection.blockId,
|
|
11464
|
+
offset: selection.offset
|
|
11465
|
+
}];
|
|
11466
|
+
return [];
|
|
11467
|
+
}
|
|
11468
|
+
/**
|
|
11469
|
+
* The per-chain-block segments of a peer's range selection: the from block's
|
|
11470
|
+
* tail, the middle text blocks in full, the to block's head. Chain blocks
|
|
11471
|
+
* missing from the store (stale presence — the remote selection points at a
|
|
11472
|
+
* removed block) are skipped leniently, and the offsets clamp to the current
|
|
11473
|
+
* block lengths.
|
|
11474
|
+
*/
|
|
11475
|
+
function rangeSegmentsOf(selection, doc, blocks) {
|
|
11476
|
+
if (selection === null || !("from" in selection)) return [];
|
|
11477
|
+
const chain = selection.blocks ?? (selection.from.blockId === selection.to.blockId ? [selection.from.blockId] : [selection.from.blockId, selection.to.blockId]);
|
|
11478
|
+
const segments = [];
|
|
11479
|
+
for (const blockId of chain) {
|
|
11480
|
+
if (blocks[blockId] === void 0) continue;
|
|
11481
|
+
const length = doc.getTextLength(blockId);
|
|
11482
|
+
const from = blockId === selection.from.blockId ? selection.from.offset : 0;
|
|
11483
|
+
const to = blockId === selection.to.blockId ? Math.min(selection.to.offset, length) : length;
|
|
11484
|
+
if (from >= to || from >= length) continue;
|
|
11485
|
+
segments.push({
|
|
11486
|
+
blockId,
|
|
11487
|
+
from,
|
|
11488
|
+
to
|
|
11489
|
+
});
|
|
11490
|
+
}
|
|
11491
|
+
return segments;
|
|
11492
|
+
}
|
|
11493
|
+
/**
|
|
11494
|
+
* The client rects of a segment inside the block element, relative to the
|
|
11495
|
+
* root's rect. Environments without layout (jsdom — no getClientRects)
|
|
11496
|
+
* yield no rects, so a missing rect maps to a single zero box.
|
|
11497
|
+
*/
|
|
11498
|
+
function rangeRects(root, block, from, to) {
|
|
11499
|
+
const rootRect = root.getBoundingClientRect();
|
|
11500
|
+
const start = locatePosition(block, from);
|
|
11501
|
+
const end = locatePosition(block, to);
|
|
11502
|
+
const range = document.createRange();
|
|
11503
|
+
range.setStart(start.node, start.offset);
|
|
11504
|
+
range.setEnd(end.node, end.offset);
|
|
11505
|
+
if (typeof range.getClientRects !== "function") return [];
|
|
11506
|
+
return Array.from(range.getClientRects()).map((rect) => ({
|
|
11507
|
+
left: rect.left - rootRect.left,
|
|
11508
|
+
top: rect.top - rootRect.top,
|
|
11509
|
+
width: rect.width,
|
|
11510
|
+
height: rect.height
|
|
11511
|
+
}));
|
|
11512
|
+
}
|
|
11513
|
+
/**
|
|
11514
|
+
* The client rect of a collapsed range at the flat offset inside the
|
|
11515
|
+
* block element, relative to the root's rect. Environments without layout
|
|
11516
|
+
* (jsdom — no getClientRects) yield zero rects, so a missing rect maps to 0.
|
|
11517
|
+
*/
|
|
11518
|
+
function caretGeometry(root, block, offset) {
|
|
11519
|
+
const position = locatePosition(block, offset);
|
|
11520
|
+
const range = document.createRange();
|
|
11521
|
+
range.setStart(position.node, position.offset);
|
|
11522
|
+
range.collapse(true);
|
|
11523
|
+
let rect;
|
|
11524
|
+
if (typeof range.getClientRects === "function") {
|
|
11525
|
+
const rects = range.getClientRects();
|
|
11526
|
+
rect = rects.length > 0 ? rects[0] : void 0;
|
|
11527
|
+
}
|
|
11528
|
+
const rootRect = root.getBoundingClientRect();
|
|
11529
|
+
return {
|
|
11530
|
+
left: rect !== void 0 ? rect.left - rootRect.left : 0,
|
|
11531
|
+
top: rect !== void 0 ? rect.top - rootRect.top : 0,
|
|
11532
|
+
height: rect !== void 0 ? rect.height : 0
|
|
11533
|
+
};
|
|
11534
|
+
}
|
|
11535
|
+
//#endregion
|
|
11536
|
+
//#region src/presence/PresenceCaretOverlay.tsx
|
|
11537
|
+
/** Renders remote peers' selections as absolutely positioned boxes over
|
|
11538
|
+
* the editor root — a pure view of the presence slice (it never
|
|
11539
|
+
* mutates). A caret selection draws a caret bar; a range selection
|
|
11540
|
+
* (cross-block included) draws its highlight boxes — one per client rect
|
|
11541
|
+
* of each chain-block segment. The boxes are recomputed on presence
|
|
11542
|
+
* changes, on block snapshot changes (text shifts the pixels), and on
|
|
11543
|
+
* window resize; a peer whose block is not rendered (removed, unknown
|
|
11544
|
+
* type) draws nothing. The structural classes of the overlay boxes (the
|
|
11545
|
+
* per-peer hue comes from the inline --docblocks-presence-hue property,
|
|
11546
|
+
* the geometry from the inline layout properties): the caret bar is the
|
|
11547
|
+
* fixed width token; the range highlight is a softened box of the same
|
|
11548
|
+
* hue. */
|
|
11549
|
+
/**
|
|
11550
|
+
* The block ids a peer's selection references (the caret's block, the
|
|
11551
|
+
* range's endpoints + the chain blocks). The overlay stands no boxes over
|
|
11552
|
+
* the other blocks — those stand out of its subscription.
|
|
11553
|
+
*/
|
|
11554
|
+
function peerBlockIdsOf(presence) {
|
|
11555
|
+
const ids = /* @__PURE__ */ new Set();
|
|
11556
|
+
for (const peer of presence) {
|
|
11557
|
+
const selection = peer.selection;
|
|
11558
|
+
if (selection === null) continue;
|
|
11559
|
+
if ("blockId" in selection) ids.add(selection.blockId);
|
|
11560
|
+
else if ("from" in selection) {
|
|
11561
|
+
ids.add(selection.from.blockId);
|
|
11562
|
+
ids.add(selection.to.blockId);
|
|
11563
|
+
for (const id of selection.blocks ?? []) ids.add(id);
|
|
11564
|
+
}
|
|
11565
|
+
}
|
|
11566
|
+
return [...ids].sort();
|
|
11567
|
+
}
|
|
11568
|
+
/** The peer-referenced blocks' snapshots (the absent ones stand dropped). The ids come from the remote presence frames — the open-key stand: `__proto__` stands the own property (no prototype reassignment over the derived map). */
|
|
11569
|
+
function peerBlocksOf(presence, blocks) {
|
|
11570
|
+
const next = {};
|
|
11571
|
+
for (const id of peerBlockIdsOf(presence)) {
|
|
11572
|
+
const block = blocks[id];
|
|
11573
|
+
if (block !== void 0) Object.defineProperty(next, id, {
|
|
11574
|
+
value: block,
|
|
11575
|
+
enumerable: true,
|
|
11576
|
+
writable: true,
|
|
11577
|
+
configurable: true
|
|
11578
|
+
});
|
|
11579
|
+
}
|
|
11580
|
+
return next;
|
|
11581
|
+
}
|
|
11582
|
+
/**
|
|
11583
|
+
* The structural classes of the overlay boxes (the per-peer hue comes
|
|
11584
|
+
* from the inline --docblocks-presence-hue property, the geometry from
|
|
11585
|
+
* the inline layout properties). The caret bar is the fixed width token;
|
|
11586
|
+
* the range highlight is a softened box of the same hue.
|
|
11587
|
+
*/
|
|
11588
|
+
const CARET_CLASS = "pointer-events-none absolute w-(--docblocks-presence-caret-width) bg-[hsl(var(--docblocks-presence-hue,200)_var(--docblocks-presence-caret-saturation)_var(--docblocks-presence-caret-lightness))]";
|
|
11589
|
+
const RANGE_CLASS = "pointer-events-none absolute rounded-[2px] bg-[hsl(var(--docblocks-presence-hue,200)_var(--docblocks-presence-caret-saturation)_var(--docblocks-presence-caret-lightness)/var(--docblocks-presence-range-opacity))]";
|
|
11590
|
+
function PresenceCaretOverlay({ rootRef }) {
|
|
11591
|
+
const { store, doc } = useEditorContext();
|
|
11592
|
+
const presence = useStore(store, (state) => state.presence);
|
|
11593
|
+
const peerIdsKeyRef = useRef("");
|
|
11594
|
+
const peerBlocksCacheRef = useRef(null);
|
|
11595
|
+
const peerBlocks = useStore(store, (state) => {
|
|
11596
|
+
if (state.presence.length === 0) return null;
|
|
11597
|
+
const ids = peerBlockIdsOf(state.presence);
|
|
11598
|
+
const key = ids.join("\0");
|
|
11599
|
+
const cache = peerBlocksCacheRef.current;
|
|
11600
|
+
if (key === peerIdsKeyRef.current && cache !== null) {
|
|
11601
|
+
let unchanged = true;
|
|
11602
|
+
for (const id of ids) if (cache[id] !== state.blocks[id]) {
|
|
11603
|
+
unchanged = false;
|
|
11604
|
+
break;
|
|
11605
|
+
}
|
|
11606
|
+
if (unchanged) return cache;
|
|
11607
|
+
}
|
|
11608
|
+
const next = peerBlocksOf(state.presence, state.blocks);
|
|
11609
|
+
peerIdsKeyRef.current = key;
|
|
11610
|
+
peerBlocksCacheRef.current = next;
|
|
11611
|
+
return next;
|
|
11612
|
+
});
|
|
11613
|
+
const [boxes, setBoxes] = useState([]);
|
|
11614
|
+
useLayoutEffect(() => {
|
|
11615
|
+
const root = rootRef.current;
|
|
11616
|
+
if (root === null) return;
|
|
11617
|
+
const recompute = () => {
|
|
11618
|
+
const next = [];
|
|
11619
|
+
for (const peer of presence) {
|
|
11620
|
+
for (const point of caretPointsOf(peer.selection)) {
|
|
11621
|
+
const block = root.querySelector(`[data-block-id="${CSS.escape(point.blockId)}"][data-slot="docblocks/block-content"]`) ?? root.querySelector(`[data-block-id="${CSS.escape(point.blockId)}"]`);
|
|
11622
|
+
if (block === null) continue;
|
|
11623
|
+
next.push({
|
|
11624
|
+
key: `${peer.peerId}:caret:${point.blockId}:${point.offset}`,
|
|
11625
|
+
peerId: peer.peerId,
|
|
11626
|
+
slot: "docblocks/presence-caret",
|
|
11627
|
+
...caretGeometry(root, block, point.offset)
|
|
11628
|
+
});
|
|
11629
|
+
}
|
|
11630
|
+
for (const segment of rangeSegmentsOf(peer.selection, doc, peerBlocks ?? {})) {
|
|
11631
|
+
const block = root.querySelector(`[data-block-id="${CSS.escape(segment.blockId)}"][data-slot="docblocks/block-content"]`);
|
|
11632
|
+
if (block === null) continue;
|
|
11633
|
+
const rects = rangeRects(root, block, segment.from, segment.to);
|
|
11634
|
+
(rects.length > 0 ? rects : [{
|
|
11635
|
+
left: 0,
|
|
11636
|
+
top: 0,
|
|
11637
|
+
width: 0,
|
|
11638
|
+
height: 0
|
|
11639
|
+
}]).forEach((rect, index) => {
|
|
11640
|
+
next.push({
|
|
11641
|
+
key: `${peer.peerId}:range:${segment.blockId}:${segment.from}-${segment.to}:${index}`,
|
|
11642
|
+
peerId: peer.peerId,
|
|
11643
|
+
slot: "docblocks/presence-range",
|
|
11644
|
+
blockId: segment.blockId,
|
|
11645
|
+
...rect
|
|
11646
|
+
});
|
|
11647
|
+
});
|
|
11648
|
+
}
|
|
11649
|
+
}
|
|
11650
|
+
setBoxes((prev) => JSON.stringify(prev) === JSON.stringify(next) ? prev : next);
|
|
11651
|
+
};
|
|
11652
|
+
let frame = 0;
|
|
11653
|
+
const scheduleRecompute = () => {
|
|
11654
|
+
cancelAnimationFrame(frame);
|
|
11655
|
+
frame = requestAnimationFrame(() => {
|
|
11656
|
+
frame = 0;
|
|
11657
|
+
recompute();
|
|
11658
|
+
});
|
|
11659
|
+
};
|
|
11660
|
+
recompute();
|
|
11661
|
+
scheduleRecompute();
|
|
11662
|
+
window.addEventListener("resize", scheduleRecompute);
|
|
11663
|
+
return () => {
|
|
11664
|
+
cancelAnimationFrame(frame);
|
|
11665
|
+
window.removeEventListener("resize", scheduleRecompute);
|
|
11666
|
+
};
|
|
11667
|
+
}, [
|
|
11668
|
+
presence,
|
|
11669
|
+
peerBlocks,
|
|
11670
|
+
doc,
|
|
11671
|
+
rootRef
|
|
11672
|
+
]);
|
|
11673
|
+
return /* @__PURE__ */ jsx(Fragment$1, { children: boxes.map((box) => /* @__PURE__ */ jsx("span", {
|
|
11674
|
+
"data-slot": box.slot,
|
|
11675
|
+
"data-peer": box.peerId,
|
|
11676
|
+
"data-range-block": box.blockId,
|
|
11677
|
+
className: box.slot === "docblocks/presence-caret" ? CARET_CLASS : RANGE_CLASS,
|
|
11678
|
+
style: {
|
|
11679
|
+
"--docblocks-presence-hue": peerHue(box.peerId),
|
|
11680
|
+
left: box.left,
|
|
11681
|
+
top: box.top,
|
|
11682
|
+
height: box.height,
|
|
11683
|
+
...box.width !== void 0 ? { width: box.width } : null
|
|
11684
|
+
}
|
|
11685
|
+
}, box.key)) });
|
|
11686
|
+
}
|
|
11687
|
+
//#endregion
|
|
11688
|
+
//#region src/presence/sharing.ts
|
|
11689
|
+
/**
|
|
11690
|
+
* Build the awareness payload of one publish. Returns null when the grant
|
|
11691
|
+
* is targeted over no cipher (the caller keeps the previous payload — the
|
|
11692
|
+
* degradation stands, no crash, no partial publish).
|
|
11693
|
+
*/
|
|
11694
|
+
async function buildPresencePayload(params) {
|
|
11695
|
+
const { selection, sharing, viewMode, identity, crypto, seq, updatedAt } = params;
|
|
11696
|
+
const payload = {
|
|
11697
|
+
updatedAt,
|
|
11698
|
+
seq
|
|
11699
|
+
};
|
|
11700
|
+
if (identity.userId !== void 0) payload.userId = identity.userId;
|
|
11701
|
+
if (identity.name !== void 0) payload.name = identity.name;
|
|
11702
|
+
if (viewMode || selection === null) {
|
|
11703
|
+
payload.selection = null;
|
|
11704
|
+
return payload;
|
|
11705
|
+
}
|
|
11706
|
+
if (sharing === "off") {
|
|
11707
|
+
payload.selection = identity.userId === void 0 ? selection : null;
|
|
11708
|
+
return payload;
|
|
11709
|
+
}
|
|
11710
|
+
if (sharing === "everyone") {
|
|
11711
|
+
payload.selection = selection;
|
|
11712
|
+
return payload;
|
|
11713
|
+
}
|
|
11714
|
+
if (crypto === null) return null;
|
|
11715
|
+
payload.selection = null;
|
|
11716
|
+
payload.selections = await crypto.encrypt(selection, sharing);
|
|
11717
|
+
return payload;
|
|
11718
|
+
}
|
|
11719
|
+
/**
|
|
11720
|
+
* Resolve one received peer's selection: the plaintext one (the everyone /
|
|
11721
|
+
* legacy stand) stands as is; a targeted stand resolves to the first entry
|
|
11722
|
+
* the local cipher opens (the entries carry no target id, so every entry
|
|
11723
|
+
* is tried); without a cipher a targeted selection never stands (the
|
|
11724
|
+
* peer's identity frame still lists it in the roster).
|
|
11725
|
+
*/
|
|
11726
|
+
async function resolvePeerSelection(params) {
|
|
11727
|
+
const { peer, crypto } = params;
|
|
11728
|
+
if (peer.selection !== null) return peer;
|
|
11729
|
+
if (peer.selections !== void 0 && crypto !== null && peer.selections.length > 0) {
|
|
11730
|
+
const selection = (await Promise.all(peer.selections.map((entry) => crypto.decrypt(entry)))).find((value) => value !== null) ?? null;
|
|
11731
|
+
return {
|
|
11732
|
+
...peer,
|
|
11733
|
+
selection
|
|
11734
|
+
};
|
|
11735
|
+
}
|
|
11736
|
+
return peer;
|
|
11737
|
+
}
|
|
11738
|
+
//#endregion
|
|
11413
11739
|
//#region src/state/utils.ts
|
|
11414
11740
|
/** The editor store's projection utils: the known-type blocks filter
|
|
11415
11741
|
* (the store's projection holds only known-type blocks — an unknown-type
|
|
@@ -11534,9 +11860,11 @@ function createEditorStore(doc, rootId) {
|
|
|
11534
11860
|
selection: { kind: "none" },
|
|
11535
11861
|
collapsedIds: [],
|
|
11536
11862
|
presence: [],
|
|
11863
|
+
sharing: "off",
|
|
11537
11864
|
canvasClipboard: null,
|
|
11538
11865
|
applyChange,
|
|
11539
11866
|
setPresence: (peers) => store.setState({ presence: peers }),
|
|
11867
|
+
setSharing: (mode) => store.setState({ sharing: mode }),
|
|
11540
11868
|
setSelection: (selection) => store.setState({ selection }),
|
|
11541
11869
|
setCollapsed: (id, collapsed) => {
|
|
11542
11870
|
const ids = store.getState().collapsedIds;
|
|
@@ -11616,7 +11944,7 @@ function publishedStandKey(doc) {
|
|
|
11616
11944
|
* its own. Safe under StrictMode double-mount: the cleanup detaches every
|
|
11617
11945
|
* subscription and destroys the view between the two mounts.
|
|
11618
11946
|
*/
|
|
11619
|
-
function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blobStore, databaseBridge, syncEmbedResolver, pdfPreview, mentionProvider, embedSandbox, rootBlockId, commandPalettePlaceholder, accessLevel, mode, publishedDoc, children }) {
|
|
11947
|
+
function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blobStore, databaseBridge, syncEmbedResolver, pdfPreview, mentionProvider, embedSandbox, rootBlockId, commandPalettePlaceholder, accessLevel, mode, publishedDoc, presence, children }) {
|
|
11620
11948
|
const rootId = rootBlockId ?? doc.meta.id;
|
|
11621
11949
|
const [store] = useState(() => createEditorStore(doc, rootId));
|
|
11622
11950
|
const docRef = useRef(doc);
|
|
@@ -11626,6 +11954,10 @@ function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blob
|
|
|
11626
11954
|
const effectiveAccessLevel = accessLevel ?? parentContext?.accessLevel ?? "editor";
|
|
11627
11955
|
const effectiveMode = effectiveAccessLevel === "viewer" ? "published" : mode ?? parentContext?.mode ?? "edit";
|
|
11628
11956
|
const editingStandRef = useRef(effectiveMode === "edit");
|
|
11957
|
+
const presenceRef = useRef(presence);
|
|
11958
|
+
presenceRef.current = presence;
|
|
11959
|
+
const viewModeRef = useRef(effectiveMode !== "edit");
|
|
11960
|
+
viewModeRef.current = effectiveMode !== "edit";
|
|
11629
11961
|
useEffect(() => {
|
|
11630
11962
|
if (docRef.current !== doc) throw new Error("EditorProvider: the doc instance must not change after mount.");
|
|
11631
11963
|
docRef.current = doc;
|
|
@@ -11663,21 +11995,54 @@ function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blob
|
|
|
11663
11995
|
useEffect(() => {
|
|
11664
11996
|
if (provider === void 0) return;
|
|
11665
11997
|
let lastPeers = "";
|
|
11666
|
-
const
|
|
11667
|
-
|
|
11998
|
+
const latest = /* @__PURE__ */ new Map();
|
|
11999
|
+
let generation = 0;
|
|
12000
|
+
const unsubscribeAwareness = provider.onAwareness((rawPeers) => {
|
|
12001
|
+
const serialized = JSON.stringify(rawPeers);
|
|
11668
12002
|
if (serialized === lastPeers) return;
|
|
11669
12003
|
lastPeers = serialized;
|
|
11670
|
-
|
|
12004
|
+
const rawIds = new Set(rawPeers.map((peer) => peer.peerId));
|
|
12005
|
+
for (const id of [...latest.keys()]) if (!rawIds.has(id)) latest.delete(id);
|
|
12006
|
+
for (const peer of rawPeers) {
|
|
12007
|
+
const prev = latest.get(peer.peerId);
|
|
12008
|
+
if (peer.seq !== void 0 && prev?.seq !== void 0 && peer.seq < prev.seq) continue;
|
|
12009
|
+
latest.set(peer.peerId, peer);
|
|
12010
|
+
}
|
|
12011
|
+
const gen = ++generation;
|
|
12012
|
+
const crypto = presenceRef.current?.crypto ?? null;
|
|
12013
|
+
const input = [...latest.values()];
|
|
12014
|
+
(async () => {
|
|
12015
|
+
const resolved = [];
|
|
12016
|
+
for (const peer of input) resolved.push(await resolvePeerSelection({
|
|
12017
|
+
peer,
|
|
12018
|
+
crypto
|
|
12019
|
+
}));
|
|
12020
|
+
if (gen !== generation) return;
|
|
12021
|
+
store.getState().setPresence(resolved);
|
|
12022
|
+
})();
|
|
11671
12023
|
});
|
|
11672
12024
|
return () => unsubscribeAwareness();
|
|
11673
12025
|
}, [provider, store]);
|
|
11674
12026
|
useEffect(() => {
|
|
11675
12027
|
if (provider === void 0) return;
|
|
12028
|
+
let seq = 0;
|
|
12029
|
+
let publishToken = 0;
|
|
11676
12030
|
const publish = (selection) => {
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
12031
|
+
const token = ++publishToken;
|
|
12032
|
+
const sharing = store.getState().sharing;
|
|
12033
|
+
(async () => {
|
|
12034
|
+
const payload = await buildPresencePayload({
|
|
12035
|
+
selection: presenceSelectionOf(selection),
|
|
12036
|
+
sharing,
|
|
12037
|
+
viewMode: viewModeRef.current,
|
|
12038
|
+
identity: presenceRef.current ?? {},
|
|
12039
|
+
crypto: presenceRef.current?.crypto ?? null,
|
|
12040
|
+
seq: ++seq,
|
|
12041
|
+
updatedAt: Date.now()
|
|
12042
|
+
});
|
|
12043
|
+
if (token !== publishToken || payload === null) return;
|
|
12044
|
+
provider.setAwareness(payload);
|
|
12045
|
+
})();
|
|
11681
12046
|
};
|
|
11682
12047
|
let timer;
|
|
11683
12048
|
const queue = (selection) => {
|
|
@@ -11687,28 +12052,29 @@ function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blob
|
|
|
11687
12052
|
publish(selection);
|
|
11688
12053
|
}, 50);
|
|
11689
12054
|
};
|
|
11690
|
-
const
|
|
12055
|
+
const publishNow = (selection) => {
|
|
11691
12056
|
if (timer !== void 0) {
|
|
11692
12057
|
clearTimeout(timer);
|
|
11693
12058
|
timer = void 0;
|
|
11694
12059
|
}
|
|
11695
|
-
publish(
|
|
11696
|
-
};
|
|
11697
|
-
const publishCurrentNow = () => {
|
|
11698
|
-
if (timer !== void 0) return;
|
|
11699
|
-
publish(store.getState().selection);
|
|
12060
|
+
publish(selection);
|
|
11700
12061
|
};
|
|
11701
12062
|
const unsubscribe = store.subscribe((state, prev) => {
|
|
11702
12063
|
if (state.selection !== prev.selection) queue(state.selection);
|
|
12064
|
+
if (state.sharing !== prev.sharing) publish(store.getState().selection);
|
|
11703
12065
|
});
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
12066
|
+
const onBlur = () => publishNow({ kind: "none" });
|
|
12067
|
+
const onFocus = () => {
|
|
12068
|
+
if (timer !== void 0) return;
|
|
12069
|
+
publishNow(store.getState().selection);
|
|
12070
|
+
};
|
|
12071
|
+
window.addEventListener("blur", onBlur);
|
|
12072
|
+
window.addEventListener("beforeunload", onBlur);
|
|
12073
|
+
window.addEventListener("focus", onFocus);
|
|
11707
12074
|
return () => {
|
|
11708
12075
|
unsubscribe();
|
|
11709
|
-
window.removeEventListener("blur",
|
|
11710
|
-
window.removeEventListener("beforeunload",
|
|
11711
|
-
window.removeEventListener("focus", publishCurrentNow);
|
|
12076
|
+
window.removeEventListener("blur", onBlur);
|
|
12077
|
+
window.removeEventListener("beforeunload", onBlur);
|
|
11712
12078
|
if (timer !== void 0) clearTimeout(timer);
|
|
11713
12079
|
};
|
|
11714
12080
|
}, [provider, store]);
|
|
@@ -11737,7 +12103,8 @@ function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blob
|
|
|
11737
12103
|
commandPalettePlaceholder: commandPalettePlaceholder ?? parent?.commandPalettePlaceholder ?? "Type '/' for commands",
|
|
11738
12104
|
accessLevel: effectiveAccessLevel,
|
|
11739
12105
|
mode: effectiveMode,
|
|
11740
|
-
viewMode: effectiveMode !== "edit"
|
|
12106
|
+
viewMode: effectiveMode !== "edit",
|
|
12107
|
+
presence: presence ?? null
|
|
11741
12108
|
};
|
|
11742
12109
|
}, [
|
|
11743
12110
|
store,
|
|
@@ -11753,7 +12120,8 @@ function EditorProviderStand({ doc, provider, blockRenderers, blobResolver, blob
|
|
|
11753
12120
|
mentionProvider,
|
|
11754
12121
|
embedSandbox,
|
|
11755
12122
|
effectiveAccessLevel,
|
|
11756
|
-
effectiveMode
|
|
12123
|
+
effectiveMode,
|
|
12124
|
+
presence
|
|
11757
12125
|
]);
|
|
11758
12126
|
const inheritedPublishedStand = parentContext !== null && parentContext.mode === "published";
|
|
11759
12127
|
const rendered = effectiveAccessLevel === "viewer" && parentContext === null ? children : effectiveMode === "published" ? publishedDoc !== void 0 && publishedDoc !== null ? /* @__PURE__ */ jsx(EditorProvider, {
|
|
@@ -28570,242 +28938,6 @@ function useMotionDrag({ doc, rootBlockId, store, rootRef, rowKeys, nodesById })
|
|
|
28570
28938
|
};
|
|
28571
28939
|
}
|
|
28572
28940
|
//#endregion
|
|
28573
|
-
//#region src/presence/utils.ts
|
|
28574
|
-
/** Deterministic hue (0–359) from the peer id (pure function, no peer state). */
|
|
28575
|
-
function peerHue(peerId) {
|
|
28576
|
-
let hash = 0;
|
|
28577
|
-
for (let i = 0; i < peerId.length; i++) hash = hash * 31 + peerId.charCodeAt(i) >>> 0;
|
|
28578
|
-
return hash % 360;
|
|
28579
|
-
}
|
|
28580
|
-
/** The caret endpoints a peer's wire selection draws; a range draws range boxes instead. */
|
|
28581
|
-
function caretPointsOf(selection) {
|
|
28582
|
-
if (selection === null) return [];
|
|
28583
|
-
if ("blockId" in selection) return [{
|
|
28584
|
-
blockId: selection.blockId,
|
|
28585
|
-
offset: selection.offset
|
|
28586
|
-
}];
|
|
28587
|
-
return [];
|
|
28588
|
-
}
|
|
28589
|
-
/**
|
|
28590
|
-
* The per-chain-block segments of a peer's range selection: the from block's
|
|
28591
|
-
* tail, the middle text blocks in full, the to block's head. Chain blocks
|
|
28592
|
-
* missing from the store (stale presence — the remote selection points at a
|
|
28593
|
-
* removed block) are skipped leniently, and the offsets clamp to the current
|
|
28594
|
-
* block lengths.
|
|
28595
|
-
*/
|
|
28596
|
-
function rangeSegmentsOf(selection, doc, blocks) {
|
|
28597
|
-
if (selection === null || !("from" in selection)) return [];
|
|
28598
|
-
const chain = selection.blocks ?? (selection.from.blockId === selection.to.blockId ? [selection.from.blockId] : [selection.from.blockId, selection.to.blockId]);
|
|
28599
|
-
const segments = [];
|
|
28600
|
-
for (const blockId of chain) {
|
|
28601
|
-
if (blocks[blockId] === void 0) continue;
|
|
28602
|
-
const length = doc.getTextLength(blockId);
|
|
28603
|
-
const from = blockId === selection.from.blockId ? selection.from.offset : 0;
|
|
28604
|
-
const to = blockId === selection.to.blockId ? Math.min(selection.to.offset, length) : length;
|
|
28605
|
-
if (from >= to || from >= length) continue;
|
|
28606
|
-
segments.push({
|
|
28607
|
-
blockId,
|
|
28608
|
-
from,
|
|
28609
|
-
to
|
|
28610
|
-
});
|
|
28611
|
-
}
|
|
28612
|
-
return segments;
|
|
28613
|
-
}
|
|
28614
|
-
/**
|
|
28615
|
-
* The client rects of a segment inside the block element, relative to the
|
|
28616
|
-
* root's rect. Environments without layout (jsdom — no getClientRects)
|
|
28617
|
-
* yield no rects, so a missing rect maps to a single zero box.
|
|
28618
|
-
*/
|
|
28619
|
-
function rangeRects(root, block, from, to) {
|
|
28620
|
-
const rootRect = root.getBoundingClientRect();
|
|
28621
|
-
const start = locatePosition(block, from);
|
|
28622
|
-
const end = locatePosition(block, to);
|
|
28623
|
-
const range = document.createRange();
|
|
28624
|
-
range.setStart(start.node, start.offset);
|
|
28625
|
-
range.setEnd(end.node, end.offset);
|
|
28626
|
-
if (typeof range.getClientRects !== "function") return [];
|
|
28627
|
-
return Array.from(range.getClientRects()).map((rect) => ({
|
|
28628
|
-
left: rect.left - rootRect.left,
|
|
28629
|
-
top: rect.top - rootRect.top,
|
|
28630
|
-
width: rect.width,
|
|
28631
|
-
height: rect.height
|
|
28632
|
-
}));
|
|
28633
|
-
}
|
|
28634
|
-
/**
|
|
28635
|
-
* The client rect of a collapsed range at the flat offset inside the
|
|
28636
|
-
* block element, relative to the root's rect. Environments without layout
|
|
28637
|
-
* (jsdom — no getClientRects) yield zero rects, so a missing rect maps to 0.
|
|
28638
|
-
*/
|
|
28639
|
-
function caretGeometry(root, block, offset) {
|
|
28640
|
-
const position = locatePosition(block, offset);
|
|
28641
|
-
const range = document.createRange();
|
|
28642
|
-
range.setStart(position.node, position.offset);
|
|
28643
|
-
range.collapse(true);
|
|
28644
|
-
let rect;
|
|
28645
|
-
if (typeof range.getClientRects === "function") {
|
|
28646
|
-
const rects = range.getClientRects();
|
|
28647
|
-
rect = rects.length > 0 ? rects[0] : void 0;
|
|
28648
|
-
}
|
|
28649
|
-
const rootRect = root.getBoundingClientRect();
|
|
28650
|
-
return {
|
|
28651
|
-
left: rect !== void 0 ? rect.left - rootRect.left : 0,
|
|
28652
|
-
top: rect !== void 0 ? rect.top - rootRect.top : 0,
|
|
28653
|
-
height: rect !== void 0 ? rect.height : 0
|
|
28654
|
-
};
|
|
28655
|
-
}
|
|
28656
|
-
//#endregion
|
|
28657
|
-
//#region src/presence/PresenceCaretOverlay.tsx
|
|
28658
|
-
/** Renders remote peers' selections as absolutely positioned boxes over
|
|
28659
|
-
* the editor root — a pure view of the presence slice (it never
|
|
28660
|
-
* mutates). A caret selection draws a caret bar; a range selection
|
|
28661
|
-
* (cross-block included) draws its highlight boxes — one per client rect
|
|
28662
|
-
* of each chain-block segment. The boxes are recomputed on presence
|
|
28663
|
-
* changes, on block snapshot changes (text shifts the pixels), and on
|
|
28664
|
-
* window resize; a peer whose block is not rendered (removed, unknown
|
|
28665
|
-
* type) draws nothing. The structural classes of the overlay boxes (the
|
|
28666
|
-
* per-peer hue comes from the inline --docblocks-presence-hue property,
|
|
28667
|
-
* the geometry from the inline layout properties): the caret bar is the
|
|
28668
|
-
* fixed width token; the range highlight is a softened box of the same
|
|
28669
|
-
* hue. */
|
|
28670
|
-
/**
|
|
28671
|
-
* The block ids a peer's selection references (the caret's block, the
|
|
28672
|
-
* range's endpoints + the chain blocks). The overlay stands no boxes over
|
|
28673
|
-
* the other blocks — those stand out of its subscription.
|
|
28674
|
-
*/
|
|
28675
|
-
function peerBlockIdsOf(presence) {
|
|
28676
|
-
const ids = /* @__PURE__ */ new Set();
|
|
28677
|
-
for (const peer of presence) {
|
|
28678
|
-
const selection = peer.selection;
|
|
28679
|
-
if (selection === null) continue;
|
|
28680
|
-
if ("blockId" in selection) ids.add(selection.blockId);
|
|
28681
|
-
else if ("from" in selection) {
|
|
28682
|
-
ids.add(selection.from.blockId);
|
|
28683
|
-
ids.add(selection.to.blockId);
|
|
28684
|
-
for (const id of selection.blocks ?? []) ids.add(id);
|
|
28685
|
-
}
|
|
28686
|
-
}
|
|
28687
|
-
return [...ids].sort();
|
|
28688
|
-
}
|
|
28689
|
-
/** The peer-referenced blocks' snapshots (the absent ones stand dropped). The ids come from the remote presence frames — the open-key stand: `__proto__` stands the own property (no prototype reassignment over the derived map). */
|
|
28690
|
-
function peerBlocksOf(presence, blocks) {
|
|
28691
|
-
const next = {};
|
|
28692
|
-
for (const id of peerBlockIdsOf(presence)) {
|
|
28693
|
-
const block = blocks[id];
|
|
28694
|
-
if (block !== void 0) Object.defineProperty(next, id, {
|
|
28695
|
-
value: block,
|
|
28696
|
-
enumerable: true,
|
|
28697
|
-
writable: true,
|
|
28698
|
-
configurable: true
|
|
28699
|
-
});
|
|
28700
|
-
}
|
|
28701
|
-
return next;
|
|
28702
|
-
}
|
|
28703
|
-
/**
|
|
28704
|
-
* The structural classes of the overlay boxes (the per-peer hue comes
|
|
28705
|
-
* from the inline --docblocks-presence-hue property, the geometry from
|
|
28706
|
-
* the inline layout properties). The caret bar is the fixed width token;
|
|
28707
|
-
* the range highlight is a softened box of the same hue.
|
|
28708
|
-
*/
|
|
28709
|
-
const CARET_CLASS = "pointer-events-none absolute w-(--docblocks-presence-caret-width) bg-[hsl(var(--docblocks-presence-hue,200)_var(--docblocks-presence-caret-saturation)_var(--docblocks-presence-caret-lightness))]";
|
|
28710
|
-
const RANGE_CLASS = "pointer-events-none absolute rounded-[2px] bg-[hsl(var(--docblocks-presence-hue,200)_var(--docblocks-presence-caret-saturation)_var(--docblocks-presence-caret-lightness)/var(--docblocks-presence-range-opacity))]";
|
|
28711
|
-
function PresenceCaretOverlay({ rootRef }) {
|
|
28712
|
-
const { store, doc } = useEditorContext();
|
|
28713
|
-
const presence = useStore(store, (state) => state.presence);
|
|
28714
|
-
const peerIdsKeyRef = useRef("");
|
|
28715
|
-
const peerBlocksCacheRef = useRef(null);
|
|
28716
|
-
const peerBlocks = useStore(store, (state) => {
|
|
28717
|
-
if (state.presence.length === 0) return null;
|
|
28718
|
-
const ids = peerBlockIdsOf(state.presence);
|
|
28719
|
-
const key = ids.join("\0");
|
|
28720
|
-
const cache = peerBlocksCacheRef.current;
|
|
28721
|
-
if (key === peerIdsKeyRef.current && cache !== null) {
|
|
28722
|
-
let unchanged = true;
|
|
28723
|
-
for (const id of ids) if (cache[id] !== state.blocks[id]) {
|
|
28724
|
-
unchanged = false;
|
|
28725
|
-
break;
|
|
28726
|
-
}
|
|
28727
|
-
if (unchanged) return cache;
|
|
28728
|
-
}
|
|
28729
|
-
const next = peerBlocksOf(state.presence, state.blocks);
|
|
28730
|
-
peerIdsKeyRef.current = key;
|
|
28731
|
-
peerBlocksCacheRef.current = next;
|
|
28732
|
-
return next;
|
|
28733
|
-
});
|
|
28734
|
-
const [boxes, setBoxes] = useState([]);
|
|
28735
|
-
useLayoutEffect(() => {
|
|
28736
|
-
const root = rootRef.current;
|
|
28737
|
-
if (root === null) return;
|
|
28738
|
-
const recompute = () => {
|
|
28739
|
-
const next = [];
|
|
28740
|
-
for (const peer of presence) {
|
|
28741
|
-
for (const point of caretPointsOf(peer.selection)) {
|
|
28742
|
-
const block = root.querySelector(`[data-block-id="${CSS.escape(point.blockId)}"][data-slot="docblocks/block-content"]`) ?? root.querySelector(`[data-block-id="${CSS.escape(point.blockId)}"]`);
|
|
28743
|
-
if (block === null) continue;
|
|
28744
|
-
next.push({
|
|
28745
|
-
key: `${peer.peerId}:caret:${point.blockId}:${point.offset}`,
|
|
28746
|
-
peerId: peer.peerId,
|
|
28747
|
-
slot: "docblocks/presence-caret",
|
|
28748
|
-
...caretGeometry(root, block, point.offset)
|
|
28749
|
-
});
|
|
28750
|
-
}
|
|
28751
|
-
for (const segment of rangeSegmentsOf(peer.selection, doc, peerBlocks ?? {})) {
|
|
28752
|
-
const block = root.querySelector(`[data-block-id="${CSS.escape(segment.blockId)}"][data-slot="docblocks/block-content"]`);
|
|
28753
|
-
if (block === null) continue;
|
|
28754
|
-
const rects = rangeRects(root, block, segment.from, segment.to);
|
|
28755
|
-
(rects.length > 0 ? rects : [{
|
|
28756
|
-
left: 0,
|
|
28757
|
-
top: 0,
|
|
28758
|
-
width: 0,
|
|
28759
|
-
height: 0
|
|
28760
|
-
}]).forEach((rect, index) => {
|
|
28761
|
-
next.push({
|
|
28762
|
-
key: `${peer.peerId}:range:${segment.blockId}:${segment.from}-${segment.to}:${index}`,
|
|
28763
|
-
peerId: peer.peerId,
|
|
28764
|
-
slot: "docblocks/presence-range",
|
|
28765
|
-
blockId: segment.blockId,
|
|
28766
|
-
...rect
|
|
28767
|
-
});
|
|
28768
|
-
});
|
|
28769
|
-
}
|
|
28770
|
-
}
|
|
28771
|
-
setBoxes((prev) => JSON.stringify(prev) === JSON.stringify(next) ? prev : next);
|
|
28772
|
-
};
|
|
28773
|
-
let frame = 0;
|
|
28774
|
-
const scheduleRecompute = () => {
|
|
28775
|
-
cancelAnimationFrame(frame);
|
|
28776
|
-
frame = requestAnimationFrame(() => {
|
|
28777
|
-
frame = 0;
|
|
28778
|
-
recompute();
|
|
28779
|
-
});
|
|
28780
|
-
};
|
|
28781
|
-
recompute();
|
|
28782
|
-
scheduleRecompute();
|
|
28783
|
-
window.addEventListener("resize", scheduleRecompute);
|
|
28784
|
-
return () => {
|
|
28785
|
-
cancelAnimationFrame(frame);
|
|
28786
|
-
window.removeEventListener("resize", scheduleRecompute);
|
|
28787
|
-
};
|
|
28788
|
-
}, [
|
|
28789
|
-
presence,
|
|
28790
|
-
peerBlocks,
|
|
28791
|
-
doc,
|
|
28792
|
-
rootRef
|
|
28793
|
-
]);
|
|
28794
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: boxes.map((box) => /* @__PURE__ */ jsx("span", {
|
|
28795
|
-
"data-slot": box.slot,
|
|
28796
|
-
"data-peer": box.peerId,
|
|
28797
|
-
"data-range-block": box.blockId,
|
|
28798
|
-
className: box.slot === "docblocks/presence-caret" ? CARET_CLASS : RANGE_CLASS,
|
|
28799
|
-
style: {
|
|
28800
|
-
"--docblocks-presence-hue": peerHue(box.peerId),
|
|
28801
|
-
left: box.left,
|
|
28802
|
-
top: box.top,
|
|
28803
|
-
height: box.height,
|
|
28804
|
-
...box.width !== void 0 ? { width: box.width } : null
|
|
28805
|
-
}
|
|
28806
|
-
}, box.key)) });
|
|
28807
|
-
}
|
|
28808
|
-
//#endregion
|
|
28809
28941
|
//#region src/Editor.tsx
|
|
28810
28942
|
/**
|
|
28811
28943
|
* The document's editor: the root is the editor's focus/keyboard scope
|
|
@@ -28947,4 +29079,4 @@ function Editor({ view = "page", formatPalette = FORMAT_PALETTE }) {
|
|
|
28947
29079
|
});
|
|
28948
29080
|
}
|
|
28949
29081
|
//#endregion
|
|
28950
|
-
export { PopoverHeader as $, SelectTrigger as A, isBridgeErrorCode as At, DropdownMenuRadioGroup as B, useMeta as Bt, SelectContent as C, BASELINE_CAPABILITIES as Ct, SelectScrollDownButton as D, BRIDGE_ERROR_CODES as Dt, SelectLabel as E, validateProtocolDescriptor as Et, DropdownMenuContent as F, columnAnnotation as Ft, DropdownMenuSubContent as G,
|
|
29082
|
+
export { PopoverHeader as $, SelectTrigger as A, isBridgeErrorCode as At, DropdownMenuRadioGroup as B, useMeta as Bt, SelectContent as C, BASELINE_CAPABILITIES as Ct, SelectScrollDownButton as D, BRIDGE_ERROR_CODES as Dt, SelectLabel as E, validateProtocolDescriptor as Et, DropdownMenuContent as F, columnAnnotation as Ft, DropdownMenuSubContent as G, useText as Gt, DropdownMenuSeparator as H, usePresenceSharing as Ht, DropdownMenuGroup as I, ScrollArea as It, Separator as J, blobSubtypeOf as Jt, DropdownMenuSubTrigger as K, INLINE_CODE_CLASS as Kt, DropdownMenuItem as L, Input as Lt, Label as M, conditionMatches as Mt, DropdownMenu as N, parseCellValue as Nt, SelectScrollUpButton as O, BridgeError as Ot, DropdownMenuCheckboxItem as P, TEXT_TYPES as Pt, PopoverDescription as Q, DropdownMenuLabel as R, useBlock as Rt, Select as S, AlertTitle as St, SelectItem as T, isDatabaseBridge as Tt, DropdownMenuShortcut as U, useSelection as Ut, DropdownMenuRadioItem as V, usePresence as Vt, DropdownMenuSub as W, useSelectionView as Wt, PopoverAnchor as X, Popover as Y, createMemoryBlobStore as Yt, PopoverContent as Z, FilterBuilder as _, BareSelect as _t, SHAPE_FONT_STACKS as a, CardAction as at, Skeleton as b, Alert as bt, textOf as c, CardFooter as ct, useEditorContext as d, Pen as dt, PopoverTitle as et, DEFAULT_EMBED_SANDBOX as f, Eye as ft, newMutationContext as g, buttonVariants as gt, cellValueEqual as h, Button as ht, SHAPE_FONT_OPTIONS as i, Card as it, SelectValue as j, toBridgeError as jt, SelectSeparator as k, isBridgeError as kt, markToggleAction as l, CardHeader as lt, useDatabaseTable as m, BookOpen as mt, childrenSlot as n, DocblocksI18nProvider as nt, FORMAT_PALETTE as o, CardContent as ot, GridBody as p, CodeXml as pt, DropdownMenuTrigger as q, renderRuns as qt, CodeHeader as r, Checkbox as rt, TextBlock as s, CardDescription as st, Editor as t, PopoverTrigger as tt, EditorProvider as u, CardTitle as ut, emptyFilter as v, Badge as vt, SelectGroup as w, PROTOCOL_VERSION as wt, SegmentSelector as x, AlertDescription as xt, filterIsComplete as y, badgeVariants as yt, DropdownMenuPortal as z, useDoc as zt };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as BlockRendererMap } from "./index-YJcgf8Hh.mjs";
|
|
2
2
|
import { ComponentType, ReactNode } from "react";
|
|
3
|
-
import { Block, BlockContent, BlockId, BlockType, CanvasTextAttrs, Change, ConnectorAttrs, DatabaseFilterPayload, Doc, DocMeta, Geometry, PresenceState, Selection, SelectionView, ShapeAttrs, SyncEmbedRef, SyncProvider } from "docblocks-core";
|
|
3
|
+
import { Block, BlockContent, BlockId, BlockType, CanvasTextAttrs, Change, ConnectorAttrs, DatabaseFilterPayload, Doc, DocMeta, Geometry, PresenceSelection, PresenceState, Selection, SelectionView, ShapeAttrs, SyncEmbedRef, SyncProvider } from "docblocks-core";
|
|
4
4
|
import { StoreApi } from "zustand/vanilla";
|
|
5
5
|
//#region src/blob-store.d.ts
|
|
6
6
|
/**
|
|
@@ -796,6 +796,50 @@ declare function DatabaseTable({ bridge, tableId, readOnly, view, onViewChange }
|
|
|
796
796
|
*/
|
|
797
797
|
declare function useTableList(bridge: DatabaseBridge): UseTableList;
|
|
798
798
|
//#endregion
|
|
799
|
+
//#region src/presence/models.d.ts
|
|
800
|
+
/** The local selection's sharing grant: no sharing, sharing over the whole
|
|
801
|
+
* room (plaintext), or a targeted user-id list (encrypted per target). */
|
|
802
|
+
type PresenceSharingMode = "off" | "everyone" | string[];
|
|
803
|
+
/**
|
|
804
|
+
* The consumer's selection cipher (the privacy's stand): the library
|
|
805
|
+
* orchestrates the publish/receive pipeline and the consumer brings the
|
|
806
|
+
* keys. An entry is an opaque string — the algorithm is the consumer's
|
|
807
|
+
* choice.
|
|
808
|
+
*/
|
|
809
|
+
interface PresenceCrypto {
|
|
810
|
+
/**
|
|
811
|
+
* Encrypt the local selection for the grant: one opaque entry per target
|
|
812
|
+
* (grant order; no target id inside an entry — the recipient tries each
|
|
813
|
+
* entry with its own `decrypt`).
|
|
814
|
+
*/
|
|
815
|
+
encrypt(selection: PresenceSelection, grant: string[]): Promise<string[]>;
|
|
816
|
+
/** Try to decrypt a received entry for the local user. Null when the entry is not addressed to the local user. */
|
|
817
|
+
decrypt(entry: string): Promise<PresenceSelection | null>;
|
|
818
|
+
}
|
|
819
|
+
/** One live roster entry: the peer's identity and whether its selection stands visible to the local user. */
|
|
820
|
+
interface PresencePeer {
|
|
821
|
+
peerId: string;
|
|
822
|
+
userId?: string;
|
|
823
|
+
name?: string;
|
|
824
|
+
updatedAt: number;
|
|
825
|
+
/** True when the peer's selection is accepted locally (decrypted, everyone, or legacy plaintext). */
|
|
826
|
+
selectionVisible: boolean;
|
|
827
|
+
}
|
|
828
|
+
/** The presence sharing's session state (per EditorProvider instance). */
|
|
829
|
+
interface PresenceSharing {
|
|
830
|
+
/** The local grant (session state; the default "off"; not persisted, not on the wire). */
|
|
831
|
+
sharing: PresenceSharingMode;
|
|
832
|
+
/** Set the local grant. A no-op over the read-only modes. */
|
|
833
|
+
setSharing: (mode: PresenceSharingMode) => void;
|
|
834
|
+
/** The live roster of the connected peers (the identity frames are always published). */
|
|
835
|
+
peers: PresencePeer[];
|
|
836
|
+
}
|
|
837
|
+
/** The presence identity's wire payload: the stable user id + the optional display label. */
|
|
838
|
+
interface PresenceIdentity {
|
|
839
|
+
userId?: string;
|
|
840
|
+
name?: string;
|
|
841
|
+
}
|
|
842
|
+
//#endregion
|
|
799
843
|
//#region src/canvas/clipboard.d.ts
|
|
800
844
|
/** The one copied canvas item (the deep data; the ids stand the originals). */
|
|
801
845
|
type ClipboardItem = {
|
|
@@ -872,6 +916,12 @@ interface EditorState {
|
|
|
872
916
|
collapsedIds: BlockId[];
|
|
873
917
|
/** Remote peers' ephemeral state; a separate slice for the same reason. */
|
|
874
918
|
presence: PresenceState[];
|
|
919
|
+
/**
|
|
920
|
+
* The local selection's sharing grant (session state per provider
|
|
921
|
+
* instance; the default "off" — not document data, never on the wire).
|
|
922
|
+
* A separate slice so a grant change never re-renders blocks.
|
|
923
|
+
*/
|
|
924
|
+
sharing: PresenceSharingMode;
|
|
875
925
|
/**
|
|
876
926
|
* The canvas clipboard: the deep copy of
|
|
877
927
|
* the copied canvas items — the view state per editor (not doc data, no
|
|
@@ -886,6 +936,8 @@ interface EditorActions {
|
|
|
886
936
|
applyChange: (change: Change) => void;
|
|
887
937
|
/** Replaces the presence slice. */
|
|
888
938
|
setPresence: (peers: PresenceState[]) => void;
|
|
939
|
+
/** Sets the local sharing grant. */
|
|
940
|
+
setSharing: (mode: PresenceSharingMode) => void;
|
|
889
941
|
/** Replaces the selection slice. */
|
|
890
942
|
setSelection: (selection: Selection) => void;
|
|
891
943
|
/** Sets the collapsed state of one list item (view state, no doc command). */
|
|
@@ -937,6 +989,27 @@ interface MentionProvider {
|
|
|
937
989
|
interface SelectionRef {
|
|
938
990
|
view: SelectionView | null;
|
|
939
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* The local user's presence identity and the selection-sharing cipher
|
|
994
|
+
* (consumer session state — never document data). Without `userId` the
|
|
995
|
+
* peer is unaddressable: targeted sharing stands unavailable (the roster
|
|
996
|
+
* still lists the peer by its transport-level `peerId`). Without `crypto`
|
|
997
|
+
* the grant degrades to "off" / "everyone" (a targeted grant is a no-op).
|
|
998
|
+
*/
|
|
999
|
+
interface PresenceConfig {
|
|
1000
|
+
/** The stable user id — the addressable identity on the wire. */
|
|
1001
|
+
userId: string;
|
|
1002
|
+
/** The roster display label (optional). */
|
|
1003
|
+
name?: string;
|
|
1004
|
+
/**
|
|
1005
|
+
* The selection cipher (the targeted's stand): the local selection is
|
|
1006
|
+
* encrypted per target for a targeted grant, and a received ciphertext
|
|
1007
|
+
* entry is decrypted with the local user's keys. Opaque strings
|
|
1008
|
+
* throughout — the algorithm is the consumer's choice. Without it, only
|
|
1009
|
+
* "off" / "everyone" are available.
|
|
1010
|
+
*/
|
|
1011
|
+
crypto?: PresenceCrypto;
|
|
1012
|
+
}
|
|
940
1013
|
/** The consumer's user access level: the available document modes. "viewer" is locked to the published's view (the current state is never rendered); "editor" (the default) may edit and view. Session state — not document data, never on the wire. */
|
|
941
1014
|
type AccessLevel = "viewer" | "editor";
|
|
942
1015
|
/** The document's mode (consumer-controlled view state, like the page/canvas `view`): "edit" (the default) is the live document editable; "view" is the live document read-only (the current state, still syncing); "published" is the `publishedDoc`'s snapshot read-only (static). */
|
|
@@ -977,6 +1050,8 @@ interface EditorContextValue {
|
|
|
977
1050
|
mode: EditorMode;
|
|
978
1051
|
/** True over the non-edit modes ("view" / "published"): every editing surface gates off (the read-only stand). */
|
|
979
1052
|
viewMode: boolean;
|
|
1053
|
+
/** The presence identity + cipher (the provider's `presence` prop); null when none was supplied (the legacy's anonymous stand). */
|
|
1054
|
+
presence: PresenceConfig | null;
|
|
980
1055
|
}
|
|
981
1056
|
interface EditorProviderProps {
|
|
982
1057
|
/**
|
|
@@ -1119,6 +1194,22 @@ interface EditorProviderProps {
|
|
|
1119
1194
|
* inherits the enclosing value, like the resolvers.
|
|
1120
1195
|
*/
|
|
1121
1196
|
commandPalettePlaceholder?: string;
|
|
1197
|
+
/**
|
|
1198
|
+
* Optional presence identity + the selection-sharing cipher (consumer
|
|
1199
|
+
* session state, never document data): the local user's stable `userId`
|
|
1200
|
+
* (the addressable identity on the wire), the optional roster label, and
|
|
1201
|
+
* the optional `PresenceCrypto` cipher (without it a targeted grant
|
|
1202
|
+
* degrades to "off" / "everyone": the hook's setter stands a no-op over
|
|
1203
|
+
* a targeted grant without the identity or the cipher). With the prop
|
|
1204
|
+
* the selection sharing grant is session state
|
|
1205
|
+
* driven through `usePresenceSharing()` (the default "off" — the
|
|
1206
|
+
* privacy stand); without it the provider keeps the legacy stand (the
|
|
1207
|
+
* anonymous plaintext selection — always shared over the room).
|
|
1208
|
+
*
|
|
1209
|
+
* Like the other customization values, the value is read per render:
|
|
1210
|
+
* pass a stable reference.
|
|
1211
|
+
*/
|
|
1212
|
+
presence?: PresenceConfig;
|
|
1122
1213
|
children: ReactNode;
|
|
1123
1214
|
}
|
|
1124
1215
|
//#endregion
|
|
@@ -1150,4 +1241,4 @@ declare const DEFAULT_EMBED_SANDBOX = "allow-scripts allow-forms allow-popups";
|
|
|
1150
1241
|
/** Read the per-instance editor context; throws outside an EditorProvider. */
|
|
1151
1242
|
declare function useEditorContext(): EditorContextValue;
|
|
1152
1243
|
//#endregion
|
|
1153
|
-
export {
|
|
1244
|
+
export { ProtocolLimits as $, filterIsComplete as A, DatabaseBridge as B, DatabaseTableView as C, BlobSubtype as Ct, DatabaseTableRow as D, DatabaseTablePhase as E, BridgeTableDescription as F, IsoInstant as G, FilterConditionInput as H, BridgeValueInput as I, OperationStatus as J, MutationContext as K, CellMap as L, BatchOp as M, BridgeColumn as N, UseDatabaseTable as O, BridgeColumnAnnotation as P, ProtocolDescriptor as Q, CellValueParse as R, DatabaseTableProps as S, BlobStore as St, useDatabaseTable as T, createMemoryBlobStore as Tt, FilterInput as U, DomainEvent as V, Id as W, PageSnapshot as X, PageQuery as Y, ProtocolCapability as Z, PresencePeer as _, isBridgeError as _t, EditorContextValue as a, SortInput as at, useTableList as b, BlobMeta as bt, EmbedSandboxPolicy as c, SubscriptionStatus as ct, PdfPreview as d, TableEvent as dt, QueryOperator as et, PdfPreviewProps as f, TableRef as ft, PresenceIdentity as g, BridgeErrorCode as gt, PresenceCrypto as h, BridgeError as ht, AccessLevel as i, RowWire as it, FilterBuilder as j, emptyFilter as k, MentionEntry as l, TableDescription as lt, SyncEmbedResolver as m, BRIDGE_ERROR_CODES as mt, DEFAULT_EMBED_SANDBOX as n, Revision as nt, EditorMode as o, SubscriptionHandle as ot, PresenceConfig as p, TableSnapshot as pt, MutationReceipt as q, EditorProvider as r, RowPage as rt, EditorProviderProps as s, SubscriptionObserver as st, useEditorContext as t, QuerySemantics as tt, MentionProvider as u, TableEnvelopeBase as ut, PresenceSharing as v, isBridgeErrorCode as vt, UseTableList as w, blobSubtypeOf as wt, DatabaseTable as x, BlobRef as xt, PresenceSharingMode as y, toBridgeError as yt, Column as z };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as BlockRendererMap, r as BlockRendererProps, t as BlockRenderer } from "./index-YJcgf8Hh.mjs";
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as ProtocolLimits, A as filterIsComplete, B as DatabaseBridge, C as DatabaseTableView, Ct as BlobSubtype, D as DatabaseTableRow, E as DatabaseTablePhase, F as BridgeTableDescription, G as IsoInstant, H as FilterConditionInput, I as BridgeValueInput, J as OperationStatus, K as MutationContext, L as CellMap, M as BatchOp, N as BridgeColumn, O as UseDatabaseTable, P as BridgeColumnAnnotation, Q as ProtocolDescriptor, R as CellValueParse, S as DatabaseTableProps, St as BlobStore, T as useDatabaseTable, Tt as createMemoryBlobStore, U as FilterInput, V as DomainEvent, W as Id, X as PageSnapshot, Y as PageQuery, Z as ProtocolCapability, _ as PresencePeer, _t as isBridgeError, a as EditorContextValue, at as SortInput, b as useTableList, bt as BlobMeta, c as EmbedSandboxPolicy, ct as SubscriptionStatus, d as PdfPreview, dt as TableEvent, et as QueryOperator, f as PdfPreviewProps, ft as TableRef, g as PresenceIdentity, gt as BridgeErrorCode, h as PresenceCrypto, ht as BridgeError, i as AccessLevel, it as RowWire, j as FilterBuilder, k as emptyFilter, l as MentionEntry, lt as TableDescription, m as SyncEmbedResolver, mt as BRIDGE_ERROR_CODES, n as DEFAULT_EMBED_SANDBOX, nt as Revision, o as EditorMode, ot as SubscriptionHandle, p as PresenceConfig, pt as TableSnapshot, q as MutationReceipt, r as EditorProvider, rt as RowPage, s as EditorProviderProps, st as SubscriptionObserver, t as useEditorContext, tt as QuerySemantics, u as MentionProvider, ut as TableEnvelopeBase, v as PresenceSharing, vt as isBridgeErrorCode, w as UseTableList, wt as blobSubtypeOf, x as DatabaseTable, xt as BlobRef, y as PresenceSharingMode, yt as toBridgeError, z as Column } from "./index-DG0lJy_t.mjs";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { ComponentType, ReactNode } from "react";
|
|
5
5
|
import { Block, BlockId, BlockType, Doc, DocMeta, PresenceState, Selection, SelectionView, SyncEmbedRef, TextRun } from "docblocks-core";
|
|
@@ -193,6 +193,14 @@ declare function useText(id: BlockId): string;
|
|
|
193
193
|
declare function useSelection(): Selection;
|
|
194
194
|
/** Remote peers' ephemeral state; a separate slice for the same reason. */
|
|
195
195
|
declare function usePresence(): PresenceState[];
|
|
196
|
+
/**
|
|
197
|
+
* The presence sharing's session state (per provider instance): the local
|
|
198
|
+
* grant (the default "off" — the privacy stand), the grant's setter (a
|
|
199
|
+
* no-op over the read-only stand), and the live roster of the connected
|
|
200
|
+
* peers (the identity frames are always present, the selection only over
|
|
201
|
+
* the grant it stands shared).
|
|
202
|
+
*/
|
|
203
|
+
declare function usePresenceSharing(): PresenceSharing;
|
|
196
204
|
/** The document meta; the reference is replaced only when the title changed. */
|
|
197
205
|
declare function useMeta(): DocMeta;
|
|
198
206
|
/**
|
|
@@ -286,7 +294,7 @@ declare function AlertDescription({ className, ...props }: React.ComponentProps<
|
|
|
286
294
|
//#endregion
|
|
287
295
|
//#region src/shared/ui/Badge.d.ts
|
|
288
296
|
declare const badgeVariants: (props?: ({
|
|
289
|
-
variant?: "default" | "
|
|
297
|
+
variant?: "default" | "custom" | "secondary" | "destructive" | "outline" | null | undefined;
|
|
290
298
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
291
299
|
declare function Badge({ className, variant, asChild, ...props }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & {
|
|
292
300
|
asChild?: boolean;
|
|
@@ -297,7 +305,7 @@ declare function BareSelect({ className, children, ...props }: React.ComponentPr
|
|
|
297
305
|
//#endregion
|
|
298
306
|
//#region src/shared/ui/Button.d.ts
|
|
299
307
|
declare const buttonVariants: (props?: ({
|
|
300
|
-
variant?: "link" | "default" | "
|
|
308
|
+
variant?: "link" | "default" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
|
|
301
309
|
size?: "default" | "icon" | "sm" | "lg" | "xl" | null | undefined;
|
|
302
310
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
303
311
|
declare function Button({ className, variant, size, asChild, type, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
|
|
@@ -431,4 +439,4 @@ declare function TooltipContent({ className, sideOffset, children, ...props }: R
|
|
|
431
439
|
//#region src/shared/ui/cn.d.ts
|
|
432
440
|
declare function cn(...inputs: ClassValue[]): string;
|
|
433
441
|
//#endregion
|
|
434
|
-
export { type AccessLevel, Alert, AlertDescription, AlertTitle, BASELINE_CAPABILITIES, BRIDGE_ERROR_CODES, Badge, BareSelect, type BatchOp, type BlobMeta, type BlobRef, type BlobStore, type BlobSubtype, type BlockRenderer, type BlockRendererMap, type BlockRendererProps, type BridgeColumn, type BridgeColumnAnnotation, BridgeError, type BridgeErrorCode, type BridgeTableDescription, type BridgeValueInput, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CellMap, type CellValueParse, Checkbox, type Column, DEFAULT_EMBED_SANDBOX, type DatabaseBridge, DatabaseTable, type DatabaseTablePhase, type DatabaseTableProps, type DatabaseTableRow, type DatabaseTableView, DocHeader, type DomainEvent, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Editor, type EditorContextValue, type EditorMode, EditorProvider, type EditorProviderProps, type EditorView, type EmbedSandboxPolicy, FORMAT_PALETTE, FilterBuilder, type FilterConditionInput, type FilterInput, type FormatMark, type Id, Input, type IsoInstant, Label, MONO_FONT_STACK, type MentionEntry, type MentionProvider, type MutationContext, type MutationReceipt, type OperationStatus, PROTOCOL_VERSION, type PageQuery, type PageSnapshot, type PdfPreview, type PdfPreviewProps, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, type ProtocolCapability, type ProtocolDescriptor, type ProtocolLimits, type QueryOperator, type QuerySemantics, type Revision, type RowPage, type RowWire, SHAPE_FONT_OPTIONS, SHAPE_FONT_STACKS, ScrollArea, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Skeleton, type SortInput, type SubscriptionHandle, type SubscriptionObserver, type SubscriptionStatus, type SyncEmbedRef, type SyncEmbedResolver, TEXT_TYPES, type TableDescription, type TableEnvelopeBase, type TableEvent, type TableRef, type TableSnapshot, Tabs, TabsContent, TabsList, TabsTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UI_FONT_ID, UI_FONT_STACK, type UseDatabaseTable, type UseTableList, badgeVariants, blobSubtypeOf, buttonVariants, cellValueEqual, cn, columnAnnotation, conditionMatches, createMemoryBlobStore, emptyFilter, filterIsComplete, isBridgeError, isBridgeErrorCode, isDatabaseBridge, markToggleAction, newMutationContext, parseCellValue, renderRuns, toBridgeError, useBlock, useDatabaseTable, useDoc, useEditorContext, useMeta, usePresence, useSelection, useSelectionView, useTableList, useText, validateProtocolDescriptor };
|
|
442
|
+
export { type AccessLevel, Alert, AlertDescription, AlertTitle, BASELINE_CAPABILITIES, BRIDGE_ERROR_CODES, Badge, BareSelect, type BatchOp, type BlobMeta, type BlobRef, type BlobStore, type BlobSubtype, type BlockRenderer, type BlockRendererMap, type BlockRendererProps, type BridgeColumn, type BridgeColumnAnnotation, BridgeError, type BridgeErrorCode, type BridgeTableDescription, type BridgeValueInput, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CellMap, type CellValueParse, Checkbox, type Column, DEFAULT_EMBED_SANDBOX, type DatabaseBridge, DatabaseTable, type DatabaseTablePhase, type DatabaseTableProps, type DatabaseTableRow, type DatabaseTableView, DocHeader, type DomainEvent, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Editor, type EditorContextValue, type EditorMode, EditorProvider, type EditorProviderProps, type EditorView, type EmbedSandboxPolicy, FORMAT_PALETTE, FilterBuilder, type FilterConditionInput, type FilterInput, type FormatMark, type Id, Input, type IsoInstant, Label, MONO_FONT_STACK, type MentionEntry, type MentionProvider, type MutationContext, type MutationReceipt, type OperationStatus, PROTOCOL_VERSION, type PageQuery, type PageSnapshot, type PdfPreview, type PdfPreviewProps, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, type PresenceConfig, type PresenceCrypto, type PresenceIdentity, type PresencePeer, type PresenceSharing, type PresenceSharingMode, type ProtocolCapability, type ProtocolDescriptor, type ProtocolLimits, type QueryOperator, type QuerySemantics, type Revision, type RowPage, type RowWire, SHAPE_FONT_OPTIONS, SHAPE_FONT_STACKS, ScrollArea, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Skeleton, type SortInput, type SubscriptionHandle, type SubscriptionObserver, type SubscriptionStatus, type SyncEmbedRef, type SyncEmbedResolver, TEXT_TYPES, type TableDescription, type TableEnvelopeBase, type TableEvent, type TableRef, type TableSnapshot, Tabs, TabsContent, TabsList, TabsTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UI_FONT_ID, UI_FONT_STACK, type UseDatabaseTable, type UseTableList, badgeVariants, blobSubtypeOf, buttonVariants, cellValueEqual, cn, columnAnnotation, conditionMatches, createMemoryBlobStore, emptyFilter, filterIsComplete, isBridgeError, isBridgeErrorCode, isDatabaseBridge, markToggleAction, newMutationContext, parseCellValue, renderRuns, toBridgeError, useBlock, useDatabaseTable, useDoc, useEditorContext, useMeta, usePresence, usePresenceSharing, useSelection, useSelectionView, useTableList, useText, validateProtocolDescriptor };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as PopoverHeader, A as SelectTrigger, At as isBridgeErrorCode, B as DropdownMenuRadioGroup, Bt as useMeta, C as SelectContent, Ct as BASELINE_CAPABILITIES, D as SelectScrollDownButton, Dt as BRIDGE_ERROR_CODES, E as SelectLabel, Et as validateProtocolDescriptor, F as DropdownMenuContent, Ft as columnAnnotation, G as DropdownMenuSubContent, H as DropdownMenuSeparator, Ht as
|
|
1
|
+
import { $ as PopoverHeader, A as SelectTrigger, At as isBridgeErrorCode, B as DropdownMenuRadioGroup, Bt as useMeta, C as SelectContent, Ct as BASELINE_CAPABILITIES, D as SelectScrollDownButton, Dt as BRIDGE_ERROR_CODES, E as SelectLabel, Et as validateProtocolDescriptor, F as DropdownMenuContent, Ft as columnAnnotation, G as DropdownMenuSubContent, Gt as useText, H as DropdownMenuSeparator, Ht as usePresenceSharing, I as DropdownMenuGroup, It as ScrollArea, J as Separator, Jt as blobSubtypeOf, K as DropdownMenuSubTrigger, L as DropdownMenuItem, Lt as Input, M as Label, Mt as conditionMatches, N as DropdownMenu, Nt as parseCellValue, O as SelectScrollUpButton, Ot as BridgeError, P as DropdownMenuCheckboxItem, Pt as TEXT_TYPES, Q as PopoverDescription, R as DropdownMenuLabel, Rt as useBlock, S as Select, St as AlertTitle, T as SelectItem, Tt as isDatabaseBridge, U as DropdownMenuShortcut, Ut as useSelection, V as DropdownMenuRadioItem, Vt as usePresence, W as DropdownMenuSub, Wt as useSelectionView, X as PopoverAnchor, Y as Popover, Yt as createMemoryBlobStore, Z as PopoverContent, _ as FilterBuilder, _t as BareSelect, a as SHAPE_FONT_STACKS, at as CardAction, b as Skeleton, bt as Alert, ct as CardFooter, d as useEditorContext, dt as Pen, et as PopoverTitle, f as DEFAULT_EMBED_SANDBOX, ft as Eye, g as newMutationContext, gt as buttonVariants, h as cellValueEqual, ht as Button, i as SHAPE_FONT_OPTIONS, it as Card, j as SelectValue, jt as toBridgeError, k as SelectSeparator, kt as isBridgeError, l as markToggleAction, lt as CardHeader, m as useDatabaseTable, mt as BookOpen, nt as DocblocksI18nProvider, o as FORMAT_PALETTE, ot as CardContent, p as GridBody, q as DropdownMenuTrigger, qt as renderRuns, rt as Checkbox, st as CardDescription, t as Editor, tt as PopoverTrigger, u as EditorProvider, ut as CardTitle, v as emptyFilter, vt as Badge, w as SelectGroup, wt as PROTOCOL_VERSION, x as SegmentSelector, xt as AlertDescription, y as filterIsComplete, yt as badgeVariants, z as DropdownMenuPortal, zt as useDoc } from "./Editor-BvKqUGXH.mjs";
|
|
2
2
|
import { c as cn, o as preventDefault } from "./constants-D4FTL0ph.mjs";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -406,16 +406,20 @@ function DatabaseTable({ bridge, tableId, readOnly = false, view, onViewChange }
|
|
|
406
406
|
}));
|
|
407
407
|
if (onViewChange !== void 0) onViewChange(typed);
|
|
408
408
|
}, [onViewChange]);
|
|
409
|
-
return /* @__PURE__ */ jsx(DocblocksI18nProvider, { children: /* @__PURE__ */ jsx(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
409
|
+
return /* @__PURE__ */ jsx(DocblocksI18nProvider, { children: /* @__PURE__ */ jsx("div", {
|
|
410
|
+
"data-slot": "docblocks/editor",
|
|
411
|
+
"data-readonly": readOnly ? "true" : void 0,
|
|
412
|
+
children: /* @__PURE__ */ jsx(GridBody, {
|
|
413
|
+
bridge,
|
|
414
|
+
tableId,
|
|
415
|
+
filter: viewState.filter,
|
|
416
|
+
columnOrder: viewState.columnOrder,
|
|
417
|
+
hiddenColumns: viewState.hiddenColumns,
|
|
418
|
+
columnWidths: viewState.columnWidths,
|
|
419
|
+
onViewChange: handleViewChange,
|
|
420
|
+
readColumnOrder: () => viewRef.current.columnOrder,
|
|
421
|
+
readOnly
|
|
422
|
+
})
|
|
419
423
|
}) });
|
|
420
424
|
}
|
|
421
425
|
//#endregion
|
|
@@ -748,4 +752,4 @@ const UI_FONT_STACK = "'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif
|
|
|
748
752
|
*/
|
|
749
753
|
const MONO_FONT_STACK = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
|
|
750
754
|
//#endregion
|
|
751
|
-
export { Alert, AlertDescription, AlertTitle, BASELINE_CAPABILITIES, BRIDGE_ERROR_CODES, Badge, BareSelect, BridgeError, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DEFAULT_EMBED_SANDBOX, DatabaseTable, DocHeader, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Editor, EditorProvider, FORMAT_PALETTE, FilterBuilder, Input, Label, MONO_FONT_STACK, PROTOCOL_VERSION, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, SHAPE_FONT_OPTIONS, SHAPE_FONT_STACKS, ScrollArea, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Skeleton, TEXT_TYPES, Tabs, TabsContent, TabsList, TabsTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UI_FONT_ID, UI_FONT_STACK, badgeVariants, blobSubtypeOf, buttonVariants, cellValueEqual, cn, columnAnnotation, conditionMatches, createMemoryBlobStore, emptyFilter, filterIsComplete, isBridgeError, isBridgeErrorCode, isDatabaseBridge, markToggleAction, newMutationContext, parseCellValue, renderRuns, toBridgeError, useBlock, useDatabaseTable, useDoc, useEditorContext, useMeta, usePresence, useSelection, useSelectionView, useTableList, useText, validateProtocolDescriptor };
|
|
755
|
+
export { Alert, AlertDescription, AlertTitle, BASELINE_CAPABILITIES, BRIDGE_ERROR_CODES, Badge, BareSelect, BridgeError, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DEFAULT_EMBED_SANDBOX, DatabaseTable, DocHeader, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Editor, EditorProvider, FORMAT_PALETTE, FilterBuilder, Input, Label, MONO_FONT_STACK, PROTOCOL_VERSION, Popover, PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, SHAPE_FONT_OPTIONS, SHAPE_FONT_STACKS, ScrollArea, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Skeleton, TEXT_TYPES, Tabs, TabsContent, TabsList, TabsTrigger, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UI_FONT_ID, UI_FONT_STACK, badgeVariants, blobSubtypeOf, buttonVariants, cellValueEqual, cn, columnAnnotation, conditionMatches, createMemoryBlobStore, emptyFilter, filterIsComplete, isBridgeError, isBridgeErrorCode, isDatabaseBridge, markToggleAction, newMutationContext, parseCellValue, renderRuns, toBridgeError, useBlock, useDatabaseTable, useDoc, useEditorContext, useMeta, usePresence, usePresenceSharing, useSelection, useSelectionView, useTableList, useText, validateProtocolDescriptor };
|
package/dist/mermaid/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Kt as INLINE_CODE_CLASS, St as AlertTitle, b as Skeleton, bt as Alert, c as textOf, d as useEditorContext, ft as Eye, n as childrenSlot, pt as CodeXml, r as CodeHeader, s as TextBlock, x as SegmentSelector, xt as AlertDescription } from "../Editor-BvKqUGXH.mjs";
|
|
2
2
|
import { n as TriangleAlert } from "../constants-D4FTL0ph.mjs";
|
|
3
3
|
import { t as LoaderCircle } from "../LoaderCircle-AGVFE4IP.mjs";
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
package/dist/pdf/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { f as PdfPreviewProps } from "../index-
|
|
1
|
+
import { f as PdfPreviewProps } from "../index-DG0lJy_t.mjs";
|
|
2
2
|
//#region src/pdf/PdfFirstPage.d.ts
|
|
3
3
|
declare function PdfFirstPage({ id, url, name }: PdfPreviewProps): import("react").JSX.Element;
|
|
4
4
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docblocks-react-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "React rendering + editing layer for docblocks: hooks, block renderers, caret/selection, keyboard, clipboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"docblocks-core": "^0.1.
|
|
37
|
+
"docblocks-core": "^0.1.3",
|
|
38
38
|
"framer-motion": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
|
|
39
39
|
"mermaid": "^12.0.0",
|
|
40
40
|
"pdfjs-dist": "^6.0.0",
|