@fgv/ts-res-ui-components 5.0.0-10

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 (231) hide show
  1. package/.rush/temp/03c8b056281d9db0a97d8a6e25eea798a160d393.tar.log +271 -0
  2. package/.rush/temp/chunked-rush-logs/ts-res-ui-components.build.chunks.jsonl +9 -0
  3. package/.rush/temp/operation/build/all.log +9 -0
  4. package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
  5. package/.rush/temp/operation/build/state.json +3 -0
  6. package/.rush/temp/shrinkwrap-deps.json +1111 -0
  7. package/README.md +18 -0
  8. package/REFACTORING_PLAN.md +171 -0
  9. package/config/jest.config.json +16 -0
  10. package/config/jest.setup.js +64 -0
  11. package/config/rig.json +16 -0
  12. package/lib/components/common/QualifierContextControl.d.ts +14 -0
  13. package/lib/components/common/QualifierContextControl.d.ts.map +1 -0
  14. package/lib/components/common/QualifierContextControl.js +78 -0
  15. package/lib/components/common/QualifierContextControl.js.map +1 -0
  16. package/lib/components/common/ResourceListView.d.ts +11 -0
  17. package/lib/components/common/ResourceListView.d.ts.map +1 -0
  18. package/lib/components/common/ResourceListView.js +20 -0
  19. package/lib/components/common/ResourceListView.js.map +1 -0
  20. package/lib/components/common/ResourceTreeView.d.ts +12 -0
  21. package/lib/components/common/ResourceTreeView.d.ts.map +1 -0
  22. package/lib/components/common/ResourceTreeView.js +162 -0
  23. package/lib/components/common/ResourceTreeView.js.map +1 -0
  24. package/lib/components/forms/HierarchyEditor.d.ts +10 -0
  25. package/lib/components/forms/HierarchyEditor.d.ts.map +1 -0
  26. package/lib/components/forms/HierarchyEditor.js +106 -0
  27. package/lib/components/forms/HierarchyEditor.js.map +1 -0
  28. package/lib/components/forms/QualifierEditForm.d.ts +11 -0
  29. package/lib/components/forms/QualifierEditForm.d.ts.map +1 -0
  30. package/lib/components/forms/QualifierEditForm.js +181 -0
  31. package/lib/components/forms/QualifierEditForm.js.map +1 -0
  32. package/lib/components/forms/QualifierTypeEditForm.d.ts +10 -0
  33. package/lib/components/forms/QualifierTypeEditForm.d.ts.map +1 -0
  34. package/lib/components/forms/QualifierTypeEditForm.js +172 -0
  35. package/lib/components/forms/QualifierTypeEditForm.js.map +1 -0
  36. package/lib/components/forms/ResourceTypeEditForm.d.ts +10 -0
  37. package/lib/components/forms/ResourceTypeEditForm.d.ts.map +1 -0
  38. package/lib/components/forms/ResourceTypeEditForm.js +188 -0
  39. package/lib/components/forms/ResourceTypeEditForm.js.map +1 -0
  40. package/lib/components/forms/index.d.ts +9 -0
  41. package/lib/components/forms/index.d.ts.map +1 -0
  42. package/lib/components/forms/index.js +5 -0
  43. package/lib/components/forms/index.js.map +1 -0
  44. package/lib/components/orchestrator/ResourceOrchestrator.d.ts +14 -0
  45. package/lib/components/orchestrator/ResourceOrchestrator.d.ts.map +1 -0
  46. package/lib/components/orchestrator/ResourceOrchestrator.js +278 -0
  47. package/lib/components/orchestrator/ResourceOrchestrator.js.map +1 -0
  48. package/lib/components/views/CompiledView/index.d.ts +5 -0
  49. package/lib/components/views/CompiledView/index.d.ts.map +1 -0
  50. package/lib/components/views/CompiledView/index.js +595 -0
  51. package/lib/components/views/CompiledView/index.js.map +1 -0
  52. package/lib/components/views/ConfigurationView/index.d.ts +5 -0
  53. package/lib/components/views/ConfigurationView/index.d.ts.map +1 -0
  54. package/lib/components/views/ConfigurationView/index.js +363 -0
  55. package/lib/components/views/ConfigurationView/index.js.map +1 -0
  56. package/lib/components/views/FilterView/index.d.ts +5 -0
  57. package/lib/components/views/FilterView/index.d.ts.map +1 -0
  58. package/lib/components/views/FilterView/index.js +463 -0
  59. package/lib/components/views/FilterView/index.js.map +1 -0
  60. package/lib/components/views/ImportView/index.d.ts +5 -0
  61. package/lib/components/views/ImportView/index.d.ts.map +1 -0
  62. package/lib/components/views/ImportView/index.js +514 -0
  63. package/lib/components/views/ImportView/index.js.map +1 -0
  64. package/lib/components/views/ResolutionView/EditableJsonView.d.ts +21 -0
  65. package/lib/components/views/ResolutionView/EditableJsonView.d.ts.map +1 -0
  66. package/lib/components/views/ResolutionView/EditableJsonView.js +109 -0
  67. package/lib/components/views/ResolutionView/EditableJsonView.js.map +1 -0
  68. package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts +19 -0
  69. package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts.map +1 -0
  70. package/lib/components/views/ResolutionView/ResolutionEditControls.js +82 -0
  71. package/lib/components/views/ResolutionView/ResolutionEditControls.js.map +1 -0
  72. package/lib/components/views/ResolutionView/index.d.ts +5 -0
  73. package/lib/components/views/ResolutionView/index.d.ts.map +1 -0
  74. package/lib/components/views/ResolutionView/index.js +255 -0
  75. package/lib/components/views/ResolutionView/index.js.map +1 -0
  76. package/lib/components/views/SourceView/index.d.ts +5 -0
  77. package/lib/components/views/SourceView/index.d.ts.map +1 -0
  78. package/lib/components/views/SourceView/index.js +316 -0
  79. package/lib/components/views/SourceView/index.js.map +1 -0
  80. package/lib/components/views/ZipLoaderView/index.d.ts +5 -0
  81. package/lib/components/views/ZipLoaderView/index.d.ts.map +1 -0
  82. package/lib/components/views/ZipLoaderView/index.js +313 -0
  83. package/lib/components/views/ZipLoaderView/index.js.map +1 -0
  84. package/lib/hooks/useConfigurationState.d.ts +46 -0
  85. package/lib/hooks/useConfigurationState.d.ts.map +1 -0
  86. package/lib/hooks/useConfigurationState.js +239 -0
  87. package/lib/hooks/useConfigurationState.js.map +1 -0
  88. package/lib/hooks/useFilterState.d.ts +7 -0
  89. package/lib/hooks/useFilterState.d.ts.map +1 -0
  90. package/lib/hooks/useFilterState.js +80 -0
  91. package/lib/hooks/useFilterState.js.map +1 -0
  92. package/lib/hooks/useResolutionState.d.ts +8 -0
  93. package/lib/hooks/useResolutionState.d.ts.map +1 -0
  94. package/lib/hooks/useResolutionState.js +253 -0
  95. package/lib/hooks/useResolutionState.js.map +1 -0
  96. package/lib/hooks/useResourceData.d.ts +19 -0
  97. package/lib/hooks/useResourceData.d.ts.map +1 -0
  98. package/lib/hooks/useResourceData.js +368 -0
  99. package/lib/hooks/useResourceData.js.map +1 -0
  100. package/lib/hooks/useViewState.d.ts +10 -0
  101. package/lib/hooks/useViewState.d.ts.map +1 -0
  102. package/lib/hooks/useViewState.js +29 -0
  103. package/lib/hooks/useViewState.js.map +1 -0
  104. package/lib/index.d.ts +27 -0
  105. package/lib/index.d.ts.map +1 -0
  106. package/lib/index.js +34 -0
  107. package/lib/index.js.map +1 -0
  108. package/lib/test/helpers/testDataLoader.d.ts +37 -0
  109. package/lib/test/helpers/testDataLoader.d.ts.map +1 -0
  110. package/lib/test/helpers/testDataLoader.js +171 -0
  111. package/lib/test/helpers/testDataLoader.js.map +1 -0
  112. package/lib/test/unit/utils/configurationUtils.test.d.ts +2 -0
  113. package/lib/test/unit/utils/configurationUtils.test.d.ts.map +1 -0
  114. package/lib/test/unit/utils/configurationUtils.test.js +497 -0
  115. package/lib/test/unit/utils/configurationUtils.test.js.map +1 -0
  116. package/lib/test/unit/utils/fileProcessing.test.d.ts +2 -0
  117. package/lib/test/unit/utils/fileProcessing.test.d.ts.map +1 -0
  118. package/lib/test/unit/utils/fileProcessing.test.js +321 -0
  119. package/lib/test/unit/utils/fileProcessing.test.js.map +1 -0
  120. package/lib/test/unit/utils/filterResources.test.d.ts +2 -0
  121. package/lib/test/unit/utils/filterResources.test.d.ts.map +1 -0
  122. package/lib/test/unit/utils/filterResources.test.js +403 -0
  123. package/lib/test/unit/utils/filterResources.test.js.map +1 -0
  124. package/lib/test/unit/utils/resolutionEditing.test.d.ts +2 -0
  125. package/lib/test/unit/utils/resolutionEditing.test.d.ts.map +1 -0
  126. package/lib/test/unit/utils/resolutionEditing.test.js +439 -0
  127. package/lib/test/unit/utils/resolutionEditing.test.js.map +1 -0
  128. package/lib/test/unit/utils/resolutionUtils.test.d.ts +2 -0
  129. package/lib/test/unit/utils/resolutionUtils.test.d.ts.map +1 -0
  130. package/lib/test/unit/utils/resolutionUtils.test.js +397 -0
  131. package/lib/test/unit/utils/resolutionUtils.test.js.map +1 -0
  132. package/lib/test/unit/utils/tsResIntegration.test.d.ts +2 -0
  133. package/lib/test/unit/utils/tsResIntegration.test.d.ts.map +1 -0
  134. package/lib/test/unit/utils/tsResIntegration.test.js +376 -0
  135. package/lib/test/unit/utils/tsResIntegration.test.js.map +1 -0
  136. package/lib/types/index.d.ts +251 -0
  137. package/lib/types/index.d.ts.map +1 -0
  138. package/lib/types/index.js +2 -0
  139. package/lib/types/index.js.map +1 -0
  140. package/lib/utils/configurationUtils.d.ts +74 -0
  141. package/lib/utils/configurationUtils.d.ts.map +1 -0
  142. package/lib/utils/configurationUtils.js +359 -0
  143. package/lib/utils/configurationUtils.js.map +1 -0
  144. package/lib/utils/fileProcessing.d.ts +18 -0
  145. package/lib/utils/fileProcessing.d.ts.map +1 -0
  146. package/lib/utils/fileProcessing.js +142 -0
  147. package/lib/utils/fileProcessing.js.map +1 -0
  148. package/lib/utils/filterResources.d.ts +38 -0
  149. package/lib/utils/filterResources.d.ts.map +1 -0
  150. package/lib/utils/filterResources.js +153 -0
  151. package/lib/utils/filterResources.js.map +1 -0
  152. package/lib/utils/resolutionEditing.d.ts +58 -0
  153. package/lib/utils/resolutionEditing.d.ts.map +1 -0
  154. package/lib/utils/resolutionEditing.js +246 -0
  155. package/lib/utils/resolutionEditing.js.map +1 -0
  156. package/lib/utils/resolutionUtils.d.ts +28 -0
  157. package/lib/utils/resolutionUtils.d.ts.map +1 -0
  158. package/lib/utils/resolutionUtils.js +216 -0
  159. package/lib/utils/resolutionUtils.js.map +1 -0
  160. package/lib/utils/tsResIntegration.d.ts +71 -0
  161. package/lib/utils/tsResIntegration.d.ts.map +1 -0
  162. package/lib/utils/tsResIntegration.js +294 -0
  163. package/lib/utils/tsResIntegration.js.map +1 -0
  164. package/lib/utils/zipLoader/browserZipLoader.d.ts +48 -0
  165. package/lib/utils/zipLoader/browserZipLoader.d.ts.map +1 -0
  166. package/lib/utils/zipLoader/browserZipLoader.js +247 -0
  167. package/lib/utils/zipLoader/browserZipLoader.js.map +1 -0
  168. package/lib/utils/zipLoader/index.d.ts +8 -0
  169. package/lib/utils/zipLoader/index.d.ts.map +1 -0
  170. package/lib/utils/zipLoader/index.js +13 -0
  171. package/lib/utils/zipLoader/index.js.map +1 -0
  172. package/lib/utils/zipLoader/nodeZipBuilder.d.ts +55 -0
  173. package/lib/utils/zipLoader/nodeZipBuilder.d.ts.map +1 -0
  174. package/lib/utils/zipLoader/nodeZipBuilder.js +98 -0
  175. package/lib/utils/zipLoader/nodeZipBuilder.js.map +1 -0
  176. package/lib/utils/zipLoader/types.d.ts +139 -0
  177. package/lib/utils/zipLoader/types.d.ts.map +1 -0
  178. package/lib/utils/zipLoader/types.js +2 -0
  179. package/lib/utils/zipLoader/types.js.map +1 -0
  180. package/lib/utils/zipLoader/zipUtils.d.ts +53 -0
  181. package/lib/utils/zipLoader/zipUtils.d.ts.map +1 -0
  182. package/lib/utils/zipLoader/zipUtils.js +229 -0
  183. package/lib/utils/zipLoader/zipUtils.js.map +1 -0
  184. package/package.json +69 -0
  185. package/rush-logs/ts-res-ui-components.build.cache.log +3 -0
  186. package/rush-logs/ts-res-ui-components.build.log +9 -0
  187. package/src/components/common/QualifierContextControl.tsx +151 -0
  188. package/src/components/common/ResourceListView.tsx +63 -0
  189. package/src/components/common/ResourceTreeView.tsx +271 -0
  190. package/src/components/forms/HierarchyEditor.tsx +204 -0
  191. package/src/components/forms/QualifierEditForm.tsx +355 -0
  192. package/src/components/forms/QualifierTypeEditForm.tsx +347 -0
  193. package/src/components/forms/ResourceTypeEditForm.tsx +331 -0
  194. package/src/components/forms/index.ts +11 -0
  195. package/src/components/orchestrator/ResourceOrchestrator.tsx +372 -0
  196. package/src/components/views/CompiledView/index.tsx +922 -0
  197. package/src/components/views/ConfigurationView/index.tsx +800 -0
  198. package/src/components/views/FilterView/index.tsx +825 -0
  199. package/src/components/views/ImportView/index.tsx +717 -0
  200. package/src/components/views/ResolutionView/EditableJsonView.tsx +214 -0
  201. package/src/components/views/ResolutionView/ResolutionEditControls.tsx +170 -0
  202. package/src/components/views/ResolutionView/index.tsx +591 -0
  203. package/src/components/views/SourceView/index.tsx +536 -0
  204. package/src/components/views/ZipLoaderView/index.tsx +485 -0
  205. package/src/hooks/useConfigurationState.ts +374 -0
  206. package/src/hooks/useFilterState.ts +97 -0
  207. package/src/hooks/useResolutionState.ts +355 -0
  208. package/src/hooks/useResourceData.ts +467 -0
  209. package/src/hooks/useViewState.ts +44 -0
  210. package/src/index.ts +45 -0
  211. package/src/test/helpers/testDataLoader.ts +195 -0
  212. package/src/test/unit/utils/configurationUtils.test.ts +630 -0
  213. package/src/test/unit/utils/fileProcessing.test.ts +391 -0
  214. package/src/test/unit/utils/filterResources.test.ts +574 -0
  215. package/src/test/unit/utils/resolutionEditing.test.ts +556 -0
  216. package/src/test/unit/utils/resolutionUtils.test.ts +521 -0
  217. package/src/test/unit/utils/tsResIntegration.test.ts +433 -0
  218. package/src/types/index.ts +322 -0
  219. package/src/utils/configurationUtils.ts +424 -0
  220. package/src/utils/fileProcessing.ts +160 -0
  221. package/src/utils/filterResources.ts +206 -0
  222. package/src/utils/resolutionEditing.ts +319 -0
  223. package/src/utils/resolutionUtils.ts +289 -0
  224. package/src/utils/tsResIntegration.ts +440 -0
  225. package/src/utils/zipLoader/browserZipLoader.ts +319 -0
  226. package/src/utils/zipLoader/index.ts +26 -0
  227. package/src/utils/zipLoader/nodeZipBuilder.ts +153 -0
  228. package/src/utils/zipLoader/types.ts +175 -0
  229. package/src/utils/zipLoader/zipUtils.ts +266 -0
  230. package/temp/build/typescript/ts_gZid87Hu.json +1 -0
  231. package/tsconfig.json +15 -0
@@ -0,0 +1,717 @@
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
+ import {
3
+ DocumentArrowUpIcon,
4
+ CheckCircleIcon,
5
+ ExclamationTriangleIcon,
6
+ ArchiveBoxIcon,
7
+ FolderOpenIcon
8
+ } from '@heroicons/react/24/outline';
9
+ import { ImportViewProps, ImportedFile, ImportedDirectory, ExtendedProcessedResources } from '../../../types';
10
+ import { Bundle, Config } from '@fgv/ts-res';
11
+ import { isZipFile } from '../../../utils/zipLoader';
12
+
13
+ interface FileInputResult {
14
+ files: ImportedFile[];
15
+ directory?: ImportedDirectory;
16
+ bundleFile?: ImportedFile & { bundle?: Bundle.IBundle };
17
+ }
18
+
19
+ export const ImportView: React.FC<ImportViewProps> = ({
20
+ onImport,
21
+ onBundleImport,
22
+ onZipImport,
23
+ acceptedFileTypes = ['.json', '.zip'],
24
+ onMessage,
25
+ className = ''
26
+ }) => {
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [importStatus, setImportStatus] = useState<{
29
+ hasImported: boolean;
30
+ fileCount: number;
31
+ isDirectory: boolean;
32
+ isBundle: boolean;
33
+ isZip: boolean;
34
+ }>({
35
+ hasImported: false,
36
+ fileCount: 0,
37
+ isDirectory: false,
38
+ isBundle: false,
39
+ isZip: false
40
+ });
41
+ const [error, setError] = useState<string | null>(null);
42
+ const fileInputRef = useRef<HTMLInputElement>(null);
43
+ const dirInputRef = useRef<HTMLInputElement>(null);
44
+
45
+ // Check for File System Access API support
46
+ const isFileSystemAccessSupported = 'showDirectoryPicker' in window || 'showOpenFilePicker' in window;
47
+
48
+ // Handle file selection
49
+ const handleFileSelect = useCallback(
50
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
51
+ const files = event.target.files;
52
+ if (!files || files.length === 0) return;
53
+
54
+ setIsLoading(true);
55
+ setError(null);
56
+
57
+ try {
58
+ // Handle single file selection - check file type first
59
+ if (files.length === 1) {
60
+ const file = files[0];
61
+
62
+ // Check if it's a ZIP file first
63
+ if (isZipFile(file.name)) {
64
+ console.log(`[ImportView] ✅ ${file.name} detected as ZIP file`);
65
+ onMessage?.('info', `Processing ZIP file: ${file.name}`);
66
+
67
+ // Just pass the File object directly to onZipImport
68
+ // The App will handle creating the FileTree
69
+ if (onZipImport) {
70
+ setImportStatus({
71
+ hasImported: true,
72
+ fileCount: 1,
73
+ isDirectory: false,
74
+ isBundle: false,
75
+ isZip: true
76
+ });
77
+
78
+ onMessage?.('success', `ZIP file detected: ${file.name}`);
79
+ onZipImport(file, undefined);
80
+
81
+ setIsLoading(false);
82
+ return;
83
+ } else {
84
+ throw new Error('No ZIP import handler configured');
85
+ }
86
+ }
87
+ }
88
+
89
+ // Handle regular files (non-ZIP)
90
+ const importedFiles: ImportedFile[] = [];
91
+ let bundleFile: (ImportedFile & { bundle?: Bundle.IBundle }) | undefined;
92
+
93
+ for (let i = 0; i < files.length; i++) {
94
+ const file = files[i];
95
+ const content = await readFileContent(file);
96
+
97
+ const importedFile: ImportedFile = {
98
+ name: file.name,
99
+ path: file.webkitRelativePath || file.name,
100
+ content,
101
+ type: file.type
102
+ };
103
+
104
+ // Check if it's a bundle file using proper detection
105
+ let isCurrentFileBundle = false;
106
+ try {
107
+ const parsedData = JSON.parse(content);
108
+
109
+ console.log(`[ImportView] Checking if ${file.name} is a bundle...`);
110
+
111
+ // Use BundleUtils for proper bundle detection
112
+ if (Bundle.BundleUtils.isBundleFile(parsedData)) {
113
+ console.log(`[ImportView] ✅ ${file.name} detected as bundle file`);
114
+ bundleFile = { ...importedFile, bundle: parsedData };
115
+ isCurrentFileBundle = true;
116
+ } else if (Bundle.BundleUtils.isBundleFileName(file.name)) {
117
+ // File name suggests it's a bundle, but content doesn't match - log a warning
118
+ console.warn(
119
+ `[ImportView] ⚠️ File ${file.name} appears to be a bundle by name but content doesn't match bundle structure`
120
+ );
121
+ } else {
122
+ console.log(`[ImportView] ❌ ${file.name} is not a bundle file`);
123
+ }
124
+ } catch (parseError) {
125
+ console.log(`[ImportView] ❌ ${file.name} failed JSON parsing:`, parseError);
126
+ // Not valid JSON or not a bundle, treat as regular file
127
+ }
128
+
129
+ // Only add to regular files if this specific file is not a bundle
130
+ if (!isCurrentFileBundle) {
131
+ importedFiles.push(importedFile);
132
+ }
133
+ }
134
+
135
+ // Process results
136
+ if (bundleFile) {
137
+ console.log(`[ImportView] Processing bundle file: ${bundleFile.name}`, bundleFile.bundle);
138
+
139
+ setImportStatus({
140
+ hasImported: true,
141
+ fileCount: 1,
142
+ isDirectory: false,
143
+ isBundle: true,
144
+ isZip: false
145
+ });
146
+ onMessage?.('info', `Bundle file detected: ${bundleFile.name}`);
147
+ if (onBundleImport && bundleFile.bundle) {
148
+ console.log(`[ImportView] Calling onBundleImport with bundle data`);
149
+ onBundleImport(bundleFile.bundle);
150
+ } else {
151
+ console.warn(`[ImportView] No bundle import handler or bundle data missing`);
152
+ }
153
+ } else if (importedFiles.length > 0) {
154
+ setImportStatus({
155
+ hasImported: true,
156
+ fileCount: importedFiles.length,
157
+ isDirectory: false,
158
+ isBundle: false,
159
+ isZip: false
160
+ });
161
+ onMessage?.('success', `Imported ${importedFiles.length} file(s)`);
162
+ onImport?.(importedFiles);
163
+ }
164
+ } catch (err) {
165
+ const errorMsg = err instanceof Error ? err.message : String(err);
166
+ setError(errorMsg);
167
+ onMessage?.('error', `Import failed: ${errorMsg}`);
168
+ } finally {
169
+ setIsLoading(false);
170
+ // Reset input
171
+ if (event.target) {
172
+ event.target.value = '';
173
+ }
174
+ }
175
+ },
176
+ [onImport, onBundleImport, onMessage]
177
+ );
178
+
179
+ // Handle directory selection (for browsers with webkitdirectory support)
180
+ const handleDirectorySelect = useCallback(
181
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
182
+ const files = event.target.files;
183
+ if (!files || files.length === 0) return;
184
+
185
+ setIsLoading(true);
186
+ setError(null);
187
+
188
+ try {
189
+ const filesByPath = new Map<string, ImportedFile[]>();
190
+ const dirPaths = new Set<string>();
191
+
192
+ for (let i = 0; i < files.length; i++) {
193
+ const file = files[i];
194
+ const content = await readFileContent(file);
195
+ const path = file.webkitRelativePath;
196
+
197
+ if (path) {
198
+ const parts = path.split('/');
199
+ const dirPath = parts.slice(0, -1).join('/');
200
+ dirPaths.add(dirPath);
201
+
202
+ if (!filesByPath.has(dirPath)) {
203
+ filesByPath.set(dirPath, []);
204
+ }
205
+
206
+ filesByPath.get(dirPath)!.push({
207
+ name: parts[parts.length - 1],
208
+ path: path,
209
+ content,
210
+ type: file.type
211
+ });
212
+ }
213
+ }
214
+
215
+ // Build directory structure
216
+ const rootDir: ImportedDirectory = {
217
+ name: 'imported',
218
+ files: filesByPath.get('') || [],
219
+ subdirectories: []
220
+ };
221
+
222
+ // Create subdirectories
223
+ const sortedPaths = Array.from(dirPaths).sort();
224
+ for (const dirPath of sortedPaths) {
225
+ if (dirPath && dirPath !== '') {
226
+ const parts = dirPath.split('/');
227
+ let currentLevel = rootDir;
228
+
229
+ for (let i = 0; i < parts.length; i++) {
230
+ const part = parts[i];
231
+ const currentPath = parts.slice(0, i + 1).join('/');
232
+
233
+ if (!currentLevel.subdirectories) {
234
+ currentLevel.subdirectories = [];
235
+ }
236
+
237
+ let subdir = currentLevel.subdirectories.find((d) => d.name === part);
238
+ if (!subdir) {
239
+ subdir = {
240
+ name: part,
241
+ path: currentPath,
242
+ files: filesByPath.get(currentPath) || [],
243
+ subdirectories: []
244
+ };
245
+ currentLevel.subdirectories.push(subdir);
246
+ }
247
+
248
+ currentLevel = subdir;
249
+ }
250
+ }
251
+ }
252
+
253
+ setImportStatus({
254
+ hasImported: true,
255
+ fileCount: files.length,
256
+ isDirectory: true,
257
+ isBundle: false,
258
+ isZip: false
259
+ });
260
+
261
+ onMessage?.('success', `Imported directory with ${files.length} file(s)`);
262
+ onImport?.(rootDir);
263
+ } catch (err) {
264
+ const errorMsg = err instanceof Error ? err.message : String(err);
265
+ setError(errorMsg);
266
+ onMessage?.('error', `Directory import failed: ${errorMsg}`);
267
+ } finally {
268
+ setIsLoading(false);
269
+ // Reset input
270
+ if (event.target) {
271
+ event.target.value = '';
272
+ }
273
+ }
274
+ },
275
+ [onImport, onMessage]
276
+ );
277
+
278
+ // Modern File System Access API handlers
279
+ const handleModernDirectoryPick = useCallback(async () => {
280
+ if (!('showDirectoryPicker' in window)) {
281
+ onMessage?.('error', 'Directory picker not supported in this browser');
282
+ return;
283
+ }
284
+
285
+ setIsLoading(true);
286
+ setError(null);
287
+
288
+ try {
289
+ const dirHandle = await (window as any).showDirectoryPicker();
290
+ const rootDir = await processDirectoryHandle(dirHandle);
291
+
292
+ const fileCount = countFiles(rootDir);
293
+ setImportStatus({
294
+ hasImported: true,
295
+ fileCount,
296
+ isDirectory: true,
297
+ isBundle: false,
298
+ isZip: false
299
+ });
300
+
301
+ onMessage?.('success', `Imported directory "${rootDir.name}" with ${fileCount} file(s)`);
302
+ onImport?.(rootDir);
303
+ } catch (err: any) {
304
+ if (err.name !== 'AbortError') {
305
+ const errorMsg = err instanceof Error ? err.message : String(err);
306
+ setError(errorMsg);
307
+ onMessage?.('error', `Directory import failed: ${errorMsg}`);
308
+ }
309
+ } finally {
310
+ setIsLoading(false);
311
+ }
312
+ }, [onImport, onMessage]);
313
+
314
+ const handleModernFilePick = useCallback(async () => {
315
+ if (!('showOpenFilePicker' in window)) {
316
+ onMessage?.('error', 'File picker not supported in this browser');
317
+ return;
318
+ }
319
+
320
+ setIsLoading(true);
321
+ setError(null);
322
+
323
+ try {
324
+ const fileHandles = await (window as any).showOpenFilePicker({
325
+ multiple: true,
326
+ types: [
327
+ {
328
+ description: 'Resource files',
329
+ accept: {
330
+ 'application/json': ['.json'],
331
+ 'application/zip': ['.zip']
332
+ }
333
+ }
334
+ ]
335
+ });
336
+
337
+ // Check if we have a single ZIP file first
338
+ if (fileHandles.length === 1) {
339
+ const file = await fileHandles[0].getFile();
340
+
341
+ if (isZipFile(file.name)) {
342
+ console.log(`[ImportView] Modern API - ✅ ${file.name} detected as ZIP file`);
343
+ onMessage?.('info', `Processing ZIP file: ${file.name}`);
344
+
345
+ if (onZipImport) {
346
+ setImportStatus({
347
+ hasImported: true,
348
+ fileCount: 1,
349
+ isDirectory: false,
350
+ isBundle: false,
351
+ isZip: true
352
+ });
353
+
354
+ onMessage?.('success', `ZIP file detected: ${file.name}`);
355
+ onZipImport(file, undefined);
356
+
357
+ setIsLoading(false);
358
+ return;
359
+ } else {
360
+ throw new Error('No ZIP import handler configured');
361
+ }
362
+ }
363
+ }
364
+
365
+ const importedFiles: ImportedFile[] = [];
366
+ let bundleFile: (ImportedFile & { bundle?: Bundle.IBundle }) | undefined;
367
+
368
+ for (const fileHandle of fileHandles) {
369
+ const file = await fileHandle.getFile();
370
+
371
+ // Skip ZIP files in multi-file selection
372
+ if (isZipFile(file.name)) {
373
+ onMessage?.('warning', `Skipping ZIP file ${file.name} - select it individually to import`);
374
+ continue;
375
+ }
376
+
377
+ const content = await file.text();
378
+
379
+ const importedFile: ImportedFile = {
380
+ name: file.name,
381
+ content,
382
+ type: file.type
383
+ };
384
+
385
+ // Check for bundle using proper detection
386
+ let isCurrentFileBundle = false;
387
+ try {
388
+ const parsedData = JSON.parse(content);
389
+
390
+ console.log(`[ImportView] Modern API - Checking if ${file.name} is a bundle...`);
391
+
392
+ // Use BundleUtils for proper bundle detection
393
+ if (Bundle.BundleUtils.isBundleFile(parsedData)) {
394
+ console.log(`[ImportView] Modern API - ✅ ${file.name} detected as bundle file`);
395
+ bundleFile = { ...importedFile, bundle: parsedData };
396
+ isCurrentFileBundle = true;
397
+ } else if (Bundle.BundleUtils.isBundleFileName(file.name)) {
398
+ console.warn(
399
+ `[ImportView] Modern API - ⚠️ File ${file.name} appears to be a bundle by name but content doesn't match bundle structure`
400
+ );
401
+ } else {
402
+ console.log(`[ImportView] Modern API - ❌ ${file.name} is not a bundle file`);
403
+ }
404
+ } catch (parseError) {
405
+ console.log(`[ImportView] Modern API - ❌ ${file.name} failed JSON parsing:`, parseError);
406
+ }
407
+
408
+ // Only add to regular files if this specific file is not a bundle
409
+ if (!isCurrentFileBundle) {
410
+ importedFiles.push(importedFile);
411
+ }
412
+ }
413
+
414
+ // Process results
415
+ if (bundleFile) {
416
+ setImportStatus({
417
+ hasImported: true,
418
+ fileCount: 1,
419
+ isDirectory: false,
420
+ isBundle: true,
421
+ isZip: false
422
+ });
423
+ onMessage?.('info', `Bundle file detected: ${bundleFile.name}`);
424
+ if (onBundleImport && bundleFile.bundle) {
425
+ onBundleImport(bundleFile.bundle);
426
+ }
427
+ } else if (importedFiles.length > 0) {
428
+ setImportStatus({
429
+ hasImported: true,
430
+ fileCount: importedFiles.length,
431
+ isDirectory: false,
432
+ isBundle: false,
433
+ isZip: false
434
+ });
435
+ onMessage?.('success', `Imported ${importedFiles.length} file(s)`);
436
+ onImport?.(importedFiles);
437
+ }
438
+ } catch (err: any) {
439
+ if (err.name !== 'AbortError') {
440
+ const errorMsg = err instanceof Error ? err.message : String(err);
441
+ setError(errorMsg);
442
+ onMessage?.('error', `File import failed: ${errorMsg}`);
443
+ }
444
+ } finally {
445
+ setIsLoading(false);
446
+ }
447
+ }, [onImport, onBundleImport, onMessage, acceptedFileTypes]);
448
+
449
+ const handleReset = useCallback(() => {
450
+ setImportStatus({
451
+ hasImported: false,
452
+ fileCount: 0,
453
+ isDirectory: false,
454
+ isBundle: false,
455
+ isZip: false
456
+ });
457
+ setError(null);
458
+ onMessage?.('info', 'Import cleared');
459
+ }, [onMessage]);
460
+
461
+ return (
462
+ <div className={`p-6 ${className}`}>
463
+ <div className="flex items-center space-x-3 mb-6">
464
+ <DocumentArrowUpIcon className="h-8 w-8 text-blue-600" />
465
+ <h2 className="text-2xl font-bold text-gray-900">Import Resources</h2>
466
+ </div>
467
+
468
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
469
+ {/* Left column: Import Controls */}
470
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
471
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">Import Files</h3>
472
+
473
+ <div className="space-y-4">
474
+ {/* API Support Status */}
475
+ <div className="flex items-center space-x-2 text-sm text-gray-600">
476
+ {isFileSystemAccessSupported ? (
477
+ <>
478
+ <div className="w-2 h-2 bg-green-500 rounded-full"></div>
479
+ <span>Modern File System API available</span>
480
+ </>
481
+ ) : (
482
+ <>
483
+ <div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
484
+ <span>Using fallback file input</span>
485
+ </>
486
+ )}
487
+ </div>
488
+
489
+ {/* Import Buttons */}
490
+ <div className="flex flex-col space-y-3">
491
+ {isFileSystemAccessSupported ? (
492
+ <>
493
+ <button
494
+ onClick={handleModernDirectoryPick}
495
+ disabled={isLoading}
496
+ className="flex items-center justify-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
497
+ >
498
+ <FolderOpenIcon className="w-5 h-5" />
499
+ <span>{isLoading ? 'Importing...' : 'Import Resource Directory'}</span>
500
+ </button>
501
+
502
+ <button
503
+ onClick={handleModernFilePick}
504
+ disabled={isLoading}
505
+ className="flex items-center justify-center space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
506
+ >
507
+ <DocumentArrowUpIcon className="w-5 h-5" />
508
+ <span>{isLoading ? 'Importing...' : 'Import Resource Files'}</span>
509
+ </button>
510
+ </>
511
+ ) : (
512
+ <>
513
+ {/* Fallback inputs */}
514
+ <input
515
+ ref={dirInputRef}
516
+ type="file"
517
+ // @ts-ignore - webkitdirectory is not in types
518
+ webkitdirectory=""
519
+ directory=""
520
+ multiple
521
+ onChange={handleDirectorySelect}
522
+ className="hidden"
523
+ />
524
+ <button
525
+ onClick={() => dirInputRef.current?.click()}
526
+ disabled={isLoading}
527
+ className="flex items-center justify-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
528
+ >
529
+ <FolderOpenIcon className="w-5 h-5" />
530
+ <span>{isLoading ? 'Importing...' : 'Import Resource Directory'}</span>
531
+ </button>
532
+
533
+ <input
534
+ ref={fileInputRef}
535
+ type="file"
536
+ accept={acceptedFileTypes.join(',')}
537
+ multiple
538
+ onChange={handleFileSelect}
539
+ className="hidden"
540
+ />
541
+ <button
542
+ onClick={() => fileInputRef.current?.click()}
543
+ disabled={isLoading}
544
+ className="flex items-center justify-center space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
545
+ >
546
+ <DocumentArrowUpIcon className="w-5 h-5" />
547
+ <span>{isLoading ? 'Importing...' : 'Import Resource Files'}</span>
548
+ </button>
549
+ </>
550
+ )}
551
+ </div>
552
+
553
+ {/* Usage Instructions */}
554
+ <div className="text-sm text-gray-600 space-y-2">
555
+ <p className="font-medium">Import Options:</p>
556
+ <ul className="list-disc list-inside space-y-1 ml-2">
557
+ <li>
558
+ <strong>Directory:</strong> Select a folder with ts-res resources
559
+ </li>
560
+ <li>
561
+ <strong>Files:</strong> Select individual JSON resource files
562
+ </li>
563
+ <li>
564
+ <strong>Bundles:</strong> Automatically detected and loaded
565
+ </li>
566
+ </ul>
567
+ </div>
568
+ </div>
569
+ </div>
570
+
571
+ {/* Right column: Status */}
572
+ <div className="space-y-6">
573
+ {/* Import Status */}
574
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
575
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">Import Status</h3>
576
+
577
+ <div className="space-y-3">
578
+ {/* Import Status */}
579
+ <div className="flex items-center space-x-3">
580
+ {importStatus.hasImported ? (
581
+ <>
582
+ <CheckCircleIcon className="w-5 h-5 text-green-500" />
583
+ <span className="text-sm text-gray-900">
584
+ {importStatus.isBundle
585
+ ? 'Bundle imported'
586
+ : importStatus.isZip
587
+ ? 'ZIP archive imported'
588
+ : importStatus.isDirectory
589
+ ? 'Directory imported'
590
+ : `${importStatus.fileCount} file(s) imported`}
591
+ </span>
592
+ </>
593
+ ) : (
594
+ <>
595
+ <div className="w-5 h-5 rounded-full border-2 border-gray-300"></div>
596
+ <span className="text-sm text-gray-500">No files imported yet</span>
597
+ </>
598
+ )}
599
+ </div>
600
+
601
+ {/* Bundle Detection */}
602
+ {importStatus.isBundle && (
603
+ <div className="flex items-center space-x-3">
604
+ <ArchiveBoxIcon className="w-5 h-5 text-blue-500" />
605
+ <span className="text-sm text-blue-900">Bundle file detected</span>
606
+ </div>
607
+ )}
608
+
609
+ {/* ZIP Detection */}
610
+ {importStatus.isZip && (
611
+ <div className="flex items-center space-x-3">
612
+ <ArchiveBoxIcon className="w-5 h-5 text-purple-500" />
613
+ <span className="text-sm text-purple-900">ZIP archive processed</span>
614
+ </div>
615
+ )}
616
+ </div>
617
+
618
+ {/* Reset Button */}
619
+ {importStatus.hasImported && (
620
+ <button
621
+ onClick={handleReset}
622
+ className="mt-4 px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
623
+ >
624
+ Clear Import
625
+ </button>
626
+ )}
627
+ </div>
628
+
629
+ {/* Error Display */}
630
+ {error && (
631
+ <div className="bg-white rounded-lg shadow-sm border border-red-200 p-6">
632
+ <div className="flex items-start space-x-2">
633
+ <ExclamationTriangleIcon className="w-5 h-5 text-red-600 mt-0.5" />
634
+ <div className="text-sm text-red-800">
635
+ <p className="font-medium">Error</p>
636
+ <p>{error}</p>
637
+ </div>
638
+ </div>
639
+ </div>
640
+ )}
641
+
642
+ {/* Success Message */}
643
+ {importStatus.hasImported && !error && (
644
+ <div className="bg-white rounded-lg shadow-sm border border-green-200 p-6">
645
+ <div className="flex items-start space-x-2">
646
+ <CheckCircleIcon className="w-5 h-5 text-green-600 mt-0.5" />
647
+ <div className="text-sm text-green-800">
648
+ <p className="font-medium">Import Successful!</p>
649
+ <p>
650
+ {importStatus.isBundle
651
+ ? 'Bundle resources are ready to browse.'
652
+ : importStatus.isZip
653
+ ? 'ZIP archive contents have been imported and are ready for processing.'
654
+ : 'Resources are ready for processing.'}
655
+ </p>
656
+ </div>
657
+ </div>
658
+ </div>
659
+ )}
660
+ </div>
661
+ </div>
662
+ </div>
663
+ );
664
+ };
665
+
666
+ // Helper functions
667
+ async function readFileContent(file: File): Promise<string> {
668
+ return new Promise((resolve, reject) => {
669
+ const reader = new FileReader();
670
+ reader.onload = (e) => resolve(e.target?.result as string);
671
+ reader.onerror = (e) => reject(new Error(`Failed to read file: ${file.name}`));
672
+ reader.readAsText(file);
673
+ });
674
+ }
675
+
676
+ async function processDirectoryHandle(dirHandle: any, parentPath: string = ''): Promise<ImportedDirectory> {
677
+ const files: ImportedFile[] = [];
678
+ const subdirectories: ImportedDirectory[] = [];
679
+
680
+ for await (const entry of dirHandle.values()) {
681
+ if (entry.kind === 'file') {
682
+ const file = await entry.getFile();
683
+ const content = await file.text();
684
+ files.push({
685
+ name: file.name,
686
+ path: parentPath ? `${parentPath}/${file.name}` : file.name,
687
+ content,
688
+ type: file.type
689
+ });
690
+ } else if (entry.kind === 'directory') {
691
+ const subdir = await processDirectoryHandle(
692
+ entry,
693
+ parentPath ? `${parentPath}/${entry.name}` : entry.name
694
+ );
695
+ subdirectories.push(subdir);
696
+ }
697
+ }
698
+
699
+ return {
700
+ name: dirHandle.name,
701
+ path: parentPath,
702
+ files,
703
+ subdirectories: subdirectories.length > 0 ? subdirectories : undefined
704
+ };
705
+ }
706
+
707
+ function countFiles(dir: ImportedDirectory): number {
708
+ let count = dir.files?.length || 0;
709
+ if (dir.subdirectories) {
710
+ for (const subdir of dir.subdirectories) {
711
+ count += countFiles(subdir);
712
+ }
713
+ }
714
+ return count;
715
+ }
716
+
717
+ export default ImportView;