@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20

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 (265) hide show
  1. package/dist/components/button/button.cjs.js +1 -1
  2. package/dist/components/button/button.es.js +19 -18
  3. package/dist/components/button/components/base-button.cjs.js +1 -1
  4. package/dist/components/button/components/base-button.es.js +20 -20
  5. package/dist/components/calendar/calendar.cjs.js +1 -1
  6. package/dist/components/calendar/calendar.es.js +1 -0
  7. package/dist/components/date-picker/date-input.cjs.js +1 -1
  8. package/dist/components/date-picker/date-input.es.js +92 -75
  9. package/dist/components/date-picker/date-picker.cjs.js +1 -1
  10. package/dist/components/date-picker/date-picker.es.js +104 -95
  11. package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
  12. package/dist/components/date-picker/date-picker.utils.es.js +51 -43
  13. package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
  14. package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
  15. package/dist/components/menu/menu.es.js +1 -9
  16. package/dist/components/otp/otp.cjs.js +2 -0
  17. package/dist/components/otp/otp.es.js +93 -0
  18. package/dist/components/password/password.cjs.js +1 -1
  19. package/dist/components/password/password.es.js +2 -2
  20. package/dist/components/select/select.cjs.js +1 -1
  21. package/dist/components/select/select.es.js +68 -60
  22. package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
  23. package/dist/hooks/internal/is-apple-device.es.js +9 -0
  24. package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
  25. package/dist/hooks/internal/use-latest-ref.es.js +11 -0
  26. package/dist/hooks/use-array/use-array.cjs.js +1 -1
  27. package/dist/hooks/use-array/use-array.es.js +54 -42
  28. package/dist/hooks/use-async/use-async.cjs.js +1 -1
  29. package/dist/hooks/use-async/use-async.es.js +53 -20
  30. package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
  31. package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
  32. package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
  33. package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
  34. package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
  35. package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
  36. package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
  37. package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
  38. package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
  39. package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
  40. package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
  41. package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
  42. package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
  43. package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
  44. package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
  45. package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
  46. package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
  47. package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
  48. package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
  49. package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
  50. package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
  51. package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
  52. package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
  53. package/dist/hooks/use-hover/use-hover.es.js +32 -17
  54. package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
  55. package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
  56. package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
  57. package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
  58. package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
  59. package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
  60. package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
  61. package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
  62. package/dist/hooks/use-object/use-object.cjs.js +1 -1
  63. package/dist/hooks/use-object/use-object.es.js +26 -22
  64. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
  65. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
  66. package/dist/hooks/use-step/use-step.cjs.js +1 -1
  67. package/dist/hooks/use-step/use-step.es.js +25 -24
  68. package/dist/index.cjs.js +1 -1
  69. package/dist/index.es.js +308 -300
  70. package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
  71. package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
  72. package/dist/src/components/index.d.ts +1 -0
  73. package/dist/src/hooks/index.d.ts +2 -2
  74. package/dist/src/hooks/internal/index.d.ts +2 -0
  75. package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
  76. package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
  77. package/dist/src/hooks/use-array/use-array.d.ts +24 -11
  78. package/dist/src/hooks/use-async/use-async.d.ts +16 -13
  79. package/dist/src/hooks/use-boolean/index.d.ts +1 -0
  80. package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
  81. package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
  82. package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
  83. package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
  84. package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
  85. package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
  86. package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
  87. package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
  88. package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
  89. package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
  90. package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
  91. package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
  92. package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
  93. package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
  94. package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
  95. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
  96. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
  97. package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
  98. package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
  99. package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
  100. package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
  101. package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
  102. package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
  103. package/dist/src/hooks/use-object/use-object.d.ts +15 -6
  104. package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
  105. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
  106. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
  107. package/dist/src/hooks/use-step/use-step.d.ts +18 -11
  108. package/dist/src/utils/form.d.ts +10 -0
  109. package/package.json +1 -1
  110. package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
  111. package/src/components/auto-complete/auto-complete.test.tsx +4 -14
  112. package/src/components/avatar/avatar.test.tsx +7 -12
  113. package/src/components/button/button.test.tsx +10 -15
  114. package/src/components/button/button.tsx +14 -9
  115. package/src/components/button/components/base-button.tsx +2 -4
  116. package/src/components/calendar/calendar.test.tsx +12 -19
  117. package/src/components/calendar/calendar.tsx +4 -0
  118. package/src/components/card/card.test.tsx +4 -6
  119. package/src/components/checkbox/checkbox.test.tsx +12 -8
  120. package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
  121. package/src/components/combobox/combobox.test.tsx +24 -21
  122. package/src/components/date-picker/date-input-form.test.tsx +77 -0
  123. package/src/components/date-picker/date-input.stories.tsx +30 -18
  124. package/src/components/date-picker/date-input.tsx +77 -44
  125. package/src/components/date-picker/date-picker.stories.tsx +31 -1
  126. package/src/components/date-picker/date-picker.test.tsx +3 -13
  127. package/src/components/date-picker/date-picker.tsx +35 -16
  128. package/src/components/date-picker/date-picker.utils.test.ts +32 -14
  129. package/src/components/date-picker/date-picker.utils.ts +33 -0
  130. package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
  131. package/src/components/date-picker/use-hidden-field-value.ts +23 -0
  132. package/src/components/dialog/dialog.test.tsx +10 -8
  133. package/src/components/dropzone/dropzone.test.tsx +11 -13
  134. package/src/components/empty/empty.test.tsx +4 -3
  135. package/src/components/field/field.test.tsx +12 -13
  136. package/src/components/form/form.stories.tsx +16 -1
  137. package/src/components/index.ts +1 -0
  138. package/src/components/label/label.test.tsx +3 -3
  139. package/src/components/menu/menu.tsx +1 -5
  140. package/src/components/number-input/number-input.test.tsx +6 -2
  141. package/src/components/password/password.test.tsx +20 -6
  142. package/src/components/password/password.tsx +2 -2
  143. package/src/components/popover/popover.test.tsx +4 -4
  144. package/src/components/progress/progress.test.tsx +7 -8
  145. package/src/components/radio-group/radio-group.test.tsx +17 -11
  146. package/src/components/select/select.test.tsx +10 -10
  147. package/src/components/select/select.tsx +9 -1
  148. package/src/components/stepper/stepper.stories.tsx +11 -15
  149. package/src/components/stepper/stepper.test.tsx +6 -4
  150. package/src/components/switch/switch.test.tsx +3 -3
  151. package/src/components/table/table.test.tsx +9 -3
  152. package/src/components/tabs/tabs.test.tsx +6 -2
  153. package/src/components/tag/tag.test.tsx +1 -3
  154. package/src/components/textarea/textarea.test.tsx +4 -1
  155. package/src/components/timeline/timeline.test.tsx +10 -5
  156. package/src/components/toast/toast.test.tsx +11 -14
  157. package/src/components/tooltip/tooltip.test.tsx +1 -5
  158. package/src/components/tree/tree.test.tsx +3 -1
  159. package/src/hooks/index.ts +2 -2
  160. package/src/hooks/internal/index.ts +2 -0
  161. package/src/hooks/internal/is-apple-device.test.ts +41 -0
  162. package/src/hooks/internal/is-apple-device.ts +33 -0
  163. package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
  164. package/src/hooks/internal/use-latest-ref.ts +21 -0
  165. package/src/hooks/use-array/use-array.stories.tsx +435 -64
  166. package/src/hooks/use-array/use-array.test.tsx +398 -15
  167. package/src/hooks/use-array/use-array.ts +105 -66
  168. package/src/hooks/use-async/use-async.stories.tsx +255 -131
  169. package/src/hooks/use-async/use-async.test.ts +397 -0
  170. package/src/hooks/use-async/use-async.ts +117 -39
  171. package/src/hooks/use-boolean/index.ts +1 -0
  172. package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
  173. package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
  174. package/src/hooks/use-boolean/use-boolean.ts +50 -0
  175. package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
  176. package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
  177. package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
  178. package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
  179. package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
  180. package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
  181. package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
  182. package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
  183. package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
  184. package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
  185. package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
  186. package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
  187. package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
  188. package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
  189. package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
  190. package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
  191. package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
  192. package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
  193. package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
  194. package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
  195. package/src/hooks/use-hotkey/index.ts +9 -1
  196. package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
  197. package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
  198. package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
  199. package/src/hooks/use-hotkey/utils/index.ts +8 -3
  200. package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
  201. package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
  202. package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
  203. package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
  204. package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
  205. package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
  206. package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
  207. package/src/hooks/use-hover/use-hover.test.tsx +266 -26
  208. package/src/hooks/use-hover/use-hover.tsx +93 -28
  209. package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
  210. package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
  211. package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
  212. package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
  213. package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
  214. package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
  215. package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
  216. package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
  217. package/src/hooks/use-media-query/use-media-query.ts +39 -14
  218. package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
  219. package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
  220. package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
  221. package/src/hooks/use-mutation/use-mutation.ts +97 -0
  222. package/src/hooks/use-object/use-object.stories.tsx +310 -79
  223. package/src/hooks/use-object/use-object.test.tsx +235 -56
  224. package/src/hooks/use-object/use-object.ts +59 -0
  225. package/src/hooks/use-pagination/use-pagination.tsx +0 -1
  226. package/src/hooks/use-prevent-page-close/index.ts +1 -0
  227. package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
  228. package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
  229. package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
  230. package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
  231. package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
  232. package/src/hooks/use-selection/use-selection.ts +0 -1
  233. package/src/hooks/use-step/use-step.stories.tsx +178 -65
  234. package/src/hooks/use-step/use-step.test.ts +178 -53
  235. package/src/hooks/use-step/use-step.ts +57 -49
  236. package/src/utils/form.test.tsx +13 -8
  237. package/src/utils/form.tsx +10 -0
  238. package/src/utils/functions/getFormData.test.ts +1 -1
  239. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
  240. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
  241. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
  242. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
  243. package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
  244. package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
  245. package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
  246. package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
  247. package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
  248. package/dist/src/hooks/use-toggle/index.d.ts +0 -1
  249. package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
  250. package/src/hooks/use-async/use-async.test.tsx +0 -68
  251. package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
  252. package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
  253. package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
  254. package/src/hooks/use-mutation/use-mutation.tsx +0 -59
  255. package/src/hooks/use-object/use-object.tsx +0 -46
  256. package/src/hooks/use-prevent-close-window/index.ts +0 -1
  257. package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
  258. package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
  259. package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
  260. package/src/hooks/use-toggle/index.ts +0 -1
  261. package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
  262. package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
  263. package/src/hooks/use-toggle/use-toggle.ts +0 -14
  264. /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
  265. /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
@@ -1,97 +1,210 @@
1
- import { Meta } from "@storybook/react-vite";
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
2
  import { Button } from "../../components";
3
3
  import { cn } from "../../lib";
4
- import { useStep } from "../use-step";
4
+ import { useStep } from "./use-step";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Meta
8
+ // ---------------------------------------------------------------------------
5
9
 
6
10
  /**
7
- * Hook que recibe un array de elementos `T` y facita el manejo de vistas/formularios con múltiples pasos
11
+ * `useStep` manages a numeric step index with bounded navigation.
12
+ *
13
+ * It owns **position only** — consumers index their own data array with the
14
+ * returned `index`. Always starts at index 0. All action callbacks are
15
+ * referentially stable after mount.
16
+ *
17
+ * ```ts
18
+ * const [index, { next, back, goTo, reset, canGoNext, canGoBack, isFirstStep, isLastStep, progress }] =
19
+ * useStep(4)
20
+ * ```
8
21
  */
9
22
  const meta: Meta = {
10
- title: "hooks/useStep",
23
+ title: "Hooks/useStep",
11
24
  tags: ["autodocs"],
25
+ parameters: {
26
+ docs: {
27
+ description: {
28
+ component: `
29
+ \`useStep\` manages a numeric step index with bounded navigation.
30
+
31
+ It owns **position only** — consumers index their own data array with the returned \`index\`.
32
+ Always starts at index 0. All action callbacks (\`next\`, \`back\`, \`goTo\`, \`reset\`) are referentially stable after mount.
33
+
34
+ \`\`\`ts
35
+ const [index, { next, back, goTo, reset, canGoNext, canGoBack, isFirstStep, isLastStep, progress }] =
36
+ useStep(4)
37
+ \`\`\`
38
+ `.trim(),
39
+ },
40
+ },
41
+ },
42
+ argTypes: {
43
+ count: {
44
+ control: { type: "number", min: 1, step: 1 },
45
+ description: "Total number of steps. Source of truth for boundaries.",
46
+ table: { defaultValue: { summary: "—" } },
47
+ },
48
+ },
49
+ args: {
50
+ count: 4,
51
+ },
12
52
  };
13
53
 
14
54
  export default meta;
15
55
 
16
- export const Default = {
17
- render: () => {
18
- const data = [
19
- {
20
- id: 1,
21
- content: "🚌",
56
+ type Story = StoryObj<typeof meta>;
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Stories
60
+ // ---------------------------------------------------------------------------
61
+
62
+ // ── Default ───────────────────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Basic uncontrolled usage. `useStep` starts at index 0. Back and Next buttons
66
+ * navigate through the steps; the current position is shown as "Step X of N".
67
+ */
68
+ export const Default: Story = {
69
+ parameters: {
70
+ docs: {
71
+ description: {
72
+ story:
73
+ "Basic uncontrolled usage. The hook starts at index 0 and manages its own state. Click Back / Next to navigate.",
22
74
  },
23
- {
24
- id: 2,
25
- content: "🚗",
75
+ },
76
+ },
77
+ render: () => {
78
+ const [index, { next, back, isFirstStep, isLastStep }] = useStep(4);
79
+
80
+ return (
81
+ <div className="flex flex-col gap-4 w-72">
82
+ <div className="flex h-20 items-center justify-center rounded-lg border-2 border-dashed text-lg font-semibold">
83
+ Step {index + 1} of 4
84
+ </div>
85
+ <div className="flex gap-2">
86
+ <Button variant="outline" onClick={back} disabled={isFirstStep}>
87
+ Back
88
+ </Button>
89
+ <Button onClick={next} disabled={isLastStep}>
90
+ Next
91
+ </Button>
92
+ </div>
93
+ </div>
94
+ );
95
+ },
96
+ };
97
+
98
+ // ── WithProgress ──────────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Shows the `progress` ratio (0–1) as a visual progress bar.
102
+ * `progress` equals `index / (count - 1)` and is 0 when `count <= 1`.
103
+ */
104
+ export const WithProgress: Story = {
105
+ parameters: {
106
+ docs: {
107
+ description: {
108
+ story:
109
+ "`progress` is a ratio in `[0, 1]` equal to `index / (count - 1)`. Drive a progress bar width directly: `progress * 100 + '%'`.",
26
110
  },
27
- {
28
- id: 3,
29
- content: "🚕",
111
+ },
112
+ },
113
+ render: () => {
114
+ const [index, { next, back, progress, isFirstStep, isLastStep }] =
115
+ useStep(5);
116
+
117
+ return (
118
+ <div className="flex flex-col gap-4 w-72">
119
+ {/* Progress bar */}
120
+ <div className="w-full h-2 rounded-full bg-muted overflow-hidden">
121
+ <div
122
+ className="h-full rounded-full bg-primary transition-all duration-300"
123
+ style={{ width: `${progress * 100}%` }}
124
+ />
125
+ </div>
126
+ <p className="text-sm text-muted-foreground text-center">
127
+ Step {index + 1} of 5 — progress: {progress.toFixed(2)}
128
+ </p>
129
+ <div className="flex gap-2">
130
+ <Button variant="outline" onClick={back} disabled={isFirstStep}>
131
+ Back
132
+ </Button>
133
+ <Button onClick={next} disabled={isLastStep}>
134
+ Next
135
+ </Button>
136
+ </div>
137
+ </div>
138
+ );
139
+ },
140
+ };
141
+
142
+ // ── BoundaryBehavior ──────────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Demonstrates `canGoNext` and `canGoBack` driving button disabled state, plus
146
+ * dot indicators calling `goTo(i)` to jump to a specific step directly.
147
+ */
148
+ export const BoundaryBehavior: Story = {
149
+ parameters: {
150
+ docs: {
151
+ description: {
152
+ story:
153
+ "`canGoNext` and `canGoBack` are derived from `index` and `count`. Use them to disable navigation controls at the boundaries. Dot indicators call `goTo(i)` for direct jumps.",
30
154
  },
31
- ];
32
- const {
33
- steps,
34
- back,
35
- next,
36
- goTo,
37
- step,
38
- isLastStep,
39
- isFirstStep,
40
- currentStepIndex,
41
- } = useStep(data);
155
+ },
156
+ },
157
+ render: () => {
158
+ const count = 5;
159
+ const [index, { next, back, goTo, canGoNext, canGoBack }] = useStep(count);
42
160
 
43
161
  return (
44
- <div>
45
- <div className="flex gap-x-12">
46
- {data.map((transporte) => (
47
- <div
48
- key={transporte.id}
162
+ <div className="flex flex-col gap-4 w-72">
163
+ <div className="flex h-20 items-center justify-center rounded-lg border-2 border-dashed text-lg font-semibold">
164
+ Step {index + 1} of {count}
165
+ </div>
166
+
167
+ {/* Dot indicators */}
168
+ <div className="flex justify-center gap-2">
169
+ {Array.from({ length: count }, (_, i) => (
170
+ <button
171
+ key={i}
172
+ type="button"
173
+ onClick={() => goTo(i)}
49
174
  className={cn(
50
- "bg-popover grow rounded text-center shadow p-14 text-3xl",
51
- step.id === transporte.id && "bg-accent",
175
+ "h-3 w-3 rounded-full border-2 transition-colors",
176
+ i === index
177
+ ? "bg-primary border-primary"
178
+ : "bg-transparent border-muted-foreground hover:border-primary",
52
179
  )}
53
- >
54
- {transporte.content}
55
- </div>
180
+ aria-label={`Go to step ${i + 1}`}
181
+ />
56
182
  ))}
57
183
  </div>
58
184
 
59
- <div className="my-2 flex flex-wrap gap-2">
60
- <Button onClick={back} disabled={isFirstStep}>
185
+ <div className="flex gap-2">
186
+ <Button variant="outline" onClick={back} disabled={!canGoBack}>
61
187
  Back
62
188
  </Button>
63
- <Button onClick={next} disabled={isLastStep}>
189
+ <Button onClick={next} disabled={!canGoNext}>
64
190
  Next
65
191
  </Button>
66
- <Button onClick={() => goTo(0)}>Go to first step</Button>
67
- <Button onClick={() => goTo(1)}>Go to second step</Button>
68
- <Button onClick={() => goTo(2)}>Go to third step</Button>
69
192
  </div>
70
193
 
71
- <pre className="mt-2 rounded-md bg-slate-950 p-4 text-white">
72
- <code className="block">
73
- Steps: <span>{JSON.stringify(steps, null, 2)}</span>
74
- </code>
75
- <code className="block">Current step index: {currentStepIndex}</code>
76
- <code className="block">
77
- Current step:{" "}
78
- <span className="text-blue-300">
79
- {JSON.stringify(step, null, 2)}
80
- </span>
81
- </code>
82
- <code className="block">
83
- isFirstStep:{" "}
84
- <span className={isFirstStep ? "text-green-500" : "text-red-500"}>
85
- {JSON.stringify(isFirstStep)}
194
+ <div className="rounded-md bg-muted/40 px-3 py-2 text-xs font-mono space-y-1">
195
+ <p>
196
+ canGoBack:{" "}
197
+ <span className={canGoBack ? "text-green-600" : "text-red-500"}>
198
+ {String(canGoBack)}
86
199
  </span>
87
- </code>
88
- <code className="block">
89
- isLastStep:{" "}
90
- <span className={isLastStep ? "text-green-500" : "text-red-500"}>
91
- {JSON.stringify(isLastStep)}
200
+ </p>
201
+ <p>
202
+ canGoNext:{" "}
203
+ <span className={canGoNext ? "text-green-600" : "text-red-500"}>
204
+ {String(canGoNext)}
92
205
  </span>
93
- </code>
94
- </pre>
206
+ </p>
207
+ </div>
95
208
  </div>
96
209
  );
97
210
  },
@@ -1,73 +1,198 @@
1
1
  import { act, renderHook } from "@testing-library/react";
2
2
  import { describe, expect, it } from "vitest";
3
- import { useStep } from "../../hooks";
4
-
5
- describe("useStep hook", () => {
6
- const steps = ["step1", "step2", "step3"];
7
-
8
- it("starts at index 0 with isFirstStep=true", () => {
9
- const { result } = renderHook(() => useStep(steps));
10
- expect(result.current.currentStepIndex).toBe(0);
11
- expect(result.current.step).toBe("step1");
12
- expect(result.current.isFirstStep).toBe(true);
13
- expect(result.current.isLastStep).toBe(false);
14
- expect(result.current.steps).toBe(steps);
3
+ import { useStep } from "../use-step";
4
+
5
+ describe("useStep", () => {
6
+ // ── 1. Default start ──────────────────────────────────────────────────────
7
+ describe("default start", () => {
8
+ it("starts at index 0, isFirstStep true, isLastStep false", () => {
9
+ const { result } = renderHook(() => useStep(5));
10
+ const [index, { isFirstStep, isLastStep }] = result.current;
11
+ expect(index).toBe(0);
12
+ expect(isFirstStep).toBe(true);
13
+ expect(isLastStep).toBe(false);
14
+ });
15
15
  });
16
16
 
17
- it("next advances to next step", () => {
18
- const { result } = renderHook(() => useStep(steps));
19
- act(() => result.current.next());
20
- expect(result.current.currentStepIndex).toBe(1);
21
- expect(result.current.step).toBe("step2");
22
- expect(result.current.isFirstStep).toBe(false);
17
+ // ── 2. next() ─────────────────────────────────────────────────────────────
18
+ describe("next()", () => {
19
+ it("increments index when not at last step", async () => {
20
+ const { result } = renderHook(() => useStep(3));
21
+ await act(async () => {
22
+ result.current[1].next();
23
+ });
24
+ expect(result.current[0]).toBe(1);
25
+ });
26
+
27
+ it("is a no-op at the last step", async () => {
28
+ const { result } = renderHook(() => useStep(1));
29
+ await act(async () => {
30
+ result.current[1].next();
31
+ });
32
+ expect(result.current[0]).toBe(0);
33
+ });
23
34
  });
24
35
 
25
- it("back returns to previous step", () => {
26
- const { result } = renderHook(() => useStep(steps));
27
- act(() => result.current.next());
28
- act(() => result.current.back());
29
- expect(result.current.currentStepIndex).toBe(0);
30
- expect(result.current.isFirstStep).toBe(true);
36
+ // ── 3. back() ─────────────────────────────────────────────────────────────
37
+ describe("back()", () => {
38
+ it("decrements index when not at first step", async () => {
39
+ const { result } = renderHook(() => useStep(3));
40
+ await act(async () => {
41
+ result.current[1].goTo(1);
42
+ });
43
+ await act(async () => {
44
+ result.current[1].back();
45
+ });
46
+ expect(result.current[0]).toBe(0);
47
+ });
48
+
49
+ it("is a no-op at the first step", async () => {
50
+ const { result } = renderHook(() => useStep(3));
51
+ await act(async () => {
52
+ result.current[1].back();
53
+ });
54
+ expect(result.current[0]).toBe(0);
55
+ });
56
+ });
57
+
58
+ // ── 4. goTo() ─────────────────────────────────────────────────────────────
59
+ describe("goTo()", () => {
60
+ it("jumps to a valid index", async () => {
61
+ const { result } = renderHook(() => useStep(5));
62
+ await act(async () => {
63
+ result.current[1].goTo(3);
64
+ });
65
+ expect(result.current[0]).toBe(3);
66
+ });
67
+
68
+ it("clamps negative target to 0", async () => {
69
+ const { result } = renderHook(() => useStep(5));
70
+ await act(async () => {
71
+ result.current[1].goTo(-2);
72
+ });
73
+ expect(result.current[0]).toBe(0);
74
+ });
75
+
76
+ it("clamps over-bound target to count - 1", async () => {
77
+ const { result } = renderHook(() => useStep(5));
78
+ await act(async () => {
79
+ result.current[1].goTo(99);
80
+ });
81
+ expect(result.current[0]).toBe(4);
82
+ });
31
83
  });
32
84
 
33
- it("next does not advance past last step", () => {
34
- const { result } = renderHook(() => useStep(steps));
35
- act(() => result.current.goTo(2));
36
- expect(result.current.isLastStep).toBe(true);
37
- act(() => result.current.next());
38
- expect(result.current.currentStepIndex).toBe(2);
85
+ // ── 5. reset() ────────────────────────────────────────────────────────────
86
+ describe("reset()", () => {
87
+ it("returns to index 0", async () => {
88
+ const { result } = renderHook(() => useStep(5));
89
+ await act(async () => {
90
+ result.current[1].goTo(3);
91
+ });
92
+ await act(async () => {
93
+ result.current[1].reset();
94
+ });
95
+ expect(result.current[0]).toBe(0);
96
+ });
39
97
  });
40
98
 
41
- it("back does not go before first step", () => {
42
- const { result } = renderHook(() => useStep(steps));
43
- act(() => result.current.back());
44
- expect(result.current.currentStepIndex).toBe(0);
99
+ // ── 6. canGoNext / canGoBack ──────────────────────────────────────────────
100
+ describe("canGoNext / canGoBack", () => {
101
+ it("canGoNext is false and canGoBack is true at the last step", async () => {
102
+ const { result } = renderHook(() => useStep(3));
103
+ await act(async () => {
104
+ result.current[1].goTo(2);
105
+ });
106
+ const [, { canGoNext, canGoBack }] = result.current;
107
+ expect(canGoNext).toBe(false);
108
+ expect(canGoBack).toBe(true);
109
+ });
110
+
111
+ it("canGoBack is false and canGoNext is true at the first step", () => {
112
+ const { result } = renderHook(() => useStep(3));
113
+ const [, { canGoBack, canGoNext }] = result.current;
114
+ expect(canGoBack).toBe(false);
115
+ expect(canGoNext).toBe(true);
116
+ });
45
117
  });
46
118
 
47
- it("goTo jumps to specified index", () => {
48
- const { result } = renderHook(() => useStep(steps));
49
- act(() => result.current.goTo(2));
50
- expect(result.current.currentStepIndex).toBe(2);
51
- expect(result.current.isLastStep).toBe(true);
119
+ // ── 7. progress ───────────────────────────────────────────────────────────
120
+ describe("progress", () => {
121
+ it("is 0.5 at the mid-step of a 5-step sequence", async () => {
122
+ const { result } = renderHook(() => useStep(5));
123
+ await act(async () => {
124
+ result.current[1].goTo(2);
125
+ });
126
+ expect(result.current[1].progress).toBe(0.5);
127
+ });
128
+
129
+ it("is 0 at the first step", () => {
130
+ const { result } = renderHook(() => useStep(5));
131
+ expect(result.current[1].progress).toBe(0);
132
+ });
133
+
134
+ it("is 1 at the last step", async () => {
135
+ const { result } = renderHook(() => useStep(5));
136
+ await act(async () => {
137
+ result.current[1].goTo(4);
138
+ });
139
+ expect(result.current[1].progress).toBe(1);
140
+ });
141
+
142
+ it("is 0 when count is 1", () => {
143
+ const { result } = renderHook(() => useStep(1));
144
+ expect(result.current[1].progress).toBe(0);
145
+ });
52
146
  });
53
147
 
54
- it("findAndGo jumps to step matching predicate", () => {
55
- const { result } = renderHook(() => useStep(steps));
56
- act(() => result.current.findAndGo((s) => s === "step3"));
57
- expect(result.current.currentStepIndex).toBe(2);
148
+ // ── 8. isFirstStep / isLastStep ───────────────────────────────────────────
149
+ describe("isFirstStep / isLastStep", () => {
150
+ it("isLastStep is true and isFirstStep is false at the last step", async () => {
151
+ const { result } = renderHook(() => useStep(3));
152
+ await act(async () => {
153
+ result.current[1].goTo(2);
154
+ });
155
+ const [, { isLastStep, isFirstStep }] = result.current;
156
+ expect(isLastStep).toBe(true);
157
+ expect(isFirstStep).toBe(false);
158
+ });
159
+
160
+ it("isFirstStep and isLastStep are both true when count is 1", () => {
161
+ const { result } = renderHook(() => useStep(1));
162
+ const [, { isFirstStep, isLastStep }] = result.current;
163
+ expect(isFirstStep).toBe(true);
164
+ expect(isLastStep).toBe(true);
165
+ });
58
166
  });
59
167
 
60
- it("findAndGo does nothing when predicate matches nothing", () => {
61
- const { result } = renderHook(() => useStep(steps));
62
- act(() => result.current.findAndGo((s) => s === "nonexistent"));
63
- expect(result.current.currentStepIndex).toBe(0);
168
+ // ── 9. Action reference stability ─────────────────────────────────────────
169
+ describe("action reference stability", () => {
170
+ it("next, back, goTo, and reset are the same references across re-renders", async () => {
171
+ const { result, rerender } = renderHook(() => useStep(3));
172
+ const [, { next, back, goTo, reset }] = result.current;
173
+
174
+ rerender();
175
+
176
+ expect(result.current[1].next).toBe(next);
177
+ expect(result.current[1].back).toBe(back);
178
+ expect(result.current[1].goTo).toBe(goTo);
179
+ expect(result.current[1].reset).toBe(reset);
180
+ });
64
181
  });
65
182
 
66
- it("respects amountSteps override for boundary checks", () => {
67
- const { result } = renderHook(() => useStep(steps, 2));
68
- act(() => result.current.goTo(1));
69
- expect(result.current.isLastStep).toBe(true);
70
- act(() => result.current.next());
71
- expect(result.current.currentStepIndex).toBe(1);
183
+ // ── 10. Cleanup on unmount ────────────────────────────────────────────────
184
+ describe("cleanup on unmount", () => {
185
+ it("does not throw after unmount when calling actions", async () => {
186
+ const { result, unmount } = renderHook(() => useStep(3));
187
+ unmount();
188
+ await act(async () => {
189
+ try {
190
+ result.current[1].next();
191
+ } catch {
192
+ // suppress unmount-related errors
193
+ }
194
+ });
195
+ // no assertion needed — test passes if no error is thrown
196
+ });
72
197
  });
73
198
  });
@@ -1,55 +1,63 @@
1
- import { ReactNode, useState } from "react";
1
+ import { useCallback, useState } from "react";
2
2
 
3
- interface Return<T = ReactNode> {
4
- currentStepIndex: number;
5
- step: T;
6
- steps: T[];
7
- isFirstStep: boolean;
8
- isLastStep: boolean;
9
- goTo: (i: number) => void;
3
+ export interface UseStepActions {
4
+ /** Advance one step. No-op at the last step. Stable reference. */
10
5
  next: () => void;
6
+ /** Go back one step. No-op at the first step. Stable reference. */
11
7
  back: () => void;
12
- findAndGo: (predicate: (item: T) => boolean) => void;
8
+ /** Jump to an index, clamped to [0, count - 1]. Stable reference. */
9
+ goTo: (index: number) => void;
10
+ /** Return to index 0. Stable reference. */
11
+ reset: () => void;
12
+ /** True when there is a next step to advance to. */
13
+ canGoNext: boolean;
14
+ /** True when there is a previous step to go back to. */
15
+ canGoBack: boolean;
16
+ /** True when the current index is the first step. */
17
+ isFirstStep: boolean;
18
+ /** True when the current index is the last step. */
19
+ isLastStep: boolean;
20
+ /** Ratio of progress in [0, 1]. 0 when count <= 1. */
21
+ progress: number;
13
22
  }
14
23
 
15
- export function useStep<T = ReactNode>(steps: T[], amountSteps?: number): Return<T> {
16
- const [currentStepIndex, setCurrentStepIndex] = useState(0);
17
- const length = amountSteps ?? steps.length;
18
-
19
- const next = (): void => {
20
- setCurrentStepIndex((i: number) => {
21
- if (i >= length - 1) return i;
22
- return i + 1;
23
- });
24
- };
25
-
26
- const back = (): void => {
27
- setCurrentStepIndex((i: number) => {
28
- if (i <= 0) return i;
29
- return i - 1;
30
- });
31
- };
32
-
33
- const goTo = (i: number): void => {
34
- setCurrentStepIndex(i);
35
- };
36
-
37
- const findAndGo = (predicate: (item: T) => boolean): void => {
38
- const stepIndex = steps.findIndex(predicate);
39
- if (stepIndex === -1) return;
40
- goTo(stepIndex);
41
- };
42
-
43
- return {
44
- currentStepIndex,
45
- step: steps[currentStepIndex],
46
- steps,
47
- isFirstStep: currentStepIndex === 0,
48
- isLastStep: currentStepIndex === length - 1,
49
- goTo,
50
- next,
51
- back,
52
- findAndGo,
53
- };
24
+ const clamp = (value: number, min: number, max: number) =>
25
+ Math.min(Math.max(value, min), max);
26
+
27
+ export function useStep(count: number): [number, UseStepActions] {
28
+ const [index, setIndex] = useState(0);
29
+ const lastIndex = Math.max(count - 1, 0);
30
+ const currentIndex = clamp(index, 0, lastIndex);
31
+
32
+ const next = useCallback(
33
+ () => setIndex((i) => Math.min(i + 1, lastIndex)),
34
+ [lastIndex],
35
+ );
36
+
37
+ const back = useCallback(() => setIndex((i) => Math.max(i - 1, 0)), []);
38
+
39
+ const goTo = useCallback(
40
+ (target: number) => setIndex(clamp(target, 0, lastIndex)),
41
+ [lastIndex],
42
+ );
43
+
44
+ const reset = useCallback(() => setIndex(0), []);
45
+
46
+ const isFirstStep = currentIndex <= 0;
47
+ const isLastStep = currentIndex >= lastIndex;
48
+
49
+ return [
50
+ currentIndex,
51
+ {
52
+ next,
53
+ back,
54
+ goTo,
55
+ reset,
56
+ canGoNext: !isLastStep,
57
+ canGoBack: !isFirstStep,
58
+ isFirstStep,
59
+ isLastStep,
60
+ progress: count <= 1 ? 0 : currentIndex / (count - 1),
61
+ },
62
+ ];
54
63
  }
55
-