@beyondwork/docx-react-component 1.0.19 → 1.0.21

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 (71) hide show
  1. package/package.json +44 -25
  2. package/src/api/public-types.ts +336 -0
  3. package/src/api/session-state.ts +2 -0
  4. package/src/core/commands/formatting-commands.ts +1 -1
  5. package/src/core/commands/index.ts +14 -2
  6. package/src/core/search/search-text.ts +28 -0
  7. package/src/core/state/editor-state.ts +3 -0
  8. package/src/index.ts +21 -0
  9. package/src/io/docx-session.ts +363 -17
  10. package/src/io/export/serialize-comments.ts +104 -34
  11. package/src/io/export/serialize-footnotes.ts +198 -1
  12. package/src/io/export/serialize-headers-footers.ts +203 -10
  13. package/src/io/export/serialize-main-document.ts +83 -3
  14. package/src/io/export/split-review-boundaries.ts +181 -19
  15. package/src/io/normalize/normalize-text.ts +82 -8
  16. package/src/io/ooxml/highlight-colors.ts +39 -0
  17. package/src/io/ooxml/parse-comments.ts +85 -19
  18. package/src/io/ooxml/parse-fields.ts +396 -0
  19. package/src/io/ooxml/parse-footnotes.ts +240 -2
  20. package/src/io/ooxml/parse-headers-footers.ts +431 -7
  21. package/src/io/ooxml/parse-inline-media.ts +15 -1
  22. package/src/io/ooxml/parse-main-document.ts +396 -14
  23. package/src/io/ooxml/parse-revisions.ts +317 -38
  24. package/src/legal/bookmarks.ts +44 -0
  25. package/src/legal/cross-references.ts +59 -1
  26. package/src/model/canonical-document.ts +117 -1
  27. package/src/model/snapshot.ts +85 -1
  28. package/src/review/store/revision-store.ts +6 -0
  29. package/src/review/store/revision-types.ts +1 -0
  30. package/src/runtime/document-navigation.ts +52 -13
  31. package/src/runtime/document-runtime.ts +1521 -75
  32. package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
  33. package/src/runtime/session-capabilities.ts +33 -3
  34. package/src/runtime/surface-projection.ts +86 -25
  35. package/src/runtime/table-schema.ts +2 -2
  36. package/src/runtime/view-state.ts +24 -6
  37. package/src/runtime/workflow-markup.ts +349 -0
  38. package/src/ui/WordReviewEditor.tsx +915 -1314
  39. package/src/ui/editor-command-bag.ts +120 -0
  40. package/src/ui/editor-runtime-boundary.ts +1448 -0
  41. package/src/ui/editor-shell-view.tsx +134 -0
  42. package/src/ui/editor-surface-controller.tsx +55 -0
  43. package/src/ui/headless/revision-decoration-model.ts +4 -4
  44. package/src/ui/runtime-snapshot-selectors.ts +197 -0
  45. package/src/ui/workflow-surface-blocked-rails.ts +94 -0
  46. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
  47. package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
  48. package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
  49. package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
  50. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +27 -2
  51. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
  52. package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
  53. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
  54. package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
  55. package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
  56. package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
  57. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
  58. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
  59. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
  60. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
  61. package/src/ui-tailwind/page-chrome-model.ts +27 -0
  62. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
  63. package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
  64. package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
  65. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
  66. package/src/ui-tailwind/theme/editor-theme.css +130 -0
  67. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
  68. package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
  69. package/src/validation/compatibility-engine.ts +27 -4
  70. package/src/validation/compatibility-report.ts +1 -0
  71. package/src/validation/docx-comment-proof.ts +220 -0
@@ -0,0 +1,114 @@
1
+ import React from "react";
2
+
3
+ import type {
4
+ PageLayoutSnapshot,
5
+ SectionPageNumberingPatch,
6
+ SectionBreakType,
7
+ SectionLayoutPatch,
8
+ } from "../../api/public-types";
9
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
10
+
11
+ export interface TwLayoutPanelProps {
12
+ pageLayout: PageLayoutSnapshot;
13
+ readOnly: boolean;
14
+ onInsertSectionBreak?: (type: SectionBreakType) => void;
15
+ onDeleteSectionBreak?: (sectionIndex: number) => void;
16
+ onUpdateSectionLayout?: (sectionIndex: number, patch: SectionLayoutPatch) => void;
17
+ onSetSectionPageNumbering?: (
18
+ sectionIndex: number,
19
+ patch: SectionPageNumberingPatch | null,
20
+ ) => void;
21
+ }
22
+
23
+ export function TwLayoutPanel(props: TwLayoutPanelProps) {
24
+ const nextOrientation = props.pageLayout.orientation === "portrait" ? "landscape" : "portrait";
25
+ const titlePageEnabled = props.pageLayout.differentFirstPage;
26
+
27
+ return (
28
+ <div className="mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm">
29
+ <span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
30
+ Section
31
+ </span>
32
+ <ToolbarButton
33
+ ariaLabel="Insert next-page section break"
34
+ disabled={props.readOnly || !props.onInsertSectionBreak}
35
+ onClick={() => props.onInsertSectionBreak?.("nextPage")}
36
+ >
37
+ Next-page break
38
+ </ToolbarButton>
39
+ <ToolbarButton
40
+ ariaLabel={`Switch section to ${nextOrientation}`}
41
+ disabled={props.readOnly || !props.onUpdateSectionLayout}
42
+ onClick={() =>
43
+ props.onUpdateSectionLayout?.(props.pageLayout.sectionIndex, {
44
+ pageSize: {
45
+ orientation: nextOrientation,
46
+ width: props.pageLayout.pageHeight,
47
+ height: props.pageLayout.pageWidth,
48
+ },
49
+ })}
50
+ >
51
+ {nextOrientation === "landscape" ? "Landscape" : "Portrait"}
52
+ </ToolbarButton>
53
+ <ToolbarButton
54
+ ariaLabel="Delete current section break"
55
+ disabled={props.readOnly || props.pageLayout.sectionIndex === 0 || !props.onDeleteSectionBreak}
56
+ onClick={() => props.onDeleteSectionBreak?.(props.pageLayout.sectionIndex)}
57
+ >
58
+ Delete break
59
+ </ToolbarButton>
60
+ <ToolbarButton
61
+ ariaLabel="Restart page numbering at 1"
62
+ disabled={props.readOnly || !props.onSetSectionPageNumbering}
63
+ onClick={() =>
64
+ props.onSetSectionPageNumbering?.(props.pageLayout.sectionIndex, {
65
+ ...(props.pageLayout.pageNumbering ?? {}),
66
+ start: 1,
67
+ })}
68
+ >
69
+ Restart numbering
70
+ </ToolbarButton>
71
+ <ToolbarButton
72
+ ariaLabel="Use roman page numbering"
73
+ disabled={props.readOnly || !props.onSetSectionPageNumbering}
74
+ onClick={() =>
75
+ props.onSetSectionPageNumbering?.(props.pageLayout.sectionIndex, {
76
+ ...(props.pageLayout.pageNumbering ?? {}),
77
+ format: "roman",
78
+ })}
79
+ >
80
+ Roman numerals
81
+ </ToolbarButton>
82
+ <ToolbarButton
83
+ ariaLabel="Toggle different first page"
84
+ disabled={props.readOnly || !props.onUpdateSectionLayout}
85
+ onClick={() =>
86
+ props.onUpdateSectionLayout?.(props.pageLayout.sectionIndex, {
87
+ titlePage: !titlePageEnabled,
88
+ })}
89
+ >
90
+ {titlePageEnabled ? "Same first page" : "Different first page"}
91
+ </ToolbarButton>
92
+ </div>
93
+ );
94
+ }
95
+
96
+ function ToolbarButton(props: {
97
+ ariaLabel: string;
98
+ children: React.ReactNode;
99
+ disabled: boolean;
100
+ onClick?: () => void;
101
+ }) {
102
+ return (
103
+ <button
104
+ type="button"
105
+ aria-label={props.ariaLabel}
106
+ disabled={props.disabled}
107
+ onMouseDown={preserveEditorSelectionMouseDown}
108
+ onClick={props.onClick}
109
+ className="inline-flex h-8 items-center rounded-md px-2 text-xs font-medium text-primary transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-40"
110
+ >
111
+ {props.children}
112
+ </button>
113
+ );
114
+ }
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+
3
+ export interface ActiveObjectContext {
4
+ kind: "textbox" | "shape";
5
+ display: "inline" | "floating";
6
+ }
7
+
8
+ export interface TwObjectContextToolbarProps {
9
+ activeObject: ActiveObjectContext;
10
+ }
11
+
12
+ export function TwObjectContextToolbar(props: TwObjectContextToolbarProps) {
13
+ const label = props.activeObject.kind === "textbox" ? "Text box" : "Shape";
14
+
15
+ return (
16
+ <div
17
+ data-testid="object-context-toolbar"
18
+ className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
19
+ >
20
+ <span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
21
+ Object
22
+ </span>
23
+ <span className="rounded-full bg-surface px-2 py-1 text-[10px] font-medium uppercase tracking-[0.1em] text-secondary">
24
+ {label}
25
+ </span>
26
+ <span className="rounded-full bg-surface px-2 py-1 text-[10px] font-medium uppercase tracking-[0.1em] text-secondary">
27
+ {props.activeObject.display}
28
+ </span>
29
+ <span className="text-xs text-secondary">
30
+ Object selection is active.
31
+ </span>
32
+ </div>
33
+ );
34
+ }
@@ -1,7 +1,7 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import type { FocusEventHandler } from "react";
3
3
  import * as Tooltip from "@radix-ui/react-tooltip";
4
- import { Bold, Italic, MessageSquare, Underline } from "lucide-react";
4
+ import { Baseline, Bold, Highlighter, Italic, MessageSquare, Underline } from "lucide-react";
5
5
 
6
6
  import type { SelectionToolbarModel } from "../../ui/headless/selection-toolbar-model";
7
7
  import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
@@ -14,6 +14,8 @@ export interface TwSelectionToolbarProps {
14
14
  onToggleBold?: () => void;
15
15
  onToggleItalic?: () => void;
16
16
  onToggleUnderline?: () => void;
17
+ onSetTextColor?: (color: string) => void;
18
+ onSetHighlightColor?: (color: string | null) => void;
17
19
  onAddComment?: () => void;
18
20
  }
19
21
 
@@ -60,6 +62,20 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
60
62
  disabled={formattingDisabled}
61
63
  onClick={props.onToggleUnderline}
62
64
  />
65
+ <ToolbarActionButton
66
+ icon={<Baseline className="h-3.5 w-3.5" />}
67
+ label="Text color blue"
68
+ pressed={false}
69
+ disabled={formattingDisabled}
70
+ onClick={() => props.onSetTextColor?.("#1660a8")}
71
+ />
72
+ <ToolbarActionButton
73
+ icon={<Highlighter className="h-3.5 w-3.5" />}
74
+ label="Highlight yellow"
75
+ pressed={false}
76
+ disabled={formattingDisabled}
77
+ onClick={() => props.onSetHighlightColor?.("#ffff00")}
78
+ />
63
79
 
64
80
  <div className="mx-0.5 h-4 w-px bg-border" />
65
81
 
@@ -86,9 +102,18 @@ export const TwSelectionToolbar = forwardRef<HTMLDivElement, TwSelectionToolbarP
86
102
  </Tooltip.Portal>
87
103
  </Tooltip.Root>
88
104
 
89
- {contextLabel ? (
105
+ {model.previewText ? (
90
106
  <>
91
107
  <div className="mx-0.5 h-4 w-px bg-border" />
108
+ <span className="max-w-[8rem] truncate text-[11px] text-secondary">
109
+ {model.previewText}
110
+ </span>
111
+ </>
112
+ ) : null}
113
+
114
+ {contextLabel ? (
115
+ <>
116
+ {!model.previewText ? <div className="mx-0.5 h-4 w-px bg-border" /> : null}
92
117
  <span
93
118
  className={`min-w-0 max-w-[11rem] truncate rounded-full px-2 py-0.5 text-[10px] font-medium tracking-[0.08em] ${
94
119
  model.badges.some((badge) => badge.tone === "accent")
@@ -0,0 +1,128 @@
1
+ import React from "react";
2
+
3
+ import type { StyleCatalogSnapshot } from "../../api/public-types";
4
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
5
+
6
+ export interface TwTableContextToolbarProps {
7
+ disabled: boolean;
8
+ tableStyles: StyleCatalogSnapshot["tables"];
9
+ onSetTableStyle?: (styleId: string) => void;
10
+ onAddRowBefore?: () => void;
11
+ onAddRowAfter?: () => void;
12
+ onAddColumnBefore?: () => void;
13
+ onAddColumnAfter?: () => void;
14
+ onDeleteRow?: () => void;
15
+ onDeleteColumn?: () => void;
16
+ onMergeCells?: () => void;
17
+ onSplitCell?: () => void;
18
+ onSetCellBackground?: (color: string) => void;
19
+ onDeleteTable?: () => void;
20
+ }
21
+
22
+ const CELL_COLORS = [
23
+ "#ffffff",
24
+ "#f0f0ee",
25
+ "#dbeafe",
26
+ "#fef3c7",
27
+ "#dcfce7",
28
+ "#fce7f3",
29
+ ] as const;
30
+
31
+ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
32
+ return (
33
+ <div
34
+ data-testid="table-context-toolbar"
35
+ className="flex flex-wrap items-center gap-2 rounded-xl border border-border bg-canvas px-3 py-2 shadow-sm"
36
+ >
37
+ <span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
38
+ Table
39
+ </span>
40
+
41
+ <select
42
+ aria-label="Table style"
43
+ className="h-8 rounded-md border border-border bg-canvas px-2 text-xs text-primary disabled:opacity-40"
44
+ disabled={props.disabled || props.tableStyles.length === 0 || !props.onSetTableStyle}
45
+ onMouseDown={preserveEditorSelectionMouseDown}
46
+ onChange={(event) => props.onSetTableStyle?.(event.target.value)}
47
+ defaultValue=""
48
+ >
49
+ <option value="" disabled>Table style</option>
50
+ {props.tableStyles.map((style) => (
51
+ <option key={style.styleId} value={style.styleId}>
52
+ {style.displayName}
53
+ </option>
54
+ ))}
55
+ </select>
56
+
57
+ <ToolbarButton ariaLabel="Add row above" disabled={props.disabled} onClick={props.onAddRowBefore}>
58
+ Row above
59
+ </ToolbarButton>
60
+ <ToolbarButton ariaLabel="Add row below" disabled={props.disabled} onClick={props.onAddRowAfter}>
61
+ Row below
62
+ </ToolbarButton>
63
+ <ToolbarButton ariaLabel="Delete row" disabled={props.disabled} onClick={props.onDeleteRow}>
64
+ Delete row
65
+ </ToolbarButton>
66
+ <ToolbarButton ariaLabel="Add column left" disabled={props.disabled} onClick={props.onAddColumnBefore}>
67
+ Column left
68
+ </ToolbarButton>
69
+ <ToolbarButton ariaLabel="Add column right" disabled={props.disabled} onClick={props.onAddColumnAfter}>
70
+ Column right
71
+ </ToolbarButton>
72
+ <ToolbarButton ariaLabel="Delete column" disabled={props.disabled} onClick={props.onDeleteColumn}>
73
+ Delete column
74
+ </ToolbarButton>
75
+ <ToolbarButton ariaLabel="Merge cells" disabled={props.disabled} onClick={props.onMergeCells}>
76
+ Merge
77
+ </ToolbarButton>
78
+ <ToolbarButton ariaLabel="Split cell" disabled={props.disabled} onClick={props.onSplitCell}>
79
+ Split
80
+ </ToolbarButton>
81
+
82
+ <div className="flex items-center gap-1">
83
+ <span className="text-[11px] text-secondary">Fill</span>
84
+ {CELL_COLORS.map((color) => (
85
+ <button
86
+ key={color}
87
+ type="button"
88
+ aria-label={`Set cell fill ${color}`}
89
+ disabled={props.disabled || !props.onSetCellBackground}
90
+ onMouseDown={preserveEditorSelectionMouseDown}
91
+ onClick={() => props.onSetCellBackground?.(color)}
92
+ className="h-6 w-6 rounded border border-border disabled:opacity-40"
93
+ style={{ backgroundColor: color }}
94
+ />
95
+ ))}
96
+ </div>
97
+
98
+ <ToolbarButton ariaLabel="Delete table" danger disabled={props.disabled} onClick={props.onDeleteTable}>
99
+ Delete table
100
+ </ToolbarButton>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function ToolbarButton(props: {
106
+ ariaLabel: string;
107
+ children: React.ReactNode;
108
+ danger?: boolean;
109
+ disabled: boolean;
110
+ onClick?: () => void;
111
+ }) {
112
+ return (
113
+ <button
114
+ type="button"
115
+ aria-label={props.ariaLabel}
116
+ disabled={props.disabled || !props.onClick}
117
+ onMouseDown={preserveEditorSelectionMouseDown}
118
+ onClick={props.onClick}
119
+ className={`inline-flex h-8 items-center rounded-md px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
120
+ props.danger
121
+ ? "text-danger hover:bg-danger/10"
122
+ : "text-primary hover:bg-surface"
123
+ }`}
124
+ >
125
+ {props.children}
126
+ </button>
127
+ );
128
+ }
@@ -1,4 +1,16 @@
1
- export type PerfProbeKind = "typing" | "selection";
1
+ export type PerfProbeKind =
2
+ | "typing"
3
+ | "selection"
4
+ | "runtime.create"
5
+ | "snapshot.surface"
6
+ | "snapshot.compatibility"
7
+ | "snapshot.navigation"
8
+ | "pm.rebuild"
9
+ | "pm.decorations"
10
+ | "pm.mount"
11
+ | "shell.render"
12
+ | "workspace.chrome"
13
+ | "selection.sync";
2
14
 
3
15
  export interface PerfProbeSample {
4
16
  token: string;
@@ -18,14 +30,13 @@ interface PerfProbeState {
18
30
  pending?: Record<string, PendingProbe>;
19
31
  samples?: PerfProbeSample[];
20
32
  maxSamples?: number;
33
+ invalidationCounts?: Record<string, number>;
21
34
  }
22
35
 
23
36
  export interface PerfProbeSummary {
24
37
  samples: PerfProbeSample[];
25
- latest: {
26
- typing: PerfProbeSample | null;
27
- selection: PerfProbeSample | null;
28
- };
38
+ latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>>;
39
+ invalidationCounts: Record<string, number>;
29
40
  }
30
41
 
31
42
  declare global {
@@ -69,16 +80,47 @@ export function finishPerfProbe(token: string | null | undefined): PerfProbeSamp
69
80
  recordedAt: Date.now(),
70
81
  };
71
82
 
72
- state.samples ??= [];
73
- state.samples.push(sample);
74
- const maxSamples = state.maxSamples ?? 20;
75
- if (state.samples.length > maxSamples) {
76
- state.samples.splice(0, state.samples.length - maxSamples);
83
+ pushSample(state, sample);
84
+
85
+ return sample;
86
+ }
87
+
88
+ export function recordPerfSample(
89
+ kind: PerfProbeKind,
90
+ durationMs = 0,
91
+ ): PerfProbeSample | null {
92
+ const state = getEnabledState();
93
+ if (!state) {
94
+ return null;
77
95
  }
78
96
 
97
+ const token = `${kind}-${state.nextToken ?? 0}`;
98
+ state.nextToken = (state.nextToken ?? 0) + 1;
99
+ const sample: PerfProbeSample = {
100
+ token,
101
+ kind,
102
+ durationMs,
103
+ recordedAt: Date.now(),
104
+ };
105
+ pushSample(state, sample);
79
106
  return sample;
80
107
  }
81
108
 
109
+ export function incrementInvalidationCounter(
110
+ counter: string,
111
+ amount = 1,
112
+ ): number {
113
+ const state = getEnabledState();
114
+ if (!state) {
115
+ return 0;
116
+ }
117
+
118
+ state.invalidationCounts ??= {};
119
+ state.invalidationCounts[counter] =
120
+ (state.invalidationCounts[counter] ?? 0) + amount;
121
+ return state.invalidationCounts[counter]!;
122
+ }
123
+
82
124
  export function getLatestPerfSummary(): PerfProbeSummary | null {
83
125
  const state = getEnabledState();
84
126
  const samples = state?.samples ?? [];
@@ -88,13 +130,22 @@ export function getLatestPerfSummary(): PerfProbeSummary | null {
88
130
 
89
131
  return {
90
132
  samples: [...samples],
91
- latest: {
92
- typing: [...samples].reverse().find((sample) => sample.kind === "typing") ?? null,
93
- selection: [...samples].reverse().find((sample) => sample.kind === "selection") ?? null,
94
- },
133
+ latest: buildLatestSampleMap(samples),
134
+ invalidationCounts: { ...(state.invalidationCounts ?? {}) },
95
135
  };
96
136
  }
97
137
 
138
+ export function resetPerfProbeState(): void {
139
+ const state = getEnabledState();
140
+ if (!state) {
141
+ return;
142
+ }
143
+ state.nextToken = 0;
144
+ state.pending = {};
145
+ state.samples = [];
146
+ state.invalidationCounts = {};
147
+ }
148
+
98
149
  function getEnabledState(): PerfProbeState | null {
99
150
  if (typeof window === "undefined") {
100
151
  return null;
@@ -105,3 +156,24 @@ function getEnabledState(): PerfProbeState | null {
105
156
  }
106
157
  return state;
107
158
  }
159
+
160
+ function pushSample(state: PerfProbeState, sample: PerfProbeSample): void {
161
+ state.samples ??= [];
162
+ state.samples.push(sample);
163
+ const maxSamples = state.maxSamples ?? 20;
164
+ if (state.samples.length > maxSamples) {
165
+ state.samples.splice(0, state.samples.length - maxSamples);
166
+ }
167
+ }
168
+
169
+ function buildLatestSampleMap(
170
+ samples: PerfProbeSample[],
171
+ ): Partial<Record<PerfProbeKind, PerfProbeSample | null>> {
172
+ const latest: Partial<Record<PerfProbeKind, PerfProbeSample | null>> = {};
173
+ for (const sample of [...samples].reverse()) {
174
+ if (latest[sample.kind] === undefined) {
175
+ latest[sample.kind] = sample;
176
+ }
177
+ }
178
+ return latest;
179
+ }
@@ -72,9 +72,9 @@ export function createCommandBridgePlugins(
72
72
  return;
73
73
  }
74
74
 
75
- const { from, to } = view.state.selection;
75
+ const { anchor, head } = view.state.selection;
76
76
  callbacks.onSelectionChange(
77
- createSelectionSnapshot(posMap.pmToRuntime(from), posMap.pmToRuntime(to)),
77
+ createSelectionSnapshot(posMap.pmToRuntime(anchor), posMap.pmToRuntime(head)),
78
78
  );
79
79
  }
80
80
  },