# Theme Utility

Runtime theme system for Angular 19 with config-driven loading, JSON configs, assets, and signal-based state.

This doc covers:

- Runtime theme config + JSON schema
- Loading flow and caching behavior
- Theme/asset switching behavior
- How to add or extend themes safely
- Tailwind `@theme` registration for new variables

---

## Quick Start

```typescript
import { provideRuntimeTheme } from '@sixbell-telco/sdk/utils/theme';

bootstrapApplication(AppComponent, {
  providers: [provideRuntimeTheme('/assets/themes/themes.json')],
});
```

Switch theme or scheme:

```typescript
import { Component, inject } from '@angular/core';
import { ThemeService } from '@sixbell-telco/sdk/utils/theme';

@Component({
  selector: 'app-theme-switcher',
  template: `
    <select (change)="onThemeChange($event)">
      @for (theme of themeService.getAvailableThemes(); track theme) {
        <option [value]="theme">{{ theme }}</option>
      }
    </select>
    <button (click)="toggleScheme()">Current: {{ themeService.resolvedScheme() }}</button>
  `,
})
export class ThemeSwitcherComponent {
  protected readonly themeService = inject(ThemeService);

  onThemeChange(event: Event): void {
    const theme = (event.target as HTMLSelectElement).value;
    this.themeService.setTheme(theme);
  }

  toggleScheme(): void {
    const current = this.themeService.selectedScheme();
    const next = current === 'dark' ? 'light' : current === 'light' ? 'dark' : 'system';
    this.themeService.setScheme(next);
  }
}
```

---

## Runtime Model Overview

The theme system is config-based:

1. App loads a config (`themes.json`) containing a list of theme paths.
2. The selected theme file is loaded blocking (fonts + assets are ready before paint).
3. Remaining themes are prefetched non-blocking so switching is instant later.
4. Optional SSE or polling streams can mark `updateAvailable()` and let the app call `refreshTheme()`.

### Mermaid: Boot Flow

```mermaid
sequenceDiagram
  participant App
  participant Provider as provideRuntimeTheme
  participant Loader as RuntimeConfigLoader
  participant Dexie as RuntimeConfigStore
  participant ThemeSvc as ThemeService

  App->>Provider: bootstrap
  Provider->>Loader: loadLatest(theme-config)
  Loader->>Dexie: get(cacheKey)
  alt cached & schema ok
    Dexie-->>Loader: cached
    Loader-->>Provider: theme-config (source=cache)
  else cache miss / schema mismatch
    Loader->>Provider: fetch(theme-config)
    Provider->>Loader: parse + store
    Loader->>Dexie: set(cacheKey)
  end

  Provider->>ThemeSvc: applyRuntimeConfig()
  Provider->>Provider: resolve selectedTheme
  Provider->>Loader: loadLatest(theme file)
  Provider->>ThemeSvc: configureRuntime(theme, blocking=true)
  Provider->>Loader: prefetch remaining theme files (non-blocking)
  Provider->>ThemeSvc: configureRuntime(theme, blocking=false)
```

---

## Files and Schemas

### 1) Theme Config (`themes.json`)

```json
{
  "meta": {
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "showcase-themes-v1",
    "schemaVersion": "2"
  },
  "themes": ["/assets/themes/catalog/sixbell_telco.json", "/assets/themes/catalog/wom.json"],
  "defaultTheme": "sixbell_telco",
  "defaultScheme": "system",
  "excludeThemes": []
}
```

Config rules:

- `themes` is required.
- Each item is a theme JSON path.
- Theme name comes from `meta.name` inside the theme file.
- `excludeThemes` hides themes at runtime without deleting files.
- `meta.schemaVersion` is required and used to invalidate old cache entries.
- `meta.hash` is required and used for update detection.

### 2) Per-Theme Runtime Config (`<theme>.json`)

```json
{
  "meta": {
    "name": "sixbell_telco",
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "showcase-theme-sixbell-telco-v1",
    "schemaVersion": "2"
  },
  "fonts": [
    {
      "family": "Poppins",
      "faces": [{ "weight": 400, "style": "normal", "src": "./assets/fonts/Poppins-Regular.ttf" }]
    }
  ],
  "themes": {
    "light": {
      "variables": {
        "colors": {
          "--color-primary": "oklch(65.64% 0.1155 219.3)"
        },
        "radius": {},
        "sizes": {},
        "effects": {},
        "others": {
          "--font-body": "Poppins, sans-serif"
        }
      }
    },
    "dark": {
      "variables": {
        "colors": {
          "--color-primary": "oklch(65.64% 0.1155 219.3)"
        },
        "radius": {},
        "sizes": {},
        "effects": {},
        "others": {
          "--font-body": "Poppins, sans-serif"
        }
      }
    }
  },
  "assets": [
    {
      "name": "logo",
      "light": "/assets/logos/sixbell-logo-light-mode.svg",
      "dark": "/assets/logos/sixbell-logo-dark-mode.svg"
    }
  ]
}
```

Theme file rules:

- File should contain `themes.light` and `themes.dark`.
- Both `light` and `dark` variants are required.
- `meta.name`, `meta.hash`, `meta.schemaVersion`, and `meta.updatedAt` are required.
- `variables` are grouped by category and injected as CSS custom properties with a `data-theme` selector.
- `others` is required and intended for app-specific tokens not provided by the SDK.
- `assets` are optional but should have both light and dark variants for each asset.

---

## ThemeService API (Runtime)

### Signals

- `selectedTheme()` - current theme key
- `selectedScheme()` - `system | light | dark`
- `resolvedScheme()` - resolved to `light | dark`
- `isDarkTheme()` - boolean
- `finalTheme()` - string like `sixbell_telco__dark`
- `updateAvailable()` - true when a runtime update is pending
- `lastRefreshFailed()` - true when the last refresh attempt failed

### Common methods

```typescript
themeService.setTheme('wom');
themeService.setScheme('dark');
themeService.getAvailableThemes();
themeService.getDefaultTheme();
await themeService.refreshTheme();
```

---

## How the Flow Works (Detailed)

### Mermaid: Runtime Theme Selection

```mermaid
flowchart TD
  A[User sets theme/scheme] --> B[ThemeService updates signals]
  B --> C[Resolved scheme computed]
  C --> D[finalTheme computed]
  D --> E[ThemeDomService sets data-theme]
  E --> F[ThemeStorageService persists preferences]
  D --> G[ThemeDomService injects CSS vars]
```

### Config + Theme Loading (Boot)

1. `provideRuntimeTheme()` runs at app init.
2. It loads the config via `RuntimeConfigLoader`.
3. Available themes are computed with `excludeThemes`.
4. The saved theme is restored (if still available).
5. Selected theme config is loaded blocking (fonts + assets).
6. Remaining themes are loaded non-blocking.

### Asset Resolution

- Assets are resolved by theme name + asset key + scheme.
- If an asset is missing in the current config, the last working config is used.
- Asset cache is in-memory by theme.

```typescript
const url = assetsService.getAssetUrl(themeService.selectedTheme(), 'logo', themeService.resolvedScheme());
```

---

## Cache and Schema Versioning

Runtime configs are cached in IndexedDB (Dexie) as last-known-good.

When JSON schema changes, bump `THEME_SCHEMA_VERSION` and update `meta.schemaVersion` in JSON.
Old cached entries will be discarded automatically.

Hash requirements:

- `meta.hash` is required for the config and each theme file.
- Config hash drives update detection.

```ts
// projects/sdk/utils/theme/src/constants.ts
export const THEME_SCHEMA_VERSION = '2' as const;
```

```ts
// note: THEME_SCHEMA_VERSION is exported from the theme public API
// import from @sixbell-telco/sdk/utils/theme
```

---

## Tailwind Token Registration (Required for New Variables)

Runtime variables are injected at runtime, but Tailwind utility classes are compiled at build time. If you add new variables and want Tailwind classes for them, you must register the tokens in your global CSS:

```css
@import 'tailwindcss';

@theme {
  --color-counters-input: oklch(65.5% 0.0126 255.51);
  --color-counters-done: oklch(69.5% 0.2115 142.5);
  --color-counters-busy: oklch(75.89% 0.181 99.99);
}
```

When you add custom tokens under `variables.others`, register those tokens in your app `@theme` block so Tailwind utilities can use them.

---

## Extending Themes (Add New Theme)

### Step 1: Add a theme JSON

Create `/assets/themes/catalog/acme.json`:

```json
{
  "meta": {
    "name": "acme",
    "updatedAt": "2026-02-02T00:00:00Z",
    "hash": "acme-theme-v1",
    "schemaVersion": "2"
  },
  "fonts": [
    {
      "family": "Acme Sans",
      "faces": [{ "weight": 400, "style": "normal", "src": "/assets/fonts/AcmeSans-Regular.woff2" }]
    }
  ],
  "themes": {
    "light": {
      "variables": {
        "colors": {
          "--color-primary": "oklch(70% 0.1 220)"
        },
        "radius": {},
        "sizes": {},
        "effects": {},
        "others": {
          "--font-body": "Acme Sans, sans-serif"
        }
      }
    },
    "dark": {
      "variables": {
        "colors": {
          "--color-primary": "oklch(65% 0.1 220)"
        },
        "radius": {},
        "sizes": {},
        "effects": {},
        "others": {
          "--font-body": "Acme Sans, sans-serif"
        }
      }
    }
  },
  "assets": [
    {
      "name": "logo",
      "light": "/assets/logos/acme_light.svg",
      "dark": "/assets/logos/acme_dark.svg"
    }
  ]
}
```

### Step 2: Register it in the config

```json
{
  "themes": ["/assets/themes/catalog/sixbell_telco.json", "/assets/themes/catalog/wom.json", "/assets/themes/catalog/acme.json"]
}
```

### Step 3: Register tokens in `@theme` (if new variables)

Add to `projects/sdk/src/_index.css` (or your app CSS):

```css
@theme {
  --color-acme-highlight: oklch(72% 0.12 30);
  --color-acme-surface: oklch(98% 0.02 95);
}
```

### Step 4: Use it

```typescript
themeService.setTheme('acme');
themeService.setScheme('dark');
```

---

## Examples

### Minimal Bootstrap

```ts
bootstrapApplication(AppComponent, {
  providers: [provideRuntimeTheme('/assets/themes/themes.json')],
});
```

### App with SSE Update Support

```ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRuntimeTheme('/assets/themes/themes.json', {
      appId: 'host-app',
      sse: { url: 'http://localhost:4000/api/runtime/updates?resource=theme', eventType: 'message' },
    }),
  ],
});
```

### Showcase runtime mode wiring

The showcase switches between static assets, polling, and SSE in `projects/showcase/src/app/app.config.ts`:

```ts
provideRuntimeTheme('/assets/themes/themes.json');

provideRuntimeTheme(`${environment.runtime.apiBaseUrl}/api/runtime/theme/config`, {
  updateStream: runtimeUpdateStream$,
});

provideRuntimeTheme(`${environment.runtime.apiBaseUrl}/api/runtime/theme/config`, {
  sse: { url: `${environment.runtime.apiBaseUrl}/api/runtime/updates?resource=theme`, eventType: 'message' },
});
```

### React to Update Availability

```ts
const themeService = inject(ThemeService);

effect(() => {
  if (themeService.updateAvailable()) {
    // prompt user and call refreshTheme()
  }
});
```

### Manual Refresh

```ts
await themeService.refreshTheme();
```

### Using the local runtime API server

```ts
bootstrapApplication(AppComponent, {
  providers: [
    provideRuntimeTheme('http://localhost:4000/api/runtime/theme/config', {
      sse: { url: 'http://localhost:4000/api/runtime/updates?resource=theme' },
    }),
  ],
});
```

## Hash and SchemaVersion Ownership

In production, the backend or build pipeline that publishes runtime JSON should always set:

- `meta.hash`: update this on any content change (colors, assets, fonts, paths).
- `meta.schemaVersion`: update only when the JSON structure changes.

Default mode bootstrap validation:

- Cached configs are validated on boot against the latest `meta.hash`.
- If the hash changed, the selected theme reloads before render.
- If the network check fails, the cached config is used to keep the app available.

When editing theme files manually, update the file `meta.hash` and the top-level `meta.hash` so clients detect the change.

---

## Troubleshooting

### Theme not applied on load

- Check the config path is correct.
- Check the theme JSON has both `light` and `dark` variants.
- Check that `meta.name` matches the config `name`.

### New variables not usable in Tailwind utilities

- Add the token to `@theme` in global CSS.

### Old config stuck in cache

- Bump `THEME_SCHEMA_VERSION` and add `meta.schemaVersion` in JSON.

---

## Related Utilities

- `@sixbell-telco/sdk/utils/runtime-config` - Loader + cache
- `@sixbell-telco/sdk/utils/translation` - i18n runtime config
