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

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 (99) hide show
  1. package/cli.js +132 -21
  2. package/dist/bundle.js +1 -1
  3. package/dist/bundle.js.LICENSE.txt +11 -1
  4. package/package.json +42 -36
  5. package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
  6. package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
  7. package/.rush/temp/operation/build/all.log +0 -35
  8. package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
  9. package/.rush/temp/operation/build/state.json +0 -3
  10. package/.rush/temp/shrinkwrap-deps.json +0 -1225
  11. package/TS_RES_BROWSER_PLAN.md +0 -689
  12. package/babel.config.js +0 -17
  13. package/config/jest.config.json +0 -13
  14. package/config/rig.json +0 -16
  15. package/postcss.config.js +0 -3
  16. package/public/index.html +0 -15
  17. package/rush-logs/ts-res-browser.build.cache.log +0 -3
  18. package/rush-logs/ts-res-browser.build.log +0 -35
  19. package/src/App.tsx +0 -274
  20. package/src/cli.ts +0 -44
  21. package/src/components/common/FileImporter.tsx +0 -143
  22. package/src/components/common/NavigationWarningModal.tsx +0 -62
  23. package/src/components/layout/AppLayout.tsx +0 -46
  24. package/src/components/layout/Header.tsx +0 -22
  25. package/src/components/layout/MessagesWindow.tsx +0 -108
  26. package/src/components/layout/Sidebar.tsx +0 -99
  27. package/src/components/tools/CompiledBrowser.tsx +0 -949
  28. package/src/components/tools/ConfigurationTool.tsx +0 -1405
  29. package/src/components/tools/FilterTool.tsx +0 -1114
  30. package/src/components/tools/ImportTool.tsx +0 -249
  31. package/src/components/tools/ResolutionViewer.tsx +0 -1672
  32. package/src/components/tools/SourceBrowser.tsx +0 -496
  33. package/src/components/tools/ZipLoader.tsx +0 -302
  34. package/src/hooks/useAppState.ts +0 -111
  35. package/src/hooks/useFileImport.ts +0 -86
  36. package/src/hooks/useNavigationWarning.ts +0 -67
  37. package/src/hooks/useResourceManager.ts +0 -295
  38. package/src/hooks/useUrlParams.ts +0 -56
  39. package/src/index.css +0 -38
  40. package/src/main.tsx +0 -16
  41. package/src/types/app.ts +0 -70
  42. package/src/types/fileSystemAccess.d.ts +0 -94
  43. package/src/utils/browserFileTreeAccessors.ts +0 -245
  44. package/src/utils/fileImport.ts +0 -336
  45. package/src/utils/fileTreeConverter.ts +0 -160
  46. package/src/utils/filterResources.ts +0 -677
  47. package/src/utils/tsResIntegration.ts +0 -467
  48. package/src/utils/urlParams.ts +0 -238
  49. package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
  50. package/tailwind.config.js +0 -21
  51. package/test-data/README.md +0 -184
  52. package/test-data/config-variations/README.md +0 -64
  53. package/test-data/config-variations/composition-terminology.json +0 -112
  54. package/test-data/config-variations/enterprise-complex.json +0 -129
  55. package/test-data/config-variations/gaming-app.json +0 -87
  56. package/test-data/config-variations/high-priority-language.json +0 -65
  57. package/test-data/config-variations/minimal-basic.json +0 -37
  58. package/test-data/config-variations/qualifier-default-values.json +0 -71
  59. package/test-data/config-variations/territory-first.json +0 -68
  60. package/test-data/config-variations/territory-hierarchy-test.json +0 -31
  61. package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
  62. package/test-data/default-matching-test/greeting.res.json +0 -83
  63. package/test-data/default-matching-test/only-defaults.res.json +0 -48
  64. package/test-data/sample-project/package.json +0 -27
  65. package/test-data/sample-project/src/resources/app-config.json +0 -27
  66. package/test-data/sample-project/src/resources/app.json +0 -177
  67. package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
  68. package/test-data/sample-project/src/resources/config/app.json +0 -21
  69. package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
  70. package/test-data/sample-project/src/resources/feature-flags.json +0 -23
  71. package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
  72. package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
  73. package/test-data/sample-project/src/resources/images/home.json +0 -29
  74. package/test-data/sample-project/src/resources/images/logout.json +0 -14
  75. package/test-data/sample-project/src/resources/images/settings.json +0 -14
  76. package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
  77. package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
  78. package/test-data/sample-project/src/resources/images/user.json +0 -14
  79. package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
  80. package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
  81. package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
  82. package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
  83. package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
  84. package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
  85. package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
  86. package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
  87. package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
  88. package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
  89. package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
  90. package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
  91. package/test-data/sample-project/src/resources/strings.json +0 -78
  92. package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
  93. package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
  94. package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
  95. package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
  96. package/test-data/sample-project/src/resources/ui-terms.json +0 -96
  97. package/test-data/sample-project/src/resources-config.json +0 -111
  98. package/tsconfig.json +0 -27
  99. 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
- };