@boxcustodia/library 2.0.0-alpha.11 → 2.0.0-alpha.13
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/index.cjs.js +38 -38
- package/dist/index.css +1 -1
- package/dist/index.d.ts +28 -29
- package/dist/index.es.js +6804 -6792
- package/package.json +4 -3
- package/src/__doc__/Changelog.mdx +6 -0
- package/src/__doc__/Components.mdx +73 -0
- package/src/__doc__/Examples.tsx +69 -0
- package/src/__doc__/Icons.mdx +41 -0
- package/src/__doc__/Intro.mdx +138 -0
- package/src/__doc__/MCP.mdx +71 -0
- package/src/__doc__/Migration.mdx +451 -0
- package/src/__doc__/Theme.mdx +132 -0
- package/src/__doc__/Types.mdx +252 -0
- package/src/components/alert/alert.stories.tsx +142 -0
- package/src/components/alert/alert.tsx +109 -0
- package/src/components/alert/index.ts +7 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +173 -0
- package/src/components/alert-dialog/alert-dialog.test.tsx +49 -0
- package/src/components/alert-dialog/alert-dialog.tsx +265 -0
- package/src/components/alert-dialog/index.ts +1 -0
- package/src/components/auto-complete/auto-complete-primitives.tsx +155 -0
- package/src/components/auto-complete/auto-complete.stories.tsx +241 -0
- package/src/components/auto-complete/auto-complete.tsx +82 -0
- package/src/components/auto-complete/index.ts +2 -0
- package/src/components/avatar/avatar.stories.tsx +84 -0
- package/src/components/avatar/avatar.test.tsx +61 -0
- package/src/components/avatar/avatar.tsx +104 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/background-image/background-image.stories.tsx +21 -0
- package/src/components/background-image/background-image.test.tsx +29 -0
- package/src/components/background-image/background-image.tsx +23 -0
- package/src/components/background-image/index.ts +1 -0
- package/src/components/button/button.stories.tsx +396 -0
- package/src/components/button/button.test.tsx +58 -0
- package/src/components/button/button.tsx +31 -0
- package/src/components/button/button.variants.ts +44 -0
- package/src/components/button/components/base-button.tsx +86 -0
- package/src/components/button/components/loader-overlay.tsx +21 -0
- package/src/components/button/components/loading-icon.tsx +47 -0
- package/src/components/button/index.ts +3 -0
- package/src/components/calendar/calendar.model.ts +86 -0
- package/src/components/calendar/calendar.stories.tsx +155 -0
- package/src/components/calendar/calendar.test.tsx +12 -0
- package/src/components/calendar/calendar.tsx +185 -0
- package/src/components/calendar/components/calendar-navigation.tsx +141 -0
- package/src/components/calendar/components/day.tsx +61 -0
- package/src/components/calendar/components/decade-view.tsx +45 -0
- package/src/components/calendar/components/index.ts +6 -0
- package/src/components/calendar/components/month-view.tsx +58 -0
- package/src/components/calendar/components/week-days.tsx +27 -0
- package/src/components/calendar/components/year-view.tsx +29 -0
- package/src/components/calendar/hooks/index.ts +4 -0
- package/src/components/calendar/hooks/use-calendar-navigation.ts +79 -0
- package/src/components/calendar/hooks/use-calendar.ts +90 -0
- package/src/components/calendar/hooks/use-multiple-calendar.ts +34 -0
- package/src/components/calendar/hooks/use-range-calendar.ts +91 -0
- package/src/components/calendar/hooks/use-single-calendar.ts +18 -0
- package/src/components/calendar/index.ts +1 -0
- package/src/components/calendar/utils/typeguards.ts +7 -0
- package/src/components/card/card.stories.tsx +116 -0
- package/src/components/card/card.tsx +74 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/center/center.stories.tsx +81 -0
- package/src/components/center/center.tsx +24 -0
- package/src/components/center/index.ts +1 -0
- package/src/components/checkbox/checkbox.stories.tsx +307 -0
- package/src/components/checkbox/checkbox.tsx +273 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/checkbox-group/checkbox-group.stories.tsx +104 -0
- package/src/components/checkbox-group/checkbox-group.tsx +16 -0
- package/src/components/checkbox-group/index.ts +1 -0
- package/src/components/combobox/combobox.stories.tsx +339 -0
- package/src/components/combobox/combobox.tsx +898 -0
- package/src/components/combobox/index.ts +1 -0
- package/src/components/date-picker/date-input.stories.tsx +158 -0
- package/src/components/date-picker/date-input.tsx +163 -0
- package/src/components/date-picker/date-picker.model.ts +90 -0
- package/src/components/date-picker/date-picker.stories.tsx +200 -0
- package/src/components/date-picker/date-picker.test.tsx +23 -0
- package/src/components/date-picker/date-picker.tsx +298 -0
- package/src/components/date-picker/date-picker.utils.ts +260 -0
- package/src/components/date-picker/index.ts +3 -0
- package/src/components/date-picker/use-date-input-popover.ts +48 -0
- package/src/components/date-picker/use-date-input.ts +125 -0
- package/src/components/dialog/dialog.stories.tsx +171 -0
- package/src/components/dialog/dialog.test.tsx +68 -0
- package/src/components/dialog/dialog.tsx +277 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/divider/divider.stories.tsx +139 -0
- package/src/components/divider/divider.test.tsx +22 -0
- package/src/components/divider/divider.tsx +23 -0
- package/src/components/divider/index.ts +1 -0
- package/src/components/dropzone/dropzone.stories.tsx +210 -0
- package/src/components/dropzone/dropzone.tsx +154 -0
- package/src/components/dropzone/file-types.ts +64 -0
- package/src/components/dropzone/index.ts +3 -0
- package/src/components/dropzone/upload-primitives.tsx +310 -0
- package/src/components/dropzone/use-dropzone.ts +122 -0
- package/src/components/empty-state/empty-state.stories.tsx +56 -0
- package/src/components/empty-state/empty-state.tsx +39 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/field/field.stories.tsx +223 -0
- package/src/components/field/field.tsx +229 -0
- package/src/components/field/index.ts +1 -0
- package/src/components/form/form.stories.tsx +594 -0
- package/src/components/form/form.tsx +30 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/heading/heading.stories.tsx +74 -0
- package/src/components/heading/heading.tsx +28 -0
- package/src/components/heading/heading.variants.ts +27 -0
- package/src/components/heading/index.ts +1 -0
- package/src/components/index.ts +46 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.stories.tsx +104 -0
- package/src/components/input/input.tsx +75 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/kbd/kbd.stories.tsx +40 -0
- package/src/components/kbd/kbd.tsx +31 -0
- package/src/components/kbd/kbd.variants.ts +26 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.stories.tsx +68 -0
- package/src/components/label/label.test.tsx +61 -0
- package/src/components/label/label.tsx +62 -0
- package/src/components/loader/index.ts +1 -0
- package/src/components/loader/loader.stories.tsx +60 -0
- package/src/components/loader/loader.test.tsx +26 -0
- package/src/components/loader/loader.tsx +60 -0
- package/src/components/menu/index.ts +2 -0
- package/src/components/menu/menu-primitives.tsx +248 -0
- package/src/components/menu/menu.stories.tsx +203 -0
- package/src/components/menu/menu.tsx +100 -0
- package/src/components/menu/util/render-menu-item.tsx +54 -0
- package/src/components/multi-select/hooks/use-multi-select.ts +66 -0
- package/src/components/multi-select/index.ts +1 -0
- package/src/components/multi-select/multi-select.stories.tsx +294 -0
- package/src/components/multi-select/multi-select.tsx +300 -0
- package/src/components/multi-select/multi-select.variants.ts +22 -0
- package/src/components/number-input/index.ts +1 -0
- package/src/components/number-input/number-input.stories.tsx +209 -0
- package/src/components/number-input/number-input.test.tsx +87 -0
- package/src/components/number-input/number-input.tsx +232 -0
- package/src/components/pagination/components/pagination-option.tsx +27 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.stories.tsx +80 -0
- package/src/components/pagination/pagination.test.tsx +76 -0
- package/src/components/pagination/pagination.tsx +102 -0
- package/src/components/password/index.ts +1 -0
- package/src/components/password/password.stories.tsx +104 -0
- package/src/components/password/password.tsx +75 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.stories.tsx +213 -0
- package/src/components/popover/popover.tsx +203 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.stories.tsx +124 -0
- package/src/components/progress/progress.test.tsx +25 -0
- package/src/components/progress/progress.tsx +124 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +166 -0
- package/src/components/scroll-area/scroll-area.tsx +64 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.stories.tsx +253 -0
- package/src/components/select/select.tsx +430 -0
- package/src/components/show/index.ts +1 -0
- package/src/components/show/show.stories.tsx +197 -0
- package/src/components/show/show.test.tsx +41 -0
- package/src/components/show/show.tsx +16 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.stories.tsx +36 -0
- package/src/components/skeleton/skeleton.test.tsx +14 -0
- package/src/components/skeleton/skeleton.tsx +15 -0
- package/src/components/stack/index.ts +1 -0
- package/src/components/stack/stack.stories.tsx +194 -0
- package/src/components/stack/stack.tsx +52 -0
- package/src/components/stepper/Stepper.tsx +190 -0
- package/src/components/stepper/context/stepper-context.tsx +11 -0
- package/src/components/stepper/index.ts +1 -0
- package/src/components/stepper/stepper.stories.tsx +130 -0
- package/src/components/stepper/stepper.test.tsx +91 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.stories.tsx +122 -0
- package/src/components/switch/switch.test.tsx +30 -0
- package/src/components/switch/switch.tsx +86 -0
- package/src/components/table/index.ts +3 -0
- package/src/components/table/table-primitives.tsx +122 -0
- package/src/components/table/table.model.ts +20 -0
- package/src/components/table/table.stories.tsx +169 -0
- package/src/components/table/table.test.tsx +91 -0
- package/src/components/table/table.tsx +109 -0
- package/src/components/table-pagination/index.ts +2 -0
- package/src/components/table-pagination/table-pagination.model.ts +2 -0
- package/src/components/table-pagination/table-pagination.stories.tsx +23 -0
- package/src/components/table-pagination/table-pagination.test.tsx +32 -0
- package/src/components/table-pagination/table-pagination.tsx +108 -0
- package/src/components/tabs/context/tabs-context.tsx +14 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.stories.tsx +182 -0
- package/src/components/tabs/tabs.test.tsx +61 -0
- package/src/components/tabs/tabs.tsx +175 -0
- package/src/components/tag/index.ts +2 -0
- package/src/components/tag/tag.stories.tsx +170 -0
- package/src/components/tag/tag.test.tsx +18 -0
- package/src/components/tag/tag.tsx +99 -0
- package/src/components/tag/tag.variants.ts +31 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.stories.tsx +73 -0
- package/src/components/textarea/textarea.tsx +105 -0
- package/src/components/timeline/index.ts +1 -0
- package/src/components/timeline/timeline-status.ts +5 -0
- package/src/components/timeline/timeline.stories.tsx +84 -0
- package/src/components/timeline/timeline.tsx +147 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.stories.tsx +392 -0
- package/src/components/toast/toast.test.tsx +50 -0
- package/src/components/toast/toast.tsx +411 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.stories.tsx +226 -0
- package/src/components/tooltip/tooltip.test.tsx +46 -0
- package/src/components/tooltip/tooltip.tsx +171 -0
- package/src/components/tree/hooks/use-controllable-tree-state.ts +80 -0
- package/src/components/tree/index.ts +2 -0
- package/src/components/tree/tree-primitives.tsx +126 -0
- package/src/components/tree/tree.stories.tsx +468 -0
- package/src/components/tree/tree.tsx +42 -0
- package/src/hooks/index.ts +26 -0
- package/src/hooks/useArray/__doc__/useArray.stories.tsx +100 -0
- package/src/hooks/useArray/__test__/useArray.test.tsx +88 -0
- package/src/hooks/useArray/index.ts +1 -0
- package/src/hooks/useArray/useArray.ts +76 -0
- package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +149 -0
- package/src/hooks/useAsync/__test__/useAsync.test.tsx +68 -0
- package/src/hooks/useAsync/index.ts +1 -0
- package/src/hooks/useAsync/useAsync.ts +58 -0
- package/src/hooks/useClickOutside/__doc__/useClickOutside.stories.tsx +40 -0
- package/src/hooks/useClickOutside/__test__/useClickOutside.test.tsx +33 -0
- package/src/hooks/useClickOutside/index.ts +1 -0
- package/src/hooks/useClickOutside/useClickOutside.ts +26 -0
- package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +45 -0
- package/src/hooks/useClipboard/__test__/useClipboard.test.tsx +19 -0
- package/src/hooks/useClipboard/index.ts +1 -0
- package/src/hooks/useClipboard/useClipboard.tsx +28 -0
- package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +84 -0
- package/src/hooks/useDebounceCallback/index.ts +1 -0
- package/src/hooks/useDebounceCallback/useDebouncedCallback.ts +23 -0
- package/src/hooks/useDebounceValue/__doc__/useDebouncedValue.stories.tsx +75 -0
- package/src/hooks/useDebounceValue/index.ts +1 -0
- package/src/hooks/useDebounceValue/useDebouncedValue.ts +17 -0
- package/src/hooks/useDisclosure/__doc__/useDisclosure.stories.tsx +39 -0
- package/src/hooks/useDisclosure/__test__/useDisclosure.test.ts +43 -0
- package/src/hooks/useDisclosure/index.ts +1 -0
- package/src/hooks/useDisclosure/useDisclosure.ts +37 -0
- package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +26 -0
- package/src/hooks/useDocumentTitle/index.ts +1 -0
- package/src/hooks/useDocumentTitle/useDocumentTitle.tsx +11 -0
- package/src/hooks/useEventListener/__doc__/useEventListener.stories.tsx +28 -0
- package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +26 -0
- package/src/hooks/useEventListener/index.ts +1 -0
- package/src/hooks/useEventListener/useEventListener.ts +25 -0
- package/src/hooks/useFocusTrap/__doc__/useFocusTrap.stories.tsx +37 -0
- package/src/hooks/useFocusTrap/index.ts +1 -0
- package/src/hooks/useFocusTrap/scopeTab.ts +38 -0
- package/src/hooks/useFocusTrap/tabbable.ts +70 -0
- package/src/hooks/useFocusTrap/useFocusTrap.ts +78 -0
- package/src/hooks/useHotkey/__docs__/useHotkey.stories.tsx +116 -0
- package/src/hooks/useHotkey/__test__/useHotkey.test.tsx +105 -0
- package/src/hooks/useHotkey/__utils__/create-hotkey-listener.ts +25 -0
- package/src/hooks/useHotkey/__utils__/index.ts +3 -0
- package/src/hooks/useHotkey/__utils__/is-input-field.ts +14 -0
- package/src/hooks/useHotkey/__utils__/match-key-modifiers.ts +25 -0
- package/src/hooks/useHotkey/index.ts +1 -0
- package/src/hooks/useHotkey/useHotkey.ts +34 -0
- package/src/hooks/useHover/__doc__/useHover.stories.tsx +41 -0
- package/src/hooks/useHover/__test__/useHover.test.tsx +45 -0
- package/src/hooks/useHover/index.ts +1 -0
- package/src/hooks/useHover/useHover.tsx +40 -0
- package/src/hooks/useIsVisible/__doc__/useIsVisible.stories.tsx +60 -0
- package/src/hooks/useIsVisible/index.ts +1 -0
- package/src/hooks/useIsVisible/useIsVisible.tsx +50 -0
- package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +86 -0
- package/src/hooks/useLocalStorage/__test__/useLocalStorage.test.ts +85 -0
- package/src/hooks/useLocalStorage/index.ts +1 -0
- package/src/hooks/useLocalStorage/useLocalStorage.ts +57 -0
- package/src/hooks/useMediaQuery/__doc__/useMediaQuery.stories.tsx +39 -0
- package/src/hooks/useMediaQuery/index.ts +1 -0
- package/src/hooks/useMediaQuery/useMediaQuery.ts +22 -0
- package/src/hooks/useMemoizedFn/index.ts +1 -0
- package/src/hooks/useMemoizedFn/useMemoizedFn.ts +32 -0
- package/src/hooks/useMutation/__doc__/useMutation.stories.tsx +111 -0
- package/src/hooks/useMutation/__test__/useMutation.test.tsx +83 -0
- package/src/hooks/useMutation/index.ts +1 -0
- package/src/hooks/useMutation/useMutation.tsx +60 -0
- package/src/hooks/useObject/__doc__/useObject.stories.tsx +119 -0
- package/src/hooks/useObject/__test__/useObject.test.tsx +87 -0
- package/src/hooks/useObject/index.ts +1 -0
- package/src/hooks/useObject/useObject.tsx +48 -0
- package/src/hooks/usePagination/__doc__/usePagination.stories.tsx +72 -0
- package/src/hooks/usePagination/__test__/usePagination.test.tsx +98 -0
- package/src/hooks/usePagination/index.ts +2 -0
- package/src/hooks/usePagination/usePagination.tsx +74 -0
- package/src/hooks/usePortal/__doc__/usePortal.stories.tsx +19 -0
- package/src/hooks/usePortal/__test__/usePortal.test.tsx +20 -0
- package/src/hooks/usePortal/index.ts +1 -0
- package/src/hooks/usePortal/usePortal.ts +40 -0
- package/src/hooks/usePreventCloseWindow/__doc__/usePreventCloseWindow.stories.tsx +32 -0
- package/src/hooks/usePreventCloseWindow/index.ts +1 -0
- package/src/hooks/usePreventCloseWindow/usePreventCloseWindow.ts +33 -0
- package/src/hooks/useRangePagination/__test__/useRangePagination.test.tsx +63 -0
- package/src/hooks/useRangePagination/index.ts +2 -0
- package/src/hooks/useRangePagination/useRangePagination.tsx +72 -0
- package/src/hooks/useSelection/__doc__/useSelection.stories.tsx +140 -0
- package/src/hooks/useSelection/__test__/useSelection.test.tsx +57 -0
- package/src/hooks/useSelection/index.ts +1 -0
- package/src/hooks/useSelection/useSelection.ts +121 -0
- package/src/hooks/useStep/__doc__/useStep.stories.tsx +98 -0
- package/src/hooks/useStep/__test__/useStep.test.ts +51 -0
- package/src/hooks/useStep/index.ts +1 -0
- package/src/hooks/useStep/useStep.ts +57 -0
- package/src/hooks/useToggle/__doc__/useToggle.stories.tsx +25 -0
- package/src/hooks/useToggle/__test__/useToggle.test.tsx +43 -0
- package/src/hooks/useToggle/index.ts +1 -0
- package/src/hooks/useToggle/useToggle.ts +16 -0
- package/src/index.ts +6 -0
- package/src/lib/cn.ts +8 -0
- package/src/lib/index.ts +1 -0
- package/src/models/Generic.model.ts +67 -0
- package/src/models/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/library-provider.tsx +44 -0
- package/src/providers/theme/ThemeProvider.tsx +25 -0
- package/src/providers/theme/index.ts +3 -0
- package/src/providers/theme/types.ts +11 -0
- package/src/providers/theme/useThemeProps.ts +25 -0
- package/src/stores/theme.store.ts +31 -0
- package/src/styles/components.css +4 -0
- package/src/styles/index.css +2 -0
- package/src/styles/library.css +2 -0
- package/src/styles/theme.css +232 -0
- package/src/utils/dates/parseDateRange.utility.ts +39 -0
- package/src/utils/form.tsx +91 -0
- package/src/utils/functions/createSafeContext.ts +17 -0
- package/src/utils/functions/ensureReactElement.tsx +30 -0
- package/src/utils/functions/getFormData.ts +19 -0
- package/src/utils/functions/index.ts +4 -0
- package/src/utils/functions/mergeRefs.ts +18 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/strings/extractInitials.utility.ts +10 -0
- package/src/utils/strings/index.ts +1 -0
- package/src/utils/tests/click.ts +3 -0
- package/src/utils/tests/index.ts +2 -0
- package/src/utils/tests/keyboard.ts +21 -0
- package/src/utils/tests/type.ts +6 -0
- package/dist/components.css +0 -2
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
type FieldValues,
|
|
7
|
+
type SubmitHandler,
|
|
8
|
+
type UseFormProps,
|
|
9
|
+
type UseFormReturn,
|
|
10
|
+
useController,
|
|
11
|
+
useForm,
|
|
12
|
+
} from "react-hook-form";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { Button } from "../button/button";
|
|
15
|
+
import { Checkbox } from "../checkbox/checkbox";
|
|
16
|
+
import { Combobox } from "../combobox";
|
|
17
|
+
import { DateInput } from "../date-picker";
|
|
18
|
+
import { Field, FieldError, FieldLabel, FieldRoot } from "../field/field";
|
|
19
|
+
import { Input } from "../input/input";
|
|
20
|
+
import { NumberInput } from "../number-input";
|
|
21
|
+
import { PasswordRoot } from "../password/password";
|
|
22
|
+
import { Select } from "../select";
|
|
23
|
+
import { Textarea } from "../textarea/textarea";
|
|
24
|
+
import { toast } from "../toast";
|
|
25
|
+
import { Form, FormPrimitive } from "./form";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Thin wrapper around Base UI Form. `onSubmit` receives `(data, event)` —
|
|
29
|
+
* `data` is a `Record<string, unknown>` parsed from `FormData`, `event` is
|
|
30
|
+
* the native submit event. `preventDefault` is called automatically.
|
|
31
|
+
* `errors` propagates server-side messages to fields by `name`.
|
|
32
|
+
* `validationMode` lives on `FieldRoot`, not on Form — set it per-field.
|
|
33
|
+
*
|
|
34
|
+
* Reference: [Forms – Base UI](https://base-ui.com/react/handbook/forms)
|
|
35
|
+
*/
|
|
36
|
+
const meta: Meta<typeof Form> = {
|
|
37
|
+
title: "Components/Form",
|
|
38
|
+
component: Form,
|
|
39
|
+
parameters: { layout: "centered" },
|
|
40
|
+
argTypes: {
|
|
41
|
+
children: { control: false },
|
|
42
|
+
onSubmit: { control: false },
|
|
43
|
+
actionsRef: { control: false },
|
|
44
|
+
errors: { control: false },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof Form>;
|
|
50
|
+
|
|
51
|
+
const NOTIFICATION_CHANNELS = [
|
|
52
|
+
{ label: "Email", value: "email" },
|
|
53
|
+
{ label: "SMS", value: "sms" },
|
|
54
|
+
{ label: "Push", value: "push" },
|
|
55
|
+
{ label: "Slack", value: "slack" },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const ROLES = [
|
|
59
|
+
{ label: "Developer", value: "developer" },
|
|
60
|
+
{ label: "Designer", value: "designer" },
|
|
61
|
+
{ label: "Manager", value: "manager" },
|
|
62
|
+
{ label: "QA Engineer", value: "qa" },
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Native HTML constraint validation. Each `FieldRoot` carries a `name` so Form
|
|
66
|
+
* can route server errors automatically. Errors only surface after the first
|
|
67
|
+
* submit attempt — no premature invalid state.
|
|
68
|
+
*/
|
|
69
|
+
export const Default: Story = {
|
|
70
|
+
render: () => {
|
|
71
|
+
const [done, setDone] = useState(false);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Form className="flex w-80 flex-col gap-4" onSubmit={() => setDone(true)}>
|
|
75
|
+
<Field
|
|
76
|
+
name="email"
|
|
77
|
+
label="Email"
|
|
78
|
+
tooltip="We will never share your email."
|
|
79
|
+
error={[
|
|
80
|
+
{ message: "Email is required.", match: "valueMissing" },
|
|
81
|
+
{ message: "Enter a valid email address.", match: "typeMismatch" },
|
|
82
|
+
]}
|
|
83
|
+
>
|
|
84
|
+
<Input type="email" required placeholder="you@example.com" />
|
|
85
|
+
</Field>
|
|
86
|
+
|
|
87
|
+
<Field
|
|
88
|
+
name="username"
|
|
89
|
+
label="Username"
|
|
90
|
+
error={[
|
|
91
|
+
{ message: "Username is required.", match: "valueMissing" },
|
|
92
|
+
{ message: "Username must be 3–15 characters.", match: "tooShort" },
|
|
93
|
+
{
|
|
94
|
+
message: "Only letters, numbers, and underscores.",
|
|
95
|
+
match: "patternMismatch",
|
|
96
|
+
},
|
|
97
|
+
]}
|
|
98
|
+
>
|
|
99
|
+
<Input
|
|
100
|
+
required
|
|
101
|
+
pattern="[a-zA-Z0-9_]+"
|
|
102
|
+
minLength={3}
|
|
103
|
+
maxLength={15}
|
|
104
|
+
placeholder="e.g. jane_doe"
|
|
105
|
+
/>
|
|
106
|
+
</Field>
|
|
107
|
+
|
|
108
|
+
<Field
|
|
109
|
+
name="role"
|
|
110
|
+
label="Role"
|
|
111
|
+
error={[{ message: "Role is required.", match: "valueMissing" }]}
|
|
112
|
+
>
|
|
113
|
+
<Select items={ROLES} placeholder="Select a role…" required />
|
|
114
|
+
</Field>
|
|
115
|
+
|
|
116
|
+
<Field
|
|
117
|
+
name="experience"
|
|
118
|
+
label="Years of experience"
|
|
119
|
+
description="Must be between 0 and 50."
|
|
120
|
+
error={[
|
|
121
|
+
{ message: "This field is required.", match: "valueMissing" },
|
|
122
|
+
{ message: "Value must be 0 or more.", match: "rangeUnderflow" },
|
|
123
|
+
{ message: "Value cannot exceed 50.", match: "rangeOverflow" },
|
|
124
|
+
]}
|
|
125
|
+
>
|
|
126
|
+
<NumberInput required min={0} max={50} />
|
|
127
|
+
</Field>
|
|
128
|
+
|
|
129
|
+
<Field
|
|
130
|
+
name="startDate"
|
|
131
|
+
label="Start date"
|
|
132
|
+
error={{ message: "Start date is required.", match: "valueMissing" }}
|
|
133
|
+
>
|
|
134
|
+
<DateInput required />
|
|
135
|
+
</Field>
|
|
136
|
+
|
|
137
|
+
<Field
|
|
138
|
+
name="notifications"
|
|
139
|
+
label="Notification channels"
|
|
140
|
+
required
|
|
141
|
+
error={[
|
|
142
|
+
{ message: "Select at least one channel.", match: "valueMissing" },
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
<Combobox
|
|
146
|
+
items={NOTIFICATION_CHANNELS}
|
|
147
|
+
defaultValue={[NOTIFICATION_CHANNELS[0]]}
|
|
148
|
+
placeholder="Select channels…"
|
|
149
|
+
multiple
|
|
150
|
+
required
|
|
151
|
+
/>
|
|
152
|
+
</Field>
|
|
153
|
+
|
|
154
|
+
<Field
|
|
155
|
+
name="password"
|
|
156
|
+
label="Password"
|
|
157
|
+
error={{ message: "Password is required.", match: "valueMissing" }}
|
|
158
|
+
>
|
|
159
|
+
<PasswordRoot required placeholder="Enter your password" />
|
|
160
|
+
</Field>
|
|
161
|
+
|
|
162
|
+
<Field
|
|
163
|
+
name="bio"
|
|
164
|
+
label="Bio"
|
|
165
|
+
tooltip="Between 20 and 300 characters."
|
|
166
|
+
error={[
|
|
167
|
+
{ message: "Bio is required.", match: "valueMissing" },
|
|
168
|
+
{
|
|
169
|
+
message: "Bio must be at least 20 characters.",
|
|
170
|
+
match: "tooShort",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
message: "Bio must be 300 characters or less.",
|
|
174
|
+
match: "tooLong",
|
|
175
|
+
},
|
|
176
|
+
]}
|
|
177
|
+
>
|
|
178
|
+
<Textarea
|
|
179
|
+
required
|
|
180
|
+
minLength={10}
|
|
181
|
+
maxLength={300}
|
|
182
|
+
placeholder="Tell us a bit about yourself…"
|
|
183
|
+
/>
|
|
184
|
+
</Field>
|
|
185
|
+
|
|
186
|
+
<Field
|
|
187
|
+
name="terms"
|
|
188
|
+
inline
|
|
189
|
+
label="I accept the terms and conditions"
|
|
190
|
+
error={{
|
|
191
|
+
message: "You must accept the terms.",
|
|
192
|
+
match: "valueMissing",
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<Checkbox required />
|
|
196
|
+
</Field>
|
|
197
|
+
|
|
198
|
+
{done && <p className="text-sm text-green-600">Account created!</p>}
|
|
199
|
+
|
|
200
|
+
<Button type="submit">Create account</Button>
|
|
201
|
+
</Form>
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `onSubmit` receives parsed `FormData` and is the right place for cross-field
|
|
208
|
+
* and custom-rule validation. Errors are stored in state and forwarded via
|
|
209
|
+
* `Form.errors`, which routes each message to the matching field by `name` and
|
|
210
|
+
* clears it automatically as soon as the user modifies that field.
|
|
211
|
+
*
|
|
212
|
+
* Native browser validation (email format, required) runs first — `onSubmit`
|
|
213
|
+
* only fires after all native constraints pass.
|
|
214
|
+
*/
|
|
215
|
+
export const OnSubmitValidation: Story = {
|
|
216
|
+
render: () => {
|
|
217
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<Form
|
|
221
|
+
className="flex w-80 flex-col gap-4"
|
|
222
|
+
errors={errors}
|
|
223
|
+
onSubmit={(data) => {
|
|
224
|
+
const next: Record<string, string> = {};
|
|
225
|
+
const pwd = data.password as string;
|
|
226
|
+
|
|
227
|
+
if (!/[A-Z]/.test(pwd))
|
|
228
|
+
next.password = "Must contain at least 1 uppercase letter.";
|
|
229
|
+
else if (!/[a-z]/.test(pwd))
|
|
230
|
+
next.password = "Must contain at least 1 lowercase letter.";
|
|
231
|
+
else if (!/\d/.test(pwd))
|
|
232
|
+
next.password = "Must contain at least 1 number.";
|
|
233
|
+
|
|
234
|
+
if (data.password !== data.confirmPassword)
|
|
235
|
+
next.confirmPassword = "Passwords must match.";
|
|
236
|
+
|
|
237
|
+
if (Object.keys(next).length > 0) {
|
|
238
|
+
setErrors(next);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setErrors({});
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
<Field
|
|
246
|
+
name="email"
|
|
247
|
+
label="Email"
|
|
248
|
+
error={[
|
|
249
|
+
{ message: "Email is required.", match: "valueMissing" },
|
|
250
|
+
{ message: "Enter a valid email address.", match: "typeMismatch" },
|
|
251
|
+
]}
|
|
252
|
+
>
|
|
253
|
+
<Input type="email" required placeholder="you@example.com" />
|
|
254
|
+
</Field>
|
|
255
|
+
|
|
256
|
+
<Field
|
|
257
|
+
name="password"
|
|
258
|
+
label="Password"
|
|
259
|
+
error={{ message: "Password is required.", match: "valueMissing" }}
|
|
260
|
+
>
|
|
261
|
+
<PasswordRoot required placeholder="Enter your password" />
|
|
262
|
+
</Field>
|
|
263
|
+
|
|
264
|
+
<Field
|
|
265
|
+
name="confirmPassword"
|
|
266
|
+
label="Repeat password"
|
|
267
|
+
error={{
|
|
268
|
+
message: "Repeat password is required.",
|
|
269
|
+
match: "valueMissing",
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<PasswordRoot required placeholder="Repeat your password" />
|
|
273
|
+
</Field>
|
|
274
|
+
|
|
275
|
+
<Button type="submit">Create account</Button>
|
|
276
|
+
</Form>
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* `validationMode` is a `FieldRoot` prop, not a Form prop — set it per-field.
|
|
283
|
+
* `"onChange"` validates on every keystroke. `"onBlur"` validates when focus leaves.
|
|
284
|
+
* Default is `"onSubmit"`.
|
|
285
|
+
*/
|
|
286
|
+
export const ValidateOnChange: Story = {
|
|
287
|
+
render: () => (
|
|
288
|
+
<Form className="flex w-80 flex-col gap-4">
|
|
289
|
+
<Field
|
|
290
|
+
name="email"
|
|
291
|
+
label="Email"
|
|
292
|
+
required
|
|
293
|
+
validationMode="onChange"
|
|
294
|
+
error={[
|
|
295
|
+
{ message: "Email is required.", match: "valueMissing" },
|
|
296
|
+
{ message: "Enter a valid email address.", match: "typeMismatch" },
|
|
297
|
+
]}
|
|
298
|
+
>
|
|
299
|
+
<Input type="email" required placeholder="you@example.com" />
|
|
300
|
+
</Field>
|
|
301
|
+
<Button type="submit">Submit</Button>
|
|
302
|
+
</Form>
|
|
303
|
+
),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `onSubmit(data, event)` — `data` is parsed from `FormData`, `event` is the
|
|
308
|
+
* native submit event. `preventDefault` is already called by the Form wrapper.
|
|
309
|
+
*/
|
|
310
|
+
export const OnFormSubmit: Story = {
|
|
311
|
+
render: () => {
|
|
312
|
+
const [values, setValues] = useState<Record<string, unknown> | null>(null);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<Form
|
|
316
|
+
className="flex w-80 flex-col gap-4"
|
|
317
|
+
onSubmit={(data) => setValues(data)}
|
|
318
|
+
>
|
|
319
|
+
<Field name="email" label="Email">
|
|
320
|
+
<Input type="email" required placeholder="you@example.com" />
|
|
321
|
+
</Field>
|
|
322
|
+
<Field name="username" label="Username">
|
|
323
|
+
<Input required placeholder="e.g. jane_doe" />
|
|
324
|
+
</Field>
|
|
325
|
+
<Button type="submit">Submit</Button>
|
|
326
|
+
{values && (
|
|
327
|
+
<pre className="rounded-md bg-muted p-3 text-xs">
|
|
328
|
+
{JSON.stringify(values, null, 2)}
|
|
329
|
+
</pre>
|
|
330
|
+
)}
|
|
331
|
+
</Form>
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* `FormPrimitive` + `FieldRoot` primitives for full structural control.
|
|
338
|
+
* Use when you need custom ordering or elements between parts.
|
|
339
|
+
*
|
|
340
|
+
* ```tsx
|
|
341
|
+
* <FormPrimitive>
|
|
342
|
+
* <FieldRoot name="email">
|
|
343
|
+
* <FieldLabel>Email</FieldLabel>
|
|
344
|
+
* <Input type="email" required />
|
|
345
|
+
* <FieldError match="valueMissing">Required.</FieldError>
|
|
346
|
+
* </FieldRoot>
|
|
347
|
+
* </FormPrimitive>
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
export const Primitive: Story = {
|
|
351
|
+
render: () => (
|
|
352
|
+
<FormPrimitive
|
|
353
|
+
className="flex w-80 flex-col gap-4"
|
|
354
|
+
onSubmit={(e) => e.preventDefault()}
|
|
355
|
+
>
|
|
356
|
+
<FieldRoot name="email">
|
|
357
|
+
<FieldLabel>Email</FieldLabel>
|
|
358
|
+
<Input type="email" required placeholder="you@example.com" />
|
|
359
|
+
<FieldError match="valueMissing">Email is required.</FieldError>
|
|
360
|
+
<FieldError match="typeMismatch">
|
|
361
|
+
Enter a valid email address.
|
|
362
|
+
</FieldError>
|
|
363
|
+
</FieldRoot>
|
|
364
|
+
<Button type="submit">Submit</Button>
|
|
365
|
+
</FormPrimitive>
|
|
366
|
+
),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const CATEGORIES = [
|
|
370
|
+
{ label: "Frontend", value: "frontend" },
|
|
371
|
+
{ label: "Backend", value: "backend" },
|
|
372
|
+
{ label: "DevOps", value: "devops" },
|
|
373
|
+
{ label: "Design", value: "design" },
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const schema = z.object({
|
|
377
|
+
age: z.coerce
|
|
378
|
+
.number({ message: "Please enter a number." })
|
|
379
|
+
.positive({ message: "Number must be positive." }),
|
|
380
|
+
name: z.string().min(1, { message: "Please enter a name." }),
|
|
381
|
+
emoji: z.emoji({ message: "Please enter an emoji." }),
|
|
382
|
+
category: z.string().min(1, { message: "Select a category." }),
|
|
383
|
+
terms: z.literal(true, { message: "You must accept the terms." }),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
type FormDataType = z.infer<typeof schema>;
|
|
387
|
+
type Errors = Partial<Record<keyof FormDataType, string[]>>;
|
|
388
|
+
|
|
389
|
+
export const UsingZod: Story = {
|
|
390
|
+
render: () => {
|
|
391
|
+
const [loading, setLoading] = useState(false);
|
|
392
|
+
const [errors, setErrors] = useState<Errors>({});
|
|
393
|
+
|
|
394
|
+
const onSubmit = async (data: any) => {
|
|
395
|
+
setLoading(true);
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const result = schema.safeParse(data);
|
|
399
|
+
|
|
400
|
+
if (!result.success) {
|
|
401
|
+
const { fieldErrors } = z.flattenError(result.error);
|
|
402
|
+
setErrors(fieldErrors as Errors);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
setErrors({});
|
|
407
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
408
|
+
|
|
409
|
+
toast({
|
|
410
|
+
title: "Success",
|
|
411
|
+
description: (
|
|
412
|
+
<pre className="rounded-md bg-muted p-3 text-sm font-mono">
|
|
413
|
+
{JSON.stringify(result.data, null, 2)}
|
|
414
|
+
</pre>
|
|
415
|
+
),
|
|
416
|
+
});
|
|
417
|
+
} finally {
|
|
418
|
+
setLoading(false);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<Form
|
|
424
|
+
className="flex w-full max-w-64 flex-col gap-4"
|
|
425
|
+
errors={errors}
|
|
426
|
+
onSubmit={onSubmit}
|
|
427
|
+
>
|
|
428
|
+
<Field name="name" label="Name" required>
|
|
429
|
+
<Input placeholder="Enter name" />
|
|
430
|
+
</Field>
|
|
431
|
+
|
|
432
|
+
<Field name="age" label="Age" description="Must be positive." required>
|
|
433
|
+
<Input placeholder="Enter age" />
|
|
434
|
+
</Field>
|
|
435
|
+
|
|
436
|
+
<Field
|
|
437
|
+
name="emoji"
|
|
438
|
+
label="Emoji"
|
|
439
|
+
required
|
|
440
|
+
tooltip="What's your favorite emoji?"
|
|
441
|
+
>
|
|
442
|
+
<Input />
|
|
443
|
+
</Field>
|
|
444
|
+
|
|
445
|
+
<Field name="category" label="Category" required>
|
|
446
|
+
<Combobox items={CATEGORIES} placeholder="Select a category…" />
|
|
447
|
+
</Field>
|
|
448
|
+
|
|
449
|
+
<Field
|
|
450
|
+
name="terms"
|
|
451
|
+
inline
|
|
452
|
+
label="I accept the terms and conditions"
|
|
453
|
+
required
|
|
454
|
+
>
|
|
455
|
+
<Checkbox />
|
|
456
|
+
</Field>
|
|
457
|
+
|
|
458
|
+
<Button loading={loading} type="submit">
|
|
459
|
+
Submit
|
|
460
|
+
</Button>
|
|
461
|
+
</Form>
|
|
462
|
+
);
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// ── React Hook Form ───────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Integration with React Hook Form + Zod. The `RHFField` helper wraps `Field`
|
|
470
|
+
* and feeds `invalid`/`error` from `fieldState` — copy it into your project.
|
|
471
|
+
*
|
|
472
|
+
* ```tsx
|
|
473
|
+
* function RHFField({ name, label, description, rules, render }) {
|
|
474
|
+
* const { field, fieldState } = useController({ name, rules });
|
|
475
|
+
* return (
|
|
476
|
+
* <Field
|
|
477
|
+
* name={name}
|
|
478
|
+
* label={label}
|
|
479
|
+
* description={description}
|
|
480
|
+
* invalid={fieldState.invalid}
|
|
481
|
+
* error={fieldState.error?.message}
|
|
482
|
+
* >
|
|
483
|
+
* {render(field)}
|
|
484
|
+
* </Field>
|
|
485
|
+
* );
|
|
486
|
+
* }
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
export const WithReactHookForm: Story = {
|
|
490
|
+
render: () => {
|
|
491
|
+
const form = useRHFForm(FormSchema, {
|
|
492
|
+
defaultValues: { email: "", password: "" },
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<RHFForm
|
|
497
|
+
form={form}
|
|
498
|
+
onSubmit={() => {}}
|
|
499
|
+
className="flex w-80 flex-col gap-4"
|
|
500
|
+
>
|
|
501
|
+
<RHFField
|
|
502
|
+
name="email"
|
|
503
|
+
label="Email"
|
|
504
|
+
description="We'll never share your email."
|
|
505
|
+
render={(field) => (
|
|
506
|
+
<Input {...field} type="email" placeholder="you@example.com" />
|
|
507
|
+
)}
|
|
508
|
+
/>
|
|
509
|
+
|
|
510
|
+
<RHFField
|
|
511
|
+
name="password"
|
|
512
|
+
label="Password"
|
|
513
|
+
render={(field) => (
|
|
514
|
+
<PasswordRoot {...field} placeholder="Enter your password" />
|
|
515
|
+
)}
|
|
516
|
+
/>
|
|
517
|
+
|
|
518
|
+
{form.formState.isSubmitSuccessful && (
|
|
519
|
+
<p className="text-sm text-green-600">Account created!</p>
|
|
520
|
+
)}
|
|
521
|
+
|
|
522
|
+
<Button type="submit">Create account</Button>
|
|
523
|
+
</RHFForm>
|
|
524
|
+
);
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// ── RHF helpers (not exported by the library — copy into your project) ────────
|
|
529
|
+
|
|
530
|
+
const FormSchema = z.object({
|
|
531
|
+
email: z
|
|
532
|
+
.string()
|
|
533
|
+
.min(1, "Email is required.")
|
|
534
|
+
.email("Enter a valid email address."),
|
|
535
|
+
password: z.string().min(1, "Password is required."),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
function useRHFForm<T extends z.ZodType<FieldValues, FieldValues>>(
|
|
539
|
+
schema: T,
|
|
540
|
+
options?: Omit<UseFormProps<z.infer<T>>, "resolver">,
|
|
541
|
+
) {
|
|
542
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
543
|
+
return useForm<z.infer<T>>({
|
|
544
|
+
resolver: zodResolver(schema as any),
|
|
545
|
+
...options,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
type RHFFormProps<T extends FieldValues> = {
|
|
550
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
551
|
+
form: UseFormReturn<T, any, any>;
|
|
552
|
+
onSubmit: SubmitHandler<T>;
|
|
553
|
+
children: ReactNode;
|
|
554
|
+
className?: string;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
function RHFForm<T extends FieldValues>({
|
|
558
|
+
form,
|
|
559
|
+
onSubmit,
|
|
560
|
+
children,
|
|
561
|
+
...props
|
|
562
|
+
}: RHFFormProps<T>) {
|
|
563
|
+
return (
|
|
564
|
+
<Form
|
|
565
|
+
// onSubmit={(data, event) => form.handleSubmit(onSubmit)(data, event)}
|
|
566
|
+
{...props}
|
|
567
|
+
>
|
|
568
|
+
{children}
|
|
569
|
+
</Form>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
interface RHFFieldProps {
|
|
574
|
+
name: string;
|
|
575
|
+
label?: ReactNode;
|
|
576
|
+
description?: ReactNode;
|
|
577
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
578
|
+
render: (field: any) => ReactNode;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function RHFField({ name, label, description, render }: RHFFieldProps) {
|
|
582
|
+
const { field, fieldState } = useController({ name });
|
|
583
|
+
return (
|
|
584
|
+
<Field
|
|
585
|
+
name={name}
|
|
586
|
+
label={label}
|
|
587
|
+
description={description}
|
|
588
|
+
invalid={fieldState.invalid}
|
|
589
|
+
error={fieldState.error?.message}
|
|
590
|
+
>
|
|
591
|
+
{render(field)}
|
|
592
|
+
</Field>
|
|
593
|
+
);
|
|
594
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Form as FormPrimitive } from "@base-ui/react/form";
|
|
2
|
+
import type React from "react";
|
|
3
|
+
|
|
4
|
+
type BaseProps = FormPrimitive.Props;
|
|
5
|
+
|
|
6
|
+
export interface FormProps
|
|
7
|
+
extends Omit<BaseProps, "onSubmit" | "onFormSubmit"> {
|
|
8
|
+
onSubmit?: BaseProps["onFormSubmit"];
|
|
9
|
+
onFormSubmit?: BaseProps["onFormSubmit"];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Form({
|
|
13
|
+
onSubmit,
|
|
14
|
+
onFormSubmit,
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}: FormProps): React.ReactElement {
|
|
18
|
+
const handleSubmit = onSubmit ?? onFormSubmit;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<FormPrimitive
|
|
22
|
+
className={className}
|
|
23
|
+
onFormSubmit={handleSubmit}
|
|
24
|
+
data-slot="form"
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { FormPrimitive };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./form";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
2
|
+
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { Heading } from "../../components";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Heading> = {
|
|
6
|
+
title: "typography/Heading",
|
|
7
|
+
component: Heading,
|
|
8
|
+
args: {
|
|
9
|
+
children: faker.lorem.sentence(),
|
|
10
|
+
},
|
|
11
|
+
argTypes: {
|
|
12
|
+
children: {
|
|
13
|
+
control: false,
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
options: ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl"],
|
|
17
|
+
control: { type: "select" },
|
|
18
|
+
defaultValue: "xl",
|
|
19
|
+
description: "The font size of the heading. xl is the default.",
|
|
20
|
+
},
|
|
21
|
+
weight: {
|
|
22
|
+
options: ["normal", "medium", "semibold", "bold"],
|
|
23
|
+
control: { type: "select" },
|
|
24
|
+
defaultValue: "semibold",
|
|
25
|
+
description: "The font weight of the heading. semibold is the default.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
|
|
32
|
+
type Story = StoryObj<typeof Heading>;
|
|
33
|
+
|
|
34
|
+
export const Default: Story = {};
|
|
35
|
+
|
|
36
|
+
export const Sizes: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<>
|
|
39
|
+
<Heading size="sm">Heading sm (14px)</Heading>
|
|
40
|
+
<Heading size="md">Heading md (16px)</Heading>
|
|
41
|
+
<Heading size="lg">Heading lg (18px)</Heading>
|
|
42
|
+
<Heading size="xl">Heading xl (20px)</Heading>
|
|
43
|
+
<Heading size="2xl">Heading 2xl (24px)</Heading>
|
|
44
|
+
<Heading size="3xl">Heading 3xl (28px)</Heading>
|
|
45
|
+
<Heading size="4xl">Heading 4xl (32px)</Heading>
|
|
46
|
+
<Heading size="5xl">Heading 5xl (36px)</Heading>
|
|
47
|
+
<Heading size="6xl">Heading 6xl (40px)</Heading>
|
|
48
|
+
</>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Weights: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<>
|
|
55
|
+
<Heading weight="normal">Heading normal</Heading>
|
|
56
|
+
<Heading weight="medium">Heading medium</Heading>
|
|
57
|
+
<Heading weight="semibold">Heading semibold</Heading>
|
|
58
|
+
<Heading weight="bold">Heading bold</Heading>
|
|
59
|
+
</>
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const As: Story = {
|
|
64
|
+
render: () => (
|
|
65
|
+
<>
|
|
66
|
+
<Heading as="h1">Heading as h1</Heading>
|
|
67
|
+
<Heading as="h2">Heading as h2</Heading>
|
|
68
|
+
<Heading as="h3">Heading as h3</Heading>
|
|
69
|
+
<Heading as="h4">Heading as h4</Heading>
|
|
70
|
+
<Heading as="h5">Heading as h5</Heading>
|
|
71
|
+
<Heading as="h6">Heading as h6</Heading>
|
|
72
|
+
</>
|
|
73
|
+
),
|
|
74
|
+
};
|