import { customElement, html, css, cssManager, property, state, DeesElement } from '@design.estate/dees-element'; import { themeDefaultStyles } from '../../00theme.js'; import { demoFunc } from './dees-s3-browser.demo.js'; import type { IS3DataProvider, IS3ChangeEvent } from './interfaces.js'; import './dees-s3-columns.js'; import './dees-s3-keys.js'; import './dees-s3-preview.js'; declare global { interface HTMLElementTagNameMap { 'dees-s3-browser': DeesS3Browser; } } type TViewType = 'columns' | 'keys'; @customElement('dees-s3-browser') export class DeesS3Browser extends DeesElement { public static demo = demoFunc; public static demoGroups = ['Data View']; @property({ type: Object }) public accessor dataProvider: IS3DataProvider | null = null; @property({ type: String }) public accessor bucketName: string = ''; /** * Optional change stream subscription. * Pass a function that takes a callback and returns an unsubscribe function. */ @property({ type: Object }) public accessor onChangeEvent: ((callback: (event: IS3ChangeEvent) => void) => (() => void)) | null = null; @state() private accessor viewType: TViewType = 'columns'; @state() private accessor currentPrefix: string = ''; @state() private accessor selectedKey: string = ''; @state() private accessor refreshKey: number = 0; @state() private accessor previewWidth: number = 700; @state() private accessor isResizingPreview: boolean = false; @state() private accessor recentChangeCount: number = 0; @state() private accessor isStreamConnected: boolean = false; private changeUnsubscribe: (() => void) | null = null; public static styles = [ cssManager.defaultStyles, themeDefaultStyles, css` :host { display: block; height: 100%; } .browser-container { display: flex; flex-direction: column; height: 100%; } .toolbar { display: flex; align-items: center; gap: 12px; padding: 12px; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(0, 0, 0, 0.2)')}; border-radius: 8px; margin-bottom: 16px; } .breadcrumb { display: flex; align-items: center; gap: 4px; flex: 1; font-size: 14px; color: ${cssManager.bdTheme('#71717a', '#999')}; } .breadcrumb-item { cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background 0.15s; } .breadcrumb-item:hover { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')}; color: ${cssManager.bdTheme('#18181b', '#fff')}; } .breadcrumb-separator { color: ${cssManager.bdTheme('#d4d4d8', '#555')}; } .view-toggle { display: flex; gap: 4px; } .view-btn { padding: 6px 12px; background: transparent; border: 1px solid ${cssManager.bdTheme('#d4d4d8', '#444')}; color: ${cssManager.bdTheme('#71717a', '#888')}; border-radius: 4px; cursor: pointer; font-size: 13px; transition: all 0.15s; } .view-btn:hover { border-color: ${cssManager.bdTheme('#a1a1aa', '#666')}; color: ${cssManager.bdTheme('#3f3f46', '#aaa')}; } .view-btn.active { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.1)')}; border-color: ${cssManager.bdTheme('#a1a1aa', '#404040')}; color: ${cssManager.bdTheme('#18181b', '#e0e0e0')}; } .content { flex: 1; display: grid; grid-template-columns: 1fr; gap: 0; overflow: hidden; } .content.has-preview { grid-template-columns: 1fr 4px var(--preview-width, 700px); } .resize-divider { width: 4px; background: transparent; cursor: col-resize; transition: background 0.2s; } .resize-divider:hover, .resize-divider.active { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.2)')}; } .main-view { overflow: auto; background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')}; border-radius: 8px; } .preview-panel { background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(0, 0, 0, 0.2)')}; border-radius: 8px; overflow: hidden; margin-left: 12px; } @media (max-width: 1024px) { .content, .content.has-preview { grid-template-columns: 1fr; } .preview-panel, .resize-divider { display: none; } } .stream-status { display: flex; align-items: center; gap: 4px; font-size: 11px; color: ${cssManager.bdTheme('#71717a', '#888')}; margin-left: auto; margin-right: 12px; } .stream-dot { width: 6px; height: 6px; border-radius: 50%; background: ${cssManager.bdTheme('#a1a1aa', '#888')}; } .stream-dot.connected { background: #22c55e; } .change-indicator { display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: rgba(245, 158, 11, 0.2); border-radius: 4px; font-size: 11px; color: #f59e0b; margin-right: 12px; } .change-indicator.pulse { animation: pulse-orange 1s ease-in-out; } @keyframes pulse-orange { 0% { background: rgba(245, 158, 11, 0.4); } 100% { background: rgba(245, 158, 11, 0.2); } } `, ]; async connectedCallback() { super.connectedCallback(); this.subscribeToChanges(); } async disconnectedCallback() { await super.disconnectedCallback(); this.unsubscribeFromChanges(); } /** * Public method to trigger a refresh of child components */ public refresh() { this.refreshKey++; } private setViewType(type: TViewType) { this.viewType = type; } private navigateToPrefix(prefix: string) { this.currentPrefix = prefix; this.selectedKey = ''; } private handleKeySelected(e: CustomEvent) { this.selectedKey = e.detail.key; } private handleNavigate(e: CustomEvent) { this.navigateToPrefix(e.detail.prefix); } private handleObjectDeleted(e: CustomEvent) { this.selectedKey = ''; this.refreshKey++; } updated(changedProperties: Map) { if (changedProperties.has('bucketName')) { this.selectedKey = ''; this.currentPrefix = ''; this.recentChangeCount = 0; this.unsubscribeFromChanges(); this.subscribeToChanges(); } if (changedProperties.has('onChangeEvent')) { this.unsubscribeFromChanges(); this.subscribeToChanges(); } } private subscribeToChanges() { if (!this.onChangeEvent) { this.isStreamConnected = false; return; } try { this.changeUnsubscribe = this.onChangeEvent((event: IS3ChangeEvent) => { this.handleChange(event); }); this.isStreamConnected = true; } catch (error) { console.warn('[S3Browser] Failed to subscribe to changes:', error); this.isStreamConnected = false; } } private unsubscribeFromChanges() { if (this.changeUnsubscribe) { this.changeUnsubscribe(); this.changeUnsubscribe = null; } this.isStreamConnected = false; } private handleChange(event: IS3ChangeEvent) { this.recentChangeCount++; this.refreshKey++; } private startPreviewResize = (e: MouseEvent) => { e.preventDefault(); this.isResizingPreview = true; document.addEventListener('mousemove', this.handlePreviewResize); document.addEventListener('mouseup', this.endPreviewResize); }; private handlePreviewResize = (e: MouseEvent) => { if (!this.isResizingPreview) return; const contentEl = this.shadowRoot?.querySelector('.content'); if (!contentEl) return; const containerRect = contentEl.getBoundingClientRect(); const newWidth = Math.min(Math.max(containerRect.right - e.clientX, 250), 1000); this.previewWidth = newWidth; }; private endPreviewResize = () => { this.isResizingPreview = false; document.removeEventListener('mousemove', this.handlePreviewResize); document.removeEventListener('mouseup', this.endPreviewResize); }; render() { const breadcrumbParts = this.currentPrefix ? this.currentPrefix.split('/').filter(Boolean) : []; return html`
${this.onChangeEvent ? html`
${this.isStreamConnected ? 'Live' : 'Offline'}
` : ''} ${this.recentChangeCount > 0 ? html`
${this.recentChangeCount} change${this.recentChangeCount > 1 ? 's' : ''}
` : ''}
${this.viewType === 'columns' ? html` ` : html` `}
${this.selectedKey ? html`
` : ''}
`; } }