praxys-ui 1.2.9 → 1.3.1

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.
Files changed (2) hide show
  1. package/dist/index.js +788 -118
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,10 +3,10 @@ import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import ora from "ora";
5
5
  import prompts from "prompts";
6
- import { existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
7
7
  import { join } from "path";
8
8
  import { execSync } from "child_process";
9
- const VERSION = "1.2.9";
9
+ const VERSION = "1.3.1";
10
10
  // ─── Utility file contents ──────────────────────────────
11
11
  const UTILS_CONTENT = `import { clsx, type ClassValue } from "clsx";
12
12
  import { twMerge } from "tailwind-merge";
@@ -15,81 +15,90 @@ export function cn(...inputs: ClassValue[]) {
15
15
  return twMerge(clsx(inputs));
16
16
  }
17
17
  `;
18
- // ─── Component registry (slug → raw GitHub URL) ─────────
18
+ // ─── Component registry ─────────────────────────────────
19
19
  const COMPONENTS_BASE_URL = "https://raw.githubusercontent.com/sushanttverma/Praxys-UI/main/app/components/ui";
20
- const COMPONENT_LIST = [
21
- "accordion",
22
- "alert",
23
- "animated-button",
24
- "animated-counter",
25
- "animated-hero",
26
- "animated-input",
27
- "animated-number",
28
- "animated-select",
29
- "animated-tabs",
30
- "animated-textarea",
31
- "animated-toggle",
32
- "autocomplete",
33
- "avatar-group",
34
- "badge",
35
- "breadcrumbs",
36
- "checkbox",
37
- "color-picker",
38
- "combobox",
39
- "command-menu",
40
- "creepy-button",
41
- "data-table",
42
- "date-picker",
43
- "displacement-text",
44
- "divider",
45
- "dropdown-menu",
46
- "expandable-bento-grid",
47
- "file-upload",
48
- "flip-fade-text",
49
- "flip-text",
50
- "floating-menu",
51
- "folder-preview",
52
- "glass-dock",
53
- "glow-border-card",
54
- "gradient-mesh",
55
- "image-comparison",
56
- "infinite-scroll",
57
- "interactive-book",
58
- "kbd",
59
- "light-lines",
60
- "line-hover-link",
61
- "liquid-metal",
62
- "liquid-ocean",
63
- "logo-slider",
64
- "magnetic-cursor",
65
- "masked-avatars",
66
- "modal-dialog",
67
- "morphing-text",
68
- "otp-input",
69
- "pagination",
70
- "parallax-scroll",
71
- "perspective-grid",
72
- "progress-bar",
73
- "radio-group",
74
- "rating",
75
- "reveal-loader",
76
- "sheet",
77
- "skeleton-loader",
78
- "slider",
79
- "social-flip-button",
80
- "spotlight-card",
81
- "spotlight-navbar",
82
- "staggered-grid",
83
- "stats-card",
84
- "stepper",
85
- "switch",
86
- "tag-input",
87
- "testimonials-card",
88
- "timeline",
89
- "toast-notification",
90
- "tooltip",
91
- "typewriter-text",
92
- ];
20
+ const COMPONENT_REGISTRY = {
21
+ "accordion": { title: "Accordion", description: "Smooth expand/collapse panels with animated chevron, supports single or multiple open panels.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
22
+ "alert": { title: "Alert", description: "Animated alert banner with four variants (info, success, warning, error), contextual icons, optional title, dismissible with exit animation.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
23
+ "animated-button": { title: "Animated Button", description: "A button with a shiny border sweep and text reveal effect, perfect for calls to action.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
24
+ "animated-counter": { title: "Animated Counter", description: "A number counter that animates from one value to another using spring physics, triggered when scrolled into view.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
25
+ "animated-hero": { title: "Animated Hero", description: "A reusable hero section with staggered entrance animations, pulsing radial glow, grid background, badge, and dual CTA buttons.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
26
+ "animated-input": { title: "Animated Input", description: "A floating-label text input with animated border, focus ring, optional left/right icons, error state, and three sizes.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
27
+ "animated-number": { title: "Animated Number", description: "Smoothly animates between number values with a spring transition.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
28
+ "animated-select": { title: "Animated Select", description: "An accessible custom dropdown select with animated open/close, keyboard navigation, and spring transitions.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
29
+ "animated-tabs": { title: "Animated Tabs", description: "Tab navigation with a smooth sliding indicator and crossfade content transitions.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
30
+ "animated-textarea": { title: "Animated Textarea", description: "A floating-label textarea with animated border, focus ring, character counter, auto-resize support, and error state.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
31
+ "animated-toggle": { title: "Animated Toggle", description: "A switch toggle with spring-animated knob, multiple sizes, ARIA role='switch', keyboard support, and disabled state.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
32
+ "autocomplete": { title: "Autocomplete", description: "An accessible autocomplete input with async search, debouncing, keyboard navigation, loading states, and animated dropdown.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
33
+ "avatar-group": { title: "Avatar Group", description: "Stacked avatar circles with overlap, max display count with '+N' overflow indicator, three sizes, and fallback initials.", category: "visual", dependencies: ["clsx", "tailwind-merge"] },
34
+ "badge": { title: "Badge", description: "Animated badge with multiple variants (default, success, warning, error, info), three sizes, optional icon, and removable.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
35
+ "breadcrumbs": { title: "Breadcrumbs", description: "Navigation breadcrumbs with Next.js Link integration, custom separator support, aria-label accessibility, and current page indicator.", category: "navigation", dependencies: ["clsx", "tailwind-merge", "lucide-react"] },
36
+ "checkbox": { title: "Checkbox", description: "An accessible animated checkbox with spring check-mark animation, error state, label support, and keyboard interaction.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
37
+ "color-picker": { title: "Color Picker", description: "A comprehensive color picker with HSL sliders, preset swatches, hex/RGB/HSL format toggling, and copy-to-clipboard.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
38
+ "combobox": { title: "Combobox", description: "A searchable select component with keyboard navigation, multi-select support, and animated dropdown.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
39
+ "command-menu": { title: "Command Menu", description: "A command palette with search filtering, grouped items, keyboard navigation, match highlighting, and shortcut badges.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
40
+ "creepy-button": { title: "Creepy Button", description: "A horror-inspired button with flickering background, dripping accent line, and staggered glitchy text animation on hover.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
41
+ "data-table": { title: "Data Table", description: "Sortable data table with column definitions, ascending/descending sort indicators, striped rows, and hover state.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
42
+ "date-picker": { title: "Date Picker", description: "A fully-featured date picker with calendar view, keyboard navigation, month/year selectors, and optional range selection.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
43
+ "displacement-text": { title: "3D Displacement Text", description: "Mouse-reactive 3D text with depth shadows that follows cursor movement, creating a dramatic displacement effect.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
44
+ "divider": { title: "Divider", description: "Animated divider with horizontal/vertical orientation, optional centered label, and gradient mode.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
45
+ "dropdown-menu": { title: "Dropdown Menu", description: "An animated dropdown menu with full keyboard navigation, click-outside close, divider and disabled item support.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
46
+ "expandable-bento-grid": { title: "Expandable Bento Grid", description: "A bento-style grid where clicking an item expands it into a full overlay modal with smooth layout animations.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
47
+ "file-upload": { title: "File Upload", description: "An accessible drag-and-drop file upload component with validation, progress animation, file list preview, and keyboard interaction.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
48
+ "flip-fade-text": { title: "Flip Fade Text", description: "A rotating text component that cycles through words with a 3D flip and fade transition, perfect for hero taglines.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
49
+ "flip-text": { title: "Flip Text", description: "Characters flip in with a smooth 3D rotation on mount and on hover, great for headings and titles.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
50
+ "floating-menu": { title: "Floating Menu", description: "A floating pill menu that expands into a card with GSAP-animated hamburger-to-X, text scramble effect, and staggered item reveals.", category: "navigation", dependencies: ["gsap", "clsx", "tailwind-merge"], isNew: true },
51
+ "folder-preview": { title: "Folder Preview", description: "An interactive folder component that expands to reveal a file tree with staggered entrance animations and custom icons.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
52
+ "glass-dock": { title: "Glass Dock", description: "A macOS-inspired dock with glassmorphism styling, spring-animated hover magnification, and tooltips.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
53
+ "glow-border-card": { title: "Glow Border Card", description: "A card with an animated glowing border that follows cursor movement.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
54
+ "gradient-mesh": { title: "Gradient Mesh", description: "Animated multi-color gradient mesh background with smooth transitions between color states.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
55
+ "image-comparison": { title: "Image Comparison", description: "A before/after image comparison slider with pointer-capture dragging, clip-based reveal, and an animated drag handle.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
56
+ "infinite-scroll": { title: "Infinite Scroll", description: "An Intersection Observer-based infinite scroll container with loading state, configurable threshold, and animated loader.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
57
+ "interactive-book": { title: "Interactive Book", description: "A 3D page-flip book component with AnimatePresence transitions, directional flip variants, and dot navigation.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
58
+ "kbd": { title: "Kbd", description: "Keyboard shortcut indicator with monospace font, shadow border styling, and inline usage support.", category: "text", dependencies: ["clsx", "tailwind-merge"] },
59
+ "light-lines": { title: "Light Lines", description: "Animated vertical light beams that sweep across a container, creating a dramatic visual effect.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
60
+ "line-hover-link": { title: "Line Hover Link", description: "A link with an animated underline that slides in on hover.", category: "navigation", dependencies: ["clsx", "tailwind-merge"] },
61
+ "liquid-metal": { title: "Liquid Metal", description: "A cursor-reactive surface with chrome-like liquid metal reflections that follow mouse movement.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
62
+ "liquid-ocean": { title: "Liquid Ocean", description: "Animated layered SVG waves with configurable colors, wave count, and speed for a mesmerizing ocean effect.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
63
+ "logo-slider": { title: "Logo Slider", description: "An infinite-scrolling marquee of logos or brand icons with fade edges, pause-on-hover, and bidirectional support.", category: "media", dependencies: ["clsx", "tailwind-merge"] },
64
+ "magnetic-cursor": { title: "Magnetic Cursor", description: "A wrapper that creates a magnetic pull effect, attracting elements toward the cursor with configurable strength.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
65
+ "masked-avatars": { title: "Masked Avatars", description: "A stacked avatar group with overlapping circular avatars, hover-to-pop animation, tooltips, and a +N overflow indicator.", category: "media", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
66
+ "modal-dialog": { title: "Modal Dialog", description: "An animated modal dialog with backdrop blur, spring scale transition, Escape key handling, scroll lock, and ARIA attributes.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
67
+ "morphing-text": { title: "Morphing Text", description: "Text that morphs between words using a blur crossfade effect, creating smooth character interpolation transitions.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
68
+ "otp-input": { title: "OTP Input", description: "An accessible OTP/PIN input component with auto-focus, paste support, keyboard navigation, and animated focus states.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
69
+ "pagination": { title: "Pagination", description: "Accessible pagination with animated active-page indicator, smart ellipsis, two sizes, and prev/next buttons.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
70
+ "parallax-scroll": { title: "Parallax Scroll", description: "Scroll-driven parallax layers with configurable speed multipliers for creating depth effects.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
71
+ "perspective-grid": { title: "Perspective Grid", description: "A 3D perspective grid that tilts items on hover with smooth spring animations and staggered entrance.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
72
+ "progress-bar": { title: "Progress Bar", description: "An animated progress bar with multiple sizes, optional label and value display, custom colors, and candy-stripe animation.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
73
+ "radio-group": { title: "Radio Group", description: "An accessible animated radio group with spring selection animation, horizontal/vertical layout, and keyboard navigation.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
74
+ "rating": { title: "Rating", description: "An accessible animated rating component with star icons, half-star support, custom icons, hover states, and spring animations.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
75
+ "reveal-loader": { title: "Reveal Loader", description: "A loading animation with a curtain reveal effect — shows a progress bar, then slides away to reveal content.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
76
+ "sheet": { title: "Sheet", description: "A slide-in panel overlay (drawer) with four sides, backdrop blur, scroll lock, Escape-to-close, and spring transition.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
77
+ "skeleton-loader": { title: "Skeleton Loader", description: "Animated placeholder loading states with shimmer effect, supporting text, avatar, card, and button variants.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
78
+ "slider": { title: "Slider", description: "An accessible animated slider with drag interaction, keyboard navigation, tooltip value display, and spring animations.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
79
+ "social-flip-button": { title: "Social Flip Button", description: "A button that flips to reveal social media icons on hover, with a smooth 3D rotation transition.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
80
+ "spotlight-card": { title: "Spotlight Card", description: "A card with a radial spotlight that follows the cursor, creating a flashlight reveal effect.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
81
+ "spotlight-navbar": { title: "Spotlight Navbar", description: "A horizontal navigation bar with a smooth animated spotlight background that follows the hovered item.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
82
+ "staggered-grid": { title: "Staggered Grid", description: "A grid layout where children animate in with a staggered fade-up effect as they enter the viewport.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
83
+ "stats-card": { title: "Stats Card", description: "Animated statistics card with spring-based number counter, trend indicator, optional icon, and scroll-triggered entrance.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
84
+ "stepper": { title: "Stepper", description: "A multi-step indicator with animated check icons, spring-scaled active step, and animated connector lines.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
85
+ "switch": { title: "Switch", description: "An accessible animated toggle switch with spring thumb animation, size variants, label support, and keyboard interaction.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"], isNew: true },
86
+ "tag-input": { title: "Tag Input", description: "Animated tag input with Enter/comma to add, Backspace to remove, max tags limit, and AnimatePresence transitions.", category: "buttons", dependencies: ["framer-motion", "clsx", "tailwind-merge", "lucide-react"] },
87
+ "testimonials-card": { title: "Testimonials Card", description: "An auto-rotating testimonials card with smooth crossfade transitions, avatar display, and dot navigation.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
88
+ "timeline": { title: "Timeline", description: "Alternating two-column timeline with scroll-triggered animations, connecting lines, active-state pulse rings, and optional icons.", category: "cards", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
89
+ "toast-notification": { title: "Toast Notification", description: "Stackable animated toast notifications with variants (success, error, warning, info), auto-dismiss, and manual dismiss.", category: "visual", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
90
+ "tooltip": { title: "Tooltip", description: "A tooltip with 4 positions, configurable delay, direction-aware motion animation, and arrow pointer.", category: "navigation", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
91
+ "typewriter-text": { title: "Typewriter Text", description: "An animated typing effect that cycles through strings, typing and deleting characters with a blinking cursor.", category: "text", dependencies: ["framer-motion", "clsx", "tailwind-merge"] },
92
+ };
93
+ const COMPONENT_LIST = Object.keys(COMPONENT_REGISTRY);
94
+ const CATEGORY_COLORS = {
95
+ buttons: "#E84E2D",
96
+ cards: "#3B82F6",
97
+ text: "#8B5CF6",
98
+ navigation: "#10B981",
99
+ visual: "#F59E0B",
100
+ media: "#EC4899",
101
+ };
93
102
  // ─── Helpers ─────────────────────────────────────────────
94
103
  function detectPackageManager() {
95
104
  const cwd = process.cwd();
@@ -127,6 +136,83 @@ async function fetchComponent(slug) {
127
136
  }
128
137
  return res.text();
129
138
  }
139
+ function toPascalCase(slug) {
140
+ return slug
141
+ .split("-")
142
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
143
+ .join("");
144
+ }
145
+ function loadConfig() {
146
+ const configPath = join(process.cwd(), "praxys.config.json");
147
+ if (!existsSync(configPath))
148
+ return null;
149
+ try {
150
+ const raw = readFileSync(configPath, "utf-8");
151
+ return JSON.parse(raw);
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function getComponentsDir(optDir) {
158
+ if (optDir)
159
+ return optDir;
160
+ const config = loadConfig();
161
+ return config?.componentsDir ?? "components/ui";
162
+ }
163
+ function fuzzyMatch(query, text) {
164
+ const lower = text.toLowerCase();
165
+ const q = query.toLowerCase();
166
+ // simple substring match on slug, title, or description
167
+ return lower.includes(q);
168
+ }
169
+ function truncate(str, max) {
170
+ if (str.length <= max)
171
+ return str;
172
+ return str.slice(0, max - 1) + "…";
173
+ }
174
+ function colorizeSource(source) {
175
+ const lines = source.split("\n");
176
+ return lines
177
+ .map((line) => {
178
+ // Comments
179
+ if (line.trimStart().startsWith("//"))
180
+ return chalk.dim.green(line);
181
+ if (line.trimStart().startsWith("/*") || line.trimStart().startsWith("*"))
182
+ return chalk.dim.green(line);
183
+ // Highlight keywords
184
+ let colored = line;
185
+ const keywords = [
186
+ "import",
187
+ "export",
188
+ "from",
189
+ "const",
190
+ "let",
191
+ "var",
192
+ "function",
193
+ "return",
194
+ "if",
195
+ "else",
196
+ "type",
197
+ "interface",
198
+ "default",
199
+ "async",
200
+ "await",
201
+ "new",
202
+ "class",
203
+ "extends",
204
+ "implements",
205
+ ];
206
+ for (const kw of keywords) {
207
+ const regex = new RegExp(`\\b${kw}\\b`, "g");
208
+ colored = colored.replace(regex, chalk.cyan(kw));
209
+ }
210
+ // Highlight strings
211
+ colored = colored.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, (m) => chalk.yellow(m));
212
+ return colored;
213
+ })
214
+ .join("\n");
215
+ }
130
216
  // ─── Commands ────────────────────────────────────────────
131
217
  const program = new Command();
132
218
  program
@@ -141,10 +227,8 @@ program
141
227
  console.log("");
142
228
  console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — init`));
143
229
  console.log("");
144
- // Detect package manager
145
230
  const pm = detectPackageManager();
146
231
  console.log(chalk.dim(` Package manager: ${pm}`));
147
- // Ask for component directory
148
232
  const { componentDir } = await prompts({
149
233
  type: "text",
150
234
  name: "componentDir",
@@ -165,7 +249,7 @@ program
165
249
  console.log(chalk.yellow(" Cancelled."));
166
250
  return;
167
251
  }
168
- // 1. Install dependencies
252
+ // Install dependencies
169
253
  const spinner = ora("Installing dependencies...").start();
170
254
  try {
171
255
  const deps = ["clsx", "tailwind-merge", "framer-motion"];
@@ -177,7 +261,7 @@ program
177
261
  console.log(chalk.dim(" Run manually: " +
178
262
  installCmd(pm, ["clsx", "tailwind-merge", "framer-motion"])));
179
263
  }
180
- // 2. Create utils file
264
+ // Create utils file
181
265
  const utilsSpinner = ora("Creating utility files...").start();
182
266
  try {
183
267
  const utilsPath = join(process.cwd(), utilsDir);
@@ -194,9 +278,17 @@ program
194
278
  catch {
195
279
  utilsSpinner.fail("Failed to create utils file");
196
280
  }
197
- // 3. Create component directory
281
+ // Create component directory
198
282
  const compPath = join(process.cwd(), componentDir);
199
283
  ensureDir(compPath);
284
+ // Write config file
285
+ const configPath = join(process.cwd(), "praxys.config.json");
286
+ const config = {
287
+ componentsDir: componentDir,
288
+ utilsDir: utilsDir,
289
+ };
290
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
291
+ console.log(chalk.green(" ✓ Created praxys.config.json"));
200
292
  console.log("");
201
293
  console.log(chalk.green(" ✓ Praxys UI initialized!"));
202
294
  console.log("");
@@ -205,33 +297,18 @@ program
205
297
  console.log("");
206
298
  });
207
299
  // ── add <component> ──────────────────────────────────────
208
- program
209
- .command("add")
210
- .description("Add a component to your project")
211
- .argument("<component>", "Component slug (e.g. animated-button)")
212
- .option("-d, --dir <directory>", "Component directory", "components/ui")
213
- .action(async (component, opts) => {
214
- console.log("");
215
- console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — add ${chalk.cyan(component)}`));
216
- console.log("");
217
- // Validate component name
218
- if (!COMPONENT_LIST.includes(component)) {
219
- console.log(chalk.red(` Component "${component}" not found.`));
220
- console.log("");
221
- console.log(chalk.dim(" Available components:"));
222
- COMPONENT_LIST.forEach((c) => console.log(chalk.dim(` - ${c}`)));
223
- console.log("");
224
- return;
225
- }
300
+ async function addSingleComponent(component, dir, skipExisting) {
226
301
  const spinner = ora(`Fetching ${component}...`).start();
227
302
  try {
228
303
  const source = await fetchComponent(component);
229
- // Keep source as-is; import path @/lib/utils works with standard Next.js alias
230
- const rewritten = source;
231
- const compDir = join(process.cwd(), opts.dir);
304
+ const compDir = join(process.cwd(), dir);
232
305
  ensureDir(compDir);
233
306
  const filePath = join(compDir, `${component}.tsx`);
234
307
  if (existsSync(filePath)) {
308
+ if (skipExisting) {
309
+ spinner.info(`Skipped ${component} (already exists)`);
310
+ return true;
311
+ }
235
312
  spinner.stop();
236
313
  const { overwrite } = await prompts({
237
314
  type: "confirm",
@@ -241,39 +318,632 @@ program
241
318
  });
242
319
  if (!overwrite) {
243
320
  console.log(chalk.yellow(" Skipped."));
244
- return;
321
+ return true;
245
322
  }
246
323
  }
247
- writeFileSync(filePath, rewritten, "utf-8");
248
- spinner.succeed(`Added ${opts.dir}/${component}.tsx`);
249
- console.log("");
250
- console.log(chalk.dim(` Import: ${chalk.bold(`import ${toPascalCase(component)} from '@/${opts.dir}/${component}'`)}`));
251
- console.log("");
324
+ writeFileSync(filePath, source, "utf-8");
325
+ spinner.succeed(`Added ${dir}/${component}.tsx`);
326
+ return true;
252
327
  }
253
328
  catch (err) {
254
329
  spinner.fail(`Failed to fetch ${component}`);
255
330
  console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
331
+ return false;
332
+ }
333
+ }
334
+ async function installDepsForComponents(slugs) {
335
+ // Collect unique dependencies across all given components
336
+ const allDeps = new Set();
337
+ for (const slug of slugs) {
338
+ const meta = COMPONENT_REGISTRY[slug];
339
+ if (meta) {
340
+ for (const dep of meta.dependencies) {
341
+ allDeps.add(dep);
342
+ }
343
+ }
344
+ }
345
+ if (allDeps.size === 0)
346
+ return;
347
+ const pm = detectPackageManager();
348
+ const depsArr = Array.from(allDeps);
349
+ const spinner = ora(`Installing dependencies: ${depsArr.join(", ")}...`).start();
350
+ try {
351
+ execSync(installCmd(pm, depsArr), { stdio: "pipe", cwd: process.cwd() });
352
+ spinner.succeed(`Installed ${depsArr.length} dependencies`);
353
+ }
354
+ catch {
355
+ spinner.fail("Failed to install dependencies");
356
+ console.log(chalk.dim(` Run manually: ${installCmd(pm, depsArr)}`));
357
+ }
358
+ }
359
+ program
360
+ .command("add")
361
+ .description("Add a component (or all components) to your project")
362
+ .argument("[component]", 'Component slug (e.g. animated-button) or "all". Omit for interactive picker.')
363
+ .option("-d, --dir <directory>", "Component directory")
364
+ .option("-y, --yes", "Skip overwrite prompts (skip existing files)", false)
365
+ .option("--install-deps", "Install component dependencies after adding", false)
366
+ .action(async (component, opts) => {
367
+ const dir = getComponentsDir(opts.dir);
368
+ // ── interactive picker when no arg ───────────────────
369
+ if (!component) {
370
+ console.log("");
371
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — add components`));
372
+ console.log("");
373
+ const categoryOrder = ["buttons", "cards", "text", "navigation", "visual", "media"];
374
+ const choices = [];
375
+ for (const cat of categoryOrder) {
376
+ const color = CATEGORY_COLORS[cat] || "#FFFFFF";
377
+ // Category separator
378
+ choices.push({
379
+ title: chalk.hex(color).bold(`── ${cat.toUpperCase()} ──`),
380
+ value: `__sep_${cat}`,
381
+ description: "",
382
+ });
383
+ for (const [slug, meta] of Object.entries(COMPONENT_REGISTRY)) {
384
+ if (meta.category !== cat)
385
+ continue;
386
+ const newBadge = meta.isNew ? chalk.green(" NEW") : "";
387
+ choices.push({
388
+ title: `${meta.title}${newBadge}`,
389
+ value: slug,
390
+ description: truncate(meta.description, 50),
391
+ });
392
+ }
393
+ }
394
+ const { selected } = await prompts({
395
+ type: "multiselect",
396
+ name: "selected",
397
+ message: "Pick components to add (space to select, enter to confirm)",
398
+ choices: choices.map((c) => ({
399
+ title: c.title,
400
+ value: c.value,
401
+ description: c.description,
402
+ disabled: c.value.startsWith("__sep_"),
403
+ })),
404
+ hint: "- Space to select. Return to submit",
405
+ });
406
+ if (!selected || selected.length === 0) {
407
+ console.log(chalk.yellow(" No components selected."));
408
+ console.log("");
409
+ return;
410
+ }
411
+ const slugs = selected.filter((s) => !s.startsWith("__sep_"));
412
+ if (slugs.length === 0) {
413
+ console.log(chalk.yellow(" No components selected."));
414
+ console.log("");
415
+ return;
416
+ }
417
+ let added = 0;
418
+ let failed = 0;
419
+ for (const slug of slugs) {
420
+ const ok = await addSingleComponent(slug, dir, opts.yes);
421
+ if (ok)
422
+ added++;
423
+ else
424
+ failed++;
425
+ }
426
+ console.log("");
427
+ console.log(chalk.green(` ✓ ${added} components added`) +
428
+ (failed > 0 ? chalk.red(`, ${failed} failed`) : ""));
429
+ if (opts.installDeps) {
430
+ await installDepsForComponents(slugs);
431
+ }
432
+ console.log("");
433
+ return;
256
434
  }
435
+ console.log("");
436
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — add ${chalk.cyan(component)}`));
437
+ console.log("");
438
+ // ── add all ──────────────────────────────────────────
439
+ if (component === "all") {
440
+ const { confirm } = await prompts({
441
+ type: "confirm",
442
+ name: "confirm",
443
+ message: `Add all ${COMPONENT_LIST.length} components to ${dir}?`,
444
+ initial: true,
445
+ });
446
+ if (!confirm) {
447
+ console.log(chalk.yellow(" Cancelled."));
448
+ return;
449
+ }
450
+ let added = 0;
451
+ let failed = 0;
452
+ for (const slug of COMPONENT_LIST) {
453
+ const ok = await addSingleComponent(slug, dir, opts.yes);
454
+ if (ok)
455
+ added++;
456
+ else
457
+ failed++;
458
+ }
459
+ console.log("");
460
+ console.log(chalk.green(` ✓ ${added} components added`) +
461
+ (failed > 0 ? chalk.red(`, ${failed} failed`) : ""));
462
+ if (opts.installDeps) {
463
+ await installDepsForComponents(COMPONENT_LIST);
464
+ }
465
+ console.log("");
466
+ return;
467
+ }
468
+ // ── add single ───────────────────────────────────────
469
+ if (!COMPONENT_LIST.includes(component)) {
470
+ console.log(chalk.red(` Component "${component}" not found.`));
471
+ console.log("");
472
+ console.log(chalk.dim(" Available components:"));
473
+ COMPONENT_LIST.forEach((c) => console.log(chalk.dim(` - ${c}`)));
474
+ console.log(chalk.dim(` - ${chalk.bold("all")} (adds every component)`));
475
+ console.log("");
476
+ return;
477
+ }
478
+ const ok = await addSingleComponent(component, dir, false);
479
+ if (ok && opts.installDeps) {
480
+ await installDepsForComponents([component]);
481
+ }
482
+ console.log("");
483
+ console.log(chalk.dim(` Import: ${chalk.bold(`import ${toPascalCase(component)} from '@/${dir}/${component}'`)}`));
484
+ console.log("");
257
485
  });
258
486
  // ── list ─────────────────────────────────────────────────
259
487
  program
260
488
  .command("list")
261
489
  .description("List all available components")
262
- .action(() => {
490
+ .option("-c, --category <category>", "Filter by category (buttons, cards, text, navigation, visual, media)")
491
+ .option("-n, --new", "Show only new components", false)
492
+ .option("-s, --search <query>", "Search components by name or description")
493
+ .action((opts) => {
263
494
  console.log("");
264
495
  console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — components`));
265
496
  console.log("");
266
- COMPONENT_LIST.forEach((c) => console.log(` ${chalk.hex("#E84E2D")("●")} ${c}`));
497
+ let entries = Object.entries(COMPONENT_REGISTRY);
498
+ // Filter by category
499
+ if (opts.category) {
500
+ const cat = opts.category.toLowerCase();
501
+ entries = entries.filter(([, meta]) => meta.category === cat);
502
+ if (entries.length === 0) {
503
+ console.log(chalk.yellow(` No components found in category "${opts.category}".`));
504
+ console.log(chalk.dim(` Categories: buttons, cards, text, navigation, visual, media`));
505
+ console.log("");
506
+ return;
507
+ }
508
+ }
509
+ // Filter by new
510
+ if (opts.new) {
511
+ entries = entries.filter(([, meta]) => meta.isNew);
512
+ }
513
+ // Filter by search
514
+ if (opts.search) {
515
+ const q = opts.search;
516
+ entries = entries.filter(([slug, meta]) => fuzzyMatch(q, slug) ||
517
+ fuzzyMatch(q, meta.title) ||
518
+ fuzzyMatch(q, meta.description));
519
+ if (entries.length === 0) {
520
+ console.log(chalk.yellow(` No components matched "${opts.search}".`));
521
+ console.log("");
522
+ return;
523
+ }
524
+ }
525
+ // Group by category
526
+ const grouped = {};
527
+ for (const entry of entries) {
528
+ const cat = entry[1].category;
529
+ if (!grouped[cat])
530
+ grouped[cat] = [];
531
+ grouped[cat].push(entry);
532
+ }
533
+ const categoryOrder = ["buttons", "cards", "text", "navigation", "visual", "media"];
534
+ let total = 0;
535
+ for (const cat of categoryOrder) {
536
+ const items = grouped[cat];
537
+ if (!items || items.length === 0)
538
+ continue;
539
+ const color = CATEGORY_COLORS[cat] || "#FFFFFF";
540
+ console.log(chalk.hex(color).bold(` ${cat.toUpperCase()}`));
541
+ for (const [slug, meta] of items) {
542
+ const newBadge = meta.isNew ? chalk.green(" NEW") : "";
543
+ const desc = chalk.dim(` — ${truncate(meta.description, 60)}`);
544
+ console.log(` ${chalk.hex(color)("●")} ${slug}${newBadge}${desc}`);
545
+ total++;
546
+ }
547
+ console.log("");
548
+ }
549
+ console.log(chalk.dim(` ${total} components shown`));
550
+ console.log("");
551
+ });
552
+ // ── info <component> ─────────────────────────────────────
553
+ program
554
+ .command("info")
555
+ .description("Show detailed information about a component")
556
+ .argument("<component>", "Component slug")
557
+ .action((component) => {
558
+ const meta = COMPONENT_REGISTRY[component];
559
+ if (!meta) {
560
+ console.log("");
561
+ console.log(chalk.red(` Component "${component}" not found.`));
562
+ console.log(chalk.dim(` Run ${chalk.bold("praxys-ui list")} to see available components.`));
563
+ console.log("");
564
+ return;
565
+ }
566
+ const catColor = CATEGORY_COLORS[meta.category] || "#FFFFFF";
267
567
  console.log("");
268
- console.log(chalk.dim(` ${COMPONENT_LIST.length} components available`));
568
+ console.log(chalk.bold(` ${meta.title}`) + (meta.isNew ? chalk.green(" NEW") : ""));
569
+ console.log(chalk.hex(catColor)(` Category: ${meta.category}`));
570
+ console.log(` Dependencies: ${chalk.cyan(meta.dependencies.join(", "))}`);
571
+ console.log(` ${chalk.dim(meta.description)}`);
572
+ console.log(chalk.dim(` Docs: https://praxysui.vercel.app/components/${component}`));
573
+ console.log("");
574
+ });
575
+ // ── view <component> ─────────────────────────────────────
576
+ program
577
+ .command("view")
578
+ .description("View the source code of a component")
579
+ .argument("<component>", "Component slug")
580
+ .action(async (component) => {
581
+ if (!COMPONENT_REGISTRY[component]) {
582
+ console.log("");
583
+ console.log(chalk.red(` Component "${component}" not found.`));
584
+ console.log(chalk.dim(` Run ${chalk.bold("praxys-ui list")} to see available components.`));
585
+ console.log("");
586
+ return;
587
+ }
588
+ const spinner = ora(`Fetching ${component} source...`).start();
589
+ try {
590
+ const source = await fetchComponent(component);
591
+ spinner.stop();
592
+ console.log("");
593
+ console.log(chalk.bold(` ${COMPONENT_REGISTRY[component].title} — source`));
594
+ console.log(chalk.dim(" ─".repeat(30)));
595
+ console.log("");
596
+ console.log(colorizeSource(source));
597
+ console.log("");
598
+ }
599
+ catch (err) {
600
+ spinner.fail(`Failed to fetch ${component}`);
601
+ console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
602
+ }
603
+ });
604
+ // ── diff <component> ─────────────────────────────────────
605
+ program
606
+ .command("diff")
607
+ .description("Compare local component with latest remote version")
608
+ .argument("<component>", "Component slug")
609
+ .option("-d, --dir <directory>", "Component directory")
610
+ .action(async (component, opts) => {
611
+ const dir = getComponentsDir(opts.dir);
612
+ if (!COMPONENT_REGISTRY[component]) {
613
+ console.log("");
614
+ console.log(chalk.red(` Component "${component}" not found.`));
615
+ console.log("");
616
+ return;
617
+ }
618
+ const localPath = join(process.cwd(), dir, `${component}.tsx`);
619
+ if (!existsSync(localPath)) {
620
+ console.log("");
621
+ console.log(chalk.yellow(` Not installed. ${component}.tsx not found in ${dir}/`));
622
+ console.log("");
623
+ return;
624
+ }
625
+ const spinner = ora(`Fetching latest ${component}...`).start();
626
+ try {
627
+ const remote = await fetchComponent(component);
628
+ const local = readFileSync(localPath, "utf-8");
629
+ spinner.stop();
630
+ if (local === remote) {
631
+ console.log("");
632
+ console.log(chalk.green(` ✓ ${component} is up to date.`));
633
+ console.log("");
634
+ return;
635
+ }
636
+ console.log("");
637
+ console.log(chalk.bold(` ${component} — diff (local vs remote)`));
638
+ console.log(chalk.dim(" ─".repeat(30)));
639
+ console.log("");
640
+ const localLines = local.split("\n");
641
+ const remoteLines = remote.split("\n");
642
+ const maxLen = Math.max(localLines.length, remoteLines.length);
643
+ for (let i = 0; i < maxLen; i++) {
644
+ const localLine = localLines[i];
645
+ const remoteLine = remoteLines[i];
646
+ if (localLine === remoteLine)
647
+ continue;
648
+ if (localLine !== undefined && remoteLine === undefined) {
649
+ console.log(chalk.red(` - L${i + 1}: ${localLine}`));
650
+ }
651
+ else if (localLine === undefined && remoteLine !== undefined) {
652
+ console.log(chalk.green(` + L${i + 1}: ${remoteLine}`));
653
+ }
654
+ else if (localLine !== remoteLine) {
655
+ console.log(chalk.red(` - L${i + 1}: ${localLine}`));
656
+ console.log(chalk.green(` + L${i + 1}: ${remoteLine}`));
657
+ }
658
+ }
659
+ console.log("");
660
+ }
661
+ catch (err) {
662
+ spinner.fail(`Failed to fetch ${component}`);
663
+ console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
664
+ }
665
+ });
666
+ // ── remove <component> ───────────────────────────────────
667
+ program
668
+ .command("remove")
669
+ .description("Remove a component from your project")
670
+ .argument("<component>", "Component slug")
671
+ .option("-d, --dir <directory>", "Component directory")
672
+ .option("-y, --yes", "Skip confirmation prompt", false)
673
+ .action(async (component, opts) => {
674
+ const dir = getComponentsDir(opts.dir);
675
+ if (!COMPONENT_REGISTRY[component]) {
676
+ console.log("");
677
+ console.log(chalk.red(` Component "${component}" not found.`));
678
+ console.log("");
679
+ return;
680
+ }
681
+ const filePath = join(process.cwd(), dir, `${component}.tsx`);
682
+ if (!existsSync(filePath)) {
683
+ console.log("");
684
+ console.log(chalk.yellow(` ${component}.tsx not found in ${dir}/`));
685
+ console.log("");
686
+ return;
687
+ }
688
+ if (!opts.yes) {
689
+ const { confirm } = await prompts({
690
+ type: "confirm",
691
+ name: "confirm",
692
+ message: `Remove ${dir}/${component}.tsx?`,
693
+ initial: false,
694
+ });
695
+ if (!confirm) {
696
+ console.log(chalk.yellow(" Cancelled."));
697
+ return;
698
+ }
699
+ }
700
+ try {
701
+ unlinkSync(filePath);
702
+ console.log("");
703
+ console.log(chalk.green(` ✓ Removed ${dir}/${component}.tsx`));
704
+ console.log("");
705
+ }
706
+ catch (err) {
707
+ console.log("");
708
+ console.log(chalk.red(` Failed to remove ${component}.tsx`));
709
+ console.log(chalk.dim(` ${err instanceof Error ? err.message : "Unknown error"}`));
710
+ console.log("");
711
+ }
712
+ });
713
+ // ── update [component] ───────────────────────────────────
714
+ program
715
+ .command("update")
716
+ .description("Update installed components to the latest version")
717
+ .argument("[component]", "Component slug (omit to check all)")
718
+ .option("-d, --dir <directory>", "Component directory")
719
+ .option("--check", "Only check for updates, don't write files", false)
720
+ .action(async (component, opts) => {
721
+ const dir = getComponentsDir(opts.dir);
722
+ const compDir = join(process.cwd(), dir);
723
+ console.log("");
724
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — update`));
725
+ console.log("");
726
+ // Determine which components to check
727
+ let slugsToCheck;
728
+ if (component) {
729
+ if (!COMPONENT_REGISTRY[component]) {
730
+ console.log(chalk.red(` Component "${component}" not found.`));
731
+ console.log("");
732
+ return;
733
+ }
734
+ slugsToCheck = [component];
735
+ }
736
+ else {
737
+ // Find all installed components
738
+ slugsToCheck = COMPONENT_LIST.filter((slug) => existsSync(join(compDir, `${slug}.tsx`)));
739
+ if (slugsToCheck.length === 0) {
740
+ console.log(chalk.yellow(` No installed components found in ${dir}/`));
741
+ console.log("");
742
+ return;
743
+ }
744
+ console.log(chalk.dim(` Checking ${slugsToCheck.length} installed components...\n`));
745
+ }
746
+ let updatedCount = 0;
747
+ let upToDateCount = 0;
748
+ let failedCount = 0;
749
+ for (const slug of slugsToCheck) {
750
+ const localPath = join(compDir, `${slug}.tsx`);
751
+ if (!existsSync(localPath)) {
752
+ if (component) {
753
+ console.log(chalk.yellow(` Not installed. ${slug}.tsx not found in ${dir}/`));
754
+ }
755
+ continue;
756
+ }
757
+ const spinner = ora(`Checking ${slug}...`).start();
758
+ try {
759
+ const remote = await fetchComponent(slug);
760
+ const local = readFileSync(localPath, "utf-8");
761
+ if (local === remote) {
762
+ spinner.succeed(`${slug} is up to date`);
763
+ upToDateCount++;
764
+ continue;
765
+ }
766
+ if (opts.check) {
767
+ spinner.warn(`${slug} has updates available`);
768
+ updatedCount++;
769
+ continue;
770
+ }
771
+ // Show diff summary
772
+ const localLines = local.split("\n");
773
+ const remoteLines = remote.split("\n");
774
+ let additions = 0;
775
+ let removals = 0;
776
+ const maxLen = Math.max(localLines.length, remoteLines.length);
777
+ for (let i = 0; i < maxLen; i++) {
778
+ if (localLines[i] !== remoteLines[i]) {
779
+ if (localLines[i] !== undefined)
780
+ removals++;
781
+ if (remoteLines[i] !== undefined)
782
+ additions++;
783
+ }
784
+ }
785
+ spinner.stop();
786
+ console.log(` ${slug}: ${chalk.green(`+${additions}`)} ${chalk.red(`-${removals}`)} lines changed`);
787
+ const { overwrite } = await prompts({
788
+ type: "confirm",
789
+ name: "overwrite",
790
+ message: ` Update ${slug}?`,
791
+ initial: true,
792
+ });
793
+ if (overwrite) {
794
+ writeFileSync(localPath, remote, "utf-8");
795
+ console.log(chalk.green(` ✓ Updated ${slug}`));
796
+ updatedCount++;
797
+ }
798
+ else {
799
+ console.log(chalk.yellow(` Skipped ${slug}`));
800
+ }
801
+ }
802
+ catch {
803
+ spinner.fail(`Failed to check ${slug}`);
804
+ failedCount++;
805
+ }
806
+ }
807
+ console.log("");
808
+ if (opts.check) {
809
+ console.log(chalk.dim(` ${upToDateCount} up to date, ${updatedCount} with updates available`) +
810
+ (failedCount > 0 ? chalk.red(`, ${failedCount} failed`) : ""));
811
+ }
812
+ else {
813
+ console.log(chalk.dim(` ${updatedCount} updated, ${upToDateCount} up to date`) +
814
+ (failedCount > 0 ? chalk.red(`, ${failedCount} failed`) : ""));
815
+ }
816
+ console.log("");
817
+ });
818
+ // ── doctor ───────────────────────────────────────────────
819
+ program
820
+ .command("doctor")
821
+ .description("Check your Praxys UI project setup for issues")
822
+ .action(() => {
823
+ console.log("");
824
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — doctor`));
825
+ console.log("");
826
+ let issues = 0;
827
+ // 1. Config file
828
+ const config = loadConfig();
829
+ if (config) {
830
+ console.log(chalk.green(" ✓ praxys.config.json found"));
831
+ }
832
+ else {
833
+ console.log(chalk.yellow(" ✗ praxys.config.json not found"));
834
+ console.log(chalk.dim(" Run `praxys-ui init` to create it."));
835
+ issues++;
836
+ }
837
+ // 2. Components directory
838
+ const compDir = config?.componentsDir ?? "components/ui";
839
+ const compPath = join(process.cwd(), compDir);
840
+ if (existsSync(compPath)) {
841
+ console.log(chalk.green(` ✓ Components directory exists (${compDir})`));
842
+ }
843
+ else {
844
+ console.log(chalk.yellow(` ✗ Components directory missing (${compDir})`));
845
+ issues++;
846
+ }
847
+ // 3. Utils file
848
+ const utilsDir = config?.utilsDir ?? "lib";
849
+ const utilsPath = join(process.cwd(), utilsDir, "utils.ts");
850
+ if (existsSync(utilsPath)) {
851
+ console.log(chalk.green(` ✓ Utils file exists (${utilsDir}/utils.ts)`));
852
+ }
853
+ else {
854
+ console.log(chalk.yellow(` ✗ Utils file missing (${utilsDir}/utils.ts)`));
855
+ issues++;
856
+ }
857
+ // 4. Package manager
858
+ const pm = detectPackageManager();
859
+ console.log(chalk.green(` ✓ Package manager: ${pm}`));
860
+ // 5. Core dependencies
861
+ const pkgJsonPath = join(process.cwd(), "package.json");
862
+ if (existsSync(pkgJsonPath)) {
863
+ try {
864
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
865
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
866
+ const coreDeps = ["clsx", "tailwind-merge", "framer-motion"];
867
+ for (const dep of coreDeps) {
868
+ if (allDeps[dep]) {
869
+ console.log(chalk.green(` ✓ ${dep} installed (${allDeps[dep]})`));
870
+ }
871
+ else {
872
+ console.log(chalk.yellow(` ✗ ${dep} not found in package.json`));
873
+ issues++;
874
+ }
875
+ }
876
+ }
877
+ catch {
878
+ console.log(chalk.yellow(" ✗ Could not parse package.json"));
879
+ issues++;
880
+ }
881
+ }
882
+ else {
883
+ console.log(chalk.yellow(" ✗ No package.json found"));
884
+ issues++;
885
+ }
886
+ // 6. Installed components count
887
+ if (existsSync(compPath)) {
888
+ const installed = COMPONENT_LIST.filter((slug) => existsSync(join(compPath, `${slug}.tsx`)));
889
+ console.log(chalk.green(` ✓ ${installed.length}/${COMPONENT_LIST.length} components installed`));
890
+ }
891
+ console.log("");
892
+ if (issues === 0) {
893
+ console.log(chalk.green.bold(" All checks passed!"));
894
+ }
895
+ else {
896
+ console.log(chalk.yellow(` ${issues} issue${issues > 1 ? "s" : ""} found.`));
897
+ }
898
+ console.log("");
899
+ });
900
+ // ── stats ────────────────────────────────────────────────
901
+ program
902
+ .command("stats")
903
+ .description("Show statistics about installed and available components")
904
+ .option("-d, --dir <directory>", "Component directory")
905
+ .action((opts) => {
906
+ const dir = getComponentsDir(opts.dir);
907
+ const compPath = join(process.cwd(), dir);
908
+ console.log("");
909
+ console.log(chalk.bold(` ${chalk.hex("#E84E2D")("Praxys UI")} — stats`));
910
+ console.log("");
911
+ const categoryOrder = ["buttons", "cards", "text", "navigation", "visual", "media"];
912
+ // Count per category
913
+ const catStats = {};
914
+ for (const cat of categoryOrder) {
915
+ catStats[cat] = { total: 0, installed: 0 };
916
+ }
917
+ let totalInstalled = 0;
918
+ for (const [slug, meta] of Object.entries(COMPONENT_REGISTRY)) {
919
+ catStats[meta.category].total++;
920
+ if (existsSync(join(compPath, `${slug}.tsx`))) {
921
+ catStats[meta.category].installed++;
922
+ totalInstalled++;
923
+ }
924
+ }
925
+ // Header
926
+ console.log(chalk.dim(" Category Installed Available"));
927
+ console.log(chalk.dim(" " + "─".repeat(40)));
928
+ for (const cat of categoryOrder) {
929
+ const { total, installed } = catStats[cat];
930
+ const color = CATEGORY_COLORS[cat] || "#FFFFFF";
931
+ const bar = installed > 0
932
+ ? chalk.hex(color)("█".repeat(installed)) + chalk.dim("░".repeat(total - installed))
933
+ : chalk.dim("░".repeat(total));
934
+ const catLabel = cat.padEnd(16);
935
+ console.log(` ${chalk.hex(color)(catLabel)} ${String(installed).padStart(3)}/${String(total).padEnd(5)} ${bar}`);
936
+ }
937
+ console.log(chalk.dim(" " + "─".repeat(40)));
938
+ console.log(chalk.bold(` ${"Total".padEnd(16)} ${String(totalInstalled).padStart(3)}/${String(COMPONENT_LIST.length).padEnd(5)}`));
939
+ // Coverage percentage
940
+ const pct = COMPONENT_LIST.length > 0
941
+ ? Math.round((totalInstalled / COMPONENT_LIST.length) * 100)
942
+ : 0;
943
+ console.log("");
944
+ console.log(chalk.dim(` Coverage: ${pct}% of components installed`));
945
+ console.log(chalk.dim(` Directory: ${dir}`));
269
946
  console.log("");
270
947
  });
271
- // ── helpers ──────────────────────────────────────────────
272
- function toPascalCase(slug) {
273
- return slug
274
- .split("-")
275
- .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
276
- .join("");
277
- }
278
948
  // ── run ──────────────────────────────────────────────────
279
949
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxys-ui",
3
- "version": "1.2.9",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding Praxys UI components into your project",
6
6
  "bin": {