@rovula/ui 0.1.20 → 0.1.22

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 (99) hide show
  1. package/dist/cjs/bundle.css +316 -43
  2. package/dist/cjs/bundle.js +675 -675
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Badge/Badge.d.ts +40 -0
  5. package/dist/cjs/types/components/Badge/Badge.stories.d.ts +295 -0
  6. package/dist/cjs/types/components/Badge/Badge.styles.d.ts +7 -0
  7. package/dist/cjs/types/components/Badge/index.d.ts +2 -0
  8. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +4 -8
  9. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
  10. package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
  11. package/dist/cjs/types/components/DropdownMenu/DropdownMenu.stories.d.ts +45 -30
  12. package/dist/cjs/types/components/Form/Form.d.ts +2 -1
  13. package/dist/cjs/types/components/Form/Form.stories.d.ts +4 -0
  14. package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +38 -0
  15. package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +301 -0
  16. package/dist/cjs/types/index.d.ts +4 -1
  17. package/dist/cjs/types/patterns/menu/Menu.d.ts +70 -0
  18. package/dist/cjs/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
  19. package/dist/cjs/types/utils/mergeRefs.d.ts +20 -0
  20. package/dist/components/Avatar/Avatar.styles.js +2 -2
  21. package/dist/components/Badge/Badge.js +36 -0
  22. package/dist/components/Badge/Badge.stories.js +51 -0
  23. package/dist/components/Badge/Badge.styles.js +62 -0
  24. package/dist/components/Badge/index.js +2 -0
  25. package/dist/components/Dropdown/Dropdown.js +54 -163
  26. package/dist/components/Dropdown/Dropdown.stories.js +29 -0
  27. package/dist/components/DropdownMenu/DropdownMenu.js +24 -13
  28. package/dist/components/DropdownMenu/DropdownMenu.stories.js +120 -88
  29. package/dist/components/Form/Form.js +11 -4
  30. package/dist/components/Form/Form.stories.js +27 -0
  31. package/dist/components/ScrollArea/ScrollArea.js +50 -0
  32. package/dist/components/ScrollArea/ScrollArea.stories.js +56 -0
  33. package/dist/components/TextInput/TextInput.js +6 -3
  34. package/dist/esm/bundle.css +316 -43
  35. package/dist/esm/bundle.js +1545 -1545
  36. package/dist/esm/bundle.js.map +1 -1
  37. package/dist/esm/types/components/Badge/Badge.d.ts +40 -0
  38. package/dist/esm/types/components/Badge/Badge.stories.d.ts +295 -0
  39. package/dist/esm/types/components/Badge/Badge.styles.d.ts +7 -0
  40. package/dist/esm/types/components/Badge/index.d.ts +2 -0
  41. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +4 -8
  42. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
  43. package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
  44. package/dist/esm/types/components/DropdownMenu/DropdownMenu.stories.d.ts +45 -30
  45. package/dist/esm/types/components/Form/Form.d.ts +2 -1
  46. package/dist/esm/types/components/Form/Form.stories.d.ts +4 -0
  47. package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +38 -0
  48. package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +301 -0
  49. package/dist/esm/types/index.d.ts +4 -1
  50. package/dist/esm/types/patterns/menu/Menu.d.ts +70 -0
  51. package/dist/esm/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
  52. package/dist/esm/types/utils/mergeRefs.d.ts +20 -0
  53. package/dist/index.d.ts +156 -74
  54. package/dist/index.js +3 -1
  55. package/dist/patterns/menu/Menu.js +95 -0
  56. package/dist/patterns/menu/Menu.stories.js +611 -0
  57. package/dist/src/theme/global.css +485 -57
  58. package/dist/utils/mergeRefs.js +42 -0
  59. package/package.json +1 -1
  60. package/src/components/Avatar/Avatar.styles.ts +2 -2
  61. package/src/components/Badge/Badge.stories.tsx +128 -0
  62. package/src/components/Badge/Badge.styles.ts +70 -0
  63. package/src/components/Badge/Badge.tsx +103 -0
  64. package/src/components/Badge/index.ts +3 -0
  65. package/src/components/Dropdown/Dropdown.stories.tsx +170 -1
  66. package/src/components/Dropdown/Dropdown.tsx +186 -276
  67. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1375 -253
  68. package/src/components/DropdownMenu/DropdownMenu.tsx +118 -55
  69. package/src/components/Form/Form.stories.tsx +70 -0
  70. package/src/components/Form/Form.tsx +23 -0
  71. package/src/components/ScrollArea/ScrollArea.stories.tsx +229 -0
  72. package/src/components/ScrollArea/ScrollArea.tsx +72 -0
  73. package/src/components/TextInput/TextInput.tsx +6 -3
  74. package/src/index.ts +4 -1
  75. package/src/patterns/menu/Menu.stories.tsx +1100 -0
  76. package/src/patterns/menu/Menu.tsx +282 -0
  77. package/src/theme/global.css +84 -11
  78. package/src/theme/themes/xspector/baseline.css +1 -1
  79. package/src/theme/themes/xspector/components/scrollbar.css +12 -0
  80. package/src/theme/tokens/baseline.css +3 -1
  81. package/src/theme/tokens/components/badge.css +54 -0
  82. package/src/theme/tokens/components/dropdown-menu.css +16 -5
  83. package/src/theme/tokens/components/scrollbar.css +18 -0
  84. package/src/utils/mergeRefs.ts +46 -0
  85. package/dist/cjs/types/components/Menu/Menu.d.ts +0 -65
  86. package/dist/cjs/types/components/Menu/helpers.d.ts +0 -19
  87. package/dist/cjs/types/components/Menu/index.d.ts +0 -4
  88. package/dist/components/Menu/Menu.js +0 -64
  89. package/dist/components/Menu/Menu.stories.js +0 -406
  90. package/dist/components/Menu/helpers.js +0 -28
  91. package/dist/components/Menu/index.js +0 -3
  92. package/dist/esm/types/components/Menu/Menu.d.ts +0 -65
  93. package/dist/esm/types/components/Menu/helpers.d.ts +0 -19
  94. package/dist/esm/types/components/Menu/index.d.ts +0 -4
  95. package/src/components/Menu/Menu.stories.tsx +0 -586
  96. package/src/components/Menu/Menu.tsx +0 -235
  97. package/src/components/Menu/helpers.ts +0 -45
  98. package/src/components/Menu/index.ts +0 -7
  99. package/src/theme/themes/xspector/components/dropdown-menu.css +0 -28
@@ -0,0 +1,42 @@
1
+ import { useCallback, useRef } from "react";
2
+ /**
3
+ * Merges multiple refs into a single callback ref.
4
+ * NOTE: This creates a new function on every call — do NOT use inline in render.
5
+ * Use `useStableMergedRef` instead when you need a stable ref identity.
6
+ */
7
+ export function mergeRefs(...refs) {
8
+ return (node) => {
9
+ refs.forEach((ref) => {
10
+ if (!ref)
11
+ return;
12
+ if (typeof ref === "function") {
13
+ ref(node);
14
+ }
15
+ else {
16
+ ref.current = node;
17
+ }
18
+ });
19
+ };
20
+ }
21
+ /**
22
+ * Returns a **stable** callback ref (never changes identity) that forwards the
23
+ * node to all given refs. Safe to use inline in JSX — will not cause
24
+ * detach/re-attach loops in libraries like Headless UI that watch refs.
25
+ *
26
+ * @example
27
+ * const MyInput = forwardRef((props, ref) => {
28
+ * const internalRef = useRef(null);
29
+ * const stableRef = useStableMergedRef(ref, internalRef);
30
+ * return <input ref={stableRef} />;
31
+ * });
32
+ */
33
+ export function useStableMergedRef(...refs) {
34
+ // Store the latest merge logic in a ref so the callback never goes stale,
35
+ // while the callback itself keeps a stable identity (empty useCallback deps).
36
+ const latestImpl = useRef();
37
+ latestImpl.current = mergeRefs(...refs);
38
+ return useCallback((node) => {
39
+ var _a;
40
+ (_a = latestImpl.current) === null || _a === void 0 ? void 0 : _a.call(latestImpl, node);
41
+ }, []); // ← empty deps = identity never changes = no detach/re-attach loop
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,12 +2,12 @@ import { cva } from "class-variance-authority";
2
2
 
3
3
  export const avatarVariants = cva(
4
4
  [
5
- "flex items-center justify-center bg-grey2-700 text-common-black typography-subtitle2 truncate",
5
+ "flex items-center justify-center bg-grey2-900 text-text-white typography-subtitle6 truncate",
6
6
  ],
7
7
  {
8
8
  variants: {
9
9
  size: {
10
- xxs: "w-[24px] h-[24px] typography-subtitle3",
10
+ xxs: "w-[24px] h-[24px] typography-small3",
11
11
  xs: "w-[32px] h-[32px]",
12
12
  sm: "w-[40px] h-[40px]",
13
13
  md: "w-[48px] h-[48px]",
@@ -0,0 +1,128 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Badge, SeverityBadge } from "./Badge";
4
+ import type { BadgeColor, SeverityLevel } from "./Badge";
5
+
6
+ const meta = {
7
+ title: "Components/Badge",
8
+ component: Badge,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "fullscreen",
12
+ },
13
+ decorators: [
14
+ (Story) => (
15
+ <div className="p-8 bg-bg-bg1">
16
+ <Story />
17
+ </div>
18
+ ),
19
+ ],
20
+ } satisfies Meta<typeof Badge>;
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Default — single badge with controls
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ label: "To do",
32
+ color: "default",
33
+ clickable: false,
34
+ },
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Status Badges — all colors × clickable / static
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const COLORS: BadgeColor[] = ["default", "warning", "info", "error", "success"];
42
+
43
+ const COLOR_LABELS: Record<BadgeColor, string> = {
44
+ default: "To do",
45
+ warning: "In Progress",
46
+ info: "Ready to review",
47
+ error: "In review",
48
+ success: "Completed",
49
+ };
50
+
51
+ export const StatusBadges: StoryObj = {
52
+ render: () => (
53
+ <div className="flex flex-col gap-6">
54
+ {/* Clickable (with border + chevron) */}
55
+ <div>
56
+ <p className="typography-small1 text-text-g-contrast-low mb-3">
57
+ Clickable
58
+ </p>
59
+ <div className="flex flex-wrap gap-3">
60
+ {COLORS.map((color) => (
61
+ <Badge
62
+ key={color}
63
+ color={color}
64
+ label={COLOR_LABELS[color]}
65
+ clickable
66
+ />
67
+ ))}
68
+ </div>
69
+ </div>
70
+
71
+ {/* Static (no border, no chevron) */}
72
+ <div>
73
+ <p className="typography-small1 text-text-g-contrast-low mb-3">
74
+ Static
75
+ </p>
76
+ <div className="flex flex-wrap gap-3">
77
+ {COLORS.map((color) => (
78
+ <Badge key={color} color={color} label={COLOR_LABELS[color]} />
79
+ ))}
80
+ </div>
81
+ </div>
82
+
83
+ {/* Percent only */}
84
+ <div>
85
+ <p className="typography-small1 text-text-g-contrast-low mb-3">
86
+ With percentage
87
+ </p>
88
+ <div className="flex flex-wrap gap-3">
89
+ {COLORS.map((color, i) => (
90
+ <Badge
91
+ key={color}
92
+ color={color}
93
+ label={COLOR_LABELS[color]}
94
+ percent={i === 0 ? 0 : i === 4 ? 100 : 50}
95
+ />
96
+ ))}
97
+ </div>
98
+ </div>
99
+ </div>
100
+ ),
101
+ };
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Severity Badges
105
+ // ---------------------------------------------------------------------------
106
+
107
+ const SEVERITIES: SeverityLevel[] = [
108
+ "highest",
109
+ "high",
110
+ "medium",
111
+ "low",
112
+ "lowest",
113
+ ];
114
+
115
+ export const SeverityBadges: StoryObj = {
116
+ render: () => (
117
+ <div className="flex flex-col gap-3">
118
+ <p className="typography-small1 text-text-g-contrast-low mb-1">
119
+ Severity levels
120
+ </p>
121
+ <div className="flex flex-wrap gap-3">
122
+ {SEVERITIES.map((severity) => (
123
+ <SeverityBadge key={severity} severity={severity} />
124
+ ))}
125
+ </div>
126
+ </div>
127
+ ),
128
+ };
@@ -0,0 +1,70 @@
1
+ import { cva } from "class-variance-authority";
2
+
3
+ export const badgeVariants = cva(
4
+ [
5
+ "inline-flex items-center justify-center rounded-lg px-3 py-1",
6
+ "typography-body3",
7
+ ],
8
+ {
9
+ variants: {
10
+ color: {
11
+ default: [
12
+ "bg-[var(--badge-default-bg)]",
13
+ "text-[var(--badge-default-text)]",
14
+ ],
15
+ success: [
16
+ "bg-[var(--badge-success-bg)]",
17
+ "text-[var(--badge-success-text)]",
18
+ ],
19
+ warning: [
20
+ "bg-[var(--badge-warning-bg)]",
21
+ "text-[var(--badge-warning-text)]",
22
+ ],
23
+ info: [
24
+ "bg-[var(--badge-info-bg)]",
25
+ "text-[var(--badge-info-text)]",
26
+ ],
27
+ error: [
28
+ "bg-[var(--badge-error-bg)]",
29
+ "text-[var(--badge-error-text)]",
30
+ ],
31
+ },
32
+ clickable: {
33
+ true: "border border-solid",
34
+ false: "",
35
+ },
36
+ },
37
+ compoundVariants: [
38
+ { color: "default", clickable: true, className: "border-[var(--badge-default-border)]" },
39
+ { color: "success", clickable: true, className: "border-[var(--badge-success-border)]" },
40
+ { color: "warning", clickable: true, className: "border-[var(--badge-warning-border)]" },
41
+ { color: "info", clickable: true, className: "border-[var(--badge-info-border)]" },
42
+ { color: "error", clickable: true, className: "border-[var(--badge-error-border)]" },
43
+ ],
44
+ defaultVariants: {
45
+ color: "default",
46
+ clickable: false,
47
+ },
48
+ },
49
+ );
50
+
51
+ export const severityBadgeVariants = cva(
52
+ [
53
+ "inline-flex items-center justify-center rounded px-1 py-0.5",
54
+ "typography-small6 text-[var(--badge-severity-text)]",
55
+ ],
56
+ {
57
+ variants: {
58
+ severity: {
59
+ highest: "bg-[var(--badge-severity-highest-bg)]",
60
+ high: "bg-[var(--badge-severity-high-bg)]",
61
+ medium: "bg-[var(--badge-severity-medium-bg)]",
62
+ low: "bg-[var(--badge-severity-low-bg)]",
63
+ lowest: "bg-[var(--badge-severity-lowest-bg)]",
64
+ },
65
+ },
66
+ defaultVariants: {
67
+ severity: "medium",
68
+ },
69
+ },
70
+ );
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronDownIcon } from "@heroicons/react/16/solid";
5
+ import { cn } from "@/utils/cn";
6
+ import { badgeVariants, severityBadgeVariants } from "./Badge.styles";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Status Badge
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export type BadgeColor = "default" | "success" | "warning" | "info" | "error";
13
+
14
+ export type BadgeProps = {
15
+ /** Badge text label */
16
+ label: string;
17
+ /** Color variant */
18
+ color?: BadgeColor;
19
+ /**
20
+ * Show a dropdown chevron — use when the badge acts as a clickable trigger.
21
+ * Automatically adds a border to indicate interactivity.
22
+ */
23
+ clickable?: boolean;
24
+ /** Optional percentage value shown below the label */
25
+ percent?: number;
26
+ className?: string;
27
+ } & React.HTMLAttributes<HTMLSpanElement>;
28
+
29
+ const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
30
+ (
31
+ {
32
+ label,
33
+ color = "default",
34
+ clickable = false,
35
+ percent,
36
+ className,
37
+ ...props
38
+ },
39
+ ref,
40
+ ) => {
41
+ const hasPercent = percent !== undefined;
42
+
43
+ return (
44
+ <span
45
+ ref={ref}
46
+ className={cn(
47
+ badgeVariants({ color, clickable }),
48
+ hasPercent && "flex-col gap-0.5",
49
+ clickable && "cursor-pointer gap-1",
50
+ className,
51
+ )}
52
+ {...props}
53
+ >
54
+ <span className="flex items-center gap-1">
55
+ {label}
56
+ {clickable && (
57
+ <ChevronDownIcon className="size-4 shrink-0" aria-hidden />
58
+ )}
59
+ </span>
60
+ {hasPercent && (
61
+ <span className="tabular-nums">
62
+ {percent}%
63
+ </span>
64
+ )}
65
+ </span>
66
+ );
67
+ },
68
+ );
69
+ Badge.displayName = "Badge";
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Severity Badge
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export type SeverityLevel = "highest" | "high" | "medium" | "low" | "lowest";
76
+
77
+ export type SeverityBadgeProps = {
78
+ severity: SeverityLevel;
79
+ className?: string;
80
+ } & React.HTMLAttributes<HTMLSpanElement>;
81
+
82
+ const SEVERITY_LABELS: Record<SeverityLevel, string> = {
83
+ highest: "Highest",
84
+ high: "High",
85
+ medium: "Medium",
86
+ low: "Low",
87
+ lowest: "Lowest",
88
+ };
89
+
90
+ const SeverityBadge = React.forwardRef<HTMLSpanElement, SeverityBadgeProps>(
91
+ ({ severity, className, ...props }, ref) => (
92
+ <span
93
+ ref={ref}
94
+ className={cn(severityBadgeVariants({ severity }), className)}
95
+ {...props}
96
+ >
97
+ {SEVERITY_LABELS[severity]}
98
+ </span>
99
+ ),
100
+ );
101
+ SeverityBadge.displayName = "SeverityBadge";
102
+
103
+ export { Badge, SeverityBadge };
@@ -0,0 +1,3 @@
1
+ import { Badge, SeverityBadge } from "./Badge";
2
+
3
+ export { Badge, SeverityBadge };
@@ -3,8 +3,18 @@ import type { Meta, StoryObj } from "@storybook/react";
3
3
  import Dropdown, { Options } from "./Dropdown";
4
4
  import Button from "../Button/Button";
5
5
  import { cn } from "@/utils/cn";
6
- import { MenuItemType } from "../Menu/Menu";
7
6
  import Icon from "../Icon/Icon";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogDescription,
13
+ DialogBody,
14
+ DialogFooter,
15
+ DialogTrigger,
16
+ DialogClose,
17
+ } from "../Dialog/Dialog";
8
18
 
9
19
  // More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
10
20
  const meta = {
@@ -198,3 +208,162 @@ export const WithIcons: StoryObj<typeof Dropdown> = {
198
208
  );
199
209
  },
200
210
  };
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Dropdown inside Dialog — showcases the fix for overflow:hidden bug
214
+ // ---------------------------------------------------------------------------
215
+
216
+ const dialogOptions: Options[] = [
217
+ { value: "design", label: "Design" },
218
+ { value: "engineering", label: "Engineering" },
219
+ { value: "product", label: "Product" },
220
+ { value: "marketing", label: "Marketing" },
221
+ { value: "data", label: "Data & Analytics" },
222
+ { value: "ops", label: "Operations" },
223
+ ];
224
+
225
+ const filterableOptions: Options[] = new Array(20).fill("").map((_, i) => ({
226
+ value: `member-${i + 1}`,
227
+ label: `Team Member ${i + 1}`,
228
+ }));
229
+
230
+ export const InsideDialog: StoryObj<typeof Dropdown> = {
231
+ name: "Inside Dialog",
232
+ render: () => {
233
+ const [department, setDepartment] = useState<Options | undefined>();
234
+ const [member, setMember] = useState<Options | undefined>();
235
+ const [role, setRole] = useState<Options | undefined>();
236
+
237
+ return (
238
+ <div className="flex gap-4 flex-wrap">
239
+ {/* Demo 1: Single dropdown in dialog */}
240
+ <div>
241
+ <p className="typography-small4 text-text-g-contrast-medium mb-2">
242
+ Single Dropdown
243
+ </p>
244
+ <Dialog>
245
+ <DialogTrigger asChild>
246
+ <Button variant="outline">Open Dialog</Button>
247
+ </DialogTrigger>
248
+ <DialogContent showCloseButton>
249
+ <DialogHeader>
250
+ <DialogTitle>Assign to Department</DialogTitle>
251
+ <DialogDescription>
252
+ Dropdown popup appears above the dialog overlay — not clipped
253
+ by overflow.
254
+ </DialogDescription>
255
+ </DialogHeader>
256
+ <DialogBody className="gap-4 py-2">
257
+ <Dropdown
258
+ id="dept"
259
+ label="Department"
260
+ size="md"
261
+ fullwidth
262
+ options={dialogOptions}
263
+ value={department}
264
+ onSelect={setDepartment}
265
+ />
266
+ </DialogBody>
267
+ <DialogFooter>
268
+ <DialogClose asChild>
269
+ <Button variant="outline">Cancel</Button>
270
+ </DialogClose>
271
+ <Button>Confirm</Button>
272
+ </DialogFooter>
273
+ </DialogContent>
274
+ </Dialog>
275
+ </div>
276
+
277
+ {/* Demo 2: Multiple dropdowns — each opens independently */}
278
+ <div>
279
+ <p className="typography-small4 text-text-g-contrast-medium mb-2">
280
+ Multiple Dropdowns
281
+ </p>
282
+ <Dialog>
283
+ <DialogTrigger asChild>
284
+ <Button variant="outline">Open Dialog</Button>
285
+ </DialogTrigger>
286
+ <DialogContent showCloseButton>
287
+ <DialogHeader>
288
+ <DialogTitle>Invite Team Member</DialogTitle>
289
+ <DialogDescription>
290
+ Multiple dropdowns — each opens its own popup independently.
291
+ </DialogDescription>
292
+ </DialogHeader>
293
+ <DialogBody className="gap-4 py-2">
294
+ <Dropdown
295
+ id="member"
296
+ label="Member"
297
+ size="md"
298
+ fullwidth
299
+ options={filterableOptions}
300
+ value={member}
301
+ onSelect={setMember}
302
+ filterMode
303
+ />
304
+ <Dropdown
305
+ id="role"
306
+ label="Role"
307
+ size="md"
308
+ fullwidth
309
+ options={[
310
+ { value: "viewer", label: "Viewer" },
311
+ { value: "editor", label: "Editor" },
312
+ { value: "admin", label: "Admin" },
313
+ ]}
314
+ value={role}
315
+ onSelect={setRole}
316
+ />
317
+ </DialogBody>
318
+ <DialogFooter>
319
+ <DialogClose asChild>
320
+ <Button variant="outline">Cancel</Button>
321
+ </DialogClose>
322
+ <Button disabled={!member || !role}>Send Invite</Button>
323
+ </DialogFooter>
324
+ </DialogContent>
325
+ </Dialog>
326
+ </div>
327
+
328
+ {/* Demo 3: Filterable dropdown in dialog */}
329
+ <div>
330
+ <p className="typography-small4 text-text-g-contrast-medium mb-2">
331
+ Filter Mode
332
+ </p>
333
+ <Dialog>
334
+ <DialogTrigger asChild>
335
+ <Button variant="outline">Open Dialog</Button>
336
+ </DialogTrigger>
337
+ <DialogContent showCloseButton>
338
+ <DialogHeader>
339
+ <DialogTitle>Search & Select</DialogTitle>
340
+ <DialogDescription>
341
+ filterMode=true — type to filter options, popup stays properly
342
+ positioned.
343
+ </DialogDescription>
344
+ </DialogHeader>
345
+ <DialogBody className="gap-4 py-2">
346
+ <Dropdown
347
+ id="member-filter"
348
+ label="Search member"
349
+ size="md"
350
+ fullwidth
351
+ filterMode
352
+ options={filterableOptions}
353
+ value={member}
354
+ onSelect={setMember}
355
+ />
356
+ </DialogBody>
357
+ <DialogFooter>
358
+ <DialogClose asChild>
359
+ <Button variant="outline">Cancel</Button>
360
+ </DialogClose>
361
+ <Button disabled={!member}>Select</Button>
362
+ </DialogFooter>
363
+ </DialogContent>
364
+ </Dialog>
365
+ </div>
366
+ </div>
367
+ );
368
+ },
369
+ };