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

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 (173) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1083 -715
  3. package/dist/index.es.js +7077 -56175
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Examples.tsx +1 -1
  7. package/src/__doc__/Intro.mdx +3 -3
  8. package/src/__doc__/Tabs.mdx +112 -0
  9. package/src/__doc__/V2.mdx +1246 -0
  10. package/src/components/accordion/accordion.stories.tsx +143 -0
  11. package/src/components/accordion/accordion.tsx +135 -0
  12. package/src/components/accordion/index.ts +1 -0
  13. package/src/components/alert/alert.stories.tsx +24 -4
  14. package/src/components/alert/alert.tsx +17 -9
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  17. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  18. package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
  19. package/src/components/auto-complete/auto-complete.tsx +420 -68
  20. package/src/components/auto-complete/index.ts +0 -1
  21. package/src/components/avatar/avatar.stories.tsx +162 -21
  22. package/src/components/avatar/avatar.tsx +79 -20
  23. package/src/components/button/button.stories.tsx +219 -294
  24. package/src/components/button/button.test.tsx +10 -17
  25. package/src/components/button/button.tsx +78 -19
  26. package/src/components/button/components/base-button.tsx +30 -53
  27. package/src/components/button/index.ts +0 -1
  28. package/src/components/calendar/calendar.stories.tsx +1 -1
  29. package/src/components/calendar/calendar.tsx +4 -4
  30. package/src/components/card/card.stories.tsx +141 -69
  31. package/src/components/card/card.tsx +155 -54
  32. package/src/components/center/center.stories.tsx +22 -39
  33. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  34. package/src/components/checkbox/checkbox.tsx +76 -15
  35. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  36. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  37. package/src/components/combobox/combobox.stories.tsx +33 -23
  38. package/src/components/combobox/combobox.tsx +119 -103
  39. package/src/components/date-picker/date-input.stories.tsx +14 -6
  40. package/src/components/date-picker/date-input.tsx +2 -2
  41. package/src/components/date-picker/date-picker.model.ts +13 -4
  42. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  43. package/src/components/date-picker/date-picker.tsx +28 -14
  44. package/src/components/dialog/dialog.stories.tsx +18 -0
  45. package/src/components/dialog/dialog.test.tsx +1 -1
  46. package/src/components/dialog/dialog.tsx +51 -20
  47. package/src/components/divider/divider.stories.tsx +6 -0
  48. package/src/components/dropzone/dropzone.stories.tsx +71 -90
  49. package/src/components/dropzone/dropzone.tsx +383 -105
  50. package/src/components/dropzone/index.ts +0 -1
  51. package/src/components/empty/empty.stories.tsx +165 -0
  52. package/src/components/empty/empty.tsx +156 -0
  53. package/src/components/empty/index.ts +1 -0
  54. package/src/components/field/field.stories.tsx +226 -3
  55. package/src/components/field/field.tsx +77 -42
  56. package/src/components/form/form.stories.tsx +320 -197
  57. package/src/components/form/form.tsx +3 -23
  58. package/src/components/index.ts +2 -6
  59. package/src/components/input/input.stories.tsx +5 -5
  60. package/src/components/input/input.tsx +4 -4
  61. package/src/components/kbd/kbd.stories.tsx +1 -0
  62. package/src/components/label/label.stories.tsx +16 -0
  63. package/src/components/label/label.tsx +13 -2
  64. package/src/components/loader/loader.stories.tsx +7 -5
  65. package/src/components/loader/loader.tsx +8 -3
  66. package/src/components/menu/menu-primitives.tsx +207 -196
  67. package/src/components/menu/menu.stories.tsx +276 -146
  68. package/src/components/menu/menu.tsx +146 -54
  69. package/src/components/number-input/number-input.stories.tsx +27 -4
  70. package/src/components/number-input/number-input.test.tsx +2 -2
  71. package/src/components/number-input/number-input.tsx +25 -29
  72. package/src/components/otp/index.ts +1 -0
  73. package/src/components/otp/otp.stories.tsx +209 -0
  74. package/src/components/otp/otp.tsx +100 -0
  75. package/src/components/pagination/index.ts +1 -0
  76. package/src/components/pagination/pagination.model.ts +2 -0
  77. package/src/components/pagination/pagination.stories.tsx +154 -59
  78. package/src/components/pagination/pagination.test.tsx +122 -57
  79. package/src/components/pagination/pagination.tsx +575 -77
  80. package/src/components/password/password.stories.tsx +18 -3
  81. package/src/components/password/password.tsx +26 -10
  82. package/src/components/popover/popover.stories.tsx +26 -5
  83. package/src/components/popover/popover.tsx +15 -23
  84. package/src/components/progress/progress.stories.tsx +1 -0
  85. package/src/components/radio-group/index.ts +1 -0
  86. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  87. package/src/components/radio-group/radio-group.tsx +212 -0
  88. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  89. package/src/components/select/select.stories.tsx +118 -19
  90. package/src/components/select/select.tsx +67 -62
  91. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  92. package/src/components/stack/stack.stories.tsx +179 -89
  93. package/src/components/stack/stack.tsx +2 -2
  94. package/src/components/stepper/index.ts +1 -1
  95. package/src/components/stepper/stepper.stories.tsx +767 -83
  96. package/src/components/stepper/stepper.test.tsx +18 -18
  97. package/src/components/stepper/stepper.tsx +554 -0
  98. package/src/components/switch/switch.stories.tsx +15 -1
  99. package/src/components/switch/switch.tsx +17 -4
  100. package/src/components/table/index.ts +0 -2
  101. package/src/components/table/table.stories.tsx +131 -18
  102. package/src/components/table/table.test.tsx +1 -1
  103. package/src/components/table/table.tsx +183 -77
  104. package/src/components/tabs/tabs.stories.tsx +373 -155
  105. package/src/components/tabs/tabs.test.tsx +12 -12
  106. package/src/components/tabs/tabs.tsx +72 -149
  107. package/src/components/tag/index.ts +0 -1
  108. package/src/components/tag/tag.stories.tsx +155 -120
  109. package/src/components/tag/tag.tsx +47 -95
  110. package/src/components/textarea/textarea.stories.tsx +8 -22
  111. package/src/components/textarea/textarea.tsx +17 -79
  112. package/src/components/timeline/timeline.stories.tsx +323 -42
  113. package/src/components/timeline/timeline.tsx +359 -132
  114. package/src/components/toast/toast.stories.tsx +1 -0
  115. package/src/components/tooltip/tooltip.tsx +11 -9
  116. package/src/components/tree/index.ts +0 -1
  117. package/src/components/tree/tree.stories.tsx +365 -408
  118. package/src/components/tree/tree.test.tsx +163 -0
  119. package/src/components/tree/tree.tsx +212 -36
  120. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  121. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  122. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  123. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  124. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  125. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  126. package/src/hooks/usePagination/usePagination.tsx +36 -24
  127. package/src/styles/theme.css +1 -1
  128. package/src/utils/form.tsx +67 -37
  129. package/src/utils/index.ts +1 -1
  130. package/src/__doc__/Migration.mdx +0 -451
  131. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  132. package/src/components/background-image/background-image.stories.tsx +0 -21
  133. package/src/components/background-image/background-image.test.tsx +0 -29
  134. package/src/components/background-image/background-image.tsx +0 -23
  135. package/src/components/background-image/index.ts +0 -1
  136. package/src/components/button/button.variants.ts +0 -44
  137. package/src/components/button/components/loader-overlay.tsx +0 -21
  138. package/src/components/button/components/loading-icon.tsx +0 -47
  139. package/src/components/dropzone/upload-primitives.tsx +0 -310
  140. package/src/components/dropzone/use-dropzone.ts +0 -122
  141. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  142. package/src/components/empty-state/empty-state.tsx +0 -39
  143. package/src/components/empty-state/index.ts +0 -1
  144. package/src/components/heading/heading.stories.tsx +0 -74
  145. package/src/components/heading/heading.tsx +0 -28
  146. package/src/components/heading/heading.variants.ts +0 -27
  147. package/src/components/heading/index.ts +0 -1
  148. package/src/components/kbd/kbd.variants.ts +0 -26
  149. package/src/components/menu/util/render-menu-item.tsx +0 -54
  150. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  151. package/src/components/multi-select/index.ts +0 -1
  152. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  153. package/src/components/multi-select/multi-select.tsx +0 -300
  154. package/src/components/multi-select/multi-select.variants.ts +0 -22
  155. package/src/components/pagination/components/pagination-option.tsx +0 -27
  156. package/src/components/show/index.ts +0 -1
  157. package/src/components/show/show.stories.tsx +0 -197
  158. package/src/components/show/show.test.tsx +0 -41
  159. package/src/components/show/show.tsx +0 -16
  160. package/src/components/stepper/Stepper.tsx +0 -190
  161. package/src/components/stepper/context/stepper-context.tsx +0 -11
  162. package/src/components/table/table-primitives.tsx +0 -122
  163. package/src/components/table/table.model.ts +0 -20
  164. package/src/components/table-pagination/index.ts +0 -2
  165. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  167. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  168. package/src/components/table-pagination/table-pagination.tsx +0 -108
  169. package/src/components/tabs/context/tabs-context.tsx +0 -14
  170. package/src/components/tag/tag.variants.ts +0 -31
  171. package/src/components/timeline/timeline-status.ts +0 -5
  172. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  173. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -1,31 +1,90 @@
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, cloneElement, type ReactElement } 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 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]>:not([data-loader])]:opacity-0",
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:
35
+ "h-8 [--btn-gap:0.375rem] px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
36
+ xs: "h-6 [--btn-gap:0.25rem] rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
37
+ sm: "h-7 [--btn-gap:0.25rem] rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
38
+ lg: "h-9 [--btn-gap:0.375rem] px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
39
+ icon: "size-8",
40
+ "icon-xs":
41
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
42
+ "icon-sm":
43
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
44
+ "icon-lg": "size-9",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ variant: "default",
49
+ size: "default",
50
+ },
51
+ },
52
+ );
53
+
8
54
  type BaseButtonProps = ComponentProps<typeof BaseButton>;
9
- type Props = BaseButtonProps & VariantProps<typeof buttonVariants>;
55
+ type Props = BaseButtonProps &
56
+ VariantProps<typeof buttonVariants> & {
57
+ icon?: ReactElement;
58
+ };
10
59
 
11
- export const Button = (props: Props) => {
12
- const { variant, size, shape, className, ...rest } = useThemeProps(
13
- "Button",
14
- props,
15
- );
60
+ const Button = ({
61
+ variant,
62
+ size,
63
+ className,
64
+ icon,
65
+ children,
66
+ ...rest
67
+ }: Props) => {
68
+ const iconPosition =
69
+ (icon?.props as { "data-icon"?: string } | undefined)?.["data-icon"] ??
70
+ "inline-start";
71
+ const iconEl = icon
72
+ ? cloneElement(icon as ReactElement<{ "data-icon"?: string }>, {
73
+ "data-icon": iconPosition,
74
+ })
75
+ : null;
16
76
 
17
77
  return (
18
78
  <BaseButton
19
79
  {...rest}
20
- className={cn(
21
- buttonVariants({
22
- variant,
23
- size,
24
- shape,
25
- }),
26
- className,
27
- )}
80
+ className={cn(buttonVariants({ variant, size }), className)}
28
81
  data-variant={variant || "default"}
29
- />
82
+ >
83
+ {iconEl && iconPosition !== "inline-end" && iconEl}
84
+ {children}
85
+ {iconEl && iconPosition === "inline-end" && iconEl}
86
+ </BaseButton>
30
87
  );
31
88
  };
89
+
90
+ export { Button, buttonVariants };
@@ -1,86 +1,63 @@
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";
5
+ import { cn } from "../../../lib";
7
6
 
8
- interface Props extends ComponentProps<"button"> {
9
- asChild?: boolean;
7
+ interface Props extends ComponentProps<typeof ButtonPrimitive> {
10
8
  loading?: boolean;
11
- showLoader?: boolean;
12
- icon?: ReactNode;
13
- iconPosition?: "start" | "end";
14
- loaderReplace?: true;
15
9
  }
16
10
 
17
- const baseStyles = {
18
- display: "inline-flex",
19
- alignItems: "center",
20
- gap: "0.5rem",
21
- };
11
+ type OnClickEvent = Parameters<
12
+ NonNullable<ComponentProps<typeof ButtonPrimitive>["onClick"]>
13
+ >[0];
22
14
 
23
15
  export const BaseButton = ({
24
- asChild = false,
25
16
  onClick,
26
17
  loading: prop,
27
- showLoader = true,
28
- icon,
29
- iconPosition = "start",
30
- loaderReplace,
31
18
  style,
19
+ children,
32
20
  ...props
33
21
  }: Props) => {
34
- const Comp = asChild ? Slot : "button";
35
22
  const [loading = false, setLoading] = useControllableState<boolean>({
36
23
  prop,
37
24
  defaultProp: false,
38
25
  });
39
26
 
40
- const handleClick = async (e: ClickEvent) => {
27
+ const handleClick = async (e: OnClickEvent) => {
41
28
  if (!onClick || loading) return;
42
29
 
43
30
  const onClickResult = onClick(e) as unknown;
44
31
 
45
32
  if (onClickResult instanceof Promise) {
46
- showLoader && setLoading(true);
33
+ setLoading(true);
47
34
  await onClickResult.finally(() => setLoading(false));
48
35
  }
49
36
  };
50
37
 
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
38
  return (
69
- <Comp
70
- type="button"
39
+ <ButtonPrimitive
71
40
  data-slot="button"
41
+ data-loading={loading || undefined}
42
+ aria-busy={loading || undefined}
72
43
  {...props}
73
44
  onClick={handleClick}
74
- style={{
75
- ...baseStyles,
76
- position: isReplaceMode ? "relative" : undefined,
77
- ...style,
78
- }}
45
+ style={style}
79
46
  >
80
- {renderIconWithLoader("start")}
81
- <Slottable>{props.children}</Slottable>
82
- {renderIconWithLoader("end")}
83
- {isReplaceMode && <LoaderOverlay loading={true} />}
84
- </Comp>
47
+ <span className="inline-flex items-center gap-(--btn-gap)">
48
+ {children}
49
+ </span>
50
+ <LoaderCircle
51
+ data-loader
52
+ data-testid="btn-loader"
53
+ className={cn(
54
+ "hidden animate-spin",
55
+ "group-data-[loading]/button:absolute",
56
+ "group-data-[loading]/button:block",
57
+ "group-data-[loading]/button:inset-0",
58
+ "group-data-[loading]/button:m-auto",
59
+ )}
60
+ />
61
+ </ButtonPrimitive>
85
62
  );
86
63
  };
@@ -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-[32px]"
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,188 @@
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
+ tags: ["beta"],
47
+ parameters: { layout: "centered" },
51
48
  };
52
49
 
53
50
  export default meta;
54
-
55
51
  type Story = StoryObj<typeof Card>;
56
52
 
57
53
  /**
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.
54
+ * `className` styles the card root. `classNames` exposes the `header`,
55
+ * `title`, `description`, `action`, `content`, and `footer` slots for
56
+ * fine-grained styling without dropping to primitives.
60
57
  */
58
+ export const WithClassNames: Story = {
59
+ args: {
60
+ children: "Card body content.",
61
+ classNames: {
62
+ title: "text-primary",
63
+ description: "italic",
64
+ content: "text-base",
65
+ },
66
+ },
67
+ };
68
+
61
69
  export const Default: Story = {
70
+ render: (args) => (
71
+ <Card
72
+ {...args}
73
+ footer={
74
+ <>
75
+ <Button variant="outline">Read more</Button>
76
+ <Button className="ml-auto">Save</Button>
77
+ </>
78
+ }
79
+ >
80
+ <p className="text-muted-foreground leading-relaxed">
81
+ Discover strategies and tools to boost your productivity. Learn to
82
+ manage your time effectively and stay focused on what matters.
83
+ </p>
84
+ </Card>
85
+ ),
86
+ };
87
+
88
+ export const Sizes: Story = {
62
89
  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.
90
+ <div className="flex flex-col gap-4 w-[350px]">
91
+ <Card title="Total Revenue" description="Compared to last month">
92
+ <p className="text-3xl font-bold">$45,231.89</p>
93
+ <p className="text-xs text-muted-foreground mt-1">
94
+ +20.1% from last month
75
95
  </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>
96
+ </Card>
97
+ <Card size="sm" title="Active Users" description="Compared to last month">
98
+ <p className="text-3xl font-bold">2,350</p>
99
+ <p className="text-xs text-muted-foreground mt-1">+180 new users</p>
100
+ </Card>
101
+ </div>
102
+ ),
103
+ };
104
+
105
+ /**
106
+ * `CardAction` anchors to the top-right corner of the header and spans both the title and description rows.
107
+ * Pass any node — icon button, badge, menu trigger — via the `action` prop.
108
+ */
109
+ export const WithAction: Story = {
110
+ render: (args) => (
111
+ <Card
112
+ {...args}
113
+ title="Team Members"
114
+ description="Manage your workspace members"
115
+ action={
116
+ <Button variant="outline" size="sm">
117
+ Invite
118
+ </Button>
119
+ }
120
+ >
121
+ <div className="flex flex-col gap-2">
122
+ {Array.from({ length: 3 }).map((_, i) => (
123
+ <div key={i} className="flex items-center justify-between">
124
+ <span className="text-sm">{faker.person.fullName()}</span>
125
+ <span className="text-xs text-muted-foreground">
126
+ {faker.internet.email()}
127
+ </span>
128
+ </div>
129
+ ))}
130
+ </div>
131
+ </Card>
132
+ ),
133
+ };
134
+
135
+ export const WithFooter: Story = {
136
+ render: (args) => (
137
+ <Card
138
+ {...args}
139
+ title="Delete Account"
140
+ description="This action is permanent and cannot be undone."
141
+ footer={
142
+ <div className="flex gap-2 w-full justify-end">
143
+ <Button variant="outline">Cancel</Button>
144
+ <Button variant="error">Delete</Button>
145
+ </div>
146
+ }
147
+ >
148
+ <p className="text-muted-foreground text-sm leading-relaxed">
149
+ All your data, settings, and integrations will be permanently removed
150
+ from our servers.
151
+ </p>
81
152
  </Card>
82
153
  ),
83
154
  };
84
155
 
85
156
  /**
86
- * Ejemplo de Card con imagen y diseño de producto.
87
- * Demuestra cómo integrar elementos multimedia y crear layouts más complejos.
157
+ * Use `CardRoot` + sub-components for layouts the composite cannot express.
158
+ * An `<img>` placed as the first child of `CardRoot` auto-clips to the card's top-rounded corners
159
+ * via the `has-[>img:first-child]:pt-0` and `*:[img:first-child]:rounded-t-xl` selectors.
88
160
  */
89
- export const Media: Story = {
161
+ export const Primitive: Story = {
90
162
  render: () => (
91
- <Card className="w-[350px] shadow-lg rounded-lg overflow-hidden">
163
+ <CardRoot className="w-[350px]">
92
164
  <img
93
- src={faker.image.url()}
94
- alt="Featured Product"
165
+ src={faker.image.urlPicsumPhotos({ width: 350, height: 150 })}
166
+ alt="Villa Luxury"
95
167
  className="w-full h-[150px] object-cover"
96
168
  />
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>
169
+ <CardHeader>
170
+ <CardTitle>Villa Luxury</CardTitle>
171
+ <CardDescription>Ocean views and private pool.</CardDescription>
172
+ <CardAction>
173
+ <Button variant="ghost" size="sm">
174
+
175
+ </Button>
176
+ </CardAction>
177
+ </CardHeader>
178
+ <CardContent>
179
+ <span className="text-2xl font-semibold">$750</span>
180
+ <span className="ml-2 text-sm text-muted-foreground">/ night</span>
109
181
  </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>
182
+ <CardFooter>
183
+ <Button variant="outline">Details</Button>
184
+ <Button className="ml-auto">Book</Button>
113
185
  </CardFooter>
114
- </Card>
186
+ </CardRoot>
115
187
  ),
116
188
  };