cooterlabs 2.0.0 → 2.2.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 +226 -87
- package/bin/index.js +51 -1
- package/package.json +1 -1
- package/templates/_lib/utils.ts +28 -0
- package/templates/action-sheet/action-sheet.tsx +3 -8
- package/templates/alert/alert.tsx +76 -26
- package/templates/avatar/avatar.tsx +1 -6
- package/templates/badge/badge.tsx +89 -91
- package/templates/breadcrumb/breadcrumb.tsx +2 -7
- package/templates/button/button.tsx +14 -19
- package/templates/button-group/button-group.tsx +2 -7
- package/templates/card/card.tsx +1 -6
- package/templates/checkbox/checkbox.tsx +2 -7
- package/templates/context-menu/context-menu.tsx +1 -6
- package/templates/dialog/dialog.tsx +2 -7
- package/templates/dropdown/dropdown.tsx +1 -6
- package/templates/input/input.tsx +67 -28
- package/templates/label/label.tsx +1 -6
- package/templates/list/list.tsx +1 -6
- package/templates/loader/loader.tsx +1 -6
- package/templates/navbar-bottom/navbar-bottom.tsx +2 -7
- package/templates/navbar-top/navbar-top.tsx +2 -7
- package/templates/page-control/page-control.tsx +2 -7
- package/templates/pagination/pagination.tsx +2 -7
- package/templates/progress/progress.tsx +1 -6
- package/templates/radio/radio.tsx +2 -7
- package/templates/select/select.tsx +113 -0
- package/templates/stepper/stepper.tsx +1 -6
- package/templates/tabs/tabs.tsx +4 -9
- package/templates/textarea/textarea.tsx +2 -7
- package/templates/toggle/toggle.tsx +2 -7
- package/templates/tooltip/tooltip.tsx +2 -7
package/README.md
CHANGED
|
@@ -1,124 +1,263 @@
|
|
|
1
1
|
# CooterLabs UI Library CLI
|
|
2
2
|
|
|
3
|
-
A
|
|
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.
|
|
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
-
##
|
|
45
|
+
## Requirements
|
|
17
46
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
51
|
+
---
|
|
22
52
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
Available themes:
|
|
32
72
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
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
|
-
|
|
79
|
+
### `add` — scaffold components
|
|
40
80
|
|
|
41
81
|
```bash
|
|
42
|
-
npx cooterlabs
|
|
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
|
-
|
|
88
|
+
What it does, per run:
|
|
46
89
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
122
|
+
export function cn(...inputs: ClassValue[]) {
|
|
123
|
+
return twMerge(clsx(inputs))
|
|
124
|
+
}
|
|
63
125
|
```
|
|
64
126
|
|
|
65
|
-
|
|
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
|
-
|
|
129
|
+
---
|
|
68
130
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
**
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
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
|
|
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 ——— */
|
|
@@ -194,7 +212,7 @@ const THEMES = {
|
|
|
194
212
|
--color-secondary-foreground: #1c222a;
|
|
195
213
|
--color-muted: #e7e9ec;
|
|
196
214
|
--color-muted-foreground: #475569;
|
|
197
|
-
--color-accent: #
|
|
215
|
+
--color-accent: #0F3D3E;
|
|
198
216
|
--color-accent-foreground: #0a2b2b;
|
|
199
217
|
--color-destructive: #821717;
|
|
200
218
|
--color-destructive-foreground: #ffffff;
|
|
@@ -271,6 +289,7 @@ const THEMES = {
|
|
|
271
289
|
--color-info-700: #053e5d;
|
|
272
290
|
--color-info-800: #04314a;
|
|
273
291
|
--color-info-900: #032435;
|
|
292
|
+
--color-disabled: #7e8896
|
|
274
293
|
|
|
275
294
|
/* Typography — Aeon text ramp (text-h1 … text-c3, text-label, text-btn-*) */
|
|
276
295
|
--text-h1: 3rem;
|
|
@@ -351,6 +370,15 @@ const THEMES = {
|
|
|
351
370
|
--shadow-600: 0 12px 42px -4px #1319271f, 0 8px 18px -6px #1319271f;
|
|
352
371
|
--shadow-700: 0 14px 64px -4px #1319271f, 0 8px 22px -6px #1319271f;
|
|
353
372
|
--shadow-800: 0 18px 88px -4px #13192724, 0 8px 28px -6px #1319271f;
|
|
373
|
+
|
|
374
|
+
/* Focus ring — shared by every interactive component (focus-visible:shadow-focus) */
|
|
375
|
+
--shadow-focus: 0px 0px 0px 3px #c2f5f6;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* Alert auto-dismiss timeline */
|
|
379
|
+
@keyframes alert-timer {
|
|
380
|
+
from { width: 100%; }
|
|
381
|
+
to { width: 0%; }
|
|
354
382
|
}`,
|
|
355
383
|
zinc: `:root {
|
|
356
384
|
--background: 0 0% 100%;
|
|
@@ -516,6 +544,7 @@ const main = async () => {
|
|
|
516
544
|
if (fs.existsSync(templatesDir)) {
|
|
517
545
|
const dirs = fs
|
|
518
546
|
.readdirSync(templatesDir)
|
|
547
|
+
.filter((f) => !f.startsWith("_")) // _lib etc. are support files, not components
|
|
519
548
|
.filter((f) => fs.statSync(path.join(templatesDir, f)).isDirectory());
|
|
520
549
|
if (dirs.length > 0) availableComponents = dirs;
|
|
521
550
|
}
|
|
@@ -633,12 +662,33 @@ const main = async () => {
|
|
|
633
662
|
process.exit(1);
|
|
634
663
|
}
|
|
635
664
|
|
|
665
|
+
// Pull in components required by selected ones (e.g. select imports from ./input)
|
|
666
|
+
for (const comp of [...componentsToAdd]) {
|
|
667
|
+
for (const req of REGISTRY[comp]?.requires || []) {
|
|
668
|
+
if (!componentsToAdd.includes(req)) componentsToAdd.push(req);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
636
672
|
// Create target dir
|
|
637
673
|
const targetDir = path.join(cwd, env.componentsDir);
|
|
638
674
|
if (!fs.existsSync(targetDir)) {
|
|
639
675
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
640
676
|
}
|
|
641
677
|
|
|
678
|
+
// Components import cn() from "../../lib/utils" — scaffold it once
|
|
679
|
+
const utilsPath = path.join(
|
|
680
|
+
targetDir,
|
|
681
|
+
"../../lib",
|
|
682
|
+
`utils.${env.typescript ? "ts" : "js"}`,
|
|
683
|
+
);
|
|
684
|
+
if (!fs.existsSync(utilsPath)) {
|
|
685
|
+
fs.mkdirSync(path.dirname(utilsPath), { recursive: true });
|
|
686
|
+
fs.writeFileSync(utilsPath, UTILS_TEMPLATE, "utf-8");
|
|
687
|
+
console.log(
|
|
688
|
+
chalk.green(`✅ Created ${path.relative(cwd, utilsPath)}`),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
642
692
|
let allDependencies = new Set();
|
|
643
693
|
|
|
644
694
|
for (const comp of componentsToAdd) {
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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:
|
|
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:
|
|
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 {
|
|
3
|
-
import {
|
|
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-
|
|
8
|
+
"bg-primary-500 text-white [&_[data-slot=alert-description]]:text-white/80",
|
|
13
9
|
success:
|
|
14
|
-
"bg-success-
|
|
15
|
-
info: "bg-info-
|
|
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-
|
|
18
|
-
error: "bg-error-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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-md 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
|
|