@boxcustodia/library 2.0.0-alpha.12 → 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 (174) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1087 -720
  3. package/dist/index.es.js +7011 -56097
  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 +99 -77
  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 +126 -51
  48. package/src/components/divider/divider.tsx +16 -16
  49. package/src/components/dropzone/dropzone.stories.tsx +71 -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 +165 -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 +227 -4
  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 +4 -4
  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 +276 -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 +31 -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 +154 -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 +29 -9
  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 +767 -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 +373 -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 +155 -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 +323 -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 +365 -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 +67 -37
  130. package/src/utils/index.ts +1 -1
  131. package/src/__doc__/Migration.mdx +0 -475
  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
@@ -0,0 +1,100 @@
1
+ import { OTPFieldPreview as OTPPrimitive } from "@base-ui/react/otp-field";
2
+ import type React from "react";
3
+ import { Fragment } from "react";
4
+
5
+ import { cn } from "../../lib";
6
+ import { Divider } from "../divider/divider";
7
+
8
+ export function OTPRoot({
9
+ className,
10
+ size = "default",
11
+ ...props
12
+ }: OTPPrimitive.Root.Props & {
13
+ size?: "default" | "lg";
14
+ }): React.ReactElement {
15
+ return (
16
+ <OTPPrimitive.Root
17
+ className={cn(
18
+ "flex items-center gap-2 has-disabled:opacity-64 has-disabled:**:data-[slot=otp-input]:shadow-none has-disabled:**:data-[slot=otp-input]:before:shadow-none!",
19
+ className,
20
+ )}
21
+ data-size={size}
22
+ data-slot="otp"
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ export function OTPInput({
29
+ className,
30
+ ...props
31
+ }: OTPPrimitive.Input.Props): React.ReactElement {
32
+ return (
33
+ <OTPPrimitive.Input
34
+ className={cn(
35
+ "in-[[data-slot=otp][data-size=lg]]:size-10 size-9 min-w-0 rounded-lg border border-input bg-background text-center in-[[data-slot=otp][data-size=lg]]:text-lg text-base text-foreground in-[[data-slot=otp][data-size=lg]]:leading-10 leading-9 outline-none transition-shadow focus-visible:z-10 focus-visible:border-ring aria-invalid:border-error focus-visible:aria-invalid:border-error focus-visible:aria-invalid:ring-error/20 in-[[data-slot=field][data-invalid]]:border-error in-[[data-slot=field][data-invalid]]:focus-visible:border-error in-[[data-slot=field][data-invalid]]:focus-visible:ring-error/20 sm:in-[[data-slot=otp][data-size=lg]]:size-9 sm:size-8 sm:in-[[data-slot=otp][data-size=lg]]:text-base sm:text-sm sm:in-[[data-slot=otp][data-size=lg]]:leading-9 sm:leading-8",
36
+ className,
37
+ )}
38
+ data-slot="otp-input"
39
+ spellCheck={false}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ export function OTPSeparator({
46
+ className,
47
+ ...props
48
+ }: React.ComponentProps<typeof Divider>): React.ReactElement {
49
+ return (
50
+ <OTPPrimitive.Separator
51
+ render={
52
+ <Divider
53
+ className={cn(
54
+ "rounded-full bg-input data-[orientation=horizontal]:h-0.5 data-[orientation=horizontal]:w-3",
55
+ className,
56
+ )}
57
+ orientation="horizontal"
58
+ {...props}
59
+ />
60
+ }
61
+ />
62
+ );
63
+ }
64
+
65
+ interface OTPProps extends Omit<OTPPrimitive.Root.Props, "children"> {
66
+ size?: "default" | "lg";
67
+ /** Insert a separator after this slot (1-indexed). */
68
+ split?: number;
69
+ inputProps?: OTPPrimitive.Input.Props;
70
+ }
71
+
72
+ export function OTP({
73
+ split,
74
+ inputProps,
75
+ size,
76
+ length,
77
+ "aria-invalid": ariaInvalid,
78
+ ...props
79
+ }: OTPProps): React.ReactElement {
80
+ const slotProps: OTPPrimitive.Input.Props = {
81
+ ...(ariaInvalid ? { "aria-invalid": true } : {}),
82
+ ...inputProps,
83
+ };
84
+
85
+ return (
86
+ <OTPRoot size={size} length={length} aria-invalid={ariaInvalid} {...props}>
87
+ {Array.from({ length }, (_, i) => (
88
+ <Fragment key={i}>
89
+ <OTPInput
90
+ aria-label={`Character ${i + 1} of ${length}`}
91
+ {...slotProps}
92
+ />
93
+ {split === i + 1 && <OTPSeparator />}
94
+ </Fragment>
95
+ ))}
96
+ </OTPRoot>
97
+ );
98
+ }
99
+
100
+ export { OTPPrimitive };
@@ -1 +1,2 @@
1
1
  export * from "./pagination";
2
+ export * from "./pagination.model";
@@ -0,0 +1,2 @@
1
+ export const PAGINATION_SIZES = [10, 25, 50] as const;
2
+ export type PageSize = (typeof PAGINATION_SIZES)[number] | number;
@@ -1,20 +1,57 @@
1
- import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { ComponentProps } from "react";
3
- import { action } from "storybook/actions";
4
- import { Pagination } from "../../components";
5
- import { cn } from "../../lib";
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import {
4
+ Pagination,
5
+ PaginationEllipsis,
6
+ PaginationFirst,
7
+ PaginationLast,
8
+ PaginationLink,
9
+ PaginationNext,
10
+ PaginationPageInfo,
11
+ PaginationPages,
12
+ PaginationPrevious,
13
+ PaginationRange,
14
+ PaginationRoot,
15
+ PaginationSizeSelect,
16
+ PaginationTotal,
17
+ } from "../../components";
6
18
 
19
+ /**
20
+ * Composite + primitives for pagination UI. The composite renders the default table layout
21
+ * (size selector + range + prev/next). Use primitives via `PaginationRoot` to compose any
22
+ * layout — numbered pages, page info, total only, etc. State is controllable through
23
+ * `currentPage` / `defaultCurrentPage` and `pageSize` / `defaultPageSize`. Pass `getPageHref`
24
+ * (and optionally `linkComponent`) to render navigation as anchors instead of buttons.
25
+ */
7
26
  const meta: Meta<typeof Pagination> = {
8
- title: "Data display/Pagination",
27
+ title: "Components/Pagination",
9
28
  component: Pagination,
10
- tags: ["autodocs"],
29
+ subcomponents: {
30
+ PaginationRoot,
31
+ PaginationFirst,
32
+ PaginationPrevious,
33
+ PaginationNext,
34
+ PaginationLast,
35
+ PaginationLink,
36
+ PaginationPages,
37
+ PaginationEllipsis,
38
+ PaginationTotal,
39
+ PaginationPageInfo,
40
+ PaginationRange,
41
+ PaginationSizeSelect,
42
+ },
43
+ argTypes: {
44
+ onCurrentPageChange: { action: "onCurrentPageChange" },
45
+ onPageSizeChange: { action: "onPageSizeChange" },
46
+ },
11
47
  args: {
12
- siblingCount: 1,
13
48
  totalItems: 100,
14
- pageSize: 10,
15
- initialCurrentPage: 1,
16
- onChange: action("onChange"),
49
+ defaultPageSize: 10,
17
50
  },
51
+ parameters: {
52
+ layout: "centered",
53
+ },
54
+ tags: ["beta"],
18
55
  };
19
56
 
20
57
  export default meta;
@@ -22,59 +59,117 @@ type Story = StoryObj<typeof meta>;
22
59
 
23
60
  export const Default: Story = {};
24
61
 
25
- export const DotsVariants: Story = {
26
- argTypes: {
27
- totalItems: { control: false },
28
- pageSize: { control: false },
29
- initialCurrentPage: { control: false },
30
- },
62
+ export const NoSizes: Story = {
63
+ args: { sizes: false },
64
+ };
65
+
66
+ export const CustomSizes: Story = {
67
+ args: { sizes: [5, 20, 100, 1000] },
68
+ };
69
+
70
+ /**
71
+ * Controlled `currentPage` via React state. The component never owns the page —
72
+ * useful when the page lives in a parent, URL params, or Zustand store.
73
+ */
74
+ export const Controlled: Story = {
31
75
  render: (args) => {
76
+ const [page, setPage] = useState(1);
32
77
  return (
33
- <div className="space-y-2">
34
- <h2>No hay suficientes elementos para mostrar puntos</h2>
35
- <Pagination
36
- {...(args as ComponentProps<typeof Pagination>)}
37
- pageSize={20}
38
- />
39
- <h2>Puntos a la derecha</h2>
40
- <Pagination
41
- {...(args as ComponentProps<typeof Pagination>)}
42
- initialCurrentPage={1}
43
- />
44
- <h2>Puntos de ambos lados</h2>
45
- <Pagination
46
- {...(args as ComponentProps<typeof Pagination>)}
47
- initialCurrentPage={5}
48
- />
49
- <h2>Puntos a la izquierda</h2>
50
- <Pagination
51
- {...(args as ComponentProps<typeof Pagination>)}
52
- initialCurrentPage={10}
53
- />
54
- </div>
78
+ <Pagination {...args} currentPage={page} onCurrentPageChange={setPage} />
55
79
  );
56
80
  },
57
81
  };
58
82
 
59
83
  /**
60
- * Se exponen los siguientes atributos:
61
- *
62
- * | Atributo | Descripción |
63
- * | --- | --- |
64
- * | `data-active` | si el elemento es el activo |
65
- * | `data-dots` | si el elemento son puntos|
66
- **/
67
- export const Custom: Story = {
68
- args: {
69
- className: "gap-x-4",
70
- optionProps: {
71
- className: cn(
72
- "p-0 w-9 h-9 rounded-full flex justify-center items-center",
73
- "data-[active=true]:bg-error data-[active=true]:text-error-foreground data-[active=true]:hover:bg-error/90",
74
- "data-[dots=true]:border-none data-[dots=true]:bg-transparent",
75
- "[&:nth-child(-n+2)]:border-none [&:nth-child(-n+2)]:bg-background",
76
- "[&:nth-last-child(-n+2)]:border-none [&:nth-last-child(-n+2)]:bg-background",
77
- ),
78
- },
79
- },
84
+ * Compose freely with primitives. Example: numbered pagination with first/last buttons
85
+ * and a DOTS-aware page list (the legacy `Pagination` numbered API).
86
+ */
87
+ export const WithPageNumbers: Story = {
88
+ args: { totalItems: 200, defaultPageSize: 10 },
89
+ render: (args) => (
90
+ <PaginationRoot {...args}>
91
+ <PaginationFirst />
92
+ <PaginationPrevious />
93
+ <PaginationPages />
94
+ <PaginationNext />
95
+ <PaginationLast />
96
+ </PaginationRoot>
97
+ ),
98
+ };
99
+
100
+ /**
101
+ * Compact layout showing total + page info between prev/next, e.g. "25 resultados < Página 1 de 3 >".
102
+ */
103
+ export const WithTotalAndPageInfo: Story = {
104
+ args: { totalItems: 25, defaultPageSize: 10 },
105
+ render: (args) => (
106
+ <PaginationRoot {...args}>
107
+ <PaginationTotal />
108
+ <PaginationPrevious />
109
+ <PaginationPageInfo />
110
+ <PaginationNext />
111
+ </PaginationRoot>
112
+ ),
113
+ };
114
+
115
+ /**
116
+ * Custom display content via render-prop children. `Total`, `Range`, and `PageInfo` accept
117
+ * either ReactNode or a function returning ReactNode.
118
+ */
119
+ export const CustomDisplays: Story = {
120
+ args: { totalItems: 47, defaultPageSize: 5 },
121
+ render: (args) => (
122
+ <PaginationRoot {...args}>
123
+ <PaginationTotal>{(n) => <strong>{n} elementos</strong>}</PaginationTotal>
124
+ <PaginationRange>
125
+ {({ start, end, total }) => (
126
+ <em>
127
+ Mostrando {start}–{end} de {total}
128
+ </em>
129
+ )}
130
+ </PaginationRange>
131
+ <PaginationPrevious />
132
+ <PaginationPageInfo>
133
+ {({ page, max }) => (
134
+ <span>
135
+ {page}/{max}
136
+ </span>
137
+ )}
138
+ </PaginationPageInfo>
139
+ <PaginationNext />
140
+ </PaginationRoot>
141
+ ),
142
+ };
143
+
144
+ /**
145
+ * URL-driven mode. Passing `getPageHref` makes Previous/Next/Pages/First/Last render as
146
+ * anchors. Default `linkComponent` is `<a>`; pass `linkComponent={Link}` for React Router /
147
+ * Next.js wrapping a component that accepts `href`. The `onCurrentPageChange` callback
148
+ * still fires on click, so you can sync your router state imperatively.
149
+ */
150
+ export const WithUrl: Story = {
151
+ args: { totalItems: 100, defaultPageSize: 10 },
152
+ render: (args) => (
153
+ <PaginationRoot
154
+ {...args}
155
+ getPageHref={({ page, pageSize }) => `?page=${page}&size=${pageSize}`}
156
+ >
157
+ <PaginationPrevious />
158
+ <PaginationPages />
159
+ <PaginationNext />
160
+ </PaginationRoot>
161
+ ),
162
+ };
163
+
164
+ /**
165
+ * Standalone `PaginationSizeSelect` outside the composite — wire it however you want.
166
+ */
167
+ export const SizeSelectOnly: Story = {
168
+ args: { totalItems: 100 },
169
+ render: (args) => (
170
+ <PaginationRoot {...args}>
171
+ <PaginationSizeSelect />
172
+ <PaginationRange />
173
+ </PaginationRoot>
174
+ ),
80
175
  };
@@ -1,76 +1,141 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
2
- import { describe, expect, it } from "vitest";
3
- import { Pagination } from "../../components";
4
- import { DOTS } from "../../hooks";
5
- import { click } from "../../utils";
6
-
7
- describe("Pagination component", () => {
8
- it("should render correctly", () => {
9
- render(<Pagination pageSize={10} totalItems={100} />);
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ Pagination,
5
+ PaginationNext,
6
+ PaginationPageInfo,
7
+ PaginationPages,
8
+ PaginationPrevious,
9
+ PaginationRoot,
10
+ PaginationTotal,
11
+ } from "../../components";
12
+ import { click } from "../../utils/tests";
13
+
14
+ describe("Pagination composite", () => {
15
+ it("renders default range text", () => {
16
+ render(<Pagination defaultPageSize={10} totalItems={30} />);
17
+ expect(screen.getByText(/1 - 10 de 30/)).toBeInTheDocument();
10
18
  });
11
19
 
12
- it("should navigate to page", async () => {
13
- render(<Pagination pageSize={10} totalItems={100} />);
14
-
15
- const page = screen.getByText(/Ir a la página 3/i).parentElement!;
16
- click(page);
17
-
18
- waitFor(() => {
19
- expect(page).toHaveAttribute("active", "true");
20
- });
20
+ it("navigates to next page", () => {
21
+ render(<Pagination defaultPageSize={10} totalItems={30} />);
22
+ click(screen.getByRole("button", { name: /próxima página/i }));
23
+ expect(screen.getByText(/11 - 20 de 30/)).toBeInTheDocument();
21
24
  });
22
25
 
23
- it("should navigate to first page", async () => {
24
- render(<Pagination pageSize={10} totalItems={100} />);
25
-
26
- const page = screen.getByText(/Ir a primera página/i);
27
- click(page);
28
-
29
- waitFor(() => {
30
- expect(page).toHaveAttribute("active", "true");
31
- });
26
+ it("navigates to previous page", () => {
27
+ render(
28
+ <Pagination
29
+ defaultCurrentPage={2}
30
+ defaultPageSize={10}
31
+ totalItems={30}
32
+ />,
33
+ );
34
+ click(screen.getByRole("button", { name: /anterior/i }));
35
+ expect(screen.getByText(/1 - 10 de 30/)).toBeInTheDocument();
32
36
  });
33
37
 
34
- it("should navigate to previous page", async () => {
35
- render(<Pagination pageSize={10} totalItems={100} />);
36
-
37
- const page = screen.getByText(/Ir a página anterior/i);
38
- click(page);
39
-
40
- waitFor(() => {
41
- expect(page).toHaveAttribute("active", "true");
42
- });
38
+ it("disables prev on first page", () => {
39
+ render(<Pagination defaultPageSize={10} totalItems={30} />);
40
+ expect(screen.getByRole("button", { name: /anterior/i })).toBeDisabled();
43
41
  });
44
42
 
45
- it("should navigate to next page", async () => {
46
- render(<Pagination pageSize={10} totalItems={100} />);
43
+ it("disables next on last page", () => {
44
+ render(
45
+ <Pagination
46
+ defaultCurrentPage={3}
47
+ defaultPageSize={10}
48
+ totalItems={30}
49
+ />,
50
+ );
51
+ expect(screen.getByRole("button", { name: /próxima/i })).toBeDisabled();
52
+ });
47
53
 
48
- const page = screen.getByText(/Ir a próxima página/i);
49
- click(page);
54
+ it("hides size selector when sizes is false", () => {
55
+ render(<Pagination defaultPageSize={10} totalItems={30} sizes={false} />);
56
+ expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
57
+ });
50
58
 
51
- waitFor(() => {
52
- expect(page).toHaveAttribute("active", "true");
53
- });
59
+ it("calls onCurrentPageChange controlled", () => {
60
+ const onChange = vi.fn();
61
+ render(
62
+ <Pagination
63
+ currentPage={1}
64
+ defaultPageSize={10}
65
+ totalItems={30}
66
+ onCurrentPageChange={onChange}
67
+ />,
68
+ );
69
+ click(screen.getByRole("button", { name: /próxima/i }));
70
+ expect(onChange).toHaveBeenCalledWith(2);
54
71
  });
72
+ });
55
73
 
56
- it("should navigate to last page", async () => {
57
- render(<Pagination pageSize={10} totalItems={100} />);
74
+ describe("Pagination primitives", () => {
75
+ it("renders page numbers via PaginationPages", () => {
76
+ render(
77
+ <PaginationRoot defaultPageSize={10} totalItems={30}>
78
+ <PaginationPages />
79
+ </PaginationRoot>,
80
+ );
81
+ expect(
82
+ screen.getByRole("button", { name: /Ir a la página 1/i }),
83
+ ).toBeInTheDocument();
84
+ expect(
85
+ screen.getByRole("button", { name: /Ir a la página 2/i }),
86
+ ).toBeInTheDocument();
87
+ expect(
88
+ screen.getByRole("button", { name: /Ir a la página 3/i }),
89
+ ).toBeInTheDocument();
90
+ });
58
91
 
59
- const page = screen.getByText(/Ir a última página/i);
60
- click(page);
92
+ it("renders DOTS when range overflows", () => {
93
+ render(
94
+ <PaginationRoot defaultPageSize={10} totalItems={200}>
95
+ <PaginationPages />
96
+ </PaginationRoot>,
97
+ );
98
+ expect(screen.getAllByText(/Más páginas/).length).toBeGreaterThan(0);
99
+ });
61
100
 
62
- waitFor(() => {
63
- expect(page).toHaveAttribute("active", "true");
64
- });
101
+ it("renders total + pageInfo + prev/next layout", () => {
102
+ render(
103
+ <PaginationRoot defaultPageSize={10} totalItems={25}>
104
+ <PaginationTotal />
105
+ <PaginationPrevious />
106
+ <PaginationPageInfo />
107
+ <PaginationNext />
108
+ </PaginationRoot>,
109
+ );
110
+ expect(screen.getByText(/25 resultados/)).toBeInTheDocument();
111
+ expect(screen.getByText(/Página 1 de 3/)).toBeInTheDocument();
65
112
  });
66
113
 
67
- it("should render dots", async () => {
68
- render(<Pagination pageSize={10} totalItems={100} />);
69
- const dots = screen.getByText(DOTS);
114
+ it("renders anchors when getPageHref is provided", () => {
115
+ const { container } = render(
116
+ <PaginationRoot
117
+ defaultPageSize={10}
118
+ defaultCurrentPage={2}
119
+ totalItems={30}
120
+ getPageHref={({ page }) => `/users?page=${page}`}
121
+ >
122
+ <PaginationPrevious />
123
+ <PaginationNext />
124
+ </PaginationRoot>,
125
+ );
126
+ const anchors = container.querySelectorAll("a[href]");
127
+ expect(anchors.length).toBe(2);
128
+ expect(anchors[0]).toHaveAttribute("href", "/users?page=1");
129
+ expect(anchors[1]).toHaveAttribute("href", "/users?page=3");
130
+ });
70
131
 
71
- waitFor(() => {
72
- expect(dots).toBeInTheDocument();
73
- expect(dots).toHaveAttribute("dots", "true");
74
- });
132
+ it("throws when used outside Root", () => {
133
+ const consoleError = vi
134
+ .spyOn(console, "error")
135
+ .mockImplementation(() => {});
136
+ expect(() => render(<PaginationNext />)).toThrow(
137
+ /must be used inside <PaginationRoot>/,
138
+ );
139
+ consoleError.mockRestore();
75
140
  });
76
141
  });