@firecms/ui 3.1.0-canary.1df3b2c → 3.1.0-canary.501d471

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 (36) hide show
  1. package/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
  2. package/dist/components/Chip.d.ts +1 -1
  3. package/dist/components/MultiSelect.d.ts +1 -1
  4. package/dist/components/ResizablePanels.d.ts +16 -0
  5. package/dist/components/SearchableSelect.d.ts +48 -0
  6. package/dist/components/Select.d.ts +1 -1
  7. package/dist/components/Tabs.d.ts +8 -1
  8. package/dist/components/Tooltip.d.ts +18 -2
  9. package/dist/components/index.d.ts +2 -0
  10. package/dist/hooks/useOutsideAlerter.d.ts +1 -1
  11. package/dist/icons/FirestoreIcon.d.ts +6 -0
  12. package/dist/icons/components/DatabaseIcon.d.ts +6 -0
  13. package/dist/icons/index.d.ts +2 -0
  14. package/dist/index.es.js +1444 -431
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/index.umd.js +1446 -433
  17. package/dist/index.umd.js.map +1 -1
  18. package/package.json +6 -6
  19. package/src/components/BooleanSwitchWithLabel.tsx +4 -0
  20. package/src/components/Button.tsx +2 -1
  21. package/src/components/Chip.tsx +4 -3
  22. package/src/components/DateTimeField.tsx +7 -2
  23. package/src/components/DebouncedTextField.tsx +3 -3
  24. package/src/components/MultiSelect.tsx +27 -10
  25. package/src/components/ResizablePanels.tsx +181 -0
  26. package/src/components/SearchableSelect.tsx +335 -0
  27. package/src/components/Select.tsx +62 -62
  28. package/src/components/Skeleton.tsx +4 -2
  29. package/src/components/Tabs.tsx +150 -34
  30. package/src/components/TextareaAutosize.tsx +77 -212
  31. package/src/components/Tooltip.tsx +7 -6
  32. package/src/components/index.tsx +2 -0
  33. package/src/hooks/useOutsideAlerter.tsx +1 -1
  34. package/src/icons/FirestoreIcon.tsx +47 -0
  35. package/src/icons/components/DatabaseIcon.tsx +10 -0
  36. package/src/icons/index.ts +2 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/ui",
3
3
  "type": "module",
4
- "version": "3.1.0-canary.1df3b2c",
4
+ "version": "3.1.0-canary.501d471",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -80,8 +80,8 @@
80
80
  "tailwind-merge": "^2.6.0"
81
81
  },
82
82
  "peerDependencies": {
83
- "react": ">=18.0.0",
84
- "react-dom": ">=18.0.0"
83
+ "react": ">=18.3.1 || >=19.0.0",
84
+ "react-dom": ">=18.3.1 || >=19.0.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@jest/globals": "^30.2.0",
@@ -91,8 +91,8 @@
91
91
  "@types/jest": "^29.5.14",
92
92
  "@types/node": "^20.19.17",
93
93
  "@types/object-hash": "^3.0.6",
94
- "@types/react": "^18.3.24",
95
- "@types/react-dom": "^18.3.7",
94
+ "@types/react": "^19.2.3",
95
+ "@types/react-dom": "^19.2.3",
96
96
  "@types/react-measure": "^2.0.12",
97
97
  "@vitejs/plugin-react": "^4.7.0",
98
98
  "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
@@ -114,7 +114,7 @@
114
114
  "index.css",
115
115
  "tailwind.config.js"
116
116
  ],
117
- "gitHead": "5074584b15be0d0507a4dadc148f03d82e9fe495",
117
+ "gitHead": "ed0d4e02d670aff9297a710a1d45a1095a1cbaea",
118
118
  "publishConfig": {
119
119
  "access": "public"
120
120
  }
@@ -18,6 +18,7 @@ export type BooleanSwitchWithLabelProps = BooleanSwitchProps & {
18
18
  fullWidth?: boolean,
19
19
  className?: string,
20
20
  inputClassName?: string,
21
+ switchAdornment?: React.ReactNode,
21
22
  };
22
23
 
23
24
  /**
@@ -37,6 +38,7 @@ export const BooleanSwitchWithLabel = function BooleanSwitchWithLabel({
37
38
  className,
38
39
  fullWidth = true,
39
40
  inputClassName,
41
+ switchAdornment,
40
42
  ...props
41
43
  }: BooleanSwitchWithLabelProps) {
42
44
 
@@ -100,6 +102,8 @@ export const BooleanSwitchWithLabel = function BooleanSwitchWithLabel({
100
102
  {...props}
101
103
  />
102
104
 
105
+ {switchAdornment}
106
+
103
107
  <div className={cls(
104
108
  "flex-grow",
105
109
  position === "end" ? "mr-4" : "ml-4",
@@ -28,6 +28,7 @@ const ButtonInner = React.memo(React.forwardRef<
28
28
  fullWidth = false,
29
29
  component: Component,
30
30
  color = "neutral",
31
+ loading,
31
32
  ...props
32
33
  }: ButtonProps<any>, ref) => {
33
34
 
@@ -63,7 +64,7 @@ const ButtonInner = React.memo(React.forwardRef<
63
64
  "text-text-disabled dark:text-text-disabled-dark": disabled,
64
65
  "border border-transparent opacity-50": variant === "text" && disabled,
65
66
  "border border-surface-500 opacity-50": variant === "outlined" && disabled,
66
- "border border-transparent bg-surface-300 dark:bg-surface-500 opacity-40 bg-surface-300/40 dark:bg-surface-500/40": variant === "filled" && disabled,
67
+ "border border-transparent bg-surface-300 dark:bg-surface-500 opacity-70 bg-surface-300/70 dark:bg-surface-500/70": variant === "filled" && disabled,
67
68
  });
68
69
 
69
70
  const sizeClasses = cls(
@@ -29,7 +29,7 @@ const sizeClassNames = {
29
29
  /**
30
30
  * @group Preview components
31
31
  */
32
- export function Chip({
32
+ export const Chip = React.forwardRef<HTMLDivElement, ChipProps>(function Chip({
33
33
  children,
34
34
  colorScheme,
35
35
  error,
@@ -39,11 +39,12 @@ export function Chip({
39
39
  size = "large",
40
40
  className,
41
41
  style
42
- }: ChipProps) {
42
+ }: ChipProps, ref) {
43
43
 
44
44
  const usedColorScheme = typeof colorScheme === "string" ? getColorSchemeForKey(colorScheme) : colorScheme;
45
45
  return (
46
46
  <div
47
+ ref={ref}
47
48
  className={cls("rounded-lg max-w-full w-max h-fit font-regular inline-flex gap-1",
48
49
  "text-ellipsis",
49
50
  "items-center",
@@ -67,4 +68,4 @@ export function Chip({
67
68
  {icon}
68
69
  </div>
69
70
  );
70
- }
71
+ });
@@ -51,7 +51,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
51
51
  const [focused, setFocused] = useState(false);
52
52
  const [internalValue, setInternalValue] = useState<string>("");
53
53
  const [isTyping, setIsTyping] = useState(false);
54
- const invalidValue = value !== undefined && value !== null && !(value instanceof Date);
54
+ const invalidValue = value !== undefined && value !== null && (!(value instanceof Date) || isNaN((value as Date).getTime()));
55
55
 
56
56
  useInjectStyles("DateTimeField", inputStyles);
57
57
 
@@ -84,7 +84,12 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
84
84
  };
85
85
 
86
86
  const formatter = new Intl.DateTimeFormat("en-CA", options);
87
- const parts = formatter.formatToParts(dateValue);
87
+ let parts: Intl.DateTimeFormatPart[];
88
+ try {
89
+ parts = formatter.formatToParts(dateValue);
90
+ } catch {
91
+ return "";
92
+ }
88
93
 
89
94
  const getPart = (type: string) => parts.find(p => p.type === type)?.value ?? "";
90
95
 
@@ -4,7 +4,7 @@ import { TextField, TextFieldProps } from "./index";
4
4
 
5
5
  export function DebouncedTextField<T extends string | number>(props: TextFieldProps<T>) {
6
6
 
7
- const previousEventRef = React.useRef<ChangeEvent<any>>();
7
+ const previousEventRef = React.useRef<ChangeEvent<any>>(undefined);
8
8
  const [internalValue, setInternalValue] = React.useState(props.value);
9
9
 
10
10
  const deferredValue = useDeferredValue(internalValue);
@@ -28,6 +28,6 @@ export function DebouncedTextField<T extends string | number>(props: TextFieldPr
28
28
  }, []);
29
29
 
30
30
  return <TextField {...props}
31
- onChange={internalOnChange}
32
- value={internalValue}/>
31
+ onChange={internalOnChange}
32
+ value={internalValue} />
33
33
  }
@@ -55,7 +55,7 @@ interface MultiSelectProps<T extends MultiSelectValue = string> {
55
55
  multiple?: boolean,
56
56
  includeSelectAll?: boolean,
57
57
  includeClear?: boolean,
58
- inputRef?: React.RefObject<HTMLButtonElement>,
58
+ inputRef?: React.RefObject<HTMLButtonElement | null>,
59
59
  padding?: boolean,
60
60
  invisible?: boolean,
61
61
  children: React.ReactNode;
@@ -90,6 +90,7 @@ export const MultiSelect = React.forwardRef<
90
90
  open,
91
91
  onOpenChange,
92
92
  portalContainer,
93
+ endAdornment,
93
94
  },
94
95
  ref
95
96
  ) => {
@@ -118,20 +119,18 @@ export const MultiSelect = React.forwardRef<
118
119
 
119
120
  const allValues = React.useMemo(() => children
120
121
  ?
121
- // @ts-ignore
122
122
  Children.map(children, (child) => {
123
- if (React.isValidElement(child)) {
123
+ if (React.isValidElement<MultiSelectItemProps>(child)) {
124
124
  return child.props.value;
125
125
  }
126
126
  return null;
127
- }).filter(Boolean) as any[]
127
+ })?.filter(Boolean) ?? []
128
128
  : [], [children]);
129
129
 
130
130
  const optionsMap = React.useMemo(() => {
131
131
  const map = new Map<string, React.ReactNode>();
132
132
  Children.forEach(children, (child) => {
133
- if (React.isValidElement(child)) {
134
- // @ts-ignore
133
+ if (React.isValidElement<MultiSelectItemProps>(child)) {
135
134
  map.set(String(child.props.value), child.props.children);
136
135
  }
137
136
  });
@@ -271,7 +270,7 @@ export const MultiSelect = React.forwardRef<
271
270
  })}
272
271
  </div>
273
272
  <div className="flex items-center justify-between">
274
- {includeClear && <CloseIcon
273
+ {includeClear && !endAdornment && <CloseIcon
275
274
  className={"ml-4"}
276
275
  size={"small"}
277
276
  onClick={(event) => {
@@ -279,6 +278,14 @@ export const MultiSelect = React.forwardRef<
279
278
  handleClear();
280
279
  }}
281
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
+ )}
282
289
  <div className={cls("px-2 h-full flex items-center")}>
283
290
  <KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
284
291
  className={cls("transition", isPopoverOpen ? "rotate-180" : "")} />
@@ -290,9 +297,19 @@ export const MultiSelect = React.forwardRef<
290
297
  <span className="text-sm">
291
298
  {placeholder}
292
299
  </span>
293
- <div className={cls("px-2 h-full flex items-center")}>
294
- <KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
295
- 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>
296
313
  </div>
297
314
  </div>
298
315
  )}
@@ -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
+ }