@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.
Files changed (77) hide show
  1. package/README.md +9 -7
  2. package/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
  3. package/dist/components/Card.d.ts +1 -1
  4. package/dist/components/Chip.d.ts +1 -1
  5. package/dist/components/ColorPicker.d.ts +30 -0
  6. package/dist/components/DateTimeField.d.ts +7 -0
  7. package/dist/components/Dialog.d.ts +2 -1
  8. package/dist/components/FileUpload.d.ts +1 -1
  9. package/dist/components/Menu.d.ts +2 -1
  10. package/dist/components/Menubar.d.ts +2 -1
  11. package/dist/components/MultiSelect.d.ts +2 -1
  12. package/dist/components/ResizablePanels.d.ts +16 -0
  13. package/dist/components/SearchBar.d.ts +11 -1
  14. package/dist/components/SearchableSelect.d.ts +48 -0
  15. package/dist/components/Select.d.ts +2 -1
  16. package/dist/components/Sheet.d.ts +1 -0
  17. package/dist/components/Tabs.d.ts +8 -1
  18. package/dist/components/ToggleButtonGroup.d.ts +30 -0
  19. package/dist/components/Tooltip.d.ts +18 -2
  20. package/dist/components/index.d.ts +4 -0
  21. package/dist/hooks/PortalContainerContext.d.ts +31 -0
  22. package/dist/hooks/index.d.ts +1 -0
  23. package/dist/hooks/useOutsideAlerter.d.ts +1 -1
  24. package/dist/icons/FirestoreIcon.d.ts +6 -0
  25. package/dist/icons/components/DatabaseIcon.d.ts +6 -0
  26. package/dist/icons/index.d.ts +2 -0
  27. package/dist/index.css +57 -6
  28. package/dist/index.es.js +2846 -1165
  29. package/dist/index.es.js.map +1 -1
  30. package/dist/index.umd.js +2846 -1165
  31. package/dist/index.umd.js.map +1 -1
  32. package/dist/styles.d.ts +11 -11
  33. package/package.json +7 -7
  34. package/src/components/BooleanSwitch.tsx +3 -3
  35. package/src/components/BooleanSwitchWithLabel.tsx +4 -0
  36. package/src/components/Button.tsx +6 -5
  37. package/src/components/Card.tsx +7 -7
  38. package/src/components/Checkbox.tsx +1 -1
  39. package/src/components/Chip.tsx +4 -3
  40. package/src/components/ColorPicker.tsx +134 -0
  41. package/src/components/DateTimeField.tsx +129 -35
  42. package/src/components/DebouncedTextField.tsx +3 -3
  43. package/src/components/Dialog.tsx +25 -16
  44. package/src/components/DialogActions.tsx +1 -1
  45. package/src/components/ExpandablePanel.tsx +1 -1
  46. package/src/components/FileUpload.tsx +25 -24
  47. package/src/components/IconButton.tsx +3 -2
  48. package/src/components/Menu.tsx +44 -30
  49. package/src/components/Menubar.tsx +14 -3
  50. package/src/components/MultiSelect.tsx +113 -77
  51. package/src/components/Popover.tsx +11 -3
  52. package/src/components/ResizablePanels.tsx +181 -0
  53. package/src/components/SearchBar.tsx +37 -19
  54. package/src/components/SearchableSelect.tsx +335 -0
  55. package/src/components/Select.tsx +86 -73
  56. package/src/components/Separator.tsx +2 -2
  57. package/src/components/Sheet.tsx +12 -3
  58. package/src/components/Skeleton.tsx +4 -2
  59. package/src/components/Slider.tsx +4 -4
  60. package/src/components/Table.tsx +1 -1
  61. package/src/components/Tabs.tsx +150 -37
  62. package/src/components/TextField.tsx +19 -8
  63. package/src/components/TextareaAutosize.tsx +77 -212
  64. package/src/components/ToggleButtonGroup.tsx +67 -0
  65. package/src/components/Tooltip.tsx +16 -8
  66. package/src/components/index.tsx +4 -0
  67. package/src/hooks/PortalContainerContext.tsx +48 -0
  68. package/src/hooks/index.ts +1 -0
  69. package/src/hooks/useInjectStyles.tsx +12 -3
  70. package/src/hooks/useOutsideAlerter.tsx +1 -1
  71. package/src/icons/FirestoreIcon.tsx +47 -0
  72. package/src/icons/components/DatabaseIcon.tsx +10 -0
  73. package/src/icons/index.ts +2 -0
  74. package/src/index.css +57 -6
  75. package/src/styles.ts +11 -11
  76. package/src/util/cls.ts +1 -1
  77. 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
- // Properly type the state variables to match the generic props
92
- const [isPopoverOpen, setIsPopoverOpen] = React.useState(open ?? false);
93
- const [selectedValues, setSelectedValues] = React.useState<any[]>(value ?? []);
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
- }).filter(Boolean) as any[]
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
- className={cls("transition", isPopoverOpen ? "rotate-180" : "")}/>
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
- {placeholder}
298
+ {placeholder}
275
299
  </span>
276
- <div className={cls("px-2 h-full flex items-center")}>
277
- <KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
278
- className={cls("transition", isPopoverOpen ? "rotate-180" : "")}/>
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.Content
285
- className={cls("z-50 relative overflow-hidden border bg-white dark:bg-surface-900 rounded-lg w-[400px]", defaultBorderMixin)}
286
- align="start"
287
- sideOffset={8}
288
- onEscapeKeyDown={() => onPopoverOpenChange(false)}
289
- >
290
- <CommandPrimitive>
291
- <div className={"flex flex-row items-center"}>
292
- <CommandPrimitive.Input
293
- className={cls(focusedDisabled, "bg-transparent outline-none flex-1 h-full w-full m-4 flex-grow ")}
294
- placeholder="Search..."
295
- onKeyDown={handleInputKeyDown}
296
- />
297
- {selectedValues.length > 0 && (
298
- <div
299
- onClick={handleClear}
300
- className="text-sm justify-center cursor-pointer py-3 px-4 text-text-secondary dark:text-text-secondary-dark">
301
- Clear
302
- </div>
303
- )}
304
- </div>
305
- <Separator orientation={"horizontal"} className={"my-0"}/>
306
- <CommandPrimitive.List>
307
- <CommandPrimitive.Empty className={"px-4 py-2"}>
308
- No results found.
309
- </CommandPrimitive.Empty>
310
- <CommandPrimitive.Group>
311
- {includeSelectAll && <CommandPrimitive.Item
312
- key="all"
313
- onSelect={toggleAll}
314
- className={
315
- cls(
316
- "flex flex-row items-center gap-1.5",
317
- "cursor-pointer",
318
- "m-1",
319
- "ring-offset-transparent",
320
- "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",
321
- "aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900",
322
- "cursor-pointer p-2 rounded aria-[selected=true]:bg-surface-accent-100 aria-[selected=true]:dark:bg-surface-accent-900"
323
- )
324
- }
325
- >
326
- <InnerCheckBox checked={selectedValues.length === allValues.length}/>
327
- <span className={"text-sm text-text-secondary dark:text-text-secondary-dark"}>(Select All)</span>
328
- </CommandPrimitive.Item>}
329
- {children}
330
- </CommandPrimitive.Group>
331
- </CommandPrimitive.List>
332
- </CommandPrimitive>
333
- </PopoverPrimitive.Content>
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
- children,
350
- value,
351
- className
352
- }: MultiSelectItemProps<T>) {
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
- <PopoverPrimitive.Portal container={portalContainer}>
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
- onClick,
26
- onTextSearch,
27
- placeholder = "Search",
28
- expandable = false,
29
- large = false,
30
- innerClassName,
31
- className,
32
- autoFocus,
33
- disabled,
34
- loading,
35
- inputRef
36
- }: SearchBarProps) {
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
- large ? "h-14" : "h-[44px]",
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 px-4 h-full pointer-events-none flex items-center justify-center top-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
- "pl-12 h-full text-current ",
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={`${large ? "mr-2 top-1" : "mr-1 top-0"} absolute right-0 z-10`}
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={"small"}/>
119
+ <CloseIcon size={"smallest"} />
102
120
  </IconButton>
103
- : <div style={{ width: 26 }}/>
121
+ : <div style={{ width: 26 }} />
104
122
  }
105
123
  </div>
106
124
  );