cooterlabs 1.0.0 → 1.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.
- package/README.md +80 -0
- package/bin/index.js +240 -75
- package/package.json +6 -3
- package/templates/avatar/avatar.tsx +52 -0
- package/templates/badge/badge.tsx +36 -0
- package/templates/card/card.tsx +89 -0
- package/templates/checkbox/checkbox.tsx +32 -0
- package/templates/label/label.tsx +25 -0
- package/templates/textarea/textarea.tsx +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# CooterLabs UI Library CLI
|
|
2
|
+
|
|
3
|
+
A blazingly fast, standalone CLI tool for scaffolding beautiful, accessible, and highly customizable UI components built strictly for React, Vite, and Next.js applications directly into your project via the terminal!
|
|
4
|
+
|
|
5
|
+
It brings components directly to you. We do not provide a generic NPM library package to import elements from. Instead, the actual React TS/JS code is scaffolded straight into your `src/components/ui` folder, meaning you completely own the implementation, styles, and markup.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **✅ Project Detection**: Auto-detects your ecosystem (Next.js App router/Pages router, Vite, Create React App).
|
|
10
|
+
- **✅ TypeScript vs. JavaScript**: Installs `.tsx` component files if you use TypeScript, gracefully falling back to stripped `.jsx` components for vanilla JS environments.
|
|
11
|
+
- **✅ Tailwind Awareness**: Verifies you have Tailwind appropriately installed.
|
|
12
|
+
- **✅ Dependency Auto-Install**: If the component you want needs `framer-motion`, `@radix-ui/react-checkbox` or even just `clsx`, the CLI automatically resolves these dependencies silently for you.
|
|
13
|
+
- **✅ Prettier Formatting**: Uses your project's Prettier settings statically against our components before file creation so the component matches your exact code-style.
|
|
14
|
+
- **✅ Interactive Interface**: Employs interactive menus to let you visually pick the component you want to build inside your project.
|
|
15
|
+
|
|
16
|
+
## Installation / Usage
|
|
17
|
+
|
|
18
|
+
1. Open your terminal at the root of your project directory.
|
|
19
|
+
2. Ensure you have `tailwind-merge` and `clsx` properly configured in a central utility path or installed manually (some components will auto-detect and install these).
|
|
20
|
+
|
|
21
|
+
### Initialization (Themes & CSS Base)
|
|
22
|
+
|
|
23
|
+
To swiftly set up a cohesive Tailwind theme and properly configure your base CSS file (e.g. `index.css` or `globals.css`), run:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx cooterlabs init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This will give you a list of carefully curated designer themes (like `zinc`, `slate`) to apply global default variables (background, foreground, primary borders, etc.) out-of-the-box.
|
|
30
|
+
|
|
31
|
+
### Adding Components
|
|
32
|
+
|
|
33
|
+
Run the CLI tool without arguments to get an interactive autocomplete prompt:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx cooterlabs add
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If you know exactly what component you want, bypass the interactive wizard and supply the name:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Example: Adding a Button
|
|
43
|
+
npx cooterlabs add button
|
|
44
|
+
|
|
45
|
+
# Example: Adding an Avatar
|
|
46
|
+
npx cooterlabs add avatar
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Install All Components At Once
|
|
50
|
+
|
|
51
|
+
Need a complete starting block? Add the `--all` flag to scaffold the entire library in one go:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx cooterlabs add --all
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Available Components
|
|
58
|
+
|
|
59
|
+
Currently, the CooterLabs Library ships with the following carefully designed boilerplate components:
|
|
60
|
+
|
|
61
|
+
- `avatar` - An accessible React image element holding fallback identifiers.
|
|
62
|
+
- `badge` - Tiny status badge elements relying on standard `variant` mappings.
|
|
63
|
+
- `button` - A highly stylized native HTML button leveraging Radix UI primitives (`Slot`).
|
|
64
|
+
- `card` - Content display wrappers comprising headers, footers, body content, and titles.
|
|
65
|
+
- `checkbox` - Robust implementation for accessible `<input type="checkbox"/>` mappings.
|
|
66
|
+
- `input` - Your standard customized HTML `<input />`.
|
|
67
|
+
- `label` - An accessible Radix-backed `<label />`.
|
|
68
|
+
- `textarea` - A matching multiline input variation styling perfectly with standard inputs.
|
|
69
|
+
|
|
70
|
+
## How it works under-the-hood
|
|
71
|
+
|
|
72
|
+
1. When you run `npx cooterlabs add [component]`, the script analyzes your filesystem looking for `pnpm-lock.yaml`, `bun.lockb`, `yarn.lock`, etc., and detects package managers.
|
|
73
|
+
2. It looks for `tsconfig.json` to see if it should copy the raw `.tsx` component files or use regex parsers to strip types for a standard `.jsx` output.
|
|
74
|
+
3. It recursively creates target folders, usually resolving to `src/components/ui` or `components/ui`.
|
|
75
|
+
4. Formatting is applied.
|
|
76
|
+
5. Child dependencies (e.g., `lucide-react`, Radix UI dependencies) required *exclusively* for that component are installed so the codebase isn't overwhelmed with useless imported libraries.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT - Grab it, use it, scale it!
|
package/bin/index.js
CHANGED
|
@@ -29,6 +29,83 @@ const REGISTRY = {
|
|
|
29
29
|
dependencies: ["lucide-react", "clsx", "tailwind-merge"],
|
|
30
30
|
dev_dependencies: [],
|
|
31
31
|
},
|
|
32
|
+
card: {
|
|
33
|
+
name: "Card",
|
|
34
|
+
dependencies: ["clsx", "tailwind-merge"],
|
|
35
|
+
dev_dependencies: [],
|
|
36
|
+
},
|
|
37
|
+
label: {
|
|
38
|
+
name: "Label",
|
|
39
|
+
dependencies: ["clsx", "tailwind-merge", "@radix-ui/react-label"],
|
|
40
|
+
dev_dependencies: [],
|
|
41
|
+
},
|
|
42
|
+
textarea: {
|
|
43
|
+
name: "Textarea",
|
|
44
|
+
dependencies: ["clsx", "tailwind-merge"],
|
|
45
|
+
dev_dependencies: [],
|
|
46
|
+
},
|
|
47
|
+
badge: {
|
|
48
|
+
name: "Badge",
|
|
49
|
+
dependencies: ["clsx", "tailwind-merge"],
|
|
50
|
+
dev_dependencies: [],
|
|
51
|
+
},
|
|
52
|
+
avatar: {
|
|
53
|
+
name: "Avatar",
|
|
54
|
+
dependencies: ["clsx", "tailwind-merge", "@radix-ui/react-avatar"],
|
|
55
|
+
dev_dependencies: [],
|
|
56
|
+
},
|
|
57
|
+
checkbox: {
|
|
58
|
+
name: "Checkbox",
|
|
59
|
+
dependencies: ["lucide-react", "clsx", "tailwind-merge"],
|
|
60
|
+
dev_dependencies: [],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const THEMES = {
|
|
65
|
+
zinc: `:root {
|
|
66
|
+
--background: 0 0% 100%;
|
|
67
|
+
--foreground: 240 10% 3.9%;
|
|
68
|
+
--card: 0 0% 100%;
|
|
69
|
+
--card-foreground: 240 10% 3.9%;
|
|
70
|
+
--popover: 0 0% 100%;
|
|
71
|
+
--popover-foreground: 240 10% 3.9%;
|
|
72
|
+
--primary: 240 5.9% 10%;
|
|
73
|
+
--primary-foreground: 0 0% 98%;
|
|
74
|
+
--secondary: 240 4.8% 95.9%;
|
|
75
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
76
|
+
--muted: 240 4.8% 95.9%;
|
|
77
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
78
|
+
--accent: 240 4.8% 95.9%;
|
|
79
|
+
--accent-foreground: 240 5.9% 10%;
|
|
80
|
+
--destructive: 0 84.2% 60.2%;
|
|
81
|
+
--destructive-foreground: 0 0% 98%;
|
|
82
|
+
--border: 240 5.9% 90%;
|
|
83
|
+
--input: 240 5.9% 90%;
|
|
84
|
+
--ring: 240 10% 3.9%;
|
|
85
|
+
--radius: 0.5rem;
|
|
86
|
+
}`,
|
|
87
|
+
slate: `:root {
|
|
88
|
+
--background: 0 0% 100%;
|
|
89
|
+
--foreground: 222.2 84% 4.9%;
|
|
90
|
+
--card: 0 0% 100%;
|
|
91
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
92
|
+
--popover: 0 0% 100%;
|
|
93
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
94
|
+
--primary: 222.2 47.4% 11.2%;
|
|
95
|
+
--primary-foreground: 210 40% 98%;
|
|
96
|
+
--secondary: 210 40% 96.1%;
|
|
97
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
98
|
+
--muted: 210 40% 96.1%;
|
|
99
|
+
--muted-foreground: 215.4 16.3% 46.9%;
|
|
100
|
+
--accent: 210 40% 96.1%;
|
|
101
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
102
|
+
--destructive: 0 84.2% 60.2%;
|
|
103
|
+
--destructive-foreground: 210 40% 98%;
|
|
104
|
+
--border: 214.3 31.8% 91.4%;
|
|
105
|
+
--input: 214.3 31.8% 91.4%;
|
|
106
|
+
--ring: 222.2 84% 4.9%;
|
|
107
|
+
--radius: 0.5rem;
|
|
108
|
+
}`,
|
|
32
109
|
};
|
|
33
110
|
|
|
34
111
|
const templatesDir = path.join(__dirname, "../templates");
|
|
@@ -129,13 +206,20 @@ const main = async () => {
|
|
|
129
206
|
}
|
|
130
207
|
|
|
131
208
|
const args = process.argv.slice(2);
|
|
132
|
-
|
|
133
|
-
let
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
209
|
+
const isAll = args.includes("--all");
|
|
210
|
+
let command = "add";
|
|
211
|
+
let componentName = "";
|
|
212
|
+
|
|
213
|
+
// Parse command & componentName from args (ignoring flags)
|
|
214
|
+
const positionalArgs = args.filter((a) => !a.startsWith("--"));
|
|
215
|
+
if (positionalArgs.length > 0) {
|
|
216
|
+
if (positionalArgs[0] === "init" || positionalArgs[0] === "add") {
|
|
217
|
+
command = positionalArgs[0];
|
|
218
|
+
componentName = positionalArgs[1] || "";
|
|
219
|
+
} else {
|
|
220
|
+
command = "add";
|
|
221
|
+
componentName = positionalArgs[0];
|
|
222
|
+
}
|
|
139
223
|
}
|
|
140
224
|
|
|
141
225
|
let availableComponents = Object.keys(REGISTRY);
|
|
@@ -146,102 +230,183 @@ const main = async () => {
|
|
|
146
230
|
if (dirs.length > 0) availableComponents = dirs;
|
|
147
231
|
}
|
|
148
232
|
|
|
149
|
-
if (command === "
|
|
150
|
-
|
|
233
|
+
if (command === "init") {
|
|
234
|
+
const response = await prompts({
|
|
235
|
+
type: "select",
|
|
236
|
+
name: "theme",
|
|
237
|
+
message: "Which theme would you like to use?",
|
|
238
|
+
choices: Object.keys(THEMES).map((t) => ({ title: t, value: t })),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!response.theme) {
|
|
242
|
+
console.log(chalk.red("❌ Theme selection cancelled."));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const themeCSS = THEMES[response.theme];
|
|
247
|
+
let cssPath = path.join(cwd, "src", "index.css");
|
|
248
|
+
|
|
249
|
+
if (env.framework === "next") {
|
|
250
|
+
if (env.isAppDir) {
|
|
251
|
+
cssPath = fs.existsSync(path.join(cwd, "src", "app"))
|
|
252
|
+
? path.join(cwd, "src", "app", "globals.css")
|
|
253
|
+
: path.join(cwd, "app", "globals.css");
|
|
254
|
+
} else {
|
|
255
|
+
cssPath = fs.existsSync(path.join(cwd, "src", "styles", "globals.css"))
|
|
256
|
+
? path.join(cwd, "src", "styles", "globals.css")
|
|
257
|
+
: path.join(cwd, "styles", "globals.css");
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
cssPath = fs.existsSync(path.join(cwd, "src", "index.css"))
|
|
261
|
+
? path.join(cwd, "src", "index.css")
|
|
262
|
+
: path.join(cwd, "src", "globals.css");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Ensure dir exists
|
|
266
|
+
const cssDir = path.dirname(cssPath);
|
|
267
|
+
if (!fs.existsSync(cssDir)) {
|
|
268
|
+
fs.mkdirSync(cssDir, { recursive: true });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let cssContent = "";
|
|
272
|
+
if (fs.existsSync(cssPath)) {
|
|
273
|
+
cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const hasTailwindDirectives =
|
|
277
|
+
cssContent.includes("@tailwind base") ||
|
|
278
|
+
cssContent.includes('@import "tailwindcss');
|
|
279
|
+
if (!hasTailwindDirectives) {
|
|
280
|
+
cssContent = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n${cssContent}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (
|
|
284
|
+
cssContent.includes(":root {") ||
|
|
285
|
+
cssContent.includes("--background:")
|
|
286
|
+
) {
|
|
287
|
+
console.log(
|
|
288
|
+
chalk.yellow(
|
|
289
|
+
`⚠️ CSS variables may already exist in ${cssPath}. Please append them manually if needed:\n\n@layer base {\n${themeCSS}\n}\n`,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
cssContent = `${cssContent}\n@layer base {\n${themeCSS}\n}\n`;
|
|
294
|
+
fs.writeFileSync(cssPath, cssContent);
|
|
295
|
+
console.log(
|
|
296
|
+
chalk.green(`✅ Theme '${response.theme}' initialized in ${cssPath}`),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} else if (command === "add") {
|
|
300
|
+
let componentsToAdd = [];
|
|
301
|
+
|
|
302
|
+
if (isAll) {
|
|
303
|
+
componentsToAdd = availableComponents;
|
|
304
|
+
} else if (componentName) {
|
|
305
|
+
componentsToAdd = [componentName];
|
|
306
|
+
} else {
|
|
151
307
|
const response = await prompts({
|
|
152
|
-
type: "
|
|
153
|
-
name: "
|
|
154
|
-
message:
|
|
308
|
+
type: "autocompleteMultiselect",
|
|
309
|
+
name: "components",
|
|
310
|
+
message:
|
|
311
|
+
"Which components would you like to add? (Space to select, Enter to submit, or use --all flag)",
|
|
155
312
|
choices: availableComponents.map((c) => ({ title: c, value: c })),
|
|
156
313
|
});
|
|
157
|
-
|
|
314
|
+
componentsToAdd = response.components || [];
|
|
158
315
|
}
|
|
159
316
|
|
|
160
|
-
if (
|
|
161
|
-
console.log(chalk.red("❌ Operation cancelled."));
|
|
317
|
+
if (componentsToAdd.length === 0) {
|
|
318
|
+
console.log(chalk.red("❌ No components selected. Operation cancelled."));
|
|
162
319
|
process.exit(1);
|
|
163
320
|
}
|
|
164
321
|
|
|
165
|
-
|
|
166
|
-
|
|
322
|
+
// Check availability
|
|
323
|
+
const notFound = componentsToAdd.filter(
|
|
324
|
+
(c) => !availableComponents.includes(c),
|
|
325
|
+
);
|
|
326
|
+
if (notFound.length > 0) {
|
|
327
|
+
console.log(chalk.red(`❌ Components not found: ${notFound.join(", ")}`));
|
|
167
328
|
process.exit(1);
|
|
168
329
|
}
|
|
169
330
|
|
|
170
|
-
const templatePath = path.join(templatesDir, componentName);
|
|
171
|
-
|
|
172
331
|
// Create target dir
|
|
173
332
|
const targetDir = path.join(cwd, env.componentsDir);
|
|
174
333
|
if (!fs.existsSync(targetDir)) {
|
|
175
334
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
176
335
|
}
|
|
177
336
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
path.join(templatePath, srcFile),
|
|
195
|
-
"utf-8",
|
|
337
|
+
let allDependencies = new Set();
|
|
338
|
+
|
|
339
|
+
for (const comp of componentsToAdd) {
|
|
340
|
+
const templatePath = path.join(templatesDir, comp);
|
|
341
|
+
const extension = env.typescript ? ".tsx" : ".jsx";
|
|
342
|
+
const componentFile = `${comp}${extension}`;
|
|
343
|
+
const targetPath = path.join(targetDir, componentFile);
|
|
344
|
+
|
|
345
|
+
const spinner = ora(`Installing ${comp}...`).start();
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
let sourceContent = "";
|
|
349
|
+
if (fs.existsSync(templatePath)) {
|
|
350
|
+
const files = fs.readdirSync(templatePath);
|
|
351
|
+
const srcFile = files.find(
|
|
352
|
+
(f) => f.endsWith(".tsx") || f.endsWith(".jsx"),
|
|
196
353
|
);
|
|
354
|
+
if (srcFile) {
|
|
355
|
+
sourceContent = fs.readFileSync(
|
|
356
|
+
path.join(templatePath, srcFile),
|
|
357
|
+
"utf-8",
|
|
358
|
+
);
|
|
359
|
+
} else if (files.length > 0) {
|
|
360
|
+
sourceContent = fs.readFileSync(
|
|
361
|
+
path.join(templatePath, files[0]),
|
|
362
|
+
"utf-8",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
197
365
|
} else {
|
|
198
|
-
sourceContent =
|
|
199
|
-
|
|
200
|
-
|
|
366
|
+
sourceContent = `import React from 'react';\n\nexport const ${REGISTRY[comp]?.name || "Component"} = () => { return <div>${comp}</div>; };\n`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Format code
|
|
370
|
+
let formattedContent = sourceContent;
|
|
371
|
+
try {
|
|
372
|
+
formattedContent = await formatFile(sourceContent, targetPath);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.log(
|
|
375
|
+
chalk.yellow(
|
|
376
|
+
`\n⚠️ Formatting failed for ${comp}. Using original formatting.`,
|
|
377
|
+
),
|
|
201
378
|
);
|
|
202
379
|
}
|
|
203
|
-
} else {
|
|
204
|
-
// Fallback or generic content simply to show it works
|
|
205
|
-
sourceContent = `import React from 'react';\n\nexport const ${REGISTRY[componentName]?.name || "Component"} = () => { return <div>${componentName}</div>; };\n`;
|
|
206
|
-
}
|
|
207
380
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Simple type stripping if needed, but omitted for stability.
|
|
211
|
-
// It's recommended to publish compiled JS if targeting non-TS,
|
|
212
|
-
// or let the user fix up the .jsx file.
|
|
213
|
-
}
|
|
381
|
+
fs.writeFileSync(targetPath, formattedContent, "utf-8");
|
|
382
|
+
spinner.succeed(`Created ${componentFile} in ${env.componentsDir}`);
|
|
214
383
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
384
|
+
// Track dependencies
|
|
385
|
+
const registryEntry = REGISTRY[comp];
|
|
386
|
+
if (registryEntry && registryEntry.dependencies) {
|
|
387
|
+
registryEntry.dependencies.forEach((d) => allDependencies.add(d));
|
|
388
|
+
}
|
|
219
389
|
} catch (err) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
390
|
+
spinner.fail(`Failed to add ${comp}`);
|
|
391
|
+
console.error(chalk.red(err.message));
|
|
223
392
|
}
|
|
393
|
+
}
|
|
224
394
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
installDependencies(registryEntry.dependencies, env.pkgCommand);
|
|
237
|
-
depSpinner.succeed(`Installed dependencies.`);
|
|
395
|
+
if (allDependencies.size > 0) {
|
|
396
|
+
const depsArray = Array.from(allDependencies);
|
|
397
|
+
const depSpinner = ora(
|
|
398
|
+
`Installing collective dependencies: ${depsArray.join(", ")}...`,
|
|
399
|
+
).start();
|
|
400
|
+
try {
|
|
401
|
+
installDependencies(depsArray, env.pkgCommand);
|
|
402
|
+
depSpinner.succeed(`Dependencies installed successfully.`);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
depSpinner.fail(`Failed to install dependencies.`);
|
|
405
|
+
console.error(chalk.red(err.message));
|
|
238
406
|
}
|
|
239
|
-
|
|
240
|
-
console.log(chalk.green(`\n✅ ${componentName} added successfully!`));
|
|
241
|
-
} catch (err) {
|
|
242
|
-
spinner.fail(`Failed to add ${componentName}`);
|
|
243
|
-
console.error(chalk.red(err.message));
|
|
244
407
|
}
|
|
408
|
+
|
|
409
|
+
console.log(chalk.green(`\n✅ Finished adding components!`));
|
|
245
410
|
} else {
|
|
246
411
|
console.log(chalk.red(`❌ Unknown command: ${command}`));
|
|
247
412
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cooterlabs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Component library CLI for CooterLabs apps",
|
|
5
5
|
"files": [
|
|
6
6
|
"bin",
|
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"chalk": "^5.6.2",
|
|
22
|
+
"clsx": "^2.1.1",
|
|
23
|
+
"lucide-react": "^0.575.0",
|
|
22
24
|
"ora": "^9.3.0",
|
|
23
25
|
"prettier": "^3.8.1",
|
|
24
|
-
"prompts": "^2.4.2"
|
|
26
|
+
"prompts": "^2.4.2",
|
|
27
|
+
"tailwind-merge": "^3.5.0"
|
|
25
28
|
}
|
|
26
|
-
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Avatar = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<AvatarPrimitive.Root
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
))
|
|
23
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName
|
|
24
|
+
|
|
25
|
+
const AvatarImage = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<AvatarPrimitive.Image
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn("aspect-square h-full w-full", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
))
|
|
35
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
|
36
|
+
|
|
37
|
+
const AvatarFallback = React.forwardRef<
|
|
38
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
39
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
40
|
+
>(({ className, ...props }, ref) => (
|
|
41
|
+
<AvatarPrimitive.Fallback
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
|
45
|
+
className
|
|
46
|
+
)}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
))
|
|
50
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
|
51
|
+
|
|
52
|
+
export { Avatar, AvatarImage, AvatarFallback }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { clsx, type ClassValue } from "clsx"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
variant?: "default" | "secondary" | "destructive" | "outline"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const badgeVariants = {
|
|
14
|
+
default:
|
|
15
|
+
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
|
16
|
+
secondary:
|
|
17
|
+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
18
|
+
destructive:
|
|
19
|
+
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
20
|
+
outline: "text-foreground",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={cn(
|
|
27
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
28
|
+
badgeVariants[variant],
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { Badge }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "./button" // Assuming they have cn utility somewhere accessible, or we'll rewrite it to inline
|
|
4
|
+
|
|
5
|
+
export function cnFallback(...classes: (string | undefined | null | false)[]) {
|
|
6
|
+
return classes.filter(Boolean).join(" ")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// In a real scenario you'd import 'cn' from a library or a utils file.
|
|
10
|
+
// We'll provide an inline cn for fallback or rely on the user having ones installed:
|
|
11
|
+
import { clsx, type ClassValue } from "clsx"
|
|
12
|
+
import { twMerge } from "tailwind-merge"
|
|
13
|
+
|
|
14
|
+
function localCn(...inputs: ClassValue[]) {
|
|
15
|
+
return twMerge(clsx(inputs))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Card = React.forwardRef<
|
|
19
|
+
HTMLDivElement,
|
|
20
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
21
|
+
>(({ className, ...props }, ref) => (
|
|
22
|
+
<div
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={localCn(
|
|
25
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
))
|
|
31
|
+
Card.displayName = "Card"
|
|
32
|
+
|
|
33
|
+
const CardHeader = React.forwardRef<
|
|
34
|
+
HTMLDivElement,
|
|
35
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<div
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={localCn("flex flex-col space-y-1.5 p-6", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
))
|
|
43
|
+
CardHeader.displayName = "CardHeader"
|
|
44
|
+
|
|
45
|
+
const CardTitle = React.forwardRef<
|
|
46
|
+
HTMLParagraphElement,
|
|
47
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
48
|
+
>(({ className, ...props }, ref) => (
|
|
49
|
+
<h3
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={localCn("font-semibold leading-none tracking-tight", className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
))
|
|
55
|
+
CardTitle.displayName = "CardTitle"
|
|
56
|
+
|
|
57
|
+
const CardDescription = React.forwardRef<
|
|
58
|
+
HTMLParagraphElement,
|
|
59
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
60
|
+
>(({ className, ...props }, ref) => (
|
|
61
|
+
<p
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={localCn("text-sm text-muted-foreground", className)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
))
|
|
67
|
+
CardDescription.displayName = "CardDescription"
|
|
68
|
+
|
|
69
|
+
const CardContent = React.forwardRef<
|
|
70
|
+
HTMLDivElement,
|
|
71
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
72
|
+
>(({ className, ...props }, ref) => (
|
|
73
|
+
<div ref={ref} className={localCn("p-6 pt-0", className)} {...props} />
|
|
74
|
+
))
|
|
75
|
+
CardContent.displayName = "CardContent"
|
|
76
|
+
|
|
77
|
+
const CardFooter = React.forwardRef<
|
|
78
|
+
HTMLDivElement,
|
|
79
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
80
|
+
>(({ className, ...props }, ref) => (
|
|
81
|
+
<div
|
|
82
|
+
ref={ref}
|
|
83
|
+
className={localCn("flex items-center p-6 pt-0", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
))
|
|
87
|
+
CardFooter.displayName = "CardFooter"
|
|
88
|
+
|
|
89
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
3
|
+
import { Check } from "lucide-react"
|
|
4
|
+
import { clsx, type ClassValue } from "clsx"
|
|
5
|
+
import { twMerge } from "tailwind-merge"
|
|
6
|
+
|
|
7
|
+
export function cn(...inputs: ClassValue[]) {
|
|
8
|
+
return twMerge(clsx(inputs))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Checkbox = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<CheckboxPrimitive.Root
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<CheckboxPrimitive.Indicator
|
|
24
|
+
className={cn("flex items-center justify-center text-current")}
|
|
25
|
+
>
|
|
26
|
+
<Check className="h-4 w-4" />
|
|
27
|
+
</CheckboxPrimitive.Indicator>
|
|
28
|
+
</CheckboxPrimitive.Root>
|
|
29
|
+
))
|
|
30
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
|
31
|
+
|
|
32
|
+
export { Checkbox }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Label = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof LabelPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<LabelPrimitive.Root
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
))
|
|
23
|
+
Label.displayName = LabelPrimitive.Root.displayName
|
|
24
|
+
|
|
25
|
+
export { Label }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { clsx, type ClassValue } from "clsx"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TextareaProps
|
|
10
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
|
|
11
|
+
|
|
12
|
+
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
13
|
+
({ className, ...props }, ref) => {
|
|
14
|
+
return (
|
|
15
|
+
<textarea
|
|
16
|
+
className={cn(
|
|
17
|
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
ref={ref}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
Textarea.displayName = "Textarea"
|
|
27
|
+
|
|
28
|
+
export { Textarea }
|