@ng-cn/core 1.0.0

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.
@@ -0,0 +1,310 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ngAdd = ngAdd;
4
+ const tasks_1 = require("@angular-devkit/schematics/tasks");
5
+ // CSS Variables template for shadcn theming
6
+ const CSS_VARIABLES_TEMPLATE = `/* ng-cn/core - shadcn-angular styles */
7
+ @use "tailwindcss";
8
+
9
+ @custom-variant dark (&:is(.dark *));
10
+
11
+ :root {
12
+ --radius: 0.625rem;
13
+ --background: oklch(1 0 0);
14
+ --foreground: oklch(0.145 0 0);
15
+ --card: oklch(1 0 0);
16
+ --card-foreground: oklch(0.145 0 0);
17
+ --popover: oklch(1 0 0);
18
+ --popover-foreground: oklch(0.145 0 0);
19
+ --primary: oklch(0.205 0 0);
20
+ --primary-foreground: oklch(0.985 0 0);
21
+ --secondary: oklch(0.97 0 0);
22
+ --secondary-foreground: oklch(0.205 0 0);
23
+ --muted: oklch(0.97 0 0);
24
+ --muted-foreground: oklch(0.556 0 0);
25
+ --accent: oklch(0.97 0 0);
26
+ --accent-foreground: oklch(0.205 0 0);
27
+ --destructive: oklch(0.577 0.245 27.325);
28
+ --destructive-foreground: oklch(0.577 0.245 27.325);
29
+ --border: oklch(0.922 0 0);
30
+ --input: oklch(0.922 0 0);
31
+ --ring: oklch(0.708 0 0);
32
+ --chart-1: oklch(0.646 0.222 41.116);
33
+ --chart-2: oklch(0.6 0.118 184.704);
34
+ --chart-3: oklch(0.398 0.07 227.392);
35
+ --chart-4: oklch(0.828 0.189 84.429);
36
+ --chart-5: oklch(0.769 0.188 70.08);
37
+ --sidebar: oklch(0.985 0 0);
38
+ --sidebar-foreground: oklch(0.145 0 0);
39
+ --sidebar-primary: oklch(0.205 0 0);
40
+ --sidebar-primary-foreground: oklch(0.985 0 0);
41
+ --sidebar-accent: oklch(0.97 0 0);
42
+ --sidebar-accent-foreground: oklch(0.205 0 0);
43
+ --sidebar-border: oklch(0.922 0 0);
44
+ --sidebar-ring: oklch(0.708 0 0);
45
+ }
46
+
47
+ .dark {
48
+ --background: oklch(0.145 0 0);
49
+ --foreground: oklch(0.985 0 0);
50
+ --card: oklch(0.205 0 0);
51
+ --card-foreground: oklch(0.985 0 0);
52
+ --popover: oklch(0.205 0 0);
53
+ --popover-foreground: oklch(0.985 0 0);
54
+ --primary: oklch(0.985 0 0);
55
+ --primary-foreground: oklch(0.205 0 0);
56
+ --secondary: oklch(0.269 0 0);
57
+ --secondary-foreground: oklch(0.985 0 0);
58
+ --muted: oklch(0.269 0 0);
59
+ --muted-foreground: oklch(0.708 0 0);
60
+ --accent: oklch(0.269 0 0);
61
+ --accent-foreground: oklch(0.985 0 0);
62
+ --destructive: oklch(0.396 0.141 25.723);
63
+ --destructive-foreground: oklch(0.637 0.237 25.331);
64
+ --border: oklch(0.269 0 0);
65
+ --input: oklch(0.269 0 0);
66
+ --ring: oklch(0.439 0 0);
67
+ --chart-1: oklch(0.488 0.243 264.376);
68
+ --chart-2: oklch(0.696 0.17 162.48);
69
+ --chart-3: oklch(0.769 0.188 70.08);
70
+ --chart-4: oklch(0.627 0.265 303.9);
71
+ --chart-5: oklch(0.645 0.246 16.439);
72
+ --sidebar: oklch(0.205 0 0);
73
+ --sidebar-foreground: oklch(0.985 0 0);
74
+ --sidebar-primary: oklch(0.488 0.243 264.376);
75
+ --sidebar-primary-foreground: oklch(0.985 0 0);
76
+ --sidebar-accent: oklch(0.269 0 0);
77
+ --sidebar-accent-foreground: oklch(0.985 0 0);
78
+ --sidebar-border: oklch(0.269 0 0);
79
+ --sidebar-ring: oklch(0.439 0 0);
80
+ }
81
+
82
+ @theme inline {
83
+ --color-background: var(--background);
84
+ --color-foreground: var(--foreground);
85
+ --color-card: var(--card);
86
+ --color-card-foreground: var(--card-foreground);
87
+ --color-popover: var(--popover);
88
+ --color-popover-foreground: var(--popover-foreground);
89
+ --color-primary: var(--primary);
90
+ --color-primary-foreground: var(--primary-foreground);
91
+ --color-secondary: var(--secondary);
92
+ --color-secondary-foreground: var(--secondary-foreground);
93
+ --color-muted: var(--muted);
94
+ --color-muted-foreground: var(--muted-foreground);
95
+ --color-accent: var(--accent);
96
+ --color-accent-foreground: var(--accent-foreground);
97
+ --color-destructive: var(--destructive);
98
+ --color-destructive-foreground: var(--destructive-foreground);
99
+ --color-border: var(--border);
100
+ --color-input: var(--input);
101
+ --color-ring: var(--ring);
102
+ --color-chart-1: var(--chart-1);
103
+ --color-chart-2: var(--chart-2);
104
+ --color-chart-3: var(--chart-3);
105
+ --color-chart-4: var(--chart-4);
106
+ --color-chart-5: var(--chart-5);
107
+ --color-sidebar: var(--sidebar);
108
+ --color-sidebar-foreground: var(--sidebar-foreground);
109
+ --color-sidebar-primary: var(--sidebar-primary);
110
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
111
+ --color-sidebar-accent: var(--sidebar-accent);
112
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
113
+ --color-sidebar-border: var(--sidebar-border);
114
+ --color-sidebar-ring: var(--sidebar-ring);
115
+ --radius-sm: calc(var(--radius) - 4px);
116
+ --radius-md: calc(var(--radius) - 2px);
117
+ --radius-lg: var(--radius);
118
+ --radius-xl: calc(var(--radius) + 4px);
119
+ }
120
+
121
+ @layer base {
122
+ * { @apply border-border outline-ring/50; }
123
+ body { @apply bg-background text-foreground; font-feature-settings: "rlig" 1, "calt" 1; }
124
+ html { scroll-behavior: smooth; }
125
+ :focus-visible { @apply outline-2 outline-ring outline-offset-2; }
126
+ }
127
+
128
+ @layer utilities {
129
+ .animate-accordion-down { animation: accordion-down 0.2s ease-out; }
130
+ .animate-accordion-up { animation: accordion-up 0.2s ease-out; }
131
+ @keyframes accordion-down { from { height: 0; } to { height: var(--accordion-content-height); } }
132
+ @keyframes accordion-up { from { height: var(--accordion-content-height); } to { height: 0; } }
133
+ .animate-collapsible-down { animation: collapsible-down 0.2s ease-out; }
134
+ .animate-collapsible-up { animation: collapsible-up 0.2s ease-out; }
135
+ @keyframes collapsible-down { from { height: 0; } to { height: var(--collapsible-content-height); } }
136
+ @keyframes collapsible-up { from { height: var(--collapsible-content-height); } to { height: 0; } }
137
+ }
138
+ `;
139
+ // cn utility template
140
+ const CN_UTILITY_TEMPLATE = `import { clsx, type ClassValue } from 'clsx';
141
+ import { twMerge } from 'tailwind-merge';
142
+
143
+ /**
144
+ * Utility function to merge Tailwind CSS classes with proper conflict resolution.
145
+ * Combines clsx for conditional classes with tailwind-merge for deduplication.
146
+ *
147
+ * @example
148
+ * cn('px-4 py-2', 'px-6') // => 'py-2 px-6'
149
+ * cn('bg-red-500', condition && 'bg-blue-500') // conditional classes
150
+ */
151
+ export function cn(...inputs: ClassValue[]): string {
152
+ return twMerge(clsx(inputs));
153
+ }
154
+ `;
155
+ const UTILS_INDEX_TEMPLATE = `export { cn } from './cn';
156
+ `;
157
+ function ngAdd(options) {
158
+ return (tree, context) => {
159
+ context.logger.info('');
160
+ context.logger.info('╭──────────────────────────────────────────────────╮');
161
+ context.logger.info('│ │');
162
+ context.logger.info('│ 🎨 @ng-cn/core - shadcn for Angular │');
163
+ context.logger.info('│ │');
164
+ context.logger.info('╰──────────────────────────────────────────────────╯');
165
+ context.logger.info('');
166
+ // Add dependencies to package.json
167
+ const packageJsonPath = '/package.json';
168
+ if (tree.exists(packageJsonPath)) {
169
+ const packageJson = JSON.parse(tree.read(packageJsonPath).toString('utf-8'));
170
+ const requiredDependencies = {
171
+ 'lucide-angular': '^0.562.0',
172
+ 'class-variance-authority': '^0.7.1',
173
+ 'clsx': '^2.1.1',
174
+ 'tailwind-merge': '^3.4.0',
175
+ '@angular/cdk': '^21.0.5',
176
+ 'tailwindcss': '^4.1.18',
177
+ '@tailwindcss/postcss': '^4.1.18'
178
+ };
179
+ // Check and add missing dependencies
180
+ let needsInstall = false;
181
+ context.logger.info('📦 Dependencies');
182
+ for (const [pkg, version] of Object.entries(requiredDependencies)) {
183
+ if (!packageJson.dependencies?.[pkg]) {
184
+ packageJson.dependencies = packageJson.dependencies || {};
185
+ packageJson.dependencies[pkg] = version;
186
+ needsInstall = true;
187
+ context.logger.info(` + ${pkg}@${version}`);
188
+ }
189
+ }
190
+ if (needsInstall) {
191
+ tree.overwrite(packageJsonPath, JSON.stringify(packageJson, null, 2));
192
+ if (!options.skipInstall) {
193
+ context.addTask(new tasks_1.NodePackageInstallTask());
194
+ }
195
+ }
196
+ else {
197
+ context.logger.info(' ✓ All dependencies already installed');
198
+ }
199
+ }
200
+ // Create lib/utils folder structure
201
+ context.logger.info('');
202
+ context.logger.info('📁 Project Structure');
203
+ const utilsPath = '/src/app/lib/utils';
204
+ const uiPath = '/src/app/lib/components/ui';
205
+ // Create cn utility
206
+ if (!tree.exists(`${utilsPath}/cn.ts`)) {
207
+ tree.create(`${utilsPath}/cn.ts`, CN_UTILITY_TEMPLATE);
208
+ context.logger.info(` + src/app/lib/utils/cn.ts`);
209
+ }
210
+ else {
211
+ context.logger.info(` ✓ utils/cn.ts exists`);
212
+ }
213
+ // Create utils index
214
+ if (!tree.exists(`${utilsPath}/index.ts`)) {
215
+ tree.create(`${utilsPath}/index.ts`, UTILS_INDEX_TEMPLATE);
216
+ context.logger.info(` + src/app/lib/utils/index.ts`);
217
+ }
218
+ // Create ui components directory
219
+ if (!tree.exists(`${uiPath}/.gitkeep`)) {
220
+ tree.create(`${uiPath}/.gitkeep`, '');
221
+ context.logger.info(` + src/app/lib/components/ui/`);
222
+ }
223
+ // Setup styles
224
+ if (!options.skipStyles) {
225
+ context.logger.info('');
226
+ context.logger.info('🎨 Styles');
227
+ const ngCnStylesPath = '/src/ng-cn.scss';
228
+ const stylesPath = '/src/styles.scss';
229
+ const stylesCssPath = '/src/styles.css';
230
+ // Create ng-cn.scss with all CSS variables
231
+ tree.create(ngCnStylesPath, CSS_VARIABLES_TEMPLATE);
232
+ context.logger.info(` + src/ng-cn.scss (theme variables)`);
233
+ // Import in styles.scss or styles.css
234
+ if (tree.exists(stylesPath)) {
235
+ const stylesContent = tree.read(stylesPath).toString('utf-8');
236
+ if (!stylesContent.includes('ng-cn.scss')) {
237
+ const newContent = `@import './ng-cn.scss';\n\n${stylesContent}`;
238
+ tree.overwrite(stylesPath, newContent);
239
+ context.logger.info(` ✓ Imported in styles.scss`);
240
+ }
241
+ }
242
+ else if (tree.exists(stylesCssPath)) {
243
+ const stylesContent = tree.read(stylesCssPath).toString('utf-8');
244
+ if (!stylesContent.includes('ng-cn.scss')) {
245
+ const newContent = `@import './ng-cn.scss';\n\n${stylesContent}`;
246
+ tree.overwrite(stylesCssPath, newContent);
247
+ context.logger.info(` ✓ Imported in styles.css`);
248
+ }
249
+ }
250
+ }
251
+ // Update tsconfig paths
252
+ context.logger.info('');
253
+ context.logger.info('⚙️ TypeScript Config');
254
+ const tsconfigPath = '/tsconfig.json';
255
+ if (tree.exists(tsconfigPath)) {
256
+ const tsconfig = JSON.parse(tree.read(tsconfigPath).toString('utf-8'));
257
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
258
+ tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
259
+ const pathAliases = {
260
+ '@/*': ['src/*'],
261
+ '@/lib/*': ['src/app/lib/*'],
262
+ '@/ui/*': ['src/app/lib/components/ui/*'],
263
+ '@/utils/*': ['src/app/lib/utils/*']
264
+ };
265
+ let pathsUpdated = false;
266
+ for (const [alias, paths] of Object.entries(pathAliases)) {
267
+ if (!tsconfig.compilerOptions.paths[alias]) {
268
+ tsconfig.compilerOptions.paths[alias] = paths;
269
+ pathsUpdated = true;
270
+ }
271
+ }
272
+ if (pathsUpdated) {
273
+ tree.overwrite(tsconfigPath, JSON.stringify(tsconfig, null, 2));
274
+ context.logger.info(' ✓ Path aliases configured');
275
+ }
276
+ else {
277
+ context.logger.info(' ✓ Path aliases already set');
278
+ }
279
+ }
280
+ // Success message with ASCII art banner
281
+ context.logger.info('');
282
+ context.logger.info('');
283
+ context.logger.info(' ███╗ ██╗ ██████╗ ██████╗███╗ ██╗');
284
+ context.logger.info(' ████╗ ██║██╔════╝ ██╔════╝████╗ ██║');
285
+ context.logger.info(' ██╔██╗ ██║██║ ███╗█████╗██║ ██╔██╗ ██║');
286
+ context.logger.info(' ██║╚██╗██║██║ ██║╚════╝██║ ██║╚██╗██║');
287
+ context.logger.info(' ██║ ╚████║╚██████╔╝ ╚██████╗██║ ╚████║');
288
+ context.logger.info(' ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝╚═╝ ╚═══╝');
289
+ context.logger.info('');
290
+ context.logger.info(' ✅ Setup complete! shadcn for Angular');
291
+ context.logger.info('');
292
+ context.logger.info('╭──────────────────────────────────────────────────╮');
293
+ context.logger.info('│ 🚀 Next steps: │');
294
+ context.logger.info('│ │');
295
+ context.logger.info('│ 1. Add components: │');
296
+ context.logger.info('│ ng g @ng-cn/core:c button │');
297
+ context.logger.info('│ ng g @ng-cn/core:c card │');
298
+ context.logger.info('│ │');
299
+ context.logger.info('│ 2. Or install individual packages: │');
300
+ context.logger.info('│ npm i @ng-cn/button @ng-cn/card │');
301
+ context.logger.info('│ │');
302
+ context.logger.info('│ 3. Import and use: │');
303
+ context.logger.info("│ import { Button } from '@/ui/button'; │");
304
+ context.logger.info('│ │');
305
+ context.logger.info('│ 📚 Docs: https://shadcn-angular.tigayon.com │');
306
+ context.logger.info('╰──────────────────────────────────────────────────╯');
307
+ context.logger.info('');
308
+ return tree;
309
+ };
310
+ }
@@ -0,0 +1,334 @@
1
+ import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
2
+ import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
3
+
4
+ interface NgAddOptions {
5
+ project?: string;
6
+ skipInstall?: boolean;
7
+ skipStyles?: boolean;
8
+ }
9
+
10
+ // CSS Variables template for shadcn theming
11
+ const CSS_VARIABLES_TEMPLATE = `/* ng-cn/core - shadcn-angular styles */
12
+ @use "tailwindcss";
13
+
14
+ @custom-variant dark (&:is(.dark *));
15
+
16
+ :root {
17
+ --radius: 0.625rem;
18
+ --background: oklch(1 0 0);
19
+ --foreground: oklch(0.145 0 0);
20
+ --card: oklch(1 0 0);
21
+ --card-foreground: oklch(0.145 0 0);
22
+ --popover: oklch(1 0 0);
23
+ --popover-foreground: oklch(0.145 0 0);
24
+ --primary: oklch(0.205 0 0);
25
+ --primary-foreground: oklch(0.985 0 0);
26
+ --secondary: oklch(0.97 0 0);
27
+ --secondary-foreground: oklch(0.205 0 0);
28
+ --muted: oklch(0.97 0 0);
29
+ --muted-foreground: oklch(0.556 0 0);
30
+ --accent: oklch(0.97 0 0);
31
+ --accent-foreground: oklch(0.205 0 0);
32
+ --destructive: oklch(0.577 0.245 27.325);
33
+ --destructive-foreground: oklch(0.577 0.245 27.325);
34
+ --border: oklch(0.922 0 0);
35
+ --input: oklch(0.922 0 0);
36
+ --ring: oklch(0.708 0 0);
37
+ --chart-1: oklch(0.646 0.222 41.116);
38
+ --chart-2: oklch(0.6 0.118 184.704);
39
+ --chart-3: oklch(0.398 0.07 227.392);
40
+ --chart-4: oklch(0.828 0.189 84.429);
41
+ --chart-5: oklch(0.769 0.188 70.08);
42
+ --sidebar: oklch(0.985 0 0);
43
+ --sidebar-foreground: oklch(0.145 0 0);
44
+ --sidebar-primary: oklch(0.205 0 0);
45
+ --sidebar-primary-foreground: oklch(0.985 0 0);
46
+ --sidebar-accent: oklch(0.97 0 0);
47
+ --sidebar-accent-foreground: oklch(0.205 0 0);
48
+ --sidebar-border: oklch(0.922 0 0);
49
+ --sidebar-ring: oklch(0.708 0 0);
50
+ }
51
+
52
+ .dark {
53
+ --background: oklch(0.145 0 0);
54
+ --foreground: oklch(0.985 0 0);
55
+ --card: oklch(0.205 0 0);
56
+ --card-foreground: oklch(0.985 0 0);
57
+ --popover: oklch(0.205 0 0);
58
+ --popover-foreground: oklch(0.985 0 0);
59
+ --primary: oklch(0.985 0 0);
60
+ --primary-foreground: oklch(0.205 0 0);
61
+ --secondary: oklch(0.269 0 0);
62
+ --secondary-foreground: oklch(0.985 0 0);
63
+ --muted: oklch(0.269 0 0);
64
+ --muted-foreground: oklch(0.708 0 0);
65
+ --accent: oklch(0.269 0 0);
66
+ --accent-foreground: oklch(0.985 0 0);
67
+ --destructive: oklch(0.396 0.141 25.723);
68
+ --destructive-foreground: oklch(0.637 0.237 25.331);
69
+ --border: oklch(0.269 0 0);
70
+ --input: oklch(0.269 0 0);
71
+ --ring: oklch(0.439 0 0);
72
+ --chart-1: oklch(0.488 0.243 264.376);
73
+ --chart-2: oklch(0.696 0.17 162.48);
74
+ --chart-3: oklch(0.769 0.188 70.08);
75
+ --chart-4: oklch(0.627 0.265 303.9);
76
+ --chart-5: oklch(0.645 0.246 16.439);
77
+ --sidebar: oklch(0.205 0 0);
78
+ --sidebar-foreground: oklch(0.985 0 0);
79
+ --sidebar-primary: oklch(0.488 0.243 264.376);
80
+ --sidebar-primary-foreground: oklch(0.985 0 0);
81
+ --sidebar-accent: oklch(0.269 0 0);
82
+ --sidebar-accent-foreground: oklch(0.985 0 0);
83
+ --sidebar-border: oklch(0.269 0 0);
84
+ --sidebar-ring: oklch(0.439 0 0);
85
+ }
86
+
87
+ @theme inline {
88
+ --color-background: var(--background);
89
+ --color-foreground: var(--foreground);
90
+ --color-card: var(--card);
91
+ --color-card-foreground: var(--card-foreground);
92
+ --color-popover: var(--popover);
93
+ --color-popover-foreground: var(--popover-foreground);
94
+ --color-primary: var(--primary);
95
+ --color-primary-foreground: var(--primary-foreground);
96
+ --color-secondary: var(--secondary);
97
+ --color-secondary-foreground: var(--secondary-foreground);
98
+ --color-muted: var(--muted);
99
+ --color-muted-foreground: var(--muted-foreground);
100
+ --color-accent: var(--accent);
101
+ --color-accent-foreground: var(--accent-foreground);
102
+ --color-destructive: var(--destructive);
103
+ --color-destructive-foreground: var(--destructive-foreground);
104
+ --color-border: var(--border);
105
+ --color-input: var(--input);
106
+ --color-ring: var(--ring);
107
+ --color-chart-1: var(--chart-1);
108
+ --color-chart-2: var(--chart-2);
109
+ --color-chart-3: var(--chart-3);
110
+ --color-chart-4: var(--chart-4);
111
+ --color-chart-5: var(--chart-5);
112
+ --color-sidebar: var(--sidebar);
113
+ --color-sidebar-foreground: var(--sidebar-foreground);
114
+ --color-sidebar-primary: var(--sidebar-primary);
115
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
116
+ --color-sidebar-accent: var(--sidebar-accent);
117
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
118
+ --color-sidebar-border: var(--sidebar-border);
119
+ --color-sidebar-ring: var(--sidebar-ring);
120
+ --radius-sm: calc(var(--radius) - 4px);
121
+ --radius-md: calc(var(--radius) - 2px);
122
+ --radius-lg: var(--radius);
123
+ --radius-xl: calc(var(--radius) + 4px);
124
+ }
125
+
126
+ @layer base {
127
+ * { @apply border-border outline-ring/50; }
128
+ body { @apply bg-background text-foreground; font-feature-settings: "rlig" 1, "calt" 1; }
129
+ html { scroll-behavior: smooth; }
130
+ :focus-visible { @apply outline-2 outline-ring outline-offset-2; }
131
+ }
132
+
133
+ @layer utilities {
134
+ .animate-accordion-down { animation: accordion-down 0.2s ease-out; }
135
+ .animate-accordion-up { animation: accordion-up 0.2s ease-out; }
136
+ @keyframes accordion-down { from { height: 0; } to { height: var(--accordion-content-height); } }
137
+ @keyframes accordion-up { from { height: var(--accordion-content-height); } to { height: 0; } }
138
+ .animate-collapsible-down { animation: collapsible-down 0.2s ease-out; }
139
+ .animate-collapsible-up { animation: collapsible-up 0.2s ease-out; }
140
+ @keyframes collapsible-down { from { height: 0; } to { height: var(--collapsible-content-height); } }
141
+ @keyframes collapsible-up { from { height: var(--collapsible-content-height); } to { height: 0; } }
142
+ }
143
+ `;
144
+
145
+ // cn utility template
146
+ const CN_UTILITY_TEMPLATE = `import { clsx, type ClassValue } from 'clsx';
147
+ import { twMerge } from 'tailwind-merge';
148
+
149
+ /**
150
+ * Utility function to merge Tailwind CSS classes with proper conflict resolution.
151
+ * Combines clsx for conditional classes with tailwind-merge for deduplication.
152
+ *
153
+ * @example
154
+ * cn('px-4 py-2', 'px-6') // => 'py-2 px-6'
155
+ * cn('bg-red-500', condition && 'bg-blue-500') // conditional classes
156
+ */
157
+ export function cn(...inputs: ClassValue[]): string {
158
+ return twMerge(clsx(inputs));
159
+ }
160
+ `;
161
+
162
+ const UTILS_INDEX_TEMPLATE = `export { cn } from './cn';
163
+ `;
164
+
165
+ export function ngAdd(options: NgAddOptions): Rule {
166
+ return (tree: Tree, context: SchematicContext) => {
167
+ context.logger.info('');
168
+ context.logger.info('╭──────────────────────────────────────────────────╮');
169
+ context.logger.info('│ │');
170
+ context.logger.info('│ 🎨 @ng-cn/core - shadcn for Angular │');
171
+ context.logger.info('│ │');
172
+ context.logger.info('╰──────────────────────────────────────────────────╯');
173
+ context.logger.info('');
174
+
175
+ // Add dependencies to package.json
176
+ const packageJsonPath = '/package.json';
177
+ if (tree.exists(packageJsonPath)) {
178
+ const packageJson = JSON.parse(tree.read(packageJsonPath)!.toString('utf-8'));
179
+
180
+ const requiredDependencies = {
181
+ 'lucide-angular': '^0.562.0',
182
+ 'class-variance-authority': '^0.7.1',
183
+ 'clsx': '^2.1.1',
184
+ 'tailwind-merge': '^3.4.0',
185
+ '@angular/cdk': '^21.0.5',
186
+ 'tailwindcss': '^4.1.18',
187
+ '@tailwindcss/postcss': '^4.1.18'
188
+ };
189
+
190
+ // Check and add missing dependencies
191
+ let needsInstall = false;
192
+ context.logger.info('📦 Dependencies');
193
+ for (const [pkg, version] of Object.entries(requiredDependencies)) {
194
+ if (!packageJson.dependencies?.[pkg]) {
195
+ packageJson.dependencies = packageJson.dependencies || {};
196
+ packageJson.dependencies[pkg] = version;
197
+ needsInstall = true;
198
+ context.logger.info(` + ${pkg}@${version}`);
199
+ }
200
+ }
201
+
202
+ if (needsInstall) {
203
+ tree.overwrite(packageJsonPath, JSON.stringify(packageJson, null, 2));
204
+ if (!options.skipInstall) {
205
+ context.addTask(new NodePackageInstallTask());
206
+ }
207
+ } else {
208
+ context.logger.info(' ✓ All dependencies already installed');
209
+ }
210
+ }
211
+
212
+ // Create lib/utils folder structure
213
+ context.logger.info('');
214
+ context.logger.info('📁 Project Structure');
215
+
216
+ const utilsPath = '/src/app/lib/utils';
217
+ const uiPath = '/src/app/lib/components/ui';
218
+
219
+ // Create cn utility
220
+ if (!tree.exists(`${utilsPath}/cn.ts`)) {
221
+ tree.create(`${utilsPath}/cn.ts`, CN_UTILITY_TEMPLATE);
222
+ context.logger.info(` + src/app/lib/utils/cn.ts`);
223
+ } else {
224
+ context.logger.info(` ✓ utils/cn.ts exists`);
225
+ }
226
+
227
+ // Create utils index
228
+ if (!tree.exists(`${utilsPath}/index.ts`)) {
229
+ tree.create(`${utilsPath}/index.ts`, UTILS_INDEX_TEMPLATE);
230
+ context.logger.info(` + src/app/lib/utils/index.ts`);
231
+ }
232
+
233
+ // Create ui components directory
234
+ if (!tree.exists(`${uiPath}/.gitkeep`)) {
235
+ tree.create(`${uiPath}/.gitkeep`, '');
236
+ context.logger.info(` + src/app/lib/components/ui/`);
237
+ }
238
+
239
+ // Setup styles
240
+ if (!options.skipStyles) {
241
+ context.logger.info('');
242
+ context.logger.info('🎨 Styles');
243
+
244
+ const ngCnStylesPath = '/src/ng-cn.scss';
245
+ const stylesPath = '/src/styles.scss';
246
+ const stylesCssPath = '/src/styles.css';
247
+
248
+ // Create ng-cn.scss with all CSS variables
249
+ tree.create(ngCnStylesPath, CSS_VARIABLES_TEMPLATE);
250
+ context.logger.info(` + src/ng-cn.scss (theme variables)`);
251
+
252
+ // Import in styles.scss or styles.css
253
+ if (tree.exists(stylesPath)) {
254
+ const stylesContent = tree.read(stylesPath)!.toString('utf-8');
255
+ if (!stylesContent.includes('ng-cn.scss')) {
256
+ const newContent = `@import './ng-cn.scss';\n\n${stylesContent}`;
257
+ tree.overwrite(stylesPath, newContent);
258
+ context.logger.info(` ✓ Imported in styles.scss`);
259
+ }
260
+ } else if (tree.exists(stylesCssPath)) {
261
+ const stylesContent = tree.read(stylesCssPath)!.toString('utf-8');
262
+ if (!stylesContent.includes('ng-cn.scss')) {
263
+ const newContent = `@import './ng-cn.scss';\n\n${stylesContent}`;
264
+ tree.overwrite(stylesCssPath, newContent);
265
+ context.logger.info(` ✓ Imported in styles.css`);
266
+ }
267
+ }
268
+ }
269
+
270
+ // Update tsconfig paths
271
+ context.logger.info('');
272
+ context.logger.info('⚙️ TypeScript Config');
273
+
274
+ const tsconfigPath = '/tsconfig.json';
275
+ if (tree.exists(tsconfigPath)) {
276
+ const tsconfig = JSON.parse(tree.read(tsconfigPath)!.toString('utf-8'));
277
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
278
+ tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
279
+
280
+ const pathAliases: Record<string, string[]> = {
281
+ '@/*': ['src/*'],
282
+ '@/lib/*': ['src/app/lib/*'],
283
+ '@/ui/*': ['src/app/lib/components/ui/*'],
284
+ '@/utils/*': ['src/app/lib/utils/*']
285
+ };
286
+
287
+ let pathsUpdated = false;
288
+ for (const [alias, paths] of Object.entries(pathAliases)) {
289
+ if (!tsconfig.compilerOptions.paths[alias]) {
290
+ tsconfig.compilerOptions.paths[alias] = paths;
291
+ pathsUpdated = true;
292
+ }
293
+ }
294
+
295
+ if (pathsUpdated) {
296
+ tree.overwrite(tsconfigPath, JSON.stringify(tsconfig, null, 2));
297
+ context.logger.info(' ✓ Path aliases configured');
298
+ } else {
299
+ context.logger.info(' ✓ Path aliases already set');
300
+ }
301
+ }
302
+
303
+ // Success message with ASCII art banner
304
+ context.logger.info('');
305
+ context.logger.info('');
306
+ context.logger.info(' ███╗ ██╗ ██████╗ ██████╗███╗ ██╗');
307
+ context.logger.info(' ████╗ ██║██╔════╝ ██╔════╝████╗ ██║');
308
+ context.logger.info(' ██╔██╗ ██║██║ ███╗█████╗██║ ██╔██╗ ██║');
309
+ context.logger.info(' ██║╚██╗██║██║ ██║╚════╝██║ ██║╚██╗██║');
310
+ context.logger.info(' ██║ ╚████║╚██████╔╝ ╚██████╗██║ ╚████║');
311
+ context.logger.info(' ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝╚═╝ ╚═══╝');
312
+ context.logger.info('');
313
+ context.logger.info(' ✅ Setup complete! shadcn for Angular');
314
+ context.logger.info('');
315
+ context.logger.info('╭──────────────────────────────────────────────────╮');
316
+ context.logger.info('│ 🚀 Next steps: │');
317
+ context.logger.info('│ │');
318
+ context.logger.info('│ 1. Add components: │');
319
+ context.logger.info('│ ng g @ng-cn/core:c button │');
320
+ context.logger.info('│ ng g @ng-cn/core:c card │');
321
+ context.logger.info('│ │');
322
+ context.logger.info('│ 2. Or install individual packages: │');
323
+ context.logger.info('│ npm i @ng-cn/button @ng-cn/card │');
324
+ context.logger.info('│ │');
325
+ context.logger.info('│ 3. Import and use: │');
326
+ context.logger.info("│ import { Button } from '@/ui/button'; │");
327
+ context.logger.info('│ │');
328
+ context.logger.info('│ 📚 Docs: https://shadcn-angular.tigayon.com │');
329
+ context.logger.info('╰──────────────────────────────────────────────────╯');
330
+ context.logger.info('');
331
+
332
+ return tree;
333
+ };
334
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "NgCnCoreNgAddSchema",
4
+ "title": "@ng-cn/core initialization schematic",
5
+ "type": "object",
6
+ "properties": {
7
+ "project": {
8
+ "type": "string",
9
+ "description": "The name of the project to initialize.",
10
+ "$default": {
11
+ "$source": "projectName"
12
+ }
13
+ },
14
+ "skipInstall": {
15
+ "type": "boolean",
16
+ "default": false,
17
+ "description": "Skip installing dependency packages."
18
+ },
19
+ "skipStyles": {
20
+ "type": "boolean",
21
+ "default": false,
22
+ "description": "Skip creating ng-cn.scss styles file."
23
+ }
24
+ }
25
+ }