@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { ChangeEvent, Children, useEffect } from "react";
|
|
4
|
+
import { ChangeEvent, Children, useEffect, useState } from "react";
|
|
5
5
|
import { Command as CommandPrimitive } from "cmdk";
|
|
6
6
|
import { cls } from "../util";
|
|
7
7
|
import { CheckIcon, CloseIcon, KeyboardArrowDownIcon } from "../icons";
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
focusedDisabled
|
|
18
18
|
} from "../styles";
|
|
19
19
|
import { useInjectStyles } from "../hooks";
|
|
20
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
20
21
|
|
|
21
22
|
export type MultiSelectValue = string | number | boolean;
|
|
22
23
|
|
|
@@ -54,11 +55,12 @@ interface MultiSelectProps<T extends MultiSelectValue = string> {
|
|
|
54
55
|
multiple?: boolean,
|
|
55
56
|
includeSelectAll?: boolean,
|
|
56
57
|
includeClear?: boolean,
|
|
57
|
-
inputRef?: React.RefObject<HTMLButtonElement>,
|
|
58
|
+
inputRef?: React.RefObject<HTMLButtonElement | null>,
|
|
58
59
|
padding?: boolean,
|
|
59
60
|
invisible?: boolean,
|
|
60
61
|
children: React.ReactNode;
|
|
61
62
|
renderValues?: (values: T[]) => React.ReactNode;
|
|
63
|
+
portalContainer?: HTMLElement | null;
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Use generic type for the forwarded ref
|
|
@@ -81,16 +83,30 @@ export const MultiSelect = React.forwardRef<
|
|
|
81
83
|
includeSelectAll = true,
|
|
82
84
|
useChips = true,
|
|
83
85
|
className,
|
|
86
|
+
inputClassName,
|
|
87
|
+
inputRef,
|
|
84
88
|
children,
|
|
85
89
|
renderValues,
|
|
86
90
|
open,
|
|
87
91
|
onOpenChange,
|
|
92
|
+
portalContainer,
|
|
93
|
+
endAdornment,
|
|
88
94
|
},
|
|
89
95
|
ref
|
|
90
96
|
) => {
|
|
91
|
-
|
|
92
|
-
const [isPopoverOpen, setIsPopoverOpen] =
|
|
93
|
-
const [selectedValues, setSelectedValues] =
|
|
97
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
98
|
+
const [isPopoverOpen, setIsPopoverOpen] = useState(open ?? false);
|
|
99
|
+
const [selectedValues, setSelectedValues] = useState<any[]>(value ?? []);
|
|
100
|
+
|
|
101
|
+
// Get the portal container from context
|
|
102
|
+
const contextContainer = usePortalContainer();
|
|
103
|
+
|
|
104
|
+
// Prioritize manual prop, fallback to context container
|
|
105
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
setIsMounted(true);
|
|
109
|
+
}, []);
|
|
94
110
|
|
|
95
111
|
const onPopoverOpenChange = (open: boolean) => {
|
|
96
112
|
setIsPopoverOpen(open);
|
|
@@ -103,20 +119,18 @@ export const MultiSelect = React.forwardRef<
|
|
|
103
119
|
|
|
104
120
|
const allValues = React.useMemo(() => children
|
|
105
121
|
?
|
|
106
|
-
// @ts-ignore
|
|
107
122
|
Children.map(children, (child) => {
|
|
108
|
-
if (React.isValidElement(child)) {
|
|
123
|
+
if (React.isValidElement<MultiSelectItemProps>(child)) {
|
|
109
124
|
return child.props.value;
|
|
110
125
|
}
|
|
111
126
|
return null;
|
|
112
|
-
})
|
|
127
|
+
})?.filter(Boolean) ?? []
|
|
113
128
|
: [], [children]);
|
|
114
129
|
|
|
115
130
|
const optionsMap = React.useMemo(() => {
|
|
116
131
|
const map = new Map<string, React.ReactNode>();
|
|
117
132
|
Children.forEach(children, (child) => {
|
|
118
|
-
if (React.isValidElement(child)) {
|
|
119
|
-
// @ts-ignore
|
|
133
|
+
if (React.isValidElement<MultiSelectItemProps>(child)) {
|
|
120
134
|
map.set(String(child.props.value), child.props.children);
|
|
121
135
|
}
|
|
122
136
|
});
|
|
@@ -195,13 +209,13 @@ export const MultiSelect = React.forwardRef<
|
|
|
195
209
|
{typeof label === "string" ? <SelectInputLabel error={error}>{label}</SelectInputLabel> : label}
|
|
196
210
|
|
|
197
211
|
<PopoverPrimitive.Root
|
|
198
|
-
open={isPopoverOpen}
|
|
212
|
+
open={isMounted && isPopoverOpen}
|
|
199
213
|
onOpenChange={onPopoverOpenChange}
|
|
200
214
|
modal={modalPopover}
|
|
201
215
|
>
|
|
202
216
|
<PopoverPrimitive.Trigger asChild>
|
|
203
217
|
<button
|
|
204
|
-
ref={ref}
|
|
218
|
+
ref={inputRef ?? ref}
|
|
205
219
|
onClick={handleTogglePopover}
|
|
206
220
|
className={cls(
|
|
207
221
|
{
|
|
@@ -219,10 +233,12 @@ export const MultiSelect = React.forwardRef<
|
|
|
219
233
|
"px-4": size === "medium" || size === "large",
|
|
220
234
|
},
|
|
221
235
|
"select-none rounded-md text-sm",
|
|
236
|
+
"focus:ring-0 focus-visible:ring-0 outline-none focus:outline-none focus-visible:outline-none",
|
|
222
237
|
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
223
238
|
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
224
239
|
"relative flex items-center",
|
|
225
|
-
className
|
|
240
|
+
className,
|
|
241
|
+
inputClassName
|
|
226
242
|
)}
|
|
227
243
|
>
|
|
228
244
|
{selectedValues.length > 0 ? (
|
|
@@ -254,7 +270,7 @@ export const MultiSelect = React.forwardRef<
|
|
|
254
270
|
})}
|
|
255
271
|
</div>
|
|
256
272
|
<div className="flex items-center justify-between">
|
|
257
|
-
{includeClear && <CloseIcon
|
|
273
|
+
{includeClear && !endAdornment && <CloseIcon
|
|
258
274
|
className={"ml-4"}
|
|
259
275
|
size={"small"}
|
|
260
276
|
onClick={(event) => {
|
|
@@ -262,75 +278,95 @@ export const MultiSelect = React.forwardRef<
|
|
|
262
278
|
handleClear();
|
|
263
279
|
}}
|
|
264
280
|
/>}
|
|
281
|
+
{endAdornment && (
|
|
282
|
+
<div className="ml-4 flex items-center" onClick={(e) => {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
}}>
|
|
286
|
+
{endAdornment}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
265
289
|
<div className={cls("px-2 h-full flex items-center")}>
|
|
266
290
|
<KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
|
|
267
|
-
|
|
291
|
+
className={cls("transition", isPopoverOpen ? "rotate-180" : "")} />
|
|
268
292
|
</div>
|
|
269
293
|
</div>
|
|
270
294
|
</div>
|
|
271
295
|
) : (
|
|
272
296
|
<div className="flex items-center justify-between w-full mx-auto">
|
|
273
297
|
<span className="text-sm">
|
|
274
|
-
|
|
298
|
+
{placeholder}
|
|
275
299
|
</span>
|
|
276
|
-
<div className=
|
|
277
|
-
|
|
278
|
-
|
|
300
|
+
<div className="flex items-center justify-between">
|
|
301
|
+
{endAdornment && (
|
|
302
|
+
<div className="ml-4 flex items-center" onClick={(e) => {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
e.stopPropagation();
|
|
305
|
+
}}>
|
|
306
|
+
{endAdornment}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
<div className={cls("px-2 h-full flex items-center")}>
|
|
310
|
+
<KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
|
|
311
|
+
className={cls("transition", isPopoverOpen ? "rotate-180" : "")} />
|
|
312
|
+
</div>
|
|
279
313
|
</div>
|
|
280
314
|
</div>
|
|
281
315
|
)}
|
|
282
316
|
</button>
|
|
283
317
|
</PopoverPrimitive.Trigger>
|
|
284
|
-
<PopoverPrimitive.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
<
|
|
292
|
-
<
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
<CommandPrimitive.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
318
|
+
<PopoverPrimitive.Portal container={finalContainer}>
|
|
319
|
+
<PopoverPrimitive.Content
|
|
320
|
+
className={cls("z-50 overflow-hidden border bg-white dark:bg-surface-900 rounded-lg w-[400px]", defaultBorderMixin)}
|
|
321
|
+
align="start"
|
|
322
|
+
sideOffset={8}
|
|
323
|
+
onEscapeKeyDown={() => onPopoverOpenChange(false)}
|
|
324
|
+
>
|
|
325
|
+
<CommandPrimitive>
|
|
326
|
+
<div className={"flex flex-row items-center"}>
|
|
327
|
+
<CommandPrimitive.Input
|
|
328
|
+
className={cls(focusedDisabled, "bg-transparent outline-none flex-1 h-full w-full m-4 flex-grow text-surface-accent-900 dark:text-white")}
|
|
329
|
+
placeholder="Search..."
|
|
330
|
+
onKeyDown={handleInputKeyDown}
|
|
331
|
+
/>
|
|
332
|
+
{selectedValues.length > 0 && (
|
|
333
|
+
<div
|
|
334
|
+
onClick={handleClear}
|
|
335
|
+
className="text-sm justify-center cursor-pointer py-3 px-4 text-text-secondary dark:text-text-secondary-dark">
|
|
336
|
+
Clear
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
<Separator orientation={"horizontal"} className={"my-0"} />
|
|
341
|
+
<CommandPrimitive.List>
|
|
342
|
+
<CommandPrimitive.Empty className={"px-4 py-2 text-sm text-text-secondary dark:text-text-secondary-dark"}>
|
|
343
|
+
No results found.
|
|
344
|
+
</CommandPrimitive.Empty>
|
|
345
|
+
<CommandPrimitive.Group>
|
|
346
|
+
{includeSelectAll && <CommandPrimitive.Item
|
|
347
|
+
key="all"
|
|
348
|
+
onSelect={toggleAll}
|
|
349
|
+
className={
|
|
350
|
+
cls(
|
|
351
|
+
"flex flex-row items-center gap-1.5",
|
|
352
|
+
"cursor-pointer",
|
|
353
|
+
"m-1",
|
|
354
|
+
"ring-offset-transparent",
|
|
355
|
+
"p-1 rounded aria-[selected=true]:outline-none aria-[selected=true]:ring-2 aria-[selected=true]:ring-primary aria-[selected=true]:ring-opacity-75 aria-[selected=true]:ring-primary/75 aria-[selected=true]:ring-offset-2",
|
|
356
|
+
"aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900",
|
|
357
|
+
"cursor-pointer p-2 rounded aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900"
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
>
|
|
361
|
+
<InnerCheckBox checked={selectedValues.length === allValues.length} />
|
|
362
|
+
<span className={"text-sm text-text-secondary dark:text-text-secondary-dark"}>(Select All)</span>
|
|
363
|
+
</CommandPrimitive.Item>}
|
|
364
|
+
{children}
|
|
365
|
+
</CommandPrimitive.Group>
|
|
366
|
+
</CommandPrimitive.List>
|
|
367
|
+
</CommandPrimitive>
|
|
368
|
+
</PopoverPrimitive.Content>
|
|
369
|
+
</PopoverPrimitive.Portal>
|
|
334
370
|
</PopoverPrimitive.Root>
|
|
335
371
|
</MultiSelectContext.Provider>
|
|
336
372
|
);
|
|
@@ -346,10 +382,10 @@ export interface MultiSelectItemProps<T extends MultiSelectValue = string> {
|
|
|
346
382
|
}
|
|
347
383
|
|
|
348
384
|
export const MultiSelectItem = React.memo(function MultiSelectItem<T extends MultiSelectValue = string>({
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
385
|
+
children,
|
|
386
|
+
value,
|
|
387
|
+
className
|
|
388
|
+
}: MultiSelectItemProps<T>) {
|
|
353
389
|
const context = React.useContext(MultiSelectContext);
|
|
354
390
|
if (!context) throw new Error("MultiSelectItem must be used inside a MultiSelect");
|
|
355
391
|
const {
|
|
@@ -373,13 +409,14 @@ export const MultiSelectItem = React.memo(function MultiSelectItem<T extends Mul
|
|
|
373
409
|
"cursor-pointer",
|
|
374
410
|
"m-1",
|
|
375
411
|
"ring-offset-transparent",
|
|
376
|
-
"p-1 rounded aria-[selected=true]:outline-none aria-[selected=true]:ring-2 aria-[selected=true]:ring-primary aria-[selected=true]:ring-opacity-75 aria-[selected=true]:ring-offset-2",
|
|
412
|
+
"p-1 rounded aria-[selected=true]:outline-none aria-[selected=true]:ring-2 aria-[selected=true]:ring-primary aria-[selected=true]:ring-opacity-75 aria-[selected=true]:ring-primary/75 aria-[selected=true]:ring-offset-2",
|
|
377
413
|
"aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900",
|
|
378
414
|
"cursor-pointer p-2 rounded aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900",
|
|
415
|
+
"text-surface-accent-700 dark:text-surface-accent-300",
|
|
379
416
|
className
|
|
380
417
|
)}
|
|
381
418
|
>
|
|
382
|
-
<InnerCheckBox checked={isSelected}/>
|
|
419
|
+
<InnerCheckBox checked={isSelected} />
|
|
383
420
|
{children}
|
|
384
421
|
</CommandPrimitive.Item>;
|
|
385
422
|
});
|
|
@@ -398,8 +435,7 @@ const InnerCheckBox = React.memo(function InnerCheckBox({ checked }: { checked:
|
|
|
398
435
|
(checked) ? "text-surface-accent-100 dark:text-surface-accent-900" : "",
|
|
399
436
|
(checked ? "border-transparent" : "border-surface-accent-800 dark:border-surface-accent-200")
|
|
400
437
|
)}>
|
|
401
|
-
{checked && <CheckIcon size={16} className={"absolute"}/>}
|
|
438
|
+
{checked && <CheckIcon size={16} className={"absolute"} />}
|
|
402
439
|
</div>
|
|
403
440
|
</div>
|
|
404
441
|
});
|
|
405
|
-
|
|
@@ -5,6 +5,7 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
|
5
5
|
import { paperMixin } from "../styles";
|
|
6
6
|
import { cls } from "../util";
|
|
7
7
|
import { useInjectStyles } from "../hooks";
|
|
8
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
8
9
|
|
|
9
10
|
export type PopoverSide = "top" | "right" | "bottom" | "left";
|
|
10
11
|
export type PopoverAlign = "start" | "center" | "end";
|
|
@@ -49,16 +50,24 @@ export function Popover({
|
|
|
49
50
|
|
|
50
51
|
useInjectStyles("Popover", popoverStyles);
|
|
51
52
|
|
|
53
|
+
// Get the portal container from context
|
|
54
|
+
const contextContainer = usePortalContainer();
|
|
55
|
+
|
|
56
|
+
// Prioritize manual prop, fallback to context container
|
|
57
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
58
|
+
|
|
52
59
|
if (!enabled)
|
|
53
60
|
return <>{trigger}</>;
|
|
54
61
|
|
|
55
62
|
return <PopoverPrimitive.Root open={open}
|
|
56
63
|
onOpenChange={onOpenChange}
|
|
57
64
|
modal={modal}>
|
|
65
|
+
|
|
58
66
|
<PopoverPrimitive.Trigger asChild>
|
|
59
67
|
{trigger}
|
|
60
68
|
</PopoverPrimitive.Trigger>
|
|
61
|
-
|
|
69
|
+
|
|
70
|
+
<PopoverPrimitive.Portal container={finalContainer}>
|
|
62
71
|
<PopoverPrimitive.Content
|
|
63
72
|
className={cls(paperMixin,
|
|
64
73
|
"PopoverContent z-40", className)}
|
|
@@ -79,7 +88,7 @@ export function Popover({
|
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
const popoverStyles = `
|
|
82
|
-
|
|
91
|
+
/* ... (styles remain unchanged) ... */
|
|
83
92
|
.PopoverContent {
|
|
84
93
|
animation-duration: 400ms;
|
|
85
94
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
@@ -98,7 +107,6 @@ const popoverStyles = `
|
|
|
98
107
|
animation-name: slideRightAndFade;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
|
|
102
110
|
@keyframes slideUpAndFade {
|
|
103
111
|
from {
|
|
104
112
|
opacity: 0;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { cls } from "../util";
|
|
3
|
+
|
|
4
|
+
export type ResizablePanelsProps = {
|
|
5
|
+
firstPanel: React.ReactNode;
|
|
6
|
+
secondPanel: React.ReactNode;
|
|
7
|
+
/** Whether the first panel is visible (e.g. Sidebar) */
|
|
8
|
+
showFirstPanel?: boolean;
|
|
9
|
+
/** Whether the second panel is visible (e.g. Results) */
|
|
10
|
+
showSecondPanel?: boolean;
|
|
11
|
+
/** 0-100 representing the width/height of the first panel */
|
|
12
|
+
panelSizePercent: number;
|
|
13
|
+
onPanelSizeChange: (sizePercent: number) => void;
|
|
14
|
+
minPanelSizePx?: number;
|
|
15
|
+
orientation?: 'horizontal' | 'vertical';
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function ResizablePanels({
|
|
20
|
+
firstPanel,
|
|
21
|
+
secondPanel,
|
|
22
|
+
showFirstPanel = true,
|
|
23
|
+
showSecondPanel = true,
|
|
24
|
+
panelSizePercent,
|
|
25
|
+
onPanelSizeChange,
|
|
26
|
+
minPanelSizePx = 200,
|
|
27
|
+
orientation = 'horizontal',
|
|
28
|
+
className
|
|
29
|
+
}: ResizablePanelsProps) {
|
|
30
|
+
|
|
31
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
const isResizingRef = useRef(false);
|
|
33
|
+
|
|
34
|
+
// For local layout tracking without triggering React rerenders during drag
|
|
35
|
+
const firstPanelRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const startPosRef = useRef(0);
|
|
37
|
+
const startSizeRef = useRef(0);
|
|
38
|
+
|
|
39
|
+
const [isResizing, setIsResizing] = useState(false);
|
|
40
|
+
const isHorizontal = orientation === 'horizontal';
|
|
41
|
+
|
|
42
|
+
const handleResizeStart = useCallback((e: React.MouseEvent) => {
|
|
43
|
+
if (!showFirstPanel || !showSecondPanel) return;
|
|
44
|
+
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
isResizingRef.current = true;
|
|
47
|
+
setIsResizing(true);
|
|
48
|
+
|
|
49
|
+
startPosRef.current = isHorizontal ? e.clientX : e.clientY;
|
|
50
|
+
|
|
51
|
+
if (firstPanelRef.current) {
|
|
52
|
+
const rect = firstPanelRef.current.getBoundingClientRect();
|
|
53
|
+
startSizeRef.current = isHorizontal ? rect.width : rect.height;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
|
|
57
|
+
document.body.style.userSelect = 'none';
|
|
58
|
+
}, [isHorizontal, showFirstPanel, showSecondPanel]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
62
|
+
if (!isResizingRef.current || !containerRef.current) return;
|
|
63
|
+
|
|
64
|
+
const currentPos = isHorizontal ? e.clientX : e.clientY;
|
|
65
|
+
const delta = currentPos - startPosRef.current; // Dragging right/down increases first panel size
|
|
66
|
+
|
|
67
|
+
let newSize = startSizeRef.current + delta;
|
|
68
|
+
|
|
69
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
70
|
+
const containerTotal = isHorizontal ? containerRect.width : containerRect.height;
|
|
71
|
+
|
|
72
|
+
// Limit the maximum size to prevent pushing the second panel offscreen
|
|
73
|
+
const maxSize = containerTotal - minPanelSizePx;
|
|
74
|
+
|
|
75
|
+
newSize = Math.max(minPanelSizePx, Math.min(newSize, maxSize));
|
|
76
|
+
|
|
77
|
+
// Directly update the DOM for performance while dragging
|
|
78
|
+
if (firstPanelRef.current) {
|
|
79
|
+
if (isHorizontal) {
|
|
80
|
+
firstPanelRef.current.style.width = `${newSize}px`;
|
|
81
|
+
} else {
|
|
82
|
+
firstPanelRef.current.style.height = `${newSize}px`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleMouseUp = () => {
|
|
88
|
+
if (isResizingRef.current && containerRef.current && firstPanelRef.current) {
|
|
89
|
+
isResizingRef.current = false;
|
|
90
|
+
setIsResizing(false);
|
|
91
|
+
document.body.style.cursor = "";
|
|
92
|
+
document.body.style.userSelect = "";
|
|
93
|
+
|
|
94
|
+
// Calculate the final percentage and notify parent
|
|
95
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
96
|
+
const firstPanelRect = firstPanelRef.current.getBoundingClientRect();
|
|
97
|
+
|
|
98
|
+
const containerSize = isHorizontal ? containerRect.width : containerRect.height;
|
|
99
|
+
const finalSize = isHorizontal ? firstPanelRect.width : firstPanelRect.height;
|
|
100
|
+
|
|
101
|
+
if (containerSize > 0) {
|
|
102
|
+
const newPercent = (finalSize / containerSize) * 100;
|
|
103
|
+
onPanelSizeChange(Math.max(0, Math.min(100, newPercent)));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
109
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
113
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
114
|
+
};
|
|
115
|
+
}, [onPanelSizeChange, isHorizontal, minPanelSizePx]);
|
|
116
|
+
|
|
117
|
+
// Calculate applied size
|
|
118
|
+
const appliedBasis = !showFirstPanel ? "0%" : (showSecondPanel ? `${panelSizePercent}%` : "100%");
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
ref={containerRef}
|
|
123
|
+
className={cls(
|
|
124
|
+
"relative w-full h-full flex overflow-hidden",
|
|
125
|
+
isHorizontal ? "flex-row" : "flex-col",
|
|
126
|
+
className
|
|
127
|
+
)}
|
|
128
|
+
>
|
|
129
|
+
{/* First Panel */}
|
|
130
|
+
<div
|
|
131
|
+
ref={firstPanelRef}
|
|
132
|
+
className={cls(
|
|
133
|
+
"relative flex-shrink-0 flex flex-col overflow-hidden",
|
|
134
|
+
!showFirstPanel && "hidden"
|
|
135
|
+
)}
|
|
136
|
+
style={{
|
|
137
|
+
width: isHorizontal ? appliedBasis : "100%",
|
|
138
|
+
height: !isHorizontal ? appliedBasis : "100%",
|
|
139
|
+
minWidth: isHorizontal && showFirstPanel && showSecondPanel ? `${minPanelSizePx}px` : undefined,
|
|
140
|
+
minHeight: !isHorizontal && showFirstPanel && showSecondPanel ? `${minPanelSizePx}px` : undefined,
|
|
141
|
+
maxWidth: showSecondPanel ? undefined : "100%",
|
|
142
|
+
maxHeight: showSecondPanel ? undefined : "100%",
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{firstPanel}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Divider */}
|
|
149
|
+
{showFirstPanel && showSecondPanel && (
|
|
150
|
+
<div
|
|
151
|
+
className={cls(
|
|
152
|
+
"relative z-10 flex flex-shrink-0 items-center justify-center",
|
|
153
|
+
isHorizontal ? "w-px h-full cursor-col-resize" : "h-px w-full cursor-row-resize"
|
|
154
|
+
)}
|
|
155
|
+
onMouseDown={handleResizeStart}
|
|
156
|
+
>
|
|
157
|
+
{/* Transparent Hit Area with Pill inside */}
|
|
158
|
+
<div className={cls(
|
|
159
|
+
"absolute flex items-center justify-center group",
|
|
160
|
+
isHorizontal ? "w-4 h-full cursor-col-resize top-0" : "h-4 w-full cursor-row-resize left-0"
|
|
161
|
+
)}>
|
|
162
|
+
<div className={cls(
|
|
163
|
+
"bg-primary/60 dark:bg-primary rounded-full opacity-0 group-hover:opacity-100 transition-all duration-200",
|
|
164
|
+
isHorizontal ? "w-1 h-8" : "h-1 w-8"
|
|
165
|
+
)} />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Second Panel */}
|
|
171
|
+
<div
|
|
172
|
+
className={cls(
|
|
173
|
+
"flex-grow relative flex flex-col overflow-hidden min-w-0 min-h-0",
|
|
174
|
+
!showSecondPanel && "hidden"
|
|
175
|
+
)}
|
|
176
|
+
>
|
|
177
|
+
{secondPanel}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -12,6 +12,16 @@ interface SearchBarProps {
|
|
|
12
12
|
onTextSearch?: (searchString?: string) => void;
|
|
13
13
|
placeholder?: string;
|
|
14
14
|
expandable?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Size of the search bar.
|
|
17
|
+
* - "small": 32px height (matches TextField small)
|
|
18
|
+
* - "medium": 44px height (matches TextField medium)
|
|
19
|
+
* @default "medium"
|
|
20
|
+
*/
|
|
21
|
+
size?: "small" | "medium";
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated Use size="medium" or size="small" instead. This prop will be removed in a future version.
|
|
24
|
+
*/
|
|
15
25
|
large?: boolean;
|
|
16
26
|
innerClassName?: string;
|
|
17
27
|
className?: string;
|
|
@@ -22,18 +32,19 @@ interface SearchBarProps {
|
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
export function SearchBar({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
onClick,
|
|
36
|
+
onTextSearch,
|
|
37
|
+
placeholder = "Search",
|
|
38
|
+
expandable = false,
|
|
39
|
+
size = "medium",
|
|
40
|
+
large,
|
|
41
|
+
innerClassName,
|
|
42
|
+
className,
|
|
43
|
+
autoFocus,
|
|
44
|
+
disabled,
|
|
45
|
+
loading,
|
|
46
|
+
inputRef
|
|
47
|
+
}: SearchBarProps) {
|
|
37
48
|
|
|
38
49
|
const [searchText, setSearchText] = useState<string>("");
|
|
39
50
|
const [active, setActive] = useState<boolean>(false);
|
|
@@ -58,18 +69,23 @@ export function SearchBar({
|
|
|
58
69
|
onTextSearch(undefined);
|
|
59
70
|
}, [onTextSearch]);
|
|
60
71
|
|
|
72
|
+
// Height classes matching TextField sizes
|
|
73
|
+
const heightClass = size === "small" ? "h-[36px]" : "h-[44px]";
|
|
74
|
+
const iconPaddingClass = size === "small" ? "px-2" : "px-4";
|
|
75
|
+
const inputPaddingClass = size === "small" ? "pl-8" : "pl-12";
|
|
76
|
+
|
|
61
77
|
return (
|
|
62
78
|
<div
|
|
63
79
|
onClick={onClick}
|
|
64
80
|
className={cls("relative",
|
|
65
|
-
|
|
81
|
+
heightClass,
|
|
66
82
|
"bg-surface-accent-50 dark:bg-surface-800 border",
|
|
67
83
|
defaultBorderMixin,
|
|
68
84
|
"rounded-lg",
|
|
69
85
|
className)}>
|
|
70
86
|
<div
|
|
71
|
-
className="absolute p-0
|
|
72
|
-
{loading ? <CircularProgress size={"smallest"}/> : <SearchIcon className={"text-text-disabled dark:text-text-disabled-dark"}/>}
|
|
87
|
+
className={cls("absolute p-0 h-full pointer-events-none flex items-center justify-center top-0", iconPaddingClass)}>
|
|
88
|
+
{loading ? <CircularProgress size={"smallest"} /> : <SearchIcon className={"text-text-disabled dark:text-text-disabled-dark"} size={size === "small" ? "small" : "medium"} />}
|
|
73
89
|
</div>
|
|
74
90
|
<input
|
|
75
91
|
value={searchText ?? ""}
|
|
@@ -89,18 +105,20 @@ export function SearchBar({
|
|
|
89
105
|
(disabled || loading) && "pointer-events-none",
|
|
90
106
|
"placeholder-text-disabled dark:placeholder-text-disabled-dark",
|
|
91
107
|
"relative flex items-center rounded-lg transition-all bg-transparent outline-none appearance-none border-none",
|
|
92
|
-
"
|
|
108
|
+
inputPaddingClass, "h-full text-current",
|
|
109
|
+
size === "small" ? "text-sm" : "",
|
|
93
110
|
expandable ? (active ? "w-[220px]" : "w-[180px]") : "",
|
|
94
111
|
innerClassName
|
|
95
112
|
)}
|
|
96
113
|
/>
|
|
97
114
|
{searchText
|
|
98
115
|
? <IconButton
|
|
99
|
-
className={`${
|
|
116
|
+
className={`${size === "small" ? "mr-0 top-0" : "mr-1 top-0"} absolute right-0 z-10`}
|
|
117
|
+
size={"small"}
|
|
100
118
|
onClick={clearText}>
|
|
101
|
-
<CloseIcon size={"
|
|
119
|
+
<CloseIcon size={"smallest"} />
|
|
102
120
|
</IconButton>
|
|
103
|
-
: <div style={{ width: 26 }}/>
|
|
121
|
+
: <div style={{ width: 26 }} />
|
|
104
122
|
}
|
|
105
123
|
</div>
|
|
106
124
|
);
|