@truenas/ui-components 0.1.46 → 0.1.47

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.
Files changed (28) hide show
  1. package/fesm2022/truenas-ui-components.mjs +39 -3
  2. package/fesm2022/truenas-ui-components.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/scripts/icon-sprite/__fixtures__/consumer-templates/basic.component.html +30 -0
  5. package/scripts/icon-sprite/__fixtures__/custom-icons/brand.svg +1 -0
  6. package/scripts/icon-sprite/__fixtures__/custom-icons/logo.svg +1 -0
  7. package/scripts/icon-sprite/__fixtures__/forwarding-components/multi-icon.component.html +7 -0
  8. package/scripts/icon-sprite/__fixtures__/forwarding-components/multi-icon.component.ts +15 -0
  9. package/scripts/icon-sprite/__fixtures__/forwarding-components/no-library.component.html +3 -0
  10. package/scripts/icon-sprite/__fixtures__/forwarding-components/no-library.component.ts +11 -0
  11. package/scripts/icon-sprite/__fixtures__/forwarding-components/not-forwarding.component.html +1 -0
  12. package/scripts/icon-sprite/__fixtures__/forwarding-components/not-forwarding.component.ts +10 -0
  13. package/scripts/icon-sprite/__fixtures__/forwarding-components/single-icon.component.html +3 -0
  14. package/scripts/icon-sprite/__fixtures__/forwarding-components/single-icon.component.ts +13 -0
  15. package/scripts/icon-sprite/__fixtures__/marker-sources/icons.ts +24 -0
  16. package/scripts/icon-sprite/cli-main.ts +27 -8
  17. package/scripts/icon-sprite/generate-sprite.ts +15 -4
  18. package/scripts/icon-sprite/jest.config.ts +14 -0
  19. package/scripts/icon-sprite/lib/find-icons-in-forwarding-components.spec.ts +77 -0
  20. package/scripts/icon-sprite/lib/find-icons-in-forwarding-components.ts +209 -0
  21. package/scripts/icon-sprite/lib/find-icons-in-templates.spec.ts +119 -0
  22. package/scripts/icon-sprite/lib/find-icons-in-templates.ts +66 -17
  23. package/scripts/icon-sprite/lib/find-icons-with-marker.spec.ts +58 -0
  24. package/scripts/icon-sprite/lib/find-icons-with-marker.ts +37 -22
  25. package/scripts/icon-sprite/lib/validate-icons.spec.ts +170 -0
  26. package/scripts/icon-sprite/lib/validate-icons.ts +185 -0
  27. package/scripts/icon-sprite/tsconfig.json +14 -0
  28. package/types/truenas-ui-components.d.ts +42 -4
@@ -0,0 +1,185 @@
1
+ import fs from 'fs';
2
+ import { resolve } from 'path';
3
+ import { discoverForwardingMappings } from './find-icons-in-forwarding-components';
4
+ import { findIconsInTemplates } from './find-icons-in-templates';
5
+ import type { ScanResult } from './find-icons-in-templates';
6
+ import { findIconsWithMarker } from './find-icons-with-marker';
7
+ import type { ResolvedSpriteConfig } from '../sprite-config-interface';
8
+
9
+ export interface ValidationResult {
10
+ scannedIcons: Set<string>;
11
+ spriteIcons: Set<string>;
12
+ missingFromSprite: Set<string>;
13
+ staleInSprite: Set<string>;
14
+ sources: Map<string, string[]>;
15
+ spriteConfigFound: boolean;
16
+ }
17
+
18
+ /**
19
+ * Validates icon usage against the current sprite manifest.
20
+ * Reports missing icons (in code but not in sprite) and stale icons (in sprite but not in code).
21
+ */
22
+ export function validateIcons(resolved: ResolvedSpriteConfig): ValidationResult {
23
+ const srcDirs = resolved.srcDirs.map(dir => resolve(resolved.projectRoot, dir));
24
+ const configPath = resolve(resolved.projectRoot, resolved.outputDir, 'sprite-config.json');
25
+
26
+ // Load library icons
27
+ const libraryIcons = loadLibraryIconsForValidation(resolved.projectRoot);
28
+
29
+ // Discover forwarding component mappings
30
+ const forwardingMappings = discoverForwardingMappings(srcDirs, resolved.projectRoot);
31
+
32
+ // Scan all source directories
33
+ const allSources = new Map<string, string[]>();
34
+ const scannedIcons = new Set<string>();
35
+
36
+ for (const srcDir of srcDirs) {
37
+ if (!fs.existsSync(srcDir)) {
38
+ continue;
39
+ }
40
+
41
+ const templateResult = findIconsInTemplates(srcDir, libraryIcons, forwardingMappings);
42
+ const markerResult = findIconsWithMarker(srcDir, libraryIcons);
43
+
44
+ mergeScanResult(scannedIcons, allSources, templateResult);
45
+ mergeScanResult(scannedIcons, allSources, markerResult);
46
+ }
47
+
48
+ // Include library icons in the scanned set (they're expected in the sprite)
49
+ for (const icon of libraryIcons) {
50
+ scannedIcons.add(icon);
51
+ }
52
+
53
+ // Include custom icons (they're always added to the sprite by generate)
54
+ if (resolved.customIconsDir) {
55
+ const customDir = resolve(resolved.projectRoot, resolved.customIconsDir);
56
+ if (fs.existsSync(customDir)) {
57
+ for (const filename of fs.readdirSync(customDir)) {
58
+ if (filename.endsWith('.svg')) {
59
+ scannedIcons.add(`tn-${filename.replace('.svg', '')}`);
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // Load existing sprite config
66
+ let spriteIcons = new Set<string>();
67
+ let spriteConfigFound = false;
68
+
69
+ if (fs.existsSync(configPath)) {
70
+ spriteConfigFound = true;
71
+ try {
72
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
73
+ spriteIcons = new Set<string>(config.icons || []);
74
+ } catch {
75
+ // Corrupt config — treat as empty
76
+ }
77
+ }
78
+
79
+ // Compute differences
80
+ const missingFromSprite = new Set<string>();
81
+ for (const icon of scannedIcons) {
82
+ if (!spriteIcons.has(icon)) {
83
+ missingFromSprite.add(icon);
84
+ }
85
+ }
86
+
87
+ const staleInSprite = new Set<string>();
88
+ for (const icon of spriteIcons) {
89
+ if (!scannedIcons.has(icon)) {
90
+ staleInSprite.add(icon);
91
+ }
92
+ }
93
+
94
+ return {
95
+ scannedIcons,
96
+ spriteIcons,
97
+ missingFromSprite,
98
+ staleInSprite,
99
+ sources: allSources,
100
+ spriteConfigFound,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Prints the validation report to stdout.
106
+ * Returns the appropriate exit code (0 = clean, 1 = missing icons).
107
+ */
108
+ export function printValidationReport(result: ValidationResult): number {
109
+ console.log('Icon Sprite Validation Report');
110
+ console.log('==============================\n');
111
+
112
+ if (!result.spriteConfigFound) {
113
+ console.log('⚠ No sprite-config.json found. Run "npx truenas-icons generate" first.\n');
114
+ console.log(`Scanned: ${result.scannedIcons.size} icon(s) across templates and markers`);
115
+ return 1;
116
+ }
117
+
118
+ console.log(`Scanned: ${result.scannedIcons.size} icon(s) across templates and markers`);
119
+ console.log(`Sprite: ${result.spriteIcons.size} icon(s) in current sprite\n`);
120
+
121
+ if (result.missingFromSprite.size > 0) {
122
+ console.log(`Missing from sprite (${result.missingFromSprite.size} new icon(s) found):`);
123
+ for (const icon of [...result.missingFromSprite].sort()) {
124
+ const sourceFiles = result.sources.get(icon);
125
+ const sourceHint = sourceFiles?.length
126
+ ? ` (from ${sourceFiles[0]})`
127
+ : '';
128
+ console.log(` + ${icon}${sourceHint}`);
129
+ }
130
+ console.log('');
131
+ }
132
+
133
+ if (result.staleInSprite.size > 0) {
134
+ console.log(`Stale icons (${result.staleInSprite.size} in sprite but not referenced):`);
135
+ for (const icon of [...result.staleInSprite].sort()) {
136
+ console.log(` - ${icon}`);
137
+ }
138
+ console.log('');
139
+ }
140
+
141
+ if (result.missingFromSprite.size === 0 && result.staleInSprite.size === 0) {
142
+ console.log('✓ Sprite is up to date — no missing or stale icons.\n');
143
+ return 0;
144
+ }
145
+
146
+ if (result.missingFromSprite.size > 0) {
147
+ console.log('Run "npx truenas-icons generate" to rebuild the sprite.\n');
148
+ return 1;
149
+ }
150
+
151
+ return 0;
152
+ }
153
+
154
+ function mergeScanResult(
155
+ icons: Set<string>,
156
+ sources: Map<string, string[]>,
157
+ result: ScanResult,
158
+ ): void {
159
+ for (const icon of result.icons) {
160
+ icons.add(icon);
161
+ }
162
+ for (const [icon, files] of result.sources) {
163
+ const existing = sources.get(icon) || [];
164
+ existing.push(...files);
165
+ sources.set(icon, existing);
166
+ }
167
+ }
168
+
169
+ function loadLibraryIconsForValidation(projectRoot: string): Set<string> {
170
+ const librarySpritePath = resolve(
171
+ projectRoot,
172
+ 'node_modules/@truenas/ui-components/assets/tn-icons/sprite-config.json'
173
+ );
174
+
175
+ if (!fs.existsSync(librarySpritePath)) {
176
+ return new Set<string>();
177
+ }
178
+
179
+ try {
180
+ const config = JSON.parse(fs.readFileSync(librarySpritePath, 'utf-8'));
181
+ return new Set<string>(config.icons || []);
182
+ } catch {
183
+ return new Set<string>();
184
+ }
185
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "esModuleInterop": true,
7
+ "isolatedModules": true,
8
+ "strict": true,
9
+ "types": ["jest", "node"],
10
+ "rootDir": ".",
11
+ "outDir": "../../dist/out-tsc/scripts"
12
+ },
13
+ "include": ["**/*.ts"]
14
+ }
@@ -628,6 +628,7 @@ interface IconResult {
628
628
  spriteUrl?: string;
629
629
  }
630
630
  declare class TnIconComponent {
631
+ private static warnedIcons;
631
632
  name: _angular_core.InputSignal<string>;
632
633
  size: _angular_core.InputSignal<IconSize>;
633
634
  color: _angular_core.InputSignal<string | undefined>;
@@ -658,6 +659,12 @@ declare class TnIconComponent {
658
659
  private tryCssIcon;
659
660
  private tryUnicodeIcon;
660
661
  private generateTextAbbreviation;
662
+ /**
663
+ * Warns once per icon name in dev mode when a sprite-prefixed icon is not
664
+ * found in the sprite. Only fires when the sprite is loaded and the icon
665
+ * name has a prefix indicating it should be a sprite icon.
666
+ */
667
+ private warnMissingSpriteIcon;
661
668
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<TnIconComponent, never>;
662
669
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<TnIconComponent, "tn-icon", never, { "name": { "alias": "name"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "tooltip": { "alias": "tooltip"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "library": { "alias": "library"; "required": false; "isSignal": true; }; "fullSize": { "alias": "fullSize"; "required": false; "isSignal": true; }; "customSize": { "alias": "customSize"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
663
670
  }
@@ -832,7 +839,38 @@ declare enum InputType {
832
839
  PlainText = "text"
833
840
  }
834
841
 
835
- declare class TnInputComponent implements AfterViewInit, ControlValueAccessor {
842
+ /**
843
+ * Marker interface for components that forward icon inputs to `<tn-icon>`.
844
+ *
845
+ * When a component implements this interface, the sprite generation scanner
846
+ * will read its template to discover which inputs map to `<tn-icon>` `[name]`
847
+ * and `[library]` bindings, then scan consumer templates for those attributes.
848
+ *
849
+ * This means icons passed as template attributes to these components are
850
+ * automatically included in the sprite — no `tnIconMarker()` needed.
851
+ *
852
+ * @example
853
+ * ```typescript
854
+ * @Component({
855
+ * selector: 'tn-empty',
856
+ * templateUrl: './empty.component.html',
857
+ * })
858
+ * export class TnEmptyComponent implements TnIconForwardingComponent {
859
+ * icon = input<string>();
860
+ * iconLibrary = input<IconLibraryType>('mdi');
861
+ * }
862
+ * ```
863
+ *
864
+ * The scanner will detect that `tn-empty` forwards `icon` to `<tn-icon [name]>`,
865
+ * so `<tn-empty icon="inbox">` in consumer templates will automatically include
866
+ * `mdi-inbox` in the sprite.
867
+ *
868
+ * @public
869
+ */
870
+ interface TnIconForwardingComponent {
871
+ }
872
+
873
+ declare class TnInputComponent implements TnIconForwardingComponent, AfterViewInit, ControlValueAccessor {
836
874
  inputEl: _angular_core.Signal<ElementRef<HTMLInputElement | HTMLTextAreaElement>>;
837
875
  inputType: _angular_core.InputSignal<InputType>;
838
876
  placeholder: _angular_core.InputSignal<string>;
@@ -1174,7 +1212,7 @@ declare class TnInputDirective {
1174
1212
  }
1175
1213
 
1176
1214
  type ChipColor = 'primary' | 'secondary' | 'accent';
1177
- declare class TnChipComponent implements AfterViewInit, OnDestroy {
1215
+ declare class TnChipComponent implements TnIconForwardingComponent, AfterViewInit, OnDestroy {
1178
1216
  chipEl: _angular_core.Signal<ElementRef<HTMLElement>>;
1179
1217
  label: _angular_core.InputSignal<string>;
1180
1218
  icon: _angular_core.InputSignal<string | undefined>;
@@ -5514,7 +5552,7 @@ declare class TnFilePickerPopupComponent implements OnInit, AfterViewInit, After
5514
5552
  }
5515
5553
 
5516
5554
  type TnEmptySize = 'default' | 'compact';
5517
- declare class TnEmptyComponent {
5555
+ declare class TnEmptyComponent implements TnIconForwardingComponent {
5518
5556
  title: _angular_core.InputSignal<string>;
5519
5557
  description: _angular_core.InputSignal<string | undefined>;
5520
5558
  icon: _angular_core.InputSignal<string | undefined>;
@@ -6062,4 +6100,4 @@ declare const TN_THEME_DEFINITIONS: readonly TnThemeDefinition[];
6062
6100
  declare const THEME_MAP: Map<TnTheme, TnThemeDefinition>;
6063
6101
 
6064
6102
  export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
6065
- export type { AutocompleteHarnessFilters, BannerHarnessFilters, ButtonHarnessFilters, ButtonToggleHarnessFilters, CalendarCell, CheckboxHarnessFilters, ChipColor, CreateFolderEvent, DateInputHarnessFilters, DateRange, DateRangeInputHarnessFilters, DialogHarnessFilters, EmptyHarnessFilters, ExpansionPanelHarnessFilters, FilePickerCallbacks, FilePickerError, FilePickerMode, FileSystemItem, FormFieldHarnessFilters, IconButtonHarnessFilters, IconHarnessFilters, IconLibrary, IconLibraryType, IconResult, IconSize, IconSource, IconTestingMockOverrides, InputHarnessFilters, KeyCombination, LabelType, LucideIconOptions, MenuHarnessFilters, MockIconRegistry, MockSpriteLoader, PathSegment, PlatformType, ProgressBarMode, RadioHarnessFilters, ResolvedIcon, SelectHarnessFilters, ShortcutHandler, SidePanelHarnessFilters, SlideToggleColor, SlideToggleHarnessFilters, SpinnerMode, SpriteConfig, SubscriptSizing, TabChangeEvent, TabHarnessFilters, TabPanelHarnessFilters, TabsHarnessFilters, TnBannerType, TnButtonToggleType, TnCardAction, TnCardControl, TnCardFooterLink, TnCardHeaderStatus, TnConfirmDialogData, TnDialogDefaults, TnDialogOpenTarget, TnDrawerMode, TnDrawerPosition, TnEmptySize, TnFlatTreeNode, TnMenuItem, TnSelectOption, TnSelectOptionGroup, TnSelectionChange, TnSortEvent, TnTableDataSource, TnTableHarnessFilters, TnThemeDefinition, TnToastCall, TnToastConfig, TooltipPosition, YearCell };
6103
+ export type { AutocompleteHarnessFilters, BannerHarnessFilters, ButtonHarnessFilters, ButtonToggleHarnessFilters, CalendarCell, CheckboxHarnessFilters, ChipColor, CreateFolderEvent, DateInputHarnessFilters, DateRange, DateRangeInputHarnessFilters, DialogHarnessFilters, EmptyHarnessFilters, ExpansionPanelHarnessFilters, FilePickerCallbacks, FilePickerError, FilePickerMode, FileSystemItem, FormFieldHarnessFilters, IconButtonHarnessFilters, IconHarnessFilters, IconLibrary, IconLibraryType, IconResult, IconSize, IconSource, IconTestingMockOverrides, InputHarnessFilters, KeyCombination, LabelType, LucideIconOptions, MenuHarnessFilters, MockIconRegistry, MockSpriteLoader, PathSegment, PlatformType, ProgressBarMode, RadioHarnessFilters, ResolvedIcon, SelectHarnessFilters, ShortcutHandler, SidePanelHarnessFilters, SlideToggleColor, SlideToggleHarnessFilters, SpinnerMode, SpriteConfig, SubscriptSizing, TabChangeEvent, TabHarnessFilters, TabPanelHarnessFilters, TabsHarnessFilters, TnBannerType, TnButtonToggleType, TnCardAction, TnCardControl, TnCardFooterLink, TnCardHeaderStatus, TnConfirmDialogData, TnDialogDefaults, TnDialogOpenTarget, TnDrawerMode, TnDrawerPosition, TnEmptySize, TnFlatTreeNode, TnIconForwardingComponent, TnMenuItem, TnSelectOption, TnSelectOptionGroup, TnSelectionChange, TnSortEvent, TnTableDataSource, TnTableHarnessFilters, TnThemeDefinition, TnToastCall, TnToastConfig, TooltipPosition, YearCell };