sparesdev 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +31 -0
- package/bin/index.js +126 -0
- package/core/add.js +84 -0
- package/core/doctor.js +34 -0
- package/core/frontend.js +57 -0
- package/core/init.js +137 -0
- package/core/list.js +108 -0
- package/package.json +23 -0
- package/templates/typescript/components/Accordion/Accordion.tsx +17 -0
- package/templates/typescript/components/Accordion/AccordionButton.tsx +52 -0
- package/templates/typescript/components/Accordion/AccordionContent.tsx +15 -0
- package/templates/typescript/components/Accordion/AccordionItem.tsx +17 -0
- package/templates/typescript/components/Accordion/index.ts +4 -0
- package/templates/typescript/components/Avatar/Avatar.tsx +34 -0
- package/templates/typescript/components/Avatar/AvatarFallback.tsx +35 -0
- package/templates/typescript/components/Avatar/AvatarImage.tsx +23 -0
- package/templates/typescript/components/Avatar/index.ts +3 -0
- package/templates/typescript/components/Backdrop/Backdrop.tsx +19 -0
- package/templates/typescript/components/Backdrop/index.ts +1 -0
- package/templates/typescript/components/Badge/Badge.tsx +63 -0
- package/templates/typescript/components/Badge/index.ts +1 -0
- package/templates/typescript/components/Breadcrumb/Breadcrumb.tsx +35 -0
- package/templates/typescript/components/Breadcrumb/BreadcrumbItem.tsx +18 -0
- package/templates/typescript/components/Breadcrumb/BreadcrumbLink.tsx +21 -0
- package/templates/typescript/components/Breadcrumb/BreadcrumbList.tsx +18 -0
- package/templates/typescript/components/Breadcrumb/BreadcrumbPage.tsx +19 -0
- package/templates/typescript/components/Breadcrumb/BreadcrumbSeparator.tsx +25 -0
- package/templates/typescript/components/Breadcrumb/index.ts +6 -0
- package/templates/typescript/components/Button/Button.tsx +49 -0
- package/templates/typescript/components/Button/index.ts +1 -0
- package/templates/typescript/frontend/src/components/Avatar/Avatar.tsx +34 -0
- package/templates/typescript/frontend/src/components/Avatar/AvatarFallback.tsx +35 -0
- package/templates/typescript/frontend/src/components/Avatar/AvatarImage.tsx +23 -0
- package/templates/typescript/frontend/src/components/Avatar/index.ts +3 -0
- package/templates/typescript/frontend/src/pages/Hero.tsx +8 -0
- package/templates/typescript/frontend/src/pages/index.ts +1 -0
- package/templates/typescript/layouts/Header/Header.tsx +9 -0
- package/templates/typescript/layouts/Header/index.ts +1 -0
- package/templates/typescript/pages/Hero/Hero.tsx +8 -0
- package/templates/typescript/pages/Hero/index.ts +1 -0
- package/templates/typescript/utils/cn.ts +6 -0
- package/utils/copy.js +26 -0
- 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,84 @@
|
|
|
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
|
+
|
|
6
|
+
export function add(layer, components, options = {}) {
|
|
7
|
+
const { force = false, safe = false } = options;
|
|
8
|
+
if (!components.length) {
|
|
9
|
+
console.error("❌ Provide component names");
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
const lang = "typescript";
|
|
15
|
+
const packageRoot = path.dirname(
|
|
16
|
+
require.resolve("sparesdev/package.json")
|
|
17
|
+
);
|
|
18
|
+
const projectRoot = process.cwd();
|
|
19
|
+
|
|
20
|
+
const searchLayers =
|
|
21
|
+
layer === "all"
|
|
22
|
+
? ["components", "layouts", "blocks", "pages"]
|
|
23
|
+
: [layer];
|
|
24
|
+
|
|
25
|
+
const basePaths = searchLayers.map((section) => ({
|
|
26
|
+
type: section,
|
|
27
|
+
path: path.join(packageRoot, "templates", lang, section),
|
|
28
|
+
target: path.join(projectRoot, "src", section),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const installed = [];
|
|
32
|
+
|
|
33
|
+
for (const nameInput of components) {
|
|
34
|
+
const name = nameInput;
|
|
35
|
+
let found = null;
|
|
36
|
+
|
|
37
|
+
for (const bp of basePaths) {
|
|
38
|
+
if (!fs.existsSync(bp.path)) continue;
|
|
39
|
+
const available = fs.readdirSync(bp.path);
|
|
40
|
+
const match = available.find(
|
|
41
|
+
(item) => item.toLowerCase() === name.toLowerCase()
|
|
42
|
+
);
|
|
43
|
+
if (match) {
|
|
44
|
+
found = {
|
|
45
|
+
match,
|
|
46
|
+
src: path.join(bp.path, match),
|
|
47
|
+
dest: path.join(bp.target, match),
|
|
48
|
+
type: bp.type,
|
|
49
|
+
};
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!found) {
|
|
55
|
+
console.log(`❌ ${name} not found`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check existing
|
|
60
|
+
if (fs.existsSync(found.dest)) {
|
|
61
|
+
if (safe) {
|
|
62
|
+
console.log(`ℹ️ Skipped existing: ${found.dest}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!force) {
|
|
66
|
+
console.log(
|
|
67
|
+
`⚠️ Exists (use --force to overwrite): ${found.dest}`
|
|
68
|
+
);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fs.mkdirSync(path.dirname(found.dest), { recursive: true });
|
|
74
|
+
copyRecursive(found.src, found.dest, { force });
|
|
75
|
+
installed.push(found.match);
|
|
76
|
+
console.log(`✅ ${found.match} → src/${found.type}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (installed.length) {
|
|
80
|
+
console.log(`\n🎉 Installed: ${installed.join(", ")}\n`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log("❌ No components installed");
|
|
83
|
+
}
|
|
84
|
+
}
|
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
|
+
}
|
package/core/frontend.js
ADDED
|
@@ -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,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
// Helpers
|
|
7
|
+
function createConfig() {
|
|
8
|
+
const config = {
|
|
9
|
+
language: "typescript",
|
|
10
|
+
version: "3.0",
|
|
11
|
+
style: "default",
|
|
12
|
+
componentsPath: "src/components",
|
|
13
|
+
utilsPath: "src/utils",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync("sparesdev.config.json")) {
|
|
17
|
+
fs.writeFileSync(
|
|
18
|
+
"sparesdev.config.json",
|
|
19
|
+
JSON.stringify(config, null, 2)
|
|
20
|
+
);
|
|
21
|
+
console.log("✅ sparesdev.config.json created");
|
|
22
|
+
} else {
|
|
23
|
+
console.log("ℹ️ Config already exists, skipping");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return config;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createTokensCss() {
|
|
30
|
+
const tokensPath = "src/tokens.css";
|
|
31
|
+
if (!fs.existsSync(tokensPath)) {
|
|
32
|
+
fs.mkdirSync("src", { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
tokensPath,
|
|
35
|
+
`@theme {
|
|
36
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
37
|
+
--color-background: 255, 255, 255;
|
|
38
|
+
--color-text: 0,0,0;
|
|
39
|
+
}`
|
|
40
|
+
);
|
|
41
|
+
console.log("✅ tokens.css created");
|
|
42
|
+
} else {
|
|
43
|
+
console.log("ℹ️ tokens.css already exists");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function patchIndexCss() {
|
|
48
|
+
const indexCss = "src/index.css";
|
|
49
|
+
if (!fs.existsSync(indexCss)) {
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
indexCss,
|
|
52
|
+
`@import "tailwindcss";
|
|
53
|
+
@import "./tokens.css";
|
|
54
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
|
55
|
+
|
|
56
|
+
body {
|
|
57
|
+
background: rgb(var(--color-background));
|
|
58
|
+
color: rgb(var(--color-text));
|
|
59
|
+
font-feature-settings: "rlig" 1, "calt" 1;
|
|
60
|
+
font-family: var(--font-sans);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.hide-scrollbar {
|
|
64
|
+
-ms-overflow-style: none;
|
|
65
|
+
scrollbar-width: none;
|
|
66
|
+
}
|
|
67
|
+
.hide-scrollbar::-webkit-scrollbar {
|
|
68
|
+
display: none;
|
|
69
|
+
}`
|
|
70
|
+
);
|
|
71
|
+
console.log("✅ index.css created");
|
|
72
|
+
} else {
|
|
73
|
+
console.log("ℹ️ index.css already exists, skipping");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function patchViteConfig() {
|
|
78
|
+
const vitePath = "vite.config.ts";
|
|
79
|
+
if (!fs.existsSync(vitePath)) {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
vitePath,
|
|
82
|
+
`import { defineConfig } from "vite";
|
|
83
|
+
import react from "@vitejs/plugin-react";
|
|
84
|
+
import path from "path";
|
|
85
|
+
|
|
86
|
+
export default defineConfig({
|
|
87
|
+
plugins: [react()],
|
|
88
|
+
resolve: {
|
|
89
|
+
alias: { "@": path.resolve(__dirname, "./src") }
|
|
90
|
+
}
|
|
91
|
+
});`
|
|
92
|
+
);
|
|
93
|
+
console.log("✅ vite.config.ts created");
|
|
94
|
+
} else {
|
|
95
|
+
console.log("ℹ️ Vite config exists, skipping patch");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function installDeps() {
|
|
100
|
+
console.log("🚀 Installing dependencies...");
|
|
101
|
+
const deps = [
|
|
102
|
+
"react-router-dom",
|
|
103
|
+
"clsx",
|
|
104
|
+
"tailwind-merge",
|
|
105
|
+
"class-variance-authority",
|
|
106
|
+
"tailwindcss",
|
|
107
|
+
"@tailwindcss/vite",
|
|
108
|
+
];
|
|
109
|
+
execSync(`npm install -D ${deps.join(" ")}`, { stdio: "inherit" });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ------------------------
|
|
113
|
+
// INIT MAIN
|
|
114
|
+
// ------------------------
|
|
115
|
+
export async function init(mode = "minimal") {
|
|
116
|
+
console.log(`🎯 Initializing sparesdev (${mode} mode)...`);
|
|
117
|
+
|
|
118
|
+
const config = createConfig();
|
|
119
|
+
|
|
120
|
+
// utils folder
|
|
121
|
+
const utilsPath = config.utilsPath || "src/utils";
|
|
122
|
+
if (!fs.existsSync(utilsPath)) {
|
|
123
|
+
fs.mkdirSync(utilsPath, { recursive: true });
|
|
124
|
+
console.log("📁 Created utils folder");
|
|
125
|
+
} else {
|
|
126
|
+
console.log("ℹ️ utils already exists, skipping");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (mode === "full") {
|
|
130
|
+
installDeps();
|
|
131
|
+
patchViteConfig();
|
|
132
|
+
createTokensCss();
|
|
133
|
+
patchIndexCss();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log("🎉 Setup completed");
|
|
137
|
+
}
|
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.1",
|
|
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,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,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 @@
|
|
|
1
|
+
export { default as Hero } from './Hero';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Header } from './Header';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Hero } from './Hero';
|
package/utils/copy.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function copyRecursive(src, dest, options = {}) {
|
|
5
|
+
const { force = false } = options;
|
|
6
|
+
if (!fs.existsSync(src)) return;
|
|
7
|
+
|
|
8
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
9
|
+
|
|
10
|
+
for (const item of fs.readdirSync(src)) {
|
|
11
|
+
const srcItem = path.join(src, item);
|
|
12
|
+
const destItem = path.join(dest, item);
|
|
13
|
+
const stats = fs.statSync(srcItem);
|
|
14
|
+
|
|
15
|
+
if (stats.isDirectory()) {
|
|
16
|
+
copyRecursive(srcItem, destItem, options);
|
|
17
|
+
} else {
|
|
18
|
+
if (fs.existsSync(destItem) && !force) {
|
|
19
|
+
console.log(`ℹ️ Skipped existing: ${destItem}`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
fs.copyFileSync(srcItem, destItem);
|
|
23
|
+
console.log(`✅ Copied: ${destItem}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|