@firecms/ui 3.0.1 → 3.1.0-canary.02232f4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/components/BooleanSwitchWithLabel.d.ts +2 -1
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Chip.d.ts +1 -1
- package/dist/components/ColorPicker.d.ts +30 -0
- package/dist/components/DateTimeField.d.ts +7 -0
- package/dist/components/Dialog.d.ts +2 -1
- package/dist/components/FileUpload.d.ts +1 -1
- package/dist/components/Menu.d.ts +2 -1
- package/dist/components/Menubar.d.ts +2 -1
- package/dist/components/MultiSelect.d.ts +2 -1
- package/dist/components/ResizablePanels.d.ts +16 -0
- package/dist/components/SearchBar.d.ts +11 -1
- package/dist/components/SearchableSelect.d.ts +48 -0
- package/dist/components/Select.d.ts +2 -1
- package/dist/components/Sheet.d.ts +1 -0
- package/dist/components/Tabs.d.ts +8 -1
- package/dist/components/ToggleButtonGroup.d.ts +30 -0
- package/dist/components/Tooltip.d.ts +18 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/hooks/PortalContainerContext.d.ts +31 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useOutsideAlerter.d.ts +1 -1
- package/dist/icons/FirestoreIcon.d.ts +6 -0
- package/dist/icons/components/DatabaseIcon.d.ts +6 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/index.css +57 -6
- package/dist/index.es.js +2846 -1165
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2846 -1165
- package/dist/index.umd.js.map +1 -1
- package/dist/styles.d.ts +11 -11
- package/package.json +7 -7
- package/src/components/BooleanSwitch.tsx +3 -3
- package/src/components/BooleanSwitchWithLabel.tsx +4 -0
- package/src/components/Button.tsx +6 -5
- package/src/components/Card.tsx +7 -7
- package/src/components/Checkbox.tsx +1 -1
- package/src/components/Chip.tsx +4 -3
- package/src/components/ColorPicker.tsx +134 -0
- package/src/components/DateTimeField.tsx +129 -35
- package/src/components/DebouncedTextField.tsx +3 -3
- package/src/components/Dialog.tsx +25 -16
- package/src/components/DialogActions.tsx +1 -1
- package/src/components/ExpandablePanel.tsx +1 -1
- package/src/components/FileUpload.tsx +25 -24
- package/src/components/IconButton.tsx +3 -2
- package/src/components/Menu.tsx +44 -30
- package/src/components/Menubar.tsx +14 -3
- package/src/components/MultiSelect.tsx +113 -77
- package/src/components/Popover.tsx +11 -3
- package/src/components/ResizablePanels.tsx +181 -0
- package/src/components/SearchBar.tsx +37 -19
- package/src/components/SearchableSelect.tsx +335 -0
- package/src/components/Select.tsx +86 -73
- package/src/components/Separator.tsx +2 -2
- package/src/components/Sheet.tsx +12 -3
- package/src/components/Skeleton.tsx +4 -2
- package/src/components/Slider.tsx +4 -4
- package/src/components/Table.tsx +1 -1
- package/src/components/Tabs.tsx +150 -37
- package/src/components/TextField.tsx +19 -8
- package/src/components/TextareaAutosize.tsx +77 -212
- package/src/components/ToggleButtonGroup.tsx +67 -0
- package/src/components/Tooltip.tsx +16 -8
- package/src/components/index.tsx +4 -0
- package/src/hooks/PortalContainerContext.tsx +48 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useInjectStyles.tsx +12 -3
- package/src/hooks/useOutsideAlerter.tsx +1 -1
- package/src/icons/FirestoreIcon.tsx +47 -0
- package/src/icons/components/DatabaseIcon.tsx +10 -0
- package/src/icons/index.ts +2 -0
- package/src/index.css +57 -6
- package/src/styles.ts +11 -11
- package/src/util/cls.ts +1 -1
- package/tailwind.config.js +2 -3
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
|
|
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.
|
|
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": "^
|
|
95
|
-
"@types/react-dom": "^
|
|
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": "
|
|
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-
|
|
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(
|
package/src/components/Card.tsx
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
package/src/components/Chip.tsx
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
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 =
|
|
74
|
-
const minutes =
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
99
|
-
|
|
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
|
-
|
|
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?.(
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
31
|
+
onChange={internalOnChange}
|
|
32
|
+
value={internalValue} />
|
|
33
33
|
}
|