UNPKG

lightswind

Version:

A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.

1,442 lines (1,267 loc) 48.4 kB
#!/usr/bin/env node const fs = require("fs-extra"); const path = require("path"); const { execSync } = require("child_process"); const readline = require("readline"); // --- Configuration --- const COMPONENT_DEPS_MAP = require("./component-deps.json"); const PACKAGE_ROOT = path.join(__dirname, ".."); const DIST_COMPONENTS_DIR = path.join(PACKAGE_ROOT, "dist", "components"); // Source paths from the package const ALL_UI_FROM = path.join(DIST_COMPONENTS_DIR, "ui"); const LIB_FROM = path.join(DIST_COMPONENTS_DIR, "lib"); const HOOKS_FROM = path.join(PACKAGE_ROOT, "dist", "hooks"); const STYLES_FROM = path.join(PACKAGE_ROOT, "src", "styles", "lightswind.css"); // User's current working directory const USER_CWD = process.cwd(); /** * Read user's package.json */ function readUserPackageJson() { const pkgPath = path.join(USER_CWD, "package.json"); if (!fs.existsSync(pkgPath)) { console.error("❌ No package.json found in current directory"); console.error("Please run this command from your project root."); process.exit(1); } return fs.readJsonSync(pkgPath); } /** * Detect the framework being used */ function detectFramework(packageJson) { const deps = { ...(packageJson.dependencies || {}), ...(packageJson.devDependencies || {}) }; if (deps['next']) return { name: 'Next.js', type: 'nextjs' }; if (deps['vite']) return { name: 'Vite', type: 'vite' }; if (deps['react-scripts']) return { name: 'Create React App', type: 'cra' }; return { name: 'React', type: 'react' }; } /** * Detect the best components directory for this project */ function detectComponentsPath() { // Check existing directories first const possiblePaths = [ path.join(USER_CWD, 'src', 'components'), path.join(USER_CWD, 'components'), path.join(USER_CWD, 'app', 'components'), ]; // Return first existing path for (const p of possiblePaths) { if (fs.existsSync(p)) { return { components: p, lib: path.join(path.dirname(p), 'lib'), hooks: path.join(path.dirname(p), 'hooks'), styles: path.join(path.dirname(p), 'lightswind.css'), framework: null // Will be set by caller }; } } // No existing components directory, choose based on framework const packageJson = readUserPackageJson(); const framework = detectFramework(packageJson); let basePath; if (framework.type === 'nextjs') { // Check if Next.js uses src/ directory const hasSrc = fs.existsSync(path.join(USER_CWD, 'src')); basePath = hasSrc ? path.join(USER_CWD, 'src', 'components') : path.join(USER_CWD, 'components'); } else { // Vite, CRA, generic React - use src/components basePath = path.join(USER_CWD, 'src', 'components'); } return { components: basePath, lib: path.join(path.dirname(basePath), 'lib'), hooks: path.join(path.dirname(basePath), 'hooks'), styles: path.join(path.dirname(basePath), 'lightswind.css'), framework: framework }; } /** * Get all destination paths for the current project */ function getPaths() { const detected = detectComponentsPath(); const packageJson = readUserPackageJson(); const framework = detected.framework || detectFramework(packageJson); return { ALL_UI_TO: path.join(detected.components, 'lightswind'), LIB_TO: detected.lib, HOOKS_TO: detected.hooks, STYLES_TO: detected.styles, COMPONENTS_DIR: detected.components, FRAMEWORK: framework }; } /** * Get missing dependencies */ function getMissingDependencies(required, userPkg) { const installed = { ...(userPkg.dependencies || {}), ...(userPkg.devDependencies || {}) }; // Always enforce core baseline dependencies const coreDeps = ["lightswind", "clsx", "tailwind-merge", "class-variance-authority", "lucide-react"]; coreDeps.forEach(dep => { if (!installed[dep] && !required.includes(dep)) { required.push(dep); } }); return required.filter(dep => !installed[dep]); } /** * Prompt user for yes/no input */ function promptUser(question) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(question, (answer) => { rl.close(); const normalized = answer.toLowerCase().trim(); resolve(normalized === 'y' || normalized === 'yes' || normalized === ''); }); }); } /** * Install npm dependencies */ function installDependencies(deps) { if (deps.length === 0) return; console.log(`\n⏳ Installing ${deps.join(", ")}...`); const cmd = `npm install ${deps.join(" ")}`; try { execSync(cmd, { stdio: "inherit", cwd: USER_CWD }); console.log("✅ Dependencies installed successfully\n"); } catch (error) { console.error("❌ Failed to install dependencies"); console.error("You can install them manually:", deps.join(" ")); } } /** * Copy shared utilities (lib, hooks, styles) */ async function copySharedUtils() { const paths = getPaths(); let utilsInstalled = false; let isNewStylesFile = false; // Preserve existing theme before overwriting styles file let existingPrimaryLw = null; let existingPrimaryLw2 = null; let existingDarkPrimaryLw = null; let existingDarkPrimaryLw2 = null; if (fs.existsSync(paths.STYLES_TO)) { try { const existingContent = fs.readFileSync(paths.STYLES_TO, 'utf8'); // Extract from :root const rootMatch = existingContent.match(/:root\s*{([^}]*)}/); if (rootMatch) { const rootContent = rootMatch[1]; const p1 = rootContent.match(/--primarylw:\s*(#[0-9a-fA-F]+);/); const p2 = rootContent.match(/--primarylw-2:\s*(#[0-9a-fA-F]+);/); if (p1) existingPrimaryLw = p1[1]; if (p2) existingPrimaryLw2 = p2[1]; } // Extract from .dark const darkMatch = existingContent.match(/\.dark\s*{([^}]*)}/); if (darkMatch) { const darkContent = darkMatch[1]; const dp1 = darkContent.match(/--primarylw:\s*(#[0-9a-fA-F]+);/); const dp2 = darkContent.match(/--primarylw-2:\s*(#[0-9a-fA-F]+);/); if (dp1) existingDarkPrimaryLw = dp1[1]; if (dp2) existingDarkPrimaryLw2 = dp2[1]; } } catch(e) {} } else { isNewStylesFile = true; } // Copy lib folder if (fs.existsSync(LIB_FROM)) { fs.ensureDirSync(paths.LIB_TO); fs.copySync(LIB_FROM, paths.LIB_TO, { overwrite: true }); utilsInstalled = true; } // Copy hooks folder if (fs.existsSync(HOOKS_FROM)) { fs.ensureDirSync(paths.HOOKS_TO); fs.copySync(HOOKS_FROM, paths.HOOKS_TO, { overwrite: true }); utilsInstalled = true; } // Copy styles file if (fs.existsSync(STYLES_FROM)) { fs.ensureDirSync(path.dirname(paths.STYLES_TO)); fs.copySync(STYLES_FROM, paths.STYLES_TO, { overwrite: true }); utilsInstalled = true; // Restore existing theme if it wasn't a new file if (!isNewStylesFile && existingPrimaryLw) { let content = fs.readFileSync(paths.STYLES_TO, 'utf8'); content = content.replace(/(:root\s*{[^}]*--primarylw:\s*)#[0-9a-fA-F]+;/g, `$1${existingPrimaryLw};`); if (existingPrimaryLw2) { content = content.replace(/(:root\s*{[^}]*--primarylw-2:\s*)#[0-9a-fA-F]+;/g, `$1${existingPrimaryLw2};`); } if (existingDarkPrimaryLw) { content = content.replace(/(\.dark\s*{[^}]*--primarylw:\s*)#[0-9a-fA-F]+;/g, `$1${existingDarkPrimaryLw};`); } if (existingDarkPrimaryLw2) { content = content.replace(/(\.dark\s*{[^}]*--primarylw-2:\s*)#[0-9a-fA-F]+;/g, `$1${existingDarkPrimaryLw2};`); } fs.writeFileSync(paths.STYLES_TO, content, 'utf8'); } } if (utilsInstalled) { console.log("✅ Installed shared utilities (lib, hooks, styles)"); } // If this is a new CSS file install, prompt for the theme if (isNewStylesFile && fs.existsSync(paths.STYLES_TO)) { await promptAndApplyTheme(paths.STYLES_TO); } } /** * Prompt user for initial theme configuration */ async function promptAndApplyTheme(stylesPath) { console.log("\n🎨 Choose a primary color theme for your project (You can always change this later in lightswind.css):"); console.log(" 1) Default (Blue) - Professional and clean"); console.log(" 2) Deep Ocean - Darker sleek blue/purple"); console.log(" 3) Crimson - Vibrant red/rose"); console.log(" 4) Emerald - Crisp green"); console.log(" 5) Amber - Warm orange/yellow"); console.log(" 6) Amethyst - Rich purple"); console.log(" 7) Monospace - Black & white minimal\n"); const choice = await new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question("Select a theme (1-7) [Default: 1]: ", (answer) => { rl.close(); resolve(answer.trim() || '1'); }); }); const themes = { '1': { name: 'Default', primarylw: '#173eff', primarylw2: '#3758f9' }, '2': { name: 'Deep Ocean', primarylw: '#0ea5e9', primarylw2: '#38bdf8' }, '3': { name: 'Crimson', primarylw: '#e11d48', primarylw2: '#fb7185' }, '4': { name: 'Emerald', primarylw: '#10b981', primarylw2: '#34d399' }, '5': { name: 'Amber', primarylw: '#f59e0b', primarylw2: '#fbbf24' }, '6': { name: 'Amethyst', primarylw: '#8b5cf6', primarylw2: '#a78bfa' }, '7': { name: 'Monospace', primarylw: '#000000', primarylw2: '#333333' } }; const selectedTheme = themes[choice] || themes['1']; console.log(`\n✨ Applying ${selectedTheme.name} theme...`); let content = fs.readFileSync(stylesPath, 'utf8'); // Replace in :root and .dark blocks content = content.replace(/(:root\s*{[^}]*--primarylw:\s*)#[0-9a-fA-F]+;/g, `$1${selectedTheme.primarylw};`); content = content.replace(/(:root\s*{[^}]*--primarylw-2:\s*)#[0-9a-fA-F]+;/g, `$1${selectedTheme.primarylw2};`); if (choice === '7') { // Monospace specific: dark mode should be white content = content.replace(/(\.dark\s*{[^}]*--primarylw:\s*)#[0-9a-fA-F]+;/g, `$1#ffffff;`); content = content.replace(/(\.dark\s*{[^}]*--primarylw-2:\s*)#[0-9a-fA-F]+;/g, `$1#cccccc;`); } else { // Normal dark mode same as light mode content = content.replace(/(\.dark\s*{[^}]*--primarylw:\s*)#[0-9a-fA-F]+;/g, `$1${selectedTheme.primarylw};`); content = content.replace(/(\.dark\s*{[^}]*--primarylw-2:\s*)#[0-9a-fA-F]+;/g, `$1${selectedTheme.primarylw2};`); } fs.writeFileSync(stylesPath, content, 'utf8'); console.log(`✅ ${selectedTheme.name} theme applied successfully.`); } /** * Rewrite imports in a copied component file to use correct relative paths. * All dist components use @/ canonical imports. This function converts them * to relative paths that work in any framework (Next.js, Vite, CRA, etc.) * * Install path: src/components/lightswind/<component>.tsx * lib path: src/lib/utils.ts * Relative: ../../lib/utils (two levels up from lightswind/ to src/) */ function rewriteImports(filePath) { if (!fs.existsSync(filePath)) return; try { let content = fs.readFileSync(filePath, 'utf8'); let changed = false; // @/lib/utils → ../../lib/utils // (components at src/components/lightswind/ → lib at src/lib/) if (content.includes('@/lib/utils')) { content = content.replace(/from\s+["']@\/lib\/utils["']/g, 'from "../../lib/utils"'); changed = true; } // @/components/lib/utils → ../../lib/utils if (content.includes('@/components/lib/utils')) { content = content.replace(/from\s+["']@\/components\/lib\/utils["']/g, 'from "../../lib/utils"'); changed = true; } // @/lib/hooks → ../../lib/hooks if (content.includes('@/lib/hooks')) { content = content.replace(/from\s+["']@\/lib\/hooks["']/g, 'from "../../lib/hooks"'); changed = true; } // @/hooks/<hook> → ../../hooks/<hook> if (content.includes('@/hooks/')) { content = content.replace(/from\s+["']@\/hooks\/([^"']+)["']/g, 'from "../../hooks/$1"'); changed = true; } // @/components/lightswind/<comp> → ./<comp> (same folder) if (content.includes('@/components/lightswind/')) { content = content.replace(/from\s+["']@\/components\/lightswind\/([^"']+)["']/g, 'from "./$1"'); changed = true; } // Catch any leaked internal Next.js app paths → correct relative paths if (content.includes('@/app/component2/')) { // @/app/component2/ui/card → ./card content = content.replace(/from\s+["']@\/app\/component2\/ui\/([^"']+)["']/g, 'from "./$1"'); // @/app/component2/lib/utils → ../../lib/utils content = content.replace(/from\s+["']@\/app\/component2\/lib\/([^"']+)["']/g, 'from "../../lib/$1"'); // @/app/component2/utils → ../../lib/utils content = content.replace(/from\s+["']@\/app\/component2\/utils["']/g, 'from "../../lib/utils"'); changed = true; } // Fix incorrect relative paths (../hooks → ../../hooks) if (content.includes('../hooks/')) { content = content.replace(/from\s+["']\.\.\/hooks\/([^"']+)["']/g, 'from "../../hooks/$1"'); changed = true; } if (content.includes('../lib/')) { content = content.replace(/from\s+["']\.\.\/lib\/([^"']+)["']/g, 'from "../../lib/$1"'); changed = true; } // Fix over-extended relative paths (../../../ → ../../) if (content.includes('../../../lib/')) { content = content.replace(/from\s+["']\.\.\/\.\.\/\.\.\/lib\/([^"']+)["']/g, 'from "../../lib/$1"'); changed = true; } if (content.includes('../../../hooks/')) { content = content.replace(/from\s+["']\.\.\/\.\.\/\.\.\/hooks\/([^"']+)["']/g, 'from "../../hooks/$1"'); changed = true; } if (changed) { fs.writeFileSync(filePath, content, 'utf8'); } } catch (e) { // Non-critical, skip silently } } /** * Copy a single component */ function copyComponent(componentName) { const paths = getPaths(); const fileName = `${componentName}.tsx`; const fromPath = path.join(ALL_UI_FROM, fileName); const toPath = path.join(paths.ALL_UI_TO, fileName); if (!fs.existsSync(fromPath)) { return false; } fs.ensureDirSync(path.dirname(toPath)); fs.copySync(fromPath, toPath, { overwrite: true }); // Rewrite any @/ or internal Next.js imports to correct relative paths rewriteImports(toPath); return true; } /** * Detect Tailwind CSS version */ function detectTailwindVersion() { try { const userPkg = readUserPackageJson(); const deps = { ...(userPkg.dependencies || {}), ...(userPkg.devDependencies || {}) }; const tailwindVersion = deps['tailwindcss']; if (!tailwindVersion) { return null; } // Extract major version number // Match first digit(s), handling ^4, ~3.0.0, 4.0.0 etc const versionMatch = tailwindVersion.match(/(\d+)/); if (versionMatch) { return parseInt(versionMatch[1]); } return null; } catch (error) { return null; } } /** * Find Tailwind config file */ function findTailwindConfig() { const possibleConfigs = [ 'tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs', 'tailwind.config.cjs' ]; for (const config of possibleConfigs) { const configPath = path.join(USER_CWD, config); if (fs.existsSync(configPath)) { return configPath; } } return null; } /** * Find main CSS file (for Tailwind v4) */ function findMainCSSFile() { const possiblePaths = [ 'src/app/globals.css', 'src/globals.css', 'app/globals.css', 'src/styles/globals.css', 'src/index.css', 'src/App.css', 'styles/globals.css' ]; for (const cssPath of possiblePaths) { const fullPath = path.join(USER_CWD, cssPath); if (fs.existsSync(fullPath)) { return fullPath; } } // Fallback: Search for any CSS file in src/ that might be the main one // We prioritize files that likely contain Tailwind directives const srcDir = path.join(USER_CWD, 'src'); if (fs.existsSync(srcDir)) { try { const files = fs.readdirSync(srcDir); // First pass: look for index.css, style.css, main.css const priorityNames = ['index.css', 'style.css', 'main.css', 'app.css']; for (const file of files) { if (priorityNames.includes(file.toLowerCase())) { return path.join(srcDir, file); } } // Second pass: return the first CSS file found for (const file of files) { if (file.endsWith('.css')) { return path.join(srcDir, file); } } } catch (e) { // Ignore errors accessing src directory } } return null; } /** * Configure Tailwind plugin for v3 (tailwind.config.js) */ function configureTailwindV3Plugin() { const configPath = findTailwindConfig(); if (!configPath) { console.log("\n⚠️ Tailwind config not found"); console.log("💡 Add Lightswind plugin manually to tailwind.config.js:"); console.log(" plugins: [require('lightswind/plugin')]"); return false; } try { let content = fs.readFileSync(configPath, 'utf-8'); // Check if already configured if (content.includes("lightswind/plugin")) { console.log("✅ Lightswind plugin already in tailwind.config"); return true; } // Add plugin to config if (content.includes('plugins:')) { // Plugins array exists, add to it content = content.replace( /plugins:\s*\[/, "plugins: [\n require('lightswind/plugin')," ); } else { // No plugins array, add it content = content.replace( /module\.exports\s*=\s*{/, "module.exports = {\n plugins: [require('lightswind/plugin')]," ); } fs.writeFileSync(configPath, content, 'utf-8'); console.log("✅ Added Lightswind plugin to tailwind.config"); return true; } catch (error) { console.log("\n⚠️ Could not auto-configure Tailwind plugin"); console.log("💡 Add manually to tailwind.config.js:"); console.log(" plugins: [require('lightswind/plugin')]"); return false; } } /** * Configure Tailwind plugin for v4 (CSS file) */ function configureTailwindV4Plugin() { const cssPath = findMainCSSFile(); if (!cssPath) { console.log("\n⚠️ Main CSS file not found"); console.log("💡 Add Lightswind plugin manually to your CSS file:"); console.log(" @import 'tailwindcss';"); console.log(" @plugin 'lightswind/plugin';"); return false; } try { let content = fs.readFileSync(cssPath, 'utf-8'); // Check if already configured if (content.includes("lightswind/plugin")) { console.log("✅ Lightswind plugin already in CSS"); return true; } // Add @plugin after @import 'tailwindcss' if (content.includes("@import 'tailwindcss'") || content.includes('@import "tailwindcss"')) { content = content.replace( /@import ['"]tailwindcss['"];?\s*/, "@import 'tailwindcss';\n@plugin 'lightswind/plugin';\n" ); } else { // Tailwind not imported yet, add both content = "@import 'tailwindcss';\n@plugin 'lightswind/plugin';\n\n" + content; } fs.writeFileSync(cssPath, content, 'utf-8'); console.log(`✅ Added Lightswind plugin to ${path.basename(cssPath)}`); return true; } catch (error) { console.log("\n⚠️ Could not auto-configure Tailwind plugin"); console.log("💡 Add manually to your main CSS file:"); console.log(" @plugin 'lightswind/plugin';"); return false; } } /** * Auto-configure Tailwind plugin based on version */ function autoConfigureTailwindPlugin() { const tailwindVersion = detectTailwindVersion(); if (!tailwindVersion) { console.log("\n⚠️ Tailwind CSS not found in package.json"); console.log("💡 Install Tailwind CSS first:"); console.log(" npm install -D tailwindcss"); return; } console.log(`\n🔧 Configuring Lightswind for Tailwind CSS v${tailwindVersion}...`); if (tailwindVersion >= 4) { // Tailwind v4 - add to CSS file if (!configureTailwindV4Plugin()) { appendCssFallback(); } } else { // Tailwind v3 or earlier - add to config file if (!configureTailwindV3Plugin()) { appendCssFallback(); } } } /** * Append Lightswind CSS content to main CSS file as fallback */ function appendCssFallback() { console.log("\n⚠️ Falling back to manual CSS injection..."); const cssPath = findMainCSSFile(); if (!cssPath) { console.error("❌ Could not find main CSS file to inject styles."); return; } const paths = getPaths(); // We can read from the installed location since we copied it there const stylesPath = paths.STYLES_TO; if (!fs.existsSync(stylesPath)) { console.error("❌ Could not find source styles file."); return; } try { const stylesContent = fs.readFileSync(stylesPath, 'utf-8'); let mainCssContent = fs.readFileSync(cssPath, 'utf-8'); if (mainCssContent.includes("/* lightswind.css */")) { console.log("✅ Lightswind styles already present in CSS"); return; } // Append styles mainCssContent += "\n\n/* lightswind.css */\n" + stylesContent; fs.writeFileSync(cssPath, mainCssContent, 'utf-8'); console.log(`✅ Appended Lightswind styles to ${path.basename(cssPath)}`); } catch (error) { console.error("❌ Failed to append styles:", error.message); } } /** * Install all components */ async function installAll(filter = null) { const paths = getPaths(); const groups = groupComponentsByCategory(); let targetComponents = []; let title = "all Lightswind components"; if (filter === "professional") { title = "all Professional Lightswind components"; const proCats = ["utility", "layout", "ui", "form", "navigation"]; proCats.forEach(cat => { if (groups[cat]) { targetComponents.push(...groups[cat].components); } }); } else if (filter === "animated") { title = "all Animated Lightswind components"; const animCats = ["utility", "background", "button", "3d", "cursor", "text", "components"]; animCats.forEach(cat => { if (groups[cat]) { targetComponents.push(...groups[cat].components); } }); } else { // Default: install everything in the map Object.values(groups).forEach(cat => { targetComponents.push(...cat.components); }); } // Remove duplicates targetComponents = Array.from(new Set(targetComponents)); console.log(`\n🚀 Installing ${title}...`); console.log(`📦 Detected: ${paths.FRAMEWORK.name}`); console.log(`📁 Installing to: ${paths.ALL_UI_TO}\n`); // Get all unique dependencies for the selected components const allDeps = new Set(); targetComponents.forEach(comp => { const deps = COMPONENT_DEPS_MAP[comp] || []; deps.forEach(dep => allDeps.add(dep)); }); const requiredDeps = Array.from(allDeps); const userPkg = readUserPackageJson(); const missingDeps = getMissingDependencies(requiredDeps, userPkg); // Prompt for dependencies if (missingDeps.length > 0) { console.log("📦 The following dependencies are required:"); console.log(missingDeps.map(d => ` • ${d}`).join("\n")); console.log(`\nTotal: ${missingDeps.length} packages\n`); const shouldInstall = await promptUser("Install all dependencies? (Y/n): "); if (shouldInstall) { installDependencies(missingDeps); } else { console.log("⚠️ Skipping dependency installation."); console.log("Some components may not work without their dependencies.\n"); } } // Copy selected components let installedCount = 0; if (fs.existsSync(ALL_UI_FROM)) { fs.ensureDirSync(paths.ALL_UI_TO); const visited = new Set(); for (const comp of targetComponents) { await installComponent(comp, visited, true); installedCount++; } console.log(`✅ Installed ${installedCount} components`); } else { console.error("❌ UI components source directory not found."); process.exit(1); } // Copy shared utilities await copySharedUtils(); // Auto-configure Tailwind plugin autoConfigureTailwindPlugin(); console.log(`\n🎉 Success! ${installedCount} Lightswind components installed.`); console.log("\nNext steps:"); console.log(" 1. Import components: import { Button } from '@/components/lightswind/button'"); console.log(" 2. Start building! 🚀\n"); } /** * Install a specific component */ async function installComponent(componentName, visited = new Set(), isCategoryInstall = false) { if (!componentName) { console.error("❌ Please specify a component name"); console.error("Usage: npx lightswind@latest add <component-name>"); console.error("Example: npx lightswind@latest add button\n"); process.exit(1); } // Prevent infinite loops on recursive installs if (visited.has(componentName)) return; const isRoot = visited.size === 0; visited.add(componentName); const paths = getPaths(); if (isRoot && !isCategoryInstall) { console.log(`\n📦 Installing ${componentName}...`); console.log(`📦 Detected: ${paths.FRAMEWORK.name}`); console.log(`📁 Installing to: ${paths.ALL_UI_TO}\n`); } // Check if component exists const componentFile = `${componentName}.tsx`; const fromPath = path.join(ALL_UI_FROM, componentFile); if (!fs.existsSync(fromPath)) { console.error(`❌ Component '${componentName}' not found.`); if (visited.size === 1) { console.error("\nRun 'npx lightswind@latest list' to see available components.\n"); process.exit(1); } else { return; // Optional component, e.g. imported type file } } // Parse internal relative component imports (e.g. import { Progress } from "./progress") const content = fs.readFileSync(fromPath, 'utf8'); // Match `import ... from "./xyz"` or `import ... from "@/components/lightswind/xyz"` const importRegex = /from\s+["'](?:@\/components\/lightswind\/|\.\/)([^"']+)["']/g; let match; const internalComponents = []; while ((match = importRegex.exec(content)) !== null) { const depName = match[1].replace('.tsx', ''); if (depName && depName !== 'utils' && !depName.includes('/')) { internalComponents.push(depName); } } // Get required dependencies const requiredDeps = COMPONENT_DEPS_MAP[componentName] || []; // Check missing dependencies const userPkg = readUserPackageJson(); const missingDeps = getMissingDependencies(requiredDeps, userPkg); if (missingDeps.length > 0) { if (visited.size === 1) { console.log(`📦 Requirements resolving: ${missingDeps.join(", ")}`); // Auto-install without prompting when doing recursive adds installDependencies(missingDeps); } else { installDependencies(missingDeps); } } // Copy component if (copyComponent(componentName)) { console.log(`✅ Installed ${componentName} component`); } // Install any internally imported components recursively for (const internalComp of internalComponents) { await installComponent(internalComp, visited, isCategoryInstall); } // Only run post-install configuration if this is the root component being installed and not part of a category install if (isRoot && !isCategoryInstall) { // Copy shared utilities await copySharedUtils(); // Auto-configure Tailwind plugin autoConfigureTailwindPlugin(); console.log(`\n🎉 Success! ${componentName} is ready to use.`); console.log(`\nImport it: import { ${toPascalCase(componentName)} } from '@/components/lightswind/${componentName}'\n`); } } /** * Group components by category */ function groupComponentsByCategory() { // Map of components to their categories based on component-categories.ts const COMPONENT_CATEGORY_MAP = { // Utilities "toggle-theme": "utility", "animated-copy-button": "utility", "cool-theme-toggle": "utility", "draggable-reorder-list": "utility", "expandable-search-bar": "utility", "expandable-speed-dial": "utility", "slide-to-confirm": "utility", "SpectrumLoader": "utility", "spectrum-loader": "utility", // Background "animated-wave": "background", "animated-bubble-particles": "background", "aurora-shader": "background", "beam-grid-background": "background", "fall-beam-background": "background", "grid-dot-backgrounds": "background", "gradient-background": "background", "hell-background": "background", "interactive-grid-background": "background", "particles-background": "background", "rays-background": "background", "reflect-background": "background", "smokey-background": "background", "shader-background": "background", "sparkle-particles": "background", "stripes-background": "background", "wave-background": "background", "liquid-fluid": "background", "animated-blob-background": "background", "animated-ocean-waves": "background", "aurora-background": "background", "dot-grid-background": "background", "dot-pattern": "background", "glowing-background": "background", "glowing-lights": "background", "liquid-surface": "background", // Button "border-beam": "button", "confetti-button": "button", "gradient-button": "button", "ripple-button": "button", "shine-button": "button", "trial-button": "button", "magnetic-button": "button", "gradient-btn-home": "button", // Text "aurora-text-effect": "text", "scroll-reveal": "text", "shiny-text": "text", "text-scroll-marquee": "text", "typewriter-input": "text", "typing-text": "text", "video-text": "text", "looping-words": "text", "rolling-text-3d": "text", "video-modal": "ui", // 3D Elements "3d-image-ring": "3d", "3d-image-carousel": "3d", "3d-carousel": "3d", "3d-hover-gallery": "3d", "3d-marquee": "3d", "3d-model-viewer": "3d", "3d-perspective-card": "3d", "3d-scroll-trigger": "3d", "3d-slider": "3d", "beam-circle": "3d", "chain-carousel": "3d", "plasma-globe": "3d", "scroll-carousel": "3d", "sparkle-navbar": "3d", "angled-slider": "3d", "3d-MarqueewithCustomComponents": "3d", "3d-image-gallery": "3d", "3d-image-slider": "3d", "HangingIdCard": "3d", "ThreeDImageCarousel": "3d", "ascii-wave": "3d", "infinite-drift": "3d", "infinite-webgl-scroll": "3d", "stylish-carousel": "3d", "scroll-para-3d": "3d", "scroll-trigger-carousel": "3d", // Cursor "canvas-confetti-cursor": "cursor", "particle-orbit-effect": "cursor", "smokey-cursor": "cursor", "smooth-cursor": "cursor", "SparkleCursor": "cursor", "sparkle-cursor": "cursor", "smokey-cursor-hero": "cursor", // Components (Animated) "animated-notification": "components", "bento-grid": "components", "code-hover-cards": "components", "count-up": "components", "dock": "components", "Dock": "components", "drag-order-list": "components", "dynamic-navigation": "components", "electro-border": "components", "glass-folder": "components", "globe": "components", "glowing-cards": "components", "hamburger-menu-overlay": "components", "image-reveal": "components", "image-trail-effect": "components", "interactive-card": "components", "interactive-gradient-card": "components", "iphone16-pro": "components", "lens": "components", "magic-loader": "components", "morphing-navigation": "components", "orbit-card": "components", "password-strength-indicator": "components", "scroll-list": "components", "scroll-cards": "components", "scroll-para": "components", "scroll-stack": "components", "scroll-timeline": "components", "seasonal-hover-cards": "components", "sliding-cards": "components", "sliding-logo-marquee": "components", "stack-list": "components", "team-carousel": "components", "terminal-card": "components", "top-loader": "components", "top-sticky-bar": "components", "trusted-users": "components", "ripple-loader": "components", "woofy-hover-image": "components", "connection-graph": "components", "magic-card": "components", "CinematicScroll": "components", "ScrollSnapCarouselPin": "components", "ScrollVelocityContainer": "components", "ai-prompt": "components", "animated-range-input": "components", "cool-bento-effect": "components", "dynamic-island": "components", "image-sliding-marquee": "components", "interactive-card-gallery": "components", "marquee-menu": "components", // Layout "accordion": "layout", "aspect-ratio": "layout", "resizable": "layout", "scroll-area": "layout", "separator": "layout", "tabs": "layout", // UI Elements "alert": "ui", "alert-dialog": "ui", "avatar": "ui", "badge": "ui", "button": "ui", "card": "ui", "carousel": "ui", "chart": "ui", "collapsible": "ui", "context-menu": "ui", "dialog": "ui", "drawer": "ui", "dropdown-menu": "ui", "hover-card": "ui", "popover": "ui", "progress": "ui", "sheet": "ui", "skeleton": "ui", "table": "ui", "toast": "ui", "toaster": "ui", "tooltip": "ui", // Form Controls "calendar": "form", "checkbox": "form", "command": "form", "form": "form", "input": "form", "input-otp": "form", "label": "form", "radio-group": "form", "select": "form", "slider": "form", "switch": "form", "textarea": "form", "toggle": "form", "toggle-group": "form", // Navigation "breadcrumb": "navigation", "navigation-menu": "navigation", "pagination": "navigation", "sidebar": "navigation", "menubar": "navigation", "nav-effect": "navigation" }; const groups = { utility: { name: 'Utilities', emoji: '🛠️', components: [] }, background: { name: 'Background', emoji: '🌅', components: [] }, button: { name: 'Button', emoji: '🔘', components: [] }, text: { name: 'Text', emoji: '📝', components: [] }, '3d': { name: '3D Elements', emoji: '🧊', components: [] }, cursor: { name: 'Cursor', emoji: '🖱️', components: [] }, components: { name: 'Components', emoji: '🧩', components: [] }, layout: { name: 'Layout', emoji: '📐', components: [] }, ui: { name: 'UI Elements', emoji: '🎨', components: [] }, form: { name: 'Form Controls', emoji: '📝', components: [] }, navigation: { name: 'Navigation', emoji: '🧭', components: [] }, // Keep basic/specialized as fallbacks or aliases if needed, but primarily use the new ones basic: { name: 'Basic UI', emoji: '✨', components: [] }, charts: { name: 'Chart Components', emoji: '📊', components: [] }, specialized: { name: 'Specialized Components', emoji: '🔮', components: [] } }; Object.keys(COMPONENT_DEPS_MAP).forEach((component) => { const category = COMPONENT_CATEGORY_MAP[component]; if (category && groups[category]) { groups[category].components.push(component); } else { // Fallback logic for components not in the map const deps = COMPONENT_DEPS_MAP[component]; if (deps.length === 0) { groups.basic.components.push(component); } else if (deps.includes("recharts")) { groups.charts.components.push(component); } else { groups.specialized.components.push(component); } } }); return groups; } /** * Helper to normalize and match category input, supporting aliases and meta-categories. */ function getCategoryKey(name) { if (!name) return null; const norm = name.toLowerCase().trim().replace(/[-_\s]+/g, ""); if (norm === "utility" || norm === "utilities") return "utility"; if (norm === "background" || norm === "backgrounds") return "background"; if (norm === "button" || norm === "buttons" || norm === "animatedbutton" || norm === "animatedbuttons") return "button"; if (norm === "text" || norm === "texts" || norm === "animatedtext" || norm === "animatedtexts") return "text"; if (norm === "3d" || norm === "3delements" || norm === "3d-elements" || norm === "threedelements") return "3d"; if (norm === "cursor" || norm === "cursors" || norm === "animatedcursor" || norm === "animatedcursors") return "cursor"; if (norm === "components" || norm === "component" || norm === "animatedcomponents") return "components"; if (norm === "layout" || norm === "layouts") return "layout"; if (norm === "ui" || norm === "uielements" || norm === "ui-elements" || norm === "uielement") return "ui"; if (norm === "form" || norm === "forms" || norm === "formcontrol" || norm === "formcontrols" || norm === "form-controls") return "form"; if (norm === "navigation" || norm === "navigations") return "navigation"; if (norm === "professional" || norm === "pro") return "professional"; if (norm === "animated" || norm === "animation" || norm === "animations") return "animated"; return null; } /** * Install all components from a specific category */ async function installCategory(categoryName) { const paths = getPaths(); const groups = groupComponentsByCategory(); const key = getCategoryKey(categoryName); if (!key) { console.error(`\n❌ Category '${categoryName}' not found.`); console.log("Available categories:"); console.log(" • professional (All Professional components)"); console.log(" • animated (All Animated components)"); Object.entries(groups).forEach(([k, cat]) => { if (cat.components.length > 0) { console.log(` • ${k} - ${cat.name} (${cat.components.length} components)`); } }); console.log(""); process.exit(1); } let targetComponents = []; let categoryDisplayName = ""; let emoji = "📦"; if (key === "professional") { categoryDisplayName = "Professional Components"; emoji = "💼"; const proCats = ["utility", "layout", "ui", "form", "navigation"]; proCats.forEach(cat => { if (groups[cat]) { targetComponents.push(...groups[cat].components); } }); } else if (key === "animated") { categoryDisplayName = "Animated Components"; emoji = "✨"; const animCats = ["utility", "background", "button", "3d", "cursor", "text", "components"]; animCats.forEach(cat => { if (groups[cat]) { targetComponents.push(...groups[cat].components); } }); } else { const category = groups[key]; if (category) { targetComponents = category.components; categoryDisplayName = category.name; emoji = category.emoji; } } // Remove duplicates targetComponents = Array.from(new Set(targetComponents)); if (targetComponents.length === 0) { console.error(`\n❌ No components found in category '${categoryDisplayName}'.\n`); process.exit(1); } console.log(`\n${emoji} Installing ${categoryDisplayName}...`); console.log(`📦 Detected: ${paths.FRAMEWORK.name}`); console.log(`📁 Installing to: ${paths.ALL_UI_TO}`); console.log(`📊 Total components: ${targetComponents.length}\n`); // Collect all unique dependencies from target components const allDeps = new Set(); targetComponents.forEach(comp => { const deps = COMPONENT_DEPS_MAP[comp] || []; deps.forEach(dep => allDeps.add(dep)); }); const requiredDeps = Array.from(allDeps); const userPkg = readUserPackageJson(); const missingDeps = getMissingDependencies(requiredDeps, userPkg); // Prompt for dependencies if (missingDeps.length > 0) { console.log("📦 The following dependencies are required:"); console.log(missingDeps.map(d => ` • ${d}`).join("\n")); console.log(`\nTotal: ${missingDeps.length} packages\n`); const shouldInstall = await promptUser("Install dependencies? (Y/n): "); if (shouldInstall) { installDependencies(missingDeps); } else { console.log("⚠️ Skipping dependency installation."); console.log("Some components may not work without their dependencies.\n"); } } // Copy target components let installedCount = 0; const visited = new Set(); for (const comp of targetComponents) { await installComponent(comp, visited, true); installedCount++; } // Copy shared utilities await copySharedUtils(); // Auto-configure Tailwind plugin autoConfigureTailwindPlugin(); console.log(`\n✅ Installed ${installedCount} components from ${categoryDisplayName}`); console.log(`\n🎉 Success! ${categoryDisplayName} ready to use.`); console.log(`\nComponents installed:`); console.log(targetComponents.slice(0, 8).map(c => ` • ${c}`).join("\n")); if (targetComponents.length > 8) { console.log(` ... and ${targetComponents.length - 8} more\n`); } else { console.log(""); } } /** * List all available components */ function listComponents() { console.log("\n📋 Available Lightswind Components:\n"); // Group components by dependency type const groups = { noDeps: [], lucideOnly: [], framerMotion: [], threeDLibs: [], charts: [], specialized: [] }; Object.entries(COMPONENT_DEPS_MAP).forEach(([component, deps]) => { if (deps.length === 0) { groups.noDeps.push(component); } else if (deps.includes("recharts")) { groups.charts.push(component); } else if (deps.includes("@react-three/fiber") || deps.includes("@react-three/drei")) { groups.threeDLibs.push(component); } else if (deps.includes("framer-motion") && deps.length > 1) { groups.specialized.push(component); } else if (deps.includes("framer-motion")) { groups.framerMotion.push(component); } else if (deps.length === 1 && deps[0] === "lucide-react") { groups.lucideOnly.push(component); } else { groups.specialized.push(component); } }); // Display groups if (groups.noDeps.length > 0) { console.log("✨ Basic UI (no external dependencies):"); groups.noDeps.forEach(c => console.log(` • ${c}`)); console.log(""); } if (groups.lucideOnly.length > 0) { console.log("🎯 UI Components (requires lucide-react only):"); groups.lucideOnly.forEach(c => console.log(` • ${c}`)); console.log(""); } if (groups.framerMotion.length > 0) { console.log("🎨 Animated Components (requires framer-motion):"); groups.framerMotion.forEach(c => console.log(` • ${c}`)); console.log(""); } if (groups.threeDLibs.length > 0) { console.log("🌐 3D Components (requires @react-three/fiber, @react-three/drei):"); groups.threeDLibs.forEach(c => console.log(` • ${c}`)); console.log(""); } if (groups.charts.length > 0) { console.log("📊 Chart Components (requires recharts):"); groups.charts.forEach(c => console.log(` • ${c}`)); console.log(""); } if (groups.specialized.length > 0) { console.log("🔮 Specialized Components (various dependencies):"); groups.specialized.forEach(c => console.log(` • ${c}`)); console.log(""); } const total = Object.values(groups).reduce((sum, arr) => sum + arr.length, 0); console.log(`Total: ${total} components available\n`); console.log("Usage:"); console.log(" npx lightswind@latest add <component-name>"); console.log(" npx lightswind@latest init (install all)\n"); } /** * Show help message */ function showHelp() { console.log("\n⚡ Lightswind UI CLI\n"); console.log("Usage:"); console.log(" npx lightswind@latest <command> [options]\n"); console.log("Commands:"); console.log(" init Install all components and utilities"); console.log(" init --professional, -p Install only professional components"); console.log(" init --animated, -a Install only animated components"); console.log(" add <component-name> Install a specific component"); console.log(" add --category <name> Install all components from a category or group"); console.log(" list Show all available components"); console.log(" theme Change project color theme\n"); console.log("Categories / Groups:"); console.log(" professional, animated, utility, background, button, text, 3d, cursor, components, layout, ui, form, navigation\n"); console.log("Examples:"); console.log(" npx lightswind@latest init --professional"); console.log(" npx lightswind@latest add button"); console.log(" npx lightswind@latest add --category 3d"); console.log(" npx lightswind@latest add --category professional"); console.log(" npx lightswind@latest theme"); console.log("\nLearn more: https://lightswind.com\n"); } /** * Convert kebab-case to PascalCase */ function toPascalCase(str) { return str .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); } // --- Main --- const command = process.argv[2]; const arg = process.argv[3]; (async () => { switch (command) { case "init": let initFilter = null; if (arg === "--professional" || arg === "-p") { initFilter = "professional"; } else if (arg === "--animated" || arg === "-a") { initFilter = "animated"; } await installAll(initFilter); break; case "add": // Check if it's a category installation if (arg === "--category" || arg === "-c") { const categoryName = process.argv[4]; if (!categoryName) { console.error("\n❌ Please specify a category name"); console.error("Usage: npx lightswind@latest add --category <name>"); console.error("\nAvailable categories/groups: professional, animated, utility, background, button, text, 3d, cursor, components, layout, ui, form, navigation\n"); process.exit(1); } await installCategory(categoryName); } else { await installComponent(arg); } break; case "list": listComponents(); break; case "theme": const themePaths = getPaths(); if (!fs.existsSync(themePaths.STYLES_TO)) { console.error(`\n❌ 'lightswind.css' not found at ${themePaths.STYLES_TO}`); console.error("Please run 'npx lightswind init' first to setup styles.\n"); process.exit(1); } await promptAndApplyTheme(themePaths.STYLES_TO); break; case "--help": case "-h": case "help": showHelp(); break; default: if (!command) { showHelp(); } else { console.error(`\n❌ Unknown command: ${command}\n`); showHelp(); } process.exit(1); } })();