@dbcdk/react-components 0.0.19 → 0.0.20

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.
@@ -1,7 +1,3 @@
1
- /* ==========================================================================
2
- * BASE BUTTON
3
- * ======================================================================= */
4
-
5
1
  .button {
6
2
  display: inline-flex;
7
3
  align-items: center;
@@ -226,6 +222,14 @@
226
222
  border-color: var(--color-border-selected);
227
223
  }
228
224
 
225
+ .inline.active {
226
+ color: var(--button-bg-primary);
227
+ }
228
+
229
229
  .active:hover {
230
230
  background-color: var(--button-bg-primary-hover);
231
231
  }
232
+
233
+ .inline.active:hover {
234
+ color: var(--button-bg-primary-hover);
235
+ }
@@ -1,5 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TextWrap } from 'lucide-react';
3
+ import { useMemo, useState } from 'react';
2
4
  import styles from './CodeBlock.module.css';
5
+ import { Button } from '../button/Button';
3
6
  import { CopyButton } from '../copy-button/CopyButton';
4
7
  const looksLikeStackFrame = (line) => {
5
8
  const t = line.trim();
@@ -16,24 +19,30 @@ export function CodeBlock({ code, children, copyButton, copyText, size = 'md', s
16
19
  var _a;
17
20
  const text = typeof code === 'string' ? code : undefined;
18
21
  const copy = (_a = copyText !== null && copyText !== void 0 ? copyText : text) !== null && _a !== void 0 ? _a : '';
19
- // If children are provided, render them as-is (no line processing).
20
22
  const hasChildren = children !== undefined && children !== null;
21
- // Smart rendering only when we have plain text + no children.
22
- const lines = smart && !hasChildren && typeof text === 'string' ? text.split('\n') : null;
23
- return (_jsxs("pre", { className: [styles.container, styles[size], wrap ? styles.wrap : styles.noWrap].join(' '), tabIndex: 0, children: [copyButton && (_jsx("span", { className: styles.copyButton, children: _jsx(CopyButton, { shape: "round", variant: "inline", text: copy }) })), _jsx("code", { className: styles.code, children: hasChildren
24
- ? children
25
- : lines
26
- ? lines.map((line, i) => {
27
- const isFirst = i === 0;
28
- const isFrame = looksLikeStackFrame(line);
29
- const cls = [
30
- styles.line,
31
- isFirst ? styles.lineFirst : '',
32
- isFrame ? styles.lineFrame : '',
33
- ]
34
- .filter(Boolean)
35
- .join(' ');
36
- return (_jsxs("span", { className: cls, children: [line, '\n'] }, i));
37
- })
38
- : text })] }));
23
+ const [isWrapped, setIsWrapped] = useState(wrap);
24
+ const lines = useMemo(() => (smart && !hasChildren && typeof text === 'string' ? text.split('\n') : null), [smart, hasChildren, text]);
25
+ return (_jsxs("div", { className: [
26
+ styles.wrapper,
27
+ styles[size],
28
+ isWrapped ? styles.wrap : styles.noWrap,
29
+ copyButton ? styles.hasActions : '',
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' '), children: [copyButton && (_jsxs("span", { className: styles.actions, "aria-hidden": false, children: [_jsx(Button, { type: "button", variant: "inline", size: "sm", shape: "round", onClick: () => setIsWrapped(v => !v), "aria-pressed": isWrapped, active: isWrapped, title: isWrapped ? 'Ombryd ikke tekst' : 'Ombryd tekst', children: _jsx(TextWrap, { size: 16 }) }), _jsx(CopyButton, { size: "sm", shape: "round", variant: "inline", text: copy })] })), _jsx("pre", { className: styles.container, tabIndex: 0, children: _jsx("code", { className: styles.code, children: hasChildren
33
+ ? children
34
+ : lines
35
+ ? lines.map((line, i) => {
36
+ const isFirst = i === 0;
37
+ const isFrame = looksLikeStackFrame(line);
38
+ const cls = [
39
+ styles.line,
40
+ isFirst ? styles.lineFirst : '',
41
+ isFrame ? styles.lineFrame : '',
42
+ ]
43
+ .filter(Boolean)
44
+ .join(' ');
45
+ return (_jsxs("span", { className: cls, children: [line, '\n'] }, i));
46
+ })
47
+ : text }) })] }));
39
48
  }
@@ -1,79 +1,80 @@
1
+ .wrapper {
2
+ position: relative;
3
+ --code-actions-h: var(--component-size-sm);
4
+ --code-actions-inset: var(--spacing-xs);
5
+ }
6
+
7
+ /* <pre> */
1
8
  .container {
2
9
  position: relative;
3
10
  margin-block: 0;
4
-
5
- background: var(--color-bg-contextual-subtle);
6
- border: var(--border-width-thin) solid var(--color-border-default);
11
+ background: var(--color-bg-surface-strong);
12
+ border: var(--border-width-thin) solid var(--color-border-subtle);
7
13
  border-radius: var(--border-radius-lg);
8
14
  box-shadow: var(--shadow-xs);
9
-
10
15
  padding: var(--spacing-sm);
11
- padding-inline-end: calc(var(--spacing-sm) + 40px);
12
-
13
16
  font-family: var(--font-family-mono);
14
- line-height: var(--line-height-relaxed);
15
-
16
- overflow-x: auto;
17
- overflow-y: hidden;
18
-
19
- /* Nice: avoids layout shift if/when scrollbars appear (supported in modern browsers) */
17
+ line-height: 1.35;
18
+ overflow: auto;
20
19
  scrollbar-gutter: stable;
21
- }
22
20
 
23
- .container:focus-within {
24
- border-color: var(--color-border-selected);
25
- box-shadow: var(--shadow-xs), var(--focus-ring);
21
+ display: flex;
22
+ align-items: center;
26
23
  }
27
24
 
28
- .container.sm {
25
+ /* Sizes */
26
+ .sm .container {
29
27
  padding: var(--spacing-xs);
30
- padding-inline-end: calc(var(--spacing-xs) + 40px);
31
28
  }
32
29
 
33
- .container.sm .code {
30
+ .sm .code {
34
31
  font-size: var(--font-size-xs);
35
32
  }
36
33
 
37
- .container.md .code {
34
+ .md .code {
38
35
  font-size: var(--font-size-sm);
39
36
  }
40
37
 
41
- .container.lg .code {
38
+ .lg .code {
42
39
  font-size: var(--font-size-base);
43
40
  }
44
41
 
45
- .code {
46
- display: block;
47
- margin: 0;
48
- font-family: var(--font-family-mono);
49
- color: var(--color-fg-default);
50
- white-space: pre-wrap;
51
- overflow-wrap: anywhere;
52
- word-break: normal; /* <- not break-all */
42
+ .hasActions .container {
43
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-sm) + var(--spacing-sm));
53
44
  }
54
45
 
55
- /* Copy button stays overlayed; does not affect layout */
56
- .copyButton {
57
- position: absolute;
58
- top: var(--spacing-xs);
59
- right: var(--spacing-xs);
60
- z-index: 2;
46
+ .sm.hasActions .container {
47
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-xs) + var(--spacing-xs));
48
+ }
61
49
 
62
- opacity: 0;
63
- pointer-events: none;
64
- transition: opacity var(--transition-fast) var(--ease-standard);
50
+ .md.hasActions .container {
51
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-sm) + var(--spacing-sm));
65
52
  }
66
53
 
67
- .container:hover .copyButton,
68
- .container:focus-within .copyButton {
69
- opacity: 1;
70
- pointer-events: auto;
54
+ .lg.hasActions .container {
55
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-sm) + var(--spacing-sm));
71
56
  }
72
57
 
73
- /* --- New: wrap control --- */
58
+ /* Focus ring */
59
+ .wrapper:focus-within .container {
60
+ border-color: var(--color-border-selected);
61
+ box-shadow: var(--shadow-xs), var(--focus-ring);
62
+ }
63
+
64
+ /* <code> */
65
+ .code {
66
+ display: block;
67
+ margin: 0;
68
+ font-family: var(--font-family-mono);
69
+ color: var(--color-fg-default);
70
+ flex: 1 1 auto;
71
+ min-width: 0;
72
+ }
73
+
74
+ /* Wrapping modes */
74
75
  .wrap .code {
75
76
  white-space: pre-wrap;
76
- overflow-wrap: anywhere;
77
+ overflow-wrap: break-word;
77
78
  word-break: normal;
78
79
  }
79
80
 
@@ -83,9 +84,31 @@
83
84
  word-break: normal;
84
85
  }
85
86
 
86
- /* --- New: per-line styling (for smart mode) --- */
87
+ .actions {
88
+ position: absolute;
89
+ top: var(--code-actions-inset);
90
+ right: var(--code-actions-inset);
91
+ z-index: 3;
92
+ display: inline-flex;
93
+ gap: var(--spacing-xs);
94
+ align-items: center;
95
+ padding: var(--spacing-2xs);
96
+ border-radius: var(--border-radius-lg);
97
+ background: color-mix(in oklab, var(--color-bg-surface) 70%, transparent);
98
+ backdrop-filter: blur(6px);
99
+ opacity: 0;
100
+ pointer-events: none;
101
+ transition: opacity var(--transition-fast) var(--ease-standard);
102
+ }
103
+
104
+ .wrapper:hover .actions,
105
+ .wrapper:focus-within .actions {
106
+ opacity: 1;
107
+ pointer-events: auto;
108
+ }
109
+
87
110
  .line {
88
- display: inline; /* keep selection/copy behavior natural */
111
+ display: inline;
89
112
  }
90
113
 
91
114
  .lineFirst {
@@ -94,27 +117,6 @@
94
117
  color: var(--color-fg-default);
95
118
  }
96
119
 
97
- /* Common stack frames are “noise”; deemphasize without hiding */
98
120
  .lineFrame {
99
121
  color: var(--color-fg-subtle);
100
122
  }
101
-
102
- /* Optional: make the container a bit denser for logs */
103
- .container {
104
- /* keep your existing properties ... */
105
-
106
- /* Easy win: stacktraces feel less tall */
107
- line-height: 1.35; /* instead of relaxed */
108
- }
109
-
110
- /* Optional: differentiate the code area slightly from surrounding UI */
111
- .container {
112
- background: var(--color-bg-surface-strong); /* a touch more neutral than contextual */
113
- border-color: var(--color-border-subtle);
114
- }
115
-
116
- /* Keep focus ring behavior */
117
- .container:focus-within {
118
- border-color: var(--color-border-selected);
119
- box-shadow: var(--shadow-xs), var(--focus-ring);
120
- }
@@ -19,5 +19,6 @@ export type ModalProps = {
19
19
  severity?: Severity;
20
20
  disableContentSpacing?: boolean;
21
21
  dataCy?: string;
22
+ width?: number | string;
22
23
  };
23
- export declare function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick, severity, disableContentSpacing, isLoading, dataCy, }: ModalProps): React.ReactNode;
24
+ export declare function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick, severity, disableContentSpacing, isLoading, dataCy, width, }: ModalProps): React.ReactNode;
@@ -5,7 +5,7 @@ import { useEffect, useId, useRef } from 'react';
5
5
  import { Button } from '../../../components/button/Button';
6
6
  import { Headline } from '../../../components/headline/Headline';
7
7
  import styles from './Modal.module.css';
8
- export function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick = true, severity, disableContentSpacing = false, isLoading, dataCy, }) {
8
+ export function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick = true, severity, disableContentSpacing = false, isLoading, dataCy, width, }) {
9
9
  const titleId = useId();
10
10
  const dialogRef = useRef(null);
11
11
  const lastActiveElementRef = useRef(null);
@@ -30,7 +30,6 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
30
30
  const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
31
31
  const focusable = dialog.querySelectorAll(focusableSelectors);
32
32
  if (focusable.length > 0) {
33
- // Prefer focusing the first input/select/textarea if present
34
33
  const preferred = (_a = dialog.querySelector('input, select, textarea')) !== null && _a !== void 0 ? _a : focusable[0];
35
34
  preferred.focus();
36
35
  }
@@ -68,13 +67,11 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
68
67
  document.addEventListener('keydown', handleKeyDown);
69
68
  return () => {
70
69
  document.removeEventListener('keydown', handleKeyDown);
71
- // Restore focus only when closing (true -> false) happens elsewhere
72
- // so we do it when modal unmounts while open.
73
70
  if (lastActiveElementRef.current) {
74
71
  lastActiveElementRef.current.focus();
75
72
  }
76
73
  };
77
- }, [isOpen]); // <-- IMPORTANT: only depend on isOpen
74
+ }, [isOpen]);
78
75
  if (!isOpen)
79
76
  return null;
80
77
  const handleOverlayClick = () => {
@@ -88,5 +85,8 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
88
85
  const resolvedSecondaryAction = secondaryAction !== null && secondaryAction !== void 0 ? secondaryAction : (primaryAction ? { label: 'Luk', onClick: onRequestCloseRef.current } : undefined);
89
86
  const shouldRenderFooter = Boolean(primaryAction || resolvedSecondaryAction);
90
87
  const body = children !== null && children !== void 0 ? children : content;
91
- return (_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, 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 })] }))] }))] }) }));
88
+ const resolvedWidth = typeof width === 'number' ? `${width}px` : typeof width === 'string' ? width : undefined;
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
+ ? { ['--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 })] }))] }))] }) }));
92
92
  }
@@ -2,62 +2,87 @@
2
2
  position: fixed;
3
3
  inset: 0;
4
4
  background: var(--overlay-scrim);
5
+
5
6
  display: flex;
6
- align-items: flex-start;
7
7
  justify-content: center;
8
- padding-top: clamp(var(--spacing-md), 12vh, 24vh);
9
- padding-bottom: var(--spacing-md);
8
+
9
+ padding: clamp(var(--spacing-sm), 10vh, var(--spacing-xl));
10
10
  z-index: var(--z-backdrop-modal);
11
+
12
+ /* Overlay can scroll if modal is taller than viewport */
11
13
  overflow-y: auto;
12
14
  }
13
15
 
16
+ /* Default width can be overridden by --modal-width from props */
14
17
  .modal {
18
+ --modal-width: 700px;
19
+
15
20
  background: var(--color-bg-surface);
16
21
  border-radius: var(--border-radius-lg);
17
- min-width: 320px;
18
- max-width: 700px;
19
- max-height: calc(100vh - (2 * var(--spacing-md)));
20
- display: flex;
21
- flex-direction: column;
22
22
  box-shadow: var(--shadow-lg);
23
23
  font-family: var(--font-family);
24
- min-width: 500px;
25
- z-index: var(--z-modal);
26
24
  color: var(--color-fg-default);
27
- }
25
+ z-index: var(--z-modal);
26
+
27
+ /* Responsive width: never exceed viewport */
28
+ width: min(
29
+ var(--modal-width),
30
+ calc(100vw - 2 * clamp(var(--spacing-sm), 4vw, var(--spacing-lg)))
31
+ );
32
+ min-width: 320px;
33
+
34
+ /* Critical: prevent “below bottom of screen”
35
+ Prefer svh on mobile; fallback to vh. */
36
+ max-height: calc(100svh - 2 * clamp(var(--spacing-sm), 10vh, var(--spacing-xl)));
37
+ max-height: calc(100vh - 2 * clamp(var(--spacing-sm), 10vh, var(--spacing-xl)));
38
+
39
+ display: flex;
40
+ flex-direction: column;
28
41
 
29
- /* Slightly more relaxed on small screens */
30
- @media (max-width: var(--bp-sm)) {
31
- .modal {
32
- width: calc(100% - 2 * var(--spacing-md));
33
- max-width: 100%;
34
- }
42
+ /* If you want it slightly lower than top padding, keep it aligned start */
43
+ align-self: flex-start;
44
+
45
+ /* Helps on iOS when address bar changes */
46
+ overscroll-behavior: contain;
35
47
  }
36
48
 
49
+ /* Header/footer pinned; body scrolls */
37
50
  .header {
38
51
  font-size: var(--font-size-md);
39
52
  font-weight: var(--font-weight-semibold);
53
+
40
54
  display: flex;
41
55
  justify-content: space-between;
42
56
  align-items: center;
57
+
43
58
  padding: var(--spacing-md);
44
59
  padding-bottom: 0;
60
+
61
+ /* Keeps header readable if body scrolls underneath */
62
+ flex: 0 0 auto;
45
63
  }
46
64
 
47
65
  .body {
48
- overflow-y: auto;
66
+ flex: 1 1 auto;
67
+ overflow: auto;
68
+
49
69
  font-size: var(--font-size-sm);
50
70
  line-height: var(--line-height-normal);
51
71
  color: var(--color-fg-muted);
72
+
52
73
  padding: var(--spacing-md);
74
+ min-height: 0; /* IMPORTANT: allows flex child to actually scroll */
53
75
  }
54
76
 
55
77
  .footer {
78
+ flex: 0 0 auto;
79
+
56
80
  display: flex;
57
81
  justify-content: flex-end;
58
82
  gap: var(--spacing-xs);
59
- padding-top: 0;
83
+
60
84
  padding: var(--spacing-md);
85
+ padding-top: 0;
61
86
  }
62
87
 
63
88
  .icon {
@@ -65,3 +90,11 @@
65
90
  align-items: center;
66
91
  justify-content: center;
67
92
  }
93
+
94
+ .contentSpacing .body > :first-child {
95
+ margin-top: 0;
96
+ }
97
+
98
+ .contentSpacing .body > :last-child {
99
+ margin-bottom: 0;
100
+ }
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import type { HTMLAttributes, JSX, ReactNode } from 'react';
3
2
  import { Severity } from '../../constants/severity.types';
4
3
  import { PageChangeEvent } from '../../components/pagination/Pagination';
@@ -25,7 +24,7 @@ type HeaderExtrasArgs<T> = {
25
24
  index: number;
26
25
  };
27
26
  export type TableVariant = 'primary' | 'embedded';
28
- export type TableProps<T extends Record<string, any>> = Omit<HTMLAttributes<HTMLTableElement>, 'onClick'> & {
27
+ export type TableProps<T extends Record<string, any>> = Omit<HTMLAttributes<HTMLDivElement>, 'onClick'> & {
29
28
  data: T[];
30
29
  dataKey: keyof T;
31
30
  columns: ColumnItem<T>[];
@@ -40,7 +39,13 @@ export type TableProps<T extends Record<string, any>> = Omit<HTMLAttributes<HTML
40
39
  sortDirection?: SortDirection;
41
40
  loading?: boolean;
42
41
  headerExtras?: (args: HeaderExtrasArgs<T>) => ReactNode;
43
- columnStyles?: Partial<Record<string, React.CSSProperties>>;
42
+ /**
43
+ * Grid layout control
44
+ *
45
+ * Example:
46
+ * "34px minmax(160px, 2fr) minmax(120px, 1fr) minmax(120px, 1fr)"
47
+ */
48
+ gridTemplateColumns?: string;
44
49
  toolbar?: ReactNode;
45
50
  striped?: boolean;
46
51
  fillViewport?: boolean;
@@ -58,6 +63,6 @@ export type TableProps<T extends Record<string, any>> = Omit<HTMLAttributes<HTML
58
63
  showFirstLast?: boolean;
59
64
  viewMode?: ViewMode;
60
65
  emptyConfig?: TableEmptyConfig;
61
- } & Omit<HTMLAttributes<HTMLTableElement>, 'onClick'>;
62
- export declare function Table<T extends Record<string, any>>({ data, columns, selectedRows, onRowSelect, selectionMode, onSortChange, onRowClick, sortById, sortDirection, dataKey, headerExtras, columnStyles, toolbar, striped, fillViewport, viewportBottomOffset, viewportMin, viewportIncludeMarginTop, take, skip, paginationPlacement, totalItemsCount, onPageChange, loading, variant, size, getRowSeverity, showFirstLast, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, ...rest }: TableProps<T>): JSX.Element;
66
+ };
67
+ export declare function Table<T extends Record<string, any>>({ data, columns, selectedRows, onRowSelect, selectionMode, onSortChange, onRowClick, sortById, sortDirection, dataKey, headerExtras, gridTemplateColumns, toolbar, striped, fillViewport, viewportBottomOffset, viewportMin, viewportIncludeMarginTop, take, skip, paginationPlacement, totalItemsCount, onPageChange, loading, variant, size, getRowSeverity, showFirstLast, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, ...rest }: TableProps<T>): JSX.Element;
63
68
  export {};
@@ -9,8 +9,17 @@ import { Pagination } from '../../components/pagination/Pagination';
9
9
  import { SkeletonLoaderItem } from '../../components/skeleton-loader/skeleton-loader-item/SkeletonLoaderItem';
10
10
  import { TableEmptyState } from './components/empty-state/EmptyState';
11
11
  import styles from './Table.module.css';
12
- import { getAriaSort, getCellDisplayValue, getColumnStyle, getHeaderLabel, getNextSortDirection, getRowKey, getVisibleColumns, isModifierClick, shouldAllowWrap, shouldToggleOnKey, isActiveSort, } from './table.utils';
13
- export function Table({ data, columns, selectedRows, onRowSelect, selectionMode = 'single', onSortChange, onRowClick, sortById, sortDirection, dataKey, headerExtras, columnStyles, toolbar, striped, fillViewport = false, viewportBottomOffset = 0, viewportMin = 120, viewportIncludeMarginTop = false, take, skip, paginationPlacement = 'bottom', totalItemsCount, onPageChange, loading, variant = 'primary', size = 'md', getRowSeverity, showFirstLast = false, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, ...rest }) {
12
+ import { getAriaSort, getCellDisplayValue, getHeaderLabel, getNextSortDirection, getVisibleColumns, isModifierClick, shouldAllowWrap, shouldToggleOnKey, isActiveSort, } from './table.utils';
13
+ function buildDefaultGridTemplate(args) {
14
+ const { hasSelection, colCount } = args;
15
+ const parts = [];
16
+ if (hasSelection)
17
+ parts.push('34px');
18
+ for (let i = 0; i < colCount; i++)
19
+ parts.push('minmax(120px, 1fr)');
20
+ return parts.join(' ');
21
+ }
22
+ export function Table({ data, columns, selectedRows, onRowSelect, selectionMode = 'single', onSortChange, onRowClick, sortById, sortDirection, dataKey, headerExtras, gridTemplateColumns, toolbar, striped, fillViewport = false, viewportBottomOffset = 0, viewportMin = 120, viewportIncludeMarginTop = false, take, skip, paginationPlacement = 'bottom', totalItemsCount, onPageChange, loading, variant = 'primary', size = 'md', getRowSeverity, showFirstLast = false, allRowsSelected, onSelectAllRows, viewMode, emptyConfig, ...rest }) {
14
23
  const filteredColumns = useMemo(() => getVisibleColumns(columns), [columns]);
15
24
  const handlePageChange = useCallback((e) => onPageChange === null || onPageChange === void 0 ? void 0 : onPageChange(e), [onPageChange]);
16
25
  const scrollRef = useRef(null);
@@ -19,64 +28,87 @@ export function Table({ data, columns, selectedRows, onRowSelect, selectionMode
19
28
  min: viewportMin,
20
29
  includeMarginTop: viewportIncludeMarginTop,
21
30
  });
22
- const tableEl = (_jsxs(_Fragment, { children: [toolbar ? _jsx("div", { style: { marginBottom: 12 }, children: toolbar }) : null, _jsxs("table", { ...rest, className: `${styles.table} ${styles[variant]} ${styles[size]} ${getRowSeverity ? styles.severityTable : ''}`, children: [_jsx("thead", { children: _jsxs("tr", { children: [selectedRows && onRowSelect && dataKey && (_jsx("th", { className: `${styles.th} ${styles.selectionCell}`, children: selectionMode === 'multiple' ? (_jsx(Checkbox, { size: "sm", variant: "primary", checked: allRowsSelected, onChange: checked => onSelectAllRows === null || onSelectAllRows === void 0 ? void 0 : onSelectAllRows(checked) })) : null })), filteredColumns.map((column, index) => {
23
- const active = isActiveSort(sortById, column.id);
24
- const ariaSort = getAriaSort(column.sortable, active, sortDirection !== null && sortDirection !== void 0 ? sortDirection : null);
25
- const toggleSort = () => {
26
- if (!onSortChange || !column.sortable)
27
- return;
28
- const nextDir = getNextSortDirection(column.sortable, active, sortDirection !== null && sortDirection !== void 0 ? sortDirection : null);
29
- onSortChange(column, nextDir);
30
- };
31
- return (_jsx("th", { style: getColumnStyle(column.id, columnStyles, column.align, 'middle'), "aria-sort": ariaSort, className: `${styles.th}`, children: _jsxs("div", { className: styles.thInner, children: [column.sortable ? (_jsxs("button", { type: "button", className: [
32
- styles.thButton,
33
- column.align === 'right' ? styles.thButtonRight : '',
34
- column.align === 'center' ? styles.thButtonCenter : '',
35
- ].join(' '), onClick: toggleSort, onKeyDown: e => {
36
- if (shouldToggleOnKey(e.key)) {
37
- e.preventDefault();
38
- toggleSort();
39
- }
40
- }, children: [_jsx("span", { className: [
41
- styles.thLabel,
42
- column.align === 'right' ? styles.thLabelRight : '',
43
- column.align === 'center' ? styles.thLabelCenter : '',
44
- ].join(' '), children: getHeaderLabel(column.header) }), _jsxs("span", { className: styles.sortIndicator, "aria-hidden": "true", children: [active && sortDirection === 'asc' && _jsx(ArrowUp, {}), active && sortDirection === 'desc' && (_jsx(ArrowDown, { className: styles.descending })), !active && (_jsx(ArrowDown, { className: `${styles.descending} ${styles.inActiveSort}` }))] })] })) : (_jsx("span", { className: [
45
- styles.thLabel,
46
- column.align === 'right' ? styles.thLabelRight : '',
47
- column.align === 'center' ? styles.thLabelCenter : '',
48
- ].join(' '), children: getHeaderLabel(column.header) })), headerExtras ? (_jsx("div", { className: styles.thExtras, children: headerExtras({ column, index }) })) : null] }) }, column.id));
49
- })] }) }), loading && !data.length ? (_jsx("tbody", { className: `${styles.tBody} ${striped ? styles.striped : ''}`, children: Array.from({ length: take !== null && take !== void 0 ? take : 5 }).map((_, rowIndex) => (_jsx("tr", { children: filteredColumns.map((column, colIndex) => (_jsx("td", { style: getColumnStyle(column.id, columnStyles, column.align, 'middle'), className: `${styles.tableCell}`, children: _jsx(SkeletonLoaderItem, { height: 20, width: "100%" }) }, `${column.id}-${colIndex}`))) }, `loading-row-${rowIndex}`))) })) : (_jsx("tbody", { className: `${styles.tBody} ${striped ? styles.striped : ''}`, children: data === null || data === void 0 ? void 0 : data.map(row => {
50
- const rowSeverity = getRowSeverity === null || getRowSeverity === void 0 ? void 0 : getRowSeverity(row);
51
- const rowId = row[dataKey];
52
- const isSelected = Boolean(selectedRows === null || selectedRows === void 0 ? void 0 : selectedRows.has(rowId));
53
- return (_jsxs("tr", { tabIndex: onRowClick ? 0 : -1, onKeyDown: e => {
54
- if (!onRowClick)
55
- return;
31
+ const hasSelection = Boolean(selectedRows && onRowSelect && dataKey);
32
+ const template = useMemo(() => {
33
+ return (gridTemplateColumns !== null && gridTemplateColumns !== void 0 ? gridTemplateColumns : buildDefaultGridTemplate({ hasSelection, colCount: filteredColumns.length }));
34
+ }, [gridTemplateColumns, hasSelection, filteredColumns.length]);
35
+ const gridStyle = useMemo(() => ({ ['--grid-template']: template }), [template]);
36
+ const headerEl = (_jsxs("div", { className: `${styles.headerRow}`, style: gridStyle, role: "row", children: [hasSelection && (_jsx("div", { className: `${styles.headerCell} ${styles.selectionCell}`, role: "columnheader", children: selectionMode === 'multiple' ? (_jsx(Checkbox, { size: "sm", variant: "primary", checked: allRowsSelected, onChange: checked => onSelectAllRows === null || onSelectAllRows === void 0 ? void 0 : onSelectAllRows(checked) })) : null })), filteredColumns.map((column, index) => {
37
+ var _a;
38
+ const active = isActiveSort(sortById, column.id);
39
+ const ariaSort = getAriaSort(column.sortable, active, sortDirection !== null && sortDirection !== void 0 ? sortDirection : null);
40
+ const toggleSort = () => {
41
+ if (!onSortChange || !column.sortable)
42
+ return;
43
+ const nextDir = getNextSortDirection(column.sortable, active, sortDirection !== null && sortDirection !== void 0 ? sortDirection : null);
44
+ onSortChange(column, nextDir);
45
+ };
46
+ return (_jsx("div", { className: styles.headerCell, role: "columnheader", "aria-sort": ariaSort, "data-align": (_a = column.align) !== null && _a !== void 0 ? _a : 'left', children: _jsxs("div", { className: styles.thInner, children: [column.sortable ? (_jsxs("button", { type: "button", className: [
47
+ styles.thButton,
48
+ column.align === 'right' ? styles.thButtonRight : '',
49
+ column.align === 'center' ? styles.thButtonCenter : '',
50
+ ].join(' '), onClick: toggleSort, onKeyDown: e => {
56
51
  if (shouldToggleOnKey(e.key)) {
57
52
  e.preventDefault();
58
- onRowClick(row);
59
- }
60
- }, onClick: e => {
61
- const canSelect = Boolean(selectedRows && onRowSelect && dataKey);
62
- if (isModifierClick(e) && canSelect) {
63
- e.preventDefault();
64
- e.stopPropagation();
65
- onRowSelect(rowId, !selectedRows.has(rowId));
66
- return;
53
+ toggleSort();
67
54
  }
68
- onRowClick === null || onRowClick === void 0 ? void 0 : onRowClick(row);
69
- }, style: {
70
- ['--row-severity-color']: rowSeverity
71
- ? SeverityBgColor[rowSeverity]
72
- : undefined,
73
- }, className: `${onRowClick ? styles.clickableRow : ''} ${isSelected ? styles.selectedRow : ''} ${rowSeverity ? styles.severity : ''}`, children: [selectedRows && onRowSelect && dataKey && (_jsx("td", { className: `${styles.selectionCell}`, children: _jsx(Checkbox, { variant: "primary", checked: selectedRows.has(rowId), size: "sm", onChange: (checked, e) => {
74
- e.stopPropagation();
75
- onRowSelect === null || onRowSelect === void 0 ? void 0 : onRowSelect(rowId, checked);
76
- } }) })), filteredColumns.map(column => (_jsx("td", { style: getColumnStyle(column.id, columnStyles, column.align, column.verticalAlign), className: `${styles.tableCell} ${shouldAllowWrap(column.allowWrap, isSelected, viewMode)
77
- ? styles.allowWrap
78
- : styles.nowrap}`, children: getCellDisplayValue(row, column) }, column.id)))] }, getRowKey(rowId)));
79
- }) }))] }), !data.length && !loading && _jsx(TableEmptyState, { config: emptyConfig })] }));
55
+ }, children: [_jsx("span", { className: [
56
+ styles.thLabel,
57
+ column.align === 'right' ? styles.thLabelRight : '',
58
+ column.align === 'center' ? styles.thLabelCenter : '',
59
+ ].join(' '), children: getHeaderLabel(column.header) }), _jsxs("span", { className: styles.sortIndicator, "aria-hidden": "true", children: [active && sortDirection === 'asc' && _jsx(ArrowUp, {}), active && sortDirection === 'desc' && (_jsx(ArrowDown, { className: styles.descending })), !active && (_jsx(ArrowDown, { className: `${styles.descending} ${styles.inActiveSort}` }))] })] })) : (_jsx("span", { className: [
60
+ styles.thLabel,
61
+ column.align === 'right' ? styles.thLabelRight : '',
62
+ column.align === 'center' ? styles.thLabelCenter : '',
63
+ ].join(' '), children: getHeaderLabel(column.header) })), headerExtras ? (_jsx("div", { className: styles.thExtras, children: headerExtras({ column, index }) })) : null] }) }, column.id));
64
+ })] }));
65
+ const bodyEl = loading && !data.length ? (_jsx("div", { className: styles.body, role: "rowgroup", children: Array.from({ length: take !== null && take !== void 0 ? take : 5 }).map((_, rowIndex) => (_jsxs("div", { className: styles.row, style: gridStyle, role: "row", children: [hasSelection ? (_jsx("div", { className: `${styles.cell} ${styles.selectionCell}`, role: "cell" })) : null, filteredColumns.map(column => {
66
+ var _a;
67
+ return (_jsx("div", { className: styles.cell, role: "cell", "data-align": (_a = column.align) !== null && _a !== void 0 ? _a : 'left', children: _jsx(SkeletonLoaderItem, { height: 20, width: "100%" }) }, column.id));
68
+ })] }, `loading-row-${rowIndex}`))) })) : (_jsx("div", { className: `${styles.body} ${striped ? styles.striped : ''}`, role: "rowgroup", children: data === null || data === void 0 ? void 0 : data.map(row => {
69
+ const rowSeverity = getRowSeverity === null || getRowSeverity === void 0 ? void 0 : getRowSeverity(row);
70
+ const rowId = row[dataKey];
71
+ const isSelected = Boolean(selectedRows === null || selectedRows === void 0 ? void 0 : selectedRows.has(rowId));
72
+ return (_jsxs("div", { className: [
73
+ styles.row,
74
+ onRowClick ? styles.clickableRow : '',
75
+ isSelected ? styles.selectedRow : '',
76
+ rowSeverity ? styles.severity : '',
77
+ ].join(' '), style: {
78
+ ...gridStyle,
79
+ ['--row-severity-color']: rowSeverity
80
+ ? SeverityBgColor[rowSeverity]
81
+ : undefined,
82
+ }, role: "row", tabIndex: onRowClick ? 0 : -1, onKeyDown: e => {
83
+ if (!onRowClick)
84
+ return;
85
+ if (shouldToggleOnKey(e.key)) {
86
+ e.preventDefault();
87
+ onRowClick(row);
88
+ }
89
+ }, onClick: e => {
90
+ const canSelect = Boolean(selectedRows && onRowSelect && dataKey);
91
+ if (isModifierClick(e) && canSelect) {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ onRowSelect(rowId, !selectedRows.has(rowId));
95
+ return;
96
+ }
97
+ onRowClick === null || onRowClick === void 0 ? void 0 : onRowClick(row);
98
+ }, children: [hasSelection && (_jsx("div", { className: `${styles.cell} ${styles.selectionCell}`, role: "cell", children: _jsx(Checkbox, { variant: "primary", checked: selectedRows.has(rowId), size: "sm", onChange: (checked, e) => {
99
+ e.stopPropagation();
100
+ onRowSelect === null || onRowSelect === void 0 ? void 0 : onRowSelect(rowId, checked);
101
+ } }) })), filteredColumns.map(column => {
102
+ var _a;
103
+ return (_jsx("div", { className: [
104
+ styles.cell,
105
+ shouldAllowWrap(column.allowWrap, isSelected, viewMode)
106
+ ? styles.allowWrap
107
+ : styles.nowrap,
108
+ ].join(' '), role: "cell", "data-align": (_a = column.align) !== null && _a !== void 0 ? _a : 'left', children: getCellDisplayValue(row, column) }, column.id));
109
+ })] }, `gridRow-${String(rowId)}`));
110
+ }) }));
111
+ const gridEl = (_jsxs(_Fragment, { children: [toolbar ? _jsx("div", { style: { marginBottom: 12 }, children: toolbar }) : null, _jsxs("div", { ...rest, className: `${styles.grid} ${styles[variant]} ${styles[size]} ${getRowSeverity ? styles.severityTable : ''}`, role: "table", "aria-rowcount": data.length, children: [_jsx("div", { className: styles.header, role: "rowgroup", children: headerEl }), bodyEl] }), !data.length && !loading && _jsx(TableEmptyState, { config: emptyConfig })] }));
80
112
  if (fillViewport) {
81
113
  return (_jsxs("div", { style: {
82
114
  display: 'flex',
@@ -84,10 +116,10 @@ export function Table({ data, columns, selectedRows, onRowSelect, selectionMode
84
116
  gap: '20px',
85
117
  flexFlow: paginationPlacement === 'top' ? 'column-reverse' : 'column',
86
118
  position: 'relative',
87
- }, children: [_jsx("div", { ref: scrollRef, style: viewportStyle, className: styles.tableScroll, children: tableEl }), onPageChange && data.length > 0 && (_jsx(Pagination, { itemsCount: totalItemsCount, take: take, skip: skip, onPageChange: handlePageChange, showFirstLast: showFirstLast }))] }));
119
+ }, children: [_jsx("div", { ref: scrollRef, style: viewportStyle, className: styles.tableScroll, children: gridEl }), onPageChange && data.length > 0 && (_jsx(Pagination, { itemsCount: totalItemsCount, take: take, skip: skip, onPageChange: handlePageChange, showFirstLast: showFirstLast }))] }));
88
120
  }
89
121
  return (_jsxs("div", { className: "dbc-flex dbc-flex-column dbc-gap-md", style: {
90
122
  flexFlow: paginationPlacement === 'top' ? 'column-reverse' : 'column',
91
123
  position: 'relative',
92
- }, children: [tableEl, onPageChange && data.length > 0 && (_jsx(Pagination, { itemsCount: totalItemsCount, take: take, skip: skip, onPageChange: handlePageChange }))] }));
124
+ }, children: [gridEl, onPageChange && data.length > 0 && (_jsx(Pagination, { itemsCount: totalItemsCount, take: take, skip: skip, onPageChange: handlePageChange }))] }));
93
125
  }
@@ -1,19 +1,10 @@
1
- /* Table.module.css (updated) */
2
-
3
- /* =========================
4
- Base table
5
- ========================= */
6
-
7
- .table {
1
+ .grid {
8
2
  inline-size: 100%;
9
3
  max-inline-size: 100%;
10
- overflow: visible;
11
- border-collapse: collapse;
12
4
  font-family: var(--font-family);
13
5
  font-size: var(--font-size-sm);
14
6
  color: var(--color-fg-default);
15
7
  background: var(--color-bg-surface);
16
- table-layout: fixed;
17
8
  }
18
9
 
19
10
  .tableScroll {
@@ -23,38 +14,39 @@
23
14
  -webkit-overflow-scrolling: touch;
24
15
  }
25
16
 
26
- /* =========================
27
- Header
28
- ========================= */
29
-
30
- .table thead {
17
+ /* Header wrapper sticky */
18
+ .header {
31
19
  position: sticky;
32
20
  top: 0;
33
21
  z-index: 10;
34
22
  background-color: var(--color-bg-surface);
35
23
  }
36
24
 
37
- .table.primary thead {
25
+ .primary .header {
38
26
  box-shadow: var(--shadow-md);
39
27
  }
40
28
 
41
- .table .th {
29
+ /* Shared row grid template */
30
+ .headerRow,
31
+ .row {
42
32
  position: relative;
33
+ display: grid;
34
+ grid-template-columns: var(--grid-template);
35
+ align-items: stretch;
36
+ inline-size: 100%;
37
+ }
43
38
 
39
+ /* Header cells */
40
+ .headerCell {
41
+ position: relative;
44
42
  padding-block: var(--spacing-xs);
45
43
  padding-inline: var(--spacing-sm);
46
44
 
47
- text-align: left;
48
- vertical-align: middle;
49
-
50
- background: inherit;
51
-
52
45
  /* Typography */
53
46
  font-size: var(--font-size-xs);
54
47
  font-weight: var(--font-weight-normal);
55
48
  letter-spacing: var(--letter-spacing-wide);
56
49
  text-transform: uppercase;
57
-
58
50
  color: var(--color-fg-subtle);
59
51
 
60
52
  /* Truncation */
@@ -62,159 +54,65 @@
62
54
  overflow: hidden;
63
55
  text-overflow: ellipsis;
64
56
 
65
- /* Width control */
66
57
  min-width: 0;
67
58
  }
68
59
 
69
- /* Small variant: header padding */
70
- .table.sm .th {
60
+ .sm .headerCell {
71
61
  padding-inline: var(--spacing-md);
72
62
  }
73
63
 
74
- /* Header layout */
75
- .th > .thInner {
76
- display: flex;
77
- align-items: center;
78
- inline-size: 100%;
79
- }
80
-
81
- /* Sort button (full width click target, but content aligns via modifiers) */
82
- .thButton {
83
- all: unset;
84
- display: flex;
85
- align-items: center;
86
- gap: var(--spacing-xxs);
87
- inline-size: 100%;
88
- cursor: pointer;
89
- user-select: none;
90
- border-radius: var(--border-radius-default);
91
- padding-block: 2px;
92
- padding-inline: 2px;
93
-
94
- /* default */
95
- justify-content: flex-start;
96
- }
97
-
98
- /* IMPORTANT: use consistent PascalCase suffixes */
99
- .thButtonRight {
100
- justify-content: flex-end;
101
- }
102
-
103
- .thButtonCenter {
104
- justify-content: center;
105
- }
106
-
107
- /* Label should NOT grow to fill space (it breaks alignment) */
108
- .thLabel {
109
- overflow: hidden;
110
- text-overflow: ellipsis;
111
- white-space: nowrap;
112
- flex: 0 1 auto;
113
- min-width: 0;
114
- }
115
-
116
- /* Match label text alignment to column alignment */
117
- .thLabelRight {
118
- text-align: right;
119
- }
120
-
121
- .thLabelCenter {
122
- text-align: center;
123
- }
124
-
125
- .thExtras {
126
- display: inline-flex;
127
- align-items: center;
128
- }
129
-
130
- .sortIndicator {
131
- display: inline-flex;
132
- flex: 0 0 auto;
133
- align-items: center;
134
- }
135
-
136
- .sortIndicator svg {
137
- inline-size: var(--icon-size-sm);
138
- block-size: var(--icon-size-sm);
139
- }
140
-
141
- /* =========================
142
- Body + cells
143
- ========================= */
144
-
145
- .tBody::before {
64
+ /* Body */
65
+ .body::before {
146
66
  content: '';
147
67
  height: var(--spacing-xs);
148
68
  display: block;
149
69
  }
150
70
 
151
- .tBody tr td {
71
+ /* Data cells */
72
+ .cell {
152
73
  position: relative;
153
74
  padding-block: var(--spacing-xs);
154
75
  padding-inline: var(--spacing-sm);
155
-
156
76
  border-block-end: var(--border-width-thin) solid var(--color-border-default);
157
-
158
77
  text-align: start;
159
78
  vertical-align: top;
160
-
161
- overflow-wrap: break-word;
162
- word-break: normal;
79
+ min-width: 0;
80
+ overflow: hidden;
81
+ text-overflow: ellipsis;
163
82
  }
164
83
 
165
- /* Small variant applies to all cells by default */
166
- .table.sm .tBody tr td {
84
+ .sm .cell {
167
85
  padding-block: var(--spacing-xxs);
168
86
  padding-inline: var(--spacing-md);
169
87
  font-size: var(--font-size-xs);
170
88
  line-height: var(--line-height-normal);
171
89
  }
172
90
 
173
- /* =========================
174
- Selection column
175
- ========================= */
176
-
177
- .tBody tr td.selectionCell,
178
- .table .tBody tr td.selectionCell,
179
- .table td.selectionCell {
180
- padding-inline: var(--spacing-xxs);
181
- }
182
-
183
- .table .th.selectionCell,
184
- .table th.selectionCell,
185
- th.selectionCell {
186
- width: 34px !important;
91
+ .headerCell[data-align='right'],
92
+ .cell[data-align='right'] {
93
+ text-align: end;
94
+ font-variant-numeric: tabular-nums;
187
95
  }
188
96
 
189
- /* Override the .table.sm .tBody tr td padding-inline */
190
- .table.sm .tBody tr td.selectionCell,
191
- .table.table.sm .tBody tr td.selectionCell {
192
- padding-inline: var(--spacing-xxs);
97
+ .headerCell[data-align='center'],
98
+ .cell[data-align='center'] {
99
+ text-align: center;
193
100
  }
194
101
 
195
- .table.severityTable .tBody tr td:first-child {
196
- padding-inline-start: var(--spacing-md);
197
- }
198
- .table.severityTable .tBody tr td.selectionCell,
199
- .table.severityTable td.selectionCell,
200
- .table.severityTable .th.selectionCell,
201
- .table.severityTable th.selectionCell {
202
- padding-inline: var(--spacing-xxs);
102
+ /* Selection column */
103
+ .selectionCell {
104
+ padding: inherit 0 !important;
105
+ overflow: visible !important;
106
+ display: flex;
107
+ align-items: flex-start;
108
+ justify-content: center;
203
109
  }
204
110
 
205
- .table.sm.severityTable .tBody tr td.selectionCell,
206
- .table.table.sm.severityTable .tBody tr td.selectionCell,
207
- .table.sm.severityTable .th.selectionCell,
208
- .table.table.sm.severityTable .th.selectionCell,
209
- .table.sm.severityTable th.selectionCell {
210
- padding-inline: var(--spacing-xxs);
211
- padding-inline-start: 14px;
111
+ /* Rows (interaction + states) */
112
+ .row {
113
+ background: transparent;
212
114
  }
213
115
 
214
- /* =========================
215
- Rows (interaction + states)
216
- ========================= */
217
-
218
116
  .clickableRow {
219
117
  cursor: pointer;
220
118
  }
@@ -231,16 +129,12 @@ th.selectionCell {
231
129
  background-color: var(--color-bg-selected-hover);
232
130
  }
233
131
 
234
- .tr--hover:hover {
235
- background-color: var(--color-bg-contextual);
236
- }
237
-
238
- .striped tr:nth-child(even):not(.selectedRow):not(:hover) {
132
+ .striped .row:nth-child(even):not(.selectedRow):not(:hover) {
239
133
  background-color: var(--color-bg-surface-subtle);
240
134
  }
241
135
 
242
- /* Focus ring (fix selector: .tBody, not .tbody) */
243
- .table .tBody tr:focus-within {
136
+ /* Focus ring: apply when something inside row is focused */
137
+ .row:focus-within {
244
138
  outline: none;
245
139
  box-shadow:
246
140
  inset 2px 0 0 var(--color-brand),
@@ -249,10 +143,7 @@ th.selectionCell {
249
143
  inset 0 -2px 0 var(--color-brand);
250
144
  }
251
145
 
252
- /* =========================
253
- Content utilities
254
- ========================= */
255
-
146
+ /* Content utilities */
256
147
  .nowrap {
257
148
  white-space: nowrap;
258
149
  overflow: hidden;
@@ -260,31 +151,74 @@ th.selectionCell {
260
151
  }
261
152
 
262
153
  .allowWrap {
263
- word-break: break-all !important;
264
- overflow-wrap: anywhere !important;
154
+ white-space: normal;
155
+ overflow-wrap: break-word;
156
+ word-break: normal;
265
157
  }
266
158
 
267
- .td--numeric {
268
- text-align: end;
159
+ /* Header inner layout (same as before) */
160
+ .thInner {
161
+ display: flex;
162
+ align-items: center;
163
+ inline-size: 100%;
269
164
  }
270
165
 
271
- .td--muted {
272
- color: var(--color-fg-muted);
166
+ /* Sort button */
167
+ .thButton {
168
+ all: unset;
169
+ display: flex;
170
+ align-items: center;
171
+ gap: var(--spacing-xxs);
172
+ inline-size: 100%;
173
+ cursor: pointer;
174
+ user-select: none;
175
+ border-radius: var(--border-radius-default);
176
+ padding-block: 2px;
177
+ padding-inline: 2px;
178
+ justify-content: flex-start;
273
179
  }
274
180
 
275
- .td .error {
276
- color: var(--color-danger);
181
+ .thButtonRight {
182
+ justify-content: flex-end;
277
183
  }
278
184
 
279
- /* =========================
280
- Severity rail
281
- ========================= */
185
+ .thButtonCenter {
186
+ justify-content: center;
187
+ }
282
188
 
283
- .tBody tr.severity td:first-child {
284
- position: relative;
189
+ .thLabel {
190
+ overflow: hidden;
191
+ text-overflow: ellipsis;
192
+ white-space: nowrap;
193
+ flex: 0 1 auto;
194
+ min-width: 0;
195
+ }
196
+
197
+ .thLabelRight {
198
+ text-align: right;
199
+ }
200
+
201
+ .thLabelCenter {
202
+ text-align: center;
203
+ }
204
+
205
+ .thExtras {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ }
209
+
210
+ .sortIndicator {
211
+ display: inline-flex;
212
+ flex: 0 0 auto;
213
+ align-items: center;
285
214
  }
286
215
 
287
- .table.severityTable .tBody tr.severity td:first-child::before {
216
+ .sortIndicator svg {
217
+ inline-size: var(--icon-size-sm);
218
+ block-size: var(--icon-size-sm);
219
+ }
220
+
221
+ .severityTable .row.severity::before {
288
222
  content: '';
289
223
  position: absolute;
290
224
  left: 2px;
@@ -293,11 +227,12 @@ th.selectionCell {
293
227
  width: 3px;
294
228
  background-color: var(--row-severity-color);
295
229
  border-radius: 1px;
296
-
297
- z-index: 0;
230
+ z-index: 2;
231
+ pointer-events: none;
298
232
  }
299
233
 
300
- .table.severityTable .tBody tr.severity td:first-child > * {
234
+ /* Ensure content is above the rail */
235
+ .severityTable .row.severity > * {
301
236
  position: relative;
302
- z-index: 1;
237
+ z-index: 3;
303
238
  }
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import { type TableProps, type TableVariant } from './Table';
4
4
  import { ViewMode } from '../../hooks/useTableSettings';
5
5
  type Filterable<T> = Array<keyof T>;
6
- export type TanstackTableProps<T extends Record<string, any>> = Omit<TableProps<T>, 'columns' | 'onSortChange' | 'sortById' | 'sortDirection' | 'headerBelowRow' | 'headerExtras' | 'columnStyles' | 'toolbar'> & {
6
+ export type TanstackTableProps<T extends Record<string, any>> = Omit<TableProps<T>, 'columns' | 'onSortChange' | 'sortById' | 'sortDirection' | 'headerExtras' | 'gridTemplateColumns' | 'toolbar'> & {
7
7
  columns: ReadonlyArray<ColumnDef<T, any>>;
8
8
  filterable?: Filterable<T>;
9
9
  sorting?: SortingState;
@@ -4,9 +4,33 @@ import { useReactTable, getCoreRowModel, getSortedRowModel, getFilteredRowModel,
4
4
  import * as React from 'react';
5
5
  import ColumnResizer from './components/column-resizer/ColumnResizer';
6
6
  import { Table } from './Table';
7
- import { buildColumnVisibilityFromVisibleIds, mapDefsToColumnItems, sortingEqual, getSortPropsFromSorting, buildColumnStyles, columnItemsToIdSet, } from './tanstackTable.utils';
7
+ import { buildColumnVisibilityFromVisibleIds, mapDefsToColumnItems, sortingEqual, getSortPropsFromSorting, columnItemsToIdSet, } from './tanstackTable.utils';
8
+ function buildGridTemplateColumns(args) {
9
+ var _a, _b, _c, _d;
10
+ const { table, allowedIds, hasSelection, selectionPx, defaultMinPx } = args;
11
+ const parts = [];
12
+ if (hasSelection)
13
+ parts.push(`${selectionPx}px`);
14
+ const leaf = table.getVisibleLeafColumns().filter(c => allowedIds.has(c.id));
15
+ for (const c of leaf) {
16
+ const def = c.columnDef;
17
+ const min = Number((_a = def.minSize) !== null && _a !== void 0 ? _a : defaultMinPx);
18
+ const maxRaw = def.maxSize;
19
+ const max = maxRaw == null ? Number.POSITIVE_INFINITY : Number(maxRaw);
20
+ const size = Number((_d = (_c = (_b = c.getSize) === null || _b === void 0 ? void 0 : _b.call(c)) !== null && _c !== void 0 ? _c : def.size) !== null && _d !== void 0 ? _d : min);
21
+ if (Number.isFinite(min) && Number.isFinite(max) && min > 0 && max === min) {
22
+ parts.push(`${Math.round(min)}px`);
23
+ continue;
24
+ }
25
+ // Weight based on size so wide columns keep more room
26
+ const weight = Math.max(1, Math.min(6, Math.round(size / 100)));
27
+ // Respect minSize; scroll if mins don't fit
28
+ parts.push(`minmax(${Math.round(min)}px, ${weight}fr)`);
29
+ }
30
+ return parts.join(' ');
31
+ }
8
32
  export function TanstackTable(props) {
9
- const { data, dataKey, columns, filterable = [], sorting: controlledSorting, onSortingChange, optimisticSortingUi = true, visibleColumnIds, manualSorting, ...tableProps } = props;
33
+ const { data, dataKey, columns, sorting: controlledSorting, onSortingChange, optimisticSortingUi = true, visibleColumnIds, manualSorting, selectedRows, onRowSelect, ...tableProps } = props;
10
34
  const isControlledSorting = controlledSorting != null;
11
35
  const [uiSorting, setUiSorting] = React.useState(controlledSorting !== null && controlledSorting !== void 0 ? controlledSorting : []);
12
36
  React.useEffect(() => {
@@ -53,19 +77,11 @@ export function TanstackTable(props) {
53
77
  const allowedIds = React.useMemo(() => columnItemsToIdSet(columnItems), [columnItems]);
54
78
  const visibleData = table.getRowModel().rows.map(r => r.original);
55
79
  const { sortById, sortDirection } = getSortPropsFromSorting(uiSorting);
56
- const columnStyles = React.useMemo(() => {
57
- const leafCols = table.getAllLeafColumns();
58
- return buildColumnStyles(leafCols, allowedIds, { minWidth: 80, maxWidth: 800 });
59
- }, [table, columnSizing, allowedIds]);
60
80
  const handleSortChange = React.useCallback((column, direction) => {
61
- // Translate Table's direction -> TanStack SortingState
62
81
  const next = direction == null ? [] : [{ id: column.id, desc: direction === 'desc' }];
63
- // Mirror TanStack's onSortingChange behavior
64
82
  if (optimisticSortingUi)
65
83
  setUiSorting(next);
66
84
  onSortingChange === null || onSortingChange === void 0 ? void 0 : onSortingChange(next);
67
- // If you are doing server-side sorting, you likely also want:
68
- // table.resetPageIndex?.() or external pagination reset (depends on your setup)
69
85
  }, [optimisticSortingUi, onSortingChange]);
70
86
  const headerExtras = React.useCallback(({ index }) => {
71
87
  var _a, _b, _c, _d;
@@ -80,5 +96,15 @@ export function TanstackTable(props) {
80
96
  return null;
81
97
  return _jsx(ColumnResizer, { id: header.column.id, handler: handler });
82
98
  }, [table]);
83
- return (_jsx(Table, { ...tableProps, onSortChange: handleSortChange, dataKey: dataKey, data: visibleData, columns: columnItems, sortById: sortById, sortDirection: sortDirection, columnStyles: columnStyles, headerExtras: headerExtras }));
99
+ const hasSelection = Boolean(selectedRows && onRowSelect && dataKey);
100
+ const gridTemplateColumns = React.useMemo(() => {
101
+ return buildGridTemplateColumns({
102
+ table,
103
+ allowedIds,
104
+ hasSelection,
105
+ selectionPx: 36,
106
+ defaultMinPx: 80,
107
+ });
108
+ }, [table, allowedIds, hasSelection, columnSizing]);
109
+ return (_jsx(Table, { ...tableProps, onSortChange: handleSortChange, dataKey: dataKey, data: visibleData, columns: columnItems, sortById: sortById, sortDirection: sortDirection, gridTemplateColumns: gridTemplateColumns, headerExtras: headerExtras, selectedRows: selectedRows, onRowSelect: onRowSelect }));
84
110
  }
@@ -2,14 +2,12 @@ export function getVisibleColumns(columns) {
2
2
  return columns.filter(c => !c.hidden);
3
3
  }
4
4
  export function getColumnStyle(columnId, columnStyles, alignment, verticalAlignment) {
5
- var _a;
6
5
  const baseStyle = columnStyles === null || columnStyles === void 0 ? void 0 : columnStyles[columnId];
7
6
  return {
8
7
  ...(baseStyle !== null && baseStyle !== void 0 ? baseStyle : {}),
9
8
  ...(alignment === 'right' ? { fontVariantNumeric: 'tabular-nums' } : null),
10
9
  verticalAlign: verticalAlignment !== null && verticalAlignment !== void 0 ? verticalAlignment : 'top',
11
10
  textAlign: alignment !== null && alignment !== void 0 ? alignment : 'left',
12
- minWidth: (_a = baseStyle === null || baseStyle === void 0 ? void 0 : baseStyle.minWidth) !== null && _a !== void 0 ? _a : 0,
13
11
  };
14
12
  }
15
13
  export function getHeaderLabel(header) {
@@ -60,12 +60,15 @@ body.dbc-app {
60
60
 
61
61
  .dbc-table {
62
62
  --card-label-width: 260px;
63
-
64
63
  border-collapse: collapse;
65
64
  font-size: var(--font-size-sm);
66
65
  line-height: var(--line-height-normal);
67
66
  }
68
67
 
68
+ .dbc-table--hover tr:hover {
69
+ background-color: var(--color-bg-contextual);
70
+ }
71
+
69
72
  .dbc-table tr + tr th,
70
73
  .dbc-table tr + tr td {
71
74
  padding-block: var(--spacing-xxs);
@@ -60,12 +60,15 @@ body.dbc-app {
60
60
 
61
61
  .dbc-table {
62
62
  --card-label-width: 260px;
63
-
64
63
  border-collapse: collapse;
65
64
  font-size: var(--font-size-sm);
66
65
  line-height: var(--line-height-normal);
67
66
  }
68
67
 
68
+ .dbc-table--hover tr:hover {
69
+ background-color: var(--color-bg-contextual);
70
+ }
71
+
69
72
  .dbc-table tr + tr th,
70
73
  .dbc-table tr + tr td {
71
74
  padding-block: var(--spacing-xxs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",