import { DeesElement, customElement, html, css, cssManager, property, type TemplateResult, } from '@design.estate/dees-element'; import type { ISgPackage, TSgProtocol } from '../interfaces.js'; import './sg-protocol-badge.js'; declare global { interface HTMLElementTagNameMap { 'sg-public-search-view': SgPublicSearchView; } } const ALL_PROTOCOLS: TSgProtocol[] = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems']; @customElement('sg-public-search-view') export class SgPublicSearchView extends DeesElement { public static demo = () => html`
`; public static demoGroups = ['Public']; @property({ type: Array }) public accessor packages: ISgPackage[] = []; @property({ type: Number }) public accessor total: number = 0; @property({ type: String }) public accessor query: string = ''; @property({ type: Array }) public accessor protocols: string[] = []; @property({ type: String }) public accessor activeProtocol: string = ''; @property({ type: Boolean }) public accessor loading: boolean = false; public static styles = [ cssManager.defaultStyles, css` :host { display: block; color: ${cssManager.bdTheme('#111', '#fff')}; } /* Hero section */ .hero { display: flex; flex-direction: column; align-items: center; padding: 80px 24px 64px; text-align: center; } .hero-title { font-size: 40px; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 12px; font-family: 'JetBrains Mono', monospace; color: ${cssManager.bdTheme('#111', '#fff')}; } .hero-subtitle { font-size: 16px; color: ${cssManager.bdTheme('#666', '#999')}; max-width: 600px; line-height: 1.5; margin-bottom: 32px; } .hero-search { display: flex; gap: 0; width: 100%; max-width: 600px; border: 1px solid ${cssManager.bdTheme('#ddd', '#333')}; } .hero-search-input { flex: 1; padding: 14px 18px; background: ${cssManager.bdTheme('#fff', '#0a0a0a')}; border: none; font-size: 16px; color: ${cssManager.bdTheme('#111', '#fff')}; outline: none; font-family: inherit; } .hero-search-input::placeholder { color: ${cssManager.bdTheme('#aaa', '#555')}; } .hero-search-btn { padding: 14px 28px; background: ${cssManager.bdTheme('#111', '#fff')}; border: none; font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#fff', '#111')}; cursor: pointer; transition: opacity 150ms ease; } .hero-search-btn:hover { opacity: 0.85; } .protocol-row { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-top: 24px; } /* Compact search bar (shown in results mode) */ .compact-search { max-width: 960px; margin: 0 auto; padding: 24px 24px 0; display: flex; gap: 0; } .compact-search-input { flex: 1; padding: 10px 14px; background: ${cssManager.bdTheme('#fff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#ddd', '#333')}; border-right: none; font-size: 14px; color: ${cssManager.bdTheme('#111', '#fff')}; outline: none; font-family: inherit; } .compact-search-input::placeholder { color: ${cssManager.bdTheme('#aaa', '#555')}; } .compact-search-input:focus { border-color: ${cssManager.bdTheme('#111', '#fff')}; } .compact-search-btn { padding: 10px 20px; background: ${cssManager.bdTheme('#111', '#fff')}; border: none; font-size: 13px; font-weight: 600; color: ${cssManager.bdTheme('#fff', '#111')}; cursor: pointer; transition: opacity 150ms ease; } .compact-search-btn:hover { opacity: 0.85; } /* Results section */ .results { max-width: 960px; margin: 0 auto; padding: 0 24px 48px; } .results-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .results-count { font-size: 13px; color: ${cssManager.bdTheme('#888', '#777')}; } /* Protocol filter tabs */ .filters { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 20px; } .filter-btn { padding: 6px 12px; background: transparent; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; font-size: 12px; font-weight: 500; color: ${cssManager.bdTheme('#666', '#999')}; cursor: pointer; transition: all 150ms ease; text-transform: uppercase; letter-spacing: 0.04em; } .filter-btn:hover { border-color: ${cssManager.bdTheme('#999', '#666')}; color: ${cssManager.bdTheme('#111', '#fff')}; } .filter-btn.active { background: ${cssManager.bdTheme('#111', '#fff')}; color: ${cssManager.bdTheme('#fff', '#111')}; border-color: ${cssManager.bdTheme('#111', '#fff')}; } /* Package cards */ .package-grid { display: flex; flex-direction: column; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .package-card { display: flex; align-items: center; justify-content: space-between; padding: 16px 18px; background: ${cssManager.bdTheme('#fff', '#111')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; cursor: pointer; transition: background 100ms ease; } .package-card:last-child { border-bottom: none; } .package-card:hover { background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')}; } .package-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; } .package-name-row { display: flex; align-items: center; gap: 8px; } .package-name { font-size: 14px; font-weight: 600; font-family: 'JetBrains Mono', monospace; color: ${cssManager.bdTheme('#111', '#fff')}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .package-description { font-size: 13px; color: ${cssManager.bdTheme('#666', '#aaa')}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .package-meta { display: flex; align-items: center; gap: 4px; margin-top: 2px; } .meta-item { font-size: 11px; color: ${cssManager.bdTheme('#aaa', '#666')}; } .meta-separator { font-size: 11px; color: ${cssManager.bdTheme('#ddd', '#444')}; } .package-right { display: flex; align-items: center; gap: 16px; flex-shrink: 0; margin-left: 16px; } .version-tag { font-size: 12px; font-family: 'JetBrains Mono', monospace; color: ${cssManager.bdTheme('#666', '#999')}; background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')}; padding: 2px 8px; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .download-count { font-size: 12px; color: ${cssManager.bdTheme('#888', '#777')}; font-family: 'JetBrains Mono', monospace; white-space: nowrap; } /* Pagination */ .pagination { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; } .pagination-info { font-size: 13px; color: ${cssManager.bdTheme('#888', '#777')}; } .pagination-buttons { display: flex; gap: 4px; } .page-btn { padding: 6px 12px; background: transparent; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; font-size: 13px; color: ${cssManager.bdTheme('#666', '#999')}; cursor: pointer; transition: all 150ms ease; } .page-btn:hover { background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')}; color: ${cssManager.bdTheme('#111', '#fff')}; } .page-btn:disabled { opacity: 0.4; cursor: not-allowed; } /* Loading state */ .loading-overlay { display: flex; align-items: center; justify-content: center; padding: 64px 24px; font-size: 14px; color: ${cssManager.bdTheme('#888', '#777')}; } .spinner { width: 20px; height: 20px; border: 2px solid transparent; border-top-color: ${cssManager.bdTheme('#111', '#fff')}; border-radius: 50%; animation: spin 0.7s linear infinite; margin-right: 12px; } @keyframes spin { to { transform: rotate(360deg); } } /* Empty state */ .empty-state { text-align: center; padding: 48px 32px; font-size: 14px; color: ${cssManager.bdTheme('#888', '#777')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; background: ${cssManager.bdTheme('#fff', '#111')}; } /* Hero empty placeholder */ .hero-placeholder { margin-top: 32px; padding: 32px; border: 1px dashed ${cssManager.bdTheme('#ddd', '#333')}; text-align: center; } .hero-placeholder-icon { font-size: 48px; margin-bottom: 12px; opacity: 0.4; } .hero-placeholder-title { font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#666', '#999')}; margin-bottom: 8px; } .hero-placeholder-text { font-size: 13px; color: ${cssManager.bdTheme('#999', '#666')}; line-height: 1.5; max-width: 400px; margin: 0 auto; } `, ]; public render(): TemplateResult { const showHero = this.packages.length === 0 && !this.query && !this.loading; const showResults = this.packages.length > 0 || this.query || this.loading; const availableProtocols = this.protocols.length > 0 ? this.protocols : ALL_PROTOCOLS; return html` ${showHero ? this.renderHero() : ''} ${showResults ? this.renderResults(availableProtocols) : ''} `; } private renderHero(): TemplateResult { return html`
Stack.Gallery Registry
Browse and discover packages across npm, OCI, Maven, Cargo, PyPI, Composer, and RubyGems
${ALL_PROTOCOLS.map( (proto) => html`` )}
${this.total === 0 ? html`
\u{1F4E6}
No public packages yet
This registry is ready to host packages. Sign in to create an organization and start publishing.
` : ''}
`; } private renderCompactSearch(): TemplateResult { return html` `; } private renderResults(availableProtocols: string[]): TemplateResult { return html` ${this.renderCompactSearch()}
${availableProtocols.map( (proto) => html` ` )}
${this.total} package${this.total !== 1 ? 's' : ''} found${this.query ? ` for "${this.query}"` : ''}
${this.loading ? html`
Searching packages...
` : this.packages.length > 0 ? html`
${this.packages.map( (pkg) => html`
this.handleSelect(pkg.id)}>
${pkg.name}
${pkg.description ? html`
${pkg.description}
` : ''}
${this.formatNumber(pkg.downloadCount)} downloads · ${this.formatDate(pkg.updatedAt)} · ${pkg.organizationId}
${pkg.latestVersion ? html`${pkg.latestVersion}` : ''} ${this.formatNumber(pkg.downloadCount)}
` )}
` : html`
No packages found${this.query ? ` for "${this.query}"` : ''}
` }
`; } private formatNumber(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return n.toString(); } private formatDate(dateStr: string): string { if (!dateStr) return ''; try { return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } catch { return dateStr; } } private handleSearch() { this.dispatchEvent( new CustomEvent('search', { detail: { query: this.query }, bubbles: true, composed: true, }) ); } private handleFilter(protocol: string) { this.activeProtocol = protocol; this.requestUpdate(); this.dispatchEvent( new CustomEvent('filter', { detail: { protocol }, bubbles: true, composed: true, }) ); } private handleSelect(packageId: string) { this.dispatchEvent( new CustomEvent('select', { detail: { packageId }, bubbles: true, composed: true, }) ); } private handlePage(direction: number) { const currentOffset = this.packages.length; const offset = direction > 0 ? currentOffset : Math.max(0, currentOffset - this.packages.length * 2); this.dispatchEvent( new CustomEvent('page', { detail: { offset }, bubbles: true, composed: true, }) ); } }