@design.estate/dees-catalog 3.47.0 → 3.48.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist_bundle/bundle.js +4908 -2056
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-browser.d.ts +50 -0
  4. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-browser.demo.d.ts +2 -0
  5. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-browser.demo.js +148 -0
  6. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-browser.js +520 -0
  7. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.d.ts +99 -0
  8. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.js +1731 -0
  9. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.d.ts +64 -0
  10. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.js +1232 -0
  11. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-preview.d.ts +37 -0
  12. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-preview.js +626 -0
  13. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/index.d.ts +6 -0
  14. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/index.js +7 -0
  15. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/interfaces.d.ts +48 -0
  16. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/interfaces.js +5 -0
  17. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/utilities.d.ts +39 -0
  18. package/dist_ts_web/elements/00group-dataview/dees-s3-browser/utilities.js +109 -0
  19. package/dist_ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.js +6 -1
  20. package/dist_ts_web/elements/00group-dataview/index.d.ts +1 -0
  21. package/dist_ts_web/elements/00group-dataview/index.js +2 -1
  22. package/dist_watch/bundle.js +4933 -2090
  23. package/dist_watch/bundle.js.map +4 -4
  24. package/package.json +3 -3
  25. package/ts_web/00_commitinfo_data.ts +1 -1
  26. package/ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-browser.demo.ts +156 -0
  27. package/ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-browser.ts +439 -0
  28. package/ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-columns.ts +1652 -0
  29. package/ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-keys.ts +1094 -0
  30. package/ts_web/elements/00group-dataview/dees-s3-browser/dees-s3-preview.ts +540 -0
  31. package/ts_web/elements/00group-dataview/dees-s3-browser/index.ts +6 -0
  32. package/ts_web/elements/00group-dataview/dees-s3-browser/interfaces.ts +37 -0
  33. package/ts_web/elements/00group-dataview/dees-s3-browser/utilities.ts +120 -0
  34. package/ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts +5 -0
  35. package/ts_web/elements/00group-dataview/index.ts +1 -0
@@ -0,0 +1,1094 @@
1
+ import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
2
+ import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
3
+ import { themeDefaultStyles } from '../../00theme.js';
4
+ import type { IS3DataProvider, IS3Object } from './interfaces.js';
5
+ import { formatSize, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';
6
+
7
+ declare global {
8
+ interface HTMLElementTagNameMap {
9
+ 'dees-s3-keys': DeesS3Keys;
10
+ }
11
+ }
12
+
13
+ @customElement('dees-s3-keys')
14
+ export class DeesS3Keys extends DeesElement {
15
+ @property({ type: Object })
16
+ public accessor dataProvider: IS3DataProvider | null = null;
17
+
18
+ @property({ type: String })
19
+ public accessor bucketName: string = '';
20
+
21
+ @property({ type: String })
22
+ public accessor currentPrefix: string = '';
23
+
24
+ @property({ type: Number })
25
+ public accessor refreshKey: number = 0;
26
+
27
+ @state()
28
+ private accessor allKeys: IS3Object[] = [];
29
+
30
+ @state()
31
+ private accessor prefixes: string[] = [];
32
+
33
+ @state()
34
+ private accessor loading: boolean = false;
35
+
36
+ @state()
37
+ private accessor selectedKey: string = '';
38
+
39
+ @state()
40
+ private accessor filterText: string = '';
41
+
42
+ @state()
43
+ private accessor showCreateDialog: boolean = false;
44
+
45
+ @state()
46
+ private accessor createDialogType: 'folder' | 'file' = 'folder';
47
+
48
+ @state()
49
+ private accessor createDialogPrefix: string = '';
50
+
51
+ @state()
52
+ private accessor createDialogName: string = '';
53
+
54
+ // Move dialog state
55
+ @state()
56
+ private accessor showMoveDialog: boolean = false;
57
+
58
+ @state()
59
+ private accessor moveSource: { key: string; isFolder: boolean } | null = null;
60
+
61
+ @state()
62
+ private accessor moveDestination: string = '';
63
+
64
+ @state()
65
+ private accessor moveInProgress: boolean = false;
66
+
67
+ @state()
68
+ private accessor moveError: string | null = null;
69
+
70
+ // Move picker dialog state
71
+ @state()
72
+ private accessor showMovePickerDialog: boolean = false;
73
+
74
+ @state()
75
+ private accessor movePickerSource: { key: string; isFolder: boolean } | null = null;
76
+
77
+ @state()
78
+ private accessor movePickerCurrentPrefix: string = '';
79
+
80
+ @state()
81
+ private accessor movePickerPrefixes: string[] = [];
82
+
83
+ @state()
84
+ private accessor movePickerLoading: boolean = false;
85
+
86
+ // Rename dialog state
87
+ @state()
88
+ private accessor showRenameDialog: boolean = false;
89
+
90
+ @state()
91
+ private accessor renameSource: { key: string; isFolder: boolean } | null = null;
92
+
93
+ @state()
94
+ private accessor renameName: string = '';
95
+
96
+ @state()
97
+ private accessor renameInProgress: boolean = false;
98
+
99
+ @state()
100
+ private accessor renameError: string | null = null;
101
+
102
+ public static styles = [
103
+ cssManager.defaultStyles,
104
+ themeDefaultStyles,
105
+ css`
106
+ :host {
107
+ display: block;
108
+ height: 100%;
109
+ overflow: hidden;
110
+ }
111
+
112
+ .keys-container {
113
+ display: flex;
114
+ flex-direction: column;
115
+ height: 100%;
116
+ }
117
+
118
+ .filter-bar {
119
+ padding: 12px;
120
+ border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
121
+ }
122
+
123
+ .filter-input {
124
+ width: 100%;
125
+ padding: 8px 12px;
126
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.3)')};
127
+ border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')};
128
+ border-radius: 6px;
129
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
130
+ font-size: 14px;
131
+ }
132
+
133
+ .filter-input:focus {
134
+ outline: none;
135
+ border-color: ${cssManager.bdTheme('#a1a1aa', '#404040')};
136
+ }
137
+
138
+ .filter-input::placeholder {
139
+ color: ${cssManager.bdTheme('#a1a1aa', '#666')};
140
+ }
141
+
142
+ .keys-list {
143
+ flex: 1;
144
+ overflow-y: auto;
145
+ }
146
+
147
+ table {
148
+ width: 100%;
149
+ border-collapse: collapse;
150
+ }
151
+
152
+ thead {
153
+ position: sticky;
154
+ top: 0;
155
+ background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
156
+ z-index: 1;
157
+ }
158
+
159
+ th {
160
+ text-align: left;
161
+ padding: 10px 12px;
162
+ font-size: 12px;
163
+ font-weight: 500;
164
+ color: ${cssManager.bdTheme('#71717a', '#666')};
165
+ text-transform: uppercase;
166
+ border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
167
+ }
168
+
169
+ td {
170
+ padding: 8px 12px;
171
+ font-size: 13px;
172
+ border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#2a2a3e')};
173
+ }
174
+
175
+ tr:hover td {
176
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.03)')};
177
+ }
178
+
179
+ tr.selected td {
180
+ background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.08)', 'rgba(255, 255, 255, 0.08)')};
181
+ }
182
+
183
+ .key-cell {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 8px;
187
+ cursor: pointer;
188
+ }
189
+
190
+ .key-icon {
191
+ width: 16px;
192
+ height: 16px;
193
+ flex-shrink: 0;
194
+ }
195
+
196
+ .folder-icon {
197
+ color: #fbbf24;
198
+ }
199
+
200
+ .key-name {
201
+ white-space: nowrap;
202
+ overflow: hidden;
203
+ text-overflow: ellipsis;
204
+ }
205
+
206
+ .size-cell {
207
+ color: ${cssManager.bdTheme('#71717a', '#888')};
208
+ font-variant-numeric: tabular-nums;
209
+ }
210
+
211
+ .empty-state {
212
+ padding: 32px;
213
+ text-align: center;
214
+ color: ${cssManager.bdTheme('#a1a1aa', '#666')};
215
+ }
216
+
217
+ .dialog-overlay {
218
+ position: fixed;
219
+ top: 0;
220
+ left: 0;
221
+ right: 0;
222
+ bottom: 0;
223
+ background: rgba(0, 0, 0, 0.7);
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ z-index: 1000;
228
+ }
229
+
230
+ .dialog {
231
+ background: ${cssManager.bdTheme('#ffffff', '#1e1e1e')};
232
+ border-radius: 12px;
233
+ padding: 24px;
234
+ min-width: 400px;
235
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
236
+ }
237
+
238
+ .dialog-title {
239
+ font-size: 18px;
240
+ font-weight: 600;
241
+ margin-bottom: 16px;
242
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
243
+ }
244
+
245
+ .dialog-location {
246
+ font-size: 12px;
247
+ color: ${cssManager.bdTheme('#71717a', '#888')};
248
+ margin-bottom: 12px;
249
+ font-family: monospace;
250
+ }
251
+
252
+ .dialog-input {
253
+ width: 100%;
254
+ padding: 10px 12px;
255
+ background: ${cssManager.bdTheme('#f4f4f5', '#141414')};
256
+ border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#333')};
257
+ border-radius: 6px;
258
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
259
+ font-size: 14px;
260
+ margin-bottom: 8px;
261
+ box-sizing: border-box;
262
+ }
263
+
264
+ .dialog-input:focus {
265
+ outline: none;
266
+ border-color: ${cssManager.bdTheme('#a1a1aa', '#e0e0e0')};
267
+ }
268
+
269
+ .dialog-hint {
270
+ font-size: 11px;
271
+ color: ${cssManager.bdTheme('#a1a1aa', '#666')};
272
+ margin-bottom: 16px;
273
+ }
274
+
275
+ .dialog-actions {
276
+ display: flex;
277
+ gap: 12px;
278
+ justify-content: flex-end;
279
+ }
280
+
281
+ .dialog-btn {
282
+ padding: 8px 16px;
283
+ border-radius: 6px;
284
+ font-size: 14px;
285
+ cursor: pointer;
286
+ transition: all 0.2s;
287
+ }
288
+
289
+ .dialog-btn-cancel {
290
+ background: transparent;
291
+ border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')};
292
+ color: ${cssManager.bdTheme('#71717a', '#aaa')};
293
+ }
294
+
295
+ .dialog-btn-cancel:hover {
296
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
297
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
298
+ }
299
+
300
+ .dialog-btn-create {
301
+ background: ${cssManager.bdTheme('#e5e7eb', '#404040')};
302
+ border: none;
303
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
304
+ }
305
+
306
+ .dialog-btn-create:hover {
307
+ background: ${cssManager.bdTheme('#d4d4d8', '#505050')};
308
+ }
309
+
310
+ .dialog-btn-create:disabled {
311
+ opacity: 0.5;
312
+ cursor: not-allowed;
313
+ }
314
+
315
+ /* Move dialog styles */
316
+ .move-summary {
317
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')};
318
+ border-radius: 8px;
319
+ padding: 16px;
320
+ margin-bottom: 16px;
321
+ }
322
+
323
+ .move-item {
324
+ display: flex;
325
+ gap: 8px;
326
+ margin-bottom: 8px;
327
+ }
328
+
329
+ .move-item:last-child {
330
+ margin-bottom: 0;
331
+ }
332
+
333
+ .move-label {
334
+ color: ${cssManager.bdTheme('#71717a', '#888')};
335
+ min-width: 40px;
336
+ }
337
+
338
+ .move-path {
339
+ color: ${cssManager.bdTheme('#3f3f46', '#e0e0e0')};
340
+ font-family: monospace;
341
+ font-size: 12px;
342
+ word-break: break-all;
343
+ }
344
+
345
+ .move-error {
346
+ background: rgba(239, 68, 68, 0.15);
347
+ border: 1px solid rgba(239, 68, 68, 0.3);
348
+ color: #f87171;
349
+ padding: 12px 16px;
350
+ border-radius: 8px;
351
+ margin-bottom: 16px;
352
+ font-size: 13px;
353
+ }
354
+
355
+ /* Move picker dialog styles */
356
+ .move-picker-dialog {
357
+ min-width: 450px;
358
+ max-height: 500px;
359
+ }
360
+
361
+ .picker-breadcrumb {
362
+ display: flex;
363
+ align-items: center;
364
+ gap: 4px;
365
+ padding: 8px 12px;
366
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')};
367
+ border-radius: 6px;
368
+ margin-bottom: 12px;
369
+ font-size: 12px;
370
+ overflow-x: auto;
371
+ }
372
+
373
+ .picker-crumb {
374
+ color: ${cssManager.bdTheme('#71717a', '#888')};
375
+ cursor: pointer;
376
+ white-space: nowrap;
377
+ }
378
+
379
+ .picker-crumb:hover {
380
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
381
+ }
382
+
383
+ .picker-separator {
384
+ color: ${cssManager.bdTheme('#d4d4d8', '#555')};
385
+ }
386
+
387
+ .picker-list {
388
+ max-height: 280px;
389
+ overflow-y: auto;
390
+ border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
391
+ border-radius: 6px;
392
+ margin-bottom: 16px;
393
+ min-height: 100px;
394
+ }
395
+
396
+ .picker-item {
397
+ display: flex;
398
+ align-items: center;
399
+ gap: 8px;
400
+ padding: 8px 12px;
401
+ cursor: pointer;
402
+ color: #fbbf24;
403
+ }
404
+
405
+ .picker-item:hover {
406
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
407
+ }
408
+
409
+ .picker-item .icon {
410
+ width: 16px;
411
+ height: 16px;
412
+ flex-shrink: 0;
413
+ }
414
+
415
+ .picker-item .name {
416
+ flex: 1;
417
+ overflow: hidden;
418
+ text-overflow: ellipsis;
419
+ white-space: nowrap;
420
+ }
421
+
422
+ .picker-empty {
423
+ padding: 24px;
424
+ text-align: center;
425
+ color: ${cssManager.bdTheme('#a1a1aa', '#666')};
426
+ font-size: 13px;
427
+ }
428
+ `,
429
+ ];
430
+
431
+ async connectedCallback() {
432
+ super.connectedCallback();
433
+ await this.loadObjects();
434
+ }
435
+
436
+ updated(changedProperties: Map<string, unknown>) {
437
+ if (changedProperties.has('bucketName') || changedProperties.has('currentPrefix') || changedProperties.has('refreshKey')) {
438
+ this.loadObjects();
439
+ }
440
+ }
441
+
442
+ private async loadObjects() {
443
+ if (!this.dataProvider) return;
444
+ this.loading = true;
445
+ try {
446
+ const result = await this.dataProvider.listObjects(this.bucketName, this.currentPrefix, '/');
447
+ this.allKeys = result.objects;
448
+ this.prefixes = result.prefixes;
449
+ } catch (err) {
450
+ console.error('Error loading objects:', err);
451
+ this.allKeys = [];
452
+ this.prefixes = [];
453
+ }
454
+ this.loading = false;
455
+ }
456
+
457
+ private handleFilterInput(e: Event) {
458
+ this.filterText = (e.target as HTMLInputElement).value;
459
+ }
460
+
461
+ private selectKey(key: string, isFolder: boolean) {
462
+ this.selectedKey = key;
463
+
464
+ if (isFolder) {
465
+ this.dispatchEvent(
466
+ new CustomEvent('navigate', {
467
+ detail: { prefix: key },
468
+ bubbles: true,
469
+ composed: true,
470
+ })
471
+ );
472
+ } else {
473
+ this.dispatchEvent(
474
+ new CustomEvent('key-selected', {
475
+ detail: { key },
476
+ bubbles: true,
477
+ composed: true,
478
+ })
479
+ );
480
+ }
481
+ }
482
+
483
+ private get filteredItems() {
484
+ const filter = this.filterText.toLowerCase();
485
+ const folders = this.prefixes
486
+ .filter((p) => !filter || getFileName(p).toLowerCase().includes(filter))
487
+ .map((p) => ({ key: p, isFolder: true, size: undefined as number | undefined }));
488
+ const files = this.allKeys
489
+ .filter((o) => !filter || getFileName(o.key).toLowerCase().includes(filter))
490
+ .map((o) => ({ key: o.key, isFolder: false, size: o.size }));
491
+ return [...folders, ...files];
492
+ }
493
+
494
+ private handleItemContextMenu(event: MouseEvent, key: string, isFolder: boolean) {
495
+ if (!this.dataProvider) return;
496
+ event.preventDefault();
497
+
498
+ if (isFolder) {
499
+ DeesContextmenu.openContextMenuWithOptions(event, [
500
+ {
501
+ name: 'Open',
502
+ iconName: 'lucide:folderOpen',
503
+ action: async () => {
504
+ this.selectKey(key, true);
505
+ },
506
+ },
507
+ {
508
+ name: 'Copy Path',
509
+ iconName: 'lucide:copy',
510
+ action: async () => {
511
+ await navigator.clipboard.writeText(key);
512
+ },
513
+ },
514
+ {
515
+ name: 'Rename',
516
+ iconName: 'lucide:pencil',
517
+ action: async () => this.openRenameDialog(key, true),
518
+ },
519
+ { divider: true },
520
+ {
521
+ name: 'Move to...',
522
+ iconName: 'lucide:folderInput',
523
+ action: async () => this.openMovePickerDialog(key, true),
524
+ },
525
+ { divider: true },
526
+ {
527
+ name: 'New Folder Inside',
528
+ iconName: 'lucide:folderPlus',
529
+ action: async () => this.openCreateDialog('folder', key),
530
+ },
531
+ {
532
+ name: 'New File Inside',
533
+ iconName: 'lucide:filePlus',
534
+ action: async () => this.openCreateDialog('file', key),
535
+ },
536
+ { divider: true },
537
+ {
538
+ name: 'Delete Folder',
539
+ iconName: 'lucide:trash2',
540
+ action: async () => {
541
+ if (confirm(`Delete folder "${getFileName(key)}" and all its contents?`)) {
542
+ const success = await this.dataProvider!.deletePrefix(this.bucketName, key);
543
+ if (success) {
544
+ await this.loadObjects();
545
+ }
546
+ }
547
+ },
548
+ },
549
+ ]);
550
+ } else {
551
+ DeesContextmenu.openContextMenuWithOptions(event, [
552
+ {
553
+ name: 'Preview',
554
+ iconName: 'lucide:eye',
555
+ action: async () => {
556
+ this.selectKey(key, false);
557
+ },
558
+ },
559
+ {
560
+ name: 'Download',
561
+ iconName: 'lucide:download',
562
+ action: async () => {
563
+ const url = await this.dataProvider!.getObjectUrl(this.bucketName, key);
564
+ const link = document.createElement('a');
565
+ link.href = url;
566
+ link.download = getFileName(key);
567
+ link.click();
568
+ },
569
+ },
570
+ {
571
+ name: 'Copy Path',
572
+ iconName: 'lucide:copy',
573
+ action: async () => {
574
+ await navigator.clipboard.writeText(key);
575
+ },
576
+ },
577
+ {
578
+ name: 'Rename',
579
+ iconName: 'lucide:pencil',
580
+ action: async () => this.openRenameDialog(key, false),
581
+ },
582
+ { divider: true },
583
+ {
584
+ name: 'Move to...',
585
+ iconName: 'lucide:folderInput',
586
+ action: async () => this.openMovePickerDialog(key, false),
587
+ },
588
+ { divider: true },
589
+ {
590
+ name: 'Delete',
591
+ iconName: 'lucide:trash2',
592
+ action: async () => {
593
+ if (confirm(`Delete file "${getFileName(key)}"?`)) {
594
+ const success = await this.dataProvider!.deleteObject(this.bucketName, key);
595
+ if (success) {
596
+ await this.loadObjects();
597
+ }
598
+ }
599
+ },
600
+ },
601
+ ]);
602
+ }
603
+ }
604
+
605
+ private handleEmptySpaceContextMenu(event: MouseEvent) {
606
+ if ((event.target as HTMLElement).closest('tr')) return;
607
+ event.preventDefault();
608
+
609
+ DeesContextmenu.openContextMenuWithOptions(event, [
610
+ {
611
+ name: 'New Folder',
612
+ iconName: 'lucide:folderPlus',
613
+ action: async () => this.openCreateDialog('folder', this.currentPrefix),
614
+ },
615
+ {
616
+ name: 'New File',
617
+ iconName: 'lucide:filePlus',
618
+ action: async () => this.openCreateDialog('file', this.currentPrefix),
619
+ },
620
+ ]);
621
+ }
622
+
623
+ private openCreateDialog(type: 'folder' | 'file', prefix: string) {
624
+ this.createDialogType = type;
625
+ this.createDialogPrefix = prefix;
626
+ this.createDialogName = '';
627
+ this.showCreateDialog = true;
628
+ }
629
+
630
+ private async handleCreate() {
631
+ if (!this.createDialogName.trim() || !this.dataProvider) return;
632
+
633
+ const name = this.createDialogName.trim();
634
+ let path: string;
635
+
636
+ if (this.createDialogType === 'folder') {
637
+ path = this.createDialogPrefix + name + '/.keep';
638
+ } else {
639
+ path = this.createDialogPrefix + name;
640
+ }
641
+
642
+ const ext = name.split('.').pop()?.toLowerCase() || '';
643
+ const contentType = this.createDialogType === 'file' ? getContentType(ext) : 'application/octet-stream';
644
+ const content = this.createDialogType === 'file' ? getDefaultContent(ext) : '';
645
+
646
+ const success = await this.dataProvider.putObject(
647
+ this.bucketName,
648
+ path,
649
+ btoa(content),
650
+ contentType
651
+ );
652
+
653
+ if (success) {
654
+ this.showCreateDialog = false;
655
+ await this.loadObjects();
656
+ }
657
+ }
658
+
659
+ private renderCreateDialog() {
660
+ if (!this.showCreateDialog) return '';
661
+
662
+ const isFolder = this.createDialogType === 'folder';
663
+ const title = isFolder ? 'Create New Folder' : 'Create New File';
664
+ const placeholder = isFolder ? 'folder-name or path/to/folder' : 'filename.txt or path/to/file.txt';
665
+
666
+ return html`
667
+ <div class="dialog-overlay" @click=${() => this.showCreateDialog = false}>
668
+ <div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
669
+ <div class="dialog-title">${title}</div>
670
+ <div class="dialog-location">
671
+ Location: ${this.bucketName}/${this.createDialogPrefix}
672
+ </div>
673
+ <input
674
+ type="text"
675
+ class="dialog-input"
676
+ placeholder=${placeholder}
677
+ .value=${this.createDialogName}
678
+ @input=${(e: InputEvent) => this.createDialogName = (e.target as HTMLInputElement).value}
679
+ @keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.handleCreate()}
680
+ />
681
+ <div class="dialog-hint">
682
+ Use "/" to create nested ${isFolder ? 'folders' : 'path'} (e.g., ${isFolder ? 'parent/child' : 'folder/file.txt'})
683
+ </div>
684
+ <div class="dialog-actions">
685
+ <button class="dialog-btn dialog-btn-cancel" @click=${() => this.showCreateDialog = false}>
686
+ Cancel
687
+ </button>
688
+ <button
689
+ class="dialog-btn dialog-btn-create"
690
+ ?disabled=${!this.createDialogName.trim()}
691
+ @click=${() => this.handleCreate()}
692
+ >
693
+ Create
694
+ </button>
695
+ </div>
696
+ </div>
697
+ </div>
698
+ `;
699
+ }
700
+
701
+ // --- Move picker dialog ---
702
+
703
+ private async openMovePickerDialog(key: string, isFolder: boolean) {
704
+ this.movePickerSource = { key, isFolder };
705
+ this.movePickerCurrentPrefix = '';
706
+ this.showMovePickerDialog = true;
707
+ await this.loadMovePickerPrefixes('');
708
+ }
709
+
710
+ private async navigateMovePicker(prefix: string) {
711
+ this.movePickerCurrentPrefix = prefix;
712
+ await this.loadMovePickerPrefixes(prefix);
713
+ }
714
+
715
+ private async loadMovePickerPrefixes(prefix: string) {
716
+ if (!this.dataProvider) return;
717
+ this.movePickerLoading = true;
718
+ try {
719
+ const result = await this.dataProvider.listObjects(this.bucketName, prefix, '/');
720
+ this.movePickerPrefixes = result.prefixes;
721
+ } catch {
722
+ this.movePickerPrefixes = [];
723
+ }
724
+ this.movePickerLoading = false;
725
+ }
726
+
727
+ private selectMoveDestination(destPrefix: string) {
728
+ if (!this.movePickerSource) return;
729
+ this.closeMovePickerDialog();
730
+ this.initiateMove(this.movePickerSource.key, this.movePickerSource.isFolder, destPrefix);
731
+ }
732
+
733
+ private closeMovePickerDialog() {
734
+ this.showMovePickerDialog = false;
735
+ this.movePickerSource = null;
736
+ this.movePickerCurrentPrefix = '';
737
+ this.movePickerPrefixes = [];
738
+ }
739
+
740
+ // --- Move confirmation dialog ---
741
+
742
+ private initiateMove(sourceKey: string, isFolder: boolean, destPrefix: string) {
743
+ const validation = validateMove(sourceKey, destPrefix);
744
+
745
+ if (!validation.valid) {
746
+ this.moveSource = { key: sourceKey, isFolder };
747
+ this.moveDestination = destPrefix;
748
+ this.moveError = validation.error!;
749
+ this.showMoveDialog = true;
750
+ return;
751
+ }
752
+
753
+ this.moveSource = { key: sourceKey, isFolder };
754
+ this.moveDestination = destPrefix;
755
+ this.moveError = null;
756
+ this.showMoveDialog = true;
757
+ }
758
+
759
+ private async executeMove() {
760
+ if (!this.moveSource || !this.dataProvider) return;
761
+
762
+ this.moveInProgress = true;
763
+
764
+ try {
765
+ const sourceName = getFileName(this.moveSource.key);
766
+ const destKey = this.moveDestination + sourceName;
767
+
768
+ let result: { success: boolean; error?: string };
769
+ if (this.moveSource.isFolder) {
770
+ result = await this.dataProvider.movePrefix(
771
+ this.bucketName,
772
+ this.moveSource.key,
773
+ destKey
774
+ );
775
+ } else {
776
+ result = await this.dataProvider.moveObject(
777
+ this.bucketName,
778
+ this.moveSource.key,
779
+ destKey
780
+ );
781
+ }
782
+
783
+ if (result.success) {
784
+ this.closeMoveDialog();
785
+ await this.loadObjects();
786
+ } else {
787
+ this.moveError = result.error || 'Move operation failed';
788
+ }
789
+ } catch (err) {
790
+ this.moveError = `Error: ${err}`;
791
+ }
792
+
793
+ this.moveInProgress = false;
794
+ }
795
+
796
+ private closeMoveDialog() {
797
+ this.showMoveDialog = false;
798
+ this.moveSource = null;
799
+ this.moveDestination = '';
800
+ this.moveError = null;
801
+ this.moveInProgress = false;
802
+ }
803
+
804
+ // --- Rename dialog methods ---
805
+
806
+ private openRenameDialog(key: string, isFolder: boolean) {
807
+ this.renameSource = { key, isFolder };
808
+ this.renameName = getFileName(key);
809
+ this.renameError = null;
810
+ this.showRenameDialog = true;
811
+
812
+ this.updateComplete.then(() => {
813
+ const input = this.shadowRoot?.querySelector('.rename-dialog-input') as HTMLInputElement;
814
+ if (input) {
815
+ input.focus();
816
+ if (!isFolder) {
817
+ const lastDot = this.renameName.lastIndexOf('.');
818
+ if (lastDot > 0) {
819
+ input.setSelectionRange(0, lastDot);
820
+ } else {
821
+ input.select();
822
+ }
823
+ } else {
824
+ input.select();
825
+ }
826
+ }
827
+ });
828
+ }
829
+
830
+ private async executeRename() {
831
+ if (!this.renameSource || !this.renameName.trim() || !this.dataProvider) return;
832
+
833
+ const newName = this.renameName.trim();
834
+ const currentName = getFileName(this.renameSource.key);
835
+
836
+ if (newName === currentName) {
837
+ this.renameError = 'Name is the same as current';
838
+ return;
839
+ }
840
+ if (!newName) {
841
+ this.renameError = 'Name cannot be empty';
842
+ return;
843
+ }
844
+ if (newName.includes('/')) {
845
+ this.renameError = 'Name cannot contain "/"';
846
+ return;
847
+ }
848
+
849
+ this.renameInProgress = true;
850
+ this.renameError = null;
851
+
852
+ try {
853
+ const parentPrefix = getParentPrefix(this.renameSource.key);
854
+ const newKey = parentPrefix + newName + (this.renameSource.isFolder ? '/' : '');
855
+
856
+ let result: { success: boolean; error?: string };
857
+ if (this.renameSource.isFolder) {
858
+ result = await this.dataProvider.movePrefix(this.bucketName, this.renameSource.key, newKey);
859
+ } else {
860
+ result = await this.dataProvider.moveObject(this.bucketName, this.renameSource.key, newKey);
861
+ }
862
+
863
+ if (result.success) {
864
+ this.closeRenameDialog();
865
+ await this.loadObjects();
866
+ } else {
867
+ this.renameError = result.error || 'Rename failed';
868
+ }
869
+ } catch (err) {
870
+ this.renameError = `Error: ${err}`;
871
+ }
872
+ this.renameInProgress = false;
873
+ }
874
+
875
+ private closeRenameDialog() {
876
+ this.showRenameDialog = false;
877
+ this.renameSource = null;
878
+ this.renameName = '';
879
+ this.renameError = null;
880
+ this.renameInProgress = false;
881
+ }
882
+
883
+ private renderRenameDialog() {
884
+ if (!this.showRenameDialog || !this.renameSource) return '';
885
+
886
+ const isFolder = this.renameSource.isFolder;
887
+ const title = isFolder ? 'Rename Folder' : 'Rename File';
888
+
889
+ return html`
890
+ <div class="dialog-overlay" @click=${() => this.closeRenameDialog()}>
891
+ <div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
892
+ <div class="dialog-title">${title}</div>
893
+ <div class="dialog-location">
894
+ Location: ${this.bucketName}/${getParentPrefix(this.renameSource.key)}
895
+ </div>
896
+ ${this.renameError ? html`<div class="move-error">${this.renameError}</div>` : ''}
897
+ <input
898
+ type="text"
899
+ class="dialog-input rename-dialog-input"
900
+ placeholder=${isFolder ? 'folder-name' : 'filename.ext'}
901
+ .value=${this.renameName}
902
+ @input=${(e: InputEvent) => {
903
+ this.renameName = (e.target as HTMLInputElement).value;
904
+ this.renameError = null;
905
+ }}
906
+ @keydown=${(e: KeyboardEvent) => {
907
+ if (e.key === 'Enter') this.executeRename();
908
+ if (e.key === 'Escape') this.closeRenameDialog();
909
+ }}
910
+ />
911
+ <div class="dialog-actions">
912
+ <button class="dialog-btn dialog-btn-cancel"
913
+ @click=${() => this.closeRenameDialog()}
914
+ ?disabled=${this.renameInProgress}>Cancel</button>
915
+ <button class="dialog-btn dialog-btn-create"
916
+ @click=${() => this.executeRename()}
917
+ ?disabled=${this.renameInProgress || !this.renameName.trim()}>
918
+ ${this.renameInProgress ? 'Renaming...' : 'Rename'}
919
+ </button>
920
+ </div>
921
+ </div>
922
+ </div>
923
+ `;
924
+ }
925
+
926
+ private renderMoveDialog() {
927
+ if (!this.showMoveDialog || !this.moveSource) return '';
928
+
929
+ const sourceName = getFileName(this.moveSource.key);
930
+
931
+ return html`
932
+ <div class="dialog-overlay" @click=${() => this.closeMoveDialog()}>
933
+ <div class="dialog" @click=${(e: Event) => e.stopPropagation()}>
934
+ <div class="dialog-title">Move ${this.moveSource.isFolder ? 'Folder' : 'File'}</div>
935
+
936
+ ${this.moveError ? html`
937
+ <div class="move-error">${this.moveError}</div>
938
+ ` : html`
939
+ <div class="move-summary">
940
+ <div class="move-item">
941
+ <span class="move-label">From:</span>
942
+ <span class="move-path">${this.moveSource.key}</span>
943
+ </div>
944
+ <div class="move-item">
945
+ <span class="move-label">To:</span>
946
+ <span class="move-path">${this.moveDestination}${sourceName}</span>
947
+ </div>
948
+ </div>
949
+ `}
950
+
951
+ <div class="dialog-actions">
952
+ <button class="dialog-btn dialog-btn-cancel"
953
+ @click=${() => this.closeMoveDialog()}
954
+ ?disabled=${this.moveInProgress}>
955
+ Cancel
956
+ </button>
957
+ ${!this.moveError ? html`
958
+ <button class="dialog-btn dialog-btn-create"
959
+ @click=${() => this.executeMove()}
960
+ ?disabled=${this.moveInProgress}>
961
+ ${this.moveInProgress ? 'Moving...' : 'Move'}
962
+ </button>
963
+ ` : ''}
964
+ </div>
965
+ </div>
966
+ </div>
967
+ `;
968
+ }
969
+
970
+ private renderMovePickerDialog() {
971
+ if (!this.showMovePickerDialog || !this.movePickerSource) return '';
972
+
973
+ const sourceName = getFileName(this.movePickerSource.key);
974
+ const sourceParent = getParentPrefix(this.movePickerSource.key);
975
+
976
+ return html`
977
+ <div class="dialog-overlay" @click=${() => this.closeMovePickerDialog()}>
978
+ <div class="dialog move-picker-dialog" @click=${(e: Event) => e.stopPropagation()}>
979
+ <div class="dialog-title">Move "${sourceName}" to...</div>
980
+
981
+ <div class="picker-breadcrumb">
982
+ <span class="picker-crumb" @click=${() => this.navigateMovePicker('')}>
983
+ ${this.bucketName}
984
+ </span>
985
+ ${getPathSegments(this.movePickerCurrentPrefix).map(seg => html`
986
+ <span class="picker-separator">/</span>
987
+ <span class="picker-crumb" @click=${() => this.navigateMovePicker(seg)}>
988
+ ${getFileName(seg)}
989
+ </span>
990
+ `)}
991
+ </div>
992
+
993
+ <div class="picker-list">
994
+ ${this.movePickerLoading ? html`<div class="picker-empty">Loading...</div>` : ''}
995
+ ${!this.movePickerLoading && this.movePickerPrefixes.filter(p => p !== this.movePickerSource!.key).length === 0 ? html`
996
+ <div class="picker-empty">No subfolders</div>
997
+ ` : ''}
998
+ ${this.movePickerPrefixes
999
+ .filter(p => p !== this.movePickerSource!.key)
1000
+ .map(prefix => html`
1001
+ <div class="picker-item"
1002
+ @click=${() => this.navigateMovePicker(prefix)}
1003
+ @dblclick=${() => this.selectMoveDestination(prefix)}>
1004
+ <svg class="icon" viewBox="0 0 24 24" fill="currentColor">
1005
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
1006
+ </svg>
1007
+ <span class="name">${getFileName(prefix)}</span>
1008
+ </div>
1009
+ `)}
1010
+ </div>
1011
+
1012
+ <div class="dialog-actions">
1013
+ <button class="dialog-btn dialog-btn-cancel" @click=${() => this.closeMovePickerDialog()}>
1014
+ Cancel
1015
+ </button>
1016
+ <button class="dialog-btn dialog-btn-create"
1017
+ @click=${() => this.selectMoveDestination(this.movePickerCurrentPrefix)}
1018
+ ?disabled=${this.movePickerCurrentPrefix === sourceParent}>
1019
+ Move Here
1020
+ </button>
1021
+ </div>
1022
+ </div>
1023
+ </div>
1024
+ `;
1025
+ }
1026
+
1027
+ render() {
1028
+ return html`
1029
+ <div class="keys-container">
1030
+ <div class="filter-bar">
1031
+ <input
1032
+ type="text"
1033
+ class="filter-input"
1034
+ placeholder="Filter files..."
1035
+ .value=${this.filterText}
1036
+ @input=${this.handleFilterInput}
1037
+ />
1038
+ </div>
1039
+
1040
+ <div class="keys-list" @contextmenu=${(e: MouseEvent) => this.handleEmptySpaceContextMenu(e)}>
1041
+ ${this.loading
1042
+ ? html`<div class="empty-state">Loading...</div>`
1043
+ : this.filteredItems.length === 0
1044
+ ? html`<div class="empty-state">No objects found</div>`
1045
+ : html`
1046
+ <table>
1047
+ <thead>
1048
+ <tr>
1049
+ <th>Name</th>
1050
+ <th style="width: 100px;">Size</th>
1051
+ </tr>
1052
+ </thead>
1053
+ <tbody>
1054
+ ${this.filteredItems.map(
1055
+ (item) => html`
1056
+ <tr
1057
+ class="${this.selectedKey === item.key ? 'selected' : ''}"
1058
+ @click=${() => this.selectKey(item.key, item.isFolder)}
1059
+ @contextmenu=${(e: MouseEvent) => this.handleItemContextMenu(e, item.key, item.isFolder)}
1060
+ >
1061
+ <td>
1062
+ <div class="key-cell">
1063
+ ${item.isFolder
1064
+ ? html`
1065
+ <svg class="key-icon folder-icon" viewBox="0 0 24 24" fill="currentColor">
1066
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
1067
+ </svg>
1068
+ `
1069
+ : html`
1070
+ <svg class="key-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1071
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
1072
+ </svg>
1073
+ `}
1074
+ <span class="key-name">${getFileName(item.key)}</span>
1075
+ </div>
1076
+ </td>
1077
+ <td class="size-cell">
1078
+ ${item.isFolder ? '-' : formatSize(item.size)}
1079
+ </td>
1080
+ </tr>
1081
+ `
1082
+ )}
1083
+ </tbody>
1084
+ </table>
1085
+ `}
1086
+ </div>
1087
+ </div>
1088
+ ${this.renderCreateDialog()}
1089
+ ${this.renderMoveDialog()}
1090
+ ${this.renderMovePickerDialog()}
1091
+ ${this.renderRenameDialog()}
1092
+ `;
1093
+ }
1094
+ }