@beyondwork/docx-react-component 1.0.35 → 1.0.37
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/README.md +103 -13
- package/package.json +1 -1
- package/src/api/package-version.ts +13 -0
- package/src/api/public-types.ts +84 -1
- package/src/core/commands/index.ts +19 -2
- package/src/core/selection/mapping.ts +6 -0
- package/src/io/docx-session.ts +24 -9
- package/src/io/export/build-app-properties-xml.ts +88 -0
- package/src/io/export/serialize-comments.ts +6 -1
- package/src/io/export/serialize-footnotes.ts +10 -9
- package/src/io/export/serialize-headers-footers.ts +11 -10
- package/src/io/export/serialize-main-document.ts +337 -50
- package/src/io/export/serialize-numbering.ts +115 -24
- package/src/io/export/serialize-tables.ts +13 -11
- package/src/io/export/table-properties-xml.ts +35 -16
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +5 -0
- package/src/io/ooxml/parse-footnotes.ts +2 -1
- package/src/io/ooxml/parse-headers-footers.ts +2 -1
- package/src/io/ooxml/parse-main-document.ts +21 -1
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +11 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-navigation.ts +1 -305
- package/src/runtime/document-runtime.ts +178 -16
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +188 -0
- package/src/runtime/layout/inert-layout-facet.ts +45 -0
- package/src/runtime/layout/layout-engine-instance.ts +618 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
- package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +433 -0
- package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
- package/src/runtime/layout/page-story-resolver.ts +195 -0
- package/src/runtime/layout/paginated-layout-engine.ts +788 -0
- package/src/runtime/layout/public-facet.ts +705 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/session-capabilities.ts +7 -4
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/ui/WordReviewEditor.tsx +15 -0
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +235 -0
- package/src/ui/headless/scoped-chrome-policy.ts +164 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
- package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
- package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
- package/src/ui-tailwind/theme/editor-theme.css +40 -14
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +27 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorSurfaceSnapshot,
|
|
3
|
+
SurfaceBlockSnapshot,
|
|
4
|
+
} from "../../api/public-types.ts";
|
|
5
|
+
import type { ScopeTagRegistry } from "../../runtime/scope-tag-registry.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pre-flight check the FastTextEditLane consults before applying a predicted
|
|
9
|
+
* transaction. Returns true when the proposed edit range intersects any tag
|
|
10
|
+
* whose registry behavior is `bailIfCrossed: true` — today, that is fields,
|
|
11
|
+
* SDTs, and opaque (preserve-only) blocks. Such edits would be rolled back by
|
|
12
|
+
* the runtime's workflow / structural-divergence checks anyway; bailing
|
|
13
|
+
* before predicting saves the predicted-then-restored PM churn.
|
|
14
|
+
*
|
|
15
|
+
* Phase 1 scope: top-level surface blocks (paragraph, opaque_block, sdt_block)
|
|
16
|
+
* plus inline field_ref segments inside paragraphs. Does NOT recurse into
|
|
17
|
+
* sdt_block.children (the block boundary already bails). Does NOT walk into
|
|
18
|
+
* `table` blocks or their cells (left to the runtime safety net).
|
|
19
|
+
*
|
|
20
|
+
* Boundary semantics: this uses strict-open intersection
|
|
21
|
+
* (`aFrom < bTo && aTo > bFrom`). A collapsed cursor sitting exactly at
|
|
22
|
+
* a tag boundary is NOT considered intersecting. This intentionally
|
|
23
|
+
* under-bails on boundary-touching cursors: an insert at the left edge
|
|
24
|
+
* of a field is a legal edit (the field shifts), so over-bailing would
|
|
25
|
+
* cost a predicted-tx optimization for no correctness benefit. The
|
|
26
|
+
* downside is that a delete-forward at the left edge or a delete-backward
|
|
27
|
+
* at the right edge of a bail-if-crossed tag will fall through to the
|
|
28
|
+
* runtime, which still rejects the edit — the lane pays one
|
|
29
|
+
* predicted-then-rolled-back PM cycle for those keystrokes. A future
|
|
30
|
+
* phase that takes the predicted intent's direction can tighten this.
|
|
31
|
+
*/
|
|
32
|
+
export function hasBailIfCrossedTagInRange(
|
|
33
|
+
surface: EditorSurfaceSnapshot,
|
|
34
|
+
registry: ScopeTagRegistry,
|
|
35
|
+
fromRuntime: number,
|
|
36
|
+
toRuntime: number,
|
|
37
|
+
): boolean {
|
|
38
|
+
const opaqueBails = registry.get("opaque").bailIfCrossed;
|
|
39
|
+
const sdtBails = registry.get("sdt").bailIfCrossed;
|
|
40
|
+
const fieldBails = registry.get("field").bailIfCrossed;
|
|
41
|
+
for (const block of surface.blocks) {
|
|
42
|
+
if (!intersects(block.from, block.to, fromRuntime, toRuntime)) continue;
|
|
43
|
+
if (block.kind === "opaque_block" && opaqueBails) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (block.kind === "sdt_block" && sdtBails) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (block.kind === "paragraph" && fieldBails) {
|
|
50
|
+
for (const segment of block.segments) {
|
|
51
|
+
if (segment.kind !== "field_ref") continue;
|
|
52
|
+
if (intersects(segment.from, segment.to, fromRuntime, toRuntime)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function intersects(aFrom: number, aTo: number, bFrom: number, bTo: number): boolean {
|
|
62
|
+
return aFrom < bTo && aTo > bFrom;
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Plugin, PluginKey, type Transaction } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Key used to stamp predicted PM transactions with their `opId`. The gate
|
|
5
|
+
* plugin reads this meta and allows a doc-changing transaction through only
|
|
6
|
+
* when the lane has registered the `opId`.
|
|
7
|
+
*/
|
|
8
|
+
export const PREDICTED_META_KEY = "bounded-local-first/predicted";
|
|
9
|
+
|
|
10
|
+
export interface PredictedMeta {
|
|
11
|
+
opId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PredictedTxGateOptions {
|
|
15
|
+
/** The lane's `LocalEditSessionState.isPredicted(opId)` — consulted per tx. */
|
|
16
|
+
isPredicted(opId: string): boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const gateKey = new PluginKey("predicted-tx-gate");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* PredictedTxGate — replaces the unconditional "block every `docChanged`"
|
|
23
|
+
* filter with one that lets through predicted transactions whose `opId` the
|
|
24
|
+
* lane has registered. Unregistered or unstamped doc-changing transactions
|
|
25
|
+
* are still blocked — the runtime remains the canonical mutation path.
|
|
26
|
+
*
|
|
27
|
+
* Selection-only transactions always pass.
|
|
28
|
+
*/
|
|
29
|
+
export function createPredictedTxGate(options: PredictedTxGateOptions): Plugin {
|
|
30
|
+
return new Plugin({
|
|
31
|
+
key: gateKey,
|
|
32
|
+
filterTransaction(tr: Transaction) {
|
|
33
|
+
if (!tr.docChanged) return true;
|
|
34
|
+
const meta = tr.getMeta(PREDICTED_META_KEY) as PredictedMeta | undefined;
|
|
35
|
+
if (!meta) return false;
|
|
36
|
+
return options.isPredicted(meta.opId);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -52,7 +52,12 @@ import {
|
|
|
52
52
|
recordPerfSample,
|
|
53
53
|
startPerfProbe,
|
|
54
54
|
} from "./perf-probe";
|
|
55
|
-
import type
|
|
55
|
+
import { buildPositionMap, type PositionMap } from "./pm-position-map";
|
|
56
|
+
import { createLocalEditSessionState } from "./local-edit-session-state";
|
|
57
|
+
import { createFastTextEditLane } from "./fast-text-edit-lane";
|
|
58
|
+
import { createPredictedTxGate } from "./predicted-tx-gate";
|
|
59
|
+
import { createScopeTagRegistry } from "../../runtime/scope-tag-registry";
|
|
60
|
+
import { hasBailIfCrossedTagInRange } from "./predicted-tag-preflight";
|
|
56
61
|
import {
|
|
57
62
|
clearSearch as clearSearchPlugin,
|
|
58
63
|
createSearchPlugin,
|
|
@@ -111,6 +116,15 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
111
116
|
activeWorkflowWorkItemId?: string | null;
|
|
112
117
|
activeWorkflowScopeIds?: readonly string[];
|
|
113
118
|
workflowMetadata?: readonly WorkflowMetadataMarkup[];
|
|
119
|
+
/**
|
|
120
|
+
* Synchronous dispatcher for predicted-lane runtime commands. When provided,
|
|
121
|
+
* the surface routes text input through `FastTextEditLane` for immediate
|
|
122
|
+
* local PM feel; when absent, it falls back to the legacy callback-based
|
|
123
|
+
* round-trip that calls `runtime.applyActiveStoryTextCommand` externally.
|
|
124
|
+
*/
|
|
125
|
+
dispatchRuntimeCommand?: (
|
|
126
|
+
command: import("./fast-text-edit-lane").LaneRuntimeCommand,
|
|
127
|
+
) => import("../../api/public-types").TextCommandAck;
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
export interface TwProseMirrorSurfaceRef {
|
|
@@ -157,6 +171,9 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
157
171
|
const documentBuildKeyRef = useRef<string | null>(null);
|
|
158
172
|
const decorationBuildKeyRef = useRef<string | null>(null);
|
|
159
173
|
const suppressSelectionEchoRef = useRef(false);
|
|
174
|
+
const sessionRef = useRef<import("./local-edit-session-state").LocalEditSessionState | null>(null);
|
|
175
|
+
const laneRef = useRef<import("./fast-text-edit-lane").FastTextEditLane | null>(null);
|
|
176
|
+
const equivalentAckKeyRef = useRef<string | null>(null);
|
|
160
177
|
const selectionToolbarFrameRef = useRef<number | null>(null);
|
|
161
178
|
const lastSelectionToolbarMeasurementRef = useRef<{
|
|
162
179
|
key: string | null;
|
|
@@ -165,6 +182,13 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
165
182
|
key: null,
|
|
166
183
|
anchor: null,
|
|
167
184
|
});
|
|
185
|
+
const snapshotRef = useRef(snapshot);
|
|
186
|
+
snapshotRef.current = snapshot;
|
|
187
|
+
|
|
188
|
+
const scopeTagRegistry = useMemo(
|
|
189
|
+
() => createScopeTagRegistry(),
|
|
190
|
+
[],
|
|
191
|
+
);
|
|
168
192
|
|
|
169
193
|
// Keep callbacks ref up to date (avoids stale closures in PM plugins)
|
|
170
194
|
callbacksRef.current = {
|
|
@@ -263,6 +287,10 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
263
287
|
callbacksRef.current?.isSelectionSyncSuppressed?.() ?? false,
|
|
264
288
|
};
|
|
265
289
|
|
|
290
|
+
const gate = createPredictedTxGate({
|
|
291
|
+
isPredicted: (opId) => sessionRef.current?.isPredicted(opId) ?? false,
|
|
292
|
+
});
|
|
293
|
+
|
|
266
294
|
const corePlugins = props.ydoc
|
|
267
295
|
? createCollabPlugins({
|
|
268
296
|
ydoc: props.ydoc,
|
|
@@ -271,11 +299,42 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
271
299
|
})
|
|
272
300
|
: createCommandBridgePlugins({
|
|
273
301
|
...selectionCallbacks,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
302
|
+
gate,
|
|
303
|
+
onInsertText: (text) => {
|
|
304
|
+
if (laneRef.current) {
|
|
305
|
+
laneRef.current.onInsertText(text);
|
|
306
|
+
} else {
|
|
307
|
+
callbacksRef.current?.onInsertText(text);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
onDeleteBackward: () => {
|
|
311
|
+
if (laneRef.current) {
|
|
312
|
+
laneRef.current.onDeleteBackward();
|
|
313
|
+
} else {
|
|
314
|
+
callbacksRef.current?.onDeleteBackward();
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
onDeleteForward: () => {
|
|
318
|
+
if (laneRef.current) {
|
|
319
|
+
laneRef.current.onDeleteForward();
|
|
320
|
+
} else {
|
|
321
|
+
callbacksRef.current?.onDeleteForward();
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
onSplitParagraph: () => {
|
|
325
|
+
if (laneRef.current) {
|
|
326
|
+
laneRef.current.onSplitParagraph();
|
|
327
|
+
} else {
|
|
328
|
+
callbacksRef.current?.onSplitParagraph();
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
onInsertHardBreak: () => {
|
|
332
|
+
if (laneRef.current) {
|
|
333
|
+
laneRef.current.onInsertHardBreak();
|
|
334
|
+
} else {
|
|
335
|
+
callbacksRef.current?.onInsertHardBreak();
|
|
336
|
+
}
|
|
337
|
+
},
|
|
279
338
|
onInsertTab: () => callbacksRef.current?.onInsertTab(),
|
|
280
339
|
onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
|
|
281
340
|
onUndo: () => callbacksRef.current?.onUndo(),
|
|
@@ -338,6 +397,72 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
338
397
|
],
|
|
339
398
|
);
|
|
340
399
|
|
|
400
|
+
// Create the local edit session once per mount. The session is scoped to the
|
|
401
|
+
// view lifetime; the rebuild effect seeds the base revision token below.
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
sessionRef.current = createLocalEditSessionState({
|
|
404
|
+
baseRevisionToken: snapshot.revisionToken,
|
|
405
|
+
});
|
|
406
|
+
return () => {
|
|
407
|
+
sessionRef.current = null;
|
|
408
|
+
laneRef.current = null;
|
|
409
|
+
};
|
|
410
|
+
// Intentionally empty deps: session is scoped to the view lifetime.
|
|
411
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
412
|
+
}, []);
|
|
413
|
+
|
|
414
|
+
// Build the FastTextEditLane whenever `dispatchRuntimeCommand` changes.
|
|
415
|
+
// The lane is consulted via `laneRef.current` inside PM plugin callbacks,
|
|
416
|
+
// so the plugins memo does not need to depend on this effect.
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (!props.dispatchRuntimeCommand || !sessionRef.current) {
|
|
419
|
+
laneRef.current = null;
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
laneRef.current = createFastTextEditLane({
|
|
423
|
+
session: sessionRef.current,
|
|
424
|
+
getView: () => viewRef.current,
|
|
425
|
+
getPositionMap: () => positionMapRef.current,
|
|
426
|
+
dispatchRuntimeCommand: props.dispatchRuntimeCommand,
|
|
427
|
+
suppressSelectionSync: (suppressed) => {
|
|
428
|
+
suppressSelectionEchoRef.current = suppressed;
|
|
429
|
+
},
|
|
430
|
+
shouldBailBeforePredict: (_intent, fromRuntime, toRuntime) => {
|
|
431
|
+
const surface = snapshotRef.current.surface;
|
|
432
|
+
if (!surface) return false;
|
|
433
|
+
return hasBailIfCrossedTagInRange(
|
|
434
|
+
surface,
|
|
435
|
+
scopeTagRegistry,
|
|
436
|
+
fromRuntime,
|
|
437
|
+
toRuntime,
|
|
438
|
+
);
|
|
439
|
+
},
|
|
440
|
+
onEquivalentAck: () => {
|
|
441
|
+
// INVARIANT: this marker is set only by onEquivalentAck, which the
|
|
442
|
+
// runtime invokes synchronously from dispatchRuntimeCommand. The
|
|
443
|
+
// rebuild effect's short-circuit (search for "Predicted-lane
|
|
444
|
+
// short-circuit" below) reads it during the same React render cycle
|
|
445
|
+
// that the predicted dispatch triggered. If the runtime ack ever
|
|
446
|
+
// becomes async (microtask, animation frame, network round-trip),
|
|
447
|
+
// this marker will be stale by the time the rebuild effect runs and
|
|
448
|
+
// the short-circuit must be replaced with a
|
|
449
|
+
// `pendingEquivalentAckOpIds: Set<string>` ledger keyed by opId.
|
|
450
|
+
equivalentAckKeyRef.current = documentBuildKeyRef.current;
|
|
451
|
+
},
|
|
452
|
+
onAdjustedAck: () => {
|
|
453
|
+
// Adjusted path: allow the rebuild effect to run (it will call
|
|
454
|
+
// view.updateState with the canonical snapshot).
|
|
455
|
+
equivalentAckKeyRef.current = null;
|
|
456
|
+
},
|
|
457
|
+
onRejectedAck: () => {
|
|
458
|
+
equivalentAckKeyRef.current = null;
|
|
459
|
+
},
|
|
460
|
+
onStructuralDivergence: () => {
|
|
461
|
+
equivalentAckKeyRef.current = null;
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}, [props.dispatchRuntimeCommand, scopeTagRegistry]);
|
|
465
|
+
|
|
341
466
|
useEffect(() => {
|
|
342
467
|
if (!mountRef.current || !surface) return;
|
|
343
468
|
|
|
@@ -350,6 +475,34 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
350
475
|
return;
|
|
351
476
|
}
|
|
352
477
|
|
|
478
|
+
// Predicted-lane short-circuit: if the lane just confirmed an equivalent
|
|
479
|
+
// ack, the PM doc already matches the canonical snapshot. Update tracking
|
|
480
|
+
// refs and decorations without rebuilding the PM state.
|
|
481
|
+
//
|
|
482
|
+
// INVARIANT: reads `equivalentAckKeyRef.current` set by `onEquivalentAck`
|
|
483
|
+
// above. Depends on the runtime ack being synchronous so the marker is
|
|
484
|
+
// already in place when this effect runs after the predicted dispatch.
|
|
485
|
+
// See the comment at `onEquivalentAck` for the async-ack migration path.
|
|
486
|
+
if (
|
|
487
|
+
viewRef.current &&
|
|
488
|
+
equivalentAckKeyRef.current !== null &&
|
|
489
|
+
sessionRef.current &&
|
|
490
|
+
!sessionRef.current.hasPending() &&
|
|
491
|
+
sessionRef.current.getBaseRevisionToken() === snapshot.revisionToken
|
|
492
|
+
) {
|
|
493
|
+
// Rebuild the position map so runtime↔PM translations track the new
|
|
494
|
+
// canonical surface shape. The PM document itself is already correct.
|
|
495
|
+
positionMapRef.current = buildPositionMap(surface);
|
|
496
|
+
documentBuildKeyRef.current = documentBuildKey;
|
|
497
|
+
applyDecorationProps(viewRef.current, positionMapRef.current);
|
|
498
|
+
equivalentAckKeyRef.current = null;
|
|
499
|
+
if (pendingTypingProbeRef.current) {
|
|
500
|
+
finishPerfProbe(pendingTypingProbeRef.current);
|
|
501
|
+
pendingTypingProbeRef.current = null;
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
353
506
|
const { state, positionMap } = createPMStateFromSnapshot(
|
|
354
507
|
surface,
|
|
355
508
|
snapshot.selection,
|
|
@@ -473,6 +626,20 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
473
626
|
return;
|
|
474
627
|
}
|
|
475
628
|
|
|
629
|
+
// Skip the dispatch when PM is blurred. `view.dispatch(setSelection)`
|
|
630
|
+
// synchronizes the DOM selection, and on a blurred contenteditable some
|
|
631
|
+
// browsers pull DOM focus back to the editor as a side effect — which
|
|
632
|
+
// would steal focus from any toolbar button or other chrome the user
|
|
633
|
+
// just clicked. PM's state.selection is allowed to diverge from the
|
|
634
|
+
// runtime's selection while PM is blurred; the natural focus flow
|
|
635
|
+
// (click → onFocus → activeRuntime.focus() → snapshot update → this
|
|
636
|
+
// effect fires again with view.hasFocus() === true) resyncs them when
|
|
637
|
+
// PM regains focus, and the next canonical rebuild also re-derives PM
|
|
638
|
+
// selection from the snapshot.
|
|
639
|
+
if (!view.hasFocus()) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
476
643
|
suppressSelectionEchoRef.current = true;
|
|
477
644
|
view.dispatch(view.state.tr.setSelection(nextSelection));
|
|
478
645
|
recordPerfSample("selection.sync");
|
|
@@ -332,23 +332,40 @@
|
|
|
332
332
|
|
|
333
333
|
.prosemirror-surface .ProseMirror .wre-workflow-rail {
|
|
334
334
|
position: relative;
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
isolation: isolate;
|
|
336
|
+
overflow: visible;
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
.prosemirror-surface .ProseMirror .wre-workflow-rail::before {
|
|
340
340
|
content: "";
|
|
341
341
|
position: absolute;
|
|
342
|
-
left: 0;
|
|
343
|
-
top: 0
|
|
344
|
-
bottom: 0
|
|
345
|
-
width: 0.
|
|
342
|
+
left: -0.85rem;
|
|
343
|
+
top: 0;
|
|
344
|
+
bottom: 0;
|
|
345
|
+
width: 0.22rem;
|
|
346
346
|
border-radius: 999px;
|
|
347
347
|
background: var(--wre-workflow-rail-color, var(--color-border-strong));
|
|
348
|
+
z-index: 1;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.prosemirror-surface .ProseMirror .wre-workflow-rail::after {
|
|
352
|
+
content: "";
|
|
353
|
+
position: absolute;
|
|
354
|
+
left: -0.35rem;
|
|
355
|
+
right: -0.25rem;
|
|
356
|
+
top: -0.2rem;
|
|
357
|
+
bottom: -0.2rem;
|
|
358
|
+
border-radius: 0.45rem;
|
|
359
|
+
background: var(--wre-workflow-overlay-fill, transparent);
|
|
360
|
+
box-shadow:
|
|
361
|
+
inset 0 0 0 1px var(--wre-workflow-overlay-stroke, transparent),
|
|
362
|
+
0 8px 22px -22px color-mix(in srgb, var(--wre-workflow-rail-color, var(--color-border-strong)) 28%, transparent);
|
|
363
|
+
pointer-events: none;
|
|
364
|
+
z-index: -1;
|
|
348
365
|
}
|
|
349
366
|
|
|
350
367
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-active::before {
|
|
351
|
-
width: 0.
|
|
368
|
+
width: 0.26rem;
|
|
352
369
|
opacity: 1;
|
|
353
370
|
box-shadow: 0 0 0 1px color-mix(in oklab, var(--wre-workflow-rail-color, var(--color-border-strong)) 30%, transparent);
|
|
354
371
|
}
|
|
@@ -366,27 +383,32 @@
|
|
|
366
383
|
|
|
367
384
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-edit {
|
|
368
385
|
--wre-workflow-rail-color: var(--color-accent);
|
|
369
|
-
|
|
386
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-accent) 9%, transparent);
|
|
387
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-accent) 16%, transparent);
|
|
370
388
|
}
|
|
371
389
|
|
|
372
390
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-suggest {
|
|
373
391
|
--wre-workflow-rail-color: var(--color-warning);
|
|
374
|
-
|
|
392
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-warning) 10%, transparent);
|
|
393
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-warning) 17%, transparent);
|
|
375
394
|
}
|
|
376
395
|
|
|
377
396
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-comment {
|
|
378
397
|
--wre-workflow-rail-color: var(--color-insert);
|
|
379
|
-
|
|
398
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-insert) 9%, transparent);
|
|
399
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-insert) 16%, transparent);
|
|
380
400
|
}
|
|
381
401
|
|
|
382
402
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-view {
|
|
383
403
|
--wre-workflow-rail-color: var(--color-secondary);
|
|
384
|
-
|
|
404
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-secondary) 7%, transparent);
|
|
405
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-secondary) 12%, transparent);
|
|
385
406
|
}
|
|
386
407
|
|
|
387
408
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-candidate {
|
|
388
409
|
--wre-workflow-rail-color: var(--color-warning);
|
|
389
|
-
|
|
410
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-warning) 5%, transparent);
|
|
411
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-warning) 14%, transparent);
|
|
390
412
|
}
|
|
391
413
|
|
|
392
414
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-candidate::before {
|
|
@@ -402,7 +424,8 @@
|
|
|
402
424
|
|
|
403
425
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-preserve-only {
|
|
404
426
|
--wre-workflow-rail-color: var(--color-danger);
|
|
405
|
-
|
|
427
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-danger) 7%, transparent);
|
|
428
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-danger) 16%, transparent);
|
|
406
429
|
}
|
|
407
430
|
|
|
408
431
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-preserve-only::before {
|
|
@@ -411,7 +434,8 @@
|
|
|
411
434
|
|
|
412
435
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-blocked-import {
|
|
413
436
|
--wre-workflow-rail-color: var(--color-danger);
|
|
414
|
-
|
|
437
|
+
--wre-workflow-overlay-fill: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
|
438
|
+
--wre-workflow-overlay-stroke: color-mix(in srgb, var(--color-danger) 20%, transparent);
|
|
415
439
|
}
|
|
416
440
|
|
|
417
441
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-blocked-import::before {
|
|
@@ -431,6 +455,8 @@
|
|
|
431
455
|
|
|
432
456
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-selection-zone {
|
|
433
457
|
background: transparent;
|
|
458
|
+
--wre-workflow-overlay-fill: transparent;
|
|
459
|
+
--wre-workflow-overlay-stroke: transparent;
|
|
434
460
|
}
|
|
435
461
|
|
|
436
462
|
.prosemirror-surface .ProseMirror .wre-workflow-rail-selection-zone::before {
|
|
@@ -26,7 +26,7 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
|
|
|
26
26
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
27
27
|
onClick={props.onClick}
|
|
28
28
|
className={[
|
|
29
|
-
"inline-flex h-
|
|
29
|
+
"inline-flex h-6 w-6 items-center justify-center rounded-md border border-transparent transition-colors outline-none",
|
|
30
30
|
"disabled:opacity-30 disabled:cursor-not-allowed",
|
|
31
31
|
props.emphasis
|
|
32
32
|
? "text-accent hover:border-border/60 hover:bg-surface"
|
|
@@ -36,7 +36,7 @@ export function TwToolbarIconButton(props: TwToolbarIconButtonProps) {
|
|
|
36
36
|
focusRingClass,
|
|
37
37
|
].join(" ")}
|
|
38
38
|
>
|
|
39
|
-
<props.icon className="h-
|
|
39
|
+
<props.icon className="h-3.5 w-3.5" />
|
|
40
40
|
</button>
|
|
41
41
|
</Tooltip.Trigger>
|
|
42
42
|
<Tooltip.Portal>
|