@gilav21/shadcn-angular 0.0.15 → 0.0.17

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.
@@ -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,12 @@ 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
+ libFiles?: string[]; // Lib utility files this component requires (e.g. 'xlsx.ts')
10
+ shortcutDefinitions?: {
11
+ exportName: string;
12
+ componentName: string;
13
+ sourceFile: string;
14
+ }[];
9
15
  }
10
16
 
11
17
  export type ComponentName = keyof typeof registry;
@@ -49,6 +55,7 @@ export const registry: Record<string, ComponentDefinition> = {
49
55
  button: {
50
56
  name: 'button',
51
57
  files: ['button.component.ts'],
58
+ dependencies: ['ripple'],
52
59
  },
53
60
  'button-group': {
54
61
  name: 'button-group',
@@ -89,6 +96,13 @@ export const registry: Record<string, ComponentDefinition> = {
89
96
  name: 'command',
90
97
  files: ['command.component.ts'],
91
98
  dependencies: ['dialog'],
99
+ shortcutDefinitions: [
100
+ {
101
+ exportName: 'COMMAND_DIALOG_SHORTCUT_DEFINITIONS',
102
+ componentName: 'command-dialog',
103
+ sourceFile: 'command.component.ts',
104
+ },
105
+ ],
92
106
  },
93
107
  'context-menu': {
94
108
  name: 'context-menu',
@@ -142,6 +156,7 @@ export const registry: Record<string, ComponentDefinition> = {
142
156
  'component-outlet',
143
157
  'icon',
144
158
  ],
159
+ libFiles: ['xlsx.ts'],
145
160
  },
146
161
  dialog: {
147
162
  name: 'dialog',
@@ -353,7 +368,7 @@ export const registry: Record<string, ComponentDefinition> = {
353
368
  'emoji-picker': {
354
369
  name: 'emoji-picker',
355
370
  files: ['emoji-picker.component.ts', 'emoji-data.ts'],
356
- dependencies: ['button', 'input', 'scroll-area', 'popover'],
371
+ dependencies: ['input', 'scroll-area', 'tooltip'],
357
372
  },
358
373
  'rich-text-editor': {
359
374
  name: 'rich-text-editor',
@@ -362,18 +377,31 @@ export const registry: Record<string, ComponentDefinition> = {
362
377
  'rich-text-toolbar.component.ts',
363
378
  'rich-text-sanitizer.service.ts',
364
379
  'rich-text-markdown.service.ts',
380
+ 'rich-text-paste-normalizer.service.ts',
381
+ 'rich-text-command-registry.service.ts',
365
382
  'rich-text-mention.component.ts',
366
383
  'rich-text-image-resizer.component.ts',
384
+ 'rich-text-locales.ts',
367
385
  ],
368
386
  dependencies: [
369
387
  'button',
370
388
  'separator',
371
389
  'popover',
372
390
  'emoji-picker',
391
+ 'autocomplete',
373
392
  'select',
374
393
  'input',
394
+ 'dialog',
375
395
  'scroll-area',
376
396
  ],
397
+ libFiles: ['pdf-parser.ts', 'image-validator.ts', 'svg-sanitizer.ts'],
398
+ shortcutDefinitions: [
399
+ {
400
+ exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
401
+ componentName: 'rich-text-editor',
402
+ sourceFile: 'rich-text-editor.component.ts',
403
+ },
404
+ ],
377
405
  },
378
406
  // Chart Components
379
407
  'pie-chart': {
@@ -475,4 +503,75 @@ export const registry: Record<string, ComponentDefinition> = {
475
503
  files: ['split-button.component.ts'],
476
504
  dependencies: ['button', 'dropdown-menu'],
477
505
  },
506
+ // Animations
507
+ 'gradient-text': {
508
+ name: 'gradient-text',
509
+ files: ['gradient-text.component.ts'],
510
+ },
511
+ 'flip-text': {
512
+ name: 'flip-text',
513
+ files: ['flip-text.component.ts'],
514
+ },
515
+ meteors: {
516
+ name: 'meteors',
517
+ files: ['meteors.component.ts'],
518
+ },
519
+ 'shine-border': {
520
+ name: 'shine-border',
521
+ files: ['shine-border.component.ts'],
522
+ },
523
+ 'scroll-progress': {
524
+ name: 'scroll-progress',
525
+ files: ['scroll-progress.component.ts'],
526
+ },
527
+ 'blur-fade': {
528
+ name: 'blur-fade',
529
+ files: ['blur-fade.component.ts'],
530
+ },
531
+ ripple: {
532
+ name: 'ripple',
533
+ files: ['ripple.directive.ts'],
534
+ },
535
+ marquee: {
536
+ name: 'marquee',
537
+ files: ['marquee.component.ts'],
538
+ },
539
+ 'word-rotate': {
540
+ name: 'word-rotate',
541
+ files: ['word-rotate.component.ts'],
542
+ },
543
+ 'morphing-text': {
544
+ name: 'morphing-text',
545
+ files: ['morphing-text.component.ts'],
546
+ },
547
+ 'typing-animation': {
548
+ name: 'typing-animation',
549
+ files: ['typing-animation.component.ts'],
550
+ },
551
+ 'wobble-card': {
552
+ name: 'wobble-card',
553
+ files: ['wobble-card.component.ts'],
554
+ },
555
+ magnetic: {
556
+ name: 'magnetic',
557
+ files: ['magnetic.directive.ts'],
558
+ },
559
+ orbit: {
560
+ name: 'orbit',
561
+ files: ['orbit.component.ts'],
562
+ },
563
+ 'stagger-children': {
564
+ name: 'stagger-children',
565
+ files: ['stagger-children.component.ts'],
566
+ },
567
+ particles: {
568
+ name: 'particles',
569
+ files: ['particles.component.ts'],
570
+ },
571
+ // Kanban
572
+ kanban: {
573
+ name: 'kanban',
574
+ files: ['kanban.component.ts'],
575
+ dependencies: ['badge', 'avatar', 'scroll-area', 'separator'],
576
+ },
478
577
  };
@@ -6,7 +6,7 @@ import { twMerge } from 'tailwind-merge';
6
6
  * Utility function for merging Tailwind CSS classes with proper precedence
7
7
  */
8
8
  export function cn(...inputs: ClassValue[]): string {
9
- return twMerge(clsx(inputs));
9
+ return twMerge(clsx(inputs));
10
10
  }
11
11
 
12
12
  /**
@@ -17,5 +17,35 @@ export function isRtl(el: HTMLElement): boolean {
17
17
  return getComputedStyle(el).direction === 'rtl';
18
18
  }
19
19
 
20
+ /**
21
+ * Returns the bounding rect of the nearest ancestor that clips overflow
22
+ * (overflow: hidden | auto | scroll | clip on either axis).
23
+ * Falls back to the full viewport rect when no such ancestor exists.
24
+ *
25
+ * Use this instead of \`window.innerWidth/innerHeight\` when calculating
26
+ * popup collision boundaries so that containers like sidebars or
27
+ * fixed-height scroll panes are respected.
28
+ */
29
+ export function getClippingRect(element: HTMLElement): DOMRect {
30
+ let parent = element.parentElement;
31
+ while (parent && parent !== document.documentElement) {
32
+ const style = window.getComputedStyle(parent);
33
+ if (
34
+ /^(hidden|auto|scroll|clip)$/.test(style.overflowX) ||
35
+ /^(hidden|auto|scroll|clip)$/.test(style.overflowY)
36
+ ) {
37
+ return parent.getBoundingClientRect();
38
+ }
39
+ parent = parent.parentElement;
40
+ }
41
+ return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
42
+ }
43
+
44
+ /**
45
+ * Check if the user prefers reduced motion via the OS-level accessibility setting.
46
+ */
47
+ export function prefersReducedMotion(): boolean {
48
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
49
+ }
20
50
  `;
21
51
  }
@@ -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
+ }