@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/dist/components/MultiSelect.d.ts +1 -1
- package/dist/components/Select.d.ts +1 -1
- package/dist/hooks/useOutsideAlerter.d.ts +1 -1
- package/dist/index.es.js +136 -22
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +136 -22
- package/dist/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/components/DebouncedTextField.tsx +3 -3
- package/src/components/MultiSelect.tsx +4 -6
- package/src/components/Select.tsx +62 -62
- package/src/components/Tabs.tsx +121 -33
- package/src/hooks/useOutsideAlerter.tsx +1 -1
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.
|
|
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": "^
|
|
95
|
-
"@types/react-dom": "^
|
|
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": "
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
<SelectPrimitive.Value
|
|
196
|
+
onClick={(e) => {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
e.stopPropagation();
|
|
199
|
+
}}
|
|
200
|
+
placeholder={placeholder}
|
|
201
|
+
className={"w-full"}>
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
{hasValue && value !== undefined && renderValue
|
|
204
|
+
? renderValue(value)
|
|
205
|
+
: (displayChildren || placeholder)
|
|
206
|
+
}
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
</SelectPrimitive.Value>
|
|
209
|
+
</div>
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
295
|
+
label,
|
|
296
|
+
children,
|
|
297
|
+
className
|
|
298
|
+
}: SelectGroupProps) {
|
|
299
299
|
return <>
|
|
300
300
|
<SelectPrimitive.Group
|
|
301
301
|
className={cls(
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
131
|
+
value,
|
|
132
|
+
className,
|
|
133
|
+
innerClassName,
|
|
134
|
+
children,
|
|
135
|
+
disabled
|
|
136
|
+
}: TabProps) {
|
|
49
137
|
return <TabsPrimitive.Trigger value={value}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|