@makolabs/ripple 0.0.1 → 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 (144) hide show
  1. package/README.md +575 -8
  2. package/dist/adapters/storage/BaseAdapter.d.ts +20 -0
  3. package/dist/adapters/storage/BaseAdapter.js +171 -0
  4. package/dist/adapters/storage/S3Adapter.d.ts +21 -0
  5. package/dist/adapters/storage/S3Adapter.js +194 -0
  6. package/dist/adapters/storage/index.d.ts +3 -0
  7. package/dist/adapters/storage/index.js +3 -0
  8. package/dist/adapters/storage/types.d.ts +102 -0
  9. package/dist/adapters/storage/types.js +4 -0
  10. package/dist/button/Button.svelte +48 -0
  11. package/dist/button/Button.svelte.d.ts +4 -0
  12. package/dist/button/button.d.ts +113 -0
  13. package/dist/button/button.js +168 -0
  14. package/dist/charts/Chart.svelte +545 -0
  15. package/dist/charts/Chart.svelte.d.ts +4 -0
  16. package/dist/drawer/Drawer.svelte +224 -0
  17. package/dist/drawer/Drawer.svelte.d.ts +4 -0
  18. package/dist/drawer/drawer.d.ts +160 -0
  19. package/dist/drawer/drawer.js +80 -0
  20. package/dist/elements/accordion/Accordion.svelte +98 -0
  21. package/dist/elements/accordion/Accordion.svelte.d.ts +4 -0
  22. package/dist/elements/accordion/accordion.d.ts +227 -0
  23. package/dist/elements/accordion/accordion.js +138 -0
  24. package/dist/elements/alert/Alert.svelte +57 -0
  25. package/dist/elements/alert/Alert.svelte.d.ts +4 -0
  26. package/dist/elements/badge/Badge.svelte +43 -0
  27. package/dist/elements/badge/Badge.svelte.d.ts +4 -0
  28. package/dist/elements/badge/badge.d.ts +181 -0
  29. package/dist/elements/badge/badge.js +65 -0
  30. package/dist/elements/dropdown/Dropdown.svelte +234 -0
  31. package/dist/elements/dropdown/Dropdown.svelte.d.ts +4 -0
  32. package/dist/elements/dropdown/Select.svelte +333 -0
  33. package/dist/elements/dropdown/Select.svelte.d.ts +4 -0
  34. package/dist/elements/dropdown/dropdown.d.ts +251 -0
  35. package/dist/elements/dropdown/dropdown.js +95 -0
  36. package/dist/elements/dropdown/select.d.ts +200 -0
  37. package/dist/elements/dropdown/select.js +82 -0
  38. package/dist/elements/file-upload/FileUpload.svelte +135 -0
  39. package/dist/elements/file-upload/FileUpload.svelte.d.ts +4 -0
  40. package/dist/elements/file-upload/FilesPreview.svelte +93 -0
  41. package/dist/elements/file-upload/FilesPreview.svelte.d.ts +4 -0
  42. package/dist/elements/progress/Progress.svelte +145 -0
  43. package/dist/elements/progress/Progress.svelte.d.ts +4 -0
  44. package/dist/elements/timeline/Timeline.svelte +92 -0
  45. package/dist/elements/timeline/Timeline.svelte.d.ts +7 -0
  46. package/dist/file-browser/FileBrowser.svelte +877 -0
  47. package/dist/file-browser/FileBrowser.svelte.d.ts +14 -0
  48. package/dist/file-browser/index.d.ts +1 -0
  49. package/dist/file-browser/index.js +1 -0
  50. package/dist/filters/CompactFilters.svelte +147 -0
  51. package/dist/filters/CompactFilters.svelte.d.ts +4 -0
  52. package/dist/filters/index.d.ts +1 -0
  53. package/dist/filters/index.js +1 -0
  54. package/dist/forms/Checkbox.svelte +54 -0
  55. package/dist/forms/Checkbox.svelte.d.ts +4 -0
  56. package/dist/forms/DateRange.svelte +493 -0
  57. package/dist/forms/DateRange.svelte.d.ts +4 -0
  58. package/dist/forms/Form.svelte +39 -0
  59. package/dist/forms/Form.svelte.d.ts +4 -0
  60. package/dist/forms/Input.svelte +86 -0
  61. package/dist/forms/Input.svelte.d.ts +4 -0
  62. package/dist/forms/NumberInput.svelte +159 -0
  63. package/dist/forms/NumberInput.svelte.d.ts +4 -0
  64. package/dist/forms/RadioInputs.svelte +64 -0
  65. package/dist/forms/RadioInputs.svelte.d.ts +4 -0
  66. package/dist/forms/RadioPill.svelte +66 -0
  67. package/dist/forms/RadioPill.svelte.d.ts +4 -0
  68. package/dist/forms/Slider.svelte +342 -0
  69. package/dist/forms/Slider.svelte.d.ts +4 -0
  70. package/dist/forms/Tags.svelte +181 -0
  71. package/dist/forms/Tags.svelte.d.ts +4 -0
  72. package/dist/forms/Toggle.svelte +132 -0
  73. package/dist/forms/Toggle.svelte.d.ts +4 -0
  74. package/dist/forms/slider.d.ts +143 -0
  75. package/dist/forms/slider.js +62 -0
  76. package/dist/header/Breadcrumbs.svelte +73 -0
  77. package/dist/header/Breadcrumbs.svelte.d.ts +4 -0
  78. package/dist/header/PageHeader.svelte +68 -0
  79. package/dist/header/PageHeader.svelte.d.ts +4 -0
  80. package/dist/header/breadcrumbs.d.ts +226 -0
  81. package/dist/header/breadcrumbs.js +87 -0
  82. package/dist/helper/cls.d.ts +1 -0
  83. package/dist/helper/cls.js +4 -0
  84. package/dist/helper/date.d.ts +7 -0
  85. package/dist/helper/date.js +15 -0
  86. package/dist/helper/nav.svelte.d.ts +6 -0
  87. package/dist/helper/nav.svelte.js +23 -0
  88. package/dist/index.d.ts +856 -1
  89. package/dist/index.js +78 -1
  90. package/dist/layout/card/Card.svelte +41 -0
  91. package/dist/layout/card/Card.svelte.d.ts +4 -0
  92. package/dist/layout/card/MetricCard.svelte +64 -0
  93. package/dist/layout/card/MetricCard.svelte.d.ts +4 -0
  94. package/dist/layout/card/StatsCard.svelte +266 -0
  95. package/dist/layout/card/StatsCard.svelte.d.ts +4 -0
  96. package/dist/layout/card/card.d.ts +128 -0
  97. package/dist/layout/card/card.js +51 -0
  98. package/dist/layout/card/metric-card.d.ts +49 -0
  99. package/dist/layout/card/metric-card.js +10 -0
  100. package/dist/layout/card/stats-card.d.ts +191 -0
  101. package/dist/layout/card/stats-card.js +73 -0
  102. package/dist/layout/navbar/Navbar.svelte +206 -0
  103. package/dist/layout/navbar/Navbar.svelte.d.ts +4 -0
  104. package/dist/layout/navbar/navbar.d.ts +205 -0
  105. package/dist/layout/navbar/navbar.js +98 -0
  106. package/dist/layout/sidebar/NavGroup.svelte +91 -0
  107. package/dist/layout/sidebar/NavGroup.svelte.d.ts +4 -0
  108. package/dist/layout/sidebar/NavItem.svelte +29 -0
  109. package/dist/layout/sidebar/NavItem.svelte.d.ts +4 -0
  110. package/dist/layout/sidebar/Sidebar.svelte +193 -0
  111. package/dist/layout/sidebar/Sidebar.svelte.d.ts +4 -0
  112. package/dist/layout/table/Cells.svelte +111 -0
  113. package/dist/layout/table/Cells.svelte.d.ts +27 -0
  114. package/dist/layout/table/Table.svelte +790 -0
  115. package/dist/layout/table/Table.svelte.d.ts +4 -0
  116. package/dist/layout/table/table.d.ts +256 -0
  117. package/dist/layout/table/table.js +141 -0
  118. package/dist/layout/tabs/Tab.svelte +60 -0
  119. package/dist/layout/tabs/Tab.svelte.d.ts +4 -0
  120. package/dist/layout/tabs/TabContent.svelte +30 -0
  121. package/dist/layout/tabs/TabContent.svelte.d.ts +4 -0
  122. package/dist/layout/tabs/TabGroup.svelte +62 -0
  123. package/dist/layout/tabs/TabGroup.svelte.d.ts +4 -0
  124. package/dist/layout/tabs/tabs.d.ts +140 -0
  125. package/dist/layout/tabs/tabs.js +298 -0
  126. package/dist/modal/Modal.svelte +207 -0
  127. package/dist/modal/Modal.svelte.d.ts +4 -0
  128. package/dist/modal/modal.d.ts +211 -0
  129. package/dist/modal/modal.js +81 -0
  130. package/dist/sonner/sonner.svelte +13 -0
  131. package/dist/sonner/sonner.svelte.d.ts +4 -0
  132. package/dist/types/variants.d.ts +1 -0
  133. package/dist/types/variants.js +1 -0
  134. package/dist/utils/Portal.svelte +108 -0
  135. package/dist/utils/Portal.svelte.d.ts +8 -0
  136. package/dist/utils/dateUtils.d.ts +7 -0
  137. package/dist/utils/dateUtils.js +26 -0
  138. package/dist/variants.d.ts +30 -0
  139. package/dist/variants.js +36 -0
  140. package/package.json +39 -6
  141. package/dist/layout/Card.svelte +0 -179
  142. package/dist/layout/Card.svelte.d.ts +0 -208
  143. package/dist/layout/index.d.ts +0 -1
  144. package/dist/layout/index.js +0 -1
@@ -0,0 +1,877 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Button, Table, Color, Size } from '../index.js';
4
+ import type { TableColumn } from '../index.js';
5
+ import { formatDate } from '../utils/dateUtils.js';
6
+ import type {
7
+ StorageAdapter,
8
+ FileItem,
9
+ Breadcrumb,
10
+ FileAction,
11
+ FileActionBatch,
12
+ FileActionSingle
13
+ } from '../adapters/storage/index.js';
14
+
15
+ let {
16
+ adapter,
17
+ startPath = '',
18
+ actions = [],
19
+ infoSection,
20
+ selectedFiles = $bindable([])
21
+ } = $props<{
22
+ adapter: StorageAdapter;
23
+ startPath?: string;
24
+ actions?: FileAction[];
25
+ selectedFiles?: string[];
26
+ infoSection?: (props: {
27
+ selectedFiles: string[];
28
+ navToFileFolder: (fileKey: string) => void;
29
+ }) => any;
30
+ }>();
31
+
32
+ let files = $state<FileItem[]>([]);
33
+ let displayFiles = $state<FileItem[]>([]);
34
+ let currentPath = $state(startPath || '');
35
+ let isLoading = $state(true);
36
+ let isAuthenticated = $state(false);
37
+ let error = $state<string | null>(null);
38
+ let breadcrumbs = $state<Breadcrumb[]>([]);
39
+ let searchQuery = $state('');
40
+
41
+ let sortState = $state<{ column: string | null; direction: 'asc' | 'desc' | 'default' | null }>({
42
+ column: null,
43
+ direction: null
44
+ });
45
+
46
+ // Add state for folder traversal
47
+ let isFetchingRecursively = $state(false);
48
+ let fileQueue = $state<FileItem[]>([]);
49
+ let processedFolders = $state<Set<string>>(new Set());
50
+ let fetchingFolderName = $state<string | null>(null);
51
+
52
+ // Format file size in human readable format
53
+ function formatFileSize(bytes: number): string {
54
+ if (bytes === 0) return '0 B';
55
+ const k = 1024;
56
+ const sizes = ['B', 'KB', 'MB', 'GB'];
57
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
58
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
59
+ }
60
+
61
+ // Function that determines if a batch action should be shown
62
+ function isBatchActionAllowed(action: FileAction): action is FileActionBatch {
63
+ if (isFetchingRecursively) return false;
64
+
65
+ return (
66
+ 'batchAction' in action &&
67
+ typeof action.batchAction === 'function' &&
68
+ action.isAllowed(allFilesAcquired)
69
+ );
70
+ }
71
+
72
+ // Function that determines if a single action should be shown for a file
73
+ function isSingleActionAllowed(action: FileAction, file: FileItem): action is FileActionSingle {
74
+ return 'action' in action && typeof action.action === 'function' && action.isAllowed(file);
75
+ }
76
+
77
+ function navigateUp() {
78
+ // Normalize currentPath and startPath by removing leading/trailing slashes
79
+ const normCurrent = currentPath.replace(/^\/+|\/+$/g, '');
80
+ const normStart = startPath.replace(/^\/+|\/+$/g, '');
81
+
82
+ if (normCurrent !== normStart) {
83
+ const segments = normCurrent.split('/');
84
+ segments.pop(); // Remove last segment
85
+ let newPath = segments.join('/');
86
+ if (!newPath) newPath = startPath || '';
87
+ navigateToFolder(newPath);
88
+ }
89
+ }
90
+
91
+ function navToFileFolder(fileKey: string) {
92
+ // Get the parent folder path by removing the filename from the key
93
+ const parentPath = fileKey.substring(0, fileKey.lastIndexOf('/'));
94
+
95
+ // Check if we're already in this folder, don't navigate in that case
96
+ if (parentPath === currentPath) {
97
+ return;
98
+ }
99
+
100
+ // Store the currently selected files before navigation
101
+ const currentSelection = [...selectedFiles];
102
+
103
+ // Make sure we preserve the selection of the file that triggered this navigation
104
+ if (!currentSelection.includes(fileKey)) {
105
+ currentSelection.push(fileKey);
106
+ }
107
+
108
+ // Navigate to folder, then restore selection
109
+ navigateToFolder(parentPath);
110
+
111
+ // In case the navigateToFolder resets selection, ensure this file is still selected
112
+ // after a short delay to allow the folder contents to load
113
+ setTimeout(() => {
114
+ if (!selectedFiles.includes(fileKey)) {
115
+ selectedFiles = [...selectedFiles, fileKey];
116
+
117
+ // Update table selection as well
118
+ const fileItem = files.find((f) => f.key === fileKey);
119
+ if (fileItem && !selected.some((s) => s.key === fileKey)) {
120
+ selected = [...selected, fileItem];
121
+ }
122
+ }
123
+ }, 100);
124
+ }
125
+
126
+ // On component mount, check auth status and list files
127
+ onMount(async () => {
128
+ await checkAdapterConfiguration();
129
+ });
130
+
131
+ async function checkAdapterConfiguration() {
132
+ try {
133
+ isAuthenticated = await adapter.isConfigured();
134
+
135
+ if (isAuthenticated) {
136
+ await listFiles(currentPath || startPath);
137
+ }
138
+ } catch (err) {
139
+ console.error('Error checking adapter configuration:', err);
140
+ isAuthenticated = false;
141
+ error = 'Failed to initialize storage adapter';
142
+ }
143
+ }
144
+
145
+ async function authenticate() {
146
+ if (adapter.authenticate) {
147
+ try {
148
+ // Use the adapter's method to set the reopening flag
149
+ if (adapter.setReopenFlag) {
150
+ adapter.setReopenFlag();
151
+ }
152
+
153
+ await adapter.authenticate();
154
+ // The adapter's authenticate method might redirect, so this may not complete
155
+ isAuthenticated = await adapter.isConfigured();
156
+ if (isAuthenticated) {
157
+ await listFiles(currentPath || startPath);
158
+ }
159
+ } catch (err) {
160
+ console.error('Authentication error:', err);
161
+ error = 'Failed to authenticate with storage provider';
162
+ }
163
+ } else {
164
+ error = 'This storage provider does not support authentication';
165
+ }
166
+ }
167
+
168
+ // Loading files using the adapter
169
+ async function listFiles(path: string) {
170
+ isLoading = true;
171
+ error = null;
172
+
173
+ // Remember the previous selection before loading new files
174
+ const previousSelectedFiles = [...selectedFiles];
175
+ const previousFileQueue = [...fileQueue];
176
+
177
+ try {
178
+ // Only pass the searchQuery if it's not empty
179
+ const result = await adapter.list(path, searchQuery.trim() ? searchQuery : undefined);
180
+
181
+ files = result.files;
182
+ currentPath = result.currentPath;
183
+
184
+ if (result.breadcrumbs) {
185
+ breadcrumbs = result.breadcrumbs;
186
+ }
187
+
188
+ // Default sort: folders alphabetically, files by date (newest first)
189
+ sortState = { column: 'default', direction: 'default' };
190
+ displayFiles = getSortedFiles(files);
191
+
192
+ // Retain selected files that are still valid
193
+ // We need to filter out any folder paths that might no longer exist
194
+ selectedFiles = previousSelectedFiles;
195
+ fileQueue = previousFileQueue;
196
+ } catch (err) {
197
+ console.error('Error fetching files:', err);
198
+ error = err instanceof Error ? err.message : 'An unknown error occurred';
199
+ } finally {
200
+ isLoading = false;
201
+ }
202
+ }
203
+
204
+ function navigateToFolder(folderPath: string) {
205
+ // If we have the file info for this folder, cache its name in the adapter
206
+ // before navigating (useful for Google Drive adapter)
207
+ const folderFile = files.find((file) => file.key === folderPath);
208
+ if (folderFile && folderFile.name && adapter.getName() === 'Google Drive') {
209
+ // Assuming the adapter might have a setFolderName method
210
+ (adapter as any).setFolderName?.(folderPath, folderFile.name);
211
+ }
212
+
213
+ // Store the currently selected files before navigation
214
+ const previouslySelectedFiles = [...selectedFiles];
215
+
216
+ // Figure out if we're navigating back up to a parent
217
+ const isNavigatingUp = currentPath.includes(folderPath) && currentPath !== folderPath;
218
+
219
+ // If we're navigating up, make sure to keep the folder we're coming from selected
220
+ const shouldKeepCurrent = isNavigatingUp && currentPath;
221
+
222
+ // Keep track of existing fileQueue
223
+ const existingQueue = [...fileQueue];
224
+
225
+ // Navigate to the folder
226
+ listFiles(folderPath).then(() => {
227
+ // After navigation is complete, restore the selection
228
+ let filesToSelect = [];
229
+
230
+ // If navigating up, keep the current folder selected
231
+ if (shouldKeepCurrent) {
232
+ // Find the folder we just came from
233
+ const currentFolder = files.find(
234
+ (f) => f.key === currentPath || (f.isFolder && currentPath.startsWith(f.key + '/'))
235
+ );
236
+
237
+ if (currentFolder) {
238
+ filesToSelect.push(currentFolder.key);
239
+ }
240
+ }
241
+
242
+ // Also keep previously selected files that are still visible
243
+ const visibleSelectedFiles = previouslySelectedFiles.filter((key) =>
244
+ files.some((file) => file.key === key)
245
+ );
246
+
247
+ filesToSelect = [...new Set([...filesToSelect, ...visibleSelectedFiles])];
248
+
249
+ // Update selection
250
+ if (filesToSelect.length > 0) {
251
+ selectedFiles = filesToSelect;
252
+ selected = displayFiles.filter((file) => filesToSelect.includes(file.key));
253
+ }
254
+
255
+ // Restore fileQueue to ensure we keep track of all selected files
256
+ // Only keep files that are still selected
257
+ fileQueue = [...existingQueue];
258
+ });
259
+ }
260
+
261
+ async function handleRowClick(file: FileItem) {
262
+ if (file.isFolder) {
263
+ navigateToFolder(file.key);
264
+ } else {
265
+ // Check if this file is allowed to be selected based on actions
266
+ const hasAllowedAction = actions.some((action: FileAction) => {
267
+ // Only check single file actions for individual file selection
268
+ if ('action' in action && typeof action.action === 'function') {
269
+ return action.isAllowed(file);
270
+ }
271
+ return false;
272
+ });
273
+
274
+ // Only allow selection if the file has at least one allowed action
275
+ if (!hasAllowedAction) {
276
+ return; // Exit early if file is not allowed to be selected
277
+ }
278
+
279
+ // Toggle selection for files
280
+ const isCurrentlySelected = selectedFiles.includes(file.key);
281
+
282
+ if (isCurrentlySelected) {
283
+ // Remove from selection
284
+ selectedFiles = selectedFiles.filter(key => key !== file.key);
285
+ selected = selected.filter(item => item.key !== file.key);
286
+ } else {
287
+ // Add to selection
288
+ selectedFiles = [...selectedFiles, file.key];
289
+ selected = [...selected, file];
290
+ }
291
+ }
292
+ }
293
+
294
+ // Sort files based on current sort state
295
+ function getSortedFiles(filesArray: FileItem[]): FileItem[] {
296
+ // Separate folders and files
297
+ const folders = filesArray.filter((file) => file.isFolder);
298
+ const files = filesArray.filter((file) => !file.isFolder);
299
+
300
+ // Handle default sort (folders alphabetically, files by date newest first)
301
+ if (sortState.column === 'default' && sortState.direction === 'default') {
302
+ // Sort folders alphabetically (A-Z)
303
+ const sortedFolders = [...folders].sort((a, b) => a.name.localeCompare(b.name));
304
+
305
+ // Sort files by date (newest first)
306
+ const sortedFiles = [...files].sort((a, b) => {
307
+ // Use createdAt if available, otherwise fallback to lastModified
308
+ const aTime = a.createdAt ? a.createdAt.getTime() : a.lastModified.getTime();
309
+ const bTime = b.createdAt ? b.createdAt.getTime() : b.lastModified.getTime();
310
+ return bTime - aTime; // Descending order (newest first)
311
+ });
312
+
313
+ // Return folders first, then files
314
+ return [...sortedFolders, ...sortedFiles];
315
+ }
316
+
317
+ // For user-specified sorting
318
+ // If no sort specified, default to sorting by name
319
+ if (!sortState.column || !sortState.direction) {
320
+ sortState = { column: 'name', direction: 'asc' };
321
+ }
322
+
323
+ // Create a comparison function based on the current sort state
324
+ const compareFiles = (a: FileItem, b: FileItem): number => {
325
+ let comparison = 0;
326
+
327
+ switch (sortState.column) {
328
+ case 'name':
329
+ comparison = a.name.localeCompare(b.name);
330
+ break;
331
+ case 'size':
332
+ comparison = a.size - b.size;
333
+ break;
334
+ case 'lastModified':
335
+ comparison = a.lastModified.getTime() - b.lastModified.getTime();
336
+ break;
337
+ default:
338
+ // Default to sorting by name
339
+ return a.name.localeCompare(b.name);
340
+ }
341
+
342
+ // Reverse for descending order
343
+ return sortState.direction === 'asc' ? comparison : -comparison;
344
+ };
345
+
346
+ // Sort folders and files separately
347
+ const sortedFolders = [...folders].sort(compareFiles);
348
+ const sortedFiles = [...files].sort(compareFiles);
349
+
350
+ // Return sorted folders followed by sorted files
351
+ return [...sortedFolders, ...sortedFiles];
352
+ }
353
+
354
+ // Handle sort column click
355
+ function handleSort(newSortState: { column: string | null; direction: 'asc' | 'desc' | null }) {
356
+ // Update the component's sort state
357
+ sortState = {
358
+ column: newSortState.column,
359
+ direction: newSortState.direction
360
+ };
361
+
362
+ // Update the display files with the new sort
363
+ displayFiles = getSortedFiles(files);
364
+ }
365
+
366
+ // Handle search
367
+ function handleSearch() {
368
+ // Make sure we're actually using the current searchQuery value
369
+ listFiles(currentPath);
370
+ }
371
+
372
+ // Clear the search and reset results
373
+ function clearSearch() {
374
+ // Only refresh if there was a previous search
375
+ if (searchQuery) {
376
+ searchQuery = '';
377
+ listFiles(currentPath);
378
+ }
379
+ }
380
+
381
+ // Derived: all files to import (not folders), including recursively fetched
382
+ const allFilesAcquired = $derived.by(() => {
383
+ const selectedFileKeys = new Set(selectedFiles);
384
+
385
+ // Get files from current view that are selected and not folders
386
+ const selectedCurrentFiles = displayFiles.filter(
387
+ (f) => selectedFileKeys.has(f.key) && !f.isFolder
388
+ );
389
+
390
+ // Get files from the queue (these are explicitly added when folders are processed)
391
+ const queuedFiles = fileQueue.filter((f) => !f.isFolder);
392
+
393
+ // Combine both sources
394
+ const allFiles = [...selectedCurrentFiles, ...queuedFiles];
395
+
396
+ // Remove duplicates by key
397
+ return Array.from(new Map(allFiles.map((f) => [f.key, f])).values());
398
+ });
399
+
400
+ // Ensure our selectedFiles state is updated when the Table selection changes
401
+ function onselect(selectedItems: FileItem[]) {
402
+ // Filter out files that aren't allowed to be selected based on actions' isAllowed logic
403
+ const allowedItems = selectedItems.filter((item) => {
404
+ // Check if any single action allows this file
405
+ const hasAllowedAction = actions.some((action: FileAction) => {
406
+ // Only check single file actions for individual file selection
407
+ if ('action' in action && typeof action.action === 'function') {
408
+ return action.isAllowed(item);
409
+ }
410
+ return false;
411
+ });
412
+
413
+ // Always allow folders to be selected (for recursive selection)
414
+ return item.isFolder || hasAllowedAction;
415
+ });
416
+
417
+ // Get keys from allowed selected items
418
+ const newSelectedKeys = allowedItems.map((item) => item.key);
419
+
420
+ // Only update if there's a meaningful change to prevent unnecessary resets
421
+ const currentKeys = [...selectedFiles].sort();
422
+ const sortedNewKeys = [...newSelectedKeys].sort();
423
+
424
+ if (JSON.stringify(currentKeys) !== JSON.stringify(sortedNewKeys)) {
425
+ // Update the global selection state with only allowed items
426
+ handleSelectByKeys(newSelectedKeys, allowedItems);
427
+ }
428
+ }
429
+
430
+ // Track the selected items for the Table component's internal state
431
+ let selected = $state<FileItem[]>([]);
432
+
433
+ // Effect to update the Table component's selection state when selectedFiles changes
434
+ $effect(() => {
435
+ if (displayFiles.length === 0) return;
436
+
437
+ // Get all files that should be selected based on our current selection state
438
+ const filesToSelect = displayFiles.filter((file) => isRowSelected(file));
439
+
440
+ // We need to update the Table's internal selection state without triggering additional updates
441
+ // This ensures the UI is properly updated
442
+ if (
443
+ JSON.stringify(selected.map((f) => f.key).sort()) !==
444
+ JSON.stringify(filesToSelect.map((f) => f.key).sort())
445
+ ) {
446
+ // Only update if the selection has actually changed
447
+ selected = filesToSelect;
448
+ }
449
+ });
450
+
451
+ // Handle selection based on keys to avoid redundant code
452
+ function handleSelectByKeys(keys: string[], items: FileItem[] = []) {
453
+ // Get folder items from the selection
454
+ const folderItems = items.filter((item) => item.isFolder);
455
+
456
+ // Only update selectedFiles if the new keys are actually different
457
+ // This prevents unnecessary resets of the bound array
458
+ const currentKeys = [...selectedFiles].sort();
459
+ const newKeys = [...keys].sort();
460
+
461
+ if (JSON.stringify(currentKeys) !== JSON.stringify(newKeys)) {
462
+ // Update the selectedFiles array only if there's a real change
463
+ selectedFiles = keys;
464
+ }
465
+
466
+ // Process any newly selected folders recursively to get their contents
467
+ const newFolders = folderItems.filter((folder) => !processedFolders.has(folder.key));
468
+ newFolders.forEach((folder) => {
469
+ handleSelectFolder(folder);
470
+ });
471
+
472
+ // Update select all state
473
+ const nonFolderFiles = displayFiles.filter((file) => !file.isFolder);
474
+ }
475
+
476
+ async function fetchFilesFromFolder(folderKey: string, folderName: string) {
477
+ if (processedFolders.has(folderKey)) return;
478
+
479
+ fetchingFolderName = folderName;
480
+ processedFolders.add(folderKey);
481
+
482
+ try {
483
+ // Use the adapter to list files in this folder
484
+ const result = await adapter.list(folderKey);
485
+
486
+ // Process files in this folder
487
+ const folderItems = result.files || [];
488
+
489
+ // Add non-folder files to selection
490
+ folderItems.forEach((item: FileItem) => {
491
+ if (!item.isFolder) {
492
+ if (!selectedFiles.includes(item.key)) {
493
+ selectedFiles = [...selectedFiles, item.key];
494
+ }
495
+ // Add to fileQueue for tracking
496
+ fileQueue = [...fileQueue, item];
497
+ } else {
498
+ // Add folders to queue for reference
499
+ fileQueue = [...fileQueue, item];
500
+ }
501
+ });
502
+
503
+ // Return any folders found for further processing
504
+ return folderItems.filter((item: FileItem) => item.isFolder);
505
+ } catch (err) {
506
+ console.error(`Error fetching files from folder ${folderName}:`, err);
507
+ // Note: In a real app, you'd want to import a toast library
508
+ console.error(`Failed to fetch files from ${folderName}`);
509
+ return [];
510
+ }
511
+ }
512
+
513
+ // Handle selecting a folder to process recursively
514
+ async function handleSelectFolder(folder: FileItem) {
515
+ if (!folder.isFolder) return;
516
+
517
+ isFetchingRecursively = true;
518
+ processedFolders = new Set();
519
+ // Don't clear fileQueue entirely, it will lose files from other folders
520
+ // Store existing fileQueue
521
+ const existingQueue = [...fileQueue];
522
+
523
+ try {
524
+ // Start with the selected folder
525
+ let foldersToProcess = [folder];
526
+
527
+ // Process folders breadth-first
528
+ while (foldersToProcess.length > 0) {
529
+ const currentFolder = foldersToProcess.shift();
530
+ if (!currentFolder) continue;
531
+
532
+ const subFolders = await fetchFilesFromFolder(currentFolder.key, currentFolder.name);
533
+
534
+ if (subFolders && subFolders.length > 0) {
535
+ foldersToProcess.push(...subFolders);
536
+ }
537
+ }
538
+
539
+ // Merge new files with existing queue, avoiding duplicates
540
+ fileQueue = [
541
+ ...existingQueue,
542
+ ...fileQueue.filter(
543
+ (newItem) => !existingQueue.some((existingItem) => existingItem.key === newItem.key)
544
+ )
545
+ ];
546
+
547
+ console.log(`Selected ${selectedFiles.length} files from folders`);
548
+ } catch (err) {
549
+ console.error('Error in recursive folder processing:', err);
550
+ console.error('Failed to process some folders');
551
+ } finally {
552
+ isFetchingRecursively = false;
553
+ fetchingFolderName = null;
554
+ }
555
+ }
556
+
557
+ // Check if a file is within a selected folder by examining its key path
558
+ function isFileInSelectedFolder(fileKey: string): boolean {
559
+ if (!fileKey) return false;
560
+
561
+ // Iterate through selected folders and check if the file key starts with any selected folder's key
562
+ // This assumes folder paths are prefixes of their contained files
563
+ for (const selectedKey of selectedFiles) {
564
+ const selectedFile =
565
+ files.find((f) => f.key === selectedKey) || fileQueue.find((f) => f.key === selectedKey);
566
+
567
+ // Only check folders
568
+ if (selectedFile && selectedFile.isFolder) {
569
+ // If the file's key starts with the folder's key and has additional path elements, it's inside the folder
570
+ if (fileKey.startsWith(selectedKey) && fileKey !== selectedKey) {
571
+ return true;
572
+ }
573
+ }
574
+ }
575
+
576
+ return false;
577
+ }
578
+
579
+ const columns: TableColumn<FileItem>[] = [
580
+ {
581
+ key: 'name',
582
+ header: 'Name',
583
+ cell: NameCell,
584
+ sortable: true
585
+ },
586
+ {
587
+ key: 'size',
588
+ header: 'Size',
589
+ cell: SizeCell,
590
+ sortable: true
591
+ },
592
+ {
593
+ key: 'lastModified',
594
+ header: 'Last Modified',
595
+ cell: DateCell,
596
+ sortable: true
597
+ },
598
+ {
599
+ key: 'status',
600
+ header: 'Status',
601
+ cell: StatusCell
602
+ }
603
+ ];
604
+
605
+ async function handleUnauthenticated() {
606
+ isLoading = true;
607
+
608
+ try {
609
+ // Use the adapter's method to set the reopening flag
610
+ if (adapter.setReopenFlag) {
611
+ adapter.setReopenFlag();
612
+ }
613
+
614
+ // Try to authenticate with the adapter
615
+ const authenticated = adapter.authenticate ? await adapter.authenticate() : false;
616
+
617
+ // If authentication failed, but not due to redirection, show error message
618
+ if (!authenticated) {
619
+ console.error('Failed to authenticate with storage provider');
620
+ }
621
+
622
+ // Note: authentication usually redirects the page, so code below won't run
623
+ isLoading = false;
624
+ } catch (error) {
625
+ console.error('Authentication error:', error);
626
+ console.error('Authentication error');
627
+ isLoading = false;
628
+ }
629
+ }
630
+
631
+ // Handle "Not authorized" messages from the adapter by calling authenticate
632
+ $effect(() => {
633
+ if (error && error.includes('not authorized')) {
634
+ handleUnauthenticated();
635
+ }
636
+ });
637
+
638
+ // Calculate if a row should be treated as selected
639
+ function isRowSelected(row: FileItem): boolean {
640
+ return selectedFiles.includes(row.key) || isFileInSelectedFolder(row.key);
641
+ }
642
+
643
+ const batchActions = $derived(actions.filter(isBatchActionAllowed));
644
+
645
+ const singularActions = (file: FileItem): FileActionSingle[] => {
646
+ return actions.filter((a: FileAction) => isSingleActionAllowed(a, file)) as FileActionSingle[];
647
+ };
648
+ </script>
649
+
650
+ {#snippet StatusCell(file: FileItem)}
651
+ {#if file.isFolder}
652
+ <span class="text-default-500">-</span>
653
+ {:else}
654
+ <div class="flex items-center gap-2">
655
+ {#each singularActions(file) as action (action.label)}
656
+ {#if action.isAllowed(file)}
657
+ <Button size={Size.XS} onclick={() => action.action?.(file)}>{action.label(file)}</Button>
658
+ {/if}
659
+ {/each}
660
+ </div>
661
+ {/if}
662
+ {/snippet}
663
+
664
+ {#snippet NameCell(file: FileItem)}
665
+ <div class="flex items-center gap-2">
666
+ {#if file.isFolder}
667
+ <svg
668
+ xmlns="http://www.w3.org/2000/svg"
669
+ width="16"
670
+ height="16"
671
+ viewBox="0 0 24 24"
672
+ class="text-amber-500"
673
+ >
674
+ <path
675
+ fill="currentColor"
676
+ d="M3.5 6.25V8h4.629a.75.75 0 0 0 .53-.22l1.53-1.53l-1.53-1.53a.75.75 0 0 0-.53-.22H5.25A1.75 1.75 0 0 0 3.5 6.25m-1.5 0A3.25 3.25 0 0 1 5.25 3h2.879a2.25 2.25 0 0 1 1.59.659L11.562 5.5h7.189A3.25 3.25 0 0 1 22 8.75v9A3.25 3.25 0 0 1 18.75 21H5.25A3.25 3.25 0 0 1 2 17.75zM3.5 9.5v8.25c0 .966.784 1.75 1.75 1.75h13.5a1.75 1.75 0 0 0 1.75-1.75v-9A1.75 1.75 0 0 0 18.75 7h-7.19L9.72 8.841a2.25 2.25 0 0 1-1.591.659z"
677
+ />
678
+ </svg>
679
+ {:else}
680
+ <svg
681
+ xmlns="http://www.w3.org/2000/svg"
682
+ width="16"
683
+ height="16"
684
+ viewBox="0 0 24 24"
685
+ class="text-blue-500"
686
+ >
687
+ <path
688
+ fill="currentColor"
689
+ d="M6 2a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414l-5.828-5.828A2 2 0 0 0 12.172 2zm-.5 2a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5zm11.88 4.5H14a.5.5 0 0 1-.5-.5V4.62z"
690
+ />
691
+ </svg>
692
+ {/if}
693
+ <span class="font-medium">{file.name}</span>
694
+ </div>
695
+ {/snippet}
696
+
697
+ {#snippet SizeCell(file: FileItem)}
698
+ {#if file.isFolder}
699
+ <span class="text-default-500">-</span>
700
+ {:else}
701
+ <span class="font-mono">{formatFileSize(file.size)}</span>
702
+ {/if}
703
+ {/snippet}
704
+
705
+ {#snippet DateCell(file: FileItem)}
706
+ <span class="text-default-600">{formatDate(file.lastModified, 'DD.MM.YYYY HH:mm')}</span>
707
+ {/snippet}
708
+
709
+ <div class="relative flex h-[calc(100vh-100px)] px-0">
710
+ <div class="min-w-0 flex-1">
711
+ {#if !isAuthenticated && adapter.authenticate}
712
+ <div class="flex h-full flex-col items-center justify-center">
713
+ <div class="mb-4 text-center text-lg">
714
+ Authentication required to access {adapter.getName()}
715
+ </div>
716
+ <Button color={Color.PRIMARY} onclick={authenticate}>Authenticate</Button>
717
+ </div>
718
+ {:else}
719
+ <div class="mb-2 flex flex-wrap items-center justify-between border-b border-gray-100 pb-3">
720
+ <div class="flex flex-wrap items-center">
721
+ {#if breadcrumbs.length > 1}
722
+ <button
723
+ class="text-default-600 mr-1 rounded-full px-2 py-1 hover:bg-gray-100"
724
+ onclick={navigateUp}
725
+ title="Go up"
726
+ aria-label="Navigate to parent folder"
727
+ >
728
+ <svg
729
+ xmlns="http://www.w3.org/2000/svg"
730
+ width="16"
731
+ height="16"
732
+ viewBox="0 0 24 24"
733
+ fill="none"
734
+ stroke="currentColor"
735
+ stroke-width="2"
736
+ stroke-linecap="round"
737
+ stroke-linejoin="round"
738
+ >
739
+ <path d="M19 12H5" />
740
+ <path d="M12 19l-7-7 7-7" />
741
+ </svg>
742
+ </button>
743
+ {/if}
744
+ <span class="text-default-400 mx-1 text-sm">/</span>
745
+
746
+ <div class="flex flex-wrap items-center">
747
+ {#each breadcrumbs as crumb, i}
748
+ {#if i > 0}
749
+ <span class="text-default-400 mx-1 text-sm">/</span>
750
+ {/if}
751
+
752
+ {#if crumb.clickable && !crumb.current}
753
+ <button
754
+ class="cursor-pointer border-0 bg-transparent px-1 py-0 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
755
+ onclick={() => navigateToFolder(crumb.path)}
756
+ >
757
+ {crumb.name}
758
+ </button>
759
+ {:else}
760
+ <span
761
+ class={`px-1 text-sm ${crumb.current ? 'font-semibold text-gray-800' : 'text-gray-600'}`}
762
+ >
763
+ {crumb.name}
764
+ </span>
765
+ {/if}
766
+ {/each}
767
+ </div>
768
+ </div>
769
+
770
+ <div class="flex items-center gap-1">
771
+ {#if isFetchingRecursively}
772
+ <div
773
+ class="flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-1 text-xs"
774
+ >
775
+ <div
776
+ class="h-3 w-3 animate-spin rounded-full border-2 border-amber-500 border-t-transparent"
777
+ ></div>
778
+ <span>
779
+ {#if fetchingFolderName}
780
+ Fetching: <span class="font-medium">{fetchingFolderName}</span>
781
+ {:else}
782
+ Fetching files
783
+ {/if}
784
+ <span class="ml-1 font-medium text-amber-700">
785
+ ({selectedFiles.length})
786
+ </span>
787
+ </span>
788
+ </div>
789
+ {:else}
790
+ {#each batchActions as action (action.label)}
791
+ <Button
792
+ color={Color.PRIMARY}
793
+ onclick={() => action.batchAction?.(allFilesAcquired)}
794
+ disabled={!action.isAllowed(allFilesAcquired)}
795
+ class="h-7 px-3 py-1 text-xs"
796
+ >
797
+ {action.label(allFilesAcquired)}
798
+ </Button>
799
+ {/each}
800
+ {/if}
801
+ <div class="flex items-center gap-1">
802
+ <div class="relative flex items-center">
803
+ <input
804
+ type="text"
805
+ class="h-7 w-40 rounded border border-gray-300 px-2 py-1 text-xs"
806
+ placeholder="Search files..."
807
+ value={searchQuery}
808
+ oninput={(e) => {
809
+ searchQuery = (e.target as HTMLInputElement).value;
810
+ }}
811
+ onkeydown={(e) => {
812
+ if (e.key === 'Enter') handleSearch();
813
+ }}
814
+ />
815
+ {#if searchQuery}
816
+ <button
817
+ class="absolute right-1 text-xs text-gray-400 hover:text-gray-600"
818
+ onclick={clearSearch}
819
+ title="Clear search"
820
+ >
821
+ ×
822
+ </button>
823
+ {/if}
824
+ </div>
825
+ <button
826
+ onclick={handleSearch}
827
+ class="rounded bg-gray-200 px-2 py-1 text-xs hover:bg-gray-300"
828
+ >
829
+ Search
830
+ </button>
831
+ </div>
832
+ </div>
833
+ </div>
834
+
835
+ {#if isLoading}
836
+ <div class="flex h-full items-center justify-center">
837
+ <div class="text-default-500">Loading files...</div>
838
+ </div>
839
+ {:else if error}
840
+ <div class="flex h-full flex-col items-center justify-center">
841
+ <div class="text-danger-500 mb-4">{error}</div>
842
+ <Button color={Color.PRIMARY} onclick={() => listFiles(currentPath)}>Retry</Button>
843
+ </div>
844
+ {:else if files.length === 0}
845
+ <div class="flex h-full items-center justify-center">
846
+ <div class="text-default-500">No files found in this directory</div>
847
+ </div>
848
+ {:else}
849
+ <Table
850
+ {columns}
851
+ data={displayFiles}
852
+ loading={isLoading}
853
+ bordered={false}
854
+ onrowclick={handleRowClick}
855
+ rowclass={(row) => {
856
+ let classes = row.isFolder ? 'hover:bg-amber-50 cursor-pointer' : 'hover:bg-blue-50';
857
+ if (isRowSelected(row)) {
858
+ classes += ' bg-primary-50';
859
+ }
860
+ return classes;
861
+ }}
862
+ onsort={handleSort}
863
+ selectable={true}
864
+ {onselect}
865
+ {selected}
866
+ />
867
+ {/if}
868
+ {/if}
869
+ </div>
870
+
871
+ {#if infoSection}
872
+ {@render infoSection({
873
+ selectedFiles: allFilesAcquired.map((file) => file.key),
874
+ navToFileFolder
875
+ })}
876
+ {/if}
877
+ </div>