@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.
- package/README.md +335 -0
- package/assets/tn-icons/custom/cloud-off.svg +1 -0
- package/assets/tn-icons/custom/dataset-root.svg +3 -0
- package/assets/tn-icons/custom/dataset.svg +3 -0
- package/assets/tn-icons/custom/enclosure.svg +21 -0
- package/assets/tn-icons/custom/ha-disabled.svg +10 -0
- package/assets/tn-icons/custom/ha-enabled.svg +1 -0
- package/assets/tn-icons/custom/ha-reconnecting.svg +10 -0
- package/assets/tn-icons/custom/hdd-mirror.svg +3 -0
- package/assets/tn-icons/custom/hdd.svg +3 -0
- package/assets/tn-icons/custom/iscsi-share.svg +3 -0
- package/assets/tn-icons/custom/layout-full.svg +8 -0
- package/assets/tn-icons/custom/layout-half-and-quarters.svg +7 -0
- package/assets/tn-icons/custom/layout-halves.svg +6 -0
- package/assets/tn-icons/custom/layout-quarters-and-half.svg +7 -0
- package/assets/tn-icons/custom/layout-quarters.svg +8 -0
- package/assets/tn-icons/custom/network-upload-download-both.svg +4 -0
- package/assets/tn-icons/custom/network-upload-download-disabled.svg +7 -0
- package/assets/tn-icons/custom/network-upload-download-down.svg +4 -0
- package/assets/tn-icons/custom/network-upload-download-up.svg +4 -0
- package/assets/tn-icons/custom/network-upload-download.svg +4 -0
- package/assets/tn-icons/custom/nfs-share.svg +3 -0
- package/assets/tn-icons/custom/nvme-share.svg +7 -0
- package/assets/tn-icons/custom/replication.svg +14 -0
- package/assets/tn-icons/custom/smb-share.svg +3 -0
- package/assets/tn-icons/custom/ssd-mirror.svg +3 -0
- package/assets/tn-icons/custom/ssd.svg +3 -0
- package/assets/tn-icons/custom/true-cloud.svg +10 -0
- package/assets/tn-icons/custom/truecommand-logo-mark-color.svg +14 -0
- package/assets/tn-icons/custom/truecommand-logo-mark.svg +10 -0
- package/assets/tn-icons/custom/truenas-connect-logo.svg +1 -0
- package/assets/tn-icons/custom/truenas-logo-ce-color.svg +1 -0
- package/assets/tn-icons/custom/truenas-logo-ce.svg +4 -0
- package/assets/tn-icons/custom/truenas-logo-enterprise-color.svg +19 -0
- package/assets/tn-icons/custom/truenas-logo-enterprise.svg +13 -0
- package/assets/tn-icons/custom/truenas-logo-mark-color.svg +7 -0
- package/assets/tn-icons/custom/truenas-logo-mark.svg +4 -0
- package/assets/tn-icons/custom/truenas-logo-type-color.svg +4 -0
- package/assets/tn-icons/custom/truenas-logo-type.svg +4 -0
- package/assets/tn-icons/custom/truenas-logo.svg +4 -0
- package/assets/tn-icons/custom/two-factor-auth.svg +1 -0
- package/assets/tn-icons/sprite-config.json +78 -0
- package/assets/tn-icons/sprite.svg +1 -0
- package/fesm2022/truenas-ui-components.mjs +8063 -0
- package/fesm2022/truenas-ui-components.mjs.map +1 -0
- package/package.json +45 -0
- package/scripts/icon-sprite/cli-main.ts +174 -0
- package/scripts/icon-sprite/cli-wrapper.js +32 -0
- package/scripts/icon-sprite/cli.js +30 -0
- package/scripts/icon-sprite/generate-sprite.ts +171 -0
- package/scripts/icon-sprite/lib/add-custom-icons.ts +33 -0
- package/scripts/icon-sprite/lib/build-sprite.ts +25 -0
- package/scripts/icon-sprite/lib/find-icons-in-templates.ts +108 -0
- package/scripts/icon-sprite/lib/find-icons-with-marker.ts +63 -0
- package/scripts/icon-sprite/lib/get-icon-paths.ts +95 -0
- package/scripts/icon-sprite/lib/warn-about-duplicates.ts +13 -0
- package/scripts/icon-sprite/make-sprite.ts +26 -0
- package/scripts/icon-sprite/sprite-config-interface.ts +72 -0
- package/src/assets/icons/dataset.svg +3 -0
- package/src/styles/dialog.css +150 -0
- package/src/styles/themes.css +665 -0
- 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
|
+
}
|