@truenas/ui-components 0.1.2

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 (62) hide show
  1. package/README.md +335 -0
  2. package/assets/tn-icons/custom/cloud-off.svg +1 -0
  3. package/assets/tn-icons/custom/dataset-root.svg +3 -0
  4. package/assets/tn-icons/custom/dataset.svg +3 -0
  5. package/assets/tn-icons/custom/enclosure.svg +21 -0
  6. package/assets/tn-icons/custom/ha-disabled.svg +10 -0
  7. package/assets/tn-icons/custom/ha-enabled.svg +1 -0
  8. package/assets/tn-icons/custom/ha-reconnecting.svg +10 -0
  9. package/assets/tn-icons/custom/hdd-mirror.svg +3 -0
  10. package/assets/tn-icons/custom/hdd.svg +3 -0
  11. package/assets/tn-icons/custom/iscsi-share.svg +3 -0
  12. package/assets/tn-icons/custom/layout-full.svg +8 -0
  13. package/assets/tn-icons/custom/layout-half-and-quarters.svg +7 -0
  14. package/assets/tn-icons/custom/layout-halves.svg +6 -0
  15. package/assets/tn-icons/custom/layout-quarters-and-half.svg +7 -0
  16. package/assets/tn-icons/custom/layout-quarters.svg +8 -0
  17. package/assets/tn-icons/custom/network-upload-download-both.svg +4 -0
  18. package/assets/tn-icons/custom/network-upload-download-disabled.svg +7 -0
  19. package/assets/tn-icons/custom/network-upload-download-down.svg +4 -0
  20. package/assets/tn-icons/custom/network-upload-download-up.svg +4 -0
  21. package/assets/tn-icons/custom/network-upload-download.svg +4 -0
  22. package/assets/tn-icons/custom/nfs-share.svg +3 -0
  23. package/assets/tn-icons/custom/nvme-share.svg +7 -0
  24. package/assets/tn-icons/custom/replication.svg +14 -0
  25. package/assets/tn-icons/custom/smb-share.svg +3 -0
  26. package/assets/tn-icons/custom/ssd-mirror.svg +3 -0
  27. package/assets/tn-icons/custom/ssd.svg +3 -0
  28. package/assets/tn-icons/custom/true-cloud.svg +10 -0
  29. package/assets/tn-icons/custom/truecommand-logo-mark-color.svg +14 -0
  30. package/assets/tn-icons/custom/truecommand-logo-mark.svg +10 -0
  31. package/assets/tn-icons/custom/truenas-connect-logo.svg +1 -0
  32. package/assets/tn-icons/custom/truenas-logo-ce-color.svg +1 -0
  33. package/assets/tn-icons/custom/truenas-logo-ce.svg +4 -0
  34. package/assets/tn-icons/custom/truenas-logo-enterprise-color.svg +19 -0
  35. package/assets/tn-icons/custom/truenas-logo-enterprise.svg +13 -0
  36. package/assets/tn-icons/custom/truenas-logo-mark-color.svg +7 -0
  37. package/assets/tn-icons/custom/truenas-logo-mark.svg +4 -0
  38. package/assets/tn-icons/custom/truenas-logo-type-color.svg +4 -0
  39. package/assets/tn-icons/custom/truenas-logo-type.svg +4 -0
  40. package/assets/tn-icons/custom/truenas-logo.svg +4 -0
  41. package/assets/tn-icons/custom/two-factor-auth.svg +1 -0
  42. package/assets/tn-icons/sprite-config.json +78 -0
  43. package/assets/tn-icons/sprite.svg +1 -0
  44. package/fesm2022/truenas-ui-components.mjs +8063 -0
  45. package/fesm2022/truenas-ui-components.mjs.map +1 -0
  46. package/package.json +45 -0
  47. package/scripts/icon-sprite/cli-main.ts +174 -0
  48. package/scripts/icon-sprite/cli-wrapper.js +32 -0
  49. package/scripts/icon-sprite/cli.js +30 -0
  50. package/scripts/icon-sprite/generate-sprite.ts +171 -0
  51. package/scripts/icon-sprite/lib/add-custom-icons.ts +33 -0
  52. package/scripts/icon-sprite/lib/build-sprite.ts +25 -0
  53. package/scripts/icon-sprite/lib/find-icons-in-templates.ts +108 -0
  54. package/scripts/icon-sprite/lib/find-icons-with-marker.ts +63 -0
  55. package/scripts/icon-sprite/lib/get-icon-paths.ts +95 -0
  56. package/scripts/icon-sprite/lib/warn-about-duplicates.ts +13 -0
  57. package/scripts/icon-sprite/make-sprite.ts +26 -0
  58. package/scripts/icon-sprite/sprite-config-interface.ts +72 -0
  59. package/src/assets/icons/dataset.svg +3 -0
  60. package/src/styles/dialog.css +150 -0
  61. package/src/styles/themes.css +665 -0
  62. package/types/truenas-ui-components.d.ts +2687 -0
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@truenas/ui-components",
3
+ "version": "0.1.2",
4
+ "publishConfig": {
5
+ "registry": "https://registry.npmjs.org",
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/iXsystems/truenas-ui-components.git"
11
+ },
12
+ "bin": {
13
+ "truenas-icons": "scripts/icon-sprite/cli.js"
14
+ },
15
+ "peerDependencies": {
16
+ "@angular/cdk": "^21.0.0",
17
+ "@angular/common": "^21.0.0",
18
+ "@angular/core": "^21.0.0",
19
+ "@mdi/angular-material": "^7.2.96",
20
+ "@mdi/js": "^7.4.47"
21
+ },
22
+ "dependencies": {
23
+ "@material-design-icons/svg": "~0.14.13",
24
+ "@mdi/svg": "~7.4.47",
25
+ "@types/svg-sprite": "~0.0.39",
26
+ "@types/vinyl": "~2.0.12",
27
+ "cheerio": "~1.0.0",
28
+ "svg-sprite": "~2.0.4",
29
+ "tslib": "^2.3.0",
30
+ "tsx": "~4.19.1",
31
+ "vinyl": "~3.0.0"
32
+ },
33
+ "sideEffects": false,
34
+ "module": "fesm2022/truenas-ui-components.mjs",
35
+ "typings": "types/truenas-ui-components.d.ts",
36
+ "exports": {
37
+ "./package.json": {
38
+ "default": "./package.json"
39
+ },
40
+ ".": {
41
+ "types": "./types/truenas-ui-components.d.ts",
42
+ "default": "./fesm2022/truenas-ui-components.mjs"
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ /**
4
+ * CLI entry point for truenas-icons sprite generation
5
+ *
6
+ * Usage:
7
+ * npx truenas-icons generate [options]
8
+ *
9
+ * Options:
10
+ * --src <dirs> Comma-separated source directories to scan (default: ./src/lib,./src/app)
11
+ * --output <dir> Output directory for sprite files (default: ./src/assets/icons)
12
+ * --url <path> Runtime URL path for sprite (defaults to output dir with './' stripped)
13
+ * --custom <dir> Custom icons directory (optional)
14
+ * --config <file> Configuration file path (default: truenas-icons.config.js)
15
+ * --help Show help
16
+ *
17
+ * Configuration File:
18
+ * Create truenas-icons.config.js in your project root:
19
+ *
20
+ * export default {
21
+ * srcDirs: ['./src/lib', './src/app'],
22
+ * outputDir: './src/assets/icons',
23
+ * customIconsDir: './custom-icons'
24
+ * };
25
+ */
26
+
27
+ import { generateSprite } from './generate-sprite.js';
28
+ import fs from 'fs';
29
+ import path from 'path';
30
+
31
+ const HELP_TEXT = `
32
+ truenas-icons - Icon sprite generation for TrueNAS UI components
33
+
34
+ Usage:
35
+ npx truenas-icons generate [options]
36
+
37
+ Options:
38
+ --src <dirs> Comma-separated source directories to scan
39
+ Default: ./src/lib,./src/app
40
+
41
+ --output <dir> Output directory for sprite files
42
+ Default: ./src/assets/icons
43
+
44
+ --url <path> Runtime URL path for sprite (in sprite-config.json)
45
+ Default: output dir with './' stripped
46
+ Use when build transforms paths (e.g., Angular strips 'src/')
47
+ Example: --output ./src/assets/tn-icons --url assets/tn-icons
48
+
49
+ --custom <dir> Custom icons directory (optional)
50
+ Icons will be prefixed with 'tn-'
51
+
52
+ --config <file> Configuration file path
53
+ Default: truenas-icons.config.js
54
+
55
+ --help Show this help message
56
+
57
+ Configuration File:
58
+ Create truenas-icons.config.js in your project root:
59
+
60
+ export default {
61
+ srcDirs: ['./src/lib', './src/app'],
62
+ outputDir: './src/assets/icons',
63
+ customIconsDir: './custom-icons'
64
+ };
65
+
66
+ Examples:
67
+ # Generate with defaults
68
+ npx truenas-icons generate
69
+
70
+ # Specify custom source directories
71
+ npx truenas-icons generate --src ./src,./app
72
+
73
+ # Specify output directory
74
+ npx truenas-icons generate --output ./public/icons
75
+
76
+ # Use custom icons
77
+ npx truenas-icons generate --custom ./my-icons
78
+ `;
79
+
80
+ function parseArgs() {
81
+ const args = process.argv.slice(2);
82
+ const parsed = {
83
+ command: null as string | null,
84
+ srcDirs: null as string[] | null,
85
+ outputDir: null as string | null,
86
+ spriteUrlPath: null as string | null,
87
+ customIconsDir: null as string | null,
88
+ configFile: 'truenas-icons.config.js',
89
+ showHelp: false,
90
+ };
91
+
92
+ for (let i = 0; i < args.length; i++) {
93
+ const arg = args[i];
94
+
95
+ if (arg === '--help' || arg === '-h') {
96
+ parsed.showHelp = true;
97
+ } else if (arg === 'generate') {
98
+ parsed.command = 'generate';
99
+ } else if (arg === '--src') {
100
+ parsed.srcDirs = args[++i]?.split(',') || null;
101
+ } else if (arg === '--output') {
102
+ parsed.outputDir = args[++i] || null;
103
+ } else if (arg === '--url') {
104
+ parsed.spriteUrlPath = args[++i] || null;
105
+ } else if (arg === '--custom') {
106
+ parsed.customIconsDir = args[++i] || null;
107
+ } else if (arg === '--config') {
108
+ parsed.configFile = args[++i] || parsed.configFile;
109
+ }
110
+ }
111
+
112
+ return parsed;
113
+ }
114
+
115
+ async function loadConfig(configFile: string) {
116
+ const configPath = path.resolve(process.cwd(), configFile);
117
+
118
+ if (!fs.existsSync(configPath)) {
119
+ return {};
120
+ }
121
+
122
+ try {
123
+ // Try to load as ES module
124
+ const config = await import(configPath);
125
+ return config.default || config;
126
+ } catch (error) {
127
+ // Fallback to CommonJS
128
+ try {
129
+ const config = require(configPath);
130
+ return config.default || config;
131
+ } catch (err) {
132
+ console.warn(`Warning: Could not load config file: ${configPath}`);
133
+ return {};
134
+ }
135
+ }
136
+ }
137
+
138
+ async function main() {
139
+ const args = parseArgs();
140
+
141
+ if (args.showHelp) {
142
+ console.log(HELP_TEXT);
143
+ process.exit(0);
144
+ }
145
+
146
+ if (!args.command || args.command !== 'generate') {
147
+ console.error('Error: No command specified. Use "generate" to create icon sprite.');
148
+ console.log('\nRun "truenas-icons --help" for usage information.');
149
+ process.exit(1);
150
+ }
151
+
152
+ try {
153
+ // Load configuration file
154
+ const fileConfig = await loadConfig(args.configFile);
155
+
156
+ // Merge configurations (CLI args take precedence)
157
+ const config = {
158
+ srcDirs: args.srcDirs || fileConfig.srcDirs,
159
+ outputDir: args.outputDir || fileConfig.outputDir,
160
+ spriteUrlPath: args.spriteUrlPath || fileConfig.spriteUrlPath,
161
+ customIconsDir: args.customIconsDir || fileConfig.customIconsDir,
162
+ projectRoot: process.cwd(),
163
+ };
164
+
165
+ console.log('Generating icon sprite...\n');
166
+ await generateSprite(config);
167
+ console.log('\nIcon sprite generated successfully!');
168
+ } catch (error: any) {
169
+ console.error('\nError generating sprite:', error.message);
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ main();
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI wrapper that uses tsx to run the TypeScript CLI
5
+ * This allows us to distribute TypeScript files without needing to compile them
6
+ */
7
+
8
+ const { spawn } = require('child_process');
9
+ const path = require('path');
10
+
11
+ // Get the directory where this script is located
12
+ const scriptDir = __dirname;
13
+ const tsxPath = path.join(scriptDir, '../../node_modules/.bin/tsx');
14
+ const cliPath = path.join(scriptDir, 'cli-main.ts');
15
+
16
+ // Check if tsx exists in the package
17
+ const fs = require('fs');
18
+ if (!fs.existsSync(path.join(scriptDir, '../../node_modules/tsx'))) {
19
+ console.error('Error: tsx is required but not found.');
20
+ console.error('This should have been installed as a dependency of truenas-ui.');
21
+ process.exit(1);
22
+ }
23
+
24
+ // Spawn tsx to run the TypeScript CLI
25
+ const child = spawn('node', [tsxPath, cliPath, ...process.argv.slice(2)], {
26
+ stdio: 'inherit',
27
+ cwd: process.cwd()
28
+ });
29
+
30
+ child.on('exit', (code) => {
31
+ process.exit(code || 0);
32
+ });
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI wrapper that uses tsx to run the TypeScript CLI
5
+ * This allows us to distribute TypeScript files without needing to compile them
6
+ */
7
+
8
+ const { spawn } = require('child_process');
9
+ const path = require('path');
10
+
11
+ // Get the directory where this script is located
12
+ const scriptDir = __dirname;
13
+ const cliPath = path.join(scriptDir, 'cli-main.ts');
14
+
15
+ // Use npx to run tsx, which will find it in node_modules
16
+ const child = spawn('npx', ['tsx', cliPath, ...process.argv.slice(2)], {
17
+ stdio: 'inherit',
18
+ cwd: process.cwd(),
19
+ shell: process.platform === 'win32'
20
+ });
21
+
22
+ child.on('exit', (code) => {
23
+ process.exit(code || 0);
24
+ });
25
+
26
+ child.on('error', (err) => {
27
+ console.error('Failed to start tsx:', err.message);
28
+ console.error('Make sure tsx is installed as a dependency of truenas-ui.');
29
+ process.exit(1);
30
+ });
@@ -0,0 +1,171 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import { resolve } from 'path';
4
+ import { buildSprite } from './lib/build-sprite';
5
+ import { findIconsInTemplates } from './lib/find-icons-in-templates';
6
+ import { findIconsWithMarker } from './lib/find-icons-with-marker';
7
+ import { getIconPaths } from './lib/get-icon-paths';
8
+ import { warnAboutDuplicates } from './lib/warn-about-duplicates';
9
+ import { SpriteGeneratorConfig, resolveConfig } from './sprite-config-interface';
10
+
11
+ /**
12
+ * Generates an icon sprite based on the provided configuration
13
+ *
14
+ * @param config - Configuration options for sprite generation
15
+ * @returns Promise that resolves when sprite generation is complete
16
+ */
17
+ export async function generateSprite(config: SpriteGeneratorConfig = {}): Promise<void> {
18
+ try {
19
+ const resolved = resolveConfig(config);
20
+
21
+ // Resolve all paths relative to project root
22
+ const srcDirs = resolved.srcDirs.map(dir => resolve(resolved.projectRoot, dir));
23
+ const outputDir = resolve(resolved.projectRoot, resolved.outputDir);
24
+ const targetPath = resolve(outputDir, 'sprite.svg');
25
+ const configPath = resolve(outputDir, 'sprite-config.json');
26
+
27
+ // Ensure output directory exists
28
+ if (!fs.existsSync(outputDir)) {
29
+ fs.mkdirSync(outputDir, { recursive: true });
30
+ }
31
+
32
+ // Load library icons FIRST (if truenas-ui is installed as a dependency)
33
+ const libraryIcons = loadLibraryIcons(resolved.projectRoot);
34
+ if (libraryIcons.size > 0) {
35
+ console.info(`Loaded ${libraryIcons.size} icon(s) from truenas-ui library`);
36
+ }
37
+
38
+ // Scan all source directories for icon usage, skipping icons already in library
39
+ const templateIcons = new Set<string>();
40
+ const markerIcons = new Set<string>();
41
+
42
+ for (const srcDir of srcDirs) {
43
+ if (fs.existsSync(srcDir)) {
44
+ const dirTemplateIcons = findIconsInTemplates(srcDir, libraryIcons);
45
+ const dirMarkerIcons = findIconsWithMarker(srcDir, libraryIcons);
46
+
47
+ dirTemplateIcons.forEach(icon => templateIcons.add(icon));
48
+ dirMarkerIcons.forEach(icon => markerIcons.add(icon));
49
+ } else {
50
+ console.warn(`Source directory not found, skipping: ${srcDir}`);
51
+ }
52
+ }
53
+
54
+ // Combine library icons + consumer-specific icons
55
+ const consumerIconCount = templateIcons.size + markerIcons.size;
56
+ const usedIcons = new Set([...libraryIcons, ...templateIcons, ...markerIcons]);
57
+
58
+ if (consumerIconCount > 0) {
59
+ console.info(`Found ${consumerIconCount} additional icon(s) in consumer application`);
60
+ }
61
+
62
+ // Add custom icons if directory is specified
63
+ let allIcons: Set<string>;
64
+ if (resolved.customIconsDir) {
65
+ const customDir = resolve(resolved.projectRoot, resolved.customIconsDir);
66
+ if (fs.existsSync(customDir)) {
67
+ // Temporarily set __dirname context for addCustomIcons
68
+ // This is a workaround since addCustomIcons uses __dirname
69
+ allIcons = addCustomIconsFromPath(usedIcons, customDir);
70
+ } else {
71
+ console.warn(`Custom icons directory not found: ${customDir}`);
72
+ allIcons = usedIcons;
73
+ }
74
+ } else {
75
+ allIcons = usedIcons;
76
+ }
77
+
78
+ warnAboutDuplicates(allIcons);
79
+
80
+ if (!allIcons.size) {
81
+ throw new Error('No icons found in the project. Make sure your templates include <tn-icon> elements or use tnIconMarker() for dynamic icons.');
82
+ }
83
+
84
+ const icons = getIconPaths(allIcons, resolved.projectRoot);
85
+
86
+ const result = await buildSprite(icons);
87
+ const file = Object.values(result)[0].sprite;
88
+
89
+ const buffer = file.contents as Buffer;
90
+ const size = buffer.length / 1024;
91
+
92
+ fs.writeFileSync(targetPath, buffer);
93
+
94
+ // eslint-disable-next-line sonarjs/hashing
95
+ const hash = crypto.createHash('md5').update(buffer).digest('hex').slice(0, 10);
96
+
97
+ // Use configured sprite URL path (allows consumers to specify runtime URL separately from output dir)
98
+ const versionedUrl = `${resolved.spriteUrlPath}/sprite.svg?v=${hash}`;
99
+
100
+ fs.writeFileSync(configPath, JSON.stringify({
101
+ iconUrl: versionedUrl,
102
+ icons: Array.from(allIcons).sort()
103
+ }, null, 2));
104
+
105
+ console.info(`✓ Generated icon sprite with ${allIcons.size} icons (${size.toFixed(2)} KiB)`);
106
+ console.info(`✓ Versioned sprite URL: ${versionedUrl}`);
107
+ console.info(`✓ Output: ${targetPath}`);
108
+ } catch (error) {
109
+ console.error('Error when building the icon sprite:', error);
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Helper function to add custom icons from a specific path
116
+ */
117
+ function addCustomIconsFromPath(usedIcons: Set<string>, customIconsPath: string): Set<string> {
118
+ const customIcons = new Set<string>();
119
+ const unusedCustomIcons = new Set<string>();
120
+
121
+ if (!fs.existsSync(customIconsPath)) {
122
+ return usedIcons;
123
+ }
124
+
125
+ fs.readdirSync(customIconsPath).forEach((filename) => {
126
+ if (!filename.endsWith('.svg')) {
127
+ return;
128
+ }
129
+
130
+ const icon = `tn-${filename.replace('.svg', '')}`;
131
+ if (!usedIcons.has(icon)) {
132
+ unusedCustomIcons.add(icon);
133
+ }
134
+
135
+ customIcons.add(icon);
136
+ });
137
+
138
+ if (unusedCustomIcons.size > 0) {
139
+ console.info(
140
+ `Including ${unusedCustomIcons.size} custom icon(s) not currently used (available for runtime)`,
141
+ );
142
+ }
143
+
144
+ return new Set([...customIcons, ...usedIcons]);
145
+ }
146
+
147
+ /**
148
+ * Load library icons from truenas-ui package if installed
149
+ * This ensures library-internal icons (chevrons, folder, etc.) are available in consumer apps
150
+ * Returns a Set of icon names from the library, or an empty Set if library is not installed
151
+ */
152
+ function loadLibraryIcons(projectRoot: string): Set<string> {
153
+ const librarySpritePath = resolve(
154
+ projectRoot,
155
+ 'node_modules/@truenas/ui-components/assets/tn-icons/sprite-config.json'
156
+ );
157
+
158
+ // Skip if truenas-ui is not installed (e.g., when building the library itself)
159
+ if (!fs.existsSync(librarySpritePath)) {
160
+ return new Set<string>();
161
+ }
162
+
163
+ try {
164
+ const libraryConfig = JSON.parse(fs.readFileSync(librarySpritePath, 'utf-8'));
165
+ const libraryIcons = libraryConfig.icons || [];
166
+ return new Set<string>(libraryIcons);
167
+ } catch (error) {
168
+ console.warn('Warning: Could not load library sprite config:', error);
169
+ return new Set<string>();
170
+ }
171
+ }
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ export function addCustomIcons(usedIcons: Set<string>): Set<string> {
9
+ // Library structure: custom icons are in assets/tn-icons/custom/ (not src/assets/)
10
+ const customIconsPath = resolve(__dirname, '../../../assets/tn-icons/custom');
11
+
12
+ const customIcons = new Set<string>();
13
+ const unusedCustomIcons = new Set<string>();
14
+
15
+ fs.readdirSync(customIconsPath).forEach((filename) => {
16
+ const icon = `tn-${filename.replace('.svg', '')}`;
17
+ if (!usedIcons.has(icon)) {
18
+ unusedCustomIcons.add(icon);
19
+ }
20
+
21
+ customIcons.add(icon);
22
+ });
23
+
24
+ // For a library, custom icons should always be included so consumers can use them
25
+ // Just log unused icons for informational purposes
26
+ if (unusedCustomIcons.size > 0) {
27
+ console.info(
28
+ `Including ${unusedCustomIcons.size} custom icon(s) not currently used in library components (available for consumers)`,
29
+ );
30
+ }
31
+
32
+ return new Set([...customIcons, ...usedIcons]);
33
+ }
@@ -0,0 +1,25 @@
1
+ import fs from 'fs';
2
+ import Spriter from 'svg-sprite';
3
+ import File from 'vinyl';
4
+
5
+ export type SpriteResult = Record<string, { sprite: File }>;
6
+
7
+ export async function buildSprite(icons: Map<string, string>): Promise<SpriteResult> {
8
+ const spriter = new Spriter({
9
+ mode: {
10
+ stack: true,
11
+ },
12
+ });
13
+
14
+ icons.forEach((path, name) => {
15
+ try {
16
+ spriter.add(name, null, fs.readFileSync(path, 'utf-8'));
17
+ } catch (error) {
18
+ console.error(`Failed to add icon "${name}": `);
19
+ throw error;
20
+ }
21
+ });
22
+
23
+ const { result } = await spriter.compileAsync() as { result: SpriteResult };
24
+ return result;
25
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'fs';
2
+ import fg from 'fast-glob';
3
+ import * as cheerio from 'cheerio';
4
+
5
+ export function findIconsInTemplates(path: string, skipIcons?: Set<string>): Set<string> {
6
+ const iconNames = new Set<string>();
7
+
8
+ const templates = fg.sync(`${path}/**/*.html`);
9
+
10
+ templates.forEach((template) => {
11
+ const content = fs.readFileSync(template, 'utf-8');
12
+ const parsedTemplate = cheerio.load(content);
13
+
14
+ // Helper function to extract icon names from elements (used for both tn-icon and tn-icon-button)
15
+ const processIconElement = (iconTag: cheerio.Element) => {
16
+ // Check both 'name' and '[name]' attributes (Angular binding syntax)
17
+ const staticName = parsedTemplate(iconTag).attr('name');
18
+ const boundName = parsedTemplate(iconTag).attr('[name]');
19
+ const library = parsedTemplate(iconTag).attr('library');
20
+
21
+ const extractedNames: string[] = [];
22
+
23
+ // Handle static name attribute: name="folder"
24
+ if (staticName) {
25
+ extractedNames.push(staticName);
26
+ }
27
+
28
+ // Handle bound name attribute: [name]="expression"
29
+ // Extract string literals from the expression
30
+ if (boundName) {
31
+ // Match string literals that are values, not comparison operands
32
+ // This handles:
33
+ // - Ternary: isExpanded ? 'chevron-down' : 'chevron-right'
34
+ // - But skips comparisons: element.status === 'Active' ? 'check' : 'close'
35
+ //
36
+ // Strategy: Match quoted strings that come after ? or : (ternary results)
37
+ // or are standalone (not part of a comparison)
38
+ const ternaryResultRegex = /[?:]\s*['"]([^'"]+)['"]/g;
39
+ let match;
40
+ while ((match = ternaryResultRegex.exec(boundName)) !== null) {
41
+ const iconName = match[1];
42
+ // Valid icon names only contain alphanumeric, hyphens, and underscores
43
+ if (/^[a-z0-9\-_]+$/i.test(iconName)) {
44
+ extractedNames.push(iconName);
45
+ }
46
+ }
47
+
48
+ // Also handle simple string literals (no ternary, no comparison)
49
+ // e.g., [name]="'folder'" (though this is unusual)
50
+ if (!boundName.includes('?') && !boundName.includes('=')) {
51
+ const simpleStringRegex = /^['"]([^'"]+)['"]$/;
52
+ const simpleMatch = boundName.match(simpleStringRegex);
53
+ if (simpleMatch && /^[a-z0-9\-_]+$/i.test(simpleMatch[1])) {
54
+ extractedNames.push(simpleMatch[1]);
55
+ }
56
+ }
57
+ }
58
+
59
+ if (extractedNames.length === 0) {
60
+ return;
61
+ }
62
+
63
+ // Process each extracted name
64
+ extractedNames.forEach((iconName) => {
65
+ // Skip icons with registry format (e.g., "mdi:menu", "lucide:home")
66
+ // These are resolved at runtime via icon registry, not sprite
67
+ if (iconName.includes(':')) {
68
+ return;
69
+ }
70
+
71
+ // Determine the final icon name with appropriate prefix
72
+ let finalIconName: string;
73
+
74
+ // Handle library attribute - prefix the icon name with library prefix
75
+ if (library === 'mdi' && !iconName.startsWith('mdi-')) {
76
+ finalIconName = `mdi-${iconName}`;
77
+ } else if (library === 'custom' && !iconName.startsWith('app-') && !iconName.startsWith('tn-')) {
78
+ // Consumer custom icons get app- prefix
79
+ // (Library templates should never use library="custom", they use libIconMarker() instead)
80
+ finalIconName = `app-${iconName}`;
81
+ } else if (library === 'material' && !iconName.startsWith('mat-')) {
82
+ finalIconName = `mat-${iconName}`; // Material icons get mat- prefix
83
+ } else {
84
+ finalIconName = iconName;
85
+ }
86
+
87
+ // Skip if already provided by library
88
+ if (skipIcons?.has(finalIconName)) {
89
+ return;
90
+ }
91
+
92
+ iconNames.add(finalIconName);
93
+ });
94
+ };
95
+
96
+ // Scan tn-icon elements
97
+ parsedTemplate('tn-icon').each((_, iconTag) => {
98
+ processIconElement(iconTag);
99
+ });
100
+
101
+ // Scan tn-icon-button elements (they also have name and library attributes)
102
+ parsedTemplate('tn-icon-button').each((_, iconTag) => {
103
+ processIconElement(iconTag);
104
+ });
105
+ });
106
+
107
+ return iconNames;
108
+ }
@@ -0,0 +1,63 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ export function findIconsWithMarker(path: string, skipIcons?: Set<string>): Set<string> {
4
+ // Updated regex to capture tnIconMarker() and libIconMarker() calls with optional second parameter
5
+ // Matches: tnIconMarker('name') or tnIconMarker('name', 'library') or libIconMarker('tn-name')
6
+ const command = `grep -rEo "(tn|lib)IconMarker\\\\('[^']+',?\\s*'?[^'\\)]*'?\\)" --include="*.ts" --include="*.html" ${path}`;
7
+
8
+ const icons = new Set<string>();
9
+
10
+ try {
11
+ const output = execSync(command, { encoding: 'utf-8' });
12
+ output
13
+ .split('\n')
14
+ .filter(Boolean)
15
+ .forEach((line) => {
16
+ const [, match] = line.split(':');
17
+ if (!match) {
18
+ return;
19
+ }
20
+
21
+ // Extract icon name (first parameter)
22
+ const iconNameMatch = /'([^']+)'/.exec(match);
23
+ if (!iconNameMatch) {
24
+ return;
25
+ }
26
+
27
+ let iconName = iconNameMatch[1];
28
+
29
+ // Extract library parameter (second parameter) if present
30
+ // Look for pattern: , 'library' after the icon name
31
+ const libraryMatch = /,\s*'([^']+)'/.exec(match);
32
+ const library = libraryMatch?.[1];
33
+
34
+ // Apply prefix transformation (matching runtime tnIconMarker() logic)
35
+ if (library === 'mdi' && !iconName.startsWith('mdi-')) {
36
+ iconName = `mdi-${iconName}`;
37
+ } else if (library === 'custom' && !iconName.startsWith('app-')) {
38
+ iconName = `app-${iconName}`;
39
+ } else if (library === 'material' && !iconName.startsWith('mat-')) {
40
+ iconName = `mat-${iconName}`;
41
+ }
42
+ // Material icons get mat- prefix
43
+ // libIconMarker already has tn- prefix, no transformation needed
44
+
45
+ // Skip if already provided by library
46
+ if (skipIcons?.has(iconName)) {
47
+ return;
48
+ }
49
+
50
+ icons.add(iconName);
51
+ });
52
+ } catch (error: any) {
53
+ // grep returns exit code 1 when no matches are found, which is not an error
54
+ if (error.status === 1 && !error.stderr) {
55
+ // No matches found, return empty set
56
+ return icons;
57
+ }
58
+ // Re-throw actual errors
59
+ throw error;
60
+ }
61
+
62
+ return icons;
63
+ }