create-bdpa-react-scaffold 0.1.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/README.md +85 -0
- package/create-ui-lib.js +714 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# create-bdpa-react-scaffold
|
|
2
|
+
|
|
3
|
+
Scaffold a ready-to-run React + Tailwind UI library demo built with Vite. This CLI creates a project with common UI components and a demo app.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Run with npx (no global install needed):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-bdpa-react-scaffold my-app
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then start the dev server:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd my-app
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Options
|
|
21
|
+
|
|
22
|
+
- `--pm <npm|yarn|pnpm>`: Choose package manager (default: `npm`).
|
|
23
|
+
- `--no-install`: Skip dependency installation.
|
|
24
|
+
- `--force`: Allow writing into a non-empty directory.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx create-bdpa-react-scaffold my-app --pm pnpm
|
|
30
|
+
npx create-bdpa-react-scaffold . --force --no-install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What You Get
|
|
34
|
+
|
|
35
|
+
- Vite + React 18 project
|
|
36
|
+
- Tailwind CSS configured (`postcss.config.cjs`, `tailwind.config.cjs`)
|
|
37
|
+
- Example UI components (Button, Card, Input, FormField, Table, Navbar, Sidebar, Modal, Tabs, Toast)
|
|
38
|
+
- Simple Auth pages (Login, Register)
|
|
39
|
+
- Demo app and wiring
|
|
40
|
+
|
|
41
|
+
## Local Development (for this CLI)
|
|
42
|
+
|
|
43
|
+
Link the CLI locally to test changes without publishing:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install
|
|
47
|
+
npm link
|
|
48
|
+
|
|
49
|
+
# Now you can run it from anywhere
|
|
50
|
+
create-bdpa-react-scaffold my-test-app --no-install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To unlink later:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm unlink -g create-bdpa-react-scaffold
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Publishing to npm
|
|
60
|
+
|
|
61
|
+
1) Pick a unique package name. This repo uses `create-bdpa-react-scaffold`.
|
|
62
|
+
|
|
63
|
+
2) Login to npm:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm login
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3) Publish (public):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm publish --access public
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If your package name is scoped (e.g. `@yourname/create-bdpa-react-scaffold`), include `--access public` on first publish.
|
|
76
|
+
|
|
77
|
+
After publishing, users can run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx create-bdpa-react-scaffold my-app
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
UNLICENSED (update as desired).
|
package/create-ui-lib.js
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-ui-lib.js
|
|
3
|
+
// Full upgraded scaffolding script for a complete React + Tailwind UI library
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
// -------------------------------
|
|
10
|
+
// CLI args parsing
|
|
11
|
+
// -------------------------------
|
|
12
|
+
const argv = process.argv.slice(2);
|
|
13
|
+
let targetArg = ".";
|
|
14
|
+
let packageManager = "npm";
|
|
15
|
+
let doInstall = true;
|
|
16
|
+
let forceWrite = false;
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
if (!a) continue;
|
|
21
|
+
if (a === "--pm" && i + 1 < argv.length) {
|
|
22
|
+
packageManager = argv[i + 1];
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (a === "--no-install") {
|
|
27
|
+
doInstall = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (a === "--force") {
|
|
31
|
+
forceWrite = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!a.startsWith("-")) {
|
|
35
|
+
targetArg = a;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BASE_DIR = path.resolve(process.cwd(), targetArg);
|
|
41
|
+
|
|
42
|
+
function ensureTargetDir(dir, { force } = { force: false }) {
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const entries = fs.readdirSync(dir).filter((e) => e !== ".git");
|
|
48
|
+
if (entries.length > 0 && !force) {
|
|
49
|
+
console.error(`\nDirectory ${dir} is not empty. Use --force to continue.`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function write(filePath, content) {
|
|
55
|
+
const fullPath = path.join(BASE_DIR, filePath);
|
|
56
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(fullPath, content.trimStart(), "utf8");
|
|
58
|
+
console.log("✔ Created:", filePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function installDependencies(pm, cwd) {
|
|
62
|
+
const pmCmd = pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm" : "npm";
|
|
63
|
+
console.log(`\n📦 Installing dependencies with ${pmCmd}...`);
|
|
64
|
+
const args = pmCmd === "npm" ? ["install"] : ["install"];
|
|
65
|
+
const result = spawnSync(pmCmd, args, { stdio: "inherit", cwd });
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
console.error(`\n❌ ${pmCmd} install failed.`);
|
|
68
|
+
process.exit(result.status || 1);
|
|
69
|
+
}
|
|
70
|
+
console.log("\n✅ Dependencies installed.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Prepare target directory
|
|
74
|
+
ensureTargetDir(BASE_DIR, { force: forceWrite });
|
|
75
|
+
|
|
76
|
+
// -------------------------------
|
|
77
|
+
// Root files
|
|
78
|
+
// -------------------------------
|
|
79
|
+
|
|
80
|
+
write("package.json", `
|
|
81
|
+
{
|
|
82
|
+
"name": "my-ui-lib",
|
|
83
|
+
"version": "2.0.0",
|
|
84
|
+
"private": true,
|
|
85
|
+
"scripts": {
|
|
86
|
+
"dev": "vite",
|
|
87
|
+
"build": "vite build",
|
|
88
|
+
"preview": "vite preview"
|
|
89
|
+
},
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"react": "^18.2.0",
|
|
92
|
+
"react-dom": "^18.2.0",
|
|
93
|
+
"lucide-react": "^0.344.0"
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@vitejs/plugin-react-swc": "^3.5.0",
|
|
97
|
+
"autoprefixer": "^10.4.20",
|
|
98
|
+
"postcss": "^8.4.47",
|
|
99
|
+
"tailwindcss": "^3.4.0",
|
|
100
|
+
"vite": "^5.0.0"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
write("postcss.config.cjs", `
|
|
106
|
+
module.exports = {
|
|
107
|
+
plugins: {
|
|
108
|
+
tailwindcss: {},
|
|
109
|
+
autoprefixer: {}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
write("tailwind.config.cjs", `
|
|
115
|
+
module.exports = {
|
|
116
|
+
content: [
|
|
117
|
+
"./index.html",
|
|
118
|
+
"./src/**/*.{js,jsx,ts,tsx}"
|
|
119
|
+
],
|
|
120
|
+
theme: {
|
|
121
|
+
extend: {
|
|
122
|
+
colors: {
|
|
123
|
+
milwaukeeBlue: "#2563eb",
|
|
124
|
+
milwaukeeGold: "#fbbf24"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
plugins: []
|
|
129
|
+
};
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
write("vite.config.mts", `
|
|
133
|
+
import { defineConfig } from "vite";
|
|
134
|
+
import react from "@vitejs/plugin-react-swc";
|
|
135
|
+
|
|
136
|
+
export default defineConfig({
|
|
137
|
+
plugins: [react()]
|
|
138
|
+
});
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
write("index.html", `
|
|
142
|
+
<!doctype html>
|
|
143
|
+
<html lang="en">
|
|
144
|
+
<head>
|
|
145
|
+
<meta charset="UTF-8" />
|
|
146
|
+
<title>My UI Library Demo</title>
|
|
147
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
148
|
+
</head>
|
|
149
|
+
<body class="bg-gray-100">
|
|
150
|
+
<div id="root"></div>
|
|
151
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
152
|
+
</body>
|
|
153
|
+
</html>
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
// -------------------------------
|
|
157
|
+
// src root
|
|
158
|
+
// -------------------------------
|
|
159
|
+
|
|
160
|
+
write("src/index.css", `
|
|
161
|
+
@tailwind base;
|
|
162
|
+
@tailwind components;
|
|
163
|
+
@tailwind utilities;
|
|
164
|
+
|
|
165
|
+
body {
|
|
166
|
+
@apply bg-gray-100 text-gray-900;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
h1, h2, h3, h4 {
|
|
170
|
+
@apply font-semibold;
|
|
171
|
+
}
|
|
172
|
+
`);
|
|
173
|
+
|
|
174
|
+
write("src/main.jsx", `
|
|
175
|
+
import React from "react";
|
|
176
|
+
import ReactDOM from "react-dom/client";
|
|
177
|
+
import App from "./App.jsx";
|
|
178
|
+
import "./index.css";
|
|
179
|
+
import { ToastProvider } from "./components/ui/ToastProvider.jsx";
|
|
180
|
+
|
|
181
|
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
182
|
+
<React.StrictMode>
|
|
183
|
+
<ToastProvider>
|
|
184
|
+
<App />
|
|
185
|
+
</ToastProvider>
|
|
186
|
+
</React.StrictMode>
|
|
187
|
+
);
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
write("src/index.js", `
|
|
191
|
+
export { default as Button } from "./components/ui/Button.jsx";
|
|
192
|
+
export { default as Card } from "./components/ui/Card.jsx";
|
|
193
|
+
export { default as Input } from "./components/ui/Input.jsx";
|
|
194
|
+
export { default as FormField } from "./components/ui/FormField.jsx";
|
|
195
|
+
export { default as Table } from "./components/ui/Table.jsx";
|
|
196
|
+
export { default as Navbar } from "./components/ui/Navbar.jsx";
|
|
197
|
+
export { default as Sidebar } from "./components/ui/Sidebar.jsx";
|
|
198
|
+
export { default as Modal } from "./components/ui/Modal.jsx";
|
|
199
|
+
export { default as Tabs } from "./components/ui/Tabs.jsx";
|
|
200
|
+
export { ToastProvider, useToast } from "./components/ui/ToastProvider.jsx";
|
|
201
|
+
|
|
202
|
+
export { default as Login } from "./components/auth/Login.jsx";
|
|
203
|
+
export { default as Register } from "./components/auth/Register.jsx";
|
|
204
|
+
|
|
205
|
+
export { default as Container } from "./components/layout/Container.jsx";
|
|
206
|
+
export { default as Section } from "./components/layout/Section.jsx";
|
|
207
|
+
`);
|
|
208
|
+
|
|
209
|
+
// -------------------------------
|
|
210
|
+
// App.jsx (FULL DEMO)
|
|
211
|
+
// -------------------------------
|
|
212
|
+
|
|
213
|
+
write("src/App.jsx", `
|
|
214
|
+
import { useState } from "react";
|
|
215
|
+
import {
|
|
216
|
+
Button,
|
|
217
|
+
Card,
|
|
218
|
+
Input,
|
|
219
|
+
FormField,
|
|
220
|
+
Table,
|
|
221
|
+
Navbar,
|
|
222
|
+
Sidebar,
|
|
223
|
+
Modal,
|
|
224
|
+
Tabs,
|
|
225
|
+
useToast
|
|
226
|
+
} from "./index.js";
|
|
227
|
+
|
|
228
|
+
const columns = [
|
|
229
|
+
{ key: "name", label: "Student" },
|
|
230
|
+
{ key: "course", label: "Course" },
|
|
231
|
+
{ key: "status", label: "Status" }
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const data = [
|
|
235
|
+
{ name: "Alex", course: "Web Design Fundamentals", status: "Enrolled" },
|
|
236
|
+
{ name: "Jordan", course: "Advanced Web App Design", status: "Waitlisted" },
|
|
237
|
+
{ name: "Taylor", course: "eSports Strategy", status: "Enrolled" }
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
export default function App() {
|
|
241
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
242
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
243
|
+
const toast = useToast();
|
|
244
|
+
|
|
245
|
+
const tabs = [
|
|
246
|
+
{ label: "Overview", content: <p>Welcome to the UI Library demo.</p> },
|
|
247
|
+
{ label: "Components", content: <p>Buttons, Cards, Inputs, Tables, and more.</p> },
|
|
248
|
+
{ label: "Auth", content: <p>Login + Registration pages included.</p> }
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<div className="flex h-screen overflow-hidden">
|
|
253
|
+
|
|
254
|
+
{/* Sidebar */}
|
|
255
|
+
<Sidebar
|
|
256
|
+
open={sidebarOpen}
|
|
257
|
+
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
|
258
|
+
links={[
|
|
259
|
+
{ label: "Home", href: "#" },
|
|
260
|
+
{ label: "Login", href: "/login" },
|
|
261
|
+
{ label: "Register", href: "/register" }
|
|
262
|
+
]}
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
{/* Main content */}
|
|
266
|
+
<div className="flex-1 flex flex-col">
|
|
267
|
+
|
|
268
|
+
{/* Navbar */}
|
|
269
|
+
<Navbar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
|
|
270
|
+
|
|
271
|
+
{/* Page content */}
|
|
272
|
+
<div className="p-6 space-y-6 overflow-auto">
|
|
273
|
+
|
|
274
|
+
<Tabs tabs={tabs} />
|
|
275
|
+
|
|
276
|
+
<div className="grid md:grid-cols-2 gap-6">
|
|
277
|
+
|
|
278
|
+
{/* Form/Card example */}
|
|
279
|
+
<Card>
|
|
280
|
+
<h2 className="text-lg font-semibold mb-4">Sample Form</h2>
|
|
281
|
+
|
|
282
|
+
<div className="space-y-4">
|
|
283
|
+
<FormField label="Student Name">
|
|
284
|
+
<Input placeholder="e.g. Alex Johnson" />
|
|
285
|
+
</FormField>
|
|
286
|
+
|
|
287
|
+
<FormField label="Email">
|
|
288
|
+
<Input type="email" placeholder="student@example.com" />
|
|
289
|
+
</FormField>
|
|
290
|
+
|
|
291
|
+
<FormField label="Course">
|
|
292
|
+
<Input placeholder="Web Design Fundamentals" />
|
|
293
|
+
</FormField>
|
|
294
|
+
|
|
295
|
+
<div className="flex gap-2 pt-2">
|
|
296
|
+
<Button variant="primary">Save</Button>
|
|
297
|
+
<Button variant="secondary">Cancel</Button>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</Card>
|
|
301
|
+
|
|
302
|
+
{/* Table example */}
|
|
303
|
+
<Card>
|
|
304
|
+
<h2 className="text-lg font-semibold mb-4">Enrollment Overview</h2>
|
|
305
|
+
<Table columns={columns} data={data} />
|
|
306
|
+
</Card>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{/* Buttons */}
|
|
310
|
+
<Card>
|
|
311
|
+
<h2 className="text-lg font-semibold mb-4">Button Variants</h2>
|
|
312
|
+
<div className="flex flex-wrap gap-3">
|
|
313
|
+
<Button variant="primary">Primary</Button>
|
|
314
|
+
<Button variant="secondary">Secondary</Button>
|
|
315
|
+
<Button variant="danger">Danger</Button>
|
|
316
|
+
<Button variant="outline">Outline</Button>
|
|
317
|
+
</div>
|
|
318
|
+
</Card>
|
|
319
|
+
|
|
320
|
+
{/* Modal + Toast */}
|
|
321
|
+
<div className="flex gap-4">
|
|
322
|
+
<Button onClick={() => setModalOpen(true)}>Open Modal</Button>
|
|
323
|
+
<Button onClick={() => toast.show("This is a toast!", "success")}>
|
|
324
|
+
Show Toast
|
|
325
|
+
</Button>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<Modal open={modalOpen} onClose={() => setModalOpen(false)}>
|
|
329
|
+
<h2 className="text-lg font-semibold mb-4">Modal Title</h2>
|
|
330
|
+
<p>This is a modal example.</p>
|
|
331
|
+
<Button className="mt-4" onClick={() => setModalOpen(false)}>
|
|
332
|
+
Close
|
|
333
|
+
</Button>
|
|
334
|
+
</Modal>
|
|
335
|
+
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
`);
|
|
342
|
+
|
|
343
|
+
// -------------------------------
|
|
344
|
+
// UI Components
|
|
345
|
+
// -------------------------------
|
|
346
|
+
|
|
347
|
+
write("src/components/ui/Button.jsx", `
|
|
348
|
+
export default function Button({
|
|
349
|
+
variant = "primary",
|
|
350
|
+
children,
|
|
351
|
+
className = "",
|
|
352
|
+
...props
|
|
353
|
+
}) {
|
|
354
|
+
const base =
|
|
355
|
+
"inline-flex items-center justify-center px-4 py-2 rounded-md font-semibold text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2";
|
|
356
|
+
|
|
357
|
+
const variants = {
|
|
358
|
+
primary:
|
|
359
|
+
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
|
|
360
|
+
secondary:
|
|
361
|
+
"bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-400",
|
|
362
|
+
danger:
|
|
363
|
+
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
|
|
364
|
+
outline:
|
|
365
|
+
"border border-gray-300 text-gray-800 hover:bg-gray-100 focus:ring-gray-400"
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<button
|
|
370
|
+
className={\`\${base} \${variants[variant]} \${className}\`}
|
|
371
|
+
{...props}
|
|
372
|
+
>
|
|
373
|
+
{children}
|
|
374
|
+
</button>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
`);
|
|
378
|
+
|
|
379
|
+
write("src/components/ui/Card.jsx", `
|
|
380
|
+
export default function Card({ children, className = "" }) {
|
|
381
|
+
return (
|
|
382
|
+
<div
|
|
383
|
+
className={\`bg-white shadow-sm rounded-lg p-4 md:p-6 border border-gray-200 \${className}\`}
|
|
384
|
+
>
|
|
385
|
+
{children}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
`);
|
|
390
|
+
|
|
391
|
+
write("src/components/ui/Input.jsx", `
|
|
392
|
+
export default function Input({ label, className = "", ...props }) {
|
|
393
|
+
return (
|
|
394
|
+
<label className="flex flex-col gap-1 text-sm">
|
|
395
|
+
{label && (
|
|
396
|
+
<span className="font-medium text-gray-700">
|
|
397
|
+
{label}
|
|
398
|
+
</span>
|
|
399
|
+
)}
|
|
400
|
+
<input
|
|
401
|
+
className={\`border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none text-sm \${className}\`}
|
|
402
|
+
{...props}
|
|
403
|
+
/>
|
|
404
|
+
</label>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
`);
|
|
408
|
+
|
|
409
|
+
write("src/components/ui/FormField.jsx", `
|
|
410
|
+
export default function FormField({ label, error, children, helperText }) {
|
|
411
|
+
return (
|
|
412
|
+
<div className="flex flex-col gap-1 text-sm">
|
|
413
|
+
{label && (
|
|
414
|
+
<label className="font-medium text-gray-700">
|
|
415
|
+
{label}
|
|
416
|
+
</label>
|
|
417
|
+
)}
|
|
418
|
+
|
|
419
|
+
{children}
|
|
420
|
+
|
|
421
|
+
{helperText && !error && (
|
|
422
|
+
<p className="text-xs text-gray-500">{helperText}</p>
|
|
423
|
+
)}
|
|
424
|
+
|
|
425
|
+
{error && (
|
|
426
|
+
<p className="text-xs text-red-600">
|
|
427
|
+
{error}
|
|
428
|
+
</p>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
`);
|
|
434
|
+
|
|
435
|
+
write("src/components/ui/Table.jsx", `
|
|
436
|
+
export default function Table({ columns, data }) {
|
|
437
|
+
return (
|
|
438
|
+
<div className="overflow-x-auto">
|
|
439
|
+
<table className="min-w-full border border-gray-200 bg-white rounded-lg overflow-hidden">
|
|
440
|
+
<thead className="bg-gray-100">
|
|
441
|
+
<tr>
|
|
442
|
+
{columns.map((col) => (
|
|
443
|
+
<th
|
|
444
|
+
key={col.key}
|
|
445
|
+
className="px-4 py-2 text-left text-xs font-semibold text-gray-700 border-b border-gray-200"
|
|
446
|
+
>
|
|
447
|
+
{col.label}
|
|
448
|
+
</th>
|
|
449
|
+
))}
|
|
450
|
+
</tr>
|
|
451
|
+
</thead>
|
|
452
|
+
|
|
453
|
+
<tbody>
|
|
454
|
+
{data.map((row, i) => (
|
|
455
|
+
<tr
|
|
456
|
+
key={i}
|
|
457
|
+
className={i % 2 === 0 ? "bg-white" : "bg-gray-50"}
|
|
458
|
+
>
|
|
459
|
+
{columns.map((col) => (
|
|
460
|
+
<td
|
|
461
|
+
key={col.key}
|
|
462
|
+
className="px-4 py-2 text-sm text-gray-800 border-b border-gray-100"
|
|
463
|
+
>
|
|
464
|
+
{row[col.key]}
|
|
465
|
+
</td>
|
|
466
|
+
))}
|
|
467
|
+
</tr>
|
|
468
|
+
))}
|
|
469
|
+
</tbody>
|
|
470
|
+
</table>
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
`);
|
|
475
|
+
|
|
476
|
+
write("src/components/ui/Navbar.jsx", `
|
|
477
|
+
import { Menu } from "lucide-react";
|
|
478
|
+
|
|
479
|
+
export default function Navbar({ onMenuClick }) {
|
|
480
|
+
return (
|
|
481
|
+
<nav className="bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between">
|
|
482
|
+
<button className="md:hidden" onClick={onMenuClick}>
|
|
483
|
+
<Menu />
|
|
484
|
+
</button>
|
|
485
|
+
<h1 className="text-xl font-bold">My UI Library</h1>
|
|
486
|
+
</nav>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
`);
|
|
490
|
+
|
|
491
|
+
write("src/components/ui/Sidebar.jsx", `
|
|
492
|
+
export default function Sidebar({ open, onToggle, links }) {
|
|
493
|
+
return (
|
|
494
|
+
<div
|
|
495
|
+
className={\`
|
|
496
|
+
fixed md:static inset-y-0 left-0 z-40
|
|
497
|
+
bg-white border-r border-gray-200
|
|
498
|
+
h-full w-64 transform
|
|
499
|
+
transition-transform duration-200
|
|
500
|
+
\${open ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
|
|
501
|
+
\`}
|
|
502
|
+
>
|
|
503
|
+
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
|
504
|
+
<h2 className="font-semibold">Menu</h2>
|
|
505
|
+
<button className="md:hidden" onClick={onToggle}>✕</button>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<ul className="p-4 space-y-2">
|
|
509
|
+
{links.map((l) => (
|
|
510
|
+
<li key={l.label}>
|
|
511
|
+
<a href={l.href} className="block px-2 py-2 rounded hover:bg-gray-100">
|
|
512
|
+
{l.label}
|
|
513
|
+
</a>
|
|
514
|
+
</li>
|
|
515
|
+
))}
|
|
516
|
+
</ul>
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
`);
|
|
521
|
+
|
|
522
|
+
write("src/components/ui/Modal.jsx", `
|
|
523
|
+
export default function Modal({ open, onClose, children }) {
|
|
524
|
+
if (!open) return null;
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
|
528
|
+
<div className="bg-white rounded-lg shadow-lg p-6 w-full max-w-md relative">
|
|
529
|
+
<button
|
|
530
|
+
className="absolute top-3 right-3 text-gray-500 hover:text-gray-700"
|
|
531
|
+
onClick={onClose}
|
|
532
|
+
>
|
|
533
|
+
✕
|
|
534
|
+
</button>
|
|
535
|
+
|
|
536
|
+
{children}
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
`);
|
|
542
|
+
|
|
543
|
+
write("src/components/ui/Tabs.jsx", `
|
|
544
|
+
import { useState } from "react";
|
|
545
|
+
|
|
546
|
+
export default function Tabs({ tabs }) {
|
|
547
|
+
const [active, setActive] = useState(0);
|
|
548
|
+
|
|
549
|
+
return (
|
|
550
|
+
<div>
|
|
551
|
+
<div className="flex gap-4 border-b border-gray-200">
|
|
552
|
+
{tabs.map((t, i) => (
|
|
553
|
+
<button
|
|
554
|
+
key={i}
|
|
555
|
+
onClick={() => setActive(i)}
|
|
556
|
+
className={\`pb-2 text-sm font-medium \${active === i
|
|
557
|
+
? "border-b-2 border-blue-600 text-blue-600"
|
|
558
|
+
: "text-gray-600 hover:text-gray-800"
|
|
559
|
+
}\`}
|
|
560
|
+
>
|
|
561
|
+
{t.label}
|
|
562
|
+
</button>
|
|
563
|
+
))}
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
<div className="mt-4">{tabs[active].content}</div>
|
|
567
|
+
</div>
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
`);
|
|
571
|
+
|
|
572
|
+
write("src/components/ui/ToastProvider.jsx", `
|
|
573
|
+
import { createContext, useContext, useState } from "react";
|
|
574
|
+
|
|
575
|
+
const ToastContext = createContext();
|
|
576
|
+
|
|
577
|
+
export function useToast() {
|
|
578
|
+
return useContext(ToastContext);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export function ToastProvider({ children }) {
|
|
582
|
+
const [toasts, setToasts] = useState([]);
|
|
583
|
+
|
|
584
|
+
const show = (message, type = "info") => {
|
|
585
|
+
const id = Date.now();
|
|
586
|
+
setToasts((t) => [...t, { id, message, type }]);
|
|
587
|
+
setTimeout(() => {
|
|
588
|
+
setToasts((t) => t.filter((toast) => toast.id !== id));
|
|
589
|
+
}, 3000);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<ToastContext.Provider value={{ show }}>
|
|
594
|
+
{children}
|
|
595
|
+
|
|
596
|
+
<div className="fixed bottom-4 right-4 space-y-3 z-50">
|
|
597
|
+
{toasts.map((t) => (
|
|
598
|
+
<div
|
|
599
|
+
key={t.id}
|
|
600
|
+
className={\`px-4 py-2 rounded-md shadow text-white \${t.type === "success"
|
|
601
|
+
? "bg-green-600"
|
|
602
|
+
: t.type === "error"
|
|
603
|
+
? "bg-red-600"
|
|
604
|
+
: "bg-gray-800"
|
|
605
|
+
}\`}
|
|
606
|
+
>
|
|
607
|
+
{t.message}
|
|
608
|
+
</div>
|
|
609
|
+
))}
|
|
610
|
+
</div>
|
|
611
|
+
</ToastContext.Provider>
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
`);
|
|
615
|
+
|
|
616
|
+
// -------------------------------
|
|
617
|
+
// Auth Components
|
|
618
|
+
// -------------------------------
|
|
619
|
+
|
|
620
|
+
write("src/components/auth/Login.jsx", `
|
|
621
|
+
import Button from "../ui/Button.jsx";
|
|
622
|
+
import Input from "../ui/Input.jsx";
|
|
623
|
+
import Card from "../ui/Card.jsx";
|
|
624
|
+
|
|
625
|
+
export default function Login({ onSubmit }) {
|
|
626
|
+
return (
|
|
627
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
|
628
|
+
<Card className="w-full max-w-sm space-y-4">
|
|
629
|
+
<h2 className="text-xl font-bold">Sign In</h2>
|
|
630
|
+
|
|
631
|
+
<Input label="Email" type="email" placeholder="you@example.com" />
|
|
632
|
+
<Input label="Password" type="password" placeholder="••••••••" />
|
|
633
|
+
|
|
634
|
+
<Button className="w-full" onClick={onSubmit}>
|
|
635
|
+
Sign In
|
|
636
|
+
</Button>
|
|
637
|
+
|
|
638
|
+
<p className="text-sm text-center text-gray-600">
|
|
639
|
+
Don’t have an account?{" "}
|
|
640
|
+
<a href="/register" className="text-blue-600 hover:underline">
|
|
641
|
+
Create one
|
|
642
|
+
</a>
|
|
643
|
+
</p>
|
|
644
|
+
</Card>
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
}`);
|
|
648
|
+
|
|
649
|
+
write("src/components/auth/Register.jsx", `
|
|
650
|
+
import Button from "../ui/Button.jsx";
|
|
651
|
+
import Input from "../ui/Input.jsx";
|
|
652
|
+
import Card from "../ui/Card.jsx";
|
|
653
|
+
|
|
654
|
+
export default function Register({ onSubmit }) {
|
|
655
|
+
return (
|
|
656
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
|
657
|
+
<Card className="w-full max-w-sm space-y-4">
|
|
658
|
+
<h2 className="text-xl font-bold">Create Account</h2>
|
|
659
|
+
|
|
660
|
+
<Input label="Full Name" placeholder="Your Name" />
|
|
661
|
+
<Input label="Email" type="email" placeholder="you@example.com" />
|
|
662
|
+
<Input label="Password" type="password" placeholder="" />
|
|
663
|
+
|
|
664
|
+
<Button className="w-full" onClick={onSubmit}>
|
|
665
|
+
Register
|
|
666
|
+
</Button>
|
|
667
|
+
|
|
668
|
+
<p className="text-sm text-center text-gray-600">
|
|
669
|
+
Already have an account?{" "}
|
|
670
|
+
<a href="/login" className="text-blue-600 hover:underline">
|
|
671
|
+
Sign in
|
|
672
|
+
</a>
|
|
673
|
+
</p>
|
|
674
|
+
</Card>
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
`);
|
|
679
|
+
|
|
680
|
+
// -------------------------------
|
|
681
|
+
// Layout Components
|
|
682
|
+
// -------------------------------
|
|
683
|
+
|
|
684
|
+
write("src/components/layout/Container.jsx", `
|
|
685
|
+
export default function Container({ children, className = "" }) {
|
|
686
|
+
return (
|
|
687
|
+
<div className={\`max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 \${className}\`}>
|
|
688
|
+
{children}
|
|
689
|
+
</div>
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
`);
|
|
693
|
+
|
|
694
|
+
write("src/components/layout/Section.jsx", `
|
|
695
|
+
export default function Section({ children, className = "" }) {
|
|
696
|
+
return (
|
|
697
|
+
<section className={\`py-12 \${className}\`}>
|
|
698
|
+
{children}
|
|
699
|
+
</section>
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
`);
|
|
703
|
+
|
|
704
|
+
console.log("\n✅ UI Library scaffolding complete!");
|
|
705
|
+
console.log("\nNext steps:");
|
|
706
|
+
console.log(" 1. npm run dev");
|
|
707
|
+
console.log(" 3. Open http://localhost:5173 in your browser\n");
|
|
708
|
+
|
|
709
|
+
// Install dependencies unless disabled
|
|
710
|
+
if (doInstall) {
|
|
711
|
+
installDependencies(packageManager, BASE_DIR);
|
|
712
|
+
} else {
|
|
713
|
+
console.log("\nℹ️ Skipping install (flag --no-install). Run manually later.");
|
|
714
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-bdpa-react-scaffold",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Scaffold a React + Tailwind UI library demo via Vite.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-bdpa-react-scaffold": "./create-ui-lib.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"create-ui-lib.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"create",
|
|
15
|
+
"scaffold",
|
|
16
|
+
"react",
|
|
17
|
+
"tailwind",
|
|
18
|
+
"vite",
|
|
19
|
+
"ui",
|
|
20
|
+
"template"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/lxdavis9lxd/create-bdpa-react-scaffold-.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/lxdavis9lxd/create-bdpa-react-scaffold-/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/lxdavis9lxd/create-bdpa-react-scaffold-#readme",
|
|
33
|
+
"license": "UNLICENSED",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"prepublishOnly": "node -e \"require('fs').accessSync('./create-ui-lib.js');\"",
|
|
36
|
+
"publish:public": "npm publish --access public"
|
|
37
|
+
}
|
|
38
|
+
}
|