@emara/ui 1.1.0
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/components/ui/.gitkeep +0 -0
- package/components/ui/accordion.stories.tsx +231 -0
- package/components/ui/accordion.tsx +250 -0
- package/components/ui/app-shell.stories.tsx +270 -0
- package/components/ui/app-shell.tsx +491 -0
- package/components/ui/avatar.stories.tsx +174 -0
- package/components/ui/avatar.tsx +257 -0
- package/components/ui/badge.stories.tsx +127 -0
- package/components/ui/badge.tsx +146 -0
- package/components/ui/breadcrumb.stories.tsx +92 -0
- package/components/ui/breadcrumb.tsx +302 -0
- package/components/ui/button.stories.tsx +186 -0
- package/components/ui/button.tsx +128 -0
- package/components/ui/card.stories.tsx +279 -0
- package/components/ui/card.tsx +250 -0
- package/components/ui/checkbox.stories.tsx +93 -0
- package/components/ui/checkbox.tsx +131 -0
- package/components/ui/combobox.stories.tsx +489 -0
- package/components/ui/combobox.tsx +874 -0
- package/components/ui/context-menu.stories.tsx +202 -0
- package/components/ui/context-menu.tsx +309 -0
- package/components/ui/data-table.stories.tsx +227 -0
- package/components/ui/data-table.tsx +539 -0
- package/components/ui/date-picker.stories.tsx +225 -0
- package/components/ui/date-picker.tsx +597 -0
- package/components/ui/dialog.stories.tsx +193 -0
- package/components/ui/dialog.tsx +262 -0
- package/components/ui/divider.stories.tsx +84 -0
- package/components/ui/divider.tsx +135 -0
- package/components/ui/drawer.stories.tsx +218 -0
- package/components/ui/drawer.tsx +329 -0
- package/components/ui/dropdown-menu.stories.tsx +270 -0
- package/components/ui/dropdown-menu.tsx +353 -0
- package/components/ui/empty-state.stories.tsx +121 -0
- package/components/ui/empty-state.tsx +289 -0
- package/components/ui/field-group.stories.tsx +201 -0
- package/components/ui/field-group.tsx +276 -0
- package/components/ui/form.stories.tsx +219 -0
- package/components/ui/form.tsx +542 -0
- package/components/ui/input.stories.tsx +154 -0
- package/components/ui/input.tsx +208 -0
- package/components/ui/label.stories.tsx +84 -0
- package/components/ui/label.tsx +98 -0
- package/components/ui/page-header.stories.tsx +136 -0
- package/components/ui/page-header.tsx +315 -0
- package/components/ui/pagination.stories.tsx +136 -0
- package/components/ui/pagination.tsx +427 -0
- package/components/ui/popover.stories.tsx +212 -0
- package/components/ui/popover.tsx +167 -0
- package/components/ui/radio-group.stories.tsx +96 -0
- package/components/ui/radio-group.tsx +250 -0
- package/components/ui/select.stories.tsx +203 -0
- package/components/ui/select.tsx +318 -0
- package/components/ui/sidebar.stories.tsx +186 -0
- package/components/ui/sidebar.tsx +623 -0
- package/components/ui/skeleton.stories.tsx +131 -0
- package/components/ui/skeleton.tsx +311 -0
- package/components/ui/switch.stories.tsx +74 -0
- package/components/ui/switch.tsx +186 -0
- package/components/ui/table.stories.tsx +107 -0
- package/components/ui/table.tsx +285 -0
- package/components/ui/tabs.stories.tsx +222 -0
- package/components/ui/tabs.tsx +287 -0
- package/components/ui/textarea.stories.tsx +96 -0
- package/components/ui/textarea.tsx +182 -0
- package/components/ui/toast.stories.tsx +169 -0
- package/components/ui/toast.tsx +250 -0
- package/components/ui/tooltip.stories.tsx +146 -0
- package/components/ui/tooltip.tsx +156 -0
- package/components/ui/top-bar.stories.tsx +182 -0
- package/components/ui/top-bar.tsx +155 -0
- package/dist/components/ui/accordion.d.ts +45 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +99 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/app-shell.d.ts +70 -0
- package/dist/components/ui/app-shell.d.ts.map +1 -0
- package/dist/components/ui/app-shell.js +199 -0
- package/dist/components/ui/app-shell.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +41 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +104 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +27 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +65 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +35 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +88 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/button.d.ts +26 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +73 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/card.d.ts +52 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +96 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +18 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +59 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +194 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +361 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +46 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +95 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +53 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +163 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/date-picker.d.ts +103 -0
- package/dist/components/ui/date-picker.d.ts.map +1 -0
- package/dist/components/ui/date-picker.js +306 -0
- package/dist/components/ui/date-picker.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +40 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +110 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/divider.d.ts +30 -0
- package/dist/components/ui/divider.d.ts.map +1 -0
- package/dist/components/ui/divider.js +62 -0
- package/dist/components/ui/divider.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +56 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +147 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +63 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +116 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/empty-state.d.ts +43 -0
- package/dist/components/ui/empty-state.d.ts.map +1 -0
- package/dist/components/ui/empty-state.js +128 -0
- package/dist/components/ui/empty-state.js.map +1 -0
- package/dist/components/ui/field-group.d.ts +38 -0
- package/dist/components/ui/field-group.d.ts.map +1 -0
- package/dist/components/ui/field-group.js +107 -0
- package/dist/components/ui/field-group.js.map +1 -0
- package/dist/components/ui/form.d.ts +67 -0
- package/dist/components/ui/form.d.ts.map +1 -0
- package/dist/components/ui/form.js +286 -0
- package/dist/components/ui/form.js.map +1 -0
- package/dist/components/ui/input.d.ts +36 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +99 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/label.d.ts +37 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +34 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +65 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +140 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +67 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +109 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +28 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +85 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +35 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +103 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/select.d.ts +42 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +86 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +59 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +189 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +77 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +115 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/switch.d.ts +26 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +84 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +52 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +109 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +42 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +163 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +26 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +96 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/toast.d.ts +77 -0
- package/dist/components/ui/toast.d.ts.map +1 -0
- package/dist/components/ui/toast.js +141 -0
- package/dist/components/ui/toast.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +31 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +71 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/components/ui/top-bar.d.ts +30 -0
- package/dist/components/ui/top-bar.d.ts.map +1 -0
- package/dist/components/ui/top-bar.js +64 -0
- package/dist/components/ui/top-bar.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/lib/utils.ts +6 -0
- package/package.json +112 -0
- package/styles/globals.css +685 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, useId } from "react";
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
5
|
+
import { RiCheckLine, RiSubtractLine } from "@remixicon/react";
|
|
6
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
7
|
+
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
// Per docs/emara-ui-phase-2-components.md §1.
|
|
11
|
+
|
|
12
|
+
const checkboxVariants = cva(
|
|
13
|
+
[
|
|
14
|
+
"peer shrink-0 rounded-sm border border-input bg-background",
|
|
15
|
+
"transition-colors",
|
|
16
|
+
"hover:border-foreground/40 data-[state=checked]:hover:bg-primary/90 data-[state=indeterminate]:hover:bg-primary/90",
|
|
17
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
18
|
+
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-input",
|
|
19
|
+
"data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground",
|
|
20
|
+
"data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:text-primary-foreground",
|
|
21
|
+
].join(" "),
|
|
22
|
+
{
|
|
23
|
+
variants: {
|
|
24
|
+
size: {
|
|
25
|
+
sm: "size-4",
|
|
26
|
+
md: "size-5",
|
|
27
|
+
lg: "size-6",
|
|
28
|
+
},
|
|
29
|
+
invalid: {
|
|
30
|
+
true: "border-destructive focus-visible:ring-destructive data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=indeterminate]:bg-destructive data-[state=indeterminate]:border-destructive",
|
|
31
|
+
false: "",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: { size: "md", invalid: false },
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const indicatorIconSize: Record<"sm" | "md" | "lg", string> = {
|
|
39
|
+
sm: "size-3",
|
|
40
|
+
md: "size-3.5",
|
|
41
|
+
lg: "size-4",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const labelSize: Record<"sm" | "md" | "lg", string> = {
|
|
45
|
+
sm: "text-xs",
|
|
46
|
+
md: "text-sm",
|
|
47
|
+
lg: "text-base",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const descriptionSize: Record<"sm" | "md" | "lg", string> = {
|
|
51
|
+
sm: "text-2xs",
|
|
52
|
+
md: "text-xs",
|
|
53
|
+
lg: "text-sm",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type CheckboxVariants = VariantProps<typeof checkboxVariants>;
|
|
57
|
+
|
|
58
|
+
type CheckboxProps = Omit<React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, "size"> &
|
|
59
|
+
CheckboxVariants & {
|
|
60
|
+
label?: React.ReactNode;
|
|
61
|
+
description?: React.ReactNode;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const Checkbox = forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, CheckboxProps>(
|
|
65
|
+
function Checkbox({ className, size = "md", invalid, label, description, id, ...props }, ref) {
|
|
66
|
+
const reactId = useId();
|
|
67
|
+
const checkboxId = id ?? reactId;
|
|
68
|
+
const descriptionId = description ? `${checkboxId}-description` : undefined;
|
|
69
|
+
const resolvedSize = size ?? "md";
|
|
70
|
+
|
|
71
|
+
const checkbox = (
|
|
72
|
+
<CheckboxPrimitive.Root
|
|
73
|
+
ref={ref}
|
|
74
|
+
id={checkboxId}
|
|
75
|
+
aria-invalid={invalid || undefined}
|
|
76
|
+
aria-describedby={descriptionId}
|
|
77
|
+
className={cn(checkboxVariants({ size, invalid }), className)}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<CheckboxPrimitive.Indicator
|
|
81
|
+
className={cn("flex items-center justify-center text-current")}
|
|
82
|
+
>
|
|
83
|
+
{props.checked === "indeterminate" ? (
|
|
84
|
+
<RiSubtractLine className={indicatorIconSize[resolvedSize]} />
|
|
85
|
+
) : (
|
|
86
|
+
<RiCheckLine className={indicatorIconSize[resolvedSize]} />
|
|
87
|
+
)}
|
|
88
|
+
</CheckboxPrimitive.Indicator>
|
|
89
|
+
</CheckboxPrimitive.Root>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (!label && !description) return checkbox;
|
|
93
|
+
|
|
94
|
+
// When `label` is provided, render the full row pattern: checkbox + label + description.
|
|
95
|
+
return (
|
|
96
|
+
<div className="inline-flex items-start gap-2">
|
|
97
|
+
{checkbox}
|
|
98
|
+
<div className="space-y-0.5 leading-none">
|
|
99
|
+
{label ? (
|
|
100
|
+
<label
|
|
101
|
+
htmlFor={checkboxId}
|
|
102
|
+
className={cn(
|
|
103
|
+
"cursor-pointer font-medium select-none",
|
|
104
|
+
labelSize[resolvedSize],
|
|
105
|
+
props.disabled && "cursor-not-allowed opacity-50",
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{label}
|
|
109
|
+
</label>
|
|
110
|
+
) : null}
|
|
111
|
+
{description ? (
|
|
112
|
+
<p
|
|
113
|
+
id={descriptionId}
|
|
114
|
+
className={cn(
|
|
115
|
+
"text-muted-foreground",
|
|
116
|
+
descriptionSize[resolvedSize],
|
|
117
|
+
props.disabled && "opacity-50",
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{description}
|
|
121
|
+
</p>
|
|
122
|
+
) : null}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
Checkbox.displayName = "Checkbox";
|
|
129
|
+
|
|
130
|
+
export { Checkbox, checkboxVariants };
|
|
131
|
+
export type { CheckboxProps };
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { RiGlobalLine } from "@remixicon/react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Combobox,
|
|
7
|
+
ComboboxContent,
|
|
8
|
+
ComboboxGroup,
|
|
9
|
+
ComboboxInput,
|
|
10
|
+
ComboboxItem,
|
|
11
|
+
ComboboxList,
|
|
12
|
+
ComboboxSeparator,
|
|
13
|
+
ComboboxTrigger,
|
|
14
|
+
} from "./combobox";
|
|
15
|
+
|
|
16
|
+
const FRAMEWORKS = [
|
|
17
|
+
{ value: "next", label: "Next.js" },
|
|
18
|
+
{ value: "remix", label: "Remix" },
|
|
19
|
+
{ value: "astro", label: "Astro" },
|
|
20
|
+
{ value: "vite", label: "Vite" },
|
|
21
|
+
{ value: "svelte", label: "SvelteKit" },
|
|
22
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const COUNTRIES = [
|
|
26
|
+
{ value: "ma", label: "Morocco" },
|
|
27
|
+
{ value: "fr", label: "France" },
|
|
28
|
+
{ value: "de", label: "Germany" },
|
|
29
|
+
{ value: "us", label: "United States" },
|
|
30
|
+
{ value: "jp", label: "Japan" },
|
|
31
|
+
{ value: "br", label: "Brazil" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const meta: Meta = {
|
|
35
|
+
title: "Forms/Combobox",
|
|
36
|
+
parameters: { layout: "centered" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj;
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {
|
|
43
|
+
render: () => {
|
|
44
|
+
const Wrapper = () => {
|
|
45
|
+
const [value, setValue] = useState("");
|
|
46
|
+
const selected = FRAMEWORKS.find((f) => f.value === value);
|
|
47
|
+
return (
|
|
48
|
+
<div className="w-64">
|
|
49
|
+
<Combobox value={value} onValueChange={setValue}>
|
|
50
|
+
<ComboboxTrigger placeholder="Pick a framework">{selected?.label}</ComboboxTrigger>
|
|
51
|
+
<ComboboxContent emptyMessage="No results.">
|
|
52
|
+
<ComboboxInput placeholder="Search frameworks…" />
|
|
53
|
+
<ComboboxList>
|
|
54
|
+
{FRAMEWORKS.map((f) => (
|
|
55
|
+
<ComboboxItem key={f.value} value={f.value}>
|
|
56
|
+
{f.label}
|
|
57
|
+
</ComboboxItem>
|
|
58
|
+
))}
|
|
59
|
+
</ComboboxList>
|
|
60
|
+
</ComboboxContent>
|
|
61
|
+
</Combobox>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
return <Wrapper />;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Controlled: Story = {
|
|
70
|
+
render: () => {
|
|
71
|
+
const Wrapper = () => {
|
|
72
|
+
const [value, setValue] = useState("remix");
|
|
73
|
+
const selected = FRAMEWORKS.find((f) => f.value === value);
|
|
74
|
+
return (
|
|
75
|
+
<div className="w-64 space-y-2">
|
|
76
|
+
<Combobox value={value} onValueChange={setValue}>
|
|
77
|
+
<ComboboxTrigger placeholder="Framework" clearable>
|
|
78
|
+
{selected?.label}
|
|
79
|
+
</ComboboxTrigger>
|
|
80
|
+
<ComboboxContent emptyMessage="No results.">
|
|
81
|
+
<ComboboxInput />
|
|
82
|
+
<ComboboxList>
|
|
83
|
+
{FRAMEWORKS.map((f) => (
|
|
84
|
+
<ComboboxItem key={f.value} value={f.value}>
|
|
85
|
+
{f.label}
|
|
86
|
+
</ComboboxItem>
|
|
87
|
+
))}
|
|
88
|
+
</ComboboxList>
|
|
89
|
+
</ComboboxContent>
|
|
90
|
+
</Combobox>
|
|
91
|
+
<p className="text-muted-foreground text-xs">value: {JSON.stringify(value)}</p>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
return <Wrapper />;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const Clearable: Story = {
|
|
100
|
+
render: () => {
|
|
101
|
+
const Wrapper = () => {
|
|
102
|
+
const [value, setValue] = useState("next");
|
|
103
|
+
const selected = FRAMEWORKS.find((f) => f.value === value);
|
|
104
|
+
return (
|
|
105
|
+
<div className="w-64">
|
|
106
|
+
<Combobox value={value} onValueChange={setValue}>
|
|
107
|
+
<ComboboxTrigger placeholder="Pick a framework" clearable>
|
|
108
|
+
{selected?.label}
|
|
109
|
+
</ComboboxTrigger>
|
|
110
|
+
<ComboboxContent emptyMessage="No results.">
|
|
111
|
+
<ComboboxInput />
|
|
112
|
+
<ComboboxList>
|
|
113
|
+
{FRAMEWORKS.map((f) => (
|
|
114
|
+
<ComboboxItem key={f.value} value={f.value}>
|
|
115
|
+
{f.label}
|
|
116
|
+
</ComboboxItem>
|
|
117
|
+
))}
|
|
118
|
+
</ComboboxList>
|
|
119
|
+
</ComboboxContent>
|
|
120
|
+
</Combobox>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
return <Wrapper />;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const WithIconsAndDescriptions: Story = {
|
|
129
|
+
render: () => (
|
|
130
|
+
<div className="w-72">
|
|
131
|
+
<Combobox>
|
|
132
|
+
<ComboboxTrigger placeholder="Pick a country" />
|
|
133
|
+
<ComboboxContent emptyMessage="No matches.">
|
|
134
|
+
<ComboboxInput />
|
|
135
|
+
<ComboboxList>
|
|
136
|
+
{COUNTRIES.map((c) => (
|
|
137
|
+
<ComboboxItem
|
|
138
|
+
key={c.value}
|
|
139
|
+
value={c.value}
|
|
140
|
+
icon={<RiGlobalLine />}
|
|
141
|
+
description={`Code: ${c.value.toUpperCase()}`}
|
|
142
|
+
>
|
|
143
|
+
{c.label}
|
|
144
|
+
</ComboboxItem>
|
|
145
|
+
))}
|
|
146
|
+
</ComboboxList>
|
|
147
|
+
</ComboboxContent>
|
|
148
|
+
</Combobox>
|
|
149
|
+
</div>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const Grouped: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<div className="w-64">
|
|
156
|
+
<Combobox>
|
|
157
|
+
<ComboboxTrigger placeholder="Pick a region" />
|
|
158
|
+
<ComboboxContent emptyMessage="No matches.">
|
|
159
|
+
<ComboboxInput />
|
|
160
|
+
<ComboboxList>
|
|
161
|
+
<ComboboxGroup heading="Africa">
|
|
162
|
+
<ComboboxItem value="ma">Morocco</ComboboxItem>
|
|
163
|
+
<ComboboxItem value="eg">Egypt</ComboboxItem>
|
|
164
|
+
<ComboboxItem value="ke">Kenya</ComboboxItem>
|
|
165
|
+
</ComboboxGroup>
|
|
166
|
+
<ComboboxSeparator />
|
|
167
|
+
<ComboboxGroup heading="Europe">
|
|
168
|
+
<ComboboxItem value="fr">France</ComboboxItem>
|
|
169
|
+
<ComboboxItem value="de">Germany</ComboboxItem>
|
|
170
|
+
<ComboboxItem value="es">Spain</ComboboxItem>
|
|
171
|
+
</ComboboxGroup>
|
|
172
|
+
</ComboboxList>
|
|
173
|
+
</ComboboxContent>
|
|
174
|
+
</Combobox>
|
|
175
|
+
</div>
|
|
176
|
+
),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const Invalid: Story = {
|
|
180
|
+
render: () => (
|
|
181
|
+
<div className="w-64">
|
|
182
|
+
<Combobox invalid>
|
|
183
|
+
<ComboboxTrigger placeholder="Required" />
|
|
184
|
+
<ComboboxContent emptyMessage="—">
|
|
185
|
+
<ComboboxInput />
|
|
186
|
+
<ComboboxList>
|
|
187
|
+
{FRAMEWORKS.slice(0, 3).map((f) => (
|
|
188
|
+
<ComboboxItem key={f.value} value={f.value}>
|
|
189
|
+
{f.label}
|
|
190
|
+
</ComboboxItem>
|
|
191
|
+
))}
|
|
192
|
+
</ComboboxList>
|
|
193
|
+
</ComboboxContent>
|
|
194
|
+
</Combobox>
|
|
195
|
+
</div>
|
|
196
|
+
),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const Loading: Story = {
|
|
200
|
+
render: () => (
|
|
201
|
+
<div className="w-64">
|
|
202
|
+
<Combobox loading>
|
|
203
|
+
<ComboboxTrigger placeholder="Loading…" />
|
|
204
|
+
</Combobox>
|
|
205
|
+
</div>
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const Disabled: Story = {
|
|
210
|
+
render: () => (
|
|
211
|
+
<div className="w-64">
|
|
212
|
+
<Combobox defaultValue="vite" disabled>
|
|
213
|
+
<ComboboxTrigger placeholder="Disabled" />
|
|
214
|
+
</Combobox>
|
|
215
|
+
</div>
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Multi-select mode. The trigger renders one pill per selected value;
|
|
221
|
+
* past 3 selections, additional items collapse into a "+N more" badge.
|
|
222
|
+
* Selecting an already-selected item deselects it and keeps the popover
|
|
223
|
+
* open so users can keep picking.
|
|
224
|
+
*/
|
|
225
|
+
export const Multiple: Story = {
|
|
226
|
+
render: () => {
|
|
227
|
+
const Wrapper = () => {
|
|
228
|
+
const [values, setValues] = useState<string[]>(["next", "vite"]);
|
|
229
|
+
const labelFor = (v: string) => FRAMEWORKS.find((f) => f.value === v)?.label ?? v;
|
|
230
|
+
return (
|
|
231
|
+
<div className="w-72 space-y-2">
|
|
232
|
+
<Combobox multiple value={values} onValueChange={setValues}>
|
|
233
|
+
<ComboboxTrigger placeholder="Pick frameworks" clearable getOptionLabel={labelFor} />
|
|
234
|
+
<ComboboxContent emptyMessage="No results.">
|
|
235
|
+
<ComboboxInput placeholder="Search…" />
|
|
236
|
+
<ComboboxList>
|
|
237
|
+
{FRAMEWORKS.map((f) => (
|
|
238
|
+
<ComboboxItem key={f.value} value={f.value}>
|
|
239
|
+
{f.label}
|
|
240
|
+
</ComboboxItem>
|
|
241
|
+
))}
|
|
242
|
+
</ComboboxList>
|
|
243
|
+
</ComboboxContent>
|
|
244
|
+
</Combobox>
|
|
245
|
+
<p className="text-muted-foreground text-xs">value: {JSON.stringify(values)}</p>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
return <Wrapper />;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Multi-select with `maxSelected={3}`. Once three items are picked, the
|
|
255
|
+
* remaining unselected items become disabled. Already-selected items can
|
|
256
|
+
* still be toggled off.
|
|
257
|
+
*/
|
|
258
|
+
export const MultipleWithMaxSelected: Story = {
|
|
259
|
+
render: () => {
|
|
260
|
+
const Wrapper = () => {
|
|
261
|
+
const [values, setValues] = useState<string[]>([]);
|
|
262
|
+
const labelFor = (v: string) => FRAMEWORKS.find((f) => f.value === v)?.label ?? v;
|
|
263
|
+
return (
|
|
264
|
+
<div className="w-72">
|
|
265
|
+
<Combobox multiple value={values} onValueChange={setValues} maxSelected={3}>
|
|
266
|
+
<ComboboxTrigger placeholder="Pick up to 3 frameworks" getOptionLabel={labelFor} />
|
|
267
|
+
<ComboboxContent emptyMessage="No results.">
|
|
268
|
+
<ComboboxInput placeholder="Search…" />
|
|
269
|
+
<ComboboxList>
|
|
270
|
+
{FRAMEWORKS.map((f) => (
|
|
271
|
+
<ComboboxItem key={f.value} value={f.value}>
|
|
272
|
+
{f.label}
|
|
273
|
+
</ComboboxItem>
|
|
274
|
+
))}
|
|
275
|
+
</ComboboxList>
|
|
276
|
+
</ComboboxContent>
|
|
277
|
+
</Combobox>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
return <Wrapper />;
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Creatable: when the user types a search that doesn't match an existing
|
|
287
|
+
* option, a "+ Create '...' " row appears below the list. `onCreate` is
|
|
288
|
+
* the consumer's hook to add the new option to their data source AND
|
|
289
|
+
* (optionally) select it.
|
|
290
|
+
*/
|
|
291
|
+
export const Creatable: Story = {
|
|
292
|
+
render: () => {
|
|
293
|
+
const Wrapper = () => {
|
|
294
|
+
const [options, setOptions] = useState<{ value: string; label: string }[]>([
|
|
295
|
+
{ value: "react", label: "React" },
|
|
296
|
+
{ value: "vue", label: "Vue" },
|
|
297
|
+
]);
|
|
298
|
+
const [value, setValue] = useState("");
|
|
299
|
+
const labelFor = (v: string) => options.find((o) => o.value === v)?.label ?? v;
|
|
300
|
+
return (
|
|
301
|
+
<div className="w-72 space-y-2">
|
|
302
|
+
<Combobox
|
|
303
|
+
value={value}
|
|
304
|
+
onValueChange={setValue}
|
|
305
|
+
creatable
|
|
306
|
+
onCreate={(q) => {
|
|
307
|
+
const slug = q.toLowerCase().replace(/\s+/g, "-");
|
|
308
|
+
setOptions((prev) => [...prev, { value: slug, label: q }]);
|
|
309
|
+
setValue(slug);
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<ComboboxTrigger placeholder="Pick or create a framework" getOptionLabel={labelFor}>
|
|
313
|
+
{labelFor(value)}
|
|
314
|
+
</ComboboxTrigger>
|
|
315
|
+
<ComboboxContent emptyMessage="No matches.">
|
|
316
|
+
<ComboboxInput placeholder="Type a new one…" />
|
|
317
|
+
<ComboboxList>
|
|
318
|
+
{options.map((o) => (
|
|
319
|
+
<ComboboxItem key={o.value} value={o.value}>
|
|
320
|
+
{o.label}
|
|
321
|
+
</ComboboxItem>
|
|
322
|
+
))}
|
|
323
|
+
</ComboboxList>
|
|
324
|
+
</ComboboxContent>
|
|
325
|
+
</Combobox>
|
|
326
|
+
<p className="text-muted-foreground text-xs">value: {JSON.stringify(value)}</p>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
return <Wrapper />;
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Creatable + multi-select. Consumer is responsible for both adding the
|
|
336
|
+
* new option to its data source AND appending the value to the selected
|
|
337
|
+
* array — the component does not mutate state in `onCreate`.
|
|
338
|
+
*/
|
|
339
|
+
export const CreatableMultiple: Story = {
|
|
340
|
+
render: () => {
|
|
341
|
+
const Wrapper = () => {
|
|
342
|
+
const [options, setOptions] = useState<{ value: string; label: string }[]>([
|
|
343
|
+
{ value: "next", label: "Next.js" },
|
|
344
|
+
{ value: "remix", label: "Remix" },
|
|
345
|
+
]);
|
|
346
|
+
const [values, setValues] = useState<string[]>([]);
|
|
347
|
+
const labelFor = (v: string) => options.find((o) => o.value === v)?.label ?? v;
|
|
348
|
+
return (
|
|
349
|
+
<div className="w-72 space-y-2">
|
|
350
|
+
<Combobox
|
|
351
|
+
multiple
|
|
352
|
+
value={values}
|
|
353
|
+
onValueChange={setValues}
|
|
354
|
+
creatable
|
|
355
|
+
onCreate={(q) => {
|
|
356
|
+
const slug = q.toLowerCase().replace(/\s+/g, "-");
|
|
357
|
+
setOptions((prev) => [...prev, { value: slug, label: q }]);
|
|
358
|
+
setValues((prev) => [...prev, slug]);
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
<ComboboxTrigger placeholder="Pick or create" getOptionLabel={labelFor} />
|
|
362
|
+
<ComboboxContent emptyMessage="No matches.">
|
|
363
|
+
<ComboboxInput placeholder="Type to filter or create…" />
|
|
364
|
+
<ComboboxList>
|
|
365
|
+
{options.map((o) => (
|
|
366
|
+
<ComboboxItem key={o.value} value={o.value}>
|
|
367
|
+
{o.label}
|
|
368
|
+
</ComboboxItem>
|
|
369
|
+
))}
|
|
370
|
+
</ComboboxList>
|
|
371
|
+
</ComboboxContent>
|
|
372
|
+
</Combobox>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
return <Wrapper />;
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Async option loading via `loadOptions`. The component handles debounce,
|
|
382
|
+
* race-condition guarding (stale responses are dropped), and auto-renders
|
|
383
|
+
* the returned options as items. Consumer just supplies the fetcher.
|
|
384
|
+
*/
|
|
385
|
+
export const AsyncLoadOptions: Story = {
|
|
386
|
+
render: () => {
|
|
387
|
+
type O = { value: string; label: string };
|
|
388
|
+
const ALL: O[] = [
|
|
389
|
+
{ value: "next", label: "Next.js" },
|
|
390
|
+
{ value: "remix", label: "Remix" },
|
|
391
|
+
{ value: "astro", label: "Astro" },
|
|
392
|
+
{ value: "vite", label: "Vite" },
|
|
393
|
+
{ value: "svelte", label: "SvelteKit" },
|
|
394
|
+
{ value: "nuxt", label: "Nuxt" },
|
|
395
|
+
{ value: "qwik", label: "Qwik" },
|
|
396
|
+
{ value: "solid", label: "SolidStart" },
|
|
397
|
+
];
|
|
398
|
+
const fetcher = async (query: string): Promise<O[]> => {
|
|
399
|
+
// Pretend network: 300ms artificial delay.
|
|
400
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
401
|
+
const q = query.toLowerCase();
|
|
402
|
+
return ALL.filter((o) => o.label.toLowerCase().includes(q));
|
|
403
|
+
};
|
|
404
|
+
return (
|
|
405
|
+
<div className="w-72">
|
|
406
|
+
<Combobox loadOptions={fetcher}>
|
|
407
|
+
<ComboboxTrigger placeholder="Search frameworks…" />
|
|
408
|
+
<ComboboxContent emptyMessage="No matches.">
|
|
409
|
+
<ComboboxInput placeholder="Type to search…" />
|
|
410
|
+
</ComboboxContent>
|
|
411
|
+
</Combobox>
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* `groupBy` partitions loaded options into auto-headed groups. Saves the
|
|
419
|
+
* boilerplate of writing `<ComboboxGroup>` manually around each region.
|
|
420
|
+
* Groups appear in first-seen insertion order — sort your options array
|
|
421
|
+
* if you want alphabetical ordering.
|
|
422
|
+
*/
|
|
423
|
+
export const AsyncGrouped: Story = {
|
|
424
|
+
render: () => {
|
|
425
|
+
type O = { value: string; label: string; region: string };
|
|
426
|
+
const ALL: O[] = [
|
|
427
|
+
{ value: "ma", label: "Morocco", region: "Africa" },
|
|
428
|
+
{ value: "eg", label: "Egypt", region: "Africa" },
|
|
429
|
+
{ value: "ke", label: "Kenya", region: "Africa" },
|
|
430
|
+
{ value: "fr", label: "France", region: "Europe" },
|
|
431
|
+
{ value: "de", label: "Germany", region: "Europe" },
|
|
432
|
+
{ value: "es", label: "Spain", region: "Europe" },
|
|
433
|
+
{ value: "jp", label: "Japan", region: "Asia" },
|
|
434
|
+
{ value: "kr", label: "South Korea", region: "Asia" },
|
|
435
|
+
];
|
|
436
|
+
const fetcher = async (q: string): Promise<O[]> => {
|
|
437
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
438
|
+
return ALL.filter((o) => o.label.toLowerCase().includes(q.toLowerCase()));
|
|
439
|
+
};
|
|
440
|
+
return (
|
|
441
|
+
<div className="w-72">
|
|
442
|
+
<Combobox loadOptions={fetcher} groupBy={(o) => (o as O).region}>
|
|
443
|
+
<ComboboxTrigger placeholder="Pick a country" />
|
|
444
|
+
<ComboboxContent emptyMessage="No matches.">
|
|
445
|
+
<ComboboxInput placeholder="Type to search…" />
|
|
446
|
+
</ComboboxContent>
|
|
447
|
+
</Combobox>
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* `keywords` on each item adds extra search terms beyond the value and
|
|
455
|
+
* rendered children. Type "nyc" to match New York, "windy" to match
|
|
456
|
+
* Chicago, etc. — none of those strings appear in the visible label.
|
|
457
|
+
*/
|
|
458
|
+
export const WithKeywords: Story = {
|
|
459
|
+
render: () => {
|
|
460
|
+
const CITIES: { value: string; label: string; keywords: string[] }[] = [
|
|
461
|
+
{ value: "new-york", label: "New York", keywords: ["nyc", "ny", "big apple"] },
|
|
462
|
+
{ value: "los-angeles", label: "Los Angeles", keywords: ["la", "city of angels"] },
|
|
463
|
+
{ value: "chicago", label: "Chicago", keywords: ["chi", "windy city"] },
|
|
464
|
+
{ value: "san-francisco", label: "San Francisco", keywords: ["sf", "the city", "bay area"] },
|
|
465
|
+
];
|
|
466
|
+
const Wrapper = () => {
|
|
467
|
+
const [value, setValue] = useState("");
|
|
468
|
+
const selected = CITIES.find((c) => c.value === value);
|
|
469
|
+
return (
|
|
470
|
+
<div className="w-64">
|
|
471
|
+
<Combobox value={value} onValueChange={setValue}>
|
|
472
|
+
<ComboboxTrigger placeholder="Pick a city">{selected?.label}</ComboboxTrigger>
|
|
473
|
+
<ComboboxContent emptyMessage="No matching city.">
|
|
474
|
+
<ComboboxInput placeholder="Try 'nyc', 'la', 'windy'…" />
|
|
475
|
+
<ComboboxList>
|
|
476
|
+
{CITIES.map((c) => (
|
|
477
|
+
<ComboboxItem key={c.value} value={c.value} keywords={c.keywords}>
|
|
478
|
+
{c.label}
|
|
479
|
+
</ComboboxItem>
|
|
480
|
+
))}
|
|
481
|
+
</ComboboxList>
|
|
482
|
+
</ComboboxContent>
|
|
483
|
+
</Combobox>
|
|
484
|
+
</div>
|
|
485
|
+
);
|
|
486
|
+
};
|
|
487
|
+
return <Wrapper />;
|
|
488
|
+
},
|
|
489
|
+
};
|