cooterlabs 1.0.1 → 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.
Files changed (3) hide show
  1. package/README.md +23 -4
  2. package/bin/index.js +211 -81
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,15 +17,26 @@ It brings components directly to you. We do not provide a generic NPM library pa
17
17
 
18
18
  1. Open your terminal at the root of your project directory.
19
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
- 3. Run the CLI tool:
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:
21
24
 
22
25
  ```bash
23
- npx cooterlabs add
26
+ npx cooterlabs init
24
27
  ```
25
28
 
26
- ### Direct Component Install
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
+ ```
27
38
 
28
- If you know exactly what component you want, just bypass the interactive wizard and supply the name:
39
+ If you know exactly what component you want, bypass the interactive wizard and supply the name:
29
40
 
30
41
  ```bash
31
42
  # Example: Adding a Button
@@ -35,6 +46,14 @@ npx cooterlabs add button
35
46
  npx cooterlabs add avatar
36
47
  ```
37
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
+
38
57
  ## Available Components
39
58
 
40
59
  Currently, the CooterLabs Library ships with the following carefully designed boilerplate components:
package/bin/index.js CHANGED
@@ -56,16 +56,58 @@ const REGISTRY = {
56
56
  },
57
57
  checkbox: {
58
58
  name: "Checkbox",
59
- dependencies: [
60
- "lucide-react",
61
- "clsx",
62
- "tailwind-merge",
63
- "@radix-ui/react-checkbox",
64
- ],
59
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
65
60
  dev_dependencies: [],
66
61
  },
67
62
  };
68
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
+ }`,
109
+ };
110
+
69
111
  const templatesDir = path.join(__dirname, "../templates");
70
112
 
71
113
  const detectEnvironment = () => {
@@ -164,13 +206,20 @@ const main = async () => {
164
206
  }
165
207
 
166
208
  const args = process.argv.slice(2);
167
- let command = args[0];
168
- let componentName = args[1];
169
-
170
- if (!command || command !== "add") {
171
- // If command isn't provided or is entirely missing, treat the first arg as the component name
172
- command = "add";
173
- 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
+ }
174
223
  }
175
224
 
176
225
  let availableComponents = Object.keys(REGISTRY);
@@ -181,102 +230,183 @@ const main = async () => {
181
230
  if (dirs.length > 0) availableComponents = dirs;
182
231
  }
183
232
 
184
- if (command === "add") {
185
- 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 {
186
307
  const response = await prompts({
187
- type: "select",
188
- name: "component",
189
- 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)",
190
312
  choices: availableComponents.map((c) => ({ title: c, value: c })),
191
313
  });
192
- componentName = response.component;
314
+ componentsToAdd = response.components || [];
193
315
  }
194
316
 
195
- if (!componentName) {
196
- console.log(chalk.red("❌ Operation cancelled."));
317
+ if (componentsToAdd.length === 0) {
318
+ console.log(chalk.red("❌ No components selected. Operation cancelled."));
197
319
  process.exit(1);
198
320
  }
199
321
 
200
- if (!availableComponents.includes(componentName)) {
201
- 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(", ")}`));
202
328
  process.exit(1);
203
329
  }
204
330
 
205
- const templatePath = path.join(templatesDir, componentName);
206
-
207
331
  // Create target dir
208
332
  const targetDir = path.join(cwd, env.componentsDir);
209
333
  if (!fs.existsSync(targetDir)) {
210
334
  fs.mkdirSync(targetDir, { recursive: true });
211
335
  }
212
336
 
213
- // Determine target path
214
- const extension = env.typescript ? ".tsx" : ".jsx";
215
- const componentFile = `${componentName}${extension}`;
216
- const targetPath = path.join(targetDir, componentFile);
217
-
218
- const spinner = ora(`Installing ${componentName}...`).start();
219
-
220
- try {
221
- let sourceContent = "";
222
- if (fs.existsSync(templatePath)) {
223
- const files = fs.readdirSync(templatePath);
224
- const srcFile = files.find(
225
- (f) => f.endsWith(".tsx") || f.endsWith(".jsx"),
226
- );
227
- if (srcFile) {
228
- sourceContent = fs.readFileSync(
229
- path.join(templatePath, srcFile),
230
- "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"),
231
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
+ }
232
365
  } else {
233
- sourceContent = fs.readFileSync(
234
- path.join(templatePath, files[0]),
235
- "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
+ ),
236
378
  );
237
379
  }
238
- } else {
239
- // Fallback or generic content simply to show it works
240
- sourceContent = `import React from 'react';\n\nexport const ${REGISTRY[componentName]?.name || "Component"} = () => { return <div>${componentName}</div>; };\n`;
241
- }
242
380
 
243
- // Hacky TS -> JS stripping if they don't use TypeScript
244
- if (!env.typescript) {
245
- // Simple type stripping if needed, but omitted for stability.
246
- // It's recommended to publish compiled JS if targeting non-TS,
247
- // or let the user fix up the .jsx file.
248
- }
381
+ fs.writeFileSync(targetPath, formattedContent, "utf-8");
382
+ spinner.succeed(`Created ${componentFile} in ${env.componentsDir}`);
249
383
 
250
- // Format code
251
- let formattedContent = sourceContent;
252
- try {
253
- 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
+ }
254
389
  } catch (err) {
255
- console.log(
256
- chalk.yellow("⚠️ Formatting failed. Using original formatting."),
257
- );
390
+ spinner.fail(`Failed to add ${comp}`);
391
+ console.error(chalk.red(err.message));
258
392
  }
393
+ }
259
394
 
260
- // Write file
261
- fs.writeFileSync(targetPath, formattedContent, "utf-8");
262
-
263
- spinner.succeed(`Created ${componentFile} in ${env.componentsDir}`);
264
-
265
- // Install dependencies
266
- const registryEntry = REGISTRY[componentName];
267
- if (registryEntry && registryEntry.dependencies?.length > 0) {
268
- const depSpinner = ora(
269
- `Installing dependencies: ${registryEntry.dependencies.join(", ")}...`,
270
- ).start();
271
- installDependencies(registryEntry.dependencies, env.pkgCommand);
272
- 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));
273
406
  }
274
-
275
- console.log(chalk.green(`\n✅ ${componentName} added successfully!`));
276
- } catch (err) {
277
- spinner.fail(`Failed to add ${componentName}`);
278
- console.error(chalk.red(err.message));
279
407
  }
408
+
409
+ console.log(chalk.green(`\n✅ Finished adding components!`));
280
410
  } else {
281
411
  console.log(chalk.red(`❌ Unknown command: ${command}`));
282
412
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cooterlabs",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Component library CLI for CooterLabs apps",
5
5
  "files": [
6
6
  "bin",