Skip to content

Angular Version โ€‹

HaloLight Angular version is built on Angular 21 with Signals + Standalone Components + TypeScript.

Live Preview: https://halolight-angular.h7ml.cn/

GitHub: https://github.com/halolight/halolight-angular

Features โ€‹

  • ๐Ÿ—๏ธ Angular 21 - Latest enterprise framework with Signals + Standalone Components
  • โšก NgRx Signals - Lightweight reactive state management
  • ๐ŸŽจ Theme System - 11 skins, light/dark mode, View Transitions
  • ๐Ÿ” Authentication - Complete login/register/password recovery flow
  • ๐Ÿ“Š Dashboard - Data visualization and business management
  • ๐Ÿ›ก๏ธ Permission Control - RBAC fine-grained permission management
  • ๐Ÿ“‘ Multi-tabs - Tab bar management
  • โŒ˜ Command Palette - Keyboard shortcuts navigation

Tech Stack โ€‹

TechnologyVersionDescription
Angular21.xEnterprise framework
TypeScript5.xType safety
Tailwind CSS4.xAtomic CSS
spartan/uilatestUI component library (Radix-style)
NgRx Signals21.xReactive state management
TanStack Query5.xServer state
Mock.js1.xData mocking

Core Features โ€‹

  • Configurable Dashboard - 9 widgets, drag-and-drop layout, responsive design
  • Multi-tab Navigation - Browser-style tabs, context menu, state caching
  • Permission System - RBAC permission control, route guards, permission directives/components
  • Theme System - 11 skins, light/dark mode, View Transitions
  • Multi-account Switching - Quick account switching, remember login state
  • Command Palette - Keyboard shortcuts (โŒ˜K), global search
  • Real-time Notifications - WebSocket push, notification center

Directory Structure โ€‹

halolight-angular/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ app/
โ”‚   โ”‚   โ”œโ”€โ”€ pages/                  # Page components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ admin/             # Admin pages
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dashboard/     # Dashboard
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ users/         # User management
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ roles/         # Role management
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ permissions/   # Permission management
โ”‚   โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ settings/      # System settings
โ”‚   โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ profile/       # User profile
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ auth/              # Auth pages
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ login/
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ register/
โ”‚   โ”‚   โ”‚       โ”œโ”€โ”€ forgot-password/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ reset-password/
โ”‚   โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ui/                # spartan/ui components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ layout/            # Layout components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ dashboard/         # Dashboard components
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ shared/            # Shared components
โ”‚   โ”‚   โ”œโ”€โ”€ services/              # Service layer
โ”‚   โ”‚   โ”œโ”€โ”€ stores/                # NgRx Signals Stores
โ”‚   โ”‚   โ”œโ”€โ”€ guards/                # Route guards
โ”‚   โ”‚   โ”œโ”€โ”€ interceptors/          # HTTP interceptors
โ”‚   โ”‚   โ”œโ”€โ”€ directives/            # Directives
โ”‚   โ”‚   โ”œโ”€โ”€ pipes/                 # Pipes
โ”‚   โ”‚   โ”œโ”€โ”€ lib/                   # Utility library
โ”‚   โ”‚   โ”œโ”€โ”€ types/                 # Type definitions
โ”‚   โ”‚   โ”œโ”€โ”€ mocks/                 # Mock data
โ”‚   โ”‚   โ”œโ”€โ”€ app.routes.ts          # Route configuration
โ”‚   โ”‚   โ”œโ”€โ”€ app.config.ts          # App configuration
โ”‚   โ”‚   โ””โ”€โ”€ app.component.ts       # Root component
โ”‚   โ”œโ”€โ”€ environments/              # Environment configuration
โ”‚   โ””โ”€โ”€ styles.css                 # Global styles
โ”œโ”€โ”€ public/                        # Static assets
โ”œโ”€โ”€ angular.json
โ”œโ”€โ”€ tailwind.config.js
โ”œโ”€โ”€ tsconfig.json
โ””โ”€โ”€ package.json

Quick Start โ€‹

Environment Requirements โ€‹

  • Node.js >= 18.0.0
  • pnpm >= 9.x

Installation โ€‹

bash
git clone https://github.com/halolight/halolight-angular.git
cd halolight-angular
pnpm install

Environment Variables โ€‹

bash
cp src/environments/environment.example.ts src/environments/environment.development.ts
ts
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: '/api',
  useMock: true,
  appTitle: 'Admin Pro',
  brandName: 'Halolight',
  demoEmail: 'admin@halolight.h7ml.cn',
  demoPassword: '123456',
  showDemoHint: true,
};

Start Development โ€‹

bash
pnpm start

Visit http://localhost:4200

Build for Production โ€‹

bash
pnpm build
ng build --configuration production

Demo Account โ€‹

RoleEmailPassword
Adminadmin@halolight.h7ml.cn123456
Useruser@halolight.h7ml.cn123456

Core Functionality โ€‹

State Management (NgRx Signals) โ€‹

ts
// stores/auth.store.ts
import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { computed, inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

interface AuthState {
  user: User | null;
  token: string | null;
  loading: boolean;
}

const initialState: AuthState = {
  user: null,
  token: null,
  loading: false,
};

export const AuthStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    isAuthenticated: computed(() => !!store.token() && !!store.user()),
    permissions: computed(() => store.user()?.permissions ?? []),
  })),
  withMethods((store, authService = inject(AuthService)) => ({
    async login(credentials: LoginCredentials) {
      patchState(store, { loading: true });
      try {
        const response = await authService.login(credentials);
        patchState(store, {
          user: response.user,
          token: response.token,
          loading: false,
        });
      } catch (error) {
        patchState(store, { loading: false });
        throw error;
      }
    },

    logout() {
      patchState(store, { user: null, token: null });
    },

    hasPermission(permission: string): boolean {
      const permissions = store.permissions();
      return permissions.some(p =>
        p === '*' || p === permission ||
        (p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
      );
    },
  }))
);

Data Fetching (TanStack Query) โ€‹

ts
// services/users.service.ts
import { Injectable, inject } from '@angular/core';
import { injectQuery, injectMutation, injectQueryClient } from '@tanstack/angular-query-experimental';
import { ApiService } from './api.service';

@Injectable({ providedIn: 'root' })
export class UsersService {
  private api = inject(ApiService);
  private queryClient = injectQueryClient();

  getUsers(params?: UserQueryParams) {
    return injectQuery(() => ({
      queryKey: ['users', params],
      queryFn: () => this.api.get<UserListResponse>('/users', { params }),
    }));
  }

  createUser() {
    return injectMutation(() => ({
      mutationFn: (data: CreateUserDto) => this.api.post<User>('/users', data),
      onSuccess: () => {
        this.queryClient.invalidateQueries({ queryKey: ['users'] });
      },
    }));
  }
}

Permission Control โ€‹

ts
// directives/permission.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef, inject, effect } from '@angular/core';
import { AuthStore } from '../stores/auth.store';

@Directive({
  selector: '[appPermission]',
  standalone: true,
})
export class PermissionDirective {
  private templateRef = inject(TemplateRef<unknown>);
  private viewContainer = inject(ViewContainerRef);
  private authStore = inject(AuthStore);

  @Input() set appPermission(permission: string) {
    effect(() => {
      const hasPermission = this.authStore.hasPermission(permission);
      this.viewContainer.clear();
      if (hasPermission) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      }
    });
  }
}
html
<!-- Using directive -->
<button *appPermission="'users:delete'">Delete</button>
ts
// components/permission-guard.component.ts
import { Component, Input, inject, computed } from '@angular/core';
import { AuthStore } from '../../stores/auth.store';

@Component({
  selector: 'app-permission-guard',
  standalone: true,
  template: `
    @if (hasPermission()) {
      <ng-content />
    } @else {
      <ng-content select="[fallback]" />
    }
  `,
})
export class PermissionGuardComponent {
  @Input({ required: true }) permission!: string;

  private authStore = inject(AuthStore);
  hasPermission = computed(() => this.authStore.hasPermission(this.permission));
}
html
<!-- Using component -->
<app-permission-guard permission="users:delete">
  <app-delete-button />
  <span fallback>No permission</span>
</app-permission-guard>

Draggable Dashboard โ€‹

ts
// components/dashboard/dashboard-grid.component.ts
import { Component, inject, computed } from '@angular/core';
import { GridsterModule, GridsterConfig, GridsterItem } from 'angular-gridster2';
import { DashboardStore } from '../../stores/dashboard.store';

@Component({
  selector: 'app-dashboard-grid',
  standalone: true,
  imports: [GridsterModule, WidgetWrapperComponent],
  template: `
    <gridster [options]="options()">
      @for (widget of widgets(); track widget.id) {
        <gridster-item [item]="widget">
          <app-widget-wrapper [widget]="widget" />
        </gridster-item>
      }
    </gridster>
  `,
})
export class DashboardGridComponent {
  private dashboardStore = inject(DashboardStore);

  widgets = this.dashboardStore.widgets;
  isEditing = this.dashboardStore.isEditing;

  options = computed<GridsterConfig>(() => ({
    gridType: 'fit',
    displayGrid: this.isEditing() ? 'always' : 'none',
    draggable: { enabled: this.isEditing() },
    resizable: { enabled: this.isEditing() },
    pushItems: true,
    minCols: 12,
    maxCols: 12,
    minRows: 4,
    defaultItemCols: 3,
    defaultItemRows: 2,
    itemChangeCallback: (item) => this.dashboardStore.updateWidget(item),
  }));
}

Theme System โ€‹

Skin Presets โ€‹

Supports 11 preset skins, switch via the quick settings panel:

SkinPrimary ColorCSS Variable
DefaultPurple--primary: 51.1% 0.262 276.97
BlueBlue--primary: 54.8% 0.243 264.05
EmeraldEmerald--primary: 64.6% 0.178 142.49
RoseRose--primary: 59.3% 0.214 12.76
OrangeOrange--primary: 65.4% 0.194 35.76
AmberAmber--primary: 74.2% 0.167 83.25
YellowYellow--primary: 84.5% 0.181 99.58
LimeLime--primary: 76.5% 0.165 128.35
TealTeal--primary: 59.8% 0.134 179.61
CyanCyan--primary: 68.3% 0.148 192.18
SkySky--primary: 68.5% 0.171 227.08

CSS Variables (OKLch) โ€‹

css
/* Example variable definitions */
:root {
  --background: 100% 0 0;
  --foreground: 14.9% 0.017 285.75;
  --primary: 51.1% 0.262 276.97;
  --primary-foreground: 98% 0 0;
  --secondary: 96.1% 0.002 286.08;
  --secondary-foreground: 14.9% 0.017 285.75;
  --muted: 96.1% 0.002 286.08;
  --muted-foreground: 55.4% 0.009 285.82;
  --accent: 96.1% 0.002 286.08;
  --accent-foreground: 14.9% 0.017 285.75;
  --border: 92.2% 0.004 285.86;
  --input: 92.2% 0.004 285.86;
  --ring: 51.1% 0.262 276.97;
}

.dark {
  --background: 14.9% 0.017 285.75;
  --foreground: 98% 0 0;
  --primary: 56.1% 0.287 277.04;
  /* ... */
}

Theme Switching โ€‹

ts
// Toggle theme
const uiSettingsStore = inject(UiSettingsStore);
uiSettingsStore.setTheme('dark'); // 'light' | 'dark' | 'system'

// Change skin
uiSettingsStore.setSkin('rose'); // 11 skin presets

Page Routes โ€‹

PathPagePermission
/Redirect to /dashboard-
/loginLoginPublic
/registerRegisterPublic
/forgot-passwordForgot passwordPublic
/reset-passwordReset passwordPublic
/dashboardDashboarddashboard:view
/usersUser listusers:list
/users/createCreate userusers:create
/users/:idUser detailsusers:view
/users/:id/editEdit userusers:update
/rolesRole managementroles:list
/permissionsPermission managementpermissions:list
/settingsSystem settingssettings:view
/profileUser profileAuthenticated

Environment Variables โ€‹

Configuration Example โ€‹

ts
// src/environments/environment.development.ts
export const environment = {
  production: false,
  apiUrl: '/api',
  useMock: true,
  appTitle: 'Admin Pro',
  brandName: 'Halolight',
  demoEmail: 'admin@halolight.h7ml.cn',
  demoPassword: '123456',
  showDemoHint: true,
};

Variable Description โ€‹

VariableDescriptionDefault Value
productionProduction environmentfalse
apiUrlAPI base path/api
useMockUse Mock datatrue
appTitleApplication titleAdmin Pro
brandNameBrand nameHalolight
demoEmailDemo account emailadmin@halolight.h7ml.cn
demoPasswordDemo account password123456
showDemoHintShow demo hinttrue

Usage โ€‹

ts
import { inject } from '@angular/core';
import { environment } from '../environments/environment';

// Use in components or services
export class ApiService {
  private apiUrl = environment.apiUrl;
  private useMock = environment.useMock;

  // ...
}

Common Commands โ€‹

bash
pnpm start          # Start development server
pnpm build          # Production build
pnpm lint           # Lint code
pnpm lint:fix       # Auto fix
pnpm type-check     # Type check
pnpm test           # Run tests
pnpm test:coverage  # Test coverage

Testing โ€‹

bash
pnpm test           # Run tests (watch mode)
pnpm test:run       # Single run
pnpm test:coverage  # Coverage report
pnpm test:ui        # Vitest UI

Test Example โ€‹

ts
// auth.store.spec.ts
import { TestBed } from '@angular/core/testing';
import { AuthStore } from './auth.store';
import { AuthService } from '../services/auth.service';

describe('AuthStore', () => {
  let store: InstanceType<typeof AuthStore>;
  let authService: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    const authServiceSpy = jasmine.createSpyObj('AuthService', ['login', 'logout']);

    TestBed.configureTestingModule({
      providers: [
        AuthStore,
        { provide: AuthService, useValue: authServiceSpy },
      ],
    });

    store = TestBed.inject(AuthStore);
    authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
  });

  it('should initialize with default state', () => {
    expect(store.user()).toBeNull();
    expect(store.token()).toBeNull();
    expect(store.isAuthenticated()).toBe(false);
  });

  it('should login successfully', async () => {
    const mockResponse = {
      user: { id: '1', email: 'test@example.com', permissions: ['users:view'] },
      token: 'mock-token',
    };
    authService.login.and.returnValue(Promise.resolve(mockResponse));

    await store.login({ email: 'test@example.com', password: '123456' });

    expect(store.user()).toEqual(mockResponse.user);
    expect(store.token()).toBe('mock-token');
    expect(store.isAuthenticated()).toBe(true);
  });

  it('should check permissions correctly', async () => {
    const mockResponse = {
      user: { id: '1', email: 'test@example.com', permissions: ['users:*', 'dashboard:view'] },
      token: 'mock-token',
    };
    authService.login.and.returnValue(Promise.resolve(mockResponse));
    await store.login({ email: 'test@example.com', password: '123456' });

    expect(store.hasPermission('users:view')).toBe(true);
    expect(store.hasPermission('users:delete')).toBe(true);
    expect(store.hasPermission('dashboard:view')).toBe(true);
    expect(store.hasPermission('settings:view')).toBe(false);
  });
});

Configuration โ€‹

Angular Configuration โ€‹

ts
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideQueryClient } from '@tanstack/angular-query-experimental';
import { QueryClient } from '@tanstack/query-core';

import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withInterceptors([authInterceptor, errorInterceptor])),
    provideAnimations(),
    provideQueryClient(new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 1000 * 60 * 5, // 5 minutes
          gcTime: 1000 * 60 * 10, // 10 minutes
        },
      },
    })),
  ],
};

Tailwind Configuration โ€‹

js
// tailwind.config.js
import { fontFamily } from 'tailwindcss/defaultTheme';

export default {
  darkMode: ['class'],
  content: ['./src/**/*.{html,ts}'],
  theme: {
    container: {
      center: true,
      padding: '2rem',
      screens: {
        '2xl': '1400px',
      },
    },
    extend: {
      fontFamily: {
        sans: ['Inter var', ...fontFamily.sans],
      },
      colors: {
        border: 'oklch(var(--border))',
        input: 'oklch(var(--input))',
        ring: 'oklch(var(--ring))',
        background: 'oklch(var(--background))',
        foreground: 'oklch(var(--foreground))',
        primary: {
          DEFAULT: 'oklch(var(--primary))',
          foreground: 'oklch(var(--primary-foreground))',
        },
        secondary: {
          DEFAULT: 'oklch(var(--secondary))',
          foreground: 'oklch(var(--secondary-foreground))',
        },
        // ... more color definitions
      },
      keyframes: {
        'accordion-down': {
          from: { height: '0' },
          to: { height: 'var(--radix-accordion-content-height)' },
        },
        'accordion-up': {
          from: { height: 'var(--radix-accordion-content-height)' },
          to: { height: '0' },
        },
      },
      animation: {
        'accordion-down': 'accordion-down 0.2s ease-out',
        'accordion-up': 'accordion-up 0.2s ease-out',
      },
    },
  },
  plugins: [require('tailwindcss-animate')],
};

Deployment โ€‹

Deploy with Vercel

bash
vercel

Docker โ€‹

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM nginx:alpine
COPY --from=builder /app/dist/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
bash
docker build -t halolight-angular .
docker run -p 3000:80 halolight-angular

Other Platforms โ€‹

CI/CD โ€‹

The project is configured with a complete GitHub Actions CI workflow:

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm audit --audit-level=high

Advanced Features โ€‹

Route Guards โ€‹

ts
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthStore } from '../stores/auth.store';

export const authGuard: CanActivateFn = (route, state) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);

  if (!authStore.isAuthenticated()) {
    router.navigate(['/login'], { queryParams: { redirect: state.url } });
    return false;
  }

  return true;
};

// guards/permission.guard.ts
export const permissionGuard: CanActivateFn = (route) => {
  const authStore = inject(AuthStore);
  const router = inject(Router);
  const permission = route.data['permission'] as string;

  if (permission && !authStore.hasPermission(permission)) {
    router.navigate(['/403']);
    return false;
  }

  return true;
};

HTTP Interceptors โ€‹

ts
// interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthStore } from '../stores/auth.store';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authStore = inject(AuthStore);
  const token = authStore.token();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` },
    });
  }

  return next(req);
};

// interceptors/error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router);
  const authStore = inject(AuthStore);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        authStore.logout();
        router.navigate(['/login']);
      }
      return throwError(() => error);
    })
  );
};

Signals Computed Properties โ€‹

ts
// stores/ui-settings.store.ts
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

interface UiSettingsState {
  theme: 'light' | 'dark' | 'system';
  skin: string;
  sidebarCollapsed: boolean;
}

const initialState: UiSettingsState = {
  theme: 'system',
  skin: 'default',
  sidebarCollapsed: false,
};

export const UiSettingsStore = signalStore(
  { providedIn: 'root' },
  withState(initialState),
  withComputed((store) => ({
    effectiveTheme: computed(() => {
      if (store.theme() === 'system') {
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
      }
      return store.theme();
    }),
    isDarkMode: computed(() => store.effectiveTheme() === 'dark'),
  })),
  withMethods((store) => ({
    setTheme(theme: 'light' | 'dark' | 'system') {
      patchState(store, { theme });
      document.documentElement.classList.toggle('dark', store.isDarkMode());
    },
    setSkin(skin: string) {
      patchState(store, { skin });
      document.documentElement.setAttribute('data-theme', skin);
    },
    toggleSidebar() {
      patchState(store, { sidebarCollapsed: !store.sidebarCollapsed() });
    },
  }))
);

Performance Optimization โ€‹

Image Optimization โ€‹

ts
// Using NgOptimizedImage
import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <img
      ngSrc="/assets/images/hero.jpg"
      width="1200"
      height="600"
      priority
      alt="Hero image"
    />
  `,
})
export class HeroComponent {}

Lazy Loading Components โ€‹

ts
// app.routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./pages/admin/dashboard/dashboard.component')
      .then(m => m.DashboardComponent),
  },
  {
    path: 'users',
    loadChildren: () => import('./pages/admin/users/users.routes')
      .then(m => m.USERS_ROUTES),
  },
];

Preloading Strategy โ€‹

ts
// app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules),
      withComponentInputBinding()
    ),
  ],
};

OnPush Change Detection โ€‹

ts
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-user-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (user of users(); track user.id) {
      <app-user-card [user]="user" />
    }
  `,
})
export class UserListComponent {
  users = signal<User[]>([]);
}

FAQ โ€‹

Q: How to configure Mock data? โ€‹

A: Set useMock: true in environment.ts and define Mock data in the src/mocks directory:

ts
// mocks/users.mock.ts
import Mock from 'mockjs';

Mock.mock('/api/users', 'get', {
  'data|10-20': [{
    'id|+1': 1,
    'name': '@cname',
    'email': '@email',
    'avatar': '@image(100x100)',
    'role': '@pick(["admin", "user", "guest"])',
    'status': '@pick(["active", "inactive"])',
    'createdAt': '@datetime',
  }],
  total: '@integer(10, 100)',
});

Q: How to implement route permission control? โ€‹

A: Use permissionGuard and specify the required permission in the route configuration:

ts
// app.routes.ts
{
  path: 'users',
  loadComponent: () => import('./pages/admin/users/users.component'),
  data: { permission: 'users:view' },
  canActivate: [authGuard, permissionGuard],
}

Q: How to customize theme colors? โ€‹

A: Override CSS variables in styles.css:

css
:root {
  --primary: 51.1% 0.262 276.97; /* Custom primary color */
  --primary-foreground: 98% 0 0;
}

.dark {
  --primary: 56.1% 0.287 277.04;
  --primary-foreground: 98% 0 0;
}

Q: How to integrate third-party UI component libraries? โ€‹

A: spartan/ui is already integrated. To add other components, extend with Angular CDK:

ts
import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop';

@Component({
  imports: [CdkDrag, CdkDropList],
  template: `
    <div cdkDropList (cdkDropListDropped)="drop($event)">
      @for (item of items(); track item.id) {
        <div cdkDrag>{{ item.name }}</div>
      }
    </div>
  `,
})
export class DraggableListComponent {}

Comparison with Other Versions โ€‹

FeatureAngular VersionNext.jsVue
SSR/SSGโœ… Angular SSRโœ…โœ… (Nuxt)
State ManagementNgRx SignalsZustandPinia
RoutingAngular RouterApp RouterVue Router
Build ToolAngular CLI + esbuildNext.jsVite
Type SafetyTypeScript (enforced)TypeScriptTypeScript
Enterprise SupportGoogleVercelCommunity