@boxcustodia/library 2.0.0-alpha.13 → 2.0.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1083 -717
  3. package/dist/index.es.js +7059 -56179
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Changelog.mdx +6 -6
  7. package/src/__doc__/Examples.tsx +1 -1
  8. package/src/__doc__/Intro.mdx +3 -3
  9. package/src/__doc__/Tabs.mdx +112 -0
  10. package/src/__doc__/V2.mdx +1245 -0
  11. package/src/components/accordion/accordion.stories.tsx +143 -0
  12. package/src/components/accordion/accordion.tsx +135 -0
  13. package/src/components/accordion/index.ts +1 -0
  14. package/src/components/alert/alert.stories.tsx +24 -4
  15. package/src/components/alert/alert.tsx +17 -9
  16. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  17. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  18. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  19. package/src/components/auto-complete/auto-complete.stories.tsx +615 -200
  20. package/src/components/auto-complete/auto-complete.tsx +420 -68
  21. package/src/components/auto-complete/index.ts +0 -1
  22. package/src/components/avatar/avatar.stories.tsx +162 -21
  23. package/src/components/avatar/avatar.tsx +79 -20
  24. package/src/components/button/button.stories.tsx +236 -294
  25. package/src/components/button/button.test.tsx +10 -17
  26. package/src/components/button/button.tsx +53 -18
  27. package/src/components/button/components/base-button.tsx +25 -53
  28. package/src/components/button/index.ts +0 -1
  29. package/src/components/calendar/calendar.stories.tsx +1 -1
  30. package/src/components/calendar/calendar.tsx +4 -4
  31. package/src/components/card/card.stories.tsx +140 -69
  32. package/src/components/card/card.tsx +155 -54
  33. package/src/components/center/center.stories.tsx +22 -39
  34. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  35. package/src/components/checkbox/checkbox.tsx +76 -15
  36. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  37. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  38. package/src/components/combobox/combobox.stories.tsx +33 -23
  39. package/src/components/combobox/combobox.tsx +120 -104
  40. package/src/components/date-picker/date-input.stories.tsx +14 -6
  41. package/src/components/date-picker/date-input.tsx +3 -3
  42. package/src/components/date-picker/date-picker.model.ts +13 -4
  43. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  44. package/src/components/date-picker/date-picker.tsx +29 -15
  45. package/src/components/dialog/dialog.stories.tsx +18 -0
  46. package/src/components/dialog/dialog.test.tsx +1 -1
  47. package/src/components/dialog/dialog.tsx +51 -20
  48. package/src/components/divider/divider.stories.tsx +6 -0
  49. package/src/components/dropzone/dropzone.stories.tsx +70 -90
  50. package/src/components/dropzone/dropzone.tsx +383 -105
  51. package/src/components/dropzone/index.ts +0 -1
  52. package/src/components/empty/empty.stories.tsx +164 -0
  53. package/src/components/empty/empty.tsx +156 -0
  54. package/src/components/empty/index.ts +1 -0
  55. package/src/components/field/field.stories.tsx +226 -3
  56. package/src/components/field/field.tsx +77 -42
  57. package/src/components/form/form.stories.tsx +320 -197
  58. package/src/components/form/form.tsx +3 -23
  59. package/src/components/index.ts +2 -6
  60. package/src/components/input/input.stories.tsx +5 -5
  61. package/src/components/input/input.tsx +5 -5
  62. package/src/components/kbd/kbd.stories.tsx +1 -0
  63. package/src/components/label/label.stories.tsx +16 -0
  64. package/src/components/label/label.tsx +13 -2
  65. package/src/components/loader/loader.stories.tsx +7 -5
  66. package/src/components/loader/loader.tsx +8 -3
  67. package/src/components/menu/menu-primitives.tsx +207 -196
  68. package/src/components/menu/menu.stories.tsx +275 -146
  69. package/src/components/menu/menu.tsx +146 -54
  70. package/src/components/number-input/number-input.stories.tsx +27 -4
  71. package/src/components/number-input/number-input.test.tsx +2 -2
  72. package/src/components/number-input/number-input.tsx +29 -33
  73. package/src/components/otp/index.ts +1 -0
  74. package/src/components/otp/otp.stories.tsx +209 -0
  75. package/src/components/otp/otp.tsx +100 -0
  76. package/src/components/pagination/index.ts +1 -0
  77. package/src/components/pagination/pagination.model.ts +2 -0
  78. package/src/components/pagination/pagination.stories.tsx +153 -59
  79. package/src/components/pagination/pagination.test.tsx +122 -57
  80. package/src/components/pagination/pagination.tsx +575 -77
  81. package/src/components/password/password.stories.tsx +18 -3
  82. package/src/components/password/password.tsx +26 -10
  83. package/src/components/popover/popover.stories.tsx +26 -5
  84. package/src/components/popover/popover.tsx +15 -23
  85. package/src/components/progress/progress.stories.tsx +1 -0
  86. package/src/components/radio-group/index.ts +1 -0
  87. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  88. package/src/components/radio-group/radio-group.tsx +212 -0
  89. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  90. package/src/components/select/select.stories.tsx +118 -19
  91. package/src/components/select/select.tsx +67 -62
  92. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  93. package/src/components/stack/stack.stories.tsx +179 -89
  94. package/src/components/stack/stack.tsx +2 -2
  95. package/src/components/stepper/index.ts +1 -1
  96. package/src/components/stepper/stepper.stories.tsx +766 -83
  97. package/src/components/stepper/stepper.test.tsx +18 -18
  98. package/src/components/stepper/stepper.tsx +554 -0
  99. package/src/components/switch/switch.stories.tsx +15 -1
  100. package/src/components/switch/switch.tsx +17 -4
  101. package/src/components/table/index.ts +0 -2
  102. package/src/components/table/table.stories.tsx +131 -18
  103. package/src/components/table/table.test.tsx +1 -1
  104. package/src/components/table/table.tsx +183 -77
  105. package/src/components/tabs/tabs.stories.tsx +372 -155
  106. package/src/components/tabs/tabs.test.tsx +12 -12
  107. package/src/components/tabs/tabs.tsx +72 -149
  108. package/src/components/tag/index.ts +0 -1
  109. package/src/components/tag/tag.stories.tsx +147 -120
  110. package/src/components/tag/tag.tsx +47 -95
  111. package/src/components/textarea/textarea.stories.tsx +8 -22
  112. package/src/components/textarea/textarea.tsx +17 -79
  113. package/src/components/timeline/timeline.stories.tsx +322 -42
  114. package/src/components/timeline/timeline.tsx +359 -132
  115. package/src/components/toast/toast.stories.tsx +1 -0
  116. package/src/components/tooltip/tooltip.tsx +11 -9
  117. package/src/components/tree/index.ts +0 -1
  118. package/src/components/tree/tree.stories.tsx +364 -408
  119. package/src/components/tree/tree.test.tsx +163 -0
  120. package/src/components/tree/tree.tsx +212 -36
  121. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  122. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  123. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  124. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  125. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  126. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  127. package/src/hooks/usePagination/usePagination.tsx +36 -24
  128. package/src/styles/theme.css +1 -1
  129. package/src/utils/form.tsx +69 -37
  130. package/src/utils/index.ts +1 -1
  131. package/src/__doc__/Migration.mdx +0 -451
  132. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  133. package/src/components/background-image/background-image.stories.tsx +0 -21
  134. package/src/components/background-image/background-image.test.tsx +0 -29
  135. package/src/components/background-image/background-image.tsx +0 -23
  136. package/src/components/background-image/index.ts +0 -1
  137. package/src/components/button/button.variants.ts +0 -44
  138. package/src/components/button/components/loader-overlay.tsx +0 -21
  139. package/src/components/button/components/loading-icon.tsx +0 -47
  140. package/src/components/dropzone/upload-primitives.tsx +0 -310
  141. package/src/components/dropzone/use-dropzone.ts +0 -122
  142. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  143. package/src/components/empty-state/empty-state.tsx +0 -39
  144. package/src/components/empty-state/index.ts +0 -1
  145. package/src/components/heading/heading.stories.tsx +0 -74
  146. package/src/components/heading/heading.tsx +0 -28
  147. package/src/components/heading/heading.variants.ts +0 -27
  148. package/src/components/heading/index.ts +0 -1
  149. package/src/components/kbd/kbd.variants.ts +0 -26
  150. package/src/components/menu/util/render-menu-item.tsx +0 -54
  151. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  152. package/src/components/multi-select/index.ts +0 -1
  153. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  154. package/src/components/multi-select/multi-select.tsx +0 -300
  155. package/src/components/multi-select/multi-select.variants.ts +0 -22
  156. package/src/components/pagination/components/pagination-option.tsx +0 -27
  157. package/src/components/show/index.ts +0 -1
  158. package/src/components/show/show.stories.tsx +0 -197
  159. package/src/components/show/show.test.tsx +0 -41
  160. package/src/components/show/show.tsx +0 -16
  161. package/src/components/stepper/Stepper.tsx +0 -190
  162. package/src/components/stepper/context/stepper-context.tsx +0 -11
  163. package/src/components/table/table-primitives.tsx +0 -122
  164. package/src/components/table/table.model.ts +0 -20
  165. package/src/components/table-pagination/index.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  167. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  168. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  169. package/src/components/table-pagination/table-pagination.tsx +0 -108
  170. package/src/components/tabs/context/tabs-context.tsx +0 -14
  171. package/src/components/tag/tag.variants.ts +0 -31
  172. package/src/components/timeline/timeline-status.ts +0 -5
  173. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  174. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -18,21 +18,14 @@ describe("Button component", () => {
18
18
  expect(button).toHaveTextContent("Click me");
19
19
  });
20
20
 
21
- it("should render as child correctly", () => {
22
- render(
23
- <Button asChild>
24
- <a href="#">Click me</a>
25
- </Button>,
26
- );
27
-
28
- // No debe renderizarse como un button
29
- const button = screen.queryByRole("button");
30
- expect(button).not.toBeInTheDocument();
31
-
32
- // Debe renderizarse como un link
33
- const link = screen.getByRole("link");
34
- expect(link).toBeInTheDocument();
35
- expect(link).toHaveTextContent("Click me");
21
+ it("should render as custom element via render prop", () => {
22
+ render(<Button render={<a href="#">Click me</a>}>Click me</Button>);
23
+
24
+ // Base UI always applies role="button" even on custom elements
25
+ const button = screen.getByRole("button");
26
+ expect(button).toBeInTheDocument();
27
+ expect(button).toHaveTextContent("Click me");
28
+ expect(button.tagName.toLowerCase()).toBe("a");
36
29
  });
37
30
 
38
31
  it("should accept className prop", () => {
@@ -51,8 +44,8 @@ describe("Button component", () => {
51
44
  expect(ref.current).toBeInstanceOf(HTMLButtonElement);
52
45
  });
53
46
 
54
- it("should show loader", () => {
47
+ it("should set data-loading when loading", () => {
55
48
  render(<Button loading />);
56
- expect(screen.getByTestId("btn-loader")).toBeInTheDocument();
49
+ expect(screen.getByRole("button")).toHaveAttribute("data-loading");
57
50
  });
58
51
  });
@@ -1,31 +1,66 @@
1
- import { type VariantProps } from "class-variance-authority";
2
- import { ComponentProps } from "react";
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import { type ComponentProps } from "react";
3
3
  import { cn } from "../../lib";
4
- import { useThemeProps } from "../../providers/theme/useThemeProps";
5
- import { buttonVariants } from "./button.variants";
6
4
  import { BaseButton } from "./components/base-button";
7
5
 
6
+ const buttonVariants = cva(
7
+ [
8
+ "group/button relative inline-flex shrink-0 items-center justify-center gap-1 rounded-lg border border-transparent bg-clip-padding",
9
+ "text-sm font-medium whitespace-nowrap transition-all outline-none select-none",
10
+ "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
11
+ "active:not-aria-[haspopup]:translate-y-px",
12
+ "disabled:pointer-events-none disabled:opacity-50",
13
+ "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
14
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
15
+ "[&[data-loading]>[data-content]]:invisible",
16
+ "[&[data-loading]]:pointer-events-none",
17
+ ],
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
22
+ outline:
23
+ "border-border hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:hover:bg-input/50",
24
+ secondary:
25
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
26
+ ghost:
27
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
28
+ error: "bg-error text-error-foreground hover:bg-error/90",
29
+ success: "bg-success text-success-foreground hover:bg-success/90",
30
+ warning: "bg-warning text-warning-foreground hover:bg-warning/90",
31
+ link: "text-primary underline-offset-4 hover:underline",
32
+ },
33
+ size: {
34
+ default: "h-8 px-2.5",
35
+ xs: "h-6 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
36
+ sm: "h-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3.5",
37
+ lg: "h-9 px-2.5",
38
+ icon: "size-8",
39
+ "icon-xs":
40
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
41
+ "icon-sm":
42
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
43
+ "icon-lg": "size-9",
44
+ },
45
+ },
46
+ defaultVariants: {
47
+ variant: "default",
48
+ size: "default",
49
+ },
50
+ },
51
+ );
52
+
8
53
  type BaseButtonProps = ComponentProps<typeof BaseButton>;
9
54
  type Props = BaseButtonProps & VariantProps<typeof buttonVariants>;
10
55
 
11
- export const Button = (props: Props) => {
12
- const { variant, size, shape, className, ...rest } = useThemeProps(
13
- "Button",
14
- props,
15
- );
16
-
56
+ const Button = ({ variant, size, className, ...rest }: Props) => {
17
57
  return (
18
58
  <BaseButton
19
59
  {...rest}
20
- className={cn(
21
- buttonVariants({
22
- variant,
23
- size,
24
- shape,
25
- }),
26
- className,
27
- )}
60
+ className={cn(buttonVariants({ variant, size }), className)}
28
61
  data-variant={variant || "default"}
29
62
  />
30
63
  );
31
64
  };
65
+
66
+ export { Button, buttonVariants };
@@ -1,86 +1,58 @@
1
- import { Slot, Slottable } from "@radix-ui/react-slot";
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
2
2
  import { useControllableState } from "@radix-ui/react-use-controllable-state";
3
- import { ComponentProps, ReactNode } from "react";
4
- import { ClickEvent } from "@/models";
5
- import { LoaderOverlay } from "./loader-overlay";
6
- import { LoadingIcon } from "./loading-icon";
3
+ import { LoaderCircle } from "lucide-react";
4
+ import { type ComponentProps } from "react";
7
5
 
8
- interface Props extends ComponentProps<"button"> {
9
- asChild?: boolean;
6
+ interface Props extends ComponentProps<typeof ButtonPrimitive> {
10
7
  loading?: boolean;
11
- showLoader?: boolean;
12
- icon?: ReactNode;
13
- iconPosition?: "start" | "end";
14
- loaderReplace?: true;
15
8
  }
16
9
 
17
- const baseStyles = {
18
- display: "inline-flex",
19
- alignItems: "center",
20
- gap: "0.5rem",
21
- };
10
+ type OnClickEvent = Parameters<
11
+ NonNullable<ComponentProps<typeof ButtonPrimitive>["onClick"]>
12
+ >[0];
22
13
 
23
14
  export const BaseButton = ({
24
- asChild = false,
25
15
  onClick,
26
16
  loading: prop,
27
- showLoader = true,
28
- icon,
29
- iconPosition = "start",
30
- loaderReplace,
31
17
  style,
18
+ children,
32
19
  ...props
33
20
  }: Props) => {
34
- const Comp = asChild ? Slot : "button";
35
21
  const [loading = false, setLoading] = useControllableState<boolean>({
36
22
  prop,
37
23
  defaultProp: false,
38
24
  });
39
25
 
40
- const handleClick = async (e: ClickEvent) => {
26
+ const handleClick = async (e: OnClickEvent) => {
41
27
  if (!onClick || loading) return;
42
28
 
43
29
  const onClickResult = onClick(e) as unknown;
44
30
 
45
31
  if (onClickResult instanceof Promise) {
46
- showLoader && setLoading(true);
32
+ setLoading(true);
47
33
  await onClickResult.finally(() => setLoading(false));
48
34
  }
49
35
  };
50
36
 
51
- // TODO: Refactorizar lógica de renderización de iconos
52
- const renderIconWithLoader = (position: "start" | "end") => {
53
- if (position !== iconPosition) return null;
54
- const isLoading = loaderReplace ? false : loading;
55
-
56
- return (
57
- <>
58
- <LoadingIcon loading={isLoading && !icon} animate={true} />
59
- {icon && <LoadingIcon loading={isLoading} animate={false} />}
60
- {icon && !isLoading && icon}
61
- </>
62
- );
63
- };
64
-
65
- const isLoading = loading && showLoader;
66
- const isReplaceMode = loaderReplace && isLoading;
67
-
68
37
  return (
69
- <Comp
70
- type="button"
38
+ <ButtonPrimitive
71
39
  data-slot="button"
40
+ data-loading={loading || undefined}
41
+ aria-busy={loading || undefined}
72
42
  {...props}
73
43
  onClick={handleClick}
74
- style={{
75
- ...baseStyles,
76
- position: isReplaceMode ? "relative" : undefined,
77
- ...style,
78
- }}
44
+ style={style}
79
45
  >
80
- {renderIconWithLoader("start")}
81
- <Slottable>{props.children}</Slottable>
82
- {renderIconWithLoader("end")}
83
- {isReplaceMode && <LoaderOverlay loading={true} />}
84
- </Comp>
46
+ <span data-content className="contents">
47
+ {children}
48
+ </span>
49
+ {loading && (
50
+ <LoaderCircle
51
+ data-loader
52
+ data-testid="btn-loader"
53
+ className="absolute inset-0 m-auto size-4 animate-spin"
54
+ />
55
+ )}
56
+ </ButtonPrimitive>
85
57
  );
86
58
  };
@@ -1,3 +1,2 @@
1
1
  export * from "./button";
2
- export * from "./button.variants";
3
2
  export * from "./components/base-button";
@@ -9,7 +9,7 @@ import { Calendar } from "./calendar";
9
9
  * modos de selección `single`, `range` y `multiple`. Acepta todas las props de `DayPicker`.
10
10
  *
11
11
  * Por defecto usa `captionLayout="dropdown"` con el componente `Select` de la librería
12
- * para navegar por mes y año. El rango navegable cubre 100 años hacia atrás y 10 hacia
12
+ * para navegar por mes y año. El rango navegable cubre 100 años hacia atrás y 100 hacia
13
13
  * adelante desde el año actual. Para volver al caption de texto, pasá `captionLayout="label"`.
14
14
  *
15
15
  * Sobreescribí `components.Dropdown` para reemplazar el dropdown por defecto con cualquier
@@ -14,7 +14,7 @@ import { Select } from "../select/select";
14
14
 
15
15
  const _year = new Date().getFullYear();
16
16
  const DEFAULT_START_MONTH = new Date(_year - 100, 0);
17
- const DEFAULT_END_MONTH = new Date(_year + 10, 11);
17
+ const DEFAULT_END_MONTH = new Date(_year + 100, 11);
18
18
 
19
19
  type _Opt = { label: string; value: number };
20
20
 
@@ -33,10 +33,10 @@ function CalendarDropdown({
33
33
  <Select
34
34
  items={items}
35
35
  value={selected}
36
- className="py-1"
36
+ className="py-1 h-7"
37
37
  getLabel={(o) => o.label}
38
- getValue={(o) => String(o.value)}
39
- onChange={(item) => {
38
+ getId={(o) => String(o.value)}
39
+ onValueChange={(item) => {
40
40
  if (!onChange || item === null) return;
41
41
  onChange({
42
42
  target: { value: String(item.value) },
@@ -1,116 +1,187 @@
1
1
  import { faker } from "@faker-js/faker";
2
- import { Meta, StoryObj } from "@storybook/react-vite";
2
+ import type { Meta, StoryObj } from "@storybook/react-vite";
3
3
  import { ComponentType } from "react";
4
4
  import {
5
5
  Button,
6
6
  Card,
7
+ CardAction,
7
8
  CardContent,
8
9
  CardDescription,
9
10
  CardFooter,
10
11
  CardHeader,
12
+ CardRoot,
11
13
  CardTitle,
12
14
  } from "../../components";
13
15
 
14
16
  /**
15
- * El componente Card es un contenedor versátil que permite organizar y presentar contenido
16
- * de manera estructurada y visualmente atractiva.
17
- *
18
- * ### Características principales
19
- * - Diseño modular y flexible
20
- * - Soporte para encabezados, contenido y pie de página
21
- * - Personalizable a través de className
22
- * - Ideal para mostrar información, productos, artículos, etc.
23
- *
24
- * ### Estructura básica
25
- * ```tsx
26
- * <Card>
27
- * <CardHeader>
28
- * <CardTitle>Título del Card</CardTitle>
29
- * <CardDescription>Descripción opcional</CardDescription>
30
- * </CardHeader>
31
- * <CardContent>
32
- * Contenido principal
33
- * </CardContent>
34
- * <CardFooter>
35
- * Acciones o contenido adicional
36
- * </CardFooter>
37
- * </Card>
38
- * ```
17
+ * Composite card surface. Pass `title`, `description`, `action`, and `footer` as props
18
+ * to render the full structure without manually composing sub-components.
19
+ * Use `CardRoot` + primitives (`CardHeader`, `CardTitle`, `CardDescription`, `CardAction`,
20
+ * `CardContent`, `CardFooter`) directly for layouts the composite cannot express —
21
+ * for example, placing an `<img>` as the first child automatically clips it to the card's top corners.
22
+ * `CardAction` renders in the top-right of the header, spanning both title and description rows.
23
+ * `size="sm"` reduces padding and font size across all nested slots via `data-size` and CSS group selectors.
24
+ * Sub-components use `data-slot` attributes following the [Base UI](https://base-ui.com/react) slot convention.
39
25
  */
40
26
  const meta: Meta<typeof Card> = {
41
- title: "data display/Card",
27
+ title: "Components/Card",
42
28
  component: Card,
43
- tags: ["autodocs"],
44
29
  subcomponents: {
30
+ CardRoot: CardRoot as ComponentType<unknown>,
45
31
  CardHeader: CardHeader as ComponentType<unknown>,
46
32
  CardTitle: CardTitle as ComponentType<unknown>,
47
33
  CardDescription: CardDescription as ComponentType<unknown>,
34
+ CardAction: CardAction as ComponentType<unknown>,
48
35
  CardContent: CardContent as ComponentType<unknown>,
49
36
  CardFooter: CardFooter as ComponentType<unknown>,
50
37
  },
38
+ args: {
39
+ className: "w-[350px]",
40
+ title: "How to Improve Your Productivity",
41
+ description: "By Jane Doe · 5 min read",
42
+ },
43
+ argTypes: {
44
+ classNames: { control: false },
45
+ },
46
+ parameters: { layout: "centered" },
51
47
  };
52
48
 
53
49
  export default meta;
54
-
55
50
  type Story = StoryObj<typeof Card>;
56
51
 
57
52
  /**
58
- * Ejemplo básico de un Card con todos sus componentes.
59
- * Muestra la estructura típica con encabezado, contenido y pie de página.
53
+ * `className` styles the card root. `classNames` exposes the `header`,
54
+ * `title`, `description`, `action`, `content`, and `footer` slots for
55
+ * fine-grained styling without dropping to primitives.
60
56
  */
57
+ export const WithClassNames: Story = {
58
+ args: {
59
+ children: "Card body content.",
60
+ classNames: {
61
+ title: "text-primary",
62
+ description: "italic",
63
+ content: "text-base",
64
+ },
65
+ },
66
+ };
67
+
61
68
  export const Default: Story = {
69
+ render: (args) => (
70
+ <Card
71
+ {...args}
72
+ footer={
73
+ <>
74
+ <Button variant="outline">Read more</Button>
75
+ <Button className="ml-auto">Save</Button>
76
+ </>
77
+ }
78
+ >
79
+ <p className="text-muted-foreground leading-relaxed">
80
+ Discover strategies and tools to boost your productivity. Learn to
81
+ manage your time effectively and stay focused on what matters.
82
+ </p>
83
+ </Card>
84
+ ),
85
+ };
86
+
87
+ export const Sizes: Story = {
62
88
  render: () => (
63
- <Card className="w-[350px] shadow-md rounded-lg">
64
- <CardHeader className="p-4">
65
- <CardTitle className="text-2xl">
66
- How to Improve Your Productivity
67
- </CardTitle>
68
- <CardDescription className="text-sm">By Jane Doe</CardDescription>
69
- </CardHeader>
70
- <CardContent className="p-4">
71
- <p className="text-gray-700 text-base">
72
- Discover the best strategies and tools to boost your productivity in
73
- work and life. Learn how to manage your time effectively and stay
74
- focused on what really matters.
89
+ <div className="flex flex-col gap-4 w-[350px]">
90
+ <Card title="Total Revenue" description="Compared to last month">
91
+ <p className="text-3xl font-bold">$45,231.89</p>
92
+ <p className="text-xs text-muted-foreground mt-1">
93
+ +20.1% from last month
75
94
  </p>
76
- </CardContent>
77
- <CardFooter className="px-4 py-3 flex justify-between">
78
- <Button variant="outline">Read More</Button>
79
- <Button className="bg-blue-600 text-white">Save</Button>
80
- </CardFooter>
95
+ </Card>
96
+ <Card size="sm" title="Active Users" description="Compared to last month">
97
+ <p className="text-3xl font-bold">2,350</p>
98
+ <p className="text-xs text-muted-foreground mt-1">+180 new users</p>
99
+ </Card>
100
+ </div>
101
+ ),
102
+ };
103
+
104
+ /**
105
+ * `CardAction` anchors to the top-right corner of the header and spans both the title and description rows.
106
+ * Pass any node — icon button, badge, menu trigger — via the `action` prop.
107
+ */
108
+ export const WithAction: Story = {
109
+ render: (args) => (
110
+ <Card
111
+ {...args}
112
+ title="Team Members"
113
+ description="Manage your workspace members"
114
+ action={
115
+ <Button variant="outline" size="sm">
116
+ Invite
117
+ </Button>
118
+ }
119
+ >
120
+ <div className="flex flex-col gap-2">
121
+ {Array.from({ length: 3 }).map((_, i) => (
122
+ <div key={i} className="flex items-center justify-between">
123
+ <span className="text-sm">{faker.person.fullName()}</span>
124
+ <span className="text-xs text-muted-foreground">
125
+ {faker.internet.email()}
126
+ </span>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ </Card>
131
+ ),
132
+ };
133
+
134
+ export const WithFooter: Story = {
135
+ render: (args) => (
136
+ <Card
137
+ {...args}
138
+ title="Delete Account"
139
+ description="This action is permanent and cannot be undone."
140
+ footer={
141
+ <div className="flex gap-2 w-full justify-end">
142
+ <Button variant="outline">Cancel</Button>
143
+ <Button variant="error">Delete</Button>
144
+ </div>
145
+ }
146
+ >
147
+ <p className="text-muted-foreground text-sm leading-relaxed">
148
+ All your data, settings, and integrations will be permanently removed
149
+ from our servers.
150
+ </p>
81
151
  </Card>
82
152
  ),
83
153
  };
84
154
 
85
155
  /**
86
- * Ejemplo de Card con imagen y diseño de producto.
87
- * Demuestra cómo integrar elementos multimedia y crear layouts más complejos.
156
+ * Use `CardRoot` + sub-components for layouts the composite cannot express.
157
+ * An `<img>` placed as the first child of `CardRoot` auto-clips to the card's top-rounded corners
158
+ * via the `has-[>img:first-child]:pt-0` and `*:[img:first-child]:rounded-t-xl` selectors.
88
159
  */
89
- export const Media: Story = {
160
+ export const Primitive: Story = {
90
161
  render: () => (
91
- <Card className="w-[350px] shadow-lg rounded-lg overflow-hidden">
162
+ <CardRoot className="w-[350px]">
92
163
  <img
93
- src={faker.image.url()}
94
- alt="Featured Product"
164
+ src={faker.image.urlPicsumPhotos({ width: 350, height: 150 })}
165
+ alt="Villa Luxury"
95
166
  className="w-full h-[150px] object-cover"
96
167
  />
97
- <CardContent className="p-4">
98
- <CardTitle className="text-xl font-bold text-gray-900">
99
- Villa Luxury
100
- </CardTitle>
101
- <CardDescription className="text-sm text-gray-600">
102
- Escapa a esta luxuriosa aldea con vistas maravillosas del océano y
103
- piscina privada.
104
- </CardDescription>
105
- <div className="flex items-center mt-4">
106
- <span className="text-2xl font-semibold text-gray-900">$750</span>
107
- <span className="ml-2 text-sm text-gray-500">/ noche</span>
108
- </div>
168
+ <CardHeader>
169
+ <CardTitle>Villa Luxury</CardTitle>
170
+ <CardDescription>Ocean views and private pool.</CardDescription>
171
+ <CardAction>
172
+ <Button variant="ghost" size="sm">
173
+
174
+ </Button>
175
+ </CardAction>
176
+ </CardHeader>
177
+ <CardContent>
178
+ <span className="text-2xl font-semibold">$750</span>
179
+ <span className="ml-2 text-sm text-muted-foreground">/ night</span>
109
180
  </CardContent>
110
- <CardFooter className="px-4 py-3 bg-gray-100 flex justify-between">
111
- <Button variant="outline">Detalle</Button>
112
- <Button className="bg-blue-600 text-white">Comprar</Button>
181
+ <CardFooter>
182
+ <Button variant="outline">Details</Button>
183
+ <Button className="ml-auto">Book</Button>
113
184
  </CardFooter>
114
- </Card>
185
+ </CardRoot>
115
186
  ),
116
187
  };