@firecms/ui 3.1.0-canary.1df3b2c → 3.1.0-canary.24c8270

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/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.24c8270",
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": "fa98925bad34308ed66e7ea68adcc07eb38184ae",
118
118
  "publishConfig": {
119
119
  "access": "public"
120
120
  }
@@ -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;
@@ -118,20 +118,18 @@ export const MultiSelect = React.forwardRef<
118
118
 
119
119
  const allValues = React.useMemo(() => children
120
120
  ?
121
- // @ts-ignore
122
121
  Children.map(children, (child) => {
123
- if (React.isValidElement(child)) {
122
+ if (React.isValidElement<MultiSelectItemProps>(child)) {
124
123
  return child.props.value;
125
124
  }
126
125
  return null;
127
- }).filter(Boolean) as any[]
126
+ })?.filter(Boolean) ?? []
128
127
  : [], [children]);
129
128
 
130
129
  const optionsMap = React.useMemo(() => {
131
130
  const map = new Map<string, React.ReactNode>();
132
131
  Children.forEach(children, (child) => {
133
- if (React.isValidElement(child)) {
134
- // @ts-ignore
132
+ if (React.isValidElement<MultiSelectItemProps>(child)) {
135
133
  map.set(String(child.props.value), child.props.children);
136
134
  }
137
135
  });
@@ -36,7 +36,7 @@ export type SelectProps<T extends SelectValue = string> = {
36
36
  error?: boolean,
37
37
  position?: "item-aligned" | "popper",
38
38
  endAdornment?: React.ReactNode,
39
- inputRef?: React.RefObject<HTMLButtonElement>,
39
+ inputRef?: React.RefObject<HTMLButtonElement | null>,
40
40
  padding?: boolean,
41
41
  invisible?: boolean,
42
42
  children?: React.ReactNode;
@@ -45,33 +45,33 @@ export type SelectProps<T extends SelectValue = string> = {
45
45
  };
46
46
 
47
47
  export const Select = forwardRef<HTMLDivElement, SelectProps>(({
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) => {
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) => {
75
75
 
76
76
  const [openInternal, setOpenInternal] = useState(open ?? false);
77
77
 
@@ -115,7 +115,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
115
115
  // Find the child that matches the current value to display its content
116
116
  let found: React.ReactNode = null;
117
117
  Children.forEach(children, (child) => {
118
- if (React.isValidElement(child) && String((child.props as any).value) === String(value)) {
118
+ if (React.isValidElement<SelectItemProps>(child) && String(child.props.value) === String(value)) {
119
119
  found = child.props.children;
120
120
  }
121
121
  });
@@ -192,30 +192,30 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
192
192
  "min-h-[64px]": size === "large"
193
193
  }
194
194
  )}>
195
- <SelectPrimitive.Value
196
- onClick={(e) => {
197
- e.preventDefault();
198
- e.stopPropagation();
199
- }}
200
- placeholder={placeholder}
201
- className={"w-full"}>
195
+ <SelectPrimitive.Value
196
+ onClick={(e) => {
197
+ e.preventDefault();
198
+ e.stopPropagation();
199
+ }}
200
+ placeholder={placeholder}
201
+ className={"w-full"}>
202
202
 
203
- {hasValue && value !== undefined && renderValue
204
- ? renderValue(value)
205
- : (displayChildren || placeholder)
206
- }
203
+ {hasValue && value !== undefined && renderValue
204
+ ? renderValue(value)
205
+ : (displayChildren || placeholder)
206
+ }
207
207
 
208
- </SelectPrimitive.Value>
209
- </div>
208
+ </SelectPrimitive.Value>
209
+ </div>
210
210
 
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>
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>
219
219
 
220
220
  {endAdornment && (
221
221
  <div
@@ -232,9 +232,9 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
232
232
  {/* Pass the calculated finalContainer */}
233
233
  <SelectPrimitive.Portal container={finalContainer}>
234
234
  <SelectPrimitive.Content position={position}
235
- className={cls(focusedDisabled, "z-50 relative overflow-hidden border bg-white dark:bg-surface-900 p-2 rounded-lg", defaultBorderMixin)}>
235
+ className={cls(focusedDisabled, "z-50 relative overflow-hidden border bg-white dark:bg-surface-900 p-2 rounded-lg", defaultBorderMixin)}>
236
236
  <SelectPrimitive.Viewport className={cls("p-1", viewportClassName)}
237
- style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
237
+ style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
238
238
  {children}
239
239
  </SelectPrimitive.Viewport>
240
240
  </SelectPrimitive.Content>
@@ -254,11 +254,11 @@ export type SelectItemProps<T extends SelectValue = string> = {
254
254
  };
255
255
 
256
256
  export const SelectItem = React.memo(function SelectItem<T extends SelectValue = string>({
257
- value,
258
- children,
259
- disabled,
260
- className
261
- }: SelectItemProps<T>) {
257
+ value,
258
+ children,
259
+ disabled,
260
+ className
261
+ }: SelectItemProps<T>) {
262
262
  // Convert value to string for Radix UI
263
263
  const stringValue = String(value);
264
264
 
@@ -280,7 +280,7 @@ export const SelectItem = React.memo(function SelectItem<T extends SelectValue =
280
280
  <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
281
281
  <div
282
282
  className="absolute left-1 data-[state=checked]:block hidden">
283
- <CheckIcon size={16}/>
283
+ <CheckIcon size={16} />
284
284
  </div>
285
285
  </SelectPrimitive.Item>;
286
286
  });
@@ -292,10 +292,10 @@ export type SelectGroupProps = {
292
292
  };
293
293
 
294
294
  export const SelectGroup = React.memo(function SelectGroup({
295
- label,
296
- children,
297
- className
298
- }: SelectGroupProps) {
295
+ label,
296
+ children,
297
+ className
298
+ }: SelectGroupProps) {
299
299
  return <>
300
300
  <SelectPrimitive.Group
301
301
  className={cls(
@@ -1,7 +1,9 @@
1
- import React from "react";
1
+ import React, { useRef, useState, useEffect } from "react";
2
2
  import * as TabsPrimitive from "@radix-ui/react-tabs";
3
3
  import { cls } from "../util";
4
4
  import { defaultBorderMixin } from "../styles";
5
+ import { IconButton } from "./IconButton";
6
+ import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
5
7
 
6
8
  export type TabsProps = {
7
9
  value: string,
@@ -12,22 +14,108 @@ export type TabsProps = {
12
14
  };
13
15
 
14
16
  export function Tabs({
15
- value,
16
- onValueChange,
17
- className,
18
- innerClassName,
19
- children
20
- }: TabsProps) {
21
- return <TabsPrimitive.Root value={value} onValueChange={onValueChange} className={className}>
22
- <TabsPrimitive.List className={cls(
23
- "border",
24
- defaultBorderMixin,
25
- "gap-2",
26
- "inline-flex h-10 items-center justify-center rounded-md bg-surface-50 p-1 text-surface-600 dark:bg-surface-900 dark:text-surface-400",
27
- innerClassName)
28
- }>
29
- {children}
30
- </TabsPrimitive.List>
17
+ value,
18
+ onValueChange,
19
+ className,
20
+ innerClassName,
21
+ children
22
+ }: TabsProps) {
23
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
24
+ const [showLeftScroll, setShowLeftScroll] = useState(false);
25
+ const [showRightScroll, setShowRightScroll] = useState(false);
26
+ const [isScrollable, setIsScrollable] = useState(false);
27
+
28
+ const checkScroll = () => {
29
+ if (scrollContainerRef.current) {
30
+ const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
31
+ setShowLeftScroll(scrollLeft > 0);
32
+ setShowRightScroll(Math.ceil(scrollLeft + clientWidth) < scrollWidth);
33
+ setIsScrollable(scrollWidth > clientWidth);
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ checkScroll();
39
+ window.addEventListener("resize", checkScroll);
40
+
41
+ let observer: ResizeObserver;
42
+ if (scrollContainerRef.current) {
43
+ observer = new ResizeObserver(checkScroll);
44
+ observer.observe(scrollContainerRef.current);
45
+ if (scrollContainerRef.current.firstElementChild) {
46
+ observer.observe(scrollContainerRef.current.firstElementChild);
47
+ }
48
+ }
49
+
50
+ return () => {
51
+ window.removeEventListener("resize", checkScroll);
52
+ observer?.disconnect();
53
+ };
54
+ }, [children]);
55
+
56
+ const scroll = (direction: "left" | "right") => {
57
+ if (scrollContainerRef.current) {
58
+ const container = scrollContainerRef.current;
59
+ const scrollAmount = Math.max(container.clientWidth / 2, 200);
60
+ const targetScroll = container.scrollLeft + (direction === "left" ? -scrollAmount : scrollAmount);
61
+
62
+ container.scrollTo({
63
+ left: targetScroll,
64
+ behavior: "smooth"
65
+ });
66
+ // checkScroll will be called by onScroll event
67
+ }
68
+ };
69
+
70
+ return <TabsPrimitive.Root value={value} onValueChange={onValueChange} className={cls("flex flex-row items-center min-w-0", className)}>
71
+ {isScrollable && (
72
+ <button
73
+ type="button"
74
+ disabled={!showLeftScroll}
75
+ onClick={() => scroll("left")}
76
+ className={cls(
77
+ "flex-shrink-0 z-10 flex items-center justify-center rounded-md px-0.5 py-1.5 transition-all h-10 w-6",
78
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
79
+ "disabled:pointer-events-none disabled:opacity-0",
80
+ "text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
81
+ "mr-1 bg-surface-50 dark:bg-surface-900 border", defaultBorderMixin
82
+ )}
83
+ >
84
+ <ChevronLeftIcon size="small" />
85
+ </button>
86
+ )}
87
+ <div
88
+ ref={scrollContainerRef}
89
+ className="flex-1 overflow-x-auto no-scrollbar min-w-0"
90
+ onScroll={checkScroll}
91
+ style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
92
+ >
93
+ <TabsPrimitive.List className={cls(
94
+ "border",
95
+ defaultBorderMixin,
96
+ "gap-2",
97
+ "inline-flex h-10 items-center justify-center rounded-md bg-surface-50 p-1 text-surface-600 dark:bg-surface-900 dark:text-surface-400",
98
+ innerClassName)
99
+ }>
100
+ {children}
101
+ </TabsPrimitive.List>
102
+ </div>
103
+ {isScrollable && (
104
+ <button
105
+ type="button"
106
+ disabled={!showRightScroll}
107
+ onClick={() => scroll("right")}
108
+ className={cls(
109
+ "flex-shrink-0 z-10 flex items-center justify-center rounded-md px-0.5 py-1.5 transition-all h-10 w-6",
110
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
111
+ "disabled:pointer-events-none disabled:opacity-0",
112
+ "text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
113
+ "ml-1 bg-surface-50 dark:bg-surface-900 border", defaultBorderMixin
114
+ )}
115
+ >
116
+ <ChevronRightIcon size="small" />
117
+ </button>
118
+ )}
31
119
  </TabsPrimitive.Root>
32
120
  }
33
121
 
@@ -40,23 +128,23 @@ export type TabProps = {
40
128
  };
41
129
 
42
130
  export function Tab({
43
- value,
44
- className,
45
- innerClassName,
46
- children,
47
- disabled
48
- }: TabProps) {
131
+ value,
132
+ className,
133
+ innerClassName,
134
+ children,
135
+ disabled
136
+ }: TabProps) {
49
137
  return <TabsPrimitive.Trigger value={value}
50
- disabled={disabled}
51
- className={cls(
52
- "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all",
53
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
54
- "disabled:pointer-events-none disabled:opacity-50",
55
- "data-[state=active]:bg-white data-[state=active]:text-surface-900 dark:data-[state=active]:bg-surface-950 dark:data-[state=active]:text-surface-50",
56
- // "data-[state=active]:border",
57
- // defaultBorderMixin,
58
- className,
59
- innerClassName)}>
138
+ disabled={disabled}
139
+ className={cls(
140
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all",
141
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
142
+ "disabled:pointer-events-none disabled:opacity-50",
143
+ "data-[state=active]:bg-white data-[state=active]:text-surface-900 dark:data-[state=active]:bg-surface-950 dark:data-[state=active]:text-surface-50",
144
+ // "data-[state=active]:border",
145
+ // defaultBorderMixin,
146
+ className,
147
+ innerClassName)}>
60
148
  {children}
61
149
  </TabsPrimitive.Trigger>;
62
150
  }
@@ -5,7 +5,7 @@ import { RefObject, useEffect } from "react";
5
5
  /**
6
6
  * Hook that alerts clicks outside the passed ref
7
7
  */
8
- export function useOutsideAlerter(ref: RefObject<HTMLElement>, onOutsideClick: () => void, active = true): void {
8
+ export function useOutsideAlerter(ref: RefObject<HTMLElement | null>, onOutsideClick: () => void, active = true): void {
9
9
  useEffect(() => {
10
10
  if (!active)
11
11
  return;