@firecms/ui 3.0.1 → 3.1.0-canary.02232f4
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/README.md +9 -7
- package/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Chip.d.ts +1 -1
- package/dist/components/ColorPicker.d.ts +30 -0
- package/dist/components/DateTimeField.d.ts +7 -0
- package/dist/components/Dialog.d.ts +2 -1
- package/dist/components/FileUpload.d.ts +1 -1
- package/dist/components/Menu.d.ts +2 -1
- package/dist/components/Menubar.d.ts +2 -1
- package/dist/components/MultiSelect.d.ts +2 -1
- package/dist/components/ResizablePanels.d.ts +16 -0
- package/dist/components/SearchBar.d.ts +11 -1
- package/dist/components/SearchableSelect.d.ts +48 -0
- package/dist/components/Select.d.ts +2 -1
- package/dist/components/Sheet.d.ts +1 -0
- package/dist/components/Tabs.d.ts +8 -1
- package/dist/components/ToggleButtonGroup.d.ts +30 -0
- package/dist/components/Tooltip.d.ts +18 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/hooks/PortalContainerContext.d.ts +31 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useOutsideAlerter.d.ts +1 -1
- package/dist/icons/FirestoreIcon.d.ts +6 -0
- package/dist/icons/components/DatabaseIcon.d.ts +6 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/index.css +57 -6
- package/dist/index.es.js +2846 -1165
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2846 -1165
- package/dist/index.umd.js.map +1 -1
- package/dist/styles.d.ts +11 -11
- package/package.json +7 -7
- package/src/components/BooleanSwitch.tsx +3 -3
- package/src/components/BooleanSwitchWithLabel.tsx +4 -0
- package/src/components/Button.tsx +6 -5
- package/src/components/Card.tsx +7 -7
- package/src/components/Checkbox.tsx +1 -1
- package/src/components/Chip.tsx +4 -3
- package/src/components/ColorPicker.tsx +134 -0
- package/src/components/DateTimeField.tsx +129 -35
- package/src/components/DebouncedTextField.tsx +3 -3
- package/src/components/Dialog.tsx +25 -16
- package/src/components/DialogActions.tsx +1 -1
- package/src/components/ExpandablePanel.tsx +1 -1
- package/src/components/FileUpload.tsx +25 -24
- package/src/components/IconButton.tsx +3 -2
- package/src/components/Menu.tsx +44 -30
- package/src/components/Menubar.tsx +14 -3
- package/src/components/MultiSelect.tsx +113 -77
- package/src/components/Popover.tsx +11 -3
- package/src/components/ResizablePanels.tsx +181 -0
- package/src/components/SearchBar.tsx +37 -19
- package/src/components/SearchableSelect.tsx +335 -0
- package/src/components/Select.tsx +86 -73
- package/src/components/Separator.tsx +2 -2
- package/src/components/Sheet.tsx +12 -3
- package/src/components/Skeleton.tsx +4 -2
- package/src/components/Slider.tsx +4 -4
- package/src/components/Table.tsx +1 -1
- package/src/components/Tabs.tsx +150 -37
- package/src/components/TextField.tsx +19 -8
- package/src/components/TextareaAutosize.tsx +77 -212
- package/src/components/ToggleButtonGroup.tsx +67 -0
- package/src/components/Tooltip.tsx +16 -8
- package/src/components/index.tsx +4 -0
- package/src/hooks/PortalContainerContext.tsx +48 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useInjectStyles.tsx +12 -3
- package/src/hooks/useOutsideAlerter.tsx +1 -1
- package/src/icons/FirestoreIcon.tsx +47 -0
- package/src/icons/components/DatabaseIcon.tsx +10 -0
- package/src/icons/index.ts +2 -0
- package/src/index.css +57 -6
- package/src/styles.ts +11 -11
- package/src/util/cls.ts +1 -1
- package/tailwind.config.js +2 -3
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
6
|
+
import { cls } from "../util";
|
|
7
|
+
import { CheckIcon, KeyboardArrowDownIcon } from "../icons";
|
|
8
|
+
import { Separator } from "./Separator";
|
|
9
|
+
import {
|
|
10
|
+
defaultBorderMixin,
|
|
11
|
+
fieldBackgroundDisabledMixin,
|
|
12
|
+
fieldBackgroundHoverMixin,
|
|
13
|
+
fieldBackgroundInvisibleMixin,
|
|
14
|
+
fieldBackgroundMixin,
|
|
15
|
+
focusedDisabled
|
|
16
|
+
} from "../styles";
|
|
17
|
+
import { useInjectStyles } from "../hooks";
|
|
18
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
19
|
+
|
|
20
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface SearchableSelectProps {
|
|
23
|
+
/** Currently selected value. Can be one of the items or a custom string. */
|
|
24
|
+
value?: string;
|
|
25
|
+
/** Callback when the value changes (from selection or custom input). */
|
|
26
|
+
onValueChange?: (value: string) => void;
|
|
27
|
+
/** Placeholder shown when no value is selected. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Label above the field. */
|
|
30
|
+
label?: React.ReactNode | string;
|
|
31
|
+
/** Size variant. */
|
|
32
|
+
size?: "smallest" | "small" | "medium" | "large";
|
|
33
|
+
/** Whether the field is disabled. */
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
/** Whether to show an error state. */
|
|
36
|
+
error?: boolean;
|
|
37
|
+
/** Whether to use the invisible (borderless) style. */
|
|
38
|
+
invisible?: boolean;
|
|
39
|
+
/** CSS class for the trigger button. */
|
|
40
|
+
className?: string;
|
|
41
|
+
/** CSS class for the trigger input area. */
|
|
42
|
+
inputClassName?: string;
|
|
43
|
+
/** Render the selected value in a custom way in the trigger. */
|
|
44
|
+
renderValue?: (value: string) => React.ReactNode;
|
|
45
|
+
/** Whether the popover should trap focus. */
|
|
46
|
+
modalPopover?: boolean;
|
|
47
|
+
/** If true, allow accepting the typed text as the value even if it doesn't match an item. */
|
|
48
|
+
allowCustomValues?: boolean;
|
|
49
|
+
/** Portal container element. */
|
|
50
|
+
portalContainer?: HTMLElement | null;
|
|
51
|
+
/** If true, auto-open the popover on mount so the user can start typing immediately. */
|
|
52
|
+
autoFocus?: boolean;
|
|
53
|
+
/** The option items — use SearchableSelectItem. */
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SearchableSelectItemProps {
|
|
58
|
+
value: string;
|
|
59
|
+
children?: React.ReactNode;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Component ──────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const SearchableSelect = React.forwardRef<
|
|
66
|
+
HTMLButtonElement,
|
|
67
|
+
SearchableSelectProps
|
|
68
|
+
>(
|
|
69
|
+
(
|
|
70
|
+
{
|
|
71
|
+
value,
|
|
72
|
+
onValueChange,
|
|
73
|
+
placeholder = "Select...",
|
|
74
|
+
label,
|
|
75
|
+
size = "large",
|
|
76
|
+
disabled,
|
|
77
|
+
error,
|
|
78
|
+
invisible,
|
|
79
|
+
className,
|
|
80
|
+
inputClassName,
|
|
81
|
+
renderValue,
|
|
82
|
+
modalPopover = false,
|
|
83
|
+
allowCustomValues = true,
|
|
84
|
+
portalContainer,
|
|
85
|
+
autoFocus,
|
|
86
|
+
children,
|
|
87
|
+
},
|
|
88
|
+
ref
|
|
89
|
+
) => {
|
|
90
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
91
|
+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
92
|
+
const [search, setSearch] = useState("");
|
|
93
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
94
|
+
|
|
95
|
+
const contextContainer = usePortalContainer();
|
|
96
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setIsMounted(true);
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
// Auto-open popover on mount when autoFocus is true
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (autoFocus && isMounted) {
|
|
105
|
+
onPopoverOpenChange(true);
|
|
106
|
+
}
|
|
107
|
+
}, [autoFocus, isMounted]);
|
|
108
|
+
|
|
109
|
+
// Collect all item values + labels from children
|
|
110
|
+
const itemsMap = React.useMemo(() => {
|
|
111
|
+
const map = new Map<string, React.ReactNode>();
|
|
112
|
+
React.Children.forEach(children, (child) => {
|
|
113
|
+
if (React.isValidElement<SearchableSelectItemProps>(child) && child.props.value != null) {
|
|
114
|
+
map.set(String(child.props.value), child.props.children ?? child.props.value);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return map;
|
|
118
|
+
}, [children]);
|
|
119
|
+
|
|
120
|
+
const onPopoverOpenChange = (open: boolean) => {
|
|
121
|
+
setIsPopoverOpen(open);
|
|
122
|
+
if (open) {
|
|
123
|
+
// Pre-fill search with current value for easy editing
|
|
124
|
+
setSearch(value ?? "");
|
|
125
|
+
// Focus the input after popover opens
|
|
126
|
+
setTimeout(() => inputRef.current?.focus(), 0);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleSelect = (selectedValue: string) => {
|
|
131
|
+
onValueChange?.(selectedValue);
|
|
132
|
+
setIsPopoverOpen(false);
|
|
133
|
+
setSearch("");
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
137
|
+
if (e.key === "Enter" && allowCustomValues) {
|
|
138
|
+
const trimmed = search.trim();
|
|
139
|
+
if (trimmed) {
|
|
140
|
+
// If cmdk found no match, accept custom value
|
|
141
|
+
// If there are matches, cmdk will handle selecting the highlighted one
|
|
142
|
+
// We check if the current search is NOT one of the items
|
|
143
|
+
const isExistingItem = itemsMap.has(trimmed);
|
|
144
|
+
if (!isExistingItem) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
handleSelect(trimmed);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else if (e.key === "Escape") {
|
|
150
|
+
setIsPopoverOpen(false);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Resolve display label for the trigger
|
|
155
|
+
const displayLabel = React.useMemo(() => {
|
|
156
|
+
if (!value) return null;
|
|
157
|
+
if (renderValue) return renderValue(value);
|
|
158
|
+
const itemLabel = itemsMap.get(value);
|
|
159
|
+
if (itemLabel) return itemLabel;
|
|
160
|
+
return <span className="text-sm">{value}</span>;
|
|
161
|
+
}, [value, renderValue, itemsMap]);
|
|
162
|
+
|
|
163
|
+
useInjectStyles("SearchableSelect", `
|
|
164
|
+
[cmdk-group] {
|
|
165
|
+
max-height: 45vh;
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
}`);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div>
|
|
171
|
+
{label && (
|
|
172
|
+
typeof label === "string"
|
|
173
|
+
? <div className={cls("text-sm font-medium ml-3.5 mb-1",
|
|
174
|
+
error ? "text-red-500 dark:text-red-600" : "text-surface-accent-500 dark:text-surface-accent-300",
|
|
175
|
+
)}>{label}</div>
|
|
176
|
+
: label
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
<PopoverPrimitive.Root
|
|
180
|
+
open={isMounted && isPopoverOpen}
|
|
181
|
+
onOpenChange={onPopoverOpenChange}
|
|
182
|
+
modal={modalPopover}
|
|
183
|
+
>
|
|
184
|
+
<PopoverPrimitive.Trigger asChild>
|
|
185
|
+
<button
|
|
186
|
+
ref={ref}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
onClick={() => !disabled && onPopoverOpenChange(!isPopoverOpen)}
|
|
189
|
+
className={cls(
|
|
190
|
+
{
|
|
191
|
+
"min-h-[28px]": size === "smallest",
|
|
192
|
+
"min-h-[32px]": size === "small",
|
|
193
|
+
"min-h-[44px]": size === "medium",
|
|
194
|
+
"min-h-[64px]": size === "large",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"py-0.5": size === "smallest",
|
|
198
|
+
"py-1": size === "small",
|
|
199
|
+
"py-2": size === "medium" || size === "large",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"px-2": size === "small" || size === "smallest",
|
|
203
|
+
"px-4": size === "medium" || size === "large",
|
|
204
|
+
},
|
|
205
|
+
"select-none rounded-md text-sm w-full text-start",
|
|
206
|
+
"focus:ring-0 focus-visible:ring-0 outline-none focus:outline-none focus-visible:outline-none",
|
|
207
|
+
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
208
|
+
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
209
|
+
"relative flex items-center",
|
|
210
|
+
className,
|
|
211
|
+
inputClassName
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
214
|
+
<div className="flex items-center justify-between w-full gap-1">
|
|
215
|
+
<div className="flex-grow min-w-0 truncate">
|
|
216
|
+
{displayLabel ?? (
|
|
217
|
+
<span className="text-sm text-surface-accent-500 dark:text-surface-accent-400">
|
|
218
|
+
{placeholder}
|
|
219
|
+
</span>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
<div className={cls("flex-shrink-0 flex items-center")}>
|
|
223
|
+
<KeyboardArrowDownIcon
|
|
224
|
+
size={size === "large" ? "medium" : "small"}
|
|
225
|
+
className={cls("transition", isPopoverOpen ? "rotate-180" : "")}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</button>
|
|
230
|
+
</PopoverPrimitive.Trigger>
|
|
231
|
+
<PopoverPrimitive.Portal container={finalContainer}>
|
|
232
|
+
<PopoverPrimitive.Content
|
|
233
|
+
className={cls(
|
|
234
|
+
"z-50 overflow-hidden border bg-white dark:bg-surface-900 rounded-lg",
|
|
235
|
+
defaultBorderMixin
|
|
236
|
+
)}
|
|
237
|
+
align="start"
|
|
238
|
+
sideOffset={4}
|
|
239
|
+
style={{ width: "var(--radix-popover-trigger-width)", minWidth: 180 }}
|
|
240
|
+
onEscapeKeyDown={() => onPopoverOpenChange(false)}
|
|
241
|
+
>
|
|
242
|
+
<CommandPrimitive shouldFilter={true}>
|
|
243
|
+
<div className="flex flex-row items-center">
|
|
244
|
+
<CommandPrimitive.Input
|
|
245
|
+
ref={inputRef}
|
|
246
|
+
value={search}
|
|
247
|
+
onValueChange={setSearch}
|
|
248
|
+
className={cls(
|
|
249
|
+
focusedDisabled,
|
|
250
|
+
"bg-transparent outline-none flex-1 h-full w-full text-surface-accent-900 dark:text-white",
|
|
251
|
+
{
|
|
252
|
+
"m-2 text-xs": size === "smallest" || size === "small",
|
|
253
|
+
"m-3 text-sm": size === "medium" || size === "large",
|
|
254
|
+
},
|
|
255
|
+
)}
|
|
256
|
+
placeholder="Search..."
|
|
257
|
+
onKeyDown={handleInputKeyDown}
|
|
258
|
+
/>
|
|
259
|
+
</div>
|
|
260
|
+
<Separator orientation="horizontal" className="my-0" />
|
|
261
|
+
<CommandPrimitive.List>
|
|
262
|
+
{allowCustomValues && search.trim() && !itemsMap.has(search.trim()) ? (
|
|
263
|
+
<CommandPrimitive.Empty
|
|
264
|
+
className="px-3 py-2 text-xs cursor-pointer text-primary hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800"
|
|
265
|
+
onClick={() => handleSelect(search.trim())}
|
|
266
|
+
>
|
|
267
|
+
Use “{search.trim()}”
|
|
268
|
+
</CommandPrimitive.Empty>
|
|
269
|
+
) : (
|
|
270
|
+
<CommandPrimitive.Empty className="px-3 py-2 text-xs text-text-secondary dark:text-text-secondary-dark">
|
|
271
|
+
No results found.
|
|
272
|
+
</CommandPrimitive.Empty>
|
|
273
|
+
)}
|
|
274
|
+
<CommandPrimitive.Group>
|
|
275
|
+
{React.Children.map(children, (child) => {
|
|
276
|
+
if (!React.isValidElement<SearchableSelectItemProps>(child)) return child;
|
|
277
|
+
const itemValue = child.props.value;
|
|
278
|
+
const isSelected = String(value) === String(itemValue);
|
|
279
|
+
return (
|
|
280
|
+
<CommandPrimitive.Item
|
|
281
|
+
key={String(itemValue)}
|
|
282
|
+
value={String(itemValue)}
|
|
283
|
+
onMouseDown={(e) => {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
}}
|
|
287
|
+
onSelect={() => handleSelect(String(itemValue))}
|
|
288
|
+
className={cls(
|
|
289
|
+
"flex flex-row items-center gap-1.5",
|
|
290
|
+
isSelected ? "bg-surface-accent-200 dark:bg-surface-accent-950" : "",
|
|
291
|
+
"cursor-pointer",
|
|
292
|
+
"m-0.5",
|
|
293
|
+
"ring-offset-transparent",
|
|
294
|
+
"p-1.5 rounded",
|
|
295
|
+
"aria-[selected=true]:outline-none",
|
|
296
|
+
"aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900",
|
|
297
|
+
"text-surface-accent-700 dark:text-surface-accent-300",
|
|
298
|
+
child.props.className
|
|
299
|
+
)}
|
|
300
|
+
>
|
|
301
|
+
<div className={cls(
|
|
302
|
+
"w-4 h-4 flex items-center justify-center flex-shrink-0",
|
|
303
|
+
isSelected ? "text-primary" : "text-transparent",
|
|
304
|
+
)}>
|
|
305
|
+
{isSelected && <CheckIcon size={14} />}
|
|
306
|
+
</div>
|
|
307
|
+
{child.props.children ?? child.props.value}
|
|
308
|
+
</CommandPrimitive.Item>
|
|
309
|
+
);
|
|
310
|
+
})}
|
|
311
|
+
</CommandPrimitive.Group>
|
|
312
|
+
</CommandPrimitive.List>
|
|
313
|
+
</CommandPrimitive>
|
|
314
|
+
</PopoverPrimitive.Content>
|
|
315
|
+
</PopoverPrimitive.Portal>
|
|
316
|
+
</PopoverPrimitive.Root>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
SearchableSelect.displayName = "SearchableSelect";
|
|
323
|
+
|
|
324
|
+
// ─── Item ───────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A single option inside a SearchableSelect.
|
|
328
|
+
* The `value` prop is the string value that gets selected.
|
|
329
|
+
* The `children` is what's displayed in the dropdown.
|
|
330
|
+
* This component is not rendered directly — SearchableSelect reads its props.
|
|
331
|
+
*/
|
|
332
|
+
export function SearchableSelectItem(_props: SearchableSelectItemProps) {
|
|
333
|
+
// Rendered by SearchableSelect, not by React
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import React, { ChangeEvent, Children, forwardRef, useCallback, useEffect, useState } from "react";
|
|
2
|
+
import React, { ChangeEvent, Children, forwardRef, useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
4
4
|
import {
|
|
5
5
|
defaultBorderMixin,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { CheckIcon, KeyboardArrowDownIcon } from "../icons";
|
|
13
13
|
import { cls } from "../util";
|
|
14
14
|
import { SelectInputLabel } from "./common/SelectInputLabel";
|
|
15
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
15
16
|
|
|
16
17
|
export type SelectValue = string | number | boolean;
|
|
17
18
|
|
|
@@ -35,40 +36,42 @@ export type SelectProps<T extends SelectValue = string> = {
|
|
|
35
36
|
error?: boolean,
|
|
36
37
|
position?: "item-aligned" | "popper",
|
|
37
38
|
endAdornment?: React.ReactNode,
|
|
38
|
-
inputRef?: React.RefObject<HTMLButtonElement>,
|
|
39
|
+
inputRef?: React.RefObject<HTMLButtonElement | null>,
|
|
39
40
|
padding?: boolean,
|
|
40
41
|
invisible?: boolean,
|
|
41
42
|
children?: React.ReactNode;
|
|
42
43
|
dataType?: "string" | "number" | "boolean";
|
|
44
|
+
portalContainer?: HTMLElement | null; // Explicitly added to props type if missing
|
|
43
45
|
};
|
|
44
46
|
|
|
45
47
|
export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
48
|
+
inputRef,
|
|
49
|
+
open,
|
|
50
|
+
name,
|
|
51
|
+
fullWidth = false,
|
|
52
|
+
id,
|
|
53
|
+
onOpenChange,
|
|
54
|
+
value,
|
|
55
|
+
onChange,
|
|
56
|
+
onValueChange,
|
|
57
|
+
className,
|
|
58
|
+
inputClassName,
|
|
59
|
+
viewportClassName,
|
|
60
|
+
placeholder,
|
|
61
|
+
renderValue,
|
|
62
|
+
label,
|
|
63
|
+
size = "large",
|
|
64
|
+
error,
|
|
65
|
+
disabled,
|
|
66
|
+
padding = true,
|
|
67
|
+
position = "item-aligned",
|
|
68
|
+
endAdornment,
|
|
69
|
+
invisible,
|
|
70
|
+
children,
|
|
71
|
+
dataType = "string",
|
|
72
|
+
portalContainer: manualContainer, // Rename to avoid confusion
|
|
73
|
+
...props
|
|
74
|
+
}, ref) => {
|
|
72
75
|
|
|
73
76
|
const [openInternal, setOpenInternal] = useState(open ?? false);
|
|
74
77
|
|
|
@@ -76,8 +79,13 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
76
79
|
setOpenInternal(open ?? false);
|
|
77
80
|
}, [open]);
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
// Get the portal container from context
|
|
83
|
+
const contextContainer = usePortalContainer();
|
|
84
|
+
|
|
85
|
+
// Resolve final container (Manual Prop > Context Container > Undefined/Body)
|
|
86
|
+
const finalContainer = (manualContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
80
87
|
|
|
88
|
+
const onValueChangeInternal = useCallback((newValue: string) => {
|
|
81
89
|
let typedValue: SelectValue = newValue;
|
|
82
90
|
if (dataType === "boolean") {
|
|
83
91
|
if (newValue === "true") typedValue = true;
|
|
@@ -96,23 +104,22 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
96
104
|
} as unknown as ChangeEvent<HTMLSelectElement>;
|
|
97
105
|
onChange(event);
|
|
98
106
|
}
|
|
99
|
-
}, [onChange, onValueChange, name]);
|
|
107
|
+
}, [onChange, onValueChange, name, dataType]);
|
|
100
108
|
|
|
101
109
|
const hasValue = Array.isArray(value) ? value.length > 0 : (value != null && value !== "" && value !== undefined);
|
|
102
|
-
// Convert non-string values to strings for Radix UI
|
|
103
110
|
const stringValue = value !== undefined ? String(value) : undefined;
|
|
104
111
|
|
|
105
|
-
const
|
|
112
|
+
const displayChildren = useMemo(() => {
|
|
106
113
|
if (!hasValue || renderValue) return null;
|
|
107
|
-
// @ts-ignore
|
|
108
|
-
const childrenProps: SelectItemProps[] = Children.map(children, (child) => {
|
|
109
|
-
if (React.isValidElement(child)) {
|
|
110
|
-
return child.props;
|
|
111
|
-
}
|
|
112
|
-
}).filter(Boolean);
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
// Find the child that matches the current value to display its content
|
|
116
|
+
let found: React.ReactNode = null;
|
|
117
|
+
Children.forEach(children, (child) => {
|
|
118
|
+
if (React.isValidElement<SelectItemProps>(child) && String(child.props.value) === String(value)) {
|
|
119
|
+
found = child.props.children;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return found;
|
|
116
123
|
}, [children, hasValue, renderValue, value]);
|
|
117
124
|
|
|
118
125
|
return (
|
|
@@ -155,6 +162,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
155
162
|
"px-3": size === "medium",
|
|
156
163
|
"px-2": size === "small" || size === "smallest",
|
|
157
164
|
} : "",
|
|
165
|
+
"outline-hidden focus:outline-hidden",
|
|
158
166
|
"outline-none focus:outline-none",
|
|
159
167
|
"select-none rounded-md text-sm",
|
|
160
168
|
error ? "text-red-500 dark:text-red-600" : "focus:text-text-primary dark:focus:text-text-primary-dark",
|
|
@@ -184,28 +192,30 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
184
192
|
"min-h-[64px]": size === "large"
|
|
185
193
|
}
|
|
186
194
|
)}>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{hasValue && value !== undefined && renderValue ? renderValue(value) : placeholder}
|
|
195
|
-
{/*{hasValue && !renderValue && value}*/}
|
|
196
|
-
{hasValue && !renderValue && selectedChild}
|
|
195
|
+
<SelectPrimitive.Value
|
|
196
|
+
onClick={(e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
}}
|
|
200
|
+
placeholder={placeholder}
|
|
201
|
+
className={"w-full"}>
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
{hasValue && value !== undefined && renderValue
|
|
204
|
+
? renderValue(value)
|
|
205
|
+
: (displayChildren || placeholder)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
</SelectPrimitive.Value>
|
|
209
|
+
</div>
|
|
200
210
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
<SelectPrimitive.Icon asChild>
|
|
212
|
+
<KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
|
|
213
|
+
className={cls("transition", open ? "rotate-180" : "", {
|
|
214
|
+
"px-2": size === "large",
|
|
215
|
+
"px-1": size === "medium" || size === "small",
|
|
216
|
+
})} />
|
|
217
|
+
</SelectPrimitive.Icon>
|
|
218
|
+
</SelectPrimitive.Trigger>
|
|
209
219
|
|
|
210
220
|
{endAdornment && (
|
|
211
221
|
<div
|
|
@@ -218,11 +228,13 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
218
228
|
</div>
|
|
219
229
|
)}
|
|
220
230
|
</div>
|
|
221
|
-
|
|
231
|
+
|
|
232
|
+
{/* Pass the calculated finalContainer */}
|
|
233
|
+
<SelectPrimitive.Portal container={finalContainer}>
|
|
222
234
|
<SelectPrimitive.Content position={position}
|
|
223
|
-
|
|
235
|
+
className={cls(focusedDisabled, "z-50 relative overflow-hidden border bg-white dark:bg-surface-900 p-2 rounded-lg", defaultBorderMixin)}>
|
|
224
236
|
<SelectPrimitive.Viewport className={cls("p-1", viewportClassName)}
|
|
225
|
-
|
|
237
|
+
style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
|
|
226
238
|
{children}
|
|
227
239
|
</SelectPrimitive.Viewport>
|
|
228
240
|
</SelectPrimitive.Content>
|
|
@@ -233,6 +245,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
233
245
|
|
|
234
246
|
Select.displayName = "Select";
|
|
235
247
|
|
|
248
|
+
// ... (SelectItem and SelectGroup remain unchanged)
|
|
236
249
|
export type SelectItemProps<T extends SelectValue = string> = {
|
|
237
250
|
value: T,
|
|
238
251
|
children?: React.ReactNode,
|
|
@@ -241,11 +254,11 @@ export type SelectItemProps<T extends SelectValue = string> = {
|
|
|
241
254
|
};
|
|
242
255
|
|
|
243
256
|
export const SelectItem = React.memo(function SelectItem<T extends SelectValue = string>({
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
value,
|
|
258
|
+
children,
|
|
259
|
+
disabled,
|
|
260
|
+
className
|
|
261
|
+
}: SelectItemProps<T>) {
|
|
249
262
|
// Convert value to string for Radix UI
|
|
250
263
|
const stringValue = String(value);
|
|
251
264
|
|
|
@@ -267,7 +280,7 @@ export const SelectItem = React.memo(function SelectItem<T extends SelectValue =
|
|
|
267
280
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
268
281
|
<div
|
|
269
282
|
className="absolute left-1 data-[state=checked]:block hidden">
|
|
270
|
-
<CheckIcon size={16}/>
|
|
283
|
+
<CheckIcon size={16} />
|
|
271
284
|
</div>
|
|
272
285
|
</SelectPrimitive.Item>;
|
|
273
286
|
});
|
|
@@ -279,10 +292,10 @@ export type SelectGroupProps = {
|
|
|
279
292
|
};
|
|
280
293
|
|
|
281
294
|
export const SelectGroup = React.memo(function SelectGroup({
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
295
|
+
label,
|
|
296
|
+
children,
|
|
297
|
+
className
|
|
298
|
+
}: SelectGroupProps) {
|
|
286
299
|
return <>
|
|
287
300
|
<SelectPrimitive.Group
|
|
288
301
|
className={cls(
|
|
@@ -15,12 +15,12 @@ export function Separator({
|
|
|
15
15
|
<SeparatorPrimitive.Root
|
|
16
16
|
decorative={decorative}
|
|
17
17
|
orientation="horizontal"
|
|
18
|
-
className={cls("dark:bg-opacity-80 dark:bg-surface-800 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px my-4", className)}/>
|
|
18
|
+
className={cls("dark:bg-opacity-80 dark:bg-surface-800 dark:bg-surface-800/80 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px my-4", className)}/>
|
|
19
19
|
);
|
|
20
20
|
else
|
|
21
21
|
return (
|
|
22
22
|
<SeparatorPrimitive.Root
|
|
23
|
-
className={cls("dark:bg-opacity-80 dark:bg-surface-800 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px mx-4", className)}
|
|
23
|
+
className={cls("dark:bg-opacity-80 dark:bg-surface-800 dark:bg-surface-800/80 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px mx-4", className)}
|
|
24
24
|
decorative={decorative}
|
|
25
25
|
orientation="vertical"
|
|
26
26
|
/>
|
package/src/components/Sheet.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
|
|
|
3
3
|
import { cls } from "../util";
|
|
4
4
|
import { defaultBorderMixin } from "../styles";
|
|
5
5
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
6
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
6
7
|
|
|
7
8
|
interface SheetProps {
|
|
8
9
|
children: React.ReactNode;
|
|
@@ -18,6 +19,7 @@ interface SheetProps {
|
|
|
18
19
|
style?: React.CSSProperties;
|
|
19
20
|
overlayClassName?: string;
|
|
20
21
|
overlayStyle?: React.CSSProperties;
|
|
22
|
+
portalContainer?: HTMLElement | null;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export const Sheet: React.FC<SheetProps> = ({
|
|
@@ -33,10 +35,17 @@ export const Sheet: React.FC<SheetProps> = ({
|
|
|
33
35
|
style,
|
|
34
36
|
overlayClassName,
|
|
35
37
|
overlayStyle,
|
|
38
|
+
portalContainer,
|
|
36
39
|
...props
|
|
37
40
|
}) => {
|
|
38
41
|
const [displayed, setDisplayed] = useState(false);
|
|
39
42
|
|
|
43
|
+
// Get the portal container from context
|
|
44
|
+
const contextContainer = usePortalContainer();
|
|
45
|
+
|
|
46
|
+
// Prioritize manual prop, fallback to context container
|
|
47
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
48
|
+
|
|
40
49
|
useEffect(() => {
|
|
41
50
|
const timeout = setTimeout(() => {
|
|
42
51
|
setDisplayed(open);
|
|
@@ -62,7 +71,7 @@ export const Sheet: React.FC<SheetProps> = ({
|
|
|
62
71
|
<DialogPrimitive.Root open={displayed || open}
|
|
63
72
|
modal={modal}
|
|
64
73
|
onOpenChange={onOpenChange}>
|
|
65
|
-
<DialogPrimitive.Portal>
|
|
74
|
+
<DialogPrimitive.Portal container={finalContainer}>
|
|
66
75
|
<DialogPrimitive.Title autoFocus tabIndex={0}>
|
|
67
76
|
{title ?? "Sheet"}
|
|
68
77
|
</DialogPrimitive.Title>
|
|
@@ -70,8 +79,8 @@ export const Sheet: React.FC<SheetProps> = ({
|
|
|
70
79
|
className={cls(
|
|
71
80
|
"outline-none",
|
|
72
81
|
"fixed inset-0 transition-opacity z-20 ease-in-out duration-100 backdrop-blur-sm",
|
|
73
|
-
"bg-black bg-opacity-50",
|
|
74
|
-
"dark:bg-surface-900 dark:bg-opacity-60",
|
|
82
|
+
"bg-black bg-opacity-50 bg-black/50",
|
|
83
|
+
"dark:bg-surface-900 dark:bg-opacity-60 dark:bg-surface-900/60",
|
|
75
84
|
displayed && open ? "opacity-100" : "opacity-0",
|
|
76
85
|
overlayClassName
|
|
77
86
|
)}
|