@firecms/ui 3.0.1 → 3.1.0-canary.7d91b7c

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 (59) hide show
  1. package/README.md +9 -7
  2. package/dist/components/Card.d.ts +1 -1
  3. package/dist/components/ColorPicker.d.ts +30 -0
  4. package/dist/components/DateTimeField.d.ts +7 -0
  5. package/dist/components/Dialog.d.ts +2 -1
  6. package/dist/components/FileUpload.d.ts +1 -1
  7. package/dist/components/Menu.d.ts +2 -1
  8. package/dist/components/Menubar.d.ts +2 -1
  9. package/dist/components/MultiSelect.d.ts +2 -1
  10. package/dist/components/SearchBar.d.ts +11 -1
  11. package/dist/components/Select.d.ts +2 -1
  12. package/dist/components/Sheet.d.ts +1 -0
  13. package/dist/components/ToggleButtonGroup.d.ts +30 -0
  14. package/dist/components/index.d.ts +2 -0
  15. package/dist/hooks/PortalContainerContext.d.ts +31 -0
  16. package/dist/hooks/index.d.ts +1 -0
  17. package/dist/hooks/useOutsideAlerter.d.ts +1 -1
  18. package/dist/index.css +57 -6
  19. package/dist/index.es.js +1604 -936
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +1604 -936
  22. package/dist/index.umd.js.map +1 -1
  23. package/dist/styles.d.ts +11 -11
  24. package/package.json +7 -7
  25. package/src/components/BooleanSwitch.tsx +3 -3
  26. package/src/components/Button.tsx +5 -5
  27. package/src/components/Card.tsx +7 -7
  28. package/src/components/Checkbox.tsx +1 -1
  29. package/src/components/ColorPicker.tsx +134 -0
  30. package/src/components/DateTimeField.tsx +123 -34
  31. package/src/components/DebouncedTextField.tsx +3 -3
  32. package/src/components/Dialog.tsx +25 -16
  33. package/src/components/DialogActions.tsx +1 -1
  34. package/src/components/ExpandablePanel.tsx +1 -1
  35. package/src/components/FileUpload.tsx +25 -24
  36. package/src/components/IconButton.tsx +3 -2
  37. package/src/components/Menu.tsx +44 -30
  38. package/src/components/Menubar.tsx +14 -3
  39. package/src/components/MultiSelect.tsx +91 -74
  40. package/src/components/Popover.tsx +11 -3
  41. package/src/components/SearchBar.tsx +37 -19
  42. package/src/components/Select.tsx +86 -73
  43. package/src/components/Separator.tsx +2 -2
  44. package/src/components/Sheet.tsx +12 -3
  45. package/src/components/Slider.tsx +4 -4
  46. package/src/components/Table.tsx +1 -1
  47. package/src/components/Tabs.tsx +14 -17
  48. package/src/components/TextField.tsx +19 -8
  49. package/src/components/ToggleButtonGroup.tsx +67 -0
  50. package/src/components/Tooltip.tsx +9 -2
  51. package/src/components/index.tsx +2 -0
  52. package/src/hooks/PortalContainerContext.tsx +48 -0
  53. package/src/hooks/index.ts +1 -0
  54. package/src/hooks/useInjectStyles.tsx +12 -3
  55. package/src/hooks/useOutsideAlerter.tsx +1 -1
  56. package/src/index.css +57 -6
  57. package/src/styles.ts +11 -11
  58. package/src/util/cls.ts +1 -1
  59. package/tailwind.config.js +2 -3
@@ -12,6 +12,16 @@ interface SearchBarProps {
12
12
  onTextSearch?: (searchString?: string) => void;
13
13
  placeholder?: string;
14
14
  expandable?: boolean;
15
+ /**
16
+ * Size of the search bar.
17
+ * - "small": 32px height (matches TextField small)
18
+ * - "medium": 44px height (matches TextField medium)
19
+ * @default "medium"
20
+ */
21
+ size?: "small" | "medium";
22
+ /**
23
+ * @deprecated Use size="medium" or size="small" instead. This prop will be removed in a future version.
24
+ */
15
25
  large?: boolean;
16
26
  innerClassName?: string;
17
27
  className?: string;
@@ -22,18 +32,19 @@ interface SearchBarProps {
22
32
  }
23
33
 
24
34
  export function SearchBar({
25
- onClick,
26
- onTextSearch,
27
- placeholder = "Search",
28
- expandable = false,
29
- large = false,
30
- innerClassName,
31
- className,
32
- autoFocus,
33
- disabled,
34
- loading,
35
- inputRef
36
- }: SearchBarProps) {
35
+ onClick,
36
+ onTextSearch,
37
+ placeholder = "Search",
38
+ expandable = false,
39
+ size = "medium",
40
+ large,
41
+ innerClassName,
42
+ className,
43
+ autoFocus,
44
+ disabled,
45
+ loading,
46
+ inputRef
47
+ }: SearchBarProps) {
37
48
 
38
49
  const [searchText, setSearchText] = useState<string>("");
39
50
  const [active, setActive] = useState<boolean>(false);
@@ -58,18 +69,23 @@ export function SearchBar({
58
69
  onTextSearch(undefined);
59
70
  }, [onTextSearch]);
60
71
 
72
+ // Height classes matching TextField sizes
73
+ const heightClass = size === "small" ? "h-[36px]" : "h-[44px]";
74
+ const iconPaddingClass = size === "small" ? "px-2" : "px-4";
75
+ const inputPaddingClass = size === "small" ? "pl-8" : "pl-12";
76
+
61
77
  return (
62
78
  <div
63
79
  onClick={onClick}
64
80
  className={cls("relative",
65
- large ? "h-14" : "h-[44px]",
81
+ heightClass,
66
82
  "bg-surface-accent-50 dark:bg-surface-800 border",
67
83
  defaultBorderMixin,
68
84
  "rounded-lg",
69
85
  className)}>
70
86
  <div
71
- className="absolute p-0 px-4 h-full pointer-events-none flex items-center justify-center top-0">
72
- {loading ? <CircularProgress size={"smallest"}/> : <SearchIcon className={"text-text-disabled dark:text-text-disabled-dark"}/>}
87
+ className={cls("absolute p-0 h-full pointer-events-none flex items-center justify-center top-0", iconPaddingClass)}>
88
+ {loading ? <CircularProgress size={"smallest"} /> : <SearchIcon className={"text-text-disabled dark:text-text-disabled-dark"} size={size === "small" ? "small" : "medium"} />}
73
89
  </div>
74
90
  <input
75
91
  value={searchText ?? ""}
@@ -89,18 +105,20 @@ export function SearchBar({
89
105
  (disabled || loading) && "pointer-events-none",
90
106
  "placeholder-text-disabled dark:placeholder-text-disabled-dark",
91
107
  "relative flex items-center rounded-lg transition-all bg-transparent outline-none appearance-none border-none",
92
- "pl-12 h-full text-current ",
108
+ inputPaddingClass, "h-full text-current",
109
+ size === "small" ? "text-sm" : "",
93
110
  expandable ? (active ? "w-[220px]" : "w-[180px]") : "",
94
111
  innerClassName
95
112
  )}
96
113
  />
97
114
  {searchText
98
115
  ? <IconButton
99
- className={`${large ? "mr-2 top-1" : "mr-1 top-0"} absolute right-0 z-10`}
116
+ className={`${size === "small" ? "mr-0 top-0" : "mr-1 top-0"} absolute right-0 z-10`}
117
+ size={"small"}
100
118
  onClick={clearText}>
101
- <CloseIcon size={"small"}/>
119
+ <CloseIcon size={"smallest"} />
102
120
  </IconButton>
103
- : <div style={{ width: 26 }}/>
121
+ : <div style={{ width: 26 }} />
104
122
  }
105
123
  </div>
106
124
  );
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import React, { ChangeEvent, Children, forwardRef, useCallback, useEffect, useState } from "react";
2
+ import React, { ChangeEvent, Children, forwardRef, useCallback, useEffect, useMemo, useState } from "react";
3
3
  import * as SelectPrimitive from "@radix-ui/react-select";
4
4
  import {
5
5
  defaultBorderMixin,
@@ -12,6 +12,7 @@ import {
12
12
  import { CheckIcon, KeyboardArrowDownIcon } from "../icons";
13
13
  import { cls } from "../util";
14
14
  import { SelectInputLabel } from "./common/SelectInputLabel";
15
+ import { usePortalContainer } from "../hooks/PortalContainerContext";
15
16
 
16
17
  export type SelectValue = string | number | boolean;
17
18
 
@@ -35,40 +36,42 @@ export type SelectProps<T extends SelectValue = string> = {
35
36
  error?: boolean,
36
37
  position?: "item-aligned" | "popper",
37
38
  endAdornment?: React.ReactNode,
38
- inputRef?: React.RefObject<HTMLButtonElement>,
39
+ inputRef?: React.RefObject<HTMLButtonElement | null>,
39
40
  padding?: boolean,
40
41
  invisible?: boolean,
41
42
  children?: React.ReactNode;
42
43
  dataType?: "string" | "number" | "boolean";
44
+ portalContainer?: HTMLElement | null; // Explicitly added to props type if missing
43
45
  };
44
46
 
45
47
  export const Select = forwardRef<HTMLDivElement, SelectProps>(({
46
- inputRef,
47
- open,
48
- name,
49
- fullWidth = false,
50
- id,
51
- onOpenChange,
52
- value,
53
- onChange,
54
- onValueChange,
55
- className,
56
- inputClassName,
57
- viewportClassName,
58
- placeholder,
59
- renderValue,
60
- label,
61
- size = "large",
62
- error,
63
- disabled,
64
- padding = true,
65
- position = "item-aligned",
66
- endAdornment,
67
- invisible,
68
- children,
69
- dataType = "string",
70
- ...props
71
- }, 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) => {
72
75
 
73
76
  const [openInternal, setOpenInternal] = useState(open ?? false);
74
77
 
@@ -76,8 +79,13 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
76
79
  setOpenInternal(open ?? false);
77
80
  }, [open]);
78
81
 
79
- const onValueChangeInternal = useCallback((newValue: string) => {
82
+ // Get the portal container from context
83
+ const contextContainer = usePortalContainer();
84
+
85
+ // Resolve final container (Manual Prop > Context Container > Undefined/Body)
86
+ const finalContainer = (manualContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
80
87
 
88
+ const onValueChangeInternal = useCallback((newValue: string) => {
81
89
  let typedValue: SelectValue = newValue;
82
90
  if (dataType === "boolean") {
83
91
  if (newValue === "true") typedValue = true;
@@ -96,23 +104,22 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
96
104
  } as unknown as ChangeEvent<HTMLSelectElement>;
97
105
  onChange(event);
98
106
  }
99
- }, [onChange, onValueChange, name]);
107
+ }, [onChange, onValueChange, name, dataType]);
100
108
 
101
109
  const hasValue = Array.isArray(value) ? value.length > 0 : (value != null && value !== "" && value !== undefined);
102
- // Convert non-string values to strings for Radix UI
103
110
  const stringValue = value !== undefined ? String(value) : undefined;
104
111
 
105
- const selectedChild = React.useMemo(() => {
112
+ const displayChildren = useMemo(() => {
106
113
  if (!hasValue || renderValue) return null;
107
- // @ts-ignore
108
- const childrenProps: SelectItemProps[] = Children.map(children, (child) => {
109
- if (React.isValidElement(child)) {
110
- return child.props;
111
- }
112
- }).filter(Boolean);
113
114
 
114
- const option = childrenProps.find((o) => String(o.value) === String(value));
115
- return option?.children;
115
+ // Find the child that matches the current value to display its content
116
+ let found: React.ReactNode = null;
117
+ Children.forEach(children, (child) => {
118
+ if (React.isValidElement<SelectItemProps>(child) && String(child.props.value) === String(value)) {
119
+ found = child.props.children;
120
+ }
121
+ });
122
+ return found;
116
123
  }, [children, hasValue, renderValue, value]);
117
124
 
118
125
  return (
@@ -155,6 +162,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
155
162
  "px-3": size === "medium",
156
163
  "px-2": size === "small" || size === "smallest",
157
164
  } : "",
165
+ "outline-hidden focus:outline-hidden",
158
166
  "outline-none focus:outline-none",
159
167
  "select-none rounded-md text-sm",
160
168
  error ? "text-red-500 dark:text-red-600" : "focus:text-text-primary dark:focus:text-text-primary-dark",
@@ -184,28 +192,30 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
184
192
  "min-h-[64px]": size === "large"
185
193
  }
186
194
  )}>
187
- <SelectPrimitive.Value
188
- onClick={(e) => {
189
- e.preventDefault();
190
- e.stopPropagation();
191
- }}
192
- placeholder={placeholder}
193
- className={"w-full"}>
194
- {hasValue && value !== undefined && renderValue ? renderValue(value) : placeholder}
195
- {/*{hasValue && !renderValue && value}*/}
196
- {hasValue && !renderValue && selectedChild}
195
+ <SelectPrimitive.Value
196
+ onClick={(e) => {
197
+ e.preventDefault();
198
+ e.stopPropagation();
199
+ }}
200
+ placeholder={placeholder}
201
+ className={"w-full"}>
197
202
 
198
- </SelectPrimitive.Value>
199
- </div>
203
+ {hasValue && value !== undefined && renderValue
204
+ ? renderValue(value)
205
+ : (displayChildren || placeholder)
206
+ }
207
+
208
+ </SelectPrimitive.Value>
209
+ </div>
200
210
 
201
- <SelectPrimitive.Icon asChild>
202
- <KeyboardArrowDownIcon size={size === "large" ? "medium" : "small"}
203
- className={cls("transition", open ? "rotate-180" : "", {
204
- "px-2": size === "large",
205
- "px-1": size === "medium" || size === "small",
206
- })}/>
207
- </SelectPrimitive.Icon>
208
- </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>
209
219
 
210
220
  {endAdornment && (
211
221
  <div
@@ -218,11 +228,13 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
218
228
  </div>
219
229
  )}
220
230
  </div>
221
- <SelectPrimitive.Portal>
231
+
232
+ {/* Pass the calculated finalContainer */}
233
+ <SelectPrimitive.Portal container={finalContainer}>
222
234
  <SelectPrimitive.Content position={position}
223
- 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)}>
224
236
  <SelectPrimitive.Viewport className={cls("p-1", viewportClassName)}
225
- style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
237
+ style={{ maxHeight: "var(--radix-select-content-available-height)" }}>
226
238
  {children}
227
239
  </SelectPrimitive.Viewport>
228
240
  </SelectPrimitive.Content>
@@ -233,6 +245,7 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
233
245
 
234
246
  Select.displayName = "Select";
235
247
 
248
+ // ... (SelectItem and SelectGroup remain unchanged)
236
249
  export type SelectItemProps<T extends SelectValue = string> = {
237
250
  value: T,
238
251
  children?: React.ReactNode,
@@ -241,11 +254,11 @@ export type SelectItemProps<T extends SelectValue = string> = {
241
254
  };
242
255
 
243
256
  export const SelectItem = React.memo(function SelectItem<T extends SelectValue = string>({
244
- value,
245
- children,
246
- disabled,
247
- className
248
- }: SelectItemProps<T>) {
257
+ value,
258
+ children,
259
+ disabled,
260
+ className
261
+ }: SelectItemProps<T>) {
249
262
  // Convert value to string for Radix UI
250
263
  const stringValue = String(value);
251
264
 
@@ -267,7 +280,7 @@ export const SelectItem = React.memo(function SelectItem<T extends SelectValue =
267
280
  <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
268
281
  <div
269
282
  className="absolute left-1 data-[state=checked]:block hidden">
270
- <CheckIcon size={16}/>
283
+ <CheckIcon size={16} />
271
284
  </div>
272
285
  </SelectPrimitive.Item>;
273
286
  });
@@ -279,10 +292,10 @@ export type SelectGroupProps = {
279
292
  };
280
293
 
281
294
  export const SelectGroup = React.memo(function SelectGroup({
282
- label,
283
- children,
284
- className
285
- }: SelectGroupProps) {
295
+ label,
296
+ children,
297
+ className
298
+ }: SelectGroupProps) {
286
299
  return <>
287
300
  <SelectPrimitive.Group
288
301
  className={cls(
@@ -15,12 +15,12 @@ export function Separator({
15
15
  <SeparatorPrimitive.Root
16
16
  decorative={decorative}
17
17
  orientation="horizontal"
18
- className={cls("dark:bg-opacity-80 dark:bg-surface-800 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px my-4", className)}/>
18
+ className={cls("dark:bg-opacity-80 dark:bg-surface-800 dark:bg-surface-800/80 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px my-4", className)}/>
19
19
  );
20
20
  else
21
21
  return (
22
22
  <SeparatorPrimitive.Root
23
- className={cls("dark:bg-opacity-80 dark:bg-surface-800 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px mx-4", className)}
23
+ className={cls("dark:bg-opacity-80 dark:bg-surface-800 dark:bg-surface-800/80 bg-surface-100 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px mx-4", className)}
24
24
  decorative={decorative}
25
25
  orientation="vertical"
26
26
  />
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
3
3
  import { cls } from "../util";
4
4
  import { defaultBorderMixin } from "../styles";
5
5
  import * as DialogPrimitive from "@radix-ui/react-dialog";
6
+ import { usePortalContainer } from "../hooks/PortalContainerContext";
6
7
 
7
8
  interface SheetProps {
8
9
  children: React.ReactNode;
@@ -18,6 +19,7 @@ interface SheetProps {
18
19
  style?: React.CSSProperties;
19
20
  overlayClassName?: string;
20
21
  overlayStyle?: React.CSSProperties;
22
+ portalContainer?: HTMLElement | null;
21
23
  }
22
24
 
23
25
  export const Sheet: React.FC<SheetProps> = ({
@@ -33,10 +35,17 @@ export const Sheet: React.FC<SheetProps> = ({
33
35
  style,
34
36
  overlayClassName,
35
37
  overlayStyle,
38
+ portalContainer,
36
39
  ...props
37
40
  }) => {
38
41
  const [displayed, setDisplayed] = useState(false);
39
42
 
43
+ // Get the portal container from context
44
+ const contextContainer = usePortalContainer();
45
+
46
+ // Prioritize manual prop, fallback to context container
47
+ const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
48
+
40
49
  useEffect(() => {
41
50
  const timeout = setTimeout(() => {
42
51
  setDisplayed(open);
@@ -62,7 +71,7 @@ export const Sheet: React.FC<SheetProps> = ({
62
71
  <DialogPrimitive.Root open={displayed || open}
63
72
  modal={modal}
64
73
  onOpenChange={onOpenChange}>
65
- <DialogPrimitive.Portal>
74
+ <DialogPrimitive.Portal container={finalContainer}>
66
75
  <DialogPrimitive.Title autoFocus tabIndex={0}>
67
76
  {title ?? "Sheet"}
68
77
  </DialogPrimitive.Title>
@@ -70,8 +79,8 @@ export const Sheet: React.FC<SheetProps> = ({
70
79
  className={cls(
71
80
  "outline-none",
72
81
  "fixed inset-0 transition-opacity z-20 ease-in-out duration-100 backdrop-blur-sm",
73
- "bg-black bg-opacity-50",
74
- "dark:bg-surface-900 dark:bg-opacity-60",
82
+ "bg-black bg-opacity-50 bg-black/50",
83
+ "dark:bg-surface-900 dark:bg-opacity-60 dark:bg-surface-900/60",
75
84
  displayed && open ? "opacity-100" : "opacity-0",
76
85
  overlayClassName
77
86
  )}
@@ -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,6 +1,7 @@
1
1
  import React from "react";
2
2
  import * as TabsPrimitive from "@radix-ui/react-tabs";
3
3
  import { cls } from "../util";
4
+ import { defaultBorderMixin } from "../styles";
4
5
 
5
6
  export type TabsProps = {
6
7
  value: string,
@@ -17,11 +18,12 @@ export function Tabs({
17
18
  innerClassName,
18
19
  children
19
20
  }: TabsProps) {
20
-
21
21
  return <TabsPrimitive.Root value={value} onValueChange={onValueChange} className={className}>
22
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",
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",
25
27
  innerClassName)
26
28
  }>
27
29
  {children}
@@ -47,19 +49,14 @@ export function Tab({
47
49
  return <TabsPrimitive.Trigger value={value}
48
50
  disabled={disabled}
49
51
  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",
61
- innerClassName)}>
62
- {children}
63
- </div>
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)}>
60
+ {children}
64
61
  </TabsPrimitive.Trigger>;
65
62
  }
@@ -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",
@@ -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
+ }