@beyondwork/docx-react-component 1.0.38 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/package.json +41 -31
  2. package/src/api/public-types.ts +183 -6
  3. package/src/core/commands/table-structure-commands.ts +31 -2
  4. package/src/core/commands/text-commands.ts +122 -2
  5. package/src/io/docx-session.ts +1 -0
  6. package/src/io/export/serialize-numbering.ts +42 -8
  7. package/src/io/export/serialize-paragraph-formatting.ts +152 -0
  8. package/src/io/export/serialize-run-formatting.ts +90 -0
  9. package/src/io/export/serialize-styles.ts +212 -0
  10. package/src/io/ooxml/parse-fields.ts +10 -3
  11. package/src/io/ooxml/parse-numbering.ts +41 -1
  12. package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
  13. package/src/io/ooxml/parse-run-formatting.ts +129 -0
  14. package/src/io/ooxml/parse-styles.ts +31 -0
  15. package/src/io/ooxml/xml-attr-helpers.ts +60 -0
  16. package/src/io/ooxml/xml-element.ts +19 -0
  17. package/src/model/canonical-document.ts +83 -3
  18. package/src/runtime/collab/event-types.ts +165 -0
  19. package/src/runtime/collab/index.ts +22 -0
  20. package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
  21. package/src/runtime/collab/runtime-collab-sync.ts +273 -0
  22. package/src/runtime/document-runtime.ts +134 -18
  23. package/src/runtime/layout/index.ts +2 -0
  24. package/src/runtime/layout/inert-layout-facet.ts +2 -0
  25. package/src/runtime/layout/layout-engine-instance.ts +69 -2
  26. package/src/runtime/layout/layout-invalidation.ts +14 -5
  27. package/src/runtime/layout/page-graph.ts +36 -0
  28. package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
  29. package/src/runtime/layout/paginated-layout-engine.ts +342 -28
  30. package/src/runtime/layout/project-block-fragments.ts +154 -20
  31. package/src/runtime/layout/public-facet.ts +40 -1
  32. package/src/runtime/layout/resolve-page-fields.ts +70 -0
  33. package/src/runtime/layout/resolve-page-previews.ts +185 -0
  34. package/src/runtime/layout/resolved-formatting-state.ts +30 -26
  35. package/src/runtime/layout/table-render-plan.ts +21 -1
  36. package/src/runtime/numbering-prefix.ts +5 -0
  37. package/src/runtime/paragraph-style-resolver.ts +194 -0
  38. package/src/runtime/render/render-kernel.ts +5 -1
  39. package/src/runtime/resolved-numbering-geometry.ts +9 -1
  40. package/src/runtime/surface-projection.ts +129 -9
  41. package/src/runtime/table-schema.ts +11 -0
  42. package/src/ui/WordReviewEditor.tsx +285 -5
  43. package/src/ui/editor-command-bag.ts +4 -0
  44. package/src/ui/editor-runtime-boundary.ts +16 -0
  45. package/src/ui/editor-shell-view.tsx +4 -0
  46. package/src/ui/editor-surface-controller.tsx +9 -1
  47. package/src/ui/headless/chrome-registry.ts +34 -5
  48. package/src/ui/headless/scoped-chrome-policy.ts +29 -0
  49. package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
  50. package/src/ui-tailwind/chrome/role-action-sets.ts +14 -8
  51. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +7 -10
  52. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +11 -0
  53. package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
  54. package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
  55. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +101 -21
  56. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
  57. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +5 -1
  58. package/src/ui-tailwind/chrome-overlay/index.ts +0 -6
  59. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +27 -17
  60. package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
  61. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +28 -3
  62. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
  63. package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
  64. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
  65. package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
  66. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
  67. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -78
  68. package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
  69. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
  70. package/src/ui-tailwind/index.ts +1 -5
  71. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +122 -1
  72. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +36 -3
  73. package/src/ui-tailwind/tw-review-workspace.tsx +132 -54
  74. package/src/runtime/collab-review-sync.ts +0 -254
  75. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +0 -95
  76. package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
@@ -1,4 +1,4 @@
1
- import { Fragment, type Node as PMNode } from "prosemirror-model";
1
+ import { Fragment, type Mark, type Node as PMNode, type Schema } from "prosemirror-model";
2
2
  import { EditorState, NodeSelection, type Plugin, Selection, TextSelection } from "prosemirror-state";
3
3
 
4
4
  import type {
@@ -17,6 +17,123 @@ export interface PMStateResult {
17
17
  positionMap: PositionMap;
18
18
  }
19
19
 
20
+ /**
21
+ * Test-friendly wrapper: build a PM EditorState from a minimal snapshot with no
22
+ * selection or plugins. The snapshot is cast to EditorSurfaceSnapshot so callers
23
+ * can pass partial objects in tests.
24
+ */
25
+ export function buildPmStateFromSnapshot(snapshot: EditorSurfaceSnapshot): EditorState {
26
+ const doc = buildPMDoc(snapshot, {}, false);
27
+ return EditorState.create({ doc, plugins: [] });
28
+ }
29
+
30
+ /**
31
+ * Build the set of PM marks for a text segment by merging direct marks with the
32
+ * cascaded resolvedRunFormatting. Direct marks win over cascade values.
33
+ */
34
+ function applyEffectiveMarks(
35
+ segment: Extract<SurfaceInlineSegment, { kind: "text" }>,
36
+ schema: Schema,
37
+ ): Mark[] {
38
+ const marks: Mark[] = [];
39
+ const directList = segment.marks ?? [];
40
+ const directAttrs = segment.markAttrs ?? {};
41
+ const cascade = segment.resolvedRunFormatting ?? {};
42
+
43
+ // Boolean marks: direct presence wins; direct absence (explicit false in markAttrs is not
44
+ // representable in the marks array, so we check the marks array truthiness).
45
+ // If the mark is absent from the direct marks list but cascade supplies it, apply it.
46
+ const hasDirect = (name: string) => directList.includes(name as never);
47
+
48
+ // bold
49
+ const directBold = hasDirect("bold");
50
+ const cascadeBold = cascade.bold === true;
51
+ if (directBold || (!("bold" in directAttrs) && cascadeBold)) {
52
+ marks.push(schema.marks.bold.create());
53
+ }
54
+
55
+ // italic
56
+ const directItalic = hasDirect("italic");
57
+ const cascadeItalic = cascade.italic === true;
58
+ if (directItalic || (!("italic" in directAttrs) && cascadeItalic)) {
59
+ marks.push(schema.marks.italic.create());
60
+ }
61
+
62
+ // underline
63
+ const directUnderline = hasDirect("underline");
64
+ const cascadeUnderline = cascade.underline && cascade.underline !== "none";
65
+ if (directUnderline || (!("underline" in directAttrs) && cascadeUnderline)) {
66
+ marks.push(schema.marks.underline.create());
67
+ }
68
+
69
+ // strikethrough
70
+ const directStrikethrough = hasDirect("strikethrough");
71
+ const cascadeStrikethrough = cascade.strikethrough === true;
72
+ if (directStrikethrough || (!("strikethrough" in directAttrs) && cascadeStrikethrough)) {
73
+ marks.push(schema.marks.strikethrough.create());
74
+ }
75
+
76
+ // pass-through marks from marks array (no cascade equivalent)
77
+ if (hasDirect("doubleStrikethrough")) marks.push(schema.marks.doubleStrikethrough.create());
78
+ if (hasDirect("vanish")) marks.push(schema.marks.vanish.create());
79
+ if (hasDirect("emboss")) marks.push(schema.marks.emboss.create());
80
+ if (hasDirect("imprint")) marks.push(schema.marks.imprint.create());
81
+ if (hasDirect("shadow")) marks.push(schema.marks.shadow.create());
82
+
83
+ // smallCaps / allCaps
84
+ if (hasDirect("smallCaps")) marks.push(schema.marks.small_caps.create());
85
+ if (hasDirect("allCaps")) marks.push(schema.marks.all_caps.create());
86
+
87
+ // superscript / subscript from marks array
88
+ if (hasDirect("superscript")) marks.push(schema.marks.superscript.create());
89
+ if (hasDirect("subscript")) marks.push(schema.marks.subscript.create());
90
+
91
+ // fontSize: direct.markAttrs.fontSize is in half-points; cascade.fontSizeHalfPoints is half-points
92
+ const directFontSize = typeof directAttrs.fontSize === "number" ? directAttrs.fontSize / 2 : undefined;
93
+ const cascadeFontSize = typeof cascade.fontSizeHalfPoints === "number" ? cascade.fontSizeHalfPoints / 2 : undefined;
94
+ const effectiveFontSize = directFontSize ?? cascadeFontSize;
95
+ if (typeof effectiveFontSize === "number") {
96
+ marks.push(schema.marks.font_size.create({ size: effectiveFontSize }));
97
+ }
98
+
99
+ // fontFamily: direct.markAttrs.fontFamily wins over cascade
100
+ const effectiveFontFamily = directAttrs.fontFamily ?? cascade.fontFamily ?? cascade.fontFamilyAscii;
101
+ if (effectiveFontFamily) {
102
+ marks.push(schema.marks.font_family.create({ family: effectiveFontFamily }));
103
+ }
104
+
105
+ // textColor: direct.markAttrs.textColor wins over cascade.colorHex
106
+ if (directAttrs.textFill && !directAttrs.textColor) {
107
+ const colorMatch = directAttrs.textFill.match(/\bval="([0-9A-Fa-f]{6})"/);
108
+ if (colorMatch) {
109
+ marks.push(schema.marks.text_color.create({ color: `#${colorMatch[1]}` }));
110
+ }
111
+ } else {
112
+ const directColor = directAttrs.textColor ? `#${directAttrs.textColor}` : undefined;
113
+ const cascadeColor = cascade.colorHex && cascade.colorHex !== "auto" ? `#${cascade.colorHex}` : undefined;
114
+ const effectiveColor = directColor ?? cascadeColor;
115
+ if (effectiveColor) {
116
+ marks.push(schema.marks.text_color.create({ color: effectiveColor }));
117
+ }
118
+ }
119
+
120
+ // highlight / background color
121
+ const effectiveBg = directAttrs.backgroundColor;
122
+ if (effectiveBg) {
123
+ marks.push(schema.marks.highlight.create({ color: `#${effectiveBg}` }));
124
+ }
125
+
126
+ // charSpacing / kerning
127
+ if (typeof directAttrs.charSpacing === "number") {
128
+ marks.push(schema.marks.char_spacing.create({ value: directAttrs.charSpacing }));
129
+ }
130
+ if (typeof directAttrs.kerning === "number") {
131
+ marks.push(schema.marks.font_kerning.create({ threshold: directAttrs.kerning }));
132
+ }
133
+
134
+ return marks;
135
+ }
136
+
20
137
  export interface MediaPreviewDescriptor {
21
138
  src: string;
22
139
  widthEmu?: number;
@@ -222,6 +339,9 @@ function buildParagraph(
222
339
  (previousParagraph.styleId ?? "__default__") === (block.styleId ?? "__default__"),
223
340
  );
224
341
 
342
+ const cascade = block.resolvedParagraphFormatting;
343
+ const cascadeBorders = cascade?.borders as Record<string, unknown> | undefined;
344
+
225
345
  return editorSchema.nodes.paragraph.create(
226
346
  {
227
347
  styleId: block.styleId ?? null,
@@ -233,12 +353,12 @@ function buildParagraph(
233
353
  numberingSuffix:
234
354
  (block as typeof block & { numberingSuffix?: string }).numberingSuffix ??
235
355
  null,
236
- alignment: block.alignment ?? null,
356
+ alignment: block.alignment ?? cascade?.alignment ?? null,
237
357
  spacingBefore: paragraphLayout.spacing?.before ?? null,
238
358
  spacingAfter: paragraphLayout.spacing?.after ?? null,
239
359
  lineSpacing: paragraphLayout.spacing?.line ?? null,
240
360
  lineRule: paragraphLayout.spacing?.lineRule ?? null,
241
- contextualSpacing: block.contextualSpacing ?? null,
361
+ contextualSpacing: block.contextualSpacing ?? cascade?.contextualSpacing ?? null,
242
362
  listContinuation: listContinuation || null,
243
363
  contextualSpacingBefore: contextualSpacingBefore || null,
244
364
  contextualSpacingAfter: contextualSpacingAfter || null,
@@ -248,14 +368,15 @@ function buildParagraph(
248
368
  indentHanging: paragraphLayout.indentation?.hanging ?? null,
249
369
  numberingMarkerWidth: paragraphLayout.markerLane?.width ?? null,
250
370
  numberingMarkerJustification: paragraphLayout.markerJustification ?? null,
251
- shadingFill: block.shading?.fill ?? null,
252
- borderTop: (block.borders as Record<string, unknown>)?.top ?? null,
253
- borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? null,
254
- borderLeft: (block.borders as Record<string, unknown>)?.left ?? null,
255
- borderRight: (block.borders as Record<string, unknown>)?.right ?? null,
256
- outlineLevel: block.outlineLevel ?? null,
257
- bidi: block.bidi ?? null,
258
- pageBreakBefore: block.pageBreakBefore ?? null,
371
+ numberingMarkerRunProperties: block.resolvedNumbering?.markerRunProperties ?? null,
372
+ shadingFill: block.shading?.fill ?? cascade?.shading?.fill ?? null,
373
+ borderTop: (block.borders as Record<string, unknown>)?.top ?? cascadeBorders?.top ?? null,
374
+ borderBottom: (block.borders as Record<string, unknown>)?.bottom ?? cascadeBorders?.bottom ?? null,
375
+ borderLeft: (block.borders as Record<string, unknown>)?.left ?? cascadeBorders?.left ?? null,
376
+ borderRight: (block.borders as Record<string, unknown>)?.right ?? cascadeBorders?.right ?? null,
377
+ outlineLevel: block.outlineLevel ?? cascade?.outlineLevel ?? null,
378
+ bidi: block.bidi ?? cascade?.bidi ?? null,
379
+ pageBreakBefore: block.pageBreakBefore ?? cascade?.pageBreakBefore ?? null,
259
380
  hiddenTextOnly: fullyVanishedParagraph || null,
260
381
  },
261
382
  content.length > 0 ? Fragment.from(content) : undefined,
@@ -275,9 +396,16 @@ function resolveParagraphLayout(
275
396
  NonNullable<Extract<SurfaceBlockSnapshot, { kind: "paragraph" }>["resolvedNumbering"]>["geometry"]["markerJustification"]
276
397
  > | undefined;
277
398
  } {
399
+ const cascadeFormatting = block.resolvedParagraphFormatting;
278
400
  return {
279
- spacing: block.resolvedNumbering?.geometry.spacing ?? block.spacing,
280
- indentation: block.resolvedNumbering?.geometry.indentation ?? block.indentation,
401
+ spacing:
402
+ block.resolvedNumbering?.geometry.spacing ??
403
+ block.spacing ??
404
+ cascadeFormatting?.spacing,
405
+ indentation:
406
+ block.resolvedNumbering?.geometry.indentation ??
407
+ block.indentation ??
408
+ cascadeFormatting?.indentation,
281
409
  tabStops: block.resolvedNumbering?.geometry.tabStops ?? block.tabStops ?? [],
282
410
  markerLane: block.resolvedNumbering?.geometry.markerLane,
283
411
  markerJustification: block.resolvedNumbering?.geometry.markerJustification,
@@ -293,55 +421,7 @@ function buildInlineContent(
293
421
  case "text": {
294
422
  if (!segment.text) return [];
295
423
 
296
- // Build PM marks from segment marks
297
- let marks = editorSchema.marks.bold.isInSet([])
298
- ? [] // shouldn't happen, just type safety
299
- : [];
300
-
301
- const pmMarks = [];
302
- if (segment.marks) {
303
- for (const mark of segment.marks) {
304
- // Map surface mark names that differ from PM schema mark names
305
- if (mark === "smallCaps") {
306
- pmMarks.push(editorSchema.marks.small_caps.create());
307
- continue;
308
- }
309
- if (mark === "allCaps") {
310
- pmMarks.push(editorSchema.marks.all_caps.create());
311
- continue;
312
- }
313
- const pmMark = editorSchema.marks[mark];
314
- if (pmMark) {
315
- pmMarks.push(pmMark.create());
316
- }
317
- }
318
- }
319
- if (segment.kind === "text" && segment.markAttrs) {
320
- if (segment.markAttrs.backgroundColor) {
321
- pmMarks.push(editorSchema.marks.highlight.create({ color: `#${segment.markAttrs.backgroundColor}` }));
322
- }
323
- if (segment.markAttrs.fontFamily) {
324
- pmMarks.push(editorSchema.marks.font_family.create({ family: segment.markAttrs.fontFamily }));
325
- }
326
- if (segment.markAttrs.fontSize) {
327
- pmMarks.push(editorSchema.marks.font_size.create({ size: segment.markAttrs.fontSize / 2 }));
328
- }
329
- if (segment.markAttrs.textColor) {
330
- pmMarks.push(editorSchema.marks.text_color.create({ color: `#${segment.markAttrs.textColor}` }));
331
- }
332
- if (segment.markAttrs.charSpacing) {
333
- pmMarks.push(editorSchema.marks.char_spacing.create({ value: segment.markAttrs.charSpacing }));
334
- }
335
- if (segment.markAttrs.kerning) {
336
- pmMarks.push(editorSchema.marks.font_kerning.create({ threshold: segment.markAttrs.kerning }));
337
- }
338
- if (segment.markAttrs.textFill && !segment.markAttrs.textColor) {
339
- const colorMatch = segment.markAttrs.textFill.match(/\bval="([0-9A-Fa-f]{6})"/);
340
- if (colorMatch) {
341
- pmMarks.push(editorSchema.marks.text_color.create({ color: `#${colorMatch[1]}` }));
342
- }
343
- }
344
- }
424
+ const pmMarks = applyEffectiveMarks(segment, editorSchema);
345
425
  if (segment.hyperlinkHref) {
346
426
  pmMarks.push(editorSchema.marks.link.create({ href: segment.hyperlinkHref }));
347
427
  }
@@ -433,6 +513,7 @@ function buildTable(
433
513
  borderRight: cell.borderRight ?? null,
434
514
  borderBottom: cell.borderBottom ?? null,
435
515
  borderLeft: cell.borderLeft ?? null,
516
+ bandClasses: cell.bandClasses ?? null,
436
517
  },
437
518
  Fragment.from(cellContent),
438
519
  ),
@@ -460,6 +541,7 @@ function buildTable(
460
541
  tblLookLastColumn: block.tblLook?.lastColumn ?? false,
461
542
  tblLookNoHBand: block.tblLook?.noHBand ?? false,
462
543
  tblLookNoVBand: block.tblLook?.noVBand ?? false,
544
+ tblLookVal: block.tblLook?.val ?? null,
463
545
  },
464
546
  Fragment.from(rows),
465
547
  );
@@ -0,0 +1,179 @@
1
+ import { Plugin, PluginKey } from "prosemirror-state";
2
+ import type { EditorState, Transaction } from "prosemirror-state";
3
+ import { Decoration, DecorationSet } from "prosemirror-view";
4
+ import type { EditorView } from "prosemirror-view";
5
+ import type { Awareness } from "y-protocols/awareness";
6
+
7
+ import type { EditorStoryTarget } from "../../api/public-types";
8
+ import { storyTargetsEqual } from "../../core/selection/mapping";
9
+ import type { PositionMap } from "./pm-position-map";
10
+ import {
11
+ getRemoteCursorStates,
12
+ type RemoteCursorState,
13
+ } from "../../runtime/collab/remote-cursor-awareness";
14
+
15
+ interface RemoteCursorPluginState {
16
+ cursors: RemoteCursorState[];
17
+ }
18
+
19
+ export const remoteCursorPluginKey = new PluginKey<RemoteCursorPluginState>(
20
+ "remoteCursor",
21
+ );
22
+
23
+ export interface RemoteCursorPluginOptions {
24
+ awareness: Awareness;
25
+ localClientId: number;
26
+ getPositionMap: () => PositionMap | null;
27
+ getActiveStory: () => EditorStoryTarget;
28
+ }
29
+
30
+ export function createRemoteCursorPlugin(
31
+ options: RemoteCursorPluginOptions,
32
+ ): Plugin<RemoteCursorPluginState> {
33
+ const { awareness, localClientId, getPositionMap, getActiveStory } = options;
34
+
35
+ function computeCursors(): RemoteCursorState[] {
36
+ const activeStory = getActiveStory();
37
+ const allCursors = getRemoteCursorStates(awareness, localClientId);
38
+ return allCursors.filter((cursor) =>
39
+ storyTargetsEqual(cursor.storyTarget, activeStory),
40
+ );
41
+ }
42
+
43
+ function buildDecorations(state: EditorState): DecorationSet {
44
+ const positionMap = getPositionMap();
45
+ if (!positionMap) {
46
+ return DecorationSet.empty;
47
+ }
48
+
49
+ const pluginState = remoteCursorPluginKey.getState(state);
50
+ const cursors = pluginState?.cursors ?? [];
51
+ if (cursors.length === 0) {
52
+ return DecorationSet.empty;
53
+ }
54
+
55
+ const decorations: Decoration[] = [];
56
+
57
+ for (const cursor of cursors) {
58
+ const pmAnchor = positionMap.runtimeToPm(cursor.anchor);
59
+ const pmHead = positionMap.runtimeToPm(cursor.head);
60
+
61
+ const from = Math.min(pmAnchor, pmHead);
62
+ const to = Math.max(pmAnchor, pmHead);
63
+
64
+ if (from < 1 || to > state.doc.content.size) {
65
+ continue;
66
+ }
67
+
68
+ if (from !== to) {
69
+ decorations.push(
70
+ Decoration.inline(from, to, {
71
+ style: `background-color: ${cursor.color}33;`,
72
+ class: "remote-cursor-selection",
73
+ }),
74
+ );
75
+ }
76
+
77
+ decorations.push(
78
+ Decoration.widget(to, () => createCursorWidget(cursor), {
79
+ side: 1,
80
+ key: cursor.userId,
81
+ }),
82
+ );
83
+ }
84
+
85
+ return DecorationSet.create(state.doc, decorations);
86
+ }
87
+
88
+ const plugin = new Plugin<RemoteCursorPluginState>({
89
+ key: remoteCursorPluginKey,
90
+
91
+ state: {
92
+ init(): RemoteCursorPluginState {
93
+ return { cursors: computeCursors() };
94
+ },
95
+
96
+ apply(
97
+ tr: Transaction,
98
+ pluginState: RemoteCursorPluginState,
99
+ ): RemoteCursorPluginState {
100
+ const meta = tr.getMeta(remoteCursorPluginKey);
101
+ if (meta !== undefined) {
102
+ return meta;
103
+ }
104
+ return pluginState;
105
+ },
106
+ },
107
+
108
+ props: {
109
+ decorations(state: EditorState): DecorationSet {
110
+ return buildDecorations(state);
111
+ },
112
+ },
113
+
114
+ view(editorView: EditorView) {
115
+ const onChange = (): void => {
116
+ const newCursors = computeCursors();
117
+ const tr = editorView.state.tr.setMeta(remoteCursorPluginKey, {
118
+ cursors: newCursors,
119
+ });
120
+ editorView.dispatch(tr);
121
+ };
122
+
123
+ awareness.on("change", onChange);
124
+
125
+ return {
126
+ destroy() {
127
+ awareness.off("change", onChange);
128
+ },
129
+ };
130
+ },
131
+ });
132
+
133
+ return plugin;
134
+ }
135
+
136
+ function createCursorWidget(cursor: RemoteCursorState): HTMLElement {
137
+ const container = document.createElement("span");
138
+ container.className = "remote-cursor-widget";
139
+ container.style.cssText = `
140
+ position: relative;
141
+ display: inline-block;
142
+ pointer-events: none;
143
+ `;
144
+
145
+ const caret = document.createElement("span");
146
+ caret.style.cssText = `
147
+ position: absolute;
148
+ left: -1px;
149
+ top: -0.2em;
150
+ width: 2px;
151
+ height: 1.2em;
152
+ background-color: ${cursor.color};
153
+ border-radius: 1px;
154
+ `;
155
+ container.appendChild(caret);
156
+
157
+ const label = document.createElement("span");
158
+ label.style.cssText = `
159
+ position: absolute;
160
+ left: 0;
161
+ top: -1.4em;
162
+ font-size: 11px;
163
+ font-family: system-ui, -apple-system, sans-serif;
164
+ font-weight: 500;
165
+ color: white;
166
+ background-color: ${cursor.color};
167
+ padding: 1px 4px;
168
+ border-radius: 3px;
169
+ white-space: nowrap;
170
+ line-height: 1.3;
171
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
172
+ `;
173
+ label.textContent = cursor.displayName;
174
+ container.appendChild(label);
175
+
176
+ return container;
177
+ }
178
+
179
+ export type { RemoteCursorState };