@fgv/ts-res-browser 5.0.0-2 → 5.0.0-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/cli.js +97 -21
  2. package/package.json +15 -7
  3. package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
  4. package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
  5. package/.rush/temp/operation/build/all.log +0 -35
  6. package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
  7. package/.rush/temp/operation/build/state.json +0 -3
  8. package/.rush/temp/shrinkwrap-deps.json +0 -1225
  9. package/TS_RES_BROWSER_PLAN.md +0 -689
  10. package/babel.config.js +0 -17
  11. package/config/jest.config.json +0 -13
  12. package/config/rig.json +0 -16
  13. package/postcss.config.js +0 -3
  14. package/public/index.html +0 -15
  15. package/rush-logs/ts-res-browser.build.cache.log +0 -3
  16. package/rush-logs/ts-res-browser.build.log +0 -35
  17. package/src/App.tsx +0 -274
  18. package/src/cli.ts +0 -44
  19. package/src/components/common/FileImporter.tsx +0 -143
  20. package/src/components/common/NavigationWarningModal.tsx +0 -62
  21. package/src/components/layout/AppLayout.tsx +0 -46
  22. package/src/components/layout/Header.tsx +0 -22
  23. package/src/components/layout/MessagesWindow.tsx +0 -108
  24. package/src/components/layout/Sidebar.tsx +0 -99
  25. package/src/components/tools/CompiledBrowser.tsx +0 -949
  26. package/src/components/tools/ConfigurationTool.tsx +0 -1405
  27. package/src/components/tools/FilterTool.tsx +0 -1114
  28. package/src/components/tools/ImportTool.tsx +0 -249
  29. package/src/components/tools/ResolutionViewer.tsx +0 -1672
  30. package/src/components/tools/SourceBrowser.tsx +0 -496
  31. package/src/components/tools/ZipLoader.tsx +0 -302
  32. package/src/hooks/useAppState.ts +0 -111
  33. package/src/hooks/useFileImport.ts +0 -86
  34. package/src/hooks/useNavigationWarning.ts +0 -67
  35. package/src/hooks/useResourceManager.ts +0 -295
  36. package/src/hooks/useUrlParams.ts +0 -56
  37. package/src/index.css +0 -38
  38. package/src/main.tsx +0 -16
  39. package/src/types/app.ts +0 -70
  40. package/src/types/fileSystemAccess.d.ts +0 -94
  41. package/src/utils/browserFileTreeAccessors.ts +0 -245
  42. package/src/utils/fileImport.ts +0 -336
  43. package/src/utils/fileTreeConverter.ts +0 -160
  44. package/src/utils/filterResources.ts +0 -677
  45. package/src/utils/tsResIntegration.ts +0 -467
  46. package/src/utils/urlParams.ts +0 -238
  47. package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
  48. package/tailwind.config.js +0 -21
  49. package/test-data/README.md +0 -184
  50. package/test-data/config-variations/README.md +0 -64
  51. package/test-data/config-variations/composition-terminology.json +0 -112
  52. package/test-data/config-variations/enterprise-complex.json +0 -129
  53. package/test-data/config-variations/gaming-app.json +0 -87
  54. package/test-data/config-variations/high-priority-language.json +0 -65
  55. package/test-data/config-variations/minimal-basic.json +0 -37
  56. package/test-data/config-variations/qualifier-default-values.json +0 -71
  57. package/test-data/config-variations/territory-first.json +0 -68
  58. package/test-data/config-variations/territory-hierarchy-test.json +0 -31
  59. package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
  60. package/test-data/default-matching-test/greeting.res.json +0 -83
  61. package/test-data/default-matching-test/only-defaults.res.json +0 -48
  62. package/test-data/sample-project/package.json +0 -27
  63. package/test-data/sample-project/src/resources/app-config.json +0 -27
  64. package/test-data/sample-project/src/resources/app.json +0 -177
  65. package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
  66. package/test-data/sample-project/src/resources/config/app.json +0 -21
  67. package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
  68. package/test-data/sample-project/src/resources/feature-flags.json +0 -23
  69. package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
  70. package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
  71. package/test-data/sample-project/src/resources/images/home.json +0 -29
  72. package/test-data/sample-project/src/resources/images/logout.json +0 -14
  73. package/test-data/sample-project/src/resources/images/settings.json +0 -14
  74. package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
  75. package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
  76. package/test-data/sample-project/src/resources/images/user.json +0 -14
  77. package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
  78. package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
  79. package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
  80. package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
  81. package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
  82. package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
  83. package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
  84. package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
  85. package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
  86. package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
  87. package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
  88. package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
  89. package/test-data/sample-project/src/resources/strings.json +0 -78
  90. package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
  91. package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
  92. package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
  93. package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
  94. package/test-data/sample-project/src/resources/ui-terms.json +0 -96
  95. package/test-data/sample-project/src/resources-config.json +0 -111
  96. package/tsconfig.json +0 -27
  97. package/webpack.config.js +0 -60
@@ -1,302 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import {
3
- ArchiveBoxIcon,
4
- FolderOpenIcon,
5
- ExclamationTriangleIcon,
6
- CheckCircleIcon,
7
- ArrowRightIcon
8
- } from '@heroicons/react/24/outline';
9
- import { Message } from '../../types/app';
10
- import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
11
- import { BrowserZipFileTreeAccessors } from '../../utils/zip/browserZipFileTreeAccessors';
12
- import { FileTree } from '@fgv/ts-utils';
13
-
14
- interface ZipLoaderProps {
15
- onMessage: (type: Message['type'], message: string) => void;
16
- resourceManager: UseResourceManagerReturn;
17
- zipFile?: string;
18
- zipPath?: string;
19
- onLoadComplete?: () => void;
20
- }
21
-
22
- interface ZipManifest {
23
- timestamp: string;
24
- input?: {
25
- type: 'file' | 'directory';
26
- originalPath: string;
27
- archivePath: string;
28
- };
29
- config?: {
30
- type: 'file';
31
- originalPath: string;
32
- archivePath: string;
33
- };
34
- }
35
-
36
- const ZipLoader: React.FC<ZipLoaderProps> = ({
37
- onMessage,
38
- resourceManager,
39
- zipFile,
40
- zipPath,
41
- onLoadComplete
42
- }) => {
43
- const [isLoading, setIsLoading] = useState(false);
44
- const [loadingStep, setLoadingStep] = useState<string>('');
45
- const [zipFound, setZipFound] = useState<boolean | null>(null);
46
- const [manifest, setManifest] = useState<ZipManifest | null>(null);
47
- const [autoloadAttempted, setAutoloadAttempted] = useState(false);
48
-
49
- // Auto-load ZIP if we have a specific file reference
50
- useEffect(() => {
51
- if ((zipFile || zipPath) && !autoloadAttempted) {
52
- setAutoloadAttempted(true);
53
- handleLoadZip();
54
- }
55
- }, [zipFile, zipPath, autoloadAttempted]);
56
-
57
- const handleLoadZip = async () => {
58
- if (!zipFile && !zipPath) {
59
- onMessage('error', 'No ZIP file specified');
60
- return;
61
- }
62
-
63
- setIsLoading(true);
64
- setLoadingStep('Opening file picker...');
65
-
66
- try {
67
- // Try to load the ZIP file using File System Access API
68
- let fileHandle: FileSystemFileHandle | null = null;
69
-
70
- if ('showOpenFilePicker' in window) {
71
- try {
72
- const [handle] = await (window as any).showOpenFilePicker({
73
- types: [
74
- {
75
- description: 'ZIP Archives',
76
- accept: { 'application/zip': ['.zip'] }
77
- }
78
- ],
79
- startIn: 'downloads',
80
- multiple: false
81
- });
82
- fileHandle = handle;
83
- setZipFound(true);
84
- } catch (error) {
85
- if ((error as DOMException).name !== 'AbortError') {
86
- onMessage('warning', 'Could not open file picker. Please select the ZIP file manually.');
87
- }
88
- setZipFound(false);
89
- return;
90
- }
91
- } else {
92
- onMessage('error', 'File System Access API not supported. Please use a modern browser.');
93
- setZipFound(false);
94
- return;
95
- }
96
-
97
- if (!fileHandle) {
98
- setZipFound(false);
99
- return;
100
- }
101
-
102
- setLoadingStep('Reading ZIP archive...');
103
- const file = await fileHandle.getFile();
104
-
105
- // Create ZIP FileTree
106
- const zipAccessorsResult = await BrowserZipFileTreeAccessors.fromFile(file);
107
- if (zipAccessorsResult.isFailure()) {
108
- onMessage('error', `Failed to read ZIP: ${zipAccessorsResult.message}`);
109
- return;
110
- }
111
-
112
- const zipAccessors = zipAccessorsResult.value;
113
- const fileTree = FileTree.FileTree.create(zipAccessors);
114
- if (fileTree.isFailure()) {
115
- onMessage('error', `Failed to create file tree: ${fileTree.message}`);
116
- return;
117
- }
118
-
119
- setLoadingStep('Reading manifest...');
120
-
121
- // Try to read manifest
122
- const manifestResult = fileTree.value.getFile('/manifest.json');
123
- if (manifestResult.isSuccess()) {
124
- const manifestContents = manifestResult.value.getContents();
125
- if (manifestContents.isSuccess()) {
126
- const parsedManifest = manifestContents.value as ZipManifest;
127
- setManifest(parsedManifest);
128
- onMessage('info', `ZIP created: ${new Date(parsedManifest.timestamp).toLocaleString()}`);
129
- }
130
- }
131
-
132
- // Process resources from the ZIP
133
- setLoadingStep('Processing resources...');
134
-
135
- // Look for input directory/files
136
- let inputProcessed = false;
137
- if (manifest?.input) {
138
- const inputPath = `/${manifest.input.archivePath}`;
139
- const inputResult = fileTree.value.getItem(inputPath);
140
-
141
- if (inputResult.isSuccess()) {
142
- const inputItem = inputResult.value;
143
-
144
- if (inputItem.type === 'directory') {
145
- // Process directory
146
- await resourceManager.actions.processFileTree(fileTree.value, inputPath);
147
- inputProcessed = true;
148
- } else if (inputItem.type === 'file') {
149
- // Process single file
150
- await resourceManager.actions.processFileTree(fileTree.value, inputPath);
151
- inputProcessed = true;
152
- }
153
- }
154
- }
155
-
156
- // Fallback: look for common input patterns
157
- if (!inputProcessed) {
158
- const commonInputPaths = ['/input', '/resources', '/data'];
159
- for (const inputPath of commonInputPaths) {
160
- const inputResult = fileTree.value.getItem(inputPath);
161
- if (inputResult.isSuccess() && inputResult.value.type === 'directory') {
162
- await resourceManager.actions.processFileTree(fileTree.value, inputPath);
163
- inputProcessed = true;
164
- break;
165
- }
166
- }
167
- }
168
-
169
- // Load configuration if present
170
- if (manifest?.config) {
171
- const configPath = `/${manifest.config.archivePath}`;
172
- const configResult = fileTree.value.getFile(configPath);
173
-
174
- if (configResult.isSuccess()) {
175
- const configContents = configResult.value.getContents();
176
- if (configContents.isSuccess()) {
177
- resourceManager.actions.applyConfiguration(configContents.value as any);
178
- onMessage('success', 'Configuration loaded from ZIP');
179
- }
180
- }
181
- }
182
-
183
- if (inputProcessed) {
184
- onMessage('success', 'Resources loaded successfully from ZIP!');
185
- onLoadComplete?.();
186
- } else {
187
- onMessage('warning', 'ZIP loaded but no resources found. Check ZIP contents.');
188
- }
189
- } catch (error) {
190
- onMessage('error', `Failed to load ZIP: ${error instanceof Error ? error.message : String(error)}`);
191
- setZipFound(false);
192
- } finally {
193
- setIsLoading(false);
194
- setLoadingStep('');
195
- }
196
- };
197
-
198
- const expectedFileName = zipFile || 'ts-res-bundle-*.zip';
199
-
200
- return (
201
- <div className="p-6">
202
- <div className="flex items-center space-x-3 mb-6">
203
- <ArchiveBoxIcon className="h-8 w-8 text-purple-600" />
204
- <h2 className="text-2xl font-bold text-gray-900">Load ZIP Archive</h2>
205
- </div>
206
-
207
- <div className="max-w-2xl">
208
- {/* ZIP File Info */}
209
- <div className="bg-blue-50 rounded-lg p-4 mb-6">
210
- <div className="flex items-start space-x-3">
211
- <ArchiveBoxIcon className="h-5 w-5 text-blue-600 mt-0.5" />
212
- <div className="text-sm text-blue-800">
213
- <p className="font-medium">ZIP Archive Ready</p>
214
- {zipPath && (
215
- <p className="mt-1">
216
- <span className="font-medium">Location:</span> {zipPath}
217
- </p>
218
- )}
219
- {zipFile && (
220
- <p className="mt-1">
221
- <span className="font-medium">File:</span> {zipFile}
222
- </p>
223
- )}
224
- <p className="mt-2">
225
- The CLI has created a ZIP archive containing your resources and configuration. Click below to
226
- load it directly into the browser.
227
- </p>
228
- </div>
229
- </div>
230
- </div>
231
-
232
- {/* Load Button */}
233
- <div className="space-y-4">
234
- <button
235
- onClick={handleLoadZip}
236
- disabled={isLoading}
237
- className="w-full flex items-center justify-center space-x-2 px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
238
- >
239
- {isLoading ? (
240
- <>
241
- <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
242
- <span>{loadingStep || 'Loading...'}</span>
243
- </>
244
- ) : (
245
- <>
246
- <FolderOpenIcon className="w-5 h-5" />
247
- <span>Load ZIP Archive</span>
248
- <ArrowRightIcon className="w-5 h-5" />
249
- </>
250
- )}
251
- </button>
252
-
253
- {/* Instructions */}
254
- <div className="bg-gray-50 rounded-lg p-4">
255
- <h3 className="text-sm font-medium text-gray-900 mb-2">Instructions:</h3>
256
- <ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
257
- <li>Click "Load ZIP Archive" above</li>
258
- <li>
259
- Select the ZIP file: <code className="bg-gray-200 px-1 rounded">{expectedFileName}</code>
260
- </li>
261
- <li>The browser will automatically process your resources and configuration</li>
262
- </ol>
263
- </div>
264
-
265
- {/* Manifest Info */}
266
- {manifest && (
267
- <div className="bg-green-50 rounded-lg p-4">
268
- <div className="flex items-start space-x-3">
269
- <CheckCircleIcon className="h-5 w-5 text-green-600 mt-0.5" />
270
- <div className="text-sm text-green-800">
271
- <p className="font-medium">ZIP Archive Contents</p>
272
- <div className="mt-2 space-y-1">
273
- {manifest.input && <p>📁 Resources: {manifest.input.originalPath}</p>}
274
- {manifest.config && <p>⚙️ Configuration: {manifest.config.originalPath}</p>}
275
- </div>
276
- </div>
277
- </div>
278
- </div>
279
- )}
280
-
281
- {/* Error State */}
282
- {zipFound === false && (
283
- <div className="bg-yellow-50 rounded-lg p-4">
284
- <div className="flex items-start space-x-3">
285
- <ExclamationTriangleIcon className="h-5 w-5 text-yellow-600 mt-0.5" />
286
- <div className="text-sm text-yellow-800">
287
- <p className="font-medium">ZIP File Not Found</p>
288
- <p className="mt-1">
289
- Could not locate or open the ZIP file. Please make sure it exists in your Downloads folder
290
- and try again.
291
- </p>
292
- </div>
293
- </div>
294
- </div>
295
- )}
296
- </div>
297
- </div>
298
- </div>
299
- );
300
- };
301
-
302
- export default ZipLoader;
@@ -1,111 +0,0 @@
1
- import { useState, useCallback } from 'react';
2
- import { AppState, AppActions, Tool, Message, FilterState } from '../types/app';
3
-
4
- export const useAppState = (): { state: AppState; actions: AppActions } => {
5
- const [state, setState] = useState<AppState>({
6
- selectedTool: 'import',
7
- messages: [],
8
- filterState: {
9
- enabled: false,
10
- values: {},
11
- appliedValues: {},
12
- hasPendingChanges: false,
13
- reduceQualifiers: false
14
- }
15
- });
16
-
17
- const setSelectedTool = useCallback((tool: Tool, force?: boolean) => {
18
- setState((prev) => ({ ...prev, selectedTool: tool }));
19
- }, []);
20
-
21
- // Alias for setSelectedTool to match the interface
22
- const setActiveTool = setSelectedTool;
23
-
24
- const addMessage = useCallback((type: Message['type'], message: string) => {
25
- const newMessage: Message = {
26
- id: Date.now().toString(),
27
- type,
28
- message,
29
- timestamp: new Date()
30
- };
31
- setState((prev) => ({
32
- ...prev,
33
- messages: [...prev.messages, newMessage]
34
- }));
35
- }, []);
36
-
37
- const clearMessages = useCallback(() => {
38
- setState((prev) => ({ ...prev, messages: [] }));
39
- }, []);
40
-
41
- const updateFilterEnabled = useCallback((enabled: boolean) => {
42
- setState((prev) => ({
43
- ...prev,
44
- filterState: {
45
- ...prev.filterState,
46
- enabled,
47
- // Reset applied values when disabling
48
- appliedValues: enabled ? prev.filterState.appliedValues : {},
49
- hasPendingChanges: false
50
- }
51
- }));
52
- }, []);
53
-
54
- const updateFilterValues = useCallback((values: Record<string, string>) => {
55
- setState((prev) => ({
56
- ...prev,
57
- filterState: {
58
- ...prev.filterState,
59
- values,
60
- hasPendingChanges: JSON.stringify(values) !== JSON.stringify(prev.filterState.appliedValues)
61
- }
62
- }));
63
- }, []);
64
-
65
- const applyFilterValues = useCallback(() => {
66
- setState((prev) => ({
67
- ...prev,
68
- filterState: {
69
- ...prev.filterState,
70
- appliedValues: { ...prev.filterState.values },
71
- hasPendingChanges: false
72
- }
73
- }));
74
- }, []);
75
-
76
- const resetFilterValues = useCallback(() => {
77
- setState((prev) => ({
78
- ...prev,
79
- filterState: {
80
- ...prev.filterState,
81
- values: {},
82
- appliedValues: {},
83
- hasPendingChanges: false
84
- }
85
- }));
86
- }, []);
87
-
88
- const updateReduceQualifiers = useCallback((reduceQualifiers: boolean) => {
89
- setState((prev) => ({
90
- ...prev,
91
- filterState: {
92
- ...prev.filterState,
93
- reduceQualifiers
94
- }
95
- }));
96
- }, []);
97
-
98
- const actions: AppActions = {
99
- setSelectedTool,
100
- setActiveTool,
101
- addMessage,
102
- clearMessages,
103
- updateFilterEnabled,
104
- updateFilterValues,
105
- applyFilterValues,
106
- resetFilterValues,
107
- updateReduceQualifiers
108
- };
109
-
110
- return { state, actions };
111
- };
@@ -1,86 +0,0 @@
1
- import { useState, useCallback } from 'react';
2
- import { Result } from '@fgv/ts-utils';
3
- import { ImportedDirectory, ImportedFile } from '../utils/fileImport';
4
-
5
- export interface FileImportState {
6
- isLoading: boolean;
7
- importedDirectory: ImportedDirectory | null;
8
- importedFiles: ImportedFile[];
9
- error: string | null;
10
- hasImportedData: boolean;
11
- }
12
-
13
- export interface UseFileImportReturn {
14
- state: FileImportState;
15
- actions: {
16
- handleDirectoryImport: (directory: ImportedDirectory) => void;
17
- handleFilesImport: (files: ImportedFile[]) => void;
18
- handleError: (error: string) => void;
19
- clearError: () => void;
20
- reset: () => void;
21
- };
22
- }
23
-
24
- const initialState: FileImportState = {
25
- isLoading: false,
26
- importedDirectory: null,
27
- importedFiles: [],
28
- error: null,
29
- hasImportedData: false
30
- };
31
-
32
- export const useFileImport = (): UseFileImportReturn => {
33
- const [state, setState] = useState<FileImportState>(initialState);
34
-
35
- const handleDirectoryImport = useCallback((directory: ImportedDirectory) => {
36
- setState((prev) => ({
37
- ...prev,
38
- importedDirectory: directory,
39
- importedFiles: [], // Clear individual files when directory is imported
40
- error: null,
41
- hasImportedData: true
42
- }));
43
- }, []);
44
-
45
- const handleFilesImport = useCallback((files: ImportedFile[]) => {
46
- setState((prev) => ({
47
- ...prev,
48
- importedFiles: files,
49
- importedDirectory: null, // Clear directory when individual files are imported
50
- error: null,
51
- hasImportedData: true
52
- }));
53
- }, []);
54
-
55
- const handleError = useCallback((error: string) => {
56
- setState((prev) => ({
57
- ...prev,
58
- error,
59
- isLoading: false
60
- }));
61
- }, []);
62
-
63
- const clearError = useCallback(() => {
64
- setState((prev) => ({
65
- ...prev,
66
- error: null
67
- }));
68
- }, []);
69
-
70
- const reset = useCallback(() => {
71
- setState(initialState);
72
- }, []);
73
-
74
- return {
75
- state,
76
- actions: {
77
- handleDirectoryImport,
78
- handleFilesImport,
79
- handleError,
80
- clearError,
81
- reset
82
- }
83
- };
84
- };
85
-
86
- export default useFileImport;
@@ -1,67 +0,0 @@
1
- import { useState, useCallback } from 'react';
2
- import { Tool } from '../types/app';
3
-
4
- export interface NavigationWarningState {
5
- isWarningOpen: boolean;
6
- pendingTool: Tool | null;
7
- hasUnsavedChanges: boolean;
8
- }
9
-
10
- export interface NavigationWarningActions {
11
- setHasUnsavedChanges: (hasChanges: boolean) => void;
12
- showWarning: (pendingTool: Tool) => void;
13
- hideWarning: () => void;
14
- confirmNavigation: () => Tool | null;
15
- }
16
-
17
- export const useNavigationWarning = (): {
18
- state: NavigationWarningState;
19
- actions: NavigationWarningActions;
20
- } => {
21
- const [state, setState] = useState<NavigationWarningState>({
22
- isWarningOpen: false,
23
- pendingTool: null,
24
- hasUnsavedChanges: false
25
- });
26
-
27
- const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
28
- setState((prev) => ({ ...prev, hasUnsavedChanges: hasChanges }));
29
- }, []);
30
-
31
- const showWarning = useCallback((pendingTool: Tool) => {
32
- setState((prev) => ({
33
- ...prev,
34
- isWarningOpen: true,
35
- pendingTool
36
- }));
37
- }, []);
38
-
39
- const hideWarning = useCallback(() => {
40
- setState((prev) => ({
41
- ...prev,
42
- isWarningOpen: false,
43
- pendingTool: null
44
- }));
45
- }, []);
46
-
47
- const confirmNavigation = useCallback(() => {
48
- const toolToNavigateTo = state.pendingTool;
49
- setState((prev) => ({
50
- ...prev,
51
- isWarningOpen: false,
52
- pendingTool: null,
53
- hasUnsavedChanges: false
54
- }));
55
- return toolToNavigateTo;
56
- }, [state.pendingTool]);
57
-
58
- return {
59
- state,
60
- actions: {
61
- setHasUnsavedChanges,
62
- showWarning,
63
- hideWarning,
64
- confirmNavigation
65
- }
66
- };
67
- };