@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.
Files changed (107) 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 +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Scope posture menu — replaces the old "Mark section" button with a
3
+ * topnav dropdown listing the seven `ScopeRailPosture` values so
4
+ * editors can mark regions with an explicit workflow mode instead of a
5
+ * single "marked" flag.
6
+ *
7
+ * Per runtime-rendering-and-chrome-phase.md §6.4, the menu lives inline
8
+ * in the editor role's primary action region (not in the review queue
9
+ * strip). Postures align 1:1 with the rail vocabulary so the rail
10
+ * updates visually as soon as the user picks one.
11
+ */
12
+
13
+ import React, { useState } from "react";
14
+ import * as Popover from "@radix-ui/react-popover";
15
+ import {
16
+ BookmarkPlus,
17
+ ChevronDown,
18
+ Eye,
19
+ Flag,
20
+ Lock,
21
+ MessageCircle,
22
+ Pencil,
23
+ Sparkles,
24
+ } from "lucide-react";
25
+
26
+ import type { ScopeRailPosture } from "../../api/public-types";
27
+ import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
28
+
29
+ export interface TwScopePostureMenuProps {
30
+ disabled?: boolean;
31
+ /** Called when the user picks a posture. Host decides the scope to mark. */
32
+ onSelect: (posture: ScopeRailPosture) => void;
33
+ /** Optional explicit label override (defaults to "Mark…"). */
34
+ label?: string;
35
+ "data-testid"?: string;
36
+ }
37
+
38
+ interface PostureEntry {
39
+ posture: ScopeRailPosture;
40
+ label: string;
41
+ hint: string;
42
+ icon: React.ComponentType<{ className?: string }>;
43
+ tone: "accent" | "warning" | "comment" | "secondary" | "danger";
44
+ }
45
+
46
+ /**
47
+ * Posture catalog. Mirrors `POSTURE_STYLES` in `tw-scope-rail-layer.tsx`
48
+ * but with lucide icon components (the rail uses CSS pseudo-element
49
+ * glyphs via the `data-icon` attribute). Extract both into a single
50
+ * source of truth in a follow-up.
51
+ */
52
+ const POSTURE_ENTRIES: readonly PostureEntry[] = [
53
+ {
54
+ posture: "edit",
55
+ label: "Edit scope",
56
+ hint: "Full authoring inside this region",
57
+ icon: Pencil,
58
+ tone: "accent",
59
+ },
60
+ {
61
+ posture: "suggest",
62
+ label: "Suggest scope",
63
+ hint: "Tracked-change suggestions only",
64
+ icon: Sparkles,
65
+ tone: "warning",
66
+ },
67
+ {
68
+ posture: "comment",
69
+ label: "Comment scope",
70
+ hint: "Comments only; body is read-only",
71
+ icon: MessageCircle,
72
+ tone: "comment",
73
+ },
74
+ {
75
+ posture: "view",
76
+ label: "In scope",
77
+ hint: "Read-only, in scope of review",
78
+ icon: Eye,
79
+ tone: "secondary",
80
+ },
81
+ {
82
+ posture: "candidate",
83
+ label: "Propose scope",
84
+ hint: "Candidate — not yet committed",
85
+ icon: Flag,
86
+ tone: "warning",
87
+ },
88
+ {
89
+ posture: "preserve-only",
90
+ label: "Preserve only",
91
+ hint: "Blocked — export-preserving only",
92
+ icon: Lock,
93
+ tone: "danger",
94
+ },
95
+ {
96
+ posture: "blocked-import",
97
+ label: "Blocked import",
98
+ hint: "Blocked — imported region is locked",
99
+ icon: Lock,
100
+ tone: "danger",
101
+ },
102
+ ];
103
+
104
+ export function TwScopePostureMenu(props: TwScopePostureMenuProps): React.JSX.Element {
105
+ const [open, setOpen] = useState(false);
106
+
107
+ return (
108
+ <Popover.Root open={open} onOpenChange={setOpen}>
109
+ <Popover.Trigger asChild>
110
+ <button
111
+ type="button"
112
+ aria-label={`${props.label ?? "Mark"} section`}
113
+ aria-expanded={open}
114
+ disabled={props.disabled}
115
+ onMouseDown={preserveEditorSelectionMouseDown}
116
+ className="inline-flex h-6 items-center gap-1 rounded-md border border-border bg-canvas px-2 text-[11px] font-medium text-primary transition-colors hover:bg-surface outline-none disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
117
+ data-testid={props["data-testid"] ?? "scope-posture-menu-trigger"}
118
+ >
119
+ <BookmarkPlus className="h-3.5 w-3.5" />
120
+ <span>{props.label ?? "Mark…"}</span>
121
+ <ChevronDown className="h-3.5 w-3.5 text-tertiary" />
122
+ </button>
123
+ </Popover.Trigger>
124
+ <Popover.Portal>
125
+ <Popover.Content
126
+ className="z-50 w-[260px] rounded-lg bg-canvas p-1 shadow-lg ring-1 ring-border"
127
+ sideOffset={8}
128
+ align="start"
129
+ data-testid="scope-posture-menu-content"
130
+ >
131
+ <div className="mb-1 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-tertiary">
132
+ Mark section with posture
133
+ </div>
134
+ {POSTURE_ENTRIES.map((entry) => (
135
+ <Popover.Close key={entry.posture} asChild>
136
+ <button
137
+ type="button"
138
+ aria-label={`Mark section as ${entry.label}`}
139
+ onMouseDown={preserveEditorSelectionMouseDown}
140
+ onClick={() => {
141
+ props.onSelect(entry.posture);
142
+ }}
143
+ className="flex w-full items-start gap-2 rounded-md px-2 py-1.5 text-left text-[11px] transition-colors hover:bg-surface focus-visible:outline-none focus-visible:bg-surface"
144
+ data-testid={`scope-posture-option-${entry.posture}`}
145
+ data-posture={entry.posture}
146
+ >
147
+ <entry.icon
148
+ className={[
149
+ "mt-0.5 h-3.5 w-3.5 shrink-0",
150
+ toneClass(entry.tone),
151
+ ].join(" ")}
152
+ />
153
+ <span className="flex flex-col">
154
+ <span className="font-medium text-primary">{entry.label}</span>
155
+ <span className="text-[10px] text-secondary">{entry.hint}</span>
156
+ </span>
157
+ </button>
158
+ </Popover.Close>
159
+ ))}
160
+ </Popover.Content>
161
+ </Popover.Portal>
162
+ </Popover.Root>
163
+ );
164
+ }
165
+
166
+ function toneClass(tone: PostureEntry["tone"]): string {
167
+ switch (tone) {
168
+ case "accent":
169
+ return "text-accent";
170
+ case "warning":
171
+ return "text-warning";
172
+ case "comment":
173
+ return "text-comment";
174
+ case "danger":
175
+ return "text-danger";
176
+ case "secondary":
177
+ default:
178
+ return "text-secondary";
179
+ }
180
+ }
181
+
182
+ export default TwScopePostureMenu;
@@ -0,0 +1,162 @@
1
+ import React, { type ReactNode } from "react";
2
+ import * as Tabs from "@radix-ui/react-tabs";
3
+
4
+ /**
5
+ * TwShellHeader — the top "app chrome" bar above the document canvas.
6
+ *
7
+ * Anatomy matches the editorial reference mock:
8
+ * ┌─────────────────────────────────────────────────────────────────────┐
9
+ * │ [brand] Edit | Review | Workflow | More [⋯] [CTA] │
10
+ * └─────────────────────────────────────────────────────────────────────┘
11
+ *
12
+ * All three regions are optional slots — hosts opt in by supplying the
13
+ * corresponding prop. When nothing is supplied the header renders empty but
14
+ * preserves layout height so the document canvas does not jump when a CTA
15
+ * appears.
16
+ */
17
+
18
+ export type ShellHeaderMode = "edit" | "review" | "workflow" | "more";
19
+
20
+ export interface ShellHeaderModeOption {
21
+ id: ShellHeaderMode;
22
+ label: string;
23
+ disabled?: boolean;
24
+ }
25
+
26
+ export interface ShellHeaderPrimaryAction {
27
+ label: string;
28
+ onClick: () => void;
29
+ tone?: "accent" | "neutral";
30
+ disabled?: boolean;
31
+ }
32
+
33
+ export interface ShellHeaderIconAction {
34
+ id: string;
35
+ label: string;
36
+ icon: ReactNode;
37
+ onClick?: () => void;
38
+ href?: string;
39
+ }
40
+
41
+ export interface TwShellHeaderProps {
42
+ brand?: ReactNode;
43
+ modes?: readonly ShellHeaderModeOption[];
44
+ activeMode?: ShellHeaderMode;
45
+ onModeChange?: (mode: ShellHeaderMode) => void;
46
+ iconActions?: readonly ShellHeaderIconAction[];
47
+ primaryAction?: ShellHeaderPrimaryAction;
48
+ /** Thin bottom border appears only when the document has scrolled. */
49
+ isScrolled?: boolean;
50
+ className?: string;
51
+ }
52
+
53
+ const focusRingClass =
54
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-canvas";
55
+
56
+ export function TwShellHeader(props: TwShellHeaderProps) {
57
+ const hasContent =
58
+ props.brand ||
59
+ (props.modes && props.modes.length > 0) ||
60
+ (props.iconActions && props.iconActions.length > 0) ||
61
+ props.primaryAction;
62
+
63
+ if (!hasContent) {
64
+ return null;
65
+ }
66
+
67
+ const className = [
68
+ "flex h-12 shrink-0 items-center justify-between gap-4 px-4 bg-canvas/92 backdrop-blur-sm",
69
+ props.isScrolled ? "border-b border-border" : "border-b border-transparent",
70
+ "transition-colors duration-[var(--motion-fast)]",
71
+ props.className,
72
+ ]
73
+ .filter(Boolean)
74
+ .join(" ");
75
+
76
+ return (
77
+ <header className={className} data-testid="tw-shell-header">
78
+ <div className="flex min-w-0 items-center gap-3">
79
+ {props.brand ? (
80
+ <div
81
+ className="font-[family-name:var(--font-legal-serif)] text-[15px] font-semibold text-primary truncate"
82
+ data-testid="tw-shell-header__brand"
83
+ >
84
+ {props.brand}
85
+ </div>
86
+ ) : null}
87
+ </div>
88
+
89
+ {props.modes && props.modes.length > 0 && props.activeMode ? (
90
+ <Tabs.Root
91
+ value={props.activeMode}
92
+ onValueChange={(v: string) =>
93
+ props.onModeChange?.(v as ShellHeaderMode)
94
+ }
95
+ >
96
+ <Tabs.List
97
+ aria-label="Workspace modes"
98
+ className="flex items-center gap-1"
99
+ >
100
+ {props.modes.map((mode) => (
101
+ <Tabs.Trigger
102
+ key={mode.id}
103
+ value={mode.id}
104
+ disabled={mode.disabled}
105
+ className={`wre-rail-tab ${focusRingClass}`}
106
+ data-testid={`tw-shell-header__mode-${mode.id}`}
107
+ >
108
+ {mode.label}
109
+ </Tabs.Trigger>
110
+ ))}
111
+ </Tabs.List>
112
+ </Tabs.Root>
113
+ ) : (
114
+ <div aria-hidden="true" />
115
+ )}
116
+
117
+ <div className="flex items-center gap-1">
118
+ {props.iconActions?.map((action) => {
119
+ const commonProps = {
120
+ key: action.id,
121
+ "aria-label": action.label,
122
+ title: action.label,
123
+ className: `inline-flex h-8 w-8 items-center justify-center rounded-sm text-secondary transition-colors hover:bg-surface-hover hover:text-primary ${focusRingClass}`,
124
+ } as const;
125
+
126
+ if (action.href) {
127
+ return (
128
+ <a {...commonProps} href={action.href}>
129
+ <span aria-hidden="true">{action.icon}</span>
130
+ </a>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <button {...commonProps} type="button" onClick={action.onClick}>
136
+ <span aria-hidden="true">{action.icon}</span>
137
+ </button>
138
+ );
139
+ })}
140
+
141
+ {props.primaryAction ? (
142
+ <button
143
+ type="button"
144
+ disabled={props.primaryAction.disabled}
145
+ onClick={props.primaryAction.onClick}
146
+ data-tone={props.primaryAction.tone ?? "accent"}
147
+ className={[
148
+ "ml-2 inline-flex h-8 items-center rounded-sm px-3 text-xs font-semibold transition-colors disabled:opacity-40",
149
+ props.primaryAction.tone === "neutral"
150
+ ? "bg-surface text-primary hover:bg-surface-hover"
151
+ : "bg-accent text-white hover:bg-[color-mix(in_srgb,var(--color-accent)_85%,#000)]",
152
+ focusRingClass,
153
+ ].join(" ")}
154
+ data-testid="tw-shell-header__primary-action"
155
+ >
156
+ {props.primaryAction.label}
157
+ </button>
158
+ ) : null}
159
+ </div>
160
+ </header>
161
+ );
162
+ }
@@ -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>