From 52c9c7251e63abc1211fb226cdf7b0746a27042d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 18 Mar 2026 02:34:14 +0000 Subject: [PATCH] feat(elements): add app store view component for browsing and deploying app templates --- changelog.md | 6 + ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/index.ts | 1 + ts_web/elements/sz-app-store-view.ts | 568 +++++++++++++++++++++++++++ 4 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 ts_web/elements/sz-app-store-view.ts diff --git a/changelog.md b/changelog.md index 5a9f26b..b9c11ea 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2026-03-18 - 2.8.0 - feat(elements) +add app store view component for browsing and deploying app templates + +- introduces a new sz-app-store-view element with app card rendering, search/filter empty states, and deploy action events +- exports the new app store view from the elements index for public consumption + ## 2026-03-17 - 2.7.0 - feat(sz-service-detail-view) replace the custom logs panel with dees-chart-log in the service detail view diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index da8dc5a..c102b8d 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/catalog', - version: '2.7.0', + version: '2.8.0', description: 'UI component catalog for serve.zone' } diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index b373504..911663b 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -28,6 +28,7 @@ export * from './sz-registry-external-view.js'; export * from './sz-services-list-view.js'; export * from './sz-services-backups-view.js'; export * from './sz-service-detail-view.js'; +export * from './sz-app-store-view.js'; // Tokens View export * from './sz-tokens-view.js'; diff --git a/ts_web/elements/sz-app-store-view.ts b/ts_web/elements/sz-app-store-view.ts new file mode 100644 index 0000000..345622e --- /dev/null +++ b/ts_web/elements/sz-app-store-view.ts @@ -0,0 +1,568 @@ +import { + DeesElement, + customElement, + html, + css, + cssManager, + property, + state, + type TemplateResult, +} from '@design.estate/dees-element'; + +declare global { + interface HTMLElementTagNameMap { + 'sz-app-store-view': SzAppStoreView; + } +} + +export interface IAppTemplate { + id: string; + name: string; + description: string; + category: string; + iconUrl?: string; + iconName?: string; + image: string; + port: number; + envVars?: Array<{ key: string; value: string; description: string; required?: boolean }>; + volumes?: string[]; + enableMongoDB?: boolean; + enableS3?: boolean; + enableClickHouse?: boolean; +} + +@customElement('sz-app-store-view') +export class SzAppStoreView extends DeesElement { + public static demo = () => html` +
+ +
+ `; + + public static demoGroups = ['Services']; + + @property({ type: Array }) + public accessor apps: IAppTemplate[] = []; + + @state() + private accessor selectedCategory: string = 'All'; + + @state() + private accessor searchQuery: string = ''; + + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + } + + .header { + margin-bottom: 24px; + } + + .header-title { + font-size: 20px; + font-weight: 600; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + .header-subtitle { + font-size: 14px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + margin-top: 4px; + } + + .filter-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-bottom: 24px; + } + + .category-pills { + display: flex; + flex-wrap: wrap; + gap: 8px; + flex: 1; + min-width: 0; + } + + .category-pill { + padding: 6px 14px; + border-radius: 9999px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 200ms ease; + border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + background: ${cssManager.bdTheme('#ffffff', '#09090b')}; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + white-space: nowrap; + } + + .category-pill:hover { + border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + .category-pill.active { + background: ${cssManager.bdTheme('#18181b', '#fafafa')}; + color: ${cssManager.bdTheme('#fafafa', '#18181b')}; + border-color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + .search-input { + width: 240px; + padding: 8px 12px 8px 36px; + background: ${cssManager.bdTheme('#ffffff', '#18181b')}; + border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + border-radius: 6px; + font-size: 14px; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + outline: none; + transition: border-color 200ms ease; + box-sizing: border-box; + } + + .search-input:focus { + border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; + } + + .search-input::placeholder { + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + } + + .search-wrapper { + position: relative; + flex-shrink: 0; + } + + .search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + pointer-events: none; + } + + .app-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + } + + @media (max-width: 1024px) { + .app-grid { + grid-template-columns: repeat(2, 1fr); + } + } + + @media (max-width: 640px) { + .app-grid { + grid-template-columns: 1fr; + } + + .filter-bar { + flex-direction: column; + align-items: stretch; + } + + .search-input { + width: 100%; + } + } + + .app-card { + background: ${cssManager.bdTheme('#ffffff', '#09090b')}; + border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + border-radius: 8px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; + transition: all 200ms ease; + cursor: default; + } + + .app-card:hover { + border-color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; + box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(0,0,0,0.2)')}; + transform: translateY(-1px); + } + + .card-top { + display: flex; + align-items: flex-start; + gap: 14px; + } + + .app-icon { + width: 44px; + height: 44px; + border-radius: 10px; + background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: hidden; + } + + .app-icon svg { + width: 22px; + height: 22px; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + .app-icon img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .app-icon .letter-fallback { + font-size: 20px; + font-weight: 600; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + user-select: none; + } + + .card-info { + flex: 1; + min-width: 0; + } + + .app-name { + font-size: 15px; + font-weight: 600; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + line-height: 1.3; + } + + .category-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 9999px; + font-size: 11px; + font-weight: 500; + margin-top: 4px; + background: ${cssManager.bdTheme('#eff6ff', 'rgba(59, 130, 246, 0.15)')}; + color: ${cssManager.bdTheme('#2563eb', '#60a5fa')}; + } + + .app-description { + font-size: 13px; + line-height: 1.5; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + } + + .card-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 12px; + border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')}; + margin-top: auto; + } + + .image-tag { + font-family: monospace; + font-size: 12px; + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; + } + + .deploy-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 16px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 200ms ease; + border: none; + background: ${cssManager.bdTheme('#18181b', '#fafafa')}; + color: ${cssManager.bdTheme('#fafafa', '#18181b')}; + white-space: nowrap; + flex-shrink: 0; + } + + .deploy-button:hover { + opacity: 0.85; + } + + .deploy-button svg { + width: 14px; + height: 14px; + } + + .empty-state { + grid-column: 1 / -1; + padding: 64px 24px; + text-align: center; + } + + .empty-icon { + width: 48px; + height: 48px; + color: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')}; + margin: 0 auto 16px; + } + + .empty-title { + font-size: 16px; + font-weight: 600; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + margin-bottom: 4px; + } + + .empty-description { + font-size: 14px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + } + `, + ]; + + private get categories(): string[] { + const cats = new Set(this.apps.map((app) => app.category)); + return ['All', ...Array.from(cats).sort()]; + } + + private get filteredApps(): IAppTemplate[] { + let result = this.apps; + + if (this.selectedCategory !== 'All') { + result = result.filter((app) => app.category === this.selectedCategory); + } + + if (this.searchQuery.trim()) { + const query = this.searchQuery.trim().toLowerCase(); + result = result.filter( + (app) => + app.name.toLowerCase().includes(query) || + app.description.toLowerCase().includes(query) || + app.category.toLowerCase().includes(query) + ); + } + + return result; + } + + public render(): TemplateResult { + const filtered = this.filteredApps; + + return html` +
+
App Store
+
Deploy popular applications with one click
+
+ +
+
+ ${this.categories.map( + (cat) => html` + + ` + )} +
+
+ + + + + { this.searchQuery = (e.target as HTMLInputElement).value; }} + > +
+
+ +
+ ${filtered.length > 0 + ? filtered.map((app) => this.renderAppCard(app)) + : html` +
+ + + + + +
No apps found
+
+ Try adjusting your search or filter to find what you're looking for. +
+
+ `} +
+ `; + } + + private renderAppCard(app: IAppTemplate): TemplateResult { + return html` +
+
+
+ ${app.iconUrl + ? html`${app.name}` + : app.iconName + ? this.renderIconByName(app.iconName) + : html`${app.name.charAt(0).toUpperCase()}`} +
+
+
${app.name}
+
${app.category}
+
+
+
${app.description}
+ +
+ `; + } + + private renderIconByName(name: string): TemplateResult { + const icons: Record = { + 'file-text': html``, + 'git-branch': html``, + 'book-open': html``, + 'globe': html``, + 'database': html``, + 'server': html``, + 'package': html``, + 'mail': html``, + 'shield': html``, + 'monitor': html``, + }; + + return icons[name] || html`?`; + } + + private handleDeploy(app: IAppTemplate) { + this.dispatchEvent( + new CustomEvent('deploy-app', { + detail: { app }, + bubbles: true, + composed: true, + }) + ); + } +}