@dbcdk/react-components 0.0.65 → 0.0.67

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.
Files changed (26) hide show
  1. package/dist/components/button-select/ButtonSelect.d.ts +16 -0
  2. package/dist/components/button-select/ButtonSelect.js +7 -0
  3. package/dist/components/button-select/ButtonSelect.module.css +40 -0
  4. package/dist/components/forms/input/Input.module.css +6 -5
  5. package/dist/components/nav-bar/NavBar.d.ts +1 -0
  6. package/dist/components/overlay/tooltip/Tooltip.module.css +0 -2
  7. package/dist/components/search-box/SearchBox.d.ts +1 -0
  8. package/dist/components/search-box/SearchBox.js +5 -2
  9. package/dist/components/sidebar/Sidebar.d.ts +6 -1
  10. package/dist/components/sidebar/Sidebar.js +2 -3
  11. package/dist/components/sidebar/components/SidebarItem.d.ts +2 -1
  12. package/dist/components/sidebar/components/SidebarItem.js +2 -2
  13. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.d.ts +2 -1
  14. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +3 -3
  15. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.d.ts +7 -2
  16. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +132 -2
  17. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +67 -5
  18. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.d.ts +2 -1
  19. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +3 -2
  20. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +11 -0
  21. package/dist/components/sidebar/components/sidebar-items/SidebarItems.js +4 -4
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/styles/themes/dbc/dark.css +5 -5
  25. package/dist/styles/themes/dbc/light.css +5 -5
  26. package/package.json +2 -6
@@ -0,0 +1,16 @@
1
+ import type { JSX } from 'react';
2
+ import React from 'react';
3
+ import type { ButtonSize } from '../../components/button/Button';
4
+ export interface ButtonSelectOption<T extends string = string> {
5
+ value: T;
6
+ label: React.ReactNode;
7
+ disabled?: boolean;
8
+ }
9
+ export interface ButtonSelectProps<T extends string = string> {
10
+ options: ButtonSelectOption<T>[];
11
+ value: T;
12
+ onChange: (value: T) => void;
13
+ size?: ButtonSize;
14
+ disabled?: boolean;
15
+ }
16
+ export declare function ButtonSelect<T extends string>({ options, value, onChange, size, disabled, }: ButtonSelectProps<T>): JSX.Element;
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Button } from '../../components/button/Button';
4
+ import styles from './ButtonSelect.module.css';
5
+ export function ButtonSelect({ options, value, onChange, size = 'md', disabled, }) {
6
+ return (_jsx("div", { className: styles.group, role: "group", children: options.map(option => (_jsx(Button, { className: option.value === value ? `${styles.btn} ${styles.btnSelected}` : styles.btn, variant: "outlined", shape: "default", size: size, active: option.value === value, disabled: disabled || option.disabled, onClick: () => onChange(option.value), children: option.label }, option.value))) }));
7
+ }
@@ -0,0 +1,40 @@
1
+ .group {
2
+ display: inline-flex;
3
+ flex-wrap: nowrap;
4
+ }
5
+
6
+ /* Collapse the doubled border between adjacent buttons */
7
+ .group .btn:not(:first-child) {
8
+ margin-left: -1px;
9
+ }
10
+
11
+ /* Flatten interior corners */
12
+ .group .btn:not(:first-child) {
13
+ border-top-left-radius: 0;
14
+ border-bottom-left-radius: 0;
15
+ }
16
+
17
+ .group .btn:not(:last-child) {
18
+ border-top-right-radius: 0;
19
+ border-bottom-right-radius: 0;
20
+ }
21
+
22
+ /* Raise hovered / focused / selected button so its full border is visible above siblings */
23
+ .group .btn:hover,
24
+ .group .btn:focus-visible,
25
+ .group .btn:active,
26
+ .group .btnSelected {
27
+ position: relative;
28
+ z-index: 1;
29
+ }
30
+
31
+ /* Selected state follows the system's selection language (same as Chip) */
32
+ .group .btnSelected {
33
+ background-color: var(--color-bg-selected);
34
+ color: var(--color-brand);
35
+ border-color: var(--color-border-selected);
36
+ }
37
+
38
+ .group .btnSelected:hover {
39
+ background-color: var(--color-bg-selected);
40
+ }
@@ -163,9 +163,9 @@
163
163
 
164
164
  .standalone {
165
165
  background-color: var(--color-bg-surface);
166
- border-color: var(--color-border-default);
166
+ border-color: var(--color-border-subtle);
167
167
  border-radius: var(--border-radius-rounded);
168
- box-shadow: var(--shadow-xs), var(--shadow-md);
168
+ box-shadow: var(--shadow-xs), var(--shadow-sm);
169
169
  }
170
170
 
171
171
  .standalone .input {
@@ -401,14 +401,15 @@
401
401
  .withButton:has(.standalone) .trailingButton {
402
402
  border-top-right-radius: var(--border-radius-rounded);
403
403
  border-bottom-right-radius: var(--border-radius-rounded);
404
- border-left-color: var(--color-border-default);
404
+ border-left-color: var(--color-border-subtle);
405
+ border-color: var(--color-border-subtle);
405
406
  background-color: var(--color-bg-surface);
406
- box-shadow: var(--shadow-xs), var(--shadow-md);
407
+ box-shadow: var(--shadow-xs), var(--shadow-sm);
407
408
  }
408
409
 
409
410
  .withButton:has(.standalone) .trailingButton:hover {
410
411
  border-color: var(--color-border-strong);
411
- box-shadow: var(--shadow-sm), var(--shadow-md);
412
+ box-shadow: var(--shadow-sm), var(--shadow-sm);
412
413
  }
413
414
 
414
415
  /* Date/time picker indicator (WebKit) */
@@ -6,6 +6,7 @@ type NavBarBase = {
6
6
  icon?: ReactNode;
7
7
  enabled?: boolean;
8
8
  tags?: string[];
9
+ truncateLabel?: boolean;
9
10
  };
10
11
  /** Simple clickable item */
11
12
  export type NavBarLinkItem = NavBarBase & {
@@ -21,7 +21,6 @@
21
21
  position: fixed;
22
22
  z-index: var(--z-tooltip);
23
23
  pointer-events: none;
24
-
25
24
  background: var(--color-fg-default);
26
25
  color: var(--color-fg-on-strong);
27
26
  font-size: var(--font-size-xs);
@@ -30,7 +29,6 @@
30
29
  border-radius: var(--border-radius-default);
31
30
 
32
31
  /*
33
- ✅ Width behavior:
34
32
  - Don't let it run wild horizontally
35
33
  - But don't clip: allow wrap
36
34
  - Keep some relation to viewport
@@ -3,6 +3,7 @@ import { InputVariant } from '../../components/forms/input/Input';
3
3
  import { Size } from '../../types/sizes.types';
4
4
  type SearchBoxProps<T extends Record<string, unknown>> = {
5
5
  inputWidth?: string | number;
6
+ maxWidth?: string | number;
6
7
  inputSize?: Exclude<Size, 'xl'>;
7
8
  variant?: InputVariant;
8
9
  result?: T[];
@@ -7,7 +7,7 @@ import { Menu } from '../../components/menu/Menu';
7
7
  import { Popover } from '../../components/popover/Popover';
8
8
  import { SkeletonLoaderItem } from '../../components/skeleton-loader/skeleton-loader-item/SkeletonLoaderItem';
9
9
  import styles from './SearchBox.module.css';
10
- export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputSize, variant, result, debounce = true, debounceMs = 800, onSearch, onSelect, displayPopover, resultKeys, resultTemplate, initialTemplate, popoverMinWidth = '500px', noResultText = 'Ingen resultater', loading, enableHotkey = true, onButtonClick, buttonLabel, buttonIcon, fullWidth = false, value, onChange, ...rest }, ref) {
10
+ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWidth, inputSize, variant, result, debounce = true, debounceMs = 800, onSearch, onSelect, displayPopover, resultKeys, resultTemplate, initialTemplate, popoverMinWidth = '500px', noResultText = 'Ingen resultater', loading, enableHotkey = true, onButtonClick, buttonLabel, buttonIcon, fullWidth = false, value, onChange, ...rest }, ref) {
11
11
  const isControlled = value !== undefined;
12
12
  // What the user sees immediately in the textbox
13
13
  const [draft, setDraft] = useState(() => (isControlled ? String(value !== null && value !== void 0 ? value : '') : ''));
@@ -156,5 +156,8 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, inputS
156
156
  buttonIcon,
157
157
  fullWidth,
158
158
  ]);
159
- return _jsx("div", { style: fullWidth ? { width: '100%' } : undefined, children: inputField });
159
+ return (_jsx("div", { style: {
160
+ ...(fullWidth ? { width: '100%' } : undefined),
161
+ ...(maxWidth !== undefined ? { maxWidth } : undefined),
162
+ }, children: inputField }));
160
163
  });
@@ -11,6 +11,11 @@ interface SidebarProps {
11
11
  activeLink?: string;
12
12
  version?: string | number;
13
13
  hideSearch?: boolean;
14
+ footer?: React.ReactNode;
15
+ resizable?: boolean;
16
+ defaultWidth?: number;
17
+ minWidth?: number;
18
+ storageKey?: string;
14
19
  }
15
- export declare function Sidebar({ items, productLogo, activeLink, version, hideSearch, }: SidebarProps): JSX.Element;
20
+ export declare function Sidebar({ items, productLogo, activeLink, version, hideSearch, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarProps): JSX.Element;
16
21
  export {};
@@ -1,7 +1,6 @@
1
- 'use client';
2
1
  import { jsx as _jsx } from "react/jsx-runtime";
3
2
  import { SidebarContainer } from './components/sidebar-container/SidebarContainer';
4
3
  import { SidebarProvider } from './providers/SidebarProvider';
5
- export function Sidebar({ items, productLogo, activeLink, version, hideSearch, }) {
6
- return (_jsx(SidebarProvider, { items: items, children: _jsx(SidebarContainer, { productLogo: productLogo, activeLink: activeLink, version: version, hideSearch: hideSearch }) }));
4
+ export function Sidebar({ items, productLogo, activeLink, version, hideSearch, footer, resizable, defaultWidth, minWidth, storageKey, }) {
5
+ return (_jsx(SidebarProvider, { items: items, children: _jsx(SidebarContainer, { productLogo: productLogo, activeLink: activeLink, version: version, hideSearch: hideSearch, footer: footer, resizable: resizable, defaultWidth: defaultWidth, minWidth: minWidth, storageKey: storageKey }) }));
7
6
  }
@@ -4,6 +4,7 @@ interface SidebarItemProps {
4
4
  label: string;
5
5
  icon: React.ReactNode;
6
6
  href?: string;
7
+ truncateLabel?: boolean;
7
8
  }
8
- export declare function SidebarItem({ component: Component, label, icon, href, }: SidebarItemProps): React.ReactNode;
9
+ export declare function SidebarItem({ component: Component, label, icon, href, truncateLabel, }: SidebarItemProps): React.ReactNode;
9
10
  export {};
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { SidebarItemContent } from './sidebar-item-content/SidebarItemContent';
3
- export function SidebarItem({ component: Component, label, icon, href, }) {
3
+ export function SidebarItem({ component: Component, label, icon, href, truncateLabel, }) {
4
4
  if (!Component) {
5
5
  return null;
6
6
  }
7
- return (_jsx(Component, { children: _jsx(SidebarItemContent, { icon: icon, label: label, href: href }) }));
7
+ return (_jsx(Component, { children: _jsx(SidebarItemContent, { icon: icon, label: label, href: href, truncateLabel: truncateLabel }) }));
8
8
  }
@@ -6,6 +6,7 @@ type ExpandableSidebarItemProps = {
6
6
  component: React.ElementType;
7
7
  icon: React.ReactNode;
8
8
  href: string;
9
+ truncateLabel?: boolean;
9
10
  };
10
- export declare function ExpandableSidebarItem({ items, label, icon, component: Component, href, }: ExpandableSidebarItemProps): React.ReactNode;
11
+ export declare function ExpandableSidebarItem({ items, label, icon, component: Component, href, truncateLabel, }: ExpandableSidebarItemProps): React.ReactNode;
11
12
  export {};
@@ -10,7 +10,7 @@ import { SidebarItemContent } from '../sidebar-item-content/SidebarItemContent';
10
10
  import { SidebarItem } from '../SidebarItem';
11
11
  const isGroup = (item) => item.type === 'group';
12
12
  const isExpandable = (item) => item.type === 'expandable';
13
- export function ExpandableSidebarItem({ items, label, icon, component: Component, href, }) {
13
+ export function ExpandableSidebarItem({ items, label, icon, component: Component, href, truncateLabel, }) {
14
14
  const { defaultExpanded, resetExpandAll, isSidebarCollapsed, handleSidebarCollapseChange, expandItem, collapseItem, isExpanded, } = useSidebar();
15
15
  // Local-only state for animation coordination
16
16
  const [closing, setClosing] = useState(false);
@@ -60,7 +60,7 @@ export function ExpandableSidebarItem({ items, label, icon, component: Component
60
60
  if (isExpandable(item)) {
61
61
  return (_jsx(ExpandableChild, { items: (_b = item.children) !== null && _b !== void 0 ? _b : [], label: item.label, icon: item.icon, href: item.href, component: item.component }, key));
62
62
  }
63
- return (_jsx(SidebarItem, { component: item.component, label: item.label, icon: item.icon, href: item.href }, key));
63
+ return (_jsx(SidebarItem, { component: item.component, label: item.label, icon: item.icon, href: item.href, truncateLabel: item.truncateLabel }, key));
64
64
  };
65
- return (_jsxs("div", { className: `${styles.container}`, children: [_jsx(Component, { onClick: () => toggleAccordion(undefined, true), children: _jsx(SidebarItemContent, { headerStyle: expanded, icon: icon, label: label, href: href, disableActiveStyles: expanded, suffixIcon: isSidebarCollapsed ? null : (_jsx(Button, { variant: "outlined", onClick: toggleAccordion, children: _jsx(ChevronDown, { className: `${styles.chevron} ${expanded ? styles.chevronExpanded : ''}` }) })) }) }), expanded && !isSidebarCollapsed && (_jsx("div", { onAnimationEnd: handleAnimationEnd, className: `${styles.childrenContainer} ${closing ? 'animate--collapse' : ''} ${expanded ? 'animate--expand' : 'visually-hidden'}`, children: items.map((item, idx) => renderNavItem(item, `${href}-${idx}`)) }))] }));
65
+ return (_jsxs("div", { className: `${styles.container}`, children: [_jsx(Component, { onClick: () => toggleAccordion(undefined, true), children: _jsx(SidebarItemContent, { headerStyle: expanded, icon: icon, label: label, href: href, disableActiveStyles: expanded, truncateLabel: truncateLabel, suffixIcon: isSidebarCollapsed ? null : (_jsx(Button, { variant: "outlined", onClick: toggleAccordion, children: _jsx(ChevronDown, { className: `${styles.chevron} ${expanded ? styles.chevronExpanded : ''}` }) })) }) }), expanded && !isSidebarCollapsed && (_jsx("div", { onAnimationEnd: handleAnimationEnd, className: `${styles.childrenContainer} ${closing ? 'animate--collapse' : ''} ${expanded ? 'animate--expand' : 'visually-hidden'}`, children: items.map((item, idx) => renderNavItem(item, `${href}-${idx}`)) }))] }));
66
66
  }
@@ -1,4 +1,4 @@
1
- import type { ReactNode, JSX } from 'react';
1
+ import type { JSX, ReactNode } from 'react';
2
2
  interface SidebarContainerProps {
3
3
  logo?: ReactNode;
4
4
  productName?: string;
@@ -6,6 +6,11 @@ interface SidebarContainerProps {
6
6
  activeLink?: string;
7
7
  version?: string | number;
8
8
  hideSearch?: boolean;
9
+ footer?: ReactNode;
10
+ resizable?: boolean;
11
+ defaultWidth?: number;
12
+ minWidth?: number;
13
+ storageKey?: string;
9
14
  }
10
- export declare function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, }: SidebarContainerProps): JSX.Element;
15
+ export declare function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarContainerProps): JSX.Element;
11
16
  export {};
@@ -1,12 +1,142 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { ChevronLeft } from 'lucide-react';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
5
  import { Logo } from '../../../../assets/logo';
4
6
  import { Button } from '../../../../components/button/Button';
5
7
  import { SidebarItems } from '../../../../components/sidebar/components/sidebar-items/SidebarItems';
6
8
  import SidenavFiltering from '../../../../components/sidebar/components/sidenav-filteirng/SidenavFiltering';
7
9
  import { useSidebar } from '../../../../components/sidebar/providers/SidebarProvider';
8
10
  import styles from './SidebarContainer.module.css';
9
- export function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, }) {
11
+ function clamp(n, min, max) {
12
+ return Math.max(min, Math.min(max, n));
13
+ }
14
+ function readStoredWidth(key) {
15
+ try {
16
+ const raw = localStorage.getItem(key);
17
+ if (!raw)
18
+ return null;
19
+ const num = Number(raw);
20
+ return Number.isFinite(num) ? num : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function writeStoredWidth(key, value) {
27
+ try {
28
+ localStorage.setItem(key, String(Math.round(value)));
29
+ }
30
+ catch {
31
+ // ignore
32
+ }
33
+ }
34
+ function removeStoredWidth(key) {
35
+ try {
36
+ localStorage.removeItem(key);
37
+ }
38
+ catch {
39
+ // ignore
40
+ }
41
+ }
42
+ export function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, footer, resizable, defaultWidth = 240, minWidth = 160, storageKey, }) {
10
43
  const { isSidebarCollapsed, handleSidebarCollapseChange } = useSidebar();
11
- return (_jsxs("div", { className: `${styles.container} ${isSidebarCollapsed ? styles.collapsed : ''}`, children: [_jsx("div", { className: styles.header, children: _jsxs("div", { className: styles.productHeader, children: [_jsx("div", { className: styles.productLogo, children: productLogo }), _jsx(Button, { size: "md", variant: "inline", shape: "round", "aria-label": "Collapse sidebar", icon: _jsx(ChevronLeft, { className: isSidebarCollapsed ? styles.collapsedIcon : '' }), onClick: () => handleSidebarCollapseChange(!isSidebarCollapsed) })] }) }), _jsxs("div", { className: styles.content, children: [!hideSearch && (_jsx("div", { className: styles.filter, children: _jsx(SidenavFiltering, {}) })), _jsx("div", { className: `${styles.links} hideScrollBar`, children: _jsx(SidebarItems, { activeLink: activeLink }) })] }), _jsxs("div", { className: styles.footer, children: [_jsx("div", { className: styles.companyLogo, children: logo !== null && logo !== void 0 ? logo : _jsx(Logo, {}) }), version && _jsx("div", { className: `${styles.version} dbc-muted-text dbc-sm-text`, children: version })] })] }));
44
+ const initialStoredWidth = typeof window !== 'undefined' && storageKey ? readStoredWidth(storageKey) : null;
45
+ const [sidebarWidth, setSidebarWidth] = useState(() => initialStoredWidth !== null && initialStoredWidth !== void 0 ? initialStoredWidth : defaultWidth);
46
+ const [manualWidth, setManualWidth] = useState(() => initialStoredWidth);
47
+ const [isResizing, setIsResizing] = useState(false);
48
+ const [ariaMaxWidth, setAriaMaxWidth] = useState(undefined);
49
+ const containerRef = useRef(null);
50
+ const draggingRef = useRef(false);
51
+ const pointerIdRef = useRef(null);
52
+ const startXRef = useRef(0);
53
+ const startWidthRef = useRef(0);
54
+ const maxWidthRef = useRef(Infinity);
55
+ // Use the viewport width as the ceiling — avoids the circular-dependency
56
+ // that occurs when the sidebar lives in an `auto`-width grid column whose
57
+ // size is determined by the sidebar itself (e.g. PageLayout vertical).
58
+ const getMaxWidth = useCallback(() => {
59
+ const viewportWidth = typeof window !== 'undefined'
60
+ ? window.innerWidth || document.documentElement.clientWidth
61
+ : Infinity;
62
+ return Math.max(minWidth, viewportWidth - minWidth);
63
+ }, [minWidth]);
64
+ useEffect(() => {
65
+ if (!storageKey)
66
+ return;
67
+ if (manualWidth === null) {
68
+ removeStoredWidth(storageKey);
69
+ return;
70
+ }
71
+ writeStoredWidth(storageKey, manualWidth);
72
+ }, [manualWidth, storageKey]);
73
+ useEffect(() => {
74
+ setAriaMaxWidth(getMaxWidth());
75
+ }, [getMaxWidth]);
76
+ useEffect(() => {
77
+ return () => {
78
+ document.body.style.cursor = '';
79
+ document.body.style.userSelect = '';
80
+ };
81
+ }, []);
82
+ const updateWidth = useCallback((nextWidth) => {
83
+ setManualWidth(nextWidth);
84
+ setSidebarWidth(nextWidth);
85
+ }, []);
86
+ const onResizePointerDown = useCallback((e) => {
87
+ const maxWidth = getMaxWidth();
88
+ if (maxWidth === null)
89
+ return;
90
+ e.currentTarget.setPointerCapture(e.pointerId);
91
+ maxWidthRef.current = maxWidth;
92
+ draggingRef.current = true;
93
+ pointerIdRef.current = e.pointerId;
94
+ startXRef.current = e.clientX;
95
+ startWidthRef.current = sidebarWidth;
96
+ setIsResizing(true);
97
+ document.body.style.userSelect = 'none';
98
+ document.body.style.cursor = 'col-resize';
99
+ }, [getMaxWidth, sidebarWidth]);
100
+ const onResizePointerMove = useCallback((e) => {
101
+ if (!draggingRef.current)
102
+ return;
103
+ if (pointerIdRef.current !== null && e.pointerId !== pointerIdRef.current)
104
+ return;
105
+ const next = startWidthRef.current + (e.clientX - startXRef.current);
106
+ updateWidth(clamp(next, minWidth, maxWidthRef.current));
107
+ }, [minWidth, updateWidth]);
108
+ const endResizeDrag = useCallback(() => {
109
+ if (!draggingRef.current)
110
+ return;
111
+ draggingRef.current = false;
112
+ pointerIdRef.current = null;
113
+ setIsResizing(false);
114
+ document.body.style.cursor = '';
115
+ document.body.style.userSelect = '';
116
+ }, []);
117
+ const resetWidth = useCallback(() => {
118
+ setManualWidth(null);
119
+ setSidebarWidth(clamp(defaultWidth, minWidth, getMaxWidth()));
120
+ }, [defaultWidth, getMaxWidth, minWidth]);
121
+ const onKeyDown = useCallback((e) => {
122
+ const maxWidth = getMaxWidth();
123
+ const step = e.shiftKey ? 32 : 8;
124
+ let next = null;
125
+ if (e.key === 'ArrowLeft')
126
+ next = sidebarWidth - step;
127
+ if (e.key === 'ArrowRight')
128
+ next = sidebarWidth + step;
129
+ if (e.key === 'Home')
130
+ next = minWidth;
131
+ if (e.key === 'End')
132
+ next = maxWidth;
133
+ if (next === null)
134
+ return;
135
+ e.preventDefault();
136
+ updateWidth(clamp(next, minWidth, maxWidth));
137
+ }, [getMaxWidth, minWidth, sidebarWidth, updateWidth]);
138
+ const containerStyle = useMemo(() => ({
139
+ '--sidebar-width': `${sidebarWidth}px`,
140
+ }), [sidebarWidth]);
141
+ return (_jsxs("div", { ref: containerRef, role: "complementary", className: `${styles.container} ${isSidebarCollapsed ? styles.collapsed : ''} ${isResizing ? styles.resizing : ''}`, style: containerStyle, children: [_jsx("div", { className: styles.header, children: _jsxs("div", { className: styles.productHeader, children: [_jsx("div", { className: styles.productLogo, children: productLogo }), _jsx(Button, { size: "md", variant: "inline", shape: "round", "aria-label": "Collapse sidebar", icon: _jsx(ChevronLeft, { className: isSidebarCollapsed ? styles.collapsedIcon : '' }), onClick: () => handleSidebarCollapseChange(!isSidebarCollapsed) })] }) }), _jsxs("div", { className: styles.content, children: [!hideSearch && (_jsx("div", { className: styles.filter, children: _jsx(SidenavFiltering, {}) })), _jsx("div", { className: `${styles.links} hideScrollBar`, children: _jsx(SidebarItems, { activeLink: activeLink }) })] }), footer && _jsx("div", { className: styles.footerSlot, children: footer }), _jsxs("div", { className: styles.footer, children: [_jsx("div", { className: styles.companyLogo, children: logo !== null && logo !== void 0 ? logo : _jsx(Logo, {}) }), version && _jsx("div", { className: `${styles.version} dbc-muted-text dbc-sm-text`, children: version })] }), resizable && (_jsx("div", { className: styles.resizeHandle, role: "separator", "aria-label": "Resize sidebar", "aria-orientation": "vertical", "aria-valuemin": Math.round(minWidth), "aria-valuemax": ariaMaxWidth !== undefined ? Math.round(ariaMaxWidth) : undefined, "aria-valuenow": Math.round(sidebarWidth), tabIndex: isSidebarCollapsed ? -1 : 0, onPointerDown: onResizePointerDown, onPointerMove: onResizePointerMove, onPointerUp: endResizeDrag, onPointerCancel: endResizeDrag, onDoubleClick: resetWidth, onKeyDown: onKeyDown }))] }));
12
142
  }
@@ -1,4 +1,5 @@
1
1
  .container {
2
+ position: relative;
2
3
  flex-shrink: 1;
3
4
  height: 100%;
4
5
  overflow: auto;
@@ -14,6 +15,10 @@
14
15
  inline-size var(--transition-fast) var(--ease-standard);
15
16
  }
16
17
 
18
+ .container.resizing {
19
+ transition: none;
20
+ }
21
+
17
22
  /* Collapsed state */
18
23
  .container.collapsed {
19
24
  width: var(--component-size-lg);
@@ -37,10 +42,12 @@
37
42
  /* HEADER (product + collapse) */
38
43
  .header {
39
44
  flex: 0 0 auto;
40
- border-bottom: 1px solid var(--color-border-default);
41
- padding: 0 var(--spacing-sm);
45
+ box-sizing: border-box;
46
+ box-shadow: 0 1px 0 var(--color-border-default);
47
+ padding-inline: var(--spacing-xs);
42
48
  min-block-size: 60px;
43
49
  display: flex;
50
+ align-items: center;
44
51
  justify-content: space-between;
45
52
  }
46
53
 
@@ -57,15 +64,15 @@
57
64
  display: flex;
58
65
  align-items: center;
59
66
  max-inline-size: 100%;
67
+ block-size: 25px;
60
68
  min-width: 0;
61
69
  }
62
70
 
63
71
  /* Keep product logo visible in expanded state */
64
72
  .productLogo img,
65
73
  .productLogo svg {
66
- inline-size: 50px;
67
- max-inline-size: 100%;
68
- block-size: auto;
74
+ block-size: 100%;
75
+ inline-size: 100%;
69
76
  }
70
77
 
71
78
  /* Collapse button */
@@ -131,6 +138,61 @@
131
138
  padding: 0;
132
139
  }
133
140
 
141
+ /* RESIZE HANDLE */
142
+ .resizeHandle {
143
+ position: absolute;
144
+ top: 0;
145
+ bottom: 0;
146
+ right: 0;
147
+ width: 8px;
148
+ cursor: col-resize;
149
+ z-index: 1;
150
+ user-select: none;
151
+ touch-action: none;
152
+ outline: none;
153
+ }
154
+
155
+ .resizeHandle::after {
156
+ content: '';
157
+ position: absolute;
158
+ top: 0;
159
+ bottom: 0;
160
+ right: 0;
161
+ width: var(--border-width-thin);
162
+ background-color: var(--color-border-subtle);
163
+ opacity: 0;
164
+ transition:
165
+ opacity var(--transition-fast) var(--ease-standard),
166
+ background-color var(--transition-fast) var(--ease-standard);
167
+ }
168
+
169
+ .resizeHandle:hover::after,
170
+ .resizeHandle:active::after {
171
+ opacity: 1;
172
+ background-color: var(--color-border-strong);
173
+ }
174
+
175
+ .resizeHandle:active::after {
176
+ background-color: var(--color-brand);
177
+ }
178
+
179
+ .resizeHandle:focus-visible {
180
+ box-shadow: var(--focus-ring);
181
+ }
182
+
183
+ .container.collapsed .resizeHandle {
184
+ display: none;
185
+ }
186
+
187
+ /* CUSTOM FOOTER SLOT */
188
+ .footerSlot {
189
+ flex: 0 0 auto;
190
+ }
191
+
192
+ .container:not(.collapsed) .footerSlot {
193
+ padding-inline: var(--spacing-xs);
194
+ }
195
+
134
196
  /* FOOTER (company logo) */
135
197
  .footer {
136
198
  flex: 0 0 auto;
@@ -7,5 +7,6 @@ export interface SidebarItemContentProps {
7
7
  href?: string;
8
8
  disableActiveStyles?: boolean;
9
9
  headerStyle?: boolean;
10
+ truncateLabel?: boolean;
10
11
  }
11
- export declare function SidebarItemContent({ icon, label, suffixIcon, href, disableActiveStyles, headerStyle, }: SidebarItemContentProps): JSX.Element;
12
+ export declare function SidebarItemContent({ icon, label, suffixIcon, href, disableActiveStyles, headerStyle, truncateLabel, }: SidebarItemContentProps): JSX.Element;
@@ -1,7 +1,8 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import styles from './SidebarItemContent.module.css';
3
4
  import { useSidebar } from '../../providers/SidebarProvider';
4
- export function SidebarItemContent({ icon, label, suffixIcon, href, disableActiveStyles = false, headerStyle, }) {
5
+ export function SidebarItemContent({ icon, label, suffixIcon, href, disableActiveStyles = false, headerStyle, truncateLabel = false, }) {
5
6
  const { activeLink, isSidebarCollapsed } = useSidebar();
6
- return (_jsxs("span", { className: `${styles.container} ${!disableActiveStyles && activeLink === href ? styles.active : ''} ${isSidebarCollapsed ? styles.collapsed : ''} ${headerStyle ? styles.headerStyle : ''}`, children: [_jsxs("span", { children: [_jsx("span", { className: styles.icon, children: icon }), !isSidebarCollapsed && _jsx("span", { className: styles.label, children: label })] }), suffixIcon && !isSidebarCollapsed && _jsx("span", { className: styles.suffixIcon, children: suffixIcon })] }));
7
+ return (_jsxs("span", { className: `${styles.container} ${!disableActiveStyles && activeLink === href ? styles.active : ''} ${isSidebarCollapsed ? styles.collapsed : ''} ${headerStyle ? styles.headerStyle : ''}`, children: [_jsxs("span", { children: [_jsx("span", { className: styles.icon, children: icon }), !isSidebarCollapsed && (_jsx("span", { className: `${styles.label} ${truncateLabel ? styles.truncate : ''}`, title: truncateLabel && typeof label === 'string' ? label : undefined, children: label }))] }), suffixIcon && !isSidebarCollapsed && _jsx("span", { className: styles.suffixIcon, children: suffixIcon })] }));
7
8
  }
@@ -41,6 +41,7 @@
41
41
  display: flex;
42
42
  align-items: center;
43
43
  gap: var(--spacing-sm);
44
+ min-inline-size: 0;
44
45
  }
45
46
 
46
47
  .container:not(.active):hover {
@@ -83,3 +84,13 @@
83
84
  letter-spacing: 0.04em;
84
85
  transition: 0.15s ease-in-out;
85
86
  }
87
+
88
+ .label {
89
+ min-inline-size: 0;
90
+ }
91
+
92
+ .truncate {
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ white-space: nowrap;
96
+ }
@@ -16,11 +16,11 @@ export function SidebarItems({ activeLink }) {
16
16
  return isSidebarCollapsed ? ((_a = item.children) === null || _a === void 0 ? void 0 : _a.map((child, idx) => renderItem(child, `${key}-c${idx}`))) : (_jsxs("div", { className: styles.group, children: [_jsx("div", { className: styles.groupLabel, children: item.label }), (_b = item.children) === null || _b === void 0 ? void 0 : _b.map((child, idx) => renderItem(child, `${key}-c${idx}`))] }, key));
17
17
  }
18
18
  if (item.type === 'expandable') {
19
- const { component: Component, label, icon, children, href } = item;
20
- return (_jsx(ExpandableSidebarItem, { items: children, label: label, icon: icon, href: href, component: Component }, key));
19
+ const { component: Component, label, icon, children, href, truncateLabel } = item;
20
+ return (_jsx(ExpandableSidebarItem, { items: children, label: label, icon: icon, href: href, truncateLabel: truncateLabel, component: Component }, key));
21
21
  }
22
- const { component: Component, label, icon, href } = item;
23
- return _jsx(SidebarItem, { component: Component, label: label, icon: icon, href: href }, key);
22
+ const { component: Component, label, icon, href, truncateLabel } = item;
23
+ return (_jsx(SidebarItem, { component: Component, label: label, icon: icon, href: href, truncateLabel: truncateLabel }, key));
24
24
  };
25
25
  return filteredItems === null || filteredItems === void 0 ? void 0 : filteredItems.map((item, idx) => renderItem(item, `nav-${idx}-${item.label}`));
26
26
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './components/button/Button';
2
+ export * from './components/button-select/ButtonSelect';
2
3
  export * from './components/nav-bar/NavBar';
3
4
  export * from './components/avatar/Avatar';
4
5
  export * from './components/popover/Popover';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './components/button/Button';
2
+ export * from './components/button-select/ButtonSelect';
2
3
  export * from './components/nav-bar/NavBar';
3
4
  export * from './components/avatar/Avatar';
4
5
  export * from './components/popover/Popover';
@@ -181,11 +181,11 @@ html[data-theme='dark'] {
181
181
  --opac-bg-dark-invert: rgba(0, 0, 0, 0.15);
182
182
 
183
183
  /* Shadows */
184
- --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.2);
185
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.35);
186
- --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.32);
187
- --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
188
- --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.45);
184
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.18);
185
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.24);
186
+ --shadow-md: 0 2px 6px rgba(0, 0, 0, 0.32);
187
+ --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.4);
188
+ --shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.48);
189
189
 
190
190
  /* Data viz */
191
191
  --viz-cat-1: #2563eb;
@@ -180,11 +180,11 @@ html[data-theme='light'] {
180
180
  --opac-bg-dark-invert: rgba(255, 255, 255, 0.1);
181
181
 
182
182
  /* Shadows */
183
- --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
184
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
185
- --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.08);
186
- --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
187
- --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
183
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
184
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
185
+ --shadow-md: 0 2px 6px rgba(0, 0, 0, 0.08);
186
+ --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.1);
187
+ --shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.12);
188
188
 
189
189
  /* Data viz */
190
190
  --viz-cat-1: #2563eb;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.65",
3
+ "version": "0.0.67",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -46,7 +46,7 @@
46
46
  "build": "npm run clean && npm run build:code && npm run postbuild",
47
47
  "build:code": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
48
48
  "dev": "tsc -p tsconfig.build.json --watch",
49
- "test": "jest",
49
+ "test": "vitest run",
50
50
  "format": "prettier . --write",
51
51
  "format:fix": "prettier . --write",
52
52
  "lint": "eslint \"src/**/*.{ts,tsx}\" --max-warnings=0",
@@ -77,11 +77,9 @@
77
77
  "@storybook/addon-docs": "^10.3.5",
78
78
  "@storybook/react-vite": "^10.3.5",
79
79
  "@swc/core": "^1.15.11",
80
- "@swc/jest": "^0.2.39",
81
80
  "@tanstack/react-table": "^8.20.0",
82
81
  "@testing-library/jest-dom": "^6.9.1",
83
82
  "@testing-library/react": "^16.3.2",
84
- "@types/jest": "^30.0.0",
85
83
  "@types/react": "^19.2.14",
86
84
  "@types/react-dom": "^19.2.3",
87
85
  "@typescript-eslint/eslint-plugin": "^8.41.0",
@@ -98,8 +96,6 @@
98
96
  "eslint-plugin-react-hooks": "^7.0.1",
99
97
  "globals": "^17.3.0",
100
98
  "identity-obj-proxy": "^3.0.0",
101
- "jest": "^30.2.0",
102
- "jest-environment-jsdom": "^30.2.0",
103
99
  "jsdom": "^29.0.2",
104
100
  "prettier": "^3.2.4",
105
101
  "react": "19.2.4",