@beyondwork/docx-react-component 1.0.83 → 1.0.85
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/package.json +1 -1
- package/src/api/internal/build-ref-projections.ts +3 -0
- package/src/api/public-types.ts +86 -4
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/content.ts +148 -1
- package/src/api/v3/runtime/formatting.ts +41 -0
- package/src/api/v3/runtime/review.ts +98 -0
- package/src/api/v3/runtime/workflow.ts +154 -6
- package/src/core/commands/index.ts +81 -25
- package/src/core/state/editor-state.ts +15 -0
- package/src/io/export/serialize-main-document.ts +72 -6
- package/src/io/ooxml/header-footer-reference.ts +38 -0
- package/src/io/ooxml/parse-headers-footers.ts +11 -23
- package/src/io/ooxml/parse-main-document.ts +7 -10
- package/src/io/ooxml/workflow-payload-validator.ts +24 -0
- package/src/io/ooxml/workflow-payload.ts +12 -0
- package/src/model/canonical-document.ts +9 -0
- package/src/model/review/comment-types.ts +2 -0
- package/src/runtime/document-runtime.ts +718 -68
- package/src/runtime/formatting/field/resolver.ts +73 -8
- package/src/runtime/layout/layout-engine-version.ts +31 -12
- package/src/runtime/layout/paginated-layout-engine.ts +18 -11
- package/src/runtime/layout/public-facet.ts +119 -16
- package/src/runtime/layout/resolve-page-fields.ts +68 -6
- package/src/runtime/layout/resolve-page-previews.ts +1 -1
- package/src/runtime/scopes/_scope-dependencies.ts +1 -0
- package/src/runtime/scopes/action-validation.ts +54 -45
- package/src/runtime/scopes/workflow-overlap.ts +41 -9
- package/src/runtime/suggestions-snapshot.ts +24 -0
- package/src/runtime/surface-projection.ts +59 -2
- package/src/runtime/workflow/coordinator.ts +66 -14
- package/src/runtime/workflow/scope-writer.ts +83 -5
- package/src/shell/ref-commands.ts +3 -354
- package/src/shell/session-bootstrap.ts +10 -0
- package/src/ui/WordReviewEditor.tsx +99 -9
- package/src/ui/editor-command-bag.ts +3 -1
- package/src/ui/headless/revision-decoration-model.ts +13 -0
- package/src/ui/headless/selection-tool-types.ts +2 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +7 -3
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +175 -25
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +12 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +18 -30
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +1 -1
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +20 -11
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +9 -4
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +12 -7
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +29 -10
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +1 -1
- package/src/ui-tailwind/review-workspace/types.ts +3 -2
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +11 -1
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -1
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +2 -1
- package/src/ui-tailwind/tw-review-workspace.tsx +18 -2
|
@@ -32,9 +32,12 @@ const focusRingClass =
|
|
|
32
32
|
export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
33
33
|
const contextLabel = summarizeSuggestionContext(props.model);
|
|
34
34
|
const commentDisabled = !props.model.canAddComment;
|
|
35
|
+
const replyCount = props.model.replyCount ?? 0;
|
|
35
36
|
const tooltipLabel = commentDisabled
|
|
36
37
|
? props.model.disabledReason ?? "Commenting is unavailable for this selection"
|
|
37
|
-
:
|
|
38
|
+
: props.model.commentThreadIds?.length
|
|
39
|
+
? "Reply to tracked change"
|
|
40
|
+
: "Start tracked-change discussion";
|
|
38
41
|
|
|
39
42
|
return (
|
|
40
43
|
<div
|
|
@@ -87,14 +90,15 @@ export function TwSuggestionCard(props: TwSuggestionCardProps) {
|
|
|
87
90
|
<Tooltip.Trigger asChild>
|
|
88
91
|
<button
|
|
89
92
|
type="button"
|
|
90
|
-
aria-label="
|
|
93
|
+
aria-label="Reply to tracked change"
|
|
94
|
+
data-testid="suggestion-card-reply"
|
|
91
95
|
disabled={commentDisabled}
|
|
92
96
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
93
97
|
onClick={props.onAddComment}
|
|
94
98
|
className={`inline-flex h-7 items-center gap-1 rounded-md border border-[var(--color-border-default)] px-2 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-hover)] disabled:cursor-not-allowed disabled:opacity-40 ${focusRingClass}`}
|
|
95
99
|
>
|
|
96
100
|
<MessageSquare className="h-3 w-3" />
|
|
97
|
-
|
|
101
|
+
Reply{replyCount > 0 ? ` ${replyCount}` : ""}
|
|
98
102
|
</button>
|
|
99
103
|
</Tooltip.Trigger>
|
|
100
104
|
<Tooltip.Portal>
|
|
@@ -31,23 +31,18 @@ export interface TwTableContextToolbarProps {
|
|
|
31
31
|
/**
|
|
32
32
|
* Phase D.2 — progressive-disclosure compact mode.
|
|
33
33
|
*
|
|
34
|
-
* When `true`, the toolbar
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
34
|
+
* When `true`, the toolbar stays action-first:
|
|
35
|
+
* - a small context label
|
|
36
|
+
* - the tier's highest-frequency table actions
|
|
37
|
+
* - a single "More…" button that opens the shared command graph
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* accessed via right-click OR the "More…" button (which opens the
|
|
42
|
-
* same context menu). Per DESIGN-EDITOR.md §6.4 ("right-click
|
|
43
|
-
* cannot be richer than the floating surface; cannot duplicate
|
|
44
|
-
* command trees") the compact variant pins that rule — there IS no
|
|
45
|
-
* richer surface to diverge from.
|
|
39
|
+
* Diagnostic metadata such as "3 x 4" or "R1 C1" is intentionally
|
|
40
|
+
* omitted from the compact surface. It belongs in properties /
|
|
41
|
+
* diagnostics, not primary editing chrome.
|
|
46
42
|
*
|
|
47
|
-
* Default `false` preserves the rich in-tree behavior
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `onContextMenuRequested` wiring is proven.
|
|
43
|
+
* Default `false` preserves the rich in-tree behavior for standalone
|
|
44
|
+
* and back-compat mounts. The product selection-tool path opts into
|
|
45
|
+
* compact mode.
|
|
51
46
|
*/
|
|
52
47
|
compact?: boolean;
|
|
53
48
|
/**
|
|
@@ -146,29 +141,37 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
146
141
|
: null;
|
|
147
142
|
const selectionLabel = tableContext ? formatSelectionLabel(tableContext, tier) : null;
|
|
148
143
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
144
|
+
// Product compact variant: action-first local chrome + one "More…"
|
|
145
|
+
// button that opens the shared context menu. The full action set
|
|
146
|
+
// still lives in editor-action-registry so right-click and "More…"
|
|
147
|
+
// stay identical; this surface only promotes the tier's obvious next
|
|
148
|
+
// actions and leaves metadata to deeper surfaces.
|
|
153
149
|
if (props.compact) {
|
|
154
150
|
return (
|
|
155
151
|
<div
|
|
156
152
|
data-testid="table-context-toolbar"
|
|
157
153
|
data-tier={tier}
|
|
158
154
|
data-variant="compact"
|
|
159
|
-
|
|
155
|
+
data-purpose="table-actions"
|
|
156
|
+
className="inline-flex max-w-[min(28rem,calc(100vw-1.5rem))] flex-wrap items-center gap-[4px] rounded-[var(--radius-lg)] border border-[var(--color-border-subtle)] bg-[var(--color-bg-canvas)] px-2 py-1 shadow-[var(--shadow-float)]"
|
|
160
157
|
>
|
|
161
|
-
<span
|
|
162
|
-
|
|
158
|
+
<span
|
|
159
|
+
data-testid="table-context-toolbar-label"
|
|
160
|
+
className="mr-0.5 text-[9px] font-semibold uppercase tracking-[0.12em] text-[var(--color-text-tertiary)]"
|
|
161
|
+
>
|
|
162
|
+
{compactTierLabel(tier)}
|
|
163
163
|
</span>
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
<CompactTableActions
|
|
165
|
+
props={props}
|
|
166
|
+
tableContext={tableContext}
|
|
167
|
+
tier={tier}
|
|
168
|
+
/>
|
|
166
169
|
<button
|
|
167
170
|
type="button"
|
|
168
171
|
data-testid="table-context-toolbar-more"
|
|
169
172
|
aria-label="Table actions menu"
|
|
170
173
|
className="inline-flex h-6 items-center gap-1 rounded-[var(--radius-sm)] px-2 text-xs font-medium text-secondary hover:bg-hover focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]"
|
|
171
|
-
disabled={props.disabled}
|
|
174
|
+
disabled={props.disabled || !props.onOpenMore}
|
|
172
175
|
onMouseDown={preserveEditorSelectionMouseDown}
|
|
173
176
|
onClick={(ev) => {
|
|
174
177
|
const rect = (ev.currentTarget as HTMLButtonElement).getBoundingClientRect();
|
|
@@ -457,6 +460,149 @@ export function TwTableContextToolbar(props: TwTableContextToolbarProps) {
|
|
|
457
460
|
);
|
|
458
461
|
}
|
|
459
462
|
|
|
463
|
+
function CompactTableActions(args: {
|
|
464
|
+
props: TwTableContextToolbarProps;
|
|
465
|
+
tableContext: TableStructureContextSnapshot | null;
|
|
466
|
+
tier: TableTier;
|
|
467
|
+
}) {
|
|
468
|
+
const { props, tableContext, tier } = args;
|
|
469
|
+
switch (tier) {
|
|
470
|
+
case "caret-in-cell":
|
|
471
|
+
return (
|
|
472
|
+
<>
|
|
473
|
+
<ToolbarButton
|
|
474
|
+
ariaLabel="Insert row below"
|
|
475
|
+
capability={tableContext?.operations.addRowAfter}
|
|
476
|
+
disabled={props.disabled}
|
|
477
|
+
onClick={props.onAddRowAfter}
|
|
478
|
+
>
|
|
479
|
+
Row +
|
|
480
|
+
</ToolbarButton>
|
|
481
|
+
<ToolbarButton
|
|
482
|
+
ariaLabel="Insert column right"
|
|
483
|
+
capability={tableContext?.operations.addColumnAfter}
|
|
484
|
+
disabled={props.disabled}
|
|
485
|
+
onClick={props.onAddColumnAfter}
|
|
486
|
+
>
|
|
487
|
+
Col +
|
|
488
|
+
</ToolbarButton>
|
|
489
|
+
</>
|
|
490
|
+
);
|
|
491
|
+
case "multi-cell":
|
|
492
|
+
return (
|
|
493
|
+
<>
|
|
494
|
+
<ToolbarButton
|
|
495
|
+
ariaLabel="Merge cells"
|
|
496
|
+
capability={tableContext?.operations.mergeCells}
|
|
497
|
+
disabled={props.disabled}
|
|
498
|
+
onClick={props.onMergeCells}
|
|
499
|
+
>
|
|
500
|
+
Merge
|
|
501
|
+
</ToolbarButton>
|
|
502
|
+
<ToolbarButton
|
|
503
|
+
ariaLabel="Split cell"
|
|
504
|
+
capability={tableContext?.operations.splitCell}
|
|
505
|
+
disabled={props.disabled}
|
|
506
|
+
onClick={props.onSplitCell}
|
|
507
|
+
>
|
|
508
|
+
Split
|
|
509
|
+
</ToolbarButton>
|
|
510
|
+
</>
|
|
511
|
+
);
|
|
512
|
+
case "row-selected":
|
|
513
|
+
return (
|
|
514
|
+
<>
|
|
515
|
+
<ToolbarButton
|
|
516
|
+
ariaLabel="Insert row below"
|
|
517
|
+
capability={tableContext?.operations.addRowAfter}
|
|
518
|
+
disabled={props.disabled}
|
|
519
|
+
onClick={props.onAddRowAfter}
|
|
520
|
+
>
|
|
521
|
+
Row +
|
|
522
|
+
</ToolbarButton>
|
|
523
|
+
<ToolbarButton
|
|
524
|
+
ariaLabel="Delete row"
|
|
525
|
+
capability={tableContext?.operations.deleteRow}
|
|
526
|
+
danger
|
|
527
|
+
disabled={props.disabled}
|
|
528
|
+
onClick={props.onDeleteRow}
|
|
529
|
+
>
|
|
530
|
+
Delete
|
|
531
|
+
</ToolbarButton>
|
|
532
|
+
<ToolbarButton
|
|
533
|
+
ariaLabel="Toggle header row"
|
|
534
|
+
capability={tableContext?.operations.setRowIsHeader}
|
|
535
|
+
active={tableContext?.currentCell.isHeader}
|
|
536
|
+
disabled={props.disabled}
|
|
537
|
+
onClick={props.onToggleRowHeader}
|
|
538
|
+
>
|
|
539
|
+
Header
|
|
540
|
+
</ToolbarButton>
|
|
541
|
+
</>
|
|
542
|
+
);
|
|
543
|
+
case "column-selected":
|
|
544
|
+
return (
|
|
545
|
+
<>
|
|
546
|
+
<ToolbarButton
|
|
547
|
+
ariaLabel="Insert column right"
|
|
548
|
+
capability={tableContext?.operations.addColumnAfter}
|
|
549
|
+
disabled={props.disabled}
|
|
550
|
+
onClick={props.onAddColumnAfter}
|
|
551
|
+
>
|
|
552
|
+
Col +
|
|
553
|
+
</ToolbarButton>
|
|
554
|
+
<ToolbarButton
|
|
555
|
+
ariaLabel="Delete column"
|
|
556
|
+
capability={tableContext?.operations.deleteColumn}
|
|
557
|
+
danger
|
|
558
|
+
disabled={props.disabled}
|
|
559
|
+
onClick={props.onDeleteColumn}
|
|
560
|
+
>
|
|
561
|
+
Delete
|
|
562
|
+
</ToolbarButton>
|
|
563
|
+
<ToolbarButton
|
|
564
|
+
ariaLabel="Distribute columns evenly"
|
|
565
|
+
capability={tableContext?.operations.distributeColumnsEvenly}
|
|
566
|
+
disabled={props.disabled}
|
|
567
|
+
onClick={props.onDistributeColumnsEvenly}
|
|
568
|
+
>
|
|
569
|
+
Distribute
|
|
570
|
+
</ToolbarButton>
|
|
571
|
+
</>
|
|
572
|
+
);
|
|
573
|
+
case "whole-table":
|
|
574
|
+
return (
|
|
575
|
+
<>
|
|
576
|
+
<ToolbarButton
|
|
577
|
+
ariaLabel="Insert row below"
|
|
578
|
+
capability={tableContext?.operations.addRowAfter}
|
|
579
|
+
disabled={props.disabled}
|
|
580
|
+
onClick={props.onAddRowAfter}
|
|
581
|
+
>
|
|
582
|
+
Row +
|
|
583
|
+
</ToolbarButton>
|
|
584
|
+
<ToolbarButton
|
|
585
|
+
ariaLabel="Insert column right"
|
|
586
|
+
capability={tableContext?.operations.addColumnAfter}
|
|
587
|
+
disabled={props.disabled}
|
|
588
|
+
onClick={props.onAddColumnAfter}
|
|
589
|
+
>
|
|
590
|
+
Col +
|
|
591
|
+
</ToolbarButton>
|
|
592
|
+
<ToolbarButton
|
|
593
|
+
ariaLabel="Delete table"
|
|
594
|
+
capability={tableContext?.operations.deleteTable}
|
|
595
|
+
danger
|
|
596
|
+
disabled={props.disabled}
|
|
597
|
+
onClick={props.onDeleteTable}
|
|
598
|
+
>
|
|
599
|
+
Delete
|
|
600
|
+
</ToolbarButton>
|
|
601
|
+
</>
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
460
606
|
function formatSelectionLabel(
|
|
461
607
|
ctx: TableStructureContextSnapshot,
|
|
462
608
|
tier: TableTier,
|
|
@@ -491,6 +637,10 @@ function tierLabel(tier: TableTier): string {
|
|
|
491
637
|
}
|
|
492
638
|
}
|
|
493
639
|
|
|
640
|
+
function compactTierLabel(tier: TableTier): string {
|
|
641
|
+
return tier === "whole-table" ? "Table" : tierLabel(tier);
|
|
642
|
+
}
|
|
643
|
+
|
|
494
644
|
function tierWidthCap(tier: TableTier): string {
|
|
495
645
|
switch (tier) {
|
|
496
646
|
case "caret-in-cell":
|
|
@@ -154,7 +154,7 @@ export interface TwChromeOverlayProps {
|
|
|
154
154
|
*/
|
|
155
155
|
activeStory?: EditorStoryTarget;
|
|
156
156
|
/**
|
|
157
|
-
* Fired when the user clicks a per-page header / footer band to
|
|
157
|
+
* Fired when the user double-clicks a per-page header / footer band to
|
|
158
158
|
* promote it into the active editing surface. Task 10 will route PM
|
|
159
159
|
* into the matching band via React portals; today the handler is a
|
|
160
160
|
* pass-through to `runtime.openStory`.
|
|
@@ -513,6 +513,18 @@ export function buildDecorations(
|
|
|
513
513
|
}),
|
|
514
514
|
);
|
|
515
515
|
revisionCount += 1;
|
|
516
|
+
} else if (rev.kind === "property-change" || rev.kind === "formatting") {
|
|
517
|
+
const propertyChangeClass =
|
|
518
|
+
buildClassFromRevisionDisplay(revDisplayFlags) ||
|
|
519
|
+
"underline decoration-accent/70 decoration-dotted decoration-1 underline-offset-2";
|
|
520
|
+
decorations.push(
|
|
521
|
+
Decoration.inline(pmFrom, pmTo, {
|
|
522
|
+
class: propertyChangeClass,
|
|
523
|
+
"data-revision-id": rev.revisionId,
|
|
524
|
+
"data-revision-kind": rev.kind,
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
revisionCount += 1;
|
|
516
528
|
}
|
|
517
529
|
continue;
|
|
518
530
|
}
|
|
@@ -392,7 +392,7 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
|
|
|
392
392
|
root.style.userSelect = "none";
|
|
393
393
|
|
|
394
394
|
if (input.posture === "canvas") {
|
|
395
|
-
// Single dotted horizontal line with
|
|
395
|
+
// Single dotted horizontal line with an unframed page-number label.
|
|
396
396
|
root.style.height = `${input.interGapPx + 1}px`;
|
|
397
397
|
root.style.position = "relative";
|
|
398
398
|
|
|
@@ -406,35 +406,23 @@ function buildChromeWidgetDomUncached(input: ChromeWidgetInput): HTMLElement {
|
|
|
406
406
|
line.style.borderTop = "1px dotted var(--color-border-default)";
|
|
407
407
|
root.appendChild(line);
|
|
408
408
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
badge.style.alignItems = "center";
|
|
427
|
-
badge.style.height = `${PILL_HEIGHT_PX}px`;
|
|
428
|
-
badge.style.padding = "0 10px";
|
|
429
|
-
badge.style.fontSize = "10px";
|
|
430
|
-
badge.style.letterSpacing = "0.12em";
|
|
431
|
-
badge.style.textTransform = "uppercase";
|
|
432
|
-
badge.style.color = "var(--color-text-tertiary)";
|
|
433
|
-
badge.style.backgroundColor = "var(--color-surface)";
|
|
434
|
-
badge.style.border = "1px solid var(--color-border-default)";
|
|
435
|
-
badge.style.borderRadius = "var(--radius-pill)";
|
|
436
|
-
badge.style.boxShadow = "var(--shadow-soft)";
|
|
437
|
-
root.appendChild(badge);
|
|
409
|
+
const label = document.createElement("span");
|
|
410
|
+
label.className = "wre-page-chrome-canvas-page-number";
|
|
411
|
+
label.setAttribute("data-kind", "canvas-seam-page-number");
|
|
412
|
+
label.textContent = input.nextPageLabel;
|
|
413
|
+
label.style.position = "absolute";
|
|
414
|
+
label.style.top = `${Math.round(input.interGapPx / 2)}px`;
|
|
415
|
+
label.style.left = "50%";
|
|
416
|
+
label.style.transform = "translate(-50%, -50%)";
|
|
417
|
+
label.style.display = "inline-flex";
|
|
418
|
+
label.style.alignItems = "center";
|
|
419
|
+
label.style.padding = "0 2px";
|
|
420
|
+
label.style.fontSize = "10px";
|
|
421
|
+
label.style.letterSpacing = "0.12em";
|
|
422
|
+
label.style.textTransform = "uppercase";
|
|
423
|
+
label.style.color = "var(--color-text-tertiary)";
|
|
424
|
+
label.style.pointerEvents = "none";
|
|
425
|
+
root.appendChild(label);
|
|
438
426
|
return root;
|
|
439
427
|
}
|
|
440
428
|
|
|
@@ -35,7 +35,8 @@ export interface TwPageChromeEntryProps {
|
|
|
35
35
|
page: PublicPageNode;
|
|
36
36
|
facet: WordReviewEditorLayoutFacet;
|
|
37
37
|
activeStory: EditorStoryTarget;
|
|
38
|
-
|
|
38
|
+
activeStoryPageIndex?: number | null;
|
|
39
|
+
onOpenStory?: (target: EditorStoryTarget, pageIndex: number) => void;
|
|
39
40
|
visiblePageIndexRange?: { start: number; end: number } | null;
|
|
40
41
|
renderFrameRevision: number;
|
|
41
42
|
/** Preview catalog threaded into header/footer/footnote region renderers
|
|
@@ -58,6 +59,7 @@ function TwPageChromeEntryInner({
|
|
|
58
59
|
page,
|
|
59
60
|
facet,
|
|
60
61
|
activeStory,
|
|
62
|
+
activeStoryPageIndex,
|
|
61
63
|
onOpenStory,
|
|
62
64
|
visiblePageIndexRange,
|
|
63
65
|
renderFrameRevision,
|
|
@@ -129,14 +131,14 @@ function TwPageChromeEntryInner({
|
|
|
129
131
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
132
|
}, [facet, pageIndex, page, renderFrameRevision]);
|
|
131
133
|
|
|
132
|
-
const
|
|
133
|
-
() => headerStory && onOpenStory?.(headerStory),
|
|
134
|
-
[onOpenStory, headerStory],
|
|
134
|
+
const handleHeaderDoubleClick = React.useCallback(
|
|
135
|
+
() => headerStory && onOpenStory?.(headerStory, pageIndex),
|
|
136
|
+
[onOpenStory, headerStory, pageIndex],
|
|
135
137
|
);
|
|
136
138
|
|
|
137
|
-
const
|
|
138
|
-
() => footerStory && onOpenStory?.(footerStory),
|
|
139
|
-
[onOpenStory, footerStory],
|
|
139
|
+
const handleFooterDoubleClick = React.useCallback(
|
|
140
|
+
() => footerStory && onOpenStory?.(footerStory, pageIndex),
|
|
141
|
+
[onOpenStory, footerStory, pageIndex],
|
|
140
142
|
);
|
|
141
143
|
|
|
142
144
|
const frameHeightPx = rect.bottomPx - rect.topPx;
|
|
@@ -175,8 +177,14 @@ function TwPageChromeEntryInner({
|
|
|
175
177
|
const sectionNumber = (page.sectionIndex ?? 0) + 1;
|
|
176
178
|
const headerSectionLabel = `Header — Section ${sectionNumber}`;
|
|
177
179
|
const footerSectionLabel = `Footer — Section ${sectionNumber}`;
|
|
178
|
-
const headerActive =
|
|
179
|
-
|
|
180
|
+
const headerActive =
|
|
181
|
+
headerStory &&
|
|
182
|
+
isActiveStoryMatch(activeStory, headerStory) &&
|
|
183
|
+
(activeStoryPageIndex == null || activeStoryPageIndex === pageIndex);
|
|
184
|
+
const footerActive =
|
|
185
|
+
footerStory &&
|
|
186
|
+
isActiveStoryMatch(activeStory, footerStory) &&
|
|
187
|
+
(activeStoryPageIndex == null || activeStoryPageIndex === pageIndex);
|
|
180
188
|
|
|
181
189
|
return (
|
|
182
190
|
<div
|
|
@@ -201,7 +209,7 @@ function TwPageChromeEntryInner({
|
|
|
201
209
|
bandHeightPx={px(headerRegion.heightTwips)}
|
|
202
210
|
isActiveSlot={Boolean(headerActive)}
|
|
203
211
|
sectionLabel={headerActive ? headerSectionLabel : undefined}
|
|
204
|
-
|
|
212
|
+
onDoubleClick={handleHeaderDoubleClick}
|
|
205
213
|
mediaPreviews={mediaPreviews}
|
|
206
214
|
ribbonProps={headerActive ? activeBandRibbonProps ?? null : null}
|
|
207
215
|
/>
|
|
@@ -216,7 +224,7 @@ function TwPageChromeEntryInner({
|
|
|
216
224
|
bandHeightPx={px(footerRegion.heightTwips)}
|
|
217
225
|
isActiveSlot={Boolean(footerActive)}
|
|
218
226
|
sectionLabel={footerActive ? footerSectionLabel : undefined}
|
|
219
|
-
|
|
227
|
+
onDoubleClick={handleFooterDoubleClick}
|
|
220
228
|
mediaPreviews={mediaPreviews}
|
|
221
229
|
ribbonProps={footerActive ? activeBandRibbonProps ?? null : null}
|
|
222
230
|
/>
|
|
@@ -256,6 +264,7 @@ function propsAreEqual(
|
|
|
256
264
|
prev.page === next.page &&
|
|
257
265
|
prev.facet === next.facet &&
|
|
258
266
|
prev.activeStory === next.activeStory &&
|
|
267
|
+
prev.activeStoryPageIndex === next.activeStoryPageIndex &&
|
|
259
268
|
prev.onOpenStory === next.onOpenStory &&
|
|
260
269
|
prev.visiblePageIndexRange === next.visiblePageIndexRange &&
|
|
261
270
|
prev.renderFrameRevision === next.renderFrameRevision &&
|
|
@@ -37,7 +37,7 @@ export interface TwPageFooterBandProps {
|
|
|
37
37
|
* Only rendered when `isActiveSlot` is true.
|
|
38
38
|
*/
|
|
39
39
|
sectionLabel?: string;
|
|
40
|
-
|
|
40
|
+
onDoubleClick: () => void;
|
|
41
41
|
"data-testid"?: string;
|
|
42
42
|
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
43
43
|
/**
|
|
@@ -57,7 +57,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
57
57
|
widthPx,
|
|
58
58
|
isActiveSlot,
|
|
59
59
|
sectionLabel,
|
|
60
|
-
|
|
60
|
+
onDoubleClick,
|
|
61
61
|
"data-testid": testId,
|
|
62
62
|
mediaPreviews,
|
|
63
63
|
ribbonProps,
|
|
@@ -69,14 +69,18 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
69
69
|
data-page-index={pageIndex}
|
|
70
70
|
data-active={isActiveSlot ? "true" : undefined}
|
|
71
71
|
data-testid={testId}
|
|
72
|
-
|
|
72
|
+
onDoubleClick={(event) => {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
event.stopPropagation();
|
|
75
|
+
onDoubleClick();
|
|
76
|
+
}}
|
|
73
77
|
style={{
|
|
74
78
|
position: "absolute",
|
|
75
79
|
bottom: `${bottomPx}px`,
|
|
76
80
|
left: `${leftPx}px`,
|
|
77
81
|
width: `${widthPx}px`,
|
|
78
82
|
height: `${bandHeightPx}px`,
|
|
79
|
-
cursor: "
|
|
83
|
+
cursor: "text",
|
|
80
84
|
}}
|
|
81
85
|
>
|
|
82
86
|
{isActiveSlot && sectionLabel ? (
|
|
@@ -95,6 +99,7 @@ export const TwPageFooterBand: React.FC<TwPageFooterBandProps> = React.memo(({
|
|
|
95
99
|
<div
|
|
96
100
|
data-pm-portal-slot
|
|
97
101
|
data-page-band-slot="footer"
|
|
102
|
+
data-page-index={pageIndex}
|
|
98
103
|
style={{ width: "100%", height: "100%" }}
|
|
99
104
|
/>
|
|
100
105
|
) : (
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
// promoted this band to the active story slot (P8.10 wires the PM
|
|
20
20
|
// surface into this target via React portal).
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// the
|
|
22
|
+
// Double-clicks on the band dispatch to the chrome layer's `openStory`
|
|
23
|
+
// handler so legal reviewers can promote a header into the active editing
|
|
24
|
+
// surface without single-clicking out of the main body flow.
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
|
|
27
27
|
export interface TwPageHeaderBandProps {
|
|
@@ -38,7 +38,7 @@ export interface TwPageHeaderBandProps {
|
|
|
38
38
|
* Only rendered when `isActiveSlot` is true.
|
|
39
39
|
*/
|
|
40
40
|
sectionLabel?: string;
|
|
41
|
-
|
|
41
|
+
onDoubleClick: () => void;
|
|
42
42
|
"data-testid"?: string;
|
|
43
43
|
/** Preview catalog threaded to the region renderer so header images
|
|
44
44
|
* (CCEP logos on 7-of-8 CCEP docs) render as real <img>s instead of
|
|
@@ -63,7 +63,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
63
63
|
widthPx,
|
|
64
64
|
isActiveSlot,
|
|
65
65
|
sectionLabel,
|
|
66
|
-
|
|
66
|
+
onDoubleClick,
|
|
67
67
|
"data-testid": testId,
|
|
68
68
|
mediaPreviews,
|
|
69
69
|
ribbonProps,
|
|
@@ -75,14 +75,18 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
75
75
|
data-page-index={pageIndex}
|
|
76
76
|
data-active={isActiveSlot ? "true" : undefined}
|
|
77
77
|
data-testid={testId}
|
|
78
|
-
|
|
78
|
+
onDoubleClick={(event) => {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
onDoubleClick();
|
|
82
|
+
}}
|
|
79
83
|
style={{
|
|
80
84
|
position: "absolute",
|
|
81
85
|
top: `${topPx}px`,
|
|
82
86
|
left: `${leftPx}px`,
|
|
83
87
|
width: `${widthPx}px`,
|
|
84
88
|
height: `${bandHeightPx}px`,
|
|
85
|
-
cursor: "
|
|
89
|
+
cursor: "text",
|
|
86
90
|
}}
|
|
87
91
|
>
|
|
88
92
|
{isActiveSlot && sectionLabel ? (
|
|
@@ -101,6 +105,7 @@ export const TwPageHeaderBand: React.FC<TwPageHeaderBandProps> = React.memo(({
|
|
|
101
105
|
<div
|
|
102
106
|
data-pm-portal-slot
|
|
103
107
|
data-page-band-slot="header"
|
|
108
|
+
data-page-index={pageIndex}
|
|
104
109
|
style={{ width: "100%", height: "100%" }}
|
|
105
110
|
/>
|
|
106
111
|
) : (
|
|
@@ -111,7 +111,7 @@ export interface TwPageStackChromeLayerProps {
|
|
|
111
111
|
renderFrameRevision: number;
|
|
112
112
|
/** Current active story target — used to promote the matching band to slot mode. */
|
|
113
113
|
activeStory: EditorStoryTarget;
|
|
114
|
-
/** Fires when a band is clicked.
|
|
114
|
+
/** Fires when a band is double-clicked. Routes PM into the matching band. */
|
|
115
115
|
onOpenStory?: (target: EditorStoryTarget) => void;
|
|
116
116
|
/**
|
|
117
117
|
* PM surface DOM element (typically `view.dom`, i.e. the outer
|
|
@@ -189,6 +189,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
189
189
|
});
|
|
190
190
|
const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
|
|
191
191
|
const rafHandleRef = React.useRef<number | null>(null);
|
|
192
|
+
const [activeStoryPageIndex, setActiveStoryPageIndex] = React.useState<number | null>(null);
|
|
192
193
|
|
|
193
194
|
// --------------------------------------------------------------------
|
|
194
195
|
// rAF-debounced refresh. Mirrors the pattern in
|
|
@@ -293,6 +294,20 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
293
294
|
};
|
|
294
295
|
}, [refreshRects, renderFrameRevision, scrollRoot]);
|
|
295
296
|
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
if (activeStory.kind !== "header" && activeStory.kind !== "footer") {
|
|
299
|
+
setActiveStoryPageIndex(null);
|
|
300
|
+
}
|
|
301
|
+
}, [activeStory]);
|
|
302
|
+
|
|
303
|
+
const handleOpenStoryForPage = React.useCallback(
|
|
304
|
+
(target: EditorStoryTarget, pageIndex: number) => {
|
|
305
|
+
setActiveStoryPageIndex(pageIndex);
|
|
306
|
+
onOpenStory?.(target);
|
|
307
|
+
},
|
|
308
|
+
[onOpenStory],
|
|
309
|
+
);
|
|
310
|
+
|
|
296
311
|
// Observe scroll-root size changes.
|
|
297
312
|
React.useEffect(() => {
|
|
298
313
|
if (geometryFacet) return;
|
|
@@ -357,14 +372,17 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
357
372
|
React.useLayoutEffect(() => {
|
|
358
373
|
if (!pmSurfaceElement) return;
|
|
359
374
|
const overlay = overlayRootRef.current;
|
|
360
|
-
// Find
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
375
|
+
// Find the active portal slot. Double-click activation records the
|
|
376
|
+
// page index, so repeated shared header/footer stories still route PM
|
|
377
|
+
// into the actual page band the user opened.
|
|
378
|
+
const pageScopedSelector =
|
|
379
|
+
activeStoryPageIndex == null
|
|
380
|
+
? null
|
|
381
|
+
: `[data-pm-portal-slot][data-page-index="${activeStoryPageIndex}"]`;
|
|
367
382
|
const activeSlot =
|
|
383
|
+
(pageScopedSelector
|
|
384
|
+
? overlay?.querySelector<HTMLElement>(pageScopedSelector)
|
|
385
|
+
: null) ??
|
|
368
386
|
overlay?.querySelector<HTMLElement>("[data-pm-portal-slot]") ?? null;
|
|
369
387
|
// Body slot lives outside the chrome overlay root. Walk up from
|
|
370
388
|
// the PM element's owner document so jsdom tests and the real
|
|
@@ -396,7 +414,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
396
414
|
// updateState pass.
|
|
397
415
|
}
|
|
398
416
|
}
|
|
399
|
-
}, [pmSurfaceElement, pmView, activeStory, rects, scrollRoot]);
|
|
417
|
+
}, [pmSurfaceElement, pmView, activeStory, activeStoryPageIndex, rects, scrollRoot]);
|
|
400
418
|
|
|
401
419
|
// --------------------------------------------------------------------
|
|
402
420
|
// Render
|
|
@@ -440,7 +458,8 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
440
458
|
page={page}
|
|
441
459
|
facet={facet}
|
|
442
460
|
activeStory={activeStory}
|
|
443
|
-
|
|
461
|
+
activeStoryPageIndex={activeStoryPageIndex}
|
|
462
|
+
onOpenStory={handleOpenStoryForPage}
|
|
444
463
|
visiblePageIndexRange={visiblePageIndexRange}
|
|
445
464
|
renderFrameRevision={renderFrameRevision}
|
|
446
465
|
mediaPreviews={mediaPreviews}
|