@bearmenu/ui 0.1.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/{chunk-2CUFS5LF.js → chunk-2BMJRXQX.js} +1 -1
  2. package/dist/chunk-HLQ2IRE7.js +279 -0
  3. package/dist/chunk-IQASCH2V.js +25 -0
  4. package/dist/{chunk-UXAYXRFN.js → chunk-QJ54Z7TC.js} +15 -1
  5. package/dist/{chunk-X6GATUI2.js → chunk-UISA4CIG.js} +1 -0
  6. package/dist/components/alert-dialog.js +1 -1
  7. package/dist/components/alert.d.ts +1 -1
  8. package/dist/components/badge.d.ts +1 -1
  9. package/dist/components/button.d.ts +1 -1
  10. package/dist/components/button.js +1 -1
  11. package/dist/components/calendar.d.ts +9 -7
  12. package/dist/components/calendar.js +153 -39
  13. package/dist/components/card.d.ts +2 -1
  14. package/dist/components/card.js +1 -1
  15. package/dist/components/carousel.js +1 -1
  16. package/dist/components/checkbox.js +3 -25
  17. package/dist/components/command.d.ts +13 -13
  18. package/dist/components/dropdown-menu.js +1 -1
  19. package/dist/components/empty-state.js +1 -1
  20. package/dist/components/haptic-button.js +1 -1
  21. package/dist/components/link-button.js +1 -1
  22. package/dist/components/multi-select-combobox.js +1 -1
  23. package/dist/components/multi-select-menu.d.ts +26 -0
  24. package/dist/components/multi-select-menu.js +71 -0
  25. package/dist/components/pagination.js +1 -1
  26. package/dist/components/phone-frame.d.ts +17 -0
  27. package/dist/components/phone-frame.js +41 -0
  28. package/dist/components/place-about-tab.d.ts +17 -0
  29. package/dist/components/place-about-tab.js +6 -0
  30. package/dist/components/place-card.d.ts +17 -0
  31. package/dist/components/place-card.js +145 -0
  32. package/dist/components/place-details-mobile.d.ts +18 -0
  33. package/dist/components/place-details-mobile.js +187 -0
  34. package/dist/components/place-types.d.ts +145 -0
  35. package/dist/components/place-types.js +1 -0
  36. package/dist/components/search-bar.js +1 -1
  37. package/dist/components/searchable-select.js +1 -1
  38. package/dist/components/skeleton-card.js +1 -1
  39. package/dist/components/skeleton.js +1 -1
  40. package/dist/components/sliding-panels.js +1 -1
  41. package/dist/components/sonner.js +11 -2
  42. package/dist/components/tabs.js +1 -1
  43. package/dist/components/text.d.ts +3 -3
  44. package/dist/components/toaster.js +1 -1
  45. package/package.json +28 -9
  46. package/src/components/accordion.stories.tsx +51 -0
  47. package/src/components/alert-dialog.stories.tsx +45 -0
  48. package/src/components/alert.stories.tsx +85 -0
  49. package/src/components/aspect-ratio.stories.tsx +47 -0
  50. package/src/components/avatar.stories.tsx +66 -0
  51. package/src/components/badge.stories.tsx +60 -0
  52. package/src/components/breadcrumb.stories.tsx +63 -0
  53. package/src/components/button.stories.tsx +115 -0
  54. package/src/components/button.tsx +1 -1
  55. package/src/components/calendar.stories.tsx +47 -0
  56. package/src/components/calendar.tsx +160 -40
  57. package/src/components/card.stories.tsx +72 -0
  58. package/src/components/card.tsx +13 -1
  59. package/src/components/carousel.stories.tsx +62 -0
  60. package/src/components/checkbox.stories.tsx +54 -0
  61. package/src/components/checkbox.tsx +1 -1
  62. package/src/components/collapsible.stories.tsx +34 -0
  63. package/src/components/command.stories.tsx +64 -0
  64. package/src/components/currency-input.stories.tsx +50 -0
  65. package/src/components/dialog.stories.tsx +75 -0
  66. package/src/components/drawer.stories.tsx +47 -0
  67. package/src/components/dropdown-menu.stories.tsx +64 -0
  68. package/src/components/dropdown-menu.tsx +1 -1
  69. package/src/components/empty-state.stories.tsx +67 -0
  70. package/src/components/form.stories.tsx +73 -0
  71. package/src/components/haptic-button.stories.tsx +41 -0
  72. package/src/components/input-otp.stories.tsx +42 -0
  73. package/src/components/input.stories.tsx +41 -0
  74. package/src/components/label.stories.tsx +35 -0
  75. package/src/components/markdown-renderer.stories.tsx +45 -0
  76. package/src/components/multi-select-combobox.stories.tsx +78 -0
  77. package/src/components/multi-select-menu.tsx +96 -0
  78. package/src/components/pagination.stories.tsx +48 -0
  79. package/src/components/phone-frame.stories.tsx +51 -0
  80. package/src/components/phone-frame.test.tsx +82 -0
  81. package/src/components/phone-frame.tsx +59 -0
  82. package/src/components/place-about-tab.stories.tsx +164 -0
  83. package/src/components/place-about-tab.test.tsx +325 -0
  84. package/src/components/place-about-tab.tsx +474 -0
  85. package/src/components/place-card.stories.tsx +120 -0
  86. package/src/components/place-card.test.tsx +187 -0
  87. package/src/components/place-card.tsx +187 -0
  88. package/src/components/place-details-mobile.tsx +251 -0
  89. package/src/components/place-types.ts +127 -0
  90. package/src/components/popover.stories.tsx +42 -0
  91. package/src/components/progress.stories.tsx +35 -0
  92. package/src/components/radio-group.stories.tsx +46 -0
  93. package/src/components/rating.stories.tsx +45 -0
  94. package/src/components/scroll-area.stories.tsx +48 -0
  95. package/src/components/search-bar.stories.tsx +92 -0
  96. package/src/components/searchable-select.stories.tsx +55 -0
  97. package/src/components/select.stories.tsx +92 -0
  98. package/src/components/separator.stories.tsx +33 -0
  99. package/src/components/sheet.stories.tsx +95 -0
  100. package/src/components/skeleton-card.stories.tsx +53 -0
  101. package/src/components/skeleton.stories.tsx +42 -0
  102. package/src/components/skeleton.tsx +1 -0
  103. package/src/components/slider.stories.tsx +27 -0
  104. package/src/components/sliding-panels.stories.tsx +68 -0
  105. package/src/components/sonner.tsx +17 -2
  106. package/src/components/sticky-container.stories.tsx +33 -0
  107. package/src/components/switch.stories.tsx +35 -0
  108. package/src/components/table.stories.tsx +60 -0
  109. package/src/components/tabs.stories.tsx +53 -0
  110. package/src/components/tabs.tsx +1 -1
  111. package/src/components/text.stories.tsx +82 -0
  112. package/src/components/textarea.stories.tsx +35 -0
  113. package/src/components/toast.stories.tsx +63 -0
  114. package/src/components/toggle-group.stories.tsx +60 -0
  115. package/src/components/toggle.stories.tsx +48 -0
  116. package/src/components/tooltip.stories.tsx +71 -0
  117. package/src/globals.css +21 -2
  118. package/src/test/setup.ts +9 -0
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ Drawer,
4
+ DrawerClose,
5
+ DrawerContent,
6
+ DrawerDescription,
7
+ DrawerFooter,
8
+ DrawerHeader,
9
+ DrawerTitle,
10
+ DrawerTrigger,
11
+ } from "./drawer";
12
+ import { Button } from "./button";
13
+
14
+ const meta = {
15
+ title: "Components/Drawer",
16
+ component: Drawer,
17
+ tags: ["autodocs"],
18
+ } satisfies Meta<typeof Drawer>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ export const Default: Story = {
24
+ render: () => (
25
+ <Drawer>
26
+ <DrawerTrigger asChild>
27
+ <Button variant="outline">Open Drawer</Button>
28
+ </DrawerTrigger>
29
+ <DrawerContent>
30
+ <DrawerHeader>
31
+ <DrawerTitle>Move Goal</DrawerTitle>
32
+ <DrawerDescription>Set your daily activity goal.</DrawerDescription>
33
+ </DrawerHeader>
34
+ <div className="p-4">
35
+ <p className="text-center text-4xl font-bold">350</p>
36
+ <p className="text-center text-muted-foreground">calories/day</p>
37
+ </div>
38
+ <DrawerFooter>
39
+ <Button>Submit</Button>
40
+ <DrawerClose asChild>
41
+ <Button variant="outline">Cancel</Button>
42
+ </DrawerClose>
43
+ </DrawerFooter>
44
+ </DrawerContent>
45
+ </Drawer>
46
+ ),
47
+ };
@@ -0,0 +1,64 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ DropdownMenu,
4
+ DropdownMenuContent,
5
+ DropdownMenuGroup,
6
+ DropdownMenuItem,
7
+ DropdownMenuLabel,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuShortcut,
10
+ DropdownMenuTrigger,
11
+ } from "./dropdown-menu";
12
+ import { Button } from "./button";
13
+ import { LogOut, Settings, User, CreditCard, Keyboard } from "lucide-react";
14
+
15
+ const meta = {
16
+ title: "Components/DropdownMenu",
17
+ component: DropdownMenu,
18
+ tags: ["autodocs"],
19
+ } satisfies Meta<typeof DropdownMenu>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ render: () => (
26
+ <DropdownMenu>
27
+ <DropdownMenuTrigger asChild>
28
+ <Button variant="outline">Open Menu</Button>
29
+ </DropdownMenuTrigger>
30
+ <DropdownMenuContent className="w-56">
31
+ <DropdownMenuLabel>My Account</DropdownMenuLabel>
32
+ <DropdownMenuSeparator />
33
+ <DropdownMenuGroup>
34
+ <DropdownMenuItem>
35
+ <User className="mr-2 h-4 w-4" />
36
+ <span>Profile</span>
37
+ <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
38
+ </DropdownMenuItem>
39
+ <DropdownMenuItem>
40
+ <CreditCard className="mr-2 h-4 w-4" />
41
+ <span>Billing</span>
42
+ <DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
43
+ </DropdownMenuItem>
44
+ <DropdownMenuItem>
45
+ <Settings className="mr-2 h-4 w-4" />
46
+ <span>Settings</span>
47
+ <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
48
+ </DropdownMenuItem>
49
+ <DropdownMenuItem>
50
+ <Keyboard className="mr-2 h-4 w-4" />
51
+ <span>Keyboard shortcuts</span>
52
+ <DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
53
+ </DropdownMenuItem>
54
+ </DropdownMenuGroup>
55
+ <DropdownMenuSeparator />
56
+ <DropdownMenuItem variant="destructive">
57
+ <LogOut className="mr-2 h-4 w-4" />
58
+ <span>Log out</span>
59
+ <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
60
+ </DropdownMenuItem>
61
+ </DropdownMenuContent>
62
+ </DropdownMenu>
63
+ ),
64
+ };
@@ -72,7 +72,7 @@ function DropdownMenuItem({
72
72
  function DropdownMenuCheckboxItem({
73
73
  className,
74
74
  children,
75
- checked,
75
+ checked = false,
76
76
  ...props
77
77
  }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
78
78
  return (
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { EmptyState, SearchEmptyState, ErrorEmptyState } from "./empty-state";
3
+
4
+ const meta = {
5
+ title: "Components/EmptyState",
6
+ component: EmptyState,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ variant: {
10
+ control: "select",
11
+ options: ["default", "search", "error"],
12
+ },
13
+ size: {
14
+ control: "select",
15
+ options: ["sm", "md", "lg"],
16
+ },
17
+ },
18
+ } satisfies Meta<typeof EmptyState>;
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof meta>;
22
+
23
+ export const Default: Story = {
24
+ args: {
25
+ title: "No items yet",
26
+ description: "Get started by creating your first item.",
27
+ action: { label: "Create item", onClick: () => {} },
28
+ },
29
+ };
30
+
31
+ export const Search: Story = {
32
+ render: () => (
33
+ <SearchEmptyState
34
+ title="No results found"
35
+ description="Try adjusting your search or filters to find what you're looking for."
36
+ action={{ label: "Clear filters", onClick: () => {} }}
37
+ />
38
+ ),
39
+ };
40
+
41
+ export const Error: Story = {
42
+ render: () => (
43
+ <ErrorEmptyState
44
+ title="Something went wrong"
45
+ description="We couldn't load the data. Please try again."
46
+ action={{ label: "Retry", onClick: () => {} }}
47
+ secondaryAction={{ label: "Go back", onClick: () => {} }}
48
+ />
49
+ ),
50
+ };
51
+
52
+ export const Small: Story = {
53
+ args: {
54
+ title: "No data",
55
+ description: "Nothing to show here.",
56
+ size: "sm",
57
+ },
58
+ };
59
+
60
+ export const Large: Story = {
61
+ args: {
62
+ title: "Welcome!",
63
+ description: "This is your dashboard. Start by adding some content.",
64
+ size: "lg",
65
+ action: { label: "Get started", onClick: () => {} },
66
+ },
67
+ };
@@ -0,0 +1,73 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useForm } from "react-hook-form";
3
+ import {
4
+ Form,
5
+ FormControl,
6
+ FormDescription,
7
+ FormField,
8
+ FormItem,
9
+ FormLabel,
10
+ FormMessage,
11
+ } from "./form";
12
+ import { Input } from "./input";
13
+ import { Button } from "./button";
14
+
15
+ const meta = {
16
+ title: "Components/Form",
17
+ component: Form,
18
+ tags: ["autodocs"],
19
+ } satisfies Meta<typeof Form>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ function FormDemo() {
25
+ const form = useForm({
26
+ defaultValues: {
27
+ username: "",
28
+ email: "",
29
+ },
30
+ });
31
+
32
+ return (
33
+ <Form {...form}>
34
+ <form onSubmit={form.handleSubmit(() => {})} className="space-y-6 w-[400px]">
35
+ <FormField
36
+ control={form.control}
37
+ name="username"
38
+ rules={{ required: "Username is required" }}
39
+ render={({ field }) => (
40
+ <FormItem>
41
+ <FormLabel>Username</FormLabel>
42
+ <FormControl>
43
+ <Input placeholder="johndoe" {...field} />
44
+ </FormControl>
45
+ <FormDescription>This is your public display name.</FormDescription>
46
+ <FormMessage />
47
+ </FormItem>
48
+ )}
49
+ />
50
+ <FormField
51
+ control={form.control}
52
+ name="email"
53
+ rules={{ required: "Email is required" }}
54
+ render={({ field }) => (
55
+ <FormItem>
56
+ <FormLabel>Email</FormLabel>
57
+ <FormControl>
58
+ <Input type="email" placeholder="john@example.com" {...field} />
59
+ </FormControl>
60
+ <FormDescription>We'll never share your email.</FormDescription>
61
+ <FormMessage />
62
+ </FormItem>
63
+ )}
64
+ />
65
+ <Button type="submit">Submit</Button>
66
+ </form>
67
+ </Form>
68
+ );
69
+ }
70
+
71
+ export const Default: Story = {
72
+ render: () => <FormDemo />,
73
+ };
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { HapticButton } from "./haptic-button";
3
+
4
+ const meta = {
5
+ title: "Components/HapticButton",
6
+ component: HapticButton,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof HapticButton>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Light: Story = {
14
+ render: () => <HapticButton hapticStyle="light">Light Haptic</HapticButton>,
15
+ };
16
+
17
+ export const Medium: Story = {
18
+ render: () => (
19
+ <HapticButton hapticStyle="medium" variant="secondary">
20
+ Medium Haptic
21
+ </HapticButton>
22
+ ),
23
+ };
24
+
25
+ export const Heavy: Story = {
26
+ render: () => (
27
+ <HapticButton hapticStyle="heavy" variant="destructive">
28
+ Heavy Haptic
29
+ </HapticButton>
30
+ ),
31
+ };
32
+
33
+ export const AllStyles: Story = {
34
+ render: () => (
35
+ <div className="flex gap-3">
36
+ <HapticButton hapticStyle="light">Light</HapticButton>
37
+ <HapticButton hapticStyle="medium" variant="secondary">Medium</HapticButton>
38
+ <HapticButton hapticStyle="heavy" variant="outline">Heavy</HapticButton>
39
+ </div>
40
+ ),
41
+ };
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "./input-otp";
3
+
4
+ const meta = {
5
+ title: "Components/InputOTP",
6
+ component: InputOTP,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof InputOTP>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ render: () => (
15
+ <InputOTP maxLength={6}>
16
+ <InputOTPGroup>
17
+ <InputOTPSlot index={0} />
18
+ <InputOTPSlot index={1} />
19
+ <InputOTPSlot index={2} />
20
+ </InputOTPGroup>
21
+ <InputOTPSeparator />
22
+ <InputOTPGroup>
23
+ <InputOTPSlot index={3} />
24
+ <InputOTPSlot index={4} />
25
+ <InputOTPSlot index={5} />
26
+ </InputOTPGroup>
27
+ </InputOTP>
28
+ ),
29
+ };
30
+
31
+ export const FourDigits: Story = {
32
+ render: () => (
33
+ <InputOTP maxLength={4}>
34
+ <InputOTPGroup>
35
+ <InputOTPSlot index={0} />
36
+ <InputOTPSlot index={1} />
37
+ <InputOTPSlot index={2} />
38
+ <InputOTPSlot index={3} />
39
+ </InputOTPGroup>
40
+ </InputOTP>
41
+ ),
42
+ };
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Input } from "./input";
3
+ import { Label } from "./label";
4
+
5
+ const meta = {
6
+ title: "Components/Input",
7
+ component: Input,
8
+ tags: ["autodocs"],
9
+ } satisfies Meta<typeof Input>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: { placeholder: "Enter text..." },
16
+ };
17
+
18
+ export const WithLabel: Story = {
19
+ render: () => (
20
+ <div className="grid w-full max-w-sm gap-1.5">
21
+ <Label htmlFor="email">Email</Label>
22
+ <Input type="email" id="email" placeholder="name@example.com" />
23
+ </div>
24
+ ),
25
+ };
26
+
27
+ export const Password: Story = {
28
+ args: { type: "password", placeholder: "Enter password..." },
29
+ };
30
+
31
+ export const Disabled: Story = {
32
+ args: { placeholder: "Disabled", disabled: true },
33
+ };
34
+
35
+ export const WithValue: Story = {
36
+ args: { defaultValue: "hello@bearmenu.com", type: "email" },
37
+ };
38
+
39
+ export const File: Story = {
40
+ args: { type: "file" },
41
+ };
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Label } from "./label";
3
+ import { Input } from "./input";
4
+ import { Checkbox } from "./checkbox";
5
+
6
+ const meta = {
7
+ title: "Components/Label",
8
+ component: Label,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof Label>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ args: { children: "Label" },
17
+ };
18
+
19
+ export const WithInput: Story = {
20
+ render: () => (
21
+ <div className="grid w-full max-w-sm gap-1.5">
22
+ <Label htmlFor="username">Username</Label>
23
+ <Input id="username" placeholder="Enter username" />
24
+ </div>
25
+ ),
26
+ };
27
+
28
+ export const WithCheckbox: Story = {
29
+ render: () => (
30
+ <div className="flex items-center gap-2">
31
+ <Checkbox id="terms" />
32
+ <Label htmlFor="terms">Accept terms and conditions</Label>
33
+ </div>
34
+ ),
35
+ };
@@ -0,0 +1,45 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { MarkdownRenderer } from "./markdown-renderer";
3
+
4
+ const meta = {
5
+ title: "Components/MarkdownRenderer",
6
+ component: MarkdownRenderer,
7
+ tags: ["autodocs"],
8
+ } satisfies Meta<typeof MarkdownRenderer>;
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const Default: Story = {
14
+ render: () => (
15
+ <MarkdownRenderer
16
+ content={`# Heading 1
17
+ ## Heading 2
18
+ ### Heading 3
19
+
20
+ This is a paragraph with **bold** and *italic* text.
21
+
22
+ - List item 1
23
+ - List item 2
24
+ - List item 3
25
+
26
+ 1. Ordered item
27
+ 2. Another item
28
+
29
+ > This is a blockquote
30
+
31
+ \`inline code\` and a code block:
32
+
33
+ \`\`\`
34
+ const hello = "world";
35
+ \`\`\`
36
+
37
+ | Column 1 | Column 2 |
38
+ |----------|----------|
39
+ | Cell 1 | Cell 2 |
40
+ | Cell 3 | Cell 4 |
41
+ `}
42
+ className="prose dark:prose-invert max-w-none"
43
+ />
44
+ ),
45
+ };
@@ -0,0 +1,78 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { MultiSelectCombobox } from "./multi-select-combobox";
4
+ import { Label } from "./label";
5
+
6
+ const meta = {
7
+ title: "Components/MultiSelectCombobox",
8
+ component: MultiSelectCombobox,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof MultiSelectCombobox>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ const cuisines = [
16
+ { value: "italian", label: "Italian" },
17
+ { value: "japanese", label: "Japanese" },
18
+ { value: "mexican", label: "Mexican" },
19
+ { value: "indian", label: "Indian" },
20
+ { value: "thai", label: "Thai" },
21
+ { value: "french", label: "French" },
22
+ { value: "chinese", label: "Chinese" },
23
+ { value: "korean", label: "Korean" },
24
+ ];
25
+
26
+ function MultiSelectDemo() {
27
+ const [selected, setSelected] = useState<string[]>([]);
28
+ return (
29
+ <div className="w-[350px] space-y-2">
30
+ <Label>Cuisines</Label>
31
+ <MultiSelectCombobox
32
+ options={cuisines}
33
+ selected={selected}
34
+ onSelectionChange={setSelected}
35
+ placeholder="Select cuisines..."
36
+ searchPlaceholder="Search cuisines..."
37
+ />
38
+ <p className="text-sm text-muted-foreground">
39
+ Selected: {selected.length ? selected.join(", ") : "none"}
40
+ </p>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export const Default: Story = {
46
+ render: () => <MultiSelectDemo />,
47
+ };
48
+
49
+ function PreselectedDemo() {
50
+ const [selected, setSelected] = useState(["italian", "japanese", "thai"]);
51
+ return (
52
+ <div className="w-[350px]">
53
+ <MultiSelectCombobox
54
+ options={cuisines}
55
+ selected={selected}
56
+ onSelectionChange={setSelected}
57
+ placeholder="Select cuisines..."
58
+ />
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export const Preselected: Story = {
64
+ render: () => <PreselectedDemo />,
65
+ };
66
+
67
+ export const Disabled: Story = {
68
+ render: () => (
69
+ <div className="w-[350px]">
70
+ <MultiSelectCombobox
71
+ options={cuisines}
72
+ selected={["italian"]}
73
+ onSelectionChange={() => {}}
74
+ disabled
75
+ />
76
+ </div>
77
+ ),
78
+ };
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cn } from "../lib/utils";
5
+ import { Checkbox } from "./checkbox";
6
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
7
+
8
+ export interface MultiSelectMenuOption {
9
+ value: string;
10
+ label: React.ReactNode;
11
+ disabled?: boolean;
12
+ }
13
+
14
+ export interface MultiSelectMenuProps {
15
+ options: MultiSelectMenuOption[];
16
+ selected: string[];
17
+ onSelectedChange: (values: string[]) => void;
18
+ /** Trigger element rendered via Radix `asChild` — typically a Button. */
19
+ trigger: React.ReactNode;
20
+ /** Optional heading rendered above the option list. */
21
+ label?: string;
22
+ align?: "start" | "center" | "end";
23
+ /** Pixel width of the popover content. Defaults to 220. */
24
+ width?: number;
25
+ /** className for the popover content. */
26
+ contentClassName?: string;
27
+ emptyText?: string;
28
+ }
29
+
30
+ export function MultiSelectMenu({
31
+ options,
32
+ selected,
33
+ onSelectedChange,
34
+ trigger,
35
+ label,
36
+ align = "end",
37
+ width = 220,
38
+ contentClassName,
39
+ emptyText = "No options",
40
+ }: MultiSelectMenuProps) {
41
+ const selectedSet = React.useMemo(() => new Set(selected), [selected]);
42
+
43
+ function handleToggle(value: string) {
44
+ const next = new Set(selectedSet);
45
+ if (next.has(value)) {
46
+ next.delete(value);
47
+ } else {
48
+ next.add(value);
49
+ }
50
+ onSelectedChange(Array.from(next));
51
+ }
52
+
53
+ return (
54
+ <Popover>
55
+ <PopoverTrigger asChild>{trigger}</PopoverTrigger>
56
+ <PopoverContent
57
+ align={align}
58
+ className={cn("p-2", contentClassName)}
59
+ style={{ width }}
60
+ >
61
+ {label && (
62
+ <div className="text-foreground px-2 py-1.5 text-sm font-semibold">
63
+ {label}
64
+ </div>
65
+ )}
66
+ <div role="group" aria-label={label} className="flex flex-col gap-0.5">
67
+ {options.length === 0 ? (
68
+ <p className="text-muted-foreground px-2 py-1.5 text-sm">{emptyText}</p>
69
+ ) : (
70
+ options.map((option) => {
71
+ const checked = selectedSet.has(option.value);
72
+ return (
73
+ <label
74
+ key={option.value}
75
+ className={cn(
76
+ "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm select-none",
77
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
78
+ option.disabled && "pointer-events-none opacity-50",
79
+ )}
80
+ >
81
+ <Checkbox
82
+ checked={checked}
83
+ onCheckedChange={() => handleToggle(option.value)}
84
+ disabled={option.disabled}
85
+ aria-label={typeof option.label === "string" ? option.label : option.value}
86
+ />
87
+ <span className="flex-1">{option.label}</span>
88
+ </label>
89
+ );
90
+ })
91
+ )}
92
+ </div>
93
+ </PopoverContent>
94
+ </Popover>
95
+ );
96
+ }
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ Pagination,
4
+ PaginationContent,
5
+ PaginationEllipsis,
6
+ PaginationItem,
7
+ PaginationLink,
8
+ PaginationNext,
9
+ PaginationPrevious,
10
+ } from "./pagination";
11
+
12
+ const meta = {
13
+ title: "Components/Pagination",
14
+ component: Pagination,
15
+ tags: ["autodocs"],
16
+ } satisfies Meta<typeof Pagination>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = {
22
+ render: () => (
23
+ <Pagination>
24
+ <PaginationContent>
25
+ <PaginationItem>
26
+ <PaginationPrevious href="#" />
27
+ </PaginationItem>
28
+ <PaginationItem>
29
+ <PaginationLink href="#">1</PaginationLink>
30
+ </PaginationItem>
31
+ <PaginationItem>
32
+ <PaginationLink href="#" isActive>
33
+ 2
34
+ </PaginationLink>
35
+ </PaginationItem>
36
+ <PaginationItem>
37
+ <PaginationLink href="#">3</PaginationLink>
38
+ </PaginationItem>
39
+ <PaginationItem>
40
+ <PaginationEllipsis />
41
+ </PaginationItem>
42
+ <PaginationItem>
43
+ <PaginationNext href="#" />
44
+ </PaginationItem>
45
+ </PaginationContent>
46
+ </Pagination>
47
+ ),
48
+ };