@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
|
@@ -35,56 +35,42 @@ 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,
|
|
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
|
-
|
|
43
|
-
|
|
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');
|
|
51
|
-
export function
|
|
52
|
-
|
|
53
|
-
if (!m)
|
|
54
|
-
return null;
|
|
55
|
-
const y = +m[1];
|
|
56
|
-
const mo = +m[2];
|
|
57
|
-
const d = +m[3];
|
|
58
|
-
if (mo < 1 || mo > 12 || d < 1 || d > 31)
|
|
59
|
-
return null;
|
|
60
|
-
return { y, m: mo, d };
|
|
50
|
+
export function isUtcIsoString(v) {
|
|
51
|
+
return typeof v === 'string' && /Z$/.test(v) && !Number.isNaN(Date.parse(v));
|
|
61
52
|
}
|
|
62
|
-
export function
|
|
63
|
-
|
|
64
|
-
const m = dLocal.getMonth() + 1;
|
|
65
|
-
const d = dLocal.getDate();
|
|
66
|
-
return `${y}-${pad2(m)}-${pad2(d)}`;
|
|
53
|
+
export function utcMillisFromIso(iso) {
|
|
54
|
+
return Date.parse(iso);
|
|
67
55
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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();
|
|
73
63
|
}
|
|
74
|
-
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
if (!p)
|
|
78
|
-
return NaN;
|
|
79
|
-
return Date.UTC(p.y, p.m - 1, p.d);
|
|
64
|
+
/** Convert a local Date to a UTC ISO string (Z). */
|
|
65
|
+
export function isoFromLocalDate(dLocal) {
|
|
66
|
+
return dLocal.toISOString();
|
|
80
67
|
}
|
|
81
|
-
|
|
82
|
-
export function
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
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))
|
|
85
72
|
return null;
|
|
86
|
-
|
|
87
|
-
return new Date(p.y, p.m - 1, p.d, 12, 0, 0, 0);
|
|
73
|
+
return new Date(ms); // local rendering
|
|
88
74
|
}
|
|
89
75
|
/* ---------- Formatting (UI shows local) ---------- */
|
|
90
76
|
// From Date → "DD-MM-YYYY" or "DD-MM-YYYY HH:mm" (local time)
|
|
@@ -97,12 +83,6 @@ export function toMaskedFromDate(d, enableTime) {
|
|
|
97
83
|
out += ` ${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
|
|
98
84
|
return out;
|
|
99
85
|
}
|
|
100
|
-
export function toMaskedFromYMD(ymd) {
|
|
101
|
-
const p = parseYMD(ymd);
|
|
102
|
-
if (!p)
|
|
103
|
-
return '';
|
|
104
|
-
return `${pad2(p.d)}-${pad2(p.m)}-${p.y}`;
|
|
105
|
-
}
|
|
106
86
|
/* ---------- Parsing helpers (no deps) ---------- */
|
|
107
87
|
// Accepts: YYYY-MM-DD, DD-MM-YYYY, DD/MM/YYYY, DD.MM.YYYY (+ optional HH:mm)
|
|
108
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
|
-
|
|
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 = '
|
|
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
|
-
|
|
23
|
-
if (noContainer || (!containerLabel && !error))
|
|
22
|
+
if (noContainer)
|
|
24
23
|
return content;
|
|
25
|
-
|
|
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 = '
|
|
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
|
-
|
|
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 = '
|
|
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:
|
|
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
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { JSX } from 'react';
|
|
3
|
+
type Variant = 'default' | 'primary' | 'outlined';
|
|
4
|
+
type Size = 'sm' | 'md' | 'lg';
|
|
5
|
+
export interface RadioButtonProps {
|
|
6
|
+
/** group name (required for radio behavior) */
|
|
7
|
+
name: string;
|
|
8
|
+
/** this option's value */
|
|
9
|
+
value: string;
|
|
10
|
+
/** controlled selected value for the group */
|
|
11
|
+
selectedValue?: string;
|
|
12
|
+
/** uncontrolled checked (rare; prefer selectedValue on group) */
|
|
13
|
+
checked?: boolean;
|
|
14
|
+
/** called with (value, event) */
|
|
15
|
+
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
label?: string;
|
|
18
|
+
variant?: Variant;
|
|
19
|
+
size?: Size;
|
|
20
|
+
containerLabel?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
helpText?: string;
|
|
23
|
+
orientation?: 'vertical' | 'horizontal';
|
|
24
|
+
labelWidth?: string;
|
|
25
|
+
fullWidth?: boolean;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* If true, do NOT wrap with InputContainer.
|
|
29
|
+
* Use inside RadioGroup.
|
|
30
|
+
*/
|
|
31
|
+
noContainer?: boolean;
|
|
32
|
+
id?: string;
|
|
33
|
+
'data-cy'?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function RadioButton({ name, value, selectedValue, checked, onChange, disabled, label, variant, size, containerLabel, error, helpText, orientation, labelWidth, fullWidth, required, noContainer, id, 'data-cy': dataCy, }: RadioButtonProps): JSX.Element;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import styles from './RadioButtons.module.css';
|
|
5
|
+
import { InputContainer } from '../input-container/InputContainer';
|
|
6
|
+
export function RadioButton({ name, value, selectedValue, checked, onChange, disabled, label, variant = 'outlined', size = 'md', containerLabel, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required = false, noContainer = false, id, 'data-cy': dataCy, }) {
|
|
7
|
+
const generatedId = useId();
|
|
8
|
+
const controlId = id !== null && id !== void 0 ? id : `radio-${generatedId}`;
|
|
9
|
+
const isChecked = selectedValue !== undefined ? selectedValue === value : Boolean(checked);
|
|
10
|
+
const content = (_jsxs("span", { className: styles.container, "data-cy": dataCy, children: [_jsxs("span", { className: styles.controlWrap, children: [_jsx("input", { id: controlId, className: styles.input, type: "radio", name: name, value: value, checked: isChecked, disabled: disabled, required: required, "aria-invalid": Boolean(error) || undefined, onChange: e => {
|
|
11
|
+
if (disabled)
|
|
12
|
+
return;
|
|
13
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(e.target.value, e);
|
|
14
|
+
} }), _jsx("span", { className: [
|
|
15
|
+
styles.radio,
|
|
16
|
+
isChecked ? styles.checked : '',
|
|
17
|
+
disabled ? styles.disabled : '',
|
|
18
|
+
styles[variant],
|
|
19
|
+
styles[size],
|
|
20
|
+
]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(' '), "aria-hidden": "true", children: _jsx("span", { className: styles.dot }) })] }), label && (_jsx("label", { className: styles.label, htmlFor: controlId, children: label }))] }));
|
|
23
|
+
if (noContainer || (!containerLabel && !error))
|
|
24
|
+
return content;
|
|
25
|
+
return (_jsx(InputContainer, { label: containerLabel, htmlFor: controlId, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: content }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { JSX } from 'react';
|
|
2
|
+
export type RadioOption<V extends string = string> = {
|
|
3
|
+
label: string;
|
|
4
|
+
value: V;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export interface RadioGroupProps<V extends string = string> {
|
|
8
|
+
name?: string;
|
|
9
|
+
value: V | null;
|
|
10
|
+
onChange: (value: V) => void;
|
|
11
|
+
options: RadioOption<V>[];
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
label?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
helpText?: string;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
orientation?: 'vertical' | 'horizontal';
|
|
18
|
+
labelWidth?: string;
|
|
19
|
+
fullWidth?: boolean;
|
|
20
|
+
modified?: boolean;
|
|
21
|
+
/** layout of radios themselves */
|
|
22
|
+
direction?: 'row' | 'column';
|
|
23
|
+
'data-cy'?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function RadioButtonGroup<V extends string = string>({ name, value, onChange, options, disabled, label, error, helpText, required, orientation, labelWidth, fullWidth, modified, direction, 'data-cy': dataCy, }: RadioGroupProps<V>): JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { RadioButton } from './RadioButton';
|
|
5
|
+
import { InputContainer } from '../input-container/InputContainer';
|
|
6
|
+
export function RadioButtonGroup({ name, value, onChange, options, disabled = false, label, error, helpText, required = false, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, modified = false, direction = 'column', 'data-cy': dataCy, }) {
|
|
7
|
+
const generated = useId();
|
|
8
|
+
const groupName = name !== null && name !== void 0 ? name : `radio-group-${generated}`;
|
|
9
|
+
const content = (_jsx("div", { role: "radiogroup", "aria-invalid": Boolean(error) || undefined, "data-cy": dataCy, style: {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: direction,
|
|
12
|
+
gap: '8px',
|
|
13
|
+
}, children: options.map(opt => (_jsx(RadioButton, { noContainer: true, name: groupName, value: String(opt.value), selectedValue: value !== null && value !== void 0 ? value : undefined, disabled: disabled || opt.disabled, label: opt.label, required: required, onChange: v => onChange(v), "data-cy": dataCy ? `${dataCy}-option-${String(opt.value)}` : undefined }, String(opt.value)))) }));
|
|
14
|
+
// If no InputContainer props were provided, just return group content
|
|
15
|
+
if (!label && !error)
|
|
16
|
+
return content;
|
|
17
|
+
// Wrap once as a proper field
|
|
18
|
+
return (_jsx(InputContainer, { label: label, htmlFor: undefined, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, modified: modified, children: content }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.controlWrap {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Real input stays for a11y, but visually hidden */
|
|
14
|
+
.input {
|
|
15
|
+
position: absolute;
|
|
16
|
+
inset: 0;
|
|
17
|
+
width: var(--component-size-xs);
|
|
18
|
+
height: var(--component-size-xs);
|
|
19
|
+
margin: 0;
|
|
20
|
+
opacity: 0;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The visible radio */
|
|
25
|
+
.radio {
|
|
26
|
+
width: var(--component-size-xs);
|
|
27
|
+
height: var(--component-size-xs);
|
|
28
|
+
border-radius: 999px;
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
|
|
33
|
+
background: var(--color-bg-surface);
|
|
34
|
+
border: 2px solid rgba(0, 0, 0, 0.25);
|
|
35
|
+
|
|
36
|
+
transition:
|
|
37
|
+
border-color 120ms ease,
|
|
38
|
+
box-shadow 120ms ease,
|
|
39
|
+
background-color 120ms ease;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.dot {
|
|
43
|
+
width: 10px;
|
|
44
|
+
height: 10px;
|
|
45
|
+
border-radius: 999px;
|
|
46
|
+
transform: scale(0);
|
|
47
|
+
transition: transform 120ms ease;
|
|
48
|
+
background: var(--color-brand);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* checked state */
|
|
52
|
+
.checked {
|
|
53
|
+
border-color: var(--color-brand);
|
|
54
|
+
}
|
|
55
|
+
.checked .dot {
|
|
56
|
+
transform: scale(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* hover (only when not disabled) */
|
|
60
|
+
.input:not(:disabled):hover + .radio {
|
|
61
|
+
border-color: var(--color-brand-hover);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* focus ring */
|
|
65
|
+
.input:focus-visible + .radio {
|
|
66
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-brand) 30%, transparent);
|
|
67
|
+
border-color: var(--color-brand-strong);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* disabled */
|
|
71
|
+
.disabled {
|
|
72
|
+
opacity: 0.5;
|
|
73
|
+
}
|
|
74
|
+
.input:disabled {
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.label {
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
user-select: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.primary.checked {
|
|
84
|
+
background: color-mix(in srgb, var(--color-brand) 10%, var(--color-bg-surface));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.default {
|
|
88
|
+
border-color: rgba(0, 0, 0, 0.2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Sizes */
|
|
92
|
+
.sm {
|
|
93
|
+
width: 16px;
|
|
94
|
+
height: 16px;
|
|
95
|
+
}
|
|
96
|
+
.sm .dot {
|
|
97
|
+
width: 8px;
|
|
98
|
+
height: 8px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.md {
|
|
102
|
+
width: 20px;
|
|
103
|
+
height: 20px;
|
|
104
|
+
}
|
|
105
|
+
.md .dot {
|
|
106
|
+
width: 10px;
|
|
107
|
+
height: 10px;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.lg {
|
|
111
|
+
width: 24px;
|
|
112
|
+
height: 24px;
|
|
113
|
+
}
|
|
114
|
+
.lg .dot {
|
|
115
|
+
width: 12px;
|
|
116
|
+
height: 12px;
|
|
117
|
+
}
|
|
@@ -18,5 +18,5 @@ export type SelectProps<T> = Omit<InputContainerProps, 'children' | 'htmlFor' |
|
|
|
18
18
|
tooltip?: React.ReactNode;
|
|
19
19
|
tooltipPlacement?: 'top' | 'right' | 'bottom' | 'left';
|
|
20
20
|
};
|
|
21
|
-
export declare function Select<T extends string | number | Record<string, any>>({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, id, options, selectedValue, onChange, placeholder, size, variant, onClear, datakey, dataCy, disabled, }: SelectProps<T>): React.ReactNode;
|
|
21
|
+
export declare function Select<T extends string | number | Record<string, any>>({ label, error, helpText, orientation, labelWidth, fullWidth, required, tooltip, tooltipPlacement, modified, id, options, selectedValue, onChange, placeholder, size, variant, onClear, datakey, dataCy, disabled, }: SelectProps<T>): React.ReactNode;
|
|
22
22
|
export {};
|
|
@@ -10,7 +10,7 @@ import { Popover } from '../../popover/Popover';
|
|
|
10
10
|
import { InputContainer } from '../input-container/InputContainer';
|
|
11
11
|
export function Select({
|
|
12
12
|
// InputContainer props
|
|
13
|
-
label, error, helpText, orientation = 'vertical', labelWidth = '
|
|
13
|
+
label, error, helpText, orientation = 'vertical', labelWidth = '160px', fullWidth = true, required, tooltip, tooltipPlacement = 'right', modified = false,
|
|
14
14
|
// Select props
|
|
15
15
|
id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'outlined', onClear, datakey, dataCy, disabled, }) {
|
|
16
16
|
const generatedId = useId();
|
|
@@ -72,10 +72,10 @@ id, options, selectedValue, onChange, placeholder = 'Vælg', size, variant = 'ou
|
|
|
72
72
|
ids.push(tooltipId);
|
|
73
73
|
return ids.length ? ids.join(' ') : undefined;
|
|
74
74
|
})();
|
|
75
|
-
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, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
|
|
75
|
+
return (_jsxs(InputContainer, { label: label, htmlFor: controlId, fullWidth: fullWidth, error: error, helpText: helpText, orientation: orientation, labelWidth: labelWidth, required: required, modified: modified, children: [_jsx(Popover, { ref: popoverRef, trigger: (onClick, icon) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
|
|
76
76
|
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0);
|
|
77
77
|
onClick(e);
|
|
78
|
-
}, 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: [_jsx("span", { children: selected ? selected.label : placeholder }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
|
|
78
|
+
}, size: size, type: "button", "data-forminput": true, "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: [_jsx("span", { children: selected ? selected.label : placeholder }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
|
|
79
79
|
const isSelected = typeof opt.value === 'object' && typeof selectedValue === 'object' && datakey
|
|
80
80
|
? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
|
|
81
81
|
: opt.value === selectedValue;
|
|
@@ -4,9 +4,9 @@ import { useCallback, useId, useMemo } from 'react';
|
|
|
4
4
|
import { useTooltipTrigger } from '../../../components/overlay/tooltip/useTooltipTrigger';
|
|
5
5
|
import styles from './Textarea.module.css';
|
|
6
6
|
import { InputContainer } from '../input-container/InputContainer';
|
|
7
|
-
export const Textarea = function Textarea({ value, inputChanged, disabled, rows = 3, showCount, tooltip, tooltipPlacement = 'right', showTooltip, placeholder, adjustHeight, id,
|
|
7
|
+
export const Textarea = function Textarea({ value, inputChanged, disabled, rows = 3, showCount, tooltip, tooltipPlacement = 'right', showTooltip, placeholder, adjustHeight, id, modified = false,
|
|
8
8
|
// InputContainer props
|
|
9
|
-
label, error, helpText, orientation = 'horizontal', labelWidth = '
|
|
9
|
+
label, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required,
|
|
10
10
|
// Native textarea props
|
|
11
11
|
className, ...rest }) {
|
|
12
12
|
const generatedId = useId();
|
|
@@ -28,6 +28,6 @@ className, ...rest }) {
|
|
|
28
28
|
placement: tooltipPlacement,
|
|
29
29
|
offset: 8,
|
|
30
30
|
});
|
|
31
|
-
return (_jsx(InputContainer, { label: label, htmlFor: textareaId, error: error, helpText: helpText, helpTextAddition: showCount ? `${value === null || value === void 0 ? void 0 : value.length} tegn i denne boks` : undefined, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: _jsx("div", { className: styles.container, children: _jsx("div", { ...(tooltipEnabled ? triggerProps : {}), children: inputField }) }) }));
|
|
31
|
+
return (_jsx(InputContainer, { modified: modified, label: label, htmlFor: textareaId, error: error, helpText: helpText, helpTextAddition: showCount ? `${value === null || value === void 0 ? void 0 : value.length} tegn i denne boks` : undefined, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: _jsx("div", { className: styles.container, children: _jsx("div", { ...(tooltipEnabled ? triggerProps : {}), children: inputField }) }) }));
|
|
32
32
|
};
|
|
33
33
|
Textarea.displayName = 'Textarea';
|
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
font-family: var(--font-family);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
.container textarea:disabled {
|
|
13
|
+
background-color: var(--color-disabled-bg);
|
|
14
|
+
border: 0;
|
|
15
|
+
color: var(--color-disabled-fg);
|
|
16
|
+
cursor: not-allowed;
|
|
17
|
+
opacity: 0.5;
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
.internalCount {
|
|
13
21
|
text-align: right;
|
|
14
22
|
font-size: var(--font-size-sm);
|
|
@@ -22,5 +30,4 @@
|
|
|
22
30
|
.container textarea:focus-visible {
|
|
23
31
|
outline: none;
|
|
24
32
|
border-color: var(--color-border-selected);
|
|
25
|
-
box-shadow: var(--focus-ring);
|
|
26
33
|
}
|