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 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
- let command = args[0];
133
- let componentName = args[1];
134
-
135
- if (!command || command !== "add") {
136
- // If command isn't provided or is entirely missing, treat the first arg as the component name
137
- command = "add";
138
- componentName = args[0] || "";
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 === "add") {
150
- if (!componentName) {
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: "select",
153
- name: "component",
154
- message: "Which component would you like to add?",
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
- componentName = response.component;
314
+ componentsToAdd = response.components || [];
158
315
  }
159
316
 
160
- if (!componentName) {
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
- if (!availableComponents.includes(componentName)) {
166
- console.log(chalk.red(`❌ Component '${componentName}' not found.`));
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
- // Determine target path
179
- const extension = env.typescript ? ".tsx" : ".jsx";
180
- const componentFile = `${componentName}${extension}`;
181
- const targetPath = path.join(targetDir, componentFile);
182
-
183
- const spinner = ora(`Installing ${componentName}...`).start();
184
-
185
- try {
186
- let sourceContent = "";
187
- if (fs.existsSync(templatePath)) {
188
- const files = fs.readdirSync(templatePath);
189
- const srcFile = files.find(
190
- (f) => f.endsWith(".tsx") || f.endsWith(".jsx"),
191
- );
192
- if (srcFile) {
193
- sourceContent = fs.readFileSync(
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 = fs.readFileSync(
199
- path.join(templatePath, files[0]),
200
- "utf-8",
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
- // Hacky TS -> JS stripping if they don't use TypeScript
209
- if (!env.typescript) {
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
- // Format code
216
- let formattedContent = sourceContent;
217
- try {
218
- formattedContent = await formatFile(sourceContent, targetPath);
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
- console.log(
221
- chalk.yellow("⚠️ Formatting failed. Using original formatting."),
222
- );
390
+ spinner.fail(`Failed to add ${comp}`);
391
+ console.error(chalk.red(err.message));
223
392
  }
393
+ }
224
394
 
225
- // Write file
226
- fs.writeFileSync(targetPath, formattedContent, "utf-8");
227
-
228
- spinner.succeed(`Created ${componentFile} in ${env.componentsDir}`);
229
-
230
- // Install dependencies
231
- const registryEntry = REGISTRY[componentName];
232
- if (registryEntry && registryEntry.dependencies?.length > 0) {
233
- const depSpinner = ora(
234
- `Installing dependencies: ${registryEntry.dependencies.join(", ")}...`,
235
- ).start();
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.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 }