cooterlabs 1.0.2 → 2.0.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
@@ -26,7 +26,23 @@ To swiftly set up a cohesive Tailwind theme and properly configure your base CSS
26
26
  npx cooterlabs init
27
27
  ```
28
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.
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.
30
+
31
+ The default theme is **`aeon`** — the complete Aeon Product Design System as a Tailwind CSS v4 `@theme` block. It ships:
32
+
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.
38
+
39
+ To apply it non-interactively:
40
+
41
+ ```bash
42
+ npx cooterlabs init aeon
43
+ ```
44
+
45
+ Legacy `zinc` and `slate` themes (Tailwind v3 / shadcn HSL format) remain available.
30
46
 
31
47
  ### Adding Components
32
48
 
@@ -56,16 +72,44 @@ npx cooterlabs add --all
56
72
 
57
73
  ## Available Components
58
74
 
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.
75
+ The full Aeon Product Design System, scaffolded as owned code:
76
+
77
+ **Form controls**
78
+ - `button` - Filled / outline / clear styles in five sizes (giant → tiny), 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.
69
113
 
70
114
  ## How it works under-the-hood
71
115
 
package/bin/index.js CHANGED
@@ -14,6 +14,116 @@ const __dirname = path.dirname(__filename);
14
14
  const cwd = process.cwd();
15
15
 
16
16
  const REGISTRY = {
17
+ alert: {
18
+ name: "Alert",
19
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
20
+ dev_dependencies: [],
21
+ },
22
+ "action-sheet": {
23
+ name: "ActionSheet",
24
+ dependencies: ["@radix-ui/react-dialog", "clsx", "tailwind-merge"],
25
+ dev_dependencies: [],
26
+ },
27
+ breadcrumb: {
28
+ name: "Breadcrumb",
29
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
30
+ dev_dependencies: [],
31
+ },
32
+ "button-group": {
33
+ name: "ButtonGroup",
34
+ dependencies: ["clsx", "tailwind-merge"],
35
+ dev_dependencies: [],
36
+ },
37
+ "context-menu": {
38
+ name: "ContextMenu",
39
+ dependencies: [
40
+ "@radix-ui/react-context-menu",
41
+ "lucide-react",
42
+ "clsx",
43
+ "tailwind-merge",
44
+ ],
45
+ dev_dependencies: [],
46
+ },
47
+ dialog: {
48
+ name: "Dialog",
49
+ dependencies: [
50
+ "@radix-ui/react-dialog",
51
+ "lucide-react",
52
+ "clsx",
53
+ "tailwind-merge",
54
+ ],
55
+ dev_dependencies: [],
56
+ },
57
+ dropdown: {
58
+ name: "Dropdown",
59
+ dependencies: [
60
+ "@radix-ui/react-dropdown-menu",
61
+ "lucide-react",
62
+ "clsx",
63
+ "tailwind-merge",
64
+ ],
65
+ dev_dependencies: [],
66
+ },
67
+ list: {
68
+ name: "List",
69
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
70
+ dev_dependencies: [],
71
+ },
72
+ loader: {
73
+ name: "Loader",
74
+ dependencies: ["clsx", "tailwind-merge"],
75
+ dev_dependencies: [],
76
+ },
77
+ "navbar-bottom": {
78
+ name: "NavbarBottom",
79
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
80
+ dev_dependencies: [],
81
+ },
82
+ "navbar-top": {
83
+ name: "NavbarTop",
84
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
85
+ dev_dependencies: [],
86
+ },
87
+ "page-control": {
88
+ name: "PageControl",
89
+ dependencies: ["clsx", "tailwind-merge"],
90
+ dev_dependencies: [],
91
+ },
92
+ pagination: {
93
+ name: "Pagination",
94
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
95
+ dev_dependencies: [],
96
+ },
97
+ progress: {
98
+ name: "Progress",
99
+ dependencies: ["@radix-ui/react-progress", "clsx", "tailwind-merge"],
100
+ dev_dependencies: [],
101
+ },
102
+ radio: {
103
+ name: "Radio",
104
+ dependencies: ["@radix-ui/react-radio-group", "clsx", "tailwind-merge"],
105
+ dev_dependencies: [],
106
+ },
107
+ stepper: {
108
+ name: "Stepper",
109
+ dependencies: ["lucide-react", "clsx", "tailwind-merge"],
110
+ dev_dependencies: [],
111
+ },
112
+ tabs: {
113
+ name: "Tabs",
114
+ dependencies: ["@radix-ui/react-tabs", "clsx", "tailwind-merge"],
115
+ dev_dependencies: [],
116
+ },
117
+ toggle: {
118
+ name: "Toggle",
119
+ dependencies: ["@radix-ui/react-switch", "clsx", "tailwind-merge"],
120
+ dev_dependencies: [],
121
+ },
122
+ tooltip: {
123
+ name: "Tooltip",
124
+ dependencies: ["@radix-ui/react-tooltip", "clsx", "tailwind-merge"],
125
+ dev_dependencies: [],
126
+ },
17
127
  button: {
18
128
  name: "Button",
19
129
  dependencies: [
@@ -56,12 +166,192 @@ const REGISTRY = {
56
166
  },
57
167
  checkbox: {
58
168
  name: "Checkbox",
59
- dependencies: ["lucide-react", "clsx", "tailwind-merge"],
169
+ dependencies: [
170
+ "@radix-ui/react-checkbox",
171
+ "lucide-react",
172
+ "clsx",
173
+ "tailwind-merge",
174
+ ],
60
175
  dev_dependencies: [],
61
176
  },
62
177
  };
63
178
 
64
179
  const THEMES = {
180
+ aeon: `@theme static {
181
+ /* ——— Aeon Product Design System ——— */
182
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
183
+
184
+ /* Semantic colors */
185
+ --color-background: #ffffff;
186
+ --color-foreground: #1c222a;
187
+ --color-card: #ffffff;
188
+ --color-card-foreground: #1c222a;
189
+ --color-popover: #ffffff;
190
+ --color-popover-foreground: #1c222a;
191
+ --color-primary: #0d3435;
192
+ --color-primary-foreground: #ffffff;
193
+ --color-secondary: #f4f5f6;
194
+ --color-secondary-foreground: #1c222a;
195
+ --color-muted: #e7e9ec;
196
+ --color-muted-foreground: #475569;
197
+ --color-accent: #f1f3f3;
198
+ --color-accent-foreground: #0a2b2b;
199
+ --color-destructive: #821717;
200
+ --color-destructive-foreground: #ffffff;
201
+ --color-border: #cbcfd5;
202
+ --color-input: #cbcfd5;
203
+ --color-ring: #0d3435;
204
+ --color-success: #14532d;
205
+ --color-success-foreground: #ffffff;
206
+ --color-info: #075985;
207
+ --color-info-foreground: #ffffff;
208
+ --color-warning: #713f12;
209
+ --color-warning-foreground: #ffffff;
210
+ --color-error: #991b1b;
211
+ --color-error-foreground: #ffffff;
212
+
213
+ /* Primitive scales */
214
+ --color-primary-50: #f1f3f3;
215
+ --color-primary-100: #e0e6e6;
216
+ --color-primary-200: #bcc9c9;
217
+ --color-primary-300: #93a8a8;
218
+ --color-primary-400: #577778;
219
+ --color-primary-500: #0f3d3e;
220
+ --color-primary-600: #0d3435;
221
+ --color-primary-700: #0a2b2b;
222
+ --color-primary-800: #082222;
223
+ --color-primary-900: #061819;
224
+ --color-grey-50: #f4f5f6;
225
+ --color-grey-100: #e7e9ec;
226
+ --color-grey-200: #cbcfd5;
227
+ --color-grey-300: #acb2bc;
228
+ --color-grey-400: #7e8896;
229
+ --color-grey-500: #475569;
230
+ --color-grey-600: #3c4859;
231
+ --color-grey-700: #323c4a;
232
+ --color-grey-800: #272f3a;
233
+ --color-grey-900: #1c222a;
234
+ --color-success-50: #f1f5f2;
235
+ --color-success-100: #e0e9e4;
236
+ --color-success-200: #bdcfc4;
237
+ --color-success-300: #95b2a0;
238
+ --color-success-400: #5a876c;
239
+ --color-success-500: #14532d;
240
+ --color-success-600: #114726;
241
+ --color-success-700: #0e3a20;
242
+ --color-success-800: #0b2e19;
243
+ --color-success-900: #082112;
244
+ --color-error-50: #f9f1f1;
245
+ --color-error-100: #f2e1e1;
246
+ --color-error-200: #e2bfbf;
247
+ --color-error-300: #d19898;
248
+ --color-error-400: #b85f5f;
249
+ --color-error-500: #991b1b;
250
+ --color-error-600: #821717;
251
+ --color-error-700: #6b1313;
252
+ --color-error-800: #540f0f;
253
+ --color-error-900: #3d0b0b;
254
+ --color-warning-50: #f6f3f1;
255
+ --color-warning-100: #ede6e0;
256
+ --color-warning-200: #d7c9bd;
257
+ --color-warning-300: #bfa994;
258
+ --color-warning-400: #9c7959;
259
+ --color-warning-500: #713f12;
260
+ --color-warning-600: #60360f;
261
+ --color-warning-700: #4f2c0d;
262
+ --color-warning-800: #3e230a;
263
+ --color-warning-900: #2d1907;
264
+ --color-info-50: #f0f5f8;
265
+ --color-info-100: #dfe9ef;
266
+ --color-info-200: #bad1dd;
267
+ --color-info-300: #8fb4c8;
268
+ --color-info-400: #518baa;
269
+ --color-info-500: #075985;
270
+ --color-info-600: #064c71;
271
+ --color-info-700: #053e5d;
272
+ --color-info-800: #04314a;
273
+ --color-info-900: #032435;
274
+
275
+ /* Typography — Aeon text ramp (text-h1 … text-c3, text-label, text-btn-*) */
276
+ --text-h1: 3rem;
277
+ --text-h1--line-height: 3.625rem;
278
+ --text-h1--font-weight: 600;
279
+ --text-h2: 2.5rem;
280
+ --text-h2--line-height: 3rem;
281
+ --text-h2--font-weight: 600;
282
+ --text-h3: 2rem;
283
+ --text-h3--line-height: 2.375rem;
284
+ --text-h3--font-weight: 600;
285
+ --text-h4: 1.75rem;
286
+ --text-h4--line-height: 2.125rem;
287
+ --text-h4--font-weight: 600;
288
+ --text-h5: 1.5rem;
289
+ --text-h5--line-height: 1.75rem;
290
+ --text-h5--font-weight: 600;
291
+ --text-s1: 1.125rem;
292
+ --text-s1--line-height: 1.75rem;
293
+ --text-s1--font-weight: 600;
294
+ --text-s2: 1rem;
295
+ --text-s2--line-height: 1.5rem;
296
+ --text-s2--font-weight: 600;
297
+ --text-b1: 1rem;
298
+ --text-b1--line-height: 1.5rem;
299
+ --text-b1--font-weight: 400;
300
+ --text-b2: 1rem;
301
+ --text-b2--line-height: 1.5rem;
302
+ --text-b2--font-weight: 500;
303
+ --text-b3: 0.875rem;
304
+ --text-b3--line-height: 1.25rem;
305
+ --text-b3--font-weight: 400;
306
+ --text-b4: 0.875rem;
307
+ --text-b4--line-height: 1.25rem;
308
+ --text-b4--font-weight: 500;
309
+ --text-c1: 0.75rem;
310
+ --text-c1--line-height: 1rem;
311
+ --text-c1--font-weight: 400;
312
+ --text-c2: 0.75rem;
313
+ --text-c2--line-height: 1rem;
314
+ --text-c2--font-weight: 500;
315
+ --text-c3: 0.625rem;
316
+ --text-c3--line-height: 0.875rem;
317
+ --text-c3--font-weight: 500;
318
+ --text-label: 0.75rem;
319
+ --text-label--line-height: 1rem;
320
+ --text-label--font-weight: 500;
321
+ --text-btn-giant: 1.125rem;
322
+ --text-btn-giant--line-height: 1.5rem;
323
+ --text-btn-giant--font-weight: 600;
324
+ --text-btn-large: 1rem;
325
+ --text-btn-large--line-height: 1.25rem;
326
+ --text-btn-large--font-weight: 600;
327
+ --text-btn-medium: 0.875rem;
328
+ --text-btn-medium--line-height: 1rem;
329
+ --text-btn-medium--font-weight: 600;
330
+ --text-btn-small: 0.75rem;
331
+ --text-btn-small--line-height: 1rem;
332
+ --text-btn-small--font-weight: 600;
333
+ --text-btn-tiny: 0.625rem;
334
+ --text-btn-tiny--line-height: 0.75rem;
335
+ --text-btn-tiny--font-weight: 600;
336
+
337
+ /* Radius scale — rounded-xxs … rounded-xl */
338
+ --radius-xxs: 0.25rem;
339
+ --radius-xs: 0.5rem;
340
+ --radius-sm: 0.75rem;
341
+ --radius-md: 1rem;
342
+ --radius-lg: 1.25rem;
343
+ --radius-xl: 1.5rem;
344
+
345
+ /* Elevation — shadow-100 … shadow-800 */
346
+ --shadow-100: 0 4px 4px -2px #13192714, 0 2px 4px -2px #1319271f;
347
+ --shadow-200: 0 8px 8px -4px #13192714, 0 4px 6px -4px #1319271f;
348
+ --shadow-300: 0 8px 16px -6px #13192714, 0 6px 8px -6px #1319271f;
349
+ --shadow-400: 0 8px 24px -4px #13192714, 0 6px 12px -6px #1319271f;
350
+ --shadow-500: 0 10px 32px -4px #1319271a, 0 6px 14px -6px #1319271f;
351
+ --shadow-600: 0 12px 42px -4px #1319271f, 0 8px 18px -6px #1319271f;
352
+ --shadow-700: 0 14px 64px -4px #1319271f, 0 8px 22px -6px #1319271f;
353
+ --shadow-800: 0 18px 88px -4px #13192724, 0 8px 28px -6px #1319271f;
354
+ }`,
65
355
  zinc: `:root {
66
356
  --background: 0 0% 100%;
67
357
  --foreground: 240 10% 3.9%;
@@ -231,19 +521,29 @@ const main = async () => {
231
521
  }
232
522
 
233
523
  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);
524
+ let theme = componentName; // support `cooterlabs init aeon`
525
+ if (!THEMES[theme]) {
526
+ const response = await prompts({
527
+ type: "select",
528
+ name: "theme",
529
+ message: "Which theme would you like to use?",
530
+ choices: Object.keys(THEMES).map((t) => ({
531
+ title: t === "aeon" ? "aeon (default)" : t,
532
+ value: t,
533
+ })),
534
+ });
535
+
536
+ if (!response.theme) {
537
+ console.log(chalk.red("❌ Theme selection cancelled."));
538
+ process.exit(1);
539
+ }
540
+ theme = response.theme;
244
541
  }
245
542
 
246
- const themeCSS = THEMES[response.theme];
543
+ // aeon is a Tailwind v4 @theme block appended at top level;
544
+ // legacy themes are :root vars wrapped in @layer base.
545
+ const isV4Theme = theme === "aeon";
546
+ const themeCSS = THEMES[theme];
247
547
  let cssPath = path.join(cwd, "src", "index.css");
248
548
 
249
549
  if (env.framework === "next") {
@@ -277,24 +577,29 @@ const main = async () => {
277
577
  cssContent.includes("@tailwind base") ||
278
578
  cssContent.includes('@import "tailwindcss');
279
579
  if (!hasTailwindDirectives) {
280
- cssContent = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n${cssContent}`;
580
+ cssContent = isV4Theme
581
+ ? `@import "tailwindcss";\n\n${cssContent}`
582
+ : `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n${cssContent}`;
281
583
  }
282
584
 
283
- if (
284
- cssContent.includes(":root {") ||
285
- cssContent.includes("--background:")
286
- ) {
585
+ const themeBlock = isV4Theme
586
+ ? `\n${themeCSS}\n`
587
+ : `\n@layer base {\n${themeCSS}\n}\n`;
588
+
589
+ const hasExistingVars = isV4Theme
590
+ ? cssContent.includes("@theme")
591
+ : cssContent.includes(":root {") || cssContent.includes("--background:");
592
+
593
+ if (hasExistingVars) {
287
594
  console.log(
288
595
  chalk.yellow(
289
- `⚠️ CSS variables may already exist in ${cssPath}. Please append them manually if needed:\n\n@layer base {\n${themeCSS}\n}\n`,
596
+ `⚠️ Theme variables may already exist in ${cssPath}. Please append them manually if needed:\n${themeBlock}`,
290
597
  ),
291
598
  );
292
599
  } else {
293
- cssContent = `${cssContent}\n@layer base {\n${themeCSS}\n}\n`;
600
+ cssContent = `${cssContent}${themeBlock}`;
294
601
  fs.writeFileSync(cssPath, cssContent);
295
- console.log(
296
- chalk.green(`✅ Theme '${response.theme}' initialized in ${cssPath}`),
297
- );
602
+ console.log(chalk.green(`✅ Theme '${theme}' initialized in ${cssPath}`));
298
603
  }
299
604
  } else if (command === "add") {
300
605
  let componentsToAdd = [];
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "cooterlabs",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "description": "Component library CLI for CooterLabs apps",
5
5
  "files": [
6
6
  "bin",
7
7
  "templates"
8
8
  ],
9
- "main": "index.js",
10
9
  "scripts": {
11
10
  "test": "echo \"Error: no test specified\" && exit 1"
12
11
  },
13
12
  "bin": {
14
- "cooterlabs": "./bin/index.js"
13
+ "cooterlabs": "bin/index.js"
15
14
  },
16
- "keywords": [],
15
+ "keywords": [
16
+ "aeon",
17
+ "react",
18
+ "typescript",
19
+ "cli",
20
+ "component-library"
21
+ ],
17
22
  "author": "",
18
23
  "license": "ISC",
19
24
  "type": "module",
@@ -25,5 +30,8 @@
25
30
  "prettier": "^3.8.1",
26
31
  "prompts": "^2.4.2",
27
32
  "tailwind-merge": "^3.5.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/react": "^19.2.14"
28
36
  }
29
37
  }
@@ -0,0 +1,113 @@
1
+ import * as React from "react"
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
+ }
9
+
10
+ const ActionSheet = DialogPrimitive.Root
11
+
12
+ const ActionSheetTrigger = DialogPrimitive.Trigger
13
+
14
+ const ActionSheetPortal = DialogPrimitive.Portal
15
+
16
+ const ActionSheetOverlay = React.forwardRef<
17
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
18
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
19
+ >(({ className, ...props }, ref) => (
20
+ <DialogPrimitive.Overlay
21
+ ref={ref}
22
+ className={cn(
23
+ "fixed inset-0 z-50 bg-black/50 transition-opacity data-[state=closed]:opacity-0",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ ))
29
+ ActionSheetOverlay.displayName = "ActionSheetOverlay"
30
+
31
+ const ActionSheetContent = React.forwardRef<
32
+ React.ElementRef<typeof DialogPrimitive.Content>,
33
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
34
+ >(({ className, children, ...props }, ref) => (
35
+ <ActionSheetPortal>
36
+ <ActionSheetOverlay />
37
+ <DialogPrimitive.Content
38
+ ref={ref}
39
+ className={cn(
40
+ "fixed inset-x-0 bottom-0 z-50 flex w-full flex-col bg-background rounded-t-lg px-4 pb-4 pt-2 transition-transform data-[state=closed]:translate-y-full",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ <div className="mx-auto mb-2 h-1 w-10 rounded-full bg-grey-200" />
46
+ {children}
47
+ </DialogPrimitive.Content>
48
+ </ActionSheetPortal>
49
+ ))
50
+ ActionSheetContent.displayName = "ActionSheetContent"
51
+
52
+ const ActionSheetHeader = ({
53
+ className,
54
+ ...props
55
+ }: React.HTMLAttributes<HTMLDivElement>) => (
56
+ <div
57
+ className={cn(
58
+ "flex flex-col gap-1 px-4 py-3 text-center text-c1 text-muted-foreground",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ ActionSheetHeader.displayName = "ActionSheetHeader"
65
+
66
+ export interface ActionSheetItemProps
67
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
68
+ variant?: "default" | "destructive"
69
+ }
70
+
71
+ const ActionSheetItem = React.forwardRef<
72
+ HTMLButtonElement,
73
+ ActionSheetItemProps
74
+ >(({ className, variant = "default", ...props }, ref) => (
75
+ <button
76
+ ref={ref}
77
+ 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",
79
+ variant === "destructive" ? "text-error" : "text-foreground",
80
+ className
81
+ )}
82
+ {...props}
83
+ />
84
+ ))
85
+ ActionSheetItem.displayName = "ActionSheetItem"
86
+
87
+ const ActionSheetCancel = React.forwardRef<
88
+ HTMLButtonElement,
89
+ React.ButtonHTMLAttributes<HTMLButtonElement>
90
+ >(({ className, ...props }, ref) => (
91
+ <DialogPrimitive.Close asChild>
92
+ <button
93
+ ref={ref}
94
+ 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",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ </DialogPrimitive.Close>
101
+ ))
102
+ ActionSheetCancel.displayName = "ActionSheetCancel"
103
+
104
+ export {
105
+ ActionSheet,
106
+ ActionSheetTrigger,
107
+ ActionSheetPortal,
108
+ ActionSheetOverlay,
109
+ ActionSheetContent,
110
+ ActionSheetHeader,
111
+ ActionSheetItem,
112
+ ActionSheetCancel,
113
+ }