@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.
- package/dist/commands/add.js +33 -6
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/templates/utils.js +31 -1
- package/package.json +1 -1
- package/src/commands/add.ts +310 -281
- package/src/registry/index.ts +3 -0
- package/src/templates/utils.ts +31 -1
package/dist/commands/add.js
CHANGED
|
@@ -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
|
|
178
|
-
remoteContent = remoteContent.replace(/(\.\.\/)+lib
|
|
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
|
|
260
|
-
content = content.replace(/(\.\.\/)+lib
|
|
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) {
|
package/dist/registry/index.d.ts
CHANGED
package/dist/registry/index.js
CHANGED
|
@@ -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',
|
package/dist/templates/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
package/src/commands/add.ts
CHANGED
|
@@ -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
|
|
208
|
-
remoteContent = remoteContent.replace(/(\.\.\/)+lib
|
|
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
|
|
298
|
-
content = content.replace(/(\.\.\/)+lib
|
|
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
|
+
}
|
package/src/registry/index.ts
CHANGED
|
@@ -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',
|
package/src/templates/utils.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|