@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.
Files changed (65) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +84 -1
  5. package/src/core/commands/index.ts +19 -2
  6. package/src/core/selection/mapping.ts +6 -0
  7. package/src/io/docx-session.ts +24 -9
  8. package/src/io/export/build-app-properties-xml.ts +88 -0
  9. package/src/io/export/serialize-comments.ts +6 -1
  10. package/src/io/export/serialize-footnotes.ts +10 -9
  11. package/src/io/export/serialize-headers-footers.ts +11 -10
  12. package/src/io/export/serialize-main-document.ts +337 -50
  13. package/src/io/export/serialize-numbering.ts +115 -24
  14. package/src/io/export/serialize-tables.ts +13 -11
  15. package/src/io/export/table-properties-xml.ts +35 -16
  16. package/src/io/export/twip.ts +66 -0
  17. package/src/io/normalize/normalize-text.ts +5 -0
  18. package/src/io/ooxml/parse-footnotes.ts +2 -1
  19. package/src/io/ooxml/parse-headers-footers.ts +2 -1
  20. package/src/io/ooxml/parse-main-document.ts +21 -1
  21. package/src/legal/bookmarks.ts +78 -0
  22. package/src/model/canonical-document.ts +11 -0
  23. package/src/review/store/scope-tag-diff.ts +130 -0
  24. package/src/runtime/document-navigation.ts +1 -305
  25. package/src/runtime/document-runtime.ts +178 -16
  26. package/src/runtime/layout/docx-font-loader.ts +143 -0
  27. package/src/runtime/layout/index.ts +188 -0
  28. package/src/runtime/layout/inert-layout-facet.ts +45 -0
  29. package/src/runtime/layout/layout-engine-instance.ts +618 -0
  30. package/src/runtime/layout/layout-invalidation.ts +257 -0
  31. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  32. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  33. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  34. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  35. package/src/runtime/layout/page-graph.ts +433 -0
  36. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  37. package/src/runtime/layout/page-story-resolver.ts +195 -0
  38. package/src/runtime/layout/paginated-layout-engine.ts +788 -0
  39. package/src/runtime/layout/public-facet.ts +705 -0
  40. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  41. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  42. package/src/runtime/scope-tag-registry.ts +95 -0
  43. package/src/runtime/session-capabilities.ts +7 -4
  44. package/src/runtime/surface-projection.ts +1 -0
  45. package/src/runtime/text-ack-range.ts +49 -0
  46. package/src/ui/WordReviewEditor.tsx +15 -0
  47. package/src/ui/editor-runtime-boundary.ts +10 -1
  48. package/src/ui/editor-surface-controller.tsx +3 -0
  49. package/src/ui/headless/chrome-registry.ts +235 -0
  50. package/src/ui/headless/scoped-chrome-policy.ts +164 -0
  51. package/src/ui/headless/selection-tool-context.ts +2 -0
  52. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  53. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +333 -0
  54. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +89 -0
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +21 -1
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +8 -1
  57. package/src/ui-tailwind/editor-surface/pm-decorations.ts +73 -13
  58. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  59. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  60. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  61. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +173 -6
  62. package/src/ui-tailwind/theme/editor-theme.css +40 -14
  63. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  64. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +235 -166
  65. 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 { PositionMap } from "./pm-position-map";
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
- onInsertText: (text) => callbacksRef.current?.onInsertText(text),
275
- onDeleteBackward: () => callbacksRef.current?.onDeleteBackward(),
276
- onDeleteForward: () => callbacksRef.current?.onDeleteForward(),
277
- onSplitParagraph: () => callbacksRef.current?.onSplitParagraph(),
278
- onInsertHardBreak: () => callbacksRef.current?.onInsertHardBreak(),
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
- padding-left: 0.875rem;
336
- border-radius: 0.25rem;
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.2rem;
344
- bottom: 0.2rem;
345
- width: 0.3rem;
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.3125rem;
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
- background: color-mix(in srgb, var(--color-accent) 7%, transparent);
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
- background: color-mix(in srgb, var(--color-warning) 8%, transparent);
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
- background: color-mix(in srgb, var(--color-insert) 7%, transparent);
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
- background: color-mix(in srgb, var(--color-secondary) 6%, transparent);
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
- background: color-mix(in srgb, var(--color-warning) 4%, transparent);
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
- background: color-mix(in srgb, var(--color-danger) 5%, transparent);
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
- background: color-mix(in srgb, var(--color-danger) 8%, transparent);
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-7 w-7 items-center justify-center rounded-lg border border-transparent transition-colors outline-none",
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-4 w-4" />
39
+ <props.icon className="h-3.5 w-3.5" />
40
40
  </button>
41
41
  </Tooltip.Trigger>
42
42
  <Tooltip.Portal>