@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.
Files changed (172) hide show
  1. package/README.md +44 -104
  2. package/package.json +50 -30
  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 +325 -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 +1506 -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,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
+ }