premium-ds 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 +113 -0
- package/dist/alert.d.ts +31 -0
- package/dist/alert.js +6 -0
- package/dist/alert.js.map +1 -0
- package/dist/avatar-group.d.ts +13 -0
- package/dist/avatar-group.js +3 -0
- package/dist/avatar-group.js.map +1 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +3 -0
- package/dist/avatar.js.map +1 -0
- package/dist/badge.d.ts +23 -0
- package/dist/badge.js +3 -0
- package/dist/badge.js.map +1 -0
- package/dist/button.d.ts +20 -0
- package/dist/button.js +3 -0
- package/dist/button.js.map +1 -0
- package/dist/checkbox.d.ts +25 -0
- package/dist/checkbox.js +3 -0
- package/dist/checkbox.js.map +1 -0
- package/dist/chunk-2OWHZ4JT.js +36 -0
- package/dist/chunk-2OWHZ4JT.js.map +1 -0
- package/dist/chunk-34SIXSYL.js +64 -0
- package/dist/chunk-34SIXSYL.js.map +1 -0
- package/dist/chunk-37O2ZXD6.js +55 -0
- package/dist/chunk-37O2ZXD6.js.map +1 -0
- package/dist/chunk-4AZL76UJ.js +89 -0
- package/dist/chunk-4AZL76UJ.js.map +1 -0
- package/dist/chunk-4HSCN5TZ.js +86 -0
- package/dist/chunk-4HSCN5TZ.js.map +1 -0
- package/dist/chunk-5DDOOT33.js +258 -0
- package/dist/chunk-5DDOOT33.js.map +1 -0
- package/dist/chunk-5FVHWIMY.js +117 -0
- package/dist/chunk-5FVHWIMY.js.map +1 -0
- package/dist/chunk-5K6KRJGX.js +147 -0
- package/dist/chunk-5K6KRJGX.js.map +1 -0
- package/dist/chunk-5PQMQBQC.js +74 -0
- package/dist/chunk-5PQMQBQC.js.map +1 -0
- package/dist/chunk-7OCTVQ7C.js +95 -0
- package/dist/chunk-7OCTVQ7C.js.map +1 -0
- package/dist/chunk-7OPMOET7.js +39 -0
- package/dist/chunk-7OPMOET7.js.map +1 -0
- package/dist/chunk-BXXS7YRC.js +270 -0
- package/dist/chunk-BXXS7YRC.js.map +1 -0
- package/dist/chunk-CV2Q4YXX.js +272 -0
- package/dist/chunk-CV2Q4YXX.js.map +1 -0
- package/dist/chunk-EIMMDWIW.js +282 -0
- package/dist/chunk-EIMMDWIW.js.map +1 -0
- package/dist/chunk-EZ2CWTBE.js +230 -0
- package/dist/chunk-EZ2CWTBE.js.map +1 -0
- package/dist/chunk-FGHDG3Y4.js +89 -0
- package/dist/chunk-FGHDG3Y4.js.map +1 -0
- package/dist/chunk-FPP2XLKX.js +127 -0
- package/dist/chunk-FPP2XLKX.js.map +1 -0
- package/dist/chunk-G6OY35DI.js +295 -0
- package/dist/chunk-G6OY35DI.js.map +1 -0
- package/dist/chunk-H6KWJNOE.js +65 -0
- package/dist/chunk-H6KWJNOE.js.map +1 -0
- package/dist/chunk-HGILYGY3.js +45 -0
- package/dist/chunk-HGILYGY3.js.map +1 -0
- package/dist/chunk-I3BCB4Z5.js +88 -0
- package/dist/chunk-I3BCB4Z5.js.map +1 -0
- package/dist/chunk-KBWNUUWM.js +582 -0
- package/dist/chunk-KBWNUUWM.js.map +1 -0
- package/dist/chunk-KN7JFAZ6.js +113 -0
- package/dist/chunk-KN7JFAZ6.js.map +1 -0
- package/dist/chunk-MEF7PI6U.js +16 -0
- package/dist/chunk-MEF7PI6U.js.map +1 -0
- package/dist/chunk-NKGMQL6I.js +310 -0
- package/dist/chunk-NKGMQL6I.js.map +1 -0
- package/dist/chunk-NMFQRGLL.js +127 -0
- package/dist/chunk-NMFQRGLL.js.map +1 -0
- package/dist/chunk-OUBWD6CX.js +433 -0
- package/dist/chunk-OUBWD6CX.js.map +1 -0
- package/dist/chunk-PFNXVBLU.js +96 -0
- package/dist/chunk-PFNXVBLU.js.map +1 -0
- package/dist/chunk-PUPZ4HME.js +165 -0
- package/dist/chunk-PUPZ4HME.js.map +1 -0
- package/dist/chunk-QFS52OK5.js +690 -0
- package/dist/chunk-QFS52OK5.js.map +1 -0
- package/dist/chunk-QNC6O3PG.js +45 -0
- package/dist/chunk-QNC6O3PG.js.map +1 -0
- package/dist/chunk-QUHOXWBK.js +82 -0
- package/dist/chunk-QUHOXWBK.js.map +1 -0
- package/dist/chunk-UIQGSTBJ.js +106 -0
- package/dist/chunk-UIQGSTBJ.js.map +1 -0
- package/dist/chunk-UJQKVP6V.js +193 -0
- package/dist/chunk-UJQKVP6V.js.map +1 -0
- package/dist/chunk-VVPGEAC6.js +11 -0
- package/dist/chunk-VVPGEAC6.js.map +1 -0
- package/dist/chunk-XA3T5KWA.js +58 -0
- package/dist/chunk-XA3T5KWA.js.map +1 -0
- package/dist/chunk-YSHJHSJM.js +19 -0
- package/dist/chunk-YSHJHSJM.js.map +1 -0
- package/dist/chunk-YVHOAVSM.js +182 -0
- package/dist/chunk-YVHOAVSM.js.map +1 -0
- package/dist/collapse.d.ts +16 -0
- package/dist/collapse.js +3 -0
- package/dist/collapse.js.map +1 -0
- package/dist/count-badge.d.ts +11 -0
- package/dist/count-badge.js +4 -0
- package/dist/count-badge.js.map +1 -0
- package/dist/date-field.d.ts +39 -0
- package/dist/date-field.js +8 -0
- package/dist/date-field.js.map +1 -0
- package/dist/date-range-field.d.ts +30 -0
- package/dist/date-range-field.js +8 -0
- package/dist/date-range-field.js.map +1 -0
- package/dist/datetime-field.d.ts +28 -0
- package/dist/datetime-field.js +10 -0
- package/dist/datetime-field.js.map +1 -0
- package/dist/dialog.d.ts +26 -0
- package/dist/dialog.js +7 -0
- package/dist/dialog.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/motion-tokens.d.ts +29 -0
- package/dist/motion-tokens.js +3 -0
- package/dist/motion-tokens.js.map +1 -0
- package/dist/multi-select.d.ts +25 -0
- package/dist/multi-select.js +7 -0
- package/dist/multi-select.js.map +1 -0
- package/dist/number-field.d.ts +24 -0
- package/dist/number-field.js +4 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +20 -0
- package/dist/otp-field.js +3 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/overlay.d.ts +31 -0
- package/dist/overlay.js +4 -0
- package/dist/overlay.js.map +1 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.js +5 -0
- package/dist/pagination.js.map +1 -0
- package/dist/radio-group.d.ts +46 -0
- package/dist/radio-group.js +6 -0
- package/dist/radio-group.js.map +1 -0
- package/dist/select-core-SAyS-8w0.d.ts +16 -0
- package/dist/select.d.ts +27 -0
- package/dist/select.js +7 -0
- package/dist/select.js.map +1 -0
- package/dist/status-badge.d.ts +17 -0
- package/dist/status-badge.js +5 -0
- package/dist/status-badge.js.map +1 -0
- package/dist/table.d.ts +65 -0
- package/dist/table.js +5 -0
- package/dist/table.js.map +1 -0
- package/dist/tabs.d.ts +44 -0
- package/dist/tabs.js +5 -0
- package/dist/tabs.js.map +1 -0
- package/dist/tag.d.ts +28 -0
- package/dist/tag.js +5 -0
- package/dist/tag.js.map +1 -0
- package/dist/text-field.d.ts +30 -0
- package/dist/text-field.js +6 -0
- package/dist/text-field.js.map +1 -0
- package/dist/textarea.d.ts +33 -0
- package/dist/textarea.js +5 -0
- package/dist/textarea.js.map +1 -0
- package/dist/time-field.d.ts +27 -0
- package/dist/time-field.js +6 -0
- package/dist/time-field.js.map +1 -0
- package/dist/toast-store.d.ts +75 -0
- package/dist/toast-store.js +3 -0
- package/dist/toast-store.js.map +1 -0
- package/dist/toast.d.ts +3 -0
- package/dist/toast.js +6 -0
- package/dist/toast.js.map +1 -0
- package/dist/toggle-tag.d.ts +24 -0
- package/dist/toggle-tag.js +4 -0
- package/dist/toggle-tag.js.map +1 -0
- package/dist/toggle.d.ts +21 -0
- package/dist/toggle.js +3 -0
- package/dist/toggle.js.map +1 -0
- package/dist/tooltip.d.ts +27 -0
- package/dist/tooltip.js +4 -0
- package/dist/tooltip.js.map +1 -0
- package/llms.txt +165 -0
- package/package.json +205 -0
- package/src/components/alert/Alert.tsx +118 -0
- package/src/components/alert/alert.css +136 -0
- package/src/components/avatar/Avatar.tsx +128 -0
- package/src/components/avatar/AvatarGroup.tsx +50 -0
- package/src/components/avatar/avatar.css +200 -0
- package/src/components/badge/Badge.tsx +66 -0
- package/src/components/badge/CountBadge.tsx +46 -0
- package/src/components/badge/StatusBadge.tsx +132 -0
- package/src/components/badge/badge.css +243 -0
- package/src/components/button/Button.tsx +68 -0
- package/src/components/button/button.css +222 -0
- package/src/components/checkbox/Checkbox.tsx +90 -0
- package/src/components/checkbox/checkbox.css +179 -0
- package/src/components/date-picker/DateField.tsx +362 -0
- package/src/components/date-picker/DateRangeField.tsx +533 -0
- package/src/components/date-picker/DateTimeField.tsx +177 -0
- package/src/components/date-picker/TimeField.tsx +100 -0
- package/src/components/date-picker/date-picker.css +591 -0
- package/src/components/date-picker/date-utils.ts +55 -0
- package/src/components/date-picker/field-shell.tsx +78 -0
- package/src/components/date-picker/glide-pill.tsx +81 -0
- package/src/components/date-picker/time-core.tsx +305 -0
- package/src/components/dialog/Dialog.tsx +181 -0
- package/src/components/dialog/dialog.css +170 -0
- package/src/components/glass/glass.css +100 -0
- package/src/components/icon/Icon.tsx +76 -0
- package/src/components/icon/IconSlot.tsx +11 -0
- package/src/components/icon/icon.css +33 -0
- package/src/components/input/NumberField.tsx +117 -0
- package/src/components/input/OtpField.tsx +118 -0
- package/src/components/input/TextField.tsx +123 -0
- package/src/components/input/input.css +335 -0
- package/src/components/motion/Collapse.tsx +33 -0
- package/src/components/motion/collapse.css +41 -0
- package/src/components/overlay/Overlay.tsx +239 -0
- package/src/components/overlay/overlay-core.tsx +565 -0
- package/src/components/overlay/overlay.css +119 -0
- package/src/components/overlay/sheet-drag.tsx +146 -0
- package/src/components/pagination/Pagination.tsx +140 -0
- package/src/components/pagination/pagination.css +48 -0
- package/src/components/radio-group/RadioGroup.tsx +182 -0
- package/src/components/radio-group/radio-group.css +277 -0
- package/src/components/select/MultiSelect.tsx +251 -0
- package/src/components/select/Select.tsx +235 -0
- package/src/components/select/select-core.tsx +417 -0
- package/src/components/select/select.css +386 -0
- package/src/components/table/Table.tsx +433 -0
- package/src/components/table/table.css +348 -0
- package/src/components/tabs/Tabs.tsx +371 -0
- package/src/components/tabs/tabs.css +228 -0
- package/src/components/tag/Tag.tsx +145 -0
- package/src/components/tag/ToggleTag.tsx +125 -0
- package/src/components/tag/tag.css +248 -0
- package/src/components/textarea/Textarea.tsx +197 -0
- package/src/components/textarea/textarea.css +219 -0
- package/src/components/toast/Toast.tsx +349 -0
- package/src/components/toast/toast-store.ts +266 -0
- package/src/components/toast/toast.css +233 -0
- package/src/components/toggle/Toggle.tsx +94 -0
- package/src/components/toggle/toggle.css +152 -0
- package/src/components/tooltip/Tooltip.tsx +365 -0
- package/src/components/tooltip/tooltip.css +86 -0
- package/src/index.ts +42 -0
- package/src/styles.css +39 -0
- package/src/tokens/avatar.css +20 -0
- package/src/tokens/color.css +56 -0
- package/src/tokens/elevation.css +20 -0
- package/src/tokens/fonts.css +3 -0
- package/src/tokens/glass.css +21 -0
- package/src/tokens/icons.css +7 -0
- package/src/tokens/layers.css +6 -0
- package/src/tokens/motion-tokens.ts +72 -0
- package/src/tokens/motion.css +49 -0
- package/src/tokens/radius.css +11 -0
- package/src/tokens/semantic.css +75 -0
- package/src/tokens/spacing.css +26 -0
- package/src/tokens/typography.css +54 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/* DateRangeField - DateField's sibling for { start, end }: a Linear/Notion two-click range; the Overlay switches popoverandsheet by viewport. */
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { motion } from 'motion/react';
|
|
7
|
+
import { UIMotion } from '../../tokens/motion-tokens';
|
|
8
|
+
import { Icon } from '../icon/Icon';
|
|
9
|
+
import { Overlay } from '../overlay/Overlay';
|
|
10
|
+
import { FieldShell, useControllable } from './field-shell';
|
|
11
|
+
import { GlidePill, useGlide } from './glide-pill';
|
|
12
|
+
import {
|
|
13
|
+
MONTHS as DRP_MONTHS,
|
|
14
|
+
DOW as DRP_DOW,
|
|
15
|
+
pad as drpPad,
|
|
16
|
+
key as drpKey,
|
|
17
|
+
parse as drpParse,
|
|
18
|
+
today as drpToday,
|
|
19
|
+
add as drpAdd,
|
|
20
|
+
col as drpCol,
|
|
21
|
+
grid as drpGrid,
|
|
22
|
+
tzLabel as drpTzLabel,
|
|
23
|
+
} from './date-utils';
|
|
24
|
+
|
|
25
|
+
const { useState, useRef, useEffect } = React;
|
|
26
|
+
const drpMotion = motion;
|
|
27
|
+
const drpSM = UIMotion;
|
|
28
|
+
|
|
29
|
+
/** A date range as wall-clock 'YYYY-MM-DD' endpoints, inclusive. */
|
|
30
|
+
export interface DateRange {
|
|
31
|
+
start: string;
|
|
32
|
+
end: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DrpPreset {
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
start: string;
|
|
39
|
+
end: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function drpDisplay(key: string, withYear: boolean): string {
|
|
43
|
+
const d = drpParse(key);
|
|
44
|
+
const mon = DRP_MONTHS[d.getMonth()].slice(0, 3);
|
|
45
|
+
return mon + ' ' + drpPad(d.getDate()) + (withYear ? ', ' + d.getFullYear() : '');
|
|
46
|
+
}
|
|
47
|
+
function drpRangeText(start: string, end: string): string {
|
|
48
|
+
const sameYear = drpParse(start).getFullYear() === drpParse(end).getFullYear();
|
|
49
|
+
const curYear = new Date().getFullYear();
|
|
50
|
+
const startYr = !sameYear || drpParse(start).getFullYear() !== curYear;
|
|
51
|
+
const endYr = drpParse(end).getFullYear() !== curYear;
|
|
52
|
+
return drpDisplay(start, startYr) + ' - ' + drpDisplay(end, endYr);
|
|
53
|
+
}
|
|
54
|
+
const drpDays = (start: string, end: string): number =>
|
|
55
|
+
Math.round((+drpParse(end) - +drpParse(start)) / 86400000) + 1;
|
|
56
|
+
|
|
57
|
+
function drpPresets(): DrpPreset[] {
|
|
58
|
+
const t = drpToday();
|
|
59
|
+
const now = new Date();
|
|
60
|
+
const firstThis = drpKey(new Date(now.getFullYear(), now.getMonth(), 1));
|
|
61
|
+
const firstPrev = drpKey(new Date(now.getFullYear(), now.getMonth() - 1, 1));
|
|
62
|
+
const lastPrev = drpKey(new Date(now.getFullYear(), now.getMonth(), 0));
|
|
63
|
+
const jan1 = drpKey(new Date(now.getFullYear(), 0, 1));
|
|
64
|
+
return [
|
|
65
|
+
{ id: 'today', label: 'Today', start: t, end: t },
|
|
66
|
+
{ id: 'yest', label: 'Yesterday', start: drpAdd(t, -1), end: drpAdd(t, -1) },
|
|
67
|
+
{ id: '7d', label: 'Last 7 days', start: drpAdd(t, -6), end: t },
|
|
68
|
+
{ id: '30d', label: 'Last 30 days', start: drpAdd(t, -29), end: t },
|
|
69
|
+
{ id: 'mtd', label: 'This month', start: firstThis, end: t },
|
|
70
|
+
{ id: 'lastm', label: 'Last month', start: firstPrev, end: lastPrev },
|
|
71
|
+
{ id: '90d', label: 'Last 90 days', start: drpAdd(t, -89), end: t },
|
|
72
|
+
{ id: 'ytd', label: 'Year to date', start: jan1, end: t },
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function useResponsiveOverlayMode(query?: string): boolean {
|
|
77
|
+
const q = query || '(max-width: 640px)';
|
|
78
|
+
const [narrow, setNarrow] = useState(() =>
|
|
79
|
+
typeof matchMedia === 'function' ? matchMedia(q).matches : false,
|
|
80
|
+
);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const mq = matchMedia(q);
|
|
83
|
+
const fn = (e: MediaQueryListEvent) => setNarrow(e.matches);
|
|
84
|
+
mq.addEventListener('change', fn);
|
|
85
|
+
setNarrow(mq.matches);
|
|
86
|
+
return () => mq.removeEventListener('change', fn);
|
|
87
|
+
}, [q]);
|
|
88
|
+
return narrow;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface DrpPanelProps {
|
|
92
|
+
value: DateRange | null;
|
|
93
|
+
commit: (value: DateRange) => void;
|
|
94
|
+
close: () => void;
|
|
95
|
+
min?: string;
|
|
96
|
+
max?: string;
|
|
97
|
+
timezone?: string;
|
|
98
|
+
label?: string;
|
|
99
|
+
months: number;
|
|
100
|
+
layout: 'popover' | 'sheet';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* the popover/sheet panel, mounted only while open */
|
|
104
|
+
function DrpPanel({
|
|
105
|
+
value,
|
|
106
|
+
commit,
|
|
107
|
+
close,
|
|
108
|
+
min,
|
|
109
|
+
max,
|
|
110
|
+
timezone,
|
|
111
|
+
label,
|
|
112
|
+
months,
|
|
113
|
+
layout,
|
|
114
|
+
}: DrpPanelProps) {
|
|
115
|
+
const seedKey = (value && value.start) || drpToday();
|
|
116
|
+
const seed = drpParse(seedKey);
|
|
117
|
+
const [view, setView] = useState<{ y: number; m: number }>({
|
|
118
|
+
y: seed.getFullYear(),
|
|
119
|
+
m: seed.getMonth(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/* anchor !== null - a range is being dragged open (awaiting the 2nd pick) */
|
|
123
|
+
const [anchor, setAnchor] = useState<string | null>(null);
|
|
124
|
+
const [hoverKey, setHoverKey] = useState<string | null>(null);
|
|
125
|
+
const [focusKey, setFocusKey] = useState<string>(seedKey);
|
|
126
|
+
|
|
127
|
+
const daysRef = useRef<HTMLDivElement>(null);
|
|
128
|
+
const presetsRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
const pendingFocusRef = useRef(false);
|
|
130
|
+
|
|
131
|
+
/* gliding hover pill per zone (see glide-pill); hoverKey drives the preview band, not the pill - the pill idles once an anchor is set. */
|
|
132
|
+
const grid = useGlide(daysRef);
|
|
133
|
+
const presetGlide = useGlide(presetsRef);
|
|
134
|
+
|
|
135
|
+
const inBounds = (key: string): boolean => (!min || key >= min) && (!max || key <= max);
|
|
136
|
+
|
|
137
|
+
/* effective lo/hi for PAINT: mid-selection - anchorandhover; else committed */
|
|
138
|
+
let lo: string | null = null,
|
|
139
|
+
hi: string | null = null,
|
|
140
|
+
provisional = false;
|
|
141
|
+
if (anchor) {
|
|
142
|
+
const other = hoverKey || anchor;
|
|
143
|
+
lo = anchor < other ? anchor : other;
|
|
144
|
+
hi = anchor < other ? other : anchor;
|
|
145
|
+
provisional = true;
|
|
146
|
+
} else if (value && value.start && value.end) {
|
|
147
|
+
lo = value.start;
|
|
148
|
+
hi = value.end;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function pickDay(key: string) {
|
|
152
|
+
if (!inBounds(key)) return;
|
|
153
|
+
if (!anchor) {
|
|
154
|
+
setAnchor(key);
|
|
155
|
+
setHoverKey(key);
|
|
156
|
+
} else {
|
|
157
|
+
const start = anchor < key ? anchor : key;
|
|
158
|
+
const end = anchor < key ? key : anchor;
|
|
159
|
+
setAnchor(null);
|
|
160
|
+
setHoverKey(null);
|
|
161
|
+
commit({ start: start, end: end });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function applyPreset(p: DrpPreset) {
|
|
166
|
+
setAnchor(null);
|
|
167
|
+
setHoverKey(null);
|
|
168
|
+
const d = drpParse(p.start);
|
|
169
|
+
setView({ y: d.getFullYear(), m: d.getMonth() });
|
|
170
|
+
setFocusKey(p.start);
|
|
171
|
+
commit({ start: p.start, end: p.end });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function nav(dir: number) {
|
|
175
|
+
setView((v) => {
|
|
176
|
+
const d = new Date(v.y, v.m + dir, 1);
|
|
177
|
+
return { y: d.getFullYear(), m: d.getMonth() };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* directional month slide via CSS keyframe on a keyed grid remount - Motion animate() fights the layoutId caps; direction from the viewIdx delta, the first mount gets none. */
|
|
182
|
+
const viewIdx = view.y * 12 + view.m;
|
|
183
|
+
const prevViewIdxRef = useRef(viewIdx);
|
|
184
|
+
const navDir = prevViewIdxRef.current === viewIdx ? 0 : viewIdx > prevViewIdxRef.current ? 1 : -1;
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
prevViewIdxRef.current = viewIdx;
|
|
187
|
+
}, [viewIdx]);
|
|
188
|
+
|
|
189
|
+
/* seed focus into the grid on open (panel portals to <body>) */
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
const el = daysRef.current;
|
|
192
|
+
if (!el) return;
|
|
193
|
+
const btn =
|
|
194
|
+
el.querySelector('[data-key="' + focusKey + '"]:not(:disabled)') ||
|
|
195
|
+
el.querySelector('.dtp__day:not(:disabled)');
|
|
196
|
+
if (btn) (btn as HTMLElement).focus({ preventScroll: true });
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!pendingFocusRef.current || !daysRef.current) return;
|
|
201
|
+
pendingFocusRef.current = false;
|
|
202
|
+
const btn = daysRef.current.querySelector('[data-key="' + focusKey + '"]');
|
|
203
|
+
if (btn) (btn as HTMLElement).focus({ preventScroll: true });
|
|
204
|
+
}, [focusKey]);
|
|
205
|
+
|
|
206
|
+
function moveFocus(deltaDays: number) {
|
|
207
|
+
const key = drpAdd(focusKey, deltaDays);
|
|
208
|
+
const d = drpParse(key);
|
|
209
|
+
pendingFocusRef.current = true;
|
|
210
|
+
setFocusKey(key);
|
|
211
|
+
if (anchor) setHoverKey(key);
|
|
212
|
+
const first = months === 2 ? view.y * 12 + view.m : view.y * 12 + view.m;
|
|
213
|
+
const idx = d.getFullYear() * 12 + d.getMonth();
|
|
214
|
+
const last = first + (months - 1);
|
|
215
|
+
if (idx < first) setView({ y: d.getFullYear(), m: d.getMonth() });
|
|
216
|
+
else if (idx > last) {
|
|
217
|
+
const back = new Date(d.getFullYear(), d.getMonth() - (months - 1), 1);
|
|
218
|
+
setView({ y: back.getFullYear(), m: back.getMonth() });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function onGridKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
|
222
|
+
const k = e.key;
|
|
223
|
+
if (k === 'ArrowLeft') moveFocus(-1);
|
|
224
|
+
else if (k === 'ArrowRight') moveFocus(1);
|
|
225
|
+
else if (k === 'ArrowUp') moveFocus(-7);
|
|
226
|
+
else if (k === 'ArrowDown') moveFocus(7);
|
|
227
|
+
else if (k === 'PageUp') {
|
|
228
|
+
nav(-1);
|
|
229
|
+
return;
|
|
230
|
+
} else if (k === 'PageDown') {
|
|
231
|
+
nav(1);
|
|
232
|
+
return;
|
|
233
|
+
} else if (k === 'Enter' || k === ' ') pickDay(focusKey);
|
|
234
|
+
else if (k === 'Escape' && anchor) {
|
|
235
|
+
setAnchor(null);
|
|
236
|
+
setHoverKey(null);
|
|
237
|
+
} else return;
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const todayKey = drpToday();
|
|
242
|
+
function renderDay(d: Date, viewMonth: number) {
|
|
243
|
+
const key = drpKey(d);
|
|
244
|
+
const out = d.getMonth() !== viewMonth;
|
|
245
|
+
const disabled = !inBounds(key);
|
|
246
|
+
const col = drpCol(d);
|
|
247
|
+
|
|
248
|
+
const isLo = lo && key === lo;
|
|
249
|
+
const isHi = hi && key === hi;
|
|
250
|
+
const single = lo && hi && lo === hi;
|
|
251
|
+
const inRange = lo && hi && key >= lo && key <= hi;
|
|
252
|
+
/* the provisional (un-committed) end gets the outlined ghost cap; the anchor stays solid. */
|
|
253
|
+
const loGhost = provisional && isLo && lo !== anchor;
|
|
254
|
+
const hiGhost = provisional && isHi && !single && hi !== anchor;
|
|
255
|
+
|
|
256
|
+
const band = inRange && !single;
|
|
257
|
+
const extL = band && !isLo && col !== 0;
|
|
258
|
+
const extR = band && !isHi && col !== 6;
|
|
259
|
+
|
|
260
|
+
/* a boundary date shows in two cells (two-month view); the cap is a shared-layoutId node, so render it in exactly one - the in-month cell. */
|
|
261
|
+
const capHere = months === 1 || !out;
|
|
262
|
+
const loCap = isLo && capHere;
|
|
263
|
+
const hiCap = isHi && !single && capHere;
|
|
264
|
+
|
|
265
|
+
const cls = ['dtp__day'];
|
|
266
|
+
if (out) cls.push('is-out');
|
|
267
|
+
if (inRange) cls.push('is-in-range');
|
|
268
|
+
if ((loGhost || hiGhost) && capHere) cls.push('is-cap-ghost');
|
|
269
|
+
else if (loCap || hiCap) cls.push('is-cap');
|
|
270
|
+
|
|
271
|
+
const bandCls = ['drp__band'];
|
|
272
|
+
if (extL) bandCls.push('drp__band--extL');
|
|
273
|
+
if (extR) bandCls.push('drp__band--extR');
|
|
274
|
+
if (!extL) bandCls.push('drp__band--roundL');
|
|
275
|
+
if (!extR) bandCls.push('drp__band--roundR');
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<button
|
|
279
|
+
key={key}
|
|
280
|
+
type="button"
|
|
281
|
+
role="gridcell"
|
|
282
|
+
data-key={key}
|
|
283
|
+
className={cls.join(' ')}
|
|
284
|
+
disabled={disabled}
|
|
285
|
+
tabIndex={key === focusKey ? 0 : -1}
|
|
286
|
+
aria-label={DRP_MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear()}
|
|
287
|
+
aria-selected={isLo || isHi || undefined}
|
|
288
|
+
onClick={() => pickDay(key)}
|
|
289
|
+
onPointerEnter={(e) => {
|
|
290
|
+
setHoverKey(key);
|
|
291
|
+
if (!anchor && !disabled) grid.enter(e.currentTarget);
|
|
292
|
+
else grid.leave();
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
{band ? <span className={bandCls.join(' ')} aria-hidden="true"></span> : null}
|
|
296
|
+
{loCap ? (
|
|
297
|
+
<drpMotion.span
|
|
298
|
+
className={'drp__cap' + (loGhost ? ' drp__cap--ghost' : '')}
|
|
299
|
+
layoutId="drp-cap-lo"
|
|
300
|
+
transition={drpSM.t.settle}
|
|
301
|
+
aria-hidden="true"
|
|
302
|
+
></drpMotion.span>
|
|
303
|
+
) : null}
|
|
304
|
+
{hiCap ? (
|
|
305
|
+
<drpMotion.span
|
|
306
|
+
className={'drp__cap' + (hiGhost ? ' drp__cap--ghost' : '')}
|
|
307
|
+
layoutId="drp-cap-hi"
|
|
308
|
+
transition={drpSM.t.settle}
|
|
309
|
+
aria-hidden="true"
|
|
310
|
+
></drpMotion.span>
|
|
311
|
+
) : null}
|
|
312
|
+
<span className="dtp__num">{d.getDate()}</span>
|
|
313
|
+
{key === todayKey ? <span className="dtp__dot" aria-hidden="true"></span> : null}
|
|
314
|
+
</button>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function renderMonth(offset: number, withPrev: boolean, withNext: boolean) {
|
|
319
|
+
const base = new Date(view.y, view.m + offset, 1);
|
|
320
|
+
const y = base.getFullYear(),
|
|
321
|
+
m = base.getMonth();
|
|
322
|
+
const cells = drpGrid(y, m);
|
|
323
|
+
const prevEnd = drpKey(new Date(y, m, 0));
|
|
324
|
+
const nextStart = drpKey(new Date(y, m + 1, 1));
|
|
325
|
+
return (
|
|
326
|
+
<div className="dtp__cal" key={offset}>
|
|
327
|
+
<div className={'drp__mhead' + (withNext && !withPrev ? ' drp__mhead--right' : '')}>
|
|
328
|
+
{withPrev ? (
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
className="dtp__nav"
|
|
332
|
+
aria-label="Previous month"
|
|
333
|
+
disabled={min && prevEnd < min}
|
|
334
|
+
onClick={() => nav(-1)}
|
|
335
|
+
>
|
|
336
|
+
<Icon name="caret-left" size="sm" />
|
|
337
|
+
</button>
|
|
338
|
+
) : null}
|
|
339
|
+
<span className="dtp__month">
|
|
340
|
+
{DRP_MONTHS[m]} <span className="dtp__year">{y}</span>
|
|
341
|
+
</span>
|
|
342
|
+
{withNext ? (
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
className="dtp__nav"
|
|
346
|
+
aria-label="Next month"
|
|
347
|
+
disabled={max && nextStart > max}
|
|
348
|
+
onClick={() => nav(1)}
|
|
349
|
+
>
|
|
350
|
+
<Icon name="caret-right" size="sm" />
|
|
351
|
+
</button>
|
|
352
|
+
) : null}
|
|
353
|
+
</div>
|
|
354
|
+
<div className="dtp__dow" aria-hidden="true">
|
|
355
|
+
{DRP_DOW.map((d) => (
|
|
356
|
+
<span key={d}>{d}</span>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
<div
|
|
360
|
+
className="dtp__days"
|
|
361
|
+
role="grid"
|
|
362
|
+
aria-label={DRP_MONTHS[m] + ' ' + y}
|
|
363
|
+
key={'days-' + viewIdx + '-' + offset}
|
|
364
|
+
data-enter={navDir || undefined}
|
|
365
|
+
>
|
|
366
|
+
{cells.map((d) => renderDay(d, m))}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const presets = drpPresets();
|
|
373
|
+
function presetActive(p: DrpPreset): boolean {
|
|
374
|
+
return value && value.start === p.start && value.end === p.end;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<div
|
|
379
|
+
className={'drp' + (layout === 'sheet' ? ' drp--sheet' : '')}
|
|
380
|
+
role="dialog"
|
|
381
|
+
aria-label={label || 'Pick a date range'}
|
|
382
|
+
>
|
|
383
|
+
<div className="drp__body">
|
|
384
|
+
<div
|
|
385
|
+
className="drp__presets"
|
|
386
|
+
role="group"
|
|
387
|
+
aria-label="Quick ranges"
|
|
388
|
+
ref={presetsRef}
|
|
389
|
+
onPointerLeave={presetGlide.leave}
|
|
390
|
+
>
|
|
391
|
+
{presets.map((p) => (
|
|
392
|
+
<button
|
|
393
|
+
key={p.id}
|
|
394
|
+
type="button"
|
|
395
|
+
className={'drp__preset' + (presetActive(p) ? ' is-active' : '')}
|
|
396
|
+
onPointerEnter={(e) => presetGlide.enter(e.currentTarget)}
|
|
397
|
+
onClick={() => applyPreset(p)}
|
|
398
|
+
>
|
|
399
|
+
{p.label}
|
|
400
|
+
</button>
|
|
401
|
+
))}
|
|
402
|
+
<GlidePill
|
|
403
|
+
className="drp__presetGlide"
|
|
404
|
+
rect={presetGlide.rect}
|
|
405
|
+
active={presetGlide.active}
|
|
406
|
+
/>
|
|
407
|
+
</div>
|
|
408
|
+
<div
|
|
409
|
+
className="drp__months"
|
|
410
|
+
ref={daysRef}
|
|
411
|
+
onKeyDown={onGridKeyDown}
|
|
412
|
+
onPointerLeave={() => {
|
|
413
|
+
if (anchor) setHoverKey(anchor);
|
|
414
|
+
grid.leave();
|
|
415
|
+
}}
|
|
416
|
+
>
|
|
417
|
+
{months === 2
|
|
418
|
+
? [renderMonth(0, true, false), renderMonth(1, false, true)]
|
|
419
|
+
: renderMonth(0, true, true)}
|
|
420
|
+
<GlidePill className="dtp__hover" rect={grid.rect} active={grid.active} />
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
<div className="drp__foot">
|
|
424
|
+
<span className={'drp__readout' + (lo && hi ? '' : ' is-empty')}>
|
|
425
|
+
{lo && hi ? (
|
|
426
|
+
<React.Fragment>
|
|
427
|
+
{drpRangeText(lo, hi)}{' '}
|
|
428
|
+
<span className="drp__count">
|
|
429
|
+
- {drpDays(lo, hi)} {drpDays(lo, hi) === 1 ? 'day' : 'days'}
|
|
430
|
+
</span>
|
|
431
|
+
</React.Fragment>
|
|
432
|
+
) : anchor ? (
|
|
433
|
+
'Pick the end date'
|
|
434
|
+
) : (
|
|
435
|
+
'Pick a start date'
|
|
436
|
+
)}
|
|
437
|
+
</span>
|
|
438
|
+
{timezone ? <span className="drp__tz">{drpTzLabel(timezone)}</span> : null}
|
|
439
|
+
<span className="drp__footSpacer"></span>
|
|
440
|
+
<button type="button" className="btn btn--primary btn--sm" onClick={close}>
|
|
441
|
+
Done
|
|
442
|
+
</button>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface DateRangeFieldProps {
|
|
449
|
+
/** Controlled value - both endpoints, or null when empty. */
|
|
450
|
+
value?: DateRange | null;
|
|
451
|
+
defaultValue?: DateRange | null;
|
|
452
|
+
/** Fires only on a COMPLETE range (a lone anchor never commits). */
|
|
453
|
+
onChange?: (value: DateRange) => void;
|
|
454
|
+
label?: string;
|
|
455
|
+
placeholder?: string;
|
|
456
|
+
/** IANA timezone (e.g. 'Europe/Riga') - display context, shown in the footer. */
|
|
457
|
+
timezone?: string;
|
|
458
|
+
/** Earliest pickable date, 'YYYY-MM-DD', inclusive. */
|
|
459
|
+
min?: string;
|
|
460
|
+
/** Latest pickable date, 'YYYY-MM-DD', inclusive. */
|
|
461
|
+
max?: string;
|
|
462
|
+
required?: boolean;
|
|
463
|
+
invalid?: boolean;
|
|
464
|
+
message?: string;
|
|
465
|
+
disabled?: boolean;
|
|
466
|
+
className?: string;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function DateRangeField({
|
|
470
|
+
value,
|
|
471
|
+
defaultValue = null,
|
|
472
|
+
onChange,
|
|
473
|
+
label,
|
|
474
|
+
placeholder = 'Pick a date range',
|
|
475
|
+
timezone,
|
|
476
|
+
min,
|
|
477
|
+
max,
|
|
478
|
+
required = false,
|
|
479
|
+
invalid = false,
|
|
480
|
+
message,
|
|
481
|
+
disabled = false,
|
|
482
|
+
className = '',
|
|
483
|
+
}: DateRangeFieldProps) {
|
|
484
|
+
const [val, commit] = useControllable(value, defaultValue, onChange);
|
|
485
|
+
|
|
486
|
+
const narrow = useResponsiveOverlayMode();
|
|
487
|
+
const mode = narrow ? 'sheet' : 'popover';
|
|
488
|
+
|
|
489
|
+
const display = val && val.start && val.end ? drpRangeText(val.start, val.end) : null;
|
|
490
|
+
|
|
491
|
+
const trigger = (
|
|
492
|
+
<button type="button" className="fld__input dtf__trigger" disabled={disabled}>
|
|
493
|
+
{display ? (
|
|
494
|
+
<span className="dtf__value">{display}</span>
|
|
495
|
+
) : (
|
|
496
|
+
<span className="dtf__placeholder">{placeholder}</span>
|
|
497
|
+
)}
|
|
498
|
+
</button>
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
return (
|
|
502
|
+
<FieldShell
|
|
503
|
+
variant="dtf"
|
|
504
|
+
label={label}
|
|
505
|
+
required={required}
|
|
506
|
+
invalid={invalid}
|
|
507
|
+
message={message}
|
|
508
|
+
icon="calendar"
|
|
509
|
+
className={className}
|
|
510
|
+
>
|
|
511
|
+
<Overlay
|
|
512
|
+
trigger={trigger}
|
|
513
|
+
mode={mode}
|
|
514
|
+
side={mode === 'sheet' ? 'bottom' : 'bottom'}
|
|
515
|
+
align="start"
|
|
516
|
+
>
|
|
517
|
+
{(api) => (
|
|
518
|
+
<DrpPanel
|
|
519
|
+
value={val}
|
|
520
|
+
commit={commit}
|
|
521
|
+
close={api.close}
|
|
522
|
+
min={min}
|
|
523
|
+
max={max}
|
|
524
|
+
timezone={timezone}
|
|
525
|
+
label={label}
|
|
526
|
+
months={narrow ? 1 : 2}
|
|
527
|
+
layout={mode}
|
|
528
|
+
/>
|
|
529
|
+
)}
|
|
530
|
+
</Overlay>
|
|
531
|
+
</FieldShell>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/* DateTimeField - DateField's sibling for 'YYYY-MM-DDTHH:mm': the calendar panel plus the segmented time machine; commits only complete datetimes. */
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Overlay } from '../overlay/Overlay';
|
|
7
|
+
import { FieldShell } from './field-shell';
|
|
8
|
+
import { TimeSegments } from './time-core';
|
|
9
|
+
import { DtpPanel } from './DateField';
|
|
10
|
+
import { MONTHS as DTTF_MONTHS, pad as dttfPad } from './date-utils';
|
|
11
|
+
|
|
12
|
+
const { useState, useEffect } = React;
|
|
13
|
+
|
|
14
|
+
interface DateTimeParts {
|
|
15
|
+
date: string | null;
|
|
16
|
+
time: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function dttfDisplayDate(key: string): string {
|
|
20
|
+
const p = key.split('-').map(Number);
|
|
21
|
+
const year = p[0] === new Date().getFullYear() ? '' : ', ' + p[0];
|
|
22
|
+
return DTTF_MONTHS[p[1] - 1].slice(0, 3) + ' ' + dttfPad(p[2]) + year;
|
|
23
|
+
}
|
|
24
|
+
function dttfDisplayTime(t: string, format?: '24h' | '12h'): string {
|
|
25
|
+
if (format !== '12h') return t;
|
|
26
|
+
const p = t.split(':').map(Number);
|
|
27
|
+
const mer = p[0] >= 12 ? 'PM' : 'AM';
|
|
28
|
+
return ((p[0] + 11) % 12) + 1 + ':' + dttfPad(p[1]) + ' ' + mer;
|
|
29
|
+
}
|
|
30
|
+
/* value: 'YYYY-MM-DDTHH:mm' - parts. Limits may be date-only. */
|
|
31
|
+
function dttfSplit(v: string | null | undefined): DateTimeParts {
|
|
32
|
+
if (!v) return { date: null, time: null };
|
|
33
|
+
const i = v.indexOf('T');
|
|
34
|
+
return i < 0 ? { date: v, time: null } : { date: v.slice(0, i), time: v.slice(i + 1) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DateTimeFieldProps {
|
|
38
|
+
/** Controlled value, 'YYYY-MM-DDTHH:mm'. */
|
|
39
|
+
value?: string | null;
|
|
40
|
+
defaultValue?: string | null;
|
|
41
|
+
onChange?: (value: string) => void;
|
|
42
|
+
label?: string;
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
/** IANA timezone (e.g. 'Europe/Riga') - display context, shown in the footer. */
|
|
45
|
+
timezone?: string;
|
|
46
|
+
/** Lower bound - 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:mm', inclusive. */
|
|
47
|
+
min?: string;
|
|
48
|
+
/** Upper bound - 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:mm', inclusive. */
|
|
49
|
+
max?: string;
|
|
50
|
+
/** Time display only; storage stays 24h. Default '24h'. */
|
|
51
|
+
format?: '24h' | '12h';
|
|
52
|
+
/** ↑/↓ step granularity in minutes (typing is exact). Default 5. */
|
|
53
|
+
minuteStep?: number;
|
|
54
|
+
required?: boolean;
|
|
55
|
+
invalid?: boolean;
|
|
56
|
+
message?: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
className?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function DateTimeField({
|
|
62
|
+
value,
|
|
63
|
+
defaultValue = null,
|
|
64
|
+
onChange,
|
|
65
|
+
label,
|
|
66
|
+
placeholder = 'Pick date & time',
|
|
67
|
+
timezone,
|
|
68
|
+
min,
|
|
69
|
+
max,
|
|
70
|
+
format = '24h',
|
|
71
|
+
minuteStep = 5,
|
|
72
|
+
required = false,
|
|
73
|
+
invalid = false,
|
|
74
|
+
message,
|
|
75
|
+
disabled = false,
|
|
76
|
+
className = '',
|
|
77
|
+
}: DateTimeFieldProps) {
|
|
78
|
+
const controlled = value !== undefined;
|
|
79
|
+
const [inner, setInner] = useState<string | null>(defaultValue);
|
|
80
|
+
const val = controlled ? value : inner;
|
|
81
|
+
const parts = dttfSplit(val);
|
|
82
|
+
|
|
83
|
+
/* incomplete halves wait here; a committed value supersedes them */
|
|
84
|
+
const [pendDate, setPendDate] = useState<string | null>(null);
|
|
85
|
+
const [pendTime, setPendTime] = useState<string | null>(null);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
setPendDate(null);
|
|
88
|
+
setPendTime(null);
|
|
89
|
+
}, [val]);
|
|
90
|
+
|
|
91
|
+
const date = pendDate != null ? pendDate : parts.date;
|
|
92
|
+
const time = pendTime != null ? pendTime : parts.time;
|
|
93
|
+
|
|
94
|
+
const minL = dttfSplit(min);
|
|
95
|
+
const maxL = dttfSplit(max);
|
|
96
|
+
/* time bounds exist only on the boundary date */
|
|
97
|
+
const minTime = date && date === minL.date ? minL.time : null;
|
|
98
|
+
const maxTime = date && date === maxL.date ? maxL.time : null;
|
|
99
|
+
|
|
100
|
+
function commitIf(d: string | null, t: string | null) {
|
|
101
|
+
if (!d || !t) return;
|
|
102
|
+
let tt = t;
|
|
103
|
+
if (minL.time && d === minL.date && tt < minL.time) tt = minL.time;
|
|
104
|
+
if (maxL.time && d === maxL.date && tt > maxL.time) tt = maxL.time;
|
|
105
|
+
if (tt !== t) setPendTime(tt);
|
|
106
|
+
const next = d + 'T' + tt;
|
|
107
|
+
if (next === val) return;
|
|
108
|
+
if (!controlled) setInner(next);
|
|
109
|
+
if (onChange) onChange(next);
|
|
110
|
+
}
|
|
111
|
+
function handleDate(d: string) {
|
|
112
|
+
setPendDate(d);
|
|
113
|
+
commitIf(d, time);
|
|
114
|
+
}
|
|
115
|
+
function handleTime(t: string) {
|
|
116
|
+
setPendTime(t);
|
|
117
|
+
commitIf(date, t);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const display = date
|
|
121
|
+
? dttfDisplayDate(date) + ', ' + (time ? dttfDisplayTime(time, format) : '--:--')
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
const trigger = (
|
|
125
|
+
<button type="button" className="fld__input dtf__trigger" disabled={disabled}>
|
|
126
|
+
{display ? (
|
|
127
|
+
<span className="dtf__value">{display}</span>
|
|
128
|
+
) : (
|
|
129
|
+
<span className="dtf__placeholder">{placeholder}</span>
|
|
130
|
+
)}
|
|
131
|
+
</button>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<FieldShell
|
|
136
|
+
variant="dtf"
|
|
137
|
+
label={label}
|
|
138
|
+
required={required}
|
|
139
|
+
invalid={invalid}
|
|
140
|
+
message={message}
|
|
141
|
+
icon="calendar"
|
|
142
|
+
className={className}
|
|
143
|
+
>
|
|
144
|
+
<Overlay trigger={trigger} side="bottom" align="start">
|
|
145
|
+
{(api) => (
|
|
146
|
+
<DtpPanel
|
|
147
|
+
val={date}
|
|
148
|
+
commit={handleDate}
|
|
149
|
+
close={api.close}
|
|
150
|
+
min={minL.date}
|
|
151
|
+
max={maxL.date}
|
|
152
|
+
timezone={timezone}
|
|
153
|
+
label={label}
|
|
154
|
+
slot={
|
|
155
|
+
<div className="dtp__time">
|
|
156
|
+
<span className="dtp__timeLabel">Time</span>
|
|
157
|
+
<span className="dtp__timeHint" aria-hidden="true">
|
|
158
|
+
<kbd>↑</kbd>
|
|
159
|
+
<kbd>↓</kbd>
|
|
160
|
+
</span>
|
|
161
|
+
<TimeSegments
|
|
162
|
+
value={time}
|
|
163
|
+
onCommit={handleTime}
|
|
164
|
+
format={format}
|
|
165
|
+
minuteStep={minuteStep}
|
|
166
|
+
min={minTime}
|
|
167
|
+
max={maxTime}
|
|
168
|
+
ariaLabel="Time"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
</Overlay>
|
|
175
|
+
</FieldShell>
|
|
176
|
+
);
|
|
177
|
+
}
|