@firecms/ui 3.0.1 → 3.1.0-canary.9e89e98
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/Card.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 +1 -0
- package/dist/components/SearchBar.d.ts +11 -1
- package/dist/components/Select.d.ts +1 -0
- package/dist/components/Sheet.d.ts +1 -0
- package/dist/components/ToggleButtonGroup.d.ts +30 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/hooks/PortalContainerContext.d.ts +31 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/index.css +57 -6
- package/dist/index.es.js +1603 -935
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1603 -935
- package/dist/index.umd.js.map +1 -1
- package/dist/styles.d.ts +11 -11
- package/package.json +3 -3
- package/src/components/BooleanSwitch.tsx +3 -3
- package/src/components/Button.tsx +5 -5
- package/src/components/Card.tsx +7 -7
- package/src/components/Checkbox.tsx +1 -1
- package/src/components/ColorPicker.tsx +134 -0
- package/src/components/DateTimeField.tsx +123 -34
- 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 +87 -68
- package/src/components/Popover.tsx +11 -3
- package/src/components/SearchBar.tsx +37 -19
- package/src/components/Select.tsx +30 -17
- package/src/components/Separator.tsx +2 -2
- package/src/components/Sheet.tsx +12 -3
- package/src/components/Slider.tsx +4 -4
- package/src/components/Table.tsx +1 -1
- package/src/components/Tabs.tsx +14 -17
- package/src/components/TextField.tsx +19 -8
- package/src/components/ToggleButtonGroup.tsx +67 -0
- package/src/components/Tooltip.tsx +9 -2
- package/src/components/index.tsx +2 -0
- package/src/hooks/PortalContainerContext.tsx +48 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useInjectStyles.tsx +12 -3
- 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.9e89e98",
|
|
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
|
},
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"index.css",
|
|
115
115
|
"tailwind.config.js"
|
|
116
116
|
],
|
|
117
|
-
"gitHead": "
|
|
117
|
+
"gitHead": "7631fbfb6cde4415513e45c0557e18073ab359b8",
|
|
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}
|
|
@@ -46,14 +46,14 @@ const ButtonInner = React.memo(React.forwardRef<
|
|
|
46
46
|
"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
47
|
|
|
48
48
|
// 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,
|
|
49
|
+
"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,
|
|
50
|
+
"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,
|
|
51
|
+
"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
52
|
"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
53
|
"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
54
|
|
|
55
55
|
// Outlined Variants
|
|
56
|
-
"border border-primary text-primary hover:text-primary hover:bg-primary-bg": variant === "outlined" && color === "primary" && !disabled,
|
|
56
|
+
"border border-primary text-primary hover:text-primary hover:bg-primary-bg hover:bg-primary/10": variant === "outlined" && color === "primary" && !disabled,
|
|
57
57
|
"border border-secondary text-secondary hover:text-secondary hover:bg-secondary-bg": variant === "outlined" && color === "secondary" && !disabled,
|
|
58
58
|
"border border-red-500 text-red-500 hover:text-red-500 hover:bg-red-500 hover:text-white": variant === "outlined" && color === "error" && !disabled,
|
|
59
59
|
"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 +63,7 @@ const ButtonInner = React.memo(React.forwardRef<
|
|
|
63
63
|
"text-text-disabled dark:text-text-disabled-dark": disabled,
|
|
64
64
|
"border border-transparent opacity-50": variant === "text" && disabled,
|
|
65
65
|
"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,
|
|
66
|
+
"border border-transparent bg-surface-300 dark:bg-surface-500 opacity-40 bg-surface-300/40 dark:bg-surface-500/40": variant === "filled" && disabled,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
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
|
|
@@ -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,22 +23,30 @@ 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>("");
|
|
@@ -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,80 @@ 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
|
+
const parts = formatter.formatToParts(dateValue);
|
|
88
|
+
|
|
89
|
+
const getPart = (type: string) => parts.find(p => p.type === type)?.value ?? "";
|
|
90
|
+
|
|
91
|
+
const year = getPart("year");
|
|
92
|
+
const month = getPart("month");
|
|
93
|
+
const day = getPart("day");
|
|
69
94
|
|
|
70
95
|
if (mode === "date") {
|
|
71
96
|
return `${year}-${month}-${day}`;
|
|
72
97
|
} else {
|
|
73
|
-
const hours =
|
|
74
|
-
const minutes =
|
|
98
|
+
const hours = getPart("hour");
|
|
99
|
+
const minutes = getPart("minute");
|
|
75
100
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
76
101
|
}
|
|
77
102
|
};
|
|
78
103
|
|
|
79
|
-
//
|
|
104
|
+
// Get the UTC offset for a specific date in the target timezone (in minutes)
|
|
105
|
+
const getTimezoneOffsetMinutes = (date: Date, tz?: string): number => {
|
|
106
|
+
if (!tz) {
|
|
107
|
+
// Local timezone: use built-in getTimezoneOffset (returns offset in minutes, inverted sign)
|
|
108
|
+
return -date.getTimezoneOffset();
|
|
109
|
+
}
|
|
110
|
+
// For named timezones, calculate the offset by comparing formatted times
|
|
111
|
+
const utcFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
112
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
113
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
114
|
+
timeZone: "UTC"
|
|
115
|
+
});
|
|
116
|
+
const tzFormatter = 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: tz
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const utcParts = utcFormatter.formatToParts(date);
|
|
123
|
+
const tzParts = tzFormatter.formatToParts(date);
|
|
124
|
+
|
|
125
|
+
const getPart = (parts: Intl.DateTimeFormatPart[], type: string) =>
|
|
126
|
+
parseInt(parts.find(p => p.type === type)?.value ?? "0", 10);
|
|
127
|
+
|
|
128
|
+
const utcDate = new Date(Date.UTC(
|
|
129
|
+
getPart(utcParts, "year"),
|
|
130
|
+
getPart(utcParts, "month") - 1,
|
|
131
|
+
getPart(utcParts, "day"),
|
|
132
|
+
getPart(utcParts, "hour"),
|
|
133
|
+
getPart(utcParts, "minute")
|
|
134
|
+
));
|
|
135
|
+
|
|
136
|
+
const tzDate = new Date(Date.UTC(
|
|
137
|
+
getPart(tzParts, "year"),
|
|
138
|
+
getPart(tzParts, "month") - 1,
|
|
139
|
+
getPart(tzParts, "day"),
|
|
140
|
+
getPart(tzParts, "hour"),
|
|
141
|
+
getPart(tzParts, "minute")
|
|
142
|
+
));
|
|
143
|
+
|
|
144
|
+
return (tzDate.getTime() - utcDate.getTime()) / 60000;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Handle input value change - convert from display timezone to UTC
|
|
80
148
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
81
149
|
const inputValue = e.target.value;
|
|
82
150
|
setInternalValue(inputValue);
|
|
@@ -88,21 +156,42 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|
|
88
156
|
}
|
|
89
157
|
|
|
90
158
|
try {
|
|
91
|
-
|
|
92
|
-
if (isNaN(parsed.getTime())) {
|
|
93
|
-
throw new Error("Invalid date");
|
|
94
|
-
}
|
|
159
|
+
let year: number, month: number, day: number, hours = 0, minutes = 0;
|
|
95
160
|
|
|
96
|
-
let newDate: Date;
|
|
97
161
|
if (mode === "date") {
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
162
|
+
// Parse date-only input: "YYYY-MM-DD"
|
|
163
|
+
[year, month, day] = inputValue.split("-").map(Number);
|
|
164
|
+
} else {
|
|
165
|
+
// Parse datetime-local input: "YYYY-MM-DDTHH:MM"
|
|
166
|
+
const [datePart, timePart] = inputValue.split("T");
|
|
167
|
+
[year, month, day] = datePart.split("-").map(Number);
|
|
168
|
+
[hours, minutes] = timePart.split(":").map(Number);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let resultDate: Date;
|
|
172
|
+
|
|
173
|
+
if (!timezone) {
|
|
174
|
+
// No timezone specified: interpret input as local time (backward compatible)
|
|
175
|
+
resultDate = new Date(year, month - 1, day, hours, minutes);
|
|
101
176
|
} else {
|
|
102
|
-
|
|
177
|
+
// Timezone specified: interpret input as that timezone and convert to UTC
|
|
178
|
+
// We need to find the UTC equivalent of the entered time in the target timezone
|
|
179
|
+
|
|
180
|
+
// Create a reference UTC date to calculate the offset for this moment
|
|
181
|
+
const refUtcDate = new Date(Date.UTC(year, month - 1, day, hours, minutes));
|
|
182
|
+
const offsetMinutes = getTimezoneOffsetMinutes(refUtcDate, timezone);
|
|
183
|
+
|
|
184
|
+
// Convert from target timezone to UTC:
|
|
185
|
+
// If user entered 00:00 in Mexico (UTC-6, offset=-360), we subtract the offset
|
|
186
|
+
// Date.UTC gives us 00:00 UTC, subtracting -360 minutes (= adding 360 min) gives 06:00 UTC
|
|
187
|
+
resultDate = new Date(Date.UTC(year, month - 1, day, hours, minutes) - offsetMinutes * 60000);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isNaN(resultDate.getTime())) {
|
|
191
|
+
throw new Error("Invalid date");
|
|
103
192
|
}
|
|
104
193
|
|
|
105
|
-
onChange?.(
|
|
194
|
+
onChange?.(resultDate);
|
|
106
195
|
} catch (e) {
|
|
107
196
|
// Don't call onChange with null while typing
|
|
108
197
|
return;
|
|
@@ -156,7 +245,7 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|
|
156
245
|
disabled ? "opacity-50" : ""
|
|
157
246
|
)}
|
|
158
247
|
shrink={true}
|
|
159
|
-
|
|
248
|
+
// shrink={hasValue || focused}
|
|
160
249
|
>
|
|
161
250
|
{label}
|
|
162
251
|
</InputLabel>
|
|
@@ -193,20 +282,20 @@ export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|
|
193
282
|
}}
|
|
194
283
|
className="absolute right-3 top-1/2 transform -translate-y-1/2 !text-surface-accent-500"
|
|
195
284
|
>
|
|
196
|
-
<CalendarMonthIcon color={"disabled"}/>
|
|
285
|
+
<CalendarMonthIcon color={"disabled"} />
|
|
197
286
|
</IconButton>
|
|
198
287
|
{clearable && value && (
|
|
199
288
|
<IconButton
|
|
200
289
|
onClick={handleClear}
|
|
201
290
|
className="absolute right-14 top-1/2 transform -translate-y-1/2 text-surface-accent-400 "
|
|
202
291
|
>
|
|
203
|
-
<CloseIcon/>
|
|
292
|
+
<CloseIcon />
|
|
204
293
|
</IconButton>
|
|
205
294
|
)}
|
|
206
295
|
</div>
|
|
207
296
|
{invalidValue && (
|
|
208
297
|
<div className="flex items-center m-2">
|
|
209
|
-
<ErrorIcon size={"small"} color={"error"}/>
|
|
298
|
+
<ErrorIcon size={"small"} color={"error"} />
|
|
210
299
|
<div className="pl-2">
|
|
211
300
|
<Typography variant={"body2"}>
|
|
212
301
|
Invalid date value for this field
|
|
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react";
|
|
|
3
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
4
|
import { paperMixin } from "../styles";
|
|
5
5
|
import { cls } from "../util";
|
|
6
|
+
import { usePortalContainer } from "../hooks/PortalContainerContext";
|
|
6
7
|
|
|
7
8
|
export type DialogProps = {
|
|
8
9
|
open?: boolean;
|
|
@@ -24,21 +25,22 @@ export type DialogProps = {
|
|
|
24
25
|
* If `true`, the dialog will not focus the first focusable element when opened.
|
|
25
26
|
*/
|
|
26
27
|
disableInitialFocus?: boolean;
|
|
28
|
+
portalContainer?: HTMLElement | null;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const widthClasses = {
|
|
30
|
-
xs: "max-w-xs
|
|
31
|
-
sm: "max-w-sm
|
|
32
|
-
md: "max-w-md
|
|
33
|
-
lg: "max-w-lg
|
|
34
|
-
xl: "max-w-xl
|
|
35
|
-
"2xl": "max-w-2xl
|
|
36
|
-
"3xl": "max-w-3xl
|
|
37
|
-
"4xl": "max-w-4xl
|
|
38
|
-
"5xl": "max-w-5xl
|
|
39
|
-
"6xl": "max-w-6xl
|
|
40
|
-
"7xl": "max-w-7xl
|
|
41
|
-
full: "max-w-full
|
|
32
|
+
xs: "max-w-xs w-xs",
|
|
33
|
+
sm: "max-w-sm w-sm",
|
|
34
|
+
md: "max-w-md w-md",
|
|
35
|
+
lg: "max-w-lg w-lg",
|
|
36
|
+
xl: "max-w-xl w-xl",
|
|
37
|
+
"2xl": "max-w-2xl w-2xl",
|
|
38
|
+
"3xl": "max-w-3xl w-3xl",
|
|
39
|
+
"4xl": "max-w-4xl w-4xl",
|
|
40
|
+
"5xl": "max-w-5xl w-5xl",
|
|
41
|
+
"6xl": "max-w-6xl w-6xl",
|
|
42
|
+
"7xl": "max-w-7xl w-7xl",
|
|
43
|
+
full: "max-w-full w-full"
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
export const Dialog = ({
|
|
@@ -57,15 +59,22 @@ export const Dialog = ({
|
|
|
57
59
|
onEscapeKeyDown,
|
|
58
60
|
onPointerDownOutside,
|
|
59
61
|
onInteractOutside,
|
|
60
|
-
disableInitialFocus = true
|
|
62
|
+
disableInitialFocus = true,
|
|
63
|
+
portalContainer
|
|
61
64
|
}: DialogProps) => {
|
|
62
65
|
const [displayed, setDisplayed] = useState(false);
|
|
63
66
|
|
|
67
|
+
// Get the portal container from context
|
|
68
|
+
const contextContainer = usePortalContainer();
|
|
69
|
+
|
|
70
|
+
// Prioritize manual prop, fallback to context container
|
|
71
|
+
const finalContainer = (portalContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;
|
|
72
|
+
|
|
64
73
|
useEffect(() => {
|
|
65
74
|
if (!open) {
|
|
66
75
|
const timeout = setTimeout(() => {
|
|
67
76
|
setDisplayed(false);
|
|
68
|
-
},
|
|
77
|
+
}, 100);
|
|
69
78
|
return () => clearTimeout(timeout);
|
|
70
79
|
} else {
|
|
71
80
|
setDisplayed(true);
|
|
@@ -78,12 +87,12 @@ export const Dialog = ({
|
|
|
78
87
|
<DialogPrimitive.Root open={displayed || open}
|
|
79
88
|
modal={modal}
|
|
80
89
|
onOpenChange={onOpenChange}>
|
|
81
|
-
<DialogPrimitive.Portal>
|
|
90
|
+
<DialogPrimitive.Portal container={finalContainer}>
|
|
82
91
|
|
|
83
92
|
<div className={cls("fixed inset-0 z-30", containerClassName)}>
|
|
84
93
|
|
|
85
94
|
<DialogPrimitive.Overlay
|
|
86
|
-
className={cls("fixed inset-0 transition-opacity z-20 ease-in-out duration-200 bg-black bg-opacity-
|
|
95
|
+
className={cls("fixed inset-0 transition-opacity z-20 ease-in-out duration-200 bg-black dark:bg-opacity-60 dark:bg-black/60 bg-opacity-50 bg-black/50 dark: bg-black/60 backdrop-blur-sm ",
|
|
87
96
|
displayed && open ? "opacity-100" : "opacity-0",
|
|
88
97
|
"z-20 fixed top-0 left-0 w-full h-full flex justify-center items-center"
|
|
89
98
|
)}
|
|
@@ -19,7 +19,7 @@ export function DialogActions({
|
|
|
19
19
|
defaultBorderMixin,
|
|
20
20
|
"pt-2 pb-4 px-4 border-t flex flex-row items-center justify-end bottom-0 right-0 left-0 text-right z-2 gap-2",
|
|
21
21
|
position,
|
|
22
|
-
"bg-white bg-opacity-60 dark:bg-surface-900 dark:bg-opacity-60",
|
|
22
|
+
"bg-white bg-opacity-60 bg-white/60 dark:bg-surface-900 dark:bg-opacity-60 dark:bg-surface-900/60",
|
|
23
23
|
translucent ? "backdrop-blur-sm" : "",
|
|
24
24
|
className)}>
|
|
25
25
|
{children}
|
|
@@ -95,7 +95,7 @@ export function ExpandablePanel({
|
|
|
95
95
|
<div
|
|
96
96
|
className={cls(
|
|
97
97
|
"rounded-t flex items-center justify-between w-full min-h-[52px]",
|
|
98
|
-
"hover:bg-surface-accent-200 hover:bg-opacity-40 dark:hover:bg-surface-800 dark:hover:bg-opacity-40",
|
|
98
|
+
"hover:bg-surface-accent-200 hover:bg-opacity-40 hover:bg-surface-accent-200/40 dark:hover:bg-surface-800 dark:hover:bg-opacity-40 dark:hover:bg-surface-800/40",
|
|
99
99
|
invisible ? "border-b px-2" : "p-4",
|
|
100
100
|
open ? "py-6" : "py-4",
|
|
101
101
|
"transition-all duration-200",
|