@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,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { useLayoutEffect } from "react";
|
|
4
|
-
import
|
|
5
|
-
import { cls, debounce } from "../util";
|
|
4
|
+
import { debounce } from "../util";
|
|
6
5
|
|
|
7
6
|
type State = {
|
|
8
7
|
outerHeightStyle: number;
|
|
@@ -13,33 +12,6 @@ function getStyleValue(value: string) {
|
|
|
13
12
|
return parseInt(value, 10) || 0;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
const styles: {
|
|
17
|
-
shadow: React.CSSProperties;
|
|
18
|
-
} = {
|
|
19
|
-
shadow: {
|
|
20
|
-
// Visibility needed to hide the extra text area on iPads
|
|
21
|
-
visibility: "hidden",
|
|
22
|
-
// Remove from the content flow
|
|
23
|
-
position: "absolute",
|
|
24
|
-
// Ignore the scrollbar width
|
|
25
|
-
overflow: "hidden",
|
|
26
|
-
height: 0,
|
|
27
|
-
top: 0,
|
|
28
|
-
left: 0,
|
|
29
|
-
// Create a new layer, increase the isolation of the computed values
|
|
30
|
-
transform: "translateZ(0)"
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function isEmpty(obj: State) {
|
|
35
|
-
return (
|
|
36
|
-
obj === undefined ||
|
|
37
|
-
obj === null ||
|
|
38
|
-
Object.keys(obj).length === 0 ||
|
|
39
|
-
(obj.outerHeightStyle === 0 && !obj.overflow)
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
15
|
export const TextareaAutosize = React.forwardRef(function TextareaAutosize(
|
|
44
16
|
props: TextareaAutosizeProps,
|
|
45
17
|
ref: React.ForwardedRef<Element>
|
|
@@ -60,166 +32,96 @@ export const TextareaAutosize = React.forwardRef(function TextareaAutosize(
|
|
|
60
32
|
} = props;
|
|
61
33
|
|
|
62
34
|
const { current: isControlled } = React.useRef(value != null);
|
|
63
|
-
const inputRef = React.useRef<
|
|
35
|
+
const inputRef = React.useRef<HTMLTextAreaElement>(null);
|
|
64
36
|
const handleRef = useForkRef(ref, inputRef);
|
|
65
|
-
const shadowRef = React.useRef<HTMLTextAreaElement>(null);
|
|
66
|
-
const renders = React.useRef(0);
|
|
67
|
-
const [state, setState] = React.useState<State>({
|
|
68
|
-
outerHeightStyle: 0
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const getUpdatedState = React.useCallback(() => {
|
|
72
|
-
|
|
73
|
-
const input = inputRef.current!;
|
|
74
|
-
if (typeof window === "undefined") {
|
|
75
|
-
return {
|
|
76
|
-
outerHeightStyle: 0
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
37
|
|
|
80
|
-
|
|
81
|
-
const
|
|
38
|
+
const syncHeight = React.useCallback(() => {
|
|
39
|
+
const el = inputRef.current;
|
|
40
|
+
if (!el || typeof window === "undefined") return;
|
|
41
|
+
if (el.offsetWidth === 0) return;
|
|
82
42
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
43
|
+
const cs = window.getComputedStyle(el);
|
|
44
|
+
const paddingY =
|
|
45
|
+
getStyleValue(cs.paddingTop) + getStyleValue(cs.paddingBottom);
|
|
46
|
+
const borderY =
|
|
47
|
+
getStyleValue(cs.borderTopWidth) + getStyleValue(cs.borderBottomWidth);
|
|
48
|
+
const boxSizing = cs.boxSizing;
|
|
89
49
|
|
|
90
|
-
|
|
91
|
-
const
|
|
50
|
+
// ── measure by temporarily collapsing the real element ──
|
|
51
|
+
const prevHeight = el.style.height;
|
|
52
|
+
const prevOverflow = el.style.overflowY;
|
|
53
|
+
el.style.overflowY = "hidden";
|
|
54
|
+
el.style.height = "0px";
|
|
92
55
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (inputShallow.value.slice(-1) === "\n") {
|
|
96
|
-
// Certain fonts which overflow the line height will cause the textarea
|
|
97
|
-
// to report a different scrollHeight depending on whether the last line
|
|
98
|
-
// is empty. Make it non-empty to avoid this issue.
|
|
99
|
-
inputShallow.value += " ";
|
|
100
|
-
}
|
|
56
|
+
// scrollHeight = content + padding (always, regardless of box-sizing)
|
|
57
|
+
const scrollH = el.scrollHeight;
|
|
101
58
|
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const minHeight = getStyleValue(computedStyle.minHeight);
|
|
59
|
+
// Measure single-row height for minRows / maxRows
|
|
60
|
+
const prevValue = el.value;
|
|
61
|
+
el.value = "x";
|
|
62
|
+
const singleRowScrollH = el.scrollHeight;
|
|
63
|
+
el.value = prevValue;
|
|
108
64
|
|
|
109
|
-
//
|
|
110
|
-
|
|
65
|
+
// Restore immediately — all of this happens before paint (useLayoutEffect)
|
|
66
|
+
el.style.height = prevHeight;
|
|
67
|
+
el.style.overflowY = prevOverflow;
|
|
111
68
|
|
|
112
|
-
|
|
113
|
-
inputShallow.value = "x";
|
|
114
|
-
const singleRowHeight = sizeReferenceElement.scrollHeight;
|
|
69
|
+
const lineHeight = singleRowScrollH - paddingY;
|
|
115
70
|
|
|
116
|
-
|
|
117
|
-
let outerHeight = innerHeight;
|
|
71
|
+
let targetHeight = scrollH; // includes padding
|
|
118
72
|
|
|
119
73
|
if (minRows) {
|
|
120
|
-
|
|
74
|
+
targetHeight = Math.max(
|
|
75
|
+
Number(minRows) * lineHeight + paddingY,
|
|
76
|
+
targetHeight
|
|
77
|
+
);
|
|
121
78
|
}
|
|
122
|
-
if (maxRows) {
|
|
123
|
-
outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight);
|
|
124
|
-
}
|
|
125
|
-
outerHeight = Math.max(outerHeight, singleRowHeight, minHeight);
|
|
126
|
-
|
|
127
|
-
// Take the box sizing into account for applying this value as a style.
|
|
128
|
-
const outerHeightStyle = outerHeight + (!ignoreBoxSizing && boxSizing === "border-box" ? padding + border : 0);
|
|
129
79
|
|
|
130
|
-
const
|
|
80
|
+
const unclampedHeight = targetHeight;
|
|
131
81
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const updateState = React.useCallback((prevState: State, newState: State) => {
|
|
139
|
-
const {
|
|
140
|
-
outerHeightStyle,
|
|
141
|
-
overflow
|
|
142
|
-
} = newState;
|
|
143
|
-
// Need a large enough difference to update the height.
|
|
144
|
-
// This prevents infinite rendering loop.
|
|
145
|
-
if (
|
|
146
|
-
renders.current < 20 &&
|
|
147
|
-
((outerHeightStyle > 0 &&
|
|
148
|
-
Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
|
|
149
|
-
prevState.overflow !== overflow)
|
|
150
|
-
) {
|
|
151
|
-
renders.current += 1;
|
|
152
|
-
return {
|
|
153
|
-
overflow,
|
|
154
|
-
outerHeightStyle
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
if (process.env.NODE_ENV !== "production") {
|
|
158
|
-
if (renders.current === 20) {
|
|
159
|
-
console.error(
|
|
160
|
-
[
|
|
161
|
-
"MUI: Too many re-renders. The layout is unstable.",
|
|
162
|
-
"TextareaAutosize limits the number of renders to prevent an infinite loop."
|
|
163
|
-
].join("\n")
|
|
164
|
-
);
|
|
165
|
-
}
|
|
82
|
+
if (maxRows) {
|
|
83
|
+
targetHeight = Math.min(
|
|
84
|
+
Number(maxRows) * lineHeight + paddingY,
|
|
85
|
+
targetHeight
|
|
86
|
+
);
|
|
166
87
|
}
|
|
167
|
-
return prevState;
|
|
168
|
-
}, []);
|
|
169
88
|
|
|
170
|
-
|
|
171
|
-
|
|
89
|
+
// For border-box, height CSS prop = content + padding + border.
|
|
90
|
+
// scrollHeight already includes padding, so only add border.
|
|
91
|
+
const extra =
|
|
92
|
+
!ignoreBoxSizing && boxSizing === "border-box" ? borderY : 0;
|
|
93
|
+
const finalHeight = Math.ceil(targetHeight + extra);
|
|
172
94
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
if (onResize) {
|
|
177
|
-
onResize(newState);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
setState((prevState) => {
|
|
181
|
-
return updateState(prevState, newState);
|
|
182
|
-
});
|
|
183
|
-
}, [getUpdatedState, onResize, updateState]);
|
|
95
|
+
const shouldScroll =
|
|
96
|
+
Math.abs(unclampedHeight - targetHeight) > 1;
|
|
184
97
|
|
|
185
|
-
|
|
186
|
-
|
|
98
|
+
el.style.height = `${finalHeight}px`;
|
|
99
|
+
el.style.overflowY = shouldScroll ? "auto" : "hidden";
|
|
187
100
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
101
|
+
if (onResize) {
|
|
102
|
+
onResize({ outerHeightStyle: finalHeight, overflow: !shouldScroll });
|
|
190
103
|
}
|
|
104
|
+
}, [maxRows, minRows, ignoreBoxSizing, onResize]);
|
|
191
105
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
setState((prevState) => {
|
|
197
|
-
return updateState(prevState, newState);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
}, [getUpdatedState, updateState]);
|
|
106
|
+
// ── sync on every layout ──
|
|
107
|
+
useLayoutEffect(() => {
|
|
108
|
+
syncHeight();
|
|
109
|
+
});
|
|
201
110
|
|
|
111
|
+
// ── sync on window resize / element resize ──
|
|
202
112
|
React.useEffect(() => {
|
|
203
113
|
const handleResize = debounce(() => {
|
|
204
|
-
renders.current = 0;
|
|
205
|
-
|
|
206
|
-
// If the TextareaAutosize component is replaced by Suspense with a fallback, the last
|
|
207
|
-
// ResizeObserver's handler that runs because of the change in the layout is trying to
|
|
208
|
-
// access a dom node that is no longer there (as the fallback component is being shown instead).
|
|
209
114
|
if (inputRef.current) {
|
|
210
|
-
|
|
115
|
+
syncHeight();
|
|
211
116
|
}
|
|
212
117
|
});
|
|
213
|
-
let resizeObserver: ResizeObserver;
|
|
214
118
|
|
|
215
119
|
const input = inputRef.current!;
|
|
216
|
-
|
|
217
|
-
if (typeof window === "undefined") {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
120
|
+
if (typeof window === "undefined") return;
|
|
220
121
|
|
|
221
|
-
|
|
122
|
+
window.addEventListener("resize", handleResize);
|
|
222
123
|
|
|
124
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
223
125
|
if (typeof ResizeObserver !== "undefined") {
|
|
224
126
|
resizeObserver = new ResizeObserver(handleResize);
|
|
225
127
|
resizeObserver.observe(input);
|
|
@@ -227,67 +129,35 @@ export const TextareaAutosize = React.forwardRef(function TextareaAutosize(
|
|
|
227
129
|
|
|
228
130
|
return () => {
|
|
229
131
|
handleResize.clear();
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
resizeObserver.disconnect();
|
|
233
|
-
}
|
|
132
|
+
window.removeEventListener("resize", handleResize);
|
|
133
|
+
resizeObserver?.disconnect();
|
|
234
134
|
};
|
|
235
|
-
}, [
|
|
236
|
-
|
|
237
|
-
useLayoutEffect(() => {
|
|
238
|
-
syncHeight();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
React.useEffect(() => {
|
|
242
|
-
renders.current = 0;
|
|
243
|
-
}, [value]);
|
|
135
|
+
}, [syncHeight]);
|
|
244
136
|
|
|
245
137
|
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
246
|
-
renders.current = 0;
|
|
247
|
-
|
|
248
138
|
if (!isControlled) {
|
|
249
139
|
syncHeight();
|
|
250
140
|
}
|
|
251
|
-
|
|
252
141
|
if (onChange) {
|
|
253
142
|
onChange(event);
|
|
254
143
|
}
|
|
255
144
|
};
|
|
256
145
|
|
|
257
146
|
return (
|
|
258
|
-
<
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
overflow: state.overflow ? "hidden" : undefined,
|
|
273
|
-
...style,
|
|
274
|
-
}}
|
|
275
|
-
onScroll={onScroll}
|
|
276
|
-
{...other}
|
|
277
|
-
/>
|
|
278
|
-
<textarea
|
|
279
|
-
aria-hidden
|
|
280
|
-
className={cls(props.className, props.shadowClassName)}
|
|
281
|
-
readOnly
|
|
282
|
-
ref={shadowRef}
|
|
283
|
-
tabIndex={-1}
|
|
284
|
-
style={{
|
|
285
|
-
padding: 0,
|
|
286
|
-
...styles.shadow,
|
|
287
|
-
...style,
|
|
288
|
-
}}
|
|
289
|
-
/>
|
|
290
|
-
</React.Fragment>
|
|
147
|
+
<textarea
|
|
148
|
+
value={value}
|
|
149
|
+
onChange={handleChange}
|
|
150
|
+
className={props.className}
|
|
151
|
+
ref={handleRef}
|
|
152
|
+
onFocus={onFocus}
|
|
153
|
+
onBlur={onBlur}
|
|
154
|
+
rows={minRows as number}
|
|
155
|
+
style={{
|
|
156
|
+
...style,
|
|
157
|
+
}}
|
|
158
|
+
onScroll={onScroll}
|
|
159
|
+
{...other}
|
|
160
|
+
/>
|
|
291
161
|
);
|
|
292
162
|
}) as React.FC<TextareaAutosizeProps & { ref?: React.ForwardedRef<Element> }>;
|
|
293
163
|
|
|
@@ -337,11 +207,6 @@ export type TextareaAutosizeProps = Omit<React.InputHTMLAttributes<HTMLTextAreaE
|
|
|
337
207
|
function useForkRef<Instance>(
|
|
338
208
|
...refs: Array<React.Ref<Instance> | undefined>
|
|
339
209
|
): React.RefCallback<Instance> | null {
|
|
340
|
-
/**
|
|
341
|
-
* This will create a new function if the refs passed to this hook change and are all defined.
|
|
342
|
-
* This means react will call the old forkRef with `null` and the new forkRef
|
|
343
|
-
* with the ref. Cleanup naturally emerges from this behavior.
|
|
344
|
-
*/
|
|
345
210
|
return React.useMemo(() => {
|
|
346
211
|
if (refs.every((ref) => ref == null)) {
|
|
347
212
|
return null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cls } from "../util";
|
|
3
|
+
|
|
4
|
+
export type ToggleButtonOption<T extends string = string> = {
|
|
5
|
+
value: T;
|
|
6
|
+
label: string;
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type ToggleButtonGroupProps<T extends string = string> = {
|
|
12
|
+
/**
|
|
13
|
+
* Currently selected value
|
|
14
|
+
*/
|
|
15
|
+
value: T;
|
|
16
|
+
/**
|
|
17
|
+
* Callback when value changes
|
|
18
|
+
*/
|
|
19
|
+
onValueChange: (value: T) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Options to display
|
|
22
|
+
*/
|
|
23
|
+
options: ToggleButtonOption<T>[];
|
|
24
|
+
/**
|
|
25
|
+
* Additional class names for the container
|
|
26
|
+
*/
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A toggle button group component for selecting one option from a set.
|
|
32
|
+
* Displays options as buttons in a horizontal row with active state styling.
|
|
33
|
+
*/
|
|
34
|
+
export function ToggleButtonGroup<T extends string = string>({
|
|
35
|
+
value,
|
|
36
|
+
onValueChange,
|
|
37
|
+
options,
|
|
38
|
+
className
|
|
39
|
+
}: ToggleButtonGroupProps<T>) {
|
|
40
|
+
return (
|
|
41
|
+
<div className={cls("inline-flex flex-row bg-surface-100 dark:bg-surface-800 rounded-lg p-1 gap-1", className)}>
|
|
42
|
+
{options.map((option) => (
|
|
43
|
+
<button
|
|
44
|
+
key={option.value}
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={(e) => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
if (!option.disabled) {
|
|
49
|
+
onValueChange(option.value);
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
disabled={option.disabled}
|
|
53
|
+
className={cls(
|
|
54
|
+
"flex flex-row items-center justify-center gap-2 py-3 px-4 rounded-md transition-colors",
|
|
55
|
+
value === option.value
|
|
56
|
+
? "bg-white dark:bg-surface-950 text-primary dark:text-primary-300"
|
|
57
|
+
: "text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700",
|
|
58
|
+
option.disabled && "opacity-50 cursor-not-allowed"
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{option.icon}
|
|
62
|
+
<span className="text-sm font-medium">{option.label}</span>
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -4,6 +4,7 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
|
4
4
|
|
|
5
5
|
import { cls } from "../util";
|
|
6
6
|
import { useInjectStyles } from "../hooks";
|
|
7
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
7
8
|
|
|
8
9
|
export type TooltipProps = {
|
|
9
10
|
open?: boolean,
|
|
@@ -21,9 +22,9 @@ export type TooltipProps = {
|
|
|
21
22
|
className?: string,
|
|
22
23
|
container?: HTMLElement,
|
|
23
24
|
style?: React.CSSProperties;
|
|
24
|
-
}
|
|
25
|
+
} & Omit<React.HTMLAttributes<HTMLDivElement>, "title">;
|
|
25
26
|
|
|
26
|
-
export const Tooltip = ({
|
|
27
|
+
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(({
|
|
27
28
|
open,
|
|
28
29
|
defaultOpen,
|
|
29
30
|
side = "bottom",
|
|
@@ -38,11 +39,18 @@ export const Tooltip = ({
|
|
|
38
39
|
asChild = false,
|
|
39
40
|
container,
|
|
40
41
|
className,
|
|
41
|
-
style
|
|
42
|
-
|
|
42
|
+
style,
|
|
43
|
+
...props
|
|
44
|
+
}, ref) => {
|
|
43
45
|
|
|
44
46
|
useInjectStyles("Tooltip", styles);
|
|
45
47
|
|
|
48
|
+
// Get the portal container from context
|
|
49
|
+
const contextContainer = usePortalContainer();
|
|
50
|
+
|
|
51
|
+
// Prioritize manual prop, fallback to context container
|
|
52
|
+
const finalContainer = (container ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
53
|
+
|
|
46
54
|
if (!title)
|
|
47
55
|
return <>{children}</>;
|
|
48
56
|
|
|
@@ -51,7 +59,7 @@ export const Tooltip = ({
|
|
|
51
59
|
{children}
|
|
52
60
|
</TooltipPrimitive.Trigger>
|
|
53
61
|
: <TooltipPrimitive.Trigger asChild={true}>
|
|
54
|
-
<div style={style} className={className}>
|
|
62
|
+
<div style={style} className={className} ref={ref} {...props}>
|
|
55
63
|
{children}
|
|
56
64
|
</div>
|
|
57
65
|
</TooltipPrimitive.Trigger>;
|
|
@@ -60,11 +68,11 @@ export const Tooltip = ({
|
|
|
60
68
|
<TooltipPrimitive.Provider delayDuration={delayDuration}>
|
|
61
69
|
<TooltipPrimitive.Root open={open} onOpenChange={onOpenChange} defaultOpen={defaultOpen}>
|
|
62
70
|
{trigger}
|
|
63
|
-
<TooltipPrimitive.Portal container={
|
|
71
|
+
<TooltipPrimitive.Portal container={finalContainer}>
|
|
64
72
|
<TooltipPrimitive.Content
|
|
65
73
|
className={cls("TooltipContent",
|
|
66
74
|
"max-w-lg leading-relaxed",
|
|
67
|
-
"z-50 rounded px-3 py-2 text-xs leading-none bg-surface-accent-700 dark:bg-surface-accent-800 bg-opacity-90 font-medium text-surface-accent-50 shadow-2xl select-none duration-400 ease-in transform opacity-100",
|
|
75
|
+
"z-50 rounded px-3 py-2 text-xs leading-none bg-surface-accent-700 dark:bg-surface-accent-800 bg-opacity-90 bg-surface-accent-700/90 dark:bg-surface-accent-800/90 font-medium text-surface-accent-50 shadow-2xl select-none duration-400 ease-in transform opacity-100",
|
|
68
76
|
tooltipClassName)}
|
|
69
77
|
style={tooltipStyle}
|
|
70
78
|
sideOffset={sideOffset === undefined ? 4 : sideOffset}
|
|
@@ -76,7 +84,7 @@ export const Tooltip = ({
|
|
|
76
84
|
</TooltipPrimitive.Root>
|
|
77
85
|
</TooltipPrimitive.Provider>
|
|
78
86
|
);
|
|
79
|
-
};
|
|
87
|
+
});
|
|
80
88
|
|
|
81
89
|
const styles = `
|
|
82
90
|
|
package/src/components/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ export * from "./Collapse";
|
|
|
11
11
|
export * from "./CircularProgress";
|
|
12
12
|
export * from "./Checkbox";
|
|
13
13
|
export * from "./Chip";
|
|
14
|
+
export * from "./ColorPicker";
|
|
14
15
|
export * from "./DateTimeField";
|
|
15
16
|
export * from "./Dialog";
|
|
16
17
|
export * from "./DialogActions";
|
|
@@ -29,7 +30,9 @@ export * from "./Menubar";
|
|
|
29
30
|
export * from "./MultiSelect";
|
|
30
31
|
export * from "./Paper";
|
|
31
32
|
export * from "./RadioGroup";
|
|
33
|
+
export * from "./ResizablePanels";
|
|
32
34
|
export * from "./SearchBar";
|
|
35
|
+
export * from "./SearchableSelect";
|
|
33
36
|
export * from "./Select";
|
|
34
37
|
export * from "./Separator";
|
|
35
38
|
export * from "./Slider";
|
|
@@ -44,4 +47,5 @@ export * from "./Popover";
|
|
|
44
47
|
export * from "./Badge";
|
|
45
48
|
export * from "./DebouncedTextField";
|
|
46
49
|
export * from "./Skeleton";
|
|
50
|
+
export * from "./ToggleButtonGroup";
|
|
47
51
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { createContext, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
export interface PortalContainerContextType {
|
|
5
|
+
container: HTMLElement | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const PortalContainerContext = createContext<PortalContainerContextType | undefined>(undefined);
|
|
9
|
+
|
|
10
|
+
export interface PortalContainerProviderProps {
|
|
11
|
+
container: HTMLElement | null;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provider component that sets the portal container for all descendants.
|
|
17
|
+
* This can be used at any level of the tree to specify where portals should be attached.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const containerRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
*
|
|
23
|
+
* <div ref={containerRef}>
|
|
24
|
+
* <PortalContainerProvider container={containerRef.current}>
|
|
25
|
+
* <YourComponents />
|
|
26
|
+
* </PortalContainerProvider>
|
|
27
|
+
* </div>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function PortalContainerProvider({ container, children }: PortalContainerProviderProps) {
|
|
31
|
+
return (
|
|
32
|
+
<PortalContainerContext.Provider value={{ container }}>
|
|
33
|
+
{children}
|
|
34
|
+
</PortalContainerContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hook to access the portal container from context.
|
|
40
|
+
* Returns null if no provider is found in the tree.
|
|
41
|
+
*
|
|
42
|
+
* @returns The portal container element or null
|
|
43
|
+
*/
|
|
44
|
+
export function usePortalContainer(): HTMLElement | null {
|
|
45
|
+
const context = useContext(PortalContainerContext);
|
|
46
|
+
return context?.container ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
+
import { usePortalContainer } from "./PortalContainerContext";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Use this hook to create a `<style>` element and inject it into the DOM.
|
|
@@ -9,13 +10,21 @@ import { useEffect } from "react";
|
|
|
9
10
|
*/
|
|
10
11
|
export function useInjectStyles(key: string, styles: string) {
|
|
11
12
|
|
|
13
|
+
const portalContainer = usePortalContainer();
|
|
14
|
+
|
|
12
15
|
useEffect(() => {
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
if (typeof document === "undefined") return;
|
|
17
|
+
|
|
18
|
+
const targetContainer: HTMLElement | null = portalContainer ?? document.head;
|
|
19
|
+
|
|
20
|
+
// Try to find an existing style element within the target container first
|
|
21
|
+
const existingStyle = (targetContainer as HTMLElement).querySelector?.(`#${key}`) as HTMLStyleElement | null;
|
|
22
|
+
|
|
23
|
+
if (!existingStyle) {
|
|
15
24
|
const style = document.createElement("style");
|
|
16
25
|
style.id = key;
|
|
17
26
|
style.innerHTML = styles;
|
|
18
|
-
document.head.appendChild(style);
|
|
27
|
+
(targetContainer || document.head).appendChild(style);
|
|
19
28
|
}
|
|
20
29
|
}, []);
|
|
21
30
|
|
|
@@ -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;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { IconProps } from "./Icon";
|
|
4
|
+
|
|
5
|
+
const sizeMap: Record<string, number> = {
|
|
6
|
+
smallest: 16,
|
|
7
|
+
small: 20,
|
|
8
|
+
medium: 24,
|
|
9
|
+
large: 28,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Firebase Firestore flame icon (monochrome, uses currentColor).
|
|
14
|
+
* @group Icons
|
|
15
|
+
*/
|
|
16
|
+
export function FirestoreIcon(props: IconProps) {
|
|
17
|
+
const s = typeof props.size === "number"
|
|
18
|
+
? props.size
|
|
19
|
+
: sizeMap[props.size ?? "medium"] ?? 24;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
className={props.className}
|
|
25
|
+
fill={"currentColor"}
|
|
26
|
+
width={s}
|
|
27
|
+
height={s}
|
|
28
|
+
viewBox="0 0 73 91"
|
|
29
|
+
>
|
|
30
|
+
<path
|
|
31
|
+
d="M22.575 87.933A52.16 52.16 0 0034.787 90.513c5.84.204 11.395-1.004 16.359-3.298a70.68 70.68 0 01-15.948-10.013c-2.98 4.778-7.393 8.548-12.623 10.731z"
|
|
32
|
+
opacity=".7"
|
|
33
|
+
/>
|
|
34
|
+
<path
|
|
35
|
+
d="M35.2 77.205c-10.505-9.714-16.878-23.776-16.339-39.2.018-.499.045-1.001.075-1.5a39.51 39.51 0 00-5.866-.855 38.77 38.77 0 00-8.34.997A53.07 53.07 0 00.022 53.236c-.544 15.58 8.884 29.191 22.553 34.697 5.23-2.18 9.642-5.948 12.625-10.728z"
|
|
36
|
+
opacity=".6"
|
|
37
|
+
/>
|
|
38
|
+
<path
|
|
39
|
+
d="M35.2 77.205a31.63 31.63 0 004.096-13.428c.452-12.985-8.278-24.155-20.36-27.273-.03.5-.058 1.002-.076 1.502-.536 15.421 5.835 29.483 16.34 39.199z"
|
|
40
|
+
opacity=".7"
|
|
41
|
+
/>
|
|
42
|
+
<path
|
|
43
|
+
d="M37.944 0a73.99 73.99 0 00-15.603 21.156 72.82 72.82 0 00-3.41 15.349c12.082 3.117 20.812 14.288 20.36 27.275a31.58 31.58 0 01-4.098 13.425 70.76 70.76 0 0015.948 10.013c11.951-5.523 20.43-17.41 20.919-31.467.318-9.11-3.181-17.228-8.126-24.081C58.711 24.424 37.944 0 37.944 0z"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
}
|