@dbcdk/react-components 0.0.66 → 0.0.68

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 (29) 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/json-viewer/JsonViewer.d.ts +11 -0
  5. package/dist/components/json-viewer/JsonViewer.js +199 -0
  6. package/dist/components/json-viewer/JsonViewer.module.css +359 -0
  7. package/dist/components/menu/Menu.d.ts +4 -0
  8. package/dist/components/menu/Menu.js +3 -0
  9. package/dist/components/menu/Menu.module.css +13 -0
  10. package/dist/components/nav-bar/NavBar.d.ts +1 -0
  11. package/dist/components/overlay/tooltip/Tooltip.module.css +0 -2
  12. package/dist/components/page-layout/PageLayout.module.css +12 -0
  13. package/dist/components/sidebar/Sidebar.d.ts +6 -1
  14. package/dist/components/sidebar/Sidebar.js +2 -3
  15. package/dist/components/sidebar/components/SidebarItem.d.ts +2 -1
  16. package/dist/components/sidebar/components/SidebarItem.js +2 -2
  17. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.d.ts +2 -1
  18. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +3 -3
  19. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.d.ts +7 -2
  20. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +143 -2
  21. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.module.css +67 -5
  22. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.d.ts +2 -1
  23. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +3 -2
  24. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +11 -0
  25. package/dist/components/sidebar/components/sidebar-items/SidebarItems.js +4 -4
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +2 -0
  28. package/package.json +3 -7
  29. /package/dist/{src/styles/styles.css → styles.css} +0 -0
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ import type { JSX } from 'react';
2
+ export interface JsonViewerProps {
3
+ value: unknown;
4
+ defaultExpandedDepth?: number;
5
+ expandAll?: boolean;
6
+ helperText?: string;
7
+ searchPlaceholder?: string;
8
+ emptySearchText?: string;
9
+ className?: string;
10
+ }
11
+ export declare function JsonViewer({ value, defaultExpandedDepth, expandAll, helperText, searchPlaceholder, emptySearchText, className, }: JsonViewerProps): JSX.Element;
@@ -0,0 +1,199 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, ChevronDown, ChevronRight, Copy, Search, X } from 'lucide-react';
4
+ import { Fragment, useEffect, useId, useMemo, useState } from 'react';
5
+ import styles from './JsonViewer.module.css';
6
+ function isJsonArray(value) {
7
+ return Array.isArray(value);
8
+ }
9
+ function isJsonObject(value) {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ }
12
+ function getValueType(value) {
13
+ if (value === null)
14
+ return 'null';
15
+ if (Array.isArray(value))
16
+ return 'array';
17
+ if (typeof value === 'object')
18
+ return 'object';
19
+ if (typeof value === 'string')
20
+ return 'string';
21
+ if (typeof value === 'number')
22
+ return 'number';
23
+ if (typeof value === 'boolean')
24
+ return 'boolean';
25
+ return 'string';
26
+ }
27
+ function formatPrimitive(value) {
28
+ if (value === null)
29
+ return 'null';
30
+ if (typeof value === 'string')
31
+ return JSON.stringify(value);
32
+ return String(value);
33
+ }
34
+ function getPrimitiveRawValue(value) {
35
+ if (value === null)
36
+ return 'null';
37
+ return String(value);
38
+ }
39
+ function getSummary(value) {
40
+ if (isJsonArray(value)) {
41
+ return `${value.length} element${value.length === 1 ? '' : 'er'}`;
42
+ }
43
+ if (!isJsonObject(value))
44
+ return '0 felter';
45
+ const count = Object.keys(value).length;
46
+ return `${count} felt${count === 1 ? '' : 'er'}`;
47
+ }
48
+ function getNodeId(path) {
49
+ return path.length === 0 ? '$' : `$.${path.join('.')}`;
50
+ }
51
+ function getPathLabel(path) {
52
+ return getNodeId(path);
53
+ }
54
+ function normalizeSearch(value) {
55
+ return value.trim().toLowerCase();
56
+ }
57
+ function valueToSearchString(value) {
58
+ return value === null ? 'null' : String(value).toLowerCase();
59
+ }
60
+ function collectSearchMatches(value, query, path = [], key) {
61
+ const matches = new Map();
62
+ const visit = (current, currentPath, currentKey) => {
63
+ const nodeId = getNodeId(currentPath);
64
+ const pathLabel = getPathLabel(currentPath).toLowerCase();
65
+ const keyLabel = (currentKey !== null && currentKey !== void 0 ? currentKey : '').toLowerCase();
66
+ let self = keyLabel.includes(query) || pathLabel.includes(query);
67
+ let descendant = false;
68
+ if (isJsonArray(current)) {
69
+ current.forEach((item, index) => {
70
+ const childMatches = visit(item, [...currentPath, String(index)], String(index));
71
+ descendant = descendant || childMatches;
72
+ });
73
+ }
74
+ else if (isJsonObject(current)) {
75
+ Object.entries(current).forEach(([childKey, childValue]) => {
76
+ const childMatches = visit(childValue, [...currentPath, childKey], childKey);
77
+ descendant = descendant || childMatches;
78
+ });
79
+ }
80
+ else {
81
+ self = self || valueToSearchString(current).includes(query);
82
+ }
83
+ const any = self || descendant;
84
+ if (any) {
85
+ matches.set(nodeId, { self, descendant });
86
+ }
87
+ return any;
88
+ };
89
+ visit(value, path, key);
90
+ return matches;
91
+ }
92
+ function getHighlightedSegments(text, query) {
93
+ if (!query)
94
+ return [{ text, matched: false }];
95
+ const lower = text.toLowerCase();
96
+ const segments = [];
97
+ let start = 0;
98
+ while (start < text.length) {
99
+ const index = lower.indexOf(query, start);
100
+ if (index === -1) {
101
+ segments.push({ text: text.slice(start), matched: false });
102
+ break;
103
+ }
104
+ if (index > start) {
105
+ segments.push({ text: text.slice(start, index), matched: false });
106
+ }
107
+ segments.push({ text: text.slice(index, index + query.length), matched: true });
108
+ start = index + query.length;
109
+ }
110
+ return segments.length > 0 ? segments : [{ text, matched: false }];
111
+ }
112
+ function HighlightText({ text, query }) {
113
+ return getHighlightedSegments(text, query).map((segment, index) => segment.matched ? (_jsx("mark", { className: styles.highlight, children: segment.text }, `${segment.text}-${index}`)) : (_jsx(Fragment, { children: segment.text }, `${segment.text}-${index}`)));
114
+ }
115
+ function useClipboardStatus() {
116
+ const [copiedId, setCopiedId] = useState(null);
117
+ useEffect(() => {
118
+ if (!copiedId)
119
+ return;
120
+ const timer = window.setTimeout(() => setCopiedId(null), 1400);
121
+ return () => window.clearTimeout(timer);
122
+ }, [copiedId]);
123
+ return { copiedId, setCopiedId };
124
+ }
125
+ function isJsonValue(value) {
126
+ if (value === null)
127
+ return true;
128
+ if (Array.isArray(value))
129
+ return value.every(isJsonValue);
130
+ if (typeof value === 'object') {
131
+ return Object.values(value).every(isJsonValue);
132
+ }
133
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
134
+ }
135
+ export function JsonViewer({ value, defaultExpandedDepth = 2, expandAll = false, helperText, searchPlaceholder = 'Søg i nøgler, stier og værdier', emptySearchText = 'Ingen matchende noder fundet.', className, }) {
136
+ const [query, setQuery] = useState('');
137
+ const [globalExpandMode, setGlobalExpandMode] = useState(expandAll ? 'expanded' : 'default');
138
+ const normalizedQuery = normalizeSearch(query);
139
+ const inputId = useId();
140
+ const { copiedId, setCopiedId } = useClipboardStatus();
141
+ const safeValue = useMemo(() => {
142
+ if (isJsonValue(value))
143
+ return value;
144
+ return {
145
+ unsupported: {
146
+ besked: 'Den angivne værdi kan ikke vises som JSON',
147
+ modtagetType: typeof value,
148
+ },
149
+ };
150
+ }, [value]);
151
+ const matches = useMemo(() => (normalizedQuery ? collectSearchMatches(safeValue, normalizedQuery) : new Map()), [normalizedQuery, safeValue]);
152
+ const hasResults = normalizedQuery ? matches.size > 0 : true;
153
+ const handleCopy = async (nodeId, rawValue) => {
154
+ try {
155
+ await navigator.clipboard.writeText(rawValue);
156
+ setCopiedId(nodeId);
157
+ }
158
+ catch {
159
+ setCopiedId(null);
160
+ }
161
+ };
162
+ return (_jsxs("section", { className: [styles.viewer, className].filter(Boolean).join(' '), children: [_jsxs("div", { className: styles.toolbar, children: [_jsxs("div", { className: styles.toolbarTop, children: [_jsxs("label", { htmlFor: inputId, className: styles.searchField, children: [_jsx(Search, { className: styles.searchIcon, "aria-hidden": "true" }), _jsx("input", { id: inputId, type: "search", value: query, onChange: event => setQuery(event.target.value), placeholder: searchPlaceholder, className: styles.searchInput }), query ? (_jsx("button", { type: "button", className: styles.clearButton, "aria-label": "Ryd s\u00F8gning", onClick: () => setQuery(''), children: _jsx(X, { "aria-hidden": "true" }) })) : null] }), _jsxs("div", { className: styles.actions, children: [_jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => setGlobalExpandMode('expanded'), disabled: Boolean(normalizedQuery), children: "Fold alle ud" }), _jsx("button", { type: "button", className: styles.toolbarButton, onClick: () => setGlobalExpandMode('collapsed'), disabled: Boolean(normalizedQuery), children: "Fold alle sammen" })] })] }), _jsxs("div", { className: styles.metaRow, children: [helperText ? _jsx("span", { className: styles.helperText, children: helperText }) : _jsx("span", {}), _jsx("span", { className: styles.statusText, children: normalizedQuery
163
+ ? `${matches.size} matchende node${matches.size === 1 ? '' : 'r'}`
164
+ : '' })] })] }), _jsx("div", { className: styles.treePane, role: "tree", "aria-label": "JSON-visning", children: hasResults ? (_jsx(JsonNode, { keyName: undefined, path: [], value: safeValue, depth: 0, defaultExpandedDepth: defaultExpandedDepth, query: normalizedQuery, matches: matches, globalExpandMode: globalExpandMode, copiedId: copiedId, onCopy: handleCopy })) : (_jsx("div", { className: styles.emptyState, children: emptySearchText })) })] }));
165
+ }
166
+ function JsonNode({ keyName, path, value, depth, defaultExpandedDepth, query, matches, globalExpandMode, copiedId, onCopy, }) {
167
+ const nodeId = getNodeId(path);
168
+ const nodeType = getValueType(value);
169
+ const match = matches.get(nodeId);
170
+ const shouldRender = query ? Boolean(match) : true;
171
+ const forceOpen = query ? Boolean(match === null || match === void 0 ? void 0 : match.descendant) : false;
172
+ const [isExpanded, setIsExpanded] = useState(() => depth < defaultExpandedDepth);
173
+ if (!shouldRender)
174
+ return null;
175
+ if (nodeType !== 'array' && nodeType !== 'object') {
176
+ const primitive = value;
177
+ const valueLabel = formatPrimitive(primitive);
178
+ const rawValue = getPrimitiveRawValue(primitive);
179
+ const copied = copiedId === nodeId;
180
+ return (_jsxs("div", { className: styles.node, style: { ['--json-indent']: depth }, children: [_jsx("div", { className: styles.row, "data-match": (match === null || match === void 0 ? void 0 : match.self) || undefined, children: _jsxs("button", { type: "button", className: styles.valueButton, onClick: () => onCopy(nodeId, rawValue), title: copied ? 'Kopieret' : 'Kopiér værdi', children: [_jsxs("span", { className: styles.rowLabel, children: [keyName !== undefined ? (_jsxs("span", { className: styles.keyChunk, children: [_jsx("span", { className: styles.key, children: _jsx(HighlightText, { text: keyName, query: query }) }), _jsx("span", { className: styles.punctuation, children: ":" })] })) : (_jsx("span", { className: styles.rootLabel, children: "$" })), _jsx("span", { className: [styles.value, styles[`value${capitalize(nodeType)}`]].join(' '), children: _jsx(HighlightText, { text: valueLabel, query: query }) })] }), _jsxs("span", { className: styles.copyState, "aria-live": "polite", children: [copied ? _jsx(Check, { "aria-hidden": "true" }) : _jsx(Copy, { "aria-hidden": "true" }), _jsx("span", { children: copied ? 'Kopieret' : 'Kopiér' })] })] }) }), query && !(match === null || match === void 0 ? void 0 : match.self) ? (_jsx("div", { className: styles.pathHint, children: _jsx(HighlightText, { text: getPathLabel(path), query: query }) })) : null] }));
181
+ }
182
+ const entries = isJsonArray(value)
183
+ ? value.map((item, index) => [String(index), item])
184
+ : isJsonObject(value)
185
+ ? Object.entries(value)
186
+ : [];
187
+ const summary = getSummary(value);
188
+ const expanded = query || forceOpen
189
+ ? true
190
+ : globalExpandMode === 'expanded'
191
+ ? true
192
+ : globalExpandMode === 'collapsed'
193
+ ? false
194
+ : isExpanded;
195
+ return (_jsxs("div", { className: styles.node, style: { ['--json-indent']: depth }, children: [_jsx("div", { className: styles.row, "data-match": (match === null || match === void 0 ? void 0 : match.self) || undefined, children: _jsxs("button", { type: "button", className: styles.toggle, onClick: () => setIsExpanded(current => !current), "aria-expanded": expanded, children: [expanded ? _jsx(ChevronDown, { "aria-hidden": "true" }) : _jsx(ChevronRight, { "aria-hidden": "true" }), _jsxs("span", { className: styles.rowLabel, children: [keyName !== undefined ? (_jsxs("span", { className: styles.keyChunk, children: [_jsx("span", { className: styles.key, children: _jsx(HighlightText, { text: keyName, query: query }) }), _jsx("span", { className: styles.punctuation, children: ":" })] })) : (_jsx("span", { className: styles.rootLabel, children: "$" })), _jsx("span", { className: styles.bracket, children: isJsonArray(value) ? '[' : '{' }), _jsx("span", { className: styles.summary, children: _jsx(HighlightText, { text: summary, query: query }) }), _jsx("span", { className: styles.bracket, children: isJsonArray(value) ? ']' : '}' })] })] }) }), query && !(match === null || match === void 0 ? void 0 : match.self) ? (_jsx("div", { className: styles.pathHint, children: _jsx(HighlightText, { text: getPathLabel(path), query: query }) })) : null, expanded ? (_jsx("div", { className: styles.children, role: depth === 0 ? 'group' : undefined, children: entries.map(([childKey, childValue]) => (_jsx(JsonNode, { keyName: isJsonArray(value) ? `[${childKey}]` : childKey, path: [...path, childKey], value: childValue, depth: depth + 1, defaultExpandedDepth: defaultExpandedDepth, query: query, matches: matches, globalExpandMode: globalExpandMode, copiedId: copiedId, onCopy: onCopy }, `${nodeId}-${childKey}`))) })) : null] }));
196
+ }
197
+ function capitalize(value) {
198
+ return value.charAt(0).toUpperCase() + value.slice(1);
199
+ }
@@ -0,0 +1,359 @@
1
+ .viewer {
2
+ --json-pane-bg:
3
+ linear-gradient(
4
+ 180deg,
5
+ color-mix(in oklab, var(--color-bg-inverse) 96%, white 4%),
6
+ color-mix(in oklab, var(--color-bg-inverse) 92%, black 8%)
7
+ );
8
+ --json-pane-border: color-mix(in oklab, var(--color-fg-inverse) 14%, transparent);
9
+ --json-pane-border-strong: color-mix(in oklab, var(--color-fg-inverse) 24%, transparent);
10
+ --json-pane-text: var(--color-fg-inverse);
11
+ --json-pane-muted: color-mix(in oklab, var(--color-fg-inverse) 64%, transparent);
12
+ --json-pane-subtle: color-mix(in oklab, var(--color-fg-inverse) 44%, transparent);
13
+ --json-pane-hover: color-mix(in oklab, var(--color-fg-inverse) 8%, transparent);
14
+ --json-pane-selected: color-mix(in oklab, var(--color-brand) 24%, transparent);
15
+ --json-pane-highlight: color-mix(in oklab, var(--color-brand) 68%, var(--dbc-blue-300) 32%);
16
+ --json-pane-key: color-mix(in oklab, var(--color-fg-inverse) 88%, var(--dbc-blue-300) 12%);
17
+ --json-pane-string: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
18
+ --json-pane-number: color-mix(in oklab, var(--dbc-amber-400) 76%, white 24%);
19
+ --json-pane-boolean: color-mix(in oklab, var(--dbc-pink-500) 74%, white 26%);
20
+ --json-pane-null: color-mix(in oklab, var(--color-fg-inverse) 54%, transparent);
21
+
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--spacing-sm);
25
+ background: var(--json-pane-bg);
26
+ color: var(--json-pane-text);
27
+ border: var(--border-width-thin) solid var(--json-pane-border);
28
+ border-radius: var(--border-radius-xl);
29
+ box-shadow: var(--shadow-lg);
30
+ padding: var(--spacing-sm);
31
+ font-family: var(--font-family-mono);
32
+ }
33
+
34
+ .toolbar {
35
+ display: grid;
36
+ gap: var(--spacing-xs);
37
+ }
38
+
39
+ .toolbarTop {
40
+ display: grid;
41
+ grid-template-columns: minmax(0, 1fr) auto;
42
+ gap: var(--spacing-sm);
43
+ align-items: center;
44
+ }
45
+
46
+ .searchField {
47
+ display: grid;
48
+ grid-template-columns: auto minmax(0, 1fr) auto;
49
+ align-items: center;
50
+ gap: var(--spacing-xs);
51
+ min-height: var(--component-size-md);
52
+ padding-inline: var(--spacing-sm);
53
+ border: var(--border-width-thin) solid var(--json-pane-border);
54
+ border-radius: var(--border-radius-lg);
55
+ background: color-mix(in oklab, var(--color-bg-inverse) 85%, var(--color-bg-surface) 15%);
56
+ }
57
+
58
+ .searchField:focus-within {
59
+ border-color: var(--json-pane-border-strong);
60
+ box-shadow: var(--focus-ring);
61
+ }
62
+
63
+ .searchIcon,
64
+ .clearButton svg {
65
+ inline-size: var(--icon-size-sm);
66
+ block-size: var(--icon-size-sm);
67
+ color: var(--json-pane-subtle);
68
+ }
69
+
70
+ .searchInput {
71
+ inline-size: 100%;
72
+ min-inline-size: 0;
73
+ min-block-size: var(--component-size-md);
74
+ background: transparent;
75
+ border: 0;
76
+ color: var(--json-pane-text);
77
+ font: inherit;
78
+ outline: none;
79
+ }
80
+
81
+ .searchInput::placeholder {
82
+ color: var(--json-pane-subtle);
83
+ }
84
+
85
+ .clearButton {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ inline-size: var(--component-size-xs);
90
+ block-size: var(--component-size-xs);
91
+ border: 0;
92
+ border-radius: var(--border-radius-round);
93
+ background: transparent;
94
+ cursor: pointer;
95
+ }
96
+
97
+ .clearButton:hover {
98
+ background: var(--json-pane-hover);
99
+ }
100
+
101
+ .metaRow {
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ gap: var(--spacing-md);
106
+ font-size: var(--font-size-xs);
107
+ color: var(--json-pane-muted);
108
+ }
109
+
110
+ .actions {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: var(--spacing-xs);
114
+ }
115
+
116
+ .toolbarButton {
117
+ min-block-size: var(--component-size-sm);
118
+ padding-inline: var(--spacing-sm);
119
+ border: var(--border-width-thin) solid var(--json-pane-border);
120
+ border-radius: var(--border-radius-md);
121
+ background: color-mix(in oklab, var(--color-bg-inverse) 82%, var(--color-bg-surface) 18%);
122
+ color: var(--json-pane-text);
123
+ font: inherit;
124
+ cursor: pointer;
125
+ }
126
+
127
+ .toolbarButton:hover:not(:disabled) {
128
+ background: var(--json-pane-hover);
129
+ }
130
+
131
+ .toolbarButton:focus-visible {
132
+ outline: none;
133
+ box-shadow: var(--focus-ring);
134
+ }
135
+
136
+ .toolbarButton:disabled {
137
+ opacity: 0.45;
138
+ cursor: default;
139
+ }
140
+
141
+ .helperText,
142
+ .statusText {
143
+ min-inline-size: 0;
144
+ }
145
+
146
+ .treePane {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: var(--spacing-2xs);
150
+ border: var(--border-width-thin) solid var(--json-pane-border);
151
+ border-radius: var(--border-radius-lg);
152
+ background:
153
+ linear-gradient(
154
+ 180deg,
155
+ color-mix(in oklab, var(--color-bg-inverse) 89%, white 11%),
156
+ color-mix(in oklab, var(--color-bg-inverse) 94%, black 6%)
157
+ );
158
+ padding: var(--spacing-sm);
159
+ overflow: auto;
160
+ }
161
+
162
+ .node {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 1px;
166
+ }
167
+
168
+ .row,
169
+ .pathHint {
170
+ padding-left: calc(var(--json-indent) * var(--spacing-md));
171
+ }
172
+
173
+ .row {
174
+ display: flex;
175
+ align-items: flex-start;
176
+ min-inline-size: 0;
177
+ border-radius: var(--border-radius-md);
178
+ }
179
+
180
+ .row[data-match='true'] {
181
+ background: linear-gradient(90deg, var(--json-pane-selected), transparent 70%);
182
+ }
183
+
184
+ .toggle,
185
+ .valueButton {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: var(--spacing-xs);
189
+ min-inline-size: 0;
190
+ inline-size: 100%;
191
+ padding-block: 6px;
192
+ padding-inline: var(--spacing-xs);
193
+ background: transparent;
194
+ border: 0;
195
+ border-radius: var(--border-radius-md);
196
+ color: inherit;
197
+ text-align: left;
198
+ cursor: pointer;
199
+ font: inherit;
200
+ }
201
+
202
+ .toggle {
203
+ justify-content: flex-start;
204
+ }
205
+
206
+ .valueButton {
207
+ justify-content: flex-start;
208
+ }
209
+
210
+ .toggle:hover,
211
+ .valueButton:hover {
212
+ background: var(--json-pane-hover);
213
+ }
214
+
215
+ .toggle:focus-visible,
216
+ .valueButton:focus-visible,
217
+ .clearButton:focus-visible {
218
+ outline: none;
219
+ box-shadow: var(--focus-ring);
220
+ }
221
+
222
+ .toggle svg,
223
+ .copyState svg {
224
+ inline-size: var(--icon-size-sm);
225
+ block-size: var(--icon-size-sm);
226
+ flex: 0 0 auto;
227
+ }
228
+
229
+ .rowLabel {
230
+ display: inline-flex;
231
+ align-items: baseline;
232
+ flex-wrap: nowrap;
233
+ gap: 6px;
234
+ min-inline-size: 0;
235
+ flex: 1 1 auto;
236
+ }
237
+
238
+ .keyChunk {
239
+ display: inline-flex;
240
+ align-items: baseline;
241
+ gap: 4px;
242
+ flex: 0 0 auto;
243
+ }
244
+
245
+ .rootLabel,
246
+ .key {
247
+ color: var(--json-pane-key);
248
+ font-weight: var(--font-weight-medium);
249
+ }
250
+
251
+ .punctuation,
252
+ .bracket {
253
+ color: var(--json-pane-subtle);
254
+ }
255
+
256
+ .summary,
257
+ .pathHint {
258
+ color: var(--json-pane-muted);
259
+ }
260
+
261
+ .children {
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 1px;
265
+ position: relative;
266
+ }
267
+
268
+ .children::before {
269
+ content: '';
270
+ position: absolute;
271
+ left: calc(var(--spacing-md) - 2px);
272
+ top: 0;
273
+ bottom: 0;
274
+ width: 1px;
275
+ background: color-mix(in oklab, var(--color-fg-inverse) 10%, transparent);
276
+ pointer-events: none;
277
+ }
278
+
279
+ .value {
280
+ min-inline-size: 0;
281
+ flex: 0 1 auto;
282
+ word-break: break-word;
283
+ }
284
+
285
+ .valueString {
286
+ color: var(--json-pane-string);
287
+ }
288
+
289
+ .valueNumber {
290
+ color: var(--json-pane-number);
291
+ }
292
+
293
+ .valueBoolean {
294
+ color: var(--json-pane-boolean);
295
+ }
296
+
297
+ .valueNull {
298
+ color: var(--json-pane-null);
299
+ }
300
+
301
+ .copyState {
302
+ display: inline-flex;
303
+ align-items: center;
304
+ gap: var(--spacing-xxs);
305
+ margin-left: auto;
306
+ color: var(--json-pane-subtle);
307
+ font-size: var(--font-size-xs);
308
+ white-space: nowrap;
309
+ opacity: 0;
310
+ transform: translateX(4px);
311
+ transition:
312
+ opacity var(--transition-fast) var(--ease-standard),
313
+ transform var(--transition-fast) var(--ease-standard);
314
+ }
315
+
316
+ .valueButton:hover .copyState,
317
+ .valueButton:focus-visible .copyState,
318
+ .valueButton:focus-within .copyState {
319
+ opacity: 1;
320
+ transform: translateX(0);
321
+ }
322
+
323
+ .valueButton[title='Kopieret'] .copyState {
324
+ opacity: 1;
325
+ transform: translateX(0);
326
+ color: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
327
+ }
328
+
329
+ .highlight {
330
+ color: var(--color-fg-inverse);
331
+ background: color-mix(in oklab, var(--json-pane-highlight) 78%, transparent);
332
+ border-radius: var(--border-radius-sm);
333
+ padding-inline: 0.1em;
334
+ }
335
+
336
+ .emptyState {
337
+ padding: var(--spacing-md);
338
+ color: var(--json-pane-muted);
339
+ text-align: center;
340
+ }
341
+
342
+ @media (max-width: 640px) {
343
+ .toolbarTop {
344
+ grid-template-columns: 1fr;
345
+ }
346
+
347
+ .actions {
348
+ flex-wrap: wrap;
349
+ }
350
+
351
+ .metaRow {
352
+ flex-direction: column;
353
+ align-items: flex-start;
354
+ }
355
+
356
+ .copyState span {
357
+ display: none;
358
+ }
359
+ }
@@ -11,6 +11,9 @@ export interface MenuProps extends React.HTMLAttributes<HTMLUListElement> {
11
11
  gap?: boolean;
12
12
  }
13
13
  export type MenuSeparatorProps = React.LiHTMLAttributes<HTMLLIElement>;
14
+ export interface MenuHeaderProps extends React.LiHTMLAttributes<HTMLLIElement> {
15
+ children: React.ReactNode;
16
+ }
14
17
  export interface MenuItemProps extends React.LiHTMLAttributes<HTMLLIElement> {
15
18
  children: React.ReactNode;
16
19
  active?: boolean;
@@ -46,4 +49,5 @@ export declare const Menu: React.ForwardRefExoticComponent<MenuProps & React.Ref
46
49
  CheckItem: React.ForwardRefExoticComponent<MenuCheckItemProps & React.RefAttributes<HTMLLIElement>>;
47
50
  RadioItem: React.ForwardRefExoticComponent<MenuRadioItemProps & React.RefAttributes<HTMLLIElement>>;
48
51
  Separator: React.ForwardRefExoticComponent<MenuSeparatorProps & React.RefAttributes<HTMLLIElement>>;
52
+ Header: React.ForwardRefExoticComponent<MenuHeaderProps & React.RefAttributes<HTMLLIElement>>;
49
53
  };
@@ -137,9 +137,12 @@ const MenuRadioItem = React.forwardRef(({ name, value, checked, disabled, label,
137
137
  MenuRadioItem.displayName = 'Menu.RadioItem';
138
138
  const MenuSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx("li", { ref: ref, role: "separator", className: [styles.separator, className].filter(Boolean).join(' '), ...props })));
139
139
  MenuSeparator.displayName = 'Menu.Separator';
140
+ const MenuHeader = React.forwardRef(({ className, children, ...props }, ref) => (_jsx("li", { ref: ref, role: "presentation", "aria-hidden": "true", className: [styles.header, className].filter(Boolean).join(' '), ...props, children: children })));
141
+ MenuHeader.displayName = 'Menu.Header';
140
142
  export const Menu = Object.assign(MenuBase, {
141
143
  Item: MenuItem,
142
144
  CheckItem: MenuCheckItem,
143
145
  RadioItem: MenuRadioItem,
144
146
  Separator: MenuSeparator,
147
+ Header: MenuHeader,
145
148
  });
@@ -174,6 +174,19 @@
174
174
  border-radius: 999px;
175
175
  }
176
176
 
177
+ .header {
178
+ padding-block: var(--spacing-xs);
179
+ padding-inline: var(--spacing-md);
180
+ color: var(--color-fg-muted);
181
+ font-size: var(--font-size-xs);
182
+ font-weight: 500;
183
+ line-height: var(--line-height-normal);
184
+ text-transform: uppercase;
185
+ letter-spacing: 0.04em;
186
+ cursor: default;
187
+ user-select: none;
188
+ }
189
+
177
190
  /* Gap between items */
178
191
  .gap {
179
192
  gap: var(--spacing-xs);
@@ -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
@@ -43,6 +43,18 @@
43
43
  -webkit-overflow-scrolling: touch;
44
44
  }
45
45
 
46
+ /* In document-scrolling vertical layouts, keep the sidebar pinned to the viewport
47
+ and let it manage its own scroll independently of body scrolling. */
48
+ .documentScrolling.vertical .sidebar {
49
+ position: sticky;
50
+ top: 0;
51
+ align-self: start;
52
+ block-size: 100vh;
53
+ block-size: 100dvh;
54
+ overflow: auto;
55
+ -webkit-overflow-scrolling: touch;
56
+ }
57
+
46
58
  /* In horizontal orientation, sidebar becomes a top block if used */
47
59
  .horizontal .sidebar {
48
60
  grid-column: 1 / -1;
@@ -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,153 @@
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
+ // Always start from defaultWidth so server and client render identical HTML.
45
+ // localStorage is read in a useEffect after hydration to avoid SSR mismatch.
46
+ const [sidebarWidth, setSidebarWidth] = useState(defaultWidth);
47
+ const [manualWidth, setManualWidth] = useState(null);
48
+ const [isResizing, setIsResizing] = useState(false);
49
+ const [ariaMaxWidth, setAriaMaxWidth] = useState(undefined);
50
+ const containerRef = useRef(null);
51
+ const draggingRef = useRef(false);
52
+ const pointerIdRef = useRef(null);
53
+ const startXRef = useRef(0);
54
+ const startWidthRef = useRef(0);
55
+ const maxWidthRef = useRef(Infinity);
56
+ // Use the viewport width as the ceiling — avoids the circular-dependency
57
+ // that occurs when the sidebar lives in an `auto`-width grid column whose
58
+ // size is determined by the sidebar itself (e.g. PageLayout vertical).
59
+ const getMaxWidth = useCallback(() => {
60
+ const viewportWidth = typeof window !== 'undefined'
61
+ ? window.innerWidth || document.documentElement.clientWidth
62
+ : Infinity;
63
+ return Math.max(minWidth, viewportWidth - minWidth);
64
+ }, [minWidth]);
65
+ // Read stored width after hydration — never during render to avoid SSR mismatch.
66
+ useEffect(() => {
67
+ if (!storageKey)
68
+ return;
69
+ const stored = readStoredWidth(storageKey);
70
+ if (stored === null)
71
+ return;
72
+ setManualWidth(stored);
73
+ setSidebarWidth(stored);
74
+ }, [storageKey]);
75
+ useEffect(() => {
76
+ if (!storageKey)
77
+ return;
78
+ if (manualWidth === null) {
79
+ removeStoredWidth(storageKey);
80
+ return;
81
+ }
82
+ writeStoredWidth(storageKey, manualWidth);
83
+ }, [manualWidth, storageKey]);
84
+ useEffect(() => {
85
+ setAriaMaxWidth(getMaxWidth());
86
+ }, [getMaxWidth]);
87
+ useEffect(() => {
88
+ return () => {
89
+ document.body.style.cursor = '';
90
+ document.body.style.userSelect = '';
91
+ };
92
+ }, []);
93
+ const updateWidth = useCallback((nextWidth) => {
94
+ setManualWidth(nextWidth);
95
+ setSidebarWidth(nextWidth);
96
+ }, []);
97
+ const onResizePointerDown = useCallback((e) => {
98
+ const maxWidth = getMaxWidth();
99
+ if (maxWidth === null)
100
+ return;
101
+ e.currentTarget.setPointerCapture(e.pointerId);
102
+ maxWidthRef.current = maxWidth;
103
+ draggingRef.current = true;
104
+ pointerIdRef.current = e.pointerId;
105
+ startXRef.current = e.clientX;
106
+ startWidthRef.current = sidebarWidth;
107
+ setIsResizing(true);
108
+ document.body.style.userSelect = 'none';
109
+ document.body.style.cursor = 'col-resize';
110
+ }, [getMaxWidth, sidebarWidth]);
111
+ const onResizePointerMove = useCallback((e) => {
112
+ if (!draggingRef.current)
113
+ return;
114
+ if (pointerIdRef.current !== null && e.pointerId !== pointerIdRef.current)
115
+ return;
116
+ const next = startWidthRef.current + (e.clientX - startXRef.current);
117
+ updateWidth(clamp(next, minWidth, maxWidthRef.current));
118
+ }, [minWidth, updateWidth]);
119
+ const endResizeDrag = useCallback(() => {
120
+ if (!draggingRef.current)
121
+ return;
122
+ draggingRef.current = false;
123
+ pointerIdRef.current = null;
124
+ setIsResizing(false);
125
+ document.body.style.cursor = '';
126
+ document.body.style.userSelect = '';
127
+ }, []);
128
+ const resetWidth = useCallback(() => {
129
+ setManualWidth(null);
130
+ setSidebarWidth(clamp(defaultWidth, minWidth, getMaxWidth()));
131
+ }, [defaultWidth, getMaxWidth, minWidth]);
132
+ const onKeyDown = useCallback((e) => {
133
+ const maxWidth = getMaxWidth();
134
+ const step = e.shiftKey ? 32 : 8;
135
+ let next = null;
136
+ if (e.key === 'ArrowLeft')
137
+ next = sidebarWidth - step;
138
+ if (e.key === 'ArrowRight')
139
+ next = sidebarWidth + step;
140
+ if (e.key === 'Home')
141
+ next = minWidth;
142
+ if (e.key === 'End')
143
+ next = maxWidth;
144
+ if (next === null)
145
+ return;
146
+ e.preventDefault();
147
+ updateWidth(clamp(next, minWidth, maxWidth));
148
+ }, [getMaxWidth, minWidth, sidebarWidth, updateWidth]);
149
+ const containerStyle = useMemo(() => ({
150
+ '--sidebar-width': `${sidebarWidth}px`,
151
+ }), [sidebarWidth]);
152
+ 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
153
  }
@@ -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';
@@ -45,6 +46,7 @@ export * from './components/split-pane/SplitPane';
45
46
  export * from './components/pagination/Pagination';
46
47
  export * from './components/meta-bar/MetaBar';
47
48
  export * from './components/code-block/CodeBlock';
49
+ export * from './components/json-viewer/JsonViewer';
48
50
  export * from './hooks/useTimeDuration';
49
51
  export * from './hooks/useSorting';
50
52
  export * from './hooks/usePagination';
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';
@@ -45,6 +46,7 @@ export * from './components/split-pane/SplitPane';
45
46
  export * from './components/pagination/Pagination';
46
47
  export * from './components/meta-bar/MetaBar';
47
48
  export * from './components/code-block/CodeBlock';
49
+ export * from './components/json-viewer/JsonViewer';
48
50
  export * from './hooks/useTimeDuration';
49
51
  export * from './hooks/useSorting';
50
52
  export * from './hooks/usePagination';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.66",
3
+ "version": "0.0.68",
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",
@@ -57,7 +57,7 @@
57
57
  "version-packages": "changeset version",
58
58
  "typecheck": "tsc -p tsconfig.json --noEmit",
59
59
  "pack:local": "rimraf ./*.tgz && npm run build && npm pack",
60
- "postbuild": "cpy \"src/**/*.css\" dist --parents && cpy \"src/styles/styles.css\" dist && cpy \"src/styles/fonts/**/*\" dist/styles/fonts"
60
+ "postbuild": "node scripts/postbuild.mjs"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@tanstack/react-table": "^8.21.3",
@@ -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",
File without changes