@gilav21/shadcn-angular 0.0.15 → 0.0.16

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.
@@ -7,10 +7,12 @@ import ora from 'ora';
7
7
  import { getConfig } from '../utils/config.js';
8
8
  import { registry } from '../registry/index.js';
9
9
  import { installPackages } from '../utils/package-manager.js';
10
+ import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
12
13
  // Base URL for the component registry (GitHub raw content)
13
14
  const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
15
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
14
16
  // Components source directory (relative to CLI dist folder) for local dev
15
17
  function getLocalComponentsDir() {
16
18
  // From dist/commands/add.js -> packages/components/ui
@@ -25,6 +27,26 @@ function getLocalComponentsDir() {
25
27
  }
26
28
  return null;
27
29
  }
30
+ function getLocalLibDir() {
31
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
32
+ if (fs.existsSync(fromDist)) {
33
+ return fromDist;
34
+ }
35
+ return null;
36
+ }
37
+ function resolveProjectPath(cwd, inputPath) {
38
+ const resolved = path.resolve(cwd, inputPath);
39
+ const relative = path.relative(cwd, resolved);
40
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
41
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
42
+ }
43
+ return resolved;
44
+ }
45
+ function aliasToProjectPath(aliasOrPath) {
46
+ return aliasOrPath.startsWith('@/')
47
+ ? path.join('src', aliasOrPath.slice(2))
48
+ : aliasOrPath;
49
+ }
28
50
  async function fetchComponentContent(file, options) {
29
51
  const localDir = getLocalComponentsDir();
30
52
  // 1. Prefer local if available and not forced remote
@@ -50,6 +72,36 @@ async function fetchComponentContent(file, options) {
50
72
  throw error;
51
73
  }
52
74
  }
75
+ async function fetchLibContent(file, options) {
76
+ const localDir = getLocalLibDir();
77
+ if (localDir && !options.remote) {
78
+ const localPath = path.join(localDir, file);
79
+ if (await fs.pathExists(localPath)) {
80
+ return fs.readFile(localPath, 'utf-8');
81
+ }
82
+ }
83
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
84
+ const response = await fetch(url);
85
+ if (!response.ok) {
86
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
87
+ }
88
+ return response.text();
89
+ }
90
+ function collectInstalledShortcutEntries(targetDir) {
91
+ const entries = [];
92
+ for (const definition of Object.values(registry)) {
93
+ if (!definition.shortcutDefinitions?.length) {
94
+ continue;
95
+ }
96
+ for (const shortcutDefinition of definition.shortcutDefinitions) {
97
+ const sourcePath = path.join(targetDir, shortcutDefinition.sourceFile);
98
+ if (fs.existsSync(sourcePath)) {
99
+ entries.push(shortcutDefinition);
100
+ }
101
+ }
102
+ }
103
+ return entries;
104
+ }
53
105
  export async function add(components, options) {
54
106
  const cwd = process.cwd();
55
107
  // Load config
@@ -103,9 +155,8 @@ export async function add(components, options) {
103
155
  }
104
156
  };
105
157
  componentsToAdd.forEach(c => resolveDeps(c));
106
- const targetDir = options.path
107
- ? path.join(cwd, options.path)
108
- : path.join(cwd, 'src/components/ui');
158
+ const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
159
+ const targetDir = resolveProjectPath(cwd, uiBasePath);
109
160
  // Check for existing files and diff
110
161
  const componentsToInstall = [];
111
162
  const componentsToSkip = [];
@@ -125,7 +176,7 @@ export async function add(components, options) {
125
176
  // Transform imports for comparison
126
177
  const utilsAlias = config.aliases.utils;
127
178
  remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
128
- const normalize = (str) => str.replace(/\s+/g, '').trim();
179
+ const normalize = (str) => str.replace(/\r\n/g, '\n').trim();
129
180
  if (normalize(localContent) !== normalize(remoteContent)) {
130
181
  hasChanges = true;
131
182
  }
@@ -252,6 +303,18 @@ export async function add(components, options) {
252
303
  }
253
304
  }
254
305
  }
306
+ const shortcutEntries = collectInstalledShortcutEntries(targetDir);
307
+ if (shortcutEntries.length > 0) {
308
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
309
+ const utilsDir = path.dirname(utilsPathResolved);
310
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
311
+ if (!await fs.pathExists(shortcutServicePath)) {
312
+ const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
313
+ await fs.ensureDir(utilsDir);
314
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
315
+ }
316
+ }
317
+ await writeShortcutRegistryIndex(cwd, config, shortcutEntries);
255
318
  if (componentsToSkip.length > 0) {
256
319
  console.log('\n' + chalk.dim('Components skipped (up to date):'));
257
320
  componentsToSkip.forEach(name => {
@@ -1,12 +1,53 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import prompts from 'prompts';
4
5
  import chalk from 'chalk';
5
6
  import ora from 'ora';
6
- import { execa } from 'execa';
7
7
  import { getDefaultConfig } from '../utils/config.js';
8
8
  import { getStylesTemplate } from '../templates/styles.js';
9
9
  import { getUtilsTemplate } from '../templates/utils.js';
10
+ import { installPackages } from '../utils/package-manager.js';
11
+ import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
12
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ function getLocalLibDir() {
16
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
17
+ if (fs.existsSync(fromDist)) {
18
+ return fromDist;
19
+ }
20
+ return null;
21
+ }
22
+ async function fetchLibFileContent(file) {
23
+ const localLibDir = getLocalLibDir();
24
+ if (localLibDir) {
25
+ const localPath = path.join(localLibDir, file);
26
+ if (await fs.pathExists(localPath)) {
27
+ return fs.readFile(localPath, 'utf-8');
28
+ }
29
+ }
30
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
31
+ const response = await fetch(url);
32
+ if (!response.ok) {
33
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
34
+ }
35
+ return response.text();
36
+ }
37
+ function resolveProjectPath(cwd, inputPath) {
38
+ const resolved = path.resolve(cwd, inputPath);
39
+ const relative = path.relative(cwd, resolved);
40
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
41
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
42
+ }
43
+ return resolved;
44
+ }
45
+ function resolveAliasOrPath(cwd, aliasOrPath) {
46
+ const normalized = aliasOrPath.startsWith('@/')
47
+ ? path.join('src', aliasOrPath.slice(2))
48
+ : aliasOrPath;
49
+ return resolveProjectPath(cwd, normalized);
50
+ }
10
51
  export async function init(options) {
11
52
  console.log(chalk.bold('\n🎨 Welcome to shadcn-angular!\n'));
12
53
  const cwd = process.cwd();
@@ -20,20 +61,24 @@ export async function init(options) {
20
61
  // Check if already initialized
21
62
  const componentsJsonPath = path.join(cwd, 'components.json');
22
63
  if (await fs.pathExists(componentsJsonPath)) {
23
- const { overwrite } = await prompts({
24
- type: 'confirm',
25
- name: 'overwrite',
26
- message: 'components.json already exists. Overwrite?',
27
- initial: false,
28
- });
64
+ const overwrite = options.yes
65
+ ? true
66
+ : (await prompts({
67
+ type: 'confirm',
68
+ name: 'overwrite',
69
+ message: 'components.json already exists. Overwrite?',
70
+ initial: false,
71
+ })).overwrite;
29
72
  if (!overwrite) {
30
73
  console.log(chalk.dim('Initialization cancelled.'));
31
74
  return;
32
75
  }
33
76
  }
34
77
  let config;
78
+ let createShortcutRegistry = true;
35
79
  if (options.defaults || options.yes) {
36
80
  config = getDefaultConfig();
81
+ createShortcutRegistry = true;
37
82
  }
38
83
  else {
39
84
  const THEME_COLORS = {
@@ -115,6 +160,12 @@ export async function init(options) {
115
160
  message: 'Where is your global styles file?',
116
161
  initial: 'src/styles.scss',
117
162
  },
163
+ {
164
+ type: 'confirm',
165
+ name: 'createShortcutRegistry',
166
+ message: 'Would you like to create a shortcut registry scaffold?',
167
+ initial: true,
168
+ },
118
169
  ]);
119
170
  config = {
120
171
  $schema: 'https://shadcn-angular.dev/schema.json',
@@ -131,6 +182,7 @@ export async function init(options) {
131
182
  ui: responses.componentsPath.replace('src/', '@/'),
132
183
  },
133
184
  };
185
+ createShortcutRegistry = responses.createShortcutRegistry ?? true;
134
186
  }
135
187
  const spinner = ora('Initializing project...').start();
136
188
  try {
@@ -143,19 +195,28 @@ export async function init(options) {
143
195
  // So we should rely on config to reconstruct the path, or better yet, if we are in 'defaults' mode, check what config is.
144
196
  // If config came from defaults, aliases are set.
145
197
  // We can reverse-map alias to path: @/ -> src/
146
- const utilsPathResolved = config.aliases.utils.replace('@/', 'src/');
147
- const utilsDir = path.dirname(path.join(cwd, utilsPathResolved + '.ts')); // utils usually ends in path/to/utils
198
+ const utilsPathResolved = resolveAliasOrPath(cwd, config.aliases.utils + '.ts');
199
+ const utilsDir = path.dirname(utilsPathResolved); // utils usually ends in path/to/utils
148
200
  await fs.ensureDir(utilsDir);
149
- await fs.writeFile(path.join(cwd, utilsPathResolved + '.ts'), getUtilsTemplate());
201
+ await fs.writeFile(utilsPathResolved, getUtilsTemplate());
150
202
  spinner.text = 'Created utils.ts';
203
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
204
+ const shortcutServiceContent = await fetchLibFileContent('shortcut-binding.service.ts');
205
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
206
+ spinner.text = 'Created shortcut-binding.service.ts';
207
+ if (createShortcutRegistry) {
208
+ await writeShortcutRegistryIndex(cwd, config, []);
209
+ spinner.text = 'Created shortcut-registry.index.ts';
210
+ }
151
211
  // Create tailwind.css file in the same directory as the global styles
152
- const stylesDir = path.dirname(path.join(cwd, config.tailwind.css));
212
+ const userStylesPath = resolveProjectPath(cwd, config.tailwind.css);
213
+ const stylesDir = path.dirname(userStylesPath);
153
214
  const tailwindCssPath = path.join(stylesDir, 'tailwind.css');
154
215
  // Write the tailwind.css file with all Tailwind directives
216
+ await fs.ensureDir(stylesDir);
155
217
  await fs.writeFile(tailwindCssPath, getStylesTemplate(config.tailwind.baseColor, config.tailwind.theme));
156
218
  spinner.text = 'Created tailwind.css';
157
219
  // Add import to the user's global styles file if not already present
158
- const userStylesPath = path.join(cwd, config.tailwind.css);
159
220
  let userStyles = await fs.pathExists(userStylesPath)
160
221
  ? await fs.readFile(userStylesPath, 'utf-8')
161
222
  : '';
@@ -167,8 +228,7 @@ export async function init(options) {
167
228
  spinner.text = 'Added tailwind.css import to styles';
168
229
  }
169
230
  // Create components/ui directory
170
- const uiPathResolved = config.aliases.ui.replace('@/', 'src/');
171
- const uiDir = path.join(cwd, uiPathResolved);
231
+ const uiDir = resolveAliasOrPath(cwd, config.aliases.ui);
172
232
  await fs.ensureDir(uiDir);
173
233
  spinner.text = 'Created components directory';
174
234
  // Install dependencies
@@ -181,7 +241,7 @@ export async function init(options) {
181
241
  'postcss',
182
242
  '@tailwindcss/postcss'
183
243
  ];
184
- await execa('npm', ['install', ...dependencies], { cwd });
244
+ await installPackages(dependencies, { cwd });
185
245
  // Setup PostCSS - create .postcssrc.json which is the preferred format for Angular
186
246
  spinner.text = 'Configuring PostCSS...';
187
247
  const postcssrcPath = path.join(cwd, '.postcssrc.json');
@@ -3,6 +3,11 @@ export interface ComponentDefinition {
3
3
  files: string[];
4
4
  dependencies?: string[];
5
5
  npmDependencies?: string[];
6
+ shortcutDefinitions?: {
7
+ exportName: string;
8
+ componentName: string;
9
+ sourceFile: string;
10
+ }[];
6
11
  }
7
12
  export type ComponentName = keyof typeof registry;
8
13
  export declare const registry: Record<string, ComponentDefinition>;
@@ -39,6 +39,7 @@ export const registry = {
39
39
  button: {
40
40
  name: 'button',
41
41
  files: ['button.component.ts'],
42
+ dependencies: ['ripple'],
42
43
  },
43
44
  'button-group': {
44
45
  name: 'button-group',
@@ -79,6 +80,13 @@ export const registry = {
79
80
  name: 'command',
80
81
  files: ['command.component.ts'],
81
82
  dependencies: ['dialog'],
83
+ shortcutDefinitions: [
84
+ {
85
+ exportName: 'COMMAND_DIALOG_SHORTCUT_DEFINITIONS',
86
+ componentName: 'command-dialog',
87
+ sourceFile: 'command.component.ts',
88
+ },
89
+ ],
82
90
  },
83
91
  'context-menu': {
84
92
  name: 'context-menu',
@@ -342,7 +350,7 @@ export const registry = {
342
350
  'emoji-picker': {
343
351
  name: 'emoji-picker',
344
352
  files: ['emoji-picker.component.ts', 'emoji-data.ts'],
345
- dependencies: ['button', 'input', 'scroll-area', 'popover'],
353
+ dependencies: ['input', 'scroll-area', 'tooltip'],
346
354
  },
347
355
  'rich-text-editor': {
348
356
  name: 'rich-text-editor',
@@ -351,18 +359,30 @@ export const registry = {
351
359
  'rich-text-toolbar.component.ts',
352
360
  'rich-text-sanitizer.service.ts',
353
361
  'rich-text-markdown.service.ts',
362
+ 'rich-text-paste-normalizer.service.ts',
363
+ 'rich-text-command-registry.service.ts',
354
364
  'rich-text-mention.component.ts',
355
365
  'rich-text-image-resizer.component.ts',
366
+ 'rich-text-locales.ts',
356
367
  ],
357
368
  dependencies: [
358
369
  'button',
359
370
  'separator',
360
371
  'popover',
361
372
  'emoji-picker',
373
+ 'autocomplete',
362
374
  'select',
363
375
  'input',
376
+ 'dialog',
364
377
  'scroll-area',
365
378
  ],
379
+ shortcutDefinitions: [
380
+ {
381
+ exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
382
+ componentName: 'rich-text-editor',
383
+ sourceFile: 'rich-text-editor.component.ts',
384
+ },
385
+ ],
366
386
  },
367
387
  // Chart Components
368
388
  'pie-chart': {
@@ -464,4 +484,75 @@ export const registry = {
464
484
  files: ['split-button.component.ts'],
465
485
  dependencies: ['button', 'dropdown-menu'],
466
486
  },
487
+ // Animations
488
+ 'gradient-text': {
489
+ name: 'gradient-text',
490
+ files: ['gradient-text.component.ts'],
491
+ },
492
+ 'flip-text': {
493
+ name: 'flip-text',
494
+ files: ['flip-text.component.ts'],
495
+ },
496
+ meteors: {
497
+ name: 'meteors',
498
+ files: ['meteors.component.ts'],
499
+ },
500
+ 'shine-border': {
501
+ name: 'shine-border',
502
+ files: ['shine-border.component.ts'],
503
+ },
504
+ 'scroll-progress': {
505
+ name: 'scroll-progress',
506
+ files: ['scroll-progress.component.ts'],
507
+ },
508
+ 'blur-fade': {
509
+ name: 'blur-fade',
510
+ files: ['blur-fade.component.ts'],
511
+ },
512
+ ripple: {
513
+ name: 'ripple',
514
+ files: ['ripple.directive.ts'],
515
+ },
516
+ marquee: {
517
+ name: 'marquee',
518
+ files: ['marquee.component.ts'],
519
+ },
520
+ 'word-rotate': {
521
+ name: 'word-rotate',
522
+ files: ['word-rotate.component.ts'],
523
+ },
524
+ 'morphing-text': {
525
+ name: 'morphing-text',
526
+ files: ['morphing-text.component.ts'],
527
+ },
528
+ 'typing-animation': {
529
+ name: 'typing-animation',
530
+ files: ['typing-animation.component.ts'],
531
+ },
532
+ 'wobble-card': {
533
+ name: 'wobble-card',
534
+ files: ['wobble-card.component.ts'],
535
+ },
536
+ magnetic: {
537
+ name: 'magnetic',
538
+ files: ['magnetic.directive.ts'],
539
+ },
540
+ orbit: {
541
+ name: 'orbit',
542
+ files: ['orbit.component.ts'],
543
+ },
544
+ 'stagger-children': {
545
+ name: 'stagger-children',
546
+ files: ['stagger-children.component.ts'],
547
+ },
548
+ particles: {
549
+ name: 'particles',
550
+ files: ['particles.component.ts'],
551
+ },
552
+ // Kanban
553
+ kanban: {
554
+ name: 'kanban',
555
+ files: ['kanban.component.ts'],
556
+ dependencies: ['badge', 'avatar', 'scroll-area', 'separator'],
557
+ },
467
558
  };
@@ -0,0 +1,7 @@
1
+ import type { Config } from './config.js';
2
+ export interface ShortcutRegistryEntry {
3
+ exportName: string;
4
+ componentName: string;
5
+ sourceFile: string;
6
+ }
7
+ export declare function writeShortcutRegistryIndex(cwd: string, config: Config, entries: ShortcutRegistryEntry[]): Promise<string>;
@@ -0,0 +1,58 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ function aliasToProjectPath(aliasOrPath) {
4
+ return aliasOrPath.startsWith('@/')
5
+ ? path.join('src', aliasOrPath.slice(2))
6
+ : aliasOrPath;
7
+ }
8
+ function resolveProjectPath(cwd, inputPath) {
9
+ const resolved = path.resolve(cwd, inputPath);
10
+ const relative = path.relative(cwd, resolved);
11
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
12
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
13
+ }
14
+ return resolved;
15
+ }
16
+ function getShortcutRegistryIndexPath(cwd, config) {
17
+ const utilsFilePath = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
18
+ const utilsDir = path.dirname(utilsFilePath);
19
+ return path.join(utilsDir, 'shortcut-registry.index.ts');
20
+ }
21
+ export async function writeShortcutRegistryIndex(cwd, config, entries) {
22
+ const registryPath = getShortcutRegistryIndexPath(cwd, config);
23
+ await fs.ensureDir(path.dirname(registryPath));
24
+ const uniqueEntries = Array.from(new Map(entries.map(entry => [entry.exportName, entry])).values())
25
+ .sort((a, b) => a.exportName.localeCompare(b.exportName));
26
+ const uiAlias = config.aliases.ui;
27
+ const utilsAliasDir = config.aliases.utils.includes('/')
28
+ ? config.aliases.utils.slice(0, config.aliases.utils.lastIndexOf('/'))
29
+ : config.aliases.utils;
30
+ const shortcutServiceImport = `${utilsAliasDir}/shortcut-binding.service`;
31
+ const imports = uniqueEntries
32
+ .map(entry => {
33
+ const importPath = `${uiAlias}/${entry.sourceFile.replace(/\.ts$/, '')}`;
34
+ return `import { ${entry.exportName} } from '${importPath}';`;
35
+ })
36
+ .join('\n');
37
+ const catalogItems = uniqueEntries
38
+ .map(entry => ` { componentName: '${entry.componentName}', definitions: ${entry.exportName} },`)
39
+ .join('\n');
40
+ const content = `// Auto-generated by shadcn-angular CLI. Do not edit manually.
41
+ import type { ShortcutBindingService, ShortcutDefinition } from '${shortcutServiceImport}';
42
+ ${imports ? `${imports}\n` : ''}
43
+ export const GENERATED_SHORTCUT_CATALOG: ReadonlyArray<{
44
+ componentName: string;
45
+ definitions: ReadonlyArray<ShortcutDefinition>;
46
+ }> = [
47
+ ${catalogItems}
48
+ ];
49
+
50
+ export function registerGeneratedShortcutCatalog(shortcuts: ShortcutBindingService): void {
51
+ for (const entry of GENERATED_SHORTCUT_CATALOG) {
52
+ shortcuts.defineShortcuts(entry.componentName, [...entry.definitions]);
53
+ }
54
+ }
55
+ `;
56
+ await fs.writeFile(registryPath, content);
57
+ return registryPath;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"
@@ -4,18 +4,20 @@ import { fileURLToPath } from 'url';
4
4
  import prompts from 'prompts';
5
5
  import chalk from 'chalk';
6
6
  import ora from 'ora';
7
- import { getConfig } from '../utils/config.js';
8
- import { registry, type ComponentName } from '../registry/index.js';
9
- import { installPackages } from '../utils/package-manager.js';
7
+ import { getConfig } from '../utils/config.js';
8
+ import { registry, type ComponentName } from '../registry/index.js';
9
+ import { installPackages } from '../utils/package-manager.js';
10
+ import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
13
14
 
14
15
  // Base URL for the component registry (GitHub raw content)
15
- const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
16
+ const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
17
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
16
18
 
17
19
  // Components source directory (relative to CLI dist folder) for local dev
18
- function getLocalComponentsDir(): string | null {
20
+ function getLocalComponentsDir(): string | null {
19
21
  // From dist/commands/add.js -> packages/components/ui
20
22
  const fromDist = path.resolve(__dirname, '../../../components/ui');
21
23
  if (fs.existsSync(fromDist)) {
@@ -29,15 +31,38 @@ function getLocalComponentsDir(): string | null {
29
31
  return null;
30
32
  }
31
33
 
32
- interface AddOptions {
34
+ interface AddOptions {
33
35
  yes?: boolean;
34
36
  overwrite?: boolean;
35
37
  all?: boolean;
36
38
  path?: string;
37
- remote?: boolean; // Force remote fetch
38
- }
39
-
40
- async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
39
+ remote?: boolean; // Force remote fetch
40
+ }
41
+
42
+ function getLocalLibDir(): string | null {
43
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
44
+ if (fs.existsSync(fromDist)) {
45
+ return fromDist;
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function resolveProjectPath(cwd: string, inputPath: string): string {
51
+ const resolved = path.resolve(cwd, inputPath);
52
+ const relative = path.relative(cwd, resolved);
53
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
54
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
55
+ }
56
+ return resolved;
57
+ }
58
+
59
+ function aliasToProjectPath(aliasOrPath: string): string {
60
+ return aliasOrPath.startsWith('@/')
61
+ ? path.join('src', aliasOrPath.slice(2))
62
+ : aliasOrPath;
63
+ }
64
+
65
+ async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
41
66
  const localDir = getLocalComponentsDir();
42
67
 
43
68
  // 1. Prefer local if available and not forced remote
@@ -62,7 +87,41 @@ async function fetchComponentContent(file: string, options: AddOptions): Promise
62
87
  }
63
88
  throw error;
64
89
  }
65
- }
90
+ }
91
+
92
+ async function fetchLibContent(file: string, options: AddOptions): Promise<string> {
93
+ const localDir = getLocalLibDir();
94
+
95
+ if (localDir && !options.remote) {
96
+ const localPath = path.join(localDir, file);
97
+ if (await fs.pathExists(localPath)) {
98
+ return fs.readFile(localPath, 'utf-8');
99
+ }
100
+ }
101
+
102
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
103
+ const response = await fetch(url);
104
+ if (!response.ok) {
105
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
106
+ }
107
+ return response.text();
108
+ }
109
+
110
+ function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEntry[] {
111
+ const entries: ShortcutRegistryEntry[] = [];
112
+ for (const definition of Object.values(registry)) {
113
+ if (!definition.shortcutDefinitions?.length) {
114
+ continue;
115
+ }
116
+ for (const shortcutDefinition of definition.shortcutDefinitions) {
117
+ const sourcePath = path.join(targetDir, shortcutDefinition.sourceFile);
118
+ if (fs.existsSync(sourcePath)) {
119
+ entries.push(shortcutDefinition);
120
+ }
121
+ }
122
+ }
123
+ return entries;
124
+ }
66
125
 
67
126
  export async function add(components: string[], options: AddOptions) {
68
127
  const cwd = process.cwd();
@@ -121,9 +180,8 @@ export async function add(components: string[], options: AddOptions) {
121
180
  };
122
181
  componentsToAdd.forEach(c => resolveDeps(c));
123
182
 
124
- const targetDir = options.path
125
- ? path.join(cwd, options.path)
126
- : path.join(cwd, 'src/components/ui');
183
+ const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
184
+ const targetDir = resolveProjectPath(cwd, uiBasePath);
127
185
 
128
186
  // Check for existing files and diff
129
187
  const componentsToInstall: ComponentName[] = [];
@@ -149,7 +207,7 @@ export async function add(components: string[], options: AddOptions) {
149
207
  const utilsAlias = config.aliases.utils;
150
208
  remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
151
209
 
152
- const normalize = (str: string) => str.replace(/\s+/g, '').trim();
210
+ const normalize = (str: string) => str.replace(/\r\n/g, '\n').trim();
153
211
  if (normalize(localContent) !== normalize(remoteContent)) {
154
212
  hasChanges = true;
155
213
  }
@@ -265,8 +323,8 @@ export async function add(components: string[], options: AddOptions) {
265
323
  spinner.info('No new components installed.');
266
324
  }
267
325
 
268
- if (finalComponents.length > 0) {
269
- const npmDependencies = new Set<string>();
326
+ if (finalComponents.length > 0) {
327
+ const npmDependencies = new Set<string>();
270
328
  for (const name of finalComponents) {
271
329
  const component = registry[name];
272
330
  if (component.npmDependencies) {
@@ -283,11 +341,26 @@ export async function add(components: string[], options: AddOptions) {
283
341
  depSpinner.fail('Failed to install dependencies.');
284
342
  console.error(e);
285
343
  }
286
- }
287
- }
288
-
289
- if (componentsToSkip.length > 0) {
290
- console.log('\n' + chalk.dim('Components skipped (up to date):'));
344
+ }
345
+ }
346
+
347
+ const shortcutEntries = collectInstalledShortcutEntries(targetDir);
348
+ if (shortcutEntries.length > 0) {
349
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
350
+ const utilsDir = path.dirname(utilsPathResolved);
351
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
352
+
353
+ if (!await fs.pathExists(shortcutServicePath)) {
354
+ const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
355
+ await fs.ensureDir(utilsDir);
356
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
357
+ }
358
+ }
359
+
360
+ await writeShortcutRegistryIndex(cwd, config, shortcutEntries);
361
+
362
+ if (componentsToSkip.length > 0) {
363
+ console.log('\n' + chalk.dim('Components skipped (up to date):'));
291
364
  componentsToSkip.forEach(name => {
292
365
  console.log(chalk.dim(' - ') + chalk.gray(name));
293
366
  });
@@ -1,19 +1,66 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
- import prompts from 'prompts';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import { execa } from 'execa';
7
- import { getDefaultConfig, type Config } from '../utils/config.js';
8
- import { getStylesTemplate } from '../templates/styles.js';
9
- import { getUtilsTemplate } from '../templates/utils.js';
10
-
11
- interface InitOptions {
12
- yes?: boolean;
13
- defaults?: boolean;
14
- }
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import prompts from 'prompts';
5
+ import chalk from 'chalk';
6
+ import ora from 'ora';
7
+ import { getDefaultConfig, type Config } from '../utils/config.js';
8
+ import { getStylesTemplate } from '../templates/styles.js';
9
+ import { getUtilsTemplate } from '../templates/utils.js';
10
+ import { installPackages } from '../utils/package-manager.js';
11
+ import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
12
+
13
+ const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ function getLocalLibDir(): string | null {
18
+ const fromDist = path.resolve(__dirname, '../../../components/lib');
19
+ if (fs.existsSync(fromDist)) {
20
+ return fromDist;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ async function fetchLibFileContent(file: string): Promise<string> {
26
+ const localLibDir = getLocalLibDir();
27
+ if (localLibDir) {
28
+ const localPath = path.join(localLibDir, file);
29
+ if (await fs.pathExists(localPath)) {
30
+ return fs.readFile(localPath, 'utf-8');
31
+ }
32
+ }
33
+
34
+ const url = `${LIB_REGISTRY_BASE_URL}/${file}`;
35
+ const response = await fetch(url);
36
+ if (!response.ok) {
37
+ throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
38
+ }
39
+ return response.text();
40
+ }
15
41
 
16
- export async function init(options: InitOptions) {
42
+ interface InitOptions {
43
+ yes?: boolean;
44
+ defaults?: boolean;
45
+ }
46
+
47
+ function resolveProjectPath(cwd: string, inputPath: string): string {
48
+ const resolved = path.resolve(cwd, inputPath);
49
+ const relative = path.relative(cwd, resolved);
50
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
51
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
52
+ }
53
+ return resolved;
54
+ }
55
+
56
+ function resolveAliasOrPath(cwd: string, aliasOrPath: string): string {
57
+ const normalized = aliasOrPath.startsWith('@/')
58
+ ? path.join('src', aliasOrPath.slice(2))
59
+ : aliasOrPath;
60
+ return resolveProjectPath(cwd, normalized);
61
+ }
62
+
63
+ export async function init(options: InitOptions) {
17
64
  console.log(chalk.bold('\n🎨 Welcome to shadcn-angular!\n'));
18
65
 
19
66
  const cwd = process.cwd();
@@ -26,26 +73,30 @@ export async function init(options: InitOptions) {
26
73
  process.exit(1);
27
74
  }
28
75
 
29
- // Check if already initialized
30
- const componentsJsonPath = path.join(cwd, 'components.json');
31
- if (await fs.pathExists(componentsJsonPath)) {
32
- const { overwrite } = await prompts({
33
- type: 'confirm',
34
- name: 'overwrite',
35
- message: 'components.json already exists. Overwrite?',
36
- initial: false,
37
- });
38
- if (!overwrite) {
39
- console.log(chalk.dim('Initialization cancelled.'));
40
- return;
76
+ // Check if already initialized
77
+ const componentsJsonPath = path.join(cwd, 'components.json');
78
+ if (await fs.pathExists(componentsJsonPath)) {
79
+ const overwrite = options.yes
80
+ ? true
81
+ : (await prompts({
82
+ type: 'confirm',
83
+ name: 'overwrite',
84
+ message: 'components.json already exists. Overwrite?',
85
+ initial: false,
86
+ })).overwrite;
87
+ if (!overwrite) {
88
+ console.log(chalk.dim('Initialization cancelled.'));
89
+ return;
41
90
  }
42
91
  }
43
92
 
44
- let config: Config;
93
+ let config: Config;
94
+ let createShortcutRegistry = true;
45
95
 
46
- if (options.defaults || options.yes) {
47
- config = getDefaultConfig();
48
- } else {
96
+ if (options.defaults || options.yes) {
97
+ config = getDefaultConfig();
98
+ createShortcutRegistry = true;
99
+ } else {
49
100
  const THEME_COLORS: Record<string, string> = {
50
101
  zinc: '#71717a',
51
102
  slate: '#64748b',
@@ -123,16 +174,22 @@ export async function init(options: InitOptions) {
123
174
  message: 'Where would you like to install utils?',
124
175
  initial: 'src/components/lib',
125
176
  },
126
- {
127
- type: 'text',
128
- name: 'globalCss',
129
- message: 'Where is your global styles file?',
130
- initial: 'src/styles.scss',
131
- },
132
- ]);
177
+ {
178
+ type: 'text',
179
+ name: 'globalCss',
180
+ message: 'Where is your global styles file?',
181
+ initial: 'src/styles.scss',
182
+ },
183
+ {
184
+ type: 'confirm',
185
+ name: 'createShortcutRegistry',
186
+ message: 'Would you like to create a shortcut registry scaffold?',
187
+ initial: true,
188
+ },
189
+ ]);
133
190
 
134
- config = {
135
- $schema: 'https://shadcn-angular.dev/schema.json',
191
+ config = {
192
+ $schema: 'https://shadcn-angular.dev/schema.json',
136
193
  style: 'default',
137
194
  tailwind: {
138
195
  css: responses.globalCss,
@@ -144,9 +201,10 @@ export async function init(options: InitOptions) {
144
201
  components: responses.componentsPath.replace('src/', '@/'), // Basic heuristic
145
202
  utils: responses.utilsPath.replace('src/', '@/').replace('.ts', ''),
146
203
  ui: responses.componentsPath.replace('src/', '@/'),
147
- },
148
- };
149
- }
204
+ },
205
+ };
206
+ createShortcutRegistry = responses.createShortcutRegistry ?? true;
207
+ }
150
208
 
151
209
  const spinner = ora('Initializing project...').start();
152
210
 
@@ -162,26 +220,37 @@ export async function init(options: InitOptions) {
162
220
  // If config came from defaults, aliases are set.
163
221
  // We can reverse-map alias to path: @/ -> src/
164
222
 
165
- const utilsPathResolved = config.aliases.utils.replace('@/', 'src/');
166
- const utilsDir = path.dirname(path.join(cwd, utilsPathResolved + '.ts')); // utils usually ends in path/to/utils
167
-
168
- await fs.ensureDir(utilsDir);
169
- await fs.writeFile(path.join(cwd, utilsPathResolved + '.ts'), getUtilsTemplate());
170
- spinner.text = 'Created utils.ts';
171
-
172
- // Create tailwind.css file in the same directory as the global styles
173
- const stylesDir = path.dirname(path.join(cwd, config.tailwind.css));
174
- const tailwindCssPath = path.join(stylesDir, 'tailwind.css');
175
-
176
- // Write the tailwind.css file with all Tailwind directives
177
- await fs.writeFile(tailwindCssPath, getStylesTemplate(config.tailwind.baseColor, config.tailwind.theme));
178
- spinner.text = 'Created tailwind.css';
179
-
180
- // Add import to the user's global styles file if not already present
181
- const userStylesPath = path.join(cwd, config.tailwind.css);
182
- let userStyles = await fs.pathExists(userStylesPath)
183
- ? await fs.readFile(userStylesPath, 'utf-8')
184
- : '';
223
+ const utilsPathResolved = resolveAliasOrPath(cwd, config.aliases.utils + '.ts');
224
+ const utilsDir = path.dirname(utilsPathResolved); // utils usually ends in path/to/utils
225
+
226
+ await fs.ensureDir(utilsDir);
227
+ await fs.writeFile(utilsPathResolved, getUtilsTemplate());
228
+ spinner.text = 'Created utils.ts';
229
+
230
+ const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
231
+ const shortcutServiceContent = await fetchLibFileContent('shortcut-binding.service.ts');
232
+ await fs.writeFile(shortcutServicePath, shortcutServiceContent);
233
+ spinner.text = 'Created shortcut-binding.service.ts';
234
+
235
+ if (createShortcutRegistry) {
236
+ await writeShortcutRegistryIndex(cwd, config, []);
237
+ spinner.text = 'Created shortcut-registry.index.ts';
238
+ }
239
+
240
+ // Create tailwind.css file in the same directory as the global styles
241
+ const userStylesPath = resolveProjectPath(cwd, config.tailwind.css);
242
+ const stylesDir = path.dirname(userStylesPath);
243
+ const tailwindCssPath = path.join(stylesDir, 'tailwind.css');
244
+
245
+ // Write the tailwind.css file with all Tailwind directives
246
+ await fs.ensureDir(stylesDir);
247
+ await fs.writeFile(tailwindCssPath, getStylesTemplate(config.tailwind.baseColor, config.tailwind.theme));
248
+ spinner.text = 'Created tailwind.css';
249
+
250
+ // Add import to the user's global styles file if not already present
251
+ let userStyles = await fs.pathExists(userStylesPath)
252
+ ? await fs.readFile(userStylesPath, 'utf-8')
253
+ : '';
185
254
 
186
255
  const tailwindImport = '@import "./tailwind.css";';
187
256
  if (!userStyles.includes('tailwind.css')) {
@@ -189,25 +258,24 @@ export async function init(options: InitOptions) {
189
258
  userStyles = tailwindImport + '\n\n' + userStyles;
190
259
  await fs.writeFile(userStylesPath, userStyles);
191
260
  spinner.text = 'Added tailwind.css import to styles';
192
- }
193
-
194
- // Create components/ui directory
195
- const uiPathResolved = config.aliases.ui.replace('@/', 'src/');
196
- const uiDir = path.join(cwd, uiPathResolved);
197
- await fs.ensureDir(uiDir);
198
- spinner.text = 'Created components directory';
261
+ }
262
+
263
+ // Create components/ui directory
264
+ const uiDir = resolveAliasOrPath(cwd, config.aliases.ui);
265
+ await fs.ensureDir(uiDir);
266
+ spinner.text = 'Created components directory';
199
267
 
200
268
  // Install dependencies
201
269
  spinner.text = 'Installing dependencies...';
202
- const dependencies = [
203
- 'clsx',
204
- 'tailwind-merge',
270
+ const dependencies = [
271
+ 'clsx',
272
+ 'tailwind-merge',
205
273
  'class-variance-authority',
206
274
  'tailwindcss',
207
- 'postcss',
208
- '@tailwindcss/postcss'
209
- ];
210
- await execa('npm', ['install', ...dependencies], { cwd });
275
+ 'postcss',
276
+ '@tailwindcss/postcss'
277
+ ];
278
+ await installPackages(dependencies, { cwd });
211
279
 
212
280
  // Setup PostCSS - create .postcssrc.json which is the preferred format for Angular
213
281
  spinner.text = 'Configuring PostCSS...';
@@ -6,6 +6,11 @@ export interface ComponentDefinition {
6
6
  files: string[]; // Relative paths to component files
7
7
  dependencies?: string[]; // Other components this depends on
8
8
  npmDependencies?: string[]; // NPM packages this depends on
9
+ shortcutDefinitions?: {
10
+ exportName: string;
11
+ componentName: string;
12
+ sourceFile: string;
13
+ }[];
9
14
  }
10
15
 
11
16
  export type ComponentName = keyof typeof registry;
@@ -49,6 +54,7 @@ export const registry: Record<string, ComponentDefinition> = {
49
54
  button: {
50
55
  name: 'button',
51
56
  files: ['button.component.ts'],
57
+ dependencies: ['ripple'],
52
58
  },
53
59
  'button-group': {
54
60
  name: 'button-group',
@@ -89,6 +95,13 @@ export const registry: Record<string, ComponentDefinition> = {
89
95
  name: 'command',
90
96
  files: ['command.component.ts'],
91
97
  dependencies: ['dialog'],
98
+ shortcutDefinitions: [
99
+ {
100
+ exportName: 'COMMAND_DIALOG_SHORTCUT_DEFINITIONS',
101
+ componentName: 'command-dialog',
102
+ sourceFile: 'command.component.ts',
103
+ },
104
+ ],
92
105
  },
93
106
  'context-menu': {
94
107
  name: 'context-menu',
@@ -353,7 +366,7 @@ export const registry: Record<string, ComponentDefinition> = {
353
366
  'emoji-picker': {
354
367
  name: 'emoji-picker',
355
368
  files: ['emoji-picker.component.ts', 'emoji-data.ts'],
356
- dependencies: ['button', 'input', 'scroll-area', 'popover'],
369
+ dependencies: ['input', 'scroll-area', 'tooltip'],
357
370
  },
358
371
  'rich-text-editor': {
359
372
  name: 'rich-text-editor',
@@ -362,18 +375,30 @@ export const registry: Record<string, ComponentDefinition> = {
362
375
  'rich-text-toolbar.component.ts',
363
376
  'rich-text-sanitizer.service.ts',
364
377
  'rich-text-markdown.service.ts',
378
+ 'rich-text-paste-normalizer.service.ts',
379
+ 'rich-text-command-registry.service.ts',
365
380
  'rich-text-mention.component.ts',
366
381
  'rich-text-image-resizer.component.ts',
382
+ 'rich-text-locales.ts',
367
383
  ],
368
384
  dependencies: [
369
385
  'button',
370
386
  'separator',
371
387
  'popover',
372
388
  'emoji-picker',
389
+ 'autocomplete',
373
390
  'select',
374
391
  'input',
392
+ 'dialog',
375
393
  'scroll-area',
376
394
  ],
395
+ shortcutDefinitions: [
396
+ {
397
+ exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
398
+ componentName: 'rich-text-editor',
399
+ sourceFile: 'rich-text-editor.component.ts',
400
+ },
401
+ ],
377
402
  },
378
403
  // Chart Components
379
404
  'pie-chart': {
@@ -475,4 +500,75 @@ export const registry: Record<string, ComponentDefinition> = {
475
500
  files: ['split-button.component.ts'],
476
501
  dependencies: ['button', 'dropdown-menu'],
477
502
  },
503
+ // Animations
504
+ 'gradient-text': {
505
+ name: 'gradient-text',
506
+ files: ['gradient-text.component.ts'],
507
+ },
508
+ 'flip-text': {
509
+ name: 'flip-text',
510
+ files: ['flip-text.component.ts'],
511
+ },
512
+ meteors: {
513
+ name: 'meteors',
514
+ files: ['meteors.component.ts'],
515
+ },
516
+ 'shine-border': {
517
+ name: 'shine-border',
518
+ files: ['shine-border.component.ts'],
519
+ },
520
+ 'scroll-progress': {
521
+ name: 'scroll-progress',
522
+ files: ['scroll-progress.component.ts'],
523
+ },
524
+ 'blur-fade': {
525
+ name: 'blur-fade',
526
+ files: ['blur-fade.component.ts'],
527
+ },
528
+ ripple: {
529
+ name: 'ripple',
530
+ files: ['ripple.directive.ts'],
531
+ },
532
+ marquee: {
533
+ name: 'marquee',
534
+ files: ['marquee.component.ts'],
535
+ },
536
+ 'word-rotate': {
537
+ name: 'word-rotate',
538
+ files: ['word-rotate.component.ts'],
539
+ },
540
+ 'morphing-text': {
541
+ name: 'morphing-text',
542
+ files: ['morphing-text.component.ts'],
543
+ },
544
+ 'typing-animation': {
545
+ name: 'typing-animation',
546
+ files: ['typing-animation.component.ts'],
547
+ },
548
+ 'wobble-card': {
549
+ name: 'wobble-card',
550
+ files: ['wobble-card.component.ts'],
551
+ },
552
+ magnetic: {
553
+ name: 'magnetic',
554
+ files: ['magnetic.directive.ts'],
555
+ },
556
+ orbit: {
557
+ name: 'orbit',
558
+ files: ['orbit.component.ts'],
559
+ },
560
+ 'stagger-children': {
561
+ name: 'stagger-children',
562
+ files: ['stagger-children.component.ts'],
563
+ },
564
+ particles: {
565
+ name: 'particles',
566
+ files: ['particles.component.ts'],
567
+ },
568
+ // Kanban
569
+ kanban: {
570
+ name: 'kanban',
571
+ files: ['kanban.component.ts'],
572
+ dependencies: ['badge', 'avatar', 'scroll-area', 'separator'],
573
+ },
478
574
  };
@@ -0,0 +1,79 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import type { Config } from './config.js';
4
+
5
+ export interface ShortcutRegistryEntry {
6
+ exportName: string;
7
+ componentName: string;
8
+ sourceFile: string;
9
+ }
10
+
11
+ function aliasToProjectPath(aliasOrPath: string): string {
12
+ return aliasOrPath.startsWith('@/')
13
+ ? path.join('src', aliasOrPath.slice(2))
14
+ : aliasOrPath;
15
+ }
16
+
17
+ function resolveProjectPath(cwd: string, inputPath: string): string {
18
+ const resolved = path.resolve(cwd, inputPath);
19
+ const relative = path.relative(cwd, resolved);
20
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
21
+ throw new Error(`Path must stay inside the project directory: ${inputPath}`);
22
+ }
23
+ return resolved;
24
+ }
25
+
26
+ function getShortcutRegistryIndexPath(cwd: string, config: Config): string {
27
+ const utilsFilePath = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
28
+ const utilsDir = path.dirname(utilsFilePath);
29
+ return path.join(utilsDir, 'shortcut-registry.index.ts');
30
+ }
31
+
32
+ export async function writeShortcutRegistryIndex(
33
+ cwd: string,
34
+ config: Config,
35
+ entries: ShortcutRegistryEntry[],
36
+ ): Promise<string> {
37
+ const registryPath = getShortcutRegistryIndexPath(cwd, config);
38
+ await fs.ensureDir(path.dirname(registryPath));
39
+
40
+ const uniqueEntries = Array.from(new Map(entries.map(entry => [entry.exportName, entry])).values())
41
+ .sort((a, b) => a.exportName.localeCompare(b.exportName));
42
+
43
+ const uiAlias = config.aliases.ui;
44
+ const utilsAliasDir = config.aliases.utils.includes('/')
45
+ ? config.aliases.utils.slice(0, config.aliases.utils.lastIndexOf('/'))
46
+ : config.aliases.utils;
47
+ const shortcutServiceImport = `${utilsAliasDir}/shortcut-binding.service`;
48
+
49
+ const imports = uniqueEntries
50
+ .map(entry => {
51
+ const importPath = `${uiAlias}/${entry.sourceFile.replace(/\.ts$/, '')}`;
52
+ return `import { ${entry.exportName} } from '${importPath}';`;
53
+ })
54
+ .join('\n');
55
+
56
+ const catalogItems = uniqueEntries
57
+ .map(entry => ` { componentName: '${entry.componentName}', definitions: ${entry.exportName} },`)
58
+ .join('\n');
59
+
60
+ const content = `// Auto-generated by shadcn-angular CLI. Do not edit manually.
61
+ import type { ShortcutBindingService, ShortcutDefinition } from '${shortcutServiceImport}';
62
+ ${imports ? `${imports}\n` : ''}
63
+ export const GENERATED_SHORTCUT_CATALOG: ReadonlyArray<{
64
+ componentName: string;
65
+ definitions: ReadonlyArray<ShortcutDefinition>;
66
+ }> = [
67
+ ${catalogItems}
68
+ ];
69
+
70
+ export function registerGeneratedShortcutCatalog(shortcuts: ShortcutBindingService): void {
71
+ for (const entry of GENERATED_SHORTCUT_CATALOG) {
72
+ shortcuts.defineShortcuts(entry.componentName, [...entry.definitions]);
73
+ }
74
+ }
75
+ `;
76
+
77
+ await fs.writeFile(registryPath, content);
78
+ return registryPath;
79
+ }