@dbcdk/react-components 0.0.24 → 0.0.26

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.
@@ -15,7 +15,6 @@
15
15
  border-color var(--transition-fast) var(--ease-standard),
16
16
  box-shadow var(--transition-fast) var(--ease-standard),
17
17
  color var(--transition-fast) var(--ease-standard);
18
- line-height: 20px;
19
18
  }
20
19
 
21
20
  /* =========================
@@ -311,6 +310,10 @@
311
310
  font-size: var(--font-size-xs);
312
311
  }
313
312
 
313
+ .filterField input {
314
+ height: 100%;
315
+ }
316
+
314
317
  .filterField input::placeholder {
315
318
  color: var(--color-fg-subtle);
316
319
  }
@@ -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', modified, containerLabel, error, helpText, orientation = 'horizontal', labelWidth = '160px', fullWidth = false, required = false, noContainer = false, id, 'data-cy': dataCy, }) {
7
+ export function Checkbox({ checked: controlled, onChange, variant = 'default', 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();
@@ -50,10 +50,11 @@
50
50
  pointer-events: none;
51
51
  }
52
52
 
53
- .outlined.checked {
54
- background-color: var(--opac-bg-default);
53
+ .default {
54
+ background-color: var(--color-bg-toolbar);
55
+ border-color: var(--color-bg-toolbar);
55
56
  &:not(:hover) {
56
- border-color: transparent;
57
+ border-color: var(--color-bg-toolbar);
57
58
  }
58
59
 
59
60
  .icon {
@@ -61,6 +62,18 @@
61
62
  }
62
63
  }
63
64
 
65
+ .outlined.checked {
66
+ background-color: transparent;
67
+ border-color: var(--color-border-default);
68
+ &:not(:hover) {
69
+ border-color: var(--color-brand);
70
+ }
71
+
72
+ .icon {
73
+ color: var(--color-brand);
74
+ }
75
+ }
76
+
64
77
  .success.checked {
65
78
  background-color: var(--color-status-success);
66
79
  &:not(:hover) {
@@ -139,6 +139,7 @@
139
139
  box-shadow: none;
140
140
  padding-inline: var(--spacing-xs);
141
141
  padding-block: 0;
142
+ block-size: 100%;
142
143
  }
143
144
 
144
145
  .embedded:hover:not(:disabled),
@@ -112,7 +112,7 @@ export function Select({ label, error, helpText, orientation = 'vertical', label
112
112
  returnFocus: true, trigger: (toggle, icon, isOpen) => (_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 => {
113
113
  resetActiveToSelected();
114
114
  toggle(e);
115
- }, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "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) => {
115
+ }, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "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 : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), onClear && selected && _jsx(ClearButton, { onClick: onClear }), icon] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
116
116
  const isSelected = typeof opt.value === 'object' && typeof selectedValue === 'object' && datakey
117
117
  ? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
118
118
  : opt.value === selectedValue;
@@ -57,7 +57,7 @@ const MenuItem = React.forwardRef(({ children, active, disabled, className, item
57
57
  });
58
58
  MenuItem.displayName = 'Menu.Item';
59
59
  const MenuCheckItem = React.forwardRef(({ label, checked, disabled, onCheckedChange, className, ...liProps }, ref) => {
60
- return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(Checkbox, { noContainer: true, checked: checked, disabled: disabled, label: label, onChange: (next, _e) => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(next) }) }) }));
60
+ return (_jsx("li", { ref: ref, role: "none", className: [styles.row, className].filter(Boolean).join(' '), ...liProps, children: _jsx("div", { className: styles.interactiveChild, children: _jsx(Checkbox, { variant: "default", size: "md", noContainer: true, checked: checked, disabled: disabled, label: label, onChange: (next, _e) => onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(next) }) }) }));
61
61
  });
62
62
  MenuCheckItem.displayName = 'Menu.CheckItem';
63
63
  const MenuRadioItem = React.forwardRef(({ name, value, checked, disabled, label, onValueChange, className, ...liProps }, ref) => {
@@ -1,7 +1,9 @@
1
1
  import React, { ReactNode } from 'react';
2
+ import { ButtonVariant } from '../../../components/button/Button';
2
3
  import { Severity } from '../../../constants/severity.types';
3
4
  export type ModalActionConfig = {
4
5
  label: string;
6
+ severity?: ButtonVariant;
5
7
  onClick?: () => void;
6
8
  icon?: ReactNode;
7
9
  disabled?: boolean;
@@ -88,5 +88,5 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
88
88
  const resolvedWidth = typeof width === 'number' ? `${width}px` : typeof width === 'string' ? width : undefined;
89
89
  return (_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, style: resolvedWidth
90
90
  ? { ['--modal-width']: resolvedWidth }
91
- : undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", variant: "outlined", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: "primary", onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }));
91
+ : undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: primaryAction.severity || 'primary', onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }));
92
92
  }
@@ -15,7 +15,7 @@
15
15
 
16
16
  /* Default width can be overridden by --modal-width from props */
17
17
  .modal {
18
- --modal-width: 700px;
18
+ --modal-width: 500px;
19
19
 
20
20
  background: var(--color-bg-surface);
21
21
  border-radius: var(--border-radius-lg);
@@ -1,10 +1,12 @@
1
1
  import React, { type ReactNode } from 'react';
2
+ import { ButtonVariant } from '../../../../components/button/Button';
2
3
  import { type ModalProps } from '../Modal';
3
4
  type ModalConfig = Omit<ModalProps, 'isOpen' | 'onRequestClose'> & {
4
5
  onRequestClose?: () => void;
5
6
  };
6
7
  type ConfirmModalConfig = Omit<ModalConfig, 'primaryAction' | 'secondaryAction' | 'onRequestClose'> & {
7
8
  confirmLabel?: string;
9
+ confirmButtonSeverity?: ButtonVariant;
8
10
  cancelLabel?: string;
9
11
  };
10
12
  type ModalContextValue = {
@@ -9,7 +9,6 @@ export function ModalProvider({ children }) {
9
9
  const [config, setConfig] = useState(null);
10
10
  const [mounted, setMounted] = useState(false);
11
11
  useEffect(() => setMounted(true), []);
12
- // Holds the resolver for the current "confirm" call, if any
13
12
  const pendingResolverRef = useRef(null);
14
13
  const resolvePending = useCallback((value) => {
15
14
  if (pendingResolverRef.current) {
@@ -21,7 +20,6 @@ export function ModalProvider({ children }) {
21
20
  setIsOpen(false);
22
21
  }, []);
23
22
  const openModal = useCallback((newConfig) => {
24
- // if a confirm was in progress, resolve it as "false" (cancelled/overridden)
25
23
  resolvePending(false);
26
24
  setConfig(newConfig);
27
25
  setIsOpen(true);
@@ -41,6 +39,7 @@ export function ModalProvider({ children }) {
41
39
  ...rest,
42
40
  primaryAction: {
43
41
  label: confirmLabel,
42
+ severity: confirmConfig.confirmButtonSeverity || 'primary',
44
43
  onClick: () => {
45
44
  resolvePending(true);
46
45
  },
@@ -9,7 +9,6 @@
9
9
  inline-size: var(--sidebar-width);
10
10
  box-sizing: border-box;
11
11
  border-inline-end: var(--border-width-thin) solid var(--color-border-default);
12
-
13
12
  transition:
14
13
  width var(--transition-fast) var(--ease-standard),
15
14
  inline-size var(--transition-fast) var(--ease-standard);
@@ -11,6 +11,7 @@
11
11
  cursor: pointer;
12
12
  position: relative;
13
13
  border-radius: var(--border-radius-default);
14
+ user-select: none;
14
15
  transition:
15
16
  background-color var(--transition-fast) var(--ease-standard),
16
17
  color var(--transition-fast) var(--ease-standard);
@@ -143,7 +143,7 @@ export function Table({ data, columns, selectedRows, onRowSelect, selectionMode
143
143
  ]
144
144
  .filter(Boolean)
145
145
  .join(' '), role: "cell", "data-align": (_a = column.align) !== null && _a !== void 0 ? _a : 'left', "data-divider": column.divider, children: _jsx("div", { className: styles.cellContent, children: allowWrap ? (cellValue) : (_jsx("div", { className: styles.cellValueEllipsis, children: cellValue })) }) }, column.id));
146
- })] }, `gridRow-${String(rowId)}`));
146
+ })] }, `gridRow-${rowId}`));
147
147
  }) }));
148
148
  const bodyContent = loading && !data.length ? loadingBodyEl : dataBodyEl;
149
149
  const tableClassName = [
@@ -165,7 +165,6 @@
165
165
  vertical-align: top;
166
166
  min-width: 0;
167
167
  overflow: hidden;
168
- text-overflow: ellipsis;
169
168
  }
170
169
 
171
170
  .sm .cell {
@@ -188,15 +187,16 @@
188
187
  }
189
188
 
190
189
  .selectionCell {
191
- padding-block: inherit !important;
192
- padding-inline: 0 !important;
193
190
  overflow: visible !important;
194
191
  display: flex;
195
- align-items: center;
192
+ align-items: flex-start;
196
193
  justify-content: flex-start;
197
194
  min-width: 0;
198
195
  }
199
196
 
197
+ .headerCell.selectionCell {
198
+ align-items: center;
199
+ }
200
200
  .clickableRow {
201
201
  cursor: pointer;
202
202
  }
@@ -346,8 +346,8 @@
346
346
  min-width: 0;
347
347
  max-inline-size: 100%;
348
348
  white-space: nowrap;
349
- overflow: hidden;
350
- text-overflow: ellipsis;
349
+ /* overflow: hidden; */
350
+ /* text-overflow: ellipsis; */
351
351
  }
352
352
 
353
353
  .allowWrap .cellContent {
@@ -467,3 +467,20 @@
467
467
  width: 1px;
468
468
  background: var(--table-divider);
469
469
  }
470
+
471
+ .newRow {
472
+ animation: tableRowFadeIn 1000ms ease-out;
473
+ }
474
+
475
+ .newRow {
476
+ animation: tableRowFadeIn 600ms ease-out;
477
+ }
478
+
479
+ @keyframes tableRowFadeIn {
480
+ from {
481
+ opacity: 0;
482
+ }
483
+ to {
484
+ opacity: 1;
485
+ }
486
+ }
@@ -117,7 +117,7 @@ export function Tabs({ header, variant, panelStyle = false, tabs, value, default
117
117
  const selected = index === activeIndex;
118
118
  const tabDomId = `${uid}-tab-${String(tab.id)}`;
119
119
  const panelDomId = `${uid}-panel-${String(tab.id)}`;
120
- return (_jsx("div", { className: `${styles.tab} ${selected ? styles.active : ''}`, children: _jsxs("button", { id: tabDomId, type: "button", className: styles.tabButton, role: "tab", "aria-selected": selected, "aria-controls": panelDomId, tabIndex: selected ? 0 : -1, disabled: tab.disabled, onClick: () => setValue(tab.id), onKeyDown: e => onKeyDownTab(e, index), children: [tab.headerIcon ? _jsx("span", { className: styles.icon, children: tab.headerIcon }) : null, _jsx("span", { className: styles.label, children: tab.header }), tab.badge !== undefined && tab.badge > 0 ? (_jsx("span", { className: styles.badge, children: _jsx(Chip, { severity: "neutral", size: "sm", children: tab.badge.toLocaleString('da-DK') }) })) : null] }) }, tab.id));
120
+ return (_jsx("div", { className: `${styles.tab} ${selected ? styles.active : ''}`, children: _jsxs("button", { id: tabDomId, type: "button", className: styles.tabButton, role: "tab", "aria-selected": selected, "aria-controls": panelDomId, tabIndex: selected ? 0 : -1, disabled: tab.disabled, onClick: () => setValue(tab.id), onKeyDown: e => onKeyDownTab(e, index), children: [tab.headerIcon ? _jsx("span", { className: styles.icon, children: tab.headerIcon }) : null, _jsx("span", { className: styles.label, children: tab.header }), tab.badge !== undefined && tab.badge > 0 ? (_jsx("span", { className: styles.badge, children: _jsx(Chip, { type: "subtle", severity: null, size: "sm", children: tab.badge.toLocaleString('da-DK') }) })) : null] }) }, tab.id));
121
121
  }) }), _jsx("div", { id: activeTab ? `${uid}-panel-${String(activeTab.id)}` : undefined, role: "tabpanel", "aria-labelledby": activeTab ? `${uid}-tab-${String(activeTab.id)}` : undefined, className: styles.tabContent, children: activeTab === null || activeTab === void 0 ? void 0 : activeTab.content })] })] }));
122
122
  }
123
123
  Tabs.Item = TabsItem;
@@ -1,12 +1,20 @@
1
1
  /**
2
- * Formats a Date as "dd:mm:yyyy" or "dd:mm:yyyy hh:mm:ss".
3
- * All parts are zero-padded numbers.
2
+ * Formats a Date as "dd/mm/yyyy" or "dd/mm/yyyy hh:mm:ss".
3
+ * Optionally returns user-friendly Danish labels like "I dag" or "I går".
4
+ *
5
+ * Examples:
6
+ * - formatDate(new Date(), { userFriendlyLabel: true }) => "I dag"
7
+ * - formatDate(new Date(), { userFriendlyLabel: true, showTime: true }) => "I dag, kl. 14:30"
8
+ * - formatDate(date, { showTime: true }) => "10/03/2026 14:30"
4
9
  *
5
10
  * @param date - The Date or date-parsable value to format
6
- * @param options.showSeconds - If true, append time "hh:mm:ss"
11
+ * @param options.showTime - If true, append time "hh:mm" or "hh:mm:ss"
12
+ * @param options.showSeconds - If true, append seconds when showTime is enabled
13
+ * @param options.userFriendlyLabel - If true, use "I dag" / "I går" when applicable
7
14
  * @returns A formatted string
8
15
  */
9
16
  export declare function formatDate(date: Date | string | number, options?: {
10
17
  showTime?: boolean;
11
18
  showSeconds?: boolean;
19
+ userFriendlyLabel?: boolean;
12
20
  }): string;
@@ -1,26 +1,51 @@
1
1
  /**
2
- * Formats a Date as "dd:mm:yyyy" or "dd:mm:yyyy hh:mm:ss".
3
- * All parts are zero-padded numbers.
2
+ * Formats a Date as "dd/mm/yyyy" or "dd/mm/yyyy hh:mm:ss".
3
+ * Optionally returns user-friendly Danish labels like "I dag" or "I går".
4
+ *
5
+ * Examples:
6
+ * - formatDate(new Date(), { userFriendlyLabel: true }) => "I dag"
7
+ * - formatDate(new Date(), { userFriendlyLabel: true, showTime: true }) => "I dag, kl. 14:30"
8
+ * - formatDate(date, { showTime: true }) => "10/03/2026 14:30"
4
9
  *
5
10
  * @param date - The Date or date-parsable value to format
6
- * @param options.showSeconds - If true, append time "hh:mm:ss"
11
+ * @param options.showTime - If true, append time "hh:mm" or "hh:mm:ss"
12
+ * @param options.showSeconds - If true, append seconds when showTime is enabled
13
+ * @param options.userFriendlyLabel - If true, use "I dag" / "I går" when applicable
7
14
  * @returns A formatted string
8
15
  */
9
16
  export function formatDate(date, options = {}) {
10
17
  const d = date instanceof Date ? date : new Date(date);
11
18
  if (isNaN(d.getTime()))
12
19
  return '';
20
+ const { showTime = false, showSeconds = false, userFriendlyLabel = false } = options;
13
21
  const pad = (n) => n.toString().padStart(2, '0');
22
+ const formatTime = () => {
23
+ const hours = pad(d.getHours());
24
+ const minutes = pad(d.getMinutes());
25
+ if (!showSeconds)
26
+ return `${hours}:${minutes}`;
27
+ const seconds = pad(d.getSeconds());
28
+ return `${hours}:${minutes}:${seconds}`;
29
+ };
30
+ const isSameCalendarDay = (a, b) => a.getDate() === b.getDate() &&
31
+ a.getMonth() === b.getMonth() &&
32
+ a.getFullYear() === b.getFullYear();
33
+ if (userFriendlyLabel) {
34
+ const now = new Date();
35
+ const yesterday = new Date(now);
36
+ yesterday.setDate(now.getDate() - 1);
37
+ if (isSameCalendarDay(d, now)) {
38
+ return showTime ? `I dag, kl. ${formatTime()}` : 'I dag';
39
+ }
40
+ if (isSameCalendarDay(d, yesterday)) {
41
+ return showTime ? `I går, kl. ${formatTime()}` : 'I går';
42
+ }
43
+ }
14
44
  const day = pad(d.getDate());
15
45
  const month = pad(d.getMonth() + 1);
16
46
  const year = d.getFullYear();
17
47
  const base = `${day}/${month}/${year}`;
18
- if (!options.showTime)
48
+ if (!showTime)
19
49
  return base;
20
- const hours = pad(d.getHours());
21
- const minutes = pad(d.getMinutes());
22
- if (!options.showSeconds)
23
- return `${base} ${hours}:${minutes}`;
24
- const seconds = pad(d.getSeconds());
25
- return `${base} ${hours}:${minutes}:${seconds}`;
50
+ return `${base} ${formatTime()}`;
26
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",