@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,81 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Inbox, Phone } from "lucide-react";
|
|
3
|
+
import { Stack } from "../stack";
|
|
4
|
+
import { Center } from ".";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Layout primitive that centers its children both horizontally and vertically using flexbox.
|
|
8
|
+
* Supports polymorphic rendering via `as` and an `inline` mode for `inline-flex` contexts.
|
|
9
|
+
*/
|
|
10
|
+
const meta: Meta<typeof Center> = {
|
|
11
|
+
title: "Components/Center",
|
|
12
|
+
component: Center,
|
|
13
|
+
argTypes: {
|
|
14
|
+
children: { control: false },
|
|
15
|
+
className: { control: false },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof Center>;
|
|
21
|
+
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<Center className="h-[300px] w-full border rounded-lg border-dashed">
|
|
25
|
+
<Stack
|
|
26
|
+
direction="vertical"
|
|
27
|
+
align="center"
|
|
28
|
+
gap={8}
|
|
29
|
+
className="text-muted-foreground"
|
|
30
|
+
>
|
|
31
|
+
<Inbox className="size-10 stroke-1" />
|
|
32
|
+
<span className="text-sm font-medium">No results found</span>
|
|
33
|
+
<span className="text-xs">Try adjusting your search or filters</span>
|
|
34
|
+
</Stack>
|
|
35
|
+
</Center>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Icon: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<Center className="w-fit p-4 bg-primary text-white rounded-full">
|
|
42
|
+
<Phone />
|
|
43
|
+
</Center>
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `inline` switches the display to `inline-flex`, allowing the Center to sit inline with text
|
|
49
|
+
* or other inline elements without breaking the flow.
|
|
50
|
+
*/
|
|
51
|
+
export const Inline: Story = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<p className="text-sm">
|
|
54
|
+
Text before{" "}
|
|
55
|
+
<Center
|
|
56
|
+
inline
|
|
57
|
+
className="w-8 h-8 bg-primary text-white rounded-full text-xs"
|
|
58
|
+
>
|
|
59
|
+
42
|
|
60
|
+
</Center>{" "}
|
|
61
|
+
text after
|
|
62
|
+
</p>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `as` renders the Center as a different HTML element for semantic markup.
|
|
68
|
+
* Common use cases: `main` for page root, `section` for content areas.
|
|
69
|
+
*/
|
|
70
|
+
export const As: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<Center
|
|
73
|
+
as="main"
|
|
74
|
+
className="bg-secondary w-[400px] aspect-square rounded p-4"
|
|
75
|
+
>
|
|
76
|
+
<div className="bg-primary text-primary-foreground rounded p-4">
|
|
77
|
+
Rendered as <main>
|
|
78
|
+
</div>
|
|
79
|
+
</Center>
|
|
80
|
+
),
|
|
81
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ComponentProps, ElementType } from "react";
|
|
2
|
+
import { cn } from "../../lib";
|
|
3
|
+
|
|
4
|
+
interface CenterProps extends ComponentProps<"div"> {
|
|
5
|
+
as?: ElementType;
|
|
6
|
+
inline?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Center = ({
|
|
10
|
+
as: Tag = "div",
|
|
11
|
+
inline = false,
|
|
12
|
+
className,
|
|
13
|
+
...props
|
|
14
|
+
}: CenterProps) => (
|
|
15
|
+
<Tag
|
|
16
|
+
className={cn(
|
|
17
|
+
inline ? "inline-flex" : "flex",
|
|
18
|
+
"items-center justify-center",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
data-slot="center"
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./center";
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { action } from "storybook/actions";
|
|
4
|
+
import { Button } from "../../components";
|
|
5
|
+
import { useSelection } from "../../hooks";
|
|
6
|
+
import { Checkbox, CheckboxIndicator, CheckboxRoot } from "./checkbox";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checkbox built on Base UI. Handles checked, unchecked, and indeterminate
|
|
10
|
+
* states natively. Built on `@base-ui/react/checkbox`.
|
|
11
|
+
*
|
|
12
|
+
* Compound API:
|
|
13
|
+
* - `Checkbox` — single checkbox, renders the control only
|
|
14
|
+
* - `Checkbox.Item` — labeled checkbox for use inside `Checkbox.Group`
|
|
15
|
+
* - `Checkbox.Group` — group wrapper with value management and a11y fieldset
|
|
16
|
+
* - `Checkbox.Legend` — composable legend for `Checkbox.Group`
|
|
17
|
+
*
|
|
18
|
+
* `controlFirst` (default `true`) on `Checkbox.Group` places the checkbox before
|
|
19
|
+
* the label on all `Checkbox.Item` children via context.
|
|
20
|
+
*
|
|
21
|
+
* When `Checkbox.Group` has an `error`, all `Checkbox.Item` children
|
|
22
|
+
* automatically receive `aria-invalid` so their border turns red.
|
|
23
|
+
*
|
|
24
|
+
* Reference: [Checkbox – Base UI](https://base-ui.com/react/components/checkbox)
|
|
25
|
+
*/
|
|
26
|
+
const meta: Meta<typeof Checkbox> = {
|
|
27
|
+
title: "Components/Checkbox",
|
|
28
|
+
component: Checkbox,
|
|
29
|
+
parameters: { layout: "centered" },
|
|
30
|
+
args: {
|
|
31
|
+
name: "checkbox",
|
|
32
|
+
},
|
|
33
|
+
argTypes: {
|
|
34
|
+
onCheckedChange: { control: false },
|
|
35
|
+
indicatorProps: { control: false },
|
|
36
|
+
tooltip: { control: false },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof Checkbox>;
|
|
42
|
+
|
|
43
|
+
export const Default: Story = {};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pass `label` for a linked `<label>` element. `children` works as an alias —
|
|
47
|
+
* `label` takes priority when both are provided.
|
|
48
|
+
*/
|
|
49
|
+
export const WithLabel: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
label: "Receive marketing emails",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Pass `children` as an alternative to `label`.
|
|
57
|
+
*/
|
|
58
|
+
export const WithChildren: Story = {
|
|
59
|
+
render: () => <Checkbox>I accept the terms and conditions</Checkbox>,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `required` shows a `*` in the label. `tooltip` renders an info icon next
|
|
64
|
+
* to the label that opens a tooltip on hover/focus.
|
|
65
|
+
*/
|
|
66
|
+
export const WithRequired: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
label: "I accept the terms and conditions",
|
|
69
|
+
required: true,
|
|
70
|
+
tooltip: "You must accept the terms to continue.",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `controlFirst={false}` places the label before the checkbox.
|
|
76
|
+
* Default is `true` (checkbox → label).
|
|
77
|
+
*/
|
|
78
|
+
export const ControlFirst: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
controlFirst: false,
|
|
81
|
+
label: "I accept the terms and conditions",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Disabled: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
disabled: true,
|
|
88
|
+
defaultChecked: true,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `aria-invalid="true"` applies error border and ring — no Field wrapper needed.
|
|
94
|
+
*/
|
|
95
|
+
export const Invalid: Story = {
|
|
96
|
+
args: { "aria-invalid": "true" },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Use `checked` + `onCheckedChange` for controlled mode.
|
|
101
|
+
*/
|
|
102
|
+
export const Controlled: Story = {
|
|
103
|
+
render: () => {
|
|
104
|
+
const [checked, setChecked] = useState(true);
|
|
105
|
+
return <Checkbox checked={checked} onCheckedChange={setChecked} />;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `indeterminate` is a first-class prop — Base UI sets `aria-checked="mixed"`
|
|
111
|
+
* and manages the visual state automatically. Clicking an indeterminate
|
|
112
|
+
* checkbox moves it to checked.
|
|
113
|
+
*/
|
|
114
|
+
export const Indeterminate: Story = {
|
|
115
|
+
render: () => {
|
|
116
|
+
const ITEMS = [1, 2, 3, 4];
|
|
117
|
+
const { selected, toggle, toggleAll, isAllSelected, isSomeSelected } =
|
|
118
|
+
useSelection(ITEMS);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<Checkbox
|
|
123
|
+
checked={isAllSelected}
|
|
124
|
+
indeterminate={isSomeSelected}
|
|
125
|
+
onCheckedChange={() => toggleAll()}
|
|
126
|
+
>
|
|
127
|
+
Select all
|
|
128
|
+
</Checkbox>
|
|
129
|
+
<div className="ml-6 space-y-2 border-l-2 pl-4">
|
|
130
|
+
{ITEMS.map((item) => (
|
|
131
|
+
<Checkbox
|
|
132
|
+
key={item}
|
|
133
|
+
checked={selected.includes(item)}
|
|
134
|
+
onCheckedChange={() => toggle(item)}
|
|
135
|
+
>
|
|
136
|
+
Option {item}
|
|
137
|
+
</Checkbox>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Base UI injects a hidden `<input type="checkbox">` so `name` and `required`
|
|
147
|
+
* work with native `FormData` out of the box.
|
|
148
|
+
*/
|
|
149
|
+
export const Required: Story = {
|
|
150
|
+
args: {
|
|
151
|
+
name: "terms",
|
|
152
|
+
required: true,
|
|
153
|
+
label: "I accept the terms and conditions",
|
|
154
|
+
},
|
|
155
|
+
render: (args) => (
|
|
156
|
+
<form
|
|
157
|
+
className="space-y-3"
|
|
158
|
+
onSubmit={(event) => {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
const data = new FormData(event.currentTarget);
|
|
161
|
+
action("submit")(Object.fromEntries(data));
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<Checkbox {...args} />
|
|
165
|
+
<Button type="submit">Submit</Button>
|
|
166
|
+
</form>
|
|
167
|
+
),
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `Checkbox.Group` manages selected values and provides an accessible
|
|
172
|
+
* `<fieldset>` wrapper. Each `Checkbox.Item` needs a `value` prop to
|
|
173
|
+
* participate in the group's state.
|
|
174
|
+
*
|
|
175
|
+
* Use `defaultValue` for uncontrolled groups.
|
|
176
|
+
*/
|
|
177
|
+
export const Group: Story = {
|
|
178
|
+
render: () => (
|
|
179
|
+
<Checkbox.Group
|
|
180
|
+
legend="Notifications"
|
|
181
|
+
defaultValue={["email"]}
|
|
182
|
+
onChange={action("onChange")}
|
|
183
|
+
>
|
|
184
|
+
<Checkbox.Item value="email" label="Email" />
|
|
185
|
+
<Checkbox.Item value="sms" label="SMS" />
|
|
186
|
+
<Checkbox.Item value="push" label="Push notifications" />
|
|
187
|
+
</Checkbox.Group>
|
|
188
|
+
),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `error` takes visual priority over `description` — both props can coexist,
|
|
193
|
+
* but description only renders when there is no error. When `error` is set,
|
|
194
|
+
* all items in the group receive `aria-invalid` and their borders turn red.
|
|
195
|
+
*/
|
|
196
|
+
export const GroupWithError: Story = {
|
|
197
|
+
render: () => (
|
|
198
|
+
<Checkbox.Group
|
|
199
|
+
legend="Preferences"
|
|
200
|
+
description="Select at least one option."
|
|
201
|
+
error="You must select at least one option."
|
|
202
|
+
>
|
|
203
|
+
<Checkbox.Item value="email" label="Email" />
|
|
204
|
+
<Checkbox.Item value="sms" label="SMS" />
|
|
205
|
+
</Checkbox.Group>
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* `allValues` enables the select-all pattern: Base UI derives the indeterminate
|
|
211
|
+
* state automatically when only some items are checked. The parent checkbox
|
|
212
|
+
* controls the entire group when clicked.
|
|
213
|
+
*/
|
|
214
|
+
export const GroupParent: Story = {
|
|
215
|
+
render: () => {
|
|
216
|
+
const ALL = ["email", "sms", "push"];
|
|
217
|
+
const [value, setValue] = useState(["email"]);
|
|
218
|
+
|
|
219
|
+
const isAll = value.length === ALL.length;
|
|
220
|
+
const isSome = value.length > 0 && !isAll;
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="space-y-2">
|
|
224
|
+
<Checkbox
|
|
225
|
+
checked={isAll}
|
|
226
|
+
indeterminate={isSome}
|
|
227
|
+
onCheckedChange={(checked) => setValue(checked ? ALL : [])}
|
|
228
|
+
/>
|
|
229
|
+
<div className="ml-6 border-l-2 pl-4">
|
|
230
|
+
<Checkbox.Group value={value} onChange={setValue} allValues={ALL}>
|
|
231
|
+
<Checkbox.Item value="email" label="Email" />
|
|
232
|
+
<Checkbox.Item value="sms" label="SMS" />
|
|
233
|
+
<Checkbox.Item value="push" label="Push notifications" />
|
|
234
|
+
</Checkbox.Group>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Use `value` + `onChange` for a controlled group.
|
|
243
|
+
*/
|
|
244
|
+
export const GroupControlled: Story = {
|
|
245
|
+
render: () => {
|
|
246
|
+
const [value, setValue] = useState<string[]>(["email"]);
|
|
247
|
+
return (
|
|
248
|
+
<Checkbox.Group
|
|
249
|
+
legend="Contact channel"
|
|
250
|
+
value={value}
|
|
251
|
+
onChange={setValue}
|
|
252
|
+
>
|
|
253
|
+
<Checkbox.Item value="email" label="Email" />
|
|
254
|
+
<Checkbox.Item value="sms" label="SMS" />
|
|
255
|
+
<Checkbox.Item value="push" label="Push notifications" />
|
|
256
|
+
</Checkbox.Group>
|
|
257
|
+
);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `Checkbox.Legend` can be placed as a direct child of `Checkbox.Group`
|
|
263
|
+
* instead of the `legend` prop when you need custom styling or want to
|
|
264
|
+
* visually hide it with `className="sr-only"`.
|
|
265
|
+
*
|
|
266
|
+
* ```tsx
|
|
267
|
+
* <Checkbox.Group>
|
|
268
|
+
* <Checkbox.Legend className="sr-only">Preferences</Checkbox.Legend>
|
|
269
|
+
* <Checkbox.Item value="a" label="Option A" />
|
|
270
|
+
* </Checkbox.Group>
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export const ComposableLegend: Story = {
|
|
274
|
+
render: () => (
|
|
275
|
+
<Checkbox.Group>
|
|
276
|
+
<Checkbox.Legend className="text-base font-semibold">
|
|
277
|
+
Contact preferences
|
|
278
|
+
</Checkbox.Legend>
|
|
279
|
+
<Checkbox.Item value="email" label="Email" />
|
|
280
|
+
<Checkbox.Item value="sms" label="SMS" />
|
|
281
|
+
</Checkbox.Group>
|
|
282
|
+
),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Direct composition with primitives for full structural control.
|
|
287
|
+
* Use when the composite props are not enough — custom ordering,
|
|
288
|
+
* extra elements between parts, or non-standard layouts.
|
|
289
|
+
*
|
|
290
|
+
* ```tsx
|
|
291
|
+
* <CheckboxRoot id="custom">
|
|
292
|
+
* <CheckboxIndicator />
|
|
293
|
+
* </CheckboxRoot>
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export const Primitive: Story = {
|
|
297
|
+
render: () => (
|
|
298
|
+
<div className="flex items-center gap-2">
|
|
299
|
+
<CheckboxRoot id="primitive-example">
|
|
300
|
+
<CheckboxIndicator />
|
|
301
|
+
</CheckboxRoot>
|
|
302
|
+
<label htmlFor="primitive-example" className="text-sm select-none">
|
|
303
|
+
Manual composition
|
|
304
|
+
</label>
|
|
305
|
+
</div>
|
|
306
|
+
),
|
|
307
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
|
|
2
|
+
import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
|
|
3
|
+
import { Field as FieldBase } from "@base-ui/react/field";
|
|
4
|
+
import { Fieldset } from "@base-ui/react/fieldset";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { createContext, type ReactNode, useContext, useId } from "react";
|
|
7
|
+
import { cn } from "../../lib";
|
|
8
|
+
import { Label } from "../label";
|
|
9
|
+
|
|
10
|
+
// ── Primitives ─────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export function CheckboxRoot({
|
|
13
|
+
className,
|
|
14
|
+
...props
|
|
15
|
+
}: CheckboxPrimitive.Root.Props): React.ReactElement {
|
|
16
|
+
return (
|
|
17
|
+
<CheckboxPrimitive.Root
|
|
18
|
+
className={cn(
|
|
19
|
+
"peer relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-[.25rem] border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none ring-ring transition-shadow",
|
|
20
|
+
"before:pointer-events-none before:absolute before:inset-0 before:rounded-[3px]",
|
|
21
|
+
"not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)]",
|
|
22
|
+
"focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
23
|
+
"aria-invalid:border-error focus-visible:aria-invalid:border-error focus-visible:aria-invalid:ring-error",
|
|
24
|
+
"data-disabled:cursor-not-allowed data-disabled:opacity-64",
|
|
25
|
+
"sm:size-4",
|
|
26
|
+
"dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
|
|
27
|
+
"[[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
data-slot="checkbox"
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function CheckboxIndicator({
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: CheckboxPrimitive.Indicator.Props): React.ReactElement {
|
|
40
|
+
return (
|
|
41
|
+
<CheckboxPrimitive.Indicator
|
|
42
|
+
className={cn(
|
|
43
|
+
"absolute -inset-px flex items-center justify-center rounded-[.25rem] text-primary-foreground",
|
|
44
|
+
"data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
data-slot="checkbox-indicator"
|
|
48
|
+
render={(
|
|
49
|
+
renderProps: React.ComponentProps<"span">,
|
|
50
|
+
state: CheckboxPrimitive.Indicator.State,
|
|
51
|
+
) => (
|
|
52
|
+
<span {...renderProps}>
|
|
53
|
+
{state.indeterminate ? (
|
|
54
|
+
<svg
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
className="size-3.5 sm:size-3"
|
|
57
|
+
fill="none"
|
|
58
|
+
height="24"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
strokeWidth="3"
|
|
63
|
+
viewBox="0 0 24 24"
|
|
64
|
+
width="24"
|
|
65
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
>
|
|
67
|
+
<path d="M5.252 12h13.496" />
|
|
68
|
+
</svg>
|
|
69
|
+
) : (
|
|
70
|
+
<svg
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
className="size-3.5 sm:size-3"
|
|
73
|
+
fill="none"
|
|
74
|
+
height="24"
|
|
75
|
+
stroke="currentColor"
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
strokeWidth="3"
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
width="24"
|
|
81
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
82
|
+
>
|
|
83
|
+
<path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
|
|
84
|
+
</svg>
|
|
85
|
+
)}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Group context ──────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
const CheckboxGroupContext = createContext<{
|
|
96
|
+
controlFirst: boolean;
|
|
97
|
+
invalid: boolean;
|
|
98
|
+
}>({
|
|
99
|
+
controlFirst: true,
|
|
100
|
+
invalid: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── Composite: Checkbox (single) ───────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
interface CheckboxProps extends Omit<CheckboxPrimitive.Root.Props, "children"> {
|
|
106
|
+
children?: ReactNode;
|
|
107
|
+
label?: ReactNode;
|
|
108
|
+
tooltip?: ReactNode;
|
|
109
|
+
indicatorProps?: CheckboxPrimitive.Indicator.Props;
|
|
110
|
+
controlFirst?: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function CheckboxSingle({
|
|
114
|
+
className,
|
|
115
|
+
indicatorProps,
|
|
116
|
+
label,
|
|
117
|
+
children,
|
|
118
|
+
tooltip,
|
|
119
|
+
controlFirst = true,
|
|
120
|
+
required,
|
|
121
|
+
id,
|
|
122
|
+
...props
|
|
123
|
+
}: CheckboxProps): React.ReactElement {
|
|
124
|
+
const generatedId = useId();
|
|
125
|
+
const idToUse = id ?? generatedId;
|
|
126
|
+
const labelContent = label ?? children;
|
|
127
|
+
|
|
128
|
+
const checkbox = (
|
|
129
|
+
<CheckboxRoot
|
|
130
|
+
id={idToUse}
|
|
131
|
+
className={className}
|
|
132
|
+
required={required}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
<CheckboxIndicator {...indicatorProps} />
|
|
136
|
+
</CheckboxRoot>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (labelContent !== undefined) {
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
className={cn(
|
|
143
|
+
"flex select-none items-center gap-x-2",
|
|
144
|
+
!controlFirst && "flex-row-reverse justify-end",
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
{checkbox}
|
|
148
|
+
<Label htmlFor={idToUse} required={required} tooltip={tooltip}>
|
|
149
|
+
{labelContent}
|
|
150
|
+
</Label>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return checkbox;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Composite: Checkbox.Item (for use in Checkbox.Group) ──────────────────────
|
|
159
|
+
|
|
160
|
+
export interface CheckboxItemProps
|
|
161
|
+
extends Omit<CheckboxPrimitive.Root.Props, "children"> {
|
|
162
|
+
label: ReactNode;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function CheckboxItem({
|
|
166
|
+
className,
|
|
167
|
+
label,
|
|
168
|
+
id,
|
|
169
|
+
...props
|
|
170
|
+
}: CheckboxItemProps): React.ReactElement {
|
|
171
|
+
const { controlFirst, invalid } = useContext(CheckboxGroupContext);
|
|
172
|
+
const generatedId = useId();
|
|
173
|
+
const idToUse = id ?? generatedId;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<FieldBase.Root invalid={invalid}>
|
|
177
|
+
<div
|
|
178
|
+
className={cn(
|
|
179
|
+
"flex select-none items-center gap-x-2",
|
|
180
|
+
!controlFirst && "flex-row-reverse justify-end",
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<CheckboxRoot id={idToUse} className={className} {...props}>
|
|
184
|
+
<CheckboxIndicator />
|
|
185
|
+
</CheckboxRoot>
|
|
186
|
+
<Label htmlFor={idToUse}>{label}</Label>
|
|
187
|
+
</div>
|
|
188
|
+
</FieldBase.Root>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Composite: Checkbox.Legend ─────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export interface CheckboxLegendProps {
|
|
195
|
+
children: ReactNode;
|
|
196
|
+
className?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function CheckboxLegend({
|
|
200
|
+
children,
|
|
201
|
+
className,
|
|
202
|
+
}: CheckboxLegendProps): React.ReactElement {
|
|
203
|
+
return (
|
|
204
|
+
<Fieldset.Legend
|
|
205
|
+
className={cn("text-sm font-medium leading-none", className)}
|
|
206
|
+
>
|
|
207
|
+
{children}
|
|
208
|
+
</Fieldset.Legend>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Composite: Checkbox.Group ──────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
export interface CheckboxGroupProps {
|
|
215
|
+
legend?: ReactNode;
|
|
216
|
+
children: ReactNode;
|
|
217
|
+
error?: string;
|
|
218
|
+
description?: ReactNode;
|
|
219
|
+
defaultValue?: string[];
|
|
220
|
+
value?: string[];
|
|
221
|
+
onChange?: (value: string[]) => void;
|
|
222
|
+
allValues?: string[];
|
|
223
|
+
disabled?: boolean;
|
|
224
|
+
controlFirst?: boolean;
|
|
225
|
+
className?: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function CheckboxGroup({
|
|
229
|
+
legend,
|
|
230
|
+
children,
|
|
231
|
+
error,
|
|
232
|
+
description,
|
|
233
|
+
defaultValue,
|
|
234
|
+
value,
|
|
235
|
+
onChange,
|
|
236
|
+
allValues,
|
|
237
|
+
disabled,
|
|
238
|
+
controlFirst = true,
|
|
239
|
+
className,
|
|
240
|
+
}: CheckboxGroupProps): React.ReactElement {
|
|
241
|
+
return (
|
|
242
|
+
<CheckboxGroupContext.Provider value={{ controlFirst, invalid: !!error }}>
|
|
243
|
+
<CheckboxGroupPrimitive
|
|
244
|
+
defaultValue={defaultValue}
|
|
245
|
+
value={value}
|
|
246
|
+
onValueChange={onChange}
|
|
247
|
+
allValues={allValues}
|
|
248
|
+
disabled={disabled}
|
|
249
|
+
>
|
|
250
|
+
<Fieldset.Root className={cn("flex flex-col gap-3", className)}>
|
|
251
|
+
{legend && <CheckboxLegend>{legend}</CheckboxLegend>}
|
|
252
|
+
<div className="flex flex-col gap-2">{children}</div>
|
|
253
|
+
{error && <p className="text-sm font-medium text-error">{error}</p>}
|
|
254
|
+
{!error && description && (
|
|
255
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
256
|
+
)}
|
|
257
|
+
</Fieldset.Root>
|
|
258
|
+
</CheckboxGroupPrimitive>
|
|
259
|
+
</CheckboxGroupContext.Provider>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Compound export ────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
export const Checkbox = Object.assign(CheckboxSingle, {
|
|
266
|
+
Item: CheckboxItem,
|
|
267
|
+
Group: CheckboxGroup,
|
|
268
|
+
Legend: CheckboxLegend,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── Primitive escape hatch ─────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export { CheckboxPrimitive };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./checkbox";
|