diff --git a/changelog.md b/changelog.md index 556c411..4c583b8 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-02-23 - 2.4.0 - feat(elements) +add configuration overview and section components with demo view and index exports + +- Adds new sz-config-section component (IConfigField interface, rich renderers for boolean, pills, badge, code, link and 'Not configured' handling). +- Adds new sz-config-overview wrapper component with heading/info banner and slot styling. +- Adds demo view sz-demo-view-config that supplies example configuration groups and fields for System, Proxy, Email, DNS, TLS, Cache, RADIUS and Remote Ingress. +- Exports new components from ts_web/elements/index.ts so they are available to the element registry. + ## 2026-02-22 - 2.3.0 - feat(routes) add route UI components and demo view with list/card and app-shell integration diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 44fe2ea..2914cfd 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.3.0', + version: '2.4.0', description: 'UI component catalog for serve.zone' } diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index a4bbd85..b373504 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -53,6 +53,10 @@ export * from './sz-mta-detail-view.js'; export * from './sz-route-card.js'; export * from './sz-route-list-view.js'; +// Config Views +export * from './sz-config-section.js'; +export * from './sz-config-overview.js'; + // Demo Views export * from './sz-demo-view-dashboard.js'; export * from './sz-demo-view-services.js'; @@ -62,3 +66,4 @@ export * from './sz-demo-view-tokens.js'; export * from './sz-demo-view-settings.js'; export * from './sz-demo-view-mta.js'; export * from './sz-demo-view-routes.js'; +export * from './sz-demo-view-config.js'; diff --git a/ts_web/elements/sz-config-overview.ts b/ts_web/elements/sz-config-overview.ts new file mode 100644 index 0000000..c30a4ee --- /dev/null +++ b/ts_web/elements/sz-config-overview.ts @@ -0,0 +1,92 @@ +import { + DeesElement, + customElement, + html, + css, + cssManager, + property, + type TemplateResult, +} from '@design.estate/dees-element'; + +declare global { + interface HTMLElementTagNameMap { + 'sz-config-overview': SzConfigOverview; + } +} + +@customElement('sz-config-overview') +export class SzConfigOverview extends DeesElement { + public static demo = () => html` +
+ Place <sz-config-section> elements here +
+
`; + + public static demoGroups = ['Configuration']; + + @property({ type: String }) + public accessor heading: string = ''; + + @property({ type: String }) + public accessor infoText: string = ''; + + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + } + + .heading { + font-size: 20px; + font-weight: 600; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + margin-bottom: 16px; + } + + .info-banner { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 14px 18px; + margin-bottom: 20px; + border-radius: 8px; + background: ${cssManager.bdTheme('#eff6ff', 'rgba(59,130,246,0.08)')}; + border: 1px solid ${cssManager.bdTheme('#bfdbfe', 'rgba(59,130,246,0.2)')}; + color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; + font-size: 13px; + line-height: 1.5; + } + + .info-banner dees-icon { + flex-shrink: 0; + font-size: 18px; + margin-top: 1px; + } + + ::slotted(sz-config-section) { + margin-bottom: 12px; + } + + ::slotted(sz-config-section:last-child) { + margin-bottom: 0; + } + `, + ]; + + public render(): TemplateResult { + return html` + ${this.heading ? html`
${this.heading}
` : ''} + ${this.infoText ? html` +
+ + ${this.infoText} +
+ ` : ''} + + `; + } +} diff --git a/ts_web/elements/sz-config-section.ts b/ts_web/elements/sz-config-section.ts new file mode 100644 index 0000000..9f4e6c2 --- /dev/null +++ b/ts_web/elements/sz-config-section.ts @@ -0,0 +1,531 @@ +import { + DeesElement, + customElement, + html, + css, + cssManager, + property, + state, + type TemplateResult, +} from '@design.estate/dees-element'; + +export interface IConfigField { + key: string; + value: string | number | boolean | string[] | null; + type?: 'text' | 'boolean' | 'badge' | 'pills' | 'code' | 'link'; + description?: string; + linkTo?: string; +} + +declare global { + interface HTMLElementTagNameMap { + 'sz-config-section': SzConfigSection; + } +} + +@customElement('sz-config-section') +export class SzConfigSection extends DeesElement { + public static demo = () => html` + + + + `; + + public static demoGroups = ['Configuration']; + + @property({ type: String }) + public accessor title: string = ''; + + @property({ type: String }) + public accessor subtitle: string = ''; + + @property({ type: String }) + public accessor icon: string = ''; + + @property({ type: String }) + public accessor status: 'enabled' | 'disabled' | 'not-configured' | 'warning' = 'enabled'; + + @property({ type: Array }) + public accessor fields: IConfigField[] = []; + + @property({ type: Boolean }) + public accessor collapsible: boolean = false; + + @property({ type: Boolean }) + public accessor collapsed: boolean = false; + + @state() + accessor isCollapsed: boolean = false; + + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + margin-bottom: 16px; + } + + .section { + background: ${cssManager.bdTheme('#ffffff', '#09090b')}; + border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + border-radius: 8px; + overflow: hidden; + } + + .section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; + border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + cursor: default; + user-select: none; + } + + :host([collapsible]) .section-header { + cursor: pointer; + } + + :host([collapsible]) .section-header:hover { + background: ${cssManager.bdTheme('#ebebed', '#1c1c1f')}; + } + + .header-left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + } + + .header-icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: ${cssManager.bdTheme('#e4e4e7', '#27272a')}; + border-radius: 8px; + flex-shrink: 0; + } + + .header-icon dees-icon { + font-size: 18px; + color: ${cssManager.bdTheme('#52525b', '#a1a1aa')}; + } + + .header-text { + min-width: 0; + } + + .header-title { + font-size: 15px; + font-weight: 600; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + line-height: 1.3; + } + + .header-subtitle { + font-size: 12px; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + line-height: 1.3; + margin-top: 1px; + } + + .header-right { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + } + + /* Status badge */ + .status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border-radius: 9999px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + } + + .status-badge.enabled { + background: ${cssManager.bdTheme('#dcfce7', 'rgba(34,197,94,0.2)')}; + color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; + } + + .status-badge.disabled { + background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.2)')}; + color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; + } + + .status-badge.not-configured { + background: ${cssManager.bdTheme('#f4f4f5', 'rgba(113,113,122,0.2)')}; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + } + + .status-badge.warning { + background: ${cssManager.bdTheme('#fef3c7', 'rgba(245,158,11,0.15)')}; + color: ${cssManager.bdTheme('#92400e', '#fbbf24')}; + } + + .status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + } + + .status-badge.enabled .status-dot { + background: ${cssManager.bdTheme('#16a34a', '#22c55e')}; + } + + .status-badge.disabled .status-dot { + background: ${cssManager.bdTheme('#dc2626', '#ef4444')}; + } + + .status-badge.not-configured .status-dot { + background: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + } + + .status-badge.warning .status-dot { + background: ${cssManager.bdTheme('#f59e0b', '#fbbf24')}; + } + + /* Chevron */ + .chevron { + display: flex; + align-items: center; + transition: transform 200ms ease; + } + + .chevron.collapsed { + transform: rotate(-90deg); + } + + .chevron dees-icon { + font-size: 16px; + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + } + + /* Content */ + .section-content { + padding: 0; + } + + .section-content.collapsed { + display: none; + } + + /* Field rows */ + .field-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 10px 20px; + border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')}; + gap: 16px; + } + + .field-row:last-child { + border-bottom: none; + } + + .field-key { + font-size: 13px; + font-weight: 500; + color: ${cssManager.bdTheme('#71717a', '#a1a1aa')}; + flex-shrink: 0; + min-width: 140px; + padding-top: 1px; + } + + .field-value { + font-size: 13px; + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + text-align: right; + word-break: break-all; + } + + .field-value.null-value { + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + font-style: italic; + font-family: inherit; + } + + /* Boolean display */ + .bool-value { + display: inline-flex; + align-items: center; + gap: 5px; + font-family: inherit; + font-size: 13px; + font-weight: 500; + } + + .bool-value.true { + color: ${cssManager.bdTheme('#16a34a', '#22c55e')}; + } + + .bool-value.false { + color: ${cssManager.bdTheme('#dc2626', '#ef4444')}; + } + + .bool-dot { + width: 6px; + height: 6px; + border-radius: 50%; + } + + .bool-value.true .bool-dot { + background: ${cssManager.bdTheme('#16a34a', '#22c55e')}; + } + + .bool-value.false .bool-dot { + background: ${cssManager.bdTheme('#dc2626', '#ef4444')}; + } + + /* Pills */ + .pills { + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: flex-end; + } + + .pill { + display: inline-flex; + align-items: center; + padding: 2px 9px; + border-radius: 9999px; + font-size: 12px; + font-weight: 500; + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + background: ${cssManager.bdTheme('#eff6ff', 'rgba(59,130,246,0.1)')}; + color: ${cssManager.bdTheme('#2563eb', '#60a5fa')}; + } + + /* Code value */ + .code-value { + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + font-size: 12px; + background: ${cssManager.bdTheme('#f4f4f5', '#18181b')}; + padding: 2px 8px; + border-radius: 4px; + color: ${cssManager.bdTheme('#18181b', '#fafafa')}; + } + + /* Link value */ + .link-value { + color: ${cssManager.bdTheme('#2563eb', '#60a5fa')}; + cursor: pointer; + text-decoration: none; + font-family: inherit; + font-size: 13px; + } + + .link-value:hover { + text-decoration: underline; + } + + /* Description hint */ + .field-description { + font-size: 11px; + color: ${cssManager.bdTheme('#a1a1aa', '#52525b')}; + margin-top: 3px; + text-align: right; + } + + /* Slot for custom content */ + .slot-content { + border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')}; + } + + .slot-content:empty { + display: none; + border-top: none; + } + + /* Badge type */ + .badge-value { + display: inline-flex; + align-items: center; + padding: 2px 9px; + border-radius: 9999px; + font-size: 12px; + font-weight: 500; + background: ${cssManager.bdTheme('#f4f4f5', '#27272a')}; + color: ${cssManager.bdTheme('#52525b', '#a1a1aa')}; + } + `, + ]; + + async connectedCallback() { + await super.connectedCallback(); + this.isCollapsed = this.collapsed; + if (this.collapsible) { + this.setAttribute('collapsible', ''); + } + } + + public render(): TemplateResult { + const statusLabels: Record = { + 'enabled': 'Enabled', + 'disabled': 'Disabled', + 'not-configured': 'Not Configured', + 'warning': 'Warning', + }; + + return html` +
+
{ + if (this.collapsible) { + this.isCollapsed = !this.isCollapsed; + } + }} + > +
+ ${this.icon ? html` +
+ +
+ ` : ''} +
+
${this.title}
+ ${this.subtitle ? html`
${this.subtitle}
` : ''} +
+
+
+ ${this.status ? html` + + + ${statusLabels[this.status] || this.status} + + ` : ''} + ${this.collapsible ? html` + + + + ` : ''} +
+
+
+ ${this.fields.map(field => this.renderField(field))} +
+ +
+
+
+ `; + } + + private renderField(field: IConfigField): TemplateResult { + return html` +
+
${field.key}
+
+ ${this.renderFieldValue(field)} + ${field.description ? html`
${field.description}
` : ''} +
+
+ `; + } + + private renderFieldValue(field: IConfigField): TemplateResult { + const value = field.value; + const type = field.type || this.inferType(value); + + // Null / undefined + if (value === null || value === undefined) { + return html`Not configured`; + } + + switch (type) { + case 'boolean': + return html` + + + ${value ? 'Enabled' : 'Disabled'} + + `; + + case 'pills': + if (Array.isArray(value) && value.length === 0) { + return html`None`; + } + return html` +
+ ${(value as string[]).map(v => html`${v}`)} +
+ `; + + case 'code': + return html`${String(value)}`; + + case 'badge': + return html`${String(value)}`; + + case 'link': + return html` + { + if (field.linkTo) { + this.dispatchEvent(new CustomEvent('navigate', { + detail: { target: field.linkTo }, + bubbles: true, + composed: true, + })); + } + }} + >${String(value)} + `; + + default: + return html`${String(value)}`; + } + } + + private inferType(value: unknown): string { + if (typeof value === 'boolean') return 'boolean'; + if (Array.isArray(value)) return 'pills'; + return 'text'; + } +} diff --git a/ts_web/elements/sz-demo-view-config.ts b/ts_web/elements/sz-demo-view-config.ts new file mode 100644 index 0000000..b327f9d --- /dev/null +++ b/ts_web/elements/sz-demo-view-config.ts @@ -0,0 +1,164 @@ +import { + DeesElement, + customElement, + html, + css, + cssManager, + type TemplateResult, +} from '@design.estate/dees-element'; +import type { IConfigField } from './sz-config-section.js'; +import './index.js'; + +declare global { + interface HTMLElementTagNameMap { + 'sz-demo-view-config': SzDemoViewConfig; + } +} + +@customElement('sz-demo-view-config') +export class SzDemoViewConfig extends DeesElement { + public static styles = [ + cssManager.defaultStyles, + css` + :host { + display: block; + padding: 24px; + height: 100%; + overflow-y: auto; + box-sizing: border-box; + } + `, + ]; + + public render(): TemplateResult { + const systemFields: IConfigField[] = [ + { key: 'Base Directory', value: '/home/user/.serve.zone/dcrouter' }, + { key: 'Data Directory', value: '/home/user/.serve.zone/dcrouter/data' }, + { key: 'Public IP', value: '203.0.113.50' }, + { key: 'Proxy IPs', value: ['203.0.113.10', '203.0.113.11'], type: 'pills' }, + { key: 'Uptime', value: '3d 14h 22m' }, + { key: 'Storage Backend', value: 'filesystem', type: 'badge' }, + ]; + + const proxyFields: IConfigField[] = [ + { key: 'Route Count', value: 12 }, + { key: 'ACME Enabled', value: true, type: 'boolean' }, + { key: 'Account Email', value: 'admin@serve.zone' }, + { key: 'Use Production', value: true, type: 'boolean' }, + { key: 'Auto Renew', value: true, type: 'boolean' }, + { key: 'Renew Threshold', value: '30 days' }, + ]; + + const emailFields: IConfigField[] = [ + { key: 'Ports', value: ['25', '465', '587'], type: 'pills' }, + { key: 'Hostname', value: 'mail.serve.zone' }, + { key: 'Domains', value: ['serve.zone', 'mail.serve.zone'], type: 'pills' }, + { key: 'Email Routes', value: 5 }, + { key: 'Received Path', value: '/data/emails' }, + ]; + + const dnsFields: IConfigField[] = [ + { key: 'Port', value: 53 }, + { key: 'NS Domains', value: ['ns1.serve.zone', 'ns2.serve.zone'], type: 'pills' }, + { key: 'Scopes', value: ['serve.zone', 'example.com'], type: 'pills' }, + { key: 'Record Count', value: 24 }, + { key: 'DNS Challenge', value: true, type: 'boolean' }, + ]; + + const tlsFields: IConfigField[] = [ + { key: 'Contact Email', value: 'admin@serve.zone' }, + { key: 'Domain', value: 'serve.zone' }, + { key: 'Source', value: 'acme', type: 'badge' }, + { key: 'Certificate Path', value: null }, + { key: 'Key Path', value: null }, + ]; + + const cacheFields: IConfigField[] = [ + { key: 'Storage Path', value: '/home/user/.serve.zone/dcrouter/tsmdb' }, + { key: 'DB Name', value: 'dcrouter' }, + { key: 'Default TTL', value: '30 days' }, + { key: 'Cleanup Interval', value: '1 hour' }, + ]; + + const radiusFields: IConfigField[] = [ + { key: 'Auth Port', value: null }, + { key: 'Accounting Port', value: null }, + ]; + + const remoteIngressFields: IConfigField[] = [ + { key: 'Tunnel Port', value: 8443 }, + { key: 'Hub Domain', value: 'hub.serve.zone' }, + { key: 'TLS Configured', value: true, type: 'boolean' }, + ]; + + return html` + + + + + + + + + + + + + + + + + + `; + } +}