@dbcdk/react-components 0.0.6 → 0.0.7

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.
@@ -6,125 +6,7 @@ import { Button } from '../../components/button/Button';
6
6
  import { Input } from '../../components/forms/input/Input';
7
7
  import { Popover } from '../../components/popover/Popover';
8
8
  import styles from './DateTimePicker.module.css';
9
- /* ---------- Mask helpers (no deps) ---------- */
10
- // Pull only 0-9
11
- const digits = (s) => (s.match(/\d/g) || []).join('');
12
- // DD-MM-YYYY
13
- function maskDateEU(text) {
14
- const d = digits(text).slice(0, 8);
15
- const dd = d.slice(0, 2);
16
- const mm = d.slice(2, 4);
17
- const yyyy = d.slice(4, 8);
18
- let out = dd;
19
- if (mm.length)
20
- out += (out ? '-' : '') + mm;
21
- if (yyyy.length)
22
- out += (out ? '-' : '') + yyyy;
23
- return out;
24
- }
25
- // HH:mm (24h)
26
- function maskTimeHM(text) {
27
- const d = digits(text).slice(0, 4);
28
- const hh = d.slice(0, 2);
29
- const mm = d.slice(2, 4);
30
- return mm.length ? `${hh}:${mm}` : hh;
31
- }
32
- // Single: "DD-MM-YYYY" or "DD-MM-YYYY HH:mm"
33
- function maskSingle(text, enableTime) {
34
- let t = text.trim().replace(/\s+/g, ' ');
35
- if (!enableTime)
36
- return maskDateEU(t);
37
- // split date + time by first space or 'T'
38
- const m = /^(.*?)[ T](.*)$/.exec(t);
39
- if (!m)
40
- return maskDateEU(t);
41
- const datePart = maskDateEU(m[1]);
42
- const timePart = maskTimeHM(m[2]);
43
- return timePart ? `${datePart} ${timePart}` : datePart;
44
- }
45
- // Range: mask both sides around common separators (–, -, to, til)
46
- function maskRange(text, enableTime) {
47
- const sepRe = /\s*(?:–|-|to|til)\s*/i;
48
- const parts = text.split(sepRe);
49
- if (parts.length === 1) {
50
- // user typing first side
51
- return maskSingle(parts[0], enableTime);
52
- }
53
- const a = maskSingle(parts[0], enableTime);
54
- const b = maskSingle(parts.slice(1).join(' '), enableTime); // everything after first sep
55
- return `${a} – ${b}`.trim();
56
- }
57
- // Pad helper
58
- const pad2 = (n) => String(n).padStart(2, '0');
59
- // From Date → "DD-MM-YYYY" or "DD-MM-YYYY HH:mm" (local time)
60
- function toMaskedFromDate(d, enableTime) {
61
- const dd = pad2(d.getDate());
62
- const mm = pad2(d.getMonth() + 1);
63
- const yyyy = String(d.getFullYear());
64
- let out = `${dd}-${mm}-${yyyy}`;
65
- if (enableTime)
66
- out += ` ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
67
- return out;
68
- }
69
- // From start/end → "DD-MM-YYYY – DD-MM-YYYY" (+ optional time)
70
- function toMaskedRange(start, end, enableTime) {
71
- if (start && end)
72
- return `${toMaskedFromDate(start, enableTime)} – ${toMaskedFromDate(end, enableTime)}`;
73
- if (start)
74
- return `${toMaskedFromDate(start, enableTime)} –`;
75
- if (end)
76
- return `– ${toMaskedFromDate(end, enableTime)}`;
77
- return '';
78
- }
79
- /* ---------- Parsing helpers (no deps) ---------- */
80
- // Accepts: YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY (+ optional HH:mm)
81
- function parseLooseDateOrDateTime(input) {
82
- const txt = input.trim().replace(/\s+/g, ' ');
83
- const dateTimeMatch = /^(?<date>[\d./-]{8,10})(?:[ T](?<hh>\d{1,2}):(?<mm>\d{2}))?$/i.exec(txt);
84
- if (!(dateTimeMatch === null || dateTimeMatch === void 0 ? void 0 : dateTimeMatch.groups))
85
- return null;
86
- const raw = dateTimeMatch.groups.date;
87
- const hh = dateTimeMatch.groups.hh ? parseInt(dateTimeMatch.groups.hh, 10) : 0;
88
- const mm = dateTimeMatch.groups.mm ? parseInt(dateTimeMatch.groups.mm, 10) : 0;
89
- if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
90
- return null;
91
- // Try YYYY-MM-DD first
92
- let y, m, d;
93
- const mIso = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw);
94
- if (mIso) {
95
- y = +mIso[1];
96
- m = +mIso[2] - 1;
97
- d = +mIso[3];
98
- }
99
- else {
100
- // Try DD-MM-YYYY or DD/MM/YYYY or DD.MM.YYYY
101
- const mEu = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4})$/.exec(raw);
102
- if (!mEu)
103
- return null;
104
- d = +mEu[1];
105
- m = +mEu[2] - 1;
106
- y = +mEu[3];
107
- }
108
- const local = new Date(y, m, d, hh, mm, 0, 0);
109
- if (Number.isNaN(local.getTime()))
110
- return null;
111
- // Guard: JS autocorrects invalid dates; re-validate exact Y/M/D
112
- if (local.getFullYear() !== y || local.getMonth() !== m || local.getDate() !== d)
113
- return null;
114
- return local;
115
- }
116
- // Parse a range string with separators: "–", "-", "to", "til"
117
- function parseLooseRange(input) {
118
- const sep = /\s*(?:–|-|to|til)\s*/i;
119
- const [a, b] = input.split(sep);
120
- if (!a || !b)
121
- return null;
122
- const s = parseLooseDateOrDateTime(a);
123
- const e = parseLooseDateOrDateTime(b);
124
- if (!s || !e)
125
- return null;
126
- return s <= e ? { start: s, end: e } : { start: e, end: s };
127
- }
9
+ import { maskRange, maskSingle, parseLooseDateOrDateTime, parseLooseRange, toMaskedFromDate, toMaskedRange, } from './dateTimeHelpers';
128
10
  /* ---------- Date grid helpers (UTC) ---------- */
129
11
  const dUTC = (y, m, day) => new Date(Date.UTC(y, m, day));
130
12
  const addDaysUTC = (utcDate, n) => dUTC(utcDate.getUTCFullYear(), utcDate.getUTCMonth(), utcDate.getUTCDate() + n);
@@ -0,0 +1,13 @@
1
+ export declare const digits: (s: string) => string;
2
+ export declare function maskDateEU(text: string): string;
3
+ export declare function maskTimeHM(text: string): string;
4
+ export declare function maskSingle(text: string, enableTime: boolean): string;
5
+ export declare function maskRange(text: string, enableTime: boolean): string;
6
+ export declare const pad2: (n: number) => string;
7
+ export declare function toMaskedFromDate(d: Date, enableTime: boolean): string;
8
+ export declare function toMaskedRange(start: Date | null, end: Date | null, enableTime: boolean): string;
9
+ export declare function parseLooseDateOrDateTime(input: string): Date | null;
10
+ export declare function parseLooseRange(input: string): {
11
+ start: Date;
12
+ end: Date;
13
+ } | null;
@@ -0,0 +1,119 @@
1
+ /* ---------- Mask helpers (no deps) ---------- */
2
+ // Pull only 0-9
3
+ export const digits = (s) => (s.match(/\d/g) || []).join('');
4
+ // DD-MM-YYYY
5
+ export function maskDateEU(text) {
6
+ const d = digits(text).slice(0, 8);
7
+ const dd = d.slice(0, 2);
8
+ const mm = d.slice(2, 4);
9
+ const yyyy = d.slice(4, 8);
10
+ let out = dd;
11
+ if (mm.length)
12
+ out += (out ? '-' : '') + mm;
13
+ if (yyyy.length)
14
+ out += (out ? '-' : '') + yyyy;
15
+ return out;
16
+ }
17
+ // HH:mm (24h)
18
+ export function maskTimeHM(text) {
19
+ const d = digits(text).slice(0, 4);
20
+ const hh = d.slice(0, 2);
21
+ const mm = d.slice(2, 4);
22
+ return mm.length ? `${hh}:${mm}` : hh;
23
+ }
24
+ // Single: "DD-MM-YYYY" or "DD-MM-YYYY HH:mm"
25
+ export function maskSingle(text, enableTime) {
26
+ let t = text.trim().replace(/\s+/g, ' ');
27
+ if (!enableTime)
28
+ return maskDateEU(t);
29
+ // split date + time by first space or 'T'
30
+ const m = /^(.*?)[ T](.*)$/.exec(t);
31
+ if (!m)
32
+ return maskDateEU(t);
33
+ const datePart = maskDateEU(m[1]);
34
+ const timePart = maskTimeHM(m[2]);
35
+ return timePart ? `${datePart} ${timePart}` : datePart;
36
+ }
37
+ // Range: mask both sides around common separators (–, -, to, til)
38
+ export function maskRange(text, enableTime) {
39
+ const sepRe = /\s*(?:–|-|to|til)\s*/i;
40
+ 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
47
+ return `${a} – ${b}`.trim();
48
+ }
49
+ // Pad helper
50
+ export const pad2 = (n) => String(n).padStart(2, '0');
51
+ // From Date → "DD-MM-YYYY" or "DD-MM-YYYY HH:mm" (local time)
52
+ export function toMaskedFromDate(d, enableTime) {
53
+ const dd = pad2(d.getDate());
54
+ const mm = pad2(d.getMonth() + 1);
55
+ const yyyy = String(d.getFullYear());
56
+ let out = `${dd}-${mm}-${yyyy}`;
57
+ if (enableTime)
58
+ out += ` ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
59
+ return out;
60
+ }
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
+ /* ---------- Parsing helpers (no deps) ---------- */
72
+ // Accepts: YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY (+ optional HH:mm)
73
+ export function parseLooseDateOrDateTime(input) {
74
+ const txt = input.trim().replace(/\s+/g, ' ');
75
+ const dateTimeMatch = /^(?<date>[\d./-]{8,10})(?:[ T](?<hh>\d{1,2}):(?<mm>\d{2}))?$/i.exec(txt);
76
+ if (!(dateTimeMatch === null || dateTimeMatch === void 0 ? void 0 : dateTimeMatch.groups))
77
+ return null;
78
+ const raw = dateTimeMatch.groups.date;
79
+ const hh = dateTimeMatch.groups.hh ? parseInt(dateTimeMatch.groups.hh, 10) : 0;
80
+ const mm = dateTimeMatch.groups.mm ? parseInt(dateTimeMatch.groups.mm, 10) : 0;
81
+ if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
82
+ return null;
83
+ // Try YYYY-MM-DD first
84
+ let y, m, d;
85
+ const mIso = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw);
86
+ if (mIso) {
87
+ y = +mIso[1];
88
+ m = +mIso[2] - 1;
89
+ d = +mIso[3];
90
+ }
91
+ else {
92
+ // Try DD-MM-YYYY or DD/MM/YYYY or DD.MM.YYYY
93
+ const mEu = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4})$/.exec(raw);
94
+ if (!mEu)
95
+ return null;
96
+ d = +mEu[1];
97
+ m = +mEu[2] - 1;
98
+ y = +mEu[3];
99
+ }
100
+ const local = new Date(y, m, d, hh, mm, 0, 0);
101
+ if (Number.isNaN(local.getTime()))
102
+ return null;
103
+ // Guard: JS autocorrects invalid dates; re-validate exact Y/M/D
104
+ if (local.getFullYear() !== y || local.getMonth() !== m || local.getDate() !== d)
105
+ return null;
106
+ return local;
107
+ }
108
+ // Parse a range string with separators: "–", "-", "to", "til"
109
+ export function parseLooseRange(input) {
110
+ const sep = /\s*(?:–|-|to|til)\s*/i;
111
+ const [a, b] = input.split(sep);
112
+ if (!a || !b)
113
+ return null;
114
+ const s = parseLooseDateOrDateTime(a);
115
+ const e = parseLooseDateOrDateTime(b);
116
+ if (!s || !e)
117
+ return null;
118
+ return s <= e ? { start: s, end: e } : { start: e, end: s };
119
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { ButtonVariant } from '../../components/button/Button';
3
+ import { InputContainer } from '../../components/forms/input-container/InputContainer';
4
+ type InputContainerProps = React.ComponentProps<typeof InputContainer>;
5
+ export type IntervalOption = {
6
+ label: string;
7
+ /** How many minutes back from baseDate (default: now). */
8
+ minutesAgo: number;
9
+ };
10
+ export type IntervalSelectValue = number | null;
11
+ export type IntervalSelectProps = Omit<InputContainerProps, 'children' | 'htmlFor' | 'tooltip' | 'tooltipPlacement'> & {
12
+ id?: string;
13
+ options: IntervalOption[];
14
+ selectedValue: IntervalSelectValue;
15
+ onChange: (date: Date, meta: {
16
+ minutesAgo: number;
17
+ option: IntervalOption;
18
+ }) => void;
19
+ /** Base date for the calculation; defaults to "now". */
20
+ baseDate?: Date;
21
+ placeholder?: string;
22
+ size?: 'sm' | 'md' | 'lg';
23
+ variant?: ButtonVariant;
24
+ onClear?: () => void;
25
+ dataCy?: string;
26
+ tooltip?: React.ReactNode;
27
+ tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
28
+ };
29
+ export declare function IntervalSelect({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, id, options, selectedValue, onChange, baseDate, placeholder, size, variant, onClear, dataCy, }: IntervalSelectProps): React.ReactNode;
30
+ export {};
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, Clock } from 'lucide-react';
4
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
5
+ import { Button } from '../../components/button/Button';
6
+ import { ClearButton } from '../../components/clear-button/ClearButton';
7
+ import { InputContainer } from '../../components/forms/input-container/InputContainer';
8
+ import { Menu } from '../../components/menu/Menu';
9
+ import { useTooltipTrigger } from '../../components/overlay/tooltip/useTooltipTrigger';
10
+ import { Popover } from '../../components/popover/Popover';
11
+ export function IntervalSelect({
12
+ // InputContainer props
13
+ label, error, helpText, orientation = 'vertical', labelWidth = '120px', fullWidth = true, required,
14
+ // tooltip
15
+ tooltip, tooltipPlacement = 'right',
16
+ // IntervalSelect props
17
+ id, options, selectedValue, onChange, baseDate, placeholder = 'Vælg interval', size, variant = 'outlined', onClear, dataCy, }) {
18
+ const generatedId = useId();
19
+ const controlId = id !== null && id !== void 0 ? id : `interval-select-${generatedId}`;
20
+ const describedById = `${controlId}-desc`;
21
+ const popoverRef = useRef(null);
22
+ const optionRefs = useRef([]);
23
+ const selectedIndex = useMemo(() => options.findIndex(o => o.minutesAgo === selectedValue), [options, selectedValue]);
24
+ const [activeIndex, setActiveIndex] = useState(selectedIndex >= 0 ? selectedIndex : 0);
25
+ useEffect(() => {
26
+ var _a;
27
+ (_a = optionRefs.current[activeIndex]) === null || _a === void 0 ? void 0 : _a.focus();
28
+ }, [activeIndex]);
29
+ const selected = useMemo(() => { var _a; return (_a = options.find(o => o.minutesAgo === selectedValue)) !== null && _a !== void 0 ? _a : null; }, [options, selectedValue]);
30
+ const tooltipEnabled = Boolean(tooltip);
31
+ const { triggerProps, id: tooltipId } = useTooltipTrigger({
32
+ content: tooltipEnabled ? tooltip : null,
33
+ placement: tooltipPlacement,
34
+ offset: 8,
35
+ });
36
+ const describedBy = (() => {
37
+ const ids = [];
38
+ if (error || helpText)
39
+ ids.push(describedById);
40
+ if (tooltipEnabled)
41
+ ids.push(tooltipId);
42
+ return ids.length ? ids.join(' ') : undefined;
43
+ })();
44
+ const handleCommit = (opt) => {
45
+ var _a;
46
+ const base = baseDate !== null && baseDate !== void 0 ? baseDate : new Date();
47
+ const dt = new Date(base.getTime() - opt.minutesAgo * 60000);
48
+ onChange(dt, { minutesAgo: opt.minutesAgo, option: opt });
49
+ (_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
50
+ };
51
+ const handleKeyDown = (e) => {
52
+ var _a;
53
+ switch (e.key) {
54
+ case 'ArrowDown':
55
+ e.preventDefault();
56
+ setActiveIndex(i => Math.min(i + 1, options.length - 1));
57
+ break;
58
+ case 'ArrowUp':
59
+ e.preventDefault();
60
+ setActiveIndex(i => Math.max(i - 1, 0));
61
+ break;
62
+ case 'Enter':
63
+ case ' ':
64
+ e.preventDefault();
65
+ if (options[activeIndex])
66
+ handleCommit(options[activeIndex]);
67
+ break;
68
+ case 'Escape':
69
+ e.preventDefault();
70
+ (_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
71
+ break;
72
+ }
73
+ };
74
+ return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, children: [_jsx(Popover, { ref: popoverRef, trigger: (onClick, icon) => (_jsx(Button, { ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'interval-select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
75
+ setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
76
+ onClick(e);
77
+ }, size: size, type: "button", "aria-haspopup": "listbox", "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [_jsx(Clock, { size: 14 }), selected ? selected.label : placeholder] }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
78
+ const isSelected = opt.minutesAgo === selectedValue;
79
+ const isActive = index === activeIndex;
80
+ return (_jsx(Menu.Item, { active: isActive, "aria-selected": isSelected, children: _jsxs("button", { ref: el => (optionRefs.current[index] = el), type: "button", tabIndex: isActive ? 0 : -1, onClick: () => handleCommit(opt), onFocus: () => setActiveIndex(index), style: { display: 'flex', alignItems: 'center', width: '100%' }, children: [_jsx("span", { style: { width: 16, display: 'inline-flex', justifyContent: 'center' }, children: isSelected ? _jsx(Check, {}) : null }), opt.label] }) }, opt.minutesAgo));
81
+ }) }) }), (error || helpText) && (_jsx("span", { id: describedById, style: { display: 'none' }, children: error !== null && error !== void 0 ? error : helpText }))] }));
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",