@serve.zone/catalog 2.1.0 → 2.3.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.
@@ -0,0 +1,332 @@
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
+ declare global {
13
+ interface HTMLElementTagNameMap {
14
+ 'sz-mta-list-view': SzMtaListView;
15
+ }
16
+ }
17
+
18
+ export type TEmailStatus = 'delivered' | 'bounced' | 'rejected' | 'deferred' | 'pending';
19
+ export type TEmailDirection = 'inbound' | 'outbound';
20
+
21
+ export interface IEmail {
22
+ id: string;
23
+ direction: TEmailDirection;
24
+ status: TEmailStatus;
25
+ from: string;
26
+ to: string;
27
+ subject: string;
28
+ timestamp: string;
29
+ messageId: string;
30
+ size: string;
31
+ }
32
+
33
+ @customElement('sz-mta-list-view')
34
+ export class SzMtaListView extends DeesElement {
35
+ public static demo = () => html`
36
+ <div style="padding: 24px; max-width: 1200px;">
37
+ <sz-mta-list-view
38
+ .emails=${[
39
+ { id: '1', direction: 'outbound', status: 'delivered', from: 'noreply@serve.zone', to: 'user@example.com', subject: 'Welcome to serve.zone', timestamp: '2024-01-15 14:30:22', messageId: '<abc123@serve.zone>', size: '12.4 KB' },
40
+ { id: '2', direction: 'outbound', status: 'bounced', from: 'alerts@serve.zone', to: 'invalid@nowhere.test', subject: 'Service Alert: CPU Usage High', timestamp: '2024-01-15 14:28:10', messageId: '<def456@serve.zone>', size: '8.2 KB' },
41
+ { id: '3', direction: 'inbound', status: 'delivered', from: 'support@customer.com', to: 'admin@serve.zone', subject: 'Re: Infrastructure Review', timestamp: '2024-01-15 14:25:00', messageId: '<ghi789@customer.com>', size: '24.1 KB' },
42
+ { id: '4', direction: 'outbound', status: 'rejected', from: 'billing@serve.zone', to: 'blocked@spam-domain.test', subject: 'Invoice #2024-001', timestamp: '2024-01-15 14:20:45', messageId: '<jkl012@serve.zone>', size: '45.6 KB' },
43
+ { id: '5', direction: 'outbound', status: 'deferred', from: 'noreply@serve.zone', to: 'slow@remote-server.test', subject: 'Password Reset Request', timestamp: '2024-01-15 14:15:30', messageId: '<mno345@serve.zone>', size: '6.8 KB' },
44
+ { id: '6', direction: 'inbound', status: 'delivered', from: 'ci@github.com', to: 'devops@serve.zone', subject: 'Build #4521 passed', timestamp: '2024-01-15 14:10:00', messageId: '<pqr678@github.com>', size: '15.3 KB' },
45
+ { id: '7', direction: 'outbound', status: 'pending', from: 'reports@serve.zone', to: 'team@serve.zone', subject: 'Weekly Infrastructure Report', timestamp: '2024-01-15 14:05:00', messageId: '<stu901@serve.zone>', size: '102.7 KB' },
46
+ ]}
47
+ ></sz-mta-list-view>
48
+ </div>
49
+ `;
50
+
51
+ public static demoGroups = ['MTA'];
52
+
53
+ @property({ type: Array })
54
+ public accessor emails: IEmail[] = [];
55
+
56
+ @state()
57
+ private accessor searchQuery: string = '';
58
+
59
+ @state()
60
+ private accessor statusFilter: TEmailStatus | 'all' = 'all';
61
+
62
+ @state()
63
+ private accessor directionFilter: TEmailDirection | 'all' = 'all';
64
+
65
+ private get filteredEmails(): IEmail[] {
66
+ return this.emails.filter((email) => {
67
+ if (this.statusFilter !== 'all' && email.status !== this.statusFilter) return false;
68
+ if (this.directionFilter !== 'all' && email.direction !== this.directionFilter) return false;
69
+ if (this.searchQuery) {
70
+ const q = this.searchQuery.toLowerCase();
71
+ return (
72
+ email.from.toLowerCase().includes(q) ||
73
+ email.to.toLowerCase().includes(q) ||
74
+ email.subject.toLowerCase().includes(q) ||
75
+ email.messageId.toLowerCase().includes(q)
76
+ );
77
+ }
78
+ return true;
79
+ });
80
+ }
81
+
82
+ public static styles = [
83
+ cssManager.defaultStyles,
84
+ css`
85
+ :host {
86
+ display: block;
87
+ }
88
+
89
+ .filter-bar {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ gap: 12px;
93
+ align-items: center;
94
+ margin-bottom: 16px;
95
+ }
96
+
97
+ .search-input {
98
+ flex: 1;
99
+ min-width: 200px;
100
+ padding: 8px 12px;
101
+ background: ${cssManager.bdTheme('#ffffff', '#09090b')};
102
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
103
+ border-radius: 6px;
104
+ font-size: 14px;
105
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
106
+ outline: none;
107
+ transition: border-color 200ms ease;
108
+ }
109
+
110
+ .search-input::placeholder {
111
+ color: ${cssManager.bdTheme('#a1a1aa', '#52525b')};
112
+ }
113
+
114
+ .search-input:focus {
115
+ border-color: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
116
+ }
117
+
118
+ .chip-group {
119
+ display: flex;
120
+ gap: 4px;
121
+ }
122
+
123
+ .chip {
124
+ padding: 6px 12px;
125
+ background: transparent;
126
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
127
+ border-radius: 9999px;
128
+ font-size: 12px;
129
+ font-weight: 500;
130
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
131
+ cursor: pointer;
132
+ transition: all 200ms ease;
133
+ white-space: nowrap;
134
+ }
135
+
136
+ .chip:hover {
137
+ background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
138
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
139
+ }
140
+
141
+ .chip.active {
142
+ background: ${cssManager.bdTheme('#18181b', '#fafafa')};
143
+ color: ${cssManager.bdTheme('#fafafa', '#18181b')};
144
+ border-color: ${cssManager.bdTheme('#18181b', '#fafafa')};
145
+ }
146
+
147
+ .results-count {
148
+ font-size: 13px;
149
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
150
+ margin-bottom: 12px;
151
+ }
152
+
153
+ .table-container {
154
+ background: ${cssManager.bdTheme('#ffffff', '#09090b')};
155
+ border: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
156
+ border-radius: 8px;
157
+ overflow: hidden;
158
+ }
159
+
160
+ .table-header {
161
+ display: grid;
162
+ grid-template-columns: 40px 90px 1.5fr 1.5fr 2fr 140px;
163
+ gap: 16px;
164
+ padding: 12px 16px;
165
+ background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
166
+ border-bottom: 1px solid ${cssManager.bdTheme('#e4e4e7', '#27272a')};
167
+ font-size: 12px;
168
+ font-weight: 600;
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.05em;
171
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
172
+ }
173
+
174
+ .table-row {
175
+ display: grid;
176
+ grid-template-columns: 40px 90px 1.5fr 1.5fr 2fr 140px;
177
+ gap: 16px;
178
+ padding: 12px 16px;
179
+ border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#27272a')};
180
+ font-size: 14px;
181
+ color: ${cssManager.bdTheme('#18181b', '#fafafa')};
182
+ align-items: center;
183
+ cursor: pointer;
184
+ transition: background 200ms ease;
185
+ }
186
+
187
+ .table-row:last-child {
188
+ border-bottom: none;
189
+ }
190
+
191
+ .table-row:hover {
192
+ background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
193
+ }
194
+
195
+ .direction-icon {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ }
200
+
201
+ .direction-icon.inbound {
202
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
203
+ }
204
+
205
+ .direction-icon.outbound {
206
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
207
+ }
208
+
209
+ .status-badge {
210
+ display: inline-flex;
211
+ align-items: center;
212
+ padding: 2px 8px;
213
+ border-radius: 9999px;
214
+ font-size: 12px;
215
+ font-weight: 500;
216
+ }
217
+
218
+ .status-badge.delivered {
219
+ background: ${cssManager.bdTheme('#dcfce7', 'rgba(34, 197, 94, 0.2)')};
220
+ color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
221
+ }
222
+
223
+ .status-badge.bounced,
224
+ .status-badge.rejected {
225
+ background: ${cssManager.bdTheme('#fee2e2', 'rgba(239, 68, 68, 0.2)')};
226
+ color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
227
+ }
228
+
229
+ .status-badge.deferred {
230
+ background: ${cssManager.bdTheme('#fef9c3', 'rgba(250, 204, 21, 0.2)')};
231
+ color: ${cssManager.bdTheme('#ca8a04', '#facc15')};
232
+ }
233
+
234
+ .status-badge.pending {
235
+ background: ${cssManager.bdTheme('#dbeafe', 'rgba(59, 130, 246, 0.2)')};
236
+ color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
237
+ }
238
+
239
+ .email-from,
240
+ .email-to {
241
+ overflow: hidden;
242
+ text-overflow: ellipsis;
243
+ white-space: nowrap;
244
+ }
245
+
246
+ .email-subject {
247
+ overflow: hidden;
248
+ text-overflow: ellipsis;
249
+ white-space: nowrap;
250
+ font-weight: 500;
251
+ }
252
+
253
+ .email-timestamp {
254
+ font-size: 13px;
255
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
256
+ white-space: nowrap;
257
+ }
258
+
259
+ .empty-state {
260
+ padding: 48px 24px;
261
+ text-align: center;
262
+ color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
263
+ }
264
+ `,
265
+ ];
266
+
267
+ public render(): TemplateResult {
268
+ const filtered = this.filteredEmails;
269
+
270
+ return html`
271
+ <div class="filter-bar">
272
+ <input
273
+ class="search-input"
274
+ type="text"
275
+ placeholder="Search by from, to, subject, or message ID..."
276
+ .value=${this.searchQuery}
277
+ @input=${(e: InputEvent) => { this.searchQuery = (e.target as HTMLInputElement).value; }}
278
+ />
279
+ <div class="chip-group">
280
+ ${(['all', 'inbound', 'outbound'] as const).map(dir => html`
281
+ <button
282
+ class="chip ${this.directionFilter === dir ? 'active' : ''}"
283
+ @click=${() => { this.directionFilter = dir; }}
284
+ >${dir === 'all' ? 'All' : dir === 'inbound' ? 'Inbound' : 'Outbound'}</button>
285
+ `)}
286
+ </div>
287
+ <div class="chip-group">
288
+ ${(['all', 'delivered', 'bounced', 'rejected', 'deferred', 'pending'] as const).map(s => html`
289
+ <button
290
+ class="chip ${this.statusFilter === s ? 'active' : ''}"
291
+ @click=${() => { this.statusFilter = s; }}
292
+ >${s === 'all' ? 'All' : s.charAt(0).toUpperCase() + s.slice(1)}</button>
293
+ `)}
294
+ </div>
295
+ </div>
296
+
297
+ <div class="results-count">Showing ${filtered.length} of ${this.emails.length} emails</div>
298
+
299
+ <div class="table-container">
300
+ <div class="table-header">
301
+ <span></span>
302
+ <span>Status</span>
303
+ <span>From</span>
304
+ <span>To</span>
305
+ <span>Subject</span>
306
+ <span>Timestamp</span>
307
+ </div>
308
+ ${filtered.length > 0 ? filtered.map(email => html`
309
+ <div class="table-row" @click=${() => this.handleEmailClick(email)}>
310
+ <span class="direction-icon ${email.direction}">
311
+ ${email.direction === 'inbound'
312
+ ? html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>`
313
+ : html`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>`
314
+ }
315
+ </span>
316
+ <span><span class="status-badge ${email.status}">${email.status}</span></span>
317
+ <span class="email-from" title="${email.from}">${email.from}</span>
318
+ <span class="email-to" title="${email.to}">${email.to}</span>
319
+ <span class="email-subject" title="${email.subject}">${email.subject}</span>
320
+ <span class="email-timestamp">${email.timestamp}</span>
321
+ </div>
322
+ `) : html`
323
+ <div class="empty-state">No emails found</div>
324
+ `}
325
+ </div>
326
+ `;
327
+ }
328
+
329
+ private handleEmailClick(email: IEmail) {
330
+ this.dispatchEvent(new CustomEvent('email-click', { detail: email, bubbles: true, composed: true }));
331
+ }
332
+ }