import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, state, } from '@design.estate/dees-element'; import '@design.estate/dees-catalog'; declare global { interface HTMLElementTagNameMap { 'opencdn-peekpage': OpencdnPeekpage; } } interface IFileNode { name: string; path: string; isFile: boolean; children?: IFileNode[]; } @customElement('opencdn-peekpage') export class OpencdnPeekpage extends DeesElement { @property({ type: String }) public accessor packageName: string = ''; @property({ type: String }) public accessor version: string = ''; @property({ type: Array }) public accessor allowedPackages: string[] = []; @state() private accessor files: string[] = []; @state() private accessor fileTree: IFileNode[] = []; @state() private accessor selectedFile: string | null = null; @state() private accessor fileContent: string = ''; @state() private accessor loading: boolean = false; @state() private accessor searchQuery: string = ''; @state() private accessor availableVersions: string[] = []; @state() private accessor expandedFolders: Set = new Set(); public static styles = [ cssManager.defaultStyles, css` :host { --background: #09090b; --foreground: #fafafa; --muted: #27272a; --muted-foreground: #a1a1aa; --border: #27272a; --primary: #fafafa; --primary-foreground: #18181b; --secondary: #27272a; --ring: #d4d4d8; display: block; height: 100vh; background: var(--background); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; color: var(--foreground); -webkit-font-smoothing: antialiased; } * { box-sizing: border-box; } .app { display: flex; flex-direction: column; height: 100%; } /* Header */ .header { display: flex; align-items: center; gap: 16px; padding: 12px 20px; background: var(--background); border-bottom: 1px solid var(--border); } .header-logo { display: flex; align-items: center; gap: 10px; font-weight: 600; color: var(--foreground); text-decoration: none; cursor: pointer; transition: color 0.15s; } .header-logo:hover { color: var(--muted-foreground); } .header-sep { color: var(--border); } .header-package { font-weight: 600; color: var(--foreground); } .header-version { padding: 4px 10px; background: var(--muted); border-radius: 6px; font-size: 12px; color: var(--muted-foreground); } .header-spacer { flex: 1; } .version-select { padding: 8px 12px; background: var(--muted); border: 1px solid var(--border); border-radius: 6px; color: var(--foreground); font-size: 13px; cursor: pointer; transition: border-color 0.15s; } .version-select:hover, .version-select:focus { border-color: var(--ring); outline: none; } /* Main Content */ .main { display: flex; flex: 1; overflow: hidden; } /* Sidebar */ .sidebar { width: 300px; background: var(--background); border-right: 1px solid var(--border); display: flex; flex-direction: column; } .sidebar-header { padding: 12px 16px; font-size: 12px; font-weight: 500; color: var(--muted-foreground); text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); } .file-tree { flex: 1; overflow-y: auto; padding: 8px 0; } /* Tree Items */ .tree-folder { user-select: none; } .folder-header { display: flex; align-items: center; gap: 8px; padding: 8px 16px; cursor: pointer; font-size: 13px; color: var(--foreground); transition: background 0.15s; } .folder-header:hover { background: var(--muted); } .folder-chevron { margin-left: auto; font-size: 10px; color: var(--muted-foreground); transition: transform 0.15s; } .folder-chevron.open { transform: rotate(90deg); } .folder-children { padding-left: 16px; } .tree-file { display: flex; align-items: center; gap: 8px; padding: 8px 16px; cursor: pointer; font-size: 13px; color: var(--muted-foreground); transition: all 0.15s; } .tree-file:hover { background: var(--muted); color: var(--foreground); } .tree-file.active { background: var(--primary); color: var(--primary-foreground); } /* Content Area */ .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .content-header { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: var(--background); border-bottom: 1px solid var(--border); } .content-path { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--muted-foreground); } .content-path span { color: var(--foreground); } .content-actions { margin-left: auto; display: flex; gap: 8px; } /* Code Viewer */ .code-viewer { flex: 1; overflow: auto; background: var(--background); } .code-viewer pre { margin: 0; padding: 16px 20px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; color: var(--foreground); } /* Empty State */ .empty-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--muted-foreground); gap: 12px; } /* Package List View */ .package-list-container { max-width: 700px; margin: 0 auto; padding: 60px 24px; width: 100%; } .page-title { font-size: 24px; font-weight: 600; margin-bottom: 24px; } .search-box { position: relative; margin-bottom: 20px; } .search-input { width: 100%; padding: 14px 16px 14px 44px; background: var(--muted); border: 1px solid var(--border); border-radius: 8px; color: var(--foreground); font-size: 14px; font-family: inherit; outline: none; transition: border-color 0.15s; } .search-input:focus { border-color: var(--ring); } .search-input::placeholder { color: var(--muted-foreground); } .search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--muted-foreground); } .package-count { color: var(--muted-foreground); font-size: 13px; margin-bottom: 16px; } .package-list { display: flex; flex-direction: column; gap: 8px; } .package-link { display: flex; align-items: center; gap: 12px; padding: 16px 20px; background: var(--muted); border: 1px solid var(--border); border-radius: 8px; color: var(--foreground); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 14px; cursor: pointer; transition: all 0.15s; } .package-link:hover { background: var(--primary); border-color: var(--primary); color: var(--primary-foreground); } .no-results { text-align: center; padding: 40px 20px; color: var(--muted-foreground); } /* Scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--muted); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #3f3f46; } `, ]; async connectedCallback() { super.connectedCallback(); // Parse URL to get package name const path = window.location.pathname; if (path.startsWith('/peek/') && path.length > 6) { const parts = path.slice(6).split('/'); if (parts[0]?.startsWith('@') && parts[1]) { this.packageName = `${parts[0]}/${parts[1]}`; await this.loadPackageFiles(); } } } private async loadPackageFiles() { if (!this.packageName) return; this.loading = true; try { // Fetch file list from API const response = await fetch(`/api/files/${this.packageName}?version=${this.version}`); if (response.ok) { const data = await response.json(); this.files = data.files || []; this.availableVersions = data.versions || []; this.fileTree = this.buildFileTree(this.files); } } catch (err) { console.error('Failed to load package files:', err); } this.loading = false; } private buildFileTree(files: string[]): IFileNode[] { const root: IFileNode[] = []; for (const filePath of files) { const parts = filePath.split('/').filter(Boolean); let currentLevel = root; let currentPath = ''; for (let i = 0; i < parts.length; i++) { const part = parts[i]; currentPath += '/' + part; const isFile = i === parts.length - 1; let existing = currentLevel.find(n => n.name === part); if (!existing) { existing = { name: part, path: currentPath, isFile, children: isFile ? undefined : [], }; currentLevel.push(existing); } if (!isFile && existing.children) { currentLevel = existing.children; } } } // Sort: folders first, then files, alphabetically const sortNodes = (nodes: IFileNode[]) => { nodes.sort((a, b) => { if (a.isFile && !b.isFile) return 1; if (!a.isFile && b.isFile) return -1; return a.name.localeCompare(b.name); }); for (const node of nodes) { if (node.children) sortNodes(node.children); } }; sortNodes(root); return root; } private toggleFolder(path: string) { const newSet = new Set(this.expandedFolders); if (newSet.has(path)) { newSet.delete(path); } else { newSet.add(path); } this.expandedFolders = newSet; } private async selectFile(filePath: string) { this.selectedFile = filePath; this.loading = true; try { const url = `/${this.packageName}${filePath}?version=${this.version}`; const response = await fetch(url); this.fileContent = await response.text(); } catch (err) { this.fileContent = 'Failed to load file'; } this.loading = false; } private navigateToPackage(pkg: string) { window.location.href = `/peek/${pkg}`; } private get filteredPackages(): string[] { if (!this.searchQuery) return this.allowedPackages; const query = this.searchQuery.toLowerCase(); return this.allowedPackages.filter(pkg => pkg.toLowerCase().includes(query)); } private renderTreeNode(node: IFileNode): TemplateResult { if (node.isFile) { return html`
this.selectFile(node.path)} > ${node.name}
`; } const isExpanded = this.expandedFolders.has(node.path); return html`
this.toggleFolder(node.path)}> ${node.name}
${isExpanded && node.children ? html`
${node.children.map(child => this.renderTreeNode(child))}
` : ''}
`; } private renderPackageList(): TemplateResult { const filtered = this.filteredPackages; return html`

Browse Packages

${filtered.length === this.allowedPackages.length ? `${this.allowedPackages.length} package${this.allowedPackages.length !== 1 ? 's' : ''} available` : `${filtered.length} of ${this.allowedPackages.length} packages`}
${filtered.length > 0 ? html`
${filtered.map(pkg => html` `)}
` : html`
No packages found matching your search.
`}
`; } private renderFileBrowser(): TemplateResult { return html`
/ ${this.packageName} ${this.version || 'latest'}
${this.availableVersions.length > 0 ? html` ` : ''} window.location.href = '/peek/'}> All Packages
${this.selectedFile ? html`
${this.selectedFile}
{ const url = `/${this.packageName}${this.selectedFile}?version=${this.version}`; window.open(url, '_blank'); }}> Raw
${this.loading ? html`
` : html`
${this.fileContent}
`}
` : html`
Select a file to view its contents
`}
`; } public render(): TemplateResult { if (this.packageName) { return this.renderFileBrowser(); } return this.renderPackageList(); } }