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.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- 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'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
|
+
}
|