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
JavaScript
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);
}
})();