@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,8 +1,7 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { useLayoutEffect } from "react";
4
- import * as ReactDOM from "react-dom";
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<HTMLInputElement>(null);
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
- const containerWindow = window;
81
- const computedStyle = containerWindow.getComputedStyle(input);
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
- // If input's width is shrunk and it's not visible, don't sync height.
84
- if (computedStyle.width === "0px") {
85
- return {
86
- outerHeightStyle: 0
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
- const sizeReferenceElement = sizeRef?.current ?? shadowRef.current!;
91
- const inputShallow = shadowRef.current!;
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
- sizeReferenceElement.style.width = computedStyle.width;
94
- inputShallow.value = input.value || props.placeholder || "x";
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
- const boxSizing = computedStyle.boxSizing;
103
- const padding =
104
- getStyleValue(computedStyle.paddingBottom) + getStyleValue(computedStyle.paddingTop);
105
- const border =
106
- getStyleValue(computedStyle.borderBottomWidth) + getStyleValue(computedStyle.borderTopWidth);
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
- // The height of the inner content
110
- const innerHeight = sizeReferenceElement.scrollHeight;
65
+ // Restore immediately — all of this happens before paint (useLayoutEffect)
66
+ el.style.height = prevHeight;
67
+ el.style.overflowY = prevOverflow;
111
68
 
112
- // Measure height of a textarea with a single row
113
- inputShallow.value = "x";
114
- const singleRowHeight = sizeReferenceElement.scrollHeight;
69
+ const lineHeight = singleRowScrollH - paddingY;
115
70
 
116
- // The height of the outer content
117
- let outerHeight = innerHeight;
71
+ let targetHeight = scrollH; // includes padding
118
72
 
119
73
  if (minRows) {
120
- outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight);
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 overflow = Math.abs(outerHeight - innerHeight) <= 1;
80
+ const unclampedHeight = targetHeight;
131
81
 
132
- return {
133
- outerHeightStyle,
134
- overflow
135
- };
136
- }, [maxRows, minRows, props.placeholder]);
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
- const syncHeight = React.useCallback(() => {
171
- const newState = getUpdatedState();
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
- if (isEmpty(newState)) {
174
- return;
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
- const syncHeightWithFlushSync = React.useCallback(() => {
186
- const newState = getUpdatedState();
98
+ el.style.height = `${finalHeight}px`;
99
+ el.style.overflowY = shouldScroll ? "auto" : "hidden";
187
100
 
188
- if (isEmpty(newState)) {
189
- return;
101
+ if (onResize) {
102
+ onResize({ outerHeightStyle: finalHeight, overflow: !shouldScroll });
190
103
  }
104
+ }, [maxRows, minRows, ignoreBoxSizing, onResize]);
191
105
 
192
- // In React 18, state updates in a ResizeObserver's callback are happening after the paint which causes flickering
193
- // when doing some visual updates in it. Using flushSync ensures that the dom will be painted after the states updates happen
194
- // Related issue - https://github.com/facebook/react/issues/24331
195
- ReactDOM.flushSync(() => {
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
- syncHeightWithFlushSync();
115
+ syncHeight();
211
116
  }
212
117
  });
213
- let resizeObserver: ResizeObserver;
214
118
 
215
119
  const input = inputRef.current!;
216
- const containerWindow = window;
217
- if (typeof window === "undefined") {
218
- return;
219
- }
120
+ if (typeof window === "undefined") return;
220
121
 
221
- containerWindow.addEventListener("resize", handleResize);
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
- containerWindow.removeEventListener("resize", handleResize);
231
- if (resizeObserver) {
232
- resizeObserver.disconnect();
233
- }
132
+ window.removeEventListener("resize", handleResize);
133
+ resizeObserver?.disconnect();
234
134
  };
235
- }, [syncHeightWithFlushSync]);
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
- <React.Fragment>
259
- <textarea
260
- value={value}
261
- onChange={handleChange}
262
- className={props.className}
263
- ref={handleRef}
264
- onFocus={onFocus}
265
- onBlur={onBlur}
266
- // Apply the rows prop to get a "correct" first SSR paint
267
- rows={minRows as number}
268
- style={{
269
- height: state.outerHeightStyle,
270
- // Need a large enough difference to allow scrolling.
271
- // This prevents infinite rendering loop.
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
- }: TooltipProps) => {
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={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
 
@@ -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
+
@@ -2,3 +2,4 @@ export * from "./useInjectStyles";
2
2
  export * from "./useOutsideAlerter";
3
3
  export * from "./useDebounceValue";
4
4
  export * from "./useIconStyles";
5
+ export * from "./PortalContainerContext";
@@ -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
- const styleElement = document.getElementById(key);
14
- if (!styleElement) {
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
+ }