@shival99/z-ui 2.1.5 → 2.1.7

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.
@@ -0,0 +1,645 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * z-init-project — bootstrap an Angular project to consume @shival99/z-ui.
4
+ *
5
+ * Run this from the ROOT of the target Angular project (the one you want to set
6
+ * up), NOT from inside z-ui. It will:
7
+ * 1. Detect whether the target is an Nx monorepo (project.json) or a single
8
+ * Angular app (angular.json), and which project to configure.
9
+ * 2. Install the required dependencies (z-ui, Tailwind v4, PostCSS, icons).
10
+ * 3. Wire up Tailwind v4 + the chosen z-ui theme into the build styles array.
11
+ * 4. Create .postcssrc.json.
12
+ * 5. Inject the Be Vietnam Pro + Inter Tight Google Fonts <link> into index.html.
13
+ * 6. Create/patch src styles (font-sans variable).
14
+ * 7. Patch app.config.ts with z-ui providers (icon loader, toast, translate, theme).
15
+ * 8. Patch the root standalone component to preloadTheme(<theme>) on init.
16
+ *
17
+ * Usage:
18
+ * node z-init-project.mjs # interactive
19
+ * node z-init-project.mjs --theme neutral --project my-app --yes
20
+ * node z-init-project.mjs --dry-run # show what would change, write nothing
21
+ *
22
+ * Flags:
23
+ * --theme <name> one of the available themes (default: neutral)
24
+ * --project <name> project name (monorepo only; defaults to the single/first app)
25
+ * --no-install skip dependency installation
26
+ * --yes, -y accept defaults, no prompts
27
+ * --dry-run print planned changes without writing
28
+ * --cwd <path> target project root (default: current directory)
29
+ */
30
+
31
+ import { execSync } from 'node:child_process';
32
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
33
+ import { createInterface } from 'node:readline';
34
+ import { dirname, join, relative, resolve } from 'node:path';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Constants
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const Z_UI_PKG = '@shival99/z-ui';
41
+
42
+ const AVAILABLE_THEMES = ['neutral', 'gray', 'slate', 'stone', 'zinc', 'green', 'orange', 'violet', 'hospital'];
43
+
44
+ const DEFAULT_THEME = 'neutral';
45
+
46
+ /** Runtime deps a consuming app needs. Versions track attendance-pull's setup. */
47
+ const DEPENDENCIES = [
48
+ `${Z_UI_PKG}@latest`,
49
+ 'tailwindcss@^4.3.0',
50
+ '@tailwindcss/postcss@^4.3.0',
51
+ 'postcss@^8.5.15',
52
+ 'autoprefixer@^10.5.0',
53
+ ];
54
+
55
+ const FONT_LINK_HREF =
56
+ 'https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Inter+Tight:ital,wght@0,100..900;1,100..900&display=swap';
57
+
58
+ const FONT_LINK_TAG = ` <link\n href="${FONT_LINK_HREF}"\n rel="stylesheet"\n />`;
59
+
60
+ const STYLES_CSS_CONTENT = `:root {
61
+ --font-sans: 'Be Vietnam Pro', 'Inter Tight', 'IBM Plex Sans', system-ui, -apple-system, sans-serif;
62
+ }
63
+
64
+ * {
65
+ font-family: var(--font-sans) !important;
66
+ }
67
+ `;
68
+
69
+ const POSTCSS_CONTENT = `${JSON.stringify({ plugins: { '@tailwindcss/postcss': {} } }, null, 2)}\n`;
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Tiny CLI helpers
73
+ // ---------------------------------------------------------------------------
74
+
75
+ const C = {
76
+ reset: '\x1b[0m',
77
+ dim: '\x1b[2m',
78
+ bold: '\x1b[1m',
79
+ green: '\x1b[32m',
80
+ yellow: '\x1b[33m',
81
+ red: '\x1b[31m',
82
+ cyan: '\x1b[36m',
83
+ };
84
+
85
+ const log = {
86
+ step: m => console.log(`${C.cyan}▸${C.reset} ${m}`),
87
+ ok: m => console.log(`${C.green}✓${C.reset} ${m}`),
88
+ warn: m => console.log(`${C.yellow}!${C.reset} ${m}`),
89
+ err: m => console.error(`${C.red}✗${C.reset} ${m}`),
90
+ info: m => console.log(`${C.dim}${m}${C.reset}`),
91
+ };
92
+
93
+ function parseArgs(argv) {
94
+ const args = { _: [] };
95
+ for (let i = 0; i < argv.length; i++) {
96
+ const a = argv[i];
97
+ if (a === '--yes' || a === '-y') args.yes = true;
98
+ else if (a === '--dry-run') args.dryRun = true;
99
+ else if (a === '--no-install') args.noInstall = true;
100
+ else if (a === '--theme') args.theme = argv[++i];
101
+ else if (a === '--project') args.project = argv[++i];
102
+ else if (a === '--cwd') args.cwd = argv[++i];
103
+ else if (a === '--help' || a === '-h') args.help = true;
104
+ else args._.push(a);
105
+ }
106
+ return args;
107
+ }
108
+
109
+ function printHelp() {
110
+ console.log(`${C.bold}z-ui-init${C.reset} — set up an Angular project to consume ${Z_UI_PKG}
111
+
112
+ ${C.bold}Usage${C.reset}
113
+ npx ${Z_UI_PKG} z-ui-init [options] ${C.dim}# in your project root${C.reset}
114
+ z-ui-init [options]
115
+
116
+ ${C.bold}Options${C.reset}
117
+ --theme <name> theme to wire in (default: ${DEFAULT_THEME})
118
+ ${C.dim}${AVAILABLE_THEMES.join(', ')}${C.reset}
119
+ --project <name> project to configure (monorepo; defaults to the single/first app)
120
+ --cwd <path> target project root (default: current directory)
121
+ --no-install skip dependency installation
122
+ --yes, -y accept defaults, no prompts
123
+ --dry-run print planned changes without writing
124
+ --help, -h show this help`);
125
+ }
126
+
127
+ function prompt(question) {
128
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
129
+ return new Promise(res =>
130
+ rl.question(question, ans => {
131
+ rl.close();
132
+ res(ans.trim());
133
+ })
134
+ );
135
+ }
136
+
137
+ async function chooseTheme(yes, preselected) {
138
+ if (preselected) {
139
+ if (!AVAILABLE_THEMES.includes(preselected)) {
140
+ log.err(`Unknown theme "${preselected}". Available: ${AVAILABLE_THEMES.join(', ')}`);
141
+ process.exit(1);
142
+ }
143
+ return preselected;
144
+ }
145
+ if (yes) return DEFAULT_THEME;
146
+
147
+ console.log(`\n${C.bold}Choose a theme:${C.reset}`);
148
+ AVAILABLE_THEMES.forEach((t, i) => {
149
+ const marker = t === DEFAULT_THEME ? `${C.dim}(default)${C.reset}` : '';
150
+ console.log(` ${i + 1}) ${t} ${marker}`);
151
+ });
152
+ const ans = await prompt(`Theme [1-${AVAILABLE_THEMES.length} or name, Enter=${DEFAULT_THEME}]: `);
153
+ if (!ans) return DEFAULT_THEME;
154
+ const byIndex = AVAILABLE_THEMES[Number(ans) - 1];
155
+ if (byIndex) return byIndex;
156
+ if (AVAILABLE_THEMES.includes(ans)) return ans;
157
+ log.warn(`"${ans}" not recognized, falling back to ${DEFAULT_THEME}.`);
158
+ return DEFAULT_THEME;
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // File utilities
163
+ // ---------------------------------------------------------------------------
164
+
165
+ let DRY_RUN = false;
166
+
167
+ function read(file) {
168
+ return readFileSync(file, 'utf8');
169
+ }
170
+
171
+ function write(file, content) {
172
+ if (DRY_RUN) {
173
+ log.info(`[dry-run] would write ${file}`);
174
+ return;
175
+ }
176
+ mkdirSync(dirname(file), { recursive: true });
177
+ writeFileSync(file, content);
178
+ }
179
+
180
+ function readJson(file) {
181
+ return JSON.parse(read(file));
182
+ }
183
+
184
+ function writeJson(file, obj) {
185
+ write(file, `${JSON.stringify(obj, null, 2)}\n`);
186
+ }
187
+
188
+ /** Find the first existing path among candidates. */
189
+ function firstExisting(paths) {
190
+ return paths.find(p => existsSync(p)) ?? null;
191
+ }
192
+
193
+ /** Absolute filesystem path inside the project's source root. */
194
+ function absSrc(ws, ...parts) {
195
+ return join(ws.root, ws.sourceRoot, ...parts);
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Workspace detection
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Resolve the target project: its name, the config file that owns its build
204
+ * target (angular.json or project.json), and the source root + index/styles.
205
+ */
206
+ function detectWorkspace(root, requestedProject) {
207
+ const nxJson = join(root, 'nx.json');
208
+ const angularJson = join(root, 'angular.json');
209
+ const isNx = existsSync(nxJson);
210
+
211
+ if (isNx) {
212
+ log.step('Detected Nx monorepo (nx.json present).');
213
+ const projects = discoverNxProjects(root);
214
+ if (projects.length === 0) {
215
+ log.err('No project.json with an Angular application target found under apps/* or libs/*.');
216
+ process.exit(1);
217
+ }
218
+ let project = projects[0];
219
+ if (requestedProject) {
220
+ const found = projects.find(p => p.name === requestedProject);
221
+ if (!found) {
222
+ log.err(`Project "${requestedProject}" not found. Available: ${projects.map(p => p.name).join(', ')}`);
223
+ process.exit(1);
224
+ }
225
+ project = found;
226
+ } else if (projects.length > 1) {
227
+ log.warn(
228
+ `Multiple projects found, using "${project.name}". Pass --project to pick another: ${projects
229
+ .map(p => p.name)
230
+ .join(', ')}`
231
+ );
232
+ }
233
+ return { kind: 'nx', root, ...project };
234
+ }
235
+
236
+ if (existsSync(angularJson)) {
237
+ log.step('Detected single Angular workspace (angular.json present).');
238
+ const cfg = readJson(angularJson);
239
+ const names = Object.keys(cfg.projects || {});
240
+ const appNames = names.filter(n => cfg.projects[n].projectType !== 'library');
241
+ if (appNames.length === 0) {
242
+ log.err('No application project found in angular.json.');
243
+ process.exit(1);
244
+ }
245
+ const name = requestedProject || cfg.defaultProject || appNames[0];
246
+ if (!cfg.projects[name]) {
247
+ log.err(`Project "${name}" not found in angular.json. Available: ${names.join(', ')}`);
248
+ process.exit(1);
249
+ }
250
+ return {
251
+ kind: 'angular',
252
+ root,
253
+ name,
254
+ configFile: angularJson,
255
+ sourceRoot: cfg.projects[name].sourceRoot || 'src',
256
+ };
257
+ }
258
+
259
+ log.err('Neither angular.json nor nx.json found. Run this from an Angular project root.');
260
+ process.exit(1);
261
+ }
262
+
263
+ function discoverNxProjects(root) {
264
+ const out = [];
265
+ for (const baseName of ['apps', 'libs']) {
266
+ const base = join(root, baseName);
267
+ if (!existsSync(base)) continue;
268
+ for (const entry of readdirSync(base)) {
269
+ const projDir = join(base, entry);
270
+ if (!statSync(projDir).isDirectory()) continue;
271
+ const projectJson = join(projDir, 'project.json');
272
+ if (!existsSync(projectJson)) continue;
273
+ const cfg = readJson(projectJson);
274
+ const target = findApplicationTarget(cfg);
275
+ if (!target) continue;
276
+ out.push({
277
+ name: cfg.name || entry,
278
+ configFile: projectJson,
279
+ // Relative to workspace root, e.g. "apps/my-app/src".
280
+ sourceRoot: cfg.sourceRoot || relative(root, join(projDir, 'src')),
281
+ buildTargetKey: target,
282
+ });
283
+ }
284
+ }
285
+ return out;
286
+ }
287
+
288
+ /** Return the name of the build target that uses the Angular application builder. */
289
+ function findApplicationTarget(projectCfg) {
290
+ const targets = projectCfg.targets || {};
291
+ for (const [key, t] of Object.entries(targets)) {
292
+ const exec = t.executor || t.builder || '';
293
+ if (exec.includes('@angular/build:application') || exec.includes('@angular-devkit/build-angular')) {
294
+ return key;
295
+ }
296
+ }
297
+ return targets.build ? 'build' : null;
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Setup steps
302
+ // ---------------------------------------------------------------------------
303
+
304
+ function installDeps(root) {
305
+ const pm = detectPackageManager(root);
306
+ const addCmd = { pnpm: 'pnpm add', npm: 'npm install', yarn: 'yarn add', bun: 'bun add' }[pm];
307
+ const cmd = `${addCmd} ${DEPENDENCIES.join(' ')}`;
308
+ log.step(`Installing dependencies with ${pm}…`);
309
+ log.info(cmd);
310
+ if (DRY_RUN) {
311
+ log.info('[dry-run] skipping install');
312
+ return;
313
+ }
314
+ execSync(cmd, { cwd: root, stdio: 'inherit' });
315
+ log.ok('Dependencies installed.');
316
+ }
317
+
318
+ function detectPackageManager(root) {
319
+ if (existsSync(join(root, 'pnpm-lock.yaml'))) return 'pnpm';
320
+ if (existsSync(join(root, 'yarn.lock'))) return 'yarn';
321
+ if (existsSync(join(root, 'bun.lockb'))) return 'bun';
322
+ return 'npm';
323
+ }
324
+
325
+ /** Inject the z-ui style entries (tailwind + chosen theme) into the build target. */
326
+ function patchBuildStyles(ws, theme) {
327
+ const tailwindEntry = `node_modules/${Z_UI_PKG}/assets/css/tailwind.css`;
328
+ const themeEntry = `node_modules/${Z_UI_PKG}/assets/css/themes/${theme}.css`;
329
+ const appStyles = `${ws.sourceRoot}/styles.css`.replace(/\\/g, '/');
330
+
331
+ const cfg = readJson(ws.configFile);
332
+ let options;
333
+ if (ws.kind === 'nx') {
334
+ const targetKey = ws.buildTargetKey || 'build';
335
+ cfg.targets ||= {};
336
+ cfg.targets[targetKey] ||= {};
337
+ cfg.targets[targetKey].options ||= {};
338
+ options = cfg.targets[targetKey].options;
339
+ } else {
340
+ const proj = cfg.projects[ws.name];
341
+ proj.architect ||= proj.targets;
342
+ const arch = proj.architect || proj.targets;
343
+ arch.build ||= {};
344
+ arch.build.options ||= {};
345
+ options = arch.build.options;
346
+ }
347
+
348
+ const styles = Array.isArray(options.styles) ? options.styles : [];
349
+ const desired = [tailwindEntry, themeEntry, appStyles];
350
+
351
+ // Drop any previously-added z-ui tailwind/theme entries so re-runs stay clean.
352
+ const cleaned = styles.filter(s => {
353
+ const str = typeof s === 'string' ? s : s.input;
354
+ if (!str) return true;
355
+ if (str.includes(`${Z_UI_PKG}/assets/css/tailwind.css`)) return false;
356
+ if (str.includes(`${Z_UI_PKG}/assets/css/themes/`)) return false;
357
+ return true;
358
+ });
359
+
360
+ // Ensure the app styles entry exists exactly once, after the z-ui entries.
361
+ const withoutApp = cleaned.filter(s => {
362
+ const str = typeof s === 'string' ? s : s.input;
363
+ return str !== appStyles;
364
+ });
365
+
366
+ options.styles = [...desired, ...withoutApp.filter(s => !desired.includes(s))];
367
+
368
+ writeJson(ws.configFile, cfg);
369
+ log.ok(`Build styles set (${relative(process.cwd(), ws.configFile)}):`);
370
+ desired.forEach(s => log.info(` • ${s}`));
371
+ }
372
+
373
+ function createPostcssrc(root) {
374
+ const target = join(root, '.postcssrc.json');
375
+ if (existsSync(target)) {
376
+ log.warn('.postcssrc.json already exists — leaving it untouched.');
377
+ return;
378
+ }
379
+ write(target, POSTCSS_CONTENT);
380
+ log.ok('Created .postcssrc.json');
381
+ }
382
+
383
+ function patchIndexHtml(ws) {
384
+ const indexPath = firstExisting([
385
+ absSrc(ws, 'index.html'),
386
+ join(ws.root, ws.sourceRoot, '..', 'index.html'),
387
+ join(ws.root, 'src', 'index.html'),
388
+ ]);
389
+ if (!indexPath) {
390
+ log.warn('index.html not found — skipping font link injection.');
391
+ return;
392
+ }
393
+ let html = read(indexPath);
394
+ if (html.includes('Be+Vietnam+Pro')) {
395
+ log.warn('Be Vietnam Pro font link already present — skipping.');
396
+ return;
397
+ }
398
+ if (html.includes('</head>')) {
399
+ html = html.replace('</head>', `${FONT_LINK_TAG}\n</head>`);
400
+ } else {
401
+ log.warn('No </head> found in index.html — appending font link at top.');
402
+ html = `${FONT_LINK_TAG}\n${html}`;
403
+ }
404
+ write(indexPath, html);
405
+ log.ok(`Injected font <link> into ${relative(process.cwd(), indexPath)}`);
406
+ }
407
+
408
+ function createStylesCss(ws) {
409
+ const stylesPath = absSrc(ws, 'styles.css');
410
+ if (existsSync(stylesPath)) {
411
+ const current = read(stylesPath);
412
+ if (current.includes('--font-sans')) {
413
+ log.warn('styles.css already defines --font-sans — leaving it untouched.');
414
+ return;
415
+ }
416
+ write(stylesPath, `${STYLES_CSS_CONTENT}\n${current}`);
417
+ log.ok('Prepended font variables to existing styles.css');
418
+ return;
419
+ }
420
+ write(stylesPath, STYLES_CSS_CONTENT);
421
+ log.ok('Created src/styles.css');
422
+ }
423
+
424
+ function patchAppConfig(ws, theme) {
425
+ const configPath = firstExisting([absSrc(ws, 'app', 'app.config.ts'), absSrc(ws, 'app', 'app.config.server.ts')]);
426
+ if (!configPath) {
427
+ log.warn('app.config.ts not found — printing the providers block to add manually.');
428
+ printAppConfigSnippet(theme);
429
+ return;
430
+ }
431
+
432
+ let src = read(configPath);
433
+ if (src.includes('provideZTheme')) {
434
+ log.warn('app.config.ts already references provideZTheme — skipping provider patch.');
435
+ return;
436
+ }
437
+
438
+ // Add imports (skip any whose symbol is already imported anywhere in the file).
439
+ const wantedImports = [
440
+ [`provideHttpClient`, `import { provideHttpClient } from '@angular/common/http';`],
441
+ [`provideZIconLoader`, `import { provideZIconLoader } from '${Z_UI_PKG}/components/z-icon';`],
442
+ [`provideZToast`, `import { provideZToast } from '${Z_UI_PKG}/components/z-toast';`],
443
+ [`provideZTheme`, `import { provideZTheme, provideZTranslate } from '${Z_UI_PKG}/providers';`],
444
+ ];
445
+ const importBlock = wantedImports
446
+ .filter(([symbol]) => !src.includes(symbol))
447
+ .map(([, imp]) => imp)
448
+ .join('\n');
449
+
450
+ if (importBlock) {
451
+ const lastImport = src.lastIndexOf('import ');
452
+ const lineEnd = src.indexOf('\n', lastImport);
453
+ src = `${src.slice(0, lineEnd + 1)}${importBlock}\n${src.slice(lineEnd + 1)}`;
454
+ }
455
+
456
+ // Provider lines to add, skipping any already present in the file.
457
+ const wantedProviders = [
458
+ [`provideHttpClient`, `provideHttpClient(),`],
459
+ [`provideZIconLoader`, `provideZIconLoader(),`],
460
+ [`provideZToast`, `provideZToast({ zPosition: 'top-right', zDuration: 3000, zCloseButton: true }),`],
461
+ [`provideZTranslate`, `provideZTranslate({ defaultLang: 'vi' }),`],
462
+ [`provideZTheme`, `provideZTheme({ defaultTheme: '${theme}', defaultDarkMode: false }),`],
463
+ ];
464
+ const newProviders = wantedProviders.filter(([symbol]) => !src.includes(`${symbol}(`)).map(([, line]) => line);
465
+
466
+ const providersMatch = src.match(/providers\s*:\s*\[/);
467
+ if (!providersMatch) {
468
+ log.warn('Could not find providers array — printing snippet to add manually.');
469
+ printAppConfigSnippet(theme);
470
+ return;
471
+ }
472
+ const insertAt = providersMatch.index + providersMatch[0].length;
473
+ const indent = '\n ';
474
+ const injection = newProviders.map(l => `${indent}${l}`).join('');
475
+ src = `${src.slice(0, insertAt)}${injection}${src.slice(insertAt)}`;
476
+
477
+ write(configPath, src);
478
+ log.ok(`Patched ${relative(process.cwd(), configPath)} with z-ui providers.`);
479
+ }
480
+
481
+ function printAppConfigSnippet(theme) {
482
+ console.log(`\n${C.bold}Add these to your app.config.ts providers:${C.reset}`);
483
+ console.log(
484
+ `${C.dim}import { provideHttpClient } from '@angular/common/http';
485
+ import { provideZIconLoader } from '${Z_UI_PKG}/components/z-icon';
486
+ import { provideZToast } from '${Z_UI_PKG}/components/z-toast';
487
+ import { provideZTheme, provideZTranslate } from '${Z_UI_PKG}/providers';
488
+
489
+ providers: [
490
+ provideHttpClient(),
491
+ provideZIconLoader(),
492
+ provideZToast({ zPosition: 'top-right', zDuration: 3000, zCloseButton: true }),
493
+ provideZTranslate({ defaultLang: 'vi' }),
494
+ provideZTheme({ defaultTheme: '${theme}', defaultDarkMode: false }),
495
+ ]${C.reset}`
496
+ );
497
+ }
498
+
499
+ function patchRootComponent(ws, theme) {
500
+ const candidates = [absSrc(ws, 'app', 'app.ts'), absSrc(ws, 'app', 'app.component.ts')];
501
+ const compPath = firstExisting(candidates);
502
+ if (!compPath) {
503
+ log.warn('Root component (app.ts/app.component.ts) not found — add preloadTheme manually.');
504
+ printPreloadSnippet(theme);
505
+ return;
506
+ }
507
+
508
+ let src = read(compPath);
509
+ if (src.includes('preloadTheme')) {
510
+ log.warn('Root component already calls preloadTheme — skipping.');
511
+ return;
512
+ }
513
+
514
+ // Ensure ZThemeService import.
515
+ if (!src.includes('ZThemeService')) {
516
+ const imp = `import { ZThemeService } from '${Z_UI_PKG}/services';`;
517
+ const lastImport = src.lastIndexOf('import ');
518
+ const lineEnd = src.indexOf('\n', lastImport);
519
+ src = `${src.slice(0, lineEnd + 1)}${imp}\n${src.slice(lineEnd + 1)}`;
520
+ }
521
+
522
+ // Ensure inject + OnInit are imported from @angular/core.
523
+ src = ensureNamedImports(src, '@angular/core', ['inject', 'OnInit']);
524
+
525
+ const classMatch = src.match(/export class (\w+)([^{]*)\{/);
526
+ if (!classMatch) {
527
+ log.warn('Could not locate the root component class — add preloadTheme manually.');
528
+ printPreloadSnippet(theme);
529
+ return;
530
+ }
531
+ const className = classMatch[1];
532
+ const heritage = classMatch[2];
533
+
534
+ // Add `implements OnInit` if not present.
535
+ if (!/implements[^{]*OnInit/.test(heritage)) {
536
+ const newHeritage = /implements/.test(heritage)
537
+ ? heritage.replace(/implements\s+([^{]+)/, (m, list) => `implements ${list.trim()}, OnInit `)
538
+ : `${heritage.trimEnd()} implements OnInit `;
539
+ src = src.replace(classMatch[0], `export class ${className}${newHeritage}{`);
540
+ }
541
+
542
+ // Inject service + ngOnInit body right after the class opening brace.
543
+ const reMatch = src.match(new RegExp(`export class ${className}[^{]*\\{`));
544
+ const braceIdx = reMatch.index + reMatch[0].length;
545
+ const body = `
546
+ private readonly _themeService = inject(ZThemeService);
547
+
548
+ ngOnInit(): void {
549
+ this._themeService.preloadTheme('${theme}');
550
+ }
551
+ `;
552
+ src = `${src.slice(0, braceIdx)}${body}${src.slice(braceIdx)}`;
553
+
554
+ write(compPath, src);
555
+ log.ok(`Patched ${relative(process.cwd(), compPath)} to preloadTheme('${theme}').`);
556
+ }
557
+
558
+ /** Make sure `names` are present in the named import from `moduleSpec`. */
559
+ function ensureNamedImports(src, moduleSpec, names) {
560
+ const re = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${moduleSpec.replace(/[/@]/g, '\\$&')}['"]`);
561
+ const m = src.match(re);
562
+ if (!m) {
563
+ // No existing import from this module — add a fresh one.
564
+ const imp = `import { ${names.join(', ')} } from '${moduleSpec}';`;
565
+ return `${imp}\n${src}`;
566
+ }
567
+ const existing = m[1]
568
+ .split(',')
569
+ .map(s => s.trim())
570
+ .filter(Boolean);
571
+ const merged = Array.from(new Set([...existing, ...names]));
572
+ return src.replace(re, `import { ${merged.join(', ')} } from '${moduleSpec}'`);
573
+ }
574
+
575
+ function printPreloadSnippet(theme) {
576
+ console.log(`\n${C.bold}Add this to your root component:${C.reset}`);
577
+ console.log(
578
+ `${C.dim}import { inject, OnInit } from '@angular/core';
579
+ import { ZThemeService } from '${Z_UI_PKG}/services';
580
+
581
+ export class App implements OnInit {
582
+ private readonly _themeService = inject(ZThemeService);
583
+ ngOnInit(): void {
584
+ this._themeService.preloadTheme('${theme}');
585
+ }
586
+ }${C.reset}`
587
+ );
588
+ }
589
+
590
+ // ---------------------------------------------------------------------------
591
+ // Main
592
+ // ---------------------------------------------------------------------------
593
+
594
+ async function main() {
595
+ const args = parseArgs(process.argv.slice(2));
596
+ if (args.help) {
597
+ printHelp();
598
+ return;
599
+ }
600
+ DRY_RUN = !!args.dryRun;
601
+ const root = resolve(args.cwd || process.cwd());
602
+
603
+ console.log(`\n${C.bold}z-ui project setup${C.reset}`);
604
+ log.info(`Target: ${root}${DRY_RUN ? ' (dry-run)' : ''}\n`);
605
+
606
+ if (!existsSync(join(root, 'package.json'))) {
607
+ log.err('No package.json in target directory. Run from an Angular project root.');
608
+ process.exit(1);
609
+ }
610
+
611
+ const ws = detectWorkspace(root, args.project);
612
+ log.ok(`Configuring project "${ws.name}" (sourceRoot: ${ws.sourceRoot}).`);
613
+
614
+ const theme = await chooseTheme(args.yes, args.theme);
615
+ log.ok(`Theme: ${theme}`);
616
+
617
+ if (!args.yes && !DRY_RUN) {
618
+ const go = await prompt(`\nProceed with setup? [Y/n] `);
619
+ if (go && !/^y(es)?$/i.test(go)) {
620
+ log.warn('Aborted.');
621
+ process.exit(0);
622
+ }
623
+ }
624
+
625
+ console.log('');
626
+ if (!args.noInstall) installDeps(root);
627
+ else log.warn('Skipping dependency installation (--no-install).');
628
+
629
+ patchBuildStyles(ws, theme);
630
+ createPostcssrc(root);
631
+ patchIndexHtml(ws);
632
+ createStylesCss(ws);
633
+ patchAppConfig(ws, theme);
634
+ patchRootComponent(ws, theme);
635
+
636
+ console.log('');
637
+ log.ok(`${C.bold}Done.${C.reset} z-ui is wired into "${ws.name}" with the "${theme}" theme.`);
638
+ log.info('Next: start your dev server and verify the theme + fonts load.');
639
+ if (DRY_RUN) log.warn('This was a dry-run — no files were written.');
640
+ }
641
+
642
+ main().catch(e => {
643
+ log.err(e?.stack || String(e));
644
+ process.exit(1);
645
+ });
@@ -1,17 +1,33 @@
1
1
  import * as _angular_core from '@angular/core';
2
2
  import { Type, EnvironmentProviders } from '@angular/core';
3
3
  import { ClassValue } from 'clsx';
4
+ import * as hugeIcons from '@hugeicons/core-free-icons';
4
5
  import * as saxBoldIcons from '@ng-icons/iconsax/bold';
5
6
  import * as saxOutlineIcons from '@ng-icons/iconsax/outline';
6
7
  import * as lucideIcons from '@ng-icons/lucide';
8
+ import * as phosphorBoldIcons from '@ng-icons/phosphor-icons/bold';
9
+ import * as phosphorDuotoneIcons from '@ng-icons/phosphor-icons/duotone';
10
+ import * as phosphorFillIcons from '@ng-icons/phosphor-icons/fill';
11
+ import * as phosphorLightIcons from '@ng-icons/phosphor-icons/light';
12
+ import * as phosphorRegularIcons from '@ng-icons/phosphor-icons/regular';
13
+ import * as phosphorThinIcons from '@ng-icons/phosphor-icons/thin';
7
14
  import * as animatedIcons from 'ng-animated-icons';
8
15
  import * as class_variance_authority_types from 'class-variance-authority/types';
9
16
  import { VariantProps } from 'class-variance-authority';
10
17
 
11
18
  type ZLucideIcon = Extract<keyof typeof lucideIcons, `lucide${string}`>;
12
19
  type ZSaxIcon = Extract<keyof typeof saxBoldIcons | keyof typeof saxOutlineIcons, `sax${string}`>;
20
+ type ZPhosphorIcon = Extract<keyof typeof phosphorRegularIcons | keyof typeof phosphorBoldIcons | keyof typeof phosphorFillIcons | keyof typeof phosphorLightIcons | keyof typeof phosphorThinIcons | keyof typeof phosphorDuotoneIcons, `phosphor${string}`>;
21
+ /**
22
+ * HugeIcons exports are PascalCase with an `Icon` suffix (e.g. `HeartIcon`,
23
+ * `Home01Icon`). We re-expose them with a `huge` prefix to stay consistent
24
+ * with the other families (e.g. `hugeHeart`, `hugeHome01`).
25
+ */
26
+ type ZHugeIcon = {
27
+ [K in keyof typeof hugeIcons as K extends `${infer Name}Icon` ? `huge${Name}` : never]: true;
28
+ } extends infer M ? Extract<keyof M, `huge${string}`> : never;
13
29
  declare const Z_ICONS: {};
14
- type ZIcon = ZLucideIcon | ZSaxIcon;
30
+ type ZIcon = ZLucideIcon | ZSaxIcon | ZPhosphorIcon | ZHugeIcon;
15
31
 
16
32
  type ZIconAnimationTrigger = 'manual' | 'hover' | 'focus' | 'interaction' | 'always';
17
33
  declare class ZIconComponent {