@beyondwork/docx-react-component 1.0.1 → 1.0.3
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 +44 -104
- package/package.json +50 -30
- package/src/README.md +85 -0
- package/src/api/README.md +22 -0
- package/src/api/public-types.ts +525 -0
- package/src/compare/diff-engine.ts +530 -0
- package/src/compare/export-redlines.ts +162 -0
- package/src/compare/snapshot.ts +37 -0
- package/src/component-inventory.md +99 -0
- package/src/core/README.md +10 -0
- package/src/core/commands/README.md +3 -0
- package/src/core/commands/formatting-commands.ts +161 -0
- package/src/core/commands/image-commands.ts +144 -0
- package/src/core/commands/index.ts +1013 -0
- package/src/core/commands/list-commands.ts +370 -0
- package/src/core/commands/review-commands.ts +108 -0
- package/src/core/commands/text-commands.ts +119 -0
- package/src/core/schema/README.md +3 -0
- package/src/core/schema/text-schema.ts +512 -0
- package/src/core/selection/README.md +3 -0
- package/src/core/selection/mapping.ts +238 -0
- package/src/core/selection/review-anchors.ts +94 -0
- package/src/core/state/README.md +3 -0
- package/src/core/state/editor-state.ts +580 -0
- package/src/core/state/text-transaction.ts +276 -0
- package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
- package/src/formats/xlsx/io/parse-sheet.ts +289 -0
- package/src/formats/xlsx/io/parse-styles.ts +57 -0
- package/src/formats/xlsx/io/parse-workbook.ts +75 -0
- package/src/formats/xlsx/io/xlsx-session.ts +306 -0
- package/src/formats/xlsx/model/cell.ts +189 -0
- package/src/formats/xlsx/model/sheet.ts +244 -0
- package/src/formats/xlsx/model/styles.ts +118 -0
- package/src/formats/xlsx/model/workbook.ts +449 -0
- package/src/index.ts +45 -0
- package/src/io/README.md +10 -0
- package/src/io/docx-session.ts +1763 -0
- package/src/io/export/README.md +3 -0
- package/src/io/export/export-session.ts +165 -0
- package/src/io/export/minimal-docx.ts +115 -0
- package/src/io/export/reattach-preserved-parts.ts +54 -0
- package/src/io/export/serialize-comments.ts +876 -0
- package/src/io/export/serialize-footnotes.ts +217 -0
- package/src/io/export/serialize-headers-footers.ts +200 -0
- package/src/io/export/serialize-main-document.ts +982 -0
- package/src/io/export/serialize-numbering.ts +97 -0
- package/src/io/export/serialize-revisions.ts +389 -0
- package/src/io/export/serialize-runtime-revisions.ts +265 -0
- package/src/io/export/serialize-tables.ts +147 -0
- package/src/io/export/split-review-boundaries.ts +194 -0
- package/src/io/normalize/README.md +3 -0
- package/src/io/normalize/normalize-text.ts +437 -0
- package/src/io/ooxml/README.md +3 -0
- package/src/io/ooxml/parse-comments.ts +779 -0
- package/src/io/ooxml/parse-complex-content.ts +287 -0
- package/src/io/ooxml/parse-fields.ts +438 -0
- package/src/io/ooxml/parse-footnotes.ts +403 -0
- package/src/io/ooxml/parse-headers-footers.ts +483 -0
- package/src/io/ooxml/parse-inline-media.ts +431 -0
- package/src/io/ooxml/parse-main-document.ts +1846 -0
- package/src/io/ooxml/parse-numbering.ts +425 -0
- package/src/io/ooxml/parse-revisions.ts +658 -0
- package/src/io/ooxml/parse-shapes.ts +271 -0
- package/src/io/ooxml/parse-tables.ts +568 -0
- package/src/io/ooxml/parse-theme.ts +314 -0
- package/src/io/ooxml/part-manifest.ts +136 -0
- package/src/io/ooxml/revision-boundaries.ts +351 -0
- package/src/io/opc/README.md +3 -0
- package/src/io/opc/corrupt-package.ts +166 -0
- package/src/io/opc/docx-package.ts +74 -0
- package/src/io/opc/package-reader.ts +325 -0
- package/src/io/opc/package-writer.ts +273 -0
- package/src/legal/bookmarks.ts +196 -0
- package/src/legal/cross-references.ts +356 -0
- package/src/legal/defined-terms.ts +203 -0
- package/src/model/README.md +3 -0
- package/src/model/canonical-document.ts +1911 -0
- package/src/model/cds-1.0.0.ts +196 -0
- package/src/model/snapshot.ts +393 -0
- package/src/preservation/README.md +3 -0
- package/src/preservation/markup-compatibility.ts +48 -0
- package/src/preservation/opaque-fragment-store.ts +89 -0
- package/src/preservation/opaque-region.ts +233 -0
- package/src/preservation/package-preservation.ts +120 -0
- package/src/preservation/preserved-part-manifest.ts +56 -0
- package/src/preservation/relationship-retention.ts +57 -0
- package/src/preservation/store.ts +185 -0
- package/src/review/README.md +16 -0
- package/src/review/store/README.md +3 -0
- package/src/review/store/comment-anchors.ts +70 -0
- package/src/review/store/comment-remapping.ts +154 -0
- package/src/review/store/comment-store.ts +331 -0
- package/src/review/store/comment-thread.ts +109 -0
- package/src/review/store/revision-actions.ts +394 -0
- package/src/review/store/revision-store.ts +303 -0
- package/src/review/store/revision-types.ts +168 -0
- package/src/review/store/runtime-comment-store.ts +43 -0
- package/src/runtime/README.md +3 -0
- package/src/runtime/ai-action-policy.ts +764 -0
- package/src/runtime/document-runtime.ts +967 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
- package/src/runtime/review-runtime.ts +44 -0
- package/src/runtime/revision-runtime.ts +107 -0
- package/src/runtime/session-capabilities.ts +138 -0
- package/src/runtime/surface-projection.ts +570 -0
- package/src/runtime/table-commands.ts +87 -0
- package/src/runtime/table-schema.ts +140 -0
- package/src/runtime/virtualized-rendering.ts +258 -0
- package/src/ui/README.md +30 -0
- package/src/ui/WordReviewEditor.tsx +1506 -0
- package/src/ui/comments/README.md +3 -0
- package/src/ui/compatibility/README.md +3 -0
- package/src/ui/editor-surface/README.md +3 -0
- package/src/ui/headless/comment-decoration-model.ts +124 -0
- package/src/ui/headless/revision-decoration-model.ts +128 -0
- package/src/ui/headless/selection-helpers.ts +34 -0
- package/src/ui/headless/use-editor-keyboard.ts +98 -0
- package/src/ui/review/README.md +3 -0
- package/src/ui/shared/revision-filters.ts +31 -0
- package/src/ui/status/README.md +3 -0
- package/src/ui/theme/README.md +3 -0
- package/src/ui/toolbar/README.md +3 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
- package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
- package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
- package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
- package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
- package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
- package/src/ui-tailwind/index.ts +61 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
- package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
- package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
- package/src/ui-tailwind/theme/editor-theme.css +190 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
- package/src/validation/README.md +3 -0
- package/src/validation/compatibility-engine.ts +317 -0
- package/src/validation/compatibility-report.ts +160 -0
- package/src/validation/diagnostics.ts +203 -0
- package/src/validation/import-diagnostics.ts +128 -0
- package/src/validation/low-priority-word-surfaces.ts +373 -0
- package/dist/chunk-32W6IVQE.js +0 -7725
- package/dist/chunk-32W6IVQE.js.map +0 -1
- package/dist/index.cjs +0 -23722
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -7
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -16011
- package/dist/index.js.map +0 -1
- package/dist/public-types-DqCURAz8.d.cts +0 -1152
- package/dist/public-types-DqCURAz8.d.ts +0 -1152
- package/dist/tailwind.cjs +0 -8295
- package/dist/tailwind.cjs.map +0 -1
- package/dist/tailwind.d.cts +0 -323
- package/dist/tailwind.d.ts +0 -323
- package/dist/tailwind.js +0 -553
- package/dist/tailwind.js.map +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
4
|
+
import * as Toggle from "@radix-ui/react-toggle";
|
|
5
|
+
import * as ToggleGroup from "@radix-ui/react-toggle-group";
|
|
6
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
7
|
+
import {
|
|
8
|
+
Download,
|
|
9
|
+
Eye,
|
|
10
|
+
EyeOff,
|
|
11
|
+
FileText,
|
|
12
|
+
MessageSquare,
|
|
13
|
+
Monitor,
|
|
14
|
+
Redo2,
|
|
15
|
+
ShieldAlert,
|
|
16
|
+
ShieldCheck,
|
|
17
|
+
Undo2,
|
|
18
|
+
} from "lucide-react";
|
|
19
|
+
|
|
20
|
+
import type { CompatibilityPanelSnapshot, EditorWarning } from "../../api/public-types";
|
|
21
|
+
import type { SessionCapabilities } from "../../runtime/session-capabilities";
|
|
22
|
+
import { TwHealthPanel } from "../review/tw-health-panel";
|
|
23
|
+
import { TwToolbarIconButton } from "./tw-toolbar-icon-button";
|
|
24
|
+
|
|
25
|
+
export type ViewMode = "canvas" | "document";
|
|
26
|
+
|
|
27
|
+
export interface TwToolbarProps {
|
|
28
|
+
sourceLabel?: string;
|
|
29
|
+
capabilities?: SessionCapabilities;
|
|
30
|
+
compatibility?: CompatibilityPanelSnapshot;
|
|
31
|
+
warnings?: EditorWarning[];
|
|
32
|
+
viewMode: ViewMode;
|
|
33
|
+
/** Display toggle for tracked change decorations (not a runtime mutation toggle). */
|
|
34
|
+
showTrackedChanges: boolean;
|
|
35
|
+
onUndo: () => void;
|
|
36
|
+
onRedo: () => void;
|
|
37
|
+
onAddComment: () => void;
|
|
38
|
+
onExport: () => void;
|
|
39
|
+
onViewModeChange: (value: ViewMode) => void;
|
|
40
|
+
onShowTrackedChangesChange: (show: boolean) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const focusRingClass =
|
|
44
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
45
|
+
|
|
46
|
+
export function TwToolbar(props: TwToolbarProps) {
|
|
47
|
+
const caps = props.capabilities;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<header className="flex h-10 shrink-0 items-center gap-1 border-b border-border px-2">
|
|
51
|
+
{/* Left cluster: undo/redo + formatting */}
|
|
52
|
+
<div className="flex items-center gap-0.5">
|
|
53
|
+
<TwToolbarIconButton
|
|
54
|
+
icon={Undo2}
|
|
55
|
+
label="Undo"
|
|
56
|
+
disabled={caps ? !caps.canUndo : true}
|
|
57
|
+
onClick={props.onUndo}
|
|
58
|
+
/>
|
|
59
|
+
<TwToolbarIconButton
|
|
60
|
+
icon={Redo2}
|
|
61
|
+
label="Redo"
|
|
62
|
+
disabled={caps ? !caps.canRedo : true}
|
|
63
|
+
onClick={props.onRedo}
|
|
64
|
+
/>
|
|
65
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
66
|
+
|
|
67
|
+
{/* Paragraph style selector and B/I/U removed — not yet supported */}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Center: document title */}
|
|
71
|
+
<div className="flex-1 text-center min-w-0">
|
|
72
|
+
<span className="text-sm font-medium truncate block">
|
|
73
|
+
{props.sourceLabel ?? "Untitled"}
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
{/* Right cluster: comment, track changes, markup, view, export */}
|
|
78
|
+
<div className="flex items-center gap-0.5">
|
|
79
|
+
<TwToolbarIconButton
|
|
80
|
+
icon={MessageSquare}
|
|
81
|
+
label="Add comment"
|
|
82
|
+
disabled={caps ? !caps.canAddComment : true}
|
|
83
|
+
emphasis
|
|
84
|
+
onClick={props.onAddComment}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<Tooltip.Root>
|
|
88
|
+
<Tooltip.Trigger asChild>
|
|
89
|
+
<Toggle.Root
|
|
90
|
+
pressed={props.showTrackedChanges}
|
|
91
|
+
onPressedChange={props.onShowTrackedChangesChange}
|
|
92
|
+
disabled={caps ? !caps.trackChangesSupported : false}
|
|
93
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none disabled:opacity-40 ${focusRingClass}`}
|
|
94
|
+
>
|
|
95
|
+
{props.showTrackedChanges ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
|
|
96
|
+
</Toggle.Root>
|
|
97
|
+
</Tooltip.Trigger>
|
|
98
|
+
<Tooltip.Portal>
|
|
99
|
+
<Tooltip.Content
|
|
100
|
+
className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
|
|
101
|
+
sideOffset={6}
|
|
102
|
+
>
|
|
103
|
+
{props.showTrackedChanges ? "Hide tracked changes" : "Show tracked changes"}
|
|
104
|
+
</Tooltip.Content>
|
|
105
|
+
</Tooltip.Portal>
|
|
106
|
+
</Tooltip.Root>
|
|
107
|
+
|
|
108
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
109
|
+
|
|
110
|
+
{/* View mode toggle group: Canvas (clean, flowing) / Document (paged, markup) */}
|
|
111
|
+
<ToggleGroup.Root
|
|
112
|
+
type="single"
|
|
113
|
+
value={props.viewMode}
|
|
114
|
+
onValueChange={(v: string) => {
|
|
115
|
+
if (v) props.onViewModeChange(v as ViewMode);
|
|
116
|
+
}}
|
|
117
|
+
className="flex items-center gap-0.5"
|
|
118
|
+
>
|
|
119
|
+
<Tooltip.Root>
|
|
120
|
+
<Tooltip.Trigger asChild>
|
|
121
|
+
<ToggleGroup.Item
|
|
122
|
+
value="canvas"
|
|
123
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none ${focusRingClass}`}
|
|
124
|
+
>
|
|
125
|
+
<Monitor className="h-3.5 w-3.5" />
|
|
126
|
+
</ToggleGroup.Item>
|
|
127
|
+
</Tooltip.Trigger>
|
|
128
|
+
<Tooltip.Portal>
|
|
129
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
130
|
+
Canvas — clean flowing text
|
|
131
|
+
</Tooltip.Content>
|
|
132
|
+
</Tooltip.Portal>
|
|
133
|
+
</Tooltip.Root>
|
|
134
|
+
<Tooltip.Root>
|
|
135
|
+
<Tooltip.Trigger asChild>
|
|
136
|
+
<ToggleGroup.Item
|
|
137
|
+
value="document"
|
|
138
|
+
className={`inline-flex h-7 w-7 items-center justify-center rounded-md text-secondary transition-colors hover:bg-surface data-[state=on]:bg-accent-soft data-[state=on]:text-accent outline-none ${focusRingClass}`}
|
|
139
|
+
>
|
|
140
|
+
<FileText className="h-3.5 w-3.5" />
|
|
141
|
+
</ToggleGroup.Item>
|
|
142
|
+
</Tooltip.Trigger>
|
|
143
|
+
<Tooltip.Portal>
|
|
144
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
145
|
+
Document — paged with markup
|
|
146
|
+
</Tooltip.Content>
|
|
147
|
+
</Tooltip.Portal>
|
|
148
|
+
</Tooltip.Root>
|
|
149
|
+
</ToggleGroup.Root>
|
|
150
|
+
|
|
151
|
+
{/* Health indicator */}
|
|
152
|
+
{props.compatibility && props.warnings ? (
|
|
153
|
+
<Popover.Root>
|
|
154
|
+
<Tooltip.Root>
|
|
155
|
+
<Tooltip.Trigger asChild>
|
|
156
|
+
<Popover.Trigger asChild>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className={`relative inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-surface hover:text-primary outline-none ${focusRingClass} ${
|
|
160
|
+
(caps?.healthIssueCount ?? 0) > 0 ? "text-secondary" : "text-secondary"
|
|
161
|
+
}`}
|
|
162
|
+
>
|
|
163
|
+
{(caps?.healthIssueCount ?? 0) > 0
|
|
164
|
+
? <ShieldAlert className="h-4 w-4" />
|
|
165
|
+
: <ShieldCheck className="h-4 w-4" />
|
|
166
|
+
}
|
|
167
|
+
{(caps?.healthIssueCount ?? 0) > 0 ? (
|
|
168
|
+
<span className="absolute -top-0.5 -right-0.5 flex h-3 min-w-[12px] items-center justify-center rounded-full bg-tertiary text-[8px] font-medium text-white">
|
|
169
|
+
{caps?.healthIssueCount}
|
|
170
|
+
</span>
|
|
171
|
+
) : null}
|
|
172
|
+
</button>
|
|
173
|
+
</Popover.Trigger>
|
|
174
|
+
</Tooltip.Trigger>
|
|
175
|
+
<Tooltip.Portal>
|
|
176
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
177
|
+
{(caps?.healthIssueCount ?? 0) > 0
|
|
178
|
+
? `Document health — ${caps?.healthIssueCount} issue${(caps?.healthIssueCount ?? 0) !== 1 ? "s" : ""}`
|
|
179
|
+
: "Document health — no issues"
|
|
180
|
+
}
|
|
181
|
+
</Tooltip.Content>
|
|
182
|
+
</Tooltip.Portal>
|
|
183
|
+
</Tooltip.Root>
|
|
184
|
+
<Popover.Portal>
|
|
185
|
+
<Popover.Content
|
|
186
|
+
className="w-[360px] max-h-[480px] overflow-y-auto rounded-lg bg-canvas shadow-lg ring-1 ring-border p-3 z-50"
|
|
187
|
+
sideOffset={8}
|
|
188
|
+
align="end"
|
|
189
|
+
>
|
|
190
|
+
<TwHealthPanel
|
|
191
|
+
compatibility={props.compatibility}
|
|
192
|
+
warnings={props.warnings}
|
|
193
|
+
/>
|
|
194
|
+
</Popover.Content>
|
|
195
|
+
</Popover.Portal>
|
|
196
|
+
</Popover.Root>
|
|
197
|
+
) : null}
|
|
198
|
+
|
|
199
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
200
|
+
|
|
201
|
+
{/* Export button */}
|
|
202
|
+
<Tooltip.Root>
|
|
203
|
+
<Tooltip.Trigger asChild>
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
disabled={caps ? !caps.canExport : true}
|
|
207
|
+
className={[
|
|
208
|
+
"inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs font-semibold transition-colors outline-none",
|
|
209
|
+
focusRingClass,
|
|
210
|
+
caps?.exportBlocked
|
|
211
|
+
? "cursor-not-allowed text-danger opacity-50"
|
|
212
|
+
: "text-accent hover:bg-accent-soft",
|
|
213
|
+
].join(" ")}
|
|
214
|
+
onClick={props.onExport}
|
|
215
|
+
>
|
|
216
|
+
<Download className="h-3.5 w-3.5" />
|
|
217
|
+
Export
|
|
218
|
+
</button>
|
|
219
|
+
</Tooltip.Trigger>
|
|
220
|
+
<Tooltip.Portal>
|
|
221
|
+
<Tooltip.Content className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50" sideOffset={6}>
|
|
222
|
+
{caps?.exportBlocked
|
|
223
|
+
? "Export blocked by unsupported content"
|
|
224
|
+
: "Export document"}
|
|
225
|
+
</Tooltip.Content>
|
|
226
|
+
</Tooltip.Portal>
|
|
227
|
+
</Tooltip.Root>
|
|
228
|
+
</div>
|
|
229
|
+
</header>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { type ReactNode, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import * as Tooltip from "@radix-ui/react-tooltip";
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CommentSidebarThreadSnapshot,
|
|
7
|
+
RuntimeRenderSnapshot,
|
|
8
|
+
TrackedChangeEntrySnapshot,
|
|
9
|
+
} from "../api/public-types";
|
|
10
|
+
import type { SessionCapabilities } from "../runtime/session-capabilities";
|
|
11
|
+
import type { MarkupDisplay } from "../ui/headless/comment-decoration-model";
|
|
12
|
+
import { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
13
|
+
import { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
|
|
14
|
+
import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
15
|
+
import { TwStatusBar } from "./status/tw-status-bar";
|
|
16
|
+
import { TwToolbar, type ViewMode } from "./toolbar/tw-toolbar";
|
|
17
|
+
|
|
18
|
+
export interface TwReviewWorkspaceProps {
|
|
19
|
+
snapshot: RuntimeRenderSnapshot;
|
|
20
|
+
currentUserId?: string;
|
|
21
|
+
capabilities?: SessionCapabilities;
|
|
22
|
+
reviewMode?: "editing" | "review";
|
|
23
|
+
document: ReactNode;
|
|
24
|
+
viewMode: ViewMode;
|
|
25
|
+
activeRailTab: ReviewRailTab;
|
|
26
|
+
activeCommentId?: string;
|
|
27
|
+
activeRevisionId?: string;
|
|
28
|
+
showTrackedChanges: boolean;
|
|
29
|
+
selectionPreview?: string | null;
|
|
30
|
+
onViewModeChange: (value: ViewMode) => void;
|
|
31
|
+
onActiveRailTabChange: (value: ReviewRailTab) => void;
|
|
32
|
+
onShowTrackedChangesChange: (show: boolean) => void;
|
|
33
|
+
onUndo: () => void;
|
|
34
|
+
onRedo: () => void;
|
|
35
|
+
onAddComment: () => void;
|
|
36
|
+
onExport: () => void;
|
|
37
|
+
onOpenComment: (thread: CommentSidebarThreadSnapshot) => void;
|
|
38
|
+
onResolveComment: (commentId: string) => void;
|
|
39
|
+
onReopenComment?: (commentId: string) => void;
|
|
40
|
+
onAddReply?: (commentId: string, body: string) => void;
|
|
41
|
+
onEditBody?: (commentId: string, body: string) => void;
|
|
42
|
+
onOpenRevision: (revision: TrackedChangeEntrySnapshot) => void;
|
|
43
|
+
onAcceptRevision: (revisionId: string) => void;
|
|
44
|
+
onRejectRevision: (revisionId: string) => void;
|
|
45
|
+
onAcceptAllChanges: () => void;
|
|
46
|
+
onRejectAllChanges: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function TwReviewWorkspace(props: TwReviewWorkspaceProps) {
|
|
50
|
+
const { snapshot } = props;
|
|
51
|
+
const caps = props.capabilities;
|
|
52
|
+
const markupDisplay: MarkupDisplay = props.viewMode === "document" ? "all" : "clean";
|
|
53
|
+
const preserveOnlyCount = caps?.preserveOnlyCount ??
|
|
54
|
+
snapshot.compatibility.featureEntries.filter(
|
|
55
|
+
(entry) => entry.featureClass === "preserve-only",
|
|
56
|
+
).length;
|
|
57
|
+
const showReviewRail = caps?.reviewRailVisible ?? true;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Tooltip.Provider delayDuration={400}>
|
|
61
|
+
<div className="flex h-full flex-col bg-canvas text-primary">
|
|
62
|
+
<TwToolbar
|
|
63
|
+
sourceLabel={snapshot.sourceLabel}
|
|
64
|
+
capabilities={caps}
|
|
65
|
+
compatibility={snapshot.compatibility}
|
|
66
|
+
warnings={snapshot.warnings}
|
|
67
|
+
viewMode={props.viewMode}
|
|
68
|
+
showTrackedChanges={props.showTrackedChanges}
|
|
69
|
+
onUndo={props.onUndo}
|
|
70
|
+
onRedo={props.onRedo}
|
|
71
|
+
onAddComment={props.onAddComment}
|
|
72
|
+
onExport={props.onExport}
|
|
73
|
+
onViewModeChange={props.onViewModeChange}
|
|
74
|
+
onShowTrackedChangesChange={props.onShowTrackedChangesChange}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<TwAlertBanner snapshot={snapshot} preserveOnlyCount={preserveOnlyCount} />
|
|
78
|
+
|
|
79
|
+
<div className="flex flex-1 min-h-0">
|
|
80
|
+
{/* Document column */}
|
|
81
|
+
<div className="flex flex-1 flex-col min-w-0">
|
|
82
|
+
<div className={`flex-1 overflow-y-auto ${props.viewMode === "document" ? "bg-surface" : "bg-canvas"}`}>
|
|
83
|
+
<div
|
|
84
|
+
className={`mx-auto min-h-full ${
|
|
85
|
+
props.viewMode === "document"
|
|
86
|
+
? "max-w-[780px] my-8 rounded-xl ring-1 ring-border shadow-sm bg-canvas"
|
|
87
|
+
: "bg-canvas"
|
|
88
|
+
}`}
|
|
89
|
+
>
|
|
90
|
+
{props.selectionPreview ? (
|
|
91
|
+
<div className="flex justify-center pt-4 px-4">
|
|
92
|
+
<TwSelectionToolbar
|
|
93
|
+
selectionPreview={props.selectionPreview}
|
|
94
|
+
readOnly={snapshot.readOnly}
|
|
95
|
+
onAddComment={props.onAddComment}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
) : null}
|
|
99
|
+
{props.document}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<TwStatusBar
|
|
104
|
+
isDirty={snapshot.isDirty}
|
|
105
|
+
isExportBlocked={snapshot.compatibility.blockExport}
|
|
106
|
+
preserveOnlyCount={preserveOnlyCount}
|
|
107
|
+
commentCount={snapshot.comments.totalCount}
|
|
108
|
+
changeCount={snapshot.trackedChanges.totalCount}
|
|
109
|
+
sessionId={snapshot.sessionId}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* Review rail — hidden in editing mode unless toggled */}
|
|
114
|
+
{showReviewRail ? <TwReviewRail
|
|
115
|
+
activeTab={props.activeRailTab}
|
|
116
|
+
currentUserId={props.currentUserId}
|
|
117
|
+
comments={snapshot.comments}
|
|
118
|
+
trackedChanges={snapshot.trackedChanges}
|
|
119
|
+
compatibility={snapshot.compatibility}
|
|
120
|
+
warnings={snapshot.warnings}
|
|
121
|
+
markupDisplay={markupDisplay}
|
|
122
|
+
activeCommentId={props.activeCommentId}
|
|
123
|
+
activeRevisionId={props.activeRevisionId}
|
|
124
|
+
onActiveTabChange={props.onActiveRailTabChange}
|
|
125
|
+
onOpenComment={props.onOpenComment}
|
|
126
|
+
onResolveComment={props.onResolveComment}
|
|
127
|
+
onReopenComment={props.onReopenComment}
|
|
128
|
+
onAddReply={props.onAddReply}
|
|
129
|
+
onEditBody={props.onEditBody}
|
|
130
|
+
onOpenRevision={props.onOpenRevision}
|
|
131
|
+
onAcceptRevision={props.onAcceptRevision}
|
|
132
|
+
onRejectRevision={props.onRejectRevision}
|
|
133
|
+
onAcceptAllChanges={props.onAcceptAllChanges}
|
|
134
|
+
onRejectAllChanges={props.onRejectAllChanges}
|
|
135
|
+
/> : null}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</Tooltip.Provider>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createRangeAnchor } from "../core/selection/mapping.ts";
|
|
2
|
+
import type {
|
|
3
|
+
CanonicalDocumentEnvelope,
|
|
4
|
+
CompatibilityFeatureEntry,
|
|
5
|
+
CompatibilityReport,
|
|
6
|
+
EditorError,
|
|
7
|
+
EditorWarning,
|
|
8
|
+
} from "../core/state/editor-state.ts";
|
|
9
|
+
import type {
|
|
10
|
+
DocumentRootNode,
|
|
11
|
+
InlineNode,
|
|
12
|
+
ParagraphNode,
|
|
13
|
+
} from "../model/canonical-document.ts";
|
|
14
|
+
import {
|
|
15
|
+
describeOpaqueFragment,
|
|
16
|
+
listOpaqueFragments,
|
|
17
|
+
listPreservedPackageParts,
|
|
18
|
+
} from "../preservation/store.ts";
|
|
19
|
+
|
|
20
|
+
export interface BuildCompatibilityReportInput {
|
|
21
|
+
document: CanonicalDocumentEnvelope;
|
|
22
|
+
warnings?: readonly EditorWarning[];
|
|
23
|
+
fatalError?: EditorError;
|
|
24
|
+
generatedAt: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildCompatibilityReport(
|
|
28
|
+
input: BuildCompatibilityReportInput,
|
|
29
|
+
): CompatibilityReport {
|
|
30
|
+
const content = normalizeDocumentRoot(input.document.content);
|
|
31
|
+
const contentFeatures = collectContentFeatures(content);
|
|
32
|
+
if (hasSupportedRuntimeComments(input.document.review.comments)) {
|
|
33
|
+
contentFeatures.push(
|
|
34
|
+
supportedEntry(
|
|
35
|
+
"comments-single-paragraph",
|
|
36
|
+
"Single-paragraph review comments stay mappable through runtime selections.",
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const featureEntries: CompatibilityFeatureEntry[] = [
|
|
41
|
+
...contentFeatures,
|
|
42
|
+
...collectPreservationFeatures(input.document),
|
|
43
|
+
];
|
|
44
|
+
const warnings = dedupeWarnings([
|
|
45
|
+
...(input.warnings ?? []),
|
|
46
|
+
...collectDiagnosticWarnings(input.document),
|
|
47
|
+
]);
|
|
48
|
+
const errors = dedupeErrors([
|
|
49
|
+
...collectDiagnosticErrors(input.document),
|
|
50
|
+
...(input.fatalError ? [input.fatalError] : []),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
reportVersion: "compatibility-report/1",
|
|
55
|
+
generatedAt: input.generatedAt,
|
|
56
|
+
blockExport:
|
|
57
|
+
featureEntries.some((entry) => entry.featureClass === "unsupported-fatal") ||
|
|
58
|
+
errors.some((error) => error.isFatal),
|
|
59
|
+
featureEntries,
|
|
60
|
+
warnings,
|
|
61
|
+
errors,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasSupportedRuntimeComments(
|
|
66
|
+
comments: CanonicalDocumentEnvelope["review"]["comments"],
|
|
67
|
+
): boolean {
|
|
68
|
+
return Object.values(comments).some((comment) => comment.anchor.kind !== "detached");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeDocumentRoot(content: unknown): DocumentRootNode {
|
|
72
|
+
if (content && typeof content === "object" && (content as { type?: string }).type === "doc") {
|
|
73
|
+
return content as DocumentRootNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(content)) {
|
|
77
|
+
return {
|
|
78
|
+
type: "doc",
|
|
79
|
+
children: content.filter(
|
|
80
|
+
(value): value is DocumentRootNode["children"][number] =>
|
|
81
|
+
Boolean(value) &&
|
|
82
|
+
typeof value === "object" &&
|
|
83
|
+
((value as { type?: string }).type === "paragraph" ||
|
|
84
|
+
(value as { type?: string }).type === "opaque_block"),
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
type: "doc",
|
|
91
|
+
children: [{ type: "paragraph", children: [] }],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function collectContentFeatures(
|
|
96
|
+
content: DocumentRootNode,
|
|
97
|
+
): CompatibilityFeatureEntry[] {
|
|
98
|
+
const flags = {
|
|
99
|
+
paragraphs: false,
|
|
100
|
+
runs: false,
|
|
101
|
+
whitespace: false,
|
|
102
|
+
headings: false,
|
|
103
|
+
lists: false,
|
|
104
|
+
hyperlinks: false,
|
|
105
|
+
images: false,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (let index = 0; index < content.children.length; index += 1) {
|
|
109
|
+
const block = content.children[index];
|
|
110
|
+
if (block.type !== "paragraph") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
flags.paragraphs = true;
|
|
115
|
+
if (block.styleId?.toLowerCase().startsWith("heading")) {
|
|
116
|
+
flags.headings = true;
|
|
117
|
+
}
|
|
118
|
+
if (block.numbering) {
|
|
119
|
+
flags.lists = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
measureParagraph(block, flags);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const entries: CompatibilityFeatureEntry[] = [];
|
|
126
|
+
if (flags.paragraphs) {
|
|
127
|
+
entries.push(supportedEntry("paragraphs", "Paragraph structure is editable and round-trippable."));
|
|
128
|
+
}
|
|
129
|
+
if (flags.runs) {
|
|
130
|
+
entries.push(supportedEntry("runs", "Runs and inline text are editable through runtime commands."));
|
|
131
|
+
}
|
|
132
|
+
if (flags.whitespace) {
|
|
133
|
+
entries.push(supportedEntry("whitespace", "Whitespace-sensitive text units stay explicit in the runtime model."));
|
|
134
|
+
}
|
|
135
|
+
if (flags.headings) {
|
|
136
|
+
entries.push(supportedEntry("headings", "Heading styles remain attached to paragraph structure."));
|
|
137
|
+
}
|
|
138
|
+
if (flags.lists) {
|
|
139
|
+
entries.push(supportedEntry("lists", "Numbering metadata stays attached to paragraph blocks."));
|
|
140
|
+
}
|
|
141
|
+
if (flags.hyperlinks) {
|
|
142
|
+
entries.push(supportedEntry("hyperlinks", "Hyperlink relationships are preserved and re-serialized."));
|
|
143
|
+
}
|
|
144
|
+
if (flags.images) {
|
|
145
|
+
entries.push(supportedEntry("inline-images", "Inline image placements stay attached to preserved media parts."));
|
|
146
|
+
}
|
|
147
|
+
return entries;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function measureParagraph(
|
|
151
|
+
paragraph: ParagraphNode,
|
|
152
|
+
flags: {
|
|
153
|
+
runs: boolean;
|
|
154
|
+
whitespace: boolean;
|
|
155
|
+
hyperlinks: boolean;
|
|
156
|
+
images: boolean;
|
|
157
|
+
},
|
|
158
|
+
): number {
|
|
159
|
+
let size = 0;
|
|
160
|
+
const children = Array.isArray(paragraph.children) ? paragraph.children : [];
|
|
161
|
+
|
|
162
|
+
for (const child of children) {
|
|
163
|
+
size += measureInlineNode(child, flags);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return size;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function measureInlineNode(
|
|
170
|
+
node: InlineNode,
|
|
171
|
+
flags: {
|
|
172
|
+
runs: boolean;
|
|
173
|
+
whitespace: boolean;
|
|
174
|
+
hyperlinks: boolean;
|
|
175
|
+
images: boolean;
|
|
176
|
+
},
|
|
177
|
+
): number {
|
|
178
|
+
switch (node.type) {
|
|
179
|
+
case "text":
|
|
180
|
+
flags.runs = flags.runs || node.text.length > 0;
|
|
181
|
+
flags.whitespace =
|
|
182
|
+
flags.whitespace ||
|
|
183
|
+
/^\s/u.test(node.text) ||
|
|
184
|
+
/\s$/u.test(node.text) ||
|
|
185
|
+
node.text.includes(" ");
|
|
186
|
+
return Array.from(node.text).length;
|
|
187
|
+
case "tab":
|
|
188
|
+
case "hard_break":
|
|
189
|
+
flags.runs = true;
|
|
190
|
+
flags.whitespace = true;
|
|
191
|
+
return 1;
|
|
192
|
+
case "hyperlink":
|
|
193
|
+
flags.runs = true;
|
|
194
|
+
flags.hyperlinks = true;
|
|
195
|
+
return node.children.reduce((size, child) => size + measureInlineNode(child, flags), 0);
|
|
196
|
+
case "image":
|
|
197
|
+
flags.images = true;
|
|
198
|
+
flags.runs = true;
|
|
199
|
+
return 1;
|
|
200
|
+
case "opaque_inline":
|
|
201
|
+
flags.runs = true;
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function collectPreservationFeatures(
|
|
207
|
+
document: CanonicalDocumentEnvelope,
|
|
208
|
+
): CompatibilityFeatureEntry[] {
|
|
209
|
+
const entries: CompatibilityFeatureEntry[] = [];
|
|
210
|
+
|
|
211
|
+
for (const fragment of listOpaqueFragments(document.preservation as never)) {
|
|
212
|
+
const descriptor = describeOpaqueFragment(fragment);
|
|
213
|
+
entries.push({
|
|
214
|
+
featureEntryId: `feature:${fragment.fragmentId}`,
|
|
215
|
+
featureKey: descriptor.featureKey,
|
|
216
|
+
featureClass: "preserve-only",
|
|
217
|
+
message: descriptor.label,
|
|
218
|
+
affectedAnchor: createRangeAnchor(
|
|
219
|
+
fragment.lastKnownRange.from,
|
|
220
|
+
fragment.lastKnownRange.to,
|
|
221
|
+
),
|
|
222
|
+
details: {
|
|
223
|
+
fragmentId: fragment.fragmentId,
|
|
224
|
+
warningId: fragment.warningId,
|
|
225
|
+
detail: descriptor.detail,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const packagePart of listPreservedPackageParts(document.preservation as never)) {
|
|
231
|
+
entries.push({
|
|
232
|
+
featureEntryId: `feature:package:${packagePart.packagePartName}`,
|
|
233
|
+
featureKey: "unknown-package-parts",
|
|
234
|
+
featureClass: "preserve-only",
|
|
235
|
+
message: `Preserved package part ${packagePart.packagePartName}.`,
|
|
236
|
+
details: {
|
|
237
|
+
packagePartName: packagePart.packagePartName,
|
|
238
|
+
contentType: packagePart.contentType,
|
|
239
|
+
relationshipIds: packagePart.relationshipIds,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return entries;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function collectDiagnosticWarnings(
|
|
248
|
+
document: CanonicalDocumentEnvelope,
|
|
249
|
+
): EditorWarning[] {
|
|
250
|
+
const diagnostics = Array.isArray(document.diagnostics?.warnings)
|
|
251
|
+
? document.diagnostics.warnings
|
|
252
|
+
: [];
|
|
253
|
+
|
|
254
|
+
return diagnostics.map((warning) => ({
|
|
255
|
+
warningId: warning.warningId,
|
|
256
|
+
code:
|
|
257
|
+
warning.source === "validation" || warning.source === "export"
|
|
258
|
+
? "export_roundtrip_risk"
|
|
259
|
+
: warning.source === "preservation"
|
|
260
|
+
? "unsupported_ooxml_preserved"
|
|
261
|
+
: "import_normalized",
|
|
262
|
+
severity: "warning",
|
|
263
|
+
message: warning.message,
|
|
264
|
+
source: warning.source,
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function collectDiagnosticErrors(
|
|
269
|
+
document: CanonicalDocumentEnvelope,
|
|
270
|
+
): EditorError[] {
|
|
271
|
+
const diagnostics = Array.isArray(document.diagnostics?.errors)
|
|
272
|
+
? document.diagnostics.errors
|
|
273
|
+
: [];
|
|
274
|
+
|
|
275
|
+
return diagnostics.map((error) => ({
|
|
276
|
+
errorId: error.diagnosticId,
|
|
277
|
+
code:
|
|
278
|
+
error.code === "load_failed"
|
|
279
|
+
? "import_failed"
|
|
280
|
+
: error.code,
|
|
281
|
+
message: error.message,
|
|
282
|
+
isFatal: error.isFatal,
|
|
283
|
+
source: error.source,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function supportedEntry(
|
|
288
|
+
featureKey: CompatibilityFeatureEntry["featureKey"],
|
|
289
|
+
message: string,
|
|
290
|
+
): CompatibilityFeatureEntry {
|
|
291
|
+
return {
|
|
292
|
+
featureEntryId: `feature:${featureKey}`,
|
|
293
|
+
featureKey,
|
|
294
|
+
featureClass: "supported-roundtrip",
|
|
295
|
+
message,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function dedupeWarnings(warnings: EditorWarning[]): EditorWarning[] {
|
|
300
|
+
const byId = new Map<string, EditorWarning>();
|
|
301
|
+
|
|
302
|
+
for (const warning of warnings) {
|
|
303
|
+
byId.set(warning.warningId, warning);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return [...byId.values()];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function dedupeErrors(errors: EditorError[]): EditorError[] {
|
|
310
|
+
const byId = new Map<string, EditorError>();
|
|
311
|
+
|
|
312
|
+
for (const error of errors) {
|
|
313
|
+
byId.set(error.errorId, error);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return [...byId.values()];
|
|
317
|
+
}
|