@dbcdk/react-components 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,3 @@
1
- /* ==========================================================================
2
- * BASE BUTTON
3
- * ======================================================================= */
4
-
5
1
  .button {
6
2
  display: inline-flex;
7
3
  align-items: center;
@@ -226,6 +222,14 @@
226
222
  border-color: var(--color-border-selected);
227
223
  }
228
224
 
225
+ .inline.active {
226
+ color: var(--button-bg-primary);
227
+ }
228
+
229
229
  .active:hover {
230
230
  background-color: var(--button-bg-primary-hover);
231
231
  }
232
+
233
+ .inline.active:hover {
234
+ color: var(--button-bg-primary-hover);
235
+ }
@@ -1,5 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TextWrap } from 'lucide-react';
3
+ import { useMemo, useState } from 'react';
2
4
  import styles from './CodeBlock.module.css';
5
+ import { Button } from '../button/Button';
3
6
  import { CopyButton } from '../copy-button/CopyButton';
4
7
  const looksLikeStackFrame = (line) => {
5
8
  const t = line.trim();
@@ -16,24 +19,30 @@ export function CodeBlock({ code, children, copyButton, copyText, size = 'md', s
16
19
  var _a;
17
20
  const text = typeof code === 'string' ? code : undefined;
18
21
  const copy = (_a = copyText !== null && copyText !== void 0 ? copyText : text) !== null && _a !== void 0 ? _a : '';
19
- // If children are provided, render them as-is (no line processing).
20
22
  const hasChildren = children !== undefined && children !== null;
21
- // Smart rendering only when we have plain text + no children.
22
- const lines = smart && !hasChildren && typeof text === 'string' ? text.split('\n') : null;
23
- return (_jsxs("pre", { className: [styles.container, styles[size], wrap ? styles.wrap : styles.noWrap].join(' '), tabIndex: 0, children: [copyButton && (_jsx("span", { className: styles.copyButton, children: _jsx(CopyButton, { shape: "round", variant: "inline", text: copy }) })), _jsx("code", { className: styles.code, children: hasChildren
24
- ? children
25
- : lines
26
- ? lines.map((line, i) => {
27
- const isFirst = i === 0;
28
- const isFrame = looksLikeStackFrame(line);
29
- const cls = [
30
- styles.line,
31
- isFirst ? styles.lineFirst : '',
32
- isFrame ? styles.lineFrame : '',
33
- ]
34
- .filter(Boolean)
35
- .join(' ');
36
- return (_jsxs("span", { className: cls, children: [line, '\n'] }, i));
37
- })
38
- : text })] }));
23
+ const [isWrapped, setIsWrapped] = useState(wrap);
24
+ const lines = useMemo(() => (smart && !hasChildren && typeof text === 'string' ? text.split('\n') : null), [smart, hasChildren, text]);
25
+ return (_jsxs("div", { className: [
26
+ styles.wrapper,
27
+ styles[size],
28
+ isWrapped ? styles.wrap : styles.noWrap,
29
+ copyButton ? styles.hasActions : '',
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' '), children: [copyButton && (_jsxs("span", { className: styles.actions, "aria-hidden": false, children: [_jsx(Button, { type: "button", variant: "inline", size: "sm", shape: "round", onClick: () => setIsWrapped(v => !v), "aria-pressed": isWrapped, active: isWrapped, title: isWrapped ? 'Ombryd ikke tekst' : 'Ombryd tekst', children: _jsx(TextWrap, { size: 16 }) }), _jsx(CopyButton, { size: "sm", shape: "round", variant: "inline", text: copy })] })), _jsx("pre", { className: styles.container, tabIndex: 0, children: _jsx("code", { className: styles.code, children: hasChildren
33
+ ? children
34
+ : lines
35
+ ? lines.map((line, i) => {
36
+ const isFirst = i === 0;
37
+ const isFrame = looksLikeStackFrame(line);
38
+ const cls = [
39
+ styles.line,
40
+ isFirst ? styles.lineFirst : '',
41
+ isFrame ? styles.lineFrame : '',
42
+ ]
43
+ .filter(Boolean)
44
+ .join(' ');
45
+ return (_jsxs("span", { className: cls, children: [line, '\n'] }, i));
46
+ })
47
+ : text }) })] }));
39
48
  }
@@ -1,79 +1,80 @@
1
+ .wrapper {
2
+ position: relative;
3
+ --code-actions-h: var(--component-size-sm);
4
+ --code-actions-inset: var(--spacing-xs);
5
+ }
6
+
7
+ /* <pre> */
1
8
  .container {
2
9
  position: relative;
3
10
  margin-block: 0;
4
-
5
- background: var(--color-bg-contextual-subtle);
6
- border: var(--border-width-thin) solid var(--color-border-default);
11
+ background: var(--color-bg-surface-strong);
12
+ border: var(--border-width-thin) solid var(--color-border-subtle);
7
13
  border-radius: var(--border-radius-lg);
8
14
  box-shadow: var(--shadow-xs);
9
-
10
15
  padding: var(--spacing-sm);
11
- padding-inline-end: calc(var(--spacing-sm) + 40px);
12
-
13
16
  font-family: var(--font-family-mono);
14
- line-height: var(--line-height-relaxed);
15
-
16
- overflow-x: auto;
17
- overflow-y: hidden;
18
-
19
- /* Nice: avoids layout shift if/when scrollbars appear (supported in modern browsers) */
17
+ line-height: 1.35;
18
+ overflow: auto;
20
19
  scrollbar-gutter: stable;
21
- }
22
20
 
23
- .container:focus-within {
24
- border-color: var(--color-border-selected);
25
- box-shadow: var(--shadow-xs), var(--focus-ring);
21
+ display: flex;
22
+ align-items: center;
26
23
  }
27
24
 
28
- .container.sm {
25
+ /* Sizes */
26
+ .sm .container {
29
27
  padding: var(--spacing-xs);
30
- padding-inline-end: calc(var(--spacing-xs) + 40px);
31
28
  }
32
29
 
33
- .container.sm .code {
30
+ .sm .code {
34
31
  font-size: var(--font-size-xs);
35
32
  }
36
33
 
37
- .container.md .code {
34
+ .md .code {
38
35
  font-size: var(--font-size-sm);
39
36
  }
40
37
 
41
- .container.lg .code {
38
+ .lg .code {
42
39
  font-size: var(--font-size-base);
43
40
  }
44
41
 
45
- .code {
46
- display: block;
47
- margin: 0;
48
- font-family: var(--font-family-mono);
49
- color: var(--color-fg-default);
50
- white-space: pre-wrap;
51
- overflow-wrap: anywhere;
52
- word-break: normal; /* <- not break-all */
42
+ .hasActions .container {
43
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-sm) + var(--spacing-sm));
53
44
  }
54
45
 
55
- /* Copy button stays overlayed; does not affect layout */
56
- .copyButton {
57
- position: absolute;
58
- top: var(--spacing-xs);
59
- right: var(--spacing-xs);
60
- z-index: 2;
46
+ .sm.hasActions .container {
47
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-xs) + var(--spacing-xs));
48
+ }
61
49
 
62
- opacity: 0;
63
- pointer-events: none;
64
- transition: opacity var(--transition-fast) var(--ease-standard);
50
+ .md.hasActions .container {
51
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-sm) + var(--spacing-sm));
65
52
  }
66
53
 
67
- .container:hover .copyButton,
68
- .container:focus-within .copyButton {
69
- opacity: 1;
70
- pointer-events: auto;
54
+ .lg.hasActions .container {
55
+ min-block-size: calc(var(--code-actions-h) + var(--spacing-sm) + var(--spacing-sm));
71
56
  }
72
57
 
73
- /* --- New: wrap control --- */
58
+ /* Focus ring */
59
+ .wrapper:focus-within .container {
60
+ border-color: var(--color-border-selected);
61
+ box-shadow: var(--shadow-xs), var(--focus-ring);
62
+ }
63
+
64
+ /* <code> */
65
+ .code {
66
+ display: block;
67
+ margin: 0;
68
+ font-family: var(--font-family-mono);
69
+ color: var(--color-fg-default);
70
+ flex: 1 1 auto;
71
+ min-width: 0;
72
+ }
73
+
74
+ /* Wrapping modes */
74
75
  .wrap .code {
75
76
  white-space: pre-wrap;
76
- overflow-wrap: anywhere;
77
+ overflow-wrap: break-word;
77
78
  word-break: normal;
78
79
  }
79
80
 
@@ -83,9 +84,31 @@
83
84
  word-break: normal;
84
85
  }
85
86
 
86
- /* --- New: per-line styling (for smart mode) --- */
87
+ .actions {
88
+ position: absolute;
89
+ top: var(--code-actions-inset);
90
+ right: var(--code-actions-inset);
91
+ z-index: 3;
92
+ display: inline-flex;
93
+ gap: var(--spacing-xs);
94
+ align-items: center;
95
+ padding: var(--spacing-2xs);
96
+ border-radius: var(--border-radius-lg);
97
+ background: color-mix(in oklab, var(--color-bg-surface) 70%, transparent);
98
+ backdrop-filter: blur(6px);
99
+ opacity: 0;
100
+ pointer-events: none;
101
+ transition: opacity var(--transition-fast) var(--ease-standard);
102
+ }
103
+
104
+ .wrapper:hover .actions,
105
+ .wrapper:focus-within .actions {
106
+ opacity: 1;
107
+ pointer-events: auto;
108
+ }
109
+
87
110
  .line {
88
- display: inline; /* keep selection/copy behavior natural */
111
+ display: inline;
89
112
  }
90
113
 
91
114
  .lineFirst {
@@ -94,27 +117,6 @@
94
117
  color: var(--color-fg-default);
95
118
  }
96
119
 
97
- /* Common stack frames are “noise”; deemphasize without hiding */
98
120
  .lineFrame {
99
121
  color: var(--color-fg-subtle);
100
122
  }
101
-
102
- /* Optional: make the container a bit denser for logs */
103
- .container {
104
- /* keep your existing properties ... */
105
-
106
- /* Easy win: stacktraces feel less tall */
107
- line-height: 1.35; /* instead of relaxed */
108
- }
109
-
110
- /* Optional: differentiate the code area slightly from surrounding UI */
111
- .container {
112
- background: var(--color-bg-surface-strong); /* a touch more neutral than contextual */
113
- border-color: var(--color-border-subtle);
114
- }
115
-
116
- /* Keep focus ring behavior */
117
- .container:focus-within {
118
- border-color: var(--color-border-selected);
119
- box-shadow: var(--shadow-xs), var(--focus-ring);
120
- }
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { JSX, ReactNode } from 'react';
3
- type Variant = 'default' | 'primary' | 'outlined' | 'success';
3
+ type Variant = 'default' | 'primary' | 'outlined' | 'success' | 'info';
4
4
  type Size = 'sm' | 'md' | 'lg';
5
5
  interface CheckboxProps {
6
6
  checked?: boolean;
@@ -66,6 +66,13 @@
66
66
  &:not(:hover) {
67
67
  border-color: transparent;
68
68
  }
69
+ }
70
+
71
+ .info.checked {
72
+ background-color: var(--color-status-info);
73
+ &:not(:hover) {
74
+ border-color: transparent;
75
+ }
69
76
 
70
77
  .icon {
71
78
  color: var(--color-fg-on-brand);
@@ -19,5 +19,6 @@ export type ModalProps = {
19
19
  severity?: Severity;
20
20
  disableContentSpacing?: boolean;
21
21
  dataCy?: string;
22
+ width?: number | string;
22
23
  };
23
- export declare function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick, severity, disableContentSpacing, isLoading, dataCy, }: ModalProps): React.ReactNode;
24
+ export declare function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick, severity, disableContentSpacing, isLoading, dataCy, width, }: ModalProps): React.ReactNode;
@@ -5,7 +5,7 @@ import { useEffect, useId, useRef } from 'react';
5
5
  import { Button } from '../../../components/button/Button';
6
6
  import { Headline } from '../../../components/headline/Headline';
7
7
  import styles from './Modal.module.css';
8
- export function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick = true, severity, disableContentSpacing = false, isLoading, dataCy, }) {
8
+ export function Modal({ isOpen, onRequestClose, header, content, children, primaryAction, secondaryAction, closeOnOverlayClick = true, severity, disableContentSpacing = false, isLoading, dataCy, width, }) {
9
9
  const titleId = useId();
10
10
  const dialogRef = useRef(null);
11
11
  const lastActiveElementRef = useRef(null);
@@ -30,7 +30,6 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
30
30
  const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
31
31
  const focusable = dialog.querySelectorAll(focusableSelectors);
32
32
  if (focusable.length > 0) {
33
- // Prefer focusing the first input/select/textarea if present
34
33
  const preferred = (_a = dialog.querySelector('input, select, textarea')) !== null && _a !== void 0 ? _a : focusable[0];
35
34
  preferred.focus();
36
35
  }
@@ -68,13 +67,11 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
68
67
  document.addEventListener('keydown', handleKeyDown);
69
68
  return () => {
70
69
  document.removeEventListener('keydown', handleKeyDown);
71
- // Restore focus only when closing (true -> false) happens elsewhere
72
- // so we do it when modal unmounts while open.
73
70
  if (lastActiveElementRef.current) {
74
71
  lastActiveElementRef.current.focus();
75
72
  }
76
73
  };
77
- }, [isOpen]); // <-- IMPORTANT: only depend on isOpen
74
+ }, [isOpen]);
78
75
  if (!isOpen)
79
76
  return null;
80
77
  const handleOverlayClick = () => {
@@ -88,5 +85,8 @@ export function Modal({ isOpen, onRequestClose, header, content, children, prima
88
85
  const resolvedSecondaryAction = secondaryAction !== null && secondaryAction !== void 0 ? secondaryAction : (primaryAction ? { label: 'Luk', onClick: onRequestCloseRef.current } : undefined);
89
86
  const shouldRenderFooter = Boolean(primaryAction || resolvedSecondaryAction);
90
87
  const body = children !== null && children !== void 0 ? children : content;
91
- return (_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", variant: "outlined", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: "primary", onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }));
88
+ const resolvedWidth = typeof width === 'number' ? `${width}px` : typeof width === 'string' ? width : undefined;
89
+ return (_jsx("div", { className: styles.overlay, onClick: handleOverlayClick, children: _jsxs("div", { "data-cy": dataCy, ref: dialogRef, className: `${styles.modal} ${disableContentSpacing ? '' : styles.contentSpacing}`, style: resolvedWidth
90
+ ? { ['--modal-width']: resolvedWidth }
91
+ : undefined, onClick: stopPropagation, role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, children: [_jsxs("div", { className: styles.header, children: [header && (_jsx(Headline, { severity: severity, size: 3, disableMargin: true, children: header })), _jsx(Button, { type: "button", variant: "inline", onClick: () => onRequestCloseRef.current(), "aria-label": "Luk", shape: "round", icon: _jsx(X, {}) })] }), _jsx("div", { className: styles.body, children: body }), shouldRenderFooter && (_jsxs("div", { className: styles.footer, children: [resolvedSecondaryAction && (_jsxs(Button, { type: "button", variant: "outlined", onClick: resolvedSecondaryAction.onClick, disabled: isLoading, children: [resolvedSecondaryAction.icon && (_jsx("span", { className: styles.icon, children: resolvedSecondaryAction.icon })), _jsx("span", { children: resolvedSecondaryAction.label })] })), primaryAction && (_jsxs(Button, { type: "button", variant: "primary", onClick: primaryAction.onClick, disabled: primaryAction.disabled || isLoading, loading: isLoading, children: [primaryAction.icon && _jsx("span", { className: styles.icon, children: primaryAction.icon }), _jsx("span", { children: primaryAction.label })] }))] }))] }) }));
92
92
  }
@@ -2,62 +2,87 @@
2
2
  position: fixed;
3
3
  inset: 0;
4
4
  background: var(--overlay-scrim);
5
+
5
6
  display: flex;
6
- align-items: flex-start;
7
7
  justify-content: center;
8
- padding-top: clamp(var(--spacing-md), 12vh, 24vh);
9
- padding-bottom: var(--spacing-md);
8
+
9
+ padding: clamp(var(--spacing-sm), 10vh, var(--spacing-xl));
10
10
  z-index: var(--z-backdrop-modal);
11
+
12
+ /* Overlay can scroll if modal is taller than viewport */
11
13
  overflow-y: auto;
12
14
  }
13
15
 
16
+ /* Default width can be overridden by --modal-width from props */
14
17
  .modal {
18
+ --modal-width: 700px;
19
+
15
20
  background: var(--color-bg-surface);
16
21
  border-radius: var(--border-radius-lg);
17
- min-width: 320px;
18
- max-width: 700px;
19
- max-height: calc(100vh - (2 * var(--spacing-md)));
20
- display: flex;
21
- flex-direction: column;
22
22
  box-shadow: var(--shadow-lg);
23
23
  font-family: var(--font-family);
24
- min-width: 500px;
25
- z-index: var(--z-modal);
26
24
  color: var(--color-fg-default);
27
- }
25
+ z-index: var(--z-modal);
26
+
27
+ /* Responsive width: never exceed viewport */
28
+ width: min(
29
+ var(--modal-width),
30
+ calc(100vw - 2 * clamp(var(--spacing-sm), 4vw, var(--spacing-lg)))
31
+ );
32
+ min-width: 320px;
33
+
34
+ /* Critical: prevent “below bottom of screen”
35
+ Prefer svh on mobile; fallback to vh. */
36
+ max-height: calc(100svh - 2 * clamp(var(--spacing-sm), 10vh, var(--spacing-xl)));
37
+ max-height: calc(100vh - 2 * clamp(var(--spacing-sm), 10vh, var(--spacing-xl)));
38
+
39
+ display: flex;
40
+ flex-direction: column;
28
41
 
29
- /* Slightly more relaxed on small screens */
30
- @media (max-width: var(--bp-sm)) {
31
- .modal {
32
- width: calc(100% - 2 * var(--spacing-md));
33
- max-width: 100%;
34
- }
42
+ /* If you want it slightly lower than top padding, keep it aligned start */
43
+ align-self: flex-start;
44
+
45
+ /* Helps on iOS when address bar changes */
46
+ overscroll-behavior: contain;
35
47
  }
36
48
 
49
+ /* Header/footer pinned; body scrolls */
37
50
  .header {
38
51
  font-size: var(--font-size-md);
39
52
  font-weight: var(--font-weight-semibold);
53
+
40
54
  display: flex;
41
55
  justify-content: space-between;
42
56
  align-items: center;
57
+
43
58
  padding: var(--spacing-md);
44
59
  padding-bottom: 0;
60
+
61
+ /* Keeps header readable if body scrolls underneath */
62
+ flex: 0 0 auto;
45
63
  }
46
64
 
47
65
  .body {
48
- overflow-y: auto;
66
+ flex: 1 1 auto;
67
+ overflow: auto;
68
+
49
69
  font-size: var(--font-size-sm);
50
70
  line-height: var(--line-height-normal);
51
71
  color: var(--color-fg-muted);
72
+
52
73
  padding: var(--spacing-md);
74
+ min-height: 0; /* IMPORTANT: allows flex child to actually scroll */
53
75
  }
54
76
 
55
77
  .footer {
78
+ flex: 0 0 auto;
79
+
56
80
  display: flex;
57
81
  justify-content: flex-end;
58
82
  gap: var(--spacing-xs);
59
- padding-top: 0;
83
+
60
84
  padding: var(--spacing-md);
85
+ padding-top: 0;
61
86
  }
62
87
 
63
88
  .icon {
@@ -65,3 +90,11 @@
65
90
  align-items: center;
66
91
  justify-content: center;
67
92
  }
93
+
94
+ .contentSpacing .body > :first-child {
95
+ margin-top: 0;
96
+ }
97
+
98
+ .contentSpacing .body > :last-child {
99
+ margin-bottom: 0;
100
+ }
@@ -1,6 +1,6 @@
1
1
  import type { JSX } from 'react';
2
2
  import * as React from 'react';
3
- import { NavBarItem } from '../../../components/nav-bar/NavBar';
3
+ import type { NavBarItem } from '../../../components/nav-bar/NavBar';
4
4
  export type SidebarContextValue = {
5
5
  defaultExpanded: boolean | null;
6
6
  expandedItems: Set<string>;
@@ -2,24 +2,54 @@
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
+ */
5
12
  const hasChildren = (item) => Array.isArray(item.children) && item.children.length > 0;
6
13
  const hasHref = (item) => typeof item.href === 'string' && item.href.length > 0;
7
- const findParentItem = (navItem, items, prevPath = '') => {
8
- for (const currentItem of items) {
9
- // Groups don't contribute to the href path; they just wrap children
10
- const nextPath = hasHref(currentItem) ? `${prevPath}.${currentItem.href}` : prevPath;
11
- if (hasChildren(currentItem)) {
12
- // Direct child match
13
- if (currentItem.children.some(child => hasHref(child) && child.href === navItem)) {
14
- return nextPath;
14
+ const normalizeHref = (href) => {
15
+ if (!href)
16
+ return href;
17
+ // strip trailing slashes except root
18
+ return href.length > 1 ? href.replace(/\/+$/, '') : href;
19
+ };
20
+ /**
21
+ * Returns the chain of "expandable parents" (hrefs) that contain `targetHref`.
22
+ *
23
+ * Behavior:
24
+ * - If targetHref matches an expandable parent item itself, the chain includes it.
25
+ * - If targetHref matches a leaf under expandables, the chain includes expandable ancestors.
26
+ *
27
+ * Expandable parent definition:
28
+ * - any node with children AND an href
29
+ * (If you want to restrict this to a specific type, update isExpandableParent.)
30
+ */
31
+ const findExpandableParentChain = (targetHref, items) => {
32
+ var _a;
33
+ const target = normalizeHref(targetHref);
34
+ const dfs = (nodes, parentExpandables) => {
35
+ for (const node of nodes) {
36
+ const nodeHref = hasHref(node) ? normalizeHref(node.href) : null;
37
+ const isExpandableParent = hasChildren(node) && hasHref(node);
38
+ const nextParents = isExpandableParent && nodeHref ? [...parentExpandables, nodeHref] : parentExpandables;
39
+ // Match this node
40
+ if (nodeHref === target) {
41
+ return isExpandableParent ? nextParents : parentExpandables;
42
+ }
43
+ // Recurse into children
44
+ if (hasChildren(node)) {
45
+ const found = dfs(node.children, nextParents);
46
+ if (found)
47
+ return found;
15
48
  }
16
- // Recurse
17
- const path = findParentItem(navItem, currentItem.children, nextPath);
18
- if (path)
19
- return path;
20
49
  }
21
- }
22
- return '';
50
+ return null;
51
+ };
52
+ return (_a = dfs(items, [])) !== null && _a !== void 0 ? _a : [];
23
53
  };
24
54
  const SidebarContext = createContext({
25
55
  defaultExpanded: null,
@@ -46,10 +76,9 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
46
76
  const [activeQuery, setActiveQuery] = useState('');
47
77
  const [areItemsCollapsed, setItemsCollapsed] = useState(initialCollapsed);
48
78
  const [activeHref, setActiveHref] = useState('');
49
- // expandedItems is now the single source of truth for "open groups"
50
- // (it includes both auto-expanded parents and user-expanded groups)
79
+ // expandedItems is the source of truth for "open groups"
51
80
  const [expandedItems, setExpandedItems] = useState(new Set());
52
- // Track items in a ref to avoid effect loops if parent recreates the items array every render
81
+ // Keep latest items without creating effect loops if parent recreates the array
53
82
  const itemsRef = useRef(items);
54
83
  useEffect(() => {
55
84
  itemsRef.current = items;
@@ -60,40 +89,41 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
60
89
  const resetExpandAll = useCallback(() => setDefaultExpanded(null), []);
61
90
  const setActiveLink = useCallback((href) => setActiveHref(href), []);
62
91
  const expandItem = useCallback((href) => {
92
+ const h = normalizeHref(href);
63
93
  setExpandedItems(prev => {
64
- if (prev.has(href))
94
+ if (prev.has(h))
65
95
  return prev;
66
96
  const next = new Set(prev);
67
- next.add(href);
97
+ next.add(h);
68
98
  return next;
69
99
  });
70
100
  }, []);
71
101
  const collapseItem = useCallback((href) => {
102
+ const h = normalizeHref(href);
72
103
  setExpandedItems(prev => {
73
- if (!prev.has(href))
104
+ if (!prev.has(h))
74
105
  return prev;
75
106
  const next = new Set(prev);
76
- next.delete(href);
107
+ next.delete(h);
77
108
  return next;
78
109
  });
79
110
  }, []);
80
- const isExpanded = useCallback((href) => expandedItems.has(href), [expandedItems]);
81
- // Auto-expand: when active link changes, ensure its parent chain is expanded.
82
- // IMPORTANT: guard so we only set state when we actually add something.
111
+ const isExpanded = useCallback((href) => expandedItems.has(normalizeHref(href)), [expandedItems]);
112
+ // Auto-expand: when active link changes, ensure its expandable parent chain is expanded.
83
113
  useEffect(() => {
84
114
  if (!activeHref)
85
115
  return;
86
116
  const currentItems = itemsRef.current;
87
- const path = findParentItem(activeHref, currentItems);
88
- const parents = path.split('.').filter(Boolean);
117
+ const parents = findExpandableParentChain(activeHref, currentItems);
89
118
  if (parents.length === 0)
90
119
  return;
91
120
  setExpandedItems(prev => {
92
121
  let changed = false;
93
122
  const next = new Set(prev);
94
123
  for (const p of parents) {
95
- if (!next.has(p)) {
96
- next.add(p);
124
+ const norm = normalizeHref(p);
125
+ if (!next.has(norm)) {
126
+ next.add(norm);
97
127
  changed = true;
98
128
  }
99
129
  }
@@ -110,8 +140,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
110
140
  })
111
141
  : items;
112
142
  }, [items, activeQuery]);
113
- // Searching should expand all, but do not fight the user forever.
114
- // We just set defaultExpanded=true, and individual components can honor it.
143
+ // Searching should expand all.
115
144
  useEffect(() => {
116
145
  if (activeQuery)
117
146
  triggerExpandAll();
@@ -128,7 +157,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
128
157
  window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, JSON.stringify(value));
129
158
  }
130
159
  catch {
131
- console.error('Failed to persist sidebar collapsed state');
160
+ // ignore persist failures
132
161
  }
133
162
  return;
134
163
  }
@@ -141,9 +170,9 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
141
170
  }
142
171
  }
143
172
  catch {
144
- console.error('Failed to parse sidebar collapsed state from storage');
173
+ // ignore parse failures
145
174
  }
146
- // Nothing stored responsive default (but we do NOT persist this automatic choice)
175
+ // Nothing stored -> responsive default (do NOT persist automatic choice)
147
176
  setSidebarCollapsed(currentBreakpoint === 'small');
148
177
  }, [hasExplicitInitialSidebarCollapsed, initialSidebarCollapsed]);
149
178
  const persistCollapsed = useCallback((collapsed) => {
@@ -153,7 +182,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
153
182
  window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, JSON.stringify(collapsed));
154
183
  }
155
184
  catch {
156
- console.error('Failed to persist sidebar collapsed state');
185
+ // ignore persist failures
157
186
  }
158
187
  }, []);
159
188
  // Only persist user-triggered changes
@@ -163,7 +192,7 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
163
192
  }, [persistCollapsed]);
164
193
  // Resize behavior:
165
194
  // - only apply auto-collapse when breakpoint changes
166
- // - do NOT persist the automatic change (only user actions persist)
195
+ // - do NOT persist automatic changes (only user actions persist)
167
196
  useEffect(() => {
168
197
  if (typeof window === 'undefined')
169
198
  return;