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,2662 @@
1
+ import { Button } from '@/components/common/Button';
2
+ import { ColorPickerDialog } from '@/components/common/ColorPickerDialog';
3
+ import { ConfirmDialog } from '@/components/common/ConfirmDialog';
4
+ import { Input } from '@/components/common/Input';
5
+ import { SegmentedControl } from '@/components/settings/SegmentedControl';
6
+ import { SettingRow } from '@/components/settings/SettingRow';
7
+ import * as Switch from '@radix-ui/react-switch';
8
+ import { useGlobalStats } from '@/contexts/GlobalStatsContext';
9
+ import { useSession } from '@/contexts/SessionContext';
10
+ import { useTheme } from '@/contexts/ThemeContext';
11
+ import { useUserSettings } from '@/contexts/UserSettingsContext';
12
+ import { cn } from '@/utils/cn';
13
+ import { getContrastTextColor } from '@/utils/colorConvert';
14
+ import logger from '@/utils/logger';
15
+ import { hasEncodingIssues, sanitizeUrl, validatePowerAutomateUrl } from '@/utils/urlHelpers';
16
+ import { motion } from 'framer-motion';
17
+ import {
18
+ Accessibility,
19
+ AlertCircle,
20
+ Archive,
21
+ Check,
22
+ CheckCircle2,
23
+ ChevronDown,
24
+ Cloud,
25
+ Download,
26
+ Globe,
27
+ HardDrive,
28
+ Laptop,
29
+ Lightbulb,
30
+ Link2,
31
+ LogOut,
32
+ Monitor,
33
+ Moon,
34
+ Palette,
35
+ RefreshCw,
36
+ RotateCcw,
37
+ Save,
38
+ Search,
39
+ Send,
40
+ Sparkles,
41
+ Sun,
42
+ Type,
43
+ User,
44
+ Wifi,
45
+ Zap
46
+ } from 'lucide-react';
47
+ import { useEffect, useRef, useState } from 'react';
48
+
49
+ const settingsSections = [
50
+ {
51
+ group: 'Account',
52
+ items: [
53
+ { id: 'profile', label: 'Profile', icon: User, description: 'Personal information' },
54
+ ],
55
+ },
56
+ {
57
+ group: 'Customization',
58
+ items: [
59
+ { id: 'appearance', label: 'Appearance', icon: Palette, description: 'Theme & colors' },
60
+ { id: 'typography', label: 'Typography', icon: Type, description: 'Fonts & text styling' },
61
+ { id: 'display', label: 'Display', icon: Monitor, description: 'Monitor settings' },
62
+ ],
63
+ },
64
+ {
65
+ group: 'System',
66
+ items: [
67
+ { id: 'language', label: 'Language', icon: Globe, description: 'Region & locale' },
68
+ { id: 'updates', label: 'Updates', icon: Download, description: 'App updates & versioning' },
69
+ {
70
+ id: 'api-connections',
71
+ label: 'API Connections',
72
+ icon: Link2,
73
+ description: 'External services',
74
+ },
75
+ {
76
+ id: 'local-dictionary',
77
+ label: 'Local Dictionary',
78
+ icon: HardDrive,
79
+ description: 'Offline hyperlink lookups',
80
+ },
81
+ {
82
+ id: 'backup-settings',
83
+ label: 'Backups',
84
+ icon: Archive,
85
+ description: 'Document backup options',
86
+ },
87
+ {
88
+ id: 'submit-idea',
89
+ label: 'Submit Idea for New Implementation',
90
+ icon: Lightbulb,
91
+ description: 'Share your ideas',
92
+ },
93
+ ],
94
+ },
95
+ ];
96
+
97
+ export function Settings() {
98
+ const [activeSection, setActiveSection] = useState('profile');
99
+ const [ideaTitle, setIdeaTitle] = useState('');
100
+ const [ideaBenefit, setIdeaBenefit] = useState('');
101
+ const [ideaSubmitted, setIdeaSubmitted] = useState(false);
102
+ const [saveSuccess, setSaveSuccess] = useState(false);
103
+ const [checkingForUpdates, setCheckingForUpdates] = useState(false);
104
+ const [updateStatus, setUpdateStatus] = useState('');
105
+ const [currentVersion, setCurrentVersion] = useState('');
106
+ const [updateAvailable, setUpdateAvailable] = useState(false);
107
+ const [updateVersion, setUpdateVersion] = useState('');
108
+ const [downloadProgress, setDownloadProgress] = useState(0);
109
+ const [updateDownloaded, setUpdateDownloaded] = useState(false);
110
+
111
+ // SharePoint update source states
112
+ const [sharePointLoginStatus, setSharePointLoginStatus] = useState<'logged-out' | 'logging-in' | 'logged-in'>('logged-out');
113
+ const [testingSharePointConnection, setTestingSharePointConnection] = useState(false);
114
+ const [sharePointConnectionResult, setSharePointConnectionResult] = useState<{ success: boolean; message: string } | null>(null);
115
+
116
+ const {
117
+ settings,
118
+ updateProfile,
119
+ updateNotifications,
120
+ updateApiConnections,
121
+ updateUpdateSettings,
122
+ updateLocalDictionary,
123
+ updateBackupSettings,
124
+ updateDisplaySettings,
125
+ updateSettings,
126
+ saveSettings,
127
+ } = useUserSettings();
128
+ const { sessions } = useSession();
129
+ const { stats, resetAllStats } = useGlobalStats();
130
+
131
+ // Local form states
132
+ const [profileForm, setProfileForm] = useState(settings.profile);
133
+ const [notificationsForm, setNotificationsForm] = useState(settings.notifications);
134
+ const [apiConnectionsForm, setApiConnectionsForm] = useState(settings.apiConnections);
135
+ const [languageForm, setLanguageForm] = useState(settings.language);
136
+ const [timezoneForm, setTimezoneForm] = useState(settings.timezone);
137
+ const [dateFormatForm, setDateFormatForm] = useState(settings.dateFormat);
138
+ const [updateSettingsForm, setUpdateSettingsForm] = useState(settings.updateSettings);
139
+ const [localDictionaryForm, setLocalDictionaryForm] = useState(settings.localDictionary);
140
+ const [backupSettingsForm, setBackupSettingsForm] = useState(settings.backupSettings);
141
+ const [displaySettingsForm, setDisplaySettingsForm] = useState(settings.displaySettings);
142
+
143
+ // Display settings states
144
+ const [availableDisplays, setAvailableDisplays] = useState<Array<{
145
+ id: number;
146
+ label: string;
147
+ bounds: { x: number; y: number; width: number; height: number };
148
+ workArea: { x: number; y: number; width: number; height: number };
149
+ isPrimary: boolean;
150
+ }>>([]);
151
+ const [identifyingMonitors, setIdentifyingMonitors] = useState(false);
152
+
153
+ // Dictionary sync states
154
+ const [dictionaryStatus, setDictionaryStatus] = useState<{
155
+ enabled: boolean;
156
+ lastSyncTime: string | null;
157
+ lastSyncSuccess: boolean;
158
+ totalEntries: number;
159
+ syncInProgress: boolean;
160
+ syncProgress: number;
161
+ syncError: string | null;
162
+ nextScheduledSync: string | null;
163
+ } | null>(null);
164
+ const [syncingDictionary, setSyncingDictionary] = useState(false);
165
+ const [clientSecretInput, setClientSecretInput] = useState('');
166
+ const [showClientSecretDialog, setShowClientSecretDialog] = useState(false);
167
+ const [credentialsSaved, setCredentialsSaved] = useState(false);
168
+ const [showResetStatsDialog, setShowResetStatsDialog] = useState(false);
169
+ const [isResettingStats, setIsResettingStats] = useState(false);
170
+
171
+ // Timeout refs for cleanup
172
+ const urlWarningTimeoutRef = useRef<NodeJS.Timeout | null>(null);
173
+ const saveSuccessTimeoutRef = useRef<NodeJS.Timeout | null>(null);
174
+ const ideaSubmittedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
175
+
176
+ // Get current version on mount
177
+ useEffect(() => {
178
+ const getVersion = async () => {
179
+ try {
180
+ if (!window.electronAPI?.getCurrentVersion) {
181
+ console.warn('Settings: electronAPI.getCurrentVersion not available');
182
+ return;
183
+ }
184
+ const version = await window.electronAPI.getCurrentVersion();
185
+ setCurrentVersion(version);
186
+ } catch (_error) {
187
+ // Silently handle version retrieval errors
188
+ }
189
+ };
190
+ getVersion();
191
+ }, []);
192
+
193
+ // Update event listeners
194
+ useEffect(() => {
195
+ // Safely check if electronAPI is available
196
+ if (typeof window.electronAPI === 'undefined') {
197
+ console.warn('Settings: electronAPI not available (running in browser mode?)');
198
+ return;
199
+ }
200
+
201
+ const unsubAvailable = window.electronAPI.onUpdateAvailable(
202
+ (info: { version: string; releaseDate: string; releaseNotes: string }) => {
203
+ setUpdateAvailable(true);
204
+ setUpdateVersion(info.version);
205
+ setUpdateStatus(`Update available: ${info.version}`);
206
+ setCheckingForUpdates(false);
207
+ }
208
+ );
209
+
210
+ const unsubProgress = window.electronAPI.onUpdateDownloadProgress(
211
+ (progress: {
212
+ bytesPerSecond: number;
213
+ percent: number;
214
+ transferred: number;
215
+ total: number;
216
+ }) => {
217
+ setDownloadProgress(progress.percent);
218
+ setUpdateStatus(`Downloading update: ${Math.round(progress.percent)}%`);
219
+ }
220
+ );
221
+
222
+ const unsubDownloaded = window.electronAPI.onUpdateDownloaded(
223
+ (info: { version: string; releaseNotes: string; fallbackUsed?: boolean }) => {
224
+ setUpdateDownloaded(true);
225
+ setDownloadProgress(100);
226
+ setUpdateStatus(`Update ${info.version} downloaded. Ready to install.`);
227
+ }
228
+ );
229
+
230
+ const unsubNotAvailable = window.electronAPI.onUpdateNotAvailable(
231
+ (_info: { version: string }) => {
232
+ setUpdateAvailable(false);
233
+ setUpdateStatus('You are up to date');
234
+ setCheckingForUpdates(false);
235
+ }
236
+ );
237
+
238
+ const unsubError = window.electronAPI.onUpdateError((error: { message: string }) => {
239
+ setUpdateStatus(`Update error: ${error.message}`);
240
+ setCheckingForUpdates(false);
241
+ });
242
+
243
+ return () => {
244
+ unsubAvailable();
245
+ unsubProgress();
246
+ unsubDownloaded();
247
+ unsubNotAvailable();
248
+ unsubError();
249
+ };
250
+ }, []);
251
+
252
+ // Update local states when settings change
253
+ useEffect(() => {
254
+ setProfileForm(settings.profile);
255
+ setNotificationsForm(settings.notifications);
256
+ setApiConnectionsForm(settings.apiConnections);
257
+ setLanguageForm(settings.language);
258
+ setTimezoneForm(settings.timezone);
259
+ setDateFormatForm(settings.dateFormat);
260
+ setUpdateSettingsForm(settings.updateSettings);
261
+ setLocalDictionaryForm(settings.localDictionary);
262
+ setBackupSettingsForm(settings.backupSettings);
263
+ setDisplaySettingsForm(settings.displaySettings);
264
+ }, [settings]);
265
+
266
+ // Fetch available displays when display section is active
267
+ useEffect(() => {
268
+ const fetchDisplays = async () => {
269
+ if (typeof window.electronAPI === 'undefined' || !window.electronAPI.display) return;
270
+ try {
271
+ const displays = await window.electronAPI.display.getAllDisplays();
272
+ setAvailableDisplays(displays);
273
+ } catch (_error) {
274
+ // Silent fail - displays not available
275
+ }
276
+ };
277
+
278
+ if (activeSection === 'display') {
279
+ fetchDisplays();
280
+ }
281
+ }, [activeSection]);
282
+
283
+ // Dictionary status polling
284
+ useEffect(() => {
285
+ const fetchDictionaryStatus = async () => {
286
+ if (typeof window.electronAPI === 'undefined') return;
287
+ try {
288
+ const result = await window.electronAPI.dictionary.getStatus();
289
+ if (result.success && result.status) {
290
+ setDictionaryStatus(result.status);
291
+ }
292
+ } catch (_error) {
293
+ // Silent fail - dictionary not initialized yet
294
+ }
295
+ };
296
+
297
+ // Fetch on mount and when dictionary section is active
298
+ if (activeSection === 'local-dictionary') {
299
+ fetchDictionaryStatus();
300
+ const interval = setInterval(fetchDictionaryStatus, 5000);
301
+ return () => clearInterval(interval);
302
+ }
303
+ }, [activeSection]);
304
+
305
+ // Dictionary sync progress listener
306
+ useEffect(() => {
307
+ if (typeof window.electronAPI === 'undefined') return;
308
+
309
+ const unsubProgress = window.electronAPI.dictionary.onSyncProgress((progress) => {
310
+ setDictionaryStatus((prev) =>
311
+ prev ? { ...prev, syncProgress: progress.progress, syncInProgress: true } : null
312
+ );
313
+ });
314
+
315
+ const unsubComplete = window.electronAPI.dictionary.onSyncComplete((result) => {
316
+ setSyncingDictionary(false);
317
+ if (result.success) {
318
+ // Refresh status after sync
319
+ window.electronAPI.dictionary.getStatus().then((res) => {
320
+ if (res.success && res.status) {
321
+ setDictionaryStatus(res.status);
322
+ }
323
+ });
324
+ }
325
+ });
326
+
327
+ return () => {
328
+ unsubProgress();
329
+ unsubComplete();
330
+ };
331
+ }, []);
332
+
333
+ // Cleanup all timeouts on unmount
334
+ useEffect(() => {
335
+ return () => {
336
+ if (urlWarningTimeoutRef.current) {
337
+ clearTimeout(urlWarningTimeoutRef.current);
338
+ }
339
+ if (saveSuccessTimeoutRef.current) {
340
+ clearTimeout(saveSuccessTimeoutRef.current);
341
+ }
342
+ if (ideaSubmittedTimeoutRef.current) {
343
+ clearTimeout(ideaSubmittedTimeoutRef.current);
344
+ }
345
+ };
346
+ }, []);
347
+
348
+ const handlePowerAutomateUrlChange = (url: string) => {
349
+ // Update form
350
+ setApiConnectionsForm({ ...apiConnectionsForm, powerAutomateUrl: url });
351
+
352
+ // Auto-sanitize if encoding issues detected
353
+ if (hasEncodingIssues(url)) {
354
+ const sanitized = sanitizeUrl(url);
355
+ setShowUrlWarning(true);
356
+ if (urlWarningTimeoutRef.current) {
357
+ clearTimeout(urlWarningTimeoutRef.current);
358
+ }
359
+ urlWarningTimeoutRef.current = setTimeout(() => {
360
+ setApiConnectionsForm({ ...apiConnectionsForm, powerAutomateUrl: sanitized });
361
+ setShowUrlWarning(false);
362
+ urlWarningTimeoutRef.current = null;
363
+ }, 1500);
364
+ }
365
+
366
+ // Validate URL
367
+ if (url.trim()) {
368
+ const validation = validatePowerAutomateUrl(url);
369
+ setUrlValidation(validation);
370
+ } else {
371
+ setUrlValidation(null);
372
+ }
373
+ };
374
+
375
+ const handleResetStats = async () => {
376
+ setIsResettingStats(true);
377
+ try {
378
+ await resetAllStats();
379
+ setShowResetStatsDialog(false);
380
+ } catch (error) {
381
+ logger.error('Failed to reset stats:', error);
382
+ } finally {
383
+ setIsResettingStats(false);
384
+ }
385
+ };
386
+
387
+ const handleSaveSettings = async () => {
388
+ // Sanitize PowerAutomate URL before saving
389
+ if (apiConnectionsForm.powerAutomateUrl) {
390
+ const sanitized = sanitizeUrl(apiConnectionsForm.powerAutomateUrl);
391
+ apiConnectionsForm.powerAutomateUrl = sanitized;
392
+ }
393
+
394
+ // Update all settings
395
+ updateProfile(profileForm);
396
+ updateNotifications(notificationsForm);
397
+ updateApiConnections(apiConnectionsForm);
398
+ updateUpdateSettings(updateSettingsForm);
399
+ updateSettings({
400
+ language: languageForm,
401
+ timezone: timezoneForm,
402
+ dateFormat: dateFormatForm,
403
+ });
404
+
405
+ // Configure update provider based on SharePoint settings
406
+ if (typeof window.electronAPI !== 'undefined') {
407
+ if (updateSettingsForm.useSharePointSource && updateSettingsForm.sharePointFolderUrl) {
408
+ await window.electronAPI.setUpdateProvider({
409
+ type: 'sharepoint',
410
+ sharePointUrl: updateSettingsForm.sharePointFolderUrl,
411
+ });
412
+ } else {
413
+ await window.electronAPI.setUpdateProvider({ type: 'github' });
414
+ }
415
+ }
416
+
417
+ // Save to localStorage
418
+ const success = await saveSettings();
419
+ if (success) {
420
+ setSaveSuccess(true);
421
+ if (saveSuccessTimeoutRef.current) {
422
+ clearTimeout(saveSuccessTimeoutRef.current);
423
+ }
424
+ saveSuccessTimeoutRef.current = setTimeout(() => {
425
+ setSaveSuccess(false);
426
+ saveSuccessTimeoutRef.current = null;
427
+ }, 2000);
428
+ }
429
+ };
430
+
431
+ const handleCheckForUpdates = async () => {
432
+ setCheckingForUpdates(true);
433
+ setUpdateStatus('Checking for updates...');
434
+ setUpdateAvailable(false);
435
+ setUpdateDownloaded(false);
436
+ setDownloadProgress(0);
437
+
438
+ try {
439
+ await window.electronAPI?.checkForUpdates();
440
+ // Status will be updated by event listeners
441
+ } catch (_error) {
442
+ setUpdateStatus('Error checking for updates');
443
+ setCheckingForUpdates(false);
444
+ }
445
+ };
446
+
447
+ const handleDownloadUpdate = async () => {
448
+ setUpdateStatus('Starting download...');
449
+ try {
450
+ await window.electronAPI?.downloadUpdate();
451
+ // Progress will be updated by event listeners
452
+ } catch (_error) {
453
+ setUpdateStatus('Download failed');
454
+ }
455
+ };
456
+
457
+ const handleInstallUpdate = () => {
458
+ // This will quit the app and install the update
459
+ window.electronAPI?.installUpdate();
460
+ };
461
+
462
+ // SharePoint update source handlers
463
+ const handleSharePointLogin = async () => {
464
+ setSharePointLoginStatus('logging-in');
465
+ try {
466
+ const result = await window.electronAPI?.sharePointLogin();
467
+ setSharePointLoginStatus(result?.success ? 'logged-in' : 'logged-out');
468
+ if (!result?.success && result?.error) {
469
+ setSharePointConnectionResult({ success: false, message: result.error });
470
+ }
471
+ } catch {
472
+ setSharePointLoginStatus('logged-out');
473
+ setSharePointConnectionResult({ success: false, message: 'Login failed' });
474
+ }
475
+ };
476
+
477
+ const handleSharePointLogout = async () => {
478
+ await window.electronAPI?.sharePointLogout();
479
+ setSharePointLoginStatus('logged-out');
480
+ setSharePointConnectionResult(null);
481
+ };
482
+
483
+ const handleTestSharePointConnection = async () => {
484
+ if (!updateSettingsForm.sharePointFolderUrl) return;
485
+ setTestingSharePointConnection(true);
486
+ setSharePointConnectionResult(null);
487
+ try {
488
+ const result = await window.electronAPI?.testSharePointConnection(updateSettingsForm.sharePointFolderUrl);
489
+ setSharePointConnectionResult(result || { success: false, message: 'Test failed' });
490
+ } catch {
491
+ setSharePointConnectionResult({ success: false, message: 'Connection test failed' });
492
+ } finally {
493
+ setTestingSharePointConnection(false);
494
+ }
495
+ };
496
+
497
+ const validateSharePointUrl = (url: string): boolean => {
498
+ if (!url) return false;
499
+ try {
500
+ const parsed = new URL(url);
501
+ return parsed.protocol === 'https:' && parsed.hostname.endsWith('.sharepoint.com');
502
+ } catch {
503
+ return false;
504
+ }
505
+ };
506
+
507
+ // Dictionary handlers
508
+ const handleSaveDictionarySettings = async () => {
509
+ // Save to context (just the form state - no scheduler logic needed)
510
+ updateLocalDictionary(localDictionaryForm);
511
+
512
+ setSaveSuccess(true);
513
+ if (saveSuccessTimeoutRef.current) {
514
+ clearTimeout(saveSuccessTimeoutRef.current);
515
+ }
516
+ saveSuccessTimeoutRef.current = setTimeout(() => {
517
+ setSaveSuccess(false);
518
+ saveSuccessTimeoutRef.current = null;
519
+ }, 2000);
520
+ };
521
+
522
+ // Interactive dictionary retrieval from SharePoint using browser login
523
+ const handleRetrieveDictionary = async () => {
524
+ if (typeof window.electronAPI === 'undefined') return;
525
+ if (!localDictionaryForm.sharePointFileUrl.trim()) return;
526
+
527
+ setSyncingDictionary(true);
528
+ setDictionaryStatus((prev) => prev ? { ...prev, syncError: null, syncInProgress: true } : null);
529
+
530
+ try {
531
+ // Initialize database first if needed
532
+ await window.electronAPI.dictionary.initialize();
533
+
534
+ // Retrieve dictionary using interactive SharePoint auth
535
+ const result = await window.electronAPI.dictionary.retrieveFromSharePoint(
536
+ localDictionaryForm.sharePointFileUrl
537
+ );
538
+
539
+ if (result.success) {
540
+ // Update form with retrieval info
541
+ const now = new Date().toISOString();
542
+ setLocalDictionaryForm((prev) => ({
543
+ ...prev,
544
+ lastRetrievalTime: now,
545
+ lastRetrievalSuccess: true,
546
+ totalEntries: result.entriesImported || 0,
547
+ }));
548
+ // Also save to context
549
+ updateLocalDictionary({
550
+ ...localDictionaryForm,
551
+ lastRetrievalTime: now,
552
+ lastRetrievalSuccess: true,
553
+ totalEntries: result.entriesImported || 0,
554
+ });
555
+ setDictionaryStatus((prev) =>
556
+ prev ? {
557
+ ...prev,
558
+ lastSyncTime: now,
559
+ lastSyncSuccess: true,
560
+ totalEntries: result.entriesImported || 0,
561
+ syncInProgress: false,
562
+ syncError: null,
563
+ } : null
564
+ );
565
+ } else {
566
+ setDictionaryStatus((prev) =>
567
+ prev ? {
568
+ ...prev,
569
+ syncError: result.error || 'Retrieval failed',
570
+ syncInProgress: false,
571
+ lastSyncSuccess: false,
572
+ } : null
573
+ );
574
+ }
575
+ } catch (error) {
576
+ const message = error instanceof Error ? error.message : 'Retrieval failed';
577
+ setDictionaryStatus((prev) =>
578
+ prev ? { ...prev, syncError: message, syncInProgress: false } : null
579
+ );
580
+ } finally {
581
+ setSyncingDictionary(false);
582
+ }
583
+ };
584
+
585
+ const handleSaveBackupSettings = () => {
586
+ updateBackupSettings(backupSettingsForm);
587
+ setSaveSuccess(true);
588
+ if (saveSuccessTimeoutRef.current) {
589
+ clearTimeout(saveSuccessTimeoutRef.current);
590
+ }
591
+ saveSuccessTimeoutRef.current = setTimeout(() => {
592
+ setSaveSuccess(false);
593
+ saveSuccessTimeoutRef.current = null;
594
+ }, 2000);
595
+ };
596
+
597
+ const handleExport = async () => {
598
+ try {
599
+ if (!window.electronAPI?.exportSettings) {
600
+ console.warn('Settings: electronAPI.exportSettings not available');
601
+ return;
602
+ }
603
+
604
+ // Show save dialog
605
+ const result = await window.electronAPI.exportSettings();
606
+
607
+ if (!result.success || result.canceled) {
608
+ return;
609
+ }
610
+
611
+ // Prepare export data
612
+ const exportData = {
613
+ version: '1.0',
614
+ exportDate: new Date().toISOString(),
615
+ data: {
616
+ userSettings: settings,
617
+ themeSettings: {
618
+ theme,
619
+ density,
620
+ accentColor,
621
+ blur,
622
+ },
623
+ sessions,
624
+ globalStats: stats,
625
+ },
626
+ };
627
+
628
+ // Save data to selected file
629
+ const saveResult = await window.electronAPI.saveExportData(result.filePath!, exportData);
630
+
631
+ if (saveResult.success) {
632
+ setSaveSuccess(true);
633
+ if (saveSuccessTimeoutRef.current) {
634
+ clearTimeout(saveSuccessTimeoutRef.current);
635
+ }
636
+ saveSuccessTimeoutRef.current = setTimeout(() => {
637
+ setSaveSuccess(false);
638
+ saveSuccessTimeoutRef.current = null;
639
+ }, 2000);
640
+ } else {
641
+ logger.error('Failed to save export data:', saveResult.error);
642
+ }
643
+ } catch (_error) {
644
+ logger.error('Export failed:', _error);
645
+ }
646
+ };
647
+
648
+ const handleImport = async () => {
649
+ try {
650
+ if (!window.electronAPI?.importSettings) {
651
+ console.warn('Settings: electronAPI.importSettings not available');
652
+ return;
653
+ }
654
+
655
+ // Show open dialog and read data
656
+ const result = await window.electronAPI.importSettings();
657
+
658
+ if (!result.success || result.canceled) {
659
+ return;
660
+ }
661
+
662
+ // Validate import data
663
+ if (!result.data?.version || !result.data?.data) {
664
+ logger.error('Invalid import file format');
665
+ return;
666
+ }
667
+
668
+ const importedData = result.data.data;
669
+
670
+ // Apply imported settings (would need to add methods to each context)
671
+ if (importedData.userSettings) {
672
+ updateSettings(importedData.userSettings);
673
+ }
674
+
675
+ // Show success message
676
+ setSaveSuccess(true);
677
+ if (saveSuccessTimeoutRef.current) {
678
+ clearTimeout(saveSuccessTimeoutRef.current);
679
+ }
680
+ saveSuccessTimeoutRef.current = setTimeout(() => {
681
+ setSaveSuccess(false);
682
+ saveSuccessTimeoutRef.current = null;
683
+ // Reload page to apply all changes
684
+ window.location.reload();
685
+ }, 2000);
686
+ } catch (_error) {
687
+ logger.error('Import failed:', _error);
688
+ }
689
+ };
690
+
691
+ const {
692
+ theme,
693
+ setTheme,
694
+ accentColor,
695
+ setAccentColor,
696
+ customAccentColor,
697
+ setCustomAccentColor,
698
+ customPrimaryColor,
699
+ setCustomPrimaryColor,
700
+ customBackgroundColor,
701
+ setCustomBackgroundColor,
702
+ customHeaderColor,
703
+ setCustomHeaderColor,
704
+ customSidebarColor,
705
+ setCustomSidebarColor,
706
+ customBorderColor,
707
+ setCustomBorderColor,
708
+ useCustomColors,
709
+ setUseCustomColors,
710
+ density,
711
+ setDensity,
712
+ animations,
713
+ setAnimations,
714
+ blur,
715
+ setBlur,
716
+ reduceMotion,
717
+ setReduceMotion,
718
+ fontSize,
719
+ setFontSize,
720
+ fontFamily,
721
+ setFontFamily,
722
+ fontWeight,
723
+ setFontWeight,
724
+ fontStyle,
725
+ setFontStyle,
726
+ letterSpacing,
727
+ setLetterSpacing,
728
+ lineHeight,
729
+ setLineHeight,
730
+ } = useTheme();
731
+
732
+ const [activeColorPicker, setActiveColorPicker] = useState<string | null>(null);
733
+ const [tempColor, setTempColor] = useState('#000000');
734
+
735
+ // URL validation states
736
+ const [urlValidation, setUrlValidation] = useState<{
737
+ valid: boolean;
738
+ issues: string[];
739
+ warnings: string[];
740
+ } | null>(null);
741
+ const [showUrlWarning, setShowUrlWarning] = useState(false);
742
+
743
+ const containerVariants = {
744
+ hidden: { opacity: 0 },
745
+ visible: {
746
+ opacity: 1,
747
+ transition: { staggerChildren: 0.1 },
748
+ },
749
+ };
750
+
751
+ const itemVariants = {
752
+ hidden: { opacity: 0, x: -20 },
753
+ visible: {
754
+ opacity: 1,
755
+ x: 0,
756
+ transition: { duration: 0.3 },
757
+ },
758
+ };
759
+
760
+ return (
761
+ <motion.div
762
+ className="p-6 max-w-7xl mx-auto"
763
+ variants={containerVariants}
764
+ initial="hidden"
765
+ animate="visible"
766
+ >
767
+ <div className="flex gap-8">
768
+ <motion.aside className="w-64 shrink-0" variants={itemVariants}>
769
+ <div className="sticky top-6">
770
+ {/* Search */}
771
+ <div className="relative mb-6">
772
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
773
+ <input
774
+ type="text"
775
+ placeholder="Search settings..."
776
+ className="w-full pl-10 pr-3 py-2 text-sm rounded-lg border border-border bg-background text-muted-foreground placeholder:text-muted-foreground/50 hover:border-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20 transition-colors"
777
+ />
778
+ </div>
779
+
780
+ {/* Navigation */}
781
+ <nav className="space-y-6">
782
+ {settingsSections.map((group, groupIndex) => (
783
+ <div key={group.group}>
784
+ <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
785
+ {group.group}
786
+ </h3>
787
+ <div className="space-y-0.5">
788
+ {group.items.map((item) => {
789
+ const Icon = item.icon;
790
+ return (
791
+ <button
792
+ key={item.id}
793
+ onClick={() => setActiveSection(item.id)}
794
+ className={cn(
795
+ 'w-full flex items-start gap-3 px-3 py-2.5 rounded-lg transition-all group',
796
+ activeSection === item.id
797
+ ? 'bg-primary/10 text-primary'
798
+ : 'hover:bg-muted text-foreground'
799
+ )}
800
+ >
801
+ <Icon
802
+ className={cn(
803
+ 'w-4 h-4 mt-0.5 transition-colors',
804
+ activeSection === item.id
805
+ ? 'text-primary'
806
+ : 'text-muted-foreground group-hover:text-foreground'
807
+ )}
808
+ />
809
+ <div className="text-left">
810
+ <div className="text-sm font-medium">{item.label}</div>
811
+ <div className="text-xs text-muted-foreground mt-0.5">
812
+ {item.description}
813
+ </div>
814
+ </div>
815
+ </button>
816
+ );
817
+ })}
818
+ </div>
819
+ {groupIndex < settingsSections.length - 1 && (
820
+ <div className="mt-4 border-b border-border" />
821
+ )}
822
+ </div>
823
+ ))}
824
+ </nav>
825
+ </div>
826
+ </motion.aside>
827
+
828
+ <motion.main className="flex-1 max-w-4xl" variants={itemVariants}>
829
+ {activeSection === 'profile' && (
830
+ <div className="space-y-6">
831
+ <div>
832
+ <h2 className="text-2xl font-bold">Profile</h2>
833
+ <p className="text-muted-foreground mt-1">
834
+ Manage your personal information and account details
835
+ </p>
836
+ </div>
837
+ <div className="space-y-4">
838
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
839
+ <Input
840
+ label="First Name"
841
+ value={profileForm.firstName}
842
+ onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
843
+ />
844
+ <Input
845
+ label="Last Name"
846
+ value={profileForm.lastName}
847
+ onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
848
+ />
849
+ </div>
850
+ <Input
851
+ label="Email"
852
+ type="email"
853
+ value={profileForm.email}
854
+ onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
855
+ />
856
+ <div className="flex justify-end gap-2">
857
+ <Button
858
+ variant="outline"
859
+ icon={<Download className="w-4 h-4" />}
860
+ onClick={handleExport}
861
+ >
862
+ Export Settings
863
+ </Button>
864
+ <Button
865
+ variant="outline"
866
+ icon={<Download className="w-4 h-4" style={{ transform: 'rotate(180deg)' }} />}
867
+ onClick={handleImport}
868
+ >
869
+ Import Settings
870
+ </Button>
871
+ <Button
872
+ onClick={handleSaveSettings}
873
+ showSuccess={saveSuccess}
874
+ icon={<Save className="w-4 h-4" />}
875
+ >
876
+ Save Changes
877
+ </Button>
878
+ </div>
879
+ </div>
880
+
881
+ {/* Reset Statistics */}
882
+ <div className="pt-6 border-t border-border">
883
+ <div className="flex items-center justify-between">
884
+ <div>
885
+ <h3 className="font-semibold text-destructive">Reset Analytics Statistics</h3>
886
+ <p className="text-sm text-muted-foreground mt-1">
887
+ Permanently delete all historical analytics data. This action cannot be undone.
888
+ </p>
889
+ </div>
890
+ <Button
891
+ variant="destructive"
892
+ onClick={() => setShowResetStatsDialog(true)}
893
+ icon={<RotateCcw className="w-4 h-4" />}
894
+ >
895
+ Reset Stats
896
+ </Button>
897
+ </div>
898
+ </div>
899
+ </div>
900
+ )}
901
+
902
+ {activeSection === 'appearance' && (
903
+ <div className="space-y-6">
904
+ {/* Header */}
905
+ <div>
906
+ <h2 className="text-2xl font-bold">Appearance</h2>
907
+ <p className="text-muted-foreground mt-1">
908
+ Customize the visual appearance of the application
909
+ </p>
910
+ </div>
911
+
912
+ {/* Theme & Display Card */}
913
+ <div className="rounded-xl border border-border bg-card p-6 space-y-6">
914
+ <div className="flex items-center gap-3">
915
+ <div className="p-2.5 rounded-lg bg-primary/10">
916
+ <Palette className="w-5 h-5 text-primary" />
917
+ </div>
918
+ <div>
919
+ <h3 className="font-semibold">Theme & Display</h3>
920
+ <p className="text-sm text-muted-foreground">Choose your preferred theme and interface density</p>
921
+ </div>
922
+ </div>
923
+
924
+ <div className="space-y-5">
925
+ {/* Theme Mode */}
926
+ <div className="flex flex-col sm:flex-row sm:items-center gap-3">
927
+ <div className="flex-1 min-w-0">
928
+ <label className="text-sm font-medium">Theme Mode</label>
929
+ <p className="text-xs text-muted-foreground mt-0.5">
930
+ Select light, dark, or follow system preference
931
+ </p>
932
+ </div>
933
+ <SegmentedControl
934
+ value={theme}
935
+ onValueChange={setTheme}
936
+ options={[
937
+ { value: 'light' as const, label: 'Light', icon: Sun },
938
+ { value: 'dark' as const, label: 'Dark', icon: Moon },
939
+ { value: 'system' as const, label: 'System', icon: Laptop },
940
+ ]}
941
+ size="sm"
942
+ />
943
+ </div>
944
+
945
+ <div className="border-t border-border" />
946
+
947
+ {/* Interface Density */}
948
+ <div className="flex flex-col sm:flex-row sm:items-center gap-3">
949
+ <div className="flex-1 min-w-0">
950
+ <label className="text-sm font-medium">Interface Density</label>
951
+ <p className="text-xs text-muted-foreground mt-0.5">
952
+ Adjust spacing and element sizes
953
+ </p>
954
+ </div>
955
+ <SegmentedControl
956
+ value={density}
957
+ onValueChange={setDensity}
958
+ options={[
959
+ { value: 'comfortable' as const, label: 'Comfortable' },
960
+ { value: 'compact' as const, label: 'Compact' },
961
+ { value: 'minimal' as const, label: 'Minimal' },
962
+ ]}
963
+ size="sm"
964
+ />
965
+ </div>
966
+ </div>
967
+ </div>
968
+
969
+ {/* Colors Card */}
970
+ <div className="rounded-xl border border-border bg-card p-6 space-y-6">
971
+ <div className="flex items-center gap-3">
972
+ <div className="p-2.5 rounded-lg bg-primary/10">
973
+ <Sparkles className="w-5 h-5 text-primary" />
974
+ </div>
975
+ <div>
976
+ <h3 className="font-semibold">Colors</h3>
977
+ <p className="text-sm text-muted-foreground">Customize your accent color and theme</p>
978
+ </div>
979
+ </div>
980
+
981
+ {/* Accent Color */}
982
+ <div>
983
+ <label className="text-sm font-medium mb-3 block">Accent Color</label>
984
+ <div className="flex flex-wrap gap-3">
985
+ {[
986
+ { name: 'blue' as const, label: 'Blue', color: '#3b82f6' },
987
+ { name: 'purple' as const, label: 'Purple', color: '#8b5cf6' },
988
+ { name: 'green' as const, label: 'Green', color: '#22c55e' },
989
+ { name: 'orange' as const, label: 'Orange', color: '#f97316' },
990
+ { name: 'pink' as const, label: 'Pink', color: '#ec4899' },
991
+ { name: 'cyan' as const, label: 'Cyan', color: '#06b6d4' },
992
+ { name: 'indigo' as const, label: 'Indigo', color: '#6366f1' },
993
+ ].map((colorOption) => (
994
+ <motion.button
995
+ key={colorOption.name}
996
+ onClick={() => setAccentColor(colorOption.name)}
997
+ whileHover={{ scale: 1.05 }}
998
+ whileTap={{ scale: 0.95 }}
999
+ className={cn(
1000
+ 'group relative flex items-center gap-2 px-3 py-2 rounded-full border-2 transition-all',
1001
+ accentColor === colorOption.name
1002
+ ? 'border-primary bg-primary/10 shadow-sm'
1003
+ : 'border-border hover:border-muted-foreground bg-background'
1004
+ )}
1005
+ aria-label={`Select ${colorOption.label} accent`}
1006
+ >
1007
+ <div
1008
+ className="w-4 h-4 rounded-full shadow-inner"
1009
+ style={{ backgroundColor: colorOption.color }}
1010
+ />
1011
+ <span className="text-xs font-medium">{colorOption.label}</span>
1012
+ {accentColor === colorOption.name && (
1013
+ <Check className="w-3 h-3 text-primary" />
1014
+ )}
1015
+ </motion.button>
1016
+ ))}
1017
+ <motion.button
1018
+ onClick={() => {
1019
+ setAccentColor('custom');
1020
+ setTempColor(customAccentColor);
1021
+ setActiveColorPicker('accent');
1022
+ }}
1023
+ whileHover={{ scale: 1.05 }}
1024
+ whileTap={{ scale: 0.95 }}
1025
+ className={cn(
1026
+ 'group relative flex items-center gap-2 px-3 py-2 rounded-full border-2 transition-all',
1027
+ accentColor === 'custom'
1028
+ ? 'border-primary bg-primary/10 shadow-sm'
1029
+ : 'border-border hover:border-muted-foreground bg-background'
1030
+ )}
1031
+ aria-label="Select custom accent color"
1032
+ >
1033
+ <div
1034
+ className="w-4 h-4 rounded-full"
1035
+ style={{
1036
+ background: accentColor === 'custom'
1037
+ ? customAccentColor
1038
+ : 'conic-gradient(from 180deg, #ef4444, #f59e0b, #22c55e, #3b82f6, #8b5cf6, #ec4899, #ef4444)',
1039
+ }}
1040
+ />
1041
+ <span className="text-xs font-medium">Custom</span>
1042
+ {accentColor === 'custom' && (
1043
+ <Check className="w-3 h-3 text-primary" />
1044
+ )}
1045
+ </motion.button>
1046
+ </div>
1047
+ </div>
1048
+
1049
+ {/* Custom Theme Colors - Collapsible */}
1050
+ <div className="border-t border-border pt-5">
1051
+ <div
1052
+ role="button"
1053
+ tabIndex={0}
1054
+ onClick={() => setUseCustomColors(!useCustomColors)}
1055
+ onKeyDown={(e) => {
1056
+ if (e.key === 'Enter' || e.key === ' ') {
1057
+ e.preventDefault();
1058
+ setUseCustomColors(!useCustomColors);
1059
+ }
1060
+ }}
1061
+ className="flex items-center justify-between w-full group cursor-pointer"
1062
+ >
1063
+ <div className="flex items-center gap-3">
1064
+ <span className="text-sm font-medium">Advanced: Custom Theme Colors</span>
1065
+ <span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground uppercase tracking-wide">
1066
+ Advanced
1067
+ </span>
1068
+ </div>
1069
+ <div className="flex items-center gap-3">
1070
+ <Switch.Root
1071
+ checked={useCustomColors}
1072
+ onCheckedChange={setUseCustomColors}
1073
+ className={cn(
1074
+ 'relative w-10 h-5 rounded-full transition-colors',
1075
+ useCustomColors ? 'bg-primary' : 'bg-input border border-border'
1076
+ )}
1077
+ onClick={(e) => e.stopPropagation()}
1078
+ >
1079
+ <Switch.Thumb
1080
+ className={cn(
1081
+ 'block w-4 h-4 bg-background rounded-full shadow-sm transition-transform',
1082
+ useCustomColors ? 'translate-x-5' : 'translate-x-0.5'
1083
+ )}
1084
+ />
1085
+ </Switch.Root>
1086
+ <ChevronDown
1087
+ className={cn(
1088
+ 'w-4 h-4 text-muted-foreground transition-transform',
1089
+ useCustomColors && 'rotate-180'
1090
+ )}
1091
+ />
1092
+ </div>
1093
+ </div>
1094
+
1095
+ {useCustomColors && (
1096
+ <motion.div
1097
+ initial={{ height: 0, opacity: 0 }}
1098
+ animate={{ height: 'auto', opacity: 1 }}
1099
+ exit={{ height: 0, opacity: 0 }}
1100
+ transition={{ duration: 0.2 }}
1101
+ className="mt-4"
1102
+ >
1103
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
1104
+ {[
1105
+ { key: 'primary', label: 'Primary', color: customPrimaryColor, setter: setCustomPrimaryColor, showTextHint: true },
1106
+ { key: 'background', label: 'Background', color: customBackgroundColor, setter: setCustomBackgroundColor, showTextHint: true },
1107
+ { key: 'header', label: 'Header', color: customHeaderColor, setter: setCustomHeaderColor, showTextHint: true },
1108
+ { key: 'sidebar', label: 'Sidebar', color: customSidebarColor, setter: setCustomSidebarColor, showTextHint: true },
1109
+ { key: 'border', label: 'Borders', color: customBorderColor, setter: setCustomBorderColor },
1110
+ ].map((item) => (
1111
+ <div key={item.key}>
1112
+ <label className="text-xs text-muted-foreground mb-1.5 block">{item.label}</label>
1113
+ <button
1114
+ aria-label={`Select ${item.label.toLowerCase()} color`}
1115
+ onClick={() => {
1116
+ setTempColor(item.color);
1117
+ setActiveColorPicker(item.key);
1118
+ }}
1119
+ className="w-full h-10 rounded-lg border border-border flex items-center justify-center gap-2 hover:bg-muted/50 transition-colors group"
1120
+ >
1121
+ <div
1122
+ className="w-6 h-6 rounded-md shadow-inner group-hover:scale-110 transition-transform"
1123
+ style={{ backgroundColor: item.color }}
1124
+ />
1125
+ </button>
1126
+ {item.showTextHint && (
1127
+ <p className="text-[10px] text-muted-foreground mt-1 text-center">
1128
+ Text: {getContrastTextColor(item.color) === '#FFFFFF' ? 'White' : 'Black'}
1129
+ </p>
1130
+ )}
1131
+ </div>
1132
+ ))}
1133
+ </div>
1134
+ </motion.div>
1135
+ )}
1136
+ </div>
1137
+ </div>
1138
+
1139
+ {/* Visual Effects Card */}
1140
+ <div className="rounded-xl border border-border bg-card p-6 space-y-4">
1141
+ <div className="flex items-center gap-3 mb-2">
1142
+ <div className="p-2.5 rounded-lg bg-primary/10">
1143
+ <Zap className="w-5 h-5 text-primary" />
1144
+ </div>
1145
+ <div>
1146
+ <h3 className="font-semibold">Visual Effects</h3>
1147
+ <p className="text-sm text-muted-foreground">Control animations and visual enhancements</p>
1148
+ </div>
1149
+ </div>
1150
+
1151
+ <div className="space-y-1 divide-y divide-border">
1152
+ <SettingRow
1153
+ icon={Sparkles}
1154
+ title="Glass morphism effects"
1155
+ description="Blur and transparency for a modern look"
1156
+ >
1157
+ <Switch.Root
1158
+ checked={blur}
1159
+ onCheckedChange={setBlur}
1160
+ className={cn(
1161
+ 'relative w-10 h-5 rounded-full transition-colors',
1162
+ blur ? 'bg-primary' : 'bg-input border border-border'
1163
+ )}
1164
+ >
1165
+ <Switch.Thumb
1166
+ className={cn(
1167
+ 'block w-4 h-4 bg-background rounded-full shadow-sm transition-transform',
1168
+ blur ? 'translate-x-5' : 'translate-x-0.5'
1169
+ )}
1170
+ />
1171
+ </Switch.Root>
1172
+ </SettingRow>
1173
+
1174
+ <SettingRow
1175
+ icon={Zap}
1176
+ title="Smooth animations"
1177
+ description="Transitions and micro-interactions"
1178
+ >
1179
+ <Switch.Root
1180
+ checked={animations}
1181
+ onCheckedChange={setAnimations}
1182
+ className={cn(
1183
+ 'relative w-10 h-5 rounded-full transition-colors',
1184
+ animations ? 'bg-primary' : 'bg-input border border-border'
1185
+ )}
1186
+ >
1187
+ <Switch.Thumb
1188
+ className={cn(
1189
+ 'block w-4 h-4 bg-background rounded-full shadow-sm transition-transform',
1190
+ animations ? 'translate-x-5' : 'translate-x-0.5'
1191
+ )}
1192
+ />
1193
+ </Switch.Root>
1194
+ </SettingRow>
1195
+
1196
+ <SettingRow
1197
+ icon={Accessibility}
1198
+ title="Reduce motion"
1199
+ description="Minimize animations for accessibility"
1200
+ >
1201
+ <Switch.Root
1202
+ checked={reduceMotion}
1203
+ onCheckedChange={setReduceMotion}
1204
+ className={cn(
1205
+ 'relative w-10 h-5 rounded-full transition-colors',
1206
+ reduceMotion ? 'bg-primary' : 'bg-input border border-border'
1207
+ )}
1208
+ >
1209
+ <Switch.Thumb
1210
+ className={cn(
1211
+ 'block w-4 h-4 bg-background rounded-full shadow-sm transition-transform',
1212
+ reduceMotion ? 'translate-x-5' : 'translate-x-0.5'
1213
+ )}
1214
+ />
1215
+ </Switch.Root>
1216
+ </SettingRow>
1217
+ </div>
1218
+
1219
+ <p className="text-xs text-muted-foreground pt-2">
1220
+ Disabling visual effects can improve performance on slower systems.
1221
+ </p>
1222
+ </div>
1223
+ </div>
1224
+ )}
1225
+
1226
+ {activeSection === 'typography' && (
1227
+ <div className="space-y-6">
1228
+ {/* Header */}
1229
+ <div>
1230
+ <h2 className="text-2xl font-bold">Typography</h2>
1231
+ <p className="text-muted-foreground mt-1">
1232
+ Customize fonts and text styling throughout the application
1233
+ </p>
1234
+ </div>
1235
+
1236
+ {/* Live Preview */}
1237
+ <div className="p-6 rounded-lg border border-border bg-muted/30">
1238
+ <div className="space-y-3">
1239
+ {}
1240
+ <h3
1241
+ className="text-lg font-semibold"
1242
+ style={{
1243
+ fontSize: `${fontSize * 1.2}px`,
1244
+ fontFamily: fontFamily,
1245
+ fontWeight: fontWeight,
1246
+ fontStyle: fontStyle,
1247
+ letterSpacing: `${letterSpacing}em`,
1248
+ lineHeight: lineHeight,
1249
+ }}
1250
+ >
1251
+ Preview: Main Heading
1252
+ </h3>
1253
+ {}
1254
+ <p
1255
+ style={{
1256
+ fontSize: `${fontSize}px`,
1257
+ fontFamily: fontFamily,
1258
+ fontWeight: fontWeight,
1259
+ fontStyle: fontStyle,
1260
+ letterSpacing: `${letterSpacing}em`,
1261
+ lineHeight: lineHeight,
1262
+ }}
1263
+ >
1264
+ This is a preview of your typography settings. The quick brown fox jumps over
1265
+ the lazy dog. Adjust the settings below to see how your text will appear
1266
+ throughout the application.
1267
+ </p>
1268
+ {}
1269
+ <p
1270
+ className="text-muted-foreground"
1271
+ style={{
1272
+ fontSize: `${fontSize * 0.875}px`,
1273
+ fontFamily: fontFamily,
1274
+ letterSpacing: `${letterSpacing}em`,
1275
+ lineHeight: lineHeight,
1276
+ }}
1277
+ >
1278
+ Secondary text appears like this, used for descriptions and supporting content.
1279
+ </p>
1280
+ </div>
1281
+ </div>
1282
+
1283
+ {/* Presets */}
1284
+ <div>
1285
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
1286
+ Quick Presets
1287
+ </h3>
1288
+ <div className="grid grid-cols-4 gap-2">
1289
+ <button
1290
+ onClick={() => {
1291
+ setFontSize(16);
1292
+ setFontFamily("'Inter', sans-serif");
1293
+ setFontWeight('400');
1294
+ setLetterSpacing(0.02);
1295
+ setLineHeight(1.7);
1296
+ }}
1297
+ aria-label="Apply reading typography preset"
1298
+ className="px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors"
1299
+ >
1300
+ Reading
1301
+ </button>
1302
+ <button
1303
+ onClick={() => {
1304
+ setFontSize(14);
1305
+ setFontFamily('system-ui');
1306
+ setFontWeight('400');
1307
+ setLetterSpacing(0);
1308
+ setLineHeight(1.4);
1309
+ }}
1310
+ aria-label="Apply compact typography preset"
1311
+ className="px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors"
1312
+ >
1313
+ Compact
1314
+ </button>
1315
+ <button
1316
+ onClick={() => {
1317
+ setFontSize(18);
1318
+ setFontFamily("'Poppins', sans-serif");
1319
+ setFontWeight('500');
1320
+ setLetterSpacing(0.01);
1321
+ setLineHeight(1.6);
1322
+ }}
1323
+ aria-label="Apply presentation typography preset"
1324
+ className="px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors"
1325
+ >
1326
+ Presentation
1327
+ </button>
1328
+ <button
1329
+ onClick={() => {
1330
+ setFontSize(15);
1331
+ setFontFamily('system-ui');
1332
+ setFontWeight('400');
1333
+ setFontStyle('normal');
1334
+ setLetterSpacing(0);
1335
+ setLineHeight(1.5);
1336
+ }}
1337
+ aria-label="Apply default typography preset"
1338
+ className="px-3 py-2 text-sm rounded-lg border border-border hover:bg-muted transition-colors"
1339
+ >
1340
+ Default
1341
+ </button>
1342
+ </div>
1343
+ </div>
1344
+
1345
+ {/* Settings */}
1346
+ <div className="grid grid-cols-2 gap-6">
1347
+ {/* Font Settings */}
1348
+ <div className="space-y-4">
1349
+ <h3 className="font-medium">Font</h3>
1350
+
1351
+ <div>
1352
+ <label
1353
+ htmlFor="font-family-select"
1354
+ className="text-sm text-muted-foreground mb-2 block"
1355
+ >
1356
+ Family
1357
+ </label>
1358
+ <select
1359
+ id="font-family-select"
1360
+ value={fontFamily}
1361
+ onChange={(e) => setFontFamily(e.target.value)}
1362
+ className="w-full px-3 py-2 rounded-lg border border-border bg-background text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
1363
+ >
1364
+ <option value="system-ui">System Default</option>
1365
+ <option value="'Inter', sans-serif">Inter</option>
1366
+ <option value="'Roboto', sans-serif">Roboto</option>
1367
+ <option value="'Open Sans', sans-serif">Open Sans</option>
1368
+ <option value="'Lato', sans-serif">Lato</option>
1369
+ <option value="'Poppins', sans-serif">Poppins</option>
1370
+ <option value="'SF Pro Display', sans-serif">SF Pro</option>
1371
+ <option value="'Segoe UI', sans-serif">Segoe UI</option>
1372
+ <option value="'JetBrains Mono', monospace">JetBrains Mono</option>
1373
+ <option value="'Fira Code', monospace">Fira Code</option>
1374
+ <option value="'Webdings', fantasy">Webdings 🎉</option>
1375
+ </select>
1376
+ </div>
1377
+
1378
+ <div>
1379
+ <label className="text-sm text-muted-foreground mb-2 block">Size</label>
1380
+ <div className="flex items-center gap-3">
1381
+ <input
1382
+ type="range"
1383
+ min="12"
1384
+ max="20"
1385
+ value={fontSize}
1386
+ onChange={(e) => setFontSize(Number(e.target.value))}
1387
+ aria-label="Font size"
1388
+ className="flex-1"
1389
+ />
1390
+ <span className="text-sm font-mono w-12 text-center bg-muted rounded px-2 py-1">
1391
+ {fontSize}px
1392
+ </span>
1393
+ </div>
1394
+ </div>
1395
+
1396
+ <div>
1397
+ <label className="text-sm text-foreground mb-2 block">Weight</label>
1398
+ <div className="grid grid-cols-2 gap-2">
1399
+ {[
1400
+ { value: '300', label: 'Light' },
1401
+ { value: '400', label: 'Regular' },
1402
+ { value: '500', label: 'Medium' },
1403
+ { value: '600', label: 'Semibold' },
1404
+ { value: '700', label: 'Bold' },
1405
+ ].map((option) => (
1406
+ <button
1407
+ key={option.value}
1408
+ onClick={() => setFontWeight(option.value)}
1409
+ aria-label={`Set font weight to ${option.label}`}
1410
+ className={cn(
1411
+ 'px-2 py-1.5 text-sm rounded transition-colors',
1412
+ fontWeight === option.value
1413
+ ? 'bg-primary text-primary-foreground'
1414
+ : 'hover:bg-muted'
1415
+ )}
1416
+ >
1417
+ {option.label}
1418
+ </button>
1419
+ ))}
1420
+ </div>
1421
+ </div>
1422
+
1423
+ <div>
1424
+ <label className="text-sm text-foreground mb-2 block">Style</label>
1425
+ <div className="grid grid-cols-2 gap-2">
1426
+ <button
1427
+ onClick={() => setFontStyle('normal')}
1428
+ aria-label="Set font style to normal"
1429
+ className={cn(
1430
+ 'px-3 py-2 rounded-lg border transition-all text-sm',
1431
+ fontStyle === 'normal'
1432
+ ? 'border-primary bg-primary/10'
1433
+ : 'border-border hover:bg-muted'
1434
+ )}
1435
+ >
1436
+ Normal
1437
+ </button>
1438
+ <button
1439
+ onClick={() => setFontStyle('italic')}
1440
+ aria-label="Set font style to italic"
1441
+ className={cn(
1442
+ 'px-3 py-2 rounded-lg border transition-all text-sm italic',
1443
+ fontStyle === 'italic'
1444
+ ? 'border-primary bg-primary/10'
1445
+ : 'border-border hover:bg-muted'
1446
+ )}
1447
+ >
1448
+ Italic
1449
+ </button>
1450
+ </div>
1451
+ </div>
1452
+ </div>
1453
+
1454
+ {/* Spacing Settings */}
1455
+ <div className="space-y-4">
1456
+ <h3 className="font-medium">Spacing</h3>
1457
+
1458
+ <div>
1459
+ <label className="text-sm text-foreground mb-2 block">Letter Spacing</label>
1460
+ <div className="flex items-center gap-3">
1461
+ <span className="text-xs text-muted-foreground">Tight</span>
1462
+ <input
1463
+ type="range"
1464
+ min="-0.05"
1465
+ max="0.1"
1466
+ step="0.01"
1467
+ value={letterSpacing}
1468
+ onChange={(e) => setLetterSpacing(Number(e.target.value))}
1469
+ aria-label="Letter spacing"
1470
+ className="flex-1"
1471
+ />
1472
+ <span className="text-xs text-muted-foreground">Wide</span>
1473
+ <span className="text-sm font-mono w-16 text-center bg-muted rounded px-2 py-1">
1474
+ {letterSpacing.toFixed(2)}em
1475
+ </span>
1476
+ </div>
1477
+ </div>
1478
+
1479
+ <div>
1480
+ <label className="text-sm text-foreground mb-2 block">Line Height</label>
1481
+ <div className="flex items-center gap-3">
1482
+ <span className="text-xs text-muted-foreground">Compact</span>
1483
+ <input
1484
+ type="range"
1485
+ min="1"
1486
+ max="2"
1487
+ step="0.1"
1488
+ value={lineHeight}
1489
+ onChange={(e) => setLineHeight(Number(e.target.value))}
1490
+ aria-label="Line height"
1491
+ className="flex-1"
1492
+ />
1493
+ <span className="text-xs text-muted-foreground">Spacious</span>
1494
+ <span className="text-sm font-mono w-12 text-center bg-muted rounded px-2 py-1">
1495
+ {lineHeight.toFixed(1)}
1496
+ </span>
1497
+ </div>
1498
+ </div>
1499
+ </div>
1500
+ </div>
1501
+ </div>
1502
+ )}
1503
+
1504
+ {activeSection === 'display' && (
1505
+ <div className="space-y-6">
1506
+ {/* Header */}
1507
+ <div>
1508
+ <h2 className="text-2xl font-bold">Display</h2>
1509
+ <p className="text-muted-foreground mt-1">
1510
+ Configure monitor settings for document comparison
1511
+ </p>
1512
+ </div>
1513
+
1514
+ {/* Monitor Selection */}
1515
+ <div className="space-y-4">
1516
+ <div>
1517
+ <h3 className="font-medium mb-4">Monitor for Comparing Files</h3>
1518
+ <p className="text-sm text-muted-foreground mb-4">
1519
+ Select which monitor to use when comparing documents side by side
1520
+ </p>
1521
+
1522
+ <div className="flex flex-col sm:flex-row gap-4 items-start">
1523
+ <div className="flex-1">
1524
+ <label htmlFor="comparison-monitor" className="block text-sm font-medium mb-2">
1525
+ Select Monitor
1526
+ </label>
1527
+ <select
1528
+ id="comparison-monitor"
1529
+ value={displaySettingsForm?.comparisonMonitorId ?? 0}
1530
+ onChange={(e) => setDisplaySettingsForm((prev) => ({
1531
+ ...prev,
1532
+ comparisonMonitorId: parseInt(e.target.value, 10),
1533
+ }))}
1534
+ className="w-full px-3 py-2 rounded-md border border-input bg-background"
1535
+ >
1536
+ {availableDisplays.length > 0 ? (
1537
+ availableDisplays.map((display) => (
1538
+ <option key={display.id} value={display.id}>
1539
+ {display.label} ({display.workArea.width} x {display.workArea.height})
1540
+ </option>
1541
+ ))
1542
+ ) : (
1543
+ <option value={0}>Primary (loading...)</option>
1544
+ )}
1545
+ </select>
1546
+ </div>
1547
+
1548
+ <div className="pt-6">
1549
+ <Button
1550
+ variant="outline"
1551
+ onClick={async () => {
1552
+ if (typeof window.electronAPI === 'undefined' || !window.electronAPI.display) return;
1553
+ setIdentifyingMonitors(true);
1554
+ try {
1555
+ await window.electronAPI.display.identifyMonitors();
1556
+ } finally {
1557
+ // The identification windows close automatically after 3 seconds
1558
+ setTimeout(() => setIdentifyingMonitors(false), 3000);
1559
+ }
1560
+ }}
1561
+ disabled={identifyingMonitors}
1562
+ >
1563
+ {identifyingMonitors ? (
1564
+ <>
1565
+ <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
1566
+ Identifying...
1567
+ </>
1568
+ ) : (
1569
+ <>
1570
+ <Monitor className="w-4 h-4 mr-2" />
1571
+ Identify Monitors
1572
+ </>
1573
+ )}
1574
+ </Button>
1575
+ </div>
1576
+ </div>
1577
+
1578
+ <p className="text-xs text-muted-foreground mt-2">
1579
+ Click "Identify Monitors" to display a number on each connected monitor for 3 seconds
1580
+ </p>
1581
+ </div>
1582
+
1583
+ {/* Save Button */}
1584
+ <div className="pt-4 border-t">
1585
+ <Button
1586
+ onClick={() => {
1587
+ updateDisplaySettings(displaySettingsForm);
1588
+ setSaveSuccess(true);
1589
+ if (saveSuccessTimeoutRef.current) {
1590
+ clearTimeout(saveSuccessTimeoutRef.current);
1591
+ }
1592
+ saveSuccessTimeoutRef.current = setTimeout(() => {
1593
+ setSaveSuccess(false);
1594
+ }, 2000);
1595
+ }}
1596
+ showSuccess={saveSuccess}
1597
+ >
1598
+ <Save className="w-4 h-4 mr-2" />
1599
+ Save Display Settings
1600
+ </Button>
1601
+ </div>
1602
+ </div>
1603
+ </div>
1604
+ )}
1605
+
1606
+ {activeSection === 'language' && (
1607
+ <div className="space-y-6">
1608
+ <div>
1609
+ <h2 className="text-2xl font-bold">Language & Region</h2>
1610
+ <p className="text-muted-foreground mt-1">
1611
+ Set your language, timezone, and regional preferences
1612
+ </p>
1613
+ </div>
1614
+ <div className="space-y-4">
1615
+ <div>
1616
+ <label htmlFor="language-select" className="block text-sm font-medium mb-2">
1617
+ Language
1618
+ </label>
1619
+ <select
1620
+ id="language-select"
1621
+ value={languageForm}
1622
+ onChange={(e) => setLanguageForm(e.target.value)}
1623
+ className="w-full px-3 py-2 rounded-md border border-input bg-background"
1624
+ >
1625
+ <option>English (US)</option>
1626
+ <option>Español (Spanish)</option>
1627
+ <option>中文 (Mandarin Chinese)</option>
1628
+ </select>
1629
+ </div>
1630
+
1631
+ <div>
1632
+ <label htmlFor="timezone-select" className="block text-sm font-medium mb-2">
1633
+ Timezone
1634
+ </label>
1635
+ <select
1636
+ id="timezone-select"
1637
+ value={timezoneForm}
1638
+ onChange={(e) => setTimezoneForm(e.target.value)}
1639
+ className="w-full px-3 py-2 rounded-md border border-input bg-background"
1640
+ >
1641
+ <optgroup label="United States">
1642
+ <option>UTC-10:00 Hawaii-Aleutian</option>
1643
+ <option>UTC-09:00 Alaska</option>
1644
+ <option>UTC-08:00 Pacific Time (PT)</option>
1645
+ <option>UTC-07:00 Mountain Time (MT)</option>
1646
+ <option>UTC-06:00 Central Time (CT)</option>
1647
+ <option>UTC-05:00 Eastern Time (ET)</option>
1648
+ </optgroup>
1649
+ <optgroup label="Common International">
1650
+ <option>UTC+00:00 Coordinated Universal Time</option>
1651
+ <option>UTC+00:00 London (GMT)</option>
1652
+ <option>UTC+01:00 Paris/Berlin (CET)</option>
1653
+ <option>UTC+02:00 Cairo/Athens</option>
1654
+ <option>UTC+03:00 Moscow/Istanbul</option>
1655
+ <option>UTC+05:30 Mumbai/Delhi</option>
1656
+ <option>UTC+08:00 Beijing/Singapore</option>
1657
+ <option>UTC+09:00 Tokyo/Seoul</option>
1658
+ <option>UTC+10:00 Sydney</option>
1659
+ </optgroup>
1660
+ </select>
1661
+ </div>
1662
+
1663
+ <div>
1664
+ <label htmlFor="date-format-select" className="block text-sm font-medium mb-2">
1665
+ Date Format
1666
+ </label>
1667
+ <select
1668
+ id="date-format-select"
1669
+ value={dateFormatForm}
1670
+ onChange={(e) => setDateFormatForm(e.target.value)}
1671
+ className="w-full px-3 py-2 rounded-md border border-input bg-background"
1672
+ >
1673
+ <option>MM/DD/YYYY</option>
1674
+ <option>DD/MM/YYYY</option>
1675
+ <option>YYYY-MM-DD</option>
1676
+ </select>
1677
+ </div>
1678
+
1679
+ <div className="flex justify-end">
1680
+ <Button
1681
+ onClick={handleSaveSettings}
1682
+ showSuccess={saveSuccess}
1683
+ icon={<Save className="w-4 h-4" />}
1684
+ >
1685
+ Save Settings
1686
+ </Button>
1687
+ </div>
1688
+ </div>
1689
+ </div>
1690
+ )}
1691
+
1692
+ {activeSection === 'updates' && (
1693
+ <div className="space-y-6">
1694
+ <div>
1695
+ <h2 className="text-2xl font-bold">Updates</h2>
1696
+ <p className="text-muted-foreground mt-1">
1697
+ Manage application updates and versioning
1698
+ </p>
1699
+ </div>
1700
+
1701
+ <div className="space-y-6">
1702
+ <div className="p-4 bg-muted/20 rounded-lg space-y-3">
1703
+ <div className="flex items-center justify-between">
1704
+ <div>
1705
+ <h3 className="font-medium">Current Version</h3>
1706
+ <p className="text-sm text-muted-foreground">
1707
+ {currentVersion || 'Loading...'}
1708
+ </p>
1709
+ </div>
1710
+ <Button
1711
+ onClick={handleCheckForUpdates}
1712
+ disabled={checkingForUpdates}
1713
+ icon={
1714
+ <RefreshCw
1715
+ className={cn('w-4 h-4', checkingForUpdates && 'animate-spin')}
1716
+ />
1717
+ }
1718
+ >
1719
+ {checkingForUpdates ? 'Checking...' : 'Check for Updates'}
1720
+ </Button>
1721
+ </div>
1722
+ {updateStatus && <p className="text-sm text-muted-foreground">{updateStatus}</p>}
1723
+ {updateAvailable && !updateDownloaded && (
1724
+ <Button
1725
+ onClick={handleDownloadUpdate}
1726
+ variant="default"
1727
+ className="w-full"
1728
+ disabled={downloadProgress > 0 && downloadProgress < 100}
1729
+ >
1730
+ {downloadProgress > 0
1731
+ ? `Downloading ${Math.round(downloadProgress)}%`
1732
+ : `Download Update ${updateVersion}`}
1733
+ </Button>
1734
+ )}
1735
+ {downloadProgress > 0 && downloadProgress < 100 && (
1736
+ <div className="w-full bg-muted rounded-full h-2 overflow-hidden">
1737
+ <div
1738
+ className="h-full bg-primary transition-all duration-300"
1739
+ style={{ width: `${downloadProgress}%` }}
1740
+ />
1741
+ </div>
1742
+ )}
1743
+ {updateDownloaded && (
1744
+ <Button
1745
+ onClick={handleInstallUpdate}
1746
+ variant="default"
1747
+ className="w-full bg-green-600 hover:bg-green-700"
1748
+ >
1749
+ Install & Restart
1750
+ </Button>
1751
+ )}
1752
+ </div>
1753
+
1754
+ <div className="space-y-4">
1755
+ <div className="flex items-center justify-between">
1756
+ <div className="space-y-1">
1757
+ <label htmlFor="auto-update" className="text-sm font-medium">
1758
+ Auto-update on launch
1759
+ </label>
1760
+ <p className="text-xs text-muted-foreground">
1761
+ Automatically check for updates when the application starts
1762
+ </p>
1763
+ </div>
1764
+ <button
1765
+ id="auto-update"
1766
+ role="switch"
1767
+ aria-checked={updateSettingsForm.autoUpdateOnLaunch}
1768
+ onClick={() =>
1769
+ setUpdateSettingsForm({
1770
+ ...updateSettingsForm,
1771
+ autoUpdateOnLaunch: !updateSettingsForm.autoUpdateOnLaunch,
1772
+ })
1773
+ }
1774
+ className={cn(
1775
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors border-2',
1776
+ updateSettingsForm.autoUpdateOnLaunch
1777
+ ? 'bg-primary border-primary toggle-checked'
1778
+ : 'bg-input border-border'
1779
+ )}
1780
+ >
1781
+ <span
1782
+ className={cn(
1783
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
1784
+ updateSettingsForm.autoUpdateOnLaunch ? 'translate-x-6' : 'translate-x-1'
1785
+ )}
1786
+ />
1787
+ </button>
1788
+ </div>
1789
+
1790
+ <div className="flex items-center justify-between">
1791
+ <div className="space-y-1">
1792
+ <label htmlFor="pre-releases" className="text-sm font-medium">
1793
+ Check for pre-releases
1794
+ </label>
1795
+ <p className="text-xs text-muted-foreground">
1796
+ Include beta and pre-release versions in update checks
1797
+ </p>
1798
+ </div>
1799
+ <button
1800
+ id="pre-releases"
1801
+ role="switch"
1802
+ aria-checked={updateSettingsForm.checkForPreReleases}
1803
+ onClick={() =>
1804
+ setUpdateSettingsForm({
1805
+ ...updateSettingsForm,
1806
+ checkForPreReleases: !updateSettingsForm.checkForPreReleases,
1807
+ })
1808
+ }
1809
+ className={cn(
1810
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors border-2',
1811
+ updateSettingsForm.checkForPreReleases
1812
+ ? 'bg-primary border-primary toggle-checked'
1813
+ : 'bg-input border-border'
1814
+ )}
1815
+ >
1816
+ <span
1817
+ className={cn(
1818
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
1819
+ updateSettingsForm.checkForPreReleases ? 'translate-x-6' : 'translate-x-1'
1820
+ )}
1821
+ />
1822
+ </button>
1823
+ </div>
1824
+ </div>
1825
+
1826
+ {/* SharePoint Update Source */}
1827
+ <div className="border border-border rounded-lg p-4 space-y-4">
1828
+ <div className="flex items-center justify-between">
1829
+ <div className="space-y-1">
1830
+ <label htmlFor="use-sharepoint" className="text-sm font-medium flex items-center gap-2">
1831
+ <Cloud className="w-4 h-4" />
1832
+ Use SharePoint for Updates
1833
+ </label>
1834
+ <p className="text-xs text-muted-foreground">
1835
+ Download updates from a SharePoint folder instead of GitHub
1836
+ </p>
1837
+ </div>
1838
+ <button
1839
+ id="use-sharepoint"
1840
+ role="switch"
1841
+ aria-checked={updateSettingsForm.useSharePointSource}
1842
+ onClick={() =>
1843
+ setUpdateSettingsForm({
1844
+ ...updateSettingsForm,
1845
+ useSharePointSource: !updateSettingsForm.useSharePointSource,
1846
+ })
1847
+ }
1848
+ className={cn(
1849
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors border-2',
1850
+ updateSettingsForm.useSharePointSource
1851
+ ? 'bg-primary border-primary toggle-checked'
1852
+ : 'bg-input border-border'
1853
+ )}
1854
+ >
1855
+ <span
1856
+ className={cn(
1857
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
1858
+ updateSettingsForm.useSharePointSource ? 'translate-x-6' : 'translate-x-1'
1859
+ )}
1860
+ />
1861
+ </button>
1862
+ </div>
1863
+
1864
+ {updateSettingsForm.useSharePointSource && (
1865
+ <div className="space-y-4 pt-2 border-t border-border">
1866
+ {/* SharePoint Folder URL */}
1867
+ <div>
1868
+ <label htmlFor="sp-url" className="block text-sm font-medium mb-2">
1869
+ SharePoint Folder URL
1870
+ </label>
1871
+ <input
1872
+ id="sp-url"
1873
+ type="url"
1874
+ value={updateSettingsForm.sharePointFolderUrl || ''}
1875
+ onChange={(e) =>
1876
+ setUpdateSettingsForm({
1877
+ ...updateSettingsForm,
1878
+ sharePointFolderUrl: e.target.value,
1879
+ })
1880
+ }
1881
+ placeholder="https://company.sharepoint.com/sites/IT/Shared Documents/Updates"
1882
+ className={cn(
1883
+ 'w-full px-3 py-2 rounded-md border bg-background focus:outline-none focus:ring-1',
1884
+ updateSettingsForm.sharePointFolderUrl && !validateSharePointUrl(updateSettingsForm.sharePointFolderUrl)
1885
+ ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
1886
+ : 'border-input focus:border-primary focus:ring-primary/20'
1887
+ )}
1888
+ />
1889
+ {updateSettingsForm.sharePointFolderUrl && !validateSharePointUrl(updateSettingsForm.sharePointFolderUrl) && (
1890
+ <p className="text-xs text-red-500 mt-1">
1891
+ Invalid URL. Must be https://*.sharepoint.com/sites/...
1892
+ </p>
1893
+ )}
1894
+ <p className="text-xs text-muted-foreground mt-1">
1895
+ Folder must contain: latest.yml and the MSI installer file
1896
+ </p>
1897
+ </div>
1898
+
1899
+ {/* Microsoft Login */}
1900
+ <div className="flex items-center gap-3">
1901
+ {sharePointLoginStatus !== 'logged-in' ? (
1902
+ <Button
1903
+ variant="outline"
1904
+ onClick={handleSharePointLogin}
1905
+ disabled={sharePointLoginStatus === 'logging-in'}
1906
+ icon={<User className="w-4 h-4" />}
1907
+ >
1908
+ {sharePointLoginStatus === 'logging-in' ? 'Signing In...' : 'Sign In to Microsoft'}
1909
+ </Button>
1910
+ ) : (
1911
+ <Button
1912
+ variant="outline"
1913
+ onClick={handleSharePointLogout}
1914
+ icon={<LogOut className="w-4 h-4" />}
1915
+ >
1916
+ Sign Out
1917
+ </Button>
1918
+ )}
1919
+
1920
+ {sharePointLoginStatus === 'logged-in' && (
1921
+ <span className="text-xs text-green-500 flex items-center gap-1">
1922
+ <CheckCircle2 className="w-3 h-3" /> Authenticated
1923
+ </span>
1924
+ )}
1925
+ </div>
1926
+
1927
+ {/* Test Connection */}
1928
+ <div className="flex items-center gap-3">
1929
+ <Button
1930
+ variant="outline"
1931
+ onClick={handleTestSharePointConnection}
1932
+ disabled={
1933
+ testingSharePointConnection ||
1934
+ !updateSettingsForm.sharePointFolderUrl ||
1935
+ sharePointLoginStatus !== 'logged-in'
1936
+ }
1937
+ icon={<Wifi className="w-4 h-4" />}
1938
+ >
1939
+ {testingSharePointConnection ? 'Testing...' : 'Test Connection'}
1940
+ </Button>
1941
+
1942
+ {sharePointConnectionResult && (
1943
+ <span
1944
+ className={cn(
1945
+ 'text-xs flex items-center gap-1',
1946
+ sharePointConnectionResult.success ? 'text-green-500' : 'text-red-500'
1947
+ )}
1948
+ >
1949
+ {sharePointConnectionResult.success ? (
1950
+ <CheckCircle2 className="w-3 h-3" />
1951
+ ) : (
1952
+ <AlertCircle className="w-3 h-3" />
1953
+ )}
1954
+ {sharePointConnectionResult.message}
1955
+ </span>
1956
+ )}
1957
+ </div>
1958
+
1959
+ {/* Info */}
1960
+ <p className="text-xs text-muted-foreground bg-muted/20 p-2 rounded">
1961
+ GitHub remains the default update source. SharePoint is only used when enabled with a valid URL
1962
+ and you&apos;re signed in. If SharePoint fails, the app will automatically fall back to GitHub.
1963
+ </p>
1964
+ </div>
1965
+ )}
1966
+ </div>
1967
+
1968
+ <div className="flex justify-end">
1969
+ <Button
1970
+ onClick={handleSaveSettings}
1971
+ showSuccess={saveSuccess}
1972
+ icon={<Save className="w-4 h-4" />}
1973
+ >
1974
+ Save Settings
1975
+ </Button>
1976
+ </div>
1977
+ </div>
1978
+ </div>
1979
+ )}
1980
+
1981
+ {activeSection === 'api-connections' && (
1982
+ <div className="space-y-6">
1983
+ <div>
1984
+ <h2 className="text-2xl font-bold">API Connections</h2>
1985
+ <p className="text-muted-foreground mt-1">
1986
+ Configure external service integrations and API endpoints
1987
+ </p>
1988
+ </div>
1989
+
1990
+ <div className="space-y-4">
1991
+ <div>
1992
+ <h3 className="font-medium mb-3">Hyperlink Processing</h3>
1993
+ <div className="space-y-4">
1994
+ <div>
1995
+ <label htmlFor="powerautomate-url" className="block text-sm font-medium mb-2">
1996
+ PowerAutomate Dictionary URL
1997
+ </label>
1998
+ <div className="relative">
1999
+ <input
2000
+ id="powerautomate-url"
2001
+ type="url"
2002
+ value={apiConnectionsForm.powerAutomateUrl}
2003
+ onChange={(e) => handlePowerAutomateUrlChange(e.target.value)}
2004
+ onPaste={(e) => {
2005
+ // Auto-sanitize on paste
2006
+ if (urlWarningTimeoutRef.current) {
2007
+ clearTimeout(urlWarningTimeoutRef.current);
2008
+ }
2009
+ urlWarningTimeoutRef.current = setTimeout(() => {
2010
+ const pasted = e.currentTarget.value;
2011
+ if (hasEncodingIssues(pasted)) {
2012
+ handlePowerAutomateUrlChange(pasted);
2013
+ }
2014
+ urlWarningTimeoutRef.current = null;
2015
+ }, 10);
2016
+ }}
2017
+ placeholder="https://prod-11.westus.logic.azure.com/workflows/..."
2018
+ className={cn(
2019
+ 'w-full px-3 py-2 pr-10 rounded-md border bg-background focus:outline-none focus:ring-1',
2020
+ urlValidation?.valid === false
2021
+ ? 'border-red-500 focus:border-red-500 focus:ring-red-500/20'
2022
+ : urlValidation?.warnings.length
2023
+ ? 'border-yellow-500 focus:border-yellow-500 focus:ring-yellow-500/20'
2024
+ : 'border-input focus:border-primary focus:ring-primary/20'
2025
+ )}
2026
+ />
2027
+ {urlValidation && (
2028
+ <div className="absolute right-3 top-1/2 -translate-y-1/2">
2029
+ {urlValidation.valid ? (
2030
+ <CheckCircle2 className="w-5 h-5 text-green-500" />
2031
+ ) : (
2032
+ <AlertCircle className="w-5 h-5 text-red-500" />
2033
+ )}
2034
+ </div>
2035
+ )}
2036
+ </div>
2037
+
2038
+ {/* Validation messages */}
2039
+ {urlValidation && !urlValidation.valid && urlValidation.issues.length > 0 && (
2040
+ <div className="mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
2041
+ <div className="flex items-start gap-2">
2042
+ <AlertCircle className="w-4 h-4 text-red-600 dark:text-red-400 mt-0.5 shrink-0" />
2043
+ <div className="flex-1 space-y-1">
2044
+ {urlValidation.issues.map((issue, idx) => (
2045
+ <p key={idx} className="text-xs text-red-700 dark:text-red-300">
2046
+ {issue}
2047
+ </p>
2048
+ ))}
2049
+ </div>
2050
+ </div>
2051
+ </div>
2052
+ )}
2053
+
2054
+ {urlValidation && urlValidation.warnings.length > 0 && (
2055
+ <div className="mt-2 p-2 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md">
2056
+ <div className="flex items-start gap-2">
2057
+ <AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 shrink-0" />
2058
+ <div className="flex-1 space-y-1">
2059
+ {urlValidation.warnings.map((warning, idx) => (
2060
+ <p
2061
+ key={idx}
2062
+ className="text-xs text-yellow-700 dark:text-yellow-300"
2063
+ >
2064
+ {warning}
2065
+ </p>
2066
+ ))}
2067
+ </div>
2068
+ </div>
2069
+ </div>
2070
+ )}
2071
+
2072
+ {showUrlWarning && (
2073
+ <div className="mt-2 p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
2074
+ <div className="flex items-start gap-2">
2075
+ <CheckCircle2 className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 shrink-0" />
2076
+ <p className="text-xs text-blue-700 dark:text-blue-300">
2077
+ URL automatically sanitized! Encoded characters have been fixed.
2078
+ </p>
2079
+ </div>
2080
+ </div>
2081
+ )}
2082
+
2083
+ <p className="text-xs text-muted-foreground mt-2">
2084
+ This URL is used by the Hyperlink Service to retrieve document metadata and
2085
+ validate links. The service will send collected document IDs to this
2086
+ endpoint and receive enriched data in response.
2087
+ </p>
2088
+ </div>
2089
+ </div>
2090
+ </div>
2091
+
2092
+ <div>
2093
+ <h3 className="font-medium mb-3">Feedback & Reporting</h3>
2094
+ <div className="space-y-4">
2095
+ <div>
2096
+ <label htmlFor="bug-report-url" className="block text-sm font-medium mb-2">
2097
+ Bug Report API URL
2098
+ </label>
2099
+ <input
2100
+ id="bug-report-url"
2101
+ type="url"
2102
+ value={apiConnectionsForm.bugReportUrl}
2103
+ onChange={(e) =>
2104
+ setApiConnectionsForm({
2105
+ ...apiConnectionsForm,
2106
+ bugReportUrl: e.target.value,
2107
+ })
2108
+ }
2109
+ placeholder="https://www.example.com"
2110
+ className="w-full px-3 py-2 rounded-md border border-input bg-background focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
2111
+ />
2112
+ <p className="text-xs text-muted-foreground mt-2">
2113
+ Bug reports will be sent to this API endpoint. Leave as default to use email
2114
+ instead.
2115
+ </p>
2116
+ </div>
2117
+
2118
+ <div>
2119
+ <label htmlFor="submit-idea-url" className="block text-sm font-medium mb-2">
2120
+ Submit Idea API URL
2121
+ </label>
2122
+ <input
2123
+ id="submit-idea-url"
2124
+ type="url"
2125
+ value={apiConnectionsForm.submitIdeaUrl}
2126
+ onChange={(e) =>
2127
+ setApiConnectionsForm({
2128
+ ...apiConnectionsForm,
2129
+ submitIdeaUrl: e.target.value,
2130
+ })
2131
+ }
2132
+ placeholder="https://www.example.com"
2133
+ className="w-full px-3 py-2 rounded-md border border-input bg-background focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
2134
+ />
2135
+ <p className="text-xs text-muted-foreground mt-2">
2136
+ Feature suggestions will be sent to this API endpoint. Leave as default to
2137
+ use email instead.
2138
+ </p>
2139
+ </div>
2140
+ </div>
2141
+ </div>
2142
+
2143
+ <div className="pt-4 border-t border-border">
2144
+ <div className="flex items-start gap-3">
2145
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
2146
+ <Link2 className="w-5 h-5 text-primary" />
2147
+ </div>
2148
+ <div className="flex-1">
2149
+ <h4 className="font-medium mb-1">About API Connections</h4>
2150
+ <p className="text-sm text-muted-foreground">
2151
+ API connections allow Documentation Hub to integrate with external services
2152
+ for enhanced functionality. These endpoints are used during document
2153
+ processing to enrich data, validate content, and automate workflows.
2154
+ </p>
2155
+ </div>
2156
+ </div>
2157
+ </div>
2158
+
2159
+ <div className="flex justify-end">
2160
+ <Button
2161
+ onClick={handleSaveSettings}
2162
+ showSuccess={saveSuccess}
2163
+ icon={<Save className="w-4 h-4" />}
2164
+ >
2165
+ Save Settings
2166
+ </Button>
2167
+ </div>
2168
+ </div>
2169
+ </div>
2170
+ )}
2171
+
2172
+ {activeSection === 'submit-idea' && (
2173
+ <div className="space-y-6">
2174
+ <div>
2175
+ <h2 className="text-2xl font-bold">Submit Idea for New Implementation</h2>
2176
+ <p className="text-muted-foreground mt-1">
2177
+ Have an idea to improve the application? We'd love to hear from you!
2178
+ </p>
2179
+ </div>
2180
+
2181
+ <div className="space-y-4">
2182
+ <div>
2183
+ <label className="block text-sm font-medium mb-2">Title for Idea</label>
2184
+ <input
2185
+ type="text"
2186
+ value={ideaTitle}
2187
+ onChange={(e) => setIdeaTitle(e.target.value)}
2188
+ placeholder="Enter a brief title for your idea"
2189
+ className="w-full px-3 py-2 rounded-md border border-input bg-background"
2190
+ />
2191
+ </div>
2192
+
2193
+ <div>
2194
+ <label className="block text-sm font-medium mb-2">
2195
+ Why is this needed / Who would this benefit?
2196
+ </label>
2197
+ <textarea
2198
+ value={ideaBenefit}
2199
+ onChange={(e) => setIdeaBenefit(e.target.value)}
2200
+ placeholder="Describe the benefits and potential users of this feature..."
2201
+ rows={6}
2202
+ className="w-full px-3 py-2 rounded-md border border-input bg-background resize-none"
2203
+ />
2204
+ </div>
2205
+
2206
+ <div className="flex justify-end">
2207
+ <Button
2208
+ onClick={async () => {
2209
+ if (!ideaTitle || !ideaBenefit) return;
2210
+
2211
+ const idea = {
2212
+ Type: 'Feedback',
2213
+ Email: settings.profile.email,
2214
+ Title: ideaTitle,
2215
+ Description: ideaBenefit,
2216
+ };
2217
+
2218
+ const apiUrl = settings.apiConnections.submitIdeaUrl;
2219
+
2220
+ // Check if using default URL - if so, fallback to mailto
2221
+ if (apiUrl === 'https://www.example.com' || !apiUrl) {
2222
+ const subject = encodeURIComponent(`Feature Idea: ${ideaTitle}`);
2223
+ const body = encodeURIComponent(`
2224
+ Feature Idea
2225
+ ------------
2226
+ Email: ${settings.profile.email}
2227
+ Title: ${ideaTitle}
2228
+
2229
+ Description:
2230
+ ${ideaBenefit}
2231
+
2232
+ Submitted: ${new Date().toLocaleString()}
2233
+ `);
2234
+
2235
+ window.location.href = `mailto:support@example.com?subject=${subject}&body=${body}`;
2236
+
2237
+ // Clear fields after mailto (assuming success)
2238
+ setIdeaTitle('');
2239
+ setIdeaBenefit('');
2240
+ setIdeaSubmitted(true);
2241
+ if (ideaSubmittedTimeoutRef.current) {
2242
+ clearTimeout(ideaSubmittedTimeoutRef.current);
2243
+ }
2244
+ ideaSubmittedTimeoutRef.current = setTimeout(() => {
2245
+ setIdeaSubmitted(false);
2246
+ ideaSubmittedTimeoutRef.current = null;
2247
+ }, 2000);
2248
+
2249
+ // Show success notification
2250
+ alert('Your idea has been sent to the Documentation Hub Admin');
2251
+ return;
2252
+ }
2253
+
2254
+ // Use API if configured
2255
+ try {
2256
+ const response = await fetch(apiUrl, {
2257
+ method: 'POST',
2258
+ headers: {
2259
+ 'Content-Type': 'application/json',
2260
+ },
2261
+ body: JSON.stringify(idea),
2262
+ });
2263
+
2264
+ if (response.ok) {
2265
+ // Only clear fields on success
2266
+ setIdeaTitle('');
2267
+ setIdeaBenefit('');
2268
+ setIdeaSubmitted(true);
2269
+ if (ideaSubmittedTimeoutRef.current) {
2270
+ clearTimeout(ideaSubmittedTimeoutRef.current);
2271
+ }
2272
+ ideaSubmittedTimeoutRef.current = setTimeout(() => {
2273
+ setIdeaSubmitted(false);
2274
+ ideaSubmittedTimeoutRef.current = null;
2275
+ }, 2000);
2276
+
2277
+ // Show success notification
2278
+ alert('Your idea has been sent to the Documentation Hub Admin');
2279
+ } else {
2280
+ alert('Failed to submit idea. Please try again.');
2281
+ }
2282
+ } catch (_error) {
2283
+ logger.error('Error submitting idea:', _error);
2284
+ alert('Failed to submit idea. Please check your API configuration.');
2285
+ }
2286
+ }}
2287
+ icon={<Send className="w-4 h-4" />}
2288
+ showSuccess={ideaSubmitted}
2289
+ disabled={!ideaTitle || !ideaBenefit}
2290
+ >
2291
+ Submit Idea
2292
+ </Button>
2293
+ </div>
2294
+
2295
+ <div className="mt-6 p-4 bg-muted/30 rounded-lg border border-border">
2296
+ <p className="text-sm text-muted-foreground">
2297
+ <strong>Note:</strong> Your ideas help shape the future of Documentation Hub. We
2298
+ review all submissions and prioritize features based on user feedback and
2299
+ technical feasibility.
2300
+ </p>
2301
+ </div>
2302
+ </div>
2303
+ </div>
2304
+ )}
2305
+
2306
+ {activeSection === 'local-dictionary' && (
2307
+ <div className="space-y-6">
2308
+ <div>
2309
+ <h2 className="text-2xl font-bold">Local Dictionary</h2>
2310
+ <p className="text-muted-foreground mt-1">
2311
+ Configure offline hyperlink lookups using a local SharePoint dictionary
2312
+ </p>
2313
+ </div>
2314
+
2315
+ <div className="space-y-6">
2316
+ {/* Enable/Disable Toggle */}
2317
+ <div className="p-4 bg-muted/20 rounded-lg border border-border">
2318
+ <div className="flex items-center justify-between">
2319
+ <div className="space-y-1">
2320
+ <label htmlFor="dictionary-enabled" className="text-sm font-medium">
2321
+ Enable Local Dictionary
2322
+ </label>
2323
+ <p className="text-xs text-muted-foreground">
2324
+ When enabled, hyperlink lookups will use the local database first, falling back to the API if not found
2325
+ </p>
2326
+ </div>
2327
+ <button
2328
+ id="dictionary-enabled"
2329
+ role="switch"
2330
+ aria-checked={localDictionaryForm.enabled}
2331
+ onClick={() =>
2332
+ setLocalDictionaryForm({
2333
+ ...localDictionaryForm,
2334
+ enabled: !localDictionaryForm.enabled,
2335
+ })
2336
+ }
2337
+ className={cn(
2338
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors border-2',
2339
+ localDictionaryForm.enabled
2340
+ ? 'bg-primary border-primary toggle-checked'
2341
+ : 'bg-input border-border'
2342
+ )}
2343
+ >
2344
+ <span
2345
+ className={cn(
2346
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
2347
+ localDictionaryForm.enabled ? 'translate-x-6' : 'translate-x-1'
2348
+ )}
2349
+ />
2350
+ </button>
2351
+ </div>
2352
+ </div>
2353
+
2354
+ {/* SharePoint File URL */}
2355
+ <div className="space-y-4">
2356
+ <h3 className="font-medium">SharePoint Dictionary File</h3>
2357
+
2358
+ <div>
2359
+ <label htmlFor="sharepoint-file-url" className="block text-sm font-medium mb-2">
2360
+ SharePoint File URL
2361
+ </label>
2362
+ <input
2363
+ id="sharepoint-file-url"
2364
+ type="url"
2365
+ value={localDictionaryForm.sharePointFileUrl}
2366
+ onChange={(e) =>
2367
+ setLocalDictionaryForm({
2368
+ ...localDictionaryForm,
2369
+ sharePointFileUrl: e.target.value,
2370
+ })
2371
+ }
2372
+ placeholder="https://company.sharepoint.com/sites/IT/Shared Documents/Dictionary.xlsx"
2373
+ className="w-full px-3 py-2 rounded-md border border-input bg-background focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
2374
+ />
2375
+ <p className="text-xs text-muted-foreground mt-1">
2376
+ Direct URL to the .xlsx dictionary file on SharePoint (Daily_Inventory sheet, Dictionary_Table table)
2377
+ </p>
2378
+ </div>
2379
+ </div>
2380
+
2381
+ {/* Retrieval Status */}
2382
+ <div className="p-4 bg-muted/20 rounded-lg border border-border space-y-3">
2383
+ <h3 className="font-medium">Dictionary Status</h3>
2384
+
2385
+ <div className="grid grid-cols-2 gap-4 text-sm">
2386
+ <div>
2387
+ <span className="text-muted-foreground">Total Entries:</span>
2388
+ <span className="ml-2 font-medium">
2389
+ {localDictionaryForm.totalEntries?.toLocaleString() || dictionaryStatus?.totalEntries?.toLocaleString() || '0'}
2390
+ </span>
2391
+ </div>
2392
+ <div>
2393
+ <span className="text-muted-foreground">Last Retrieved:</span>
2394
+ <span className="ml-2 font-medium">
2395
+ {localDictionaryForm.lastRetrievalTime
2396
+ ? new Date(localDictionaryForm.lastRetrievalTime).toLocaleString()
2397
+ : dictionaryStatus?.lastSyncTime
2398
+ ? new Date(dictionaryStatus.lastSyncTime).toLocaleString()
2399
+ : 'Never'}
2400
+ </span>
2401
+ </div>
2402
+ <div className="col-span-2">
2403
+ <span className="text-muted-foreground">Status:</span>
2404
+ <span
2405
+ className={cn(
2406
+ 'ml-2 font-medium',
2407
+ (localDictionaryForm.lastRetrievalSuccess || dictionaryStatus?.lastSyncSuccess) ? 'text-green-600' : 'text-muted-foreground'
2408
+ )}
2409
+ >
2410
+ {dictionaryStatus?.syncInProgress
2411
+ ? 'Retrieving...'
2412
+ : (localDictionaryForm.lastRetrievalSuccess || dictionaryStatus?.lastSyncSuccess)
2413
+ ? 'Ready'
2414
+ : 'Not retrieved'}
2415
+ </span>
2416
+ </div>
2417
+ </div>
2418
+
2419
+ {/* Progress bar during retrieval */}
2420
+ {dictionaryStatus?.syncInProgress && (
2421
+ <div className="space-y-1">
2422
+ <div className="flex justify-between text-xs text-muted-foreground">
2423
+ <span>Retrieving dictionary...</span>
2424
+ <span>{Math.round(dictionaryStatus.syncProgress || 0)}%</span>
2425
+ </div>
2426
+ <div className="w-full bg-muted rounded-full h-2 overflow-hidden">
2427
+ <div
2428
+ className="h-full bg-primary transition-all duration-300"
2429
+ style={{ width: `${dictionaryStatus.syncProgress || 0}%` }}
2430
+ />
2431
+ </div>
2432
+ </div>
2433
+ )}
2434
+
2435
+ {/* Error display */}
2436
+ {dictionaryStatus?.syncError && (
2437
+ <div className="p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
2438
+ <div className="flex items-start gap-2">
2439
+ <AlertCircle className="w-4 h-4 text-red-600 dark:text-red-400 mt-0.5 shrink-0" />
2440
+ <p className="text-xs text-red-700 dark:text-red-300">
2441
+ {dictionaryStatus.syncError}
2442
+ </p>
2443
+ </div>
2444
+ </div>
2445
+ )}
2446
+
2447
+ <Button
2448
+ variant="outline"
2449
+ onClick={handleRetrieveDictionary}
2450
+ disabled={syncingDictionary || !localDictionaryForm.sharePointFileUrl?.trim()}
2451
+ icon={<RefreshCw className={cn('w-4 h-4', syncingDictionary && 'animate-spin')} />}
2452
+ className="w-full"
2453
+ >
2454
+ {syncingDictionary ? 'Retrieving...' : 'Retrieve Dictionary'}
2455
+ </Button>
2456
+ <p className="text-xs text-muted-foreground text-center">
2457
+ Opens browser for Microsoft sign-in, then downloads and imports the dictionary
2458
+ </p>
2459
+ </div>
2460
+
2461
+ {/* Info Card */}
2462
+ <div className="pt-4 border-t border-border">
2463
+ <div className="flex items-start gap-3">
2464
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
2465
+ <HardDrive className="w-5 h-5 text-primary" />
2466
+ </div>
2467
+ <div className="flex-1">
2468
+ <h4 className="font-medium mb-1">About Local Dictionary</h4>
2469
+ <p className="text-sm text-muted-foreground">
2470
+ The local dictionary downloads your SharePoint dictionary file and stores it
2471
+ in a high-performance SQLite database for instant lookups. When enabled,
2472
+ hyperlink lookups check the local database first. If an ID is not found locally,
2473
+ the system automatically falls back to the API for that lookup.
2474
+ </p>
2475
+ </div>
2476
+ </div>
2477
+ </div>
2478
+
2479
+ <div className="flex justify-end">
2480
+ <Button
2481
+ onClick={handleSaveDictionarySettings}
2482
+ showSuccess={saveSuccess}
2483
+ icon={<Save className="w-4 h-4" />}
2484
+ >
2485
+ Save Settings
2486
+ </Button>
2487
+ </div>
2488
+ </div>
2489
+ </div>
2490
+ )}
2491
+
2492
+ {activeSection === 'backup-settings' && (
2493
+ <div className="space-y-6">
2494
+ <div>
2495
+ <h2 className="text-2xl font-bold">Backups</h2>
2496
+ <p className="text-muted-foreground mt-1">
2497
+ Configure automatic document backup settings
2498
+ </p>
2499
+ </div>
2500
+
2501
+ <div className="space-y-6">
2502
+ {/* Enable/Disable Toggle */}
2503
+ <div className="p-4 bg-muted/20 rounded-lg border border-border">
2504
+ <div className="flex items-center justify-between">
2505
+ <div className="space-y-1">
2506
+ <label htmlFor="backup-enabled" className="text-sm font-medium">
2507
+ Enable Automatic Backups
2508
+ </label>
2509
+ <p className="text-xs text-muted-foreground">
2510
+ Create backup copies of documents before processing changes
2511
+ </p>
2512
+ </div>
2513
+ <button
2514
+ id="backup-enabled"
2515
+ role="switch"
2516
+ aria-checked={backupSettingsForm.enabled}
2517
+ onClick={() =>
2518
+ setBackupSettingsForm({
2519
+ ...backupSettingsForm,
2520
+ enabled: !backupSettingsForm.enabled,
2521
+ })
2522
+ }
2523
+ className={cn(
2524
+ 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors border-2',
2525
+ backupSettingsForm.enabled
2526
+ ? 'bg-primary border-primary toggle-checked'
2527
+ : 'bg-input border-border'
2528
+ )}
2529
+ >
2530
+ <span
2531
+ className={cn(
2532
+ 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
2533
+ backupSettingsForm.enabled ? 'translate-x-6' : 'translate-x-1'
2534
+ )}
2535
+ />
2536
+ </button>
2537
+ </div>
2538
+ </div>
2539
+
2540
+ {/* Backup Information */}
2541
+ <div className="space-y-4">
2542
+ <h3 className="font-medium">Backup Details</h3>
2543
+
2544
+ <div className="p-4 bg-muted/10 rounded-lg border border-border space-y-3">
2545
+ <div className="flex items-start gap-3">
2546
+ <Archive className="w-5 h-5 text-muted-foreground mt-0.5" />
2547
+ <div>
2548
+ <p className="font-medium text-sm">Backup Location</p>
2549
+ <p className="text-xs text-muted-foreground">
2550
+ Backups are stored in a <code className="bg-muted px-1 rounded">DocHub_Backups</code> folder
2551
+ in the same directory as the original document.
2552
+ </p>
2553
+ </div>
2554
+ </div>
2555
+
2556
+ <div className="flex items-start gap-3">
2557
+ <Archive className="w-5 h-5 text-muted-foreground mt-0.5" />
2558
+ <div>
2559
+ <p className="font-medium text-sm">Naming Convention</p>
2560
+ <p className="text-xs text-muted-foreground">
2561
+ Backup files are named using incremental numbering:
2562
+ </p>
2563
+ <code className="text-xs bg-muted px-2 py-1 rounded block mt-1">
2564
+ filename_Backup_1.docx, filename_Backup_2.docx, ...
2565
+ </code>
2566
+ </div>
2567
+ </div>
2568
+ </div>
2569
+ </div>
2570
+
2571
+ {/* Info Card */}
2572
+ <div className="pt-4 border-t border-border">
2573
+ <div className="flex items-start gap-3">
2574
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
2575
+ <Archive className="w-5 h-5 text-primary" />
2576
+ </div>
2577
+ <div className="flex-1">
2578
+ <h4 className="font-medium mb-1">About Backups</h4>
2579
+ <p className="text-sm text-muted-foreground">
2580
+ Automatic backups protect your documents by creating a copy before any processing
2581
+ changes are applied. Each backup is numbered incrementally, allowing you to restore
2582
+ from any previous version if needed. Disable this feature only if you have your own
2583
+ backup solution in place.
2584
+ </p>
2585
+ </div>
2586
+ </div>
2587
+ </div>
2588
+
2589
+ <div className="flex justify-end">
2590
+ <Button
2591
+ onClick={handleSaveBackupSettings}
2592
+ showSuccess={saveSuccess}
2593
+ icon={<Save className="w-4 h-4" />}
2594
+ >
2595
+ Save Settings
2596
+ </Button>
2597
+ </div>
2598
+ </div>
2599
+ </div>
2600
+ )}
2601
+ </motion.main>
2602
+ </div>
2603
+
2604
+ <ColorPickerDialog
2605
+ isOpen={activeColorPicker !== null}
2606
+ onClose={() => setActiveColorPicker(null)}
2607
+ color={tempColor}
2608
+ onColorChange={(color) => {
2609
+ switch (activeColorPicker) {
2610
+ case 'accent':
2611
+ setCustomAccentColor(color);
2612
+ setAccentColor('custom');
2613
+ break;
2614
+ case 'primary':
2615
+ setCustomPrimaryColor(color);
2616
+ break;
2617
+ case 'background':
2618
+ setCustomBackgroundColor(color);
2619
+ break;
2620
+ case 'header':
2621
+ setCustomHeaderColor(color);
2622
+ break;
2623
+ case 'sidebar':
2624
+ setCustomSidebarColor(color);
2625
+ break;
2626
+ case 'border':
2627
+ setCustomBorderColor(color);
2628
+ break;
2629
+ }
2630
+ setActiveColorPicker(null);
2631
+ }}
2632
+ title={
2633
+ activeColorPicker === 'accent'
2634
+ ? 'Custom Accent Color'
2635
+ : activeColorPicker === 'primary'
2636
+ ? 'Custom Primary Color'
2637
+ : activeColorPicker === 'background'
2638
+ ? 'Custom Background Color'
2639
+ : activeColorPicker === 'header'
2640
+ ? 'Custom Header Color'
2641
+ : activeColorPicker === 'sidebar'
2642
+ ? 'Custom Sidebar Color'
2643
+ : activeColorPicker === 'border'
2644
+ ? 'Custom Border Color'
2645
+ : 'Pick a Color'
2646
+ }
2647
+ />
2648
+
2649
+ {/* Reset Stats Confirmation Dialog */}
2650
+ <ConfirmDialog
2651
+ open={showResetStatsDialog}
2652
+ onOpenChange={setShowResetStatsDialog}
2653
+ onConfirm={handleResetStats}
2654
+ title="Reset All Statistics?"
2655
+ message="This will permanently delete all historical data including daily, weekly, and monthly statistics. Your all-time totals will be reset to zero. This action cannot be undone."
2656
+ confirmText="Reset All Stats"
2657
+ variant="destructive"
2658
+ loading={isResettingStats}
2659
+ />
2660
+ </motion.div>
2661
+ );
2662
+ }