cooterlabs 1.0.2 → 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/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,219 @@ 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
+ ],
175
+ dev_dependencies: [],
176
+ },
177
+ select: {
178
+ name: "Select",
179
+ dependencies: [
180
+ "@radix-ui/react-select",
181
+ "lucide-react",
182
+ "clsx",
183
+ "tailwind-merge",
184
+ ],
60
185
  dev_dependencies: [],
186
+ requires: ["input"], // select.tsx imports shared variants from ./input
61
187
  },
62
188
  };
63
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
+
64
197
  const THEMES = {
198
+ aeon: `@theme static {
199
+ /* ——— Aeon Product Design System ——— */
200
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
201
+
202
+ /* Semantic colors */
203
+ --color-background: #ffffff;
204
+ --color-foreground: #1c222a;
205
+ --color-card: #ffffff;
206
+ --color-card-foreground: #1c222a;
207
+ --color-popover: #ffffff;
208
+ --color-popover-foreground: #1c222a;
209
+ --color-primary: #0d3435;
210
+ --color-primary-foreground: #ffffff;
211
+ --color-secondary: #f4f5f6;
212
+ --color-secondary-foreground: #1c222a;
213
+ --color-muted: #e7e9ec;
214
+ --color-muted-foreground: #475569;
215
+ --color-accent: #f1f3f3;
216
+ --color-accent-foreground: #0a2b2b;
217
+ --color-destructive: #821717;
218
+ --color-destructive-foreground: #ffffff;
219
+ --color-border: #cbcfd5;
220
+ --color-input: #cbcfd5;
221
+ --color-ring: #0d3435;
222
+ --color-success: #14532d;
223
+ --color-success-foreground: #ffffff;
224
+ --color-info: #075985;
225
+ --color-info-foreground: #ffffff;
226
+ --color-warning: #713f12;
227
+ --color-warning-foreground: #ffffff;
228
+ --color-error: #991b1b;
229
+ --color-error-foreground: #ffffff;
230
+
231
+ /* Primitive scales */
232
+ --color-primary-50: #f1f3f3;
233
+ --color-primary-100: #e0e6e6;
234
+ --color-primary-200: #bcc9c9;
235
+ --color-primary-300: #93a8a8;
236
+ --color-primary-400: #577778;
237
+ --color-primary-500: #0f3d3e;
238
+ --color-primary-600: #0d3435;
239
+ --color-primary-700: #0a2b2b;
240
+ --color-primary-800: #082222;
241
+ --color-primary-900: #061819;
242
+ --color-grey-50: #f4f5f6;
243
+ --color-grey-100: #e7e9ec;
244
+ --color-grey-200: #cbcfd5;
245
+ --color-grey-300: #acb2bc;
246
+ --color-grey-400: #7e8896;
247
+ --color-grey-500: #475569;
248
+ --color-grey-600: #3c4859;
249
+ --color-grey-700: #323c4a;
250
+ --color-grey-800: #272f3a;
251
+ --color-grey-900: #1c222a;
252
+ --color-success-50: #f1f5f2;
253
+ --color-success-100: #e0e9e4;
254
+ --color-success-200: #bdcfc4;
255
+ --color-success-300: #95b2a0;
256
+ --color-success-400: #5a876c;
257
+ --color-success-500: #14532d;
258
+ --color-success-600: #114726;
259
+ --color-success-700: #0e3a20;
260
+ --color-success-800: #0b2e19;
261
+ --color-success-900: #082112;
262
+ --color-error-50: #f9f1f1;
263
+ --color-error-100: #f2e1e1;
264
+ --color-error-200: #e2bfbf;
265
+ --color-error-300: #d19898;
266
+ --color-error-400: #b85f5f;
267
+ --color-error-500: #991b1b;
268
+ --color-error-600: #821717;
269
+ --color-error-700: #6b1313;
270
+ --color-error-800: #540f0f;
271
+ --color-error-900: #3d0b0b;
272
+ --color-warning-50: #f6f3f1;
273
+ --color-warning-100: #ede6e0;
274
+ --color-warning-200: #d7c9bd;
275
+ --color-warning-300: #bfa994;
276
+ --color-warning-400: #9c7959;
277
+ --color-warning-500: #713f12;
278
+ --color-warning-600: #60360f;
279
+ --color-warning-700: #4f2c0d;
280
+ --color-warning-800: #3e230a;
281
+ --color-warning-900: #2d1907;
282
+ --color-info-50: #f0f5f8;
283
+ --color-info-100: #dfe9ef;
284
+ --color-info-200: #bad1dd;
285
+ --color-info-300: #8fb4c8;
286
+ --color-info-400: #518baa;
287
+ --color-info-500: #075985;
288
+ --color-info-600: #064c71;
289
+ --color-info-700: #053e5d;
290
+ --color-info-800: #04314a;
291
+ --color-info-900: #032435;
292
+
293
+ /* Typography — Aeon text ramp (text-h1 … text-c3, text-label, text-btn-*) */
294
+ --text-h1: 3rem;
295
+ --text-h1--line-height: 3.625rem;
296
+ --text-h1--font-weight: 600;
297
+ --text-h2: 2.5rem;
298
+ --text-h2--line-height: 3rem;
299
+ --text-h2--font-weight: 600;
300
+ --text-h3: 2rem;
301
+ --text-h3--line-height: 2.375rem;
302
+ --text-h3--font-weight: 600;
303
+ --text-h4: 1.75rem;
304
+ --text-h4--line-height: 2.125rem;
305
+ --text-h4--font-weight: 600;
306
+ --text-h5: 1.5rem;
307
+ --text-h5--line-height: 1.75rem;
308
+ --text-h5--font-weight: 600;
309
+ --text-s1: 1.125rem;
310
+ --text-s1--line-height: 1.75rem;
311
+ --text-s1--font-weight: 600;
312
+ --text-s2: 1rem;
313
+ --text-s2--line-height: 1.5rem;
314
+ --text-s2--font-weight: 600;
315
+ --text-b1: 1rem;
316
+ --text-b1--line-height: 1.5rem;
317
+ --text-b1--font-weight: 400;
318
+ --text-b2: 1rem;
319
+ --text-b2--line-height: 1.5rem;
320
+ --text-b2--font-weight: 500;
321
+ --text-b3: 0.875rem;
322
+ --text-b3--line-height: 1.25rem;
323
+ --text-b3--font-weight: 400;
324
+ --text-b4: 0.875rem;
325
+ --text-b4--line-height: 1.25rem;
326
+ --text-b4--font-weight: 500;
327
+ --text-c1: 0.75rem;
328
+ --text-c1--line-height: 1rem;
329
+ --text-c1--font-weight: 400;
330
+ --text-c2: 0.75rem;
331
+ --text-c2--line-height: 1rem;
332
+ --text-c2--font-weight: 500;
333
+ --text-c3: 0.625rem;
334
+ --text-c3--line-height: 0.875rem;
335
+ --text-c3--font-weight: 500;
336
+ --text-label: 0.75rem;
337
+ --text-label--line-height: 1rem;
338
+ --text-label--font-weight: 500;
339
+ --text-btn-giant: 1.125rem;
340
+ --text-btn-giant--line-height: 1.5rem;
341
+ --text-btn-giant--font-weight: 600;
342
+ --text-btn-large: 1rem;
343
+ --text-btn-large--line-height: 1.25rem;
344
+ --text-btn-large--font-weight: 600;
345
+ --text-btn-medium: 0.875rem;
346
+ --text-btn-medium--line-height: 1rem;
347
+ --text-btn-medium--font-weight: 600;
348
+ --text-btn-small: 0.75rem;
349
+ --text-btn-small--line-height: 1rem;
350
+ --text-btn-small--font-weight: 600;
351
+ --text-btn-tiny: 0.625rem;
352
+ --text-btn-tiny--line-height: 0.75rem;
353
+ --text-btn-tiny--font-weight: 600;
354
+
355
+ /* Radius scale — rounded-xxs … rounded-xl */
356
+ --radius-xxs: 0.25rem;
357
+ --radius-xs: 0.5rem;
358
+ --radius-sm: 0.75rem;
359
+ --radius-md: 1rem;
360
+ --radius-lg: 1.25rem;
361
+ --radius-xl: 1.5rem;
362
+
363
+ /* Elevation — shadow-100 … shadow-800 */
364
+ --shadow-100: 0 4px 4px -2px #13192714, 0 2px 4px -2px #1319271f;
365
+ --shadow-200: 0 8px 8px -4px #13192714, 0 4px 6px -4px #1319271f;
366
+ --shadow-300: 0 8px 16px -6px #13192714, 0 6px 8px -6px #1319271f;
367
+ --shadow-400: 0 8px 24px -4px #13192714, 0 6px 12px -6px #1319271f;
368
+ --shadow-500: 0 10px 32px -4px #1319271a, 0 6px 14px -6px #1319271f;
369
+ --shadow-600: 0 12px 42px -4px #1319271f, 0 8px 18px -6px #1319271f;
370
+ --shadow-700: 0 14px 64px -4px #1319271f, 0 8px 22px -6px #1319271f;
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%; }
381
+ }`,
65
382
  zinc: `:root {
66
383
  --background: 0 0% 100%;
67
384
  --foreground: 240 10% 3.9%;
@@ -226,24 +543,35 @@ const main = async () => {
226
543
  if (fs.existsSync(templatesDir)) {
227
544
  const dirs = fs
228
545
  .readdirSync(templatesDir)
546
+ .filter((f) => !f.startsWith("_")) // _lib etc. are support files, not components
229
547
  .filter((f) => fs.statSync(path.join(templatesDir, f)).isDirectory());
230
548
  if (dirs.length > 0) availableComponents = dirs;
231
549
  }
232
550
 
233
551
  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);
552
+ let theme = componentName; // support `cooterlabs init aeon`
553
+ if (!THEMES[theme]) {
554
+ const response = await prompts({
555
+ type: "select",
556
+ name: "theme",
557
+ message: "Which theme would you like to use?",
558
+ choices: Object.keys(THEMES).map((t) => ({
559
+ title: t === "aeon" ? "aeon (default)" : t,
560
+ value: t,
561
+ })),
562
+ });
563
+
564
+ if (!response.theme) {
565
+ console.log(chalk.red("❌ Theme selection cancelled."));
566
+ process.exit(1);
567
+ }
568
+ theme = response.theme;
244
569
  }
245
570
 
246
- const themeCSS = THEMES[response.theme];
571
+ // aeon is a Tailwind v4 @theme block appended at top level;
572
+ // legacy themes are :root vars wrapped in @layer base.
573
+ const isV4Theme = theme === "aeon";
574
+ const themeCSS = THEMES[theme];
247
575
  let cssPath = path.join(cwd, "src", "index.css");
248
576
 
249
577
  if (env.framework === "next") {
@@ -277,24 +605,29 @@ const main = async () => {
277
605
  cssContent.includes("@tailwind base") ||
278
606
  cssContent.includes('@import "tailwindcss');
279
607
  if (!hasTailwindDirectives) {
280
- cssContent = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n${cssContent}`;
608
+ cssContent = isV4Theme
609
+ ? `@import "tailwindcss";\n\n${cssContent}`
610
+ : `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n${cssContent}`;
281
611
  }
282
612
 
283
- if (
284
- cssContent.includes(":root {") ||
285
- cssContent.includes("--background:")
286
- ) {
613
+ const themeBlock = isV4Theme
614
+ ? `\n${themeCSS}\n`
615
+ : `\n@layer base {\n${themeCSS}\n}\n`;
616
+
617
+ const hasExistingVars = isV4Theme
618
+ ? cssContent.includes("@theme")
619
+ : cssContent.includes(":root {") || cssContent.includes("--background:");
620
+
621
+ if (hasExistingVars) {
287
622
  console.log(
288
623
  chalk.yellow(
289
- `⚠️ CSS variables may already exist in ${cssPath}. Please append them manually if needed:\n\n@layer base {\n${themeCSS}\n}\n`,
624
+ `⚠️ Theme variables may already exist in ${cssPath}. Please append them manually if needed:\n${themeBlock}`,
290
625
  ),
291
626
  );
292
627
  } else {
293
- cssContent = `${cssContent}\n@layer base {\n${themeCSS}\n}\n`;
628
+ cssContent = `${cssContent}${themeBlock}`;
294
629
  fs.writeFileSync(cssPath, cssContent);
295
- console.log(
296
- chalk.green(`✅ Theme '${response.theme}' initialized in ${cssPath}`),
297
- );
630
+ console.log(chalk.green(`✅ Theme '${theme}' initialized in ${cssPath}`));
298
631
  }
299
632
  } else if (command === "add") {
300
633
  let componentsToAdd = [];
@@ -328,12 +661,33 @@ const main = async () => {
328
661
  process.exit(1);
329
662
  }
330
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
+
331
671
  // Create target dir
332
672
  const targetDir = path.join(cwd, env.componentsDir);
333
673
  if (!fs.existsSync(targetDir)) {
334
674
  fs.mkdirSync(targetDir, { recursive: true });
335
675
  }
336
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
+
337
691
  let allDependencies = new Set();
338
692
 
339
693
  for (const comp of componentsToAdd) {
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "cooterlabs",
3
- "version": "1.0.2",
3
+ "version": "2.1.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,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
+ }
@@ -0,0 +1,108 @@
1
+ import * as React from "react"
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
3
+ import { cn } from "../../lib/utils"
4
+
5
+ const ActionSheet = DialogPrimitive.Root
6
+
7
+ const ActionSheetTrigger = DialogPrimitive.Trigger
8
+
9
+ const ActionSheetPortal = DialogPrimitive.Portal
10
+
11
+ const ActionSheetOverlay = React.forwardRef<
12
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
13
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
14
+ >(({ className, ...props }, ref) => (
15
+ <DialogPrimitive.Overlay
16
+ ref={ref}
17
+ className={cn(
18
+ "fixed inset-0 z-50 bg-black/50 transition-opacity data-[state=closed]:opacity-0",
19
+ className
20
+ )}
21
+ {...props}
22
+ />
23
+ ))
24
+ ActionSheetOverlay.displayName = "ActionSheetOverlay"
25
+
26
+ const ActionSheetContent = React.forwardRef<
27
+ React.ElementRef<typeof DialogPrimitive.Content>,
28
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
29
+ >(({ className, children, ...props }, ref) => (
30
+ <ActionSheetPortal>
31
+ <ActionSheetOverlay />
32
+ <DialogPrimitive.Content
33
+ ref={ref}
34
+ className={cn(
35
+ "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",
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ <div className="mx-auto mb-2 h-1 w-10 rounded-full bg-grey-200" />
41
+ {children}
42
+ </DialogPrimitive.Content>
43
+ </ActionSheetPortal>
44
+ ))
45
+ ActionSheetContent.displayName = "ActionSheetContent"
46
+
47
+ const ActionSheetHeader = ({
48
+ className,
49
+ ...props
50
+ }: React.HTMLAttributes<HTMLDivElement>) => (
51
+ <div
52
+ className={cn(
53
+ "flex flex-col gap-1 px-4 py-3 text-center text-c1 text-muted-foreground",
54
+ className
55
+ )}
56
+ {...props}
57
+ />
58
+ )
59
+ ActionSheetHeader.displayName = "ActionSheetHeader"
60
+
61
+ export interface ActionSheetItemProps
62
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
63
+ variant?: "default" | "destructive"
64
+ }
65
+
66
+ const ActionSheetItem = React.forwardRef<
67
+ HTMLButtonElement,
68
+ ActionSheetItemProps
69
+ >(({ className, variant = "default", ...props }, ref) => (
70
+ <button
71
+ ref={ref}
72
+ className={cn(
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",
74
+ variant === "destructive" ? "text-error" : "text-foreground",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ ))
80
+ ActionSheetItem.displayName = "ActionSheetItem"
81
+
82
+ const ActionSheetCancel = React.forwardRef<
83
+ HTMLButtonElement,
84
+ React.ButtonHTMLAttributes<HTMLButtonElement>
85
+ >(({ className, ...props }, ref) => (
86
+ <DialogPrimitive.Close asChild>
87
+ <button
88
+ ref={ref}
89
+ className={cn(
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",
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ </DialogPrimitive.Close>
96
+ ))
97
+ ActionSheetCancel.displayName = "ActionSheetCancel"
98
+
99
+ export {
100
+ ActionSheet,
101
+ ActionSheetTrigger,
102
+ ActionSheetPortal,
103
+ ActionSheetOverlay,
104
+ ActionSheetContent,
105
+ ActionSheetHeader,
106
+ ActionSheetItem,
107
+ ActionSheetCancel,
108
+ }