@dbcdk/react-components 0.0.79 → 0.0.81

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 (28) hide show
  1. package/dist/components/forms/input/Input.module.css +30 -58
  2. package/dist/components/forms/select/Select.js +2 -1
  3. package/dist/components/forms/select/Select.module.css +19 -0
  4. package/dist/components/forms/text-area/Textarea.js +1 -1
  5. package/dist/components/forms/text-area/Textarea.module.css +22 -1
  6. package/dist/components/json-viewer/JsonViewer.js +2 -21
  7. package/dist/components/json-viewer/JsonViewer.module.css +2 -7
  8. package/dist/components/overlay/tooltip/TooltipProvider.js +15 -4
  9. package/dist/components/overlay/tooltip/useTooltipTrigger.d.ts +3 -1
  10. package/dist/components/overlay/tooltip/useTooltipTrigger.js +8 -1
  11. package/dist/components/search-box/SearchBox.js +9 -2
  12. package/dist/components/sidebar/Sidebar.d.ts +2 -1
  13. package/dist/components/sidebar/Sidebar.js +2 -2
  14. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.d.ts +2 -1
  15. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +2 -2
  16. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.js +9 -2
  17. package/dist/components/sidebar/components/sidebar-item-content/SidebarItemContent.module.css +3 -2
  18. package/dist/components/sidebar/components/sidenav-filteirng/SidenavFiltering.d.ts +1 -0
  19. package/dist/components/sidebar/components/sidenav-filteirng/SidenavFiltering.js +10 -5
  20. package/dist/components/sidebar/providers/SidebarProvider.d.ts +3 -1
  21. package/dist/components/sidebar/providers/SidebarProvider.js +26 -46
  22. package/dist/styles/styles.css +6 -1
  23. package/dist/styles/themes/dbc/dark.css +2 -0
  24. package/dist/styles/themes/dbc/light.css +2 -0
  25. package/dist/styles.css +6 -1
  26. package/dist/utils/text/get-highlighted-segments.d.ts +5 -0
  27. package/dist/utils/text/get-highlighted-segments.js +46 -0
  28. package/package.json +1 -1
@@ -191,103 +191,70 @@
191
191
 
192
192
  .modified {
193
193
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, var(--color-bg-surface));
194
- border-color: color-mix(
195
- in srgb,
196
- var(--color-status-warning-border) 45%,
197
- var(--color-border-default)
198
- );
199
- box-shadow: inset 4px 0 0 var(--color-status-warning-border);
194
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 45%, var(--color-border-default));
195
+ border-left-color: var(--color-status-warning-border);
196
+ border-left-width: 4px;
200
197
  }
201
198
 
202
199
  /* Hover should stay warm, not switch back to the normal border */
203
200
  .modified:hover:not([aria-disabled='true']) {
204
201
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
205
- border-color: color-mix(
206
- in srgb,
207
- var(--color-status-warning-border) 60%,
208
- var(--color-border-default)
209
- );
202
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 60%, var(--color-border-default));
203
+ border-left-color: var(--color-status-warning-border);
210
204
  }
211
205
 
212
206
  /* Focus should also stay warm and readable */
213
207
  .modified:focus-within:not([aria-disabled='true']) {
214
208
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
215
- border-color: color-mix(
216
- in srgb,
217
- var(--color-status-warning-border) 75%,
218
- var(--color-border-default)
219
- );
220
- box-shadow:
221
- inset 4px 0 0 var(--color-status-warning-border),
222
- inset 0 0 0 1px
223
- color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
209
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
210
+ border-left-color: var(--color-status-warning-border);
211
+ box-shadow: inset 0 0 0 1px
212
+ color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
224
213
  }
225
214
 
226
215
  /* Variant-specific tweaks when modified */
227
216
  .surface.modified {
228
- box-shadow:
229
- inset 4px 0 0 var(--color-status-warning-border),
230
- 0 1px 2px rgba(0, 0, 0, 0.03);
217
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
231
218
  }
232
219
 
233
220
  .surface.modified:hover:not([aria-disabled='true']) {
234
- box-shadow:
235
- inset 4px 0 0 var(--color-status-warning-border),
236
- 0 1px 3px rgba(0, 0, 0, 0.05);
221
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
237
222
  }
238
223
 
239
224
  .surface.modified:focus-within:not([aria-disabled='true']) {
240
- box-shadow:
241
- inset 4px 0 0 var(--color-status-warning-border),
242
- inset 0 0 0 1px
243
- color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
225
+ box-shadow: inset 0 0 0 1px
226
+ color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
244
227
  }
245
228
 
246
229
  .subtle.modified {
247
230
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 30%, var(--color-bg-toolbar));
248
231
  border-color: transparent;
249
- box-shadow:
250
- inset 4px 0 0 var(--color-status-warning-border),
251
- inset 0 0 0 1px transparent;
232
+ border-left-color: var(--color-status-warning-border);
233
+ border-left-width: 4px;
234
+ box-shadow: none;
252
235
  }
253
236
 
254
237
  .subtle.modified:hover:not([aria-disabled='true']) {
255
- background-color: color-mix(
256
- in srgb,
257
- var(--color-status-warning-bg) 36%,
258
- var(--color-bg-toolbar-hover)
259
- );
238
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 36%, var(--color-bg-toolbar-hover));
260
239
  }
261
240
 
262
241
  .subtle.modified:focus-within:not([aria-disabled='true']) {
263
- border-color: color-mix(
264
- in srgb,
265
- var(--color-status-warning-border) 75%,
266
- var(--color-border-default)
267
- );
268
- box-shadow:
269
- inset 4px 0 0 var(--color-status-warning-border),
270
- inset 0 0 0 1px
271
- color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
242
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
243
+ border-left-color: var(--color-status-warning-border);
244
+ box-shadow: inset 0 0 0 1px
245
+ color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
272
246
  }
273
247
 
274
248
  .standalone.modified {
275
- box-shadow:
276
- inset 4px 0 0 var(--color-status-warning-border),
277
- var(--shadow-xs),
278
- var(--shadow-md);
249
+ box-shadow: var(--shadow-xs), var(--shadow-md);
279
250
  }
280
251
 
281
252
  .standalone.modified:hover:not([aria-disabled='true']) {
282
- box-shadow:
283
- inset 4px 0 0 var(--color-status-warning-border),
284
- var(--shadow-sm),
285
- var(--shadow-md);
253
+ box-shadow: var(--shadow-sm), var(--shadow-md);
286
254
  }
287
255
 
288
256
  .standalone.modified:focus-within:not([aria-disabled='true']) {
289
257
  box-shadow:
290
- inset 4px 0 0 var(--color-status-warning-border),
291
258
  var(--shadow-xs),
292
259
  var(--shadow-md),
293
260
  inset 0 0 0 1px
@@ -298,20 +265,25 @@
298
265
  .embedded.modified {
299
266
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 18%, transparent);
300
267
  border-color: transparent;
301
- box-shadow: inset 3px 0 0 var(--color-status-warning-border);
268
+ border-left-color: var(--color-status-warning-border);
269
+ border-left-width: 3px;
270
+ box-shadow: none;
302
271
  }
303
272
 
304
273
  .embedded.modified:hover:not([aria-disabled='true']),
305
274
  .embedded.modified:focus-within:not([aria-disabled='true']) {
306
275
  background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, transparent);
307
276
  border-color: transparent;
308
- box-shadow: inset 3px 0 0 var(--color-status-warning-border);
277
+ border-left-color: var(--color-status-warning-border);
278
+ box-shadow: none;
309
279
  }
310
280
 
311
281
  /* Disabled modified state */
312
282
  .modified[aria-disabled='true'] {
313
283
  background-color: var(--color-disabled-bg);
314
284
  border-color: var(--color-disabled-border);
285
+ border-left-color: var(--color-disabled-border);
286
+ border-left-width: var(--border-width-thin);
315
287
  box-shadow: none;
316
288
  }
317
289
 
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check } from 'lucide-react';
4
4
  import { useEffect, useId, useMemo, useRef, useState } from 'react';
5
5
  import { useTooltipTrigger } from '../../../components/overlay/tooltip/useTooltipTrigger';
6
+ import styles from './Select.module.css';
6
7
  import { Button } from '../../button/Button';
7
8
  import { ClearButton } from '../../clear-button/ClearButton';
8
9
  import { Menu } from '../../menu/Menu';
@@ -168,7 +169,7 @@ export function Select({ label, error, helpText, orientation = 'vertical', label
168
169
  returnFocus: true, trigger: (toggle, icon, isOpen) => (_jsx(Button, { disabled: disabled, ...(tooltipEnabled ? triggerProps : {}), id: controlId, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'select-button', onKeyDown: handleKeyDown, fullWidth: fullWidth, variant: variant, onClick: e => {
169
170
  resetActiveToSelected();
170
171
  toggle(e);
171
- }, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selected && _jsx(ClearButton, { onClick: onClear }), _jsx("span", { style: { color: 'var(--color-fg-subtle)', display: 'inline-flex' }, children: icon })] })] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
172
+ }, size: size, type: "button", "data-forminput": true, "aria-haspopup": "listbox", "aria-expanded": !!isOpen, "aria-controls": listboxId, "aria-invalid": Boolean(error) || undefined, "aria-describedby": describedBy, className: modified ? styles.triggerModified : undefined, children: _jsxs("span", { className: "dbc-flex dbc-justify-between dbc-items-center dbc-gap-xxs", style: { width: '100%' }, children: [_jsx("span", { children: selected ? selected.label : _jsx("span", { className: "dbc-muted-text", children: placeholder }) }), _jsxs("span", { className: "dbc-flex dbc-items-center dbc-gap-xxs", children: [onClear && selected && _jsx(ClearButton, { onClick: onClear }), _jsx("span", { style: { color: 'var(--color-fg-subtle)', display: 'inline-flex' }, children: icon })] })] }) })), children: _jsx(Menu, { onKeyDown: handleKeyDown, role: "listbox", children: options.map((opt, index) => {
172
173
  const isSelected = typeof opt.value === 'object' && typeof selectedValue === 'object' && datakey
173
174
  ? (selectedValue === null || selectedValue === void 0 ? void 0 : selectedValue[datakey]) === opt.value[datakey]
174
175
  : opt.value === selectedValue;
@@ -0,0 +1,19 @@
1
+ .triggerModified {
2
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, var(--color-bg-surface));
3
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 45%, var(--color-border-default));
4
+ border-left-color: var(--color-status-warning-border);
5
+ border-left-width: 4px;
6
+ }
7
+
8
+ .triggerModified:hover:not([disabled]) {
9
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
10
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 60%, var(--color-border-default));
11
+ border-left-color: var(--color-status-warning-border);
12
+ }
13
+
14
+ .triggerModified:focus-visible {
15
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
16
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
17
+ border-left-color: var(--color-status-warning-border);
18
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
19
+ }
@@ -39,6 +39,6 @@ className, ...rest }) {
39
39
  placement: tooltipPlacement,
40
40
  offset: 8,
41
41
  });
42
- return (_jsx(InputContainer, { modified: modified, label: label, htmlFor: textareaId, error: error, helpText: helpText, helpTextAddition: showCount ? `${value === null || value === void 0 ? void 0 : value.length} tegn i denne boks` : undefined, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: _jsx("div", { className: styles.container, children: _jsx("div", { ...(tooltipEnabled ? triggerProps : {}), children: inputField }) }) }));
42
+ return (_jsx(InputContainer, { modified: modified, label: label, htmlFor: textareaId, error: error, helpText: helpText, helpTextAddition: showCount ? `${value === null || value === void 0 ? void 0 : value.length} tegn i denne boks` : undefined, orientation: orientation, labelWidth: labelWidth, fullWidth: fullWidth, required: required, children: _jsx("div", { className: [styles.container, modified ? styles.modified : ''].filter(Boolean).join(' '), children: _jsx("div", { ...(tooltipEnabled ? triggerProps : {}), children: inputField }) }) }));
43
43
  };
44
44
  Textarea.displayName = 'Textarea';
@@ -1,11 +1,32 @@
1
1
  .container {
2
2
  flex-grow: 1;
3
3
  }
4
+
5
+ .container.modified textarea {
6
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 22%, var(--color-bg-surface));
7
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 45%, var(--color-border-default));
8
+ border-left-color: var(--color-status-warning-border);
9
+ border-left-width: 4px;
10
+ }
11
+
12
+ .container.modified textarea:hover {
13
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
14
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 60%, var(--color-border-default));
15
+ border-left-color: var(--color-status-warning-border);
16
+ }
17
+
18
+ .container.modified textarea:focus-visible {
19
+ background-color: color-mix(in srgb, var(--color-status-warning-bg) 28%, var(--color-bg-surface));
20
+ border-color: color-mix(in srgb, var(--color-status-warning-border) 75%, var(--color-border-default));
21
+ border-left-color: var(--color-status-warning-border);
22
+ box-shadow: inset 0 0 0 1px
23
+ color-mix(in srgb, var(--color-status-warning-border) 55%, var(--color-border-default));
24
+ }
4
25
  .container textarea {
5
26
  width: 100%;
6
27
  padding: var(--spacing-xs);
7
28
  border: var(--border-width-thin) solid var(--color-border-default);
8
- border-radius: var(--border-radius-sm);
29
+ border-radius: var(--border-radius-default);
9
30
  font-family: var(--font-family);
10
31
  font-size: var(--font-size-sm);
11
32
  line-height: var(--line-height-normal);
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Check, ChevronDown, ChevronRight, Copy, Search, X } from 'lucide-react';
4
4
  import { Fragment, useEffect, useId, useMemo, useState } from 'react';
5
+ import { getHighlightedSegments } from '../../utils/text/get-highlighted-segments';
5
6
  import styles from './JsonViewer.module.css';
6
7
  function isJsonArray(value) {
7
8
  return Array.isArray(value);
@@ -89,28 +90,8 @@ function collectSearchMatches(value, query, path = [], key) {
89
90
  visit(value, path, key);
90
91
  return matches;
91
92
  }
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
93
  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}`)));
94
+ return getHighlightedSegments(text, query).map((segment, index) => segment.matched ? (_jsx("mark", { className: "dbc-highlight", children: segment.text }, `${segment.text}-${index}`)) : (_jsx(Fragment, { children: segment.text }, `${segment.text}-${index}`)));
114
95
  }
115
96
  function useClipboardStatus() {
116
97
  const [copiedId, setCopiedId] = useState(null);
@@ -8,6 +8,8 @@
8
8
  --json-pane-hover: color-mix(in oklab, var(--color-fg-inverse) 8%, transparent);
9
9
  --json-pane-selected: color-mix(in oklab, var(--color-brand) 24%, transparent);
10
10
  --json-pane-highlight: color-mix(in oklab, var(--color-brand) 68%, var(--dbc-blue-300) 32%);
11
+ --color-highlight-bg: color-mix(in oklab, var(--json-pane-highlight) 58%, transparent);
12
+ --color-highlight-fg: var(--color-fg-inverse);
11
13
  --json-pane-key: color-mix(in oklab, var(--color-fg-inverse) 88%, var(--dbc-blue-300) 12%);
12
14
  --json-pane-string: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
13
15
  --json-pane-number: color-mix(in oklab, var(--dbc-amber-400) 76%, white 24%);
@@ -314,13 +316,6 @@
314
316
  color: color-mix(in oklab, var(--dbc-green-300) 78%, white 22%);
315
317
  }
316
318
 
317
- .highlight {
318
- color: var(--color-fg-inverse);
319
- background: color-mix(in oklab, var(--json-pane-highlight) 58%, transparent);
320
- border-radius: var(--border-radius-sm);
321
- padding-inline: 0.1em;
322
- }
323
-
324
319
  .emptyState {
325
320
  padding: var(--spacing-md);
326
321
  color: var(--json-pane-muted);
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
5
5
  import styles from './Tooltip.module.css';
6
6
  export const TooltipContext = createContext(null);
7
7
  const VIEWPORT_PADDING = 8;
8
+ const SHOW_DELAY_MS = 500;
8
9
  function clamp(n, min, max) {
9
10
  return Math.max(min, Math.min(n, max));
10
11
  }
@@ -65,13 +66,23 @@ function shallowEqualActive(a, b) {
65
66
  }
66
67
  export function TooltipProvider({ children }) {
67
68
  const [active, setActive] = useState(null);
69
+ const showTimerRef = useRef(null);
68
70
  const show = useCallback((next) => {
69
- setActive(curr => {
70
- const proposed = { ...next, open: true };
71
- return shallowEqualActive(curr, proposed) ? curr : proposed;
72
- });
71
+ if (showTimerRef.current)
72
+ clearTimeout(showTimerRef.current);
73
+ showTimerRef.current = setTimeout(() => {
74
+ showTimerRef.current = null;
75
+ setActive(curr => {
76
+ const proposed = { ...next, open: true };
77
+ return shallowEqualActive(curr, proposed) ? curr : proposed;
78
+ });
79
+ }, SHOW_DELAY_MS);
73
80
  }, []);
74
81
  const hide = useCallback((id) => {
82
+ if (showTimerRef.current) {
83
+ clearTimeout(showTimerRef.current);
84
+ showTimerRef.current = null;
85
+ }
75
86
  setActive(curr => {
76
87
  if (!curr || curr.id !== id)
77
88
  return curr;
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import type { ReactNode } from 'react';
2
3
  import { TooltipPlacement } from './TooltipProvider';
3
4
  type UseTooltipOptions = {
@@ -16,7 +17,8 @@ export declare function useTooltipTrigger(options: UseTooltipOptions): {
16
17
  ref: (node: HTMLElement | null) => void;
17
18
  onPointerEnter: () => void;
18
19
  onPointerLeave: () => void;
19
- onFocus: () => void;
20
+ onPointerDown: () => void;
21
+ onFocus: (e: React.FocusEvent) => void;
20
22
  onBlur: () => void;
21
23
  'aria-describedby'?: string;
22
24
  };
@@ -81,9 +81,15 @@ export function useTooltipTrigger(options) {
81
81
  return;
82
82
  closeTimer.current = window.setTimeout(() => setOpen(false), delayCloseMs);
83
83
  };
84
- const onFocus = () => {
84
+ const onPointerDown = () => {
85
85
  clearTimers();
86
86
  if (!isControlled)
87
+ setOpen(false);
88
+ };
89
+ const onFocus = (e) => {
90
+ clearTimers();
91
+ // Only show for keyboard focus — skip pointer clicks and programmatic focus returns (e.g. from modal close)
92
+ if (!isControlled && e.currentTarget.matches(':focus-visible'))
87
93
  setTimeout(() => {
88
94
  setOpen(true);
89
95
  }, MOTION_MS.tooltipOpen);
@@ -102,6 +108,7 @@ export function useTooltipTrigger(options) {
102
108
  },
103
109
  onPointerEnter,
104
110
  onPointerLeave,
111
+ onPointerDown,
105
112
  onFocus,
106
113
  onBlur,
107
114
  'aria-describedby': content ? id : undefined,
@@ -79,6 +79,12 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWid
79
79
  setActiveIndex(null);
80
80
  (_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.close();
81
81
  }
82
+ const handleClear = React.useCallback(() => {
83
+ var _a;
84
+ reset();
85
+ onSearch === null || onSearch === void 0 ? void 0 : onSearch('');
86
+ (_a = internalInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
87
+ }, [onSearch]);
82
88
  // Reset active index when results change
83
89
  useEffect(() => {
84
90
  setActiveIndex(null);
@@ -100,7 +106,7 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWid
100
106
  var _a, _b;
101
107
  if (!((_a = popoverRef.current) === null || _a === void 0 ? void 0 : _a.isOpen()))
102
108
  (_b = popoverRef.current) === null || _b === void 0 ? void 0 : _b.open();
103
- }, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), width: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, icon: showInputIcon ? _jsx(Search, {}) : undefined, inputSize: inputSize, variant: variant, autoComplete: "off", onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, onKeyDown: e => {
109
+ }, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), width: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, icon: showInputIcon ? _jsx(Search, {}) : undefined, inputSize: inputSize, variant: variant, autoComplete: "off", onClear: handleClear, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, onKeyDown: e => {
104
110
  var _a;
105
111
  if (result === null || result === void 0 ? void 0 : result.length) {
106
112
  if (e.key === 'ArrowDown') {
@@ -132,11 +138,12 @@ export const SearchBox = forwardRef(function SearchBoxInner({ inputWidth, maxWid
132
138
  return (_jsx("td", { className: styles.suggestionCell, style: { whiteSpace: cell.length < 10 ? 'nowrap' : undefined }, children: cell }, key));
133
139
  }) }, index))) }) }) })) : !searchQuery && !loading ? (initialTemplate || _jsx("div", { className: styles.resultContainer, children: "Indtast s\u00F8geord" })) : loading ? (_jsx("table", { style: { width: '100%' }, children: _jsx("tbody", { children: Array.from({ length: 5 }).map((_, index) => (_jsx("tr", { children: resultKeys === null || resultKeys === void 0 ? void 0 : resultKeys.map(key => (_jsx("td", { style: { padding: '8px' }, children: _jsx(SkeletonLoaderItem, { height: 20, width: "100%" }) }, key))) }, index))) }) })) : (_jsx("div", { className: styles.resultContainer, children: noResultText })) }));
134
140
  }
135
- return (_jsx(Input, { ref: internalInputRef, icon: showInputIcon ? _jsx(Search, {}) : undefined, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, inputSize: inputSize, variant: variant, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
141
+ return (_jsx(Input, { ref: internalInputRef, icon: showInputIcon ? _jsx(Search, {}) : undefined, minWidth: fullWidth ? undefined : (inputWidth !== null && inputWidth !== void 0 ? inputWidth : '300px'), fullWidth: fullWidth, inputSize: inputSize, variant: variant, onClear: handleClear, onButtonClick: onButtonClick, buttonLabel: buttonLabel, buttonIcon: trailingButtonIcon, ...inputProps, placeholder: (_a = rest.placeholder) !== null && _a !== void 0 ? _a : 'Indtast søgeord' }));
136
142
  }, [
137
143
  rest,
138
144
  draft,
139
145
  handleChange,
146
+ handleClear,
140
147
  displayPopover,
141
148
  inputWidth,
142
149
  inputSize,
@@ -12,11 +12,12 @@ interface SidebarProps {
12
12
  version?: string | number;
13
13
  hideSearch?: boolean;
14
14
  searchPlaceholder?: string;
15
+ showSettings?: boolean;
15
16
  footer?: React.ReactNode;
16
17
  resizable?: boolean;
17
18
  defaultWidth?: number;
18
19
  minWidth?: number;
19
20
  storageKey?: string;
20
21
  }
21
- export declare function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarProps): JSX.Element;
22
+ export declare function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, showSettings, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarProps): JSX.Element;
22
23
  export {};
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { SidebarContainer } from './components/sidebar-container/SidebarContainer';
3
3
  import { SidebarProvider } from './providers/SidebarProvider';
4
- export function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, footer, resizable, defaultWidth, minWidth, storageKey, }) {
5
- return (_jsx(SidebarProvider, { items: items, children: _jsx(SidebarContainer, { productLogo: productLogo, activeLink: activeLink, version: version, hideSearch: hideSearch, searchPlaceholder: searchPlaceholder, footer: footer, resizable: resizable, defaultWidth: defaultWidth, minWidth: minWidth, storageKey: storageKey }) }));
4
+ export function Sidebar({ items, productLogo, activeLink, version, hideSearch, searchPlaceholder, showSettings, footer, resizable, defaultWidth, minWidth, storageKey, }) {
5
+ return (_jsx(SidebarProvider, { items: items, children: _jsx(SidebarContainer, { productLogo: productLogo, activeLink: activeLink, version: version, hideSearch: hideSearch, searchPlaceholder: searchPlaceholder, showSettings: showSettings, footer: footer, resizable: resizable, defaultWidth: defaultWidth, minWidth: minWidth, storageKey: storageKey }) }));
6
6
  }
@@ -7,11 +7,12 @@ interface SidebarContainerProps {
7
7
  version?: string | number;
8
8
  hideSearch?: boolean;
9
9
  searchPlaceholder?: string;
10
+ showSettings?: boolean;
10
11
  footer?: ReactNode;
11
12
  resizable?: boolean;
12
13
  defaultWidth?: number;
13
14
  minWidth?: number;
14
15
  storageKey?: string;
15
16
  }
16
- export declare function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarContainerProps): JSX.Element;
17
+ export declare function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder, showSettings, footer, resizable, defaultWidth, minWidth, storageKey, }: SidebarContainerProps): JSX.Element;
17
18
  export {};
@@ -39,7 +39,7 @@ function removeStoredWidth(key) {
39
39
  // ignore
40
40
  }
41
41
  }
42
- export function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder = 'Filtrer menu', footer, resizable, defaultWidth = 240, minWidth = 160, storageKey, }) {
42
+ export function SidebarContainer({ logo, productLogo, activeLink, version, hideSearch, searchPlaceholder = 'Filtrer menu', showSettings = false, footer, resizable, defaultWidth = 240, minWidth = 160, storageKey, }) {
43
43
  const { isSidebarCollapsed, handleSidebarCollapseChange } = useSidebar();
44
44
  // Always start from defaultWidth so server and client render identical HTML.
45
45
  // localStorage is read in a useEffect after hydration to avoid SSR mismatch.
@@ -149,5 +149,5 @@ export function SidebarContainer({ logo, productLogo, activeLink, version, hideS
149
149
  const containerStyle = useMemo(() => ({
150
150
  '--sidebar-width': `${sidebarWidth}px`,
151
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, { placeholder: searchPlaceholder }) })), _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 }))] }));
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, { placeholder: searchPlaceholder, showSettings: showSettings }) })), _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 }))] }));
153
153
  }
@@ -1,11 +1,18 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { getHighlightedSegments } from '../../../../utils/text/get-highlighted-segments';
3
5
  import styles from './SidebarItemContent.module.css';
4
6
  import { useSidebar } from '../../providers/SidebarProvider';
5
7
  export function SidebarItemContent({ icon, label, suffixIcon, href, iconWidth, disableActiveStyles = false, headerStyle, truncateLabel = false, }) {
6
- const { activeLink, isSidebarCollapsed } = useSidebar();
8
+ const { activeLink, activeQuery, isSidebarCollapsed, wrapItemText } = useSidebar();
7
9
  const iconStyle = iconWidth !== undefined
8
10
  ? { ['--sidebar-item-icon-width']: iconWidth }
9
11
  : undefined;
10
- return (_jsxs("span", { className: `${styles.container} ${!disableActiveStyles && activeLink === href ? styles.active : ''} ${isSidebarCollapsed ? styles.collapsed : ''} ${headerStyle ? styles.headerStyle : ''}`, children: [_jsxs("span", { className: styles.iconLabel, children: [_jsx("span", { className: styles.icon, style: iconStyle, 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 })] }));
12
+ const shouldTruncate = truncateLabel && !wrapItemText && !activeQuery;
13
+ const highlightTerms = activeQuery.trim().split(/\s+/).filter(Boolean);
14
+ const renderedLabel = typeof label === 'string' && highlightTerms.length > 0
15
+ ? getHighlightedSegments(label, highlightTerms).map((segment, index) => segment.matched ? (_jsx("mark", { className: "dbc-highlight", children: segment.text }, `${segment.text}-${index}`)) : (_jsx(React.Fragment, { children: segment.text }, `${segment.text}-${index}`)))
16
+ : label;
17
+ return (_jsxs("span", { className: `${styles.container} ${!disableActiveStyles && activeLink === href ? styles.active : ''} ${isSidebarCollapsed ? styles.collapsed : ''} ${headerStyle ? styles.headerStyle : ''}`, children: [_jsxs("span", { className: styles.iconLabel, children: [_jsx("span", { className: styles.icon, style: iconStyle, children: icon }), !isSidebarCollapsed && (_jsx("span", { className: `${styles.label} ${shouldTruncate ? styles.truncate : ''}`, title: shouldTruncate && typeof label === 'string' ? label : undefined, children: renderedLabel }))] }), suffixIcon && !isSidebarCollapsed && _jsx("span", { className: styles.suffixIcon, children: suffixIcon })] }));
11
18
  }
@@ -10,7 +10,7 @@
10
10
  color: var(--color-fg-muted);
11
11
  cursor: pointer;
12
12
  position: relative;
13
- border-radius: var(--border-radius-default);
13
+ border-radius: var(--border-radius-md);
14
14
  user-select: none;
15
15
  transition:
16
16
  background-color var(--transition-fast) var(--ease-standard),
@@ -34,7 +34,8 @@
34
34
  inset-block: 0;
35
35
  inline-size: 3px;
36
36
  background-color: var(--color-brand);
37
- border-radius: 0;
37
+ border-start-start-radius: inherit;
38
+ border-end-start-radius: inherit;
38
39
  }
39
40
 
40
41
  .iconLabel {
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  interface SidenavFilteringProps {
3
3
  placeholder?: string;
4
+ showSettings?: boolean;
4
5
  }
5
6
  declare const SidenavFiltering: React.FC<SidenavFilteringProps>;
6
7
  export default SidenavFiltering;
@@ -1,12 +1,14 @@
1
1
  'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { Search } from 'lucide-react';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Search, Settings2 } from 'lucide-react';
4
4
  import React from 'react';
5
5
  import { Button } from '../../../button/Button';
6
+ import { Menu } from '../../../menu/Menu';
7
+ import { Popover } from '../../../popover/Popover';
6
8
  import { SearchBox } from '../../../search-box/SearchBox';
7
9
  import { useSidebar } from '../../providers/SidebarProvider';
8
- const SidenavFiltering = ({ placeholder = 'Filtrer menu' }) => {
9
- const { activeQuery, setActiveQuery, isSidebarCollapsed, handleSidebarCollapseChange } = useSidebar();
10
+ const SidenavFiltering = ({ placeholder = 'Filtrer menu', showSettings = false, }) => {
11
+ const { activeQuery, setActiveQuery, isSidebarCollapsed, handleSidebarCollapseChange, wrapItemText, setWrapItemText, } = useSidebar();
10
12
  const searchBoxRef = React.useRef(null);
11
13
  const handleSearch = (value) => {
12
14
  setActiveQuery === null || setActiveQuery === void 0 ? void 0 : setActiveQuery(value);
@@ -20,6 +22,9 @@ const SidenavFiltering = ({ placeholder = 'Filtrer menu' }) => {
20
22
  }, 50);
21
23
  } }));
22
24
  }
23
- return (_jsx(SearchBox, { ref: searchBoxRef, inputWidth: "100%", inputSize: "sm", value: activeQuery !== null && activeQuery !== void 0 ? activeQuery : '', onSearch: handleSearch, placeholder: placeholder }));
25
+ return (_jsxs("div", { style: { display: 'flex', alignItems: 'stretch', gap: 'var(--spacing-xs)' }, children: [_jsx(SearchBox, { ref: searchBoxRef, inputWidth: "100%", inputSize: "sm", value: activeQuery !== null && activeQuery !== void 0 ? activeQuery : '', onSearch: handleSearch, placeholder: placeholder, fullWidth: true }), showSettings ? (_jsx(Popover, { minWidth: "220px", trigger: toggle => (_jsx(Button, { type: "button", size: "sm", variant: "outlined", shape: "round", "aria-label": "\u00C5bn indstillinger for sidemenu", onClick: toggle, icon: _jsx(Settings2, {}) })), children: close => (_jsx(Menu, { children: _jsx(Menu.Item, { active: wrapItemText, children: _jsx("button", { type: "button", onClick: () => {
26
+ setWrapItemText(!wrapItemText);
27
+ close();
28
+ }, children: "Ombryd tekst" }) }) })) })) : null] }));
24
29
  };
25
30
  export default SidenavFiltering;
@@ -6,8 +6,10 @@ export type SidebarContextValue = {
6
6
  expandedItems: Set<string>;
7
7
  resetExpandAll: () => void;
8
8
  activeQuery: string;
9
+ wrapItemText: boolean;
9
10
  areItemsCollapsed: boolean;
10
11
  setActiveQuery: (query: string) => void;
12
+ setWrapItemText: (value: boolean) => void;
11
13
  triggerExpandAll: () => void;
12
14
  setItemsCollapsed: (v: boolean) => void;
13
15
  filteredItems?: NavBarItem[];
@@ -27,6 +29,6 @@ type SidebarProviderProps = {
27
29
  initialCollapseChildren?: boolean;
28
30
  initialSidebarCollapsed?: boolean;
29
31
  };
30
- export declare function SidebarProvider({ children, items, initialCollapsed, initialSidebarCollapsed, }: SidebarProviderProps): JSX.Element;
32
+ export declare function SidebarProvider({ children, items, initialQuery, initialCollapsed, initialSidebarCollapsed, }: SidebarProviderProps): JSX.Element;
31
33
  export declare function useSidebar(): SidebarContextValue;
32
34
  export {};
@@ -2,13 +2,6 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { nestedFiltering } from '../../../utils/arrays/nested-filtering';
5
- /**
6
- * Production notes:
7
- * - No console logging.
8
- * - Auto-expands the correct expandable chain for the active link (including when the active link
9
- * points to the expandable parent itself).
10
- * - Normalizes hrefs (trailing slashes) so comparisons are stable.
11
- */
12
5
  const hasChildren = (item) => Array.isArray(item.children) && item.children.length > 0;
13
6
  const hasHref = (item) => typeof item.href === 'string' && item.href.length > 0;
14
7
  const normalizeHref = (href) => {
@@ -55,8 +48,10 @@ const SidebarContext = createContext({
55
48
  defaultExpanded: null,
56
49
  expandedItems: new Set(),
57
50
  activeQuery: '',
51
+ wrapItemText: false,
58
52
  areItemsCollapsed: false,
59
53
  setActiveQuery: () => { },
54
+ setWrapItemText: () => { },
60
55
  triggerExpandAll: () => { },
61
56
  setItemsCollapsed: () => { },
62
57
  resetExpandAll: () => { },
@@ -71,9 +66,10 @@ const SidebarContext = createContext({
71
66
  const SIDEBAR_COLLAPSED_STORAGE_KEY = 'sidebar-is-collapsed';
72
67
  const SIDEBAR_BREAKPOINT = 1024;
73
68
  const getBreakpoint = (width) => width < SIDEBAR_BREAKPOINT ? 'small' : 'large';
74
- export function SidebarProvider({ children, items, initialCollapsed = false, initialSidebarCollapsed, }) {
69
+ export function SidebarProvider({ children, items, initialQuery, initialCollapsed = false, initialSidebarCollapsed, }) {
75
70
  const [defaultExpanded, setDefaultExpanded] = useState(null);
76
- const [activeQuery, setActiveQuery] = useState('');
71
+ const [activeQuery, setActiveQuery] = useState(initialQuery !== null && initialQuery !== void 0 ? initialQuery : '');
72
+ const [wrapItemText, setWrapItemText] = useState(false);
77
73
  const [areItemsCollapsed, setItemsCollapsed] = useState(initialCollapsed);
78
74
  const [activeHref, setActiveHref] = useState('');
79
75
  // expandedItems is the source of truth for "open groups"
@@ -84,7 +80,22 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
84
80
  itemsRef.current = items;
85
81
  }, [items]);
86
82
  const [isSidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false);
87
- const hasExplicitInitialSidebarCollapsed = initialSidebarCollapsed !== undefined;
83
+ // Runs once after hydration — safe to read localStorage and window.innerWidth here.
84
+ useEffect(() => {
85
+ if (initialSidebarCollapsed !== undefined)
86
+ return;
87
+ try {
88
+ const stored = window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
89
+ if (stored !== null) {
90
+ setSidebarCollapsed(Boolean(JSON.parse(stored))); // eslint-disable-line react-hooks/set-state-in-effect -- intentional: SSR-safe initial read
91
+ return;
92
+ }
93
+ }
94
+ catch {
95
+ // ignore parse failures
96
+ }
97
+ setSidebarCollapsed(getBreakpoint(window.innerWidth) === 'small');
98
+ }, []); // intentionally empty — only runs once after first mount
88
99
  const triggerExpandAll = useCallback(() => setDefaultExpanded(true), []);
89
100
  const resetExpandAll = useCallback(() => setDefaultExpanded(null), []);
90
101
  const setActiveLink = useCallback((href) => setActiveHref(href), []);
@@ -140,41 +151,6 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
140
151
  })
141
152
  : items;
142
153
  }, [items, activeQuery]);
143
- // Searching should expand all.
144
- useEffect(() => {
145
- if (activeQuery)
146
- triggerExpandAll();
147
- }, [activeQuery, triggerExpandAll]);
148
- // Initial collapsed state: explicit prop > localStorage > responsive default.
149
- useEffect(() => {
150
- if (typeof window === 'undefined')
151
- return;
152
- const currentBreakpoint = getBreakpoint(window.innerWidth);
153
- if (hasExplicitInitialSidebarCollapsed) {
154
- const value = initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false;
155
- setSidebarCollapsed(value);
156
- try {
157
- window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, JSON.stringify(value));
158
- }
159
- catch {
160
- // ignore persist failures
161
- }
162
- return;
163
- }
164
- try {
165
- const stored = window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY);
166
- if (stored !== null) {
167
- const parsed = JSON.parse(stored);
168
- setSidebarCollapsed(Boolean(parsed));
169
- return;
170
- }
171
- }
172
- catch {
173
- // ignore parse failures
174
- }
175
- // Nothing stored -> responsive default (do NOT persist automatic choice)
176
- setSidebarCollapsed(currentBreakpoint === 'small');
177
- }, [hasExplicitInitialSidebarCollapsed, initialSidebarCollapsed]);
178
154
  const persistCollapsed = useCallback((collapsed) => {
179
155
  if (typeof window === 'undefined')
180
156
  return;
@@ -208,12 +184,14 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
208
184
  return () => window.removeEventListener('resize', onResize);
209
185
  }, []);
210
186
  const value = useMemo(() => ({
211
- defaultExpanded,
187
+ defaultExpanded: activeQuery ? true : defaultExpanded,
212
188
  expandedItems,
213
189
  filteredItems,
214
190
  activeQuery,
191
+ wrapItemText,
215
192
  areItemsCollapsed,
216
193
  setActiveQuery,
194
+ setWrapItemText,
217
195
  triggerExpandAll,
218
196
  resetExpandAll,
219
197
  setItemsCollapsed,
@@ -229,8 +207,10 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
229
207
  expandedItems,
230
208
  filteredItems,
231
209
  activeQuery,
210
+ wrapItemText,
232
211
  areItemsCollapsed,
233
212
  setActiveQuery,
213
+ setWrapItemText,
234
214
  triggerExpandAll,
235
215
  resetExpandAll,
236
216
  setItemsCollapsed,
@@ -122,7 +122,12 @@ body.dbc-app {
122
122
  }
123
123
 
124
124
  .dbc-highlight {
125
- background-color: var(--color-status-warning-bg);
125
+ color: var(--color-highlight-fg, inherit);
126
+ background-color: var(--color-highlight-bg, var(--color-status-warning-bg));
127
+ border-radius: var(--border-radius-sm);
128
+ padding-inline: var(--spacing-2xs);
129
+ box-decoration-break: clone;
130
+ -webkit-box-decoration-break: clone;
126
131
  }
127
132
 
128
133
  .dbc-muted-text {
@@ -83,6 +83,8 @@ html[data-theme='dark'] {
83
83
  /* Selected */
84
84
  --color-bg-selected: var(--dbc-blue-100);
85
85
  --color-bg-selected-hover: var(--dbc-blue-150);
86
+ --color-highlight-bg: var(--color-bg-selected-hover);
87
+ --color-highlight-fg: var(--color-fg-default);
86
88
 
87
89
  --color-neutral: var(--dbc-neutral-200);
88
90
 
@@ -83,6 +83,8 @@ html[data-theme='light'] {
83
83
  /* Selected */
84
84
  --color-bg-selected: var(--dbc-blue-100);
85
85
  --color-bg-selected-hover: var(--dbc-blue-150);
86
+ --color-highlight-bg: var(--color-bg-selected-hover);
87
+ --color-highlight-fg: var(--color-fg-default);
86
88
 
87
89
  --color-neutral: var(--dbc-neutral-200);
88
90
 
package/dist/styles.css CHANGED
@@ -122,7 +122,12 @@ body.dbc-app {
122
122
  }
123
123
 
124
124
  .dbc-highlight {
125
- background-color: var(--color-status-warning-bg);
125
+ color: var(--color-highlight-fg, inherit);
126
+ background-color: var(--color-highlight-bg, var(--color-status-warning-bg));
127
+ border-radius: var(--border-radius-sm);
128
+ padding-inline: var(--spacing-2xs);
129
+ box-decoration-break: clone;
130
+ -webkit-box-decoration-break: clone;
126
131
  }
127
132
 
128
133
  .dbc-muted-text {
@@ -0,0 +1,5 @@
1
+ export type HighlightSegment = {
2
+ text: string;
3
+ matched: boolean;
4
+ };
5
+ export declare function getHighlightedSegments(text: string, query: string | string[]): HighlightSegment[];
@@ -0,0 +1,46 @@
1
+ function normalizeTerms(query) {
2
+ const terms = Array.isArray(query) ? query : [query];
3
+ return [...new Set(terms.map(term => term.trim().toLowerCase()).filter(Boolean))].sort((a, b) => b.length - a.length);
4
+ }
5
+ export function getHighlightedSegments(text, query) {
6
+ const terms = normalizeTerms(query);
7
+ if (!text || terms.length === 0)
8
+ return [{ text, matched: false }];
9
+ const lower = text.toLowerCase();
10
+ const ranges = [];
11
+ for (const term of terms) {
12
+ let startIndex = 0;
13
+ while (startIndex < lower.length) {
14
+ const matchIndex = lower.indexOf(term, startIndex);
15
+ if (matchIndex === -1)
16
+ break;
17
+ ranges.push({ start: matchIndex, end: matchIndex + term.length });
18
+ startIndex = matchIndex + term.length;
19
+ }
20
+ }
21
+ if (ranges.length === 0)
22
+ return [{ text, matched: false }];
23
+ ranges.sort((a, b) => a.start - b.start || a.end - b.end);
24
+ const mergedRanges = [];
25
+ for (const range of ranges) {
26
+ const previous = mergedRanges[mergedRanges.length - 1];
27
+ if (!previous || range.start > previous.end) {
28
+ mergedRanges.push({ ...range });
29
+ continue;
30
+ }
31
+ previous.end = Math.max(previous.end, range.end);
32
+ }
33
+ const segments = [];
34
+ let cursor = 0;
35
+ for (const range of mergedRanges) {
36
+ if (range.start > cursor) {
37
+ segments.push({ text: text.slice(cursor, range.start), matched: false });
38
+ }
39
+ segments.push({ text: text.slice(range.start, range.end), matched: true });
40
+ cursor = range.end;
41
+ }
42
+ if (cursor < text.length) {
43
+ segments.push({ text: text.slice(cursor), matched: false });
44
+ }
45
+ return segments.length > 0 ? segments : [{ text, matched: false }];
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbcdk/react-components",
3
- "version": "0.0.79",
3
+ "version": "0.0.81",
4
4
  "description": "Reusable React components for DBC projects",
5
5
  "license": "ISC",
6
6
  "author": "",