@serve.zone/catalog 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/catalog",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "private": false,
5
5
  "description": "UI component catalog for serve.zone",
6
6
  "main": "dist_ts_web/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/catalog',
6
- version: '2.3.0',
6
+ version: '2.5.0',
7
7
  description: 'UI component catalog for serve.zone'
8
8
  }
@@ -53,6 +53,10 @@ export * from './sz-mta-detail-view.js';
53
53
  export * from './sz-route-card.js';
54
54
  export * from './sz-route-list-view.js';
55
55
 
56
+ // Config Views
57
+ export * from './sz-config-section.js';
58
+ export * from './sz-config-overview.js';
59
+
56
60
  // Demo Views
57
61
  export * from './sz-demo-view-dashboard.js';
58
62
  export * from './sz-demo-view-services.js';
@@ -62,3 +66,4 @@ export * from './sz-demo-view-tokens.js';
62
66
  export * from './sz-demo-view-settings.js';
63
67
  export * from './sz-demo-view-mta.js';
64
68
  export * from './sz-demo-view-routes.js';
69
+ export * from './sz-demo-view-config.js';
@@ -0,0 +1,92 @@
1
+ import {
2
+ DeesElement,
3
+ customElement,
4
+ html,
5
+ css,
6
+ cssManager,
7
+ property,
8
+ type TemplateResult,
9
+ } from '@design.estate/dees-element';
10
+
11
+ declare global {
12
+ interface HTMLElementTagNameMap {
13
+ 'sz-config-overview': SzConfigOverview;
14
+ }
15
+ }
16
+
17
+ @customElement('sz-config-overview')
18
+ export class SzConfigOverview extends DeesElement {
19
+ public static demo = () => html`<sz-config-overview
20
+ heading="Configuration"
21
+ infoText="This is a read-only view of the current running configuration."
22
+ >
23
+ <div style="padding: 20px; text-align: center; color: #71717a; font-size: 14px;">
24
+ Place &lt;sz-config-section&gt; elements here
25
+ </div>
26
+ </sz-config-overview>`;
27
+
28
+ public static demoGroups = ['Configuration'];
29
+
30
+ @property({ type: String })
31
+ public accessor heading: string = '';
32
+
33
+ @property({ type: String })
34
+ public accessor infoText: string = '';
35
+
36
+ public static styles = [
37
+ cssManager.defaultStyles,
38
+ css`
39
+ :host {
40
+ display: block;
41
+ }
42
+
43
+ .heading {
44
+ font-size: 20px;
45
+ font-weight: 600;
46
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
47
+ margin-bottom: 16px;
48
+ }
49
+
50
+ .info-banner {
51
+ display: flex;
52
+ align-items: flex-start;
53
+ gap: 12px;
54
+ padding: 14px 18px;
55
+ margin-bottom: 20px;
56
+ border-radius: 8px;
57
+ background: ${cssManager.bdTheme('#eff6ff', 'rgba(59,130,246,0.08)')};
58
+ border: 1px solid ${cssManager.bdTheme('#bfdbfe', 'rgba(59,130,246,0.2)')};
59
+ color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
60
+ font-size: 13px;
61
+ line-height: 1.5;
62
+ }
63
+
64
+ .info-banner dees-icon {
65
+ flex-shrink: 0;
66
+ font-size: 18px;
67
+ margin-top: 1px;
68
+ }
69
+
70
+ ::slotted(sz-config-section) {
71
+ margin-bottom: 12px;
72
+ }
73
+
74
+ ::slotted(sz-config-section:last-child) {
75
+ margin-bottom: 0;
76
+ }
77
+ `,
78
+ ];
79
+
80
+ public render(): TemplateResult {
81
+ return html`
82
+ ${this.heading ? html`<div class="heading">${this.heading}</div>` : ''}
83
+ ${this.infoText ? html`
84
+ <div class="info-banner">
85
+ <dees-icon .icon=${'lucide:info'}></dees-icon>
86
+ <span>${this.infoText}</span>
87
+ </div>
88
+ ` : ''}
89
+ <slot></slot>
90
+ `;
91
+ }
92
+ }
@@ -0,0 +1,580 @@
1
+ import {
2
+ DeesElement,
3
+ customElement,
4
+ html,
5
+ css,
6
+ cssManager,
7
+ property,
8
+ state,
9
+ type TemplateResult,
10
+ } from '@design.estate/dees-element';
11
+
12
+ export interface IConfigField {
13
+ key: string;
14
+ value: string | number | boolean | string[] | null;
15
+ type?: 'text' | 'boolean' | 'badge' | 'pills' | 'code' | 'link';
16
+ description?: string;
17
+ linkTo?: string;
18
+ }
19
+
20
+ export interface IConfigSectionAction {
21
+ label: string;
22
+ icon?: string;
23
+ event?: string;
24
+ detail?: any;
25
+ }
26
+
27
+ declare global {
28
+ interface HTMLElementTagNameMap {
29
+ 'sz-config-section': SzConfigSection;
30
+ }
31
+ }
32
+
33
+ @customElement('sz-config-section')
34
+ export class SzConfigSection extends DeesElement {
35
+ public static demo = () => html`
36
+ <sz-config-section
37
+ title="SmartProxy"
38
+ subtitle="HTTP/HTTPS and TCP/SNI reverse proxy"
39
+ icon="lucide:network"
40
+ status="enabled"
41
+ .fields=${[
42
+ { key: 'Route Count', value: 12 },
43
+ { key: 'ACME Enabled', value: true, type: 'boolean' },
44
+ { key: 'Account Email', value: 'admin@example.com' },
45
+ { key: 'Use Production', value: true, type: 'boolean' },
46
+ { key: 'Auto Renew', value: true, type: 'boolean' },
47
+ { key: 'Renew Threshold', value: '30 days' },
48
+ ] as IConfigField[]}
49
+ ></sz-config-section>
50
+ <sz-config-section
51
+ title="Email Server"
52
+ subtitle="SMTP email handling with smartmta"
53
+ icon="lucide:mail"
54
+ status="disabled"
55
+ .fields=${[
56
+ { key: 'Ports', value: ['25', '465', '587'], type: 'pills' },
57
+ { key: 'Hostname', value: null },
58
+ { key: 'Domains', value: ['example.com', 'mail.example.com'], type: 'pills' },
59
+ ] as IConfigField[]}
60
+ ></sz-config-section>
61
+ <sz-config-section
62
+ title="DNS Server"
63
+ subtitle="Authoritative DNS with smartdns"
64
+ icon="lucide:globe"
65
+ status="not-configured"
66
+ collapsible
67
+ .fields=${[
68
+ { key: 'Port', value: 53 },
69
+ { key: 'NS Domains', value: ['ns1.example.com', 'ns2.example.com'], type: 'pills' },
70
+ ] as IConfigField[]}
71
+ ></sz-config-section>
72
+ `;
73
+
74
+ public static demoGroups = ['Configuration'];
75
+
76
+ @property({ type: String })
77
+ public accessor title: string = '';
78
+
79
+ @property({ type: String })
80
+ public accessor subtitle: string = '';
81
+
82
+ @property({ type: String })
83
+ public accessor icon: string = '';
84
+
85
+ @property({ type: String })
86
+ public accessor status: 'enabled' | 'disabled' | 'not-configured' | 'warning' = 'enabled';
87
+
88
+ @property({ type: Array })
89
+ public accessor fields: IConfigField[] = [];
90
+
91
+ @property({ type: Array })
92
+ public accessor actions: IConfigSectionAction[] = [];
93
+
94
+ @property({ type: Boolean })
95
+ public accessor collapsible: boolean = false;
96
+
97
+ @property({ type: Boolean })
98
+ public accessor collapsed: boolean = false;
99
+
100
+ @state()
101
+ accessor isCollapsed: boolean = false;
102
+
103
+ public static styles = [
104
+ cssManager.defaultStyles,
105
+ css`
106
+ :host {
107
+ display: block;
108
+ margin-bottom: 16px;
109
+ }
110
+
111
+ .section {
112
+ background: ${cssManager.bdTheme('#ffffff', '#09090b')};
113
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
114
+ border-radius: 8px;
115
+ overflow: hidden;
116
+ }
117
+
118
+ .section-header {
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: space-between;
122
+ padding: 14px 20px;
123
+ background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
124
+ border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
125
+ cursor: default;
126
+ user-select: none;
127
+ }
128
+
129
+ :host([collapsible]) .section-header {
130
+ cursor: pointer;
131
+ }
132
+
133
+ :host([collapsible]) .section-header:hover {
134
+ background: ${cssManager.bdTheme('#ebebed', '#1c1c1f')};
135
+ }
136
+
137
+ .header-left {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 12px;
141
+ min-width: 0;
142
+ }
143
+
144
+ .header-icon {
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ width: 36px;
149
+ height: 36px;
150
+ background: ${cssManager.bdTheme('#e4e4e7', '#27272a')};
151
+ border-radius: 8px;
152
+ flex-shrink: 0;
153
+ }
154
+
155
+ .header-icon dees-icon {
156
+ font-size: 18px;
157
+ color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
158
+ }
159
+
160
+ .header-text {
161
+ min-width: 0;
162
+ }
163
+
164
+ .header-title {
165
+ font-size: 15px;
166
+ font-weight: 600;
167
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
168
+ line-height: 1.3;
169
+ }
170
+
171
+ .header-subtitle {
172
+ font-size: 12px;
173
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
174
+ line-height: 1.3;
175
+ margin-top: 1px;
176
+ }
177
+
178
+ .header-right {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 10px;
182
+ flex-shrink: 0;
183
+ }
184
+
185
+ /* Status badge */
186
+ .status-badge {
187
+ display: inline-flex;
188
+ align-items: center;
189
+ gap: 6px;
190
+ padding: 3px 10px;
191
+ border-radius: 9999px;
192
+ font-size: 12px;
193
+ font-weight: 500;
194
+ white-space: nowrap;
195
+ }
196
+
197
+ .status-badge.enabled {
198
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34,197,94,0.2)')};
199
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
200
+ }
201
+
202
+ .status-badge.disabled {
203
+ background: ${cssManager.bdTheme('#fee2e2', 'rgba(239,68,68,0.2)')};
204
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
205
+ }
206
+
207
+ .status-badge.not-configured {
208
+ background: ${cssManager.bdTheme('#f4f4f5', 'rgba(113,113,122,0.2)')};
209
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
210
+ }
211
+
212
+ .status-badge.warning {
213
+ background: ${cssManager.bdTheme('#fef3c7', 'rgba(245,158,11,0.15)')};
214
+ color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
215
+ }
216
+
217
+ .status-dot {
218
+ width: 7px;
219
+ height: 7px;
220
+ border-radius: 50%;
221
+ }
222
+
223
+ .status-badge.enabled .status-dot {
224
+ background: ${cssManager.bdTheme('#16a34a', '#22c55e')};
225
+ }
226
+
227
+ .status-badge.disabled .status-dot {
228
+ background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
229
+ }
230
+
231
+ .status-badge.not-configured .status-dot {
232
+ background: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
233
+ }
234
+
235
+ .status-badge.warning .status-dot {
236
+ background: ${cssManager.bdTheme('#f59e0b', '#fbbf24')};
237
+ }
238
+
239
+ /* Action buttons */
240
+ .header-action {
241
+ display: inline-flex;
242
+ align-items: center;
243
+ gap: 5px;
244
+ padding: 4px 12px;
245
+ border-radius: 6px;
246
+ font-size: 12px;
247
+ font-weight: 500;
248
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
249
+ background: transparent;
250
+ border: none;
251
+ cursor: pointer;
252
+ transition: background 150ms ease;
253
+ white-space: nowrap;
254
+ font-family: inherit;
255
+ }
256
+
257
+ .header-action:hover {
258
+ background: ${cssManager.bdTheme('rgba(37,99,235,0.08)', 'rgba(96,165,250,0.1)')};
259
+ }
260
+
261
+ .header-action dees-icon {
262
+ font-size: 14px;
263
+ }
264
+
265
+ /* Chevron */
266
+ .chevron {
267
+ display: flex;
268
+ align-items: center;
269
+ transition: transform 200ms ease;
270
+ }
271
+
272
+ .chevron.collapsed {
273
+ transform: rotate(-90deg);
274
+ }
275
+
276
+ .chevron dees-icon {
277
+ font-size: 16px;
278
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
279
+ }
280
+
281
+ /* Content */
282
+ .section-content {
283
+ padding: 0;
284
+ }
285
+
286
+ .section-content.collapsed {
287
+ display: none;
288
+ }
289
+
290
+ /* Field rows */
291
+ .field-row {
292
+ display: flex;
293
+ align-items: flex-start;
294
+ justify-content: space-between;
295
+ padding: 10px 20px;
296
+ border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')};
297
+ gap: 16px;
298
+ }
299
+
300
+ .field-row:last-child {
301
+ border-bottom: none;
302
+ }
303
+
304
+ .field-key {
305
+ font-size: 13px;
306
+ font-weight: 500;
307
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
308
+ flex-shrink: 0;
309
+ min-width: 140px;
310
+ padding-top: 1px;
311
+ }
312
+
313
+ .field-value {
314
+ font-size: 13px;
315
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
316
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
317
+ text-align: right;
318
+ word-break: break-all;
319
+ }
320
+
321
+ .field-value.null-value {
322
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
323
+ font-style: italic;
324
+ font-family: inherit;
325
+ }
326
+
327
+ /* Boolean display */
328
+ .bool-value {
329
+ display: inline-flex;
330
+ align-items: center;
331
+ gap: 5px;
332
+ font-family: inherit;
333
+ font-size: 13px;
334
+ font-weight: 500;
335
+ }
336
+
337
+ .bool-value.true {
338
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
339
+ }
340
+
341
+ .bool-value.false {
342
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
343
+ }
344
+
345
+ .bool-dot {
346
+ width: 6px;
347
+ height: 6px;
348
+ border-radius: 50%;
349
+ }
350
+
351
+ .bool-value.true .bool-dot {
352
+ background: ${cssManager.bdTheme('#16a34a', '#22c55e')};
353
+ }
354
+
355
+ .bool-value.false .bool-dot {
356
+ background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
357
+ }
358
+
359
+ /* Pills */
360
+ .pills {
361
+ display: flex;
362
+ flex-wrap: wrap;
363
+ gap: 5px;
364
+ justify-content: flex-end;
365
+ }
366
+
367
+ .pill {
368
+ display: inline-flex;
369
+ align-items: center;
370
+ padding: 2px 9px;
371
+ border-radius: 9999px;
372
+ font-size: 12px;
373
+ font-weight: 500;
374
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
375
+ background: ${cssManager.bdTheme('#eff6ff', 'rgba(59,130,246,0.1)')};
376
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
377
+ }
378
+
379
+ /* Code value */
380
+ .code-value {
381
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
382
+ font-size: 12px;
383
+ background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
384
+ padding: 2px 8px;
385
+ border-radius: 4px;
386
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
387
+ }
388
+
389
+ /* Link value */
390
+ .link-value {
391
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
392
+ cursor: pointer;
393
+ text-decoration: none;
394
+ font-family: inherit;
395
+ font-size: 13px;
396
+ }
397
+
398
+ .link-value:hover {
399
+ text-decoration: underline;
400
+ }
401
+
402
+ /* Description hint */
403
+ .field-description {
404
+ font-size: 11px;
405
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
406
+ margin-top: 3px;
407
+ text-align: right;
408
+ }
409
+
410
+ /* Slot for custom content */
411
+ .slot-content {
412
+ border-top: 1px solid ${cssManager.bdTheme('#f4f4f5', '#1a1a1e')};
413
+ }
414
+
415
+ .slot-content:empty {
416
+ display: none;
417
+ border-top: none;
418
+ }
419
+
420
+ /* Badge type */
421
+ .badge-value {
422
+ display: inline-flex;
423
+ align-items: center;
424
+ padding: 2px 9px;
425
+ border-radius: 9999px;
426
+ font-size: 12px;
427
+ font-weight: 500;
428
+ background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
429
+ color: ${cssManager.bdTheme('#52525b', '#a1a1aa')};
430
+ }
431
+ `,
432
+ ];
433
+
434
+ async connectedCallback() {
435
+ await super.connectedCallback();
436
+ this.isCollapsed = this.collapsed;
437
+ if (this.collapsible) {
438
+ this.setAttribute('collapsible', '');
439
+ }
440
+ }
441
+
442
+ public render(): TemplateResult {
443
+ const statusLabels: Record<string, string> = {
444
+ 'enabled': 'Enabled',
445
+ 'disabled': 'Disabled',
446
+ 'not-configured': 'Not Configured',
447
+ 'warning': 'Warning',
448
+ };
449
+
450
+ return html`
451
+ <div class="section">
452
+ <div
453
+ class="section-header"
454
+ @click=${() => {
455
+ if (this.collapsible) {
456
+ this.isCollapsed = !this.isCollapsed;
457
+ }
458
+ }}
459
+ >
460
+ <div class="header-left">
461
+ ${this.icon ? html`
462
+ <div class="header-icon">
463
+ <dees-icon .icon=${this.icon}></dees-icon>
464
+ </div>
465
+ ` : ''}
466
+ <div class="header-text">
467
+ <div class="header-title">${this.title}</div>
468
+ ${this.subtitle ? html`<div class="header-subtitle">${this.subtitle}</div>` : ''}
469
+ </div>
470
+ </div>
471
+ <div class="header-right">
472
+ ${this.status ? html`
473
+ <span class="status-badge ${this.status}">
474
+ <span class="status-dot"></span>
475
+ ${statusLabels[this.status] || this.status}
476
+ </span>
477
+ ` : ''}
478
+ ${this.actions.map(action => html`
479
+ <button class="header-action" @click=${(e: Event) => {
480
+ e.stopPropagation();
481
+ this.dispatchEvent(new CustomEvent(action.event || 'action', {
482
+ detail: action.detail || { label: action.label },
483
+ bubbles: true,
484
+ composed: true,
485
+ }));
486
+ }}>
487
+ ${action.icon ? html`<dees-icon .icon=${action.icon}></dees-icon>` : ''}
488
+ ${action.label}
489
+ </button>
490
+ `)}
491
+ ${this.collapsible ? html`
492
+ <span class="chevron ${this.isCollapsed ? 'collapsed' : ''}">
493
+ <dees-icon .icon=${'lucide:chevronDown'}></dees-icon>
494
+ </span>
495
+ ` : ''}
496
+ </div>
497
+ </div>
498
+ <div class="section-content ${this.isCollapsed ? 'collapsed' : ''}">
499
+ ${this.fields.map(field => this.renderField(field))}
500
+ <div class="slot-content">
501
+ <slot></slot>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ `;
506
+ }
507
+
508
+ private renderField(field: IConfigField): TemplateResult {
509
+ return html`
510
+ <div class="field-row">
511
+ <div class="field-key">${field.key}</div>
512
+ <div>
513
+ ${this.renderFieldValue(field)}
514
+ ${field.description ? html`<div class="field-description">${field.description}</div>` : ''}
515
+ </div>
516
+ </div>
517
+ `;
518
+ }
519
+
520
+ private renderFieldValue(field: IConfigField): TemplateResult {
521
+ const value = field.value;
522
+ const type = field.type || this.inferType(value);
523
+
524
+ // Null / undefined
525
+ if (value === null || value === undefined) {
526
+ return html`<span class="field-value null-value">Not configured</span>`;
527
+ }
528
+
529
+ switch (type) {
530
+ case 'boolean':
531
+ return html`
532
+ <span class="bool-value ${value ? 'true' : 'false'}">
533
+ <span class="bool-dot"></span>
534
+ ${value ? 'Enabled' : 'Disabled'}
535
+ </span>
536
+ `;
537
+
538
+ case 'pills':
539
+ if (Array.isArray(value) && value.length === 0) {
540
+ return html`<span class="field-value null-value">None</span>`;
541
+ }
542
+ return html`
543
+ <div class="pills">
544
+ ${(value as string[]).map(v => html`<span class="pill">${v}</span>`)}
545
+ </div>
546
+ `;
547
+
548
+ case 'code':
549
+ return html`<span class="code-value">${String(value)}</span>`;
550
+
551
+ case 'badge':
552
+ return html`<span class="badge-value">${String(value)}</span>`;
553
+
554
+ case 'link':
555
+ return html`
556
+ <span
557
+ class="link-value"
558
+ @click=${() => {
559
+ if (field.linkTo) {
560
+ this.dispatchEvent(new CustomEvent('navigate', {
561
+ detail: { target: field.linkTo },
562
+ bubbles: true,
563
+ composed: true,
564
+ }));
565
+ }
566
+ }}
567
+ >${String(value)}</span>
568
+ `;
569
+
570
+ default:
571
+ return html`<span class="field-value">${String(value)}</span>`;
572
+ }
573
+ }
574
+
575
+ private inferType(value: unknown): string {
576
+ if (typeof value === 'boolean') return 'boolean';
577
+ if (Array.isArray(value)) return 'pills';
578
+ return 'text';
579
+ }
580
+ }