@flusys/ng-email 1.1.0-beta → 2.0.0-rc.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/README.md CHANGED
@@ -11,15 +11,17 @@
11
11
 
12
12
  - **Email Configurations** - CRUD for SMTP, SendGrid, Mailgun providers
13
13
  - **Email Templates** - Visual template management with variable interpolation
14
- - **HTML/Plain Text** - Toggle between content types via `isHtml` field
14
+ - **HTML/Plain Text** - Toggle between content types with live preview
15
15
  - **Test Sending** - Test configurations and templates with dynamic variables
16
16
  - **Company Scoping** - Automatic company-scoped queries (when enabled)
17
17
  - **Lazy Loading** - All page components are lazy-loaded via routes
18
+ - **Responsive Design** - Mobile-first with horizontal scroll tables
19
+ - **Dark Mode** - Full dark/light mode support
18
20
 
19
21
  ### Package Hierarchy
20
22
 
21
23
  ```
22
- @flusys/ng-core <- Foundation (BaseApiService, APP_CONFIG)
24
+ @flusys/ng-core <- Foundation (APP_CONFIG, DEFAULT_APP_NAME)
23
25
  |
24
26
  @flusys/ng-shared <- Shared utilities (ApiResourceService, IBaseEntity)
25
27
  |
@@ -34,11 +36,17 @@ Email components use `LAYOUT_AUTH_STATE` from `@flusys/ng-layout` for company co
34
36
 
35
37
  ```typescript
36
38
  import { LAYOUT_AUTH_STATE } from '@flusys/ng-layout';
39
+ import { APP_CONFIG, DEFAULT_APP_NAME } from '@flusys/ng-core';
37
40
 
38
- private readonly companyContext = inject(LAYOUT_AUTH_STATE);
41
+ // Optional injection - works with or without company feature
42
+ private readonly companyContext = inject(LAYOUT_AUTH_STATE, { optional: true });
43
+
44
+ readonly showCompanyInfo = computed(
45
+ () => this.appConfig.enableCompanyFeature && !!this.companyContext,
46
+ );
39
47
 
40
48
  readonly currentCompanyName = computed(
41
- () => this.companyContext.currentCompanyInfo()?.name ?? DEFAULT_APP_NAME,
49
+ () => this.companyContext?.currentCompanyInfo()?.name ?? DEFAULT_APP_NAME,
42
50
  );
43
51
  ```
44
52
 
@@ -48,17 +56,25 @@ readonly currentCompanyName = computed(
48
56
 
49
57
  ```
50
58
  ng-email/
59
+ ├── enums/
60
+ │ ├── email-provider.enum.ts # EmailProviderEnum (smtp, sendgrid, mailgun)
61
+ │ ├── email-block-type.enum.ts # EmailBlockType (text, image, button, etc.)
62
+ │ └── public-api.ts
51
63
  ├── interfaces/
52
- │ ├── email-config.interface.ts # IEmailConfig, ICreateEmailConfigDto
53
- │ ├── email-template.interface.ts # IEmailTemplate, ICreateEmailTemplateDto
54
- │ ├── email-send.interface.ts # ISendEmailDto, ISendTemplateEmailDto
64
+ │ ├── email-config.interface.ts # IEmailConfig, ICreateEmailConfigDto
65
+ │ ├── email-template.interface.ts # IEmailTemplate, ICreateEmailTemplateDto
66
+ │ ├── email-schema.interface.ts # IEmailSchema, IEmailSection, IEmailBlock
67
+ │ ├── email-send.interface.ts # ISendEmailDto, ISendTemplateEmailDto
55
68
  │ └── public-api.ts
56
69
  ├── services/
57
- │ ├── email-config-api.service.ts # Config CRUD
58
- │ ├── email-template-api.service.ts # Template CRUD
59
- │ ├── email-send.service.ts # Email sending operations
70
+ │ ├── email-config-api.service.ts # Config CRUD + test sending
71
+ │ ├── email-template-api.service.ts # Template CRUD + getBySlug
72
+ │ ├── email-send.service.ts # Email sending operations
73
+ │ ├── email-builder-state.service.ts # Template builder state management
60
74
  │ └── public-api.ts
61
75
  ├── pages/
76
+ │ ├── email-container/
77
+ │ │ └── email-container.component.ts # Main container with tabs
62
78
  │ ├── config/
63
79
  │ │ ├── email-config-list.component.ts
64
80
  │ │ └── email-config-form.component.ts
@@ -92,12 +108,39 @@ export const routes: Routes = [
92
108
 
93
109
  | Path | Component | Description |
94
110
  |------|-----------|-------------|
95
- | `/email/configs` | EmailConfigListComponent | List email configurations |
96
- | `/email/configs/new` | EmailConfigFormComponent | Create new config |
97
- | `/email/configs/:id` | EmailConfigFormComponent | Edit config |
111
+ | `/email` | EmailContainerComponent | Main container with tab navigation |
98
112
  | `/email/templates` | TemplateListComponent | List email templates |
99
113
  | `/email/templates/new` | TemplateFormComponent | Create new template |
100
114
  | `/email/templates/:id` | TemplateFormComponent | Edit template |
115
+ | `/email/configs` | EmailConfigListComponent | List email configurations |
116
+ | `/email/configs/new` | EmailConfigFormComponent | Create new config |
117
+ | `/email/configs/:id` | EmailConfigFormComponent | Edit config |
118
+
119
+ ---
120
+
121
+ ## Enums
122
+
123
+ ### EmailProviderEnum
124
+
125
+ ```typescript
126
+ export enum EmailProviderEnum {
127
+ SMTP = 'smtp',
128
+ SENDGRID = 'sendgrid',
129
+ MAILGUN = 'mailgun',
130
+ }
131
+ ```
132
+
133
+ ### EmailBlockType
134
+
135
+ ```typescript
136
+ export enum EmailBlockType {
137
+ TEXT = 'text',
138
+ IMAGE = 'image',
139
+ BUTTON = 'button',
140
+ DIVIDER = 'divider',
141
+ HTML = 'html',
142
+ }
143
+ ```
101
144
 
102
145
  ---
103
146
 
@@ -108,11 +151,10 @@ export const routes: Routes = [
108
151
  ```typescript
109
152
  interface IEmailConfig extends IBaseEntity {
110
153
  name: string;
111
- provider: 'smtp' | 'sendgrid' | 'mailgun';
154
+ provider: EmailProviderEnum;
112
155
  config: Record<string, any>; // Provider-specific config
113
- fromEmail: string;
156
+ fromEmail: string | null;
114
157
  fromName: string | null;
115
- replyTo: string | null;
116
158
  isActive: boolean;
117
159
  isDefault: boolean; // Set as default configuration
118
160
  companyId?: string | null;
@@ -120,25 +162,16 @@ interface IEmailConfig extends IBaseEntity {
120
162
 
121
163
  interface ICreateEmailConfigDto {
122
164
  name: string;
123
- provider: 'smtp' | 'sendgrid' | 'mailgun';
165
+ provider: EmailProviderEnum;
124
166
  config: Record<string, any>;
125
- fromEmail: string;
167
+ fromEmail?: string;
126
168
  fromName?: string;
127
- replyTo?: string;
128
169
  isActive?: boolean;
129
170
  isDefault?: boolean;
130
171
  }
131
172
 
132
- interface IUpdateEmailConfigDto {
173
+ interface IUpdateEmailConfigDto extends Partial<ICreateEmailConfigDto> {
133
174
  id: string;
134
- name?: string;
135
- provider?: 'smtp' | 'sendgrid' | 'mailgun';
136
- config?: Record<string, any>;
137
- fromEmail?: string;
138
- fromName?: string;
139
- replyTo?: string;
140
- isActive?: boolean;
141
- isDefault?: boolean;
142
175
  }
143
176
  ```
144
177
 
@@ -149,11 +182,9 @@ interface IUpdateEmailConfigDto {
149
182
  interface ISmtpConfig {
150
183
  host: string;
151
184
  port: number;
152
- secure: boolean;
153
- auth: {
154
- user: string;
155
- pass: string;
156
- };
185
+ secure?: boolean;
186
+ username: string;
187
+ password: string;
157
188
  }
158
189
 
159
190
  // SendGrid
@@ -177,7 +208,7 @@ interface IEmailTemplate extends IBaseEntity {
177
208
  slug: string;
178
209
  description: string | null;
179
210
  subject: string;
180
- schema: Record<string, unknown>;
211
+ schema: IEmailSchema;
181
212
  htmlContent: string;
182
213
  textContent: string | null;
183
214
  schemaVersion: number;
@@ -192,30 +223,69 @@ interface ICreateEmailTemplateDto {
192
223
  slug: string;
193
224
  description?: string;
194
225
  subject: string;
195
- schema?: Record<string, unknown>;
226
+ schema: IEmailSchema;
196
227
  htmlContent: string;
197
228
  textContent?: string;
198
229
  isActive?: boolean;
199
230
  isHtml?: boolean;
231
+ metadata?: Record<string, unknown>;
232
+ }
233
+
234
+ interface IUpdateEmailTemplateDto extends Partial<ICreateEmailTemplateDto> {
235
+ id: string;
236
+ }
237
+ ```
238
+
239
+ ### Email Schema
240
+
241
+ ```typescript
242
+ interface IEmailSchema {
243
+ id: string;
244
+ version: string;
245
+ name: string;
246
+ subject: string;
247
+ preheader?: string;
248
+ sections: IEmailSection[];
249
+ variables?: IEmailTemplateVariable[];
250
+ settings?: IEmailSettings;
200
251
  }
201
252
 
202
- interface IUpdateEmailTemplateDto {
253
+ interface IEmailSection {
203
254
  id: string;
204
- name?: string;
205
- slug?: string;
255
+ type: 'header' | 'body' | 'footer';
256
+ name: string;
257
+ blocks: IEmailBlock[];
258
+ order: number;
259
+ style?: IEmailSectionStyle;
260
+ }
261
+
262
+ interface IEmailBlock {
263
+ id: string;
264
+ type: EmailBlockType;
265
+ order: number;
266
+ content: ITextBlockContent | IImageBlockContent | IButtonBlockContent | IDividerBlockContent | IHtmlBlockContent;
267
+ style?: IEmailBlockStyle;
268
+ }
269
+
270
+ interface IEmailTemplateVariable {
271
+ name: string;
272
+ type: 'string' | 'number' | 'boolean' | 'date';
273
+ required?: boolean;
274
+ defaultValue?: string;
206
275
  description?: string;
207
- subject?: string;
208
- schema?: Record<string, unknown>;
209
- htmlContent?: string;
210
- textContent?: string;
211
- isActive?: boolean;
212
- isHtml?: boolean;
213
276
  }
214
277
  ```
215
278
 
216
279
  ### Email Sending
217
280
 
218
281
  ```typescript
282
+ // Test send result
283
+ interface ITestSendResult {
284
+ success: boolean;
285
+ messageId?: string;
286
+ error?: string;
287
+ }
288
+
219
289
  // Direct email
220
290
  interface ISendEmailDto {
221
291
  to: string | string[];
@@ -228,7 +298,6 @@ interface ISendEmailDto {
228
298
  fromName?: string;
229
299
  replyTo?: string;
230
300
  emailConfigId?: string;
231
- attachments?: IEmailAttachmentDto[];
232
301
  }
233
302
 
234
303
  // Template email
@@ -243,27 +312,6 @@ interface ISendTemplateEmailDto {
243
312
  fromName?: string;
244
313
  replyTo?: string;
245
314
  emailConfigId?: string;
246
- attachments?: IEmailAttachmentDto[];
247
- }
248
-
249
- // Test email
250
- interface ITestEmailDto {
251
- emailConfigId: string;
252
- recipient: string;
253
- }
254
-
255
- // Attachment
256
- interface IEmailAttachmentDto {
257
- filename: string;
258
- content: string; // Base64 encoded
259
- contentType?: string;
260
- }
261
-
262
- // Result
263
- interface IEmailSendResult {
264
- success: boolean;
265
- messageId?: string;
266
- error?: string;
267
315
  }
268
316
  ```
269
317
 
@@ -277,54 +325,156 @@ All services are `providedIn: 'root'`. Company scoping is automatic when the com
277
325
 
278
326
  Email configuration CRUD. Extends `ApiResourceService<IUpdateEmailConfigDto, IEmailConfig>`.
279
327
 
280
- **Inherited CRUD methods** (from ApiResourceService): `insert`, `update`, `findById`, `getAll`, `delete` and their `*Async` variants.
328
+ ```typescript
329
+ @Injectable({ providedIn: 'root' })
330
+ export class EmailConfigApiService extends ApiResourceService<
331
+ IUpdateEmailConfigDto,
332
+ IEmailConfig
333
+ > {
334
+ constructor() {
335
+ const http = inject(HttpClient);
336
+ super('email/email-config', http);
337
+ }
338
+
339
+ /** Send test email to verify configuration */
340
+ sendTest(configId: string, recipient: string): Observable<ISingleResponse<ITestSendResult>> {
341
+ return this.http.post<ISingleResponse<ITestSendResult>>(
342
+ `${this.baseUrl.replace('/email-config', '')}/send/test`,
343
+ { emailConfigId: configId, recipient },
344
+ );
345
+ }
346
+ }
347
+ ```
281
348
 
282
349
  ### EmailTemplateApiService
283
350
 
284
351
  Email template CRUD. Extends `ApiResourceService<IUpdateEmailTemplateDto, IEmailTemplate>`.
285
352
 
286
- **Inherited CRUD methods** (from ApiResourceService): `insert`, `update`, `findById`, `getAll`, `delete` and their `*Async` variants.
353
+ ```typescript
354
+ @Injectable({ providedIn: 'root' })
355
+ export class EmailTemplateApiService extends ApiResourceService<
356
+ IUpdateEmailTemplateDto,
357
+ IEmailTemplate
358
+ > {
359
+ constructor() {
360
+ const http = inject(HttpClient);
361
+ super('email/email-template', http);
362
+ }
363
+
364
+ /** Get template by slug (POST-only RPC pattern) */
365
+ getBySlug(slug: string): Observable<ISingleResponse<IEmailTemplate | null>> {
366
+ return this.http.post<ISingleResponse<IEmailTemplate | null>>(
367
+ `${this.baseUrl}/get-by-slug`,
368
+ { slug },
369
+ );
370
+ }
371
+ }
372
+ ```
287
373
 
288
374
  ### EmailSendService
289
375
 
290
- Handles email sending operations. Extends `BaseApiService`.
376
+ Handles email sending operations.
377
+
378
+ ```typescript
379
+ @Injectable({ providedIn: 'root' })
380
+ export class EmailSendService {
381
+ private readonly http = inject(HttpClient);
382
+ private readonly baseUrl = `${inject(APP_CONFIG).apiBaseUrl}/email/send`;
383
+
384
+ /** Send email directly (without template) */
385
+ sendDirect(dto: ISendEmailDto): Observable<ISingleResponse<ITestSendResult>>;
386
+
387
+ /** Send email using template */
388
+ sendTemplate(dto: ISendTemplateEmailDto): Observable<ISingleResponse<ITestSendResult>>;
389
+ }
390
+ ```
391
+
392
+ ### EmailBuilderStateService
291
393
 
292
- | Method | Endpoint | Description |
293
- |--------|----------|-------------|
294
- | `sendDirect(dto)` | `POST /email/send/direct` | Send email directly |
295
- | `sendTemplate(dto)` | `POST /email/send/template` | Send using template |
296
- | `testConfiguration(dto)` | `POST /email/send/test` | Test email configuration |
394
+ Manages email builder UI state. Should be provided at component level, not root.
395
+
396
+ ```typescript
397
+ @Injectable()
398
+ export class EmailBuilderStateService {
399
+ // Schema state
400
+ readonly schema: Signal<IEmailSchema>;
401
+ readonly isDirty: Signal<boolean>;
402
+
403
+ // Selection state
404
+ readonly selectedSectionId: Signal<string | null>;
405
+ readonly selectedBlockId: Signal<string | null>;
406
+
407
+ // Computed signals
408
+ readonly sections: Signal<IEmailSection[]>;
409
+ readonly headerSection: Signal<IEmailSection | null>;
410
+ readonly bodySection: Signal<IEmailSection | null>;
411
+ readonly footerSection: Signal<IEmailSection | null>;
412
+
413
+ // Methods
414
+ loadSchema(schema: IEmailSchema): void;
415
+ createNewSchema(name?: string): void;
416
+ addBlock(sectionId: string, blockType: EmailBlockType): string | null;
417
+ updateBlock(sectionId: string, blockId: string, updates: Partial<IEmailBlock>): void;
418
+ deleteBlock(sectionId: string, blockId: string): void;
419
+ }
420
+ ```
297
421
 
298
422
  ---
299
423
 
300
424
  ## Components
301
425
 
302
- All components are standalone, use Angular 21 signals, and lazy-load via routes.
426
+ All components are standalone, use Angular 21 signals, OnPush change detection, and lazy-load via routes.
427
+
428
+ ### EmailContainerComponent
429
+
430
+ Main container with tab navigation between Templates and Configurations.
431
+
432
+ **Features:**
433
+ - Responsive page title (`text-xl sm:text-2xl`)
434
+ - Tab navigation with horizontal scroll on mobile
435
+ - Router outlet for child routes
436
+
437
+ ```html
438
+ <div class="p-2 sm:p-4">
439
+ <h1 class="text-xl sm:text-2xl font-bold m-0">Email</h1>
440
+ <p class="text-sm text-muted-color mt-1">Manage email templates...</p>
441
+
442
+ <div class="scrollbar-hide flex gap-1 mb-4 border-b border-surface overflow-x-auto">
443
+ @for (tab of tabs; track tab.id) {
444
+ <a [routerLink]="tab.route" routerLinkActive="tab-active">...</a>
445
+ }
446
+ </div>
447
+
448
+ <router-outlet />
449
+ </div>
450
+ ```
303
451
 
304
452
  ### EmailConfigListComponent
305
453
 
306
454
  Lists email configurations with actions.
307
455
 
308
456
  **Features:**
309
- - Lazy pagination via `p-datatable`
457
+ - Lazy pagination via `p-table` with `[paginator]="totalRecords() > 0"`
458
+ - Horizontal scroll on mobile (`overflow-x-auto -mx-4 sm:mx-0`)
310
459
  - Provider type tags (SMTP, SendGrid, Mailgun)
311
460
  - Active/Inactive status badges
461
+ - Default configuration badge
312
462
  - Test button with dialog for recipient input
313
463
  - Edit and delete actions
314
464
  - Company info display when enabled
315
465
 
316
466
  **Test Dialog:**
317
- Opens a PrimeNG dialog to enter test recipient email instead of browser `prompt()`.
318
467
 
319
468
  ```typescript
320
- readonly showTestDialog = signal(false);
469
+ showTestDialog = false; // Plain property for ngModel
321
470
  readonly selectedConfig = signal<IEmailConfig | null>(null);
471
+ readonly isSendingTest = signal(false);
322
472
  testRecipient = '';
323
473
 
324
474
  onTest(config: IEmailConfig): void {
325
475
  this.selectedConfig.set(config);
326
476
  this.testRecipient = '';
327
- this.showTestDialog.set(true);
477
+ this.showTestDialog = true;
328
478
  }
329
479
 
330
480
  async sendTestEmail(): Promise<void> {
@@ -332,10 +482,7 @@ async sendTestEmail(): Promise<void> {
332
482
  if (!config || !this.testRecipient) return;
333
483
 
334
484
  const response = await firstValueFrom(
335
- this.emailSendService.testConfiguration({
336
- emailConfigId: config.id,
337
- recipient: this.testRecipient,
338
- })
485
+ this.configService.sendTest(config.id, this.testRecipient)
339
486
  );
340
487
 
341
488
  if (response.data?.success) {
@@ -344,7 +491,7 @@ async sendTestEmail(): Promise<void> {
344
491
  summary: 'Success',
345
492
  detail: `Test email sent! Message ID: ${response.data.messageId}`,
346
493
  });
347
- this.showTestDialog.set(false);
494
+ this.showTestDialog = false;
348
495
  }
349
496
  }
350
497
  ```
@@ -354,32 +501,28 @@ async sendTestEmail(): Promise<void> {
354
501
  Create/edit email configuration with dynamic provider fields.
355
502
 
356
503
  **Features:**
504
+ - Responsive grid layout (`grid-cols-1 md:grid-cols-2`)
357
505
  - Dynamic form fields based on selected provider
358
- - SMTP: host, port, secure, username, password
359
- - SendGrid: apiKey
506
+ - SMTP: host, port, secure toggle, username, password
507
+ - SendGrid: apiKey (masked input)
360
508
  - Mailgun: apiKey, domain, region
361
- - Active toggle
362
- - **Set as Default toggle** - marks configuration as the default for sending emails
509
+ - Active toggle and Set as Default toggle
510
+ - Form actions right-aligned at bottom
363
511
  - Create/Edit mode auto-detection
364
512
 
365
- **Default Configuration:**
366
- When `isDefault: true`, this configuration will be used automatically when no `emailConfigId` is provided in send requests.
367
-
368
513
  ### TemplateListComponent
369
514
 
370
515
  Lists email templates with test send functionality.
371
516
 
372
517
  **Features:**
373
- - Lazy pagination via `p-datatable`
518
+ - Lazy pagination with conditional display
519
+ - Responsive table with hidden columns on mobile
374
520
  - HTML/Text type badges (based on `isHtml`)
375
521
  - Active/Inactive status badges
376
- - Test send button with dialog
522
+ - Test send with dynamic variables
377
523
  - Edit and delete actions
378
524
 
379
- **Test Send Dialog:**
380
- - Select email configuration
381
- - Enter recipient email
382
- - **Dynamic variable inputs** extracted from template content
525
+ **Test Send Dialog with Variables:**
383
526
 
384
527
  ```typescript
385
528
  /** Extract variables from template content */
@@ -387,14 +530,12 @@ readonly templateVariables = computed(() => {
387
530
  const template = this.selectedTemplate();
388
531
  if (!template) return [];
389
532
 
390
- // Combine all content sources to extract variables
391
533
  const content = [
392
534
  template.subject,
393
535
  template.htmlContent,
394
536
  template.textContent || '',
395
537
  ].join(' ');
396
538
 
397
- // Find all {{variableName}} patterns
398
539
  const matches = content.matchAll(/\{\{(\w+)\}\}/g);
399
540
  const variables = new Set<string>();
400
541
 
@@ -405,48 +546,112 @@ readonly templateVariables = computed(() => {
405
546
  return Array.from(variables).sort();
406
547
  });
407
548
 
408
- // Variable values bound to form inputs
409
549
  variableValues: Record<string, string> = {};
410
550
  ```
411
551
 
412
552
  ### TemplateFormComponent
413
553
 
414
- Create/edit email template with HTML/Plain Text toggle.
554
+ Create/edit email template with HTML/Plain Text toggle and live preview.
415
555
 
416
556
  **Features:**
417
- - Name, slug, description fields
418
- - Subject line with variable support
419
- - **Editor Mode Toggle** (HTML/Plain Text)
420
- - HTML content editor (when `isHtml: true`)
421
- - Plain text content editor (editable, not read-only)
557
+ - Responsive form grid (`grid-cols-1 md:grid-cols-2`)
558
+ - Name, slug, subject, description fields
559
+ - Subject line with variable support hint
560
+ - Editor Mode Toggle (HTML/Plain Text buttons)
561
+ - Same height content editor and preview (`h-[400px]`)
562
+ - Horizontal scroll for code (`wrap="off"`)
563
+ - Live HTML preview in iframe with white background
422
564
  - Content sync between modes when switching
423
- - Preview with variable highlighting
424
565
  - Active toggle
566
+ - Form actions right-aligned at bottom
425
567
 
426
- **Editor Mode Switching:**
568
+ **Preview with Light Background:**
427
569
 
428
570
  ```typescript
429
- setEditorMode(mode: 'html' | 'text'): void {
430
- const currentMode = this.editorMode();
571
+ getPreviewHtml(): string {
572
+ const baseStyles = `
573
+ <style>
574
+ body {
575
+ font-family: Arial, sans-serif;
576
+ margin: 16px;
577
+ background-color: #ffffff;
578
+ color: #333333;
579
+ }
580
+ img { max-width: 100%; height: auto; }
581
+ </style>
582
+ `;
583
+ return baseStyles + (this.formModel.htmlContent || '');
584
+ }
585
+ ```
431
586
 
432
- // Sync content when switching modes
433
- if (currentMode === 'text' && mode === 'html') {
434
- if (!this.formModel.htmlContent && this.formModel.textContent) {
435
- this.formModel.htmlContent = this.textToHtml(this.formModel.textContent);
436
- }
437
- } else if (currentMode === 'html' && mode === 'text') {
438
- if (!this.formModel.textContent && this.formModel.htmlContent) {
439
- this.formModel.textContent = this.htmlToPlainText(this.formModel.htmlContent);
440
- }
441
- }
587
+ ---
442
588
 
443
- this.editorMode.set(mode);
444
- this.formModel.isHtml = mode === 'html';
445
- }
589
+ ## Responsive Design Patterns
590
+
591
+ ### Tables
592
+
593
+ ```html
594
+ <!-- Horizontal scroll wrapper -->
595
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
596
+ <p-table
597
+ [paginator]="totalRecords() > 0"
598
+ styleClass="p-datatable-sm"
599
+ [tableStyle]="{ 'min-width': '50rem' }"
600
+ >
601
+ <!-- Hidden columns on smaller screens -->
602
+ <th class="hidden md:table-cell">Slug</th>
603
+ <th class="hidden lg:table-cell">Subject</th>
604
+ <th class="hidden xl:table-cell">Created</th>
605
+ </p-table>
606
+ </div>
607
+ ```
608
+
609
+ ### Forms
610
+
611
+ ```html
612
+ <!-- Responsive grid -->
613
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
614
+ <div class="field">...</div>
615
+ <div class="field md:col-span-2">...</div> <!-- Full width on all screens -->
616
+ </div>
617
+
618
+ <!-- Form actions -->
619
+ <div class="flex justify-end gap-2 mt-6 pt-4 border-t border-surface">
620
+ <p-button label="Cancel" severity="secondary" [outlined]="true" routerLink="../" />
621
+ <p-button [label]="isEditMode() ? 'Update' : 'Create'" icon="pi pi-save" />
622
+ </div>
623
+ ```
624
+
625
+ ### Headers
626
+
627
+ ```html
628
+ <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4">
629
+ <div>
630
+ <h3 class="text-lg sm:text-xl font-semibold m-0">Title</h3>
631
+ @if (showCompanyInfo()) {
632
+ <p class="text-sm text-muted-color mt-1">Company: {{ currentCompanyName() }}</p>
633
+ }
634
+ </div>
635
+ <p-button label="Action" styleClass="w-full sm:w-auto" />
636
+ </div>
446
637
  ```
447
638
 
448
639
  ---
449
640
 
641
+ ## Dark Mode Support
642
+
643
+ All components use PrimeNG/Tailwind dark mode classes:
644
+
645
+ | Element | Classes |
646
+ |---------|---------|
647
+ | Borders | `border-surface` |
648
+ | Text | `text-muted-color` |
649
+ | Backgrounds | `bg-surface-0 dark:bg-surface-900`, `bg-surface-100 dark:bg-surface-700` |
650
+ | Code blocks | `bg-surface-100 dark:bg-surface-700` |
651
+ | Hover states | `hover:bg-surface-hover` |
652
+
653
+ ---
654
+
450
655
  ## Usage Examples
451
656
 
452
657
  ### Send Direct Email
@@ -499,42 +704,32 @@ async sendTemplateEmail(): Promise<void> {
499
704
  ### Test Configuration
500
705
 
501
706
  ```typescript
707
+ import { EmailConfigApiService } from '@flusys/ng-email';
708
+
709
+ private readonly configService = inject(EmailConfigApiService);
710
+
502
711
  async testConfig(configId: string, recipient: string): Promise<boolean> {
503
712
  const response = await firstValueFrom(
504
- this.emailSendService.testConfiguration({
505
- emailConfigId: configId,
506
- recipient: recipient,
507
- })
713
+ this.configService.sendTest(configId, recipient)
508
714
  );
509
715
 
510
716
  return response.data?.success ?? false;
511
717
  }
512
718
  ```
513
719
 
514
- ### Create Email Template
720
+ ### Get Template by Slug
515
721
 
516
722
  ```typescript
517
723
  import { EmailTemplateApiService } from '@flusys/ng-email';
518
724
 
519
725
  private readonly templateService = inject(EmailTemplateApiService);
520
726
 
521
- async createTemplate(): Promise<void> {
727
+ async getTemplate(slug: string): Promise<IEmailTemplate | null> {
522
728
  const response = await firstValueFrom(
523
- this.templateService.insert({
524
- name: 'Welcome Email',
525
- slug: 'welcome-email',
526
- description: 'Sent to new users',
527
- subject: 'Welcome {{userName}}!',
528
- isHtml: true,
529
- htmlContent: '<h1>Welcome, {{userName}}!</h1><p>Thank you for joining.</p>',
530
- textContent: 'Welcome, {{userName}}! Thank you for joining.',
531
- isActive: true,
532
- })
729
+ this.templateService.getBySlug(slug)
533
730
  );
534
731
 
535
- if (response.success) {
536
- console.log('Template created:', response.data?.id);
537
- }
732
+ return response.data ?? null;
538
733
  }
539
734
  ```
540
735
 
@@ -546,16 +741,17 @@ All endpoints use **POST-only RPC** style (not REST).
546
741
 
547
742
  | Service | Endpoint | Description |
548
743
  |---------|----------|-------------|
549
- | Config | `POST /email/config/insert` | Create config |
550
- | Config | `POST /email/config/get/:id` | Get config by ID |
551
- | Config | `POST /email/config/get-all` | List configs (paginated) |
552
- | Config | `POST /email/config/update` | Update config |
553
- | Config | `POST /email/config/delete` | Delete config |
554
- | Template | `POST /email/template/insert` | Create template |
555
- | Template | `POST /email/template/get/:id` | Get template by ID |
556
- | Template | `POST /email/template/get-all` | List templates (paginated) |
557
- | Template | `POST /email/template/update` | Update template |
558
- | Template | `POST /email/template/delete` | Delete template |
744
+ | Config | `POST /email/email-config/insert` | Create config |
745
+ | Config | `POST /email/email-config/get/:id` | Get config by ID |
746
+ | Config | `POST /email/email-config/get-all` | List configs (paginated) |
747
+ | Config | `POST /email/email-config/update` | Update config |
748
+ | Config | `POST /email/email-config/delete` | Delete config |
749
+ | Template | `POST /email/email-template/insert` | Create template |
750
+ | Template | `POST /email/email-template/get/:id` | Get template by ID |
751
+ | Template | `POST /email/email-template/get-all` | List templates (paginated) |
752
+ | Template | `POST /email/email-template/get-by-slug` | Get template by slug |
753
+ | Template | `POST /email/email-template/update` | Update template |
754
+ | Template | `POST /email/email-template/delete` | Delete template |
559
755
  | Send | `POST /email/send/direct` | Send email directly |
560
756
  | Send | `POST /email/send/template` | Send using template |
561
757
  | Send | `POST /email/send/test` | Test configuration |
@@ -609,21 +805,29 @@ Always use the test send feature to verify configurations work before using them
609
805
 
610
806
  ```typescript
611
807
  async sendEmail(): Promise<void> {
612
- const response = await firstValueFrom(
613
- this.emailSendService.sendTemplate({ ... })
614
- );
615
-
616
- if (response.data?.success) {
617
- this.messageService.add({
618
- severity: 'success',
619
- summary: 'Email Sent',
620
- detail: `Message ID: ${response.data.messageId}`,
621
- });
622
- } else {
808
+ try {
809
+ const response = await firstValueFrom(
810
+ this.emailSendService.sendTemplate({ ... })
811
+ );
812
+
813
+ if (response.data?.success) {
814
+ this.messageService.add({
815
+ severity: 'success',
816
+ summary: 'Email Sent',
817
+ detail: `Message ID: ${response.data.messageId}`,
818
+ });
819
+ } else {
820
+ this.messageService.add({
821
+ severity: 'error',
822
+ summary: 'Send Failed',
823
+ detail: response.data?.error || 'Unknown error occurred',
824
+ });
825
+ }
826
+ } catch (error: any) {
623
827
  this.messageService.add({
624
828
  severity: 'error',
625
- summary: 'Send Failed',
626
- detail: response.data?.error || 'Unknown error occurred',
829
+ summary: 'Error',
830
+ detail: error?.message || 'Failed to send email.',
627
831
  });
628
832
  }
629
833
  }
@@ -634,13 +838,17 @@ async sendEmail(): Promise<void> {
634
838
  ## Public API Exports
635
839
 
636
840
  ```typescript
841
+ // Enums
842
+ export { EmailProviderEnum } from '@flusys/ng-email';
843
+ export { EmailBlockType } from '@flusys/ng-email';
844
+
637
845
  // Interfaces
638
846
  export {
639
847
  IEmailConfig, ICreateEmailConfigDto, IUpdateEmailConfigDto,
640
848
  ISmtpConfig, ISendGridConfig, IMailgunConfig,
641
849
  IEmailTemplate, ICreateEmailTemplateDto, IUpdateEmailTemplateDto,
642
- ISendEmailDto, ISendTemplateEmailDto, ITestEmailDto,
643
- IEmailAttachmentDto, IEmailSendResult,
850
+ IEmailSchema, IEmailSection, IEmailBlock, IEmailTemplateVariable,
851
+ ISendEmailDto, ISendTemplateEmailDto, ITestSendResult,
644
852
  } from '@flusys/ng-email';
645
853
 
646
854
  // Services
@@ -648,13 +856,17 @@ export {
648
856
  EmailConfigApiService,
649
857
  EmailTemplateApiService,
650
858
  EmailSendService,
859
+ EmailBuilderStateService,
651
860
  } from '@flusys/ng-email';
652
861
 
862
+ // Components
863
+ export { EmailContainerComponent } from '@flusys/ng-email';
864
+
653
865
  // Routes
654
866
  export { EMAIL_ROUTES } from '@flusys/ng-email';
655
867
  ```
656
868
 
657
869
  ---
658
870
 
659
- **Last Updated:** 2026-02-17
871
+ **Last Updated:** 2026-02-21
660
872
  **Angular Version:** 21