@sixthshift/design-system 0.1.0
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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/styles.css +2 -0
- package/dist/theme.generated.css +438 -0
- package/package.json +238 -0
- package/src/components/Avatar/Avatar.tsx +19 -0
- package/src/components/Avatar/index.ts +1 -0
- package/src/components/Badge/Badge.tsx +59 -0
- package/src/components/Badge/index.ts +1 -0
- package/src/components/BarChart/BarChart.tsx +51 -0
- package/src/components/BarChart/index.ts +1 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +47 -0
- package/src/components/Breadcrumb/index.ts +5 -0
- package/src/components/Button/Button.tsx +157 -0
- package/src/components/Button/index.ts +1 -0
- package/src/components/Calendar/Calendar.tsx +433 -0
- package/src/components/Calendar/calendar.hooks.ts +329 -0
- package/src/components/Calendar/calendar.types.ts +117 -0
- package/src/components/Calendar/index.ts +28 -0
- package/src/components/Card/Card.tsx +47 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/Checkbox/Checkbox.tsx +144 -0
- package/src/components/Checkbox/index.ts +1 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +173 -0
- package/src/components/CheckboxGroup/index.ts +5 -0
- package/src/components/Code/Editor/Editor.tsx +161 -0
- package/src/components/Code/Editor/index.ts +2 -0
- package/src/components/Code/Editor/semanticTokenProvider.ts +69 -0
- package/src/components/Code/Editor/themes.ts +112 -0
- package/src/components/Code/ValidationStatus/ValidationStatus.tsx +81 -0
- package/src/components/Code/ValidationStatus/index.ts +1 -0
- package/src/components/Code/Workspace/StatusBar.tsx +47 -0
- package/src/components/Code/Workspace/Toolbar.tsx +47 -0
- package/src/components/Code/Workspace/Workspace.tsx +109 -0
- package/src/components/Code/Workspace/index.ts +3 -0
- package/src/components/ColorDot/ColorDot.tsx +50 -0
- package/src/components/ColorDot/index.ts +1 -0
- package/src/components/DatePicker/DatePicker.tsx +236 -0
- package/src/components/DatePicker/datepicker.hooks.ts +381 -0
- package/src/components/DatePicker/datepicker.types.ts +140 -0
- package/src/components/DatePicker/index.ts +12 -0
- package/src/components/DateRangePicker/DateRangePicker.tsx +127 -0
- package/src/components/DateRangePicker/daterangepicker.types.ts +53 -0
- package/src/components/DateRangePicker/index.ts +2 -0
- package/src/components/DateTimePicker/DateTimePicker.tsx +399 -0
- package/src/components/DateTimePicker/datetimepicker.types.ts +48 -0
- package/src/components/DateTimePicker/index.ts +2 -0
- package/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +679 -0
- package/src/components/DateTimeRangePicker/datetimerangepicker.types.ts +62 -0
- package/src/components/DateTimeRangePicker/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +29 -0
- package/src/components/EmptyState/index.ts +1 -0
- package/src/components/Field/Field.tsx +60 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FormField/FormField.tsx +55 -0
- package/src/components/FormField/index.ts +2 -0
- package/src/components/HeatMap/HeatMapCalendar.tsx +127 -0
- package/src/components/HeatMap/HeatMapMatrix.tsx +75 -0
- package/src/components/HeatMap/heat-map.utils.ts +15 -0
- package/src/components/HeatMap/index.ts +3 -0
- package/src/components/HoverCard/HoverCard.tsx +83 -0
- package/src/components/HoverCard/components/HoverCardContent.tsx +30 -0
- package/src/components/HoverCard/components/HoverCardContext.tsx +22 -0
- package/src/components/HoverCard/components/HoverCardTrigger.tsx +32 -0
- package/src/components/HoverCard/index.ts +3 -0
- package/src/components/Input/Input.tsx +48 -0
- package/src/components/Input/index.ts +1 -0
- package/src/components/LineChart/LineChart.tsx +251 -0
- package/src/components/LineChart/index.ts +1 -0
- package/src/components/Markdown/Markdown.tsx +40 -0
- package/src/components/Markdown/index.ts +1 -0
- package/src/components/Message/Message.tsx +59 -0
- package/src/components/Message/components/MessageBody.tsx +9 -0
- package/src/components/Message/components/MessageDescription.tsx +9 -0
- package/src/components/Message/components/MessageIcon.tsx +13 -0
- package/src/components/Message/components/MessageTitle.tsx +9 -0
- package/src/components/Message/index.ts +10 -0
- package/src/components/MetricList/MetricList.tsx +13 -0
- package/src/components/MetricList/MetricRow.tsx +30 -0
- package/src/components/MetricList/index.ts +2 -0
- package/src/components/Modal/Modal.tsx +146 -0
- package/src/components/Modal/components/ModalBody.tsx +9 -0
- package/src/components/Modal/components/ModalContext.tsx +27 -0
- package/src/components/Modal/components/ModalFooter.tsx +9 -0
- package/src/components/Modal/components/ModalHeader.tsx +29 -0
- package/src/components/Modal/components/index.ts +9 -0
- package/src/components/Modal/index.ts +13 -0
- package/src/components/NavBottom/NavBottom.tsx +53 -0
- package/src/components/NavBottom/index.ts +3 -0
- package/src/components/NavSide/NavItem.tsx +39 -0
- package/src/components/NavSide/NavSide.tsx +40 -0
- package/src/components/NavSide/index.ts +2 -0
- package/src/components/NavSide/types.ts +36 -0
- package/src/components/Pagination/Pagination.tsx +86 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Popover/Popover.tsx +68 -0
- package/src/components/Popover/components/PopoverBody.tsx +32 -0
- package/src/components/Popover/components/PopoverClose.tsx +26 -0
- package/src/components/Popover/components/PopoverContext.tsx +24 -0
- package/src/components/Popover/components/PopoverTrigger.tsx +31 -0
- package/src/components/Popover/index.ts +10 -0
- package/src/components/ProgressBar/ProgressBar.tsx +42 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/RadioButton/RadioButton.tsx +89 -0
- package/src/components/RadioButton/index.ts +1 -0
- package/src/components/RadioButtonGroup/RadioButtonGroup.tsx +193 -0
- package/src/components/RadioButtonGroup/index.ts +5 -0
- package/src/components/SearchInput/SearchInput.tsx +45 -0
- package/src/components/SearchInput/index.ts +2 -0
- package/src/components/Select/Select.tsx +289 -0
- package/src/components/Select/SelectDropdown.tsx +90 -0
- package/src/components/Select/SelectTrigger.tsx +130 -0
- package/src/components/Select/index.ts +3 -0
- package/src/components/Select/useSelectKeyboard.ts +111 -0
- package/src/components/Separator/Separator.tsx +20 -0
- package/src/components/Separator/index.ts +1 -0
- package/src/components/Sheet/Sheet.tsx +136 -0
- package/src/components/Sheet/components/SheetBody.tsx +9 -0
- package/src/components/Sheet/components/SheetContext.tsx +23 -0
- package/src/components/Sheet/components/SheetFooter.tsx +9 -0
- package/src/components/Sheet/components/SheetHeader.tsx +29 -0
- package/src/components/Sheet/components/index.ts +9 -0
- package/src/components/Sheet/index.ts +13 -0
- package/src/components/Skeleton/Skeleton.tsx +7 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Sparkline/Sparkline.tsx +93 -0
- package/src/components/Sparkline/index.ts +1 -0
- package/src/components/Spinner/Spinner.tsx +41 -0
- package/src/components/Spinner/index.ts +1 -0
- package/src/components/StatsCard/StatsCard.tsx +40 -0
- package/src/components/StatsCard/index.ts +1 -0
- package/src/components/Switch/Switch.tsx +128 -0
- package/src/components/Switch/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +49 -0
- package/src/components/Tabs/components/TabsContext.tsx +35 -0
- package/src/components/Tabs/components/TabsList.tsx +98 -0
- package/src/components/Tabs/components/TabsPanels.tsx +26 -0
- package/src/components/Tabs/components/TabsTrigger.tsx +69 -0
- package/src/components/Tabs/index.ts +3 -0
- package/src/components/TagChip/TagChip.tsx +46 -0
- package/src/components/TagChip/index.ts +1 -0
- package/src/components/TagInput/TagInput.tsx +60 -0
- package/src/components/TagInput/index.ts +1 -0
- package/src/components/Text/Text.tsx +35 -0
- package/src/components/Text/index.ts +1 -0
- package/src/components/TextInline/TextInline.tsx +35 -0
- package/src/components/TextInline/index.ts +1 -0
- package/src/components/Textarea/Textarea.tsx +20 -0
- package/src/components/Textarea/index.ts +1 -0
- package/src/components/TimePicker/PeriodSelector.tsx +28 -0
- package/src/components/TimePicker/TimeColumn.tsx +62 -0
- package/src/components/TimePicker/TimePicker.tsx +323 -0
- package/src/components/TimePicker/index.ts +7 -0
- package/src/components/TimePicker/timepicker.hooks.ts +173 -0
- package/src/components/TimePicker/timepicker.types.ts +101 -0
- package/src/components/Toast/Toast.tsx +111 -0
- package/src/components/Toast/index.ts +1 -0
- package/src/components/Toggle/Toggle.tsx +72 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/components/ToggleGroup/ToggleGroup.tsx +14 -0
- package/src/components/ToggleGroup/ToggleGroupContainer.tsx +19 -0
- package/src/components/ToggleGroup/ToggleGroupItem.tsx +51 -0
- package/src/components/ToggleGroup/ToggleGroupMultiple.tsx +61 -0
- package/src/components/ToggleGroup/ToggleGroupSingle.tsx +105 -0
- package/src/components/ToggleGroup/index.ts +2 -0
- package/src/components/ToggleGroup/toggleGroup.types.ts +55 -0
- package/src/components/Tooltip/Tooltip.tsx +66 -0
- package/src/components/Tooltip/components/TooltipBody.tsx +30 -0
- package/src/components/Tooltip/components/TooltipContext.tsx +22 -0
- package/src/components/Tooltip/components/TooltipTrigger.tsx +27 -0
- package/src/components/Tooltip/index.ts +9 -0
- package/src/date-time/arithmetic.ts +101 -0
- package/src/date-time/boundaries.ts +91 -0
- package/src/date-time/compare.ts +85 -0
- package/src/date-time/duration.ts +63 -0
- package/src/date-time/elapsed.ts +18 -0
- package/src/date-time/format.ts +722 -0
- package/src/date-time/index.ts +279 -0
- package/src/date-time/now.ts +70 -0
- package/src/date-time/parse.ts +170 -0
- package/src/date-time/serialize.ts +90 -0
- package/src/date-time/timezone.ts +47 -0
- package/src/date-time/validate.ts +278 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useCollapsible.ts +16 -0
- package/src/hooks/useControllableState.ts +54 -0
- package/src/hooks/useDebouncedCallback.ts +21 -0
- package/src/hooks/useDebouncedState.ts +20 -0
- package/src/hooks/useDeviceFormFactor.ts +49 -0
- package/src/hooks/useLocalStorage.ts +67 -0
- package/src/hooks/useMergedFloatingRef.ts +29 -0
- package/src/hooks/usePresence.ts +127 -0
- package/src/hooks/useStack.ts +99 -0
- package/src/hooks/useTheme.ts +105 -0
- package/src/internal/Slot.tsx +79 -0
- package/src/internal/index.ts +2 -0
- package/src/internal/rovingFocus.ts +39 -0
- package/src/internal/types.ts +10 -0
- package/src/lib/EmptyBoundary.tsx +12 -0
- package/src/lib/ErrorBoundary.tsx +48 -0
- package/src/lib/contexts/ComponentsContext/ComponentsContext.tsx +34 -0
- package/src/lib/contexts/ComponentsContext/index.ts +1 -0
- package/src/lib/contexts/OverlayContext/OverlayContext.tsx +93 -0
- package/src/lib/contexts/OverlayContext/hooks/useModal.tsx +125 -0
- package/src/lib/contexts/OverlayContext/hooks/useToast.ts +80 -0
- package/src/lib/contexts/OverlayContext/index.ts +6 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/utils.ts +10 -0
- package/src/lib/withEmpty.tsx +14 -0
- package/src/lib/withErrorBoundary.tsx +16 -0
- package/src/lib/withSuspense.tsx +16 -0
- package/src/lib/withSuspenseAndErrorBoundary.tsx +26 -0
- package/src/styles/base.css +41 -0
- package/src/theme/index.ts +37 -0
- package/src/theme/palette.json +100 -0
- package/src/theme/palette.schema.json +40 -0
- package/src/theme/schema.ts +122 -0
- package/src/theme/theme.json +349 -0
- package/src/theme/theme.schema.json +876 -0
- package/src/theme/typography.json +7 -0
- package/src/theme/typography.schema.json +36 -0
- package/src/typography/Body.tsx +8 -0
- package/src/typography/Caption.tsx +8 -0
- package/src/typography/Code.tsx +8 -0
- package/src/typography/Display.tsx +8 -0
- package/src/typography/Emphasis.tsx +8 -0
- package/src/typography/Heading.tsx +23 -0
- package/src/typography/Label.tsx +10 -0
- package/src/typography/Lead.tsx +8 -0
- package/src/typography/Mono.tsx +8 -0
- package/src/typography/Muted.tsx +8 -0
- package/src/typography/SectionTitle.tsx +8 -0
- package/src/typography/Subtitle.tsx +8 -0
- package/src/typography/Timestamp.tsx +8 -0
- package/src/typography/index.ts +10 -0
- package/tailwind.config.ts +261 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import type { TimePeriod } from "./timepicker.types";
|
|
3
|
+
|
|
4
|
+
export type PeriodSelectorProps = {
|
|
5
|
+
value: TimePeriod;
|
|
6
|
+
onChange: (period: TimePeriod) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const PeriodSelector = ({ value, onChange }: PeriodSelectorProps) => {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex h-48 w-12 flex-col items-center justify-center gap-1 rounded-lg border border-border-subtle bg-bg-subtle/50">
|
|
12
|
+
{(["AM", "PM"] as const).map((period) => (
|
|
13
|
+
<button
|
|
14
|
+
key={period}
|
|
15
|
+
type="button"
|
|
16
|
+
onClick={() => onChange(period)}
|
|
17
|
+
className={cn(
|
|
18
|
+
`w-10 cursor-pointer rounded-md py-1.5 text-center text-sm transition-colors focus-visible:outline-hidden`,
|
|
19
|
+
`hover:bg-bg-subtle focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset`,
|
|
20
|
+
value === period && "bg-bg-brand text-fg-on-brand hover:bg-bg-brand-strong"
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
{period}
|
|
24
|
+
</button>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
export type TimeColumnProps = {
|
|
5
|
+
values: number[];
|
|
6
|
+
selected: number;
|
|
7
|
+
onSelect: (value: number) => void;
|
|
8
|
+
formatValue?: (value: number) => string;
|
|
9
|
+
disabled?: (value: number) => boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const TimeColumn = ({ values, selected, onSelect, formatValue = (v) => String(v).padStart(2, "0"), disabled }: TimeColumnProps) => {
|
|
13
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
const selectedRef = useRef<HTMLButtonElement>(null);
|
|
15
|
+
|
|
16
|
+
// Scroll selected item into view on mount and when selection changes
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (selectedRef.current && containerRef.current) {
|
|
19
|
+
const container = containerRef.current;
|
|
20
|
+
const item = selectedRef.current;
|
|
21
|
+
const containerRect = container.getBoundingClientRect();
|
|
22
|
+
const itemRect = item.getBoundingClientRect();
|
|
23
|
+
const scrollTop = item.offsetTop - container.offsetTop - containerRect.height / 2 + itemRect.height / 2;
|
|
24
|
+
container.scrollTo({ top: scrollTop, behavior: "smooth" });
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div ref={containerRef} role="listbox" className="h-48 w-14 overflow-y-auto overscroll-contain rounded-lg border border-border-subtle bg-bg-subtle/50">
|
|
30
|
+
{/* Top spacer for centering first items */}
|
|
31
|
+
<div className="h-19" aria-hidden="true" />
|
|
32
|
+
<div className="flex flex-col gap-0.5">
|
|
33
|
+
{values.map((value) => {
|
|
34
|
+
const isSelected = value === selected;
|
|
35
|
+
const isDisabled = disabled?.(value) ?? false;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
key={value}
|
|
40
|
+
ref={isSelected ? selectedRef : undefined}
|
|
41
|
+
type="button"
|
|
42
|
+
role="option"
|
|
43
|
+
aria-selected={isSelected}
|
|
44
|
+
onClick={() => !isDisabled && onSelect(value)}
|
|
45
|
+
disabled={isDisabled}
|
|
46
|
+
className={cn(
|
|
47
|
+
`mx-1 cursor-pointer rounded-md py-1.5 text-center text-sm transition-colors focus-visible:outline-hidden`,
|
|
48
|
+
`hover:bg-bg-subtle focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset`,
|
|
49
|
+
isSelected && "bg-bg-brand text-fg-on-brand hover:bg-bg-brand-strong",
|
|
50
|
+
isDisabled && "cursor-not-allowed opacity-50"
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{formatValue(value)}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</div>
|
|
58
|
+
{/* Bottom spacer for centering last items */}
|
|
59
|
+
<div className="h-19" aria-hidden="true" />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { autoUpdate, FloatingPortal, flip, offset, shift, useClick, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
|
|
2
|
+
import { useControllableState } from "@sixthshift/design-system/hooks";
|
|
3
|
+
import { cn } from "@sixthshift/design-system/utils";
|
|
4
|
+
import { Clock, X } from "lucide-react";
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { useCallback, useId, useState } from "react";
|
|
7
|
+
import { Button } from "../Button";
|
|
8
|
+
import { Separator } from "../Separator";
|
|
9
|
+
import { PeriodSelector } from "./PeriodSelector";
|
|
10
|
+
import { TimeColumn } from "./TimeColumn";
|
|
11
|
+
import {
|
|
12
|
+
formatTimeDisplay,
|
|
13
|
+
generateHours,
|
|
14
|
+
generateMinutes,
|
|
15
|
+
generateSeconds,
|
|
16
|
+
getCurrentTime,
|
|
17
|
+
isTimeDisabled,
|
|
18
|
+
parsedToTemporal,
|
|
19
|
+
temporalTimeToISO,
|
|
20
|
+
temporalToParsed,
|
|
21
|
+
to12Hour,
|
|
22
|
+
to24Hour,
|
|
23
|
+
} from "./timepicker.hooks";
|
|
24
|
+
import type { ParsedTime, TimePeriod, TimePickerProps, TimePresetOption } from "./timepicker.types";
|
|
25
|
+
|
|
26
|
+
export type { TimePickerProps };
|
|
27
|
+
|
|
28
|
+
export const TimePicker = (props: TimePickerProps) => {
|
|
29
|
+
const {
|
|
30
|
+
value,
|
|
31
|
+
defaultValue,
|
|
32
|
+
onChange,
|
|
33
|
+
format = "HH:mm",
|
|
34
|
+
clockFormat = "12h",
|
|
35
|
+
minuteStep = 1,
|
|
36
|
+
minTime,
|
|
37
|
+
maxTime,
|
|
38
|
+
presets,
|
|
39
|
+
placeholder = "Select time",
|
|
40
|
+
name,
|
|
41
|
+
isDisabled = false,
|
|
42
|
+
isInvalid = false,
|
|
43
|
+
className,
|
|
44
|
+
} = props;
|
|
45
|
+
|
|
46
|
+
// Open state
|
|
47
|
+
const [open, setOpen] = useState(false);
|
|
48
|
+
|
|
49
|
+
// Controllable value state (external/committed value)
|
|
50
|
+
const [committedValue, setCommittedValue] = useControllableState({
|
|
51
|
+
value,
|
|
52
|
+
defaultValue,
|
|
53
|
+
onChange,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Parse committed value
|
|
57
|
+
const parsedCommitted = committedValue ? temporalToParsed(committedValue) : undefined;
|
|
58
|
+
|
|
59
|
+
// Draft state (internal while popup is open)
|
|
60
|
+
const [draftHour, setDraftHour] = useState<number>(parsedCommitted?.hour ?? 12);
|
|
61
|
+
const [draftMinute, setDraftMinute] = useState<number>(parsedCommitted?.minute ?? 0);
|
|
62
|
+
const [draftSecond, setDraftSecond] = useState<number>(parsedCommitted?.second ?? 0);
|
|
63
|
+
const [draftPeriod, setDraftPeriod] = useState<TimePeriod>(() => {
|
|
64
|
+
if (parsedCommitted) {
|
|
65
|
+
return to12Hour(parsedCommitted.hour).period;
|
|
66
|
+
}
|
|
67
|
+
return "AM";
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Sync draft when popup opens
|
|
71
|
+
const handleOpenChange = useCallback(
|
|
72
|
+
(newOpen: boolean) => {
|
|
73
|
+
if (newOpen) {
|
|
74
|
+
if (committedValue) {
|
|
75
|
+
const parsed = temporalToParsed(committedValue);
|
|
76
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(parsed.hour).hour12 : parsed.hour);
|
|
77
|
+
setDraftMinute(parsed.minute);
|
|
78
|
+
setDraftSecond(parsed.second);
|
|
79
|
+
setDraftPeriod(to12Hour(parsed.hour).period);
|
|
80
|
+
} else {
|
|
81
|
+
// Default to current time
|
|
82
|
+
const now = getCurrentTime();
|
|
83
|
+
const currentHour = now.hour;
|
|
84
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(currentHour).hour12 : currentHour);
|
|
85
|
+
setDraftMinute(now.minute);
|
|
86
|
+
setDraftSecond(now.second);
|
|
87
|
+
setDraftPeriod(to12Hour(currentHour).period);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
setOpen(newOpen);
|
|
91
|
+
},
|
|
92
|
+
[committedValue, clockFormat]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Get the current draft as a parsed time (24-hour)
|
|
96
|
+
const getDraftTime = useCallback((): ParsedTime => {
|
|
97
|
+
const hour24 = clockFormat === "12h" ? to24Hour(draftHour, draftPeriod) : draftHour;
|
|
98
|
+
return {
|
|
99
|
+
hour: hour24,
|
|
100
|
+
minute: draftMinute,
|
|
101
|
+
second: draftSecond,
|
|
102
|
+
};
|
|
103
|
+
}, [clockFormat, draftHour, draftMinute, draftSecond, draftPeriod]);
|
|
104
|
+
|
|
105
|
+
// Apply changes
|
|
106
|
+
const handleApply = useCallback(() => {
|
|
107
|
+
const parsed = getDraftTime();
|
|
108
|
+
const temporalTime = parsedToTemporal(parsed);
|
|
109
|
+
|
|
110
|
+
// Check if disabled
|
|
111
|
+
if (isTimeDisabled(temporalTime, minTime, maxTime)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setCommittedValue(temporalTime);
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}, [getDraftTime, minTime, maxTime, setCommittedValue]);
|
|
118
|
+
|
|
119
|
+
// Cancel changes
|
|
120
|
+
const handleCancel = useCallback(() => {
|
|
121
|
+
setOpen(false);
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
// Clear value
|
|
125
|
+
const handleClear = useCallback(
|
|
126
|
+
(e: React.MouseEvent) => {
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
setCommittedValue(undefined);
|
|
130
|
+
},
|
|
131
|
+
[setCommittedValue]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Now button
|
|
135
|
+
const handleNow = useCallback(() => {
|
|
136
|
+
const now = getCurrentTime();
|
|
137
|
+
const currentHour = now.hour;
|
|
138
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(currentHour).hour12 : currentHour);
|
|
139
|
+
setDraftMinute(now.minute);
|
|
140
|
+
setDraftSecond(now.second);
|
|
141
|
+
setDraftPeriod(to12Hour(currentHour).period);
|
|
142
|
+
}, [clockFormat]);
|
|
143
|
+
|
|
144
|
+
// Preset selection
|
|
145
|
+
const handlePresetClick = useCallback(
|
|
146
|
+
(preset: TimePresetOption) => {
|
|
147
|
+
const parsed = temporalToParsed(preset.value);
|
|
148
|
+
setDraftHour(clockFormat === "12h" ? to12Hour(parsed.hour).hour12 : parsed.hour);
|
|
149
|
+
setDraftMinute(parsed.minute);
|
|
150
|
+
setDraftSecond(parsed.second);
|
|
151
|
+
setDraftPeriod(to12Hour(parsed.hour).period);
|
|
152
|
+
},
|
|
153
|
+
[clockFormat]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Check if a preset is currently active
|
|
157
|
+
const isPresetActive = useCallback(
|
|
158
|
+
(preset: TimePresetOption): boolean => {
|
|
159
|
+
const draftTime = getDraftTime();
|
|
160
|
+
const presetParsed = temporalToParsed(preset.value);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
draftTime.hour === presetParsed.hour && draftTime.minute === presetParsed.minute && (format === "HH:mm" || draftTime.second === presetParsed.second)
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
[getDraftTime, format]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Floating UI setup
|
|
170
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
171
|
+
open,
|
|
172
|
+
onOpenChange: handleOpenChange,
|
|
173
|
+
placement: "bottom-end",
|
|
174
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
175
|
+
whileElementsMounted: autoUpdate,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const click = useClick(context);
|
|
179
|
+
const dismiss = useDismiss(context);
|
|
180
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);
|
|
181
|
+
|
|
182
|
+
// IDs for accessibility
|
|
183
|
+
const id = useId();
|
|
184
|
+
const contentId = `timepicker-content-${id}`;
|
|
185
|
+
const inputId = `timepicker-input-${id}`;
|
|
186
|
+
|
|
187
|
+
// Display value
|
|
188
|
+
const displayValue = committedValue ? formatTimeDisplay(committedValue, clockFormat, format) : "";
|
|
189
|
+
const hasValue = Boolean(committedValue);
|
|
190
|
+
|
|
191
|
+
// Generate column values
|
|
192
|
+
const hours = generateHours(clockFormat);
|
|
193
|
+
const minutes = generateMinutes(minuteStep);
|
|
194
|
+
const seconds = generateSeconds();
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
{/* Trigger Input */}
|
|
199
|
+
<div className="relative flex items-center">
|
|
200
|
+
<Clock className="pointer-events-none absolute left-3 h-4 w-4 text-fg-subtle" />
|
|
201
|
+
<input
|
|
202
|
+
ref={refs.setReference as React.Ref<HTMLInputElement>}
|
|
203
|
+
id={inputId}
|
|
204
|
+
type="text"
|
|
205
|
+
readOnly
|
|
206
|
+
value={displayValue}
|
|
207
|
+
placeholder={placeholder}
|
|
208
|
+
disabled={isDisabled}
|
|
209
|
+
role="combobox"
|
|
210
|
+
aria-invalid={isInvalid}
|
|
211
|
+
aria-haspopup="dialog"
|
|
212
|
+
aria-expanded={open}
|
|
213
|
+
aria-controls={open ? contentId : undefined}
|
|
214
|
+
className={cn(
|
|
215
|
+
`flex h-9 w-full cursor-pointer rounded-md border border-border-normal bg-transparent py-1 pl-9 text-sm shadow-xs transition-colors placeholder:text-fg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50`,
|
|
216
|
+
hasValue ? "pr-9" : "pr-3",
|
|
217
|
+
isInvalid && "border-border-danger",
|
|
218
|
+
className
|
|
219
|
+
)}
|
|
220
|
+
{...getReferenceProps()}
|
|
221
|
+
/>
|
|
222
|
+
{hasValue && !isDisabled && (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onClick={handleClear}
|
|
226
|
+
className="absolute right-3 rounded-sm p-0.5 text-fg-subtle hover:bg-bg-subtle hover:text-fg-normal focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring"
|
|
227
|
+
aria-label="Clear time"
|
|
228
|
+
>
|
|
229
|
+
<X className="h-3.5 w-3.5" />
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{/* Hidden input for form submission */}
|
|
234
|
+
{name && committedValue && <input type="hidden" name={name} value={temporalTimeToISO(committedValue, format)} />}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Popover Content */}
|
|
238
|
+
{open && (
|
|
239
|
+
<FloatingPortal>
|
|
240
|
+
<div
|
|
241
|
+
ref={refs.setFloating}
|
|
242
|
+
id={contentId}
|
|
243
|
+
role="dialog"
|
|
244
|
+
aria-modal="true"
|
|
245
|
+
style={floatingStyles}
|
|
246
|
+
className="z-popover rounded-xl border border-border-normal bg-bg-normal p-4 shadow-lg"
|
|
247
|
+
{...getFloatingProps()}
|
|
248
|
+
>
|
|
249
|
+
<div className="flex">
|
|
250
|
+
{/* Presets Sidebar */}
|
|
251
|
+
{presets && presets.length > 0 && (
|
|
252
|
+
<div className="mr-3 flex flex-col gap-1 border-border-normal border-r pr-3">
|
|
253
|
+
{presets.map((preset) => (
|
|
254
|
+
<button
|
|
255
|
+
key={preset.label}
|
|
256
|
+
type="button"
|
|
257
|
+
onClick={() => handlePresetClick(preset)}
|
|
258
|
+
className={cn(
|
|
259
|
+
`rounded-md px-3 py-1.5 text-left text-sm transition-colors`,
|
|
260
|
+
`hover:bg-bg-subtle focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset`,
|
|
261
|
+
isPresetActive(preset) && "bg-bg-brand-subtle text-fg-brand"
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
264
|
+
{preset.label}
|
|
265
|
+
</button>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{/* Time Columns */}
|
|
271
|
+
<div className="flex flex-col gap-3">
|
|
272
|
+
{/* Column Headers */}
|
|
273
|
+
<div className="flex items-center justify-center gap-2">
|
|
274
|
+
<div className="w-14 text-center font-medium text-fg-subtle text-xs">Hour</div>
|
|
275
|
+
<div className="w-14 text-center font-medium text-fg-subtle text-xs">Min</div>
|
|
276
|
+
{format === "HH:mm:ss" && <div className="w-14 text-center font-medium text-fg-subtle text-xs">Sec</div>}
|
|
277
|
+
{clockFormat === "12h" && <div className="w-12 text-center font-medium text-fg-subtle text-xs" />}
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{/* Scrollable Columns */}
|
|
281
|
+
<div className="flex items-start gap-2">
|
|
282
|
+
{/* Hours */}
|
|
283
|
+
<TimeColumn
|
|
284
|
+
values={hours}
|
|
285
|
+
selected={draftHour}
|
|
286
|
+
onSelect={setDraftHour}
|
|
287
|
+
formatValue={clockFormat === "12h" ? (v) => String(v) : (v) => String(v).padStart(2, "0")}
|
|
288
|
+
/>
|
|
289
|
+
|
|
290
|
+
{/* Minutes */}
|
|
291
|
+
<TimeColumn values={minutes} selected={draftMinute} onSelect={setDraftMinute} />
|
|
292
|
+
|
|
293
|
+
{/* Seconds (optional) */}
|
|
294
|
+
{format === "HH:mm:ss" && <TimeColumn values={seconds} selected={draftSecond} onSelect={setDraftSecond} />}
|
|
295
|
+
|
|
296
|
+
{/* AM/PM (12-hour only) */}
|
|
297
|
+
{clockFormat === "12h" && <PeriodSelector value={draftPeriod} onChange={setDraftPeriod} />}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<Separator />
|
|
301
|
+
|
|
302
|
+
{/* Footer with Now / Cancel / Apply */}
|
|
303
|
+
<div className="flex items-center justify-between">
|
|
304
|
+
<Button type="button" variant="ghost" size="sm" onClick={handleNow}>
|
|
305
|
+
Now
|
|
306
|
+
</Button>
|
|
307
|
+
<div className="flex gap-2">
|
|
308
|
+
<Button type="button" variant="ghost" size="sm" onClick={handleCancel}>
|
|
309
|
+
Cancel
|
|
310
|
+
</Button>
|
|
311
|
+
<Button type="button" variant="solid" size="sm" onClick={handleApply}>
|
|
312
|
+
Apply
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
</FloatingPortal>
|
|
320
|
+
)}
|
|
321
|
+
</>
|
|
322
|
+
);
|
|
323
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { formatTimePadded, Temporal } from "../../date-time";
|
|
2
|
+
import type { ClockFormat, ParsedTime, TimeFormat, TimePeriod } from "./timepicker.types";
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Temporal ↔ ParsedTime Conversion
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert Temporal.PlainTime to ParsedTime
|
|
10
|
+
*/
|
|
11
|
+
export function temporalToParsed(time: Temporal.PlainTime): ParsedTime {
|
|
12
|
+
return {
|
|
13
|
+
hour: time.hour,
|
|
14
|
+
minute: time.minute,
|
|
15
|
+
second: time.second,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert ParsedTime to Temporal.PlainTime
|
|
21
|
+
*/
|
|
22
|
+
export function parsedToTemporal(parsed: ParsedTime): Temporal.PlainTime {
|
|
23
|
+
return Temporal.PlainTime.from({
|
|
24
|
+
hour: parsed.hour,
|
|
25
|
+
minute: parsed.minute,
|
|
26
|
+
second: parsed.second,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert Temporal.PlainTime to ISO string (for form submission)
|
|
32
|
+
*/
|
|
33
|
+
export function temporalTimeToISO(time: Temporal.PlainTime, format: TimeFormat = "HH:mm"): string {
|
|
34
|
+
if (format === "HH:mm:ss") {
|
|
35
|
+
return time.toString().slice(0, 8); // "HH:mm:ss"
|
|
36
|
+
}
|
|
37
|
+
return time.toString().slice(0, 5); // "HH:mm"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Time Formatting
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Format Temporal.PlainTime for display
|
|
46
|
+
*/
|
|
47
|
+
export function formatTimeDisplay(time: Temporal.PlainTime, clockFormat: ClockFormat = "12h", format: TimeFormat = "HH:mm"): string {
|
|
48
|
+
if (clockFormat === "12h") {
|
|
49
|
+
return formatTimePadded(time);
|
|
50
|
+
}
|
|
51
|
+
// 24h format
|
|
52
|
+
if (format === "HH:mm:ss") {
|
|
53
|
+
return time.toLocaleString("en-US", {
|
|
54
|
+
hour: "2-digit",
|
|
55
|
+
minute: "2-digit",
|
|
56
|
+
second: "2-digit",
|
|
57
|
+
hour12: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return time.toLocaleString("en-US", {
|
|
61
|
+
hour: "2-digit",
|
|
62
|
+
minute: "2-digit",
|
|
63
|
+
hour12: false,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// Time Comparison
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if time a is before time b
|
|
73
|
+
*/
|
|
74
|
+
export function isTimeBefore(a: Temporal.PlainTime, b: Temporal.PlainTime): boolean {
|
|
75
|
+
return Temporal.PlainTime.compare(a, b) < 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if time a is after time b
|
|
80
|
+
*/
|
|
81
|
+
export function isTimeAfter(a: Temporal.PlainTime, b: Temporal.PlainTime): boolean {
|
|
82
|
+
return Temporal.PlainTime.compare(a, b) > 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a time is within min/max bounds
|
|
87
|
+
*/
|
|
88
|
+
export function isTimeDisabled(time: Temporal.PlainTime, minTime?: Temporal.PlainTime, maxTime?: Temporal.PlainTime): boolean {
|
|
89
|
+
if (minTime && isTimeBefore(time, minTime)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (maxTime && isTimeAfter(time, maxTime)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// 12-Hour / 24-Hour Conversion
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert 24-hour hour to 12-hour format
|
|
104
|
+
*/
|
|
105
|
+
export function to12Hour(hour24: number): { hour12: number; period: TimePeriod } {
|
|
106
|
+
if (hour24 === 0) {
|
|
107
|
+
return { hour12: 12, period: "AM" };
|
|
108
|
+
}
|
|
109
|
+
if (hour24 < 12) {
|
|
110
|
+
return { hour12: hour24, period: "AM" };
|
|
111
|
+
}
|
|
112
|
+
if (hour24 === 12) {
|
|
113
|
+
return { hour12: 12, period: "PM" };
|
|
114
|
+
}
|
|
115
|
+
return { hour12: hour24 - 12, period: "PM" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert 12-hour format to 24-hour hour
|
|
120
|
+
*/
|
|
121
|
+
export function to24Hour(hour12: number, period: TimePeriod): number {
|
|
122
|
+
if (period === "AM") {
|
|
123
|
+
return hour12 === 12 ? 0 : hour12;
|
|
124
|
+
}
|
|
125
|
+
return hour12 === 12 ? 12 : hour12 + 12;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// Time Generation
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate array of hours for display
|
|
134
|
+
*/
|
|
135
|
+
export function generateHours(clockFormat: ClockFormat): number[] {
|
|
136
|
+
if (clockFormat === "12h") {
|
|
137
|
+
return [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
138
|
+
}
|
|
139
|
+
return Array.from({ length: 24 }, (_, i) => i);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate array of minutes based on step
|
|
144
|
+
*/
|
|
145
|
+
export function generateMinutes(step: number = 1): number[] {
|
|
146
|
+
const minutes: number[] = [];
|
|
147
|
+
for (let m = 0; m < 60; m += step) {
|
|
148
|
+
minutes.push(m);
|
|
149
|
+
}
|
|
150
|
+
return minutes;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generate array of seconds
|
|
155
|
+
*/
|
|
156
|
+
export function generateSeconds(): number[] {
|
|
157
|
+
return Array.from({ length: 60 }, (_, i) => i);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get current time as Temporal.PlainTime, rounded to minute precision
|
|
162
|
+
* to avoid nanosecond drift causing unnecessary re-renders
|
|
163
|
+
*/
|
|
164
|
+
export function getCurrentTime(): Temporal.PlainTime {
|
|
165
|
+
return Temporal.Now.plainTimeISO().round({ smallestUnit: "minute", roundingMode: "floor" });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Round time to nearest step
|
|
170
|
+
*/
|
|
171
|
+
export function roundToStep(minute: number, step: number): number {
|
|
172
|
+
return Math.round(minute / step) * step;
|
|
173
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimePicker types
|
|
3
|
+
*
|
|
4
|
+
* All time values use Temporal.PlainTime at the component boundary.
|
|
5
|
+
* This provides type safety and semantic clarity about what kind of time is expected.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Temporal } from "../../date-time";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Value Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Time value format for display and serialization
|
|
16
|
+
* - "HH:mm" for minute precision (default)
|
|
17
|
+
* - "HH:mm:ss" for second precision
|
|
18
|
+
*/
|
|
19
|
+
export type TimeFormat = "HH:mm" | "HH:mm:ss";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Clock format for display
|
|
23
|
+
* - "12h" shows AM/PM selector
|
|
24
|
+
* - "24h" shows 00-23 hours
|
|
25
|
+
*/
|
|
26
|
+
export type ClockFormat = "12h" | "24h";
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Preset Options
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export type TimePresetOption = {
|
|
33
|
+
label: string;
|
|
34
|
+
value: Temporal.PlainTime;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Props
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
export type TimePickerProps = {
|
|
42
|
+
/** Controlled value */
|
|
43
|
+
value?: Temporal.PlainTime | undefined;
|
|
44
|
+
|
|
45
|
+
/** Default value for uncontrolled mode */
|
|
46
|
+
defaultValue?: Temporal.PlainTime | undefined;
|
|
47
|
+
|
|
48
|
+
/** Called when time changes */
|
|
49
|
+
onChange?: (time: Temporal.PlainTime | undefined) => void;
|
|
50
|
+
|
|
51
|
+
/** Time format for the value (affects seconds visibility) */
|
|
52
|
+
format?: TimeFormat;
|
|
53
|
+
|
|
54
|
+
/** Clock display format */
|
|
55
|
+
clockFormat?: ClockFormat;
|
|
56
|
+
|
|
57
|
+
/** Minute step interval (default: 1) */
|
|
58
|
+
minuteStep?: 1 | 5 | 10 | 15 | 30;
|
|
59
|
+
|
|
60
|
+
/** Minimum selectable time */
|
|
61
|
+
minTime?: Temporal.PlainTime;
|
|
62
|
+
|
|
63
|
+
/** Maximum selectable time */
|
|
64
|
+
maxTime?: Temporal.PlainTime;
|
|
65
|
+
|
|
66
|
+
/** Preset options (shown in sidebar) */
|
|
67
|
+
presets?: TimePresetOption[];
|
|
68
|
+
|
|
69
|
+
/** Placeholder text */
|
|
70
|
+
placeholder?: string;
|
|
71
|
+
|
|
72
|
+
/** Input name for form submission */
|
|
73
|
+
name?: string;
|
|
74
|
+
|
|
75
|
+
/** Whether the picker is disabled */
|
|
76
|
+
isDisabled?: boolean;
|
|
77
|
+
|
|
78
|
+
/** Error state */
|
|
79
|
+
isInvalid?: boolean;
|
|
80
|
+
|
|
81
|
+
/** Additional class name for the trigger input */
|
|
82
|
+
className?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// Internal Types
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parsed time value for internal use
|
|
91
|
+
*/
|
|
92
|
+
export type ParsedTime = {
|
|
93
|
+
hour: number; // 0-23
|
|
94
|
+
minute: number; // 0-59
|
|
95
|
+
second: number; // 0-59
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* AM/PM period
|
|
100
|
+
*/
|
|
101
|
+
export type TimePeriod = "AM" | "PM";
|