@gilav21/shadcn-angular 0.0.10 → 0.0.11

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.
@@ -105,67 +105,139 @@ export async function add(components, options) {
105
105
  const targetDir = options.path
106
106
  ? path.join(cwd, options.path)
107
107
  : path.join(cwd, 'src/components/ui');
108
- // Check for existing files
109
- const existing = [];
108
+ // Check for existing files and diff
109
+ const componentsToInstall = [];
110
+ const componentsToSkip = [];
111
+ const conflictingComponents = [];
112
+ const contentCache = new Map();
113
+ const checkSpinner = ora('Checking for conflicts...').start();
110
114
  for (const name of allComponents) {
111
115
  const component = registry[name];
116
+ let hasChanges = false;
117
+ let isFullyPresent = true;
112
118
  for (const file of component.files) {
113
119
  const targetPath = path.join(targetDir, file);
114
120
  if (await fs.pathExists(targetPath)) {
115
- existing.push(file);
121
+ const localContent = await fs.readFile(targetPath, 'utf-8');
122
+ try {
123
+ let remoteContent = await fetchComponentContent(file, options);
124
+ // Transform imports for comparison
125
+ const utilsAlias = config.aliases.utils;
126
+ remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
127
+ const normalize = (str) => str.replace(/\s+/g, '').trim();
128
+ if (normalize(localContent) !== normalize(remoteContent)) {
129
+ hasChanges = true;
130
+ }
131
+ contentCache.set(file, remoteContent); // Cache for installation
132
+ }
133
+ catch (error) {
134
+ // unexpected error fetching remote
135
+ console.warn(`Could not fetch remote content for comparison: ${file}`);
136
+ hasChanges = true; // Assume changed/unknown
137
+ }
138
+ }
139
+ else {
140
+ isFullyPresent = false;
116
141
  }
117
142
  }
143
+ if (isFullyPresent && !hasChanges) {
144
+ componentsToSkip.push(name);
145
+ }
146
+ else if (hasChanges) {
147
+ conflictingComponents.push(name);
148
+ }
149
+ else {
150
+ componentsToInstall.push(name);
151
+ }
118
152
  }
119
- if (existing.length > 0 && !options.overwrite && !options.yes) {
120
- const { overwrite } = await prompts({
121
- type: 'confirm',
122
- name: 'overwrite',
123
- message: `The following files already exist: ${existing.join(', ')}. Overwrite?`,
124
- initial: false,
125
- });
126
- if (!overwrite) {
127
- console.log(chalk.dim('Installation cancelled.'));
128
- return;
153
+ checkSpinner.stop();
154
+ let componentsToOverwrite = [];
155
+ if (conflictingComponents.length > 0) {
156
+ if (options.overwrite) {
157
+ componentsToOverwrite = conflictingComponents;
158
+ }
159
+ else if (options.yes) {
160
+ componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
161
+ }
162
+ else {
163
+ console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
164
+ const { selected } = await prompts({
165
+ type: 'multiselect',
166
+ name: 'selected',
167
+ message: 'Select components to OVERWRITE (Unselected will be skipped):',
168
+ choices: conflictingComponents.map(name => ({
169
+ title: name,
170
+ value: name,
171
+ })),
172
+ hint: '- Space to select, Enter to confirm',
173
+ });
174
+ componentsToOverwrite = selected || [];
129
175
  }
130
176
  }
177
+ // Final list of components to process
178
+ // We process:
179
+ // 1. componentsToInstall (Brand new or partial)
180
+ // 2. componentsToOverwrite (User selected)
181
+ // We SKIP:
182
+ // 1. componentsToSkip (Identical)
183
+ // 2. conflictingComponents NOT in componentsToOverwrite
184
+ const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
185
+ if (finalComponents.length === 0 && componentsToSkip.length > 0) {
186
+ console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
187
+ return;
188
+ }
189
+ if (finalComponents.length === 0) {
190
+ console.log(chalk.dim('\nNo components to install.'));
191
+ return;
192
+ }
131
193
  const spinner = ora('Installing components...').start();
132
194
  let successCount = 0;
133
195
  try {
134
196
  await fs.ensureDir(targetDir);
135
- for (const name of allComponents) {
197
+ for (const name of finalComponents) {
136
198
  const component = registry[name];
137
199
  let componentSuccess = true;
138
200
  for (const file of component.files) {
139
201
  const targetPath = path.join(targetDir, file);
140
202
  try {
141
- let content = await fetchComponentContent(file, options);
142
- // Transform imports
143
- // Replace ../lib/utils (or similar relative paths) with the configured alias
144
- const utilsAlias = config.aliases.utils;
145
- content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
203
+ let content = contentCache.get(file);
204
+ if (!content) {
205
+ content = await fetchComponentContent(file, options);
206
+ // Transform imports if not already transformed (cached is transformed)
207
+ const utilsAlias = config.aliases.utils;
208
+ content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
209
+ }
146
210
  await fs.ensureDir(path.dirname(targetPath));
147
211
  await fs.writeFile(targetPath, content);
148
- spinner.text = `Added ${file}`;
212
+ // spinner.text = `Added ${file}`; // Too verbose?
149
213
  }
150
214
  catch (err) {
151
215
  spinner.warn(`Could not add ${file}: ${err.message}`);
152
216
  componentSuccess = false;
153
217
  }
154
218
  }
155
- if (componentSuccess)
219
+ if (componentSuccess) {
156
220
  successCount++;
221
+ spinner.text = `Added ${name}`;
222
+ }
157
223
  }
158
224
  if (successCount > 0) {
159
- spinner.succeed(chalk.green(`Added ${successCount} component(s)`));
225
+ spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
160
226
  console.log('\n' + chalk.dim('Components added:'));
161
- allComponents.forEach(name => {
227
+ finalComponents.forEach(name => {
162
228
  console.log(chalk.dim(' - ') + chalk.cyan(name));
163
229
  });
164
- console.log('');
165
230
  }
166
231
  else {
167
- spinner.fail(chalk.red('Failed to add any components.'));
232
+ spinner.info('No new components installed.');
233
+ }
234
+ if (componentsToSkip.length > 0) {
235
+ console.log('\n' + chalk.dim('Components skipped (up to date):'));
236
+ componentsToSkip.forEach(name => {
237
+ console.log(chalk.dim(' - ') + chalk.gray(name));
238
+ });
168
239
  }
240
+ console.log('');
169
241
  }
170
242
  catch (error) {
171
243
  spinner.fail('Failed to add components');
@@ -195,6 +195,35 @@ export async function init(options) {
195
195
  };
196
196
  await fs.writeJson(postcssrcPath, configContent, { spaces: 4 });
197
197
  }
198
+ // Configure app.config.ts with Lucide icons
199
+ spinner.text = 'Configuring icons in app.config.ts...';
200
+ const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
201
+ if (await fs.pathExists(appConfigPath)) {
202
+ let appConfigContent = await fs.readFile(appConfigPath, 'utf-8');
203
+ // Add imports
204
+ if (!appConfigContent.includes('LucideAngularModule')) {
205
+ const iconImports = "import { LucideAngularModule, ArrowDown, ArrowUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-angular';";
206
+ appConfigContent = iconImports + '\n' + appConfigContent;
207
+ }
208
+ if (!appConfigContent.includes('importProvidersFrom')) {
209
+ appConfigContent = "import { importProvidersFrom } from '@angular/core';\n" + appConfigContent;
210
+ }
211
+ // Add provider
212
+ const providerCode = `
213
+ importProvidersFrom(LucideAngularModule.pick({
214
+ ArrowDown,
215
+ ArrowUp,
216
+ ChevronsUpDown,
217
+ ChevronLeft,
218
+ ChevronRight,
219
+ ChevronsLeft,
220
+ ChevronsRight
221
+ }))`;
222
+ if (!appConfigContent.includes('LucideAngularModule.pick')) {
223
+ appConfigContent = appConfigContent.replace(/providers:\s*\[/, `providers: [${providerCode},`);
224
+ await fs.writeFile(appConfigPath, appConfigContent);
225
+ }
226
+ }
198
227
  spinner.succeed(chalk.green('Project initialized successfully!'));
199
228
  console.log('\n' + chalk.bold('Next steps:'));
200
229
  console.log(chalk.dim(' 1. Add components: ') + chalk.cyan('npx @gilav21/shadcn-angular add button'));
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ const program = new Command();
6
6
  program
7
7
  .name('shadcn-angular')
8
8
  .description('CLI for adding shadcn-angular components to your Angular project')
9
- .version('0.0.8');
9
+ .version('0.0.10');
10
10
  program
11
11
  .command('init')
12
12
  .description('Initialize shadcn-angular in your project')
@@ -89,6 +89,25 @@ export const registry = {
89
89
  files: ['date-picker.component.ts'],
90
90
  dependencies: ['calendar'],
91
91
  },
92
+ 'data-table': {
93
+ name: 'data-table',
94
+ files: [
95
+ 'data-table/data-table.component.ts',
96
+ 'data-table/data-table-column-header.component.ts',
97
+ 'data-table/data-table-pagination.component.ts',
98
+ 'data-table/data-table.types.ts',
99
+ 'data-table/cell-host.directive.ts',
100
+ ],
101
+ dependencies: [
102
+ 'table',
103
+ 'input',
104
+ 'button',
105
+ 'checkbox',
106
+ 'select',
107
+ 'pagination',
108
+ 'popover',
109
+ ],
110
+ },
92
111
  dialog: {
93
112
  name: 'dialog',
94
113
  files: ['dialog.component.ts'],
@@ -124,7 +143,8 @@ export const registry = {
124
143
  },
125
144
  'input-group': {
126
145
  name: 'input-group',
127
- files: ['input-group.component.ts'],
146
+ files: ['input-group.component.ts', 'input-group.token.ts'],
147
+ dependencies: ['input'],
128
148
  },
129
149
  'input-otp': {
130
150
  name: 'input-otp',
@@ -262,7 +282,7 @@ export const registry = {
262
282
  'chip-list': {
263
283
  name: 'chip-list',
264
284
  files: ['chip-list.component.ts'],
265
- dependencies: ['badge', 'button'],
285
+ dependencies: ['badge', 'button', 'input', 'input-group'],
266
286
  },
267
287
  'emoji-picker': {
268
288
  name: 'emoji-picker',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"
@@ -32,4 +32,4 @@
32
32
  "@types/prompts": "^2.4.9",
33
33
  "typescript": "^5.5.0"
34
34
  }
35
- }
35
+ }
@@ -124,38 +124,106 @@ export async function add(components: string[], options: AddOptions) {
124
124
  ? path.join(cwd, options.path)
125
125
  : path.join(cwd, 'src/components/ui');
126
126
 
127
- // Check for existing files
128
- const existing: string[] = [];
127
+ // Check for existing files and diff
128
+ const componentsToInstall: ComponentName[] = [];
129
+ const componentsToSkip: string[] = [];
130
+ const conflictingComponents: ComponentName[] = [];
131
+ const contentCache = new Map<string, string>();
132
+
133
+ const checkSpinner = ora('Checking for conflicts...').start();
134
+
129
135
  for (const name of allComponents) {
130
136
  const component = registry[name];
137
+ let hasChanges = false;
138
+ let isFullyPresent = true;
139
+
131
140
  for (const file of component.files) {
132
141
  const targetPath = path.join(targetDir, file);
133
142
  if (await fs.pathExists(targetPath)) {
134
- existing.push(file);
143
+ const localContent = await fs.readFile(targetPath, 'utf-8');
144
+
145
+ try {
146
+ let remoteContent = await fetchComponentContent(file, options);
147
+ // Transform imports for comparison
148
+ const utilsAlias = config.aliases.utils;
149
+ remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
150
+
151
+ const normalize = (str: string) => str.replace(/\s+/g, '').trim();
152
+ if (normalize(localContent) !== normalize(remoteContent)) {
153
+ hasChanges = true;
154
+ }
155
+ contentCache.set(file, remoteContent); // Cache for installation
156
+ } catch (error) {
157
+ // unexpected error fetching remote
158
+ console.warn(`Could not fetch remote content for comparison: ${file}`);
159
+ hasChanges = true; // Assume changed/unknown
160
+ }
161
+ } else {
162
+ isFullyPresent = false;
135
163
  }
136
164
  }
165
+
166
+ if (isFullyPresent && !hasChanges) {
167
+ componentsToSkip.push(name);
168
+ } else if (hasChanges) {
169
+ conflictingComponents.push(name);
170
+ } else {
171
+ componentsToInstall.push(name);
172
+ }
137
173
  }
138
174
 
139
- if (existing.length > 0 && !options.overwrite && !options.yes) {
140
- const { overwrite } = await prompts({
141
- type: 'confirm',
142
- name: 'overwrite',
143
- message: `The following files already exist: ${existing.join(', ')}. Overwrite?`,
144
- initial: false,
145
- });
146
- if (!overwrite) {
147
- console.log(chalk.dim('Installation cancelled.'));
148
- return;
175
+ checkSpinner.stop();
176
+
177
+ let componentsToOverwrite: ComponentName[] = [];
178
+
179
+ if (conflictingComponents.length > 0) {
180
+ if (options.overwrite) {
181
+ componentsToOverwrite = conflictingComponents;
182
+ } else if (options.yes) {
183
+ componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
184
+ } else {
185
+ console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
186
+ const { selected } = await prompts({
187
+ type: 'multiselect',
188
+ name: 'selected',
189
+ message: 'Select components to OVERWRITE (Unselected will be skipped):',
190
+ choices: conflictingComponents.map(name => ({
191
+ title: name,
192
+ value: name,
193
+ })),
194
+ hint: '- Space to select, Enter to confirm',
195
+ });
196
+ componentsToOverwrite = selected || [];
149
197
  }
150
198
  }
151
199
 
200
+ // Final list of components to process
201
+ // We process:
202
+ // 1. componentsToInstall (Brand new or partial)
203
+ // 2. componentsToOverwrite (User selected)
204
+ // We SKIP:
205
+ // 1. componentsToSkip (Identical)
206
+ // 2. conflictingComponents NOT in componentsToOverwrite
207
+
208
+ const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
209
+
210
+ if (finalComponents.length === 0 && componentsToSkip.length > 0) {
211
+ console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
212
+ return;
213
+ }
214
+
215
+ if (finalComponents.length === 0) {
216
+ console.log(chalk.dim('\nNo components to install.'));
217
+ return;
218
+ }
219
+
152
220
  const spinner = ora('Installing components...').start();
153
221
  let successCount = 0;
154
222
 
155
223
  try {
156
224
  await fs.ensureDir(targetDir);
157
225
 
158
- for (const name of allComponents) {
226
+ for (const name of finalComponents) {
159
227
  const component = registry[name];
160
228
  let componentSuccess = true;
161
229
 
@@ -163,35 +231,48 @@ export async function add(components: string[], options: AddOptions) {
163
231
  const targetPath = path.join(targetDir, file);
164
232
 
165
233
  try {
166
- let content = await fetchComponentContent(file, options);
234
+ let content = contentCache.get(file);
235
+ if (!content) {
236
+ content = await fetchComponentContent(file, options);
237
+ // Transform imports if not already transformed (cached is transformed)
238
+ const utilsAlias = config.aliases.utils;
239
+ content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
240
+ }
167
241
 
168
- // Transform imports
169
- // Replace ../lib/utils (or similar relative paths) with the configured alias
170
- const utilsAlias = config.aliases.utils;
171
- content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
172
242
  await fs.ensureDir(path.dirname(targetPath));
173
243
  await fs.writeFile(targetPath, content);
174
- spinner.text = `Added ${file}`;
244
+ // spinner.text = `Added ${file}`; // Too verbose?
175
245
  } catch (err: any) {
176
246
  spinner.warn(`Could not add ${file}: ${err.message}`);
177
247
  componentSuccess = false;
178
248
  }
179
249
  }
180
- if (componentSuccess) successCount++;
250
+ if (componentSuccess) {
251
+ successCount++;
252
+ spinner.text = `Added ${name}`;
253
+ }
181
254
  }
182
255
 
183
256
  if (successCount > 0) {
184
- spinner.succeed(chalk.green(`Added ${successCount} component(s)`));
257
+ spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
185
258
 
186
259
  console.log('\n' + chalk.dim('Components added:'));
187
- allComponents.forEach(name => {
260
+ finalComponents.forEach(name => {
188
261
  console.log(chalk.dim(' - ') + chalk.cyan(name));
189
262
  });
190
- console.log('');
191
263
  } else {
192
- spinner.fail(chalk.red('Failed to add any components.'));
264
+ spinner.info('No new components installed.');
193
265
  }
194
266
 
267
+ if (componentsToSkip.length > 0) {
268
+ console.log('\n' + chalk.dim('Components skipped (up to date):'));
269
+ componentsToSkip.forEach(name => {
270
+ console.log(chalk.dim(' - ') + chalk.gray(name));
271
+ });
272
+ }
273
+
274
+ console.log('');
275
+
195
276
  } catch (error) {
196
277
  spinner.fail('Failed to add components');
197
278
  console.error(error);
@@ -224,6 +224,44 @@ export async function init(options: InitOptions) {
224
224
  await fs.writeJson(postcssrcPath, configContent, { spaces: 4 });
225
225
  }
226
226
 
227
+ // Configure app.config.ts with Lucide icons
228
+ spinner.text = 'Configuring icons in app.config.ts...';
229
+ const appConfigPath = path.join(cwd, 'src/app/app.config.ts');
230
+
231
+ if (await fs.pathExists(appConfigPath)) {
232
+ let appConfigContent = await fs.readFile(appConfigPath, 'utf-8');
233
+
234
+ // Add imports
235
+ if (!appConfigContent.includes('LucideAngularModule')) {
236
+ const iconImports = "import { LucideAngularModule, ArrowDown, ArrowUp, ChevronsUpDown, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-angular';";
237
+ appConfigContent = iconImports + '\n' + appConfigContent;
238
+ }
239
+
240
+ if (!appConfigContent.includes('importProvidersFrom')) {
241
+ appConfigContent = "import { importProvidersFrom } from '@angular/core';\n" + appConfigContent;
242
+ }
243
+
244
+ // Add provider
245
+ const providerCode = `
246
+ importProvidersFrom(LucideAngularModule.pick({
247
+ ArrowDown,
248
+ ArrowUp,
249
+ ChevronsUpDown,
250
+ ChevronLeft,
251
+ ChevronRight,
252
+ ChevronsLeft,
253
+ ChevronsRight
254
+ }))`;
255
+
256
+ if (!appConfigContent.includes('LucideAngularModule.pick')) {
257
+ appConfigContent = appConfigContent.replace(
258
+ /providers:\s*\[/,
259
+ `providers: [${providerCode},`
260
+ );
261
+ await fs.writeFile(appConfigPath, appConfigContent);
262
+ }
263
+ }
264
+
227
265
  spinner.succeed(chalk.green('Project initialized successfully!'));
228
266
 
229
267
  console.log('\n' + chalk.bold('Next steps:'));
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ const program = new Command();
8
8
  program
9
9
  .name('shadcn-angular')
10
10
  .description('CLI for adding shadcn-angular components to your Angular project')
11
- .version('0.0.8');
11
+ .version('0.0.10');
12
12
 
13
13
  program
14
14
  .command('init')
@@ -98,6 +98,25 @@ export const registry: Record<string, ComponentDefinition> = {
98
98
  files: ['date-picker.component.ts'],
99
99
  dependencies: ['calendar'],
100
100
  },
101
+ 'data-table': {
102
+ name: 'data-table',
103
+ files: [
104
+ 'data-table/data-table.component.ts',
105
+ 'data-table/data-table-column-header.component.ts',
106
+ 'data-table/data-table-pagination.component.ts',
107
+ 'data-table/data-table.types.ts',
108
+ 'data-table/cell-host.directive.ts',
109
+ ],
110
+ dependencies: [
111
+ 'table',
112
+ 'input',
113
+ 'button',
114
+ 'checkbox',
115
+ 'select',
116
+ 'pagination',
117
+ 'popover',
118
+ ],
119
+ },
101
120
  dialog: {
102
121
  name: 'dialog',
103
122
  files: ['dialog.component.ts'],
@@ -133,7 +152,8 @@ export const registry: Record<string, ComponentDefinition> = {
133
152
  },
134
153
  'input-group': {
135
154
  name: 'input-group',
136
- files: ['input-group.component.ts'],
155
+ files: ['input-group.component.ts', 'input-group.token.ts'],
156
+ dependencies: ['input'],
137
157
  },
138
158
  'input-otp': {
139
159
  name: 'input-otp',
@@ -271,7 +291,7 @@ export const registry: Record<string, ComponentDefinition> = {
271
291
  'chip-list': {
272
292
  name: 'chip-list',
273
293
  files: ['chip-list.component.ts'],
274
- dependencies: ['badge', 'button'],
294
+ dependencies: ['badge', 'button', 'input', 'input-group'],
275
295
  },
276
296
  'emoji-picker': {
277
297
  name: 'emoji-picker',