@gilav21/shadcn-angular 0.0.16 → 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.
@@ -173,9 +173,9 @@ export async function add(components, options) {
173
173
  const localContent = await fs.readFile(targetPath, 'utf-8');
174
174
  try {
175
175
  let remoteContent = await fetchComponentContent(file, options);
176
- // Transform imports for comparison
177
- const utilsAlias = config.aliases.utils;
178
- remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
176
+ // Transform all lib/ imports for comparison
177
+ const libAlias = config.aliases.utils.replace(/\/[^/]+$/, '');
178
+ remoteContent = remoteContent.replace(/(\.\.\/)+lib\//g, libAlias + '/');
179
179
  const normalize = (str) => str.replace(/\r\n/g, '\n').trim();
180
180
  if (normalize(localContent) !== normalize(remoteContent)) {
181
181
  hasChanges = true;
@@ -255,9 +255,9 @@ export async function add(components, options) {
255
255
  let content = contentCache.get(file);
256
256
  if (!content) {
257
257
  content = await fetchComponentContent(file, options);
258
- // Transform imports if not already transformed (cached is transformed)
259
- const utilsAlias = config.aliases.utils;
260
- content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
258
+ // Transform all lib/ imports if not already transformed (cached is transformed)
259
+ const libAlias = config.aliases.utils.replace(/\/[^/]+$/, '');
260
+ content = content.replace(/(\.\.\/)+lib\//g, libAlias + '/');
261
261
  }
262
262
  await fs.ensureDir(path.dirname(targetPath));
263
263
  await fs.writeFile(targetPath, content);
@@ -283,6 +283,33 @@ export async function add(components, options) {
283
283
  else {
284
284
  spinner.info('No new components installed.');
285
285
  }
286
+ // Install required lib utility files
287
+ if (finalComponents.length > 0) {
288
+ const requiredLibFiles = new Set();
289
+ for (const name of allComponents) {
290
+ const component = registry[name];
291
+ if (component.libFiles) {
292
+ component.libFiles.forEach(f => requiredLibFiles.add(f));
293
+ }
294
+ }
295
+ if (requiredLibFiles.size > 0) {
296
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
297
+ const libDir = path.dirname(utilsPathResolved);
298
+ await fs.ensureDir(libDir);
299
+ for (const libFile of requiredLibFiles) {
300
+ const libTargetPath = path.join(libDir, libFile);
301
+ if (!await fs.pathExists(libTargetPath) || options.overwrite) {
302
+ try {
303
+ const libContent = await fetchLibContent(libFile, options);
304
+ await fs.writeFile(libTargetPath, libContent);
305
+ }
306
+ catch (err) {
307
+ console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${err.message}`));
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
286
313
  if (finalComponents.length > 0) {
287
314
  const npmDependencies = new Set();
288
315
  for (const name of finalComponents) {
@@ -3,6 +3,7 @@ export interface ComponentDefinition {
3
3
  files: string[];
4
4
  dependencies?: string[];
5
5
  npmDependencies?: string[];
6
+ libFiles?: string[];
6
7
  shortcutDefinitions?: {
7
8
  exportName: string;
8
9
  componentName: string;
@@ -140,6 +140,7 @@ export const registry = {
140
140
  'component-outlet',
141
141
  'icon',
142
142
  ],
143
+ libFiles: ['xlsx.ts'],
143
144
  },
144
145
  dialog: {
145
146
  name: 'dialog',
@@ -376,6 +377,7 @@ export const registry = {
376
377
  'dialog',
377
378
  'scroll-area',
378
379
  ],
380
+ libFiles: ['pdf-parser.ts', 'image-validator.ts', 'svg-sanitizer.ts'],
379
381
  shortcutDefinitions: [
380
382
  {
381
383
  exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"
@@ -1,41 +1,41 @@
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';
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
7
  import { getConfig } from '../utils/config.js';
8
8
  import { registry, type ComponentName } from '../registry/index.js';
9
9
  import { installPackages } from '../utils/package-manager.js';
10
10
  import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = path.dirname(__filename);
14
-
15
- // Base URL for the component registry (GitHub raw content)
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ // Base URL for the component registry (GitHub raw content)
16
16
  const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
17
17
  const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
18
-
19
- // Components source directory (relative to CLI dist folder) for local dev
18
+
19
+ // Components source directory (relative to CLI dist folder) for local dev
20
20
  function getLocalComponentsDir(): string | null {
21
- // From dist/commands/add.js -> packages/components/ui
22
- const fromDist = path.resolve(__dirname, '../../../components/ui');
23
- if (fs.existsSync(fromDist)) {
24
- return fromDist;
25
- }
26
- // Fallback: from src/commands/add.ts -> packages/components/ui
27
- const fromSrc = path.resolve(__dirname, '../../../components/ui');
28
- if (fs.existsSync(fromSrc)) {
29
- return fromSrc;
30
- }
31
- return null;
32
- }
33
-
21
+ // From dist/commands/add.js -> packages/components/ui
22
+ const fromDist = path.resolve(__dirname, '../../../components/ui');
23
+ if (fs.existsSync(fromDist)) {
24
+ return fromDist;
25
+ }
26
+ // Fallback: from src/commands/add.ts -> packages/components/ui
27
+ const fromSrc = path.resolve(__dirname, '../../../components/ui');
28
+ if (fs.existsSync(fromSrc)) {
29
+ return fromSrc;
30
+ }
31
+ return null;
32
+ }
33
+
34
34
  interface AddOptions {
35
- yes?: boolean;
36
- overwrite?: boolean;
37
- all?: boolean;
38
- path?: string;
35
+ yes?: boolean;
36
+ overwrite?: boolean;
37
+ all?: boolean;
38
+ path?: string;
39
39
  remote?: boolean; // Force remote fetch
40
40
  }
41
41
 
@@ -63,30 +63,30 @@ function aliasToProjectPath(aliasOrPath: string): string {
63
63
  }
64
64
 
65
65
  async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
66
- const localDir = getLocalComponentsDir();
67
-
68
- // 1. Prefer local if available and not forced remote
69
- if (localDir && !options.remote) {
70
- const localPath = path.join(localDir, file);
71
- if (await fs.pathExists(localPath)) {
72
- return fs.readFile(localPath, 'utf-8');
73
- }
74
- }
75
-
76
- // 2. Fetch from remote registry
77
- const url = `${REGISTRY_BASE_URL}/${file}`;
78
- try {
79
- const response = await fetch(url);
80
- if (!response.ok) {
81
- throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
82
- }
83
- return await response.text();
84
- } catch (error) {
85
- if (localDir) {
86
- throw new Error(`Component file not found locally or remotely: ${file}`);
87
- }
88
- throw error;
89
- }
66
+ const localDir = getLocalComponentsDir();
67
+
68
+ // 1. Prefer local if available and not forced remote
69
+ if (localDir && !options.remote) {
70
+ const localPath = path.join(localDir, file);
71
+ if (await fs.pathExists(localPath)) {
72
+ return fs.readFile(localPath, 'utf-8');
73
+ }
74
+ }
75
+
76
+ // 2. Fetch from remote registry
77
+ const url = `${REGISTRY_BASE_URL}/${file}`;
78
+ try {
79
+ const response = await fetch(url);
80
+ if (!response.ok) {
81
+ throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
82
+ }
83
+ return await response.text();
84
+ } catch (error) {
85
+ if (localDir) {
86
+ throw new Error(`Component file not found locally or remotely: ${file}`);
87
+ }
88
+ throw error;
89
+ }
90
90
  }
91
91
 
92
92
  async function fetchLibContent(file: string, options: AddOptions): Promise<string> {
@@ -122,225 +122,254 @@ function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEnt
122
122
  }
123
123
  return entries;
124
124
  }
125
-
126
- export async function add(components: string[], options: AddOptions) {
127
- const cwd = process.cwd();
128
-
129
- // Load config
130
- const config = await getConfig(cwd);
131
- if (!config) {
132
- console.log(chalk.red('Error: components.json not found.'));
133
- console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
134
- process.exit(1);
135
- }
136
-
137
- // Get components to add
138
- let componentsToAdd: ComponentName[] = [];
139
-
140
- if (options.all) {
141
- componentsToAdd = Object.keys(registry) as ComponentName[];
142
- } else if (components.length === 0) {
143
- const { selected } = await prompts({
144
- type: 'multiselect',
145
- name: 'selected',
146
- message: 'Which components would you like to add?',
147
- choices: Object.keys(registry).map(name => ({
148
- title: name,
149
- value: name,
150
- })),
151
- hint: '- Space to select, Enter to confirm',
152
- });
153
- componentsToAdd = selected;
154
- } else {
155
- componentsToAdd = components as ComponentName[];
156
- }
157
-
158
- if (!componentsToAdd || componentsToAdd.length === 0) {
159
- console.log(chalk.dim('No components selected.'));
160
- return;
161
- }
162
-
163
- // Validate components exist
164
- const invalidComponents = componentsToAdd.filter(c => !registry[c]);
165
- if (invalidComponents.length > 0) {
166
- console.log(chalk.red(`Invalid component(s): ${invalidComponents.join(', ')}`));
167
- console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
168
- process.exit(1);
169
- }
170
-
171
- // Resolve dependencies
172
- const allComponents = new Set<ComponentName>();
173
- const resolveDeps = (name: ComponentName) => {
174
- if (allComponents.has(name)) return;
175
- allComponents.add(name);
176
- const component = registry[name];
177
- if (component.dependencies) {
178
- component.dependencies.forEach(dep => resolveDeps(dep as ComponentName));
179
- }
180
- };
181
- componentsToAdd.forEach(c => resolveDeps(c));
182
-
125
+
126
+ export async function add(components: string[], options: AddOptions) {
127
+ const cwd = process.cwd();
128
+
129
+ // Load config
130
+ const config = await getConfig(cwd);
131
+ if (!config) {
132
+ console.log(chalk.red('Error: components.json not found.'));
133
+ console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
134
+ process.exit(1);
135
+ }
136
+
137
+ // Get components to add
138
+ let componentsToAdd: ComponentName[] = [];
139
+
140
+ if (options.all) {
141
+ componentsToAdd = Object.keys(registry) as ComponentName[];
142
+ } else if (components.length === 0) {
143
+ const { selected } = await prompts({
144
+ type: 'multiselect',
145
+ name: 'selected',
146
+ message: 'Which components would you like to add?',
147
+ choices: Object.keys(registry).map(name => ({
148
+ title: name,
149
+ value: name,
150
+ })),
151
+ hint: '- Space to select, Enter to confirm',
152
+ });
153
+ componentsToAdd = selected;
154
+ } else {
155
+ componentsToAdd = components as ComponentName[];
156
+ }
157
+
158
+ if (!componentsToAdd || componentsToAdd.length === 0) {
159
+ console.log(chalk.dim('No components selected.'));
160
+ return;
161
+ }
162
+
163
+ // Validate components exist
164
+ const invalidComponents = componentsToAdd.filter(c => !registry[c]);
165
+ if (invalidComponents.length > 0) {
166
+ console.log(chalk.red(`Invalid component(s): ${invalidComponents.join(', ')}`));
167
+ console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
168
+ process.exit(1);
169
+ }
170
+
171
+ // Resolve dependencies
172
+ const allComponents = new Set<ComponentName>();
173
+ const resolveDeps = (name: ComponentName) => {
174
+ if (allComponents.has(name)) return;
175
+ allComponents.add(name);
176
+ const component = registry[name];
177
+ if (component.dependencies) {
178
+ component.dependencies.forEach(dep => resolveDeps(dep as ComponentName));
179
+ }
180
+ };
181
+ componentsToAdd.forEach(c => resolveDeps(c));
182
+
183
183
  const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
184
184
  const targetDir = resolveProjectPath(cwd, uiBasePath);
185
-
186
- // Check for existing files and diff
187
- const componentsToInstall: ComponentName[] = [];
188
- const componentsToSkip: string[] = [];
189
- const conflictingComponents: ComponentName[] = [];
190
- const contentCache = new Map<string, string>();
191
-
192
- const checkSpinner = ora('Checking for conflicts...').start();
193
-
194
- for (const name of allComponents) {
195
- const component = registry[name];
196
- let hasChanges = false;
197
- let isFullyPresent = true;
198
-
199
- for (const file of component.files) {
200
- const targetPath = path.join(targetDir, file);
201
- if (await fs.pathExists(targetPath)) {
202
- const localContent = await fs.readFile(targetPath, 'utf-8');
203
-
204
- try {
205
- let remoteContent = await fetchComponentContent(file, options);
206
- // Transform imports for comparison
207
- const utilsAlias = config.aliases.utils;
208
- remoteContent = remoteContent.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
209
-
185
+
186
+ // Check for existing files and diff
187
+ const componentsToInstall: ComponentName[] = [];
188
+ const componentsToSkip: string[] = [];
189
+ const conflictingComponents: ComponentName[] = [];
190
+ const contentCache = new Map<string, string>();
191
+
192
+ const checkSpinner = ora('Checking for conflicts...').start();
193
+
194
+ for (const name of allComponents) {
195
+ const component = registry[name];
196
+ let hasChanges = false;
197
+ let isFullyPresent = true;
198
+
199
+ for (const file of component.files) {
200
+ const targetPath = path.join(targetDir, file);
201
+ if (await fs.pathExists(targetPath)) {
202
+ const localContent = await fs.readFile(targetPath, 'utf-8');
203
+
204
+ try {
205
+ let remoteContent = await fetchComponentContent(file, options);
206
+ // Transform all lib/ imports for comparison
207
+ const libAlias = config.aliases.utils.replace(/\/[^/]+$/, '');
208
+ remoteContent = remoteContent.replace(/(\.\.\/)+lib\//g, libAlias + '/');
209
+
210
210
  const normalize = (str: string) => str.replace(/\r\n/g, '\n').trim();
211
- if (normalize(localContent) !== normalize(remoteContent)) {
212
- hasChanges = true;
213
- }
214
- contentCache.set(file, remoteContent); // Cache for installation
215
- } catch (error) {
216
- // unexpected error fetching remote
217
- console.warn(`Could not fetch remote content for comparison: ${file}`);
218
- hasChanges = true; // Assume changed/unknown
219
- }
220
- } else {
221
- isFullyPresent = false;
222
- }
223
- }
224
-
225
- if (isFullyPresent && !hasChanges) {
226
- componentsToSkip.push(name);
227
- } else if (hasChanges) {
228
- conflictingComponents.push(name);
229
- } else {
230
- componentsToInstall.push(name);
231
- }
232
- }
233
-
234
- checkSpinner.stop();
235
-
236
- let componentsToOverwrite: ComponentName[] = [];
237
-
238
- if (conflictingComponents.length > 0) {
239
- if (options.overwrite) {
240
- componentsToOverwrite = conflictingComponents;
241
- } else if (options.yes) {
242
- componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
243
- } else {
244
- console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
245
- const { selected } = await prompts({
246
- type: 'multiselect',
247
- name: 'selected',
248
- message: 'Select components to OVERWRITE (Unselected will be skipped):',
249
- choices: conflictingComponents.map(name => ({
250
- title: name,
251
- value: name,
252
- })),
253
- hint: '- Space to select, Enter to confirm',
254
- });
255
- componentsToOverwrite = selected || [];
256
- }
257
- }
258
-
259
- // Final list of components to process
260
- // We process:
261
- // 1. componentsToInstall (Brand new or partial)
262
- // 2. componentsToOverwrite (User selected)
263
- // We SKIP:
264
- // 1. componentsToSkip (Identical)
265
- // 2. conflictingComponents NOT in componentsToOverwrite
266
-
267
- const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
268
-
269
- if (finalComponents.length === 0 && componentsToSkip.length > 0) {
270
- console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
271
- return;
272
- }
273
-
274
- if (finalComponents.length === 0) {
275
- console.log(chalk.dim('\nNo components to install.'));
276
- return;
277
- }
278
-
279
- const spinner = ora('Installing components...').start();
280
- let successCount = 0;
281
-
282
- try {
283
- await fs.ensureDir(targetDir);
284
-
285
- for (const name of finalComponents) {
286
- const component = registry[name];
287
- let componentSuccess = true;
288
-
289
- for (const file of component.files) {
290
- const targetPath = path.join(targetDir, file);
291
-
292
- try {
293
- let content = contentCache.get(file);
294
- if (!content) {
295
- content = await fetchComponentContent(file, options);
296
- // Transform imports if not already transformed (cached is transformed)
297
- const utilsAlias = config.aliases.utils;
298
- content = content.replace(/(\.\.\/)+lib\/utils/g, utilsAlias);
299
- }
300
-
301
- await fs.ensureDir(path.dirname(targetPath));
302
- await fs.writeFile(targetPath, content);
303
- // spinner.text = `Added ${file}`; // Too verbose?
304
- } catch (err: any) {
305
- spinner.warn(`Could not add ${file}: ${err.message}`);
306
- componentSuccess = false;
307
- }
308
- }
309
- if (componentSuccess) {
310
- successCount++;
311
- spinner.text = `Added ${name}`;
312
- }
313
- }
314
-
315
- if (successCount > 0) {
316
- spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
317
-
318
- console.log('\n' + chalk.dim('Components added:'));
319
- finalComponents.forEach(name => {
320
- console.log(chalk.dim(' - ') + chalk.cyan(name));
321
- });
322
- } else {
323
- spinner.info('No new components installed.');
324
- }
325
-
211
+ if (normalize(localContent) !== normalize(remoteContent)) {
212
+ hasChanges = true;
213
+ }
214
+ contentCache.set(file, remoteContent); // Cache for installation
215
+ } catch (error) {
216
+ // unexpected error fetching remote
217
+ console.warn(`Could not fetch remote content for comparison: ${file}`);
218
+ hasChanges = true; // Assume changed/unknown
219
+ }
220
+ } else {
221
+ isFullyPresent = false;
222
+ }
223
+ }
224
+
225
+ if (isFullyPresent && !hasChanges) {
226
+ componentsToSkip.push(name);
227
+ } else if (hasChanges) {
228
+ conflictingComponents.push(name);
229
+ } else {
230
+ componentsToInstall.push(name);
231
+ }
232
+ }
233
+
234
+ checkSpinner.stop();
235
+
236
+ let componentsToOverwrite: ComponentName[] = [];
237
+
238
+ if (conflictingComponents.length > 0) {
239
+ if (options.overwrite) {
240
+ componentsToOverwrite = conflictingComponents;
241
+ } else if (options.yes) {
242
+ componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
243
+ } else {
244
+ console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
245
+ const { selected } = await prompts({
246
+ type: 'multiselect',
247
+ name: 'selected',
248
+ message: 'Select components to OVERWRITE (Unselected will be skipped):',
249
+ choices: conflictingComponents.map(name => ({
250
+ title: name,
251
+ value: name,
252
+ })),
253
+ hint: '- Space to select, Enter to confirm',
254
+ });
255
+ componentsToOverwrite = selected || [];
256
+ }
257
+ }
258
+
259
+ // Final list of components to process
260
+ // We process:
261
+ // 1. componentsToInstall (Brand new or partial)
262
+ // 2. componentsToOverwrite (User selected)
263
+ // We SKIP:
264
+ // 1. componentsToSkip (Identical)
265
+ // 2. conflictingComponents NOT in componentsToOverwrite
266
+
267
+ const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
268
+
269
+ if (finalComponents.length === 0 && componentsToSkip.length > 0) {
270
+ console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
271
+ return;
272
+ }
273
+
274
+ if (finalComponents.length === 0) {
275
+ console.log(chalk.dim('\nNo components to install.'));
276
+ return;
277
+ }
278
+
279
+ const spinner = ora('Installing components...').start();
280
+ let successCount = 0;
281
+
282
+ try {
283
+ await fs.ensureDir(targetDir);
284
+
285
+ for (const name of finalComponents) {
286
+ const component = registry[name];
287
+ let componentSuccess = true;
288
+
289
+ for (const file of component.files) {
290
+ const targetPath = path.join(targetDir, file);
291
+
292
+ try {
293
+ let content = contentCache.get(file);
294
+ if (!content) {
295
+ content = await fetchComponentContent(file, options);
296
+ // Transform all lib/ imports if not already transformed (cached is transformed)
297
+ const libAlias = config.aliases.utils.replace(/\/[^/]+$/, '');
298
+ content = content.replace(/(\.\.\/)+lib\//g, libAlias + '/');
299
+ }
300
+
301
+ await fs.ensureDir(path.dirname(targetPath));
302
+ await fs.writeFile(targetPath, content);
303
+ // spinner.text = `Added ${file}`; // Too verbose?
304
+ } catch (err: any) {
305
+ spinner.warn(`Could not add ${file}: ${err.message}`);
306
+ componentSuccess = false;
307
+ }
308
+ }
309
+ if (componentSuccess) {
310
+ successCount++;
311
+ spinner.text = `Added ${name}`;
312
+ }
313
+ }
314
+
315
+ if (successCount > 0) {
316
+ spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
317
+
318
+ console.log('\n' + chalk.dim('Components added:'));
319
+ finalComponents.forEach(name => {
320
+ console.log(chalk.dim(' - ') + chalk.cyan(name));
321
+ });
322
+ } else {
323
+ spinner.info('No new components installed.');
324
+ }
325
+
326
+ // Install required lib utility files
327
+ if (finalComponents.length > 0) {
328
+ const requiredLibFiles = new Set<string>();
329
+ for (const name of allComponents) {
330
+ const component = registry[name];
331
+ if (component.libFiles) {
332
+ component.libFiles.forEach(f => requiredLibFiles.add(f));
333
+ }
334
+ }
335
+
336
+ if (requiredLibFiles.size > 0) {
337
+ const utilsPathResolved = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils) + '.ts');
338
+ const libDir = path.dirname(utilsPathResolved);
339
+ await fs.ensureDir(libDir);
340
+
341
+ for (const libFile of requiredLibFiles) {
342
+ const libTargetPath = path.join(libDir, libFile);
343
+ if (!await fs.pathExists(libTargetPath) || options.overwrite) {
344
+ try {
345
+ const libContent = await fetchLibContent(libFile, options);
346
+ await fs.writeFile(libTargetPath, libContent);
347
+ } catch (err: any) {
348
+ console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${err.message}`));
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+
326
355
  if (finalComponents.length > 0) {
327
356
  const npmDependencies = new Set<string>();
328
- for (const name of finalComponents) {
329
- const component = registry[name];
330
- if (component.npmDependencies) {
331
- component.npmDependencies.forEach(dep => npmDependencies.add(dep));
332
- }
333
- }
334
-
335
- if (npmDependencies.size > 0) {
336
- const depSpinner = ora('Installing dependencies...').start();
337
- try {
338
- await installPackages(Array.from(npmDependencies), { cwd });
339
- depSpinner.succeed('Dependencies installed.');
340
- } catch (e) {
341
- depSpinner.fail('Failed to install dependencies.');
342
- console.error(e);
343
- }
357
+ for (const name of finalComponents) {
358
+ const component = registry[name];
359
+ if (component.npmDependencies) {
360
+ component.npmDependencies.forEach(dep => npmDependencies.add(dep));
361
+ }
362
+ }
363
+
364
+ if (npmDependencies.size > 0) {
365
+ const depSpinner = ora('Installing dependencies...').start();
366
+ try {
367
+ await installPackages(Array.from(npmDependencies), { cwd });
368
+ depSpinner.succeed('Dependencies installed.');
369
+ } catch (e) {
370
+ depSpinner.fail('Failed to install dependencies.');
371
+ console.error(e);
372
+ }
344
373
  }
345
374
  }
346
375
 
@@ -361,16 +390,16 @@ export async function add(components: string[], options: AddOptions) {
361
390
 
362
391
  if (componentsToSkip.length > 0) {
363
392
  console.log('\n' + chalk.dim('Components skipped (up to date):'));
364
- componentsToSkip.forEach(name => {
365
- console.log(chalk.dim(' - ') + chalk.gray(name));
366
- });
367
- }
368
-
369
- console.log('');
370
-
371
- } catch (error) {
372
- spinner.fail('Failed to add components');
373
- console.error(error);
374
- process.exit(1);
375
- }
376
- }
393
+ componentsToSkip.forEach(name => {
394
+ console.log(chalk.dim(' - ') + chalk.gray(name));
395
+ });
396
+ }
397
+
398
+ console.log('');
399
+
400
+ } catch (error) {
401
+ spinner.fail('Failed to add components');
402
+ console.error(error);
403
+ process.exit(1);
404
+ }
405
+ }
@@ -6,6 +6,7 @@ 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')
9
10
  shortcutDefinitions?: {
10
11
  exportName: string;
11
12
  componentName: string;
@@ -155,6 +156,7 @@ export const registry: Record<string, ComponentDefinition> = {
155
156
  'component-outlet',
156
157
  'icon',
157
158
  ],
159
+ libFiles: ['xlsx.ts'],
158
160
  },
159
161
  dialog: {
160
162
  name: 'dialog',
@@ -392,6 +394,7 @@ export const registry: Record<string, ComponentDefinition> = {
392
394
  'dialog',
393
395
  'scroll-area',
394
396
  ],
397
+ libFiles: ['pdf-parser.ts', 'image-validator.ts', 'svg-sanitizer.ts'],
395
398
  shortcutDefinitions: [
396
399
  {
397
400
  exportName: 'RICH_TEXT_SHORTCUT_DEFINITIONS',
@@ -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
  }