@1agh/maude 1.4.5 → 1.5.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/apps/studio/annotations/ai-read.ts +288 -0
- package/apps/studio/annotations/ai-write.ts +533 -0
- package/apps/studio/annotations/board-io.ts +94 -0
- package/apps/studio/annotations/board-text.ts +45 -0
- package/apps/studio/annotations/constants.ts +58 -0
- package/apps/studio/annotations/elements/_shared.ts +125 -0
- package/apps/studio/annotations/elements/arrow.model.ts +196 -0
- package/apps/studio/annotations/elements/media.model.ts +79 -0
- package/apps/studio/annotations/elements/pen.model.ts +82 -0
- package/apps/studio/annotations/elements/section.model.ts +70 -0
- package/apps/studio/annotations/elements/shape.model.ts +141 -0
- package/apps/studio/annotations/elements/sticky.model.ts +48 -0
- package/apps/studio/annotations/elements/text.model.ts +52 -0
- package/apps/studio/annotations/fields.ts +347 -0
- package/apps/studio/annotations/fractional-index.ts +234 -0
- package/apps/studio/annotations/legacy/mini-dom.ts +207 -0
- package/apps/studio/annotations/migrate-boot.ts +172 -0
- package/apps/studio/annotations/migrate-cli.ts +37 -0
- package/apps/studio/annotations/migrate-v1.ts +383 -0
- package/apps/studio/annotations/ops.ts +474 -0
- package/apps/studio/annotations/registry.ts +159 -0
- package/apps/studio/annotations/replica.ts +264 -0
- package/apps/studio/annotations/scene.ts +235 -0
- package/apps/studio/annotations/schema.ts +188 -0
- package/apps/studio/annotations/types.ts +77 -0
- package/apps/studio/annotations/ui/board.ts +126 -0
- package/apps/studio/annotations/ui/containment.ts +118 -0
- package/apps/studio/annotations/ui/edit-actions.ts +551 -0
- package/apps/studio/annotations/ui/editor-channel.ts +62 -0
- package/apps/studio/annotations/ui/element-node.tsx +993 -0
- package/apps/studio/annotations/ui/pipeline-context.ts +17 -0
- package/apps/studio/annotations/ui/pointer-pipeline.ts +150 -0
- package/apps/studio/annotations/ui/render-model.ts +264 -0
- package/apps/studio/annotations/ui/scene.tsx +63 -0
- package/apps/studio/annotations/ui/text-editor.tsx +420 -0
- package/apps/studio/annotations/ui/text-session.ts +140 -0
- package/apps/studio/annotations/ui/text-style.ts +111 -0
- package/apps/studio/annotations/ui/world.ts +47 -0
- package/apps/studio/annotations/v1-adapter.ts +428 -0
- package/apps/studio/annotations-align.ts +21 -6
- package/apps/studio/annotations-bindings.ts +2 -0
- package/apps/studio/annotations-context-toolbar.tsx +9 -13
- package/apps/studio/annotations-groups.ts +3 -0
- package/apps/studio/annotations-layer.tsx +923 -1925
- package/apps/studio/annotations-model.ts +97 -4
- package/apps/studio/annotations-sync.ts +4 -47
- package/apps/studio/api.ts +262 -68
- package/apps/studio/bin/_import-figma.mjs +22 -12
- package/apps/studio/bin/annotate.mjs +331 -838
- package/apps/studio/bin/annotate.sh +4 -4
- package/apps/studio/bin/perf.sh +21 -7
- package/apps/studio/bin/read-annotations.mjs +184 -666
- package/apps/studio/bin/read-annotations.sh +9 -5
- package/apps/studio/canvas-artifacts.ts +9 -0
- package/apps/studio/canvas-comment-mount.tsx +35 -2
- package/apps/studio/canvas-lib.tsx +18 -1
- package/apps/studio/canvas-shell.tsx +54 -1
- package/apps/studio/client/app.jsx +101 -35
- package/apps/studio/client/comments-overlay.css +10 -0
- package/apps/studio/client/hmr.mjs +1 -1
- package/apps/studio/client/panels/git-grouping.js +2 -2
- package/apps/studio/client/tree-expansion.js +217 -0
- package/apps/studio/collab/index.ts +58 -5
- package/apps/studio/collab/persistence.ts +80 -11
- package/apps/studio/collab/registry.ts +63 -26
- package/apps/studio/commands/annotation-ops-command.ts +72 -0
- package/apps/studio/comment-anchor.ts +116 -0
- package/apps/studio/comments-overlay.tsx +78 -35
- package/apps/studio/context.ts +1 -0
- package/apps/studio/cursors-overlay.tsx +337 -109
- package/apps/studio/dist/client.bundle.js +850 -850
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/figma/to-strokes.ts +31 -10
- package/apps/studio/git/endpoints.ts +1 -1
- package/apps/studio/git/service.ts +1 -1
- package/apps/studio/git/watch.ts +1 -1
- package/apps/studio/http.ts +90 -15
- package/apps/studio/server.ts +16 -0
- package/apps/studio/sync/accepted-cold-start.ts +72 -8
- package/apps/studio/sync/agent.ts +36 -6
- package/apps/studio/sync/codec.ts +95 -40
- package/apps/studio/sync/comment-ledger.ts +229 -0
- package/apps/studio/sync/file-membership.ts +12 -2
- package/apps/studio/sync/file-plane.ts +78 -2
- package/apps/studio/sync/index.ts +75 -16
- package/apps/studio/sync/journal-client.ts +5 -0
- package/apps/studio/sync/limits.ts +7 -2
- package/apps/studio/sync/migrate-seed.ts +17 -7
- package/apps/studio/sync/projection.ts +7 -2
- package/apps/studio/sync/remote-docs.ts +34 -0
- package/apps/studio/sync/writer-registry.ts +10 -0
- package/apps/studio/text-caret.ts +11 -2
- package/apps/studio/tree-state.ts +45 -0
- package/apps/studio/undo-stack.ts +2 -2
- package/apps/studio/use-annotation-resize.tsx +48 -23
- package/apps/studio/use-annotation-selection.tsx +9 -2
- package/apps/studio/use-collab.tsx +104 -1
- package/apps/studio/use-selection-set.tsx +4 -0
- package/apps/studio/whats-new.json +63 -0
- package/cli/lib/design-link.mjs +5 -1
- package/cli/lib/gitignore-block.mjs +1 -1
- package/cli/lib/gitignore-drift.mjs +2 -1
- package/package.json +9 -8
- package/plugins/design/templates/brief-board.tsx.template +1 -1
- package/apps/studio/annotation-edit-base.ts +0 -36
- package/apps/studio/commands/annotation-strokes-command.ts +0 -137
|
@@ -20,8 +20,14 @@
|
|
|
20
20
|
import type { JSX } from 'react';
|
|
21
21
|
import { memo, useEffect, useState } from 'react';
|
|
22
22
|
|
|
23
|
+
import { REPLICA_TYPE } from './annotations/replica.ts';
|
|
23
24
|
import { useLiveViewport } from './canvas-lib.tsx';
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
type AnnotationGesture,
|
|
27
|
+
type ForeignAwareness,
|
|
28
|
+
useCollab,
|
|
29
|
+
useForeignAwareness,
|
|
30
|
+
} from './use-collab.tsx';
|
|
25
31
|
|
|
26
32
|
const CURSOR_CSS = `
|
|
27
33
|
.dc-cursor-overlay {
|
|
@@ -77,6 +83,23 @@ const CURSOR_CSS = `
|
|
|
77
83
|
box-sizing: border-box;
|
|
78
84
|
will-change: transform, width, height;
|
|
79
85
|
}
|
|
86
|
+
.dc-peer-gesture {
|
|
87
|
+
position: absolute;
|
|
88
|
+
top: 0;
|
|
89
|
+
left: 0;
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
border: 1.5px dashed;
|
|
92
|
+
border-radius: 3px;
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
}
|
|
95
|
+
.dc-peer-gesture-ink {
|
|
96
|
+
position: absolute;
|
|
97
|
+
inset: 0;
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
overflow: visible;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
80
103
|
.dc-peer-selection__label {
|
|
81
104
|
position: absolute;
|
|
82
105
|
top: -18px;
|
|
@@ -111,6 +134,9 @@ function ensureCursorStyles(): void {
|
|
|
111
134
|
document.head.appendChild(s);
|
|
112
135
|
}
|
|
113
136
|
|
|
137
|
+
/** How often a peer halo re-measures when nothing it can observe changed. */
|
|
138
|
+
const HALO_REFRESH_MS = 500;
|
|
139
|
+
|
|
114
140
|
interface ViewportSnapshot {
|
|
115
141
|
x: number;
|
|
116
142
|
y: number;
|
|
@@ -122,39 +148,75 @@ interface CursorProps {
|
|
|
122
148
|
viewport: ViewportSnapshot;
|
|
123
149
|
}
|
|
124
150
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
// Issue #131 — awareness hands out a FRESH peer object on every change (every
|
|
152
|
+
// 30 Hz cursor publish of every peer), so the default shallow `memo` never
|
|
153
|
+
// held: each cursor move re-rendered every halo, and the halos measure the DOM
|
|
154
|
+
// in render. Each component compares only what it draws.
|
|
155
|
+
const sameViewport = (a: ViewportSnapshot, b: ViewportSnapshot): boolean =>
|
|
156
|
+
a === b || (a.x === b.x && a.y === b.y && a.zoom === b.zoom);
|
|
157
|
+
const sameLabel = (a: ForeignAwareness, b: ForeignAwareness): boolean =>
|
|
158
|
+
a.clientID === b.clientID && a.name === b.name && a.color === b.color;
|
|
159
|
+
function sameSelection(a: ForeignAwareness['selection'], b: ForeignAwareness['selection']) {
|
|
160
|
+
if (a === b) return true;
|
|
161
|
+
if (!a || !b) return false;
|
|
162
|
+
const ab = a.bounds;
|
|
163
|
+
const bb = b.bounds;
|
|
131
164
|
return (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
a.cssPath === b.cssPath &&
|
|
166
|
+
(ab === bb ||
|
|
167
|
+
(!!ab && !!bb && ab.x === bb.x && ab.y === bb.y && ab.w === bb.w && ab.h === bb.h))
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
function sameIds(a: readonly string[] | undefined, b: readonly string[] | undefined): boolean {
|
|
171
|
+
if (a === b) return true;
|
|
172
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
173
|
+
return a.every((id, i) => id === b[i]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const Cursor = memo(
|
|
177
|
+
function Cursor({ peer, viewport }: CursorProps): JSX.Element | null {
|
|
178
|
+
if (!peer.cursor) return null;
|
|
179
|
+
// world → screen: screen = world * zoom + viewport.{x,y}
|
|
180
|
+
const screenX = peer.cursor.x * viewport.zoom + viewport.x;
|
|
181
|
+
const screenY = peer.cursor.y * viewport.zoom + viewport.y;
|
|
182
|
+
const editing = !!peer.editing;
|
|
183
|
+
return (
|
|
184
|
+
<div
|
|
185
|
+
className={`dc-cursor${editing ? ' dc-cursor--editing' : ''}`}
|
|
186
|
+
style={{ transform: `translate(${screenX}px, ${screenY}px)` }}
|
|
144
187
|
>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
188
|
+
{/* DS colors-presence Pointer — one plain triangle glyph, tinted by its
|
|
189
|
+
owner (the specimen's exact 24-grid path, no tail/notch). */}
|
|
190
|
+
<svg
|
|
191
|
+
className="dc-cursor-arrow"
|
|
192
|
+
width="15"
|
|
193
|
+
height="15"
|
|
194
|
+
viewBox="0 0 24 24"
|
|
195
|
+
aria-hidden="true"
|
|
196
|
+
>
|
|
197
|
+
<path d="M4 3 L20 11.5 L12.5 13 L10 21 Z" fill={peer.color} />
|
|
198
|
+
</svg>
|
|
199
|
+
<div className="dc-cursor-label" style={{ background: peer.color }}>
|
|
200
|
+
{peer.name}
|
|
201
|
+
{editing && (
|
|
202
|
+
<span className="dc-cursor-edit-mark" aria-hidden="true">
|
|
203
|
+
✎
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
154
207
|
</div>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
(a, b) =>
|
|
211
|
+
sameViewport(a.viewport, b.viewport) &&
|
|
212
|
+
sameLabel(a.peer, b.peer) &&
|
|
213
|
+
!!a.peer.editing === !!b.peer.editing &&
|
|
214
|
+
(a.peer.cursor === b.peer.cursor ||
|
|
215
|
+
(!!a.peer.cursor &&
|
|
216
|
+
!!b.peer.cursor &&
|
|
217
|
+
a.peer.cursor.x === b.peer.cursor.x &&
|
|
218
|
+
a.peer.cursor.y === b.peer.cursor.y))
|
|
219
|
+
);
|
|
158
220
|
|
|
159
221
|
/**
|
|
160
222
|
* Foreign-selection halos for stamped annotation strokes. Each peer publishes
|
|
@@ -173,50 +235,188 @@ interface PeerAnnotationSelectionProps {
|
|
|
173
235
|
* screen space while the stroke it outlines moves away underneath it.
|
|
174
236
|
*/
|
|
175
237
|
viewport: ViewportSnapshot;
|
|
238
|
+
/**
|
|
239
|
+
* Bumps when what the halo outlines may have moved WITHOUT the camera or the
|
|
240
|
+
* peer's selection changing — an annotation edit, a canvas re-render. It used
|
|
241
|
+
* to be implicit: the halo re-measured on every awareness change (30 Hz per
|
|
242
|
+
* moving peer), which was the #131 cost. Now it is this, and nothing else.
|
|
243
|
+
*/
|
|
244
|
+
tick?: number;
|
|
176
245
|
}
|
|
177
246
|
|
|
178
|
-
export const PeerAnnotationSelection = memo(
|
|
179
|
-
peer
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
247
|
+
export const PeerAnnotationSelection = memo(
|
|
248
|
+
function PeerAnnotationSelection({ peer }: PeerAnnotationSelectionProps): JSX.Element | null {
|
|
249
|
+
if (!peer.annotationSelection || peer.annotationSelection.length === 0) return null;
|
|
250
|
+
if (typeof document === 'undefined') return null;
|
|
251
|
+
const rects: { id: string; x: number; y: number; w: number; h: number }[] = [];
|
|
252
|
+
for (const id of peer.annotationSelection) {
|
|
253
|
+
try {
|
|
254
|
+
const el = document.querySelector(`[data-id="${CSS.escape(id)}"]`);
|
|
255
|
+
if (!el) continue;
|
|
256
|
+
const r = (el as Element).getBoundingClientRect();
|
|
257
|
+
if (r.width <= 0 || r.height <= 0) continue;
|
|
258
|
+
// Pad by 3px so the halo sits OUTSIDE the stroke instead of clipping it.
|
|
259
|
+
rects.push({ id, x: r.left - 3, y: r.top - 3, w: r.width + 6, h: r.height + 6 });
|
|
260
|
+
} catch {
|
|
261
|
+
/* invalid id token — skip */
|
|
262
|
+
}
|
|
194
263
|
}
|
|
195
|
-
|
|
196
|
-
|
|
264
|
+
if (rects.length === 0) return null;
|
|
265
|
+
return (
|
|
266
|
+
<>
|
|
267
|
+
{rects.map((r, i) => (
|
|
268
|
+
<div
|
|
269
|
+
key={r.id}
|
|
270
|
+
className="dc-peer-selection"
|
|
271
|
+
style={{
|
|
272
|
+
transform: `translate(${r.x}px, ${r.y}px)`,
|
|
273
|
+
width: r.w,
|
|
274
|
+
height: r.h,
|
|
275
|
+
borderColor: peer.color,
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
{i === 0 && (
|
|
279
|
+
<div className="dc-peer-selection__label" style={{ background: peer.color }}>
|
|
280
|
+
{peer.name}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</>
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
(a, b) =>
|
|
289
|
+
sameViewport(a.viewport, b.viewport) &&
|
|
290
|
+
a.tick === b.tick &&
|
|
291
|
+
sameLabel(a.peer, b.peer) &&
|
|
292
|
+
sameIds(a.peer.annotationSelection, b.peer.annotationSelection)
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Task 22 (DDR-242 AD5) — a peer's annotation gesture while it is in flight:
|
|
297
|
+
* the elements they drag, the box they resize or draw, the ink of a pen stroke
|
|
298
|
+
* — so peers watch it move instead of seeing it jump at the commit. Awareness
|
|
299
|
+
* only (sanitized in use-collab); nothing here reads or writes the board. A
|
|
300
|
+
* gesture that stops updating (a peer who vanished mid-drag) fades after
|
|
301
|
+
* GESTURE_STALE_MS even before awareness garbage-collects the peer.
|
|
302
|
+
*/
|
|
303
|
+
const GESTURE_STALE_MS = 3000;
|
|
304
|
+
const MAX_GHOSTS = 64;
|
|
305
|
+
|
|
306
|
+
function sameGesture(
|
|
307
|
+
a: AnnotationGesture | null | undefined,
|
|
308
|
+
b: AnnotationGesture | null | undefined
|
|
309
|
+
): boolean {
|
|
310
|
+
if (a === b) return true;
|
|
311
|
+
if (!a || !b) return false;
|
|
197
312
|
return (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}}
|
|
209
|
-
>
|
|
210
|
-
{i === 0 && (
|
|
211
|
-
<div className="dc-peer-selection__label" style={{ background: peer.color }}>
|
|
212
|
-
{peer.name}
|
|
213
|
-
</div>
|
|
214
|
-
)}
|
|
215
|
-
</div>
|
|
216
|
-
))}
|
|
217
|
-
</>
|
|
313
|
+
a.kind === b.kind &&
|
|
314
|
+
a.dx === b.dx &&
|
|
315
|
+
a.dy === b.dy &&
|
|
316
|
+
sameIds(a.ids, b.ids) &&
|
|
317
|
+
a.box?.x === b.box?.x &&
|
|
318
|
+
a.box?.y === b.box?.y &&
|
|
319
|
+
a.box?.w === b.box?.w &&
|
|
320
|
+
a.box?.h === b.box?.h &&
|
|
321
|
+
(a.points?.length ?? 0) === (b.points?.length ?? 0) &&
|
|
322
|
+
a.points?.[a.points.length - 1] === b.points?.[b.points.length - 1]
|
|
218
323
|
);
|
|
219
|
-
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
interface PeerAnnotationGestureProps {
|
|
327
|
+
peer: ForeignAwareness;
|
|
328
|
+
viewport: ViewportSnapshot;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export const PeerAnnotationGesture = memo(
|
|
332
|
+
function PeerAnnotationGesture({
|
|
333
|
+
peer,
|
|
334
|
+
viewport,
|
|
335
|
+
}: PeerAnnotationGestureProps): JSX.Element | null {
|
|
336
|
+
const g = peer.annotationGesture;
|
|
337
|
+
const [stale, setStale] = useState(false);
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
setStale(false);
|
|
340
|
+
if (!g) return;
|
|
341
|
+
const t = setTimeout(() => setStale(true), GESTURE_STALE_MS);
|
|
342
|
+
return () => clearTimeout(t);
|
|
343
|
+
}, [g]);
|
|
344
|
+
if (!g || stale || typeof document === 'undefined') return null;
|
|
345
|
+
const z = viewport.zoom || 1;
|
|
346
|
+
const toScreen = (x: number, y: number) => [x * z + viewport.x, y * z + viewport.y] as const;
|
|
347
|
+
const boxes: Array<{ key: string; x: number; y: number; w: number; h: number }> = [];
|
|
348
|
+
if (g.kind === 'move' && g.dx !== undefined && g.dy !== undefined) {
|
|
349
|
+
for (const id of g.ids.slice(0, MAX_GHOSTS)) {
|
|
350
|
+
try {
|
|
351
|
+
const el = document.querySelector(`.dc-annot-scene [data-id="${CSS.escape(id)}"]`);
|
|
352
|
+
if (!el) continue;
|
|
353
|
+
const r = el.getBoundingClientRect();
|
|
354
|
+
if (r.width <= 0 && r.height <= 0) continue;
|
|
355
|
+
boxes.push({
|
|
356
|
+
key: id,
|
|
357
|
+
x: r.left + g.dx * z,
|
|
358
|
+
y: r.top + g.dy * z,
|
|
359
|
+
w: r.width,
|
|
360
|
+
h: r.height,
|
|
361
|
+
});
|
|
362
|
+
} catch {
|
|
363
|
+
/* invalid id token — skip */
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} else if (g.box) {
|
|
367
|
+
const [x, y] = toScreen(g.box.x, g.box.y);
|
|
368
|
+
boxes.push({ key: 'box', x, y, w: g.box.w * z, h: g.box.h * z });
|
|
369
|
+
}
|
|
370
|
+
let ink: string | null = null;
|
|
371
|
+
if (g.kind === 'draw' && g.points && g.points.length >= 4) {
|
|
372
|
+
const pts: string[] = [];
|
|
373
|
+
for (let i = 0; i + 1 < g.points.length; i += 2) {
|
|
374
|
+
const [sx, sy] = toScreen(g.points[i] as number, g.points[i + 1] as number);
|
|
375
|
+
pts.push(`${sx},${sy}`);
|
|
376
|
+
}
|
|
377
|
+
ink = pts.join(' ');
|
|
378
|
+
}
|
|
379
|
+
if (!boxes.length && !ink) return null;
|
|
380
|
+
return (
|
|
381
|
+
<>
|
|
382
|
+
{boxes.map((b, i) => (
|
|
383
|
+
<div
|
|
384
|
+
key={`g-${b.key}`}
|
|
385
|
+
className="dc-peer-gesture"
|
|
386
|
+
data-peer-gesture={g.kind}
|
|
387
|
+
style={{
|
|
388
|
+
transform: `translate(${b.x}px, ${b.y}px)`,
|
|
389
|
+
width: b.w,
|
|
390
|
+
height: b.h,
|
|
391
|
+
borderColor: peer.color,
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
{i === 0 && (
|
|
395
|
+
<div className="dc-peer-selection__label" style={{ background: peer.color }}>
|
|
396
|
+
{peer.name}
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
))}
|
|
401
|
+
{ink ? (
|
|
402
|
+
<svg className="dc-peer-gesture-ink" data-peer-gesture="draw" aria-hidden="true">
|
|
403
|
+
<polyline
|
|
404
|
+
points={ink}
|
|
405
|
+
fill="none"
|
|
406
|
+
stroke={peer.color}
|
|
407
|
+
strokeWidth={2}
|
|
408
|
+
strokeOpacity={0.7}
|
|
409
|
+
/>
|
|
410
|
+
</svg>
|
|
411
|
+
) : null}
|
|
412
|
+
</>
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
(a, b) =>
|
|
416
|
+
sameViewport(a.viewport, b.viewport) &&
|
|
417
|
+
sameLabel(a.peer, b.peer) &&
|
|
418
|
+
sameGesture(a.peer.annotationGesture, b.peer.annotationGesture)
|
|
419
|
+
);
|
|
220
420
|
|
|
221
421
|
/**
|
|
222
422
|
* Foreign-selection halo for canvas-shell elements (cdId-based selSet).
|
|
@@ -229,46 +429,53 @@ interface PeerSelectionProps {
|
|
|
229
429
|
peer: ForeignAwareness;
|
|
230
430
|
/** See PeerAnnotationSelectionProps.viewport — the memo-invalidation tick. */
|
|
231
431
|
viewport: ViewportSnapshot;
|
|
432
|
+
/** See PeerAnnotationSelectionProps.tick. */
|
|
433
|
+
tick?: number;
|
|
232
434
|
}
|
|
233
435
|
|
|
234
|
-
export const PeerSelection = memo(
|
|
235
|
-
peer
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const { cssPath, bounds } = peer.selection;
|
|
436
|
+
export const PeerSelection = memo(
|
|
437
|
+
function PeerSelection({ peer }: PeerSelectionProps): JSX.Element | null {
|
|
438
|
+
if (!peer.selection) return null;
|
|
439
|
+
const { cssPath, bounds } = peer.selection;
|
|
239
440
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
441
|
+
let rect: { x: number; y: number; w: number; h: number } | null = bounds ?? null;
|
|
442
|
+
if (cssPath && typeof document !== 'undefined') {
|
|
443
|
+
try {
|
|
444
|
+
const el = document.querySelector(cssPath);
|
|
445
|
+
if (el && el instanceof Element) {
|
|
446
|
+
const r = el.getBoundingClientRect();
|
|
447
|
+
if (r.width > 0 && r.height > 0) {
|
|
448
|
+
rect = { x: r.left, y: r.top, w: r.width, h: r.height };
|
|
449
|
+
}
|
|
248
450
|
}
|
|
451
|
+
} catch {
|
|
452
|
+
/* invalid selector — fall through to published bounds */
|
|
249
453
|
}
|
|
250
|
-
} catch {
|
|
251
|
-
/* invalid selector — fall through to published bounds */
|
|
252
454
|
}
|
|
253
|
-
|
|
254
|
-
if (!rect) return null;
|
|
455
|
+
if (!rect) return null;
|
|
255
456
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
457
|
+
return (
|
|
458
|
+
<div
|
|
459
|
+
className="dc-peer-selection"
|
|
460
|
+
style={{
|
|
461
|
+
transform: `translate(${rect.x}px, ${rect.y}px)`,
|
|
462
|
+
width: rect.w,
|
|
463
|
+
height: rect.h,
|
|
464
|
+
borderColor: peer.color,
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
<div className="dc-peer-selection__label" style={{ background: peer.color }}>
|
|
468
|
+
{peer.name}
|
|
469
|
+
</div>
|
|
268
470
|
</div>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
471
|
+
);
|
|
472
|
+
},
|
|
473
|
+
(a, b) =>
|
|
474
|
+
sameViewport(a.viewport, b.viewport) &&
|
|
475
|
+
a.tick === b.tick &&
|
|
476
|
+
sameLabel(a.peer, b.peer) &&
|
|
477
|
+
sameSelection(a.peer.selection, b.peer.selection)
|
|
478
|
+
);
|
|
272
479
|
|
|
273
480
|
/**
|
|
274
481
|
* Subscribes to foreign awareness + the local viewport. Renders one Cursor per
|
|
@@ -294,27 +501,48 @@ export function CursorsOverlay(): JSX.Element {
|
|
|
294
501
|
// sibling CursorsOverlay wouldn't re-render, so the annotation halo would
|
|
295
502
|
// sit on the OLD bounds until awareness independently changed.
|
|
296
503
|
const collab = useCollab();
|
|
297
|
-
const [,
|
|
504
|
+
const [tick, bumpTick] = useState(0);
|
|
298
505
|
useEffect(() => {
|
|
299
506
|
if (!collab) return;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
map.
|
|
507
|
+
// The v2 replica (DDR-242): element records are nested maps, so a field
|
|
508
|
+
// edit only reaches a deep observer.
|
|
509
|
+
const map = collab.doc.getMap(REPLICA_TYPE);
|
|
510
|
+
const onChange = () => bumpTick((n) => n + 1);
|
|
511
|
+
map.observeDeep(onChange);
|
|
303
512
|
return () => {
|
|
304
513
|
try {
|
|
305
|
-
map.
|
|
514
|
+
map.unobserveDeep(onChange);
|
|
306
515
|
} catch {
|
|
307
516
|
/* doc destroyed */
|
|
308
517
|
}
|
|
309
518
|
};
|
|
310
519
|
}, [collab]);
|
|
520
|
+
// A peer's selected element can change size or move with no camera or
|
|
521
|
+
// selection change (they edit its text; the canvas re-renders). The halos
|
|
522
|
+
// used to catch that for free by re-measuring on every awareness change —
|
|
523
|
+
// up to 30× a second per moving peer (#131). A slow refresh while somebody
|
|
524
|
+
// has something selected keeps the halo honest for a tiny fraction of that.
|
|
525
|
+
const anySelection = peers.some((p) => !!p.selection || (p.annotationSelection?.length ?? 0) > 0);
|
|
526
|
+
useEffect(() => {
|
|
527
|
+
if (!anySelection) return;
|
|
528
|
+
const id = setInterval(() => bumpTick((n) => n + 1), HALO_REFRESH_MS);
|
|
529
|
+
return () => clearInterval(id);
|
|
530
|
+
}, [anySelection]);
|
|
311
531
|
return (
|
|
312
532
|
<div className="dc-cursor-overlay" aria-hidden="true">
|
|
313
533
|
{peers.map((peer) => (
|
|
314
|
-
<PeerSelection key={`sel-${peer.clientID}`} peer={peer} viewport={vp} />
|
|
534
|
+
<PeerSelection key={`sel-${peer.clientID}`} peer={peer} viewport={vp} tick={tick} />
|
|
535
|
+
))}
|
|
536
|
+
{peers.map((peer) => (
|
|
537
|
+
<PeerAnnotationSelection
|
|
538
|
+
key={`asel-${peer.clientID}`}
|
|
539
|
+
peer={peer}
|
|
540
|
+
viewport={vp}
|
|
541
|
+
tick={tick}
|
|
542
|
+
/>
|
|
315
543
|
))}
|
|
316
544
|
{peers.map((peer) => (
|
|
317
|
-
<
|
|
545
|
+
<PeerAnnotationGesture key={`gest-${peer.clientID}`} peer={peer} viewport={vp} />
|
|
318
546
|
))}
|
|
319
547
|
{peers.map((peer) => (
|
|
320
548
|
<Cursor key={peer.clientID} peer={peer} viewport={vp} />
|