@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.
- package/dist/components/button/button.cjs.js +1 -1
- package/dist/components/button/button.es.js +19 -18
- package/dist/components/button/components/base-button.cjs.js +1 -1
- package/dist/components/button/components/base-button.es.js +20 -20
- package/dist/components/calendar/calendar.cjs.js +1 -1
- package/dist/components/calendar/calendar.es.js +1 -0
- package/dist/components/date-picker/date-input.cjs.js +1 -1
- package/dist/components/date-picker/date-input.es.js +92 -75
- package/dist/components/date-picker/date-picker.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.es.js +104 -95
- package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.utils.es.js +51 -43
- package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
- package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
- package/dist/components/menu/menu.es.js +1 -9
- package/dist/components/otp/otp.cjs.js +2 -0
- package/dist/components/otp/otp.es.js +93 -0
- package/dist/components/password/password.cjs.js +1 -1
- package/dist/components/password/password.es.js +2 -2
- package/dist/components/select/select.cjs.js +1 -1
- package/dist/components/select/select.es.js +68 -60
- package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
- package/dist/hooks/internal/is-apple-device.es.js +9 -0
- package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
- package/dist/hooks/internal/use-latest-ref.es.js +11 -0
- package/dist/hooks/use-array/use-array.cjs.js +1 -1
- package/dist/hooks/use-array/use-array.es.js +54 -42
- package/dist/hooks/use-async/use-async.cjs.js +1 -1
- package/dist/hooks/use-async/use-async.es.js +53 -20
- package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
- package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
- package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
- package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
- package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
- package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
- package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
- package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
- package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
- package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
- package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
- package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
- package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
- package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
- package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
- package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
- package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
- package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
- package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
- package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
- package/dist/hooks/use-hover/use-hover.es.js +32 -17
- package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
- package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
- package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
- package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
- package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
- package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
- package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
- package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
- package/dist/hooks/use-object/use-object.cjs.js +1 -1
- package/dist/hooks/use-object/use-object.es.js +26 -22
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
- package/dist/hooks/use-step/use-step.cjs.js +1 -1
- package/dist/hooks/use-step/use-step.es.js +25 -24
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +308 -300
- package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
- package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/hooks/index.d.ts +2 -2
- package/dist/src/hooks/internal/index.d.ts +2 -0
- package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
- package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
- package/dist/src/hooks/use-array/use-array.d.ts +24 -11
- package/dist/src/hooks/use-async/use-async.d.ts +16 -13
- package/dist/src/hooks/use-boolean/index.d.ts +1 -0
- package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
- package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
- package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
- package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
- package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
- package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
- package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
- package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
- package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
- package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
- package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
- package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
- package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
- package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
- package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
- package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
- package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
- package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
- package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
- package/dist/src/hooks/use-object/use-object.d.ts +15 -6
- package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
- package/dist/src/hooks/use-step/use-step.d.ts +18 -11
- package/dist/src/utils/form.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
- package/src/components/auto-complete/auto-complete.test.tsx +4 -14
- package/src/components/avatar/avatar.test.tsx +7 -12
- package/src/components/button/button.test.tsx +10 -15
- package/src/components/button/button.tsx +14 -9
- package/src/components/button/components/base-button.tsx +2 -4
- package/src/components/calendar/calendar.test.tsx +12 -19
- package/src/components/calendar/calendar.tsx +4 -0
- package/src/components/card/card.test.tsx +4 -6
- package/src/components/checkbox/checkbox.test.tsx +12 -8
- package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
- package/src/components/combobox/combobox.test.tsx +24 -21
- package/src/components/date-picker/date-input-form.test.tsx +77 -0
- package/src/components/date-picker/date-input.stories.tsx +30 -18
- package/src/components/date-picker/date-input.tsx +77 -44
- package/src/components/date-picker/date-picker.stories.tsx +31 -1
- package/src/components/date-picker/date-picker.test.tsx +3 -13
- package/src/components/date-picker/date-picker.tsx +35 -16
- package/src/components/date-picker/date-picker.utils.test.ts +32 -14
- package/src/components/date-picker/date-picker.utils.ts +33 -0
- package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
- package/src/components/date-picker/use-hidden-field-value.ts +23 -0
- package/src/components/dialog/dialog.test.tsx +10 -8
- package/src/components/dropzone/dropzone.test.tsx +11 -13
- package/src/components/empty/empty.test.tsx +4 -3
- package/src/components/field/field.test.tsx +12 -13
- package/src/components/form/form.stories.tsx +16 -1
- package/src/components/index.ts +1 -0
- package/src/components/label/label.test.tsx +3 -3
- package/src/components/menu/menu.tsx +1 -5
- package/src/components/number-input/number-input.test.tsx +6 -2
- package/src/components/password/password.test.tsx +20 -6
- package/src/components/password/password.tsx +2 -2
- package/src/components/popover/popover.test.tsx +4 -4
- package/src/components/progress/progress.test.tsx +7 -8
- package/src/components/radio-group/radio-group.test.tsx +17 -11
- package/src/components/select/select.test.tsx +10 -10
- package/src/components/select/select.tsx +9 -1
- package/src/components/stepper/stepper.stories.tsx +11 -15
- package/src/components/stepper/stepper.test.tsx +6 -4
- package/src/components/switch/switch.test.tsx +3 -3
- package/src/components/table/table.test.tsx +9 -3
- package/src/components/tabs/tabs.test.tsx +6 -2
- package/src/components/tag/tag.test.tsx +1 -3
- package/src/components/textarea/textarea.test.tsx +4 -1
- package/src/components/timeline/timeline.test.tsx +10 -5
- package/src/components/toast/toast.test.tsx +11 -14
- package/src/components/tooltip/tooltip.test.tsx +1 -5
- package/src/components/tree/tree.test.tsx +3 -1
- package/src/hooks/index.ts +2 -2
- package/src/hooks/internal/index.ts +2 -0
- package/src/hooks/internal/is-apple-device.test.ts +41 -0
- package/src/hooks/internal/is-apple-device.ts +33 -0
- package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
- package/src/hooks/internal/use-latest-ref.ts +21 -0
- package/src/hooks/use-array/use-array.stories.tsx +435 -64
- package/src/hooks/use-array/use-array.test.tsx +398 -15
- package/src/hooks/use-array/use-array.ts +105 -66
- package/src/hooks/use-async/use-async.stories.tsx +255 -131
- package/src/hooks/use-async/use-async.test.ts +397 -0
- package/src/hooks/use-async/use-async.ts +117 -39
- package/src/hooks/use-boolean/index.ts +1 -0
- package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
- package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
- package/src/hooks/use-boolean/use-boolean.ts +50 -0
- package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
- package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
- package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
- package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
- package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
- package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
- package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
- package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
- package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
- package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
- package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
- package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
- package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
- package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
- package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
- package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
- package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
- package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
- package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
- package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
- package/src/hooks/use-hotkey/index.ts +9 -1
- package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
- package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
- package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
- package/src/hooks/use-hotkey/utils/index.ts +8 -3
- package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
- package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
- package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
- package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
- package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
- package/src/hooks/use-hover/use-hover.test.tsx +266 -26
- package/src/hooks/use-hover/use-hover.tsx +93 -28
- package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
- package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
- package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
- package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
- package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
- package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
- package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
- package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
- package/src/hooks/use-media-query/use-media-query.ts +39 -14
- package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
- package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
- package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
- package/src/hooks/use-mutation/use-mutation.ts +97 -0
- package/src/hooks/use-object/use-object.stories.tsx +310 -79
- package/src/hooks/use-object/use-object.test.tsx +235 -56
- package/src/hooks/use-object/use-object.ts +59 -0
- package/src/hooks/use-pagination/use-pagination.tsx +0 -1
- package/src/hooks/use-prevent-page-close/index.ts +1 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
- package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
- package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
- package/src/hooks/use-selection/use-selection.ts +0 -1
- package/src/hooks/use-step/use-step.stories.tsx +178 -65
- package/src/hooks/use-step/use-step.test.ts +178 -53
- package/src/hooks/use-step/use-step.ts +57 -49
- package/src/utils/form.test.tsx +13 -8
- package/src/utils/form.tsx +10 -0
- package/src/utils/functions/getFormData.test.ts +1 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
- package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
- package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
- package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
- package/dist/src/hooks/use-toggle/index.d.ts +0 -1
- package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
- package/src/hooks/use-async/use-async.test.tsx +0 -68
- package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
- package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
- package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
- package/src/hooks/use-mutation/use-mutation.tsx +0 -59
- package/src/hooks/use-object/use-object.tsx +0 -46
- package/src/hooks/use-prevent-close-window/index.ts +0 -1
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
- package/src/hooks/use-toggle/index.ts +0 -1
- package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
- package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
- package/src/hooks/use-toggle/use-toggle.ts +0 -14
- /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
- /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 {
|
|
3
|
-
import { Button
|
|
4
|
-
import { useObject } from "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
82
|
+
const INITIAL = { name: "", email: "", role: "viewer" };
|
|
83
|
+
const [state, actions] = useObject(INITIAL);
|
|
43
84
|
|
|
44
85
|
return (
|
|
45
|
-
<div className="
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
78
|
-
count: number;
|
|
79
|
-
}
|
|
135
|
+
// ─── Story 2 — FunctionalUpdaters ─────────────────────────────────────────────
|
|
80
136
|
|
|
81
|
-
export const
|
|
137
|
+
export const FunctionalUpdaters: Story = {
|
|
82
138
|
render: () => {
|
|
83
|
-
const
|
|
139
|
+
const INITIAL = { score: 0, multiplier: 2, label: "light" };
|
|
140
|
+
const [state, actions] = useObject(INITIAL);
|
|
84
141
|
|
|
85
142
|
return (
|
|
86
|
-
<div className="
|
|
87
|
-
<div className="flex items-center gap-
|
|
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
|
-
|
|
90
|
-
variant="ghost"
|
|
160
|
+
variant="outline"
|
|
91
161
|
onClick={() =>
|
|
92
|
-
|
|
93
|
-
count: state.count - 1,
|
|
94
|
-
}))
|
|
162
|
+
actions.set((s) => ({ score: s.score + s.multiplier }))
|
|
95
163
|
}
|
|
96
164
|
>
|
|
97
|
-
|
|
165
|
+
Boost score (set)
|
|
98
166
|
</Button>
|
|
99
|
-
{state?.count}
|
|
100
167
|
<Button
|
|
101
|
-
|
|
102
|
-
variant="ghost"
|
|
168
|
+
variant="outline"
|
|
103
169
|
onClick={() =>
|
|
104
|
-
|
|
105
|
-
|
|
170
|
+
actions.replace((s) => ({
|
|
171
|
+
score: 0,
|
|
172
|
+
multiplier: 1,
|
|
173
|
+
label: s.label === "light" ? "dark" : "light",
|
|
106
174
|
}))
|
|
107
175
|
}
|
|
108
176
|
>
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
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
|
};
|