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,926 @@
1
+ /**
2
+ * TableEditor - Full table editing component with cell selection
3
+ *
4
+ * Features:
5
+ * - Click to select cell, Shift+Click for range selection
6
+ * - Right-click context menu for table operations
7
+ * - Cell content editing via contentEditable
8
+ * - Row/column insert/delete operations
9
+ * - Merge/split cells
10
+ * - Shading and border controls
11
+ * - Tab navigation between cells
12
+ */
13
+
14
+ import {
15
+ useState,
16
+ useCallback,
17
+ useRef,
18
+ useEffect,
19
+ useMemo,
20
+ forwardRef,
21
+ useImperativeHandle,
22
+ } from 'react';
23
+ import { motion, AnimatePresence } from 'framer-motion';
24
+ import {
25
+ Plus,
26
+ Minus,
27
+ ArrowUp,
28
+ ArrowDown,
29
+ ArrowLeft,
30
+ ArrowRight,
31
+ Merge,
32
+ Split,
33
+ Paintbrush,
34
+ } from 'lucide-react';
35
+ import { cn } from '@/utils/cn';
36
+ import type { CellSelection, QuickActionId } from '@/types/editor';
37
+
38
+ // Table data types
39
+ interface ParagraphData {
40
+ text: string;
41
+ runs?: Array<{
42
+ text: string;
43
+ bold?: boolean;
44
+ italic?: boolean;
45
+ underline?: boolean;
46
+ color?: string;
47
+ }>;
48
+ alignment?: 'left' | 'center' | 'right' | 'justify';
49
+ }
50
+
51
+ interface TableCellData {
52
+ paragraphs: ParagraphData[];
53
+ shading?: string;
54
+ verticalMerge?: 'restart' | 'continue';
55
+ columnSpan?: number;
56
+ rowSpan?: number;
57
+ verticalAlignment?: 'top' | 'center' | 'bottom';
58
+ borders?: {
59
+ top?: { style: string; size: number; color: string };
60
+ bottom?: { style: string; size: number; color: string };
61
+ left?: { style: string; size: number; color: string };
62
+ right?: { style: string; size: number; color: string };
63
+ };
64
+ }
65
+
66
+ interface TableRowData {
67
+ cells: TableCellData[];
68
+ height?: number;
69
+ }
70
+
71
+ interface TableData {
72
+ rows: TableRowData[];
73
+ width?: number;
74
+ alignment?: 'left' | 'center' | 'right';
75
+ }
76
+
77
+ export interface TableEditorProps {
78
+ /** Table data */
79
+ table: TableData;
80
+ /** Table index in document */
81
+ tableIndex: number;
82
+ /** Callback when table changes */
83
+ onChange: (table: TableData) => void;
84
+ /** Current cell selection */
85
+ selection: CellSelection | null;
86
+ /** Callback when selection changes */
87
+ onSelectionChange: (selection: CellSelection | null) => void;
88
+ /** Table shading settings */
89
+ tableShadingSettings?: {
90
+ header2Shading: string;
91
+ otherShading: string;
92
+ };
93
+ /** Quick action to apply */
94
+ activeQuickAction: QuickActionId | null;
95
+ /** Clear quick action after applying */
96
+ onQuickActionComplete: () => void;
97
+ /** Whether editor is read-only */
98
+ readOnly?: boolean;
99
+ }
100
+
101
+ export interface TableEditorRef {
102
+ /** Insert row above selection */
103
+ insertRowAbove: () => void;
104
+ /** Insert row below selection */
105
+ insertRowBelow: () => void;
106
+ /** Insert column left of selection */
107
+ insertColumnLeft: () => void;
108
+ /** Insert column right of selection */
109
+ insertColumnRight: () => void;
110
+ /** Delete selected row */
111
+ deleteRow: () => void;
112
+ /** Delete selected column */
113
+ deleteColumn: () => void;
114
+ /** Merge selected cells */
115
+ mergeCells: () => void;
116
+ /** Split selected cell */
117
+ splitCell: () => void;
118
+ /** Apply shading to selection */
119
+ applyShading: (color: string) => void;
120
+ /** Set vertical alignment */
121
+ setVerticalAlignment: (alignment: 'top' | 'center' | 'bottom') => void;
122
+ }
123
+
124
+ /**
125
+ * Context menu item
126
+ */
127
+ interface ContextMenuItem {
128
+ id: string;
129
+ label: string;
130
+ icon: React.ElementType;
131
+ action: () => void;
132
+ disabled?: boolean;
133
+ divider?: boolean;
134
+ }
135
+
136
+ /**
137
+ * Context menu component
138
+ */
139
+ function ContextMenu({
140
+ x,
141
+ y,
142
+ items,
143
+ onClose,
144
+ }: {
145
+ x: number;
146
+ y: number;
147
+ items: ContextMenuItem[];
148
+ onClose: () => void;
149
+ }) {
150
+ const menuRef = useRef<HTMLDivElement>(null);
151
+
152
+ // Close on click outside
153
+ useEffect(() => {
154
+ const handleClickOutside = (e: MouseEvent) => {
155
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
156
+ onClose();
157
+ }
158
+ };
159
+
160
+ document.addEventListener('mousedown', handleClickOutside);
161
+ return () => document.removeEventListener('mousedown', handleClickOutside);
162
+ }, [onClose]);
163
+
164
+ // Close on escape
165
+ useEffect(() => {
166
+ const handleKeyDown = (e: KeyboardEvent) => {
167
+ if (e.key === 'Escape') onClose();
168
+ };
169
+
170
+ document.addEventListener('keydown', handleKeyDown);
171
+ return () => document.removeEventListener('keydown', handleKeyDown);
172
+ }, [onClose]);
173
+
174
+ return (
175
+ <motion.div
176
+ ref={menuRef}
177
+ initial={{ opacity: 0, scale: 0.95 }}
178
+ animate={{ opacity: 1, scale: 1 }}
179
+ exit={{ opacity: 0, scale: 0.95 }}
180
+ transition={{ duration: 0.1 }}
181
+ className="fixed z-50 bg-popover border border-border rounded-lg shadow-lg py-1 min-w-[180px]"
182
+ style={{ left: x, top: y }}
183
+ >
184
+ {items.map((item) =>
185
+ item.divider ? (
186
+ <div key={item.id} className="h-px bg-border my-1" />
187
+ ) : (
188
+ <button
189
+ key={item.id}
190
+ onClick={() => {
191
+ item.action();
192
+ onClose();
193
+ }}
194
+ disabled={item.disabled}
195
+ className={cn(
196
+ 'w-full flex items-center gap-2 px-3 py-1.5 text-sm text-left',
197
+ 'hover:bg-muted transition-colors',
198
+ 'disabled:opacity-50 disabled:cursor-not-allowed'
199
+ )}
200
+ >
201
+ <item.icon className="w-4 h-4" />
202
+ {item.label}
203
+ </button>
204
+ )
205
+ )}
206
+ </motion.div>
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Main TableEditor component
212
+ */
213
+ export const TableEditor = forwardRef<TableEditorRef, TableEditorProps>(
214
+ function TableEditor(
215
+ {
216
+ table,
217
+ tableIndex,
218
+ onChange,
219
+ selection,
220
+ onSelectionChange,
221
+ tableShadingSettings,
222
+ activeQuickAction,
223
+ onQuickActionComplete,
224
+ readOnly = false,
225
+ },
226
+ ref
227
+ ) {
228
+ const tableRef = useRef<HTMLTableElement>(null);
229
+ const [contextMenu, setContextMenu] = useState<{
230
+ x: number;
231
+ y: number;
232
+ } | null>(null);
233
+ const [isDragging, setIsDragging] = useState(false);
234
+ const [dragStart, setDragStart] = useState<{ row: number; col: number } | null>(
235
+ null
236
+ );
237
+
238
+ // Get selected cell(s) bounds
239
+ const selectionBounds = useMemo(() => {
240
+ if (!selection) return null;
241
+ return {
242
+ minRow: Math.min(selection.startRow, selection.endRow),
243
+ maxRow: Math.max(selection.startRow, selection.endRow),
244
+ minCol: Math.min(selection.startCol, selection.endCol),
245
+ maxCol: Math.max(selection.startCol, selection.endCol),
246
+ };
247
+ }, [selection]);
248
+
249
+ // Check if cell is in selection
250
+ const isCellSelected = useCallback(
251
+ (row: number, col: number) => {
252
+ if (!selectionBounds) return false;
253
+ return (
254
+ row >= selectionBounds.minRow &&
255
+ row <= selectionBounds.maxRow &&
256
+ col >= selectionBounds.minCol &&
257
+ col <= selectionBounds.maxCol
258
+ );
259
+ },
260
+ [selectionBounds]
261
+ );
262
+
263
+ // Handle cell click
264
+ const handleCellClick = useCallback(
265
+ (row: number, col: number, e: React.MouseEvent) => {
266
+ if (readOnly) return;
267
+
268
+ if (e.shiftKey && selection) {
269
+ // Extend selection
270
+ onSelectionChange({
271
+ startRow: selection.startRow,
272
+ startCol: selection.startCol,
273
+ endRow: row,
274
+ endCol: col,
275
+ });
276
+ } else {
277
+ // New selection
278
+ onSelectionChange({
279
+ startRow: row,
280
+ startCol: col,
281
+ endRow: row,
282
+ endCol: col,
283
+ });
284
+ }
285
+ },
286
+ [selection, onSelectionChange, readOnly]
287
+ );
288
+
289
+ // Handle cell right-click
290
+ const handleCellContextMenu = useCallback(
291
+ (row: number, col: number, e: React.MouseEvent) => {
292
+ if (readOnly) return;
293
+
294
+ e.preventDefault();
295
+
296
+ // Select cell if not already selected
297
+ if (!isCellSelected(row, col)) {
298
+ onSelectionChange({
299
+ startRow: row,
300
+ startCol: col,
301
+ endRow: row,
302
+ endCol: col,
303
+ });
304
+ }
305
+
306
+ setContextMenu({ x: e.clientX, y: e.clientY });
307
+ },
308
+ [isCellSelected, onSelectionChange, readOnly]
309
+ );
310
+
311
+ // Handle mouse down for drag selection
312
+ const handleCellMouseDown = useCallback(
313
+ (row: number, col: number, e: React.MouseEvent) => {
314
+ if (readOnly || e.button !== 0) return;
315
+
316
+ setIsDragging(true);
317
+ setDragStart({ row, col });
318
+ onSelectionChange({
319
+ startRow: row,
320
+ startCol: col,
321
+ endRow: row,
322
+ endCol: col,
323
+ });
324
+ },
325
+ [onSelectionChange, readOnly]
326
+ );
327
+
328
+ // Handle mouse enter during drag
329
+ const handleCellMouseEnter = useCallback(
330
+ (row: number, col: number) => {
331
+ if (!isDragging || !dragStart) return;
332
+
333
+ onSelectionChange({
334
+ startRow: dragStart.row,
335
+ startCol: dragStart.col,
336
+ endRow: row,
337
+ endCol: col,
338
+ });
339
+ },
340
+ [isDragging, dragStart, onSelectionChange]
341
+ );
342
+
343
+ // Handle mouse up
344
+ useEffect(() => {
345
+ const handleMouseUp = () => {
346
+ setIsDragging(false);
347
+ setDragStart(null);
348
+ };
349
+
350
+ document.addEventListener('mouseup', handleMouseUp);
351
+ return () => document.removeEventListener('mouseup', handleMouseUp);
352
+ }, []);
353
+
354
+ // Table operations
355
+ const insertRowAbove = useCallback(() => {
356
+ if (!selection) return;
357
+
358
+ const newTable = { ...table };
359
+ const newRow: TableRowData = {
360
+ cells: table.rows[0].cells.map(() => ({
361
+ paragraphs: [{ text: '', runs: [{ text: '' }] }],
362
+ })),
363
+ };
364
+
365
+ newTable.rows = [
366
+ ...newTable.rows.slice(0, selectionBounds!.minRow),
367
+ newRow,
368
+ ...newTable.rows.slice(selectionBounds!.minRow),
369
+ ];
370
+
371
+ onChange(newTable);
372
+ }, [table, selection, selectionBounds, onChange]);
373
+
374
+ const insertRowBelow = useCallback(() => {
375
+ if (!selection) return;
376
+
377
+ const newTable = { ...table };
378
+ const newRow: TableRowData = {
379
+ cells: table.rows[0].cells.map(() => ({
380
+ paragraphs: [{ text: '', runs: [{ text: '' }] }],
381
+ })),
382
+ };
383
+
384
+ newTable.rows = [
385
+ ...newTable.rows.slice(0, selectionBounds!.maxRow + 1),
386
+ newRow,
387
+ ...newTable.rows.slice(selectionBounds!.maxRow + 1),
388
+ ];
389
+
390
+ onChange(newTable);
391
+ }, [table, selection, selectionBounds, onChange]);
392
+
393
+ const insertColumnLeft = useCallback(() => {
394
+ if (!selection) return;
395
+
396
+ const newTable = { ...table };
397
+ newTable.rows = newTable.rows.map((row) => ({
398
+ ...row,
399
+ cells: [
400
+ ...row.cells.slice(0, selectionBounds!.minCol),
401
+ { paragraphs: [{ text: '', runs: [{ text: '' }] }] },
402
+ ...row.cells.slice(selectionBounds!.minCol),
403
+ ],
404
+ }));
405
+
406
+ onChange(newTable);
407
+ }, [table, selection, selectionBounds, onChange]);
408
+
409
+ const insertColumnRight = useCallback(() => {
410
+ if (!selection) return;
411
+
412
+ const newTable = { ...table };
413
+ newTable.rows = newTable.rows.map((row) => ({
414
+ ...row,
415
+ cells: [
416
+ ...row.cells.slice(0, selectionBounds!.maxCol + 1),
417
+ { paragraphs: [{ text: '', runs: [{ text: '' }] }] },
418
+ ...row.cells.slice(selectionBounds!.maxCol + 1),
419
+ ],
420
+ }));
421
+
422
+ onChange(newTable);
423
+ }, [table, selection, selectionBounds, onChange]);
424
+
425
+ const deleteRow = useCallback(() => {
426
+ if (!selection || table.rows.length <= 1) return;
427
+
428
+ const newTable = { ...table };
429
+ newTable.rows = newTable.rows.filter(
430
+ (_, index) =>
431
+ index < selectionBounds!.minRow || index > selectionBounds!.maxRow
432
+ );
433
+
434
+ onChange(newTable);
435
+ onSelectionChange(null);
436
+ }, [table, selection, selectionBounds, onChange, onSelectionChange]);
437
+
438
+ const deleteColumn = useCallback(() => {
439
+ if (!selection || table.rows[0].cells.length <= 1) return;
440
+
441
+ const newTable = { ...table };
442
+ newTable.rows = newTable.rows.map((row) => ({
443
+ ...row,
444
+ cells: row.cells.filter(
445
+ (_, index) =>
446
+ index < selectionBounds!.minCol || index > selectionBounds!.maxCol
447
+ ),
448
+ }));
449
+
450
+ onChange(newTable);
451
+ onSelectionChange(null);
452
+ }, [table, selection, selectionBounds, onChange, onSelectionChange]);
453
+
454
+ const mergeCells = useCallback(() => {
455
+ if (!selection || !selectionBounds) return;
456
+
457
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
458
+ if (minRow === maxRow && minCol === maxCol) return; // Single cell
459
+
460
+ const newTable = { ...table };
461
+
462
+ // Get text from all selected cells
463
+ const mergedText = newTable.rows
464
+ .slice(minRow, maxRow + 1)
465
+ .flatMap((row) =>
466
+ row.cells
467
+ .slice(minCol, maxCol + 1)
468
+ .flatMap((cell) => cell.paragraphs.map((p) => p.text))
469
+ )
470
+ .filter((t) => t)
471
+ .join(' ');
472
+
473
+ // Set merged cell properties
474
+ const mergedCell = newTable.rows[minRow].cells[minCol];
475
+ mergedCell.paragraphs = [{ text: mergedText, runs: [{ text: mergedText }] }];
476
+ mergedCell.columnSpan = maxCol - minCol + 1;
477
+ mergedCell.rowSpan = maxRow - minRow + 1;
478
+
479
+ // Mark other cells in the merge region
480
+ for (let r = minRow; r <= maxRow; r++) {
481
+ for (let c = minCol; c <= maxCol; c++) {
482
+ if (r === minRow && c === minCol) continue;
483
+
484
+ if (r === minRow) {
485
+ // Same row - remove cell (it's spanned)
486
+ // In real implementation, we'd mark it for removal
487
+ }
488
+ if (r > minRow && c === minCol) {
489
+ // First column of subsequent rows - mark as continue
490
+ newTable.rows[r].cells[c].verticalMerge = 'continue';
491
+ }
492
+ }
493
+ }
494
+
495
+ onChange(newTable);
496
+ }, [table, selection, selectionBounds, onChange]);
497
+
498
+ const splitCell = useCallback(() => {
499
+ if (!selection) return;
500
+
501
+ const newTable = { ...table };
502
+ const cell = newTable.rows[selection.startRow]?.cells[selection.startCol];
503
+
504
+ if (cell) {
505
+ cell.columnSpan = 1;
506
+ cell.rowSpan = 1;
507
+ delete cell.verticalMerge;
508
+ }
509
+
510
+ onChange(newTable);
511
+ }, [table, selection, onChange]);
512
+
513
+ const applyShading = useCallback(
514
+ (color: string) => {
515
+ if (!selection || !selectionBounds) return;
516
+
517
+ const newTable = { ...table };
518
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
519
+
520
+ for (let r = minRow; r <= maxRow; r++) {
521
+ for (let c = minCol; c <= maxCol; c++) {
522
+ if (newTable.rows[r]?.cells[c]) {
523
+ newTable.rows[r].cells[c].shading = color.replace('#', '');
524
+ }
525
+ }
526
+ }
527
+
528
+ onChange(newTable);
529
+ },
530
+ [table, selection, selectionBounds, onChange]
531
+ );
532
+
533
+ const setVerticalAlignment = useCallback(
534
+ (alignment: 'top' | 'center' | 'bottom') => {
535
+ if (!selection || !selectionBounds) return;
536
+
537
+ const newTable = { ...table };
538
+ const { minRow, maxRow, minCol, maxCol } = selectionBounds;
539
+
540
+ for (let r = minRow; r <= maxRow; r++) {
541
+ for (let c = minCol; c <= maxCol; c++) {
542
+ if (newTable.rows[r]?.cells[c]) {
543
+ newTable.rows[r].cells[c].verticalAlignment = alignment;
544
+ }
545
+ }
546
+ }
547
+
548
+ onChange(newTable);
549
+ },
550
+ [table, selection, selectionBounds, onChange]
551
+ );
552
+
553
+ // Expose methods via ref
554
+ useImperativeHandle(ref, () => ({
555
+ insertRowAbove,
556
+ insertRowBelow,
557
+ insertColumnLeft,
558
+ insertColumnRight,
559
+ deleteRow,
560
+ deleteColumn,
561
+ mergeCells,
562
+ splitCell,
563
+ applyShading,
564
+ setVerticalAlignment,
565
+ }));
566
+
567
+ // Handle quick actions
568
+ useEffect(() => {
569
+ if (!activeQuickAction || !selection) return;
570
+
571
+ switch (activeQuickAction) {
572
+ case 'table-add-row-above':
573
+ insertRowAbove();
574
+ break;
575
+ case 'table-add-row-below':
576
+ insertRowBelow();
577
+ break;
578
+ case 'table-add-col-left':
579
+ insertColumnLeft();
580
+ break;
581
+ case 'table-add-col-right':
582
+ insertColumnRight();
583
+ break;
584
+ case 'table-delete-row':
585
+ deleteRow();
586
+ break;
587
+ case 'table-delete-col':
588
+ deleteColumn();
589
+ break;
590
+ case 'table-merge-cells':
591
+ mergeCells();
592
+ break;
593
+ case 'table-split-cell':
594
+ splitCell();
595
+ break;
596
+ case 'apply-h2-shading':
597
+ if (tableShadingSettings) {
598
+ applyShading(tableShadingSettings.header2Shading);
599
+ }
600
+ break;
601
+ case 'apply-other-shading':
602
+ if (tableShadingSettings) {
603
+ applyShading(tableShadingSettings.otherShading);
604
+ }
605
+ break;
606
+ case 'table-vertical-align':
607
+ // Would show a dialog - for now default to center
608
+ setVerticalAlignment('center');
609
+ break;
610
+ }
611
+
612
+ onQuickActionComplete();
613
+ }, [
614
+ activeQuickAction,
615
+ selection,
616
+ insertRowAbove,
617
+ insertRowBelow,
618
+ insertColumnLeft,
619
+ insertColumnRight,
620
+ deleteRow,
621
+ deleteColumn,
622
+ mergeCells,
623
+ splitCell,
624
+ applyShading,
625
+ setVerticalAlignment,
626
+ tableShadingSettings,
627
+ onQuickActionComplete,
628
+ ]);
629
+
630
+ // Handle keyboard navigation
631
+ const handleKeyDown = useCallback(
632
+ (e: React.KeyboardEvent) => {
633
+ if (!selection) return;
634
+
635
+ const { startRow, startCol } = selection;
636
+
637
+ switch (e.key) {
638
+ case 'Tab':
639
+ e.preventDefault();
640
+ if (e.shiftKey) {
641
+ // Previous cell
642
+ if (startCol > 0) {
643
+ onSelectionChange({
644
+ startRow,
645
+ startCol: startCol - 1,
646
+ endRow: startRow,
647
+ endCol: startCol - 1,
648
+ });
649
+ } else if (startRow > 0) {
650
+ onSelectionChange({
651
+ startRow: startRow - 1,
652
+ startCol: table.rows[0].cells.length - 1,
653
+ endRow: startRow - 1,
654
+ endCol: table.rows[0].cells.length - 1,
655
+ });
656
+ }
657
+ } else {
658
+ // Next cell
659
+ if (startCol < table.rows[0].cells.length - 1) {
660
+ onSelectionChange({
661
+ startRow,
662
+ startCol: startCol + 1,
663
+ endRow: startRow,
664
+ endCol: startCol + 1,
665
+ });
666
+ } else if (startRow < table.rows.length - 1) {
667
+ onSelectionChange({
668
+ startRow: startRow + 1,
669
+ startCol: 0,
670
+ endRow: startRow + 1,
671
+ endCol: 0,
672
+ });
673
+ }
674
+ }
675
+ break;
676
+
677
+ case 'ArrowUp':
678
+ if (startRow > 0) {
679
+ onSelectionChange({
680
+ startRow: startRow - 1,
681
+ startCol,
682
+ endRow: startRow - 1,
683
+ endCol: startCol,
684
+ });
685
+ }
686
+ break;
687
+
688
+ case 'ArrowDown':
689
+ if (startRow < table.rows.length - 1) {
690
+ onSelectionChange({
691
+ startRow: startRow + 1,
692
+ startCol,
693
+ endRow: startRow + 1,
694
+ endCol: startCol,
695
+ });
696
+ }
697
+ break;
698
+
699
+ case 'ArrowLeft':
700
+ if (startCol > 0) {
701
+ onSelectionChange({
702
+ startRow,
703
+ startCol: startCol - 1,
704
+ endRow: startRow,
705
+ endCol: startCol - 1,
706
+ });
707
+ }
708
+ break;
709
+
710
+ case 'ArrowRight':
711
+ if (startCol < table.rows[0].cells.length - 1) {
712
+ onSelectionChange({
713
+ startRow,
714
+ startCol: startCol + 1,
715
+ endRow: startRow,
716
+ endCol: startCol + 1,
717
+ });
718
+ }
719
+ break;
720
+ }
721
+ },
722
+ [selection, table, onSelectionChange]
723
+ );
724
+
725
+ // Context menu items
726
+ const contextMenuItems: ContextMenuItem[] = [
727
+ {
728
+ id: 'insert-row-above',
729
+ label: 'Insert Row Above',
730
+ icon: ArrowUp,
731
+ action: insertRowAbove,
732
+ },
733
+ {
734
+ id: 'insert-row-below',
735
+ label: 'Insert Row Below',
736
+ icon: ArrowDown,
737
+ action: insertRowBelow,
738
+ },
739
+ {
740
+ id: 'insert-col-left',
741
+ label: 'Insert Column Left',
742
+ icon: ArrowLeft,
743
+ action: insertColumnLeft,
744
+ },
745
+ {
746
+ id: 'insert-col-right',
747
+ label: 'Insert Column Right',
748
+ icon: ArrowRight,
749
+ action: insertColumnRight,
750
+ },
751
+ { id: 'divider-1', label: '', icon: Plus, action: () => {}, divider: true },
752
+ {
753
+ id: 'delete-row',
754
+ label: 'Delete Row',
755
+ icon: Minus,
756
+ action: deleteRow,
757
+ disabled: table.rows.length <= 1,
758
+ },
759
+ {
760
+ id: 'delete-col',
761
+ label: 'Delete Column',
762
+ icon: Minus,
763
+ action: deleteColumn,
764
+ disabled: table.rows[0].cells.length <= 1,
765
+ },
766
+ { id: 'divider-2', label: '', icon: Plus, action: () => {}, divider: true },
767
+ {
768
+ id: 'merge-cells',
769
+ label: 'Merge Cells',
770
+ icon: Merge,
771
+ action: mergeCells,
772
+ disabled:
773
+ !selectionBounds ||
774
+ (selectionBounds.minRow === selectionBounds.maxRow &&
775
+ selectionBounds.minCol === selectionBounds.maxCol),
776
+ },
777
+ {
778
+ id: 'split-cell',
779
+ label: 'Split Cell',
780
+ icon: Split,
781
+ action: splitCell,
782
+ },
783
+ { id: 'divider-3', label: '', icon: Plus, action: () => {}, divider: true },
784
+ {
785
+ id: 'apply-h2-shading',
786
+ label: 'Apply Header 2 Shading',
787
+ icon: Paintbrush,
788
+ action: () =>
789
+ tableShadingSettings &&
790
+ applyShading(tableShadingSettings.header2Shading),
791
+ disabled: !tableShadingSettings,
792
+ },
793
+ {
794
+ id: 'apply-other-shading',
795
+ label: 'Apply Other Shading',
796
+ icon: Paintbrush,
797
+ action: () =>
798
+ tableShadingSettings && applyShading(tableShadingSettings.otherShading),
799
+ disabled: !tableShadingSettings,
800
+ },
801
+ ];
802
+
803
+ return (
804
+ <div
805
+ className="relative my-4 overflow-x-auto"
806
+ onKeyDown={handleKeyDown}
807
+ tabIndex={0}
808
+ >
809
+ <table
810
+ ref={tableRef}
811
+ className="w-full border-collapse border border-border"
812
+ >
813
+ <tbody>
814
+ {table.rows.map((row, rowIndex) => (
815
+ <tr key={rowIndex}>
816
+ {row.cells.map((cell, cellIndex) => {
817
+ // Skip cells that are continued from vertical merge
818
+ if (cell.verticalMerge === 'continue') {
819
+ return null;
820
+ }
821
+
822
+ const isSelected = isCellSelected(rowIndex, cellIndex);
823
+
824
+ return (
825
+ <td
826
+ key={cellIndex}
827
+ className={cn(
828
+ 'border border-border p-2 relative cursor-cell',
829
+ 'transition-colors duration-100',
830
+ isSelected &&
831
+ 'ring-2 ring-primary ring-inset bg-primary/10',
832
+ !isSelected && 'hover:bg-muted/30',
833
+ cell.verticalAlignment === 'center' && 'align-middle',
834
+ cell.verticalAlignment === 'bottom' && 'align-bottom',
835
+ (!cell.verticalAlignment ||
836
+ cell.verticalAlignment === 'top') &&
837
+ 'align-top'
838
+ )}
839
+ style={{
840
+ backgroundColor: cell.shading
841
+ ? `#${cell.shading}`
842
+ : undefined,
843
+ }}
844
+ colSpan={cell.columnSpan || 1}
845
+ rowSpan={cell.rowSpan || 1}
846
+ onClick={(e) => handleCellClick(rowIndex, cellIndex, e)}
847
+ onContextMenu={(e) =>
848
+ handleCellContextMenu(rowIndex, cellIndex, e)
849
+ }
850
+ onMouseDown={(e) =>
851
+ handleCellMouseDown(rowIndex, cellIndex, e)
852
+ }
853
+ onMouseEnter={() =>
854
+ handleCellMouseEnter(rowIndex, cellIndex)
855
+ }
856
+ data-row={rowIndex}
857
+ data-col={cellIndex}
858
+ >
859
+ {cell.paragraphs.map((para, paraIndex) => (
860
+ <div
861
+ key={paraIndex}
862
+ contentEditable={!readOnly}
863
+ suppressContentEditableWarning
864
+ className={cn(
865
+ 'min-h-[1.2em] outline-none focus:bg-primary/5',
866
+ para.alignment === 'center' && 'text-center',
867
+ para.alignment === 'right' && 'text-right',
868
+ para.alignment === 'justify' && 'text-justify'
869
+ )}
870
+ onBlur={(e) => {
871
+ const newText = e.currentTarget.textContent || '';
872
+ if (newText !== para.text) {
873
+ const newTable = { ...table };
874
+ newTable.rows[rowIndex].cells[
875
+ cellIndex
876
+ ].paragraphs[paraIndex].text = newText;
877
+ onChange(newTable);
878
+ }
879
+ }}
880
+ >
881
+ {para.runs && para.runs.length > 0 ? (
882
+ para.runs.map((run, runIndex) => (
883
+ <span
884
+ key={runIndex}
885
+ style={{
886
+ fontWeight: run.bold ? 'bold' : undefined,
887
+ fontStyle: run.italic ? 'italic' : undefined,
888
+ textDecoration: run.underline
889
+ ? 'underline'
890
+ : undefined,
891
+ color: run.color ? `#${run.color}` : undefined,
892
+ }}
893
+ >
894
+ {run.text}
895
+ </span>
896
+ ))
897
+ ) : (
898
+ <span>{para.text || '\u00A0'}</span>
899
+ )}
900
+ </div>
901
+ ))}
902
+ </td>
903
+ );
904
+ })}
905
+ </tr>
906
+ ))}
907
+ </tbody>
908
+ </table>
909
+
910
+ {/* Context menu */}
911
+ <AnimatePresence>
912
+ {contextMenu && (
913
+ <ContextMenu
914
+ x={contextMenu.x}
915
+ y={contextMenu.y}
916
+ items={contextMenuItems}
917
+ onClose={() => setContextMenu(null)}
918
+ />
919
+ )}
920
+ </AnimatePresence>
921
+ </div>
922
+ );
923
+ }
924
+ );
925
+
926
+ export default TableEditor;