@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.
- package/dist/commands/add.js +67 -4
- package/dist/commands/init.js +75 -15
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +92 -1
- package/dist/utils/shortcut-registry.d.ts +7 -0
- package/dist/utils/shortcut-registry.js +58 -0
- package/package.json +1 -1
- package/src/commands/add.ts +95 -22
- package/src/commands/init.ts +145 -77
- package/src/registry/index.ts +97 -1
- package/src/utils/shortcut-registry.ts +79 -0
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
107
|
-
|
|
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(/\
|
|
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 => {
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
147
|
-
const utilsDir = path.dirname(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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');
|
package/dist/registry/index.d.ts
CHANGED
|
@@ -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>;
|
package/dist/registry/index.js
CHANGED
|
@@ -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: ['
|
|
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
package/src/commands/add.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
125
|
-
|
|
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(/\
|
|
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
|
-
|
|
290
|
-
|
|
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
|
});
|
package/src/commands/init.ts
CHANGED
|
@@ -1,19 +1,66 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
166
|
-
const utilsDir = path.dirname(
|
|
167
|
-
|
|
168
|
-
await fs.ensureDir(utilsDir);
|
|
169
|
-
await fs.writeFile(
|
|
170
|
-
spinner.text = 'Created utils.ts';
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
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...';
|
package/src/registry/index.ts
CHANGED
|
@@ -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: ['
|
|
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
|
+
}
|