@beyondwork/docx-react-component 1.0.1 → 1.0.2

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 (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +76 -46
  3. package/src/README.md +85 -0
  4. package/src/api/README.md +22 -0
  5. package/src/api/public-types.ts +525 -0
  6. package/src/compare/diff-engine.ts +530 -0
  7. package/src/compare/export-redlines.ts +162 -0
  8. package/src/compare/snapshot.ts +37 -0
  9. package/src/component-inventory.md +99 -0
  10. package/src/core/README.md +10 -0
  11. package/src/core/commands/README.md +3 -0
  12. package/src/core/commands/formatting-commands.ts +161 -0
  13. package/src/core/commands/image-commands.ts +144 -0
  14. package/src/core/commands/index.ts +1013 -0
  15. package/src/core/commands/list-commands.ts +370 -0
  16. package/src/core/commands/review-commands.ts +108 -0
  17. package/src/core/commands/text-commands.ts +119 -0
  18. package/src/core/schema/README.md +3 -0
  19. package/src/core/schema/text-schema.ts +512 -0
  20. package/src/core/selection/README.md +3 -0
  21. package/src/core/selection/mapping.ts +238 -0
  22. package/src/core/selection/review-anchors.ts +94 -0
  23. package/src/core/state/README.md +3 -0
  24. package/src/core/state/editor-state.ts +580 -0
  25. package/src/core/state/text-transaction.ts +276 -0
  26. package/src/formats/xlsx/io/parse-shared-strings.ts +41 -0
  27. package/src/formats/xlsx/io/parse-sheet.ts +289 -0
  28. package/src/formats/xlsx/io/parse-styles.ts +57 -0
  29. package/src/formats/xlsx/io/parse-workbook.ts +75 -0
  30. package/src/formats/xlsx/io/xlsx-session.ts +306 -0
  31. package/src/formats/xlsx/model/cell.ts +189 -0
  32. package/src/formats/xlsx/model/sheet.ts +244 -0
  33. package/src/formats/xlsx/model/styles.ts +118 -0
  34. package/src/formats/xlsx/model/workbook.ts +449 -0
  35. package/src/index.ts +45 -0
  36. package/src/io/README.md +10 -0
  37. package/src/io/docx-session.ts +1763 -0
  38. package/src/io/export/README.md +3 -0
  39. package/src/io/export/export-session.ts +165 -0
  40. package/src/io/export/minimal-docx.ts +115 -0
  41. package/src/io/export/reattach-preserved-parts.ts +54 -0
  42. package/src/io/export/serialize-comments.ts +876 -0
  43. package/src/io/export/serialize-footnotes.ts +217 -0
  44. package/src/io/export/serialize-headers-footers.ts +200 -0
  45. package/src/io/export/serialize-main-document.ts +982 -0
  46. package/src/io/export/serialize-numbering.ts +97 -0
  47. package/src/io/export/serialize-revisions.ts +389 -0
  48. package/src/io/export/serialize-runtime-revisions.ts +265 -0
  49. package/src/io/export/serialize-tables.ts +147 -0
  50. package/src/io/export/split-review-boundaries.ts +194 -0
  51. package/src/io/normalize/README.md +3 -0
  52. package/src/io/normalize/normalize-text.ts +437 -0
  53. package/src/io/ooxml/README.md +3 -0
  54. package/src/io/ooxml/parse-comments.ts +779 -0
  55. package/src/io/ooxml/parse-complex-content.ts +287 -0
  56. package/src/io/ooxml/parse-fields.ts +438 -0
  57. package/src/io/ooxml/parse-footnotes.ts +403 -0
  58. package/src/io/ooxml/parse-headers-footers.ts +483 -0
  59. package/src/io/ooxml/parse-inline-media.ts +431 -0
  60. package/src/io/ooxml/parse-main-document.ts +1846 -0
  61. package/src/io/ooxml/parse-numbering.ts +425 -0
  62. package/src/io/ooxml/parse-revisions.ts +658 -0
  63. package/src/io/ooxml/parse-shapes.ts +271 -0
  64. package/src/io/ooxml/parse-tables.ts +568 -0
  65. package/src/io/ooxml/parse-theme.ts +314 -0
  66. package/src/io/ooxml/part-manifest.ts +136 -0
  67. package/src/io/ooxml/revision-boundaries.ts +351 -0
  68. package/src/io/opc/README.md +3 -0
  69. package/src/io/opc/corrupt-package.ts +166 -0
  70. package/src/io/opc/docx-package.ts +74 -0
  71. package/src/io/opc/package-reader.ts +320 -0
  72. package/src/io/opc/package-writer.ts +273 -0
  73. package/src/legal/bookmarks.ts +196 -0
  74. package/src/legal/cross-references.ts +356 -0
  75. package/src/legal/defined-terms.ts +203 -0
  76. package/src/model/README.md +3 -0
  77. package/src/model/canonical-document.ts +1911 -0
  78. package/src/model/cds-1.0.0.ts +196 -0
  79. package/src/model/snapshot.ts +393 -0
  80. package/src/preservation/README.md +3 -0
  81. package/src/preservation/markup-compatibility.ts +48 -0
  82. package/src/preservation/opaque-fragment-store.ts +89 -0
  83. package/src/preservation/opaque-region.ts +233 -0
  84. package/src/preservation/package-preservation.ts +120 -0
  85. package/src/preservation/preserved-part-manifest.ts +56 -0
  86. package/src/preservation/relationship-retention.ts +57 -0
  87. package/src/preservation/store.ts +185 -0
  88. package/src/review/README.md +16 -0
  89. package/src/review/store/README.md +3 -0
  90. package/src/review/store/comment-anchors.ts +70 -0
  91. package/src/review/store/comment-remapping.ts +154 -0
  92. package/src/review/store/comment-store.ts +331 -0
  93. package/src/review/store/comment-thread.ts +109 -0
  94. package/src/review/store/revision-actions.ts +394 -0
  95. package/src/review/store/revision-store.ts +303 -0
  96. package/src/review/store/revision-types.ts +168 -0
  97. package/src/review/store/runtime-comment-store.ts +43 -0
  98. package/src/runtime/README.md +3 -0
  99. package/src/runtime/ai-action-policy.ts +764 -0
  100. package/src/runtime/document-runtime.ts +967 -0
  101. package/src/runtime/read-only-diagnostics-runtime.ts +232 -0
  102. package/src/runtime/review-runtime.ts +44 -0
  103. package/src/runtime/revision-runtime.ts +107 -0
  104. package/src/runtime/session-capabilities.ts +138 -0
  105. package/src/runtime/surface-projection.ts +570 -0
  106. package/src/runtime/table-commands.ts +87 -0
  107. package/src/runtime/table-schema.ts +140 -0
  108. package/src/runtime/virtualized-rendering.ts +258 -0
  109. package/src/ui/README.md +30 -0
  110. package/src/ui/WordReviewEditor.tsx +1504 -0
  111. package/src/ui/comments/README.md +3 -0
  112. package/src/ui/compatibility/README.md +3 -0
  113. package/src/ui/editor-surface/README.md +3 -0
  114. package/src/ui/headless/comment-decoration-model.ts +124 -0
  115. package/src/ui/headless/revision-decoration-model.ts +128 -0
  116. package/src/ui/headless/selection-helpers.ts +34 -0
  117. package/src/ui/headless/use-editor-keyboard.ts +98 -0
  118. package/src/ui/review/README.md +3 -0
  119. package/src/ui/shared/revision-filters.ts +31 -0
  120. package/src/ui/status/README.md +3 -0
  121. package/src/ui/theme/README.md +3 -0
  122. package/src/ui/toolbar/README.md +3 -0
  123. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +48 -0
  124. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +44 -0
  125. package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +58 -0
  126. package/src/ui-tailwind/chrome/use-before-unload.ts +20 -0
  127. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +139 -0
  128. package/src/ui-tailwind/editor-surface/pm-decorations.ts +98 -0
  129. package/src/ui-tailwind/editor-surface/pm-position-map.ts +123 -0
  130. package/src/ui-tailwind/editor-surface/pm-schema.ts +452 -0
  131. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +327 -0
  132. package/src/ui-tailwind/editor-surface/search-plugin.ts +157 -0
  133. package/src/ui-tailwind/editor-surface/tw-caret.tsx +12 -0
  134. package/src/ui-tailwind/editor-surface/tw-editor-surface.tsx +150 -0
  135. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +118 -0
  136. package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +52 -0
  137. package/src/ui-tailwind/editor-surface/tw-paragraph-block.tsx +151 -0
  138. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +215 -0
  139. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +111 -0
  140. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +122 -0
  141. package/src/ui-tailwind/index.ts +61 -0
  142. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +276 -0
  143. package/src/ui-tailwind/review/tw-health-panel.tsx +120 -0
  144. package/src/ui-tailwind/review/tw-review-rail.tsx +120 -0
  145. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +164 -0
  146. package/src/ui-tailwind/status/tw-status-bar.tsx +58 -0
  147. package/src/ui-tailwind/theme/editor-theme.css +190 -0
  148. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +48 -0
  149. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +231 -0
  150. package/src/ui-tailwind/tw-review-workspace.tsx +140 -0
  151. package/src/validation/README.md +3 -0
  152. package/src/validation/compatibility-engine.ts +317 -0
  153. package/src/validation/compatibility-report.ts +160 -0
  154. package/src/validation/diagnostics.ts +203 -0
  155. package/src/validation/import-diagnostics.ts +128 -0
  156. package/src/validation/low-priority-word-surfaces.ts +373 -0
  157. package/dist/chunk-32W6IVQE.js +0 -7725
  158. package/dist/chunk-32W6IVQE.js.map +0 -1
  159. package/dist/index.cjs +0 -23722
  160. package/dist/index.cjs.map +0 -1
  161. package/dist/index.d.cts +0 -7
  162. package/dist/index.d.ts +0 -7
  163. package/dist/index.js +0 -16011
  164. package/dist/index.js.map +0 -1
  165. package/dist/public-types-DqCURAz8.d.cts +0 -1152
  166. package/dist/public-types-DqCURAz8.d.ts +0 -1152
  167. package/dist/tailwind.cjs +0 -8295
  168. package/dist/tailwind.cjs.map +0 -1
  169. package/dist/tailwind.d.cts +0 -323
  170. package/dist/tailwind.d.ts +0 -323
  171. package/dist/tailwind.js +0 -553
  172. package/dist/tailwind.js.map +0 -1
@@ -0,0 +1,3 @@
1
+ # Comments UI
2
+
3
+ Comment markers, thread navigation, and sidebar thread views belong here.
@@ -0,0 +1,3 @@
1
+ # Compatibility UI
2
+
3
+ Compatibility reports, preserve-only warnings, and unsupported-content summaries belong here.
@@ -0,0 +1,3 @@
1
+ # Editor Surface
2
+
3
+ The editable document canvas, overlays, and selection-aware rendering belong here.
@@ -0,0 +1,124 @@
1
+ import type { CommentSidebarSnapshot } from "../../api/public-types";
2
+
3
+ export interface CommentDecorationModel {
4
+ threads: CommentDecorationThread[];
5
+ activeCommentId?: string;
6
+ }
7
+
8
+ export interface CommentDecorationThread {
9
+ commentId: string;
10
+ from: number;
11
+ to: number;
12
+ status: CommentSidebarSnapshot["threads"][number]["status"];
13
+ isActive: boolean;
14
+ }
15
+
16
+ export interface CommentRangeState {
17
+ hasComments: boolean;
18
+ hasOpen: boolean;
19
+ hasResolved: boolean;
20
+ hasActive: boolean;
21
+ count: number;
22
+ overlapping: CommentDecorationThread[];
23
+ }
24
+
25
+ export function createCommentDecorationModel(
26
+ snapshot?: CommentSidebarSnapshot,
27
+ ): CommentDecorationModel | undefined {
28
+ if (!snapshot) {
29
+ return undefined;
30
+ }
31
+
32
+ return {
33
+ activeCommentId: snapshot.activeCommentId,
34
+ threads: snapshot.threads
35
+ .filter((thread) => thread.anchor.kind !== "detached")
36
+ .map((thread) => {
37
+ const anchor = thread.anchor;
38
+ const from = anchor.kind === "range" ? anchor.from : anchor.kind === "node" ? anchor.at : 0;
39
+ const to = anchor.kind === "range" ? anchor.to : anchor.kind === "node" ? anchor.at : 0;
40
+ return {
41
+ commentId: thread.commentId,
42
+ from,
43
+ to,
44
+ status: thread.status,
45
+ isActive: thread.isActive,
46
+ };
47
+ }),
48
+ };
49
+ }
50
+
51
+ export function getCommentRangeState(
52
+ model: CommentDecorationModel | undefined,
53
+ from: number,
54
+ to: number,
55
+ ): CommentRangeState {
56
+ if (!model) {
57
+ return {
58
+ hasComments: false,
59
+ hasOpen: false,
60
+ hasResolved: false,
61
+ hasActive: false,
62
+ count: 0,
63
+ overlapping: [],
64
+ };
65
+ }
66
+
67
+ const overlapping = model.threads.filter((thread) =>
68
+ rangesOverlap(thread.from, thread.to, from, to),
69
+ );
70
+ return {
71
+ hasComments: overlapping.length > 0,
72
+ hasOpen: overlapping.some((thread) => thread.status === "open"),
73
+ hasResolved: overlapping.some((thread) => thread.status === "resolved"),
74
+ hasActive: overlapping.some((thread) => thread.isActive),
75
+ count: overlapping.length,
76
+ overlapping,
77
+ };
78
+ }
79
+
80
+ export type MarkupDisplay = "clean" | "simple" | "all";
81
+
82
+ export function getCommentHighlightClass(
83
+ model: CommentDecorationModel | undefined,
84
+ from: number,
85
+ to: number,
86
+ markupDisplay: MarkupDisplay = "all",
87
+ ): string {
88
+ const state = getCommentRangeState(model, from, to);
89
+ if (!state.hasComments) {
90
+ return "";
91
+ }
92
+
93
+ switch (markupDisplay) {
94
+ case "clean":
95
+ return state.hasActive ? "bg-comment-soft" : "";
96
+ case "simple":
97
+ if (state.hasActive) {
98
+ return "underline decoration-comment decoration-2 underline-offset-4";
99
+ }
100
+ if (state.hasOpen) {
101
+ return "underline decoration-comment/60 decoration-1 underline-offset-4";
102
+ }
103
+ return "underline decoration-comment/40 decoration-1 underline-offset-4";
104
+ case "all":
105
+ if (state.hasActive) {
106
+ return "bg-comment-strong";
107
+ }
108
+ if (state.hasOpen) {
109
+ return "bg-comment-soft";
110
+ }
111
+ return "bg-comment-soft opacity-60";
112
+ }
113
+ }
114
+
115
+ export function rangesOverlap(
116
+ leftFrom: number,
117
+ leftTo: number,
118
+ rightFrom: number,
119
+ rightTo: number,
120
+ ): boolean {
121
+ const leftEnd = Math.max(leftFrom, leftTo);
122
+ const rightEnd = Math.max(rightFrom, rightTo);
123
+ return leftFrom < rightEnd && rightFrom < leftEnd;
124
+ }
@@ -0,0 +1,128 @@
1
+ import type { TrackedChangesSnapshot, TrackedChangeEntrySnapshot } from "../../api/public-types";
2
+ import { rangesOverlap, type MarkupDisplay } from "./comment-decoration-model";
3
+
4
+ export interface RevisionDecorationModel {
5
+ revisions: RevisionDecorationEntry[];
6
+ }
7
+
8
+ export interface RevisionDecorationEntry {
9
+ revisionId: string;
10
+ from: number;
11
+ to: number;
12
+ kind: TrackedChangeEntrySnapshot["kind"];
13
+ status: TrackedChangeEntrySnapshot["status"];
14
+ actionability: TrackedChangeEntrySnapshot["actionability"];
15
+ isActive: boolean;
16
+ }
17
+
18
+ export interface RevisionRangeState {
19
+ hasChanges: boolean;
20
+ hasInsertions: boolean;
21
+ hasDeletions: boolean;
22
+ hasActive: boolean;
23
+ count: number;
24
+ overlapping: RevisionDecorationEntry[];
25
+ }
26
+
27
+ export function createRevisionDecorationModel(
28
+ snapshot?: TrackedChangesSnapshot,
29
+ activeRevisionId?: string,
30
+ ): RevisionDecorationModel | undefined {
31
+ if (!snapshot) {
32
+ return undefined;
33
+ }
34
+
35
+ return {
36
+ revisions: snapshot.revisions
37
+ .filter((rev) => rev.anchor.kind !== "detached" && rev.status === "active")
38
+ .map((rev) => {
39
+ const anchor = rev.anchor;
40
+ const from = anchor.kind === "range" ? anchor.from : anchor.kind === "node" ? anchor.at : 0;
41
+ const to = anchor.kind === "range" ? anchor.to : anchor.kind === "node" ? anchor.at : 0;
42
+ return {
43
+ revisionId: rev.revisionId,
44
+ from,
45
+ to,
46
+ kind: rev.kind,
47
+ status: rev.status,
48
+ actionability: rev.actionability,
49
+ isActive: rev.revisionId === activeRevisionId,
50
+ };
51
+ }),
52
+ };
53
+ }
54
+
55
+ export function getRevisionRangeState(
56
+ model: RevisionDecorationModel | undefined,
57
+ from: number,
58
+ to: number,
59
+ ): RevisionRangeState {
60
+ if (!model) {
61
+ return {
62
+ hasChanges: false,
63
+ hasInsertions: false,
64
+ hasDeletions: false,
65
+ hasActive: false,
66
+ count: 0,
67
+ overlapping: [],
68
+ };
69
+ }
70
+
71
+ const overlapping = model.revisions.filter((rev) =>
72
+ rangesOverlap(rev.from, rev.to, from, to),
73
+ );
74
+ return {
75
+ hasChanges: overlapping.length > 0,
76
+ hasInsertions: overlapping.some((rev) => rev.kind === "insertion"),
77
+ hasDeletions: overlapping.some((rev) => rev.kind === "deletion"),
78
+ hasActive: overlapping.some((rev) => rev.isActive),
79
+ count: overlapping.length,
80
+ overlapping,
81
+ };
82
+ }
83
+
84
+ export function getRevisionHighlightClass(
85
+ model: RevisionDecorationModel | undefined,
86
+ from: number,
87
+ to: number,
88
+ markupDisplay: MarkupDisplay = "all",
89
+ ): string {
90
+ const state = getRevisionRangeState(model, from, to);
91
+ if (!state.hasChanges) {
92
+ return "";
93
+ }
94
+
95
+ const activeRing = state.hasActive ? " ring-1 ring-accent/30" : "";
96
+
97
+ switch (markupDisplay) {
98
+ case "clean":
99
+ // In clean mode, deletions are hidden entirely (caller should not render).
100
+ // Insertions render as normal text with no decoration.
101
+ return "";
102
+ case "simple":
103
+ if (state.hasInsertions) {
104
+ return `underline decoration-insert/40 decoration-1 underline-offset-2${activeRing}`;
105
+ }
106
+ if (state.hasDeletions) {
107
+ return `text-secondary line-through decoration-1${activeRing}`;
108
+ }
109
+ return activeRing;
110
+ case "all":
111
+ if (state.hasInsertions) {
112
+ return `text-insert bg-insert-soft${activeRing}`;
113
+ }
114
+ if (state.hasDeletions) {
115
+ return `text-danger line-through decoration-1 bg-delete-soft${activeRing}`;
116
+ }
117
+ return activeRing;
118
+ }
119
+ }
120
+
121
+ export function shouldHideInCleanMode(
122
+ model: RevisionDecorationModel | undefined,
123
+ from: number,
124
+ to: number,
125
+ ): boolean {
126
+ const state = getRevisionRangeState(model, from, to);
127
+ return state.hasDeletions;
128
+ }
@@ -0,0 +1,34 @@
1
+ import type { SelectionSnapshot } from "../../api/public-types";
2
+
3
+ export function createSelectionSnapshot(anchor: number, head = anchor): SelectionSnapshot {
4
+ const from = Math.min(anchor, head);
5
+ const to = Math.max(anchor, head);
6
+ return {
7
+ anchor,
8
+ head,
9
+ isCollapsed: anchor === head,
10
+ activeRange: {
11
+ kind: "range",
12
+ from,
13
+ to,
14
+ assoc: {
15
+ start: -1,
16
+ end: 1,
17
+ },
18
+ },
19
+ };
20
+ }
21
+
22
+ export function selectionTouchesRange(
23
+ selection: SelectionSnapshot,
24
+ from: number,
25
+ to: number,
26
+ ): boolean {
27
+ if (selection.isCollapsed) {
28
+ return false;
29
+ }
30
+
31
+ const selectionFrom = Math.min(selection.anchor, selection.head);
32
+ const selectionTo = Math.max(selection.anchor, selection.head);
33
+ return selectionFrom < to && from < selectionTo;
34
+ }
@@ -0,0 +1,98 @@
1
+ import type { KeyboardEvent } from "react";
2
+
3
+ import type { SelectionSnapshot } from "../../api/public-types";
4
+ import { createSelectionSnapshot } from "./selection-helpers";
5
+
6
+ export interface EditorKeyboardCallbacks {
7
+ onSelectionChange?: (selection: SelectionSnapshot) => void;
8
+ onInsertText?: (text: string) => void;
9
+ onDeleteBackward?: () => void;
10
+ onDeleteForward?: () => void;
11
+ onInsertTab?: () => void;
12
+ onInsertHardBreak?: () => void;
13
+ onSplitParagraph?: () => void;
14
+ }
15
+
16
+ export interface EditorKeyboardContext {
17
+ selection: SelectionSnapshot;
18
+ storySize: number;
19
+ canEdit: boolean;
20
+ }
21
+
22
+ export function createEditorKeyboardHandler(
23
+ context: EditorKeyboardContext,
24
+ callbacks: EditorKeyboardCallbacks,
25
+ ): (event: KeyboardEvent<HTMLDivElement>) => void {
26
+ return function handleKeyDown(event: KeyboardEvent<HTMLDivElement>): void {
27
+ const { selection, storySize, canEdit } = context;
28
+
29
+ if (event.key === "ArrowLeft") {
30
+ event.preventDefault();
31
+ moveSelection(selection.anchor, selection.head - 1, event.shiftKey, storySize, callbacks);
32
+ return;
33
+ }
34
+
35
+ if (event.key === "ArrowRight") {
36
+ event.preventDefault();
37
+ moveSelection(selection.anchor, selection.head + 1, event.shiftKey, storySize, callbacks);
38
+ return;
39
+ }
40
+
41
+ if (event.key === "Home") {
42
+ event.preventDefault();
43
+ moveSelection(selection.anchor, 0, event.shiftKey, storySize, callbacks);
44
+ return;
45
+ }
46
+
47
+ if (event.key === "End") {
48
+ event.preventDefault();
49
+ moveSelection(selection.anchor, storySize, event.shiftKey, storySize, callbacks);
50
+ return;
51
+ }
52
+
53
+ if (!canEdit || event.metaKey || event.ctrlKey || event.altKey) {
54
+ return;
55
+ }
56
+
57
+ switch (event.key) {
58
+ case "Backspace":
59
+ event.preventDefault();
60
+ callbacks.onDeleteBackward?.();
61
+ return;
62
+ case "Delete":
63
+ event.preventDefault();
64
+ callbacks.onDeleteForward?.();
65
+ return;
66
+ case "Tab":
67
+ event.preventDefault();
68
+ callbacks.onInsertTab?.();
69
+ return;
70
+ case "Enter":
71
+ event.preventDefault();
72
+ if (event.shiftKey) {
73
+ callbacks.onInsertHardBreak?.();
74
+ } else {
75
+ callbacks.onSplitParagraph?.();
76
+ }
77
+ return;
78
+ default:
79
+ if (event.key.length === 1) {
80
+ event.preventDefault();
81
+ callbacks.onInsertText?.(event.key);
82
+ }
83
+ }
84
+ };
85
+ }
86
+
87
+ function moveSelection(
88
+ anchor: number,
89
+ nextHead: number,
90
+ extend: boolean,
91
+ storySize: number,
92
+ callbacks: EditorKeyboardCallbacks,
93
+ ): void {
94
+ const clampedHead = Math.max(0, Math.min(storySize, nextHead));
95
+ callbacks.onSelectionChange?.(
96
+ createSelectionSnapshot(extend ? anchor : clampedHead, clampedHead),
97
+ );
98
+ }
@@ -0,0 +1,3 @@
1
+ # Review UI
2
+
3
+ Tracked-change display modes, review navigation, and accept/reject surfaces belong here.
@@ -0,0 +1,31 @@
1
+ import type { RuntimeRenderSnapshot } from "../../api/public-types";
2
+
3
+ type Revision = RuntimeRenderSnapshot["trackedChanges"]["revisions"][number];
4
+ type MarkupDisplay = "clean" | "simple" | "all";
5
+
6
+ export function selectVisibleRevisions(
7
+ revisions: readonly Revision[],
8
+ markupDisplay: MarkupDisplay,
9
+ ): Revision[] {
10
+ switch (markupDisplay) {
11
+ case "clean":
12
+ case "simple":
13
+ return revisions.filter(
14
+ (revision) =>
15
+ revision.status === "active" && revision.actionability === "actionable",
16
+ );
17
+ case "all":
18
+ return [...revisions];
19
+ }
20
+ }
21
+
22
+ export function describeEmptyRevisionState(
23
+ markupDisplay: MarkupDisplay,
24
+ totalCount: number,
25
+ ): string {
26
+ if ((markupDisplay === "clean" || markupDisplay === "simple") && totalCount > 0) {
27
+ return "Simple markup keeps the rail focused on actionable live changes. Switch to All to inspect preserve-only or historical revision records.";
28
+ }
29
+
30
+ return "Runtime-backed change cards will appear here when tracked changes are present.";
31
+ }
@@ -0,0 +1,3 @@
1
+ # Status UI
2
+
3
+ Dirty state, autosave state, import/export progress, and validation status surfaces belong here.
@@ -0,0 +1,3 @@
1
+ # Theme
2
+
3
+ Design tokens, CSS variable mapping, and host-theme integration belong here.
@@ -0,0 +1,3 @@
1
+ # Toolbar
2
+
3
+ The compact command toolbar and contextual editor controls belong here.
@@ -0,0 +1,48 @@
1
+ import React from "react";
2
+ import { AlertTriangle, XCircle } from "lucide-react";
3
+
4
+ import type { RuntimeRenderSnapshot } from "../../api/public-types";
5
+
6
+ export interface TwAlertBannerProps {
7
+ snapshot: RuntimeRenderSnapshot;
8
+ preserveOnlyCount: number;
9
+ }
10
+
11
+ export function TwAlertBanner(props: TwAlertBannerProps) {
12
+ const { snapshot, preserveOnlyCount } = props;
13
+
14
+ if (snapshot.fatalError) {
15
+ return (
16
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-danger-soft text-danger text-xs">
17
+ <XCircle className="h-3.5 w-3.5 shrink-0" />
18
+ <span>{snapshot.fatalError.message}</span>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ if (snapshot.compatibility.blockExport) {
24
+ return (
25
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-danger-soft text-danger text-xs">
26
+ <XCircle className="h-3.5 w-3.5 shrink-0" />
27
+ <span>
28
+ Export blocked &mdash;{" "}
29
+ {snapshot.compatibility.blockExportReasons[0] ?? "unsupported content"}
30
+ </span>
31
+ </div>
32
+ );
33
+ }
34
+
35
+ if (preserveOnlyCount > 0) {
36
+ return (
37
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-warning-soft text-comment text-xs">
38
+ <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
39
+ <span>
40
+ {preserveOnlyCount} preserve-only feature
41
+ {preserveOnlyCount !== 1 ? "s" : ""} detected
42
+ </span>
43
+ </div>
44
+ );
45
+ }
46
+
47
+ return null;
48
+ }
@@ -0,0 +1,44 @@
1
+ import React from "react";
2
+ import * as Tooltip from "@radix-ui/react-tooltip";
3
+ import { MessageSquare } from "lucide-react";
4
+
5
+ export interface TwSelectionToolbarProps {
6
+ selectionPreview: string;
7
+ readOnly: boolean;
8
+ onAddComment?: () => void;
9
+ }
10
+
11
+ const focusRingClass =
12
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
13
+
14
+ export function TwSelectionToolbar(props: TwSelectionToolbarProps) {
15
+ return (
16
+ <div className="mb-6 inline-flex items-center gap-1 rounded-lg bg-canvas shadow-lg ring-1 ring-border p-1">
17
+ <Tooltip.Root>
18
+ <Tooltip.Trigger asChild>
19
+ <button
20
+ type="button"
21
+ aria-label="Comment"
22
+ disabled={props.readOnly}
23
+ onClick={props.onAddComment}
24
+ className={`inline-flex h-7 w-7 items-center justify-center rounded-md transition-colors text-accent hover:bg-accent-soft disabled:opacity-30 disabled:cursor-not-allowed ${focusRingClass}`}
25
+ >
26
+ <MessageSquare className="h-3.5 w-3.5" />
27
+ </button>
28
+ </Tooltip.Trigger>
29
+ <Tooltip.Portal>
30
+ <Tooltip.Content
31
+ className="rounded-md bg-primary px-2 py-1 text-xs text-white shadow-md z-50"
32
+ sideOffset={6}
33
+ >
34
+ Add comment
35
+ </Tooltip.Content>
36
+ </Tooltip.Portal>
37
+ </Tooltip.Root>
38
+ <div className="h-4 w-px bg-border mx-0.5" />
39
+ <span className="text-xs text-tertiary px-2 max-w-[200px] truncate">
40
+ {props.selectionPreview}
41
+ </span>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { AlertTriangle } from "lucide-react";
3
+
4
+ export interface TwUnsavedModalProps {
5
+ open: boolean;
6
+ message?: string;
7
+ onDiscard: () => void;
8
+ onCancel: () => void;
9
+ }
10
+
11
+ export function TwUnsavedModal(props: TwUnsavedModalProps) {
12
+ if (!props.open) return null;
13
+
14
+ return (
15
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
16
+ {/* Backdrop */}
17
+ <div
18
+ className="absolute inset-0 bg-black/30 backdrop-blur-sm"
19
+ onClick={props.onCancel}
20
+ />
21
+
22
+ {/* Modal */}
23
+ <div className="relative mx-4 w-full max-w-md rounded-xl bg-canvas p-6 shadow-lg ring-1 ring-border">
24
+ <div className="flex items-start gap-3">
25
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-warning-soft">
26
+ <AlertTriangle className="h-5 w-5 text-warning" />
27
+ </div>
28
+ <div className="flex-1">
29
+ <h3 className="text-base font-semibold text-primary">
30
+ Unsaved changes
31
+ </h3>
32
+ <p className="mt-1.5 text-sm text-secondary leading-relaxed">
33
+ {props.message ??
34
+ "You have unsaved changes that will be lost. Your work is being autosaved, but the latest edits may not be saved yet."}
35
+ </p>
36
+ </div>
37
+ </div>
38
+
39
+ <div className="mt-5 flex justify-end gap-2">
40
+ <button
41
+ type="button"
42
+ onClick={props.onCancel}
43
+ className="rounded-lg px-4 py-2 text-sm font-medium text-secondary hover:bg-surface transition-colors"
44
+ >
45
+ Keep editing
46
+ </button>
47
+ <button
48
+ type="button"
49
+ onClick={props.onDiscard}
50
+ className="rounded-lg bg-danger px-4 py-2 text-sm font-medium text-white hover:bg-danger/90 transition-colors"
51
+ >
52
+ Discard changes
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect } from "react";
2
+
3
+ /**
4
+ * Prompts the browser's native "unsaved changes" dialog when the user
5
+ * tries to close the tab or navigate away while `shouldWarn` is true.
6
+ */
7
+ export function useBeforeUnload(shouldWarn: boolean): void {
8
+ useEffect(() => {
9
+ if (!shouldWarn) return;
10
+
11
+ function handleBeforeUnload(event: BeforeUnloadEvent) {
12
+ event.preventDefault();
13
+ // Modern browsers ignore custom messages and show their own.
14
+ event.returnValue = "";
15
+ }
16
+
17
+ window.addEventListener("beforeunload", handleBeforeUnload);
18
+ return () => window.removeEventListener("beforeunload", handleBeforeUnload);
19
+ }, [shouldWarn]);
20
+ }