@design.estate/dees-catalog 3.47.1 → 3.48.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 (33) hide show
  1. package/dist_bundle/bundle.js +4903 -2056
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-browser.d.ts +50 -0
  4. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-browser.demo.d.ts +2 -0
  5. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-browser.demo.js +148 -0
  6. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-browser.js +520 -0
  7. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-columns.d.ts +99 -0
  8. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-columns.js +1731 -0
  9. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-keys.d.ts +64 -0
  10. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-keys.js +1232 -0
  11. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-preview.d.ts +37 -0
  12. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-preview.js +626 -0
  13. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/index.d.ts +6 -0
  14. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/index.js +7 -0
  15. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/interfaces.d.ts +48 -0
  16. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/interfaces.js +5 -0
  17. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/utilities.d.ts +39 -0
  18. package/dist_ts_web/elements/00group-dataview/dees-storage-browser/utilities.js +109 -0
  19. package/dist_ts_web/elements/00group-dataview/index.d.ts +1 -0
  20. package/dist_ts_web/elements/00group-dataview/index.js +2 -1
  21. package/dist_watch/bundle.js +4928 -2090
  22. package/dist_watch/bundle.js.map +4 -4
  23. package/package.json +3 -3
  24. package/ts_web/00_commitinfo_data.ts +1 -1
  25. package/ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-browser.demo.ts +156 -0
  26. package/ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-browser.ts +439 -0
  27. package/ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-columns.ts +1652 -0
  28. package/ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-keys.ts +1094 -0
  29. package/ts_web/elements/00group-dataview/dees-storage-browser/dees-storage-preview.ts +540 -0
  30. package/ts_web/elements/00group-dataview/dees-storage-browser/index.ts +6 -0
  31. package/ts_web/elements/00group-dataview/dees-storage-browser/interfaces.ts +37 -0
  32. package/ts_web/elements/00group-dataview/dees-storage-browser/utilities.ts +120 -0
  33. package/ts_web/elements/00group-dataview/index.ts +1 -0
@@ -0,0 +1,540 @@
1
+ import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element';
2
+ import { themeDefaultStyles } from '../../00theme.js';
3
+ import type { IS3DataProvider } from './interfaces.js';
4
+ import { formatSize, getFileName } from './utilities.js';
5
+
6
+ declare global {
7
+ interface HTMLElementTagNameMap {
8
+ 'dees-storage-preview': DeesStoragePreview;
9
+ }
10
+ }
11
+
12
+ @customElement('dees-storage-preview')
13
+ export class DeesStoragePreview extends DeesElement {
14
+ @property({ type: Object })
15
+ public accessor dataProvider: IS3DataProvider | null = null;
16
+
17
+ @property({ type: String })
18
+ public accessor bucketName: string = '';
19
+
20
+ @property({ type: String })
21
+ public accessor objectKey: string = '';
22
+
23
+ @state()
24
+ private accessor loading: boolean = false;
25
+
26
+ @state()
27
+ private accessor saving: boolean = false;
28
+
29
+ @state()
30
+ private accessor content: string = '';
31
+
32
+ @state()
33
+ private accessor originalTextContent: string = '';
34
+
35
+ @state()
36
+ private accessor hasChanges: boolean = false;
37
+
38
+ @state()
39
+ private accessor editing: boolean = false;
40
+
41
+ @state()
42
+ private accessor contentType: string = '';
43
+
44
+ @state()
45
+ private accessor fileSize: number = 0;
46
+
47
+ @state()
48
+ private accessor lastModified: string = '';
49
+
50
+ @state()
51
+ private accessor error: string = '';
52
+
53
+ public static styles = [
54
+ cssManager.defaultStyles,
55
+ themeDefaultStyles,
56
+ css`
57
+ :host {
58
+ display: block;
59
+ height: 100%;
60
+ }
61
+
62
+ .preview-container {
63
+ display: flex;
64
+ flex-direction: column;
65
+ height: 100%;
66
+ }
67
+
68
+ .preview-header {
69
+ padding: 12px;
70
+ border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
71
+ }
72
+
73
+ .preview-title {
74
+ font-size: 14px;
75
+ font-weight: 500;
76
+ margin-bottom: 8px;
77
+ word-break: break-all;
78
+ }
79
+
80
+ .preview-meta {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ gap: 16px;
84
+ font-size: 12px;
85
+ color: ${cssManager.bdTheme('#71717a', '#888')};
86
+ }
87
+
88
+ .meta-item {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 4px;
92
+ }
93
+
94
+ .preview-content {
95
+ flex: 1;
96
+ overflow: hidden;
97
+ }
98
+
99
+ .preview-content dees-preview {
100
+ width: 100%;
101
+ height: 100%;
102
+ }
103
+
104
+ .preview-content.code-editor {
105
+ padding: 0;
106
+ overflow: hidden;
107
+ }
108
+
109
+ .preview-content.code-editor dees-input-code {
110
+ height: 100%;
111
+ }
112
+
113
+ .preview-actions {
114
+ padding: 12px;
115
+ border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#333')};
116
+ display: flex;
117
+ gap: 8px;
118
+ }
119
+
120
+ .action-btn {
121
+ flex: 1;
122
+ padding: 8px 16px;
123
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')};
124
+ border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#404040')};
125
+ color: ${cssManager.bdTheme('#3f3f46', '#e0e0e0')};
126
+ border-radius: 6px;
127
+ cursor: pointer;
128
+ font-size: 13px;
129
+ transition: all 0.15s;
130
+ }
131
+
132
+ .action-btn:hover {
133
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.15)')};
134
+ }
135
+
136
+ .action-btn.danger {
137
+ background: rgba(239, 68, 68, 0.2);
138
+ border-color: #ef4444;
139
+ color: #f87171;
140
+ }
141
+
142
+ .action-btn.danger:hover {
143
+ background: rgba(239, 68, 68, 0.3);
144
+ }
145
+
146
+ .action-btn.primary {
147
+ background: rgba(59, 130, 246, 0.3);
148
+ border-color: #3b82f6;
149
+ color: #60a5fa;
150
+ }
151
+
152
+ .action-btn.primary:hover {
153
+ background: rgba(59, 130, 246, 0.4);
154
+ }
155
+
156
+ .action-btn.primary:disabled {
157
+ opacity: 0.5;
158
+ cursor: not-allowed;
159
+ }
160
+
161
+ .action-btn.secondary {
162
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.05)')};
163
+ border-color: ${cssManager.bdTheme('#d4d4d8', '#555')};
164
+ color: ${cssManager.bdTheme('#71717a', '#aaa')};
165
+ }
166
+
167
+ .action-btn.secondary:hover {
168
+ background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
169
+ color: ${cssManager.bdTheme('#18181b', '#fff')};
170
+ }
171
+
172
+ .unsaved-indicator {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 6px;
176
+ padding: 6px 10px;
177
+ background: rgba(251, 191, 36, 0.1);
178
+ border: 1px solid rgba(251, 191, 36, 0.3);
179
+ border-radius: 4px;
180
+ font-size: 12px;
181
+ color: #fbbf24;
182
+ }
183
+
184
+ .unsaved-dot {
185
+ width: 6px;
186
+ height: 6px;
187
+ border-radius: 50%;
188
+ background: #fbbf24;
189
+ }
190
+
191
+ .empty-state {
192
+ display: flex;
193
+ flex-direction: column;
194
+ align-items: center;
195
+ justify-content: center;
196
+ height: 100%;
197
+ color: ${cssManager.bdTheme('#a1a1aa', '#666')};
198
+ text-align: center;
199
+ padding: 24px;
200
+ }
201
+
202
+ .empty-state svg {
203
+ width: 48px;
204
+ height: 48px;
205
+ margin-bottom: 12px;
206
+ opacity: 0.5;
207
+ }
208
+
209
+ .loading-state {
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ height: 100%;
214
+ color: ${cssManager.bdTheme('#71717a', '#888')};
215
+ }
216
+
217
+ .error-state {
218
+ padding: 16px;
219
+ color: #f87171;
220
+ text-align: center;
221
+ }
222
+ `,
223
+ ];
224
+
225
+ updated(changedProperties: Map<string, unknown>) {
226
+ if (changedProperties.has('objectKey') || changedProperties.has('bucketName')) {
227
+ if (this.objectKey) {
228
+ this.loadObject();
229
+ } else {
230
+ this.content = '';
231
+ this.contentType = '';
232
+ this.error = '';
233
+ this.originalTextContent = '';
234
+ this.hasChanges = false;
235
+ this.editing = false;
236
+ }
237
+ }
238
+ }
239
+
240
+ private async loadObject() {
241
+ if (!this.objectKey || !this.bucketName || !this.dataProvider) return;
242
+
243
+ this.loading = true;
244
+ this.error = '';
245
+ this.hasChanges = false;
246
+ this.editing = false;
247
+
248
+ try {
249
+ const result = await this.dataProvider.getObject(this.bucketName, this.objectKey);
250
+ if (!result) {
251
+ this.error = 'Object not found';
252
+ this.loading = false;
253
+ return;
254
+ }
255
+ this.content = result.content || '';
256
+ this.contentType = result.contentType || '';
257
+ this.fileSize = result.size || 0;
258
+ this.lastModified = result.lastModified || '';
259
+
260
+ if (this.isText()) {
261
+ this.originalTextContent = this.getTextContent();
262
+ }
263
+ } catch (err) {
264
+ console.error('Error loading object:', err);
265
+ this.error = 'Failed to load object';
266
+ }
267
+
268
+ this.loading = false;
269
+ }
270
+
271
+ private formatDate(dateStr: string): string {
272
+ if (!dateStr) return '-';
273
+ const date = new Date(dateStr);
274
+ return date.toLocaleString();
275
+ }
276
+
277
+ private isImage(): boolean {
278
+ return this.contentType.startsWith('image/');
279
+ }
280
+
281
+ private isText(): boolean {
282
+ return (
283
+ this.contentType.startsWith('text/') ||
284
+ this.contentType === 'application/json' ||
285
+ this.contentType === 'application/xml' ||
286
+ this.contentType === 'application/javascript'
287
+ );
288
+ }
289
+
290
+ private getTextContent(): string {
291
+ try {
292
+ const binaryString = atob(this.content);
293
+ const bytes = new Uint8Array(binaryString.length);
294
+ for (let i = 0; i < binaryString.length; i++) {
295
+ bytes[i] = binaryString.charCodeAt(i);
296
+ }
297
+ return new TextDecoder('utf-8').decode(bytes);
298
+ } catch {
299
+ return 'Unable to decode content';
300
+ }
301
+ }
302
+
303
+ private async handleDownload() {
304
+ try {
305
+ const blob = new Blob([Uint8Array.from(atob(this.content), (c) => c.charCodeAt(0))], {
306
+ type: this.contentType,
307
+ });
308
+ const url = URL.createObjectURL(blob);
309
+ const a = document.createElement('a');
310
+ a.href = url;
311
+ a.download = getFileName(this.objectKey);
312
+ document.body.appendChild(a);
313
+ a.click();
314
+ document.body.removeChild(a);
315
+ URL.revokeObjectURL(url);
316
+ } catch (err) {
317
+ console.error('Error downloading:', err);
318
+ }
319
+ }
320
+
321
+ private async handleDelete() {
322
+ if (!this.dataProvider) return;
323
+ if (!confirm(`Delete "${getFileName(this.objectKey)}"?`)) return;
324
+
325
+ try {
326
+ await this.dataProvider.deleteObject(this.bucketName, this.objectKey);
327
+ this.dispatchEvent(
328
+ new CustomEvent('object-deleted', {
329
+ detail: { key: this.objectKey },
330
+ bubbles: true,
331
+ composed: true,
332
+ })
333
+ );
334
+ } catch (err) {
335
+ console.error('Error deleting object:', err);
336
+ }
337
+ }
338
+
339
+ private getLanguage(): string {
340
+ const ext = this.objectKey.split('.').pop()?.toLowerCase() || '';
341
+ const languageMap: Record<string, string> = {
342
+ ts: 'typescript',
343
+ tsx: 'typescript',
344
+ js: 'javascript',
345
+ jsx: 'javascript',
346
+ mjs: 'javascript',
347
+ cjs: 'javascript',
348
+ json: 'json',
349
+ html: 'html',
350
+ htm: 'html',
351
+ css: 'css',
352
+ scss: 'scss',
353
+ sass: 'scss',
354
+ less: 'less',
355
+ md: 'markdown',
356
+ markdown: 'markdown',
357
+ xml: 'xml',
358
+ yaml: 'yaml',
359
+ yml: 'yaml',
360
+ py: 'python',
361
+ rb: 'ruby',
362
+ go: 'go',
363
+ rs: 'rust',
364
+ java: 'java',
365
+ c: 'c',
366
+ cpp: 'cpp',
367
+ h: 'c',
368
+ hpp: 'cpp',
369
+ cs: 'csharp',
370
+ php: 'php',
371
+ sh: 'shell',
372
+ bash: 'shell',
373
+ zsh: 'shell',
374
+ sql: 'sql',
375
+ graphql: 'graphql',
376
+ gql: 'graphql',
377
+ dockerfile: 'dockerfile',
378
+ txt: 'plaintext',
379
+ };
380
+ return languageMap[ext] || 'plaintext';
381
+ }
382
+
383
+ private handleContentChange(event: CustomEvent) {
384
+ const newValue = event.detail as string;
385
+ this.hasChanges = newValue !== this.originalTextContent;
386
+ }
387
+
388
+ private handleEdit() {
389
+ this.editing = true;
390
+ }
391
+
392
+ private handleDiscard() {
393
+ const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
394
+ if (codeEditor) {
395
+ codeEditor.value = this.originalTextContent;
396
+ }
397
+ this.hasChanges = false;
398
+ this.editing = false;
399
+ }
400
+
401
+ private async handleSave() {
402
+ if (!this.hasChanges || this.saving || !this.dataProvider) return;
403
+
404
+ this.saving = true;
405
+
406
+ try {
407
+ const codeEditor = this.shadowRoot?.querySelector('dees-input-code') as any;
408
+ const currentContent = codeEditor?.value ?? '';
409
+
410
+ const encoder = new TextEncoder();
411
+ const bytes = encoder.encode(currentContent);
412
+ const base64Content = btoa(String.fromCharCode(...bytes));
413
+
414
+ const success = await this.dataProvider.putObject(
415
+ this.bucketName,
416
+ this.objectKey,
417
+ base64Content,
418
+ this.contentType
419
+ );
420
+
421
+ if (success) {
422
+ this.originalTextContent = currentContent;
423
+ this.hasChanges = false;
424
+ this.editing = false;
425
+ this.content = base64Content;
426
+ }
427
+ } catch (err) {
428
+ console.error('Error saving object:', err);
429
+ }
430
+
431
+ this.saving = false;
432
+ }
433
+
434
+ render() {
435
+ if (!this.objectKey) {
436
+ return html`
437
+ <div class="preview-container">
438
+ <div class="empty-state">
439
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
440
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
441
+ <polyline points="14 2 14 8 20 8" />
442
+ </svg>
443
+ <p>Select a file to preview</p>
444
+ </div>
445
+ </div>
446
+ `;
447
+ }
448
+
449
+ if (this.loading) {
450
+ return html`
451
+ <div class="preview-container">
452
+ <div class="loading-state">Loading...</div>
453
+ </div>
454
+ `;
455
+ }
456
+
457
+ if (this.error) {
458
+ return html`
459
+ <div class="preview-container">
460
+ <div class="error-state">${this.error}</div>
461
+ </div>
462
+ `;
463
+ }
464
+
465
+ return html`
466
+ <div class="preview-container">
467
+ <div class="preview-header">
468
+ <div class="preview-title">${getFileName(this.objectKey)}</div>
469
+ <div class="preview-meta">
470
+ <span class="meta-item">${this.contentType}</span>
471
+ <span class="meta-item">${formatSize(this.fileSize)}</span>
472
+ <span class="meta-item">${this.formatDate(this.lastModified)}</span>
473
+ ${this.hasChanges ? html`
474
+ <span class="unsaved-indicator">
475
+ <span class="unsaved-dot"></span>
476
+ Unsaved changes
477
+ </span>
478
+ ` : ''}
479
+ </div>
480
+ </div>
481
+
482
+ <div class="preview-content ${this.editing ? 'code-editor' : ''}">
483
+ ${this.editing
484
+ ? html`
485
+ <dees-input-code
486
+ .value=${this.originalTextContent}
487
+ .language=${this.getLanguage()}
488
+ height="100%"
489
+ @content-change=${(e: CustomEvent) => this.handleContentChange(e)}
490
+ ></dees-input-code>
491
+ `
492
+ : this.isText()
493
+ ? html`
494
+ <dees-preview
495
+ .textContent=${this.originalTextContent}
496
+ .filename=${getFileName(this.objectKey)}
497
+ .language=${this.getLanguage()}
498
+ .showToolbar=${true}
499
+ .showFilename=${false}
500
+ ></dees-preview>
501
+ `
502
+ : html`
503
+ <dees-preview
504
+ .base64=${this.content}
505
+ .mimeType=${this.contentType}
506
+ .filename=${getFileName(this.objectKey)}
507
+ .showToolbar=${true}
508
+ .showFilename=${false}
509
+ ></dees-preview>
510
+ `
511
+ }
512
+ </div>
513
+
514
+ <div class="preview-actions">
515
+ ${this.editing
516
+ ? html`
517
+ <button class="action-btn secondary" @click=${this.handleDiscard}>
518
+ ${this.hasChanges ? 'Discard' : 'Cancel'}
519
+ </button>
520
+ <button
521
+ class="action-btn primary"
522
+ @click=${this.handleSave}
523
+ ?disabled=${this.saving || !this.hasChanges}
524
+ >
525
+ ${this.saving ? 'Saving...' : 'Save'}
526
+ </button>
527
+ `
528
+ : html`
529
+ ${this.isText()
530
+ ? html`<button class="action-btn" @click=${this.handleEdit}>Edit</button>`
531
+ : ''}
532
+ <button class="action-btn" @click=${this.handleDownload}>Download</button>
533
+ <button class="action-btn danger" @click=${this.handleDelete}>Delete</button>
534
+ `
535
+ }
536
+ </div>
537
+ </div>
538
+ `;
539
+ }
540
+ }
@@ -0,0 +1,6 @@
1
+ export * from './dees-storage-browser.js';
2
+ export * from './dees-storage-columns.js';
3
+ export * from './dees-storage-keys.js';
4
+ export * from './dees-storage-preview.js';
5
+ export * from './interfaces.js';
6
+ export { formatSize, formatCount, getFileName, validateMove, getParentPrefix, getContentType, getDefaultContent, getPathSegments } from './utilities.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * S3 Data Provider interface - implement this to connect the S3 browser to your backend
3
+ */
4
+
5
+ export interface IS3Object {
6
+ key: string;
7
+ size?: number;
8
+ lastModified?: string;
9
+ isPrefix?: boolean;
10
+ }
11
+
12
+ export interface IS3ChangeEvent {
13
+ type: 'add' | 'modify' | 'delete';
14
+ key: string;
15
+ bucket: string;
16
+ size?: number;
17
+ lastModified?: Date;
18
+ }
19
+
20
+ export interface IS3DataProvider {
21
+ listObjects(bucket: string, prefix?: string, delimiter?: string): Promise<{ objects: IS3Object[]; prefixes: string[] }>;
22
+ getObject(bucket: string, key: string): Promise<{ content: string; contentType: string; size: number; lastModified: string }>;
23
+ putObject(bucket: string, key: string, base64Content: string, contentType: string): Promise<boolean>;
24
+ deleteObject(bucket: string, key: string): Promise<boolean>;
25
+ deletePrefix(bucket: string, prefix: string): Promise<boolean>;
26
+ getObjectUrl(bucket: string, key: string): Promise<string>;
27
+ moveObject(bucket: string, sourceKey: string, destKey: string): Promise<{ success: boolean; error?: string }>;
28
+ movePrefix(bucket: string, sourcePrefix: string, destPrefix: string): Promise<{ success: boolean; movedCount?: number; error?: string }>;
29
+ }
30
+
31
+ export interface IColumn {
32
+ prefix: string;
33
+ objects: IS3Object[];
34
+ prefixes: string[];
35
+ selectedItem: string | null;
36
+ width: number;
37
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared utilities for S3 browser components
3
+ */
4
+
5
+ export interface IMoveValidation {
6
+ valid: boolean;
7
+ error?: string;
8
+ }
9
+
10
+ /**
11
+ * Format a byte size into a human-readable string
12
+ */
13
+ export function formatSize(bytes?: number): string {
14
+ if (bytes === undefined || bytes === null) return '-';
15
+ if (bytes === 0) return '0 B';
16
+
17
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
18
+ let size = bytes;
19
+ let unitIndex = 0;
20
+
21
+ while (size >= 1024 && unitIndex < units.length - 1) {
22
+ size /= 1024;
23
+ unitIndex++;
24
+ }
25
+
26
+ return `${size.toFixed(unitIndex > 0 ? 1 : 0)} ${units[unitIndex]}`;
27
+ }
28
+
29
+ /**
30
+ * Format a count into a compact human-readable string
31
+ */
32
+ export function formatCount(count?: number): string {
33
+ if (count === undefined || count === null) return '';
34
+ if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
35
+ if (count >= 1000) return `${(count / 1000).toFixed(1)}K`;
36
+ return count.toString();
37
+ }
38
+
39
+ /**
40
+ * Extract the file name from a path
41
+ */
42
+ export function getFileName(path: string): string {
43
+ const parts = path.replace(/\/$/, '').split('/');
44
+ return parts[parts.length - 1] || path;
45
+ }
46
+
47
+ /**
48
+ * Validates if a move operation is allowed
49
+ */
50
+ export function validateMove(sourceKey: string, destPrefix: string): IMoveValidation {
51
+ if (sourceKey.endsWith('/')) {
52
+ if (destPrefix.startsWith(sourceKey)) {
53
+ return { valid: false, error: 'Cannot move a folder into itself' };
54
+ }
55
+ }
56
+
57
+ const sourceParent = getParentPrefix(sourceKey);
58
+ if (sourceParent === destPrefix) {
59
+ return { valid: false, error: 'Item is already in this location' };
60
+ }
61
+
62
+ return { valid: true };
63
+ }
64
+
65
+ /**
66
+ * Gets the parent prefix (directory) of a given key
67
+ */
68
+ export function getParentPrefix(key: string): string {
69
+ const trimmed = key.endsWith('/') ? key.slice(0, -1) : key;
70
+ const lastSlash = trimmed.lastIndexOf('/');
71
+ return lastSlash >= 0 ? trimmed.substring(0, lastSlash + 1) : '';
72
+ }
73
+
74
+ /**
75
+ * Get content type from file extension
76
+ */
77
+ export function getContentType(ext: string): string {
78
+ const contentTypes: Record<string, string> = {
79
+ json: 'application/json',
80
+ txt: 'text/plain',
81
+ html: 'text/html',
82
+ css: 'text/css',
83
+ js: 'application/javascript',
84
+ ts: 'text/typescript',
85
+ md: 'text/markdown',
86
+ xml: 'application/xml',
87
+ yaml: 'text/yaml',
88
+ yml: 'text/yaml',
89
+ csv: 'text/csv',
90
+ };
91
+ return contentTypes[ext] || 'application/octet-stream';
92
+ }
93
+
94
+ /**
95
+ * Get default content for a new file based on extension
96
+ */
97
+ export function getDefaultContent(ext: string): string {
98
+ const defaults: Record<string, string> = {
99
+ json: '{\n \n}',
100
+ html: '<!DOCTYPE html>\n<html>\n<head>\n <title></title>\n</head>\n<body>\n \n</body>\n</html>',
101
+ md: '# Title\n\n',
102
+ txt: '',
103
+ };
104
+ return defaults[ext] || '';
105
+ }
106
+
107
+ /**
108
+ * Parse a prefix into cumulative path segments
109
+ */
110
+ export function getPathSegments(prefix: string): string[] {
111
+ if (!prefix) return [];
112
+ const parts = prefix.split('/').filter(p => p);
113
+ const segments: string[] = [];
114
+ let cumulative = '';
115
+ for (const part of parts) {
116
+ cumulative += part + '/';
117
+ segments.push(cumulative);
118
+ }
119
+ return segments;
120
+ }
@@ -3,3 +3,4 @@ export * from './dees-dataview-codebox/index.js';
3
3
  export * from './dees-dataview-statusobject/index.js';
4
4
  export * from './dees-table/index.js';
5
5
  export * from './dees-statsgrid/index.js';
6
+ export * from './dees-storage-browser/index.js';