@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,100 +1,471 @@
1
- import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { Trash } from "lucide-react";
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
3
  import { Button } from "../../components";
4
- import { useArray } from "../use-array";
5
-
6
- /**
7
- * Hook que facilita el manejo de estado de una lista
8
- * Se exponen los siguientes métodos:
9
- *
10
- * | Método | Descripción |
11
- * | --- | --- |
12
- * | `insertAt(index: number, item: T)` | Inserta un elemento en un índice específico en la lista. |
13
- * | `removeAt(index: number)` | Elimina un elemento en un índice específico de la lista. |
14
- * | `updateAt(index: number, newItem: T)` | Actualiza un elemento en un índice específico de la lista. |
15
- * | `clear()` | Elimina todos los elementos de la lista. |
16
- * | `reset()` | Restablece la lista a su valor por defecto o inicial. |
17
- * | `push(item: T)` | Agrega un nuevo elemento al final de la lista. |
18
- * | `pop()` | Elimina el último elemento de la lista. |
19
- * | `set(newList: T[])` | Actualiza la lista con una nueva lista. |
20
- * | `update(predicate: ListIterateeCustom<T, boolean>, newItem: T | (item: T) => T)` | Actualiza un elemento de la lista.
21
- */
4
+ import { useArray } from "./use-array";
5
+
6
+ // ─── Types ─────────────────────────────────────────────────────────────────────
7
+
8
+ interface ActiveItem {
9
+ id: number;
10
+ name: string;
11
+ active: boolean;
12
+ }
13
+
14
+ // ─── Shared UI helpers ─────────────────────────────────────────────────────────
15
+
16
+ const JsonPanel = ({ value }: { value: unknown }) => (
17
+ <pre className="rounded-md bg-slate-950 p-3 text-xs text-white leading-relaxed overflow-auto max-h-48">
18
+ {JSON.stringify(value, null, 2)}
19
+ </pre>
20
+ );
21
+
22
+ const LogPanel = ({ entries }: { entries: string[] }) =>
23
+ entries.length > 0 ? (
24
+ <div className="rounded-md border bg-muted p-3 text-xs font-mono space-y-0.5">
25
+ {entries.map((entry, i) => (
26
+ <div key={i}>{entry}</div>
27
+ ))}
28
+ </div>
29
+ ) : (
30
+ <div className="rounded-md border bg-muted p-3 text-xs font-mono text-muted-foreground">
31
+ No events yet — log is empty until the first mutation (proves onChange
32
+ does not fire on mount).
33
+ </div>
34
+ );
35
+
36
+ const timestamp = () => new Date().toISOString().slice(11, 23);
37
+
38
+ // ─── Shared data ───────────────────────────────────────────────────────────────
39
+
40
+ const INITIAL_ITEMS: ActiveItem[] = [
41
+ { id: 1, name: "Alice", active: true },
42
+ { id: 2, name: "Bob", active: false },
43
+ { id: 3, name: "Charlie", active: true },
44
+ { id: 4, name: "Diana", active: false },
45
+ ];
46
+
47
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
48
+
22
49
  const meta: Meta = {
23
50
  title: "hooks/useArray",
51
+ parameters: {
52
+ docs: {
53
+ description: {
54
+ component: `
55
+ \`useArray\` manages array state with a stable set of immutable actions.
56
+
57
+ **Key behaviors**
58
+ - All 17 action references are **stable across renders** — built once in \`useMemo([setList])\`. Safe to pass as props or include in dependency arrays without causing extra renders.
59
+ - Every mutating action returns a **new array reference** — React change detection always works correctly.
60
+ - \`sort\` and \`reverse\` spread the previous state before calling the native method, preventing in-place mutation.
61
+ - \`replace\` returns the previous ref unchanged when the predicate matches nothing — no phantom re-render, no spurious \`onChange\`.
62
+ - \`reset()\` restores the **mount-time value** regardless of prop changes after mount (frozen ref pattern).
63
+ - \`onChange\` fires after every state change but **not on initial mount**.
64
+
65
+ \`\`\`ts
66
+ const [list, actions] = useArray(initialValue, { onChange? });
67
+ \`\`\`
68
+ `.trim(),
69
+ },
70
+ },
71
+ },
72
+ argTypes: {
73
+ initialValue: {
74
+ description:
75
+ "The initial array value. Captured once on mount — `reset()` always restores this value regardless of later prop changes.",
76
+ table: {
77
+ category: "Hook options",
78
+ type: { summary: "T[]" },
79
+ },
80
+ },
81
+ },
24
82
  };
25
83
 
26
84
  export default meta;
85
+ type Story = StoryObj;
27
86
 
28
- type Story = StoryObj<typeof meta>;
87
+ // ─── Story 1 Default (Core CRUD) ────────────────────────────────────────────
29
88
 
30
89
  export const Default: Story = {
31
90
  render: () => {
32
- const [list, { insertAt, removeAt, clear, reset, pop, update }] =
33
- useArray<string>(["First", "Second", "Third"]);
91
+ const INITIAL = ["First", "Second", "Third"];
92
+ const [inputValue, setInputValue] = useState("");
93
+ const [list, actions] = useArray<string>(INITIAL);
94
+
95
+ const handlePush = () => {
96
+ const val = inputValue.trim();
97
+ if (!val) return;
98
+ actions.push(val);
99
+ setInputValue("");
100
+ };
34
101
 
35
102
  return (
36
- <div className="space-y-2">
103
+ <div className="flex flex-col gap-4 max-w-sm">
104
+ <div className="flex gap-2">
105
+ <input
106
+ className="flex-1 rounded-md border px-3 py-2 text-sm"
107
+ placeholder="Type an item..."
108
+ value={inputValue}
109
+ onChange={(e) => setInputValue(e.target.value)}
110
+ onKeyDown={(e) => e.key === "Enter" && handlePush()}
111
+ />
112
+ <Button variant="outline" onClick={handlePush}>
113
+ Push
114
+ </Button>
115
+ </div>
116
+
37
117
  <div className="flex gap-2 flex-wrap">
38
- <Button disabled={list.length < 1} onClick={() => insertAt(1, "woo")}>
39
- Insertar despúes del primero
118
+ <Button
119
+ variant="outline"
120
+ onClick={() => actions.insertAt(0, "Prepended")}
121
+ >
122
+ Prepend
40
123
  </Button>
41
- <Button disabled={list.length < 2} onClick={() => removeAt(1)}>
42
- Eliminar segundo item
124
+ <Button
125
+ variant="outline"
126
+ disabled={list.length === 0}
127
+ onClick={() => actions.pop()}
128
+ >
129
+ Pop
43
130
  </Button>
44
- <Button disabled={list.length < 2} onClick={() => pop()}>
45
- Eliminar el último item
131
+ <Button variant="outline" onClick={() => actions.reset()}>
132
+ Reset
133
+ </Button>
134
+ <Button variant="outline" onClick={() => actions.clear()}>
135
+ Clear
136
+ </Button>
137
+ </div>
138
+
139
+ {list.length > 0 && (
140
+ <ul className="space-y-1">
141
+ {list.map((item, index) => (
142
+ <li
143
+ key={index}
144
+ className="flex items-center justify-between rounded-md border px-3 py-1.5 text-sm"
145
+ >
146
+ <span>{item}</span>
147
+ <Button
148
+ variant="ghost"
149
+ size="sm"
150
+ onClick={() => actions.removeAt(index)}
151
+ >
152
+ Remove
153
+ </Button>
154
+ </li>
155
+ ))}
156
+ </ul>
157
+ )}
158
+
159
+ <JsonPanel value={list} />
160
+ </div>
161
+ );
162
+ },
163
+ parameters: {
164
+ docs: {
165
+ description: {
166
+ story:
167
+ "Core CRUD: push, pop, prepend via `insertAt(0)`, removeAt per row, reset (restores mount-time value), and clear. Type in the input and press Enter or click **Push** to add items.",
168
+ },
169
+ },
170
+ },
171
+ };
172
+
173
+ // ─── Story 2 — SearchAndTransform ─────────────────────────────────────────────
174
+
175
+ export const SearchAndTransform: Story = {
176
+ render: () => {
177
+ const [list, actions] = useArray<ActiveItem>(INITIAL_ITEMS);
178
+ const [findResult, setFindResult] = useState<ActiveItem | undefined>(
179
+ undefined,
180
+ );
181
+ const [searched, setSearched] = useState(false);
182
+
183
+ return (
184
+ <div className="flex flex-col gap-4 max-w-sm">
185
+ <div className="flex gap-2 flex-wrap">
186
+ <Button
187
+ variant="outline"
188
+ onClick={() => actions.filter((item) => item.active)}
189
+ >
190
+ Filter active only
191
+ </Button>
192
+ <Button
193
+ variant="outline"
194
+ onClick={() => {
195
+ const found = actions.find((item) => !item.active);
196
+ setFindResult(found);
197
+ setSearched(true);
198
+ }}
199
+ >
200
+ Find first inactive
201
+ </Button>
202
+ <Button
203
+ variant="outline"
204
+ onClick={() =>
205
+ actions.replace(
206
+ (item) => item.name.toLowerCase().includes("a"),
207
+ (item) => ({ ...item, active: true }),
208
+ )
209
+ }
210
+ >
211
+ Activate first with "a"
46
212
  </Button>
47
213
  <Button
48
- onClick={() => update((item: string) => item === "First", "🐙")}
214
+ variant="outline"
215
+ onClick={() => actions.removeWhere((item) => !item.active)}
49
216
  >
50
- Actualizar primer elemento por 🐙
217
+ Remove inactive
218
+ </Button>
219
+ <Button variant="outline" onClick={() => actions.set(INITIAL_ITEMS)}>
220
+ Reset to initial
51
221
  </Button>
52
- <Button onClick={reset}>Resetear</Button>
53
- <Button onClick={clear}>Limpiar</Button>
54
222
  </div>
55
223
 
56
- <pre className="rounded-md bg-slate-950 p-4 text-white">
57
- <code className="block">
58
- List:{" "}
59
- <span className="text-blue-300">
60
- {JSON.stringify(list, null, 2)}
61
- </span>
62
- </code>
63
- </pre>
224
+ {searched && (
225
+ <div className="rounded-md border bg-muted p-3 text-xs font-mono">
226
+ <span className="text-muted-foreground">find result: </span>
227
+ {findResult ? JSON.stringify(findResult) : "undefined"}
228
+ </div>
229
+ )}
230
+
231
+ <JsonPanel value={list} />
64
232
  </div>
65
233
  );
66
234
  },
235
+ parameters: {
236
+ docs: {
237
+ description: {
238
+ story:
239
+ "Predicate-based operations: `filter` retains matching items, `find` returns the first match without mutating state, `replace` transforms the first match (updater fn variant), and `removeWhere` removes all matching items.",
240
+ },
241
+ },
242
+ },
67
243
  };
68
244
 
69
- export const DynamicList: Story = {
70
- render: () => {
71
- const [list, updateList] = useArray<string>([]);
245
+ // ─── Story 3 — Reordering ─────────────────────────────────────────────────────
72
246
 
73
- const addItem = () => updateList.push(`Item ${list.length + 1}`);
247
+ export const Reordering: Story = {
248
+ render: () => {
249
+ const [list, actions] = useArray(["Apple", "Banana", "Cherry", "Date"]);
250
+ const [fromIdx, setFromIdx] = useState(0);
251
+ const [toIdx, setToIdx] = useState(2);
252
+ const [swapA, setSwapA] = useState(0);
253
+ const [swapB, setSwapB] = useState(3);
74
254
 
75
255
  return (
76
- <div className="space-y-2">
256
+ <div className="flex flex-col gap-4 max-w-sm">
77
257
  <div className="flex items-center gap-2">
78
- <Button onClick={addItem}>Add Item</Button>
79
- <Button onClick={updateList.clear}>
80
- <Trash /> Clear
258
+ <label className="text-sm shrink-0">Move from</label>
259
+ <input
260
+ type="number"
261
+ min={0}
262
+ max={list.length - 1}
263
+ value={fromIdx}
264
+ onChange={(e) => setFromIdx(Number(e.target.value))}
265
+ className="w-16 rounded-md border px-2 py-1.5 text-sm"
266
+ />
267
+ <label className="text-sm shrink-0">to</label>
268
+ <input
269
+ type="number"
270
+ min={0}
271
+ max={list.length - 1}
272
+ value={toIdx}
273
+ onChange={(e) => setToIdx(Number(e.target.value))}
274
+ className="w-16 rounded-md border px-2 py-1.5 text-sm"
275
+ />
276
+ <Button
277
+ variant="outline"
278
+ onClick={() => actions.move(fromIdx, toIdx)}
279
+ >
280
+ Move
81
281
  </Button>
82
282
  </div>
83
- <ul>
84
- {list.map((item, index) => (
85
- <li key={index}>
86
- {item}{" "}
87
- <Button
88
- size="icon"
89
- variant="ghost"
90
- onClick={() => updateList.removeAt(index)}
91
- >
92
- <Trash />
93
- </Button>
94
- </li>
95
- ))}
96
- </ul>
283
+
284
+ <div className="flex items-center gap-2">
285
+ <label className="text-sm shrink-0">Swap</label>
286
+ <input
287
+ type="number"
288
+ min={0}
289
+ max={list.length - 1}
290
+ value={swapA}
291
+ onChange={(e) => setSwapA(Number(e.target.value))}
292
+ className="w-16 rounded-md border px-2 py-1.5 text-sm"
293
+ />
294
+ <label className="text-sm shrink-0">and</label>
295
+ <input
296
+ type="number"
297
+ min={0}
298
+ max={list.length - 1}
299
+ value={swapB}
300
+ onChange={(e) => setSwapB(Number(e.target.value))}
301
+ className="w-16 rounded-md border px-2 py-1.5 text-sm"
302
+ />
303
+ <Button variant="outline" onClick={() => actions.swap(swapA, swapB)}>
304
+ Swap
305
+ </Button>
306
+ </div>
307
+
308
+ <div className="flex gap-2 flex-wrap">
309
+ <Button
310
+ variant="outline"
311
+ onClick={() => actions.sort((a, b) => a.localeCompare(b))}
312
+ >
313
+ Sort A→Z
314
+ </Button>
315
+ <Button
316
+ variant="outline"
317
+ onClick={() => actions.sort((a, b) => b.localeCompare(a))}
318
+ >
319
+ Sort Z→A
320
+ </Button>
321
+ <Button variant="outline" onClick={() => actions.reverse()}>
322
+ Reverse
323
+ </Button>
324
+ <Button
325
+ variant="outline"
326
+ onClick={() => actions.set(["Apple", "Banana", "Cherry", "Date"])}
327
+ >
328
+ Reset
329
+ </Button>
330
+ </div>
331
+
332
+ <JsonPanel value={list} />
333
+ </div>
334
+ );
335
+ },
336
+ parameters: {
337
+ docs: {
338
+ description: {
339
+ story:
340
+ "Reorder actions: `move(from, to)` repositions an element, `swap(a, b)` exchanges two, `sort` with a compareFn sorts without mutating prior state, and `reverse` reverses into a new array. Same-index `move` or `swap` returns the previous ref unchanged.",
341
+ },
342
+ },
343
+ },
344
+ };
345
+
346
+ // ─── Story 4 — BulkOperations ─────────────────────────────────────────────────
347
+
348
+ export const BulkOperations: Story = {
349
+ render: () => {
350
+ const [list, actions] = useArray<string>([]);
351
+ const [csvInput, setCsvInput] = useState("foo, bar, baz");
352
+
353
+ const handlePushMany = () => {
354
+ const items = csvInput
355
+ .split(",")
356
+ .map((s) => s.trim())
357
+ .filter(Boolean);
358
+ actions.pushMany(items);
359
+ };
360
+
361
+ return (
362
+ <div className="flex flex-col gap-4 max-w-sm">
363
+ <div className="flex gap-2">
364
+ <input
365
+ className="flex-1 rounded-md border px-3 py-2 text-sm"
366
+ placeholder="comma-separated values"
367
+ value={csvInput}
368
+ onChange={(e) => setCsvInput(e.target.value)}
369
+ />
370
+ <Button variant="outline" onClick={handlePushMany}>
371
+ Push Many
372
+ </Button>
373
+ </div>
374
+
375
+ <div className="flex gap-2 flex-wrap">
376
+ <Button
377
+ variant="outline"
378
+ onClick={() => actions.set(["🍎", "🍌", "🍒", "🍇"])}
379
+ >
380
+ Set fruits
381
+ </Button>
382
+ <Button
383
+ variant="outline"
384
+ onClick={() => actions.set(["red", "green", "blue"])}
385
+ >
386
+ Set colors
387
+ </Button>
388
+ <Button
389
+ variant="outline"
390
+ onClick={() => actions.set([1, 2, 3, 4, 5] as unknown as string[])}
391
+ >
392
+ Set numbers
393
+ </Button>
394
+ <Button variant="outline" onClick={() => actions.clear()}>
395
+ Clear
396
+ </Button>
397
+ </div>
398
+
399
+ <JsonPanel value={list} />
97
400
  </div>
98
401
  );
99
402
  },
403
+ parameters: {
404
+ docs: {
405
+ description: {
406
+ story:
407
+ "`pushMany` appends all items in one state update — more efficient than multiple `push` calls. `set` replaces the entire array at once. Calling `pushMany([])` is a no-op (no state change, no `onChange` fire).",
408
+ },
409
+ },
410
+ },
411
+ };
412
+
413
+ // ─── Story 5 — WithOnChange ────────────────────────────────────────────────────
414
+
415
+ export const WithOnChange: Story = {
416
+ render: () => {
417
+ const [log, setLog] = useState<string[]>([]);
418
+
419
+ const addLog = (action: string, list: string[]) =>
420
+ setLog((prev) => [
421
+ `[${timestamp()}] ${action} → ${JSON.stringify(list)}`,
422
+ ...prev,
423
+ ]);
424
+
425
+ const [list, actions] = useArray<string>(["a", "b", "c"], {
426
+ onChange: (updated) => addLog("onChange", updated),
427
+ });
428
+
429
+ return (
430
+ <div className="flex flex-col gap-4 max-w-sm">
431
+ <p className="text-xs text-muted-foreground">
432
+ The log panel below starts empty — proving <code>onChange</code> does
433
+ not fire on initial mount. Every button triggers a real mutation.
434
+ </p>
435
+
436
+ <div className="flex gap-2 flex-wrap">
437
+ <Button variant="outline" onClick={() => actions.push("x")}>
438
+ Push "x"
439
+ </Button>
440
+ <Button
441
+ variant="outline"
442
+ disabled={list.length === 0}
443
+ onClick={() => actions.pop()}
444
+ >
445
+ Pop
446
+ </Button>
447
+ <Button variant="outline" onClick={() => actions.clear()}>
448
+ Clear
449
+ </Button>
450
+ <Button variant="outline" onClick={() => actions.reset()}>
451
+ Reset
452
+ </Button>
453
+ <Button variant="outline" onClick={() => setLog([])}>
454
+ Clear log
455
+ </Button>
456
+ </div>
457
+
458
+ <JsonPanel value={list} />
459
+ <LogPanel entries={log} />
460
+ </div>
461
+ );
462
+ },
463
+ parameters: {
464
+ docs: {
465
+ description: {
466
+ story:
467
+ "Wires `onChange` to a visible log panel. The log is empty on mount (no initial fire). Each mutation appends a timestamped entry with the updated list. Passing a new inline `onChange` reference between renders does not cause double-fires because the callback is read through `useLatestRef`.",
468
+ },
469
+ },
470
+ },
100
471
  };