praxys-ui 1.2.8 → 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.
- package/CHANGELOG.md +41 -7
- package/dist/index.js +788 -118
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,18 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Praxys UI are documented here.
|
|
4
4
|
|
|
5
|
-
## [1.2.
|
|
5
|
+
## [1.2.6] — Feb 15, 2026
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Color Customization & Enhanced Studio**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Added comprehensive color customization to Animation Studio and Theme Customizer with random palette generation and per-color editing.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Color Scheme Customization — 10 pre-made color schemes (Ocean, Forest, Sunset, Purple, Rose Gold, Mint, Fire, Cyberpunk, Monochrome)
|
|
14
|
+
- Theme Customizer Panel — "Make it yours" section with individual color editing (Primary, Secondary, Accent, Background, Text)
|
|
15
|
+
- Random Palette Generator — 🎲 Generate harmonious color palettes with one click in both Studio and /customize
|
|
16
|
+
- Per-Color Randomization — Random button (🎲) for each individual color property
|
|
17
|
+
- Color Pickers — Visual color selection with hex/HSL text input support
|
|
10
18
|
|
|
11
19
|
### Fixed
|
|
12
20
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
- React Style Warning — Changed background to backgroundImage to avoid conflicts with backgroundClip
|
|
22
|
+
- HSL to Hex Conversion — All colors properly converted for CSS compatibility
|
|
23
|
+
|
|
24
|
+
### Improved
|
|
25
|
+
|
|
26
|
+
- Random Color Algorithm — Uses color theory (analogous + complementary hues) for harmonious palettes
|
|
27
|
+
- Color Contrast — Fixed lightness/saturation values ensure readable, professional themes
|
|
28
|
+
- Live Preview Updates — All component previews dynamically use selected color scheme
|
|
29
|
+
|
|
30
|
+
## [1.2.5] — Feb 15, 2026
|
|
31
|
+
|
|
32
|
+
**Animation Studio — Visual Animation Builder**
|
|
33
|
+
|
|
34
|
+
Introducing the Animation Studio, a killer feature that sets PraxysUI apart. Visually design animations for any component with live preview, presets, and instant code generation.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Animation Studio (/studio) — Interactive visual playground for designing component animations
|
|
39
|
+
- Live Preview Canvas — See animations play in real-time as you adjust parameters
|
|
40
|
+
- Animation Controls — Fine-tune initial state, animate state, and transitions with intuitive sliders
|
|
41
|
+
- 30+ Animation Presets — Pre-built entrance, attention, exit, and loop animations (fade, slide, bounce, scale, flip, rotate, etc.)
|
|
42
|
+
- Component Selector — Search and filter through all 69 components with category filters
|
|
43
|
+
- Code Generator — Instantly copy Framer Motion or CSS @keyframes code for your custom animations
|
|
44
|
+
- Auto-Play Mode — Automatically replay animations when changing parameters or components
|
|
45
|
+
- Category-Specific Previews — Realistic mockups for buttons, cards, text, navigation, and visual components
|
|
46
|
+
|
|
47
|
+
### Improved
|
|
48
|
+
|
|
49
|
+
- Mobile Responsive — Compact dropdown component selector on mobile, full experience preserved
|
|
50
|
+
- Navbar — Added "Studio" link for easy access
|
|
17
51
|
|
|
18
52
|
## [1.2.2] — Feb 15, 2026
|
|
19
53
|
|
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.
|
|
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
|
|
18
|
+
// ─── Component registry ─────────────────────────────────
|
|
19
19
|
const COMPONENTS_BASE_URL = "https://raw.githubusercontent.com/sushanttverma/Praxys-UI/main/app/components/ui";
|
|
20
|
-
const
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
248
|
-
spinner.succeed(`Added ${
|
|
249
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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.
|
|
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();
|