@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
@@ -14,8 +14,8 @@ export function Skeleton({
14
14
  }: SkeletonProps) {
15
15
  return <span
16
16
  style={{
17
- width: width ? `${width}px` : "100%",
18
- height: height ? `${height}px` : "12px"
17
+ width: width !== undefined ? `${width}px` : undefined,
18
+ height: height !== undefined ? `${height}px` : undefined
19
19
  }}
20
20
  className={
21
21
  cls(
@@ -23,6 +23,8 @@ export function Skeleton({
23
23
  "bg-surface-accent-200 dark:bg-surface-accent-800 rounded-md",
24
24
  "animate-pulse",
25
25
  "max-w-full max-h-full",
26
+ width === undefined ? "w-full" : "",
27
+ height === undefined ? "h-3" : "",
26
28
  className)
27
29
  }/>;
28
30
  }
@@ -37,8 +37,8 @@ function SliderThumb(props: {
37
37
  "border-surface-accent-300 bg-surface-accent-300 dark:border-surface-700 dark:bg-surface-700": props.props.disabled
38
38
  },
39
39
  props.classes,
40
- "focus-visible:ring-4 focus-visible:ring-primary focus-visible:ring-opacity-50",
41
- "hover:ring-4 hover:ring-primary hover:ring-opacity-25",
40
+ "focus-visible:ring-4 focus-visible:ring-primary focus-visible:ring-opacity-50 focus-visible:ring-primary/50",
41
+ "hover:ring-4 hover:ring-primary hover:ring-opacity-25 hover:ring-primary/25",
42
42
  "block rounded-full transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50")}
43
43
 
44
44
  />
@@ -48,7 +48,7 @@ function SliderThumb(props: {
48
48
  className={cls(
49
49
  "TooltipContent",
50
50
  "max-w-lg leading-relaxed",
51
- "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",
51
+ "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",
52
52
  )}>
53
53
  {props.props.value?.[props.index]}
54
54
  </TooltipPrimitive.Content>
@@ -84,7 +84,7 @@ const Slider = React.forwardRef<
84
84
  >
85
85
  <SliderPrimitive.Track
86
86
  style={{ height: size === "small" ? 4 : 8 }}
87
- className={"relative w-full grow overflow-hidden rounded-full bg-surface-accent-300 bg-opacity-40 dark:bg-surface-700 dark:bg-opacity-40"}>
87
+ className={"relative w-full grow overflow-hidden rounded-full bg-surface-accent-300 bg-opacity-40 bg-surface-accent-300/40 dark:bg-surface-700 dark:bg-opacity-40 dark:bg-surface-700/40"}>
88
88
 
89
89
  <SliderPrimitive.Range
90
90
  className={cls("absolute h-full", {
@@ -36,7 +36,7 @@ export const TableBody = React.memo(({
36
36
  ...rest
37
37
  }: TableBodyProps) => (
38
38
  <tbody
39
- className={cls("bg-white dark:bg-surface-950 text-sm divide-y divide-surface-100 dark:divide-surface-700 dark:divide-opacity-70", className)}
39
+ className={cls("bg-white dark:bg-surface-950 text-sm divide-y divide-surface-100 dark:divide-surface-700 dark:divide-opacity-70 dark:divide-surface-700/70", className)}
40
40
  {...rest}
41
41
  >
42
42
  {children}
@@ -1,32 +1,137 @@
1
- import React from "react";
1
+ import React, { createContext, useContext, useRef, useState, useEffect } from "react";
2
2
  import * as TabsPrimitive from "@radix-ui/react-tabs";
3
3
  import { cls } from "../util";
4
+ import { defaultBorderMixin } from "../styles";
5
+ import { IconButton } from "./IconButton";
6
+ import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
7
+
8
+ type TabsMode = "primary" | "secondary";
9
+ const TabsModeContext = createContext<TabsMode>("primary");
4
10
 
5
11
  export type TabsProps = {
6
12
  value: string,
7
13
  children: React.ReactNode,
8
14
  innerClassName?: string,
9
15
  className?: string,
10
- onValueChange: (value: string) => void
16
+ onValueChange: (value: string) => void,
17
+ /**
18
+ * "primary" renders the default pill-style tabs.
19
+ * "secondary" renders underline-style tabs suitable for inner/nested panels.
20
+ */
21
+ mode?: TabsMode
11
22
  };
12
23
 
13
24
  export function Tabs({
14
- value,
15
- onValueChange,
16
- className,
17
- innerClassName,
18
- children
19
- }: TabsProps) {
25
+ value,
26
+ onValueChange,
27
+ className,
28
+ innerClassName,
29
+ children,
30
+ mode = "primary"
31
+ }: TabsProps) {
32
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
33
+ const [showLeftScroll, setShowLeftScroll] = useState(false);
34
+ const [showRightScroll, setShowRightScroll] = useState(false);
35
+ const [isScrollable, setIsScrollable] = useState(false);
36
+
37
+ const checkScroll = () => {
38
+ if (scrollContainerRef.current) {
39
+ const { scrollLeft, scrollWidth, clientWidth } = scrollContainerRef.current;
40
+ setShowLeftScroll(scrollLeft > 0);
41
+ setShowRightScroll(Math.ceil(scrollLeft + clientWidth) < scrollWidth);
42
+ setIsScrollable(scrollWidth > clientWidth);
43
+ }
44
+ };
45
+
46
+ useEffect(() => {
47
+ checkScroll();
48
+ window.addEventListener("resize", checkScroll);
49
+
50
+ let observer: ResizeObserver;
51
+ if (scrollContainerRef.current) {
52
+ observer = new ResizeObserver(checkScroll);
53
+ observer.observe(scrollContainerRef.current);
54
+ if (scrollContainerRef.current.firstElementChild) {
55
+ observer.observe(scrollContainerRef.current.firstElementChild);
56
+ }
57
+ }
58
+
59
+ return () => {
60
+ window.removeEventListener("resize", checkScroll);
61
+ observer?.disconnect();
62
+ };
63
+ }, [children]);
64
+
65
+ const scroll = (direction: "left" | "right") => {
66
+ if (scrollContainerRef.current) {
67
+ const container = scrollContainerRef.current;
68
+ const scrollAmount = Math.max(container.clientWidth / 2, 200);
69
+ const targetScroll = container.scrollLeft + (direction === "left" ? -scrollAmount : scrollAmount);
20
70
 
21
- return <TabsPrimitive.Root value={value} onValueChange={onValueChange} className={className}>
22
- <TabsPrimitive.List className={cls(
23
- "w-max",
24
- "flex text-sm font-medium text-center text-surface-accent-800 dark:text-white max-w-full overflow-auto no-scrollbar items-end",
25
- innerClassName)
26
- }>
27
- {children}
28
- </TabsPrimitive.List>
71
+ container.scrollTo({
72
+ left: targetScroll,
73
+ behavior: "smooth"
74
+ });
75
+ // checkScroll will be called by onScroll event
76
+ }
77
+ };
78
+
79
+ return <TabsModeContext.Provider value={mode}>
80
+ <TabsPrimitive.Root value={value} onValueChange={onValueChange} className={cls("flex flex-row items-center min-w-0", className)}>
81
+ {isScrollable && (
82
+ <button
83
+ type="button"
84
+ disabled={!showLeftScroll}
85
+ onClick={() => scroll("left")}
86
+ className={cls(
87
+ "flex-shrink-0 z-10 flex items-center justify-center rounded-md px-0.5 py-1.5 transition-all h-10 w-6",
88
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
89
+ "disabled:pointer-events-none disabled:opacity-0",
90
+ "text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
91
+ mode === "primary" && "mr-1 bg-surface-50 dark:bg-surface-900 border",
92
+ mode === "primary" && defaultBorderMixin,
93
+ mode === "secondary" && "mr-1"
94
+ )}
95
+ >
96
+ <ChevronLeftIcon size="small" />
97
+ </button>
98
+ )}
99
+ <div
100
+ ref={scrollContainerRef}
101
+ className="flex-1 overflow-x-auto no-scrollbar min-w-0"
102
+ onScroll={checkScroll}
103
+ style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
104
+ >
105
+ <TabsPrimitive.List className={cls(
106
+ mode === "primary" && "border",
107
+ mode === "primary" && defaultBorderMixin,
108
+ mode === "primary" && "gap-2 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",
109
+ mode === "secondary" && "gap-1 inline-flex h-9 items-center text-surface-500 dark:text-surface-400",
110
+ innerClassName)
111
+ }>
112
+ {children}
113
+ </TabsPrimitive.List>
114
+ </div>
115
+ {isScrollable && (
116
+ <button
117
+ type="button"
118
+ disabled={!showRightScroll}
119
+ onClick={() => scroll("right")}
120
+ className={cls(
121
+ "flex-shrink-0 z-10 flex items-center justify-center rounded-md px-0.5 py-1.5 transition-all h-10 w-6",
122
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400",
123
+ "disabled:pointer-events-none disabled:opacity-0",
124
+ "text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800",
125
+ mode === "primary" && "ml-1 bg-surface-50 dark:bg-surface-900 border",
126
+ mode === "primary" && defaultBorderMixin,
127
+ mode === "secondary" && "ml-1"
128
+ )}
129
+ >
130
+ <ChevronRightIcon size="small" />
131
+ </button>
132
+ )}
29
133
  </TabsPrimitive.Root>
134
+ </TabsModeContext.Provider>
30
135
  }
31
136
 
32
137
  export type TabProps = {
@@ -38,28 +143,36 @@ export type TabProps = {
38
143
  };
39
144
 
40
145
  export function Tab({
41
- value,
42
- className,
43
- innerClassName,
44
- children,
45
- disabled
46
- }: TabProps) {
146
+ value,
147
+ className,
148
+ innerClassName,
149
+ children,
150
+ disabled
151
+ }: TabProps) {
152
+ const mode = useContext(TabsModeContext);
153
+
154
+ const primaryClasses = cls(
155
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all",
156
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
157
+ "disabled:pointer-events-none disabled:opacity-50",
158
+ "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",
159
+ );
160
+
161
+ const secondaryClasses = cls(
162
+ "inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium transition-all",
163
+ "border-b-2 border-transparent -mb-px",
164
+ "focus-visible:outline-none",
165
+ "disabled:pointer-events-none disabled:opacity-50",
166
+ "hover:text-surface-700 dark:hover:text-surface-300",
167
+ "data-[state=active]:border-b-primary data-[state=active]:text-primary dark:data-[state=active]:border-b-primary dark:data-[state=active]:text-primary-dark",
168
+ );
169
+
47
170
  return <TabsPrimitive.Trigger value={value}
48
- disabled={disabled}
49
- className={cls(
50
- "border-b-2 border-transparent",
51
- "data-[state=active]:border-secondary",
52
- disabled
53
- ? "text-surface-accent-400 dark:text-surface-accent-500"
54
- : cls("text-surface-accent-700 dark:text-surface-accent-300",
55
- "data-[state=active]:text-surface-accent-900 data-[state=active]:dark:text-white",
56
- "hover:text-surface-accent-800 dark:hover:text-surface-accent-200"),
57
- className)}>
58
- <div className={cls("line-clamp-1",
59
- "uppercase inline-block p-2 px-4 rounded",
60
- "hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-800",
171
+ disabled={disabled}
172
+ className={cls(
173
+ mode === "secondary" ? secondaryClasses : primaryClasses,
174
+ className,
61
175
  innerClassName)}>
62
- {children}
63
- </div>
176
+ {children}
64
177
  </TabsPrimitive.Trigger>;
65
178
  }
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import React, { ForwardedRef, forwardRef, useEffect, useRef } from "react";
3
3
 
4
- import { TextareaAutosize } from "./TextareaAutosize";
5
4
  import {
6
5
  fieldBackgroundDisabledMixin,
7
6
  fieldBackgroundHoverMixin,
@@ -85,10 +84,16 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
85
84
 
86
85
  const inputRef = inputRefProp ?? useRef(null);
87
86
 
88
- // @ts-ignore
89
- const [focused, setFocused] = React.useState(document.activeElement === inputRef.current);
87
+ const [focused, setFocused] = React.useState(false);
90
88
  const hasValue = value !== undefined && value !== null && value !== "";
91
89
 
90
+ useEffect(() => {
91
+ // @ts-ignore
92
+ if (inputRef.current && document.activeElement === inputRef.current) {
93
+ setFocused(true);
94
+ }
95
+ }, []);
96
+
92
97
  useEffect(() => {
93
98
  if (type !== "number") return;
94
99
  const handleWheel = (event: any) => {
@@ -105,19 +110,21 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
105
110
  }, [inputRef, type]);
106
111
 
107
112
  const input = multiline ? (
108
- <TextareaAutosize
113
+ <textarea
109
114
  {...(inputProps as any)}
110
115
  ref={inputRef}
111
116
  placeholder={focused || hasValue || !label ? placeholder : undefined}
112
117
  autoFocus={autoFocus}
113
- minRows={minRows}
114
- maxRows={maxRows}
118
+ rows={typeof minRows === "string" ? parseInt(minRows) : (minRows ?? 3)}
115
119
  value={value ?? ""}
116
120
  onChange={onChange}
121
+ onFocus={() => setFocused(true)}
122
+ onBlur={() => setFocused(false)}
117
123
  style={inputStyle}
118
124
  className={cls(
119
125
  invisible ? focusedInvisibleMixin : "",
120
- "rounded-md resize-none w-full outline-none p-[32px] text-base bg-transparent min-h-[64px] px-3 pt-8",
126
+ "rounded-md resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3",
127
+ label ? "pt-8 pb-2" : "py-2",
121
128
  disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500",
122
129
  inputClassName
123
130
  )}
@@ -144,7 +151,11 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
144
151
  ? size === "large"
145
152
  ? "pt-8 pb-2"
146
153
  : "pt-4 pb-2"
147
- : "py-2",
154
+ : size === "smallest"
155
+ ? "py-0.5"
156
+ : size === "small"
157
+ ? "py-1"
158
+ : "py-2",
148
159
  endAdornment ? "pr-12" : "pr-3",
149
160
  disabled &&
150
161
  "outline-none opacity-65 dark:opacity-60 text-surface-accent-800 dark:text-white",