@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,119 +1,350 @@
1
- import { Meta } from "@storybook/react-vite";
2
- import { ChevronLeft, ChevronRight } from "lucide-react";
3
- import { Button, Input } from "../../components";
4
- import { useObject } from "../use-object";
5
-
6
- /**
7
- * Hook para manejar un estado en forma de objeto
8
- *
9
- * `@param initialValue` - Valor inicial del estado, se recomienda pasar para evitar el caso donde el estado `T` pueda ser `T | undefined`
10
- *
11
- * `@returns` [state, setState, reset]
12
- */
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+ import { Button } from "../../components";
4
+ import { useObject } from "./use-object";
5
+
6
+ // ─── Shared UI helpers ─────────────────────────────────────────────────────────
7
+
8
+ const JsonPanel = ({ value }: { value: unknown }) => (
9
+ <pre className="rounded-md bg-slate-950 p-3 text-xs text-white leading-relaxed overflow-auto max-h-48">
10
+ {JSON.stringify(value, null, 2)}
11
+ </pre>
12
+ );
13
+
14
+ const LogPanel = ({ entries }: { entries: string[] }) =>
15
+ entries.length > 0 ? (
16
+ <div className="rounded-md border bg-muted p-3 text-xs font-mono space-y-0.5">
17
+ {entries.map((entry, i) => (
18
+ <div key={i}>{entry}</div>
19
+ ))}
20
+ </div>
21
+ ) : (
22
+ <div className="rounded-md border bg-muted p-3 text-xs font-mono text-muted-foreground">
23
+ No events yet — log is empty until the first mutation (proves onChange
24
+ does not fire on mount).
25
+ </div>
26
+ );
27
+
28
+ const timestamp = () => new Date().toISOString().slice(11, 23);
29
+
30
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
31
+
13
32
  const meta: Meta = {
14
33
  title: "hooks/useObject",
34
+ parameters: {
35
+ docs: {
36
+ description: {
37
+ component: `
38
+ \`useObject\` manages plain-object state with a stable set of typed actions.
39
+
40
+ **Key behaviors**
41
+ - All 4 action references (\`set\`, \`replace\`, \`reset\`, \`setKey\`) are **stable across renders** — built once in \`useMemo([setState])\`. Safe to pass as props or include in dependency arrays.
42
+ - \`set\` **shallow-merges** a partial (or functional updater) into current state — unmentioned keys are preserved.
43
+ - \`replace\` **swaps the entire object** — unmentioned keys are discarded. Use it when you need a clean slate.
44
+ - \`reset()\` restores the **mount-time value** regardless of prop changes after mount (frozen ref pattern).
45
+ - \`onChange\` fires after every state change but **not on initial mount**. Passing a new inline callback reference between renders does not cause double-fires (\`useLatestRef\` pattern).
46
+ - \`setKey(key, value)\` is fully type-safe: \`key\` is constrained to \`keyof T\` and \`value\` must be \`T[K]\`.
47
+
48
+ \`\`\`ts
49
+ const [state, { set, replace, reset, setKey }] = useObject(initialValue, { onChange? });
50
+ \`\`\`
51
+ `.trim(),
52
+ },
53
+ },
54
+ },
55
+ argTypes: {
56
+ initialValue: {
57
+ description:
58
+ "The initial object value. Captured once on mount — `reset()` always restores this value regardless of later prop changes.",
59
+ table: {
60
+ category: "Hook options",
61
+ type: { summary: "T extends object" },
62
+ },
63
+ },
64
+ onChange: {
65
+ description:
66
+ "Called after every state change. NOT called on initial mount. Callback is read through `useLatestRef` — safe to pass as an inline function.",
67
+ table: {
68
+ category: "Hook options",
69
+ type: { summary: "(state: T) => void" },
70
+ },
71
+ },
72
+ },
15
73
  };
16
74
 
17
75
  export default meta;
76
+ type Story = StoryObj;
18
77
 
19
- interface Form {
20
- name: string;
21
- email: string;
22
- }
23
-
24
- function Results({ state }: { state?: Record<string, any> }) {
25
- if (!state) return;
26
- return (
27
- <pre className="rounded-md bg-slate-950 p-4 text-white">
28
- {Object.keys(state).map((key, i) => (
29
- <code className="block flex gap-2" key={i}>
30
- <span>{key}:</span>
31
- <span className="text-blue-300">
32
- {JSON.stringify(state[key as string], null, 2)}
33
- </span>
34
- </code>
35
- ))}
36
- </pre>
37
- );
38
- }
78
+ // ─── Story 1 — Default (User Profile Form) ────────────────────────────────────
39
79
 
40
- export const Default = {
80
+ export const Default: Story = {
41
81
  render: () => {
42
- const [state, setState, reset] = useObject<Form>({ name: "", email: "" });
82
+ const INITIAL = { name: "", email: "", role: "viewer" };
83
+ const [state, actions] = useObject(INITIAL);
43
84
 
44
85
  return (
45
- <div className="grid gap-2">
46
- <Input
47
- name="name"
48
- autoComplete="0"
49
- value={state?.name}
50
- onChange={(name) =>
51
- setState({
52
- name,
53
- })
54
- }
55
- />
56
- <Input
57
- name="email"
58
- autoComplete="0"
59
- value={state?.email}
60
- onChange={(email) =>
61
- setState({
62
- email,
63
- })
64
- }
65
- />
66
-
67
- <Button type="button" onClick={reset}>
86
+ <div className="flex flex-col gap-4 max-w-sm">
87
+ <div className="flex flex-col gap-1">
88
+ <label className="text-sm font-medium">Name</label>
89
+ <input
90
+ className="rounded-md border px-3 py-2 text-sm"
91
+ value={state.name}
92
+ onChange={(e) => actions.setKey("name", e.target.value)}
93
+ placeholder="Full name"
94
+ />
95
+ </div>
96
+
97
+ <div className="flex flex-col gap-1">
98
+ <label className="text-sm font-medium">Email</label>
99
+ <input
100
+ className="rounded-md border px-3 py-2 text-sm"
101
+ value={state.email}
102
+ onChange={(e) => actions.setKey("email", e.target.value)}
103
+ placeholder="you@example.com"
104
+ />
105
+ </div>
106
+
107
+ <div className="flex flex-col gap-1">
108
+ <label className="text-sm font-medium">Role</label>
109
+ <input
110
+ className="rounded-md border px-3 py-2 text-sm"
111
+ value={state.role}
112
+ onChange={(e) => actions.setKey("role", e.target.value)}
113
+ placeholder="viewer / editor / admin"
114
+ />
115
+ </div>
116
+
117
+ <Button variant="outline" onClick={() => actions.reset()}>
68
118
  Reset
69
119
  </Button>
70
120
 
71
- <Results state={state} />
121
+ <JsonPanel value={state} />
72
122
  </div>
73
123
  );
74
124
  },
125
+ parameters: {
126
+ docs: {
127
+ description: {
128
+ story:
129
+ "User profile form wired via `setKey(key, value)` for type-safe single-key updates. The Reset button restores the mount-time value — change any field then click Reset to see it.",
130
+ },
131
+ },
132
+ },
75
133
  };
76
134
 
77
- interface Counter {
78
- count: number;
79
- }
135
+ // ─── Story 2 — FunctionalUpdaters ─────────────────────────────────────────────
80
136
 
81
- export const Counter = {
137
+ export const FunctionalUpdaters: Story = {
82
138
  render: () => {
83
- const [state, setState, reset] = useObject<Counter>({ count: 0 });
139
+ const INITIAL = { score: 0, multiplier: 2, label: "light" };
140
+ const [state, actions] = useObject(INITIAL);
84
141
 
85
142
  return (
86
- <div className="space-y-2">
87
- <div className="flex items-center gap-x-2">
143
+ <div className="flex flex-col gap-4 max-w-sm">
144
+ <div className="flex items-center gap-2">
145
+ <label className="text-sm shrink-0">Multiplier</label>
146
+ <input
147
+ type="number"
148
+ min={1}
149
+ max={10}
150
+ className="w-20 rounded-md border px-2 py-1.5 text-sm"
151
+ value={state.multiplier}
152
+ onChange={(e) =>
153
+ actions.setKey("multiplier", Number(e.target.value))
154
+ }
155
+ />
156
+ </div>
157
+
158
+ <div className="flex gap-2 flex-wrap">
88
159
  <Button
89
- size="icon"
90
- variant="ghost"
160
+ variant="outline"
91
161
  onClick={() =>
92
- setState((state) => ({
93
- count: state.count - 1,
94
- }))
162
+ actions.set((s) => ({ score: s.score + s.multiplier }))
95
163
  }
96
164
  >
97
- <ChevronLeft />
165
+ Boost score (set)
98
166
  </Button>
99
- {state?.count}
100
167
  <Button
101
- size="icon"
102
- variant="ghost"
168
+ variant="outline"
103
169
  onClick={() =>
104
- setState((state) => ({
105
- count: state.count + 1,
170
+ actions.replace((s) => ({
171
+ score: 0,
172
+ multiplier: 1,
173
+ label: s.label === "light" ? "dark" : "light",
106
174
  }))
107
175
  }
108
176
  >
109
- <ChevronRight />
177
+ Toggle theme (replace)
178
+ </Button>
179
+ <Button variant="outline" onClick={() => actions.reset()}>
180
+ Reset
181
+ </Button>
182
+ </div>
183
+
184
+ <p className="text-xs text-muted-foreground">
185
+ <strong>Boost score</strong> uses <code>set(fn)</code> — merges only{" "}
186
+ <code>score</code>, preserving <code>multiplier</code> and{" "}
187
+ <code>label</code>.<br />
188
+ <strong>Toggle theme</strong> uses <code>replace(fn)</code> — must
189
+ rebuild every key; anything omitted would be lost.
190
+ </p>
191
+
192
+ <JsonPanel value={state} />
193
+ </div>
194
+ );
195
+ },
196
+ parameters: {
197
+ docs: {
198
+ description: {
199
+ story:
200
+ "Contrasts `set(fn)` (shallow merge — untouched keys survive) vs `replace(fn)` (full swap — you must supply all keys). Boost score increments by the current multiplier while keeping other fields intact. Toggle theme rebuilds the whole object.",
201
+ },
202
+ },
203
+ },
204
+ };
205
+
206
+ // ─── Story 3 — ReplaceVsSet ───────────────────────────────────────────────────
207
+
208
+ export const ReplaceVsSet: Story = {
209
+ render: () => {
210
+ const INITIAL = { a: "alpha", b: "beta", c: "gamma" };
211
+ const [setState, setActions] = useObject({ ...INITIAL });
212
+ const [replaceState, replaceActions] = useObject({ ...INITIAL });
213
+
214
+ return (
215
+ <div className="flex gap-6 flex-wrap">
216
+ <div className="flex flex-col gap-3 flex-1 min-w-48">
217
+ <p className="text-sm font-semibold">
218
+ <code>set({'{ a: "new" }'})</code>
219
+ </p>
220
+ <p className="text-xs text-muted-foreground">
221
+ Merges — <code>b</code> and <code>c</code> are preserved.
222
+ </p>
223
+ <Button
224
+ variant="outline"
225
+ onClick={() => setActions.set({ a: "new" })}
226
+ >
227
+ set({' { a: "new" }'})
228
+ </Button>
229
+ <Button variant="outline" onClick={() => setActions.reset()}>
230
+ Reset
231
+ </Button>
232
+ <JsonPanel value={setState} />
233
+ </div>
234
+
235
+ <div className="flex flex-col gap-3 flex-1 min-w-48">
236
+ <p className="text-sm font-semibold">
237
+ <code>replace({'{ a: "new" }'})</code>
238
+ </p>
239
+ <p className="text-xs text-muted-foreground">
240
+ Swaps — <code>b</code> and <code>c</code> vanish.
241
+ </p>
242
+ <Button
243
+ variant="outline"
244
+ onClick={() =>
245
+ replaceActions.replace({ a: "new" } as {
246
+ a: string;
247
+ b: string;
248
+ c: string;
249
+ })
250
+ }
251
+ >
252
+ replace({' { a: "new" }'})
110
253
  </Button>
111
- <Button type="button" onClick={reset}>
254
+ <Button variant="outline" onClick={() => replaceActions.reset()}>
112
255
  Reset
113
256
  </Button>
257
+ <JsonPanel value={replaceState} />
258
+ </div>
259
+ </div>
260
+ );
261
+ },
262
+ parameters: {
263
+ docs: {
264
+ description: {
265
+ story:
266
+ 'Side-by-side comparison: `set({ a: "new" })` keeps `b` and `c` intact (shallow merge). `replace({ a: "new" })` discards `b` and `c` (full swap). The JsonPanel makes the difference visible immediately.',
267
+ },
268
+ },
269
+ },
270
+ };
271
+
272
+ // ─── Story 4 — WithOnChange ────────────────────────────────────────────────────
273
+
274
+ export const WithOnChange: Story = {
275
+ render: () => {
276
+ const INITIAL = { name: "", email: "" };
277
+ const [log, setLog] = useState<string[]>([]);
278
+
279
+ const addLog = (action: string, s: typeof INITIAL) =>
280
+ setLog((prev) => [
281
+ `[${timestamp()}] ${action} → ${JSON.stringify(s)}`,
282
+ ...prev,
283
+ ]);
284
+
285
+ const [state, actions] = useObject(INITIAL, {
286
+ onChange: (s) => addLog("onChange", s),
287
+ });
288
+
289
+ return (
290
+ <div className="flex flex-col gap-4 max-w-sm">
291
+ <p className="text-xs text-muted-foreground">
292
+ The log panel below starts empty — proving <code>onChange</code> does
293
+ not fire on initial mount. Every button triggers a real mutation.
294
+ </p>
295
+
296
+ <div className="flex flex-col gap-1">
297
+ <label className="text-sm font-medium">Name</label>
298
+ <input
299
+ className="rounded-md border px-3 py-2 text-sm"
300
+ value={state.name}
301
+ onChange={(e) => actions.setKey("name", e.target.value)}
302
+ placeholder="Type to trigger setKey..."
303
+ />
304
+ </div>
305
+
306
+ <div className="flex flex-col gap-1">
307
+ <label className="text-sm font-medium">Email</label>
308
+ <input
309
+ className="rounded-md border px-3 py-2 text-sm"
310
+ value={state.email}
311
+ onChange={(e) => actions.setKey("email", e.target.value)}
312
+ placeholder="Type to trigger setKey..."
313
+ />
314
+ </div>
315
+
316
+ <div className="flex gap-2 flex-wrap">
317
+ <Button
318
+ variant="outline"
319
+ onClick={() => actions.set({ name: "Bob" })}
320
+ >
321
+ set name
322
+ </Button>
323
+ <Button
324
+ variant="outline"
325
+ onClick={() => actions.replace({ name: "Charlie", email: "" })}
326
+ >
327
+ replace
328
+ </Button>
329
+ <Button variant="outline" onClick={() => actions.reset()}>
330
+ reset
331
+ </Button>
332
+ <Button variant="outline" onClick={() => setLog([])}>
333
+ Clear log
334
+ </Button>
114
335
  </div>
115
- <Results state={state} />
336
+
337
+ <JsonPanel value={state} />
338
+ <LogPanel entries={log} />
116
339
  </div>
117
340
  );
118
341
  },
342
+ parameters: {
343
+ docs: {
344
+ description: {
345
+ story:
346
+ "Wires `onChange` to a visible log panel. The log is empty on mount (no initial fire). Each mutation appends a timestamped entry. Passing a new inline `onChange` reference between renders does not cause double-fires because the callback is read through `useLatestRef`.",
347
+ },
348
+ },
349
+ },
119
350
  };