This document describes the integration of the @truenas/ui-components component library into the TrueNAS WebUI project.
The library is published on npm and added to package.json:
"@truenas/ui-components": "~0.1.2"Added the theme CSS to the build configuration:
"styles": [
"node_modules/@bugsplat/angular-tree-component/css/angular-tree-component.css",
"node_modules/@truenas/ui-components/styles/themes.css",
"src/assets/styles/index.scss"
]Implemented automatic theme synchronization between the webui's theme system and the component library:
import { TnThemeService, TnTheme } from '@truenas/ui-components';
@Injectable({
providedIn: 'root',
})
export class ThemeService {
private tnThemeService = inject(TnThemeService);
onThemeChanged(theme: string): void {
this.activeTheme = theme;
this.activeTheme$.next(theme);
const selectedTheme = this.findTheme(this.activeTheme);
this.setCssVars(selectedTheme);
this.updateThemeInLocalStorage(selectedTheme);
// Sync with component library theme (compatibility layer)
this.syncComponentLibraryTheme(theme);
}
private syncComponentLibraryTheme(webuiThemeName: string): void {
const tnTheme = this.mapWebuiThemeToComponentLibraryTheme(webuiThemeName);
if (tnTheme) {
this.tnThemeService.setTheme(tnTheme);
}
}
private mapWebuiThemeToComponentLibraryTheme(webuiThemeName: string): TnTheme | null {
const themeMap: Record<string, TnTheme> = {
'ix-dark': TnTheme.Dark,
'ix-blue': TnTheme.Blue,
'dracula': TnTheme.Dracula,
'nord': TnTheme.Nord,
'paper': TnTheme.Paper,
'solarized-dark': TnTheme.SolarizedDark,
'midnight': TnTheme.Midnight,
'high-contrast': TnTheme.HighContrast,
};
return themeMap[webuiThemeName] ?? null;
}
}How it works:
- When the user changes the webui theme (via System Settings), the
ThemeServiceautomatically updates both:- The webui's CSS variables and styling
- The component library's theme via
TnThemeService
- This ensures components from both systems are always styled consistently
- No manual intervention required - the synchronization is automatic
Configured the build to copy the component library's icon sprite:
"assets": [
"src/assets",
"src/sw.js",
{
"glob": "**/*",
"input": "node_modules/@truenas/ui-components/assets/tn-icons",
"output": "assets/tn-icons"
}
]This automatically copies the library's icon sprite (sprite.svg) and manifest (sprite-config.json) to the build output.
Both systems support 8 themes with automatic synchronization:
| WebUI Theme Name | Component Library Theme | Description |
|---|---|---|
ix-dark |
TnTheme.Dark |
TrueNAS default dark theme |
ix-blue |
TnTheme.Blue |
Official TrueNAS colors on light |
dracula |
TnTheme.Dracula |
Popular Dracula color scheme |
nord |
TnTheme.Nord |
Nord color palette |
paper |
TnTheme.Paper |
FreeNAS 11.2 legacy theme |
solarized-dark |
TnTheme.SolarizedDark |
Solarized dark scheme |
midnight |
TnTheme.Midnight |
Dark theme with blues and greys |
high-contrast |
TnTheme.HighContrast |
High contrast for accessibility |
User changes theme in System Settings:
- User selects new theme in webui preferences
ThemeServicereceives change from NgRx storeThemeService.onThemeChanged()is called- WebUI theme CSS variables are updated
- Compatibility layer automatically maps and updates component library theme
- Both systems are now synchronized
Result: Components from both the webui and the component library are consistently styled.
During the migration to the component library, both theme systems coexist:
- WebUI themes (prefixed with
ix-) control most of the application - Component library themes (prefixed with
tn-) control library components - The compatibility layer keeps them in sync automatically
- Eventually, the webui will use the component library's theme system exclusively
import { Component } from '@angular/core';
import {
TnButtonComponent,
TnCardComponent,
TnInputComponent,
} from '@truenas/ui-components';
@Component({
selector: 'app-example',
standalone: true,
imports: [
TnButtonComponent,
TnCardComponent,
TnInputComponent,
],
template: `
<tn-card [title]="'My Card'" [elevation]="'medium'">
<tn-button
[label]="'Click Me'"
[color]="'primary'"
(onClick)="handleClick()"
/>
</tn-card>
`
})
export class ExampleComponent {
handleClick(): void {
console.log('Button clicked!');
}
}The component library uses an automatic sprite generation system that includes only the icons you use in your application.
Two Icon Systems in Parallel:
- ix-icon: WebUI's original icon system (uses
yarn iconsto generatesrc/assets/icons/sprite.svg) - tn-icon: Component library's icon system (uses
yarn tn-iconsto generatesrc/assets/tn-icons/sprite.svg)
Both systems coexist during the migration period.
-
MDI (Material Design Icons): 7000+ icons from @mdi/svg
<tn-icon name="folder" library="mdi"></tn-icon>
-
Library Custom Icons: TrueNAS-specific icons (with
tn-prefix in sprite)<tn-icon name="dataset" library="custom"></tn-icon> <!-- Resolves to tn-dataset in sprite -->
1. Use icons in templates (automatic detection):
<!-- MDI icons -->
<tn-icon name="folder" library="mdi"></tn-icon>
<tn-icon name="server" library="mdi"></tn-icon>
<!-- Library custom icons (from @truenas/ui-components) -->
<tn-icon name="tn-dataset"></tn-icon>
<tn-icon name="tn-hdd"></tn-icon>
<!-- Your app's custom icons -->
<tn-icon name="my-custom-icon" library="custom"></tn-icon>
<!-- Dynamic icons work too -->
<tn-icon [name]="iconName" library="mdi"></tn-icon>
<tn-icon [name]="isOpen ? 'chevron-down' : 'chevron-right'" library="mdi"></tn-icon>The sprite generator automatically scans templates for <tn-icon> elements - no marker function needed!
2. For truly dynamic icons (runtime-computed, from API, etc.):
When icon names can't be detected from templates (e.g., computed from strings, stored in objects/arrays, or from API responses), use tnIconMarker():
import { tnIconMarker } from '@truenas/ui-components';
// Example: Icons determined by runtime logic
const statusIcons = {
success: tnIconMarker('check-circle', 'mdi'),
error: tnIconMarker('alert-circle', 'mdi'),
warning: tnIconMarker('alert', 'mdi'),
};
// Example: Library custom icons
tnIconMarker('tn-dataset'); // TrueNAS-specific icons
// Example: App custom icons
tnIconMarker('my-custom-icon', 'custom'); // Resolves to app-my-custom-iconNote: The library uses tnIconMarker() (not iconMarker()) to avoid conflicts with the webui's existing ix-icon system.
Important: TrueNAS library icons have the tn- prefix (e.g., tn-dataset). The library="custom" parameter is for your application's custom icons, which get prefixed with app-.
3. Generate sprites:
The webui uses the library's truenas-icons CLI tool to generate sprites that include both library icons and consumer-specific icons.
Configuration File (truenas-icons.config.js):
export default {
srcDirs: [
'./src/app/pages/component-library-demo',
// Add more directories as components migrate to tn-icon
],
outputDir: './src/assets/tn-icons',
};Day-to-Day Workflow:
- Use
<tn-icon>elements in templates - they're automatically detected - Only use
tnIconMarker()for truly dynamic icons (runtime-computed names) - Sprites are automatically generated before dev server starts and before builds
- Generated sprites are committed to git (following the same pattern as webui's
ix-iconsprites)
Manual Generation:
# Generate library icon sprite (scans entire ./src/app directory)
yarn tn-icons
# Automatically runs: truenas-icons generate --src ./src/app --output ./src/assets/tn-icons --url assets/tn-icons
# The --url parameter ensures the sprite URL is correct for Angular's build process (which strips 'src/')How It Works:
- Template scanning: Automatically detects
<tn-icon>elements in HTML templates (no marker needed) - Marker scanning: Finds
tnIconMarker()calls in TypeScript for dynamic icons - The webui's
ix-iconsystem usesiconMarker()for namespace separation - This allows scanning the entire codebase without conflicts between the two icon systems
The library ships with these TrueNAS-specific icons:
dataset,dataset-roothdd,hdd-mirror,ssd,ssd-mirrortruenas-logo,truenas-logo-mark,truenas-logo-type(and color variants)truecommand-logo-markha-enabled,ha-disabled,ha-reconnectingiscsi-share,smb-share,nfs-share,nvme-shareenclosure,replication,two-factor-auth- And more...
See the full list in: node_modules/@truenas/ui-components/assets/tn-icons/sprite-config.json
- Standalone Components: All components are standalone Angular components
- Signal-based: Uses modern Angular signals for reactive state
- Theming: Comprehensive theming system with CSS variables
- Icon System: Automatic sprite generation with MDI and custom icons
- Type-safe: Full TypeScript support with proper types
- Accessible: Built with WCAG accessibility standards in mind
The integration is complete and verified:
- ✅ Styles included in build (styles.css: 220.67 kB)
- ✅ Theme compatibility layer implemented
- ✅ Automatic theme synchronization active
- ✅ Icon sprite assets configured and copied
- ✅ Demo component created with icons showcase
- ✅ Build successful with no errors
Icon Assets Deployed:
- Sprite SVG:
dist/assets/tn-icons/sprite.svg(51 KB) - Sprite manifest:
dist/assets/tn-icons/sprite-config.json - 40+ custom TrueNAS icons included
- WebUI: Stores theme in
sessionStorage(key:theme) andlocalStorage(key:theme) - Component Library: Stores theme in
localStorage(key:tn-theme) - Both are kept in sync automatically
- WebUI: Applies CSS variables to
:rootvia JavaScript - Component Library: Applies CSS classes (e.g.,
tn-dark) todocument.documentElement - The library's themes are namespaced with
tn-prefix to avoid conflicts