@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.
- package/dist/components/accordion/Accordion.d.ts +2 -2
- package/dist/components/accordion/Accordion.js +34 -41
- package/dist/components/accordion/Accordion.module.css +13 -72
- package/dist/components/accordion/components/AccordionRow.d.ts +10 -0
- package/dist/components/accordion/components/AccordionRow.js +51 -0
- package/dist/components/accordion/components/AccordionRow.module.css +82 -0
- package/dist/components/breadcrumbs/Breadcrumbs.module.css +0 -1
- package/dist/components/button/Button.module.css +7 -7
- package/dist/components/card/Card.d.ts +15 -6
- package/dist/components/card/Card.js +39 -13
- package/dist/components/card/Card.module.css +22 -28
- package/dist/components/card/components/CardMeta.d.ts +15 -0
- package/dist/components/card/components/CardMeta.js +20 -0
- package/dist/components/card/components/CardMeta.module.css +51 -0
- package/dist/components/card-container/CardContainer.js +1 -1
- package/dist/components/card-container/CardContainer.module.css +3 -1
- package/dist/components/chip/Chip.module.css +7 -2
- package/dist/components/circle/Circle.d.ts +2 -1
- package/dist/components/circle/Circle.js +2 -2
- package/dist/components/circle/Circle.module.css +6 -2
- package/dist/components/code-block/CodeBlock.js +1 -1
- package/dist/components/code-block/CodeBlock.module.css +30 -17
- package/dist/components/copy-button/CopyButton.d.ts +1 -0
- package/dist/components/copy-button/CopyButton.js +10 -2
- package/dist/components/datetime-picker/DateTimePicker.d.ts +33 -8
- package/dist/components/datetime-picker/DateTimePicker.js +119 -78
- package/dist/components/datetime-picker/DateTimePicker.module.css +2 -0
- package/dist/components/datetime-picker/dateTimeHelpers.d.ts +15 -3
- package/dist/components/datetime-picker/dateTimeHelpers.js +137 -23
- package/dist/components/filter-field/FilterField.js +16 -11
- package/dist/components/filter-field/FilterField.module.css +137 -16
- package/dist/components/forms/checkbox/Checkbox.d.ts +2 -2
- package/dist/components/forms/checkbox-group/CheckboxGroup.js +1 -1
- package/dist/components/forms/checkbox-group/CheckboxGroup.module.css +1 -1
- package/dist/components/forms/form-select/FormSelect.d.ts +35 -0
- package/dist/components/forms/form-select/FormSelect.js +86 -0
- package/dist/components/forms/form-select/FormSelect.module.css +236 -0
- package/dist/components/forms/input/Input.d.ts +0 -3
- package/dist/components/forms/input/Input.js +1 -4
- package/dist/components/forms/input/Input.module.css +8 -7
- package/dist/components/forms/input-container/InputContainer.module.css +1 -1
- package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
- package/dist/components/forms/select/Select.js +55 -16
- package/dist/components/hyperlink/Hyperlink.d.ts +19 -7
- package/dist/components/hyperlink/Hyperlink.js +35 -11
- package/dist/components/hyperlink/Hyperlink.module.css +50 -2
- package/dist/components/interval-select/IntervalSelect.d.ts +9 -2
- package/dist/components/interval-select/IntervalSelect.js +21 -6
- package/dist/components/menu/Menu.d.ts +29 -0
- package/dist/components/menu/Menu.js +61 -16
- package/dist/components/menu/Menu.module.css +73 -5
- package/dist/components/overlay/modal/Modal.module.css +4 -3
- package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
- package/dist/components/overlay/side-panel/SidePanel.js +18 -1
- package/dist/components/overlay/side-panel/SidePanel.module.css +1 -3
- package/dist/components/overlay/tooltip/useTooltipTrigger.js +4 -2
- package/dist/components/page-layout/PageLayout.d.ts +16 -4
- package/dist/components/page-layout/PageLayout.js +57 -28
- package/dist/components/page-layout/PageLayout.module.css +153 -33
- package/dist/components/popover/Popover.d.ts +17 -4
- package/dist/components/popover/Popover.js +147 -65
- package/dist/components/popover/Popover.module.css +5 -0
- package/dist/components/sidebar/components/expandable-sidebar-item/ExpandableSidebarItem.js +22 -18
- package/dist/components/sidebar/providers/SidebarProvider.d.ts +4 -1
- package/dist/components/sidebar/providers/SidebarProvider.js +66 -18
- package/dist/components/split-button/SplitButton.d.ts +1 -1
- package/dist/components/split-button/SplitButton.js +3 -1
- package/dist/components/split-button/SplitButton.module.css +4 -4
- package/dist/components/split-pane/SplitPane.d.ts +10 -24
- package/dist/components/split-pane/SplitPane.js +83 -54
- package/dist/components/split-pane/SplitPane.module.css +11 -6
- package/dist/components/split-pane/provider/SplitPaneContext.js +5 -11
- package/dist/components/state-page/StatePage.module.css +1 -1
- package/dist/components/sticky-footer-layout/StickyFooterLayout.d.ts +3 -8
- package/dist/components/sticky-footer-layout/StickyFooterLayout.js +57 -20
- package/dist/components/table/Table.d.ts +8 -8
- package/dist/components/table/Table.js +37 -79
- package/dist/components/table/Table.module.css +62 -46
- package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +7 -3
- package/dist/components/table/TanstackTable.js +84 -0
- package/dist/components/table/components/column-resizer/ColumnResizer.js +1 -1
- package/dist/components/table/components/column-resizer/ColumnResizer.module.css +17 -7
- package/dist/components/table/components/table-settings/TableSettings.d.ts +13 -3
- package/dist/components/table/components/table-settings/TableSettings.js +55 -4
- package/dist/components/table/table.utils.d.ts +17 -0
- package/dist/components/table/table.utils.js +61 -0
- package/dist/components/table/tanstackTable.utils.d.ts +22 -0
- package/dist/components/table/tanstackTable.utils.js +104 -0
- package/dist/components/tabs/Tabs.d.ts +35 -12
- package/dist/components/tabs/Tabs.js +114 -26
- package/dist/components/tabs/Tabs.module.css +158 -71
- package/dist/hooks/useTableSettings.d.ts +23 -4
- package/dist/hooks/useTableSettings.js +64 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/src/styles/styles.css +38 -23
- package/dist/styles/animation.d.ts +5 -0
- package/dist/styles/animation.js +5 -0
- package/dist/styles/styles.css +38 -23
- package/dist/styles/themes/dbc/base.css +136 -0
- package/dist/styles/themes/dbc/dark.css +39 -202
- package/dist/styles/themes/dbc/light.css +17 -174
- package/dist/utils/localStorage.utils.d.ts +19 -0
- package/dist/utils/localStorage.utils.js +78 -0
- package/package.json +4 -4
- 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
|
-
|
|
8
|
-
const
|
|
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
|
-
|
|
51
|
+
const triggerElRef = useRef(null);
|
|
52
|
+
const lastCloseReasonRef = useRef('unknown');
|
|
12
53
|
const [mounted, setMounted] = useState(false);
|
|
13
54
|
useEffect(() => setMounted(true), []);
|
|
14
|
-
const
|
|
15
|
-
|
|
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 (!
|
|
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 =
|
|
20
|
-
//
|
|
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
|
|
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;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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 (
|
|
93
|
-
|
|
151
|
+
if (!isOpen)
|
|
152
|
+
return;
|
|
153
|
+
computeAndSetPosition();
|
|
94
154
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
-
}, [
|
|
155
|
+
}, [isOpen]);
|
|
96
156
|
useEffect(() => {
|
|
97
|
-
|
|
157
|
+
var _a;
|
|
158
|
+
if (!isOpen)
|
|
159
|
+
return;
|
|
160
|
+
const content = contentRef.current;
|
|
161
|
+
if (!content)
|
|
98
162
|
return;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
112
|
-
document.addEventListener('
|
|
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('
|
|
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
|
-
}, [
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
minWidth
|
|
129
|
-
width:
|
|
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
|
-
|
|
133
|
-
},
|
|
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,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,
|
|
15
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
45
|
+
handleSidebarCollapseChange(false);
|
|
42
46
|
if (!expanded) {
|
|
43
|
-
|
|
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
|
|
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
|
|
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.
|
|
61
|
-
|
|
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
|
|
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
|
-
//
|
|
106
|
-
|
|
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
|
-
// -
|
|
125
|
-
// -
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
}, [
|
|
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
|
]);
|
|
@@ -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:
|
|
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-
|
|
7
|
-
border-end-start-radius: var(--border-radius-
|
|
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-
|
|
14
|
-
border-end-end-radius: var(--border-radius-
|
|
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
|
|
2
|
-
import { SplitDirection } from './provider/SplitPaneContext';
|
|
3
|
-
interface SplitPaneProps {
|
|
4
|
-
children:
|
|
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):
|
|
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:
|
|
29
|
-
}):
|
|
30
|
-
export declare function SplitPaneGutter(): React.ReactNode;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}): JSX.Element;
|
|
31
17
|
export declare function SplitPaneSecondary({ children }: {
|
|
32
|
-
children:
|
|
33
|
-
}):
|
|
34
|
-
export
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}): JSX.Element;
|
|
20
|
+
export declare function SplitPaneGutter(): JSX.Element;
|