@dbcdk/react-components 0.0.8 → 0.0.10

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.
Files changed (88) hide show
  1. package/README.md +167 -0
  2. package/dist/components/__stories__/_data/tabs.d.ts +9 -0
  3. package/dist/components/__stories__/_data/tabs.js +31 -0
  4. package/dist/components/accordion/Accordion.d.ts +27 -0
  5. package/dist/components/accordion/Accordion.js +66 -0
  6. package/dist/components/accordion/Accordion.module.css +87 -0
  7. package/dist/components/button/Button.module.css +1 -0
  8. package/dist/components/circle/Circle.d.ts +4 -1
  9. package/dist/components/circle/Circle.js +2 -2
  10. package/dist/components/circle/Circle.module.css +54 -2
  11. package/dist/components/code-block/CodeBlock.module.css +1 -1
  12. package/dist/components/datetime-picker/DateTimePicker.d.ts +4 -7
  13. package/dist/components/datetime-picker/DateTimePicker.js +117 -64
  14. package/dist/components/datetime-picker/dateTimeHelpers.d.ts +14 -2
  15. package/dist/components/datetime-picker/dateTimeHelpers.js +32 -17
  16. package/dist/components/forms/checkbox/Checkbox.d.ts +2 -8
  17. package/dist/components/forms/checkbox/Checkbox.js +3 -5
  18. package/dist/components/forms/input/Input.d.ts +1 -0
  19. package/dist/components/forms/input/Input.js +2 -4
  20. package/dist/components/forms/input/Input.module.css +9 -11
  21. package/dist/components/forms/input-container/InputContainer.d.ts +2 -1
  22. package/dist/components/forms/input-container/InputContainer.js +3 -3
  23. package/dist/components/forms/input-container/InputContainer.module.css +65 -0
  24. package/dist/components/forms/radio-buttons/RadioButton.d.ts +36 -0
  25. package/dist/components/forms/radio-buttons/RadioButton.js +26 -0
  26. package/dist/components/forms/radio-buttons/RadioButtonGroup.d.ts +25 -0
  27. package/dist/components/forms/radio-buttons/RadioButtonGroup.js +19 -0
  28. package/dist/components/forms/radio-buttons/RadioButtons.module.css +117 -0
  29. package/dist/components/forms/select/Select.d.ts +1 -1
  30. package/dist/components/forms/select/Select.js +3 -3
  31. package/dist/components/forms/text-area/Textarea.js +3 -3
  32. package/dist/components/forms/text-area/Textarea.module.css +8 -1
  33. package/dist/components/headline/Headline.d.ts +2 -7
  34. package/dist/components/headline/Headline.js +5 -2
  35. package/dist/components/headline/Headline.module.css +61 -2
  36. package/dist/components/hyperlink/Hyperlink.d.ts +1 -0
  37. package/dist/components/hyperlink/Hyperlink.js +5 -1
  38. package/dist/components/icon/Icon.module.css +1 -0
  39. package/dist/components/interval-select/IntervalSelect.js +1 -1
  40. package/dist/components/nav-bar/NavBar.d.ts +24 -6
  41. package/dist/components/overlay/modal/provider/ModalProvider.d.ts +2 -2
  42. package/dist/components/overlay/modal/provider/ModalProvider.js +24 -25
  43. package/dist/components/overlay/side-panel/SidePanel.d.ts +12 -4
  44. package/dist/components/overlay/side-panel/SidePanel.js +60 -4
  45. package/dist/components/overlay/side-panel/SidePanel.module.css +151 -28
  46. package/dist/components/overlay/side-panel/useSidePanel.d.ts +1 -1
  47. package/dist/components/overlay/side-panel/useSidePanel.js +2 -2
  48. package/dist/components/page-layout/PageLayout.js +0 -2
  49. package/dist/components/popover/Popover.js +33 -14
  50. package/dist/components/popover/Popover.module.css +0 -4
  51. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.d.ts +5 -5
  52. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +16 -8
  53. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.module.css +0 -3
  54. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.d.ts +3 -1
  55. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +4 -3
  56. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +109 -79
  57. package/dist/components/sidebar/components/sidebar-items/SidebarItems.js +16 -3
  58. package/dist/components/sidebar/components/sidebar-items/SidebarItems.module.css +20 -0
  59. package/dist/components/sidebar/providers/SidebarProvider.js +25 -46
  60. package/dist/components/skeleton-loader/SkeletonLoader.d.ts +1 -1
  61. package/dist/components/skeleton-loader/SkeletonLoader.js +15 -12
  62. package/dist/components/state-page/StatePage.d.ts +9 -0
  63. package/dist/components/state-page/StatePage.js +20 -0
  64. package/dist/components/state-page/StatePage.module.css +9 -0
  65. package/dist/components/state-page/empty.d.ts +2 -0
  66. package/dist/components/state-page/empty.js +2 -0
  67. package/dist/components/state-page/error.d.ts +2 -0
  68. package/dist/components/state-page/error.js +2 -0
  69. package/dist/components/state-page/notFound.d.ts +2 -0
  70. package/dist/components/state-page/notFound.js +2 -0
  71. package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +19 -0
  72. package/dist/components/sticky-footer-layout/StickyFooterLayout.js +27 -0
  73. package/dist/components/table/Table.js +4 -4
  74. package/dist/components/table/Table.module.css +168 -60
  75. package/dist/components/table/components/empty-state/EmptyState.d.ts +1 -1
  76. package/dist/components/table/components/empty-state/EmptyState.js +6 -7
  77. package/dist/components/toast/Toast.js +5 -1
  78. package/dist/components/toast/Toast.module.css +40 -15
  79. package/dist/components/toast/provider/ToastProvider.js +1 -0
  80. package/dist/hooks/useTimeDuration.js +9 -3
  81. package/dist/hooks/useViewportFill.js +1 -0
  82. package/dist/index.d.ts +6 -0
  83. package/dist/index.js +6 -1
  84. package/dist/src/styles/styles.css +22 -3
  85. package/dist/styles/styles.css +22 -3
  86. package/dist/styles/themes/dbc/dark.css +1 -1
  87. package/dist/styles/themes/dbc/light.css +2 -1
  88. package/package.json +1 -1
@@ -5,9 +5,9 @@ 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, utcMillisFromIso, toMaskedFromDate, } from './dateTimeHelpers';
8
9
  import styles from './DateTimePicker.module.css';
9
- import { maskRange, maskSingle, parseLooseDateOrDateTime, parseLooseRange, toMaskedFromDate, toMaskedRange, } from './dateTimeHelpers';
10
- /* ---------- Date grid helpers (UTC) ---------- */
10
+ /* ---------- Date grid helpers (UTC date-only cells) ---------- */
11
11
  const dUTC = (y, m, day) => new Date(Date.UTC(y, m, day));
12
12
  const addDaysUTC = (utcDate, n) => dUTC(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate() + n);
13
13
  const startOfMonthUTC = (utcDate) => dUTC(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), 1);
@@ -15,14 +15,16 @@ 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
- const toUTCDateOnly = (local) => dUTC(local.getFullYear(), local.getMonth(), local.getDate());
18
+ // IMPORTANT: treat local calendar date as the source of truth for the grid.
19
+ // Build an equivalent "UTC date-only" for comparisons.
20
+ const toUTCDateOnlyFromLocal = (local) => dUTC(local.getFullYear(), local.getMonth(), local.getDate());
19
21
  const startOfWeekUTC = (utcDate, weekStartsOn) => {
20
22
  const dow = utcDate.getUTCDay();
21
23
  const diff = (dow - weekStartsOn + 7) % 7;
22
24
  return addDaysUTC(utcDate, -diff);
23
25
  };
24
26
  function buildMonthGrid(anchorLocalDate, weekStartsOn) {
25
- const anchorUTC = toUTCDateOnly(anchorLocalDate);
27
+ const anchorUTC = toUTCDateOnlyFromLocal(anchorLocalDate);
26
28
  const firstUTC = startOfWeekUTC(startOfMonthUTC(anchorUTC), weekStartsOn);
27
29
  const cells = [];
28
30
  for (let i = 0; i < 42; i++)
@@ -33,13 +35,6 @@ const isBetweenUTC = (d, a, b) => {
33
35
  const t = +d, s = +a, e = +b;
34
36
  return t >= Math.min(s, e) && t <= Math.max(s, e);
35
37
  };
36
- function composeLocalDateTimeISO(utcDateOnly, hh, mm) {
37
- const y = utcDateOnly.getUTCFullYear();
38
- const m = utcDateOnly.getUTCMonth();
39
- const d = utcDateOnly.getUTCDate();
40
- const local = new Date(y, m, d, hh, mm, 0, 0);
41
- return local.toISOString();
42
- }
43
38
  /* ---------- Formatting (exposed but input uses mask) ---------- */
44
39
  function defaultFormatDate(d, { locale, enableTime }) {
45
40
  const opts = enableTime
@@ -57,40 +52,77 @@ function defaultFormatRange(s, e, opts) {
57
52
  return '';
58
53
  }
59
54
  const cx = (...classes) => classes.filter(Boolean).join(' ');
60
- export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'single', value, onChange, enableTime = false, timeStep = 15, min, max, locale = typeof navigator !== 'undefined' ? navigator.language : 'da-DK', weekStartsOn = 1, presets, inputProps, formatDate = defaultFormatDate, // still exposed, not used for input text
61
- formatRange = defaultFormatRange, // still exposed, not used for input text
62
- }, _ref) {
55
+ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'single', value, onChange, enableTime = false, timeStep = 15, min, max, locale = typeof navigator !== 'undefined' ? navigator.language : 'da-DK', weekStartsOn = 1, presets, inputProps, formatDate = defaultFormatDate, formatRange = defaultFormatRange, }, _ref) {
63
56
  void formatDate;
64
57
  void formatRange;
65
58
  const popRef = useRef(null);
66
59
  const todayLocal = useMemo(() => new Date(), []);
60
+ // ---- local anchor from controlled value ----
67
61
  const initialAnchor = useMemo(() => {
68
- if (mode === 'single' && value instanceof Date && value)
69
- return value;
70
- if (mode === 'range' && value && typeof value === 'object' && 'start' in value && value.start)
71
- return value.start;
62
+ var _a, _b;
63
+ if (mode === 'single') {
64
+ if (typeof value === 'string')
65
+ return (_a = localDateFromIso(value)) !== null && _a !== void 0 ? _a : todayLocal;
66
+ return todayLocal;
67
+ }
68
+ if (mode === 'range' && value && typeof value === 'object' && 'start' in value && value.start) {
69
+ return (_b = localDateFromIso(value.start)) !== null && _b !== void 0 ? _b : todayLocal;
70
+ }
72
71
  return todayLocal;
73
72
  }, [mode, value, todayLocal]);
74
73
  const [monthAnchor, setMonthAnchor] = useState(initialAnchor);
74
+ useEffect(() => setMonthAnchor(initialAnchor), [initialAnchor]);
75
+ // time defaults (local)
75
76
  const [timeHH, setTimeHH] = useState(todayLocal.getHours());
76
77
  const [timeMM, setTimeMM] = useState(Math.floor(todayLocal.getMinutes() / timeStep) * timeStep);
78
+ // If datetime value changes externally, keep HH/MM in sync
79
+ useEffect(() => {
80
+ if (mode === 'single' && enableTime && typeof value === 'string') {
81
+ const d = localDateFromIso(value);
82
+ if (!d)
83
+ return;
84
+ setTimeHH(d.getHours());
85
+ setTimeMM(Math.floor(d.getMinutes() / timeStep) * timeStep);
86
+ }
87
+ }, [mode, enableTime, value, timeStep]);
77
88
  const [hoverUTC, setHoverUTC] = useState(null);
78
89
  const cellsUTC = useMemo(() => buildMonthGrid(monthAnchor, weekStartsOn), [monthAnchor, weekStartsOn]);
79
- const monthStartUTC = useMemo(() => startOfMonthUTC(toUTCDateOnly(monthAnchor)), [monthAnchor]);
80
- const monthEndUTC = useMemo(() => endOfMonthUTC(toUTCDateOnly(monthAnchor)), [monthAnchor]);
90
+ const monthStartUTC = useMemo(() => startOfMonthUTC(toUTCDateOnlyFromLocal(monthAnchor)), [monthAnchor]);
91
+ const monthEndUTC = useMemo(() => endOfMonthUTC(toUTCDateOnlyFromLocal(monthAnchor)), [monthAnchor]);
81
92
  const weekdayFmt = useMemo(() => new Intl.DateTimeFormat(locale, { weekday: 'short' }), [locale]);
82
93
  const monthFmt = useMemo(() => new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }), [locale]);
83
- const selectedUTC_single = mode === 'single' && value instanceof Date && value ? toUTCDateOnly(value) : null;
84
- const selectedUTC_start = mode === 'range' && value && typeof value === 'object' && 'start' in value && value.start
85
- ? toUTCDateOnly(value.start)
86
- : null;
87
- const selectedUTC_end = mode === 'range' && value && typeof value === 'object' && 'end' in value && value.end
88
- ? toUTCDateOnly(value.end)
89
- : null;
94
+ // ---- selection state for grid (compare in UTC date-only) ----
95
+ const selectedUTC_single = useMemo(() => {
96
+ if (mode !== 'single')
97
+ return null;
98
+ if (typeof value !== 'string')
99
+ return null;
100
+ const d = localDateFromIso(value);
101
+ if (!d)
102
+ return null;
103
+ return toUTCDateOnlyFromLocal(d);
104
+ }, [mode, value]);
105
+ const selectedUTC_start = useMemo(() => {
106
+ if (mode !== 'range' || !value || typeof value !== 'object' || !('start' in value))
107
+ return null;
108
+ if (!value.start)
109
+ return null;
110
+ const d = localDateFromIso(value.start);
111
+ return d ? toUTCDateOnlyFromLocal(d) : null;
112
+ }, [mode, value]);
113
+ const selectedUTC_end = useMemo(() => {
114
+ if (mode !== 'range' || !value || typeof value !== 'object' || !('end' in value))
115
+ return null;
116
+ if (!value.end)
117
+ return null;
118
+ const d = localDateFromIso(value.end);
119
+ return d ? toUTCDateOnlyFromLocal(d) : null;
120
+ }, [mode, value]);
90
121
  const isDisabledUTC = useCallback((utcDay) => {
91
- if (min && utcDay < toUTCDateOnly(min))
122
+ // Interpret min/max as local-day constraints for UI
123
+ if (min && utcDay < toUTCDateOnlyFromLocal(min))
92
124
  return true;
93
- if (max && utcDay > toUTCDateOnly(max))
125
+ if (max && utcDay > toUTCDateOnlyFromLocal(max))
94
126
  return true;
95
127
  return false;
96
128
  }, [min, max]);
@@ -98,29 +130,33 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
98
130
  var _a, _b;
99
131
  if (isDisabledUTC(utcDay))
100
132
  return;
133
+ // utcDay's UTC Y/M/D corresponds to the *local calendar day* shown in UI.
134
+ const y = utcDay.getUTCFullYear();
135
+ const m0 = utcDay.getUTCMonth();
136
+ const d = utcDay.getUTCDate();
101
137
  if (mode === 'single') {
102
- if (enableTime) {
103
- const iso = composeLocalDateTimeISO(utcDay, timeHH, timeMM);
104
- onChange(new Date(iso));
105
- }
106
- else {
107
- onChange(new Date(utcDay.getTime()));
108
- }
138
+ const iso = enableTime
139
+ ? isoFromLocalParts(y, m0, d, timeHH, timeMM)
140
+ : isoFromLocalParts(y, m0, d, 0, 0);
141
+ onChange(iso);
109
142
  (_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
110
143
  return;
111
144
  }
112
- const curr = value && typeof value === 'object' && 'start' in value ? value : { start: null, end: null };
145
+ // range (date-only in UI, but emitted as ISO instants at local midnight)
146
+ const curr = value && typeof value === 'object' && 'start' in value
147
+ ? value
148
+ : { start: null, end: null };
149
+ const picked = isoFromLocalParts(y, m0, d, 0, 0);
113
150
  if (!curr.start || (curr.start && curr.end)) {
114
- onChange({ start: new Date(utcDay.getTime()), end: null });
115
- }
116
- else {
117
- const startUTC = toUTCDateOnly(curr.start);
118
- const endUTC = utcDay;
119
- const s = new Date(Math.min(+startUTC, +endUTC));
120
- const e = new Date(Math.max(+startUTC, +endUTC));
121
- onChange({ start: s, end: e });
122
- (_b = popRef.current) === null || _b === void 0 ? void 0 : _b.close();
151
+ onChange({ start: picked, end: null });
152
+ return;
123
153
  }
154
+ const a = utcMillisFromIso(curr.start);
155
+ const b = utcMillisFromIso(picked);
156
+ const start = a <= b ? curr.start : picked;
157
+ const end = a <= b ? picked : curr.start;
158
+ onChange({ start, end });
159
+ (_b = popRef.current) === null || _b === void 0 ? void 0 : _b.close();
124
160
  };
125
161
  const gridRef = useRef(null);
126
162
  useEffect(() => {
@@ -143,7 +179,7 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
143
179
  ].includes(e.key)) {
144
180
  e.preventDefault();
145
181
  }
146
- const anchor = toUTCDateOnly(monthAnchor);
182
+ const anchor = toUTCDateOnlyFromLocal(monthAnchor);
147
183
  const move = (days) => setMonthAnchor(prev => addDaysLocal(prev, days));
148
184
  switch (e.key) {
149
185
  case 'ArrowLeft':
@@ -177,20 +213,32 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
177
213
  }, [monthAnchor]);
178
214
  const hours = useMemo(() => Array.from({ length: 24 }, (_, i) => i), []);
179
215
  const minutes = useMemo(() => Array.from({ length: Math.floor(60 / (timeStep || 1)) }, (_, i) => i * (timeStep || 1)), [timeStep]);
180
- // ---- Input display: follow mask format (not Intl) ----
216
+ // ---- Input display: always local ----
181
217
  const formatted = useMemo(() => {
182
- var _a, _b;
183
218
  if (mode === 'single') {
184
- return value instanceof Date && value ? toMaskedFromDate(value, enableTime) : '';
219
+ if (typeof value !== 'string')
220
+ return '';
221
+ const d = localDateFromIso(value);
222
+ return d ? toMaskedFromDate(d, enableTime) : '';
185
223
  }
186
224
  const v = value;
187
- return toMaskedRange((_a = v === null || v === void 0 ? void 0 : v.start) !== null && _a !== void 0 ? _a : null, (_b = v === null || v === void 0 ? void 0 : v.end) !== null && _b !== void 0 ? _b : null, enableTime);
225
+ const s = typeof (v === null || v === void 0 ? void 0 : v.start) === 'string' ? localDateFromIso(v.start) : null;
226
+ const e = typeof (v === null || v === void 0 ? void 0 : v.end) === 'string' ? localDateFromIso(v.end) : null;
227
+ const ss = s ? toMaskedFromDate(s, false) : '';
228
+ const ee = e ? toMaskedFromDate(e, false) : '';
229
+ if (ss && ee)
230
+ return `${ss} – ${ee}`;
231
+ if (ss)
232
+ return `${ss} –`;
233
+ if (ee)
234
+ return `– ${ee}`;
235
+ return '';
188
236
  }, [mode, value, enableTime]);
189
237
  const [text, setText] = useState(formatted);
190
- const [dirty, setDirty] = useState(false); // while user is typing
238
+ const [dirty, setDirty] = useState(false);
191
239
  useEffect(() => {
192
240
  if (!dirty)
193
- setText(formatted); // keep in sync when external value changes
241
+ setText(formatted);
194
242
  }, [formatted, dirty]);
195
243
  const commitTypedValue = useCallback(() => {
196
244
  if (!text.trim()) {
@@ -203,16 +251,19 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
203
251
  }
204
252
  if (mode === 'single') {
205
253
  const dLocal = parseLooseDateOrDateTime(text);
206
- if (dLocal) {
207
- onChange(dLocal);
208
- setMonthAnchor(dLocal);
209
- setDirty(false);
210
- }
254
+ if (!dLocal)
255
+ return;
256
+ // If enableTime=false, parseLooseDateOrDateTime returns 00:00 local -> still OK.
257
+ onChange(isoFromLocalDate(dLocal));
258
+ setMonthAnchor(dLocal);
259
+ setDirty(false);
211
260
  return;
212
261
  }
213
262
  const r = parseLooseRange(text);
214
263
  if (r) {
215
- onChange({ start: r.start, end: r.end });
264
+ const startIso = isoFromLocalParts(r.start.getFullYear(), r.start.getMonth(), r.start.getDate(), 0, 0);
265
+ const endIso = isoFromLocalParts(r.end.getFullYear(), r.end.getMonth(), r.end.getDate(), 0, 0);
266
+ onChange({ start: startIso, end: endIso });
216
267
  setMonthAnchor(r.start);
217
268
  setDirty(false);
218
269
  }
@@ -238,10 +289,10 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
238
289
  const fallbackPlaceholder = mode === 'single' ? 'Vælg dato' : 'Vælg interval';
239
290
  return (_jsx(Popover, { ref: popRef, trigger: toggle => {
240
291
  var _a, _b;
241
- return (_jsx("div", { onClick: toggle, className: styles.triggerWrap, children: _jsx(Input, { ...inputProps, placeholder: (_a = inputProps === null || inputProps === void 0 ? void 0 : inputProps.placeholder) !== null && _a !== void 0 ? _a : fallbackPlaceholder, value: dirty ? text : formatted, onInput: e => {
292
+ 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 => {
242
293
  setDirty(true);
243
294
  const raw = e.target.value;
244
- const masked = mode === 'single' ? maskSingle(raw, enableTime) : maskRange(raw, enableTime);
295
+ const masked = mode === 'single' ? maskSingle(raw, enableTime) : maskRange(raw, false);
245
296
  setText(masked);
246
297
  }, onBlur: commitTypedValue, onKeyDown: e => {
247
298
  var _a;
@@ -256,14 +307,16 @@ formatRange = defaultFormatRange, // still exposed, not used for input text
256
307
  }, 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: () => {
257
308
  var _a;
258
309
  const r = p.getRange();
259
- onChange({ start: r.start, end: r.end });
310
+ const startIso = isoFromLocalParts(r.start.getFullYear(), r.start.getMonth(), r.start.getDate(), 0, 0);
311
+ const endIso = isoFromLocalParts(r.end.getFullYear(), r.end.getMonth(), r.end.getDate(), 0, 0);
312
+ onChange({ start: startIso, end: endIso });
260
313
  setDirty(false);
261
- setText(toMaskedRange(r.start, r.end, enableTime));
314
+ setText(`${toMaskedFromDate(r.start, false)} – ${toMaskedFromDate(r.end, false)}`);
262
315
  setMonthAnchor(r.start);
263
316
  (_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
264
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(monthAnchor, -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(addMonthsLocal(monthAnchor, 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) => {
265
318
  const inThisMonth = utcDay >= monthStartUTC && utcDay <= monthEndUTC;
266
- const isToday = sameDayUTC(utcDay, toUTCDateOnly(todayLocal));
319
+ const isToday = sameDayUTC(utcDay, toUTCDateOnlyFromLocal(todayLocal));
267
320
  const disabledDay = isDisabledUTC(utcDay);
268
321
  let selected = false;
269
322
  let inRange = false;
@@ -2,10 +2,22 @@ export declare const digits: (s: string) => string;
2
2
  export declare function maskDateEU(text: string): string;
3
3
  export declare function maskTimeHM(text: string): string;
4
4
  export declare function maskSingle(text: string, enableTime: boolean): string;
5
- export declare function maskRange(text: string, enableTime: boolean): string;
5
+ export declare function maskRange(text: string, _enableTime: boolean): string;
6
6
  export declare const pad2: (n: number) => string;
7
+ export type UtcIsoString = string;
8
+ export declare function isUtcIsoString(v: unknown): v is UtcIsoString;
9
+ export declare function utcMillisFromIso(iso: UtcIsoString): number;
10
+ /**
11
+ * Build a *local* Date from y/m/d/hh/mm and return UTC ISO string (Z).
12
+ * This keeps the UI meaning "local wall time", while emitting a stable UTC instant.
13
+ */
14
+ 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;
18
+ /** For anchoring the calendar safely from a UTC ISO string. */
19
+ export declare function localDateFromIso(iso: UtcIsoString): Date | null;
7
20
  export declare function toMaskedFromDate(d: Date, enableTime: boolean): string;
8
- export declare function toMaskedRange(start: Date | null, end: Date | null, enableTime: boolean): string;
9
21
  export declare function parseLooseDateOrDateTime(input: string): Date | null;
10
22
  export declare function parseLooseRange(input: string): {
11
23
  start: Date;
@@ -35,19 +35,44 @@ export function maskSingle(text, enableTime) {
35
35
  return timePart ? `${datePart} ${timePart}` : datePart;
36
36
  }
37
37
  // Range: mask both sides around common separators (–, -, to, til)
38
- export function maskRange(text, enableTime) {
38
+ export function maskRange(text, _enableTime) {
39
+ // NOTE: range is date-only in the UI
39
40
  const sepRe = /\s*(?:–|-|to|til)\s*/i;
40
41
  const parts = text.split(sepRe);
41
- if (parts.length === 1) {
42
- // user typing first side
43
- return maskSingle(parts[0], enableTime);
44
- }
45
- const a = maskSingle(parts[0], enableTime);
46
- const b = maskSingle(parts.slice(1).join(' '), enableTime); // everything after first sep
42
+ if (parts.length === 1)
43
+ return maskSingle(parts[0], false);
44
+ const a = maskSingle(parts[0], false);
45
+ const b = maskSingle(parts.slice(1).join(' '), false);
47
46
  return `${a} – ${b}`.trim();
48
47
  }
49
48
  // Pad helper
50
49
  export const pad2 = (n) => String(n).padStart(2, '0');
50
+ export function isUtcIsoString(v) {
51
+ return typeof v === 'string' && /Z$/.test(v) && !Number.isNaN(Date.parse(v));
52
+ }
53
+ export function utcMillisFromIso(iso) {
54
+ return Date.parse(iso);
55
+ }
56
+ /**
57
+ * Build a *local* Date from y/m/d/hh/mm and return UTC ISO string (Z).
58
+ * This keeps the UI meaning "local wall time", while emitting a stable UTC instant.
59
+ */
60
+ export function isoFromLocalParts(y, m0, // 0-based
61
+ d, hh = 0, mm = 0) {
62
+ return new Date(y, m0, d, hh, mm, 0, 0).toISOString();
63
+ }
64
+ /** Convert a local Date to a UTC ISO string (Z). */
65
+ export function isoFromLocalDate(dLocal) {
66
+ return dLocal.toISOString();
67
+ }
68
+ /** For anchoring the calendar safely from a UTC ISO string. */
69
+ export function localDateFromIso(iso) {
70
+ const ms = Date.parse(iso);
71
+ if (Number.isNaN(ms))
72
+ return null;
73
+ return new Date(ms); // local rendering
74
+ }
75
+ /* ---------- Formatting (UI shows local) ---------- */
51
76
  // From Date → "DD-MM-YYYY" or "DD-MM-YYYY HH:mm" (local time)
52
77
  export function toMaskedFromDate(d, enableTime) {
53
78
  const dd = pad2(d.getDate());
@@ -58,16 +83,6 @@ export function toMaskedFromDate(d, enableTime) {
58
83
  out += ` ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
59
84
  return out;
60
85
  }
61
- // From start/end → "DD-MM-YYYY – DD-MM-YYYY" (+ optional time)
62
- export function toMaskedRange(start, end, enableTime) {
63
- if (start && end)
64
- return `${toMaskedFromDate(start, enableTime)} – ${toMaskedFromDate(end, enableTime)}`;
65
- if (start)
66
- return `${toMaskedFromDate(start, enableTime)} –`;
67
- if (end)
68
- return `– ${toMaskedFromDate(end, enableTime)}`;
69
- return '';
70
- }
71
86
  /* ---------- Parsing helpers (no deps) ---------- */
72
87
  // Accepts: YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY (+ optional HH:mm)
73
88
  export function parseLooseDateOrDateTime(input) {
@@ -7,7 +7,7 @@ interface CheckboxProps {
7
7
  onChange?: (checked: boolean, event: React.MouseEvent<HTMLButtonElement>) => void;
8
8
  variant?: Variant;
9
9
  disabled?: boolean;
10
- /** Text shown next to the box (per-item label) */
10
+ modified?: boolean;
11
11
  label?: string;
12
12
  size?: Size;
13
13
  containerLabel?: string;
@@ -17,15 +17,9 @@ interface CheckboxProps {
17
17
  labelWidth?: string;
18
18
  fullWidth?: boolean;
19
19
  required?: boolean;
20
- /**
21
- * If true, do NOT wrap with InputContainer.
22
- * Use this inside CheckboxGroup (so you don't get group-form layout per item).
23
- */
24
20
  noContainer?: boolean;
25
- /** Optional id for accessibility (label htmlFor) */
26
21
  id?: string;
27
- /** Data attributes pass-through */
28
22
  'data-cy'?: string;
29
23
  }
30
- export declare function Checkbox({ checked: controlled, onChange, variant, disabled, label, size, containerLabel, error, helpText, orientation, labelWidth, fullWidth, required, noContainer, id, 'data-cy': dataCy, }: CheckboxProps): JSX.Element;
24
+ export declare function Checkbox({ checked: controlled, onChange, variant, disabled, label, size, modified, containerLabel, error, helpText, orientation, labelWidth, fullWidth, required, noContainer, id, 'data-cy': dataCy, }: CheckboxProps): JSX.Element;
31
25
  export {};
@@ -4,7 +4,7 @@ import { Check } from 'lucide-react';
4
4
  import { useId, useState } from 'react';
5
5
  import styles from './Checkbox.module.css';
6
6
  import { InputContainer } from '../input-container/InputContainer';
7
- export function Checkbox({ checked: controlled, onChange, variant = 'outlined', disabled, label, size = 'md', containerLabel, error, helpText, orientation = 'horizontal', labelWidth = '120px', fullWidth = false, required = false, noContainer = false, id, 'data-cy': dataCy, }) {
7
+ export function Checkbox({ checked: controlled, onChange, variant = 'outlined', disabled, label, size = 'md', modified, containerLabel, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required = false, noContainer = false, id, 'data-cy': dataCy, }) {
8
8
  const [internal, setInternal] = useState(false);
9
9
  const isChecked = controlled !== null && controlled !== void 0 ? controlled : internal;
10
10
  const generatedId = useId();
@@ -19,9 +19,7 @@ export function Checkbox({ checked: controlled, onChange, variant = 'outlined',
19
19
  const content = (_jsxs("span", { className: styles.container, "data-cy": dataCy, children: [_jsx("button", { id: controlId, disabled: disabled, type: "button", role: "checkbox", "aria-checked": isChecked, "aria-disabled": disabled || undefined, "aria-invalid": Boolean(error) || undefined, onClick: toggle, className: [styles.checkbox, isChecked ? styles.checked : '', styles[variant], styles[size]]
20
20
  .filter(Boolean)
21
21
  .join(' '), children: isChecked && _jsx(Check, { className: styles.icon }) }), label && (_jsx("label", { className: styles.label, htmlFor: controlId, children: label }))] }));
22
- // For CheckboxGroup use-case
23
- if (noContainer || (!containerLabel && !error))
22
+ if (noContainer)
24
23
  return content;
25
- // Standalone form field use-case
26
- return (_jsx(InputContainer, { label: containerLabel, htmlFor: controlId, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: content }));
24
+ return (_jsx(InputContainer, { modified: modified, label: containerLabel, htmlFor: controlId, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: content }));
27
25
  }
@@ -15,6 +15,7 @@ export type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size
15
15
  buttonIcon?: React.ReactNode;
16
16
  tooltip?: React.ReactNode;
17
17
  tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
18
+ modified?: boolean;
18
19
  };
19
20
  /**
20
21
  * Explicit exported type annotation is required with --isolatedDeclarations.
@@ -23,9 +23,7 @@ function mergeRefs(...refs) {
23
23
  */
24
24
  export const Input = forwardRef(function Input({
25
25
  // InputContainer props
26
- label, error, helpText, orientation = 'horizontal', labelWidth = '120px', fullWidth = false, required,
27
- // ✅ Input-level tooltip props
28
- tooltip, tooltipPlacement = 'right',
26
+ label, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required, tooltip, tooltipPlacement = 'right', modified,
29
27
  // Input-only props
30
28
  icon, autoFocus, minWidth, width, inputSize = 'md', variant = 'outlined', onClear, onButtonClick, buttonLabel, buttonIcon,
31
29
  // Native input props
@@ -50,7 +48,7 @@ id, style, className, ...inputProps }, ref) {
50
48
  placement: tooltipPlacement,
51
49
  offset: 8,
52
50
  });
53
- return (_jsx(InputContainer, { label: label, htmlFor: inputId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, children: _jsxs("div", { style: rootStyle, className: [
51
+ return (_jsx(InputContainer, { label: label, htmlFor: inputId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: _jsxs("div", { style: rootStyle, className: [
54
52
  styles.container,
55
53
  fullWidth ? styles.fullWidth : '',
56
54
  onClear ? styles.withClear : '',
@@ -4,7 +4,6 @@
4
4
  display: inline-flex;
5
5
  align-items: stretch;
6
6
  gap: 0;
7
-
8
7
  /* width control */
9
8
  inline-size: var(--input-width, auto);
10
9
  min-inline-size: var(--input-min-width, 0);
@@ -33,14 +32,13 @@
33
32
  flex: 1 1 auto;
34
33
  min-inline-size: 0; /* critical */
35
34
  inline-size: 100%;
36
-
35
+ background: var(--color-bg-surface);
37
36
  font-family: var(--font-family);
38
37
  font-size: var(--font-size-sm);
39
38
  line-height: var(--line-height-normal);
40
39
  box-sizing: border-box;
41
40
  text-overflow: ellipsis;
42
41
 
43
- background-color: var(--color-bg-surface);
44
42
  border: var(--border-width-thin) solid var(--color-border-default);
45
43
  border-radius: var(--border-radius-default);
46
44
 
@@ -53,6 +51,13 @@
53
51
  box-shadow var(--transition-fast) var(--ease-standard);
54
52
  }
55
53
 
54
+ .input:disabled {
55
+ background-color: var(--color-disabled-bg);
56
+ border: 0;
57
+ color: var(--color-disabled-fg);
58
+ cursor: not-allowed;
59
+ opacity: 0.5;
60
+ }
56
61
  /* Button group styling */
57
62
  .withButton .input {
58
63
  border-top-right-radius: 0;
@@ -75,26 +80,19 @@
75
80
  }
76
81
 
77
82
  .input:focus-visible {
78
- outline: none;
79
83
  border-color: var(--color-border-selected);
80
- box-shadow: var(--focus-ring);
81
84
  }
82
85
 
83
86
  /* Variants */
84
87
  .filled {
85
88
  background-color: var(--color-bg-surface);
86
- border: 0;
87
89
  }
88
90
  .standalone {
89
91
  border-radius: var(--border-radius-rounded);
90
92
  background-color: var(--color-bg-surface);
91
93
  box-shadow: var(--shadow-xs), var(--shadow-md);
92
94
  }
93
- .standalone:focus-visible {
94
- outline: none;
95
- border-color: var(--color-border-selected);
96
- box-shadow: var(--focus-ring);
97
- }
95
+
98
96
  .outlined {
99
97
  background-color: transparent;
100
98
  }
@@ -11,5 +11,6 @@ export interface InputContainerProps {
11
11
  orientation?: 'vertical' | 'horizontal';
12
12
  labelWidth?: string;
13
13
  labelAlignment?: 'top' | 'center';
14
+ modified?: boolean;
14
15
  }
15
- export declare function InputContainer({ label, htmlFor, error, helpText, helpTextAddition, fullWidth, required, children, orientation, labelWidth, }: InputContainerProps): React.ReactElement;
16
+ export declare function InputContainer({ label, htmlFor, error, helpText, helpTextAddition, fullWidth, required, children, orientation, labelWidth, modified, }: InputContainerProps): React.ReactElement;
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import styles from './InputContainer.module.css';
3
- export function InputContainer({ label, htmlFor, error, helpText, helpTextAddition, fullWidth = false, required = false, children, orientation = 'horizontal', labelWidth = '120px', }) {
3
+ export function InputContainer({ label, htmlFor, error, helpText, helpTextAddition, fullWidth = false, required = false, children, orientation = 'horizontal', labelWidth = '160px', modified = false, }) {
4
4
  const message = error !== null && error !== void 0 ? error : helpText;
5
5
  const messageClass = error ? styles.errorText : styles.helpText;
6
6
  const renderLabel = label && (_jsxs("label", { className: styles.label, htmlFor: htmlFor, children: [label, required && _jsx("span", { className: styles.required, children: " *" })] }));
7
7
  const renderMessageRow = (message || helpTextAddition) && (_jsxs("div", { className: `${messageClass} ${styles.messageRow}`, children: [_jsx("span", { children: message }), helpTextAddition && _jsx("span", { className: styles.helpTextAddition, children: helpTextAddition })] }));
8
8
  if (orientation === 'vertical') {
9
- return (_jsxs("div", { className: "dbc-flex dbc-flex-column dbc-gap-xs", style: { width: fullWidth ? '100%' : undefined }, children: [renderLabel, children, renderMessageRow] }));
9
+ return (_jsxs("div", { "data-modified": modified ? true : undefined, className: `dbc-flex dbc-flex-column dbc-gap-xs ${styles.inputContainer}`, style: { width: fullWidth ? '100%' : undefined }, children: [renderLabel, children, renderMessageRow] }));
10
10
  }
11
- return (_jsx("div", { className: styles.inputContainer, style: {
11
+ return (_jsx("div", { "data-modified": modified ? true : undefined, className: styles.inputContainer, style: {
12
12
  '--label-width': labelWidth,
13
13
  width: fullWidth ? '100%' : undefined,
14
14
  }, children: _jsxs("div", { className: `${styles.horizontal} dbc-flex dbc-flex-column dbc-gap-xs`, children: [_jsxs("div", { className: `${styles.labelContainer} dbc-flex dbc-items-center dbc-gap-xs`, children: [renderLabel, children] }), renderMessageRow] }) }));
@@ -6,6 +6,12 @@
6
6
  gap: var(--gap);
7
7
  }
8
8
 
9
+ .label {
10
+ color: var(--color-fg-default);
11
+ font-size: var(--font-size-sm);
12
+ font-weight: var(--font-weight-medium);
13
+ }
14
+
9
15
  .horizontal .errorText,
10
16
  .horizontal .helpText {
11
17
  margin-left: calc(var(--label-width) + var(--gap));
@@ -32,3 +38,62 @@
32
38
  color: var(--color-status-error);
33
39
  font-weight: bold;
34
40
  }
41
+
42
+ /* ---------------- MODIFIED FIELD (DIRECT CONTROL TINT) ---------------- */
43
+
44
+ /* Optional scan cue: left bar only (no box around control) */
45
+ .inputContainer[data-modified] {
46
+ border-left: var(--border-width-thick) solid var(--color-status-warning-border);
47
+ padding-left: var(--spacing-xs);
48
+ }
49
+
50
+ /**
51
+ * Tint "real controls" directly.
52
+ * This covers:
53
+ * - native input/textarea
54
+ * - button-based components (Select trigger, Checkbox button, etc.)
55
+ * - combobox triggers
56
+ */
57
+ .inputContainer[data-modified]
58
+ :is(input, textarea, button[data-forminput], [role='combobox'][data-forminput]) {
59
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 45%, var(--color-bg-surface));
60
+ }
61
+
62
+ /**
63
+ * If your controls also have borders, nudge them slightly warmer.
64
+ * (Keep subtle so it doesn't look like validation.)
65
+ */
66
+ .inputContainer[data-modified]
67
+ :is(input, textarea, button[data-forminput], [role='combobox'][data-forminput]) {
68
+ border-color: color-mix(
69
+ in srgb,
70
+ var(--color-status-warning-border) 35%,
71
+ var(--color-border-default)
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Checkbox/Radio special case:
77
+ * - Their "label" is inside children.
78
+ * - InputContainer’s own label uses .label class.
79
+ * We want to tint ONLY the child labels, not the container label column.
80
+ */
81
+ .inputContainer[data-modified] label:not(.label) {
82
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 35%, transparent);
83
+ border-radius: var(--border-radius-md);
84
+ padding: 2px 6px;
85
+ }
86
+
87
+ /* Correct disabled selector (no accidental “actionable” tint) */
88
+ .inputContainer[data-modified]
89
+ :is(input, textarea, button[data-forminput], [role='combobox'][data-forminput]):disabled,
90
+ .inputContainer[data-modified]
91
+ :is(
92
+ input,
93
+ textarea,
94
+ button[data-forminput],
95
+ [role='combobox'][data-forminput]
96
+ )[aria-disabled='true'] {
97
+ background-color: var(--color-disabled-bg);
98
+ border-color: var(--color-disabled-border);
99
+ }