@dbcdk/react-components 0.0.12 → 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 +9 -18
- package/dist/components/card/Card.js +34 -23
- package/dist/components/card/Card.module.css +22 -87
- 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/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.module.css +5 -5
- 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 +0 -3
- package/dist/components/forms/input/Input.module.css +7 -7
- package/dist/components/forms/radio-buttons/RadioButtons.module.css +1 -0
- package/dist/components/forms/select/Select.js +55 -16
- 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 +11 -14
- package/dist/components/menu/Menu.js +18 -33
- package/dist/components/menu/Menu.module.css +2 -2
- package/dist/components/overlay/modal/Modal.module.css +2 -1
- package/dist/components/overlay/modal/provider/ModalProvider.js +1 -3
- package/dist/components/overlay/side-panel/SidePanel.js +1 -1
- package/dist/components/overlay/side-panel/SidePanel.module.css +1 -1
- 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/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/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 +3 -8
- package/dist/components/table/Table.js +37 -76
- package/dist/components/table/Table.module.css +45 -42
- package/dist/components/table/{tanstack.d.ts → TanstackTable.d.ts} +5 -12
- 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/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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/src/styles/styles.css +0 -1
- package/dist/styles/styles.css +0 -1
- 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/package.json +4 -4
- package/dist/components/table/tanstack.js +0 -214
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
interface PopoverProps {
|
|
3
|
-
trigger: (
|
|
4
|
-
children: ((close
|
|
2
|
+
export interface PopoverProps {
|
|
3
|
+
trigger: (toggle: (e: React.MouseEvent<HTMLElement> | React.FocusEvent<HTMLElement>) => void, icon: React.ReactNode, open?: boolean) => React.ReactNode;
|
|
4
|
+
children: ((close: () => void) => React.ReactNode) | React.ReactNode;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
defaultOpen?: boolean;
|
|
7
|
+
onOpenChange?: (open: boolean) => void;
|
|
8
|
+
contentId?: string;
|
|
9
|
+
/**
|
|
10
|
+
* CSS length, recommended "NNpx" for predictability.
|
|
11
|
+
* Used as a minimum, not as a forced width unless matchTriggerWidth=true.
|
|
12
|
+
*/
|
|
5
13
|
minWidth?: string;
|
|
14
|
+
/**
|
|
15
|
+
* If true, force the overlay width to at least the trigger width.
|
|
16
|
+
* If false, overlay width is content-driven (calendar-friendly).
|
|
17
|
+
*/
|
|
6
18
|
matchTriggerWidth?: boolean;
|
|
7
19
|
viewportPadding?: number;
|
|
8
20
|
edgeBuffer?: number;
|
|
9
21
|
dataCy?: string;
|
|
22
|
+
autoFocusContent?: boolean;
|
|
23
|
+
returnFocus?: boolean;
|
|
10
24
|
}
|
|
11
25
|
export interface PopoverHandle {
|
|
12
26
|
close: () => void;
|
|
@@ -14,4 +28,3 @@ export interface PopoverHandle {
|
|
|
14
28
|
isOpen: () => boolean;
|
|
15
29
|
}
|
|
16
30
|
export declare const Popover: React.ForwardRefExoticComponent<PopoverProps & React.RefAttributes<PopoverHandle>>;
|
|
17
|
-
export {};
|
|
@@ -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, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
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,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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { useCallback, useMemo, useRef } from 'react';
|
|
4
4
|
import { SplitPaneProvider, useSplitPaneContext } from './provider/SplitPaneContext';
|
|
5
5
|
import styles from './SplitPane.module.css';
|
|
6
6
|
function clamp(n, min, max) {
|
|
@@ -11,68 +11,97 @@ export function SplitPane({ children, initialPrimarySize = 300, minPrimarySize =
|
|
|
11
11
|
}
|
|
12
12
|
function SplitPaneContainer({ children, showDivider, gutterSize, }) {
|
|
13
13
|
const { direction, primarySize, containerRef } = useSplitPaneContext();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const style = useMemo(() => ({
|
|
15
|
+
'--split-pane-primary-size': `${primarySize}px`,
|
|
16
|
+
'--split-pane-gutter': `${gutterSize}px`,
|
|
17
|
+
}), [primarySize, gutterSize]);
|
|
18
|
+
return (_jsx("div", { ref: containerRef, className: styles.container, "data-direction": direction, "data-divider": showDivider, style: style, children: children }));
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
* IMPORTANT:
|
|
21
|
-
* This component now renders ONLY the primary content (scrollable).
|
|
22
|
-
* The resizer lives in a dedicated <SplitPaneGutter /> so it never overlaps scrollbars.
|
|
23
|
-
*/
|
|
24
20
|
export function SplitPanePrimary({ children }) {
|
|
25
21
|
return _jsx("div", { className: styles.primary, children: children });
|
|
26
22
|
}
|
|
23
|
+
export function SplitPaneSecondary({ children }) {
|
|
24
|
+
return _jsx("div", { className: styles.secondary, children: children });
|
|
25
|
+
}
|
|
27
26
|
export function SplitPaneGutter() {
|
|
28
27
|
const { direction, primarySize, setPrimarySize, minPrimarySize, minSecondarySize, containerRef } = useSplitPaneContext();
|
|
29
|
-
const
|
|
28
|
+
const draggingRef = useRef(false);
|
|
29
|
+
const pointerIdRef = useRef(null);
|
|
30
30
|
const startPosRef = useRef(0);
|
|
31
31
|
const startSizeRef = useRef(primarySize);
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const maxPrimaryRef = useRef(Infinity);
|
|
33
|
+
const getClientPos = useCallback((e) => (direction === 'horizontal' ? e.clientX : e.clientY), [direction]);
|
|
34
|
+
const computeClamp = useCallback(() => {
|
|
35
|
+
const el = containerRef.current;
|
|
36
|
+
if (!el)
|
|
37
|
+
return { maxPrimary: Infinity, total: 0 };
|
|
38
|
+
const rect = el.getBoundingClientRect();
|
|
39
|
+
const total = direction === 'horizontal' ? rect.width : rect.height;
|
|
40
|
+
const maxPrimary = Math.max(minPrimarySize, total - minSecondarySize);
|
|
41
|
+
return { maxPrimary, total };
|
|
42
|
+
}, [containerRef, direction, minPrimarySize, minSecondarySize]);
|
|
43
|
+
const onPointerDown = useCallback((e) => {
|
|
44
|
+
const el = containerRef.current;
|
|
45
|
+
if (!el)
|
|
34
46
|
return;
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return;
|
|
42
|
-
const rect = el.getBoundingClientRect();
|
|
43
|
-
const total = direction === 'horizontal' ? rect.width : rect.height;
|
|
44
|
-
const clientPos = direction === 'horizontal' ? e.clientX : e.clientY;
|
|
45
|
-
const delta = clientPos - startPosRef.current;
|
|
46
|
-
// Note: total includes gutter. That's fine because max clamps against secondary min.
|
|
47
|
-
const next = startSizeRef.current + delta;
|
|
48
|
-
const maxPrimary = Math.max(minPrimarySize, total - minSecondarySize);
|
|
49
|
-
setPrimarySize(clamp(next, minPrimarySize, maxPrimary));
|
|
50
|
-
};
|
|
51
|
-
const onUp = () => {
|
|
52
|
-
if (!isDraggingRef.current)
|
|
53
|
-
return;
|
|
54
|
-
isDraggingRef.current = false;
|
|
55
|
-
document.body.style.cursor = '';
|
|
56
|
-
document.body.style.userSelect = '';
|
|
57
|
-
};
|
|
58
|
-
window.addEventListener('mousemove', onMove);
|
|
59
|
-
window.addEventListener('mouseup', onUp);
|
|
60
|
-
return () => {
|
|
61
|
-
if (window) {
|
|
62
|
-
window.removeEventListener('mousemove', onMove);
|
|
63
|
-
window.removeEventListener('mouseup', onUp);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}, [containerRef, direction, minPrimarySize, minSecondarySize, setPrimarySize]);
|
|
67
|
-
const handleMouseDown = (event) => {
|
|
68
|
-
isDraggingRef.current = true;
|
|
69
|
-
startPosRef.current = direction === 'horizontal' ? event.clientX : event.clientY;
|
|
47
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
48
|
+
const { maxPrimary } = computeClamp();
|
|
49
|
+
maxPrimaryRef.current = maxPrimary;
|
|
50
|
+
draggingRef.current = true;
|
|
51
|
+
pointerIdRef.current = e.pointerId;
|
|
52
|
+
startPosRef.current = getClientPos(e);
|
|
70
53
|
startSizeRef.current = primarySize;
|
|
71
|
-
|
|
54
|
+
// UX: prevent text selection during drag
|
|
72
55
|
document.body.style.userSelect = 'none';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
document.body.style.cursor = direction === 'horizontal' ? 'col-resize' : 'row-resize';
|
|
57
|
+
}, [computeClamp, containerRef, direction, getClientPos, primarySize]);
|
|
58
|
+
const onPointerMove = useCallback((e) => {
|
|
59
|
+
if (!draggingRef.current)
|
|
60
|
+
return;
|
|
61
|
+
if (pointerIdRef.current !== null && e.pointerId !== pointerIdRef.current)
|
|
62
|
+
return;
|
|
63
|
+
const delta = getClientPos(e) - startPosRef.current;
|
|
64
|
+
const next = startSizeRef.current + delta;
|
|
65
|
+
const maxPrimary = maxPrimaryRef.current;
|
|
66
|
+
setPrimarySize(clamp(next, minPrimarySize, maxPrimary));
|
|
67
|
+
}, [getClientPos, minPrimarySize, setPrimarySize]);
|
|
68
|
+
const endDrag = useCallback(() => {
|
|
69
|
+
if (!draggingRef.current)
|
|
70
|
+
return;
|
|
71
|
+
draggingRef.current = false;
|
|
72
|
+
pointerIdRef.current = null;
|
|
73
|
+
document.body.style.cursor = '';
|
|
74
|
+
document.body.style.userSelect = '';
|
|
75
|
+
}, []);
|
|
76
|
+
const onPointerUp = useCallback(() => endDrag(), [endDrag]);
|
|
77
|
+
const onPointerCancel = useCallback(() => endDrag(), [endDrag]);
|
|
78
|
+
// Keyboard: arrows adjust size; Shift = bigger step; Home/End = min/max
|
|
79
|
+
const onKeyDown = useCallback((e) => {
|
|
80
|
+
const { maxPrimary } = computeClamp();
|
|
81
|
+
const step = e.shiftKey ? 32 : 8;
|
|
82
|
+
let next = null;
|
|
83
|
+
if (direction === 'horizontal') {
|
|
84
|
+
if (e.key === 'ArrowLeft')
|
|
85
|
+
next = primarySize - step;
|
|
86
|
+
if (e.key === 'ArrowRight')
|
|
87
|
+
next = primarySize + step;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
if (e.key === 'ArrowUp')
|
|
91
|
+
next = primarySize - step;
|
|
92
|
+
if (e.key === 'ArrowDown')
|
|
93
|
+
next = primarySize + step;
|
|
94
|
+
}
|
|
95
|
+
if (e.key === 'Home')
|
|
96
|
+
next = minPrimarySize;
|
|
97
|
+
if (e.key === 'End')
|
|
98
|
+
next = maxPrimary;
|
|
99
|
+
if (next === null)
|
|
100
|
+
return;
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
setPrimarySize(clamp(next, minPrimarySize, maxPrimary));
|
|
103
|
+
}, [computeClamp, direction, minPrimarySize, primarySize, setPrimarySize]);
|
|
104
|
+
const ariaOrientation = direction === 'horizontal' ? 'vertical' : 'horizontal';
|
|
105
|
+
const { maxPrimary } = computeClamp();
|
|
106
|
+
return (_jsx("div", { className: styles.gutter, children: _jsx("div", { className: styles.resizer, role: "separator", "aria-orientation": ariaOrientation, "aria-valuemin": Math.round(minPrimarySize), "aria-valuemax": Number.isFinite(maxPrimary) ? Math.round(maxPrimary) : undefined, "aria-valuenow": Math.round(primarySize), tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, onKeyDown: onKeyDown }) }));
|
|
78
107
|
}
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
min-inline-size: 0;
|
|
21
21
|
min-block-size: 0;
|
|
22
22
|
overflow: auto;
|
|
23
|
-
flex-direction: column;
|
|
24
23
|
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/* ===== Secondary pane ===== */
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
flex-direction: column;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/* ===== Gutter (spacing + hit area) ===== */
|
|
37
|
+
/* ===== Gutter (spacing + hit area wrapper) ===== */
|
|
38
38
|
.gutter {
|
|
39
39
|
position: relative;
|
|
40
40
|
flex: 0 0 var(--split-pane-gutter, 8px);
|
|
@@ -42,32 +42,37 @@
|
|
|
42
42
|
z-index: 1;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/* Vertical mode gutter */
|
|
46
45
|
.container[data-direction='vertical'] .gutter {
|
|
47
46
|
inline-size: 100%;
|
|
48
47
|
block-size: var(--split-pane-gutter, 8px);
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
/* ===== Resizer (interaction
|
|
50
|
+
/* ===== Resizer (interaction element) ===== */
|
|
52
51
|
.resizer {
|
|
53
52
|
position: absolute;
|
|
54
53
|
inset: 0;
|
|
55
54
|
cursor: col-resize;
|
|
56
55
|
user-select: none;
|
|
57
56
|
touch-action: none;
|
|
57
|
+
outline: none;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
.container[data-direction='vertical'] .resizer {
|
|
61
61
|
cursor: row-resize;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/* Focus ring for keyboard resizing */
|
|
65
|
+
.resizer:focus-visible {
|
|
66
|
+
box-shadow: var(--focus-ring);
|
|
67
|
+
}
|
|
68
|
+
|
|
64
69
|
/* ===== Divider line ===== */
|
|
65
70
|
.resizer::after {
|
|
66
71
|
content: '';
|
|
67
72
|
position: absolute;
|
|
68
73
|
inset-block: 0;
|
|
69
74
|
inset-inline: 50%;
|
|
70
|
-
inline-size: var(--border-width-
|
|
75
|
+
inline-size: var(--border-width-thin);
|
|
71
76
|
background-color: var(--color-border-subtle);
|
|
72
77
|
opacity: 0;
|
|
73
78
|
transform: translateX(-50%);
|
|
@@ -81,7 +86,7 @@
|
|
|
81
86
|
inset-inline: 0;
|
|
82
87
|
inset-block: 50%;
|
|
83
88
|
inline-size: 100%;
|
|
84
|
-
block-size: var(--border-width-
|
|
89
|
+
block-size: var(--border-width-thin);
|
|
85
90
|
transform: translateY(-50%);
|
|
86
91
|
}
|
|
87
92
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
'use client';
|
|
2
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
2
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
3
|
export const SplitPaneContext = React.createContext(null);
|
|
@@ -22,7 +21,7 @@ function writeStoredSize(key, value) {
|
|
|
22
21
|
localStorage.setItem(key, String(Math.round(value)));
|
|
23
22
|
}
|
|
24
23
|
catch {
|
|
25
|
-
// ignore
|
|
24
|
+
// ignore
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
export function useSplitPaneContext() {
|
|
@@ -33,13 +32,8 @@ export function useSplitPaneContext() {
|
|
|
33
32
|
}
|
|
34
33
|
export function SplitPaneProvider({ children, direction, initialPrimarySize, minPrimarySize, minSecondarySize, storageKey, }) {
|
|
35
34
|
const containerRef = useRef(null);
|
|
36
|
-
|
|
37
|
-
* IMPORTANT (Next.js hydration):
|
|
38
|
-
* Always start with initialPrimarySize so server HTML and first client render match.
|
|
39
|
-
* Then, after hydration, read localStorage and update.
|
|
40
|
-
*/
|
|
35
|
+
// Start with initial to avoid SSR mismatch, then hydrate from storage
|
|
41
36
|
const [primarySize, setPrimarySize] = useState(initialPrimarySize);
|
|
42
|
-
// Apply persisted size AFTER hydration (prevents SSR/client mismatch warnings)
|
|
43
37
|
useEffect(() => {
|
|
44
38
|
if (!storageKey)
|
|
45
39
|
return;
|
|
@@ -48,11 +42,12 @@ export function SplitPaneProvider({ children, direction, initialPrimarySize, min
|
|
|
48
42
|
return;
|
|
49
43
|
setPrimarySize(stored);
|
|
50
44
|
}, [storageKey]);
|
|
51
|
-
// Clamp after mount / when container is measurable; re-clamp on resize
|
|
52
45
|
useEffect(() => {
|
|
53
46
|
const el = containerRef.current;
|
|
54
47
|
if (!el)
|
|
55
48
|
return;
|
|
49
|
+
if (typeof ResizeObserver === 'undefined')
|
|
50
|
+
return;
|
|
56
51
|
const clampToContainer = () => {
|
|
57
52
|
const rect = el.getBoundingClientRect();
|
|
58
53
|
const total = direction === 'horizontal' ? rect.width : rect.height;
|
|
@@ -62,11 +57,10 @@ export function SplitPaneProvider({ children, direction, initialPrimarySize, min
|
|
|
62
57
|
setPrimarySize(prev => clamp(prev, minPrimarySize, maxPrimary));
|
|
63
58
|
};
|
|
64
59
|
clampToContainer();
|
|
65
|
-
const ro = new ResizeObserver(
|
|
60
|
+
const ro = new ResizeObserver(clampToContainer);
|
|
66
61
|
ro.observe(el);
|
|
67
62
|
return () => ro.disconnect();
|
|
68
63
|
}, [direction, minPrimarySize, minSecondarySize]);
|
|
69
|
-
// Persist on change
|
|
70
64
|
useEffect(() => {
|
|
71
65
|
if (!storageKey)
|
|
72
66
|
return;
|