@beyondwork/docx-react-component 1.0.36 → 1.0.38
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 +402 -1
- package/src/core/commands/index.ts +18 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- 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 +328 -50
- package/src/io/export/serialize-numbering.ts +114 -24
- package/src/io/export/serialize-tables.ts +87 -11
- package/src/io/export/table-properties-xml.ts +174 -20
- package/src/io/export/twip.ts +66 -0
- package/src/io/normalize/normalize-text.ts +20 -0
- package/src/io/ooxml/parse-footnotes.ts +62 -1
- package/src/io/ooxml/parse-headers-footers.ts +62 -1
- package/src/io/ooxml/parse-main-document.ts +158 -1
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/legal/bookmarks.ts +78 -0
- package/src/model/canonical-document.ts +45 -0
- package/src/review/store/scope-tag-diff.ts +130 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +2 -306
- package/src/runtime/document-runtime.ts +287 -11
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/docx-font-loader.ts +143 -0
- package/src/runtime/layout/index.ts +233 -0
- package/src/runtime/layout/inert-layout-facet.ts +59 -0
- package/src/runtime/layout/layout-engine-instance.ts +628 -0
- package/src/runtime/layout/layout-invalidation.ts +257 -0
- package/src/runtime/layout/layout-measurement-provider.ts +175 -0
- package/src/runtime/layout/margin-preset-catalog.ts +178 -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-format-catalog.ts +233 -0
- package/src/runtime/layout/page-fragment-mapper.ts +179 -0
- package/src/runtime/layout/page-graph.ts +452 -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 +921 -0
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +1398 -0
- package/src/runtime/layout/resolved-formatting-document.ts +317 -0
- package/src/runtime/layout/resolved-formatting-state.ts +430 -0
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/scope-tag-registry.ts +95 -0
- package/src/runtime/surface-projection.ts +1 -0
- package/src/runtime/text-ack-range.ts +49 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +99 -15
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/editor-surface-controller.tsx +3 -0
- package/src/ui/headless/chrome-registry.ts +501 -0
- package/src/ui/headless/scoped-chrome-policy.ts +183 -0
- package/src/ui/headless/selection-tool-context.ts +2 -0
- package/src/ui/headless/selection-tool-resolver.ts +36 -17
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
- 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 +176 -6
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +505 -144
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
- package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
|
@@ -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,16 +299,50 @@ 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(),
|
|
282
341
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
283
342
|
onBlockedInput: (command, message) => callbacksRef.current?.onBlockedInput?.(command, message),
|
|
343
|
+
onCompositionChange: (composing) => {
|
|
344
|
+
sessionRef.current?.setComposing(composing);
|
|
345
|
+
},
|
|
284
346
|
});
|
|
285
347
|
|
|
286
348
|
return [
|
|
@@ -338,6 +400,72 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
338
400
|
],
|
|
339
401
|
);
|
|
340
402
|
|
|
403
|
+
// Create the local edit session once per mount. The session is scoped to the
|
|
404
|
+
// view lifetime; the rebuild effect seeds the base revision token below.
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
sessionRef.current = createLocalEditSessionState({
|
|
407
|
+
baseRevisionToken: snapshot.revisionToken,
|
|
408
|
+
});
|
|
409
|
+
return () => {
|
|
410
|
+
sessionRef.current = null;
|
|
411
|
+
laneRef.current = null;
|
|
412
|
+
};
|
|
413
|
+
// Intentionally empty deps: session is scoped to the view lifetime.
|
|
414
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
415
|
+
}, []);
|
|
416
|
+
|
|
417
|
+
// Build the FastTextEditLane whenever `dispatchRuntimeCommand` changes.
|
|
418
|
+
// The lane is consulted via `laneRef.current` inside PM plugin callbacks,
|
|
419
|
+
// so the plugins memo does not need to depend on this effect.
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
if (!props.dispatchRuntimeCommand || !sessionRef.current) {
|
|
422
|
+
laneRef.current = null;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
laneRef.current = createFastTextEditLane({
|
|
426
|
+
session: sessionRef.current,
|
|
427
|
+
getView: () => viewRef.current,
|
|
428
|
+
getPositionMap: () => positionMapRef.current,
|
|
429
|
+
dispatchRuntimeCommand: props.dispatchRuntimeCommand,
|
|
430
|
+
suppressSelectionSync: (suppressed) => {
|
|
431
|
+
suppressSelectionEchoRef.current = suppressed;
|
|
432
|
+
},
|
|
433
|
+
shouldBailBeforePredict: (_intent, fromRuntime, toRuntime) => {
|
|
434
|
+
const surface = snapshotRef.current.surface;
|
|
435
|
+
if (!surface) return false;
|
|
436
|
+
return hasBailIfCrossedTagInRange(
|
|
437
|
+
surface,
|
|
438
|
+
scopeTagRegistry,
|
|
439
|
+
fromRuntime,
|
|
440
|
+
toRuntime,
|
|
441
|
+
);
|
|
442
|
+
},
|
|
443
|
+
onEquivalentAck: () => {
|
|
444
|
+
// INVARIANT: this marker is set only by onEquivalentAck, which the
|
|
445
|
+
// runtime invokes synchronously from dispatchRuntimeCommand. The
|
|
446
|
+
// rebuild effect's short-circuit (search for "Predicted-lane
|
|
447
|
+
// short-circuit" below) reads it during the same React render cycle
|
|
448
|
+
// that the predicted dispatch triggered. If the runtime ack ever
|
|
449
|
+
// becomes async (microtask, animation frame, network round-trip),
|
|
450
|
+
// this marker will be stale by the time the rebuild effect runs and
|
|
451
|
+
// the short-circuit must be replaced with a
|
|
452
|
+
// `pendingEquivalentAckOpIds: Set<string>` ledger keyed by opId.
|
|
453
|
+
equivalentAckKeyRef.current = documentBuildKeyRef.current;
|
|
454
|
+
},
|
|
455
|
+
onAdjustedAck: () => {
|
|
456
|
+
// Adjusted path: allow the rebuild effect to run (it will call
|
|
457
|
+
// view.updateState with the canonical snapshot).
|
|
458
|
+
equivalentAckKeyRef.current = null;
|
|
459
|
+
},
|
|
460
|
+
onRejectedAck: () => {
|
|
461
|
+
equivalentAckKeyRef.current = null;
|
|
462
|
+
},
|
|
463
|
+
onStructuralDivergence: () => {
|
|
464
|
+
equivalentAckKeyRef.current = null;
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}, [props.dispatchRuntimeCommand, scopeTagRegistry]);
|
|
468
|
+
|
|
341
469
|
useEffect(() => {
|
|
342
470
|
if (!mountRef.current || !surface) return;
|
|
343
471
|
|
|
@@ -350,6 +478,34 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
350
478
|
return;
|
|
351
479
|
}
|
|
352
480
|
|
|
481
|
+
// Predicted-lane short-circuit: if the lane just confirmed an equivalent
|
|
482
|
+
// ack, the PM doc already matches the canonical snapshot. Update tracking
|
|
483
|
+
// refs and decorations without rebuilding the PM state.
|
|
484
|
+
//
|
|
485
|
+
// INVARIANT: reads `equivalentAckKeyRef.current` set by `onEquivalentAck`
|
|
486
|
+
// above. Depends on the runtime ack being synchronous so the marker is
|
|
487
|
+
// already in place when this effect runs after the predicted dispatch.
|
|
488
|
+
// See the comment at `onEquivalentAck` for the async-ack migration path.
|
|
489
|
+
if (
|
|
490
|
+
viewRef.current &&
|
|
491
|
+
equivalentAckKeyRef.current !== null &&
|
|
492
|
+
sessionRef.current &&
|
|
493
|
+
!sessionRef.current.hasPending() &&
|
|
494
|
+
sessionRef.current.getBaseRevisionToken() === snapshot.revisionToken
|
|
495
|
+
) {
|
|
496
|
+
// Rebuild the position map so runtime↔PM translations track the new
|
|
497
|
+
// canonical surface shape. The PM document itself is already correct.
|
|
498
|
+
positionMapRef.current = buildPositionMap(surface);
|
|
499
|
+
documentBuildKeyRef.current = documentBuildKey;
|
|
500
|
+
applyDecorationProps(viewRef.current, positionMapRef.current);
|
|
501
|
+
equivalentAckKeyRef.current = null;
|
|
502
|
+
if (pendingTypingProbeRef.current) {
|
|
503
|
+
finishPerfProbe(pendingTypingProbeRef.current);
|
|
504
|
+
pendingTypingProbeRef.current = null;
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
353
509
|
const { state, positionMap } = createPMStateFromSnapshot(
|
|
354
510
|
surface,
|
|
355
511
|
snapshot.selection,
|
|
@@ -473,6 +629,20 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
473
629
|
return;
|
|
474
630
|
}
|
|
475
631
|
|
|
632
|
+
// Skip the dispatch when PM is blurred. `view.dispatch(setSelection)`
|
|
633
|
+
// synchronizes the DOM selection, and on a blurred contenteditable some
|
|
634
|
+
// browsers pull DOM focus back to the editor as a side effect — which
|
|
635
|
+
// would steal focus from any toolbar button or other chrome the user
|
|
636
|
+
// just clicked. PM's state.selection is allowed to diverge from the
|
|
637
|
+
// runtime's selection while PM is blurred; the natural focus flow
|
|
638
|
+
// (click → onFocus → activeRuntime.focus() → snapshot update → this
|
|
639
|
+
// effect fires again with view.hasFocus() === true) resyncs them when
|
|
640
|
+
// PM regains focus, and the next canonical rebuild also re-derives PM
|
|
641
|
+
// selection from the snapshot.
|
|
642
|
+
if (!view.hasFocus()) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
476
646
|
suppressSelectionEchoRef.current = true;
|
|
477
647
|
view.dispatch(view.state.tr.setSelection(nextSelection));
|
|
478
648
|
recordPerfSample("selection.sync");
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -14,10 +14,31 @@ export { TwReviewRail, type TwReviewRailProps, type ReviewRailTab } from "./revi
|
|
|
14
14
|
export { TwCommentSidebar } from "./review/tw-comment-sidebar";
|
|
15
15
|
export { TwRevisionSidebar } from "./review/tw-revision-sidebar";
|
|
16
16
|
export { TwHealthPanel } from "./review/tw-health-panel";
|
|
17
|
+
export { TwWorkflowTab, type TwWorkflowTabProps } from "./review/tw-workflow-tab";
|
|
18
|
+
export {
|
|
19
|
+
TwRailCard,
|
|
20
|
+
type TwRailCardProps,
|
|
21
|
+
type RailCardTone,
|
|
22
|
+
type RailCardAvatar,
|
|
23
|
+
type RailCardCounter,
|
|
24
|
+
type RailCardProgress,
|
|
25
|
+
} from "./review/tw-rail-card";
|
|
26
|
+
export {
|
|
27
|
+
TwReviewRailFooter,
|
|
28
|
+
type TwReviewRailFooterProps,
|
|
29
|
+
} from "./review/tw-review-rail-footer";
|
|
17
30
|
|
|
18
31
|
// Toolbar
|
|
19
32
|
export { TwToolbar, type TwToolbarProps } from "./toolbar/tw-toolbar";
|
|
20
33
|
export { TwToolbarIconButton } from "./toolbar/tw-toolbar-icon-button";
|
|
34
|
+
export {
|
|
35
|
+
TwShellHeader,
|
|
36
|
+
type TwShellHeaderProps,
|
|
37
|
+
type ShellHeaderMode,
|
|
38
|
+
type ShellHeaderModeOption,
|
|
39
|
+
type ShellHeaderPrimaryAction,
|
|
40
|
+
type ShellHeaderIconAction,
|
|
41
|
+
} from "./toolbar/tw-shell-header";
|
|
21
42
|
export type { WorkspaceMode, ZoomLevel } from "../api/public-types";
|
|
22
43
|
|
|
23
44
|
// Status
|
|
@@ -27,6 +48,18 @@ export { TwStatusBar } from "./status/tw-status-bar";
|
|
|
27
48
|
export { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
28
49
|
export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
|
|
29
50
|
|
|
51
|
+
// Chrome overlay plane (R3a — scope rail, workspace dock)
|
|
52
|
+
export {
|
|
53
|
+
TwChromeOverlay,
|
|
54
|
+
type TwChromeOverlayProps,
|
|
55
|
+
TwScopeRailLayer,
|
|
56
|
+
type TwScopeRailLayerProps,
|
|
57
|
+
TwWorkspaceViewSwitcher,
|
|
58
|
+
type TwWorkspaceViewSwitcherProps,
|
|
59
|
+
type WorkspaceView,
|
|
60
|
+
type WorkspaceViewAction,
|
|
61
|
+
} from "./chrome-overlay";
|
|
62
|
+
|
|
30
63
|
// Session capabilities
|
|
31
64
|
export {
|
|
32
65
|
deriveCapabilities,
|
|
@@ -93,10 +93,10 @@ function CommentThreadCard(props: {
|
|
|
93
93
|
role="button"
|
|
94
94
|
tabIndex={0}
|
|
95
95
|
className={[
|
|
96
|
-
"cursor-pointer rounded-
|
|
96
|
+
"cursor-pointer rounded-lg bg-surface/90 px-3 py-2.5 transition-colors ring-1 ring-border/40",
|
|
97
97
|
focusRingClass,
|
|
98
98
|
isActive
|
|
99
|
-
? "bg-accent-soft/40 ring-accent/25"
|
|
99
|
+
? "bg-accent-soft/40 ring-accent/25 shadow-[var(--shadow-soft)]"
|
|
100
100
|
: "hover:bg-surface",
|
|
101
101
|
thread.status === "detached" ? "opacity-70" : "",
|
|
102
102
|
].join(" ")}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TwRailCard — shared editorial card primitive consumed by the runtime review
|
|
5
|
+
* rail tabs (Workflow, Comments, Changes).
|
|
6
|
+
*
|
|
7
|
+
* The card is tone-aware: the tone drives a 3px left edge rule, the eyebrow
|
|
8
|
+
* color, and the optional progress-bar fill via CSS custom properties declared
|
|
9
|
+
* in `src/ui-tailwind/theme/editor-theme.css` under `.wre-rail-card`.
|
|
10
|
+
*
|
|
11
|
+
* This primitive deliberately stays host-agnostic — no workflow, comment, or
|
|
12
|
+
* revision coupling. Consumers pass display fields and an optional footer
|
|
13
|
+
* slot so the card can host accept/reject, reply composer, or resolve actions
|
|
14
|
+
* without the primitive owning any domain logic.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type RailCardTone =
|
|
18
|
+
| "neutral"
|
|
19
|
+
| "inReview"
|
|
20
|
+
| "blocked"
|
|
21
|
+
| "scheduled"
|
|
22
|
+
| "resolved";
|
|
23
|
+
|
|
24
|
+
export interface RailCardAvatar {
|
|
25
|
+
initials: string;
|
|
26
|
+
color?: string;
|
|
27
|
+
alt?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RailCardProgress {
|
|
31
|
+
value: number;
|
|
32
|
+
total?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RailCardCounter {
|
|
36
|
+
label: string;
|
|
37
|
+
value: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TwRailCardProps {
|
|
41
|
+
tone: RailCardTone;
|
|
42
|
+
eyebrow: string;
|
|
43
|
+
title: string;
|
|
44
|
+
detail?: string;
|
|
45
|
+
leadingIcon?: ReactNode;
|
|
46
|
+
avatars?: readonly RailCardAvatar[];
|
|
47
|
+
avatarOverflowCount?: number;
|
|
48
|
+
counter?: RailCardCounter;
|
|
49
|
+
progress?: RailCardProgress;
|
|
50
|
+
footer?: ReactNode;
|
|
51
|
+
onClick?: () => void;
|
|
52
|
+
onSelect?: () => void;
|
|
53
|
+
isActive?: boolean;
|
|
54
|
+
dataTestId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function TwRailCard(props: TwRailCardProps) {
|
|
58
|
+
const {
|
|
59
|
+
tone,
|
|
60
|
+
eyebrow,
|
|
61
|
+
title,
|
|
62
|
+
detail,
|
|
63
|
+
leadingIcon,
|
|
64
|
+
avatars,
|
|
65
|
+
avatarOverflowCount,
|
|
66
|
+
counter,
|
|
67
|
+
progress,
|
|
68
|
+
footer,
|
|
69
|
+
onClick,
|
|
70
|
+
onSelect,
|
|
71
|
+
isActive,
|
|
72
|
+
dataTestId,
|
|
73
|
+
} = props;
|
|
74
|
+
|
|
75
|
+
const handleClick = onClick || onSelect;
|
|
76
|
+
const tag: "article" | "button" = handleClick ? "button" : "article";
|
|
77
|
+
|
|
78
|
+
const clamped = progress
|
|
79
|
+
? Math.max(0, Math.min(1, progress.total && progress.total > 0 ? progress.value / progress.total : progress.value))
|
|
80
|
+
: 0;
|
|
81
|
+
|
|
82
|
+
const commonProps: Record<string, unknown> = {
|
|
83
|
+
className: "wre-rail-card block w-full text-left",
|
|
84
|
+
"data-tone": tone,
|
|
85
|
+
"data-active": isActive ? "true" : "false",
|
|
86
|
+
"data-testid": dataTestId,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (handleClick) {
|
|
90
|
+
commonProps.onClick = handleClick;
|
|
91
|
+
commonProps.type = "button";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return React.createElement(
|
|
95
|
+
tag,
|
|
96
|
+
commonProps,
|
|
97
|
+
<>
|
|
98
|
+
{counter ? (
|
|
99
|
+
<span className="wre-rail-card__counter" aria-label={counter.label} title={counter.label}>
|
|
100
|
+
{counter.value}
|
|
101
|
+
</span>
|
|
102
|
+
) : null}
|
|
103
|
+
|
|
104
|
+
<span className="wre-rail-card__eyebrow">
|
|
105
|
+
{leadingIcon ? <span aria-hidden="true">{leadingIcon}</span> : null}
|
|
106
|
+
{eyebrow}
|
|
107
|
+
</span>
|
|
108
|
+
|
|
109
|
+
<p className="wre-rail-card__title">{title}</p>
|
|
110
|
+
|
|
111
|
+
{detail ? <p className="wre-rail-card__detail">{detail}</p> : null}
|
|
112
|
+
|
|
113
|
+
{avatars && avatars.length > 0 ? (
|
|
114
|
+
<span className="wre-rail-card__avatars" aria-hidden={avatars.every((a) => !a.alt) ? "true" : undefined}>
|
|
115
|
+
{avatars.map((avatar, index) => (
|
|
116
|
+
<span
|
|
117
|
+
key={`${avatar.initials}-${index}`}
|
|
118
|
+
className="wre-rail-card__avatar"
|
|
119
|
+
style={avatar.color ? { background: avatar.color } : undefined}
|
|
120
|
+
title={avatar.alt}
|
|
121
|
+
aria-label={avatar.alt}
|
|
122
|
+
>
|
|
123
|
+
{avatar.initials}
|
|
124
|
+
</span>
|
|
125
|
+
))}
|
|
126
|
+
{avatarOverflowCount && avatarOverflowCount > 0 ? (
|
|
127
|
+
<span className="wre-rail-card__avatar-counter">+{avatarOverflowCount}</span>
|
|
128
|
+
) : null}
|
|
129
|
+
</span>
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
{footer ? <div className="wre-rail-card__footer">{footer}</div> : null}
|
|
133
|
+
|
|
134
|
+
{progress ? (
|
|
135
|
+
<span
|
|
136
|
+
className="wre-rail-card__progress"
|
|
137
|
+
role="progressbar"
|
|
138
|
+
aria-valuemin={0}
|
|
139
|
+
aria-valuemax={progress.total ?? 1}
|
|
140
|
+
aria-valuenow={progress.value}
|
|
141
|
+
>
|
|
142
|
+
<span
|
|
143
|
+
className="wre-rail-card__progress-fill"
|
|
144
|
+
style={{ width: `${Math.round(clamped * 100)}%` }}
|
|
145
|
+
/>
|
|
146
|
+
</span>
|
|
147
|
+
) : null}
|
|
148
|
+
</>,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { HelpCircle, Search } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thin pinned footer rendered at the bottom of the review rail. The footer
|
|
6
|
+
* auto-hides when neither `onSearch` nor `helpHref` is supplied, so existing
|
|
7
|
+
* hosts that do not opt in see no change (progressive disclosure, DESIGN.md
|
|
8
|
+
* §5.4).
|
|
9
|
+
*/
|
|
10
|
+
export interface TwReviewRailFooterProps {
|
|
11
|
+
onSearch?: () => void;
|
|
12
|
+
helpHref?: string;
|
|
13
|
+
helpLabel?: string;
|
|
14
|
+
searchLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const focusRingClass =
|
|
18
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
|
|
19
|
+
|
|
20
|
+
export function TwReviewRailFooter(props: TwReviewRailFooterProps) {
|
|
21
|
+
const searchLabel = props.searchLabel ?? "SEARCH";
|
|
22
|
+
const helpLabel = props.helpLabel ?? "HELP";
|
|
23
|
+
|
|
24
|
+
if (!props.onSearch && !props.helpHref) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<footer className="flex h-10 shrink-0 items-center gap-2 border-t border-border/60 px-3">
|
|
30
|
+
{props.onSearch ? (
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={props.onSearch}
|
|
34
|
+
className={`inline-flex items-center gap-1.5 rounded-sm px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-tertiary transition-colors hover:text-secondary ${focusRingClass}`}
|
|
35
|
+
>
|
|
36
|
+
<Search aria-hidden="true" className="h-3 w-3" />
|
|
37
|
+
<span>{searchLabel}</span>
|
|
38
|
+
</button>
|
|
39
|
+
) : null}
|
|
40
|
+
|
|
41
|
+
{props.helpHref ? (
|
|
42
|
+
<a
|
|
43
|
+
href={props.helpHref}
|
|
44
|
+
className={`inline-flex items-center gap-1.5 rounded-sm px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-tertiary transition-colors hover:text-secondary ${focusRingClass}`}
|
|
45
|
+
>
|
|
46
|
+
<HelpCircle aria-hidden="true" className="h-3 w-3" />
|
|
47
|
+
<span>{helpLabel}</span>
|
|
48
|
+
</a>
|
|
49
|
+
) : null}
|
|
50
|
+
</footer>
|
|
51
|
+
);
|
|
52
|
+
}
|