@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,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,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,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 —{" "}
|
|
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
|
+
}
|