@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
package/dist/styles.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export declare const focusedDisabled = "focus-visible:ring-0 focus-visible:ring-offset-0";
2
- export declare const focusedInvisibleMixin = "focus:bg-opacity-70 focus:bg-surface-accent-100 focus:dark:bg-surface-800 focus:dark:bg-opacity-60";
3
- export declare const focusedClasses = "z-30 outline-none ring-2 ring-primary ring-opacity-75 ring-offset-2 ring-offset-transparent ";
4
- export declare const fieldBackgroundMixin = "bg-opacity-50 bg-surface-accent-200 dark:bg-surface-800 dark:bg-opacity-60";
5
- export declare const fieldBackgroundInvisibleMixin = "bg-opacity-0 bg-surface-accent-100 dark:bg-surface-800 dark:bg-opacity-0";
6
- export declare const fieldBackgroundDisabledMixin = "dark:bg-surface-800 bg-opacity-50 dark:bg-opacity-90";
7
- export declare const fieldBackgroundHoverMixin = "hover:bg-opacity-70 dark:hover:bg-surface-700 dark:hover:bg-opacity-40";
8
- export declare const defaultBorderMixin = "border-surface-200 border-opacity-40 dark:border-surface-700 dark:border-opacity-40";
9
- export declare const paperMixin = "bg-white rounded-md dark:bg-surface-950 border border-surface-200 border-opacity-40 dark:border-surface-700 dark:border-opacity-40";
10
- export declare const cardMixin = "bg-white border border-surface-200 border-opacity-40 dark:border-transparent rounded-md dark:bg-surface-950 dark:border-surface-700 dark:border-opacity-40";
11
- export declare const cardClickableMixin = "hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800 hover:ring-2 hover:ring-primary cursor-pointer";
12
- export declare const cardSelectedMixin = "bg-primary-bg dark:bg-primary-bg bg-opacity-30 dark:bg-opacity-10 ring-1 ring-primary ring-opacity-75";
2
+ export declare const focusedInvisibleMixin = "focus:bg-opacity-70 focus:bg-surface-accent-100 focus:dark:bg-surface-800 focus:dark:bg-opacity-60 focus:bg-surface-accent-100/70 dark:focus:bg-surface-800/60";
3
+ export declare const focusedClasses = "z-30 outline-hidden outline-none ring-2 ring-primary ring-opacity-75 ring-primary/75 ring-offset-2 ring-offset-transparent ";
4
+ export declare const fieldBackgroundMixin = "bg-opacity-50 bg-surface-accent-200 bg-surface-accent-200/50 dark:bg-surface-800 dark:bg-opacity-60 dark:bg-surface-800/60";
5
+ export declare const fieldBackgroundInvisibleMixin = "bg-opacity-0 bg-surface-accent-100 dark:bg-surface-800 dark:bg-opacity-0 bg-surface-accent-200/0 dark:bg-surface-800/0";
6
+ export declare const fieldBackgroundDisabledMixin = "dark:bg-surface-800 bg-opacity-50 dark:bg-opacity-90 bg-surface-accent-200/50 dark:bg-surface-800/90";
7
+ export declare const fieldBackgroundHoverMixin = "hover:bg-opacity-70 dark:hover:bg-surface-700 dark:hover:bg-opacity-40 hover:bg-surface-accent-200/70 hover:dark:bg-surface-700/40";
8
+ export declare const defaultBorderMixin = "border-surface-200 border-opacity-40 dark:border-surface-700 dark:border-opacity-40 border-surface-200/40 dark:border-surface-700/40 ";
9
+ export declare const paperMixin = "bg-white rounded-md dark:bg-surface-950 border border-surface-200 border-opacity-40 dark:border-surface-700 dark:border-opacity-40 border-surface-200/40 dark:border-surface-700/40";
10
+ export declare const cardMixin = "bg-white dark:bg-surface-950 rounded-md border border-surface-200/40 dark:border-surface-700/40 m-1 -p-1";
11
+ export declare const cardClickableMixin = "hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800 hover:ring-2 hover:ring-primary cursor-pointer hover:bg-primary/20 dark:hover:bg-primary/10 ";
12
+ export declare const cardSelectedMixin = "bg-primary-bg dark:bg-primary-bg bg-opacity-30 bg-primary-bg/30 dark:bg-opacity-10 dark:bg-primary-bg/10 ring-1 ring-primary ring-opacity-75 ring-primary/75 bg-primary/10 dark:bg-primary/10 ring-1 ring-primary/75";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/ui",
3
3
  "type": "module",
4
- "version": "3.0.1",
4
+ "version": "3.1.0-canary.02232f4",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -48,7 +48,7 @@
48
48
  "prepublishOnly": "run-s build",
49
49
  "createTag": "PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag v$PACKAGE_VERSION && git push --tags",
50
50
  "test:lint": "eslint \"src/**\" --quiet",
51
- "test": "jest",
51
+ "test": "jest --passWithNoTests",
52
52
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
53
53
  "generateIcons": "bun --esm src/scripts/generateIcons.ts"
54
54
  },
@@ -80,8 +80,8 @@
80
80
  "tailwind-merge": "^2.6.0"
81
81
  },
82
82
  "peerDependencies": {
83
- "react": ">=18.0.0",
84
- "react-dom": ">=18.0.0"
83
+ "react": ">=18.3.1 || >=19.0.0",
84
+ "react-dom": ">=18.3.1 || >=19.0.0"
85
85
  },
86
86
  "devDependencies": {
87
87
  "@jest/globals": "^30.2.0",
@@ -91,8 +91,8 @@
91
91
  "@types/jest": "^29.5.14",
92
92
  "@types/node": "^20.19.17",
93
93
  "@types/object-hash": "^3.0.6",
94
- "@types/react": "^18.3.24",
95
- "@types/react-dom": "^18.3.7",
94
+ "@types/react": "^19.2.3",
95
+ "@types/react-dom": "^19.2.3",
96
96
  "@types/react-measure": "^2.0.12",
97
97
  "@vitejs/plugin-react": "^4.7.0",
98
98
  "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
@@ -114,7 +114,7 @@
114
114
  "index.css",
115
115
  "tailwind.config.js"
116
116
  ],
117
- "gitHead": "72d951d01d15ef5a7efcde6c63839f65964d2f7a",
117
+ "gitHead": "6281205d9f39f85991e1d8533474bfb9a542ed03",
118
118
  "publishConfig": {
119
119
  "access": "public"
120
120
  }
@@ -42,10 +42,10 @@ export const BooleanSwitch = React.forwardRef(function BooleanSwitch({
42
42
  }}
43
43
  className={cls(
44
44
  size === "smallest" ? "w-[34px] h-[18px] min-w-[34px] min-h-[18px]" : size === "small" ? "w-[38px] h-[22px] min-w-[38px] min-h-[22px]" : "w-[44px] h-[26px] min-w-[44px] min-h-[26px]",
45
- "outline-none rounded-full relative shadow-sm",
45
+ "outline-none outline-hidden rounded-full relative shadow-sm",
46
46
  value ? (disabled
47
- ? "bg-white bg-opacity-54 dark:bg-surface-accent-950 border-surface-accent-100 dark:border-surface-accent-700 ring-1 ring-surface-accent-200 dark:ring-surface-accent-700"
48
- : "ring-secondary ring-1 bg-secondary dark:bg-secondary") : "bg-white bg-opacity-54 dark:bg-surface-accent-900 ring-1 ring-surface-accent-200 dark:ring-surface-accent-700",
47
+ ? "bg-white bg-opacity-54 bg-white/54 dark:bg-surface-accent-950 border-surface-accent-100 dark:border-surface-accent-700 ring-1 ring-surface-accent-200 dark:ring-surface-accent-700"
48
+ : "ring-secondary ring-1 bg-secondary dark:bg-secondary") : "bg-white bg-opacity-54 bg-white/54 dark:bg-surface-accent-900 ring-1 ring-surface-accent-200 dark:ring-surface-accent-700",
49
49
  className
50
50
  )}
51
51
  {...props}
@@ -18,6 +18,7 @@ export type BooleanSwitchWithLabelProps = BooleanSwitchProps & {
18
18
  fullWidth?: boolean,
19
19
  className?: string,
20
20
  inputClassName?: string,
21
+ switchAdornment?: React.ReactNode,
21
22
  };
22
23
 
23
24
  /**
@@ -37,6 +38,7 @@ export const BooleanSwitchWithLabel = function BooleanSwitchWithLabel({
37
38
  className,
38
39
  fullWidth = true,
39
40
  inputClassName,
41
+ switchAdornment,
40
42
  ...props
41
43
  }: BooleanSwitchWithLabelProps) {
42
44
 
@@ -100,6 +102,8 @@ export const BooleanSwitchWithLabel = function BooleanSwitchWithLabel({
100
102
  {...props}
101
103
  />
102
104
 
105
+ {switchAdornment}
106
+
103
107
  <div className={cls(
104
108
  "flex-grow",
105
109
  position === "end" ? "mr-4" : "ml-4",
@@ -28,6 +28,7 @@ const ButtonInner = React.memo(React.forwardRef<
28
28
  fullWidth = false,
29
29
  component: Component,
30
30
  color = "neutral",
31
+ loading,
31
32
  ...props
32
33
  }: ButtonProps<any>, ref) => {
33
34
 
@@ -46,14 +47,14 @@ const ButtonInner = React.memo(React.forwardRef<
46
47
  "border border-transparent bg-surface-100 hover:bg-surface-accent-200 text-text-primary dark:bg-surface-800 dark:hover:bg-surface-accent-700 dark:text-text-primary-dark hover:text-text-primary dark:text-text-primary-dark hover:dark:text-text-primary-dark": variant === "filled" && color === "neutral" && !disabled,
47
48
 
48
49
  // Text Variants
49
- "border border-transparent text-primary hover:text-primary hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-800": variant === "text" && color === "primary" && !disabled,
50
- "border border-transparent text-secondary hover:text-secondary hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-800": variant === "text" && color === "secondary" && !disabled,
51
- "border border-transparent text-red-500 hover:text-red-500 hover:bg-red-500 hover:bg-opacity-10": variant === "text" && color === "error" && !disabled,
50
+ "border border-transparent text-primary hover:text-primary hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800": variant === "text" && color === "primary" && !disabled,
51
+ "border border-transparent text-secondary hover:text-secondary hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-800": variant === "text" && color === "secondary" && !disabled,
52
+ "border border-transparent text-red-500 hover:text-red-500 hover:bg-red-500 hover:bg-opacity-10 hover:bg-red-500/10": variant === "text" && color === "error" && !disabled,
52
53
  "border border-transparent text-text-primary hover:text-text-primary dark:text-text-primary-dark hover:dark:text-text-primary-dark hover:bg-surface-accent-200 hover:dark:bg-surface-700": variant === "text" && color === "text" && !disabled,
53
54
  "border border-transparent text-text-primary hover:text-text-primary hover:bg-surface-accent-200 dark:text-text-primary-dark dark:hover:text-text-primary-dark dark:hover:bg-surface-accent-700": variant === "text" && color === "neutral" && !disabled,
54
55
 
55
56
  // Outlined Variants
56
- "border border-primary text-primary hover:text-primary hover:bg-primary-bg": variant === "outlined" && color === "primary" && !disabled,
57
+ "border border-primary text-primary hover:text-primary hover:bg-primary-bg hover:bg-primary/10": variant === "outlined" && color === "primary" && !disabled,
57
58
  "border border-secondary text-secondary hover:text-secondary hover:bg-secondary-bg": variant === "outlined" && color === "secondary" && !disabled,
58
59
  "border border-red-500 text-red-500 hover:text-red-500 hover:bg-red-500 hover:text-white": variant === "outlined" && color === "error" && !disabled,
59
60
  "border border-surface-accent-400 text-text-primary hover:text-text-primary dark:text-text-primary-dark hover:bg-surface-accent-200": variant === "outlined" && color === "text" && !disabled,
@@ -63,7 +64,7 @@ const ButtonInner = React.memo(React.forwardRef<
63
64
  "text-text-disabled dark:text-text-disabled-dark": disabled,
64
65
  "border border-transparent opacity-50": variant === "text" && disabled,
65
66
  "border border-surface-500 opacity-50": variant === "outlined" && disabled,
66
- "border border-transparent bg-surface-300 dark:bg-surface-500 opacity-40": variant === "filled" && disabled,
67
+ "border border-transparent bg-surface-300 dark:bg-surface-500 opacity-70 bg-surface-300/70 dark:bg-surface-500/70": variant === "filled" && disabled,
67
68
  });
68
69
 
69
70
  const sizeClasses = cls(
@@ -6,17 +6,17 @@ import { cls } from "../util";
6
6
  type CardProps = {
7
7
  children: React.ReactNode;
8
8
  style?: React.CSSProperties;
9
- onClick?: () => void;
9
+ onClick?: (e?: React.MouseEvent) => void;
10
10
  className?: string;
11
11
  };
12
12
 
13
13
  const Card = React.forwardRef<HTMLDivElement, CardProps>(({
14
- children,
15
- className,
16
- onClick,
17
- style,
18
- ...props
19
- }, ref) => {
14
+ children,
15
+ className,
16
+ onClick,
17
+ style,
18
+ ...props
19
+ }, ref) => {
20
20
  const onKeyPress = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
21
21
  if (e.key === "Enter" || e.key === " ") {
22
22
  onClick?.();
@@ -71,7 +71,7 @@ export const Checkbox = React.memo(({
71
71
  padding ? paddingClasses[size] : "",
72
72
  outerSizeClasses[size],
73
73
  "inline-flex items-center justify-center text-sm font-medium focus:outline-none transition-colors ease-in-out duration-150",
74
- onCheckedChange ? "rounded-full hover:bg-surface-accent-200 hover:bg-opacity-75 dark:hover:bg-surface-accent-700 dark:hover:bg-opacity-75" : "",
74
+ onCheckedChange ? "rounded-full hover:bg-surface-accent-200 hover:bg-opacity-75 hover:bg-surface-accent-200/75 dark:hover:bg-surface-accent-700 dark:hover:bg-opacity-75 dark:hover:bg-surface-accent-700/75" : "",
75
75
  onCheckedChange ? "cursor-pointer" : "cursor-default"
76
76
  )}>
77
77
  <div
@@ -29,7 +29,7 @@ const sizeClassNames = {
29
29
  /**
30
30
  * @group Preview components
31
31
  */
32
- export function Chip({
32
+ export const Chip = React.forwardRef<HTMLDivElement, ChipProps>(function Chip({
33
33
  children,
34
34
  colorScheme,
35
35
  error,
@@ -39,11 +39,12 @@ export function Chip({
39
39
  size = "large",
40
40
  className,
41
41
  style
42
- }: ChipProps) {
42
+ }: ChipProps, ref) {
43
43
 
44
44
  const usedColorScheme = typeof colorScheme === "string" ? getColorSchemeForKey(colorScheme) : colorScheme;
45
45
  return (
46
46
  <div
47
+ ref={ref}
47
48
  className={cls("rounded-lg max-w-full w-max h-fit font-regular inline-flex gap-1",
48
49
  "text-ellipsis",
49
50
  "items-center",
@@ -67,4 +68,4 @@ export function Chip({
67
68
  {icon}
68
69
  </div>
69
70
  );
70
- }
71
+ });
@@ -0,0 +1,134 @@
1
+ import React from "react";
2
+ import { CHIP_COLORS, cls } from "../util";
3
+ import { ChipColorKey, ChipColorScheme } from "./Chip";
4
+ import { CheckIcon } from "../icons";
5
+ import { Tooltip } from "./Tooltip";
6
+
7
+ export interface ColorPickerProps {
8
+ /**
9
+ * Currently selected color key
10
+ */
11
+ value?: ChipColorKey;
12
+ /**
13
+ * Callback when color selection changes. Passes undefined when "Auto" is selected.
14
+ */
15
+ onChange: (colorKey: ChipColorKey | undefined) => void;
16
+ /**
17
+ * Size of the color swatches
18
+ */
19
+ size?: "small" | "medium";
20
+ /**
21
+ * Whether to show the "Auto" option that clears the selection
22
+ */
23
+ allowClear?: boolean;
24
+ /**
25
+ * Whether the picker is disabled
26
+ */
27
+ disabled?: boolean;
28
+ }
29
+
30
+ // Base colors in display order
31
+ const BASE_COLORS = ["blue", "cyan", "teal", "green", "yellow", "orange", "red", "pink", "purple", "gray"] as const;
32
+
33
+ // Variants in display order (darker to lighter for better visual flow)
34
+ const VARIANTS = ["Darker", "Dark", "Light", "Lighter"] as const;
35
+
36
+ // Helper to get readable name from color key
37
+ function getColorDisplayName(colorKey: ChipColorKey): string {
38
+ // Convert camelCase to readable format: "blueLighter" -> "Blue Lighter"
39
+ const base = colorKey.replace(/(Lighter|Light|Dark|Darker)$/, "");
40
+ const variant = colorKey.replace(base, "");
41
+ return `${base.charAt(0).toUpperCase()}${base.slice(1)} ${variant}`;
42
+ }
43
+
44
+ /**
45
+ * A color picker component that displays a grid of predefined CHIP_COLORS.
46
+ * Used for selecting colors for enum values, tags, and other chip-based UI elements.
47
+ *
48
+ * @group Form components
49
+ */
50
+ export function ColorPicker({
51
+ value,
52
+ onChange,
53
+ size = "medium",
54
+ allowClear = true,
55
+ disabled = false
56
+ }: ColorPickerProps) {
57
+
58
+ const swatchSize = size === "small" ? "w-5 h-5" : "w-6 h-6";
59
+ const checkSize = size === "small" ? 12 : 14;
60
+
61
+ return (
62
+ <div className="flex flex-col gap-2">
63
+ {allowClear && (
64
+ <button
65
+ type="button"
66
+ disabled={disabled}
67
+ onClick={() => onChange(undefined)}
68
+ className={cls(
69
+ "flex items-center gap-2 px-2 py-1 rounded text-sm transition-colors",
70
+ "hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800",
71
+ disabled && "opacity-50 cursor-not-allowed",
72
+ !value && "bg-surface-accent-100 dark:bg-surface-accent-800 font-medium"
73
+ )}
74
+ >
75
+ <div className={cls(
76
+ swatchSize,
77
+ "rounded-full border-2 border-dashed border-surface-accent-400 dark:border-surface-accent-600",
78
+ "flex items-center justify-center"
79
+ )}>
80
+ {!value && <CheckIcon size={checkSize} />}
81
+ </div>
82
+ <span className="text-surface-accent-700 dark:text-surface-accent-300">
83
+ Auto (based on ID)
84
+ </span>
85
+ </button>
86
+ )}
87
+
88
+ <div className="grid grid-cols-10 gap-1">
89
+ {VARIANTS.map((variant) => (
90
+ BASE_COLORS.map((base) => {
91
+ const colorKey = `${base}${variant}` as ChipColorKey;
92
+ const colorScheme = CHIP_COLORS[colorKey] as ChipColorScheme;
93
+ const isSelected = value === colorKey;
94
+ const displayName = getColorDisplayName(colorKey);
95
+
96
+ return (
97
+ <Tooltip
98
+ key={colorKey}
99
+ title={displayName}
100
+ delayDuration={300}
101
+ >
102
+ <button
103
+ type="button"
104
+ disabled={disabled}
105
+ onClick={() => onChange(colorKey)}
106
+ className={cls(
107
+ swatchSize,
108
+ "rounded-full transition-all flex items-center justify-center",
109
+ "hover:scale-110 hover:shadow-md",
110
+ "focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-1",
111
+ disabled && "opacity-50 cursor-not-allowed hover:scale-100",
112
+ isSelected && "ring-2 ring-primary ring-offset-1"
113
+ )}
114
+ style={{
115
+ backgroundColor: colorScheme.color,
116
+ }}
117
+ aria-label={displayName}
118
+ aria-pressed={isSelected}
119
+ >
120
+ {isSelected && (
121
+ <CheckIcon
122
+ size={checkSize}
123
+ style={{ color: colorScheme.text }}
124
+ />
125
+ )}
126
+ </button>
127
+ </Tooltip>
128
+ );
129
+ })
130
+ ))}
131
+ </div>
132
+ </div>
133
+ );
134
+ }
@@ -23,27 +23,35 @@ export type DateTimeFieldProps = {
23
23
  inputClassName?: string;
24
24
  invisible?: boolean;
25
25
  locale?: string;
26
+ /**
27
+ * IANA timezone string (e.g., "America/New_York", "Europe/London").
28
+ * Used to display and input dates in the specified timezone.
29
+ * The value passed to onChange will always be in UTC.
30
+ * If not provided, uses the user's local timezone.
31
+ */
32
+ timezone?: string;
26
33
  };
27
34
 
28
35
  export const DateTimeField: React.FC<DateTimeFieldProps> = ({
29
- value,
30
- label,
31
- onChange,
32
- disabled,
33
- clearable,
34
- mode = "date",
35
- error,
36
- size = "large",
37
- className,
38
- style,
39
- inputClassName,
40
- invisible,
41
- }) => {
36
+ value,
37
+ label,
38
+ onChange,
39
+ disabled,
40
+ clearable,
41
+ mode = "date",
42
+ error,
43
+ size = "large",
44
+ className,
45
+ style,
46
+ inputClassName,
47
+ invisible,
48
+ timezone,
49
+ }) => {
42
50
  const inputRef = useRef<HTMLInputElement>(null);
43
51
  const [focused, setFocused] = useState(false);
44
52
  const [internalValue, setInternalValue] = useState<string>("");
45
53
  const [isTyping, setIsTyping] = useState(false);
46
- const invalidValue = value !== undefined && value !== null && !(value instanceof Date);
54
+ const invalidValue = value !== undefined && value !== null && (!(value instanceof Date) || isNaN((value as Date).getTime()));
47
55
 
48
56
  useInjectStyles("DateTimeField", inputStyles);
49
57
 
@@ -54,7 +62,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
54
62
  onChange?.(null);
55
63
  };
56
64
 
57
- // Convert Date object to input value string
65
+ // Convert UTC Date to display string in the specified timezone
58
66
  const valueAsInputValue = (
59
67
  dateValue: Date | null,
60
68
  mode: "date" | "date_time"
@@ -63,20 +71,85 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
63
71
  return "";
64
72
  }
65
73
  const pad = (n: number) => n.toString().padStart(2, "0");
66
- const year = dateValue.getFullYear();
67
- const month = pad(dateValue.getMonth() + 1);
68
- const day = pad(dateValue.getDate());
74
+
75
+ // Use Intl.DateTimeFormat to get date parts in the target timezone
76
+ const options: Intl.DateTimeFormatOptions = {
77
+ year: "numeric",
78
+ month: "2-digit",
79
+ day: "2-digit",
80
+ hour: "2-digit",
81
+ minute: "2-digit",
82
+ hour12: false,
83
+ timeZone: timezone, // undefined = local timezone
84
+ };
85
+
86
+ const formatter = new Intl.DateTimeFormat("en-CA", options);
87
+ let parts: Intl.DateTimeFormatPart[];
88
+ try {
89
+ parts = formatter.formatToParts(dateValue);
90
+ } catch {
91
+ return "";
92
+ }
93
+
94
+ const getPart = (type: string) => parts.find(p => p.type === type)?.value ?? "";
95
+
96
+ const year = getPart("year");
97
+ const month = getPart("month");
98
+ const day = getPart("day");
69
99
 
70
100
  if (mode === "date") {
71
101
  return `${year}-${month}-${day}`;
72
102
  } else {
73
- const hours = pad(dateValue.getHours());
74
- const minutes = pad(dateValue.getMinutes());
103
+ const hours = getPart("hour");
104
+ const minutes = getPart("minute");
75
105
  return `${year}-${month}-${day}T${hours}:${minutes}`;
76
106
  }
77
107
  };
78
108
 
79
- // Handle input value change
109
+ // Get the UTC offset for a specific date in the target timezone (in minutes)
110
+ const getTimezoneOffsetMinutes = (date: Date, tz?: string): number => {
111
+ if (!tz) {
112
+ // Local timezone: use built-in getTimezoneOffset (returns offset in minutes, inverted sign)
113
+ return -date.getTimezoneOffset();
114
+ }
115
+ // For named timezones, calculate the offset by comparing formatted times
116
+ const utcFormatter = new Intl.DateTimeFormat("en-CA", {
117
+ year: "numeric", month: "2-digit", day: "2-digit",
118
+ hour: "2-digit", minute: "2-digit", hour12: false,
119
+ timeZone: "UTC"
120
+ });
121
+ const tzFormatter = new Intl.DateTimeFormat("en-CA", {
122
+ year: "numeric", month: "2-digit", day: "2-digit",
123
+ hour: "2-digit", minute: "2-digit", hour12: false,
124
+ timeZone: tz
125
+ });
126
+
127
+ const utcParts = utcFormatter.formatToParts(date);
128
+ const tzParts = tzFormatter.formatToParts(date);
129
+
130
+ const getPart = (parts: Intl.DateTimeFormatPart[], type: string) =>
131
+ parseInt(parts.find(p => p.type === type)?.value ?? "0", 10);
132
+
133
+ const utcDate = new Date(Date.UTC(
134
+ getPart(utcParts, "year"),
135
+ getPart(utcParts, "month") - 1,
136
+ getPart(utcParts, "day"),
137
+ getPart(utcParts, "hour"),
138
+ getPart(utcParts, "minute")
139
+ ));
140
+
141
+ const tzDate = new Date(Date.UTC(
142
+ getPart(tzParts, "year"),
143
+ getPart(tzParts, "month") - 1,
144
+ getPart(tzParts, "day"),
145
+ getPart(tzParts, "hour"),
146
+ getPart(tzParts, "minute")
147
+ ));
148
+
149
+ return (tzDate.getTime() - utcDate.getTime()) / 60000;
150
+ };
151
+
152
+ // Handle input value change - convert from display timezone to UTC
80
153
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
81
154
  const inputValue = e.target.value;
82
155
  setInternalValue(inputValue);
@@ -88,21 +161,42 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
88
161
  }
89
162
 
90
163
  try {
91
- const parsed = new Date(inputValue);
92
- if (isNaN(parsed.getTime())) {
93
- throw new Error("Invalid date");
94
- }
164
+ let year: number, month: number, day: number, hours = 0, minutes = 0;
95
165
 
96
- let newDate: Date;
97
166
  if (mode === "date") {
98
- // Adjust for timezone offset for date-only inputs
99
- const userTimezoneOffset = parsed.getTimezoneOffset() * 60000;
100
- newDate = new Date(parsed.getTime() + userTimezoneOffset);
167
+ // Parse date-only input: "YYYY-MM-DD"
168
+ [year, month, day] = inputValue.split("-").map(Number);
101
169
  } else {
102
- newDate = parsed;
170
+ // Parse datetime-local input: "YYYY-MM-DDTHH:MM"
171
+ const [datePart, timePart] = inputValue.split("T");
172
+ [year, month, day] = datePart.split("-").map(Number);
173
+ [hours, minutes] = timePart.split(":").map(Number);
174
+ }
175
+
176
+ let resultDate: Date;
177
+
178
+ if (!timezone) {
179
+ // No timezone specified: interpret input as local time (backward compatible)
180
+ resultDate = new Date(year, month - 1, day, hours, minutes);
181
+ } else {
182
+ // Timezone specified: interpret input as that timezone and convert to UTC
183
+ // We need to find the UTC equivalent of the entered time in the target timezone
184
+
185
+ // Create a reference UTC date to calculate the offset for this moment
186
+ const refUtcDate = new Date(Date.UTC(year, month - 1, day, hours, minutes));
187
+ const offsetMinutes = getTimezoneOffsetMinutes(refUtcDate, timezone);
188
+
189
+ // Convert from target timezone to UTC:
190
+ // If user entered 00:00 in Mexico (UTC-6, offset=-360), we subtract the offset
191
+ // Date.UTC gives us 00:00 UTC, subtracting -360 minutes (= adding 360 min) gives 06:00 UTC
192
+ resultDate = new Date(Date.UTC(year, month - 1, day, hours, minutes) - offsetMinutes * 60000);
193
+ }
194
+
195
+ if (isNaN(resultDate.getTime())) {
196
+ throw new Error("Invalid date");
103
197
  }
104
198
 
105
- onChange?.(newDate);
199
+ onChange?.(resultDate);
106
200
  } catch (e) {
107
201
  // Don't call onChange with null while typing
108
202
  return;
@@ -156,7 +250,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
156
250
  disabled ? "opacity-50" : ""
157
251
  )}
158
252
  shrink={true}
159
- // shrink={hasValue || focused}
253
+ // shrink={hasValue || focused}
160
254
  >
161
255
  {label}
162
256
  </InputLabel>
@@ -193,20 +287,20 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
193
287
  }}
194
288
  className="absolute right-3 top-1/2 transform -translate-y-1/2 !text-surface-accent-500"
195
289
  >
196
- <CalendarMonthIcon color={"disabled"}/>
290
+ <CalendarMonthIcon color={"disabled"} />
197
291
  </IconButton>
198
292
  {clearable && value && (
199
293
  <IconButton
200
294
  onClick={handleClear}
201
295
  className="absolute right-14 top-1/2 transform -translate-y-1/2 text-surface-accent-400 "
202
296
  >
203
- <CloseIcon/>
297
+ <CloseIcon />
204
298
  </IconButton>
205
299
  )}
206
300
  </div>
207
301
  {invalidValue && (
208
302
  <div className="flex items-center m-2">
209
- <ErrorIcon size={"small"} color={"error"}/>
303
+ <ErrorIcon size={"small"} color={"error"} />
210
304
  <div className="pl-2">
211
305
  <Typography variant={"body2"}>
212
306
  Invalid date value for this field
@@ -4,7 +4,7 @@ import { TextField, TextFieldProps } from "./index";
4
4
 
5
5
  export function DebouncedTextField<T extends string | number>(props: TextFieldProps<T>) {
6
6
 
7
- const previousEventRef = React.useRef<ChangeEvent<any>>();
7
+ const previousEventRef = React.useRef<ChangeEvent<any>>(undefined);
8
8
  const [internalValue, setInternalValue] = React.useState(props.value);
9
9
 
10
10
  const deferredValue = useDeferredValue(internalValue);
@@ -28,6 +28,6 @@ export function DebouncedTextField<T extends string | number>(props: TextFieldPr
28
28
  }, []);
29
29
 
30
30
  return <TextField {...props}
31
- onChange={internalOnChange}
32
- value={internalValue}/>
31
+ onChange={internalOnChange}
32
+ value={internalValue} />
33
33
  }