@stasho/ds 0.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.
Files changed (30) hide show
  1. package/package.json +88 -0
  2. package/src/components/alert/alert.tsx +204 -0
  3. package/src/components/badge/badge.tsx +139 -0
  4. package/src/components/breadcrumb/breadcrumb.tsx +130 -0
  5. package/src/components/button/button.tsx +153 -0
  6. package/src/components/card/card.tsx +48 -0
  7. package/src/components/checkbox/checkbox.tsx +82 -0
  8. package/src/components/combobox/combobox.tsx +166 -0
  9. package/src/components/copyable-text/copyable-text.tsx +194 -0
  10. package/src/components/dialog/dialog.tsx +145 -0
  11. package/src/components/form-field/form-field.tsx +71 -0
  12. package/src/components/input/input.tsx +52 -0
  13. package/src/components/logo/logo.tsx +68 -0
  14. package/src/components/multi-select/multi-select.tsx +312 -0
  15. package/src/components/pagination/pagination.tsx +218 -0
  16. package/src/components/progress-bar/progress-bar.tsx +134 -0
  17. package/src/components/radio-group/radio-group.tsx +87 -0
  18. package/src/components/select/select.tsx +124 -0
  19. package/src/components/slider/slider.tsx +127 -0
  20. package/src/components/status-dot/status-dot.tsx +53 -0
  21. package/src/components/stepper/stepper.tsx +229 -0
  22. package/src/components/switch/switch.tsx +74 -0
  23. package/src/components/table/table.tsx +260 -0
  24. package/src/components/tabs/tabs.tsx +493 -0
  25. package/src/components/textarea/textarea.tsx +56 -0
  26. package/src/components/tooltip/tooltip.tsx +35 -0
  27. package/src/components/ui/skeleton.tsx +21 -0
  28. package/src/components/ui/spinner.tsx +27 -0
  29. package/src/lib/cn.ts +6 -0
  30. package/src/styles/tokens.css +459 -0
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ type ComponentPropsWithoutRef,
6
+ type HTMLAttributes,
7
+ } from "react";
8
+ import { Dialog as DialogPrimitive } from "radix-ui";
9
+ import { X } from "@phosphor-icons/react";
10
+ import { cn } from "@ac/lib/cn";
11
+
12
+ /* ── Direct re-exports ─────────────────────────── */
13
+
14
+ const Dialog = DialogPrimitive.Root;
15
+ const DialogTrigger = DialogPrimitive.Trigger;
16
+ const DialogClose = DialogPrimitive.Close;
17
+
18
+ /* ── DialogContent (Portal + Overlay + Content) ── */
19
+
20
+ type DialogContentProps = ComponentPropsWithoutRef<
21
+ typeof DialogPrimitive.Content
22
+ > & {
23
+ locked?: boolean;
24
+ };
25
+
26
+ const DialogContent = forwardRef<HTMLDivElement, DialogContentProps>(
27
+ ({ className, children, locked, ...rest }, ref) => (
28
+ <DialogPrimitive.Portal>
29
+ <DialogPrimitive.Overlay
30
+ className={cn(
31
+ "fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
32
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0",
33
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
34
+ "motion-reduce:animate-none",
35
+ )}
36
+ />
37
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ "relative w-full max-w-md rounded-md bg-surface p-6 shadow-brand-lg",
42
+ "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
43
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
44
+ "motion-reduce:animate-none",
45
+ className,
46
+ )}
47
+ {...(locked
48
+ ? {
49
+ onInteractOutside: (e: Event) => e.preventDefault(),
50
+ onEscapeKeyDown: (e: KeyboardEvent) => e.preventDefault(),
51
+ }
52
+ : {})}
53
+ {...rest}
54
+ >
55
+ {children}
56
+ {locked ? null : (
57
+ <DialogPrimitive.Close
58
+ className={cn(
59
+ "absolute top-4 right-4 rounded-sm",
60
+ "text-muted-foreground transition-colors hover:text-foreground",
61
+ "focus-visible:outline-none focus-visible:ring-2",
62
+ "focus-visible:ring-primary-400 focus-visible:ring-offset-2",
63
+ )}
64
+ aria-label="Close"
65
+ >
66
+ <X weight="bold" className="size-4" />
67
+ </DialogPrimitive.Close>
68
+ )}
69
+ </DialogPrimitive.Content>
70
+ </div>
71
+ </DialogPrimitive.Portal>
72
+ ),
73
+ );
74
+
75
+ DialogContent.displayName = "DialogContent";
76
+
77
+ /* ── DialogTitle ───────────────────────────────── */
78
+
79
+ const DialogTitle = forwardRef<
80
+ HTMLHeadingElement,
81
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
82
+ >(({ className, ...rest }, ref) => (
83
+ <DialogPrimitive.Title
84
+ ref={ref}
85
+ className={cn(
86
+ "font-heading font-bold text-lg text-foreground",
87
+ className,
88
+ )}
89
+ {...rest}
90
+ />
91
+ ));
92
+
93
+ DialogTitle.displayName = "DialogTitle";
94
+
95
+ /* ── DialogDescription ─────────────────────────── */
96
+
97
+ const DialogDescription = forwardRef<
98
+ HTMLParagraphElement,
99
+ ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
100
+ >(({ className, ...rest }, ref) => (
101
+ <DialogPrimitive.Description
102
+ ref={ref}
103
+ className={cn("text-sm text-muted-foreground", className)}
104
+ {...rest}
105
+ />
106
+ ));
107
+
108
+ DialogDescription.displayName = "DialogDescription";
109
+
110
+ /* ── Layout helpers (plain divs) ───────────────── */
111
+
112
+ function DialogHeader({
113
+ className,
114
+ ...rest
115
+ }: HTMLAttributes<HTMLDivElement>) {
116
+ return (
117
+ <div className={cn("flex flex-col gap-1.5", className)} {...rest} />
118
+ );
119
+ }
120
+
121
+ function DialogFooter({
122
+ className,
123
+ ...rest
124
+ }: HTMLAttributes<HTMLDivElement>) {
125
+ return (
126
+ <div
127
+ className={cn("flex justify-end gap-3 pt-4", className)}
128
+ {...rest}
129
+ />
130
+ );
131
+ }
132
+
133
+ /* ── Exports ───────────────────────────────────── */
134
+
135
+ export {
136
+ Dialog,
137
+ DialogClose,
138
+ DialogContent,
139
+ DialogDescription,
140
+ DialogFooter,
141
+ DialogHeader,
142
+ DialogTitle,
143
+ DialogTrigger,
144
+ type DialogContentProps,
145
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ cloneElement,
3
+ isValidElement,
4
+ useId,
5
+ type ReactElement,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { cn } from "@ac/lib/cn";
9
+
10
+ type FormFieldProps = {
11
+ label: string;
12
+ required?: boolean;
13
+ helperText?: string;
14
+ error?: string;
15
+ children: ReactNode;
16
+ className?: string;
17
+ };
18
+
19
+ function FormField({
20
+ label,
21
+ required = false,
22
+ helperText,
23
+ error,
24
+ children,
25
+ className,
26
+ }: FormFieldProps) {
27
+ const id = useId();
28
+ const inputId = `${id}-input`;
29
+ const messageId = `${id}-message`;
30
+ const hasMessage = Boolean(error ?? helperText);
31
+
32
+ const hasError = Boolean(error);
33
+
34
+ const child = isValidElement(children)
35
+ ? cloneElement(children as ReactElement<Record<string, unknown>>, {
36
+ id: inputId,
37
+ ...(hasMessage ? { "aria-describedby": messageId } : {}),
38
+ ...(hasError ? { error: true, "aria-invalid": true } : {}),
39
+ })
40
+ : children;
41
+
42
+ return (
43
+ <div className={cn("flex flex-col gap-1.5", className)}>
44
+ <label
45
+ htmlFor={inputId}
46
+ className="text-sm font-medium text-foreground"
47
+ >
48
+ {label}
49
+ {required && (
50
+ <span className="text-error-600 ml-0.5" aria-hidden="true">
51
+ *
52
+ </span>
53
+ )}
54
+ </label>
55
+ {child}
56
+ {error ? (
57
+ <p id={messageId} role="alert" className="text-xs text-error-600">
58
+ {error}
59
+ </p>
60
+ ) : helperText ? (
61
+ <p id={messageId} className="text-xs text-muted-foreground">
62
+ {helperText}
63
+ </p>
64
+ ) : null}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ FormField.displayName = "FormField";
70
+
71
+ export { FormField, type FormFieldProps };
@@ -0,0 +1,52 @@
1
+ import { forwardRef, type InputHTMLAttributes } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@ac/lib/cn";
4
+
5
+ const inputVariants = cva(
6
+ [
7
+ "w-full font-sans text-foreground bg-primary-100 dark:bg-base-700",
8
+ "border-0 rounded-full",
9
+ "placeholder:text-muted-foreground",
10
+ "focus-visible:outline-none focus-visible:ring-3",
11
+ "focus-visible:ring-primary-500",
12
+ "disabled:opacity-50 disabled:pointer-events-none",
13
+ "ring-0 transition-colors",
14
+ ].join(" "),
15
+ {
16
+ variants: {
17
+ size: {
18
+ sm: "py-1.5 px-4 text-sm",
19
+ md: "py-2 px-5 text-base",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ size: "md",
24
+ },
25
+ },
26
+ );
27
+
28
+ type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> &
29
+ VariantProps<typeof inputVariants> & {
30
+ error?: boolean;
31
+ };
32
+
33
+ const Input = forwardRef<HTMLInputElement, InputProps>(
34
+ ({ size, error = false, className, ...rest }, ref) => {
35
+ return (
36
+ <input
37
+ ref={ref}
38
+ className={cn(
39
+ inputVariants({ size }),
40
+ error && "border-3 border-error-400 hover:border-error-500",
41
+ className,
42
+ )}
43
+ aria-invalid={error || undefined}
44
+ {...rest}
45
+ />
46
+ );
47
+ },
48
+ );
49
+
50
+ Input.displayName = "Input";
51
+
52
+ export { Input, inputVariants, type InputProps };
@@ -0,0 +1,68 @@
1
+ import { type ComponentPropsWithoutRef, forwardRef } from "react";
2
+
3
+ import { cn } from "@ac/lib/cn";
4
+
5
+ type LogoProps = Omit<ComponentPropsWithoutRef<"svg">, "viewBox" | "xmlns">;
6
+
7
+ /**
8
+ * stasho icon mark (two circles + two arcs).
9
+ * Inherits color from parent via `currentColor`.
10
+ */
11
+ const Logo = forwardRef<SVGSVGElement, LogoProps>(
12
+ ({ className, ...rest }, ref) => (
13
+ <svg
14
+ ref={ref}
15
+ viewBox="0 0 209 209"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ fill="currentColor"
18
+ className={cn("shrink-0", className)}
19
+ {...rest}
20
+ >
21
+ <path d="M170.448 76.895c21.371 0 38.552-17.181 38.552-38.447C209 17.18 191.714 0 170.448 0c-21.372 0-38.552 17.181-38.552 38.448 0 21.266 17.18 38.447 38.552 38.447Z" />
22
+ <path d="M38.553 208.057c21.371 0 38.552-17.181 38.552-38.448 0-21.267-17.286-38.448-38.552-38.448C17.181 131.161 0 148.342 0 169.609c-.104 21.267 17.182 38.448 38.553 38.448Z" />
23
+ <path d="M143.106 11.314C106.544-3.772 62.858 3.457 33.106 33 3.353 62.647-3.875 106.019 11.21 142.476L143.106 11.314Z" />
24
+ <path d="M65.792 196.847c36.562 15.086 80.247 7.857 110-21.686 29.752-29.647 36.98-73.018 21.895-109.475L65.792 196.847Z" />
25
+ </svg>
26
+ ),
27
+ );
28
+
29
+ Logo.displayName = "Logo";
30
+
31
+ /**
32
+ * stasho full logo (icon + "stasho" wordmark).
33
+ * Inherits color from parent via `currentColor`.
34
+ */
35
+ const LogoFull = forwardRef<SVGSVGElement, LogoProps>(
36
+ ({ className, ...rest }, ref) => (
37
+ <svg
38
+ ref={ref}
39
+ viewBox="0 0 1383 229"
40
+ xmlns="http://www.w3.org/2000/svg"
41
+ fill="currentColor"
42
+ className={cn("shrink-0", className)}
43
+ {...rest}
44
+ >
45
+ {/* Wordmark: "stasho" (placeholder — visual rebrand deferred) */}
46
+ <text
47
+ x="261"
48
+ y="180"
49
+ fill="currentColor"
50
+ fontFamily="inherit"
51
+ fontSize="200"
52
+ fontWeight="600"
53
+ >
54
+ stasho
55
+ </text>
56
+ {/* Icon mark */}
57
+ <path d="M170.448 76.895c21.371 0 38.552-17.181 38.552-38.447C209 17.18 191.714 0 170.448 0c-21.372 0-38.552 17.181-38.552 38.448 0 21.266 17.18 38.447 38.552 38.447Z" />
58
+ <path d="M38.553 208.056c21.371 0 38.552-17.18 38.552-38.447 0-21.267-17.286-38.448-38.552-38.448C17.181 131.161 0 148.342 0 169.609c-.104 21.267 17.182 38.447 38.553 38.447Z" />
59
+ <path d="M143.106 11.314C106.544-3.772 62.858 3.457 33.106 33 3.353 62.647-3.875 106.019 11.21 142.476L143.106 11.314Z" />
60
+ <path d="M65.792 196.847c36.562 15.086 80.247 7.857 110-21.686 29.752-29.647 36.98-73.018 21.895-109.475L65.792 196.847Z" />
61
+ </svg>
62
+ ),
63
+ );
64
+
65
+ LogoFull.displayName = "LogoFull";
66
+
67
+ export { Logo, LogoFull };
68
+ export type { LogoProps };
@@ -0,0 +1,312 @@
1
+ import { forwardRef, useState } from "react";
2
+ import { Popover } from "radix-ui";
3
+ import { Command } from "cmdk";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { CaretDown, Check, X } from "@phosphor-icons/react";
6
+ import { cn } from "@ac/lib/cn";
7
+
8
+ const triggerVariants = cva(
9
+ [
10
+ "inline-flex items-center gap-1.5",
11
+ "w-full font-sans text-foreground bg-primary-100 dark:bg-base-700",
12
+ "border-0 rounded-2xl",
13
+ "focus-visible:outline-none focus-visible:ring-3",
14
+ "focus-visible:ring-primary-500",
15
+ "aria-disabled:opacity-50 aria-disabled:pointer-events-none",
16
+ "ring-0 transition-colors",
17
+ ].join(" "),
18
+ {
19
+ variants: {
20
+ size: {
21
+ sm: "min-h-9 py-1 px-3 text-sm",
22
+ md: "min-h-11 py-1.5 px-4 text-base",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ size: "md",
27
+ },
28
+ },
29
+ );
30
+
31
+ const tagVariants = cva(
32
+ [
33
+ "inline-flex items-center gap-1 rounded-full bg-muted",
34
+ "text-foreground max-w-32 select-none",
35
+ ].join(" "),
36
+ {
37
+ variants: {
38
+ size: {
39
+ sm: "px-2 py-0.5 text-xs",
40
+ md: "px-2.5 py-0.5 text-sm",
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ size: "md",
45
+ },
46
+ },
47
+ );
48
+
49
+ type MultiSelectOption = {
50
+ value: string;
51
+ label: string;
52
+ disabled?: boolean;
53
+ };
54
+
55
+ type MultiSelectProps = VariantProps<typeof triggerVariants> & {
56
+ options: MultiSelectOption[];
57
+ value?: string[];
58
+ onValueChange?: (value: string[]) => void;
59
+ placeholder?: string;
60
+ searchPlaceholder?: string;
61
+ emptyMessage?: string;
62
+ maxDisplayedTags?: number;
63
+ size?: "sm" | "md";
64
+ error?: boolean;
65
+ disabled?: boolean;
66
+ className?: string;
67
+ id?: string;
68
+ "aria-describedby"?: string;
69
+ "aria-invalid"?: boolean;
70
+ };
71
+
72
+ const MultiSelect = forwardRef<HTMLDivElement, MultiSelectProps>(
73
+ (
74
+ {
75
+ options,
76
+ value = [],
77
+ onValueChange,
78
+ placeholder = "Select...",
79
+ searchPlaceholder = "Search...",
80
+ emptyMessage = "No results found.",
81
+ maxDisplayedTags = 2,
82
+ size,
83
+ error = false,
84
+ disabled = false,
85
+ className,
86
+ id,
87
+ "aria-describedby": ariaDescribedBy,
88
+ ...rest
89
+ },
90
+ ref,
91
+ ) => {
92
+ const [open, setOpen] = useState(false);
93
+ const [search, setSearch] = useState("");
94
+ const hasSelection = value.length > 0;
95
+
96
+ const selectedOptions = options.filter((o) =>
97
+ value.includes(o.value),
98
+ );
99
+ const displayedTags = selectedOptions.slice(0, maxDisplayedTags);
100
+ const overflowCount =
101
+ selectedOptions.length - displayedTags.length;
102
+
103
+ function toggle(optionValue: string) {
104
+ if (!onValueChange) return;
105
+ const next = value.includes(optionValue)
106
+ ? value.filter((v) => v !== optionValue)
107
+ : [...value, optionValue];
108
+ onValueChange(next);
109
+ setSearch("");
110
+ }
111
+
112
+ function removeTag(
113
+ e: React.MouseEvent,
114
+ optionValue: string,
115
+ ) {
116
+ e.stopPropagation();
117
+ onValueChange?.(value.filter((v) => v !== optionValue));
118
+ }
119
+
120
+ function clearAll(e: React.MouseEvent) {
121
+ e.stopPropagation();
122
+ onValueChange?.([]);
123
+ }
124
+
125
+ return (
126
+ <Popover.Root open={open} onOpenChange={setOpen}>
127
+ <Popover.Trigger asChild>
128
+ <div
129
+ ref={ref}
130
+ id={id}
131
+ role="button"
132
+ tabIndex={disabled ? -1 : 0}
133
+ aria-disabled={disabled || undefined}
134
+ aria-describedby={ariaDescribedBy}
135
+ aria-invalid={error || undefined}
136
+ className={cn(
137
+ triggerVariants({ size }),
138
+ "cursor-pointer",
139
+ error &&
140
+ "border-3 border-error-400 hover:border-error-500",
141
+ !hasSelection && "text-muted-foreground",
142
+ className,
143
+ )}
144
+ {...rest}
145
+ >
146
+ {hasSelection ? (
147
+ <>
148
+ <div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
149
+ {displayedTags.map((opt) => (
150
+ <span
151
+ key={opt.value}
152
+ className={cn(tagVariants({ size }), "shrink-0")}
153
+ >
154
+ <span className="truncate">{opt.label}</span>
155
+ <button
156
+ type="button"
157
+ aria-label={`Remove ${opt.label}`}
158
+ onClick={(e) => removeTag(e, opt.value)}
159
+ className={cn(
160
+ "shrink-0 rounded-full",
161
+ "hover:bg-foreground/10 transition-colors",
162
+ size === "sm" ? "size-3.5" : "size-4",
163
+ )}
164
+ tabIndex={-1}
165
+ >
166
+ <X
167
+ weight="bold"
168
+ className="size-full"
169
+ aria-hidden="true"
170
+ />
171
+ </button>
172
+ </span>
173
+ ))}
174
+ {overflowCount > 0 && (
175
+ <span
176
+ className={cn(
177
+ "text-muted-foreground shrink-0",
178
+ size === "sm" ? "text-xs" : "text-sm",
179
+ )}
180
+ >
181
+ +{overflowCount} more
182
+ </span>
183
+ )}
184
+ </div>
185
+ <button
186
+ type="button"
187
+ aria-label="Clear all"
188
+ onClick={clearAll}
189
+ className={cn(
190
+ "shrink-0 rounded-full",
191
+ "text-muted-foreground",
192
+ "hover:text-foreground transition-colors",
193
+ size === "sm" ? "size-4" : "size-5",
194
+ )}
195
+ tabIndex={-1}
196
+ >
197
+ <X
198
+ weight="bold"
199
+ className="size-full"
200
+ aria-hidden="true"
201
+ />
202
+ </button>
203
+ </>
204
+ ) : (
205
+ <>
206
+ <span className="truncate flex-1 text-left">
207
+ {placeholder}
208
+ </span>
209
+ <CaretDown
210
+ weight="bold"
211
+ className={cn(
212
+ "size-4 shrink-0 text-muted-foreground",
213
+ "transition-transform",
214
+ "motion-reduce:transition-none",
215
+ open && "rotate-180",
216
+ )}
217
+ aria-hidden="true"
218
+ />
219
+ </>
220
+ )}
221
+ </div>
222
+ </Popover.Trigger>
223
+ <Popover.Portal>
224
+ <Popover.Content
225
+ className={cn(
226
+ "z-50 w-[var(--radix-popover-trigger-width)]",
227
+ "overflow-hidden rounded-2xl",
228
+ "bg-surface border border-edge shadow-brand",
229
+ )}
230
+ sideOffset={4}
231
+ align="start"
232
+ onOpenAutoFocus={(e) => e.preventDefault()}
233
+ >
234
+ <Command shouldFilter>
235
+ <Command.Input
236
+ placeholder={searchPlaceholder}
237
+ value={search}
238
+ onValueChange={setSearch}
239
+ className={cn(
240
+ "w-full border-b border-edge bg-transparent",
241
+ "px-4 py-2.5",
242
+ "text-sm text-foreground",
243
+ "placeholder:text-muted-foreground",
244
+ "outline-none",
245
+ )}
246
+ />
247
+ <Command.List className="max-h-60 overflow-y-auto p-1">
248
+ <Command.Empty className="px-4 py-6 text-center text-sm text-muted-foreground">
249
+ {emptyMessage}
250
+ </Command.Empty>
251
+ {options.map((option) => {
252
+ const selected = value.includes(option.value);
253
+ return (
254
+ <Command.Item
255
+ key={option.value}
256
+ value={option.label}
257
+ {...(option.disabled
258
+ ? { disabled: true }
259
+ : {})}
260
+ onSelect={() => toggle(option.value)}
261
+ className={cn(
262
+ "relative flex items-center gap-2",
263
+ "rounded-xl px-3 py-2",
264
+ "text-sm text-foreground",
265
+ "cursor-pointer select-none",
266
+ "outline-none",
267
+ "data-[selected=true]:bg-muted",
268
+ "data-[disabled=true]:opacity-50",
269
+ "data-[disabled=true]:pointer-events-none",
270
+ )}
271
+ >
272
+ <span
273
+ className={cn(
274
+ "flex size-4 shrink-0 items-center",
275
+ "justify-center",
276
+ "rounded border-2 transition-colors",
277
+ selected
278
+ ? "border-primary bg-primary text-primary-foreground"
279
+ : "border-edge bg-surface",
280
+ )}
281
+ aria-hidden="true"
282
+ >
283
+ {selected && (
284
+ <Check
285
+ weight="bold"
286
+ className="size-3"
287
+ />
288
+ )}
289
+ </span>
290
+ <span className="flex-1">
291
+ {option.label}
292
+ </span>
293
+ </Command.Item>
294
+ );
295
+ })}
296
+ </Command.List>
297
+ </Command>
298
+ </Popover.Content>
299
+ </Popover.Portal>
300
+ </Popover.Root>
301
+ );
302
+ },
303
+ );
304
+
305
+ MultiSelect.displayName = "MultiSelect";
306
+
307
+ export {
308
+ MultiSelect,
309
+ triggerVariants,
310
+ type MultiSelectProps,
311
+ type MultiSelectOption,
312
+ };