@dbcdk/react-components 0.0.10 → 0.0.13

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 (106) hide show
  1. package/dist/components/accordion/Accordion.d.ts +2 -2
  2. package/dist/components/accordion/Accordion.js +34 -41
  3. package/dist/components/accordion/Accordion.module.css +13 -72
  4. package/dist/components/accordion/components/AccordionRow.d.ts +10 -0
  5. package/dist/components/accordion/components/AccordionRow.js +51 -0
  6. package/dist/components/accordion/components/AccordionRow.module.css +82 -0
  7. package/dist/components/breadcrumbs/Breadcrumbs.module.css +0 -1
  8. package/dist/components/button/Button.module.css +7 -7
  9. package/dist/components/card/Card.d.ts +15 -6
  10. package/dist/components/card/Card.js +39 -13
  11. package/dist/components/card/Card.module.css +22 -28
  12. package/dist/components/card/components/CardMeta.d.ts +15 -0
  13. package/dist/components/card/components/CardMeta.js +20 -0
  14. package/dist/components/card/components/CardMeta.module.css +51 -0
  15. package/dist/components/card-container/CardContainer.js +1 -1
  16. package/dist/components/card-container/CardContainer.module.css +3 -1
  17. package/dist/components/chip/Chip.module.css +7 -2
  18. package/dist/components/circle/Circle.d.ts +2 -1
  19. package/dist/components/circle/Circle.js +2 -2
  20. package/dist/components/circle/Circle.module.css +6 -2
  21. package/dist/components/code-block/CodeBlock.js +1 -1
  22. package/dist/components/code-block/CodeBlock.module.css +30 -17
  23. package/dist/components/copy-button/CopyButton.d.ts +1 -0
  24. package/dist/components/copy-button/CopyButton.js +10 -2
  25. package/dist/components/datetime-picker/DateTimePicker.d.ts +33 -8
  26. package/dist/components/datetime-picker/DateTimePicker.js +119 -78
  27. package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
  28. package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
  29. package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
  30. package/dist/components/filter-field/FilterField.js +16 -11
  31. package/dist/components/filter-field/FilterField.module.css +137 -16
  32. package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
  33. package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
  34. package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
  35. package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
  36. package/dist/components/forms/form-select/FormSelect.js +86 -0
  37. package/dist/components/forms/form-select/FormSelect.module.css +236 -0
  38. package/dist/components/forms/input/Input.d.ts +0 -3
  39. package/dist/components/forms/input/Input.js +1 -4
  40. package/dist/components/forms/input/Input.module.css +8 -7
  41. package/dist/components/forms/input-container/InputContainer.module.css +1 -1
  42. package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
  43. package/dist/components/forms/select/Select.js +55 -16
  44. package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
  45. package/dist/components/hyperlink/Hyperlink.js +35 -11
  46. package/dist/components/hyperlink/Hyperlink.module.css +50 -2
  47. package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
  48. package/dist/components/interval-select/IntervalSelect.js +21 -6
  49. package/dist/components/menu/Menu.d.ts +29 -0
  50. package/dist/components/menu/Menu.js +61 -16
  51. package/dist/components/menu/Menu.module.css +73 -5
  52. package/dist/components/overlay/modal/Modal.module.css +4 -3
  53. package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
  54. package/dist/components/overlay/side-panel/SidePanel.js +18 -1
  55. package/dist/components/overlay/side-panel/SidePanel.module.css +1 -3
  56. package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
  57. package/dist/components/page-layout/PageLayout.d.ts +16 -4
  58. package/dist/components/page-layout/PageLayout.js +57 -28
  59. package/dist/components/page-layout/PageLayout.module.css +153 -33
  60. package/dist/components/popover/Popover.d.ts +17 -4
  61. package/dist/components/popover/Popover.js +147 -65
  62. package/dist/components/popover/Popover.module.css +5 -0
  63. package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
  64. package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
  65. package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
  66. package/dist/components/split-button/SplitButton.d.ts +1 -1
  67. package/dist/components/split-button/SplitButton.js +3 -1
  68. package/dist/components/split-button/SplitButton.module.css +4 -4
  69. package/dist/components/split-pane/SplitPane.d.ts +10 -24
  70. package/dist/components/split-pane/SplitPane.js +83 -54
  71. package/dist/components/split-pane/SplitPane.module.css +11 -6
  72. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
  73. package/dist/components/state-page/StatePage.module.css +1 -1
  74. package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
  75. package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
  76. package/dist/components/table/Table.d.ts +8 -8
  77. package/dist/components/table/Table.js +37 -79
  78. package/dist/components/table/Table.module.css +62 -46
  79. package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +7 -3
  80. package/dist/components/table/TanstackTable.js +84 -0
  81. package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
  82. package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
  83. package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
  84. package/dist/components/table/components/table-settings/TableSettings.js +55 -4
  85. package/dist/components/table/table.utils.d.ts +17 -0
  86. package/dist/components/table/table.utils.js +61 -0
  87. package/dist/components/table/tanstackTable.utils.d.ts +22 -0
  88. package/dist/components/table/tanstackTable.utils.js +104 -0
  89. package/dist/components/tabs/Tabs.d.ts +35 -12
  90. package/dist/components/tabs/Tabs.js +114 -26
  91. package/dist/components/tabs/Tabs.module.css +158 -71
  92. package/dist/hooks/useTableSettings.d.ts +23 -4
  93. package/dist/hooks/useTableSettings.js +64 -17
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +1 -1
  96. package/dist/src/styles/styles.css +38 -23
  97. package/dist/styles/animation.d.ts +5 -0
  98. package/dist/styles/animation.js +5 -0
  99. package/dist/styles/styles.css +38 -23
  100. package/dist/styles/themes/dbc/base.css +136 -0
  101. package/dist/styles/themes/dbc/dark.css +39 -202
  102. package/dist/styles/themes/dbc/light.css +17 -174
  103. package/dist/utils/localStorage.utils.d.ts +19 -0
  104. package/dist/utils/localStorage.utils.js +78 -0
  105. package/package.json +4 -4
  106. package/dist/components/table/tanstack.js +0 -162
@@ -1,43 +1,121 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { ChevronDown, ChevronUp } from 'lucide-react';
4
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
4
5
  import { createPortal } from 'react-dom';
5
- import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
6
6
  import styles from './Popover.module.css';
7
- export const Popover = forwardRef(function Popover({ trigger: Trigger, children, minWidth = '200px', matchTriggerWidth = true, viewportPadding = 8, edgeBuffer = 100, dataCy, }, ref) {
8
- const [pos, setPos] = useState({ top: 0, left: 0, width: 0, visible: false });
7
+ function getFocusable(container) {
8
+ const els = container.querySelectorAll([
9
+ 'a[href]',
10
+ 'button:not([disabled])',
11
+ 'input:not([disabled])',
12
+ 'select:not([disabled])',
13
+ 'textarea:not([disabled])',
14
+ '[tabindex]:not([tabindex="-1"])',
15
+ ].join(','));
16
+ return Array.from(els).filter(el => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'));
17
+ }
18
+ function parseMinWidthPx(minWidth, elForEm) {
19
+ const v = minWidth.trim();
20
+ if (v.endsWith('px')) {
21
+ const n = Number.parseFloat(v);
22
+ return Number.isFinite(n) ? n : 0;
23
+ }
24
+ if (typeof window !== 'undefined' && v.endsWith('rem')) {
25
+ const n = Number.parseFloat(v);
26
+ if (!Number.isFinite(n))
27
+ return 0;
28
+ const rootFont = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16');
29
+ return n * (Number.isFinite(rootFont) ? rootFont : 16);
30
+ }
31
+ if (typeof window !== 'undefined' && v.endsWith('em')) {
32
+ const n = Number.parseFloat(v);
33
+ if (!Number.isFinite(n))
34
+ return 0;
35
+ const font = elForEm ? Number.parseFloat(getComputedStyle(elForEm).fontSize || '16') : 16;
36
+ return n * (Number.isFinite(font) ? font : 16);
37
+ }
38
+ return 0;
39
+ }
40
+ export const Popover = forwardRef(function Popover({ trigger: Trigger, children, open, defaultOpen = false, onOpenChange, contentId, minWidth = '200px', matchTriggerWidth = true, viewportPadding = 8, edgeBuffer = 100, dataCy, autoFocusContent = false, returnFocus = true, }, ref) {
41
+ const internalId = useId();
42
+ const resolvedContentId = contentId !== null && contentId !== void 0 ? contentId : `popover-${internalId}`;
43
+ const isControlled = open !== undefined;
44
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
45
+ const isOpen = isControlled ? !!open : uncontrolledOpen;
46
+ const [pos, setPos] = useState({ top: 0, left: 0 });
47
+ const [positioned, setPositioned] = useState(false);
48
+ const [triggerWidth, setTriggerWidth] = useState(null);
9
49
  const containerRef = useRef(null);
10
50
  const contentRef = useRef(null);
11
- // avoid SSR/hydration mismatch
51
+ const triggerElRef = useRef(null);
52
+ const lastCloseReasonRef = useRef('unknown');
12
53
  const [mounted, setMounted] = useState(false);
13
54
  useEffect(() => setMounted(true), []);
14
- const computeAndSetPosition = useCallback((show) => {
15
- const container = containerRef.current;
55
+ const setOpen = useCallback((next) => {
56
+ if (!isControlled)
57
+ setUncontrolledOpen(next);
58
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(next);
59
+ }, [isControlled, onOpenChange]);
60
+ const openPopover = useCallback(() => {
61
+ setPositioned(false);
62
+ setOpen(true);
63
+ }, [setOpen]);
64
+ const closePopover = useCallback((reason = 'api') => {
65
+ lastCloseReasonRef.current = reason;
66
+ setOpen(false);
67
+ }, [setOpen]);
68
+ const togglePopover = useCallback((e) => {
69
+ triggerElRef.current = e.currentTarget;
70
+ if (isOpen)
71
+ closePopover('trigger');
72
+ else
73
+ openPopover();
74
+ }, [isOpen, closePopover, openPopover]);
75
+ useImperativeHandle(ref, () => ({
76
+ close: () => closePopover('api'),
77
+ open: openPopover,
78
+ isOpen: () => isOpen,
79
+ }), [closePopover, openPopover, isOpen]);
80
+ const computeAndSetPosition = useCallback(() => {
81
+ var _a;
16
82
  const content = contentRef.current;
17
- if (!container || !content)
83
+ if (!content)
84
+ return;
85
+ const triggerEl = (_a = triggerElRef.current) !== null && _a !== void 0 ? _a : containerRef.current;
86
+ if (!triggerEl)
18
87
  return;
19
- const triggerRect = container.getBoundingClientRect();
20
- // Temporarily measure content size by forcing it into the layout.
88
+ const triggerRect = triggerEl.getBoundingClientRect();
89
+ // Only compute a forced width when requested.
90
+ let forcedWidthPx = null;
91
+ if (matchTriggerWidth) {
92
+ const minWidthPx = parseMinWidthPx(minWidth, triggerEl);
93
+ forcedWidthPx = Math.max(triggerRect.width, minWidthPx || 0);
94
+ setTriggerWidth(forcedWidthPx);
95
+ }
96
+ else {
97
+ setTriggerWidth(null);
98
+ }
99
+ // Measure height/width for collision using a temporary sizing that reflects our final sizing:
100
+ const prevHidden = content.hidden;
21
101
  const prevVis = content.style.visibility;
22
102
  const prevDisp = content.style.display;
23
103
  const prevMinWidth = content.style.minWidth;
24
104
  const prevWidth = content.style.width;
25
105
  const prevTop = content.style.top;
26
106
  const prevLeft = content.style.left;
107
+ content.hidden = false;
27
108
  content.style.visibility = 'hidden';
28
109
  content.style.display = 'block';
29
110
  content.style.top = '0px';
30
111
  content.style.left = '0px';
112
+ // Apply minWidth always; apply width only if matchTriggerWidth.
31
113
  content.style.minWidth = minWidth;
32
- content.style.width = 'auto';
33
- const minWidthPx = content.offsetWidth;
34
- const desiredWidthPx = Math.max(matchTriggerWidth ? triggerRect.width : 0, minWidthPx);
35
- // Apply desired width and re-measure final size (height may depend on width).
36
- content.style.minWidth = `${desiredWidthPx}px`;
37
- content.style.width = `${desiredWidthPx}px`;
114
+ content.style.width = forcedWidthPx != null ? `${forcedWidthPx}px` : 'auto';
38
115
  const contentWidth = content.offsetWidth;
39
116
  const contentHeight = content.offsetHeight;
40
- // Restore previous inline styles
117
+ // Restore
118
+ content.hidden = prevHidden;
41
119
  content.style.visibility = prevVis;
42
120
  content.style.display = prevDisp;
43
121
  content.style.minWidth = prevMinWidth;
@@ -58,80 +136,84 @@ export const Popover = forwardRef(function Popover({ trigger: Trigger, children,
58
136
  const placeRightOfLeftEdge = spaceRight >= contentWidth + edgeBuffer;
59
137
  const placeLeftOfRightEdge = spaceLeft >= contentWidth + edgeBuffer;
60
138
  let rawLeft;
61
- if (placeRightOfLeftEdge) {
62
- rawLeft = triggerRect.left; // align left edges
63
- }
64
- else if (placeLeftOfRightEdge) {
65
- rawLeft = triggerRect.right - contentWidth; // align right edges
66
- }
67
- else {
139
+ if (placeRightOfLeftEdge)
140
+ rawLeft = triggerRect.left;
141
+ else if (placeLeftOfRightEdge)
142
+ rawLeft = triggerRect.right - contentWidth;
143
+ else
68
144
  rawLeft = triggerRect.left + (triggerRect.width - contentWidth) / 2;
69
- }
70
145
  const clampedLeft = Math.max(viewportPadding, Math.min(rawLeft, vw - contentWidth - viewportPadding));
71
146
  const clampedTop = Math.max(viewportPadding, Math.min(rawTop, vh - contentHeight - viewportPadding));
72
- setPos({ top: clampedTop, left: clampedLeft, width: desiredWidthPx, visible: show });
147
+ setPos({ top: clampedTop, left: clampedLeft });
148
+ setPositioned(true);
73
149
  }, [edgeBuffer, viewportPadding, minWidth, matchTriggerWidth]);
74
- const openPopover = useCallback((e) => {
75
- if (pos.visible) {
76
- setPos(p => ({ ...p, visible: false }));
77
- return;
78
- }
79
- computeAndSetPosition(true);
80
- e === null || e === void 0 ? void 0 : e.stopPropagation();
81
- }, [pos.visible, computeAndSetPosition]);
82
- const closePopover = useCallback(() => {
83
- setPos(p => ({ ...p, visible: false }));
84
- }, []);
85
- useImperativeHandle(ref, () => ({
86
- close: closePopover,
87
- open: () => computeAndSetPosition(true),
88
- isOpen: () => !!pos.visible,
89
- }), [closePopover, computeAndSetPosition, pos.visible]);
90
- // Recompute position after open to account for content becoming visible / measured.
91
150
  useLayoutEffect(() => {
92
- if (pos.visible)
93
- computeAndSetPosition(true);
151
+ if (!isOpen)
152
+ return;
153
+ computeAndSetPosition();
94
154
  // eslint-disable-next-line react-hooks/exhaustive-deps
95
- }, [pos.visible]);
155
+ }, [isOpen]);
96
156
  useEffect(() => {
97
- if (!pos.visible)
157
+ var _a;
158
+ if (!isOpen)
159
+ return;
160
+ const content = contentRef.current;
161
+ if (!content)
98
162
  return;
99
- const handleClickOutside = (e) => {
100
- if (containerRef.current &&
101
- contentRef.current &&
102
- !containerRef.current.contains(e.target) &&
103
- !contentRef.current.contains(e.target)) {
104
- closePopover();
163
+ if (autoFocusContent) {
164
+ const focusables = getFocusable(content);
165
+ (_a = focusables[0]) === null || _a === void 0 ? void 0 : _a.focus();
166
+ }
167
+ const handlePointerDownCapture = (e) => {
168
+ const container = containerRef.current;
169
+ const contentEl = contentRef.current;
170
+ if (!container || !contentEl)
171
+ return;
172
+ const target = e.target;
173
+ if (!container.contains(target) && !contentEl.contains(target)) {
174
+ closePopover('outside');
105
175
  }
106
176
  };
107
177
  const handleEscape = (e) => {
108
178
  if (e.key === 'Escape')
109
- closePopover();
179
+ closePopover('escape');
110
180
  };
111
- const handleReposition = () => computeAndSetPosition(true);
112
- document.addEventListener('click', handleClickOutside);
181
+ const handleReposition = () => computeAndSetPosition();
182
+ document.addEventListener('pointerdown', handlePointerDownCapture, true);
113
183
  document.addEventListener('keydown', handleEscape, true);
114
184
  window.addEventListener('resize', handleReposition);
115
185
  window.addEventListener('scroll', handleReposition, true);
116
186
  return () => {
117
- document.removeEventListener('click', handleClickOutside);
187
+ document.removeEventListener('pointerdown', handlePointerDownCapture, true);
118
188
  document.removeEventListener('keydown', handleEscape, true);
119
189
  window.removeEventListener('resize', handleReposition);
120
190
  window.removeEventListener('scroll', handleReposition, true);
121
191
  };
122
- }, [pos.visible, closePopover, computeAndSetPosition]);
123
- return (_jsxs("div", { className: styles.container, ref: containerRef, children: [Trigger(openPopover, pos.visible ? _jsx(ChevronUp, { size: 20 }) : _jsx(ChevronDown, { size: 20 })), mounted &&
124
- createPortal(_jsx("div", { ref: contentRef, className: styles.content, style: {
192
+ }, [isOpen, closePopover, computeAndSetPosition, autoFocusContent]);
193
+ useEffect(() => {
194
+ var _a, _b;
195
+ if (isOpen)
196
+ return;
197
+ if (!returnFocus)
198
+ return;
199
+ if (lastCloseReasonRef.current === 'outside')
200
+ return;
201
+ (_b = (_a = triggerElRef.current) === null || _a === void 0 ? void 0 : _a.focus) === null || _b === void 0 ? void 0 : _b.call(_a);
202
+ }, [isOpen, returnFocus]);
203
+ const icon = isOpen ? _jsx(ChevronUp, { size: 20 }) : _jsx(ChevronDown, { size: 20 });
204
+ return (_jsxs("div", { className: styles.container, ref: containerRef, children: [Trigger(togglePopover, icon, isOpen), mounted &&
205
+ isOpen &&
206
+ createPortal(_jsx("div", { id: resolvedContentId, ref: contentRef, className: styles.content, style: {
125
207
  top: pos.top,
126
208
  left: pos.left,
127
- visibility: pos.visible ? 'visible' : 'hidden',
128
- minWidth: pos.width ? `${pos.width}px` : minWidth,
129
- width: pos.width ? `${pos.width}px` : undefined,
209
+ // Content-driven sizing by default.
210
+ minWidth,
211
+ width: triggerWidth != null ? `${triggerWidth}px` : undefined,
130
212
  maxWidth: `calc(100vw - ${viewportPadding * 2}px)`,
131
213
  maxHeight: `clamp(100px, calc(100vh - ${viewportPadding * 2}px), 400px)`,
132
- overflow: 'auto',
133
- }, role: "dialog", "aria-hidden": !pos.visible, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'popover-content', children: typeof children === 'function'
134
- ? children(closePopover)
214
+ visibility: positioned ? undefined : 'hidden',
215
+ }, "data-cy": dataCy !== null && dataCy !== void 0 ? dataCy : 'popover-content', children: typeof children === 'function'
216
+ ? children(() => closePopover('api'))
135
217
  : children }), document.body)] }));
136
218
  });
137
219
  Popover.displayName = 'Popover';
@@ -1,5 +1,6 @@
1
1
  .container {
2
2
  position: relative;
3
+ display: inline-block;
3
4
  }
4
5
 
5
6
  .content {
@@ -13,6 +14,10 @@
13
14
  box-shadow: var(--shadow-md);
14
15
  }
15
16
 
17
+ .content[hidden] {
18
+ display: none;
19
+ }
20
+
16
21
  .content svg {
17
22
  height: 20px;
18
23
  width: 20px;
@@ -1,52 +1,57 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { ChevronDown } from 'lucide-react';
4
- import { useCallback, useEffect, useState } from 'react';
4
+ import { useCallback, useEffect, useMemo, useState } from 'react';
5
5
  import styles from './ExpandableSidebarItem.module.css';
6
6
  import { Button } from '../../../button/Button';
7
7
  import { useSidebar } from '../../providers/SidebarProvider';
8
+ import { ExpandableSidebarItem as ExpandableChild } from '../expandable-sidebar-item/ExpandableSidebarItem';
8
9
  import { SidebarItemContent } from '../sidebar-item-content/SidebarItemContent';
9
10
  import { SidebarItem } from '../SidebarItem';
10
- import { ExpandableSidebarItem as ExpandableChild } from '../expandable-sidebar-item/ExpandableSidebarItem';
11
11
  const isGroup = (item) => item.type === 'group';
12
12
  const isExpandable = (item) => item.type === 'expandable';
13
13
  export function ExpandableSidebarItem({ items, label, icon, component: Component, href, }) {
14
- const { defaultExpanded, resetExpandAll, isSidebarCollapsed, handleSidebarCollapseChange, expandedItems, } = useSidebar();
15
- const [expanded, setExpanded] = useState(false);
14
+ const { defaultExpanded, resetExpandAll, isSidebarCollapsed, handleSidebarCollapseChange, expandItem, collapseItem, isExpanded, } = useSidebar();
15
+ // Local-only state for animation coordination
16
16
  const [closing, setClosing] = useState(false);
17
17
  const [ready, setReady] = useState(false);
18
18
  useEffect(() => {
19
19
  setReady(true);
20
20
  }, []);
21
- useEffect(() => {
22
- if (expandedItems.has(href)) {
23
- setExpanded(true);
24
- }
25
- }, [expandedItems, href]);
21
+ // Single source of truth: expanded comes from provider state
22
+ const expanded = useMemo(() => isExpanded(href), [href, isExpanded]);
23
+ // Expand-all behavior (e.g. search)
26
24
  useEffect(() => {
27
25
  if (defaultExpanded === null)
28
26
  return;
29
- setExpanded(defaultExpanded);
30
- }, [defaultExpanded]);
27
+ if (defaultExpanded)
28
+ expandItem(href);
29
+ else
30
+ collapseItem(href);
31
+ }, [defaultExpanded, expandItem, collapseItem, href]);
31
32
  const handleAnimationEnd = useCallback(() => {
32
- if (ready) {
33
- setExpanded(!closing);
33
+ if (!ready)
34
+ return;
35
+ if (closing) {
36
+ // After collapse animation, commit closed state
37
+ collapseItem(href);
34
38
  setClosing(false);
35
39
  }
36
- }, [closing, ready]);
40
+ }, [closing, ready, collapseItem, href]);
37
41
  const toggleAccordion = useCallback((e, onlyExpand = false) => {
38
42
  e === null || e === void 0 ? void 0 : e.preventDefault();
39
43
  e === null || e === void 0 ? void 0 : e.stopPropagation();
40
44
  resetExpandAll();
41
- handleSidebarCollapseChange === null || handleSidebarCollapseChange === void 0 ? void 0 : handleSidebarCollapseChange(false);
45
+ handleSidebarCollapseChange(false);
42
46
  if (!expanded) {
43
- setExpanded(true);
47
+ expandItem(href);
44
48
  return;
45
49
  }
46
50
  if (!isSidebarCollapsed && !onlyExpand) {
51
+ // Start collapse animation; state commit happens onAnimationEnd
47
52
  setClosing(true);
48
53
  }
49
- }, [expanded, handleSidebarCollapseChange, isSidebarCollapsed, resetExpandAll]);
54
+ }, [expanded, expandItem, href, handleSidebarCollapseChange, isSidebarCollapsed, resetExpandAll]);
50
55
  const renderNavItem = (item, key) => {
51
56
  var _a, _b;
52
57
  if (isGroup(item)) {
@@ -55,7 +60,6 @@ export function ExpandableSidebarItem({ items, label, icon, component: Component
55
60
  if (isExpandable(item)) {
56
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));
57
62
  }
58
- // Default item (type 'item' or undefined)
59
63
  return (_jsx(SidebarItem, { component: item.component, label: item.label, icon: item.icon, href: item.href }, key));
60
64
  };
61
65
  return (_jsxs("div", { className: `${styles.container} ${expanded ? styles.expanded : ''}`, children: [_jsx(Component, { onClick: () => toggleAccordion(undefined, true), children: _jsx(SidebarItemContent, { 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}`)) }))] }));
@@ -3,7 +3,7 @@ import * as React from 'react';
3
3
  import { NavBarItem } from '../../../components/nav-bar/NavBar';
4
4
  export type SidebarContextValue = {
5
5
  defaultExpanded: boolean | null;
6
- expandedItems: Set<string | undefined>;
6
+ expandedItems: Set<string>;
7
7
  resetExpandAll: () => void;
8
8
  activeQuery: string;
9
9
  areItemsCollapsed: boolean;
@@ -13,6 +13,9 @@ export type SidebarContextValue = {
13
13
  filteredItems?: NavBarItem[];
14
14
  activeLink?: string;
15
15
  setActiveLink: (href: string) => void;
16
+ expandItem: (href: string) => void;
17
+ collapseItem: (href: string) => void;
18
+ isExpanded: (href: string) => boolean;
16
19
  isSidebarCollapsed: boolean;
17
20
  handleSidebarCollapseChange: (collapsed: boolean) => void;
18
21
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { nestedFiltering } from '../../../utils/arrays/nested-filtering';
5
5
  const hasChildren = (item) => Array.isArray(item.children) && item.children.length > 0;
6
6
  const hasHref = (item) => typeof item.href === 'string' && item.href.length > 0;
@@ -32,6 +32,9 @@ const SidebarContext = createContext({
32
32
  resetExpandAll: () => { },
33
33
  activeLink: '',
34
34
  setActiveLink: () => { },
35
+ expandItem: () => { },
36
+ collapseItem: () => { },
37
+ isExpanded: () => false,
35
38
  isSidebarCollapsed: false,
36
39
  handleSidebarCollapseChange: () => { },
37
40
  });
@@ -43,24 +46,60 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
43
46
  const [activeQuery, setActiveQuery] = useState('');
44
47
  const [areItemsCollapsed, setItemsCollapsed] = useState(initialCollapsed);
45
48
  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)
46
51
  const [expandedItems, setExpandedItems] = useState(new Set());
52
+ // Track items in a ref to avoid effect loops if parent recreates the items array every render
53
+ const itemsRef = useRef(items);
54
+ useEffect(() => {
55
+ itemsRef.current = items;
56
+ }, [items]);
47
57
  const [isSidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed !== null && initialSidebarCollapsed !== void 0 ? initialSidebarCollapsed : false);
48
58
  const hasExplicitInitialSidebarCollapsed = initialSidebarCollapsed !== undefined;
49
59
  const triggerExpandAll = useCallback(() => setDefaultExpanded(true), []);
50
60
  const resetExpandAll = useCallback(() => setDefaultExpanded(null), []);
51
61
  const setActiveLink = useCallback((href) => setActiveHref(href), []);
62
+ const expandItem = useCallback((href) => {
63
+ setExpandedItems(prev => {
64
+ if (prev.has(href))
65
+ return prev;
66
+ const next = new Set(prev);
67
+ next.add(href);
68
+ return next;
69
+ });
70
+ }, []);
71
+ const collapseItem = useCallback((href) => {
72
+ setExpandedItems(prev => {
73
+ if (!prev.has(href))
74
+ return prev;
75
+ const next = new Set(prev);
76
+ next.delete(href);
77
+ return next;
78
+ });
79
+ }, []);
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.
52
83
  useEffect(() => {
53
84
  if (!activeHref)
54
85
  return;
55
- const path = findParentItem(activeHref, items);
86
+ const currentItems = itemsRef.current;
87
+ const path = findParentItem(activeHref, currentItems);
56
88
  const parents = path.split('.').filter(Boolean);
89
+ if (parents.length === 0)
90
+ return;
57
91
  setExpandedItems(prev => {
92
+ let changed = false;
58
93
  const next = new Set(prev);
59
- for (const p of parents)
60
- next.add(p);
61
- return next;
94
+ for (const p of parents) {
95
+ if (!next.has(p)) {
96
+ next.add(p);
97
+ changed = true;
98
+ }
99
+ }
100
+ return changed ? next : prev;
62
101
  });
63
- }, [activeHref, items]);
102
+ }, [activeHref]);
64
103
  const filteredItems = useMemo(() => {
65
104
  return activeQuery
66
105
  ? nestedFiltering(items, {
@@ -71,11 +110,13 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
71
110
  })
72
111
  : items;
73
112
  }, [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.
74
115
  useEffect(() => {
75
- if (activeQuery) {
116
+ if (activeQuery)
76
117
  triggerExpandAll();
77
- }
78
118
  }, [activeQuery, triggerExpandAll]);
119
+ // Initial collapsed state: explicit prop > localStorage > responsive default.
79
120
  useEffect(() => {
80
121
  if (typeof window === 'undefined')
81
122
  return;
@@ -102,9 +143,8 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
102
143
  catch {
103
144
  console.error('Failed to parse sidebar collapsed state from storage');
104
145
  }
105
- // No explicit prop and nothing stored use responsive default
106
- const defaultCollapsed = currentBreakpoint === 'small';
107
- setSidebarCollapsed(defaultCollapsed);
146
+ // Nothing stored responsive default (but we do NOT persist this automatic choice)
147
+ setSidebarCollapsed(currentBreakpoint === 'small');
108
148
  }, [hasExplicitInitialSidebarCollapsed, initialSidebarCollapsed]);
109
149
  const persistCollapsed = useCallback((collapsed) => {
110
150
  if (typeof window === 'undefined')
@@ -116,26 +156,28 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
116
156
  console.error('Failed to persist sidebar collapsed state');
117
157
  }
118
158
  }, []);
159
+ // Only persist user-triggered changes
119
160
  const handleSidebarCollapseChange = useCallback((collapsed) => {
120
161
  setSidebarCollapsed(collapsed);
121
162
  persistCollapsed(collapsed);
122
163
  }, [persistCollapsed]);
123
164
  // Resize behavior:
124
- // - Track current breakpoint.
125
- // - Only when the breakpoint actually changes (small <-> large)
126
- // do we auto-apply the "responsive default" for that breakpoint.
165
+ // - only apply auto-collapse when breakpoint changes
166
+ // - do NOT persist the automatic change (only user actions persist)
127
167
  useEffect(() => {
128
168
  if (typeof window === 'undefined')
129
169
  return;
170
+ let lastBreakpoint = getBreakpoint(window.innerWidth);
130
171
  const onResize = () => {
131
172
  const nextBreakpoint = getBreakpoint(window.innerWidth);
132
- const autoCollapsed = nextBreakpoint === 'small';
133
- setSidebarCollapsed(autoCollapsed);
134
- persistCollapsed(autoCollapsed);
173
+ if (nextBreakpoint === lastBreakpoint)
174
+ return;
175
+ lastBreakpoint = nextBreakpoint;
176
+ setSidebarCollapsed(nextBreakpoint === 'small');
135
177
  };
136
178
  window.addEventListener('resize', onResize);
137
179
  return () => window.removeEventListener('resize', onResize);
138
- }, [persistCollapsed]);
180
+ }, []);
139
181
  const value = useMemo(() => ({
140
182
  defaultExpanded,
141
183
  expandedItems,
@@ -148,6 +190,9 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
148
190
  setItemsCollapsed,
149
191
  activeLink: activeHref,
150
192
  setActiveLink,
193
+ expandItem,
194
+ collapseItem,
195
+ isExpanded,
151
196
  isSidebarCollapsed,
152
197
  handleSidebarCollapseChange,
153
198
  }), [
@@ -162,6 +207,9 @@ export function SidebarProvider({ children, items, initialCollapsed = false, ini
162
207
  setItemsCollapsed,
163
208
  activeHref,
164
209
  setActiveLink,
210
+ expandItem,
211
+ collapseItem,
212
+ isExpanded,
165
213
  isSidebarCollapsed,
166
214
  handleSidebarCollapseChange,
167
215
  ]);
@@ -2,7 +2,7 @@ import type { ComponentProps, ReactNode } from 'react';
2
2
  import { Button } from '../button/Button';
3
3
  interface SplitButtonItem {
4
4
  label?: string;
5
- onClick: () => void;
5
+ onClick: (close?: () => void) => void;
6
6
  icon?: ReactNode;
7
7
  active?: boolean;
8
8
  }
@@ -4,5 +4,7 @@ import { Button } from '../button/Button';
4
4
  import { Menu } from '../menu/Menu';
5
5
  import { Popover } from '../popover/Popover';
6
6
  export function SplitButton({ children, options, onClick, icon, ...rest }) {
7
- return (_jsxs("div", { className: styles.container, style: { display: 'inline-flex', alignItems: 'center' }, children: [_jsxs(Button, { ...rest, onClick: onClick, children: [icon, children] }), _jsx(Popover, { trigger: (handleClick, icon) => (_jsx("span", { className: styles.triggerContainer, children: _jsx(Button, { ...rest, onClick: handleClick, children: icon }) })), children: _jsx(Menu, { children: options.map(option => (_jsx(Menu.Item, { active: option.active, children: _jsxs("button", { onClick: option.onClick, children: [option.icon, option.label] }) }, option.label))) }) })] }));
7
+ return (_jsxs("div", { className: styles.container, style: { display: 'inline-flex', alignItems: 'center' }, children: [_jsxs(Button, { ...rest, onClick: onClick, children: [icon, children] }), _jsx(Popover, { trigger: (handleClick, icon) => (_jsx("span", { className: styles.triggerContainer, children: _jsx(Button, { ...rest, onClick: handleClick, children: icon }) })), children: close => (_jsx(Menu, { children: options.map(option => (_jsx(Menu.Item, { active: option.active, children: _jsxs("button", { onClick: e => {
8
+ option.onClick(close);
9
+ }, children: [option.icon, option.label] }) }, option.label))) })) })] }));
8
10
  }
@@ -3,15 +3,15 @@
3
3
  }
4
4
 
5
5
  .container > button:first-child {
6
- border-start-start-radius: var(--border-radius-md);
7
- border-end-start-radius: var(--border-radius-md);
6
+ border-start-start-radius: var(--border-radius-default);
7
+ border-end-start-radius: var(--border-radius-default);
8
8
  border-start-end-radius: 0;
9
9
  border-end-end-radius: 0;
10
10
  }
11
11
 
12
12
  .triggerContainer button {
13
- border-start-end-radius: var(--border-radius-md);
14
- border-end-end-radius: var(--border-radius-md);
13
+ border-start-end-radius: var(--border-radius-default);
14
+ border-end-end-radius: var(--border-radius-default);
15
15
  border-start-start-radius: 0;
16
16
  border-end-start-radius: 0;
17
17
  padding-block: 0;
@@ -1,34 +1,20 @@
1
- import React from 'react';
2
- import { SplitDirection } from './provider/SplitPaneContext';
3
- interface SplitPaneProps {
4
- children: React.ReactNode;
1
+ import type { JSX, ReactNode } from 'react';
2
+ import type { SplitDirection } from './provider/SplitPaneContext';
3
+ export interface SplitPaneProps {
4
+ children: ReactNode;
5
5
  initialPrimarySize?: number;
6
6
  minPrimarySize?: number;
7
7
  minSecondarySize?: number;
8
8
  direction?: SplitDirection;
9
9
  showDivider?: 'hover' | 'always' | 'never';
10
- /**
11
- * Gutter size (px). This is the space between panes and contains the resizer hit area.
12
- * Example: 8 => 4px breathing room on each side if the divider line is centered.
13
- */
14
10
  gutterSize?: number;
15
- /**
16
- * If provided, primary size is persisted per key in localStorage.
17
- * Only SplitPanes sharing the same key will share size.
18
- */
19
11
  storageKey?: string;
20
12
  }
21
- export declare function SplitPane({ children, initialPrimarySize, minPrimarySize, minSecondarySize, direction, showDivider, gutterSize, storageKey, }: SplitPaneProps): React.ReactNode;
22
- /**
23
- * IMPORTANT:
24
- * This component now renders ONLY the primary content (scrollable).
25
- * The resizer lives in a dedicated <SplitPaneGutter /> so it never overlaps scrollbars.
26
- */
13
+ export declare function SplitPane({ children, initialPrimarySize, minPrimarySize, minSecondarySize, direction, showDivider, gutterSize, storageKey, }: SplitPaneProps): JSX.Element;
27
14
  export declare function SplitPanePrimary({ children }: {
28
- children: React.ReactNode;
29
- }): React.ReactNode;
30
- export declare function SplitPaneGutter(): React.ReactNode;
15
+ children: ReactNode;
16
+ }): JSX.Element;
31
17
  export declare function SplitPaneSecondary({ children }: {
32
- children: React.ReactNode;
33
- }): React.ReactNode;
34
- export {};
18
+ children: ReactNode;
19
+ }): JSX.Element;
20
+ export declare function SplitPaneGutter(): JSX.Element;