@rebasepro/ui 0.17.3 → 0.18.1
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 +4 -0
- package/dist/components/MultiSelect.d.ts +10 -0
- package/dist/index.es.js +2 -1
- package/dist/index.es.js.map +1 -1
- package/package.json +32 -23
- package/src/components/Alert.tsx +0 -118
- package/src/components/Autocomplete.tsx +0 -93
- package/src/components/Avatar.tsx +0 -73
- package/src/components/Badge.tsx +0 -44
- package/src/components/BooleanSwitch.tsx +0 -116
- package/src/components/BooleanSwitchWithLabel.tsx +0 -120
- package/src/components/Button.tsx +0 -110
- package/src/components/Card.tsx +0 -43
- package/src/components/CenteredView.tsx +0 -35
- package/src/components/Checkbox.tsx +0 -129
- package/src/components/Chip.tsx +0 -131
- package/src/components/CircularProgress.tsx +0 -53
- package/src/components/CircularProgressCenter.tsx +0 -27
- package/src/components/Collapse.tsx +0 -66
- package/src/components/ColorPicker.tsx +0 -140
- package/src/components/Container.tsx +0 -51
- package/src/components/DateTimeField.tsx +0 -363
- package/src/components/DebouncedTextField.tsx +0 -76
- package/src/components/Dialog.tsx +0 -159
- package/src/components/DialogActions.tsx +0 -29
- package/src/components/DialogContent.tsx +0 -27
- package/src/components/DialogTitle.tsx +0 -41
- package/src/components/ErrorBoundary.tsx +0 -236
- package/src/components/ExpandablePanel.tsx +0 -130
- package/src/components/FileUpload.tsx +0 -103
- package/src/components/FilterChip.tsx +0 -89
- package/src/components/IconButton.tsx +0 -101
- package/src/components/InfoLabel.tsx +0 -23
- package/src/components/InputLabel.tsx +0 -40
- package/src/components/Label.tsx +0 -32
- package/src/components/LoadingButton.tsx +0 -32
- package/src/components/Markdown.tsx +0 -55
- package/src/components/Menu.tsx +0 -114
- package/src/components/Menubar.tsx +0 -355
- package/src/components/MultiSelect.tsx +0 -433
- package/src/components/Paper.tsx +0 -23
- package/src/components/Popover.tsx +0 -162
- package/src/components/RadioGroup.tsx +0 -74
- package/src/components/ResizablePanels.tsx +0 -189
- package/src/components/SearchBar.tsx +0 -136
- package/src/components/Select.tsx +0 -326
- package/src/components/Separator.tsx +0 -28
- package/src/components/Sheet.tsx +0 -135
- package/src/components/Skeleton.tsx +0 -54
- package/src/components/Slider.tsx +0 -110
- package/src/components/Table.tsx +0 -178
- package/src/components/Tabs.tsx +0 -185
- package/src/components/TextField.tsx +0 -270
- package/src/components/TextareaAutosize.tsx +0 -368
- package/src/components/ToggleButtonGroup.tsx +0 -69
- package/src/components/Tooltip.tsx +0 -153
- package/src/components/Typography.tsx +0 -132
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +0 -90
- package/src/components/VirtualTable/VirtualTable.tsx +0 -700
- package/src/components/VirtualTable/VirtualTableCell.tsx +0 -59
- package/src/components/VirtualTable/VirtualTableHeader.tsx +0 -327
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +0 -250
- package/src/components/VirtualTable/VirtualTableProps.tsx +0 -339
- package/src/components/VirtualTable/VirtualTableRow.tsx +0 -51
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +0 -44
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -89
- package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +0 -71
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +0 -107
- package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +0 -28
- package/src/components/VirtualTable/index.tsx +0 -10
- package/src/components/VirtualTable/selection/SelectionContext.tsx +0 -36
- package/src/components/VirtualTable/selection/SelectionStore.ts +0 -54
- package/src/components/VirtualTable/types.tsx +0 -47
- package/src/components/common/SelectInputLabel.tsx +0 -23
- package/src/components/index.tsx +0 -62
- package/src/hooks/PortalContainerContext.tsx +0 -50
- package/src/hooks/index.ts +0 -7
- package/src/hooks/useDebounceCallback.tsx +0 -21
- package/src/hooks/useDebounceValue.tsx +0 -20
- package/src/hooks/useDebouncedCallback.ts +0 -39
- package/src/hooks/useInjectStyles.tsx +0 -31
- package/src/hooks/useOutsideAlerter.tsx +0 -46
- package/src/icons/GitHubIcon.tsx +0 -21
- package/src/icons/HandleIcon.tsx +0 -23
- package/src/icons/Icon.tsx +0 -33
- package/src/icons/LucideIconByName.tsx +0 -117
- package/src/icons/cool_icon_keys.ts +0 -12
- package/src/icons/icon_keys.ts +0 -1936
- package/src/icons/index.ts +0 -179
- package/src/index.css +0 -169
- package/src/index.ts +0 -47
- package/src/scripts/fixIconSizes.cjs +0 -92
- package/src/scripts/fixIconSizesToConst.cjs +0 -106
- package/src/scripts/generateIconKeys.ts +0 -41
- package/src/scripts/generateIcons.ts +0 -4
- package/src/scripts/saveIconFiles.ts +0 -89
- package/src/styles.ts +0 -141
- package/src/theme.css +0 -168
- package/src/util/chip_colors.ts +0 -242
- package/src/util/cls.ts +0 -8
- package/src/util/debounce.ts +0 -28
- package/src/util/hash.ts +0 -11
- package/src/util/index.ts +0 -4
- package/src/util/key_to_icon_component.ts +0 -11
- package/src/util/lazy_chunk.ts +0 -101
- package/src/views/CardView.tsx +0 -281
- package/src/views/CollectionView/CollectionCardView.tsx +0 -265
- package/src/views/CollectionView/CollectionKanbanView.tsx +0 -246
- package/src/views/CollectionView/CollectionListView.tsx +0 -240
- package/src/views/CollectionView/CollectionTableView.tsx +0 -229
- package/src/views/CollectionView/CollectionView.tsx +0 -450
- package/src/views/CollectionView/CollectionViewToolbar.tsx +0 -242
- package/src/views/CollectionView/CollectionViewTypes.ts +0 -181
- package/src/views/CollectionView/DefaultCellRenderer.tsx +0 -404
- package/src/views/CollectionView/index.ts +0 -39
- package/src/views/CollectionView/resolveTitleProperty.ts +0 -52
- package/src/views/CollectionView/utils.ts +0 -14
- package/src/views/Kanban/Board.tsx +0 -384
- package/src/views/Kanban/BoardColumn.tsx +0 -138
- package/src/views/Kanban/BoardColumnTitle.tsx +0 -47
- package/src/views/Kanban/BoardSortableList.tsx +0 -182
- package/src/views/Kanban/board_types.ts +0 -80
- package/src/views/Kanban/index.tsx +0 -5
- package/src/views/Kanban/placement.ts +0 -62
- package/src/views/KanbanView.tsx +0 -32
- package/src/views/ListView.tsx +0 -296
- package/src/views/index.ts +0 -5
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { CHIP_COLORS, cls } from "../util";
|
|
4
|
-
import { ChipColorKey, ChipColorScheme } from "./Chip";
|
|
5
|
-
import type { ChipTone } from "../util/chip_colors";
|
|
6
|
-
import { CheckIcon } from "lucide-react";
|
|
7
|
-
import { Tooltip } from "./Tooltip";
|
|
8
|
-
|
|
9
|
-
export interface ColorPickerProps {
|
|
10
|
-
/**
|
|
11
|
-
* Currently selected color key
|
|
12
|
-
*/
|
|
13
|
-
value?: ChipColorKey;
|
|
14
|
-
/**
|
|
15
|
-
* Callback when color selection changes. Passes undefined when "Auto" is selected.
|
|
16
|
-
*/
|
|
17
|
-
onChange: (colorKey: ChipColorKey | undefined) => void;
|
|
18
|
-
/**
|
|
19
|
-
* Size of the color swatches
|
|
20
|
-
*/
|
|
21
|
-
size?: "small" | "medium";
|
|
22
|
-
/**
|
|
23
|
-
* Whether to show the "Auto" option that clears the selection
|
|
24
|
-
*/
|
|
25
|
-
allowClear?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Whether the picker is disabled
|
|
28
|
-
*/
|
|
29
|
-
disabled?: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Hues across, tones down — the arrangement the palette is built in.
|
|
33
|
-
const BASE_COLORS = ["blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange", "yellow", "green", "emerald", "teal", "cyan", "gray"] as const;
|
|
34
|
-
|
|
35
|
-
// Lightest first, matching how the bare hue name reads.
|
|
36
|
-
const TONES: ChipTone[] = ["Lighter", "Light", "Dark", "Darker"];
|
|
37
|
-
|
|
38
|
-
// Helper to get readable name from color key
|
|
39
|
-
function getColorDisplayName(hue: string, tone: ChipTone): string {
|
|
40
|
-
const hueName = `${hue.charAt(0).toUpperCase()}${hue.slice(1)}`;
|
|
41
|
-
return tone === "Lighter" ? hueName : `${hueName} ${tone.toLowerCase()}`;
|
|
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-lg text-sm transition-colors duration-150",
|
|
70
|
-
"hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary",
|
|
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
|
-
{/* One row per tone. Wrapping rather than a fixed 15-column grid:
|
|
89
|
-
the picker also opens inside narrow property panels, where
|
|
90
|
-
fifteen tracks would shrink each swatch to a few pixels. */}
|
|
91
|
-
{TONES.map((tone) => (
|
|
92
|
-
<div key={tone} className="flex flex-wrap gap-1.5">
|
|
93
|
-
{BASE_COLORS.map((base) => {
|
|
94
|
-
// The bare hue and its `Lighter` tone are the same
|
|
95
|
-
// scheme; the picker offers the short name so configs
|
|
96
|
-
// stay readable.
|
|
97
|
-
const colorKey = (tone === "Lighter" ? base : `${base}${tone}`) as ChipColorKey;
|
|
98
|
-
const colorScheme = CHIP_COLORS[colorKey] as ChipColorScheme;
|
|
99
|
-
const isSelected = value === colorKey;
|
|
100
|
-
const displayName = getColorDisplayName(base, tone);
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<Tooltip
|
|
104
|
-
key={colorKey}
|
|
105
|
-
title={displayName}
|
|
106
|
-
delayDuration={300}
|
|
107
|
-
>
|
|
108
|
-
<button
|
|
109
|
-
type="button"
|
|
110
|
-
disabled={disabled}
|
|
111
|
-
onClick={() => onChange(colorKey)}
|
|
112
|
-
className={cls(
|
|
113
|
-
swatchSize,
|
|
114
|
-
"rounded-full transition-all flex items-center justify-center",
|
|
115
|
-
"hover:scale-110 hover:shadow-md",
|
|
116
|
-
"focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1",
|
|
117
|
-
disabled && "opacity-50 cursor-not-allowed hover:scale-100",
|
|
118
|
-
isSelected && "ring-2 ring-primary ring-offset-1"
|
|
119
|
-
)}
|
|
120
|
-
style={{
|
|
121
|
-
backgroundColor: colorScheme.color
|
|
122
|
-
}}
|
|
123
|
-
aria-label={displayName}
|
|
124
|
-
aria-pressed={isSelected}
|
|
125
|
-
>
|
|
126
|
-
{isSelected && (
|
|
127
|
-
<CheckIcon
|
|
128
|
-
size={checkSize}
|
|
129
|
-
style={{ color: colorScheme.text }}
|
|
130
|
-
/>
|
|
131
|
-
)}
|
|
132
|
-
</button>
|
|
133
|
-
</Tooltip>
|
|
134
|
-
);
|
|
135
|
-
})}
|
|
136
|
-
</div>
|
|
137
|
-
))}
|
|
138
|
-
</div>
|
|
139
|
-
);
|
|
140
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import React, { ForwardRefRenderFunction } from "react";
|
|
3
|
-
import { cls } from "../util";
|
|
4
|
-
|
|
5
|
-
export type ContainerProps = {
|
|
6
|
-
children: React.ReactNode;
|
|
7
|
-
style?: React.CSSProperties;
|
|
8
|
-
className?: string;
|
|
9
|
-
maxWidth?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const containerMaxWidths = {
|
|
13
|
-
xs: "max-w-xs",
|
|
14
|
-
sm: "max-w-sm",
|
|
15
|
-
md: "max-w-md",
|
|
16
|
-
lg: "max-w-lg",
|
|
17
|
-
xl: "max-w-xl",
|
|
18
|
-
"2xl": "max-w-2xl",
|
|
19
|
-
"3xl": "max-w-3xl",
|
|
20
|
-
"4xl": "max-w-4xl",
|
|
21
|
-
"5xl": "max-w-5xl",
|
|
22
|
-
"6xl": "max-w-6xl",
|
|
23
|
-
"7xl": "max-w-7xl"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const ContainerInner: ForwardRefRenderFunction<HTMLDivElement, ContainerProps> = (
|
|
27
|
-
{
|
|
28
|
-
children,
|
|
29
|
-
className,
|
|
30
|
-
style,
|
|
31
|
-
maxWidth = "7xl"
|
|
32
|
-
},
|
|
33
|
-
ref
|
|
34
|
-
) => {
|
|
35
|
-
|
|
36
|
-
const classForMaxWidth = maxWidth ? containerMaxWidths[maxWidth] : "";
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div
|
|
40
|
-
ref={ref}
|
|
41
|
-
className={cls("mx-auto px-3 md:px-4 lg:px-6",
|
|
42
|
-
classForMaxWidth,
|
|
43
|
-
className)}
|
|
44
|
-
style={style}>
|
|
45
|
-
{children}
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const Container = React.forwardRef(ContainerInner);
|
|
51
|
-
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
"use client";
|
|
3
|
-
|
|
4
|
-
import React, { useId, useRef, useState } from "react";
|
|
5
|
-
import { CalendarIcon, XIcon, AlertCircleIcon } from "lucide-react";
|
|
6
|
-
import { iconSize } from "../icons/Icon";
|
|
7
|
-
import { IconButton } from "./IconButton";
|
|
8
|
-
import { fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundMixin } from "../styles";
|
|
9
|
-
import { InputLabel } from "./InputLabel";
|
|
10
|
-
import { Typography } from "./Typography";
|
|
11
|
-
import { cls } from "../util";
|
|
12
|
-
import { useInjectStyles } from "../hooks";
|
|
13
|
-
|
|
14
|
-
export type DateTimeFieldProps = {
|
|
15
|
-
value?: Date | null;
|
|
16
|
-
onChange?: (date: Date | null) => void;
|
|
17
|
-
mode?: "date" | "date_time";
|
|
18
|
-
disabled?: boolean;
|
|
19
|
-
clearable?: boolean;
|
|
20
|
-
error?: boolean;
|
|
21
|
-
size?: "smallest" | "small" | "medium" | "large";
|
|
22
|
-
label?: React.ReactNode;
|
|
23
|
-
className?: string;
|
|
24
|
-
style?: React.CSSProperties;
|
|
25
|
-
inputClassName?: string;
|
|
26
|
-
invisible?: boolean;
|
|
27
|
-
locale?: string;
|
|
28
|
-
/**
|
|
29
|
-
* IANA timezone string (e.g., "America/New_York", "Europe/London").
|
|
30
|
-
* Used to display and input dates in the specified timezone.
|
|
31
|
-
* The value passed to onChange will always be in UTC.
|
|
32
|
-
* If not provided, uses the user's local timezone.
|
|
33
|
-
*/
|
|
34
|
-
timezone?: string;
|
|
35
|
-
/**
|
|
36
|
-
* Names the control when there is no `label` — e.g. when the surrounding
|
|
37
|
-
* form draws the label itself and passes `label={undefined}`.
|
|
38
|
-
*/
|
|
39
|
-
"aria-label"?: string;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const DateTimeField: React.FC<DateTimeFieldProps> = ({
|
|
43
|
-
value,
|
|
44
|
-
label,
|
|
45
|
-
onChange,
|
|
46
|
-
disabled,
|
|
47
|
-
clearable,
|
|
48
|
-
mode = "date",
|
|
49
|
-
error,
|
|
50
|
-
size = "large",
|
|
51
|
-
className,
|
|
52
|
-
style,
|
|
53
|
-
inputClassName,
|
|
54
|
-
invisible,
|
|
55
|
-
timezone,
|
|
56
|
-
"aria-label": ariaLabel
|
|
57
|
-
}) => {
|
|
58
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
59
|
-
const [focused, setFocused] = useState(false);
|
|
60
|
-
// The label was decorative: an InputLabel with no `htmlFor` beside an input
|
|
61
|
-
// with no `id`, so a date field announced itself by its value even when it
|
|
62
|
-
// visibly had a label. Wired the way TextField already does it.
|
|
63
|
-
const inputId = useId();
|
|
64
|
-
const labelId = `${inputId}-label`;
|
|
65
|
-
const [internalValue, setInternalValue] = useState<string>("");
|
|
66
|
-
const [isTyping, setIsTyping] = useState(false);
|
|
67
|
-
const invalidValue = value !== undefined && value !== null && (!(value instanceof Date) || isNaN(value.getTime()));
|
|
68
|
-
|
|
69
|
-
// The calendar/clear adornments are IconButtons. Left at their default
|
|
70
|
-
// (`medium`, 40px) they floored the whole field at 40px, so smallest/small
|
|
71
|
-
// rendered identically to medium. Keep them a step below the field.
|
|
72
|
-
const adornmentSize = size === "large" ? "medium" : size === "medium" ? "small" : "smallest";
|
|
73
|
-
|
|
74
|
-
useInjectStyles("DateTimeField", inputStyles);
|
|
75
|
-
|
|
76
|
-
const handleClear = (e: React.MouseEvent) => {
|
|
77
|
-
e.preventDefault();
|
|
78
|
-
setInternalValue("");
|
|
79
|
-
setIsTyping(false);
|
|
80
|
-
onChange?.(null);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Convert UTC Date to display string in the specified timezone
|
|
84
|
-
const valueAsInputValue = (
|
|
85
|
-
dateValue: Date | null,
|
|
86
|
-
mode: "date" | "date_time"
|
|
87
|
-
) => {
|
|
88
|
-
if (!dateValue || !(dateValue instanceof Date) || isNaN(dateValue.getTime())) {
|
|
89
|
-
return "";
|
|
90
|
-
}
|
|
91
|
-
const pad = (n: number) => n.toString().padStart(2, "0");
|
|
92
|
-
|
|
93
|
-
// Use Intl.DateTimeFormat to get date parts in the target timezone
|
|
94
|
-
const options: Intl.DateTimeFormatOptions = {
|
|
95
|
-
year: "numeric",
|
|
96
|
-
month: "2-digit",
|
|
97
|
-
day: "2-digit",
|
|
98
|
-
hour: "2-digit",
|
|
99
|
-
minute: "2-digit",
|
|
100
|
-
hour12: false,
|
|
101
|
-
timeZone: timezone // undefined = local timezone
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const formatter = new Intl.DateTimeFormat("en-CA", options);
|
|
105
|
-
const parts = formatter.formatToParts(dateValue);
|
|
106
|
-
|
|
107
|
-
const getPart = (type: string) => parts.find(p => p.type === type)?.value ?? "";
|
|
108
|
-
|
|
109
|
-
const year = getPart("year");
|
|
110
|
-
const month = getPart("month");
|
|
111
|
-
const day = getPart("day");
|
|
112
|
-
|
|
113
|
-
if (mode === "date") {
|
|
114
|
-
return `${year}-${month}-${day}`;
|
|
115
|
-
} else {
|
|
116
|
-
const hours = getPart("hour");
|
|
117
|
-
const minutes = getPart("minute");
|
|
118
|
-
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// Get the UTC offset for a specific date in the target timezone (in minutes)
|
|
123
|
-
const getTimezoneOffsetMinutes = (date: Date, tz?: string): number => {
|
|
124
|
-
if (!tz) {
|
|
125
|
-
// Local timezone: use built-in getTimezoneOffset (returns offset in minutes, inverted sign)
|
|
126
|
-
return -date.getTimezoneOffset();
|
|
127
|
-
}
|
|
128
|
-
// For named timezones, calculate the offset by comparing formatted times
|
|
129
|
-
const utcFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
130
|
-
year: "numeric",
|
|
131
|
-
month: "2-digit",
|
|
132
|
-
day: "2-digit",
|
|
133
|
-
hour: "2-digit",
|
|
134
|
-
minute: "2-digit",
|
|
135
|
-
hour12: false,
|
|
136
|
-
timeZone: "UTC"
|
|
137
|
-
});
|
|
138
|
-
const tzFormatter = new Intl.DateTimeFormat("en-CA", {
|
|
139
|
-
year: "numeric",
|
|
140
|
-
month: "2-digit",
|
|
141
|
-
day: "2-digit",
|
|
142
|
-
hour: "2-digit",
|
|
143
|
-
minute: "2-digit",
|
|
144
|
-
hour12: false,
|
|
145
|
-
timeZone: tz
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const utcParts = utcFormatter.formatToParts(date);
|
|
149
|
-
const tzParts = tzFormatter.formatToParts(date);
|
|
150
|
-
|
|
151
|
-
const getPart = (parts: Intl.DateTimeFormatPart[], type: string) =>
|
|
152
|
-
parseInt(parts.find(p => p.type === type)?.value ?? "0", 10);
|
|
153
|
-
|
|
154
|
-
const utcDate = new Date(Date.UTC(
|
|
155
|
-
getPart(utcParts, "year"),
|
|
156
|
-
getPart(utcParts, "month") - 1,
|
|
157
|
-
getPart(utcParts, "day"),
|
|
158
|
-
getPart(utcParts, "hour"),
|
|
159
|
-
getPart(utcParts, "minute")
|
|
160
|
-
));
|
|
161
|
-
|
|
162
|
-
const tzDate = new Date(Date.UTC(
|
|
163
|
-
getPart(tzParts, "year"),
|
|
164
|
-
getPart(tzParts, "month") - 1,
|
|
165
|
-
getPart(tzParts, "day"),
|
|
166
|
-
getPart(tzParts, "hour"),
|
|
167
|
-
getPart(tzParts, "minute")
|
|
168
|
-
));
|
|
169
|
-
|
|
170
|
-
return (tzDate.getTime() - utcDate.getTime()) / 60000;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Handle input value change - convert from display timezone to UTC
|
|
174
|
-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
175
|
-
const inputValue = e.target.value;
|
|
176
|
-
setInternalValue(inputValue);
|
|
177
|
-
setIsTyping(true);
|
|
178
|
-
|
|
179
|
-
if (!inputValue) {
|
|
180
|
-
onChange?.(null);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
let year: number, month: number, day: number, hours = 0, minutes = 0;
|
|
186
|
-
|
|
187
|
-
if (mode === "date") {
|
|
188
|
-
// Parse date-only input: "YYYY-MM-DD"
|
|
189
|
-
[year, month, day] = inputValue.split("-").map(Number);
|
|
190
|
-
} else {
|
|
191
|
-
// Parse datetime-local input: "YYYY-MM-DDTHH:MM"
|
|
192
|
-
const [datePart, timePart] = inputValue.split("T");
|
|
193
|
-
[year, month, day] = datePart.split("-").map(Number);
|
|
194
|
-
[hours, minutes] = timePart.split(":").map(Number);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let resultDate: Date;
|
|
198
|
-
|
|
199
|
-
if (!timezone) {
|
|
200
|
-
// No timezone specified: interpret input as local time (backward compatible)
|
|
201
|
-
resultDate = new Date(year, month - 1, day, hours, minutes);
|
|
202
|
-
} else {
|
|
203
|
-
// Timezone specified: interpret input as that timezone and convert to UTC
|
|
204
|
-
// We need to find the UTC equivalent of the entered time in the target timezone
|
|
205
|
-
|
|
206
|
-
// Create a reference UTC date to calculate the offset for this moment
|
|
207
|
-
const refUtcDate = new Date(Date.UTC(year, month - 1, day, hours, minutes));
|
|
208
|
-
const offsetMinutes = getTimezoneOffsetMinutes(refUtcDate, timezone);
|
|
209
|
-
|
|
210
|
-
// Convert from target timezone to UTC:
|
|
211
|
-
// If user entered 00:00 in Mexico (UTC-6, offset=-360), we subtract the offset
|
|
212
|
-
// Date.UTC gives us 00:00 UTC, subtracting -360 minutes (= adding 360 min) gives 06:00 UTC
|
|
213
|
-
resultDate = new Date(Date.UTC(year, month - 1, day, hours, minutes) - offsetMinutes * 60000);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (isNaN(resultDate.getTime())) {
|
|
217
|
-
throw new Error("Invalid date");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
onChange?.(resultDate);
|
|
221
|
-
} catch (e) {
|
|
222
|
-
// Don't call onChange with null while typing
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const handleFocus = () => {
|
|
228
|
-
setFocused(true);
|
|
229
|
-
setIsTyping(true);
|
|
230
|
-
setInternalValue(valueAsInputValue(value ?? null, mode));
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const handleBlur = () => {
|
|
234
|
-
setFocused(false);
|
|
235
|
-
setIsTyping(false);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<>
|
|
240
|
-
{/* Inject the styles to hide the calendar icon */}
|
|
241
|
-
<style>{inputStyles}</style>
|
|
242
|
-
<div
|
|
243
|
-
className={cls(
|
|
244
|
-
"rounded-lg relative max-w-full",
|
|
245
|
-
!invisible && fieldBackgroundMixin,
|
|
246
|
-
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
247
|
-
{
|
|
248
|
-
"min-h-[28px]": size === "smallest",
|
|
249
|
-
"min-h-[32px]": size === "small",
|
|
250
|
-
"min-h-[40px]": size === "medium",
|
|
251
|
-
"min-h-[48px]": size === "large"
|
|
252
|
-
},
|
|
253
|
-
className
|
|
254
|
-
)}
|
|
255
|
-
style={style}
|
|
256
|
-
onClick={() => {
|
|
257
|
-
if (!disabled) {
|
|
258
|
-
inputRef.current?.focus();
|
|
259
|
-
}
|
|
260
|
-
}}
|
|
261
|
-
>
|
|
262
|
-
{label && (
|
|
263
|
-
<InputLabel
|
|
264
|
-
id={labelId}
|
|
265
|
-
htmlFor={inputId}
|
|
266
|
-
className={cls(
|
|
267
|
-
"absolute top-1 pointer-events-none",
|
|
268
|
-
!error
|
|
269
|
-
? focused
|
|
270
|
-
? "text-primary"
|
|
271
|
-
: "text-text-secondary dark:text-text-secondary-dark"
|
|
272
|
-
: "text-red-600 dark:text-red-500",
|
|
273
|
-
disabled ? "opacity-50" : ""
|
|
274
|
-
)}
|
|
275
|
-
shrink={true}
|
|
276
|
-
// shrink={hasValue || focused}
|
|
277
|
-
>
|
|
278
|
-
{label}
|
|
279
|
-
</InputLabel>
|
|
280
|
-
)}
|
|
281
|
-
|
|
282
|
-
<input
|
|
283
|
-
ref={inputRef}
|
|
284
|
-
id={inputId}
|
|
285
|
-
aria-labelledby={label ? labelId : undefined}
|
|
286
|
-
aria-label={label ? undefined : ariaLabel}
|
|
287
|
-
type={mode === "date_time" ? "datetime-local" : "date"}
|
|
288
|
-
value={isTyping ? internalValue : valueAsInputValue(value ?? null, mode)}
|
|
289
|
-
onChange={handleInputChange}
|
|
290
|
-
onFocus={handleFocus}
|
|
291
|
-
onBlur={handleBlur}
|
|
292
|
-
disabled={disabled}
|
|
293
|
-
className={cls(
|
|
294
|
-
"w-full outline-hidden bg-transparent leading-normal text-base px-3",
|
|
295
|
-
clearable ? "pr-14" : "pr-12",
|
|
296
|
-
"rounded-lg",
|
|
297
|
-
{
|
|
298
|
-
"min-h-[28px]": size === "smallest",
|
|
299
|
-
"min-h-[32px]": size === "small",
|
|
300
|
-
"min-h-[40px]": size === "medium",
|
|
301
|
-
"min-h-[48px]": size === "large"
|
|
302
|
-
},
|
|
303
|
-
// A flat py-2 (16px) plus the text-base line box floored
|
|
304
|
-
// the field at 40px, so smallest/small/medium all
|
|
305
|
-
// rendered the same height. Scale it with `size`.
|
|
306
|
-
label
|
|
307
|
-
? "pt-8 pb-2"
|
|
308
|
-
: { smallest: "py-0.5", small: "py-1", medium: "py-2", large: "py-3" }[size],
|
|
309
|
-
inputClassName,
|
|
310
|
-
disabled &&
|
|
311
|
-
"border border-transparent outline-hidden opacity-50 dark:opacity-50 text-surface-accent-600 dark:text-surface-accent-500"
|
|
312
|
-
)}
|
|
313
|
-
/>
|
|
314
|
-
<IconButton
|
|
315
|
-
size={adornmentSize}
|
|
316
|
-
onClick={(e) => {
|
|
317
|
-
e.stopPropagation();
|
|
318
|
-
inputRef.current?.showPicker();
|
|
319
|
-
}}
|
|
320
|
-
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-surface-accent-500!"
|
|
321
|
-
>
|
|
322
|
-
<CalendarIcon/>
|
|
323
|
-
</IconButton>
|
|
324
|
-
{clearable && value && (
|
|
325
|
-
<IconButton
|
|
326
|
-
size={adornmentSize}
|
|
327
|
-
onClick={handleClear}
|
|
328
|
-
className="absolute right-14 top-1/2 transform -translate-y-1/2 text-surface-accent-400 "
|
|
329
|
-
>
|
|
330
|
-
<XIcon/>
|
|
331
|
-
</IconButton>
|
|
332
|
-
)}
|
|
333
|
-
</div>
|
|
334
|
-
{invalidValue && (
|
|
335
|
-
<div className="flex items-center m-2">
|
|
336
|
-
<AlertCircleIcon size={iconSize.small} color={"error"}/>
|
|
337
|
-
<div className="pl-2">
|
|
338
|
-
<Typography variant={"body2"}>
|
|
339
|
-
Invalid date value for this field
|
|
340
|
-
</Typography>
|
|
341
|
-
<Typography variant={"body2"}>
|
|
342
|
-
{`The provided value is: ${JSON.stringify(value)}`}
|
|
343
|
-
</Typography>
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
346
|
-
)}
|
|
347
|
-
</>
|
|
348
|
-
);
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const inputStyles = `
|
|
352
|
-
/* Hide the default calendar icon in Chrome, Safari, Edge, Opera */
|
|
353
|
-
input[type="date"]::-webkit-calendar-picker-indicator,
|
|
354
|
-
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
|
|
355
|
-
display: none;
|
|
356
|
-
-webkit-appearance: none;
|
|
357
|
-
}
|
|
358
|
-
/* Hide default calendar icon in Firefox */
|
|
359
|
-
input[type="date"],
|
|
360
|
-
input[type="datetime-local"] {
|
|
361
|
-
-moz-appearance:textfield;
|
|
362
|
-
}
|
|
363
|
-
`;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { TextField, TextFieldProps } from "./TextField";
|
|
4
|
-
|
|
5
|
-
type TextFieldChangeEvent = React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>;
|
|
6
|
-
|
|
7
|
-
export function DebouncedTextField<T extends string | number>(props: TextFieldProps<T>) {
|
|
8
|
-
|
|
9
|
-
const [internalValue, setInternalValue] = useState<T | string>(props.value ?? "");
|
|
10
|
-
const lastSentValueRef = useRef<T | string>(props.value ?? "");
|
|
11
|
-
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
12
|
-
|
|
13
|
-
// Sync state with props.value when it changes externally
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
const externalValue = props.value ?? "";
|
|
16
|
-
if (externalValue !== lastSentValueRef.current) {
|
|
17
|
-
setInternalValue(externalValue);
|
|
18
|
-
lastSentValueRef.current = externalValue;
|
|
19
|
-
}
|
|
20
|
-
}, [props.value]);
|
|
21
|
-
|
|
22
|
-
// Cleanup timer on unmount
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
return () => {
|
|
25
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
26
|
-
};
|
|
27
|
-
}, []);
|
|
28
|
-
|
|
29
|
-
const flushChange = useCallback((value: T | string, event?: TextFieldChangeEvent) => {
|
|
30
|
-
if (timerRef.current) {
|
|
31
|
-
clearTimeout(timerRef.current);
|
|
32
|
-
timerRef.current = undefined;
|
|
33
|
-
}
|
|
34
|
-
if (value !== props.value && props.onChange) {
|
|
35
|
-
lastSentValueRef.current = value;
|
|
36
|
-
const e = {
|
|
37
|
-
...event,
|
|
38
|
-
target: {
|
|
39
|
-
...event?.target,
|
|
40
|
-
value: value,
|
|
41
|
-
name: props.name
|
|
42
|
-
}
|
|
43
|
-
} as TextFieldChangeEvent;
|
|
44
|
-
props.onChange(e);
|
|
45
|
-
}
|
|
46
|
-
}, [props.value, props.onChange, props.name]);
|
|
47
|
-
|
|
48
|
-
const internalOnChange = useCallback((event: TextFieldChangeEvent) => {
|
|
49
|
-
const newValue = event.target.value;
|
|
50
|
-
setInternalValue(newValue);
|
|
51
|
-
|
|
52
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
53
|
-
|
|
54
|
-
const eventCopy = {
|
|
55
|
-
...event,
|
|
56
|
-
target: {
|
|
57
|
-
...event?.target,
|
|
58
|
-
name: event?.target?.name
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
timerRef.current = setTimeout(() => {
|
|
63
|
-
flushChange(newValue, eventCopy as TextFieldChangeEvent);
|
|
64
|
-
}, 150);
|
|
65
|
-
}, [flushChange]);
|
|
66
|
-
|
|
67
|
-
const internalOnBlur = useCallback((event: React.FocusEvent<HTMLInputElement>) => {
|
|
68
|
-
flushChange(internalValue, event as TextFieldChangeEvent);
|
|
69
|
-
props.onBlur?.(event);
|
|
70
|
-
}, [internalValue, flushChange, props.onBlur]);
|
|
71
|
-
|
|
72
|
-
return <TextField {...props}
|
|
73
|
-
onChange={internalOnChange}
|
|
74
|
-
onBlur={internalOnBlur}
|
|
75
|
-
value={internalValue as T}/>;
|
|
76
|
-
}
|