cooterlabs 2.0.0 → 2.1.0

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 CHANGED
@@ -1,124 +1,263 @@
1
1
  # CooterLabs UI Library CLI
2
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!
3
+ A standalone CLI for scaffolding the **Aeon Product Design System** — beautiful, accessible, highly customizable React components directly into your React, Vite, or Next.js project.
4
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.
5
+ It brings components directly to you. This is **not** an importable NPM package: the actual React TS/JS source is scaffolded straight into your `src/components/ui` folder, so you completely own the implementation, styles, and markup. Think shadcn/ui, but for the Aeon design system.
6
+
7
+ ```bash
8
+ npx cooterlabs init # set up the theme
9
+ npx cooterlabs add button # scaffold a component
10
+ ```
11
+
12
+ ---
13
+
14
+ ## Table of contents
15
+
16
+ - [Features](#features)
17
+ - [Requirements](#requirements)
18
+ - [Commands](#commands)
19
+ - [`init` — theme setup](#init--theme-setup)
20
+ - [`add` — scaffold components](#add--scaffold-components)
21
+ - [The shared `cn()` utility](#the-shared-cn-utility)
22
+ - [The Aeon theme](#the-aeon-theme)
23
+ - [Semantic colors](#semantic-colors)
24
+ - [Primitive color scales](#primitive-color-scales)
25
+ - [Typography ramp](#typography-ramp)
26
+ - [Radius, elevation & focus ring](#radius-elevation--focus-ring)
27
+ - [Component reference](#component-reference)
28
+ - [How it works under the hood](#how-it-works-under-the-hood)
29
+ - [Changelog](#changelog)
30
+ - [License](#license)
31
+
32
+ ---
6
33
 
7
34
  ## Features
8
35
 
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.
36
+ - **Project detection** — auto-detects your ecosystem: Next.js (App router or Pages router), Vite, or Create React App, plus your package manager (`npm`, `pnpm`, `yarn`, `bun`) from lockfiles.
37
+ - **TypeScript vs. JavaScript** installs `.tsx` component files when TypeScript is detected (via `typescript` dependency or `tsconfig.json`), falling back to `.jsx` otherwise.
38
+ - **Tailwind awareness** — warns if Tailwind CSS isn't detected, since all components are styled with Tailwind utilities.
39
+ - **Dependency auto-install** each component's runtime dependencies (`@radix-ui/*`, `lucide-react`, `clsx`, `tailwind-merge`) are resolved and installed automatically, collectively, in one pass.
40
+ - **Component dependencies** components that build on other components (e.g. `select` reuses `input`'s variant styles) automatically pull in what they need.
41
+ - **Shared `cn()` helper** a single `src/lib/utils.ts` is scaffolded once and shared by every component, with a tailwind-merge config that understands the Aeon type ramp (see [below](#the-shared-cn-utility)).
42
+ - **Prettier formatting** — components are formatted with *your* project's Prettier config before being written, so they match your code style from the first commit.
43
+ - **Interactive interface** — run without arguments to get an autocomplete multi-select of every available component.
15
44
 
16
- ## Installation / Usage
45
+ ## Requirements
17
46
 
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).
47
+ - **React 18+** project (Vite, Next.js, or CRA).
48
+ - **Tailwind CSS v4** for the default `aeon` theme (it's emitted as a v4 `@theme` block). The legacy `zinc` / `slate` themes target Tailwind v3 / shadcn-style HSL variables.
49
+ - Node 18+ (the CLI is an ES module).
20
50
 
21
- ### Initialization (Themes & CSS Base)
51
+ ---
22
52
 
23
- To swiftly set up a cohesive Tailwind theme and properly configure your base CSS file (e.g. `index.css` or `globals.css`), run:
53
+ ## Commands
54
+
55
+ ### `init` — theme setup
56
+
57
+ Sets up the design-system CSS variables in your base stylesheet (`src/index.css`, `app/globals.css`, or `styles/globals.css` — resolved automatically per framework):
24
58
 
25
59
  ```bash
26
- npx cooterlabs init
60
+ npx cooterlabs init # interactive theme picker
61
+ npx cooterlabs init aeon # non-interactive, applies the default theme
27
62
  ```
28
63
 
29
- This will give you a list of carefully curated designer themes to apply global default variables (background, foreground, primary borders, etc.) out-of-the-box.
64
+ What it does:
65
+
66
+ 1. Locates (or creates) your base CSS file.
67
+ 2. Ensures the Tailwind entry point exists — `@import "tailwindcss";` for v4 themes, the classic `@tailwind base/components/utilities` trio for legacy themes.
68
+ 3. Appends the theme block. For **aeon** this is a Tailwind v4 `@theme static { … }` block plus the `alert-timer` keyframes used by the Alert component's auto-dismiss countdown.
69
+ 4. If theme variables already exist in the file, it prints the block for you to merge manually instead of clobbering your file.
30
70
 
31
- The default theme is **`aeon`** — the complete Aeon Product Design System as a Tailwind CSS v4 `@theme` block. It ships:
71
+ Available themes:
32
72
 
33
- - **Semantic colors**: `primary` (deep teal `#0d3435`), `secondary`, `muted`, `accent`, `destructive`, `border`, `ring`, plus `success` / `info` / `warning` / `error` — each with a full 50–900 primitive scale (`primary-50`…`primary-900`, `grey-*`, `success-*`, …).
34
- - **Text sizes as Tailwind CSS variables**: the full Aeon type ramp is exposed as `--text-*` theme variables, giving you utilities like `text-h1`…`text-h5` (headings), `text-s1`/`text-s2` (subtitles), `text-b1`…`text-b4` (body), `text-c1`…`text-c3` (captions), `text-label`, and button sizes `text-btn-giant`…`text-btn-tiny` — each with the correct size, line-height, and weight baked in.
35
- - **Radius scale**: `rounded-xxs` (4px) through `rounded-xl` (24px).
36
- - **Elevation**: `shadow-100` through `shadow-800`.
37
- - **Font**: Inter.
73
+ | Theme | Format | Description |
74
+ |---|---|---|
75
+ | `aeon` *(default)* | Tailwind v4 `@theme` | The full Aeon Product Design System — colors, type ramp, radii, elevation, focus ring. |
76
+ | `zinc` | v3 / shadcn HSL `:root` vars | Legacy neutral theme. |
77
+ | `slate` | v3 / shadcn HSL `:root` vars | Legacy cool-grey theme. |
38
78
 
39
- To apply it non-interactively:
79
+ ### `add` scaffold components
40
80
 
41
81
  ```bash
42
- npx cooterlabs init aeon
82
+ npx cooterlabs add # interactive multi-select
83
+ npx cooterlabs add button # one component
84
+ npx cooterlabs add card badge # ⚠️ one at a time — run twice, or use the picker
85
+ npx cooterlabs add --all # everything
43
86
  ```
44
87
 
45
- Legacy `zinc` and `slate` themes (Tailwind v3 / shadcn HSL format) remain available.
88
+ What it does, per run:
46
89
 
47
- ### Adding Components
90
+ 1. Detects framework, TypeScript, Tailwind, and package manager.
91
+ 2. Expands component requirements — e.g. `add select` also scaffolds `input`, because `select.tsx` imports its shared variant styles from `./input`.
92
+ 3. Scaffolds `src/lib/utils.ts` (the shared `cn()` helper) if it doesn't exist yet.
93
+ 4. Copies each component into `src/components/ui/` (or `components/ui/` for Next.js without `src/`), formatted with your Prettier config.
94
+ 5. Installs the union of all required runtime dependencies in a single package-manager invocation.
48
95
 
49
- Run the CLI tool without arguments to get an interactive autocomplete prompt:
96
+ Existing files with the same name are overwritten — re-running `add` is also how you *update* a component to the latest version (diff your local changes first if you've customized it).
50
97
 
51
- ```bash
52
- npx cooterlabs add
98
+ ---
99
+
100
+ ## The shared `cn()` utility
101
+
102
+ Every component imports one helper:
103
+
104
+ ```ts
105
+ import { cn } from "../../lib/utils"
53
106
  ```
54
107
 
55
- If you know exactly what component you want, bypass the interactive wizard and supply the name:
108
+ The CLI scaffolds this file once, at `src/lib/utils.ts` (or `lib/utils.js`). It is **not** the stock shadcn two-liner it uses `extendTailwindMerge` to teach tailwind-merge the Aeon font-size ramp:
56
109
 
57
- ```bash
58
- # Example: Adding a Button
59
- npx cooterlabs add button
110
+ ```ts
111
+ import { clsx, type ClassValue } from "clsx"
112
+ import { extendTailwindMerge } from "tailwind-merge"
113
+
114
+ const twMerge = extendTailwindMerge({
115
+ extend: {
116
+ classGroups: {
117
+ "font-size": [{ text: ["h1", …, "b4", …, "btn-tiny"] }],
118
+ },
119
+ },
120
+ })
60
121
 
61
- # Example: Adding an Avatar
62
- npx cooterlabs add avatar
122
+ export function cn(...inputs: ClassValue[]) {
123
+ return twMerge(clsx(inputs))
124
+ }
63
125
  ```
64
126
 
65
- ### Install All Components At Once
127
+ **Why this matters:** without it, tailwind-merge treats custom utilities like `text-b4` as text *colors* and silently drops real color classes like `text-white` when they appear together. If you already have your own `lib/utils`, the CLI won't overwrite it — but make sure it includes this classGroups extension or component styles will subtly break.
66
128
 
67
- Need a complete starting block? Add the `--all` flag to scaffold the entire library in one go:
129
+ ---
68
130
 
69
- ```bash
70
- npx cooterlabs add --all
131
+ ## The Aeon theme
132
+
133
+ Running `npx cooterlabs init aeon` gives you the complete design system as CSS variables, which Tailwind v4 turns into utilities automatically.
134
+
135
+ ### Semantic colors
136
+
137
+ `background`, `foreground`, `card`, `popover`, `primary` (deep teal `#0d3435`), `secondary`, `muted`, `accent`, `destructive`, `border`, `input`, `ring` — plus status colors `success`, `info`, `warning`, `error`, each with a `-foreground` pair. Use them as `bg-primary`, `text-muted-foreground`, `border-border`, etc.
138
+
139
+ ### Primitive color scales
140
+
141
+ Full 50–900 scales for `primary-*`, `grey-*`, `success-*`, `error-*`, `warning-*`, and `info-*` — e.g. `bg-primary-50`, `text-grey-500`, `border-error-200`. Components use these for hover/active/disabled states, so keep them if you retheme.
142
+
143
+ ### Typography ramp
144
+
145
+ The entire Aeon type ramp ships as `--text-*` variables, each with size, line-height, **and weight** baked in:
146
+
147
+ | Utilities | Role |
148
+ |---|---|
149
+ | `text-h1` … `text-h5` | Headings (48px/600 → 24px/600) |
150
+ | `text-s1`, `text-s2` | Subtitles |
151
+ | `text-b1` … `text-b4` | Body (regular & medium, 16px & 14px) |
152
+ | `text-c1` … `text-c3` | Captions (12px & 10px) |
153
+ | `text-label` | Form labels |
154
+ | `text-btn-giant` … `text-btn-tiny` | Button text sizes |
155
+
156
+ Font family: **Inter** (`--font-sans`). Load the font yourself (e.g. via Google Fonts or `@fontsource/inter`) — the theme only declares the stack.
157
+
158
+ ### Radius, elevation & focus ring
159
+
160
+ - **Radius:** `rounded-xxs` (4px) → `rounded-xl` (24px).
161
+ - **Elevation:** `shadow-100` → `shadow-800`.
162
+ - **Focus ring:** `--shadow-focus` (a 3px `#c2f5f6` halo) — every interactive component uses `focus-visible:shadow-focus` for a consistent, accessible focus style.
163
+ - **`@keyframes alert-timer`:** powers the Alert component's auto-dismiss countdown bar; emitted alongside the theme block.
164
+
165
+ ---
166
+
167
+ ## Component reference
168
+
169
+ All components live in `components/ui/`, are fully typed, and follow the Aeon size ramp (`giant / large / medium / small / tiny`) where applicable. Radix-based components keep the full Radix composition API.
170
+
171
+ ### Form controls
172
+
173
+ | Component | Exports | Key props / notes |
174
+ |---|---|---|
175
+ | `button` | `Button` | `variant`: `filled` \| `outline` \| `clear` · `size`: giant→tiny · `iconOnly` · `asChild` (Radix `Slot`). |
176
+ | `button-group` | `ButtonGroup`, `ButtonGroupItem` | Segmented control matching the button size ramp. |
177
+ | `input` | `Input` (+ exported style maps reused by `select`) | `variant`: `outline` \| `filled` · `size`: `large` \| `medium` · `status`: success/info/warning/error · `helperText` · `icon`. |
178
+ | `textarea` | `Textarea` | Multiline input matching `input` styling. |
179
+ | `select` | `Select`, `SelectTrigger`, `SelectContent`, `SelectItem`, `SelectValue`, `SelectGroup`, … | Radix select styled as an Aeon input; **requires `input`** (auto-installed). |
180
+ | `label` | `Label` | Radix label. |
181
+ | `checkbox` | `Checkbox` | Radix checkbox with indeterminate support. |
182
+ | `radio` | `RadioGroup`, `RadioGroupItem` | Radix radio group. |
183
+ | `toggle` | `Toggle` | Radix switch. |
184
+
185
+ ### Feedback
186
+
187
+ | Component | Exports | Key props / notes |
188
+ |---|---|---|
189
+ | `alert` | `Alert`, `AlertTitle`, `AlertDescription`, `AlertActions` | `severity`: default/success/info/warning/error · `variant`: `filled` \| `outline` · `position` (fixed placements) · `duration` (auto-dismiss seconds, pauses on hover, animated countdown) · `onDismiss`. |
190
+ | `badge` | `Badge` | `type`: `badge` \| `chip` (chips are dismissible/selectable) · `variant`: `filled` \| `outline` · `color`: default/success/info/warning/error · `size`: medium/small/tiny · `iconOnly`. |
191
+ | `avatar` | `Avatar`, `AvatarImage`, `AvatarFallback` | Radix avatar, `size`: giant→tiny. |
192
+ | `progress` | `Progress` | Radix progress; `label`: `left` \| `right` percentage label · `disabled`. |
193
+ | `loader` | `Loader` | CSS spinner, `size`: giant→tiny. |
194
+ | `tooltip` | `TooltipProvider`, `Tooltip`, `TooltipTrigger`, `TooltipContent` | Radix dark tooltip. |
195
+
196
+ ### Overlays
197
+
198
+ | Component | Exports | Key props / notes |
199
+ |---|---|---|
200
+ | `dialog` | `Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter`, … | Radix dialog (Pop-Up). |
201
+ | `action-sheet` | `ActionSheet`, `ActionSheetTrigger`, `ActionSheetContent`, … | Mobile bottom sheet built on Radix dialog. |
202
+ | `context-menu` | `ContextMenu`, `ContextMenuTrigger`, `ContextMenuContent`, `ContextMenuItem`, checkbox/radio items, … | Radix context menu. |
203
+ | `dropdown` | `DropdownMenu`, `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem`, … | Radix dropdown menu. |
204
+
205
+ ### Navigation
206
+
207
+ | Component | Exports | Key props / notes |
208
+ |---|---|---|
209
+ | `tabs` | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | Radix tabs. |
210
+ | `breadcrumb` | `Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage`, `BreadcrumbSeparator`, … | Accessible breadcrumb trail. |
211
+ | `pagination` | `Pagination`, `PaginationContent`, `PaginationItem`, `PaginationLink`, `PaginationPrevious`, `PaginationNext`, … | Page navigation. |
212
+ | `page-control` | `PageControl` | Mobile page-indicator dots. |
213
+ | `stepper` | `Stepper`, `StepperItem`, `StepperSeparator` | Horizontal step indicator. |
214
+ | `navbar-top` | `NavbarTop`, `NavbarTopLeft`, `NavbarTopTitle`, `NavbarTopRight` | Mobile top app bar. |
215
+ | `navbar-bottom` | `NavbarBottom`, `NavbarBottomItem` | Mobile bottom tab bar. |
216
+ | `list` | `List`, `ListHeader`, `ListItem` | Mobile list rows with leading/trailing slots. |
217
+
218
+ ### Layout
219
+
220
+ | Component | Exports | Key props / notes |
221
+ |---|---|---|
222
+ | `card` | `Card`, `CardImage`, `CardHeader`, `CardFooter`, `CardTitle`, `CardDescription`, … | Composable card wrappers. |
223
+
224
+ ### Example
225
+
226
+ ```tsx
227
+ import { Button } from "@/components/ui/button"
228
+ import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
229
+
230
+ export function Example() {
231
+ return (
232
+ <>
233
+ <Button variant="filled" size="large">Save changes</Button>
234
+ <Alert severity="success" duration={5} onDismiss={() => {}}>
235
+ <AlertTitle>Saved</AlertTitle>
236
+ <AlertDescription>Your changes are live.</AlertDescription>
237
+ </Alert>
238
+ </>
239
+ )
240
+ }
71
241
  ```
72
242
 
73
- ## Available Components
74
-
75
- The full Aeon Product Design System, scaffolded as owned code:
76
-
77
- **Form controls**
78
- - `button` - Filled / outline / clear styles in five sizes (gianttiny), icon-only mode, Radix `Slot` support.
79
- - `button-group` - Segmented control matching the button size ramp.
80
- - `input` - Aeon-styled HTML `<input />` with error and disabled states.
81
- - `textarea` - Matching multiline input.
82
- - `label` - Accessible Radix-backed `<label />`.
83
- - `checkbox` - Radix checkbox with indeterminate support.
84
- - `radio` - Radix radio group.
85
- - `toggle` - Radix switch.
86
-
87
- **Feedback**
88
- - `alert` - Filled and soft appearances across default / success / info / warning / error severities.
89
- - `badge` - Status pills plus dismissible `Chip`.
90
- - `avatar` - Radix avatar with the Aeon size ramp.
91
- - `progress` - Radix progress bar.
92
- - `loader` - CSS spinner in five sizes.
93
- - `tooltip` - Radix dark tooltip.
94
-
95
- **Overlays**
96
- - `dialog` - Radix dialog (Pop-Up).
97
- - `action-sheet` - Mobile bottom sheet built on Radix dialog.
98
- - `context-menu` - Radix context menu.
99
- - `dropdown` - Radix dropdown menu.
100
- - `list` - Mobile list rows with leading/trailing slots.
101
-
102
- **Navigation**
103
- - `tabs` - Radix tabs.
104
- - `breadcrumb` - Accessible breadcrumb trail.
105
- - `pagination` - Page navigation.
106
- - `page-control` - Mobile page-indicator dots.
107
- - `stepper` - Horizontal step indicator.
108
- - `navbar-top` - Mobile top app bar.
109
- - `navbar-bottom` - Mobile bottom tab bar.
110
-
111
- **Layout**
112
- - `card` - Header, footer, body, title, and description wrappers.
113
-
114
- ## How it works under-the-hood
115
-
116
- 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.
117
- 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.
118
- 3. It recursively creates target folders, usually resolving to `src/components/ui` or `components/ui`.
119
- 4. Formatting is applied.
120
- 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.
243
+ ---
244
+
245
+ ## How it works under the hood
246
+
247
+ 1. `npx cooterlabs add [component]` inspects your filesystem: lockfiles for the package manager, `package.json` deps for the framework, `tsconfig.json` for TypeScript, and Tailwind config/deps for styling support.
248
+ 2. Component requirements are expanded (`select``input`), then the shared `lib/utils` helper is scaffolded if missing.
249
+ 3. Templates are copied from the CLI package into `src/components/ui/` (or `components/ui/`), run through your Prettier config, and written with the right extension.
250
+ 4. The union of every selected component's runtime dependencies is installed in one package-manager call, so you only get libraries the components actually use.
251
+
252
+ ## Changelog
253
+
254
+ ### 2.1.0
255
+
256
+ - **Synced all 28 component templates** with the latest Aeon implementations (updated button/badge/alert variants, refined focus states using `shadow-focus`, tailwind-merge-safe type-ramp classes).
257
+ - **New component: `select`** — Radix select styled as an Aeon input; auto-installs `input`, whose variant styles it reuses.
258
+ - **Shared `cn()` helper** components now import from `lib/utils` instead of inlining `cn` per file; the CLI scaffolds it (with the type-ramp-aware tailwind-merge config) on first `add`.
259
+ - **Theme update** — the `aeon` theme now ships `--shadow-focus` (shared focus ring) and the `alert-timer` keyframes for Alert auto-dismiss.
121
260
 
122
261
  ## License
123
262
 
124
- MIT - Grab it, use it, scale it!
263
+ MIT Grab it, use it, scale it!
package/bin/index.js CHANGED
@@ -174,8 +174,26 @@ const REGISTRY = {
174
174
  ],
175
175
  dev_dependencies: [],
176
176
  },
177
+ select: {
178
+ name: "Select",
179
+ dependencies: [
180
+ "@radix-ui/react-select",
181
+ "lucide-react",
182
+ "clsx",
183
+ "tailwind-merge",
184
+ ],
185
+ dev_dependencies: [],
186
+ requires: ["input"], // select.tsx imports shared variants from ./input
187
+ },
177
188
  };
178
189
 
190
+ // Shared cn() helper scaffolded to src/lib/utils — components import it
191
+ // from "../../lib/utils" instead of inlining it per file.
192
+ const UTILS_TEMPLATE = fs.readFileSync(
193
+ path.join(__dirname, "../templates/_lib/utils.ts"),
194
+ "utf-8",
195
+ );
196
+
179
197
  const THEMES = {
180
198
  aeon: `@theme static {
181
199
  /* ——— Aeon Product Design System ——— */
@@ -351,6 +369,15 @@ const THEMES = {
351
369
  --shadow-600: 0 12px 42px -4px #1319271f, 0 8px 18px -6px #1319271f;
352
370
  --shadow-700: 0 14px 64px -4px #1319271f, 0 8px 22px -6px #1319271f;
353
371
  --shadow-800: 0 18px 88px -4px #13192724, 0 8px 28px -6px #1319271f;
372
+
373
+ /* Focus ring — shared by every interactive component (focus-visible:shadow-focus) */
374
+ --shadow-focus: 0px 0px 0px 3px #c2f5f6;
375
+ }
376
+
377
+ /* Alert auto-dismiss timeline */
378
+ @keyframes alert-timer {
379
+ from { width: 100%; }
380
+ to { width: 0%; }
354
381
  }`,
355
382
  zinc: `:root {
356
383
  --background: 0 0% 100%;
@@ -516,6 +543,7 @@ const main = async () => {
516
543
  if (fs.existsSync(templatesDir)) {
517
544
  const dirs = fs
518
545
  .readdirSync(templatesDir)
546
+ .filter((f) => !f.startsWith("_")) // _lib etc. are support files, not components
519
547
  .filter((f) => fs.statSync(path.join(templatesDir, f)).isDirectory());
520
548
  if (dirs.length > 0) availableComponents = dirs;
521
549
  }
@@ -633,12 +661,33 @@ const main = async () => {
633
661
  process.exit(1);
634
662
  }
635
663
 
664
+ // Pull in components required by selected ones (e.g. select imports from ./input)
665
+ for (const comp of [...componentsToAdd]) {
666
+ for (const req of REGISTRY[comp]?.requires || []) {
667
+ if (!componentsToAdd.includes(req)) componentsToAdd.push(req);
668
+ }
669
+ }
670
+
636
671
  // Create target dir
637
672
  const targetDir = path.join(cwd, env.componentsDir);
638
673
  if (!fs.existsSync(targetDir)) {
639
674
  fs.mkdirSync(targetDir, { recursive: true });
640
675
  }
641
676
 
677
+ // Components import cn() from "../../lib/utils" — scaffold it once
678
+ const utilsPath = path.join(
679
+ targetDir,
680
+ "../../lib",
681
+ `utils.${env.typescript ? "ts" : "js"}`,
682
+ );
683
+ if (!fs.existsSync(utilsPath)) {
684
+ fs.mkdirSync(path.dirname(utilsPath), { recursive: true });
685
+ fs.writeFileSync(utilsPath, UTILS_TEMPLATE, "utf-8");
686
+ console.log(
687
+ chalk.green(`✅ Created ${path.relative(cwd, utilsPath)}`),
688
+ );
689
+ }
690
+
642
691
  let allDependencies = new Set();
643
692
 
644
693
  for (const comp of componentsToAdd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cooterlabs",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Component library CLI for CooterLabs apps",
5
5
  "files": [
6
6
  "bin",
@@ -0,0 +1,28 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { extendTailwindMerge } from "tailwind-merge"
3
+
4
+ // The custom font-size ramp from index.css (--text-h1 … --text-btn-tiny) is
5
+ // unknown to tailwind-merge, so it treats text-b4 etc. as text COLORS and
6
+ // silently drops real color classes like text-white. Teach it the ramp.
7
+ const twMerge = extendTailwindMerge({
8
+ extend: {
9
+ classGroups: {
10
+ "font-size": [
11
+ {
12
+ text: [
13
+ "h1", "h2", "h3", "h4", "h5",
14
+ "s1", "s2",
15
+ "b1", "b2", "b3", "b4",
16
+ "c1", "c2", "c3",
17
+ "label",
18
+ "btn-giant", "btn-large", "btn-medium", "btn-small", "btn-tiny",
19
+ ],
20
+ },
21
+ ],
22
+ },
23
+ },
24
+ })
25
+
26
+ export function cn(...inputs: ClassValue[]) {
27
+ return twMerge(clsx(inputs))
28
+ }
@@ -1,11 +1,6 @@
1
1
  import * as React from "react"
2
2
  import * as DialogPrimitive from "@radix-ui/react-dialog"
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
- }
3
+ import { cn } from "../../lib/utils"
9
4
 
10
5
  const ActionSheet = DialogPrimitive.Root
11
6
 
@@ -75,7 +70,7 @@ const ActionSheetItem = React.forwardRef<
75
70
  <button
76
71
  ref={ref}
77
72
  className={cn(
78
- "flex h-12 w-full items-center justify-center gap-2 text-b2 transition-colors hover:bg-secondary active:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-5 [&_svg]:shrink-0",
73
+ "flex h-12 w-full items-center justify-center gap-2 text-b2 transition-colors hover:bg-secondary active:bg-muted focus-visible:outline-none focus-visible:shadow-focus disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-5 [&_svg]:shrink-0",
79
74
  variant === "destructive" ? "text-error" : "text-foreground",
80
75
  className
81
76
  )}
@@ -92,7 +87,7 @@ const ActionSheetCancel = React.forwardRef<
92
87
  <button
93
88
  ref={ref}
94
89
  className={cn(
95
- "mt-2 flex h-12 w-full items-center justify-center rounded-xs border border-border bg-background text-b2 text-foreground transition-colors hover:bg-secondary active:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
90
+ "mt-2 flex h-12 w-full items-center justify-center rounded-xs border border-border bg-background text-b2 text-foreground transition-colors hover:bg-secondary active:bg-muted focus-visible:outline-none focus-visible:shadow-focus",
96
91
  className
97
92
  )}
98
93
  {...props}
@@ -1,23 +1,19 @@
1
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
- }
2
+ import { X } from "lucide-react"
3
+ import { cn } from "../../lib/utils"
8
4
 
9
5
  const alertVariants = {
10
6
  filled: {
11
7
  default:
12
- "bg-primary-600 text-white [&_[data-slot=alert-description]]:text-white/80",
8
+ "bg-primary-500 text-white [&_[data-slot=alert-description]]:text-white/80",
13
9
  success:
14
- "bg-success-600 text-white [&_[data-slot=alert-description]]:text-white/80",
15
- info: "bg-info-600 text-white [&_[data-slot=alert-description]]:text-white/80",
10
+ "bg-success-500 text-white [&_[data-slot=alert-description]]:text-white/80",
11
+ info: "bg-info-500 text-white [&_[data-slot=alert-description]]:text-white/80",
16
12
  warning:
17
- "bg-warning-600 text-white [&_[data-slot=alert-description]]:text-white/80",
18
- error: "bg-error-600 text-white [&_[data-slot=alert-description]]:text-white/80",
13
+ "bg-warning-500 text-white [&_[data-slot=alert-description]]:text-white/80",
14
+ error: "bg-error-500 text-white [&_[data-slot=alert-description]]:text-white/80",
19
15
  },
20
- soft: {
16
+ outline: {
21
17
  default:
22
18
  "border border-primary-500 bg-primary-50 text-primary-500 [&_[data-slot=alert-description]]:text-muted-foreground",
23
19
  success:
@@ -29,27 +25,81 @@ const alertVariants = {
29
25
  },
30
26
  }
31
27
 
28
+ const alertPositions = {
29
+ "top-left": "fixed left-4 top-4 z-50",
30
+ "top-center": "fixed left-1/2 top-4 z-50 -translate-x-1/2",
31
+ "top-right": "fixed right-4 top-4 z-50",
32
+ "bottom-left": "fixed bottom-4 left-4 z-50",
33
+ "bottom-center": "fixed bottom-4 left-1/2 z-50 -translate-x-1/2",
34
+ "bottom-right": "fixed bottom-4 right-4 z-50",
35
+ }
36
+
32
37
  export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
33
38
  severity?: keyof (typeof alertVariants)["filled"]
34
- appearance?: keyof typeof alertVariants
39
+ variant?: keyof typeof alertVariants
40
+ position?: keyof typeof alertPositions
41
+ /** seconds until the alert dismisses itself; pauses on hover */
42
+ duration?: number
43
+ onDismiss?: () => void
35
44
  }
36
45
 
37
46
  const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
38
47
  (
39
- { className, severity = "default", appearance = "filled", ...props },
48
+ {
49
+ className,
50
+ severity = "default",
51
+ variant = "filled",
52
+ position,
53
+ duration,
54
+ onDismiss,
55
+ children,
56
+ ...props
57
+ },
40
58
  ref
41
- ) => (
42
- <div
43
- ref={ref}
44
- role="alert"
45
- className={cn(
46
- "relative rounded-xs p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:size-5 [&>svg~*]:pl-8",
47
- alertVariants[appearance][severity],
48
- className
49
- )}
50
- {...props}
51
- />
52
- )
59
+ ) => {
60
+ const [visible, setVisible] = React.useState(true)
61
+ if (!visible) return null
62
+ const dismissible = duration != null || onDismiss != null
63
+ const dismiss = () => {
64
+ setVisible(false)
65
+ onDismiss?.()
66
+ }
67
+ return (
68
+ <div
69
+ ref={ref}
70
+ role="alert"
71
+ className={cn(
72
+ "group relative overflow-hidden rounded-xs p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:size-5 [&>svg~*]:pl-8",
73
+ alertVariants[variant][severity],
74
+ position && alertPositions[position],
75
+ dismissible && "pr-12",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ {children}
81
+ {dismissible && (
82
+ <button
83
+ type="button"
84
+ aria-label="Dismiss"
85
+ onClick={dismiss}
86
+ className="absolute right-4 top-4"
87
+ >
88
+ <X className="size-4" />
89
+ </button>
90
+ )}
91
+ {duration != null && (
92
+ <div
93
+ className="absolute bottom-0 left-0 h-1 bg-current opacity-30 group-hover:[animation-play-state:paused]"
94
+ style={{
95
+ animation: `alert-timer ${duration}s linear forwards`,
96
+ }}
97
+ onAnimationEnd={dismiss}
98
+ />
99
+ )}
100
+ </div>
101
+ )
102
+ }
53
103
  )
54
104
  Alert.displayName = "Alert"
55
105
 
@@ -1,11 +1,6 @@
1
1
  import * as React from "react"
2
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
- }
3
+ import { cn } from "../../lib/utils"
9
4
 
10
5
  const avatarSizes = {
11
6
  tiny: "h-6 w-6",