documentation-hub 5.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,723 @@
1
+ /**
2
+ * DocumentEditor - ContentEditable-based document editor
3
+ *
4
+ * Renders document paragraphs and tables as editable elements,
5
+ * syncing changes back to docxmlater Document objects.
6
+ *
7
+ * Features:
8
+ * - Paragraph editing with contentEditable
9
+ * - Run formatting preservation
10
+ * - Text selection tracking
11
+ * - Table rendering with TableEditor integration
12
+ * - Undo/redo support via action callbacks
13
+ */
14
+
15
+ import {
16
+ useState,
17
+ useCallback,
18
+ useRef,
19
+ useEffect,
20
+ useMemo,
21
+ forwardRef,
22
+ useImperativeHandle,
23
+ } from 'react';
24
+ import { cn } from '@/utils/cn';
25
+ import type {
26
+ EditorSelection,
27
+ CellSelection,
28
+ EditorAction,
29
+ QuickActionId,
30
+ } from '@/types/editor';
31
+
32
+ // Types for document structure (matching docxmlater)
33
+ interface RunData {
34
+ text: string;
35
+ bold?: boolean;
36
+ italic?: boolean;
37
+ underline?: boolean | string;
38
+ strike?: boolean;
39
+ color?: string;
40
+ font?: string;
41
+ size?: number;
42
+ highlight?: string;
43
+ }
44
+
45
+ interface ParagraphData {
46
+ text: string;
47
+ runs: RunData[];
48
+ alignment?: 'left' | 'center' | 'right' | 'justify';
49
+ style?: string;
50
+ isHeading?: boolean;
51
+ headingLevel?: number;
52
+ }
53
+
54
+ interface TableCellData {
55
+ paragraphs: ParagraphData[];
56
+ shading?: string;
57
+ verticalMerge?: 'restart' | 'continue';
58
+ columnSpan?: number;
59
+ }
60
+
61
+ interface TableRowData {
62
+ cells: TableCellData[];
63
+ }
64
+
65
+ interface TableData {
66
+ rows: TableRowData[];
67
+ }
68
+
69
+ interface BodyElement {
70
+ type: 'paragraph' | 'table';
71
+ paragraph?: ParagraphData;
72
+ table?: TableData;
73
+ }
74
+
75
+ export interface DocumentEditorProps {
76
+ /** Document body elements (paragraphs and tables) */
77
+ bodyElements: BodyElement[];
78
+ /** Callback when content changes */
79
+ onChange: (elements: BodyElement[], action: EditorAction) => void;
80
+ /** Callback when selection changes */
81
+ onSelectionChange: (selection: EditorSelection | null) => void;
82
+ /** Callback when table cell is selected */
83
+ onTableSelectionChange: (selection: CellSelection | null, tableIndex: number | null) => void;
84
+ /** Current text selection */
85
+ selection: EditorSelection | null;
86
+ /** Current table selection */
87
+ tableSelection: CellSelection | null;
88
+ /** Currently selected table index */
89
+ selectedTableIndex: number | null;
90
+ /** Quick action to apply */
91
+ activeQuickAction: QuickActionId | null;
92
+ /** Table shading settings */
93
+ tableShadingSettings?: {
94
+ header2Shading: string;
95
+ otherShading: string;
96
+ };
97
+ /** Whether editor is read-only */
98
+ readOnly?: boolean;
99
+ }
100
+
101
+ export interface DocumentEditorRef {
102
+ /** Apply formatting to current selection */
103
+ applyFormatting: (formatting: Partial<RunData>) => void;
104
+ /** Apply paragraph style */
105
+ applyParagraphStyle: (style: string) => void;
106
+ /** Get current selection text */
107
+ getSelectionText: () => string;
108
+ /** Focus the editor */
109
+ focus: () => void;
110
+ }
111
+
112
+ /**
113
+ * Get CSS styles for a run
114
+ */
115
+ function getRunStyles(run: RunData): React.CSSProperties {
116
+ const styles: React.CSSProperties = {};
117
+
118
+ if (run.bold) styles.fontWeight = 'bold';
119
+ if (run.italic) styles.fontStyle = 'italic';
120
+ if (run.underline) styles.textDecoration = 'underline';
121
+ if (run.strike) {
122
+ styles.textDecoration = styles.textDecoration
123
+ ? `${styles.textDecoration} line-through`
124
+ : 'line-through';
125
+ }
126
+ if (run.color) styles.color = `#${run.color}`;
127
+ if (run.font) styles.fontFamily = run.font;
128
+ if (run.size) styles.fontSize = `${run.size}pt`;
129
+ if (run.highlight) {
130
+ const highlightColors: Record<string, string> = {
131
+ yellow: '#FFFF00',
132
+ green: '#00FF00',
133
+ cyan: '#00FFFF',
134
+ magenta: '#FF00FF',
135
+ blue: '#0000FF',
136
+ red: '#FF0000',
137
+ darkBlue: '#000080',
138
+ darkCyan: '#008080',
139
+ darkGreen: '#008000',
140
+ darkMagenta: '#800080',
141
+ darkRed: '#800000',
142
+ darkYellow: '#808000',
143
+ darkGray: '#808080',
144
+ lightGray: '#C0C0C0',
145
+ black: '#000000',
146
+ };
147
+ styles.backgroundColor = highlightColors[run.highlight] || run.highlight;
148
+ }
149
+
150
+ return styles;
151
+ }
152
+
153
+ /**
154
+ * Get CSS classes for paragraph alignment
155
+ */
156
+ function getParagraphAlignmentClass(alignment?: string): string {
157
+ switch (alignment) {
158
+ case 'center':
159
+ return 'text-center';
160
+ case 'right':
161
+ return 'text-right';
162
+ case 'justify':
163
+ return 'text-justify';
164
+ default:
165
+ return 'text-left';
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Editable paragraph component
171
+ */
172
+ function EditableParagraph({
173
+ paragraph,
174
+ paragraphIndex,
175
+ isSelected,
176
+ onSelect,
177
+ onChange,
178
+ onKeyDown,
179
+ readOnly,
180
+ }: {
181
+ paragraph: ParagraphData;
182
+ paragraphIndex: number;
183
+ isSelected: boolean;
184
+ onSelect: () => void;
185
+ onChange: (text: string) => void;
186
+ onKeyDown: (e: React.KeyboardEvent) => void;
187
+ readOnly?: boolean;
188
+ }) {
189
+ const ref = useRef<HTMLDivElement>(null);
190
+ const [localText, setLocalText] = useState(paragraph.text);
191
+
192
+ // Sync local text with paragraph prop
193
+ useEffect(() => {
194
+ setLocalText(paragraph.text);
195
+ }, [paragraph.text]);
196
+
197
+ // Handle blur - save changes
198
+ const handleBlur = useCallback(() => {
199
+ if (localText !== paragraph.text) {
200
+ onChange(localText);
201
+ }
202
+ }, [localText, paragraph.text, onChange]);
203
+
204
+ // Handle input
205
+ const handleInput = useCallback((e: React.FormEvent<HTMLDivElement>) => {
206
+ const newText = e.currentTarget.textContent || '';
207
+ setLocalText(newText);
208
+ }, []);
209
+
210
+ // Determine heading styles
211
+ const headingStyles = useMemo(() => {
212
+ if (paragraph.style?.includes('Heading1') || paragraph.headingLevel === 1) {
213
+ return 'text-2xl font-bold';
214
+ }
215
+ if (paragraph.style?.includes('Heading2') || paragraph.headingLevel === 2) {
216
+ return 'text-xl font-semibold';
217
+ }
218
+ if (paragraph.style?.includes('Heading3') || paragraph.headingLevel === 3) {
219
+ return 'text-lg font-medium';
220
+ }
221
+ return '';
222
+ }, [paragraph.style, paragraph.headingLevel]);
223
+
224
+ return (
225
+ <div
226
+ ref={ref}
227
+ contentEditable={!readOnly}
228
+ suppressContentEditableWarning
229
+ onClick={onSelect}
230
+ onBlur={handleBlur}
231
+ onInput={handleInput}
232
+ onKeyDown={onKeyDown}
233
+ className={cn(
234
+ 'min-h-[1.5em] px-4 py-2 outline-none cursor-text transition-colors',
235
+ 'border-l-2 border-transparent',
236
+ getParagraphAlignmentClass(paragraph.alignment),
237
+ headingStyles,
238
+ isSelected && 'bg-primary/5 border-l-primary',
239
+ !isSelected && 'hover:bg-muted/30',
240
+ readOnly && 'cursor-default'
241
+ )}
242
+ data-paragraph-index={paragraphIndex}
243
+ >
244
+ {paragraph.runs && paragraph.runs.length > 0 ? (
245
+ paragraph.runs.map((run, runIndex) => (
246
+ <span
247
+ key={runIndex}
248
+ style={getRunStyles(run)}
249
+ data-run-index={runIndex}
250
+ >
251
+ {run.text}
252
+ </span>
253
+ ))
254
+ ) : (
255
+ <span>{paragraph.text || '\u00A0'}</span>
256
+ )}
257
+ </div>
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Editable table cell component
263
+ */
264
+ function EditableTableCell({
265
+ cell,
266
+ rowIndex,
267
+ cellIndex,
268
+ tableIndex,
269
+ isSelected,
270
+ onSelect,
271
+ onChange,
272
+ readOnly,
273
+ }: {
274
+ cell: TableCellData;
275
+ rowIndex: number;
276
+ cellIndex: number;
277
+ tableIndex: number;
278
+ isSelected: boolean;
279
+ onSelect: () => void;
280
+ onChange: (text: string, paragraphIndex: number) => void;
281
+ readOnly?: boolean;
282
+ }) {
283
+ // Skip cells that are continued from vertical merge
284
+ if (cell.verticalMerge === 'continue') {
285
+ return null;
286
+ }
287
+
288
+ return (
289
+ <td
290
+ className={cn(
291
+ 'border border-border p-2 align-top',
292
+ isSelected && 'ring-2 ring-primary ring-inset bg-primary/5',
293
+ !isSelected && 'hover:bg-muted/30'
294
+ )}
295
+ style={{
296
+ backgroundColor: cell.shading ? `#${cell.shading}` : undefined,
297
+ }}
298
+ colSpan={cell.columnSpan || 1}
299
+ onClick={onSelect}
300
+ data-table-index={tableIndex}
301
+ data-row-index={rowIndex}
302
+ data-cell-index={cellIndex}
303
+ >
304
+ {cell.paragraphs.map((para, paraIndex) => (
305
+ <div
306
+ key={paraIndex}
307
+ contentEditable={!readOnly}
308
+ suppressContentEditableWarning
309
+ onBlur={(e) => {
310
+ const newText = e.currentTarget.textContent || '';
311
+ if (newText !== para.text) {
312
+ onChange(newText, paraIndex);
313
+ }
314
+ }}
315
+ className={cn(
316
+ 'min-h-[1.2em] outline-none',
317
+ getParagraphAlignmentClass(para.alignment)
318
+ )}
319
+ >
320
+ {para.runs && para.runs.length > 0 ? (
321
+ para.runs.map((run, runIndex) => (
322
+ <span key={runIndex} style={getRunStyles(run)}>
323
+ {run.text}
324
+ </span>
325
+ ))
326
+ ) : (
327
+ <span>{para.text || '\u00A0'}</span>
328
+ )}
329
+ </div>
330
+ ))}
331
+ </td>
332
+ );
333
+ }
334
+
335
+ /**
336
+ * Editable table component
337
+ */
338
+ function EditableTable({
339
+ table,
340
+ tableIndex,
341
+ selectedCell,
342
+ onCellSelect,
343
+ onCellChange,
344
+ readOnly,
345
+ }: {
346
+ table: TableData;
347
+ tableIndex: number;
348
+ selectedCell: { row: number; col: number } | null;
349
+ onCellSelect: (row: number, col: number) => void;
350
+ onCellChange: (row: number, col: number, paragraphIndex: number, text: string) => void;
351
+ readOnly?: boolean;
352
+ }) {
353
+ return (
354
+ <div className="my-4 overflow-x-auto">
355
+ <table className="w-full border-collapse border border-border">
356
+ <tbody>
357
+ {table.rows.map((row, rowIndex) => (
358
+ <tr key={rowIndex}>
359
+ {row.cells.map((cell, cellIndex) => (
360
+ <EditableTableCell
361
+ key={cellIndex}
362
+ cell={cell}
363
+ rowIndex={rowIndex}
364
+ cellIndex={cellIndex}
365
+ tableIndex={tableIndex}
366
+ isSelected={
367
+ selectedCell?.row === rowIndex && selectedCell?.col === cellIndex
368
+ }
369
+ onSelect={() => onCellSelect(rowIndex, cellIndex)}
370
+ onChange={(text, paraIndex) =>
371
+ onCellChange(rowIndex, cellIndex, paraIndex, text)
372
+ }
373
+ readOnly={readOnly}
374
+ />
375
+ ))}
376
+ </tr>
377
+ ))}
378
+ </tbody>
379
+ </table>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ /**
385
+ * Main DocumentEditor component
386
+ */
387
+ export const DocumentEditor = forwardRef<DocumentEditorRef, DocumentEditorProps>(
388
+ function DocumentEditor(
389
+ {
390
+ bodyElements,
391
+ onChange,
392
+ onSelectionChange,
393
+ onTableSelectionChange,
394
+ selection,
395
+ tableSelection,
396
+ selectedTableIndex,
397
+ activeQuickAction,
398
+ tableShadingSettings,
399
+ readOnly = false,
400
+ },
401
+ ref
402
+ ) {
403
+ const containerRef = useRef<HTMLDivElement>(null);
404
+ const [selectedParagraph, setSelectedParagraph] = useState<number | null>(null);
405
+ const [selectedTableCell, setSelectedTableCell] = useState<{
406
+ tableIndex: number;
407
+ row: number;
408
+ col: number;
409
+ } | null>(null);
410
+
411
+ // Expose methods via ref
412
+ useImperativeHandle(ref, () => ({
413
+ applyFormatting: (formatting: Partial<RunData>) => {
414
+ // Implementation would apply formatting to selected runs
415
+ console.log('Apply formatting:', formatting);
416
+ },
417
+ applyParagraphStyle: (style: string) => {
418
+ if (selectedParagraph !== null) {
419
+ const newElements = [...bodyElements];
420
+ const element = newElements[selectedParagraph];
421
+ if (element?.type === 'paragraph' && element.paragraph) {
422
+ element.paragraph.style = style;
423
+ onChange(newElements, {
424
+ type: 'formatting',
425
+ timestamp: new Date(),
426
+ undo: () => {},
427
+ redo: () => {},
428
+ description: `Apply ${style} style`,
429
+ });
430
+ }
431
+ }
432
+ },
433
+ getSelectionText: () => {
434
+ const sel = window.getSelection();
435
+ return sel?.toString() || '';
436
+ },
437
+ focus: () => {
438
+ containerRef.current?.focus();
439
+ },
440
+ }));
441
+
442
+ // Handle paragraph selection
443
+ const handleParagraphSelect = useCallback(
444
+ (index: number) => {
445
+ setSelectedParagraph(index);
446
+ setSelectedTableCell(null);
447
+ onSelectionChange({
448
+ paragraphIndex: index,
449
+ runStartIndex: 0,
450
+ runEndIndex: 0,
451
+ characterStart: 0,
452
+ characterEnd: 0,
453
+ });
454
+ onTableSelectionChange(null, null);
455
+ },
456
+ [onSelectionChange, onTableSelectionChange]
457
+ );
458
+
459
+ // Handle table cell selection
460
+ const handleTableCellSelect = useCallback(
461
+ (tableIndex: number, row: number, col: number) => {
462
+ setSelectedParagraph(null);
463
+ setSelectedTableCell({ tableIndex, row, col });
464
+ onSelectionChange(null);
465
+ onTableSelectionChange(
466
+ {
467
+ startRow: row,
468
+ startCol: col,
469
+ endRow: row,
470
+ endCol: col,
471
+ },
472
+ tableIndex
473
+ );
474
+ },
475
+ [onSelectionChange, onTableSelectionChange]
476
+ );
477
+
478
+ // Handle paragraph text change
479
+ const handleParagraphChange = useCallback(
480
+ (paragraphIndex: number, newText: string) => {
481
+ const newElements = [...bodyElements];
482
+ const element = newElements[paragraphIndex];
483
+
484
+ if (element?.type === 'paragraph' && element.paragraph) {
485
+ const oldText = element.paragraph.text;
486
+ element.paragraph.text = newText;
487
+
488
+ // Update first run text if runs exist
489
+ if (element.paragraph.runs && element.paragraph.runs.length > 0) {
490
+ element.paragraph.runs[0].text = newText;
491
+ }
492
+
493
+ onChange(newElements, {
494
+ type: 'text',
495
+ timestamp: new Date(),
496
+ undo: () => {
497
+ element.paragraph!.text = oldText;
498
+ if (element.paragraph!.runs?.[0]) {
499
+ element.paragraph!.runs[0].text = oldText;
500
+ }
501
+ },
502
+ redo: () => {
503
+ element.paragraph!.text = newText;
504
+ if (element.paragraph!.runs?.[0]) {
505
+ element.paragraph!.runs[0].text = newText;
506
+ }
507
+ },
508
+ description: 'Edit text',
509
+ });
510
+ }
511
+ },
512
+ [bodyElements, onChange]
513
+ );
514
+
515
+ // Handle table cell text change
516
+ const handleTableCellChange = useCallback(
517
+ (
518
+ tableElementIndex: number,
519
+ rowIndex: number,
520
+ cellIndex: number,
521
+ paragraphIndex: number,
522
+ newText: string
523
+ ) => {
524
+ const newElements = [...bodyElements];
525
+ const element = newElements[tableElementIndex];
526
+
527
+ if (element?.type === 'table' && element.table) {
528
+ const cell = element.table.rows[rowIndex]?.cells[cellIndex];
529
+ if (cell && cell.paragraphs[paragraphIndex]) {
530
+ const oldText = cell.paragraphs[paragraphIndex].text;
531
+ cell.paragraphs[paragraphIndex].text = newText;
532
+
533
+ onChange(newElements, {
534
+ type: 'text',
535
+ timestamp: new Date(),
536
+ undo: () => {
537
+ cell.paragraphs[paragraphIndex].text = oldText;
538
+ },
539
+ redo: () => {
540
+ cell.paragraphs[paragraphIndex].text = newText;
541
+ },
542
+ description: 'Edit table cell',
543
+ });
544
+ }
545
+ }
546
+ },
547
+ [bodyElements, onChange]
548
+ );
549
+
550
+ // Handle keyboard shortcuts
551
+ const handleKeyDown = useCallback(
552
+ (e: React.KeyboardEvent, paragraphIndex: number) => {
553
+ // Enter key - create new paragraph
554
+ if (e.key === 'Enter' && !e.shiftKey) {
555
+ e.preventDefault();
556
+ const newElements = [...bodyElements];
557
+ const newParagraph: BodyElement = {
558
+ type: 'paragraph',
559
+ paragraph: {
560
+ text: '',
561
+ runs: [{ text: '' }],
562
+ },
563
+ };
564
+ newElements.splice(paragraphIndex + 1, 0, newParagraph);
565
+ onChange(newElements, {
566
+ type: 'structure',
567
+ timestamp: new Date(),
568
+ undo: () => {
569
+ newElements.splice(paragraphIndex + 1, 1);
570
+ },
571
+ redo: () => {
572
+ newElements.splice(paragraphIndex + 1, 0, newParagraph);
573
+ },
574
+ description: 'Insert paragraph',
575
+ });
576
+ // Focus new paragraph after render
577
+ setTimeout(() => {
578
+ const newPara = containerRef.current?.querySelector(
579
+ `[data-paragraph-index="${paragraphIndex + 1}"]`
580
+ ) as HTMLElement;
581
+ newPara?.focus();
582
+ }, 0);
583
+ }
584
+
585
+ // Backspace at beginning - merge with previous paragraph
586
+ if (e.key === 'Backspace') {
587
+ const sel = window.getSelection();
588
+ if (sel && sel.anchorOffset === 0 && paragraphIndex > 0) {
589
+ e.preventDefault();
590
+ const newElements = [...bodyElements];
591
+ const current = newElements[paragraphIndex];
592
+ const previous = newElements[paragraphIndex - 1];
593
+
594
+ if (
595
+ current?.type === 'paragraph' &&
596
+ previous?.type === 'paragraph' &&
597
+ current.paragraph &&
598
+ previous.paragraph
599
+ ) {
600
+ const mergedText = previous.paragraph.text + current.paragraph.text;
601
+ previous.paragraph.text = mergedText;
602
+ if (previous.paragraph.runs?.[0]) {
603
+ previous.paragraph.runs[0].text = mergedText;
604
+ }
605
+ newElements.splice(paragraphIndex, 1);
606
+ onChange(newElements, {
607
+ type: 'structure',
608
+ timestamp: new Date(),
609
+ undo: () => {},
610
+ redo: () => {},
611
+ description: 'Merge paragraphs',
612
+ });
613
+ }
614
+ }
615
+ }
616
+
617
+ // Arrow up/down navigation
618
+ if (e.key === 'ArrowUp' && paragraphIndex > 0) {
619
+ const prevPara = containerRef.current?.querySelector(
620
+ `[data-paragraph-index="${paragraphIndex - 1}"]`
621
+ ) as HTMLElement;
622
+ if (prevPara) {
623
+ prevPara.focus();
624
+ setSelectedParagraph(paragraphIndex - 1);
625
+ }
626
+ }
627
+ if (e.key === 'ArrowDown' && paragraphIndex < bodyElements.length - 1) {
628
+ const nextPara = containerRef.current?.querySelector(
629
+ `[data-paragraph-index="${paragraphIndex + 1}"]`
630
+ ) as HTMLElement;
631
+ if (nextPara) {
632
+ nextPara.focus();
633
+ setSelectedParagraph(paragraphIndex + 1);
634
+ }
635
+ }
636
+ },
637
+ [bodyElements, onChange]
638
+ );
639
+
640
+ // Track table element indices
641
+ let tableCount = 0;
642
+
643
+ return (
644
+ <div
645
+ ref={containerRef}
646
+ className="flex-1 overflow-auto bg-white dark:bg-gray-900 p-8"
647
+ onClick={() => {
648
+ // Deselect when clicking empty area
649
+ if (selectedParagraph !== null || selectedTableCell !== null) {
650
+ setSelectedParagraph(null);
651
+ setSelectedTableCell(null);
652
+ onSelectionChange(null);
653
+ onTableSelectionChange(null, null);
654
+ }
655
+ }}
656
+ >
657
+ {/* Document content area */}
658
+ <div
659
+ className="max-w-4xl mx-auto bg-white dark:bg-gray-800 shadow-lg rounded-lg min-h-[800px]"
660
+ onClick={(e) => e.stopPropagation()}
661
+ >
662
+ {/* Simulated page margins */}
663
+ <div className="p-8">
664
+ {bodyElements.map((element, index) => {
665
+ if (element.type === 'paragraph' && element.paragraph) {
666
+ return (
667
+ <EditableParagraph
668
+ key={`para-${index}`}
669
+ paragraph={element.paragraph}
670
+ paragraphIndex={index}
671
+ isSelected={selectedParagraph === index}
672
+ onSelect={() => handleParagraphSelect(index)}
673
+ onChange={(text) => handleParagraphChange(index, text)}
674
+ onKeyDown={(e) => handleKeyDown(e, index)}
675
+ readOnly={readOnly}
676
+ />
677
+ );
678
+ }
679
+
680
+ if (element.type === 'table' && element.table) {
681
+ const currentTableIndex = tableCount;
682
+ tableCount++;
683
+ return (
684
+ <EditableTable
685
+ key={`table-${index}`}
686
+ table={element.table}
687
+ tableIndex={index}
688
+ selectedCell={
689
+ selectedTableCell?.tableIndex === index
690
+ ? { row: selectedTableCell.row, col: selectedTableCell.col }
691
+ : null
692
+ }
693
+ onCellSelect={(row, col) =>
694
+ handleTableCellSelect(index, row, col)
695
+ }
696
+ onCellChange={(row, col, paraIndex, text) =>
697
+ handleTableCellChange(index, row, col, paraIndex, text)
698
+ }
699
+ readOnly={readOnly}
700
+ />
701
+ );
702
+ }
703
+
704
+ return null;
705
+ })}
706
+
707
+ {/* Empty state */}
708
+ {bodyElements.length === 0 && (
709
+ <div className="text-center text-muted-foreground py-16">
710
+ <p>No content to display</p>
711
+ <p className="text-sm mt-2">
712
+ Click to add your first paragraph
713
+ </p>
714
+ </div>
715
+ )}
716
+ </div>
717
+ </div>
718
+ </div>
719
+ );
720
+ }
721
+ );
722
+
723
+ export default DocumentEditor;