sparesdev 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +31 -0
  2. package/bin/index.js +126 -0
  3. package/core/add.js +79 -0
  4. package/core/doctor.js +34 -0
  5. package/core/frontend.js +57 -0
  6. package/core/init.js +109 -0
  7. package/core/list.js +108 -0
  8. package/package.json +23 -0
  9. package/templates/typescript/components/Accordion/Accordion.tsx +17 -0
  10. package/templates/typescript/components/Accordion/AccordionButton.tsx +52 -0
  11. package/templates/typescript/components/Accordion/AccordionContent.tsx +15 -0
  12. package/templates/typescript/components/Accordion/AccordionItem.tsx +17 -0
  13. package/templates/typescript/components/Accordion/index.ts +4 -0
  14. package/templates/typescript/components/Avatar/Avatar.tsx +34 -0
  15. package/templates/typescript/components/Avatar/AvatarFallback.tsx +35 -0
  16. package/templates/typescript/components/Avatar/AvatarImage.tsx +23 -0
  17. package/templates/typescript/components/Avatar/index.ts +3 -0
  18. package/templates/typescript/components/Backdrop/Backdrop.tsx +19 -0
  19. package/templates/typescript/components/Backdrop/index.ts +1 -0
  20. package/templates/typescript/components/Badge/Badge.tsx +63 -0
  21. package/templates/typescript/components/Badge/index.ts +1 -0
  22. package/templates/typescript/components/Breadcrumb/Breadcrumb.tsx +35 -0
  23. package/templates/typescript/components/Breadcrumb/BreadcrumbItem.tsx +18 -0
  24. package/templates/typescript/components/Breadcrumb/BreadcrumbLink.tsx +21 -0
  25. package/templates/typescript/components/Breadcrumb/BreadcrumbList.tsx +18 -0
  26. package/templates/typescript/components/Breadcrumb/BreadcrumbPage.tsx +19 -0
  27. package/templates/typescript/components/Breadcrumb/BreadcrumbSeparator.tsx +25 -0
  28. package/templates/typescript/components/Breadcrumb/index.ts +6 -0
  29. package/templates/typescript/components/Button/Button.tsx +49 -0
  30. package/templates/typescript/components/Button/index.ts +1 -0
  31. package/templates/typescript/frontend/src/components/Avatar/Avatar.tsx +34 -0
  32. package/templates/typescript/frontend/src/components/Avatar/AvatarFallback.tsx +35 -0
  33. package/templates/typescript/frontend/src/components/Avatar/AvatarImage.tsx +23 -0
  34. package/templates/typescript/frontend/src/components/Avatar/index.ts +3 -0
  35. package/templates/typescript/frontend/src/pages/Hero.tsx +8 -0
  36. package/templates/typescript/frontend/src/pages/index.ts +1 -0
  37. package/templates/typescript/layouts/Header/Header.tsx +9 -0
  38. package/templates/typescript/layouts/Header/index.ts +1 -0
  39. package/templates/typescript/pages/Hero/Hero.tsx +8 -0
  40. package/templates/typescript/pages/Hero/index.ts +1 -0
  41. package/templates/typescript/utils/cn.ts +6 -0
  42. package/utils/copy.js +42 -0
  43. package/utils/getConfig.js +19 -0
package/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ Copyright (c) 2026 Spares
2
+
3
+ All rights reserved.
4
+
5
+ This software (Spares) is provided strictly for demonstration and evaluation purposes only.
6
+
7
+ Permission is granted to access and use the Software solely to evaluate its functionality, features, and capabilities.
8
+
9
+ You may:
10
+
11
+ - Use the Software in a limited, non-production environment for evaluation
12
+ - Create internal demos for review purposes only
13
+
14
+ You may NOT, without explicit prior written permission:
15
+
16
+ - Use the Software in any production environment
17
+ - Use the Software in any commercial, business, or client project
18
+ - Build products, SaaS applications, or services using the Software
19
+ - Copy, modify for redistribution, sublicense, or resell the Software
20
+ - Repackage or distribute the Software in any form
21
+ - Create competing tools, libraries, or systems based on the Software
22
+
23
+ All evaluation usage must remain internal and must not be publicly distributed unless explicitly permitted.
24
+
25
+ The user is solely responsible for reviewing and testing the Software. Any use is at your own risk.
26
+
27
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
28
+
29
+ TO THE FULLEST EXTENT PERMITTED BY LAW, SPARES SHALL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM OR RELATED TO THE USE OR INABILITY TO USE THE SOFTWARE.
30
+
31
+ You agree to indemnify and hold harmless Spares from any claims, damages, or liabilities arising from your use, misuse, or violation of these terms.
package/bin/index.js ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import { add } from "../core/add.js";
3
+ import { init } from "../core/init.js";
4
+ import { list } from "../core/list.js";
5
+ import { doctor } from "../core/doctor.js";
6
+ import { frontend } from "../core/frontend.js";
7
+
8
+ const args = process.argv.slice(2);
9
+
10
+ // aliases
11
+ const layerMap = {
12
+ c: "components",
13
+ l: "layouts",
14
+ b: "blocks",
15
+ p: "pages"
16
+ };
17
+
18
+ // ------------------------
19
+ // INIT / DOCTOR / LIST
20
+ // ------------------------
21
+ if (args[0] === "init") {
22
+ const mode = args.includes("--full")
23
+ ? "full"
24
+ : "minimal";
25
+
26
+ await init(mode);
27
+ process.exit();
28
+ }
29
+
30
+ if (args[0] === "doctor") {
31
+ doctor();
32
+ process.exit();
33
+ }
34
+
35
+ if (args[0] === "list") {
36
+ list();
37
+ process.exit();
38
+ }
39
+
40
+ // ------------------------
41
+ // SIMPLE ADD
42
+ // sparesdev add button
43
+ // sparesdev add frontend
44
+ // ------------------------
45
+ if (args[0] === "add") {
46
+ const item = args[1];
47
+
48
+ if (!item) {
49
+ console.log("❌ Provide a component, block, page, or frontend");
50
+ process.exit(1);
51
+ }
52
+
53
+ // frontend starter
54
+ if (item === "frontend") {
55
+ await frontend();
56
+ process.exit();
57
+ }
58
+
59
+ add("all", args.slice(1));
60
+ process.exit();
61
+ }
62
+
63
+ // ------------------------
64
+ // CATEGORY ADD
65
+ // sparesdev components add button
66
+ // ------------------------
67
+ let [layer, command, ...rest] = args;
68
+
69
+ layer = layerMap[layer] || layer;
70
+
71
+ const validLayers = [
72
+ "components",
73
+ "layouts",
74
+ "blocks",
75
+ "pages"
76
+ ];
77
+
78
+ if (validLayers.includes(layer)) {
79
+ switch (command) {
80
+ case "add":
81
+ add(layer, rest);
82
+ break;
83
+
84
+ case "list":
85
+ list(layer);
86
+ break;
87
+
88
+ default:
89
+ console.log("❌ Invalid command");
90
+ }
91
+
92
+ process.exit();
93
+ }
94
+
95
+ // ------------------------
96
+ // HELP
97
+ // ------------------------
98
+ console.log(`
99
+ sparesdev CLI
100
+
101
+ Usage:
102
+ sparesdev init
103
+ sparesdev doctor
104
+ sparesdev list
105
+
106
+ sparesdev add frontend
107
+ sparesdev add button
108
+ sparesdev add login-page
109
+
110
+ Optional category:
111
+ sparesdev components add button
112
+ sparesdev layouts add header
113
+ sparesdev blocks add hero
114
+ sparesdev pages add login-page
115
+
116
+ Commands:
117
+ init Initialize project
118
+ doctor Check setup health
119
+ list List all available templates
120
+
121
+ Aliases:
122
+ c = components
123
+ l = layouts
124
+ b = blocks
125
+ p = pages
126
+ `);
package/core/add.js ADDED
@@ -0,0 +1,79 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { copyRecursive } from "../utils/copy.js";
4
+ import { getConfig } from "../utils/getConfig.js";
5
+ import { createRequire } from "module";
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ function normalize(str) {
10
+ return str.toLowerCase().replace(/[^a-z0-9]/g, "");
11
+ }
12
+
13
+ export function add(layer, components, options = {}) {
14
+ if (!components.length) {
15
+ console.error("❌ Provide component names");
16
+ process.exit(1);
17
+ }
18
+
19
+ const config = getConfig();
20
+ const lang = "typescript";
21
+ const packageName = "sparesdev";
22
+
23
+ const packageRoot = path.dirname(
24
+ require.resolve(`${packageName}/package.json`)
25
+ );
26
+ const projectRoot = process.cwd();
27
+
28
+ const installed = [];
29
+
30
+ const searchLayers =
31
+ layer === "all"
32
+ ? ["components", "layouts", "blocks", "pages"]
33
+ : [layer];
34
+
35
+ const basePaths = searchLayers.map(section => ({
36
+ type: section,
37
+ path: path.join(packageRoot, "templates", lang, section),
38
+ target: path.join(projectRoot, "src", section)
39
+ }));
40
+
41
+ for (const nameInput of components) {
42
+ const name = nameInput;
43
+
44
+ let found = null;
45
+
46
+ for (const bp of basePaths) {
47
+ if (!fs.existsSync(bp.path)) continue;
48
+ const available = fs.readdirSync(bp.path);
49
+ const match = available.find(
50
+ item => normalize(item) === normalize(name)
51
+ );
52
+ if (match) {
53
+ found = {
54
+ match,
55
+ src: path.join(bp.path, match),
56
+ dest: path.join(bp.target, match),
57
+ type: bp.type
58
+ };
59
+ break;
60
+ }
61
+ }
62
+
63
+ if (!found) {
64
+ console.log(`❌ ${name} not found`);
65
+ continue;
66
+ }
67
+
68
+ fs.mkdirSync(path.dirname(found.dest), { recursive: true });
69
+ copyRecursive(found.src, found.dest, options);
70
+ installed.push(found.match);
71
+ console.log(`✅ ${found.match} → src/${found.type}`);
72
+ }
73
+
74
+ if (installed.length) {
75
+ console.log(`\n🎉 Installed: ${installed.join(", ")}\n`);
76
+ } else {
77
+ console.log("❌ No components installed");
78
+ }
79
+ }
package/core/doctor.js ADDED
@@ -0,0 +1,34 @@
1
+ import fs from "fs";
2
+ import { getConfig } from "../utils/getConfig.js";
3
+
4
+ export function doctor() {
5
+ console.log("\n🩺 Sparesdev Doctor:\n");
6
+
7
+ let config;
8
+
9
+ try {
10
+ config = getConfig();
11
+ console.log(`✅ Language: TypeScript`);
12
+ } catch {
13
+ console.log("❌ sparesdev.config.json missing");
14
+ return;
15
+ }
16
+
17
+ if (fs.existsSync("src")) console.log("✅ src folder");
18
+ else console.log("❌ src missing");
19
+
20
+ if (fs.existsSync("src/components")) console.log("✅ components folder");
21
+ else console.log("❌ components folder missing");
22
+
23
+ console.log("\n📦 Tailwind v4 check:\n");
24
+
25
+ if (fs.existsSync("src/index.css"))
26
+ console.log("✅ index.css");
27
+ else console.log("❌ index.css missing");
28
+
29
+ if (fs.existsSync("src/tokens.css"))
30
+ console.log("✅ tokens.css");
31
+ else console.log("❌ tokens.css missing");
32
+
33
+ console.log("\n💡 Fix issues based on selected system\n");
34
+ }
@@ -0,0 +1,57 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ function writeFile(filePath, content) {
5
+ fs.mkdirSync(path.dirname(filePath), {
6
+ recursive: true
7
+ });
8
+
9
+ fs.writeFileSync(filePath, content);
10
+ }
11
+
12
+ export async function frontend() {
13
+ const pages = {
14
+ "src/pages/LandingPage.tsx": `function LandingPage() {
15
+ return <div>Landing Page</div>;
16
+ }
17
+
18
+ export default LandingPage;
19
+ `,
20
+ "src/pages/PricingPage.tsx": `function PricingPage() {
21
+ return <div>Pricing Page</div>;
22
+ }
23
+
24
+ export default PricingPage;
25
+ `,
26
+ "src/pages/LoginPage.tsx": `function LoginPage() {
27
+ return <div>Login Page</div>;
28
+ }
29
+
30
+ export default LoginPage;
31
+ `,
32
+ "src/pages/DashboardPage.tsx": `function DashboardPage() {
33
+ return <div>Dashboard Page</div>;
34
+ }
35
+
36
+ export default DashboardPage;
37
+ `,
38
+ "src/pages/TermsPage.tsx": `function TermsPage() {
39
+ return <div>Terms Page</div>;
40
+ }
41
+
42
+ export default TermsPage;
43
+ `,
44
+ "src/pages/NotFoundPage.tsx": `function NotFoundPage() {
45
+ return <div>404 Page</div>;
46
+ }
47
+
48
+ export default NotFoundPage;
49
+ `
50
+ };
51
+
52
+ for (const [file, content] of Object.entries(pages)) {
53
+ writeFile(file, content);
54
+ }
55
+
56
+ console.log("✅ Frontend starter created");
57
+ }
package/core/init.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { copyRecursive } from "../utils/copy.js";
5
+ import { createRequire } from "module";
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ // -------------------------------
10
+ // ⚡ STEP 3 → CREATE CONFIG
11
+ // -------------------------------
12
+ export function createConfig() {
13
+ const configPath = path.join(process.cwd(), "sparesdev.config.json");
14
+
15
+ if (fs.existsSync(configPath)) {
16
+ console.log("ℹ️ Config already exists, skipping");
17
+ return;
18
+ }
19
+
20
+ const config = {
21
+ language: "typescript",
22
+ version: "3.0",
23
+ style: "default",
24
+ componentsPath: "src/components",
25
+ utilsPath: "src/utils"
26
+ };
27
+
28
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
29
+ console.log("✅ sparesdev.config.json created");
30
+ }
31
+
32
+ // -------------------------------
33
+ // ⚡ INIT FUNCTION
34
+ // -------------------------------
35
+ export async function init(mode = "minimal") {
36
+ console.log(`\n🎯 Initializing sparesdev (${mode} mode)...\n`);
37
+
38
+ // STEP 1 → create config only
39
+ createConfig();
40
+
41
+ if (mode === "minimal") {
42
+ console.log("🎉 Minimal setup completed. No dependencies or files modified.\n");
43
+ return;
44
+ }
45
+
46
+ // -------------------------------
47
+ // Full setup
48
+ // -------------------------------
49
+ console.log("🚀 Running full setup...\n");
50
+
51
+ const projectRoot = process.cwd();
52
+ const packageName = "sparesdev";
53
+ const lang = "typescript";
54
+
55
+ let packageRoot;
56
+ try {
57
+ packageRoot = path.dirname(
58
+ require.resolve(`${packageName}/package.json`)
59
+ );
60
+ } catch {
61
+ console.log("❌ sparesdev package not found. Full setup aborted.");
62
+ return;
63
+ }
64
+
65
+ // -------------------------------
66
+ // Install utils if missing
67
+ // -------------------------------
68
+ const utilsPath = path.join(packageRoot, "templates", lang, "utils");
69
+ const targetUtils = path.join(projectRoot, "src", "utils");
70
+
71
+ if (!fs.existsSync(targetUtils)) fs.mkdirSync(targetUtils, { recursive: true });
72
+
73
+ const cnPath = path.join(targetUtils, "cn.ts");
74
+ if (!fs.existsSync(cnPath)) {
75
+ copyRecursive(utilsPath, targetUtils);
76
+ console.log("✅ utils (cn.ts) added");
77
+ } else {
78
+ console.log("ℹ️ utils already exists, skipping");
79
+ }
80
+
81
+ // -------------------------------
82
+ // Install tokens.css if missing
83
+ // -------------------------------
84
+ const tokensSrc = path.join(packageRoot, "templates", lang, "tokens.css");
85
+ const tokensDest = path.join(projectRoot, "src", "tokens.css");
86
+
87
+ if (!fs.existsSync(tokensDest) && fs.existsSync(tokensSrc)) {
88
+ copyRecursive(tokensSrc, tokensDest);
89
+ console.log("✅ tokens.css added");
90
+ } else if (fs.existsSync(tokensDest)) {
91
+ console.log("ℹ️ tokens.css already exists, skipping");
92
+ }
93
+
94
+ // -------------------------------
95
+ // Patch Vite / TS config (if missing)
96
+ // -------------------------------
97
+ const viteConfig = path.join(projectRoot, "vite.config.ts");
98
+ if (fs.existsSync(viteConfig)) {
99
+ console.log("ℹ️ Vite config exists, skipping patch");
100
+ } else {
101
+ const viteTemplate = path.join(packageRoot, "templates", lang, "vite.config.ts");
102
+ if (fs.existsSync(viteTemplate)) {
103
+ copyRecursive(viteTemplate, viteConfig);
104
+ console.log("✅ Vite config added");
105
+ }
106
+ }
107
+
108
+ console.log("\n🎉 Full setup completed.\n");
109
+ }
package/core/list.js ADDED
@@ -0,0 +1,108 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { createRequire } from "module";
4
+
5
+ const require = createRequire(import.meta.url);
6
+
7
+ // -------------------------------
8
+ // 🌳 Recursive tree printer
9
+ // -------------------------------
10
+ function printTree(dir, indent = " ") {
11
+ const items = fs.readdirSync(dir);
12
+
13
+ for (const item of items) {
14
+ const fullPath = path.join(dir, item);
15
+ const isDir = fs.statSync(fullPath).isDirectory();
16
+
17
+ console.log(`${indent}- ${item}${isDir ? "/" : ""}`);
18
+
19
+ if (isDir) {
20
+ printTree(fullPath, indent + " ");
21
+ }
22
+ }
23
+ }
24
+
25
+ // -------------------------------
26
+ // 📦 LIST
27
+ // -------------------------------
28
+ export function list(layer) {
29
+ const lang = "typescript";
30
+ const packageName = "sparesdev";
31
+
32
+ let packageRoot;
33
+
34
+ try {
35
+ packageRoot = path.dirname(
36
+ require.resolve(`${packageName}/package.json`)
37
+ );
38
+ } catch {
39
+ console.log("❌ CLI package not found");
40
+ return;
41
+ }
42
+
43
+ // -------------------------------
44
+ // 📁 Sections
45
+ // -------------------------------
46
+ const sections = [
47
+ {
48
+ key: "components",
49
+ label: "Components"
50
+ },
51
+ {
52
+ key: "layouts",
53
+ label: "Layouts"
54
+ },
55
+ {
56
+ key: "blocks",
57
+ label: "Blocks"
58
+ },
59
+ {
60
+ key: "pages",
61
+ label: "Pages"
62
+ }
63
+ ];
64
+
65
+ const selectedSections = layer
66
+ ? sections.filter(section => section.key === layer)
67
+ : sections;
68
+
69
+ console.log("");
70
+
71
+ let hasAny = false;
72
+
73
+ for (const section of selectedSections) {
74
+ const dir = path.join(
75
+ packageRoot,
76
+ "templates",
77
+ lang,
78
+ section.key
79
+ );
80
+
81
+ if (!fs.existsSync(dir)) continue;
82
+
83
+ const items = fs.readdirSync(dir);
84
+
85
+ if (!items.length) continue;
86
+
87
+ hasAny = true;
88
+
89
+ console.log(`📦 ${section.label}:\n`);
90
+
91
+ for (const item of items) {
92
+ const fullPath = path.join(dir, item);
93
+ const isDir = fs.statSync(fullPath).isDirectory();
94
+
95
+ console.log(` - ${item}${isDir ? "/" : ""}`);
96
+
97
+ if (isDir) {
98
+ printTree(fullPath, " ");
99
+ }
100
+ }
101
+
102
+ console.log("");
103
+ }
104
+
105
+ if (!hasAny) {
106
+ console.log("❌ No templates found\n");
107
+ }
108
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "sparesdev",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "bin": {
7
+ "sparesdev": "bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "core",
12
+ "utils",
13
+ "templates",
14
+ "README.md"
15
+ ],
16
+ "dependencies": {
17
+ "prompts": "^2.4.2"
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=18",
21
+ "react-dom": ">=18"
22
+ }
23
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactNode } from 'react'
2
+ import { cn } from '@/utils/cn'
3
+
4
+ interface AccordionProps {
5
+ children: ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ function Accordion({children, className}: AccordionProps) {
10
+ return (
11
+ <div className={cn(className)}>
12
+ {children}
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export default Accordion
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import { cn } from '@/utils/cn'
3
+
4
+ interface AccordionButtonProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ icon?: React.ReactNode;
8
+ iconColor?: string;
9
+ isOpen?: boolean;
10
+ onClick?: () => void;
11
+ }
12
+
13
+ function AccordionButton({
14
+ children,
15
+ className,
16
+ icon,
17
+ iconColor = 'black',
18
+ isOpen = false,
19
+ onClick,
20
+ }: AccordionButtonProps) {
21
+ const defaultIcon = (
22
+ <svg
23
+ width="5.94"
24
+ height="9"
25
+ viewBox="0 0 6 9"
26
+ fill="none"
27
+ style={{
28
+ transform: isOpen ? 'rotate(270deg)' : 'rotate(90deg)',
29
+ transition: 'transform 0.3s ease',
30
+ }}
31
+ >
32
+ <path
33
+ d="M1.5 7.5L4.5 4.5L1.5 1.5"
34
+ stroke={iconColor}
35
+ strokeWidth="1"
36
+ strokeLinecap="round"
37
+ />
38
+ </svg>
39
+ );
40
+
41
+ return (
42
+ <button
43
+ onClick={onClick}
44
+ className={cn("flex flex-row items-center justify-between text-base font-normal tracking-wide", className)}
45
+ >
46
+ <span>{children}</span>
47
+ {icon || defaultIcon}
48
+ </button>
49
+ );
50
+ }
51
+
52
+ export default AccordionButton;
@@ -0,0 +1,15 @@
1
+ import React from 'react'
2
+ import { cn } from '@/utils/cn'
3
+
4
+ interface AccordionContentProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ function AccordionContent({children, className}: AccordionContentProps) {
10
+ return (
11
+ <p className={cn("text-base font-normal", className)}>{children}</p>
12
+ )
13
+ }
14
+
15
+ export default AccordionContent
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+ import { cn } from '@/utils/cn'
3
+
4
+ interface AccordionItemProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ function AccordionItem({children, className}: AccordionItemProps) {
10
+ return (
11
+ <div className={cn(className)}>
12
+ {children}
13
+ </div>
14
+ )
15
+ }
16
+
17
+ export default AccordionItem
@@ -0,0 +1,4 @@
1
+ export { default as Accordion } from './Accordion';
2
+ export { default as AccordionButton } from './AccordionButton';
3
+ export { default as AccordionContent } from './AccordionContent';
4
+ export { default as AccordionItem } from './AccordionItem';
@@ -0,0 +1,34 @@
1
+ import React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/utils/cn"
4
+
5
+ const avatarVariants = cva(
6
+ "relative flex shrink-0 overflow-hidden rounded-full items-center justify-center bg-neutral-000",
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: "w-6 h-6 text-xs font-normal",
11
+ md: "w-9 h-9 text-sm font-normal",
12
+ lg: "w-12 h-12 text-tiny font-normal",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: "md",
17
+ },
18
+ }
19
+ )
20
+
21
+ interface AvatarProps
22
+ extends React.HTMLAttributes<HTMLDivElement>,
23
+ VariantProps<typeof avatarVariants> {}
24
+
25
+ function Avatar({ className, size, ...props }: AvatarProps) {
26
+ return (
27
+ <div
28
+ className={cn(avatarVariants({ size }), className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ export default Avatar
@@ -0,0 +1,35 @@
1
+ import React from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface AvatarFallbackProps
5
+ extends React.HTMLAttributes<HTMLSpanElement> {
6
+ initials?: string
7
+ }
8
+
9
+ function AvatarFallback({
10
+ initials,
11
+ className,
12
+ children,
13
+ ...props
14
+ }: AvatarFallbackProps) {
15
+ const fallbackIcon = (
16
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
17
+ <path d="M7 0.5C8.38071 0.5 9.5 1.61929 9.5 3C9.5 4.38071 8.38071 5.5 7 5.5C5.61929 5.5 4.5 4.38071 4.5 3C4.5 1.61929 5.61929 0.5 7 0.5Z" fill="currentColor" stroke="currentColor"/>
18
+ <path d="M7 7.5C9.48528 7.5 11.5 9.51472 11.5 12V13.5H2.5V12C2.5 9.51472 4.51472 7.5 7 7.5Z" fill="currentColor" stroke="currentColor"/>
19
+ </svg>
20
+ )
21
+
22
+ return (
23
+ <span
24
+ className={cn(
25
+ "flex h-full w-full items-center justify-center",
26
+ className
27
+ )}
28
+ {...props}
29
+ >
30
+ {children ?? initials ?? fallbackIcon}
31
+ </span>
32
+ )
33
+ }
34
+
35
+ export default AvatarFallback
@@ -0,0 +1,23 @@
1
+ import React, { useState } from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface AvatarImageProps
5
+ extends React.ImgHTMLAttributes<HTMLImageElement> {
6
+ className?: "string";
7
+ }
8
+
9
+ function AvatarImage({ className, ...props }: AvatarImageProps) {
10
+ const [error, setError] = useState(false)
11
+
12
+ if (error) return null
13
+
14
+ return (
15
+ <img
16
+ className={cn("aspect-square h-full w-full object-cover", className)}
17
+ onError={() => setError(true)}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ export default AvatarImage
@@ -0,0 +1,3 @@
1
+ export { default as Avatar } from './Avatar';
2
+ export { default as AvatarFallback } from './AvatarFallback';
3
+ export { default as AvatarImage } from './AvatarImage';
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { cn } from "@/utils/cn";
3
+
4
+ interface BackdropProps {
5
+ className?: string;
6
+ }
7
+
8
+ function Backdrop({ className }: BackdropProps) {
9
+ return (
10
+ <div
11
+ className={cn(
12
+ "fixed inset-0 bg-neutral-000/70 backdrop-blur-sm z-40",
13
+ className
14
+ )}
15
+ />
16
+ );
17
+ }
18
+
19
+ export default Backdrop;
@@ -0,0 +1 @@
1
+ export { default as Backdrop } from './Backdrop';
@@ -0,0 +1,63 @@
1
+ import { cn } from "@/utils/cn"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import React from "react"
4
+
5
+ const badgeVariants = cva(
6
+ "font-normal rounded-md inline-flex items-center justify-center gap-2",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-neutral-700 text-white",
11
+ text: "bg-transparent text-neutral-900 px-0",
12
+ capsule: "rounded-full bg-neutral-700 text-white",
13
+ outline: "border border-neutral-900",
14
+ icon: "bg-neutral-900 text-white px-0",
15
+ indicator: "bg-neutral-900 rounded-full p-0",
16
+ },
17
+
18
+ size: {
19
+ sm: "h-5 px-2 text-xs",
20
+ md: "h-6 px-3 text-sm",
21
+ lg: "h-8 px-4 text-base",
22
+ },
23
+ },
24
+
25
+ compoundVariants: [
26
+ {
27
+ variant: "indicator",
28
+ size: "sm",
29
+ className: "w-1 h-1 px-0",
30
+ },
31
+ {
32
+ variant: "indicator",
33
+ size: "md",
34
+ className: "w-2 h-2 px-0",
35
+ },
36
+ {
37
+ variant: "indicator",
38
+ size: "lg",
39
+ className: "w-3 h-3 px-0",
40
+ },
41
+ ],
42
+
43
+ defaultVariants: {
44
+ variant: "default",
45
+ size: "sm",
46
+ },
47
+ }
48
+ )
49
+
50
+ interface BadgeProps extends VariantProps<typeof badgeVariants> {
51
+ children?: React.ReactNode
52
+ className?: string
53
+ }
54
+
55
+ function Badge({ children, className, variant, size }: BadgeProps) {
56
+ return (
57
+ <span className={cn(badgeVariants({ variant, size }), className)}>
58
+ {children}
59
+ </span>
60
+ )
61
+ }
62
+
63
+ export default Badge
@@ -0,0 +1 @@
1
+ export { default as Badge } from './Badge';
@@ -0,0 +1,35 @@
1
+ import React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/utils/cn"
4
+
5
+ const breadcrumbVariants = cva(
6
+ "flex items-center text-neutral-500",
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: "text-xs gap-1",
11
+ md: "text-sm gap-2",
12
+ lg: "text-base gap-3",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: "md",
17
+ },
18
+ }
19
+ )
20
+
21
+ interface BreadcrumbProps
22
+ extends React.HTMLAttributes<HTMLElement>,
23
+ VariantProps<typeof breadcrumbVariants> {}
24
+
25
+ function Breadcrumb({ className, size, ...props }: BreadcrumbProps) {
26
+ return (
27
+ <nav
28
+ aria-label="breadcrumb"
29
+ className={cn(breadcrumbVariants({ size }), className)}
30
+ {...props}
31
+ />
32
+ )
33
+ }
34
+
35
+ export default Breadcrumb
@@ -0,0 +1,18 @@
1
+ import React from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface BreadcrumbItemProps
5
+ extends React.HTMLAttributes<HTMLLIElement> {
6
+ className?: string;
7
+ }
8
+
9
+ function BreadcrumbItem({ className, ...props }: BreadcrumbItemProps) {
10
+ return (
11
+ <li
12
+ className={cn("inline-flex items-center", className)}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export default BreadcrumbItem
@@ -0,0 +1,21 @@
1
+ import React from "react"
2
+ import { Link, type LinkProps } from "react-router-dom"
3
+ import { cn } from "@/utils/cn"
4
+
5
+ interface BreadcrumbLinkProps extends LinkProps {
6
+ className?: string
7
+ }
8
+
9
+ function BreadcrumbLink({ className, ...props }: BreadcrumbLinkProps) {
10
+ return (
11
+ <Link
12
+ className={cn(
13
+ "transition-colors hover:text-black",
14
+ className
15
+ )}
16
+ {...props}
17
+ />
18
+ )
19
+ }
20
+
21
+ export default BreadcrumbLink
@@ -0,0 +1,18 @@
1
+ import React from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface BreadcrumbListProps
5
+ extends React.HTMLAttributes<HTMLOListElement> {
6
+ className?: string;
7
+ }
8
+
9
+ function BreadcrumbList({ className, ...props }: BreadcrumbListProps) {
10
+ return (
11
+ <ol
12
+ className={cn("flex items-center flex-wrap", className)}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export default BreadcrumbList
@@ -0,0 +1,19 @@
1
+ import React from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface BreadcrumbPageProps
5
+ extends React.HTMLAttributes<HTMLSpanElement> {
6
+ className?: string;
7
+ }
8
+
9
+ function BreadcrumbPage({ className, ...props }: BreadcrumbPageProps) {
10
+ return (
11
+ <span
12
+ aria-current="page"
13
+ className={cn("font-medium text-black", className)}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export default BreadcrumbPage
@@ -0,0 +1,25 @@
1
+ import React from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface BreadcrumbSeparatorProps
5
+ extends React.HTMLAttributes<HTMLSpanElement> {
6
+ children?: React.ReactNode
7
+ }
8
+
9
+ function BreadcrumbSeparator({
10
+ className,
11
+ children = "/",
12
+ ...props
13
+ }: BreadcrumbSeparatorProps) {
14
+ return (
15
+ <span
16
+ role="presentation"
17
+ className={cn("mx-2 text-neutral-400", className)}
18
+ {...props}
19
+ >
20
+ {children}
21
+ </span>
22
+ )
23
+ }
24
+
25
+ export default BreadcrumbSeparator
@@ -0,0 +1,6 @@
1
+ export { default as Breadcrumb } from './Breadcrumb';
2
+ export { default as BreadcrumbItem } from './BreadcrumbItem';
3
+ export { default as BreadcrumbLink } from './BreadcrumbLink';
4
+ export { default as BreadcrumbList } from './BreadcrumbList';
5
+ export { default as BreadcrumbPage } from './BreadcrumbPage';
6
+ export { default as BreadcrumbSeparator } from './BreadcrumbSeparator';
@@ -0,0 +1,49 @@
1
+ import React from 'react'
2
+ import { cn } from '@/utils/cn'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+
5
+
6
+ const buttonVariants = cva (
7
+ "flex flex-row items-center justify-center gap-3 text-sm font-normal rounded-sm px-5",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ primary: "bg-neutral-900 text-white hover:bg-neutral-800 active:bg-neutral-700 disabled:bg-neutral-100 disabled:text-neutral-400",
12
+ secondary: "bg-neutral-100 text-neutral-700 hover:bg-neutral-200 hover:text-neutral-800 active:bg-neutral-300 active:text-neutral-900 disabled:bg-neutral-100 disabled:text-neutral-400",
13
+ capsule: "bg-neutral-900 text-white hover:bg-neutral-800 active:bg-neutral-700 disabled:bg-neutral-100 disabled:text-neutral-400 rounded-full",
14
+ outline: "bg-transparent text-neutral-900 border border-neutral-900 hover:border-neutral-500 hover:text-neutral-500 active:border-neutral-700 active:text-neutral-700 disabled:border-neutral-100 disabled:text-neutral-400",
15
+ ghost: "bg-transparent text-neutral-700 hover:text-neutral-800 hover:bg-neutral-100 active:bg-neutral-200 active:text-neutral-900 disabled:text-neutral-400",
16
+ icon: "bg-transparent text-neutral-900 hover:text-neutral-700 active:text-neutral-500 disabled:text-neutral-400 px-0 aspect-square",
17
+ iconfilled: "bg-neutral-900 text-white hover:bg-neutral-800 active:bg-neutral-700 disabled:bg-neutral-200 rounded-sm px-0 aspect-square"
18
+ },
19
+ size: {
20
+ xs: "h-6",
21
+ sm: "h-9",
22
+ md: "h-12",
23
+ lg: "h-14"
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: "primary",
28
+ size: "sm",
29
+ },
30
+ }
31
+ )
32
+
33
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,
34
+ VariantProps<typeof buttonVariants> {
35
+ className?: string;
36
+ children?: React.ReactNode;
37
+ }
38
+
39
+ function Button({className, variant, size, children, ...props}: ButtonProps) {
40
+ return (
41
+ <button className={cn(buttonVariants({variant, size}), className)}
42
+ {...props}
43
+ >
44
+ {children}
45
+ </button>
46
+ )
47
+ }
48
+
49
+ export default Button
@@ -0,0 +1 @@
1
+ export { default as Button } from './Button';
@@ -0,0 +1,34 @@
1
+ import React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "@/utils/cn"
4
+
5
+ const avatarVariants = cva(
6
+ "relative flex shrink-0 overflow-hidden rounded-full items-center justify-center bg-neutral-000",
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: "w-6 h-6 text-xs font-normal",
11
+ md: "w-9 h-9 text-sm font-normal",
12
+ lg: "w-12 h-12 text-tiny font-normal",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: "md",
17
+ },
18
+ }
19
+ )
20
+
21
+ interface AvatarProps
22
+ extends React.HTMLAttributes<HTMLDivElement>,
23
+ VariantProps<typeof avatarVariants> {}
24
+
25
+ function Avatar({ className, size, ...props }: AvatarProps) {
26
+ return (
27
+ <div
28
+ className={cn(avatarVariants({ size }), className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ export default Avatar
@@ -0,0 +1,35 @@
1
+ import React from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface AvatarFallbackProps
5
+ extends React.HTMLAttributes<HTMLSpanElement> {
6
+ initials?: string
7
+ }
8
+
9
+ function AvatarFallback({
10
+ initials,
11
+ className,
12
+ children,
13
+ ...props
14
+ }: AvatarFallbackProps) {
15
+ const fallbackIcon = (
16
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
17
+ <path d="M7 0.5C8.38071 0.5 9.5 1.61929 9.5 3C9.5 4.38071 8.38071 5.5 7 5.5C5.61929 5.5 4.5 4.38071 4.5 3C4.5 1.61929 5.61929 0.5 7 0.5Z" fill="currentColor" stroke="currentColor"/>
18
+ <path d="M7 7.5C9.48528 7.5 11.5 9.51472 11.5 12V13.5H2.5V12C2.5 9.51472 4.51472 7.5 7 7.5Z" fill="currentColor" stroke="currentColor"/>
19
+ </svg>
20
+ )
21
+
22
+ return (
23
+ <span
24
+ className={cn(
25
+ "flex h-full w-full items-center justify-center",
26
+ className
27
+ )}
28
+ {...props}
29
+ >
30
+ {children ?? initials ?? fallbackIcon}
31
+ </span>
32
+ )
33
+ }
34
+
35
+ export default AvatarFallback
@@ -0,0 +1,23 @@
1
+ import React, { useState } from "react"
2
+ import { cn } from "@/utils/cn"
3
+
4
+ interface AvatarImageProps
5
+ extends React.ImgHTMLAttributes<HTMLImageElement> {
6
+ className?: "string";
7
+ }
8
+
9
+ function AvatarImage({ className, ...props }: AvatarImageProps) {
10
+ const [error, setError] = useState(false)
11
+
12
+ if (error) return null
13
+
14
+ return (
15
+ <img
16
+ className={cn("aspect-square h-full w-full object-cover", className)}
17
+ onError={() => setError(true)}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ export default AvatarImage
@@ -0,0 +1,3 @@
1
+ export { default as Avatar } from './Avatar';
2
+ export { default as AvatarFallback } from './AvatarFallback';
3
+ export { default as AvatarImage } from './AvatarImage';
@@ -0,0 +1,8 @@
1
+
2
+ function Hero() {
3
+ return (
4
+ <div>Hero</div>
5
+ )
6
+ }
7
+
8
+ export default Hero
@@ -0,0 +1 @@
1
+ export { default as Hero } from './Hero';
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+
3
+ function Header() {
4
+ return (
5
+ <div>Header</div>
6
+ )
7
+ }
8
+
9
+ export default Header
@@ -0,0 +1 @@
1
+ export { default as Header } from './Header';
@@ -0,0 +1,8 @@
1
+
2
+ function Hero() {
3
+ return (
4
+ <div>Hero</div>
5
+ )
6
+ }
7
+
8
+ export default Hero
@@ -0,0 +1 @@
1
+ export { default as Hero } from './Hero';
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(...inputs));
6
+ }
package/utils/copy.js ADDED
@@ -0,0 +1,42 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * copyRecursive(src, dest, options)
6
+ * options:
7
+ * - force: overwrite existing files
8
+ * - safe: skip if exists
9
+ */
10
+ export function copyRecursive(src, dest, options = {}) {
11
+ if (!fs.existsSync(src)) {
12
+ console.log(`❌ Source not found: ${src}`);
13
+ return;
14
+ }
15
+
16
+ fs.mkdirSync(dest, { recursive: true });
17
+
18
+ for (const item of fs.readdirSync(src)) {
19
+ const srcItem = path.join(src, item);
20
+ const destItem = path.join(dest, item);
21
+
22
+ const stats = fs.statSync(srcItem);
23
+
24
+ if (stats.isDirectory()) {
25
+ copyRecursive(srcItem, destItem, options);
26
+ } else {
27
+ if (fs.existsSync(destItem)) {
28
+ if (options.force) {
29
+ fs.copyFileSync(srcItem, destItem);
30
+ console.log(`✅ Overwritten: ${destItem}`);
31
+ } else if (options.safe) {
32
+ console.log(`ℹ️ Skipped existing: ${destItem}`);
33
+ } else {
34
+ console.log(`⚠️ Exists (use --force to overwrite): ${destItem}`);
35
+ }
36
+ continue;
37
+ }
38
+ fs.copyFileSync(srcItem, destItem);
39
+ console.log(`✅ Copied: ${destItem}`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,19 @@
1
+ import fs from "fs";
2
+
3
+ const CONFIG_FILE = "sparesdev.config.json";
4
+
5
+ export function getConfig() {
6
+ if (!fs.existsSync(CONFIG_FILE)) {
7
+ console.error("❌ Run 'sparesdev init' first");
8
+ process.exit(1);
9
+ }
10
+
11
+ try {
12
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
13
+ return JSON.parse(content);
14
+ } catch {
15
+ console.error(`❌ Invalid ${CONFIG_FILE}`);
16
+ console.error("💡 Delete it and run 'sparesdev init' again");
17
+ process.exit(1);
18
+ }
19
+ }