@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.
- package/README.md +44 -104
- package/package.json +76 -46
- 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 +320 -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 +1504 -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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProseMirror NodeView implementations for table nodes.
|
|
3
|
+
*
|
|
4
|
+
* These NodeViews render table structure as proper HTML tables with
|
|
5
|
+
* colspan/rowspan support for merged cells (from gridSpan/verticalMerge attrs).
|
|
6
|
+
*
|
|
7
|
+
* Usage with EditorView:
|
|
8
|
+
* new EditorView(mount, {
|
|
9
|
+
* nodeViews: tableNodeViews,
|
|
10
|
+
* ...
|
|
11
|
+
* })
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
15
|
+
|
|
16
|
+
function resolveRenderedColspan(node: PMNode): number {
|
|
17
|
+
const colspan = node.attrs.colspan as number | undefined;
|
|
18
|
+
if (typeof colspan === "number" && colspan > 1) {
|
|
19
|
+
return colspan;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const gridSpan = node.attrs.gridSpan as number | undefined;
|
|
23
|
+
if (typeof gridSpan === "number" && gridSpan > 1) {
|
|
24
|
+
return gridSpan;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* NodeView for the table node.
|
|
32
|
+
* Renders as <table><tbody>...</tbody></table>.
|
|
33
|
+
* ProseMirror places row content into the tbody via contentDOM.
|
|
34
|
+
*/
|
|
35
|
+
export class TableNodeView {
|
|
36
|
+
dom: HTMLElement;
|
|
37
|
+
contentDOM: HTMLElement;
|
|
38
|
+
|
|
39
|
+
constructor(_node: PMNode) {
|
|
40
|
+
const table = document.createElement("table");
|
|
41
|
+
table.className = "border-collapse w-full my-2 text-sm";
|
|
42
|
+
|
|
43
|
+
const tbody = document.createElement("tbody");
|
|
44
|
+
table.appendChild(tbody);
|
|
45
|
+
|
|
46
|
+
this.dom = table;
|
|
47
|
+
this.contentDOM = tbody;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* NodeView for table_row nodes.
|
|
53
|
+
* Renders as <tr>...</tr>.
|
|
54
|
+
*/
|
|
55
|
+
export class TableRowNodeView {
|
|
56
|
+
dom: HTMLElement;
|
|
57
|
+
contentDOM: HTMLElement;
|
|
58
|
+
|
|
59
|
+
constructor(_node: PMNode) {
|
|
60
|
+
const tr = document.createElement("tr");
|
|
61
|
+
this.dom = tr;
|
|
62
|
+
this.contentDOM = tr;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* NodeView for table_cell and table_header_cell nodes.
|
|
68
|
+
*
|
|
69
|
+
* Applies colspan/rowspan from node attrs (mapped from gridSpan/verticalMerge
|
|
70
|
+
* in the OOXML model). Distinguishes header cells by tableRole spec attribute.
|
|
71
|
+
*/
|
|
72
|
+
export class TableCellNodeView {
|
|
73
|
+
dom: HTMLElement;
|
|
74
|
+
contentDOM: HTMLElement;
|
|
75
|
+
|
|
76
|
+
constructor(node: PMNode) {
|
|
77
|
+
const isHeader = (node.type.spec as { tableRole?: string }).tableRole === "header_cell";
|
|
78
|
+
const cell = document.createElement(isHeader ? "th" : "td");
|
|
79
|
+
|
|
80
|
+
cell.className = isHeader
|
|
81
|
+
? "border border-primary/20 p-2 align-top font-semibold bg-surface-raised"
|
|
82
|
+
: "border border-primary/20 p-2 align-top";
|
|
83
|
+
|
|
84
|
+
const colspan = resolveRenderedColspan(node);
|
|
85
|
+
const rowspan = node.attrs.rowspan as number;
|
|
86
|
+
if (colspan > 1) (cell as HTMLTableCellElement).colSpan = colspan;
|
|
87
|
+
if (rowspan > 1) (cell as HTMLTableCellElement).rowSpan = rowspan;
|
|
88
|
+
|
|
89
|
+
this.dom = cell;
|
|
90
|
+
this.contentDOM = cell;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update the DOM when the node's attrs change (e.g., after a merge/split operation).
|
|
95
|
+
* Return false to let ProseMirror rebuild the node view from scratch.
|
|
96
|
+
*/
|
|
97
|
+
update(node: PMNode): boolean {
|
|
98
|
+
const isHeader = (node.type.spec as { tableRole?: string }).tableRole === "header_cell";
|
|
99
|
+
const expectedTag = isHeader ? "TH" : "TD";
|
|
100
|
+
if (this.dom.tagName !== expectedTag) return false;
|
|
101
|
+
|
|
102
|
+
const colspan = resolveRenderedColspan(node);
|
|
103
|
+
const rowspan = node.attrs.rowspan as number;
|
|
104
|
+
const cell = this.dom as HTMLTableCellElement;
|
|
105
|
+
cell.colSpan = colspan > 1 ? colspan : 1;
|
|
106
|
+
cell.rowSpan = rowspan > 1 ? rowspan : 1;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* NodeView factory map for use with EditorView.nodeViews.
|
|
113
|
+
*
|
|
114
|
+
* Pass this object directly to the EditorView constructor options:
|
|
115
|
+
* new EditorView(mount, { nodeViews: tableNodeViews, ... })
|
|
116
|
+
*/
|
|
117
|
+
export const tableNodeViews = {
|
|
118
|
+
table: (node: PMNode) => new TableNodeView(node),
|
|
119
|
+
table_row: (node: PMNode) => new TableRowNodeView(node),
|
|
120
|
+
table_cell: (node: PMNode) => new TableCellNodeView(node),
|
|
121
|
+
table_header_cell: (node: PMNode) => new TableCellNodeView(node),
|
|
122
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Workspace shell
|
|
2
|
+
export { TwReviewWorkspace, type TwReviewWorkspaceProps } from "./tw-review-workspace";
|
|
3
|
+
|
|
4
|
+
// Editor surface
|
|
5
|
+
export { TwEditorSurface, type TwEditorSurfaceProps } from "./editor-surface/tw-editor-surface";
|
|
6
|
+
export { TwParagraphBlock } from "./editor-surface/tw-paragraph-block";
|
|
7
|
+
export { TwOpaqueBlock } from "./editor-surface/tw-opaque-block";
|
|
8
|
+
export { TwSegmentView } from "./editor-surface/tw-segment-view";
|
|
9
|
+
export { TwInlineToken } from "./editor-surface/tw-inline-token";
|
|
10
|
+
export { renderTwCaret } from "./editor-surface/tw-caret";
|
|
11
|
+
|
|
12
|
+
// Review rail
|
|
13
|
+
export { TwReviewRail, type TwReviewRailProps, type ReviewRailTab } from "./review/tw-review-rail";
|
|
14
|
+
export { TwCommentSidebar } from "./review/tw-comment-sidebar";
|
|
15
|
+
export { TwRevisionSidebar } from "./review/tw-revision-sidebar";
|
|
16
|
+
export { TwHealthPanel } from "./review/tw-health-panel";
|
|
17
|
+
|
|
18
|
+
// Toolbar
|
|
19
|
+
export { TwToolbar, type TwToolbarProps, type ViewMode } from "./toolbar/tw-toolbar";
|
|
20
|
+
export { TwToolbarIconButton } from "./toolbar/tw-toolbar-icon-button";
|
|
21
|
+
|
|
22
|
+
// Status
|
|
23
|
+
export { TwStatusBar } from "./status/tw-status-bar";
|
|
24
|
+
|
|
25
|
+
// Chrome
|
|
26
|
+
export { TwAlertBanner } from "./chrome/tw-alert-banner";
|
|
27
|
+
export { TwSelectionToolbar } from "./chrome/tw-selection-toolbar";
|
|
28
|
+
|
|
29
|
+
// Session capabilities
|
|
30
|
+
export {
|
|
31
|
+
deriveCapabilities,
|
|
32
|
+
type SessionCapabilities,
|
|
33
|
+
} from "../runtime/session-capabilities";
|
|
34
|
+
|
|
35
|
+
// Headless (re-export for convenience)
|
|
36
|
+
export {
|
|
37
|
+
createCommentDecorationModel,
|
|
38
|
+
getCommentHighlightClass,
|
|
39
|
+
getCommentRangeState,
|
|
40
|
+
type CommentDecorationModel,
|
|
41
|
+
type MarkupDisplay,
|
|
42
|
+
} from "../ui/headless/comment-decoration-model";
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
createRevisionDecorationModel,
|
|
46
|
+
getRevisionHighlightClass,
|
|
47
|
+
getRevisionRangeState,
|
|
48
|
+
shouldHideInCleanMode,
|
|
49
|
+
type RevisionDecorationModel,
|
|
50
|
+
} from "../ui/headless/revision-decoration-model";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
createEditorKeyboardHandler,
|
|
54
|
+
type EditorKeyboardCallbacks,
|
|
55
|
+
type EditorKeyboardContext,
|
|
56
|
+
} from "../ui/headless/use-editor-keyboard";
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
createSelectionSnapshot,
|
|
60
|
+
selectionTouchesRange,
|
|
61
|
+
} from "../ui/headless/selection-helpers";
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import { Check, MessageSquarePlus } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import type { CommentSidebarSnapshot, CommentSidebarThreadSnapshot } from "../../api/public-types";
|
|
5
|
+
|
|
6
|
+
export interface TwCommentSidebarProps {
|
|
7
|
+
comments: CommentSidebarSnapshot;
|
|
8
|
+
activeCommentId?: string;
|
|
9
|
+
currentUserId?: string;
|
|
10
|
+
onOpenComment?: (thread: CommentSidebarThreadSnapshot) => void;
|
|
11
|
+
onResolveComment?: (commentId: string) => void;
|
|
12
|
+
onReopenComment?: (commentId: string) => void;
|
|
13
|
+
onAddReply?: (commentId: string, body: string) => void;
|
|
14
|
+
onEditBody?: (commentId: string, body: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const focusRingClass =
|
|
18
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas";
|
|
19
|
+
|
|
20
|
+
export function TwCommentSidebar(props: TwCommentSidebarProps) {
|
|
21
|
+
const { comments, activeCommentId, currentUserId } = props;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="outline-none">
|
|
25
|
+
<p className="text-xs text-tertiary mb-3">
|
|
26
|
+
{comments.openCommentIds.length} open · {comments.resolvedCommentIds.length} resolved · {comments.detachedCommentIds.length} detached
|
|
27
|
+
</p>
|
|
28
|
+
{comments.threads.length > 0 ? (
|
|
29
|
+
<div className="space-y-1">
|
|
30
|
+
{comments.threads.map((thread) => {
|
|
31
|
+
const isActive = activeCommentId === thread.commentId;
|
|
32
|
+
const leadEntry = thread.entries[0];
|
|
33
|
+
const isOwnComment = currentUserId != null && leadEntry?.authorId === currentUserId;
|
|
34
|
+
const canEdit = isOwnComment && thread.status === "open" && props.onEditBody != null;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
key={thread.commentId}
|
|
39
|
+
role="button"
|
|
40
|
+
tabIndex={0}
|
|
41
|
+
className={`rounded-lg p-2.5 transition-colors cursor-pointer ${focusRingClass} ${isActive ? "bg-accent-soft" : "hover:bg-surface"}`}
|
|
42
|
+
onClick={() => props.onOpenComment?.(thread)}
|
|
43
|
+
onKeyDown={(event) => {
|
|
44
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
props.onOpenComment?.(thread);
|
|
47
|
+
}
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<div className="flex items-start justify-between gap-2 mb-1">
|
|
51
|
+
<span className="text-sm font-medium text-primary">{thread.createdBy}</span>
|
|
52
|
+
<StatusBadge status={thread.status} />
|
|
53
|
+
</div>
|
|
54
|
+
<p className="text-xs text-tertiary mb-1">{thread.createdAt}</p>
|
|
55
|
+
<p className="text-xs font-medium text-comment bg-comment-soft rounded px-1 py-0.5 inline-block mb-1.5">
|
|
56
|
+
{thread.excerpt}
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
{/* Comment body — inline editable for own comments on open threads */}
|
|
60
|
+
{leadEntry?.body ? (
|
|
61
|
+
canEdit ? (
|
|
62
|
+
<InlineEditableBody
|
|
63
|
+
commentId={thread.commentId}
|
|
64
|
+
body={leadEntry.body}
|
|
65
|
+
onSave={(newBody) => props.onEditBody?.(thread.commentId, newBody)}
|
|
66
|
+
/>
|
|
67
|
+
) : (
|
|
68
|
+
<p className="text-sm text-secondary leading-relaxed">{leadEntry.body}</p>
|
|
69
|
+
)
|
|
70
|
+
) : null}
|
|
71
|
+
|
|
72
|
+
{/* Show reply entries */}
|
|
73
|
+
{thread.entries.slice(1).map((entry) => (
|
|
74
|
+
<div key={entry.entryId} className="mt-2 pl-2 border-l border-border">
|
|
75
|
+
<p className="text-xs text-tertiary">{entry.authorId} · {entry.createdAt}</p>
|
|
76
|
+
<p className="text-sm text-secondary leading-relaxed">{entry.body}</p>
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
|
|
80
|
+
{thread.entryCount > thread.entries.length ? (
|
|
81
|
+
<p className="text-xs text-tertiary mt-1.5">
|
|
82
|
+
+{thread.entryCount - thread.entries.length} more repl{thread.entryCount - thread.entries.length === 1 ? "y" : "ies"}
|
|
83
|
+
</p>
|
|
84
|
+
) : null}
|
|
85
|
+
|
|
86
|
+
{thread.resolvedAt && thread.resolvedBy ? (
|
|
87
|
+
<p className="text-xs text-tertiary mt-1">
|
|
88
|
+
Resolved by {thread.resolvedBy} at {thread.resolvedAt}
|
|
89
|
+
</p>
|
|
90
|
+
) : null}
|
|
91
|
+
|
|
92
|
+
<div className="flex gap-1.5 mt-2">
|
|
93
|
+
{thread.status === "open" ? (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-insert hover:bg-insert-soft transition-colors"
|
|
97
|
+
onClick={(e) => {
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
props.onResolveComment?.(thread.commentId);
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<Check className="h-3 w-3" /> Resolve
|
|
103
|
+
</button>
|
|
104
|
+
) : thread.status === "resolved" ? (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-secondary hover:bg-surface transition-colors"
|
|
108
|
+
onClick={(e) => {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
props.onReopenComment?.(thread.commentId);
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
Reopen
|
|
114
|
+
</button>
|
|
115
|
+
) : (
|
|
116
|
+
<span className="text-xs text-tertiary px-2 py-1">Detached</span>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Reply input — only for open threads */}
|
|
121
|
+
{thread.status === "open" && props.onAddReply ? (
|
|
122
|
+
<ReplyInput commentId={thread.commentId} onAddReply={props.onAddReply} />
|
|
123
|
+
) : null}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
) : (
|
|
129
|
+
<p className="text-xs text-tertiary py-4">
|
|
130
|
+
Comment threads will appear here when the runtime loads them.
|
|
131
|
+
</p>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function InlineEditableBody(props: {
|
|
138
|
+
commentId: string;
|
|
139
|
+
body: string;
|
|
140
|
+
onSave: (newBody: string) => void;
|
|
141
|
+
}) {
|
|
142
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
143
|
+
const [draft, setDraft] = useState(props.body);
|
|
144
|
+
|
|
145
|
+
if (!isEditing) {
|
|
146
|
+
return (
|
|
147
|
+
<p
|
|
148
|
+
className="text-sm text-secondary leading-relaxed cursor-text rounded px-1 -mx-1 hover:bg-surface transition-colors"
|
|
149
|
+
onClick={(e) => {
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
setDraft(props.body);
|
|
152
|
+
setIsEditing(true);
|
|
153
|
+
}}
|
|
154
|
+
title="Click to edit"
|
|
155
|
+
>
|
|
156
|
+
{props.body}
|
|
157
|
+
</p>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<textarea
|
|
163
|
+
className="w-full text-sm text-primary leading-relaxed bg-surface rounded-md border border-border px-2 py-1.5 resize-none focus:outline-none focus:ring-1 focus:ring-accent"
|
|
164
|
+
rows={Math.max(2, props.body.split("\n").length)}
|
|
165
|
+
value={draft}
|
|
166
|
+
autoFocus
|
|
167
|
+
onClick={(e) => e.stopPropagation()}
|
|
168
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
169
|
+
onBlur={() => {
|
|
170
|
+
if (draft.trim() && draft.trim() !== props.body) {
|
|
171
|
+
props.onSave(draft.trim());
|
|
172
|
+
}
|
|
173
|
+
setIsEditing(false);
|
|
174
|
+
}}
|
|
175
|
+
onKeyDown={(e) => {
|
|
176
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
if (draft.trim() && draft.trim() !== props.body) {
|
|
179
|
+
props.onSave(draft.trim());
|
|
180
|
+
}
|
|
181
|
+
setIsEditing(false);
|
|
182
|
+
}
|
|
183
|
+
if (e.key === "Escape") {
|
|
184
|
+
setDraft(props.body);
|
|
185
|
+
setIsEditing(false);
|
|
186
|
+
}
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ReplyInput(props: { commentId: string; onAddReply: (commentId: string, body: string) => void }) {
|
|
194
|
+
const [body, setBody] = useState("");
|
|
195
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
196
|
+
|
|
197
|
+
if (!isOpen) {
|
|
198
|
+
return (
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
className="inline-flex items-center gap-1 text-xs text-tertiary hover:text-secondary transition-colors mt-1"
|
|
202
|
+
onClick={(e) => {
|
|
203
|
+
e.stopPropagation();
|
|
204
|
+
setIsOpen(true);
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<MessageSquarePlus className="h-3 w-3" /> Reply
|
|
208
|
+
</button>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className="mt-2" onClick={(e) => e.stopPropagation()}>
|
|
214
|
+
<textarea
|
|
215
|
+
className="w-full rounded-md border border-border bg-surface px-2 py-1.5 text-xs text-primary placeholder:text-tertiary resize-none focus:outline-none focus:ring-1 focus:ring-accent"
|
|
216
|
+
rows={2}
|
|
217
|
+
placeholder="Write a reply..."
|
|
218
|
+
value={body}
|
|
219
|
+
onChange={(e) => setBody(e.target.value)}
|
|
220
|
+
onKeyDown={(e) => {
|
|
221
|
+
if (e.key === "Enter" && !e.shiftKey && body.trim()) {
|
|
222
|
+
e.preventDefault();
|
|
223
|
+
props.onAddReply(props.commentId, body.trim());
|
|
224
|
+
setBody("");
|
|
225
|
+
setIsOpen(false);
|
|
226
|
+
}
|
|
227
|
+
if (e.key === "Escape") {
|
|
228
|
+
setBody("");
|
|
229
|
+
setIsOpen(false);
|
|
230
|
+
}
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
}}
|
|
233
|
+
autoFocus
|
|
234
|
+
/>
|
|
235
|
+
<div className="flex gap-1.5 mt-1">
|
|
236
|
+
<button
|
|
237
|
+
type="button"
|
|
238
|
+
disabled={!body.trim()}
|
|
239
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-accent hover:bg-accent-soft transition-colors disabled:opacity-40"
|
|
240
|
+
onClick={() => {
|
|
241
|
+
if (body.trim()) {
|
|
242
|
+
props.onAddReply(props.commentId, body.trim());
|
|
243
|
+
setBody("");
|
|
244
|
+
setIsOpen(false);
|
|
245
|
+
}
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
Reply
|
|
249
|
+
</button>
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-tertiary hover:bg-surface transition-colors"
|
|
253
|
+
onClick={() => {
|
|
254
|
+
setBody("");
|
|
255
|
+
setIsOpen(false);
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
Cancel
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function StatusBadge(props: { status: string }) {
|
|
266
|
+
const styles: Record<string, string> = {
|
|
267
|
+
open: "text-accent bg-accent-soft",
|
|
268
|
+
resolved: "text-insert bg-insert-soft",
|
|
269
|
+
detached: "text-comment bg-warning-soft",
|
|
270
|
+
};
|
|
271
|
+
return (
|
|
272
|
+
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${styles[props.status] ?? "text-secondary bg-subtle"}`}>
|
|
273
|
+
{props.status}
|
|
274
|
+
</span>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { AlertTriangle, Info, Shield, ShieldAlert, ShieldCheck } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CompatibilityFeatureEntry,
|
|
6
|
+
CompatibilityPanelSnapshot,
|
|
7
|
+
EditorWarning,
|
|
8
|
+
} from "../../api/public-types";
|
|
9
|
+
|
|
10
|
+
export interface TwHealthPanelProps {
|
|
11
|
+
compatibility: CompatibilityPanelSnapshot;
|
|
12
|
+
warnings: EditorWarning[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function TwHealthPanel(props: TwHealthPanelProps) {
|
|
16
|
+
const { compatibility, warnings } = props;
|
|
17
|
+
const supportedCount = compatibility.featureEntries.filter(
|
|
18
|
+
(e) => e.featureClass === "supported-roundtrip",
|
|
19
|
+
).length;
|
|
20
|
+
const preserveOnlyCount = compatibility.featureEntries.filter(
|
|
21
|
+
(e) => e.featureClass === "preserve-only",
|
|
22
|
+
).length;
|
|
23
|
+
const blockedCount = compatibility.featureEntries.filter(
|
|
24
|
+
(e) => e.featureClass === "unsupported-fatal",
|
|
25
|
+
).length;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="outline-none">
|
|
29
|
+
<p className="text-xs text-tertiary mb-3">
|
|
30
|
+
{supportedCount} supported · {preserveOnlyCount} preserve-only · {blockedCount} blocked
|
|
31
|
+
{warnings.length > 0 ? ` · ${warnings.length} warning${warnings.length !== 1 ? "s" : ""}` : ""}
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<div className="space-y-1">
|
|
35
|
+
{compatibility.featureEntries.map((entry) => (
|
|
36
|
+
<div key={entry.featureEntryId} className="flex rounded-lg transition-colors hover:bg-surface">
|
|
37
|
+
{entry.featureClass !== "supported-roundtrip" ? (
|
|
38
|
+
<div className={`w-0.5 shrink-0 rounded-l-lg ${
|
|
39
|
+
entry.featureClass === "unsupported-fatal" ? "bg-danger" : "bg-comment"
|
|
40
|
+
}`} />
|
|
41
|
+
) : null}
|
|
42
|
+
<div className="flex items-start gap-2 p-2.5 flex-1">
|
|
43
|
+
<HealthIcon featureClass={entry.featureClass} />
|
|
44
|
+
<div className="flex-1 min-w-0">
|
|
45
|
+
<div className="flex items-start justify-between gap-2">
|
|
46
|
+
<span className="text-sm font-medium text-primary">{entry.message}</span>
|
|
47
|
+
<FeatureClassBadge featureClass={entry.featureClass} />
|
|
48
|
+
</div>
|
|
49
|
+
<p className="text-xs text-tertiary mt-0.5">{entry.featureKey}</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
|
|
55
|
+
{warnings.map((warning) => (
|
|
56
|
+
<div key={warning.warningId} className="flex rounded-lg transition-colors hover:bg-surface">
|
|
57
|
+
<div className={`w-0.5 shrink-0 rounded-l-lg ${
|
|
58
|
+
warning.severity === "warning" ? "bg-comment" : "bg-accent"
|
|
59
|
+
}`} />
|
|
60
|
+
<div className="flex items-start gap-2 p-2.5 flex-1">
|
|
61
|
+
{warning.severity === "warning" ? (
|
|
62
|
+
<AlertTriangle className="h-4 w-4 text-comment shrink-0 mt-0.5" />
|
|
63
|
+
) : (
|
|
64
|
+
<Info className="h-4 w-4 text-accent shrink-0 mt-0.5" />
|
|
65
|
+
)}
|
|
66
|
+
<div className="flex-1 min-w-0">
|
|
67
|
+
<div className="flex items-start justify-between gap-2">
|
|
68
|
+
<span className="text-sm font-medium text-primary">{warning.message}</span>
|
|
69
|
+
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${
|
|
70
|
+
warning.severity === "warning"
|
|
71
|
+
? "text-comment bg-warning-soft"
|
|
72
|
+
: "text-accent bg-accent-soft"
|
|
73
|
+
}`}>
|
|
74
|
+
{warning.code.replace(/_/g, " ")}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
<p className="text-xs text-tertiary mt-0.5">{warning.source}</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
82
|
+
|
|
83
|
+
{compatibility.featureEntries.length === 0 && warnings.length === 0 ? (
|
|
84
|
+
<p className="text-xs text-tertiary py-4">
|
|
85
|
+
No compatibility entries or warnings to display.
|
|
86
|
+
</p>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function HealthIcon(props: { featureClass: CompatibilityFeatureEntry["featureClass"] }) {
|
|
94
|
+
switch (props.featureClass) {
|
|
95
|
+
case "supported-roundtrip":
|
|
96
|
+
return <ShieldCheck className="h-4 w-4 text-insert shrink-0 mt-0.5" />;
|
|
97
|
+
case "preserve-only":
|
|
98
|
+
return <Shield className="h-4 w-4 text-comment shrink-0 mt-0.5" />;
|
|
99
|
+
case "unsupported-fatal":
|
|
100
|
+
return <ShieldAlert className="h-4 w-4 text-danger shrink-0 mt-0.5" />;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function FeatureClassBadge(props: { featureClass: CompatibilityFeatureEntry["featureClass"] }) {
|
|
105
|
+
const styles: Record<string, string> = {
|
|
106
|
+
"supported-roundtrip": "text-insert bg-insert-soft",
|
|
107
|
+
"preserve-only": "text-comment bg-warning-soft",
|
|
108
|
+
"unsupported-fatal": "text-danger bg-delete-soft",
|
|
109
|
+
};
|
|
110
|
+
const labels: Record<string, string> = {
|
|
111
|
+
"supported-roundtrip": "supported",
|
|
112
|
+
"preserve-only": "preserve-only",
|
|
113
|
+
"unsupported-fatal": "blocked",
|
|
114
|
+
};
|
|
115
|
+
return (
|
|
116
|
+
<span className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium ${styles[props.featureClass]}`}>
|
|
117
|
+
{labels[props.featureClass]}
|
|
118
|
+
</span>
|
|
119
|
+
);
|
|
120
|
+
}
|