MODULE TEMPLATE

Comprehensive guidelines for AI-assisted development within the Editor platform

Module Creation Template - editor2 Platform

Optimized for AI Code Generation Version: 1.0.0 | Last Updated: December 27, 2025


Quick Context

What: Template for creating business modules in editor2 ecosystem
Where: libs/libs-modules/<module-name>/
Library Alias: @libs-modules/<module-name>
Contract: Must implement BrowserModuleContract from @libs-core/module-base


Generation Methods (Choose Based on Use Case)

Method 1: AI Platform Generator (Complete Platforms)

Use for: Multi-module business platforms
Route: /modularity/ai-platform
Output: Complete universe with 5-10 modules
Time: 5-10 minutes

Method 2: Module Workbench (Single Modules - Guided)

Use for: Individual modules with step-by-step guidance
Route: /modularity/workbench
Output: Single Nx library module
Time: 10-15 minutes (visual) or 2-4 hours (code-first)

Method 3: AI Builder (Quick Scaffolding)

Use for: Quick module scaffolding
Route: /modularity/ai-builder
Output: Module definition (can export to Nx library)
Time: 1-2 minutes

Method 4: Modular Universe (Visual Canvas)

Use for: Visual universe building with AI-generated modules
Route: /modular-universe
Output: Visual universe structure
Time: Variable (drag-drop modules)

Module Generation Command (Code-First)

# Generate new module library
npx nx g @nx/angular:lib <module-name> --directory=libs/libs-modules --standalone --changeDetection=OnPush

# Example: Product Catalog
npx nx g @nx/angular:lib product-catalog --directory=libs/libs-modules --standalone --changeDetection=OnPush

# Update tsconfig.base.json paths after generation:
"@libs-modules/<module-name>": ["libs/libs-modules/<module-name>/src/index.ts"]

Required File Structure

libs/libs-modules/<module-name>/
├── src/
│   ├── lib/
│   │   ├── actions/                    # Public actions (cross-module)
│   │   │   ├── index.ts
│   │   │   ├── navigate-to-<entity>.action.ts
│   │   │   ├── create-<entity>.action.ts
│   │   │   ├── update-<entity>.action.ts
│   │   │   └── delete-<entity>.action.ts
│   │   ├── components/                 # UI components (separate files!)
│   │   │   ├── index.ts
│   │   │   ├── <entity>-list/
│   │   │   │   ├── <entity>-list.component.ts
│   │   │   │   ├── <entity>-list.component.html
│   │   │   │   └── <entity>-list.component.scss
│   │   │   ├── <entity>-detail/
│   │   │   │   ├── <entity>-detail.component.ts
│   │   │   │   ├── <entity>-detail.component.html
│   │   │   │   └── <entity>-detail.component.scss
│   │   │   ├── <entity>-form/
│   │   │   │   ├── <entity>-form.component.ts
│   │   │   │   ├── <entity>-form.component.html
│   │   │   │   └── <entity>-form.component.scss
│   │   │   └── <entity>-filters/
│   │   │       ├── <entity>-filters.component.ts
│   │   │       ├── <entity>-filters.component.html
│   │   │       └── <entity>-filters.component.scss
│   │   ├── data/                       # Data access layer
│   │   │   ├── repositories/
│   │   │   │   ├── index.ts
│   │   │   │   └── <entity>.repository.ts
│   │   │   └── index.ts
│   │   ├── embeddables/                # Widget components
│   │   │   ├── index.ts
│   │   │   └── <entity>-widget/
│   │   │       ├── <entity>-widget.component.ts
│   │   │       ├── <entity>-widget.component.html
│   │   │       └── <entity>-widget.component.scss
│   │   ├── events/                     # Domain events
│   │   │   ├── index.ts
│   │   │   └── <entity>.events.ts
│   │   ├── models/                     # Interfaces
│   │   │   ├── index.ts
│   │   │   ├── <entity>.model.ts
│   │   │   └── <entity>-filters.model.ts
│   │   ├── services/                   # Module services
│   │   │   ├── index.ts
│   │   │   └── <entity>-api.service.ts
│   │   ├── <module-name>.module.ts     # Main module class
│   │   └── <module-name>.routes.ts     # Angular routes
│   ├── index.ts                        # Barrel exports
│   └── test-setup.ts
├── project.json
├── tsconfig.json
├── tsconfig.lib.json
├── vite.config.mts
└── README.md

Core Library Imports (MANDATORY)

// Module base contract
import { BrowserModuleContract, ModuleManifest, ModuleDecorator } from '@libs-core/module-base';

// Logging
import { LoggerService } from '@libs-core/log-system';

// Action registry
import { PublicAction, ExecutableAction, ActionContext, ActionParams } from '@libs-core/action-registry';

// Security
import { authGuard, permissionGuard, roleGuard, ContextService } from '@libs-core/security';
import { SystemRoles } from '@libs-core/shared-models';

// Data access
import { BaseRepository } from '@libs-core/data-access';

// Event system
import { EventBusService, DomainEvent } from '@libs-core/event-system';

// Shared models (use these FIRST before creating local types)
import { BaseEntity, AuditFields, ApiResponse, Permission } from '@libs-core/shared-models';

// Flash persistence (editor2-specific)
import { autoFlashState, GgStorage } from '@storage-system';

Component Template (CRITICAL - Separate Files)

TypeScript File (.component.ts)

// libs/libs-modules/<module-name>/src/lib/components/<entity>-list/<entity>-list.component.ts

import { Component, inject, signal, computed, input, output, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, RouterModule } from '@angular/router';
import { LoggerService } from '@libs-core/log-system';
import { autoFlashState } from '@storage-system';
import { <Entity>Repository } from '../../data/repositories/<entity>.repository';
import { <Entity>, <Entity>Status, <Entity>Filters } from '../../models';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'lib-<entity>-list',
  standalone: true,
  imports: [CommonModule, RouterModule],
  templateUrl: './<entity>-list.component.html',     // ✅ Separate file
  styleUrl: './<entity>-list.component.scss',        // ✅ Separate file
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class <Entity>ListComponent implements OnInit {
  private readonly repository = inject(<Entity>Repository);
  private readonly router = inject(Router);
  private readonly logger = inject(LoggerService).child({ source: '<Entity>ListComponent' });

  // Component state
  readonly entities = signal<<Entity>[]>([]);
  readonly loading = signal(false);
  readonly error = signal<string | null>(null);

  // Persistent filters using flash-persistence
  readonly filters = autoFlashState<<Entity>Filters>('<module-name>-<entity>-filters', {
    status: 'all',
    searchTerm: '',
    page: 1,
    pageSize: 20,
  });

  // Computed values
  readonly filteredEntities = computed(() => {
    const filters = this.filters();
    let result = this.entities();

    if (filters.status !== 'all') {
      result = result.filter(e => e.status === filters.status);
    }

    if (filters.searchTerm) {
      const term = filters.searchTerm.toLowerCase();
      result = result.filter(e => e.name.toLowerCase().includes(term));
    }

    return result;
  });

  readonly isEmpty = computed(() => this.filteredEntities().length === 0);
  readonly hasEntities = computed(() => this.entities().length > 0);

  ngOnInit(): void {
    this.loadEntities();
  }

  private async loadEntities(): Promise<void> {
    this.loading.set(true);
    this.error.set(null);

    try {
      // MUST use findAll() from BaseRepository
      const entities = await firstValueFrom(this.repository.findAll());
      this.entities.set(entities);
      this.logger.info('Loaded entities', { count: entities.length });
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Unknown error';
      this.error.set(errorMessage);
      this.logger.error('Failed to load entities', { error: errorMessage });
    } finally {
      this.loading.set(false);
    }
  }

  onFilterChange(newFilters: Partial<<Entity>Filters>): void {
    this.filters.update(current => ({ ...current, ...newFilters }));
  }

  onEntityClick(entity: <Entity>): void {
    void this.router.navigate(['/<base-path>', entity.id]);
  }

  onCreate(): void {
    void this.router.navigate(['/<base-path>/create']);
  }

  async onRefresh(): Promise<void> {
    await this.loadEntities();
  }
}

HTML Template (.component.html)

<!-- <entity>-list/<entity>-list.component.html -->

<div class="page-container">
  <header class="page-header">
    <h1><Entity> Management</h1>
    <div class="header-actions">
      <button class="btn-refresh" (click)="onRefresh()" [disabled]="loading()">
        Refresh
      </button>
      <button class="btn-primary" (click)="onCreate()">
        Create New
      </button>
    </div>
  </header>

  @if (loading()) {
    <div class="loading-state">
      <p>Loading <entities>...</p>
    </div>
  } @else if (error()) {
    <div class="error-state">
      <p>Error: {{ error() }}</p>
      <button (click)="onRefresh()">Retry</button>
    </div>
  } @else {
    <div class="filters-section">
      <div class="filter-group">
        <label>Status:</label>
        <select [value]="filters().status" 
                (change)="onFilterChange({ status: $any($event.target).value })">
          <option value="all">All Status</option>
          <option value="active">Active</option>
          <option value="pending">Pending</option>
          <option value="inactive">Inactive</option>
        </select>
      </div>

      <div class="filter-group">
        <label>Search:</label>
        <input type="text" 
               [value]="filters().searchTerm"
               (input)="onFilterChange({ searchTerm: $any($event.target).value })"
               placeholder="Search <entities>...">
      </div>
    </div>

    @if (isEmpty()) {
      <div class="empty-state">
        <p>No <entities> found</p>
        @if (filters().status !== 'all' || filters().searchTerm) {
          <button (click)="onFilterChange({ status: 'all', searchTerm: '' })">
            Clear Filters
          </button>
        }
      </div>
    } @else {
      <table class="data-table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Status</th>
            <th>Created</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          @for (entity of filteredEntities(); track entity.id) {
            <tr (click)="onEntityClick(entity)">
              <td>{{ entity.name }}</td>
              <td>
                <span class="status-badge" [class]="'status-' + entity.status">
                  {{ entity.status }}
                </span>
              </td>
              <td>{{ entity.createdAt | date:'short' }}</td>
              <td class="actions-cell">
                <a [routerLink]="['../', entity.id]">View</a>
                <a [routerLink]="['../', entity.id, 'edit']">Edit</a>
              </td>
            </tr>
          }
        </tbody>
      </table>

      <div class="pagination">
        <p>Showing {{ filteredEntities().length }} of {{ entities().length }} <entities></p>
      </div>
    }
  }
</div>

SCSS Styles (.component.scss)

// <entity>-list/<entity>-list.component.scss

.page-container {
  padding: var(--spacing-4);
  backdrop-filter: blur(16px);
  background: rgba(var(--ux-surface-rgb), 0.8);
  border-radius: var(--radius-lg);
  min-height: 100%;
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--spacing-6);
  padding-bottom: var(--spacing-4);
  border-bottom: 1px solid var(--ux-border);

  h1 {
    margin: 0;
    color: var(--ux-text-primary);
    font-size: var(--font-size-2xl);
    font-weight: 600;
  }

  .header-actions {
    display: flex;
    gap: var(--spacing-3);

    button {
      padding: var(--spacing-2) var(--spacing-4);
      border-radius: var(--radius-md);
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s ease;

      &:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }
    }

    .btn-refresh {
      background: rgba(var(--ux-surface-rgb), 0.6);
      border: 1px solid var(--ux-border);
      color: var(--ux-text-primary);

      &:hover:not(:disabled) {
        background: rgba(var(--ux-surface-rgb), 0.8);
      }
    }

    .btn-primary {
      background: var(--ux-primary);
      border: none;
      color: white;

      &:hover:not(:disabled) {
        background: var(--ux-primary-hover);
      }
    }
  }
}

.filters-section {
  display: flex;
  gap: var(--spacing-4);
  margin-bottom: var(--spacing-4);
  padding: var(--spacing-4);
  backdrop-filter: blur(8px);
  background: rgba(var(--ux-surface-rgb), 0.4);
  border-radius: var(--radius-md);

  .filter-group {
    display: flex;
    align-items: center;
    gap: var(--spacing-2);

    label {
      color: var(--ux-text-secondary);
      font-size: var(--font-size-sm);
      font-weight: 500;
    }

    select,
    input {
      padding: var(--spacing-2) var(--spacing-3);
      background: rgba(var(--ux-surface-rgb), 0.6);
      border: 1px solid var(--ux-border);
      border-radius: var(--radius-sm);
      color: var(--ux-text-primary);
      font-size: var(--font-size-sm);

      &:focus {
        outline: none;
        border-color: var(--ux-primary);
      }
    }
  }
}

.loading-state,
.error-state,
.empty-state {
  padding: var(--spacing-12);
  text-align: center;

  p {
    color: var(--ux-text-secondary);
    font-size: var(--font-size-lg);
    margin: 0 0 var(--spacing-4) 0;
  }

  button {
    padding: var(--spacing-2) var(--spacing-4);
    background: var(--ux-primary);
    border: none;
    border-radius: var(--radius-md);
    color: white;
    cursor: pointer;

    &:hover {
      background: var(--ux-primary-hover);
    }
  }
}

.error-state p {
  color: var(--ux-error);
}

.data-table {
  width: 100%;
  border-collapse: collapse;

  thead {
    background: rgba(var(--ux-surface-rgb), 0.6);

    th {
      padding: var(--spacing-3) var(--spacing-4);
      text-align: left;
      color: var(--ux-text-secondary);
      font-size: var(--font-size-sm);
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      border-bottom: 2px solid var(--ux-border);
    }
  }

  tbody {
    tr {
      border-bottom: 1px solid var(--ux-border);
      cursor: pointer;
      transition: background 0.2s ease;

      &:hover {
        background: rgba(var(--ux-primary-rgb), 0.05);
      }

      td {
        padding: var(--spacing-3) var(--spacing-4);
        color: var(--ux-text-primary);
        font-size: var(--font-size-sm);
      }

      .status-badge {
        padding: var(--spacing-1) var(--spacing-2);
        border-radius: var(--radius-sm);
        font-size: var(--font-size-xs);
        font-weight: 500;

        &.status-active {
          background: rgba(var(--ux-success-rgb), 0.1);
          color: var(--ux-success);
        }

        &.status-pending {
          background: rgba(var(--ux-warning-rgb), 0.1);
          color: var(--ux-warning);
        }

        &.status-inactive {
          background: rgba(var(--ux-error-rgb), 0.1);
          color: var(--ux-error);
        }
      }

      .actions-cell {
        display: flex;
        gap: var(--spacing-2);

        a {
          color: var(--ux-primary);
          text-decoration: none;
          font-weight: 500;

          &:hover {
            text-decoration: underline;
          }
        }
      }
    }
  }
}

.pagination {
  margin-top: var(--spacing-4);
  padding: var(--spacing-3);
  text-align: center;

  p {
    margin: 0;
    color: var(--ux-text-secondary);
    font-size: var(--font-size-sm);
  }
}

Widget Component Template (Embeddables)

TypeScript File

// embeddables/<entity>-widget/<entity>-widget.component.ts

import { Component, inject, signal, computed, input, output, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { LoggerService } from '@libs-core/log-system';
import { <Entity>Repository } from '../../data/repositories/<entity>.repository';
import { <Entity> } from '../../models/<entity>.model';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'lib-<entity>-widget',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './<entity>-widget.component.html',
  styleUrl: './<entity>-widget.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class <Entity>WidgetComponent implements OnInit {
  private readonly repository = inject(<Entity>Repository);
  private readonly router = inject(Router);
  private readonly logger = inject(LoggerService).child({ source: '<Entity>WidgetComponent' });

  // Widget inputs (defined in manifest.embeddableComponents)
  readonly widgetId = input<string>('');
  readonly title = input<string>('<Entities>');
  readonly limit = input<number>(5);
  readonly showStatus = input<boolean>(true);

  // Widget outputs
  readonly entitySelected = output<<Entity>>();

  // Component state
  readonly entities = signal<<Entity>[]>([]);
  readonly loading = signal(false);
  readonly error = signal<string | null>(null);

  // Computed
  readonly displayedEntities = computed(() => 
    this.entities().slice(0, this.limit())
  );
  readonly isEmpty = computed(() => this.displayedEntities().length === 0);

  ngOnInit(): void {
    this.loadEntities();
  }

  private async loadEntities(): Promise<void> {
    this.loading.set(true);
    this.error.set(null);

    try {
      const entities = await firstValueFrom(this.repository.findAll());
      this.entities.set(entities);
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Unknown error';
      this.error.set(errorMessage);
      this.logger.error('Failed to load entities', { error: errorMessage });
    } finally {
      this.loading.set(false);
    }
  }

  onEntityClick(entity: <Entity>): void {
    this.entitySelected.emit(entity);
  }

  onViewAll(): void {
    void this.router.navigate(['/<base-path>']);
  }
}

HTML Template

<!-- <entity>-widget/<entity>-widget.component.html -->

<div class="widget-container">
  <div class="widget-header">
    <h3>{{ title() }}</h3>
    <button class="view-all-btn" (click)="onViewAll()">
      View All
    </button>
  </div>

  @if (loading()) {
    <div class="widget-loading">
      <p>Loading...</p>
    </div>
  } @else if (error()) {
    <div class="widget-error">
      <p>{{ error() }}</p>
    </div>
  } @else if (isEmpty()) {
    <div class="widget-empty">
      <p>No <entities> found</p>
    </div>
  } @else {
    <ul class="widget-list">
      @for (entity of displayedEntities(); track entity.id) {
        <li class="widget-item" (click)="onEntityClick(entity)">
          <span class="entity-name">{{ entity.name }}</span>
          @if (showStatus() && entity.status) {
            <span class="entity-status" [class]="'status-' + entity.status">
              {{ entity.status }}
            </span>
          }
        </li>
      }
    </ul>
  }
</div>

SCSS Styles

// <entity>-widget/<entity>-widget.component.scss

.widget-container {
  padding: var(--spacing-4);
  backdrop-filter: blur(12px);
  background: rgba(var(--ux-surface-rgb), 0.8);
  border: 1px solid var(--ux-border);
  border-radius: var(--radius-lg);
  height: 100%;
  display: flex;
  flex-direction: column;
}

.widget-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--spacing-4);
  padding-bottom: var(--spacing-3);
  border-bottom: 1px solid var(--ux-border);

  h3 {
    margin: 0;
    color: var(--ux-text-primary);
    font-size: var(--font-size-lg);
    font-weight: 600;
  }

  .view-all-btn {
    padding: var(--spacing-1) var(--spacing-3);
    background: transparent;
    border: 1px solid var(--ux-primary);
    border-radius: var(--radius-sm);
    color: var(--ux-primary);
    font-size: var(--font-size-sm);
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;

    &:hover {
      background: var(--ux-primary);
      color: white;
    }
  }
}

.widget-loading,
.widget-error,
.widget-empty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;

  p {
    margin: 0;
    color: var(--ux-text-secondary);
    font-size: var(--font-size-sm);
  }
}

.widget-error p {
  color: var(--ux-error);
}

.widget-list {
  list-style: none;
  padding: 0;
  margin: 0;
  flex: 1;
  overflow-y: auto;
}

.widget-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: var(--spacing-3);
  border-bottom: 1px solid var(--ux-border);
  cursor: pointer;
  transition: background 0.2s ease;

  &:last-child {
    border-bottom: none;
  }

  &:hover {
    background: rgba(var(--ux-primary-rgb), 0.05);
  }

  .entity-name {
    color: var(--ux-text-primary);
    font-size: var(--font-size-sm);
    font-weight: 500;
  }

  .entity-status {
    padding: var(--spacing-1) var(--spacing-2);
    border-radius: var(--radius-sm);
    font-size: var(--font-size-xs);
    font-weight: 500;

    &.status-active {
      background: rgba(var(--ux-success-rgb), 0.1);
      color: var(--ux-success);
    }

    &.status-pending {
      background: rgba(var(--ux-warning-rgb), 0.1);
      color: var(--ux-warning);
    }

    &.status-inactive {
      background: rgba(var(--ux-error-rgb), 0.1);
      color: var(--ux-error);
    }
  }
}

Module Class Template

// <module-name>/<module-name>.module.ts

import { Injectable, inject } from '@angular/core';
import { Observable, of } from 'rxjs';
import { BrowserModuleContract, ModuleManifest, ModuleDecorator } from '@libs-core/module-base';
import { LoggerService } from '@libs-core/log-system';
import { EventBusService } from '@libs-core/event-system';
import { <Entity>Repository } from './data/repositories/<entity>.repository';

@ModuleDecorator({
  id: '<module-name>',
  version: '1.0.0',
  name: '<Module Name>',
  description: '<Module description>',
  permissions: ['<resource>:read', '<resource>:write']
})
@Injectable({ providedIn: 'root' })
export class <ModuleName>Module implements BrowserModuleContract {
  private readonly logger = inject(LoggerService).child({ source: '<ModuleName>Module' });
  private readonly eventBus = inject(EventBusService);
  private readonly repository = inject(<Entity>Repository);

  readonly manifest: ModuleManifest = {
    id: '<module-name>',
    version: '1.0.0',
    name: '<Module Name>',
    description: '<Module description>',
    permissions: ['<resource>:read', '<resource>:write'],
    publicActions: [
      {
        id: '<resource>:navigate-to-<entity>',
        name: 'Navigate to <Entity>',
        description: 'Navigate to <entity> detail page',
        parameters: [{ name: '<entity>Id', type: 'string', required: true }],
      },
      // ... more actions
    ],
    embeddableComponents: [
      {
        id: '<entity>-widget',
        name: '<Entity> Widget',
        description: 'Display list of <entities>',
        inputs: [
          { name: 'limit', type: 'number', default: 5 },
          { name: 'showStatus', type: 'boolean', default: true },
        ],
        outputs: [
          { name: 'entitySelected', type: '<Entity>' },
        ],
      },
    ],
  };

  configure<T = Record<string, unknown>>(config: T): Observable<void> {
    this.logger.info('Configuring module', config);
    return of(void 0);
  }

  initialize(): Observable<void> {
    this.logger.info('Initializing module');
    // Setup services, load initial data if needed
    return of(void 0);
  }

  start(): Observable<void> {
    this.logger.info('Starting module');
    this.eventBus.emit({
      type: 'MODULE_STARTED',
      payload: { moduleId: this.manifest.id },
    });
    return of(void 0);
  }

  stop(): Observable<void> {
    this.logger.info('Stopping module');
    return of(void 0);
  }

  destroy(): Observable<void> {
    this.logger.info('Destroying module');
    return of(void 0);
  }
}

Routes Template

// <module-name>/<module-name>.routes.ts

import { Routes } from '@angular/router';
import { authGuard, permissionGuard, roleGuard } from '@libs-core/security';
import { SystemRoles } from '@libs-core/shared-models';

export const <moduleName>Routes: Routes = [
  {
    path: '',
    redirectTo: 'list',
    pathMatch: 'full',
  },
  {
    path: 'list',
    loadComponent: () => import('./components/<entity>-list/<entity>-list.component')
      .then(m => m.<Entity>ListComponent),
    canActivate: [authGuard, permissionGuard('<resource>:read')],
    data: {
      title: '<Entities>',
      breadcrumb: '<Entities>',
      permissions: ['<resource>:read'],
      animation: 'fade',
    },
  },
  {
    path: 'create',
    loadComponent: () => import('./components/<entity>-form/<entity>-form.component')
      .then(m => m.<Entity>FormComponent),
    canActivate: [authGuard, permissionGuard('<resource>:create')],
    data: {
      title: 'Create <Entity>',
      breadcrumb: 'Create',
      permissions: ['<resource>:create'],
      mode: 'create',
      animation: 'slideLeft',
    },
  },
  {
    path: ':id',
    loadComponent: () => import('./components/<entity>-detail/<entity>-detail.component')
      .then(m => m.<Entity>DetailComponent),
    canActivate: [authGuard, permissionGuard('<resource>:read')],
    data: {
      title: '<Entity> Detail',
      breadcrumb: 'Detail',
      permissions: ['<resource>:read'],
      animation: 'slideLeft',
    },
  },
  {
    path: ':id/edit',
    loadComponent: () => import('./components/<entity>-form/<entity>-form.component')
      .then(m => m.<Entity>FormComponent),
    canActivate: [authGuard, permissionGuard('<resource>:update')],
    data: {
      title: 'Edit <Entity>',
      breadcrumb: 'Edit',
      permissions: ['<resource>:update'],
      mode: 'edit',
      animation: 'slideLeft',
    },
  },
];

Model Template

// models/<entity>.model.ts

import { BaseEntity } from '@libs-core/shared-models';

/**
 * <Entity> status enum
 */
export type <Entity>Status = 'active' | 'inactive' | 'pending';

/**
 * <Entity> interface
 * Extends BaseEntity which provides: id, createdAt, updatedAt
 */
export interface <Entity> extends BaseEntity {
  name: string;
  description: string;
  status: <Entity>Status;
  // Add entity-specific fields
}

/**
 * Create <Entity> request
 */
export interface Create<Entity>Request {
  name: string;
  description: string;
  status?: <Entity>Status;
}

/**
 * Update <Entity> request
 */
export interface Update<Entity>Request {
  name?: string;
  description?: string;
  status?: <Entity>Status;
}

/**
 * <Entity> filters
 */
export interface <Entity>Filters {
  status?: <Entity>Status | 'all';
  searchTerm?: string;
  page?: number;
  pageSize?: number;
}

Action Template

// actions/navigate-to-<entity>.action.ts

import { Injectable, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PublicAction, ExecutableAction, ActionContext, ActionParams } from '@libs-core/action-registry';
import { LoggerService } from '@libs-core/log-system';

export interface NavigateTo<Entity>Input extends ActionParams {
  <entity>Id: string;
  mode?: 'detail' | 'edit';
}

export interface NavigateTo<Entity>Output {
  success: boolean;
  targetRoute: string;
}

@PublicAction({
  id: '<resource>:navigate-to-<entity>',
  name: 'Navigate to <Entity>',
  description: 'Navigate to <entity> detail or edit page',
  permissions: ['<resource>:read'],
  tags: ['navigation', '<resource>'],
  parameters: [
    {
      name: '<entity>Id',
      type: 'string',
      required: true,
      description: '<Entity> ID to navigate to',
    },
    {
      name: 'mode',
      type: 'string',
      required: false,
      description: 'Navigation mode (detail or edit)',
      default: 'detail',
    },
  ],
  config: { category: 'navigation', module: '<module-name>' },
})
@Injectable({ providedIn: 'root' })
export class NavigateTo<Entity>Action implements ExecutableAction<NavigateTo<Entity>Output> {
  readonly metadata = {
    id: '<resource>:navigate-to-<entity>',
    name: 'Navigate to <Entity>',
    description: 'Navigate to <entity> detail or edit page',
    requiredPermissions: ['<resource>:read'] as const,
    category: 'navigation',
    enabled: true,
  };

  private readonly router = inject(Router);
  private readonly logger = inject(LoggerService).child({ source: 'NavigateTo<Entity>Action' });

  execute(
    params: NavigateTo<Entity>Input,
    context: ActionContext
  ): Observable<NavigateTo<Entity>Output> {
    this.logger.info('Navigating to <entity>', { <entity>Id: params.<entity>Id, mode: params.mode });

    const route = params.mode === 'edit'
      ? ['/<base-path>', params.<entity>Id, 'edit']
      : ['/<base-path>', params.<entity>Id];

    return of({ success: true, targetRoute: route.join('/') }).pipe(
      tap(() => this.router.navigate(route))
    );
  }

  validate(params: NavigateTo<Entity>Input): Observable<boolean> {
    return of(!!params.<entity>Id);
  }

  cleanup(): void {
    // Cleanup if needed
  }
}

Service Template

// services/<entity>-api.service.ts

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { LoggerService } from '@libs-core/log-system';
import { ApiResponse } from '@libs-core/shared-models';
import { <Entity>, <Entity>Filters, Create<Entity>Request, Update<Entity>Request } from '../models';

@Injectable({ providedIn: 'root' })
export class <Entity>ApiService {
  private readonly http = inject(HttpClient);
  private readonly logger = inject(LoggerService).child({ source: '<Entity>ApiService' });
  private readonly baseUrl = '/api/<entities>';

  get<Entities>(filters: <Entity>Filters): Observable<ApiResponse<{ <entities>: <Entity>[] }>> {
    this.logger.debug('Fetching <entities>', { filters });
    
    const params = new URLSearchParams();
    if (filters.status && filters.status !== 'all') {
      params.set('status', filters.status);
    }
    if (filters.searchTerm) {
      params.set('q', filters.searchTerm);
    }
    if (filters.page) {
      params.set('page', filters.page.toString());
    }
    if (filters.pageSize) {
      params.set('pageSize', filters.pageSize.toString());
    }

    return this.http.get<ApiResponse<{ <entities>: <Entity>[] }>>(
      `${this.baseUrl}?${params.toString()}`
    );
  }

  get<Entity>ById(id: string): Observable<ApiResponse<<Entity>>> {
    this.logger.debug('Fetching <entity> by id', { id });
    return this.http.get<ApiResponse<<Entity>>>(`${this.baseUrl}/${id}`);
  }

  create<Entity>(data: Create<Entity>Request): Observable<ApiResponse<<Entity>>> {
    this.logger.debug('Creating <entity>', { data });
    return this.http.post<ApiResponse<<Entity>>>(this.baseUrl, data);
  }

  update<Entity>(id: string, data: Update<Entity>Request): Observable<ApiResponse<<Entity>>> {
    this.logger.debug('Updating <entity>', { id, data });
    return this.http.put<ApiResponse<<Entity>>>(`${this.baseUrl}/${id}`, data);
  }

  delete<Entity>(id: string): Observable<ApiResponse<void>> {
    this.logger.debug('Deleting <entity>', { id });
    return this.http.delete<ApiResponse<void>>(`${this.baseUrl}/${id}`);
  }
}

Event Template

// events/<entity>.events.ts

import { DomainEvent, DomainEventPayload } from '@libs-core/event-system';
import { <Entity> } from '../models/<entity>.model';

// <Entity> Created Event
export interface <Entity>CreatedPayload extends DomainEventPayload {
  <entity>: <Entity>;
  createdBy: string;
}

export class <Entity>CreatedEvent extends DomainEvent<<Entity>CreatedPayload> {
  static readonly TYPE = '<entity>.created';

  constructor(payload: <Entity>CreatedPayload) {
    super(<Entity>CreatedEvent.TYPE, '<module-name>', payload);
  }
}

// <Entity> Updated Event
export interface <Entity>UpdatedPayload extends DomainEventPayload {
  <entity>: <Entity>;
  changes: Partial<<Entity>>;
  updatedBy: string;
}

export class <Entity>UpdatedEvent extends DomainEvent<<Entity>UpdatedPayload> {
  static readonly TYPE = '<entity>.updated';

  constructor(payload: <Entity>UpdatedPayload) {
    super(<Entity>UpdatedEvent.TYPE, '<module-name>', payload);
  }
}

// <Entity> Deleted Event
export interface <Entity>DeletedPayload extends DomainEventPayload {
  <entity>Id: string;
  deletedBy: string;
}

export class <Entity>DeletedEvent extends DomainEvent<<Entity>DeletedPayload> {
  static readonly TYPE = '<entity>.deleted';

  constructor(payload: <Entity>DeletedPayload) {
    super(<Entity>DeletedEvent.TYPE, '<module-name>', payload);
  }
}

Placeholder Variables

When generating a module, replace these placeholders:

Placeholder Example Description
<module-name> product-catalog Module folder name (kebab-case)
<module-id> product-catalog Module ID in manifest
<ModuleName> ProductCatalog Module class prefix (PascalCase)
<moduleName> productCatalog Module variable name (camelCase)
<Entity> Product Main entity (PascalCase)
<entity> product Entity lowercase
<entities> products Entity plural lowercase
<Entities> Products Entity plural (PascalCase)
<base-path> products URL base path
<resource> products Permission resource name

Module Lifecycle

All modules must implement these lifecycle methods:

REGISTERED → CONFIGURED → INITIALIZED → STARTED → STOPPED → DESTROYED
configure<T>(config: T): Observable<void>    // Apply configuration
initialize(): Observable<void>               // Setup services, connections
start(): Observable<void>                    // Start the module
stop(): Observable<void>                     // Stop the module
destroy(): Observable<void>                  // Cleanup resources

Integration with editor2 Systems

Flash Persistence (MUST)

// For persistent UI state
readonly settings = autoFlashState('<module-name>-settings', {
  viewMode: 'grid',
  sortBy: 'name',
});

// Flash persistence handles:
// ✅ Automatic IndexedDB sync
// ✅ Tenant context
// ✅ Conflict resolution

Theme System (MUST)

// Always use CSS variables
.my-component {
  background: var(--ux-surface);
  color: var(--ux-text-primary);
  backdrop-filter: blur(16px);
  background: rgba(var(--ux-surface-rgb), 0.8);
}

Intelligence Pyramid (MUST)

// Before generation
await mcp.preflight({ task: 'Generate product-catalog module' });

// After generation
await mcp.log_decision({
  type: 'implementation',
  question: 'How to structure product-catalog module?',
  chosen: 'CRUD with inventory tracking',
  reasoning: 'User requested stock management',
  confidence: 0.9,
  files: ['product-catalog.module.ts'],
});

Validation Checklist

Before marking module complete:

Structure Validation

Integration Validation

Quality Validation

Documentation


Related Documentation


Version: 1.0.0
Last Updated: December 27, 2025
Platform: editor2
Maintained By: editor2 development team