@dbcdk/react-components 0.0.9 → 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.
- package/dist/components/accordion/Accordion.d.ts +27 -0
- package/dist/components/accordion/Accordion.js +66 -0
- package/dist/components/accordion/Accordion.module.css +87 -0
- package/dist/components/button/Button.module.css +1 -0
- package/dist/components/circle/Circle.d.ts +4 -1
- package/dist/components/circle/Circle.js +2 -2
- package/dist/components/circle/Circle.module.css +54 -2
- package/dist/components/datetime-picker/DateTimePicker.d.ts +4 -8
- package/dist/components/datetime-picker/DateTimePicker.js +72 -92
- package/dist/components/datetime-picker/dateTimeHelpers.d.ts +14 -12
- package/dist/components/datetime-picker/dateTimeHelpers.js +25 -45
- package/dist/components/forms/checkbox/Checkbox.d.ts +2 -8
- package/dist/components/forms/checkbox/Checkbox.js +3 -5
- package/dist/components/forms/input/Input.d.ts +1 -0
- package/dist/components/forms/input/Input.js +2 -4
- package/dist/components/forms/input/Input.module.css +9 -11
- package/dist/components/forms/input-container/InputContainer.d.ts +2 -1
- package/dist/components/forms/input-container/InputContainer.js +3 -3
- package/dist/components/forms/input-container/InputContainer.module.css +65 -0
- package/dist/components/forms/radio-buttons/RadioButton.d.ts +36 -0
- package/dist/components/forms/radio-buttons/RadioButton.js +26 -0
- package/dist/components/forms/radio-buttons/RadioButtonGroup.d.ts +25 -0
- package/dist/components/forms/radio-buttons/RadioButtonGroup.js +19 -0
- package/dist/components/forms/radio-buttons/RadioButtons.module.css +117 -0
- package/dist/components/forms/select/Select.d.ts +1 -1
- package/dist/components/forms/select/Select.js +3 -3
- package/dist/components/forms/text-area/Textarea.js +3 -3
- package/dist/components/forms/text-area/Textarea.module.css +8 -1
- package/dist/components/headline/Headline.d.ts +2 -7
- package/dist/components/headline/Headline.js +5 -2
- package/dist/components/headline/Headline.module.css +61 -2
- package/dist/components/hyperlink/Hyperlink.d.ts +1 -0
- package/dist/components/hyperlink/Hyperlink.js +5 -1
- package/dist/components/icon/Icon.module.css +1 -0
- package/dist/components/interval-select/IntervalSelect.js +1 -1
- package/dist/components/nav-bar/NavBar.d.ts +24 -6
- package/dist/components/overlay/side-panel/SidePanel.d.ts +12 -4
- package/dist/components/overlay/side-panel/SidePanel.js +60 -4
- package/dist/components/overlay/side-panel/SidePanel.module.css +151 -28
- package/dist/components/overlay/side-panel/useSidePanel.d.ts +1 -1
- package/dist/components/overlay/side-panel/useSidePanel.js +2 -2
- package/dist/components/page-layout/PageLayout.js +0 -2
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.d.ts +5 -5
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +16 -8
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.module.css +0 -3
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.d.ts +3 -1
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +4 -3
- package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +109 -79
- package/dist/components/sidebar/components/sidebar-items/SidebarItems.js +16 -3
- package/dist/components/sidebar/components/sidebar-items/SidebarItems.module.css +20 -0
- package/dist/components/sidebar/providers/SidebarProvider.js +25 -46
- package/dist/components/skeleton-loader/SkeletonLoader.d.ts +1 -1
- package/dist/components/skeleton-loader/SkeletonLoader.js +15 -12
- package/dist/components/state-page/StatePage.d.ts +9 -0
- package/dist/components/state-page/StatePage.js +20 -0
- package/dist/components/state-page/StatePage.module.css +9 -0
- package/dist/components/state-page/empty.d.ts +2 -0
- package/dist/components/state-page/empty.js +2 -0
- package/dist/components/state-page/error.d.ts +2 -0
- package/dist/components/state-page/error.js +2 -0
- package/dist/components/state-page/notFound.d.ts +2 -0
- package/dist/components/state-page/notFound.js +2 -0
- package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +19 -0
- package/dist/components/sticky-footer-layout/StickyFooterLayout.js +27 -0
- package/dist/components/table/Table.js +4 -4
- package/dist/components/table/Table.module.css +168 -60
- package/dist/components/table/components/empty-state/EmptyState.d.ts +1 -1
- package/dist/components/table/components/empty-state/EmptyState.js +6 -7
- package/dist/components/toast/Toast.js +5 -1
- package/dist/components/toast/Toast.module.css +40 -15
- package/dist/components/toast/provider/ToastProvider.js +1 -0
- package/dist/hooks/useTimeDuration.js +9 -3
- package/dist/hooks/useViewportFill.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -1
- package/dist/src/styles/styles.css +22 -3
- package/dist/styles/styles.css +22 -3
- package/dist/styles/themes/dbc/dark.css +1 -1
- package/dist/styles/themes/dbc/light.css +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode, JSX } from 'react';
|
|
2
|
+
import { Severity } from '../../constants/severity.types';
|
|
3
|
+
export interface AccordionItem {
|
|
4
|
+
header: string;
|
|
5
|
+
headerIcon?: ReactNode;
|
|
6
|
+
severity?: Severity;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
11
|
+
type Mode = 'single' | 'multiple';
|
|
12
|
+
export interface AccordionProps {
|
|
13
|
+
items: AccordionItem[];
|
|
14
|
+
mode?: Mode;
|
|
15
|
+
size?: Size;
|
|
16
|
+
/** Uncontrolled defaults */
|
|
17
|
+
defaultOpenIndex?: number | null;
|
|
18
|
+
defaultOpenIndexes?: number[];
|
|
19
|
+
/** Controlled state */
|
|
20
|
+
openIndex?: number | null;
|
|
21
|
+
openIndexes?: number[];
|
|
22
|
+
/** Change callbacks */
|
|
23
|
+
onOpenIndexChange?: (index: number | null) => void;
|
|
24
|
+
onOpenIndexesChange?: (indexes: number[]) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function Accordion({ items, mode, size, defaultOpenIndex, defaultOpenIndexes, openIndex, openIndexes, onOpenIndexChange, onOpenIndexesChange, }: AccordionProps): JSX.Element;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useMemo, useState } from 'react';
|
|
4
|
+
import styles from './Accordion.module.css';
|
|
5
|
+
import { Headline } from '../headline/Headline';
|
|
6
|
+
function uniqSorted(nums) {
|
|
7
|
+
return Array.from(new Set(nums)).sort((a, b) => a - b);
|
|
8
|
+
}
|
|
9
|
+
export function Accordion({ items, mode = 'single', size = 'md', defaultOpenIndex = null, defaultOpenIndexes = [], openIndex, openIndexes, onOpenIndexChange, onOpenIndexesChange, }) {
|
|
10
|
+
const uid = useId();
|
|
11
|
+
const isControlledSingle = mode === 'single' && openIndex !== undefined;
|
|
12
|
+
const isControlledMultiple = mode === 'multiple' && openIndexes !== undefined;
|
|
13
|
+
const [internalOpenIndex, setInternalOpenIndex] = useState(mode === 'single' ? defaultOpenIndex : null);
|
|
14
|
+
const [internalOpenIndexes, setInternalOpenIndexes] = useState(mode === 'multiple' ? uniqSorted(defaultOpenIndexes) : []);
|
|
15
|
+
const currentOpenIndex = mode === 'single' ? (isControlledSingle ? openIndex : internalOpenIndex) : null;
|
|
16
|
+
const currentOpenIndexes = useMemo(() => mode === 'multiple'
|
|
17
|
+
? isControlledMultiple
|
|
18
|
+
? uniqSorted(openIndexes)
|
|
19
|
+
: internalOpenIndexes
|
|
20
|
+
: [], [mode, isControlledMultiple, openIndexes, internalOpenIndexes]);
|
|
21
|
+
const openSet = useMemo(() => new Set(mode === 'single'
|
|
22
|
+
? currentOpenIndex !== null
|
|
23
|
+
? [currentOpenIndex]
|
|
24
|
+
: []
|
|
25
|
+
: currentOpenIndexes), [mode, currentOpenIndex, currentOpenIndexes]);
|
|
26
|
+
function setSingle(next) {
|
|
27
|
+
if (isControlledSingle)
|
|
28
|
+
onOpenIndexChange === null || onOpenIndexChange === void 0 ? void 0 : onOpenIndexChange(next);
|
|
29
|
+
else {
|
|
30
|
+
setInternalOpenIndex(next);
|
|
31
|
+
onOpenIndexChange === null || onOpenIndexChange === void 0 ? void 0 : onOpenIndexChange(next);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function setMultiple(next) {
|
|
35
|
+
const normalized = uniqSorted(next);
|
|
36
|
+
if (isControlledMultiple)
|
|
37
|
+
onOpenIndexesChange === null || onOpenIndexesChange === void 0 ? void 0 : onOpenIndexesChange(normalized);
|
|
38
|
+
else {
|
|
39
|
+
setInternalOpenIndexes(normalized);
|
|
40
|
+
onOpenIndexesChange === null || onOpenIndexesChange === void 0 ? void 0 : onOpenIndexesChange(normalized);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function toggle(index) {
|
|
44
|
+
const item = items[index];
|
|
45
|
+
if (!item || item.disabled)
|
|
46
|
+
return;
|
|
47
|
+
if (mode === 'single') {
|
|
48
|
+
const isOpen = openSet.has(index);
|
|
49
|
+
setSingle(isOpen ? null : index);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// multiple
|
|
53
|
+
const isOpen = openSet.has(index);
|
|
54
|
+
if (isOpen)
|
|
55
|
+
setMultiple(currentOpenIndexes.filter(i => i !== index));
|
|
56
|
+
else
|
|
57
|
+
setMultiple([...currentOpenIndexes, index]);
|
|
58
|
+
}
|
|
59
|
+
return (_jsx("div", { className: `${styles.container} ${styles[size]}`, children: items.map((item, i) => {
|
|
60
|
+
const isOpen = openSet.has(i);
|
|
61
|
+
const isDisabled = !!item.disabled;
|
|
62
|
+
const buttonId = `${uid}-acc-btn-${i}`;
|
|
63
|
+
const panelId = `${uid}-acc-panel-${i}`;
|
|
64
|
+
return (_jsxs("section", { className: `${styles.item} ${isOpen ? styles.open : ''} ${isDisabled ? styles.disabled : ''}`, children: [_jsxs("button", { type: "button", id: buttonId, className: styles.trigger, "aria-expanded": isOpen, "aria-controls": panelId, onClick: () => toggle(i), disabled: isDisabled, children: [_jsx("span", { className: styles.title, children: _jsx(Headline, { disableMargin: true, size: 4, weight: 500, severity: item.severity, allowWrap: isOpen, children: item.header }) }), _jsx("span", { className: styles.chevron, "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 20 20", focusable: "false", children: _jsx("path", { d: "M5.5 7.5L10 12l4.5-4.5", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) })] }), _jsx("div", { id: panelId, role: "region", "aria-labelledby": buttonId, className: styles.panel, "data-open": isOpen ? 'true' : 'false', children: _jsx("div", { className: styles.content, children: item.children }) })] }, i));
|
|
65
|
+
}) }));
|
|
66
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
border-radius: var(--border-radius-default);
|
|
3
|
+
background-color: var(--color-bg-surface);
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
gap: var(--spacing-xxs);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.trigger {
|
|
12
|
+
all: unset;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
width: 100%;
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
gap: var(--spacing-sm);
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
user-select: none;
|
|
21
|
+
padding: var(--spacing-xs) var(--spacing-md);
|
|
22
|
+
background: var(--color-bg-contextual);
|
|
23
|
+
|
|
24
|
+
/* IMPORTANT: allow flex children to actually shrink */
|
|
25
|
+
min-width: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.trigger:focus-visible {
|
|
29
|
+
outline: none;
|
|
30
|
+
box-shadow: var(--focus-ring);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.disabled .trigger {
|
|
34
|
+
cursor: not-allowed;
|
|
35
|
+
color: var(--color-disabled-fg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.title {
|
|
39
|
+
/* IMPORTANT: this is the shrinking area that contains Headline */
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
flex: 1 1 auto;
|
|
44
|
+
|
|
45
|
+
/* ensures any overflow is clipped so ellipsis can show */
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.chevron {
|
|
50
|
+
width: var(--icon-size-md);
|
|
51
|
+
height: var(--icon-size-md);
|
|
52
|
+
flex: 0 0 auto;
|
|
53
|
+
transition: transform var(--transition-normal) var(--ease-standard);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.open .chevron {
|
|
57
|
+
transform: rotate(180deg);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Collapsible panel using max-height */
|
|
61
|
+
.panel {
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
max-height: 0;
|
|
64
|
+
transition: max-height var(--transition-slow) var(--ease-decelerate);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.panel[data-open='true'] {
|
|
68
|
+
max-height: 999px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.content {
|
|
72
|
+
padding: var(--spacing-md) 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Sizes */
|
|
76
|
+
.sm .trigger,
|
|
77
|
+
.sm .content {
|
|
78
|
+
padding: var(--spacing-sm) var(--spacing-md);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.md .trigger {
|
|
82
|
+
padding: var(--spacing-sm) var(--spacing-md);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.lg .trigger {
|
|
86
|
+
padding: var(--spacing-md) var(--spacing-md);
|
|
87
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ReactNode, JSX } from 'react';
|
|
2
2
|
import { Severity } from '../../constants/severity.types';
|
|
3
|
+
type CircleSize = 'xs' | 'sm' | 'md' | 'lg';
|
|
3
4
|
interface CircleProps {
|
|
4
5
|
severity: Severity;
|
|
5
6
|
children?: ReactNode;
|
|
6
7
|
glow?: boolean;
|
|
8
|
+
pulse?: boolean;
|
|
9
|
+
size?: CircleSize;
|
|
7
10
|
}
|
|
8
|
-
export declare function Circle({ severity, children, glow }: CircleProps): JSX.Element;
|
|
11
|
+
export declare function Circle({ severity, children, glow, pulse, size }: CircleProps): JSX.Element;
|
|
9
12
|
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import styles from './Circle.module.css';
|
|
3
|
-
export function Circle({ severity, children, glow }) {
|
|
4
|
-
return (_jsxs("span", { className: styles.container, children: [_jsx("span", { "data-glow": glow, className: `${styles.circle} ${styles[severity]}` }), children
|
|
3
|
+
export function Circle({ severity, children, glow, pulse, size = 'sm' }) {
|
|
4
|
+
return (_jsxs("span", { className: styles.container, children: [_jsx("span", { "data-glow": glow, "data-pulse": pulse, "data-size": size, className: `${styles.circle} ${styles[severity]}` }), children] }));
|
|
5
5
|
}
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.circle {
|
|
13
|
+
position: relative;
|
|
13
14
|
display: inline-block;
|
|
14
|
-
inline-size: var(--component-size-
|
|
15
|
-
block-size: var(--component-size-
|
|
15
|
+
inline-size: var(--component-size-sm);
|
|
16
|
+
block-size: var(--component-size-sm);
|
|
16
17
|
border-radius: var(--border-radius-round);
|
|
17
18
|
flex-shrink: 0;
|
|
18
19
|
|
|
@@ -60,3 +61,54 @@
|
|
|
60
61
|
.info[data-glow='true'] {
|
|
61
62
|
box-shadow: 0 0 0 2px var(--color-status-info-bg);
|
|
62
63
|
}
|
|
64
|
+
|
|
65
|
+
.circle[data-size='xs'] {
|
|
66
|
+
inline-size: var(--component-size-xxs);
|
|
67
|
+
block-size: var(--component-size-xxs);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.circle[data-size='sm'] {
|
|
71
|
+
inline-size: 14px;
|
|
72
|
+
block-size: 14px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.circle[data-size='md'] {
|
|
76
|
+
inline-size: 18px;
|
|
77
|
+
block-size: 18px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.circle[data-size='lg'] {
|
|
81
|
+
inline-size: 22px;
|
|
82
|
+
block-size: 22px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.circle[data-pulse='true']::after {
|
|
86
|
+
content: '';
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset: 0;
|
|
89
|
+
border-radius: inherit;
|
|
90
|
+
background-color: inherit;
|
|
91
|
+
animation: circle-pulse 1.6s ease-out infinite;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes circle-pulse {
|
|
96
|
+
0% {
|
|
97
|
+
transform: scale(1);
|
|
98
|
+
opacity: 0.6;
|
|
99
|
+
}
|
|
100
|
+
60% {
|
|
101
|
+
transform: scale(2);
|
|
102
|
+
opacity: 0;
|
|
103
|
+
}
|
|
104
|
+
100% {
|
|
105
|
+
transform: scale(2);
|
|
106
|
+
opacity: 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
.circle[data-pulse='true']::after {
|
|
112
|
+
animation: none;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Input } from '../../components/forms/input/Input';
|
|
3
|
-
import {
|
|
3
|
+
import { type UtcIsoString } from './dateTimeHelpers';
|
|
4
4
|
type Mode = 'single' | 'range';
|
|
5
5
|
type WeekStart = 0 | 1;
|
|
6
|
-
export type DateValue =
|
|
7
|
-
start:
|
|
8
|
-
end:
|
|
6
|
+
export type DateValue = UtcIsoString | null | {
|
|
7
|
+
start: UtcIsoString | null;
|
|
8
|
+
end: UtcIsoString | null;
|
|
9
9
|
};
|
|
10
10
|
type InputProps = React.ComponentProps<typeof Input>;
|
|
11
11
|
export interface DateTimePickerProps {
|
|
@@ -25,10 +25,6 @@ export interface DateTimePickerProps {
|
|
|
25
25
|
end: Date;
|
|
26
26
|
};
|
|
27
27
|
}[];
|
|
28
|
-
/**
|
|
29
|
-
* Forwarded to the internal <Input />.
|
|
30
|
-
* DateTimePicker controls: value, onInput/onBlur/onKeyDown, icon, onClear.
|
|
31
|
-
*/
|
|
32
28
|
inputProps?: Omit<InputProps, 'value' | 'onInput' | 'onBlur' | 'icon' | 'onClear' | 'type'>;
|
|
33
29
|
formatDate?: (d: Date, opts: {
|
|
34
30
|
locale: string;
|
|
@@ -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 {
|
|
8
|
+
import { isoFromLocalDate, isoFromLocalParts, localDateFromIso, maskRange, maskSingle, parseLooseDateOrDateTime, parseLooseRange, utcMillisFromIso, toMaskedFromDate, } from './dateTimeHelpers';
|
|
9
9
|
import styles from './DateTimePicker.module.css';
|
|
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
|
-
|
|
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 =
|
|
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++)
|
|
@@ -55,77 +57,72 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
55
57
|
void formatRange;
|
|
56
58
|
const popRef = useRef(null);
|
|
57
59
|
const todayLocal = useMemo(() => new Date(), []);
|
|
58
|
-
// ----
|
|
60
|
+
// ---- local anchor from controlled value ----
|
|
59
61
|
const initialAnchor = useMemo(() => {
|
|
60
62
|
var _a, _b;
|
|
61
63
|
if (mode === 'single') {
|
|
62
|
-
if (
|
|
63
|
-
return
|
|
64
|
-
if (!enableTime && typeof value === 'string')
|
|
65
|
-
return (_a = localDateFromYMD(value)) !== null && _a !== void 0 ? _a : todayLocal;
|
|
64
|
+
if (typeof value === 'string')
|
|
65
|
+
return (_a = localDateFromIso(value)) !== null && _a !== void 0 ? _a : todayLocal;
|
|
66
66
|
return todayLocal;
|
|
67
67
|
}
|
|
68
68
|
if (mode === 'range' && value && typeof value === 'object' && 'start' in value && value.start) {
|
|
69
|
-
return (_b =
|
|
69
|
+
return (_b = localDateFromIso(value.start)) !== null && _b !== void 0 ? _b : todayLocal;
|
|
70
70
|
}
|
|
71
71
|
return todayLocal;
|
|
72
|
-
}, [mode, value,
|
|
72
|
+
}, [mode, value, todayLocal]);
|
|
73
73
|
const [monthAnchor, setMonthAnchor] = useState(initialAnchor);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setMonthAnchor(initialAnchor);
|
|
77
|
-
}, [initialAnchor]);
|
|
74
|
+
useEffect(() => setMonthAnchor(initialAnchor), [initialAnchor]);
|
|
75
|
+
// time defaults (local)
|
|
78
76
|
const [timeHH, setTimeHH] = useState(todayLocal.getHours());
|
|
79
77
|
const [timeMM, setTimeMM] = useState(Math.floor(todayLocal.getMinutes() / timeStep) * timeStep);
|
|
80
|
-
// If
|
|
78
|
+
// If datetime value changes externally, keep HH/MM in sync
|
|
81
79
|
useEffect(() => {
|
|
82
|
-
if (mode === 'single' && enableTime && typeof value === '
|
|
83
|
-
const d =
|
|
80
|
+
if (mode === 'single' && enableTime && typeof value === 'string') {
|
|
81
|
+
const d = localDateFromIso(value);
|
|
82
|
+
if (!d)
|
|
83
|
+
return;
|
|
84
84
|
setTimeHH(d.getHours());
|
|
85
85
|
setTimeMM(Math.floor(d.getMinutes() / timeStep) * timeStep);
|
|
86
86
|
}
|
|
87
87
|
}, [mode, enableTime, value, timeStep]);
|
|
88
88
|
const [hoverUTC, setHoverUTC] = useState(null);
|
|
89
89
|
const cellsUTC = useMemo(() => buildMonthGrid(monthAnchor, weekStartsOn), [monthAnchor, weekStartsOn]);
|
|
90
|
-
const monthStartUTC = useMemo(() => startOfMonthUTC(
|
|
91
|
-
const monthEndUTC = useMemo(() => endOfMonthUTC(
|
|
90
|
+
const monthStartUTC = useMemo(() => startOfMonthUTC(toUTCDateOnlyFromLocal(monthAnchor)), [monthAnchor]);
|
|
91
|
+
const monthEndUTC = useMemo(() => endOfMonthUTC(toUTCDateOnlyFromLocal(monthAnchor)), [monthAnchor]);
|
|
92
92
|
const weekdayFmt = useMemo(() => new Intl.DateTimeFormat(locale, { weekday: 'short' }), [locale]);
|
|
93
93
|
const monthFmt = useMemo(() => new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }), [locale]);
|
|
94
|
-
// ----
|
|
94
|
+
// ---- selection state for grid (compare in UTC date-only) ----
|
|
95
95
|
const selectedUTC_single = useMemo(() => {
|
|
96
|
-
if (mode !== 'single'
|
|
96
|
+
if (mode !== 'single')
|
|
97
97
|
return null;
|
|
98
|
-
if (enableTime) {
|
|
99
|
-
if (typeof value !== 'number')
|
|
100
|
-
return null;
|
|
101
|
-
return toUTCDateOnly(new Date(value));
|
|
102
|
-
}
|
|
103
98
|
if (typeof value !== 'string')
|
|
104
99
|
return null;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
const d = localDateFromIso(value);
|
|
101
|
+
if (!d)
|
|
102
|
+
return null;
|
|
103
|
+
return toUTCDateOnlyFromLocal(d);
|
|
104
|
+
}, [mode, value]);
|
|
108
105
|
const selectedUTC_start = useMemo(() => {
|
|
109
106
|
if (mode !== 'range' || !value || typeof value !== 'object' || !('start' in value))
|
|
110
107
|
return null;
|
|
111
108
|
if (!value.start)
|
|
112
109
|
return null;
|
|
113
|
-
const
|
|
114
|
-
return
|
|
110
|
+
const d = localDateFromIso(value.start);
|
|
111
|
+
return d ? toUTCDateOnlyFromLocal(d) : null;
|
|
115
112
|
}, [mode, value]);
|
|
116
113
|
const selectedUTC_end = useMemo(() => {
|
|
117
114
|
if (mode !== 'range' || !value || typeof value !== 'object' || !('end' in value))
|
|
118
115
|
return null;
|
|
119
116
|
if (!value.end)
|
|
120
117
|
return null;
|
|
121
|
-
const
|
|
122
|
-
return
|
|
118
|
+
const d = localDateFromIso(value.end);
|
|
119
|
+
return d ? toUTCDateOnlyFromLocal(d) : null;
|
|
123
120
|
}, [mode, value]);
|
|
124
121
|
const isDisabledUTC = useCallback((utcDay) => {
|
|
125
|
-
// min/max
|
|
126
|
-
if (min && utcDay <
|
|
122
|
+
// Interpret min/max as local-day constraints for UI
|
|
123
|
+
if (min && utcDay < toUTCDateOnlyFromLocal(min))
|
|
127
124
|
return true;
|
|
128
|
-
if (max && utcDay >
|
|
125
|
+
if (max && utcDay > toUTCDateOnlyFromLocal(max))
|
|
129
126
|
return true;
|
|
130
127
|
return false;
|
|
131
128
|
}, [min, max]);
|
|
@@ -133,33 +130,29 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
133
130
|
var _a, _b;
|
|
134
131
|
if (isDisabledUTC(utcDay))
|
|
135
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();
|
|
136
137
|
if (mode === 'single') {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const d = utcDay.getUTCDate();
|
|
142
|
-
const local = new Date(y, m, d, timeHH, timeMM, 0, 0);
|
|
143
|
-
onChange(local.getTime());
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
// Date-only: emit timezone-free day label
|
|
147
|
-
onChange(ymdFromUTCDateOnly(utcDay));
|
|
148
|
-
}
|
|
138
|
+
const iso = enableTime
|
|
139
|
+
? isoFromLocalParts(y, m0, d, timeHH, timeMM)
|
|
140
|
+
: isoFromLocalParts(y, m0, d, 0, 0);
|
|
141
|
+
onChange(iso);
|
|
149
142
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
150
143
|
return;
|
|
151
144
|
}
|
|
152
|
-
//
|
|
145
|
+
// range (date-only in UI, but emitted as ISO instants at local midnight)
|
|
153
146
|
const curr = value && typeof value === 'object' && 'start' in value
|
|
154
147
|
? value
|
|
155
148
|
: { start: null, end: null };
|
|
156
|
-
const picked =
|
|
149
|
+
const picked = isoFromLocalParts(y, m0, d, 0, 0);
|
|
157
150
|
if (!curr.start || (curr.start && curr.end)) {
|
|
158
151
|
onChange({ start: picked, end: null });
|
|
159
152
|
return;
|
|
160
153
|
}
|
|
161
|
-
const a =
|
|
162
|
-
const b =
|
|
154
|
+
const a = utcMillisFromIso(curr.start);
|
|
155
|
+
const b = utcMillisFromIso(picked);
|
|
163
156
|
const start = a <= b ? curr.start : picked;
|
|
164
157
|
const end = a <= b ? picked : curr.start;
|
|
165
158
|
onChange({ start, end });
|
|
@@ -186,7 +179,7 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
186
179
|
].includes(e.key)) {
|
|
187
180
|
e.preventDefault();
|
|
188
181
|
}
|
|
189
|
-
const anchor =
|
|
182
|
+
const anchor = toUTCDateOnlyFromLocal(monthAnchor);
|
|
190
183
|
const move = (days) => setMonthAnchor(prev => addDaysLocal(prev, days));
|
|
191
184
|
switch (e.key) {
|
|
192
185
|
case 'ArrowLeft':
|
|
@@ -223,31 +216,26 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
223
216
|
// ---- Input display: always local ----
|
|
224
217
|
const formatted = useMemo(() => {
|
|
225
218
|
if (mode === 'single') {
|
|
226
|
-
if (!value)
|
|
227
|
-
return '';
|
|
228
|
-
if (enableTime) {
|
|
229
|
-
if (typeof value !== 'number')
|
|
230
|
-
return '';
|
|
231
|
-
return toMaskedFromDate(new Date(value), true);
|
|
232
|
-
}
|
|
233
219
|
if (typeof value !== 'string')
|
|
234
220
|
return '';
|
|
235
|
-
|
|
221
|
+
const d = localDateFromIso(value);
|
|
222
|
+
return d ? toMaskedFromDate(d, enableTime) : '';
|
|
236
223
|
}
|
|
237
|
-
// range (date-only)
|
|
238
224
|
const v = value;
|
|
239
|
-
const s = typeof (v === null || v === void 0 ? void 0 : v.start) === 'string' ?
|
|
240
|
-
const e = typeof (v === null || v === void 0 ? void 0 : v.end) === 'string' ?
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
return `${
|
|
245
|
-
if (
|
|
246
|
-
return
|
|
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}`;
|
|
247
235
|
return '';
|
|
248
236
|
}, [mode, value, enableTime]);
|
|
249
237
|
const [text, setText] = useState(formatted);
|
|
250
|
-
const [dirty, setDirty] = useState(false);
|
|
238
|
+
const [dirty, setDirty] = useState(false);
|
|
251
239
|
useEffect(() => {
|
|
252
240
|
if (!dirty)
|
|
253
241
|
setText(formatted);
|
|
@@ -265,28 +253,21 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
265
253
|
const dLocal = parseLooseDateOrDateTime(text);
|
|
266
254
|
if (!dLocal)
|
|
267
255
|
return;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
onChange(dLocal.getTime());
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
// Emit date-only string (local calendar day)
|
|
274
|
-
onChange(ymdFromLocalDate(dLocal));
|
|
275
|
-
}
|
|
256
|
+
// If enableTime=false, parseLooseDateOrDateTime returns 00:00 local -> still OK.
|
|
257
|
+
onChange(isoFromLocalDate(dLocal));
|
|
276
258
|
setMonthAnchor(dLocal);
|
|
277
259
|
setDirty(false);
|
|
278
260
|
return;
|
|
279
261
|
}
|
|
280
262
|
const r = parseLooseRange(text);
|
|
281
263
|
if (r) {
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
onChange({ start, 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 });
|
|
286
267
|
setMonthAnchor(r.start);
|
|
287
268
|
setDirty(false);
|
|
288
269
|
}
|
|
289
|
-
}, [text, mode, onChange
|
|
270
|
+
}, [text, mode, onChange]);
|
|
290
271
|
const clear = useCallback(() => {
|
|
291
272
|
if (mode === 'single')
|
|
292
273
|
onChange(null);
|
|
@@ -308,7 +289,7 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
308
289
|
const fallbackPlaceholder = mode === 'single' ? 'Vælg dato' : 'Vælg interval';
|
|
309
290
|
return (_jsx(Popover, { ref: popRef, trigger: toggle => {
|
|
310
291
|
var _a, _b;
|
|
311
|
-
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 => {
|
|
312
293
|
setDirty(true);
|
|
313
294
|
const raw = e.target.value;
|
|
314
295
|
const masked = mode === 'single' ? maskSingle(raw, enableTime) : maskRange(raw, false);
|
|
@@ -326,17 +307,16 @@ export const DateTimePicker = forwardRef(function DateTimePicker({ mode = 'singl
|
|
|
326
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: () => {
|
|
327
308
|
var _a;
|
|
328
309
|
const r = p.getRange();
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
onChange({ start, 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 });
|
|
333
313
|
setDirty(false);
|
|
334
|
-
setText(`${
|
|
314
|
+
setText(`${toMaskedFromDate(r.start, false)} – ${toMaskedFromDate(r.end, false)}`);
|
|
335
315
|
setMonthAnchor(r.start);
|
|
336
316
|
(_a = popRef.current) === null || _a === void 0 ? void 0 : _a.close();
|
|
337
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) => {
|
|
338
318
|
const inThisMonth = utcDay >= monthStartUTC && utcDay <= monthEndUTC;
|
|
339
|
-
const isToday = sameDayUTC(utcDay,
|
|
319
|
+
const isToday = sameDayUTC(utcDay, toUTCDateOnlyFromLocal(todayLocal));
|
|
340
320
|
const disabledDay = isDisabledUTC(utcDay);
|
|
341
321
|
let selected = false;
|
|
342
322
|
let inRange = false;
|
|
@@ -2,20 +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,
|
|
5
|
+
export declare function maskRange(text: string, _enableTime: boolean): string;
|
|
6
6
|
export declare const pad2: (n: number) => string;
|
|
7
|
-
export type
|
|
8
|
-
export declare function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export declare function
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
17
20
|
export declare function toMaskedFromDate(d: Date, enableTime: boolean): string;
|
|
18
|
-
export declare function toMaskedFromYMD(ymd: DateOnly): string;
|
|
19
21
|
export declare function parseLooseDateOrDateTime(input: string): Date | null;
|
|
20
22
|
export declare function parseLooseRange(input: string): {
|
|
21
23
|
start: Date;
|