@dbcdk/react-components 0.0.12 → 0.0.14
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/dist/components/accordion/Accordion.d.ts +2 -2
- package/dist/components/accordion/Accordion.js +34 -41
- package/dist/components/accordion/Accordion.module.css +13 -72
- package/dist/components/accordion/components/AccordionRow.d.ts +10 -0
- package/dist/components/accordion/components/AccordionRow.js +51 -0
- package/dist/components/accordion/components/AccordionRow.module.css +82 -0
- package/dist/components/breadcrumbs/Breadcrumbs.module.css +0 -1
- package/dist/components/button/Button.module.css +7 -7
- package/dist/components/card/Card.d.ts +9 -18
- package/dist/components/card/Card.js +34 -23
- package/dist/components/card/Card.module.css +22 -87
- package/dist/components/card/components/CardMeta.d.ts +15 -0
- package/dist/components/card/components/CardMeta.js +20 -0
- package/dist/components/card/components/CardMeta.module.css +51 -0
- package/dist/components/card-container/CardContainer.js +1 -1
- package/dist/components/card-container/CardContainer.module.css +3 -1
- package/dist/components/chip/Chip.module.css +7 -2
- package/dist/components/datetime-picker/DateTimePicker.d.ts +33 -8
- package/dist/components/datetime-picker/DateTimePicker.js +119 -78
- package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
- package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
- package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
- package/dist/components/filter-field/FilterField.js +21 -6
- package/dist/components/filter-field/FilterField.module.css +5 -5
- package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
- package/dist/components/forms/form-select/FormSelect.js +86 -0
- package/dist/components/forms/form-select/FormSelect.module.css +236 -0
- package/dist/components/forms/input/Input.d.ts +0 -3
- package/dist/components/forms/input/Input.js +0 -3
- package/dist/components/forms/input/Input.module.css +7 -7
- package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
- package/dist/components/forms/select/Select.js +55 -16
- package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
- package/dist/components/interval-select/IntervalSelect.js +21 -6
- package/dist/components/menu/Menu.d.ts +11 -14
- package/dist/components/menu/Menu.js +18 -33
- package/dist/components/menu/Menu.module.css +2 -2
- package/dist/components/overlay/modal/Modal.module.css +2 -1
- package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
- package/dist/components/overlay/side-panel/SidePanel.js +1 -1
- package/dist/components/overlay/side-panel/SidePanel.module.css +1 -1
- package/dist/components/page-layout/PageLayout.d.ts +16 -4
- package/dist/components/page-layout/PageLayout.js +57 -28
- package/dist/components/page-layout/PageLayout.module.css +153 -33
- package/dist/components/popover/Popover.d.ts +17 -4
- package/dist/components/popover/Popover.js +147 -65
- package/dist/components/popover/Popover.module.css +5 -0
- package/dist/components/split-pane/SplitPane.d.ts +10 -24
- package/dist/components/split-pane/SplitPane.js +83 -54
- package/dist/components/split-pane/SplitPane.module.css +11 -6
- package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
- package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
- package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
- package/dist/components/table/Table.d.ts +3 -8
- package/dist/components/table/Table.js +37 -76
- package/dist/components/table/Table.module.css +45 -42
- package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +5 -12
- package/dist/components/table/TanstackTable.js +84 -0
- package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
- package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
- package/dist/components/table/table.utils.d.ts +17 -0
- package/dist/components/table/table.utils.js +61 -0
- package/dist/components/table/tanstackTable.utils.d.ts +22 -0
- package/dist/components/table/tanstackTable.utils.js +104 -0
- package/dist/components/tabs/Tabs.d.ts +35 -12
- package/dist/components/tabs/Tabs.js +114 -26
- package/dist/components/tabs/Tabs.module.css +158 -71
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/src/styles/styles.css +0 -1
- package/dist/styles/styles.css +0 -1
- package/dist/styles/themes/dbc/base.css +136 -0
- package/dist/styles/themes/dbc/dark.css +39 -202
- package/dist/styles/themes/dbc/light.css +17 -174
- package/package.json +4 -4
- package/dist/components/table/tanstack.js +0 -214
|
@@ -5,7 +5,7 @@ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'r
|
|
|
5
5
|
import { Button } from '../../components/button/Button';
|
|
6
6
|
import { Input } from '../../components/forms/input/Input';
|
|
7
7
|
import { Popover } from '../../components/popover/Popover';
|
|
8
|
-
import { isoFromLocalDate, isoFromLocalParts, localDateFromIso, maskRange, maskSingle, parseLooseDateOrDateTime, parseLooseRange,
|
|
8
|
+
import { compareDateOnly, dateOnlyFromLocalParts, isoFromLocalDate, isoFromLocalParts, localDateFromDateOnly, localDateFromIso, maskRange, maskSingle, parseLooseDateOrDateTime, parseLooseRange, toMaskedFromDate, } from './dateTimeHelpers';
|
|
9
9
|
import styles from './DateTimePicker.module.css';
|
|
10
10
|
/* ---------- Date grid helpers (UTC date-only cells) ---------- */
|
|
11
11
|
const dUTC = (y, m, day) => new Date(Date.UTC(y, m, day));
|
|
@@ -15,7 +15,7 @@ const endOfMonthUTC = (utcDate) => dUTC(utcDate.getUTCFullYear(), utcDate.getUTC
|
|
|
15
15
|
const sameDayUTC = (a, b) => a.getUTCFullYear() === b.getUTCFullYear() &&
|
|
16
16
|
a.getUTCMonth() === b.getUTCMonth() &&
|
|
17
17
|
a.getUTCDate() === b.getUTCDate();
|
|
18
|
-
//
|
|
18
|
+
// Treat local calendar date as the source of truth for the grid.
|
|
19
19
|
// Build an equivalent "UTC date-only" for comparisons.
|
|
20
20
|
const toUTCDateOnlyFromLocal = (local) => dUTC(local.getFullYear(), local.getMonth(), local.getDate());
|
|
21
21
|
const startOfWeekUTC = (utcDate, weekStartsOn) => {
|
|
@@ -56,20 +56,30 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
56
56
|
void formatDate;
|
|
57
57
|
void formatRange;
|
|
58
58
|
const popRef = useRef(null);
|
|
59
|
+
// Narrowed onChange call signatures (avoids union-of-functions intersection issues)
|
|
60
|
+
const onChangeRange = onChange;
|
|
61
|
+
const onChangeIso = onChange;
|
|
62
|
+
const onChangeDateOnly = onChange;
|
|
59
63
|
const todayLocal = useMemo(() => new Date(), []);
|
|
60
64
|
// ---- local anchor from controlled value ----
|
|
61
65
|
const initialAnchor = useMemo(() => {
|
|
62
|
-
var _a, _b;
|
|
63
66
|
if (mode === 'single') {
|
|
64
|
-
if (typeof value === 'string')
|
|
65
|
-
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
const d = enableTime
|
|
69
|
+
? localDateFromIso(value)
|
|
70
|
+
: localDateFromDateOnly(value);
|
|
71
|
+
return d !== null && d !== void 0 ? d : todayLocal;
|
|
72
|
+
}
|
|
66
73
|
return todayLocal;
|
|
67
74
|
}
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
// range: anchor from start if present
|
|
76
|
+
const v = value;
|
|
77
|
+
if (v === null || v === void 0 ? void 0 : v.start) {
|
|
78
|
+
const d = localDateFromDateOnly(v.start);
|
|
79
|
+
return d !== null && d !== void 0 ? d : todayLocal;
|
|
70
80
|
}
|
|
71
81
|
return todayLocal;
|
|
72
|
-
}, [mode, value, todayLocal]);
|
|
82
|
+
}, [mode, value, enableTime, todayLocal]);
|
|
73
83
|
const [monthAnchor, setMonthAnchor] = useState(initialAnchor);
|
|
74
84
|
useEffect(() => setMonthAnchor(initialAnchor), [initialAnchor]);
|
|
75
85
|
// time defaults (local)
|
|
@@ -97,25 +107,27 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
97
107
|
return null;
|
|
98
108
|
if (typeof value !== 'string')
|
|
99
109
|
return null;
|
|
100
|
-
const d = localDateFromIso(value);
|
|
110
|
+
const d = enableTime ? localDateFromIso(value) : localDateFromDateOnly(value);
|
|
101
111
|
if (!d)
|
|
102
112
|
return null;
|
|
103
113
|
return toUTCDateOnlyFromLocal(d);
|
|
104
|
-
}, [mode, value]);
|
|
114
|
+
}, [mode, value, enableTime]);
|
|
105
115
|
const selectedUTC_start = useMemo(() => {
|
|
106
|
-
if (mode !== 'range'
|
|
116
|
+
if (mode !== 'range')
|
|
107
117
|
return null;
|
|
108
|
-
|
|
118
|
+
const v = value;
|
|
119
|
+
if (!(v === null || v === void 0 ? void 0 : v.start))
|
|
109
120
|
return null;
|
|
110
|
-
const d =
|
|
121
|
+
const d = localDateFromDateOnly(v.start);
|
|
111
122
|
return d ? toUTCDateOnlyFromLocal(d) : null;
|
|
112
123
|
}, [mode, value]);
|
|
113
124
|
const selectedUTC_end = useMemo(() => {
|
|
114
|
-
if (mode !== 'range'
|
|
125
|
+
if (mode !== 'range')
|
|
115
126
|
return null;
|
|
116
|
-
|
|
127
|
+
const v = value;
|
|
128
|
+
if (!(v === null || v === void 0 ? void 0 : v.end))
|
|
117
129
|
return null;
|
|
118
|
-
const d =
|
|
130
|
+
const d = localDateFromDateOnly(v.end);
|
|
119
131
|
return d ? toUTCDateOnlyFromLocal(d) : null;
|
|
120
132
|
}, [mode, value]);
|
|
121
133
|
const isDisabledUTC = useCallback((utcDay) => {
|
|
@@ -126,7 +138,7 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
126
138
|
return true;
|
|
127
139
|
return false;
|
|
128
140
|
}, [min, max]);
|
|
129
|
-
const selectDay = (utcDay) => {
|
|
141
|
+
const selectDay = useCallback((utcDay) => {
|
|
130
142
|
var _a, _b;
|
|
131
143
|
if (isDisabledUTC(utcDay))
|
|
132
144
|
return;
|
|
@@ -135,29 +147,51 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
135
147
|
const m0 = utcDay.getUTCMonth();
|
|
136
148
|
const d = utcDay.getUTCDate();
|
|
137
149
|
if (mode === 'single') {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
if (enableTime) {
|
|
151
|
+
const iso = isoFromLocalParts(y, m0, d, timeHH, timeMM);
|
|
152
|
+
if (!iso)
|
|
153
|
+
return; // DST-invalid time; you may want to surface validation UI
|
|
154
|
+
onChangeIso(iso);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const dateOnly = dateOnlyFromLocalParts(y, m0, d);
|
|
158
|
+
onChangeDateOnly(dateOnly);
|
|
159
|
+
}
|
|
142
160
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
143
161
|
return;
|
|
144
162
|
}
|
|
145
|
-
// range
|
|
146
|
-
const curr = value
|
|
147
|
-
|
|
148
|
-
: { start: null, end: null };
|
|
149
|
-
const picked = isoFromLocalParts(y, m0, d, 0, 0);
|
|
163
|
+
// range: date-only
|
|
164
|
+
const curr = value;
|
|
165
|
+
const picked = dateOnlyFromLocalParts(y, m0, d);
|
|
150
166
|
if (!curr.start || (curr.start && curr.end)) {
|
|
151
|
-
|
|
167
|
+
onChangeRange({ start: picked, end: null });
|
|
152
168
|
return;
|
|
153
169
|
}
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const end = a <= b ? picked : curr.start;
|
|
158
|
-
onChange({ start, end });
|
|
170
|
+
const start = compareDateOnly(curr.start, picked) <= 0 ? curr.start : picked;
|
|
171
|
+
const end = compareDateOnly(curr.start, picked) <= 0 ? picked : curr.start;
|
|
172
|
+
onChangeRange({ start, end });
|
|
159
173
|
(_b = popRef.current) === null || _b === void 0 ? void 0 : _b.close();
|
|
160
|
-
}
|
|
174
|
+
}, [
|
|
175
|
+
enableTime,
|
|
176
|
+
isDisabledUTC,
|
|
177
|
+
mode,
|
|
178
|
+
onChangeDateOnly,
|
|
179
|
+
onChangeIso,
|
|
180
|
+
onChangeRange,
|
|
181
|
+
timeHH,
|
|
182
|
+
timeMM,
|
|
183
|
+
value,
|
|
184
|
+
]);
|
|
185
|
+
function addMonthsLocal(dt, n) {
|
|
186
|
+
const y = dt.getFullYear();
|
|
187
|
+
const m = dt.getMonth();
|
|
188
|
+
return new Date(y, m + n, 1);
|
|
189
|
+
}
|
|
190
|
+
function addDaysLocal(dt, n) {
|
|
191
|
+
const d = new Date(dt);
|
|
192
|
+
d.setDate(d.getDate() + n);
|
|
193
|
+
return d;
|
|
194
|
+
}
|
|
161
195
|
const gridRef = useRef(null);
|
|
162
196
|
useEffect(() => {
|
|
163
197
|
const el = gridRef.current;
|
|
@@ -179,33 +213,35 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
179
213
|
].includes(e.key)) {
|
|
180
214
|
e.preventDefault();
|
|
181
215
|
}
|
|
182
|
-
const anchor = toUTCDateOnlyFromLocal(monthAnchor);
|
|
183
|
-
const move = (days) => setMonthAnchor(prev => addDaysLocal(prev, days));
|
|
184
216
|
switch (e.key) {
|
|
185
217
|
case 'ArrowLeft':
|
|
186
|
-
|
|
218
|
+
setMonthAnchor(prev => addDaysLocal(prev, -1));
|
|
187
219
|
break;
|
|
188
220
|
case 'ArrowRight':
|
|
189
|
-
|
|
221
|
+
setMonthAnchor(prev => addDaysLocal(prev, 1));
|
|
190
222
|
break;
|
|
191
223
|
case 'ArrowUp':
|
|
192
|
-
|
|
224
|
+
setMonthAnchor(prev => addDaysLocal(prev, -7));
|
|
193
225
|
break;
|
|
194
226
|
case 'ArrowDown':
|
|
195
|
-
|
|
227
|
+
setMonthAnchor(prev => addDaysLocal(prev, 7));
|
|
196
228
|
break;
|
|
197
229
|
case 'PageUp':
|
|
198
|
-
setMonthAnchor(addMonthsLocal(
|
|
230
|
+
setMonthAnchor(prev => addMonthsLocal(prev, -1));
|
|
199
231
|
break;
|
|
200
232
|
case 'PageDown':
|
|
201
|
-
setMonthAnchor(addMonthsLocal(
|
|
233
|
+
setMonthAnchor(prev => addMonthsLocal(prev, 1));
|
|
202
234
|
break;
|
|
203
|
-
case 'Home':
|
|
204
|
-
|
|
235
|
+
case 'Home': {
|
|
236
|
+
const a = toUTCDateOnlyFromLocal(monthAnchor);
|
|
237
|
+
setMonthAnchor(new Date(a.getUTCFullYear(), a.getUTCMonth(), 1));
|
|
205
238
|
break;
|
|
206
|
-
|
|
207
|
-
|
|
239
|
+
}
|
|
240
|
+
case 'End': {
|
|
241
|
+
const a = toUTCDateOnlyFromLocal(monthAnchor);
|
|
242
|
+
setMonthAnchor(new Date(a.getUTCFullYear(), a.getUTCMonth() + 1, 0));
|
|
208
243
|
break;
|
|
244
|
+
}
|
|
209
245
|
}
|
|
210
246
|
};
|
|
211
247
|
el.addEventListener('keydown', onKey);
|
|
@@ -218,12 +254,12 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
218
254
|
if (mode === 'single') {
|
|
219
255
|
if (typeof value !== 'string')
|
|
220
256
|
return '';
|
|
221
|
-
const d = localDateFromIso(value);
|
|
257
|
+
const d = enableTime ? localDateFromIso(value) : localDateFromDateOnly(value);
|
|
222
258
|
return d ? toMaskedFromDate(d, enableTime) : '';
|
|
223
259
|
}
|
|
224
260
|
const v = value;
|
|
225
|
-
const s = typeof (v === null || v === void 0 ? void 0 : v.start) === 'string' ?
|
|
226
|
-
const e = typeof (v === null || v === void 0 ? void 0 : v.end) === 'string' ?
|
|
261
|
+
const s = typeof (v === null || v === void 0 ? void 0 : v.start) === 'string' ? localDateFromDateOnly(v.start) : null;
|
|
262
|
+
const e = typeof (v === null || v === void 0 ? void 0 : v.end) === 'string' ? localDateFromDateOnly(v.end) : null;
|
|
227
263
|
const ss = s ? toMaskedFromDate(s, false) : '';
|
|
228
264
|
const ee = e ? toMaskedFromDate(e, false) : '';
|
|
229
265
|
if (ss && ee)
|
|
@@ -242,10 +278,15 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
242
278
|
}, [formatted, dirty]);
|
|
243
279
|
const commitTypedValue = useCallback(() => {
|
|
244
280
|
if (!text.trim()) {
|
|
245
|
-
if (mode === 'single')
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
281
|
+
if (mode === 'single') {
|
|
282
|
+
if (enableTime)
|
|
283
|
+
onChangeIso(null);
|
|
284
|
+
else
|
|
285
|
+
onChangeDateOnly(null);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
onChangeRange({ start: null, end: null });
|
|
289
|
+
}
|
|
249
290
|
setDirty(false);
|
|
250
291
|
return;
|
|
251
292
|
}
|
|
@@ -253,41 +294,41 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
253
294
|
const dLocal = parseLooseDateOrDateTime(text);
|
|
254
295
|
if (!dLocal)
|
|
255
296
|
return;
|
|
256
|
-
|
|
257
|
-
|
|
297
|
+
if (enableTime) {
|
|
298
|
+
onChangeIso(isoFromLocalDate(dLocal));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
onChangeDateOnly(dateOnlyFromLocalParts(dLocal.getFullYear(), dLocal.getMonth(), dLocal.getDate()));
|
|
302
|
+
}
|
|
258
303
|
setMonthAnchor(dLocal);
|
|
259
304
|
setDirty(false);
|
|
260
305
|
return;
|
|
261
306
|
}
|
|
307
|
+
// range: date-only
|
|
262
308
|
const r = parseLooseRange(text);
|
|
263
309
|
if (r) {
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
310
|
+
const start = dateOnlyFromLocalParts(r.start.getFullYear(), r.start.getMonth(), r.start.getDate());
|
|
311
|
+
const end = dateOnlyFromLocalParts(r.end.getFullYear(), r.end.getMonth(), r.end.getDate());
|
|
312
|
+
onChangeRange({ start, end });
|
|
267
313
|
setMonthAnchor(r.start);
|
|
268
314
|
setDirty(false);
|
|
269
315
|
}
|
|
270
|
-
}, [text, mode,
|
|
316
|
+
}, [text, mode, enableTime, onChangeDateOnly, onChangeIso, onChangeRange]);
|
|
271
317
|
const clear = useCallback(() => {
|
|
272
|
-
if (mode === 'single')
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
318
|
+
if (mode === 'single') {
|
|
319
|
+
if (enableTime)
|
|
320
|
+
onChangeIso(null);
|
|
321
|
+
else
|
|
322
|
+
onChangeDateOnly(null);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
onChangeRange({ start: null, end: null });
|
|
326
|
+
}
|
|
276
327
|
setDirty(false);
|
|
277
328
|
setText('');
|
|
278
|
-
}, [mode,
|
|
279
|
-
function addMonthsLocal(dt, n) {
|
|
280
|
-
const y = dt.getFullYear();
|
|
281
|
-
const m = dt.getMonth();
|
|
282
|
-
return new Date(y, m + n, 1);
|
|
283
|
-
}
|
|
284
|
-
function addDaysLocal(dt, n) {
|
|
285
|
-
const d = new Date(dt);
|
|
286
|
-
d.setDate(d.getDate() + n);
|
|
287
|
-
return d;
|
|
288
|
-
}
|
|
329
|
+
}, [mode, enableTime, onChangeDateOnly, onChangeIso, onChangeRange]);
|
|
289
330
|
const fallbackPlaceholder = mode === 'single' ? 'Vælg dato' : 'Vælg interval';
|
|
290
|
-
return (_jsx(Popover, { ref: popRef, trigger: toggle => {
|
|
331
|
+
return (_jsx(Popover, { matchTriggerWidth: false, ref: popRef, trigger: toggle => {
|
|
291
332
|
var _a, _b;
|
|
292
333
|
return (_jsx("div", { onClick: toggle, className: styles.triggerWrap, children: _jsx(Input, { ...inputProps, autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: "false", placeholder: (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.placeholder) !== null && _a !== void 0 ? _a : fallbackPlaceholder, value: dirty ? text : formatted, onInput: e => {
|
|
293
334
|
setDirty(true);
|
|
@@ -307,14 +348,14 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
307
348
|
}, viewportPadding: 8, children: _jsxs("div", { className: cx(styles.panel, !!(presets === null || presets === void 0 ? void 0 : presets.length) && styles.panelWithPresets), children: [(presets === null || presets === void 0 ? void 0 : presets.length) ? (_jsxs("div", { className: styles.presetsCol, children: [_jsx("div", { className: styles.presetsLabel, children: "Forvalg" }), _jsxs("div", { className: styles.presetsList, children: [presets.map(p => (_jsx(Button, { variant: "outlined", size: "sm", onClick: () => {
|
|
308
349
|
var _a;
|
|
309
350
|
const r = p.getRange();
|
|
310
|
-
const
|
|
311
|
-
const
|
|
312
|
-
|
|
351
|
+
const start = dateOnlyFromLocalParts(r.start.getFullYear(), r.start.getMonth(), r.start.getDate());
|
|
352
|
+
const end = dateOnlyFromLocalParts(r.end.getFullYear(), r.end.getMonth(), r.end.getDate());
|
|
353
|
+
onChangeRange({ start, end });
|
|
313
354
|
setDirty(false);
|
|
314
355
|
setText(`${toMaskedFromDate(r.start, false)} – ${toMaskedFromDate(r.end, false)}`);
|
|
315
356
|
setMonthAnchor(r.start);
|
|
316
357
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
317
|
-
}, children: p.label }, p.label))), mode === 'range' && (_jsx(Button, { variant: "danger", size: "sm", onClick: clear, icon: _jsx(X, { size: 14 }), children: "Ryd" }))] })] })) : null, _jsxs("div", { className: styles.calendarArea, children: [_jsxs("div", { className: styles.header, children: [_jsx(Button, { variant: "outlined", size: "sm", "aria-label": "Forrige m\u00E5ned", icon: _jsx(ChevronLeft, { size: 16 }), onClick: () => setMonthAnchor(addMonthsLocal(
|
|
358
|
+
}, children: p.label }, p.label))), mode === 'range' && (_jsx(Button, { variant: "danger", size: "sm", onClick: clear, icon: _jsx(X, { size: 14 }), children: "Ryd" }))] })] })) : null, _jsxs("div", { className: styles.calendarArea, children: [_jsxs("div", { className: styles.header, children: [_jsx(Button, { variant: "outlined", size: "sm", "aria-label": "Forrige m\u00E5ned", icon: _jsx(ChevronLeft, { size: 16 }), onClick: () => setMonthAnchor(prev => addMonthsLocal(prev, -1)) }), _jsx("div", { "aria-live": "polite", className: styles.headerTitle, children: monthFmt.format(monthAnchor) }), _jsx(Button, { variant: "outlined", size: "sm", "aria-label": "N\u00E6ste m\u00E5ned", icon: _jsx(ChevronRight, { size: 16 }), onClick: () => setMonthAnchor(prev => addMonthsLocal(prev, 1)) })] }), _jsx("div", { className: styles.weekRow, "aria-hidden": true, children: Array.from({ length: 7 }, (_, i) => (i + weekStartsOn) % 7).map(dow => (_jsx("div", { className: styles.weekCell, children: weekdayFmt.format(dUTC(2024, 8, dow + 1)).slice(0, 2) }, dow))) }), _jsx("div", { ref: gridRef, role: "grid", "aria-label": "Kalender", tabIndex: 0, className: styles.grid, onMouseLeave: () => setHoverUTC(null), children: cellsUTC.map((utcDay, idx) => {
|
|
318
359
|
const inThisMonth = utcDay >= monthStartUTC && utcDay <= monthEndUTC;
|
|
319
360
|
const isToday = sameDayUTC(utcDay, toUTCDateOnlyFromLocal(todayLocal));
|
|
320
361
|
const disabledDay = isDisabledUTC(utcDay);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
.panel {
|
|
6
6
|
display: grid;
|
|
7
7
|
grid-template-columns: 1fr;
|
|
8
|
+
width: fit-content;
|
|
8
9
|
gap: 8px;
|
|
9
10
|
padding: 8px;
|
|
10
11
|
background: var(--color-bg-surface);
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
|
|
33
34
|
.calendarArea {
|
|
34
35
|
display: grid;
|
|
36
|
+
width: max-content;
|
|
35
37
|
gap: 8px;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -5,18 +5,30 @@ export declare function maskSingle(text: string, enableTime: boolean): string;
|
|
|
5
5
|
export declare function maskRange(text: string, _enableTime: boolean): string;
|
|
6
6
|
export declare const pad2: (n: number) => string;
|
|
7
7
|
export type UtcIsoString = string;
|
|
8
|
+
export type DateOnlyString = string;
|
|
8
9
|
export declare function isUtcIsoString(v: unknown): v is UtcIsoString;
|
|
10
|
+
export declare function isDateOnlyString(v: unknown): v is DateOnlyString;
|
|
9
11
|
export declare function utcMillisFromIso(iso: UtcIsoString): number;
|
|
12
|
+
/** Convert a local DateTime to a UTC ISO string (Z). */
|
|
13
|
+
export declare function isoFromLocalDate(dLocal: Date): UtcIsoString;
|
|
10
14
|
/**
|
|
11
15
|
* Build a *local* Date from y/m/d/hh/mm and return UTC ISO string (Z).
|
|
12
16
|
* This keeps the UI meaning "local wall time", while emitting a stable UTC instant.
|
|
17
|
+
*
|
|
18
|
+
* DST NOTE:
|
|
19
|
+
* If the requested local wall time does not exist (spring-forward), JS will auto-correct.
|
|
20
|
+
* We detect that and return null so caller can handle it (e.g., show validation error).
|
|
13
21
|
*/
|
|
14
22
|
export declare function isoFromLocalParts(y: number, m0: number, // 0-based
|
|
15
|
-
d: number, hh?: number, mm?: number): UtcIsoString;
|
|
16
|
-
/** Convert a local Date to a UTC ISO string (Z). */
|
|
17
|
-
export declare function isoFromLocalDate(dLocal: Date): UtcIsoString;
|
|
23
|
+
d: number, hh?: number, mm?: number): UtcIsoString | null;
|
|
18
24
|
/** For anchoring the calendar safely from a UTC ISO string. */
|
|
19
25
|
export declare function localDateFromIso(iso: UtcIsoString): Date | null;
|
|
26
|
+
/** Date-only: build 'YYYY-MM-DD' from local parts */
|
|
27
|
+
export declare function dateOnlyFromLocalParts(y: number, m0: number, d: number): DateOnlyString;
|
|
28
|
+
/** Parse 'YYYY-MM-DD' into a local Date at midnight */
|
|
29
|
+
export declare function localDateFromDateOnly(s: DateOnlyString): Date | null;
|
|
30
|
+
/** Compare date-only strings by UTC date-only time (stable ordering) */
|
|
31
|
+
export declare function compareDateOnly(a: DateOnlyString, b: DateOnlyString): number;
|
|
20
32
|
export declare function toMaskedFromDate(d: Date, enableTime: boolean): string;
|
|
21
33
|
export declare function parseLooseDateOrDateTime(input: string): Date | null;
|
|
22
34
|
export declare function parseLooseRange(input: string): {
|
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
/* ---------- Range separator helpers ---------- */
|
|
2
|
+
/**
|
|
3
|
+
* IMPORTANT:
|
|
4
|
+
* "-" is only treated as a range separator when surrounded by whitespace: " - "
|
|
5
|
+
* so we don't split inside "DD-MM-YYYY".
|
|
6
|
+
*
|
|
7
|
+
* Supported separators:
|
|
8
|
+
* - En dash: "–" (with optional surrounding spaces)
|
|
9
|
+
* - Hyphen as range sep: " - " (spaces required)
|
|
10
|
+
* - Words: "to", "til" (with optional surrounding spaces)
|
|
11
|
+
*
|
|
12
|
+
* COMPAT:
|
|
13
|
+
* - Also supports digit-only ranges like "01022026-02022026" (no spaces),
|
|
14
|
+
* because that cannot be confused with "DD-MM-YYYY".
|
|
15
|
+
*/
|
|
16
|
+
const RANGE_SEP_RE = /\s*(?:–|\bto\b|\btil\b|\s-\s)\s*/i;
|
|
17
|
+
function normalizeSpaces(s) {
|
|
18
|
+
return s.trim().replace(/\s+/g, ' ');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Special-case for digit-only ranges: 8 digits + '-' + 8 digits (optional spaces).
|
|
22
|
+
* This keeps legacy UX working without reintroducing the DD-MM-YYYY split bug.
|
|
23
|
+
*/
|
|
24
|
+
function splitDigitOnlyRangeParts(input) {
|
|
25
|
+
const m = /^\s*(\d{8})\s*-\s*(\d{8})\s*$/.exec(input);
|
|
26
|
+
if (!m)
|
|
27
|
+
return null;
|
|
28
|
+
return [m[1], m[2]];
|
|
29
|
+
}
|
|
30
|
+
function splitRangeParts(input) {
|
|
31
|
+
var _a;
|
|
32
|
+
// 1) digit-only fast path: 01022026-02022026
|
|
33
|
+
const digitOnly = splitDigitOnlyRangeParts(input);
|
|
34
|
+
if (digitOnly)
|
|
35
|
+
return digitOnly;
|
|
36
|
+
// 2) normal path: use safe separators (–, to, til, " - ")
|
|
37
|
+
const txt = normalizeSpaces(input);
|
|
38
|
+
const parts = txt.split(RANGE_SEP_RE);
|
|
39
|
+
if (parts.length < 2)
|
|
40
|
+
return null;
|
|
41
|
+
const left = ((_a = parts[0]) !== null && _a !== void 0 ? _a : '').trim();
|
|
42
|
+
const right = parts.slice(1).join(' ').trim();
|
|
43
|
+
if (!left || !right)
|
|
44
|
+
return null;
|
|
45
|
+
return [left, right];
|
|
46
|
+
}
|
|
1
47
|
/* ---------- Mask helpers (no deps) ---------- */
|
|
2
48
|
// Pull only 0-9
|
|
3
49
|
export const digits = (s) => (s.match(/\d/g) || []).join('');
|
|
@@ -23,7 +69,7 @@ export function maskTimeHM(text) {
|
|
|
23
69
|
}
|
|
24
70
|
// Single: "DD-MM-YYYY" or "DD-MM-YYYY HH:mm"
|
|
25
71
|
export function maskSingle(text, enableTime) {
|
|
26
|
-
|
|
72
|
+
const t = normalizeSpaces(text);
|
|
27
73
|
if (!enableTime)
|
|
28
74
|
return maskDateEU(t);
|
|
29
75
|
// split date + time by first space or 'T'
|
|
@@ -34,15 +80,15 @@ export function maskSingle(text, enableTime) {
|
|
|
34
80
|
const timePart = maskTimeHM(m[2]);
|
|
35
81
|
return timePart ? `${datePart} ${timePart}` : datePart;
|
|
36
82
|
}
|
|
37
|
-
// Range: mask both sides around common separators (–,
|
|
83
|
+
// Range: mask both sides around common separators (–, " - ", to, til, or digit-only "01022026-02022026")
|
|
38
84
|
export function maskRange(text, _enableTime) {
|
|
39
85
|
// NOTE: range is date-only in the UI
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const a = maskSingle(
|
|
45
|
-
const b = maskSingle(
|
|
86
|
+
const parts = splitRangeParts(text);
|
|
87
|
+
if (!parts)
|
|
88
|
+
return maskSingle(text, false);
|
|
89
|
+
const [left, right] = parts;
|
|
90
|
+
const a = maskSingle(left, false);
|
|
91
|
+
const b = maskSingle(right, false);
|
|
46
92
|
return `${a} – ${b}`.trim();
|
|
47
93
|
}
|
|
48
94
|
// Pad helper
|
|
@@ -50,20 +96,52 @@ export const pad2 = (n) => String(n).padStart(2, '0');
|
|
|
50
96
|
export function isUtcIsoString(v) {
|
|
51
97
|
return typeof v === 'string' && /Z$/.test(v) && !Number.isNaN(Date.parse(v));
|
|
52
98
|
}
|
|
99
|
+
export function isDateOnlyString(v) {
|
|
100
|
+
if (typeof v !== 'string')
|
|
101
|
+
return false;
|
|
102
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
|
|
103
|
+
if (!m)
|
|
104
|
+
return false;
|
|
105
|
+
const y = +m[1];
|
|
106
|
+
const mo = +m[2];
|
|
107
|
+
const d = +m[3];
|
|
108
|
+
if (mo < 1 || mo > 12)
|
|
109
|
+
return false;
|
|
110
|
+
if (d < 1 || d > 31)
|
|
111
|
+
return false;
|
|
112
|
+
// Validate exact date (guard against 2026-02-31 etc.)
|
|
113
|
+
const local = new Date(y, mo - 1, d, 0, 0, 0, 0);
|
|
114
|
+
return local.getFullYear() === y && local.getMonth() === mo - 1 && local.getDate() === d;
|
|
115
|
+
}
|
|
53
116
|
export function utcMillisFromIso(iso) {
|
|
54
117
|
return Date.parse(iso);
|
|
55
118
|
}
|
|
119
|
+
/** Convert a local DateTime to a UTC ISO string (Z). */
|
|
120
|
+
export function isoFromLocalDate(dLocal) {
|
|
121
|
+
return dLocal.toISOString();
|
|
122
|
+
}
|
|
56
123
|
/**
|
|
57
124
|
* Build a *local* Date from y/m/d/hh/mm and return UTC ISO string (Z).
|
|
58
125
|
* This keeps the UI meaning "local wall time", while emitting a stable UTC instant.
|
|
126
|
+
*
|
|
127
|
+
* DST NOTE:
|
|
128
|
+
* If the requested local wall time does not exist (spring-forward), JS will auto-correct.
|
|
129
|
+
* We detect that and return null so caller can handle it (e.g., show validation error).
|
|
59
130
|
*/
|
|
60
131
|
export function isoFromLocalParts(y, m0, // 0-based
|
|
61
132
|
d, hh = 0, mm = 0) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
133
|
+
const local = new Date(y, m0, d, hh, mm, 0, 0);
|
|
134
|
+
if (Number.isNaN(local.getTime()))
|
|
135
|
+
return null;
|
|
136
|
+
// Guard: JS autocorrects invalid dates/times; re-validate exact parts
|
|
137
|
+
if (local.getFullYear() !== y ||
|
|
138
|
+
local.getMonth() !== m0 ||
|
|
139
|
+
local.getDate() !== d ||
|
|
140
|
+
local.getHours() !== hh ||
|
|
141
|
+
local.getMinutes() !== mm) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return local.toISOString();
|
|
67
145
|
}
|
|
68
146
|
/** For anchoring the calendar safely from a UTC ISO string. */
|
|
69
147
|
export function localDateFromIso(iso) {
|
|
@@ -72,6 +150,37 @@ export function localDateFromIso(iso) {
|
|
|
72
150
|
return null;
|
|
73
151
|
return new Date(ms); // local rendering
|
|
74
152
|
}
|
|
153
|
+
/** Date-only: build 'YYYY-MM-DD' from local parts */
|
|
154
|
+
export function dateOnlyFromLocalParts(y, m0, d) {
|
|
155
|
+
return `${String(y).padStart(4, '0')}-${pad2(m0 + 1)}-${pad2(d)}`;
|
|
156
|
+
}
|
|
157
|
+
/** Parse 'YYYY-MM-DD' into a local Date at midnight */
|
|
158
|
+
export function localDateFromDateOnly(s) {
|
|
159
|
+
if (!isDateOnlyString(s))
|
|
160
|
+
return null;
|
|
161
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
162
|
+
if (!m)
|
|
163
|
+
return null;
|
|
164
|
+
const y = +m[1];
|
|
165
|
+
const mo0 = +m[2] - 1;
|
|
166
|
+
const d = +m[3];
|
|
167
|
+
const local = new Date(y, mo0, d, 0, 0, 0, 0);
|
|
168
|
+
if (Number.isNaN(local.getTime()))
|
|
169
|
+
return null;
|
|
170
|
+
if (local.getFullYear() !== y || local.getMonth() !== mo0 || local.getDate() !== d)
|
|
171
|
+
return null;
|
|
172
|
+
return local;
|
|
173
|
+
}
|
|
174
|
+
/** Compare date-only strings by UTC date-only time (stable ordering) */
|
|
175
|
+
export function compareDateOnly(a, b) {
|
|
176
|
+
const da = localDateFromDateOnly(a);
|
|
177
|
+
const db = localDateFromDateOnly(b);
|
|
178
|
+
if (!da || !db)
|
|
179
|
+
return 0;
|
|
180
|
+
const ta = Date.UTC(da.getFullYear(), da.getMonth(), da.getDate());
|
|
181
|
+
const tb = Date.UTC(db.getFullYear(), db.getMonth(), db.getDate());
|
|
182
|
+
return ta - tb;
|
|
183
|
+
}
|
|
75
184
|
/* ---------- Formatting (UI shows local) ---------- */
|
|
76
185
|
// From Date → "DD-MM-YYYY" or "DD-MM-YYYY HH:mm" (local time)
|
|
77
186
|
export function toMaskedFromDate(d, enableTime) {
|
|
@@ -86,7 +195,7 @@ export function toMaskedFromDate(d, enableTime) {
|
|
|
86
195
|
/* ---------- Parsing helpers (no deps) ---------- */
|
|
87
196
|
// Accepts: YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY (+ optional HH:mm)
|
|
88
197
|
export function parseLooseDateOrDateTime(input) {
|
|
89
|
-
const txt = input
|
|
198
|
+
const txt = normalizeSpaces(input);
|
|
90
199
|
const dateTimeMatch = /^(?<date>[\d./-]{8,10})(?:[ T](?<hh>\d{1,2}):(?<mm>\d{2}))?$/i.exec(txt);
|
|
91
200
|
if (!(dateTimeMatch === null || dateTimeMatch === void 0 ? void 0 : dateTimeMatch.groups))
|
|
92
201
|
return null;
|
|
@@ -95,7 +204,7 @@ export function parseLooseDateOrDateTime(input) {
|
|
|
95
204
|
const mm = dateTimeMatch.groups.mm ? parseInt(dateTimeMatch.groups.mm, 10) : 0;
|
|
96
205
|
if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
|
|
97
206
|
return null;
|
|
98
|
-
// Try YYYY-MM-DD first
|
|
207
|
+
// Try YYYY-MM-DD first (still accepts 1-2 digits for month/day in ISO form)
|
|
99
208
|
let y, m, d;
|
|
100
209
|
const mIso = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw);
|
|
101
210
|
if (mIso) {
|
|
@@ -104,8 +213,8 @@ export function parseLooseDateOrDateTime(input) {
|
|
|
104
213
|
d = +mIso[3];
|
|
105
214
|
}
|
|
106
215
|
else {
|
|
107
|
-
//
|
|
108
|
-
const mEu = /^(\d{
|
|
216
|
+
// EU formats must be strict DD-MM-YYYY / DD/MM/YYYY / DD.MM.YYYY
|
|
217
|
+
const mEu = /^(\d{2})[./-](\d{2})[./-](\d{4})$/.exec(raw);
|
|
109
218
|
if (!mEu)
|
|
110
219
|
return null;
|
|
111
220
|
d = +mEu[1];
|
|
@@ -115,17 +224,22 @@ export function parseLooseDateOrDateTime(input) {
|
|
|
115
224
|
const local = new Date(y, m, d, hh, mm, 0, 0);
|
|
116
225
|
if (Number.isNaN(local.getTime()))
|
|
117
226
|
return null;
|
|
118
|
-
// Guard: JS autocorrects invalid dates; re-validate exact Y/M/D
|
|
119
|
-
if (local.getFullYear() !== y ||
|
|
227
|
+
// Guard: JS autocorrects invalid dates; re-validate exact Y/M/D/H/M
|
|
228
|
+
if (local.getFullYear() !== y ||
|
|
229
|
+
local.getMonth() !== m ||
|
|
230
|
+
local.getDate() !== d ||
|
|
231
|
+
local.getHours() !== hh ||
|
|
232
|
+
local.getMinutes() !== mm) {
|
|
120
233
|
return null;
|
|
234
|
+
}
|
|
121
235
|
return local;
|
|
122
236
|
}
|
|
123
|
-
// Parse a range string with separators: "–", "-", "to", "til"
|
|
237
|
+
// Parse a range string with separators: "–", " - ", "to", "til", or digit-only "01022026-02022026"
|
|
124
238
|
export function parseLooseRange(input) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
if (!a || !b)
|
|
239
|
+
const parts = splitRangeParts(input);
|
|
240
|
+
if (!parts)
|
|
128
241
|
return null;
|
|
242
|
+
const [a, b] = parts;
|
|
129
243
|
const s = parseLooseDateOrDateTime(a);
|
|
130
244
|
const e = parseLooseDateOrDateTime(b);
|
|
131
245
|
if (!s || !e)
|