@noya-app/noya-designsystem 0.1.76 → 0.1.78
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/.turbo/turbo-build.log +20 -12
- package/CHANGELOG.md +19 -0
- package/dist/chunk-D57E6H3M.mjs +36 -0
- package/dist/chunk-D57E6H3M.mjs.map +1 -0
- package/dist/emojis.d.mts +1 -0
- package/dist/emojis.d.ts +1 -0
- package/dist/emojis.js +31 -0
- package/dist/emojis.js.map +1 -0
- package/dist/emojis.mjs +8 -0
- package/dist/emojis.mjs.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +192 -93
- package/dist/index.d.ts +192 -93
- package/dist/index.js +3355 -1763
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3622 -2030
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -9
- package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
- package/src/components/Avatar.tsx +10 -10
- package/src/components/Banner.tsx +1 -1
- package/src/components/BaseToolbar.tsx +20 -6
- package/src/components/Button.tsx +2 -1
- package/src/components/Chip.tsx +1 -1
- package/src/components/ColorSwatchControl.tsx +22 -14
- package/src/components/CommandPalette.tsx +10 -6
- package/src/components/ContextMenu.tsx +249 -38
- package/src/components/Dialog.tsx +70 -67
- package/src/components/Drawer.tsx +56 -17
- package/src/components/DropdownMenu.tsx +307 -46
- package/src/components/EditableText.tsx +1 -1
- package/src/components/EmojiPicker.tsx +645 -0
- package/src/components/GridBackground.tsx +40 -0
- package/src/components/GridView.tsx +5 -1
- package/src/components/IVirtualizedList.tsx +21 -1
- package/src/components/InputField.tsx +2 -10
- package/src/components/InspectorContainer.tsx +4 -2
- package/src/components/Message.tsx +5 -16
- package/src/components/OverlayToolbar.tsx +97 -0
- package/src/components/Popover.tsx +73 -107
- package/src/components/Progress.tsx +18 -18
- package/src/components/ScrollArea.tsx +66 -31
- package/src/components/ScrollableSidebar.tsx +1 -1
- package/src/components/SegmentedControl.tsx +166 -38
- package/src/components/SelectMenu.tsx +193 -101
- package/src/components/Slider.tsx +40 -38
- package/src/components/StackNavigator.tsx +1 -0
- package/src/components/Switch.tsx +4 -4
- package/src/components/TextArea.tsx +1 -1
- package/src/components/Toast.tsx +99 -26
- package/src/components/Toolbar.tsx +133 -24
- package/src/components/Tooltip.tsx +18 -8
- package/src/components/UserPointer.tsx +5 -3
- package/src/components/Virtualized.tsx +193 -14
- package/src/components/VisuallyHidden.tsx +20 -0
- package/src/components/__tests__/Virtualized.math.test.ts +426 -1
- package/src/components/__tests__/Virtualized.test.tsx +129 -1
- package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
- package/src/components/internal/Menu.tsx +4 -0
- package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
- package/src/components/listView/ListViewRoot.tsx +5 -1
- package/src/components/resizablePanels/Panel.tsx +94 -0
- package/src/components/resizablePanels/PanelGroup.tsx +498 -0
- package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
- package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
- package/src/components/resizablePanels/index.ts +7 -0
- package/src/components/resizablePanels/types.ts +65 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
- package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
- package/src/components/workspace/WorkspaceLayout.tsx +82 -76
- package/src/components/workspace/types.ts +6 -4
- package/src/contexts/DialogContext.tsx +15 -24
- package/src/emojis.ts +1 -0
- package/src/hooks/useTriggerToggle.ts +95 -0
- package/src/index.css +2 -0
- package/src/index.tsx +3 -2
- package/src/theme/proseTheme.ts +22 -0
- package/src/utils/mergeProps.ts +57 -0
- package/src/utils/skinTone.ts +90 -0
- package/tailwind.config.ts +6 -1
- package/tsup.config.ts +1 -1
- package/src/__tests__/workspaceLayout.test.ts +0 -281
- package/src/components/ScrollArea2.tsx +0 -76
- package/src/components/internal/MenuViewport.tsx +0 -178
- package/src/components/internal/SelectItem.tsx +0 -138
- package/src/components/workspace/panelStorage.ts +0 -216
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
forwardRef,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { cx } from "../../utils/classNames";
|
|
11
|
+
import { usePanelGroupContext } from "./PanelGroupContext";
|
|
12
|
+
import { ImperativePanelHandle } from "./types";
|
|
13
|
+
|
|
14
|
+
interface PanelProps {
|
|
15
|
+
id?: string;
|
|
16
|
+
order: number;
|
|
17
|
+
className?: string;
|
|
18
|
+
defaultSize?: number;
|
|
19
|
+
minSize?: number;
|
|
20
|
+
maxSize?: number;
|
|
21
|
+
collapsible?: boolean;
|
|
22
|
+
defaultCollapsed?: boolean;
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Panel = forwardRef<ImperativePanelHandle, PanelProps>(
|
|
27
|
+
function Panel(
|
|
28
|
+
{ id: propId, order, className, defaultSize, minSize, maxSize, collapsible, defaultCollapsed, children },
|
|
29
|
+
ref
|
|
30
|
+
) {
|
|
31
|
+
const generatedId = useId();
|
|
32
|
+
const id = propId ?? generatedId;
|
|
33
|
+
|
|
34
|
+
const context = usePanelGroupContext();
|
|
35
|
+
const {
|
|
36
|
+
registerPanel,
|
|
37
|
+
unregisterPanel,
|
|
38
|
+
getPanelSize,
|
|
39
|
+
expandPanel,
|
|
40
|
+
collapsePanel,
|
|
41
|
+
isPanelExpanded,
|
|
42
|
+
} = context;
|
|
43
|
+
|
|
44
|
+
// Register panel on mount
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
registerPanel({
|
|
47
|
+
id,
|
|
48
|
+
order,
|
|
49
|
+
defaultSize,
|
|
50
|
+
minSize,
|
|
51
|
+
maxSize,
|
|
52
|
+
collapsible,
|
|
53
|
+
defaultCollapsed,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
unregisterPanel(id);
|
|
58
|
+
};
|
|
59
|
+
}, [id, order, defaultSize, minSize, maxSize, collapsible, defaultCollapsed, registerPanel, unregisterPanel]);
|
|
60
|
+
|
|
61
|
+
const size = getPanelSize(id);
|
|
62
|
+
const isFlexPanel = defaultSize === undefined;
|
|
63
|
+
|
|
64
|
+
useImperativeHandle(
|
|
65
|
+
ref,
|
|
66
|
+
() => ({
|
|
67
|
+
expand: () => expandPanel(id),
|
|
68
|
+
collapse: () => collapsePanel(id),
|
|
69
|
+
isExpanded: () => isPanelExpanded(id),
|
|
70
|
+
getSize: () => getPanelSize(id),
|
|
71
|
+
}),
|
|
72
|
+
[id, expandPanel, collapsePanel, isPanelExpanded, getPanelSize]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Don't render if size is 0 (collapsed) - but flex panels always render
|
|
76
|
+
if (size === 0 && !isFlexPanel) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
data-panel-id={id}
|
|
83
|
+
className={cx("n-relative n-overflow-hidden", className)}
|
|
84
|
+
style={{
|
|
85
|
+
...(isFlexPanel
|
|
86
|
+
? { flex: 1, minWidth: 0 }
|
|
87
|
+
: { width: size, flexShrink: 0 }),
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { clientStorage, useSize } from "@noya-app/react-utils";
|
|
4
|
+
import React, {
|
|
5
|
+
forwardRef,
|
|
6
|
+
ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useImperativeHandle,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { cx } from "../../utils/classNames";
|
|
15
|
+
import { PanelGroupContext } from "./PanelGroupContext";
|
|
16
|
+
import {
|
|
17
|
+
ImperativePanelGroupHandle,
|
|
18
|
+
PanelGroupContextValue,
|
|
19
|
+
PanelRegistration,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
interface PanelGroupProps {
|
|
23
|
+
id?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
direction: "horizontal";
|
|
26
|
+
autoSaveId?: string;
|
|
27
|
+
onLayout?: (sizes: number[]) => void;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type PanelState = PanelRegistration & {
|
|
32
|
+
size: number;
|
|
33
|
+
collapsed: boolean;
|
|
34
|
+
sizeBeforeCollapse?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getStorageKey(autoSaveId: string) {
|
|
38
|
+
return `noya-panels:${autoSaveId}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadSavedSizes(
|
|
42
|
+
autoSaveId: string | undefined
|
|
43
|
+
): Record<string, number> | null {
|
|
44
|
+
if (!autoSaveId) return null;
|
|
45
|
+
const saved = clientStorage.getItem(getStorageKey(autoSaveId));
|
|
46
|
+
if (!saved) return null;
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(saved);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function saveSizes(autoSaveId: string | undefined, sizes: Record<string, number>) {
|
|
55
|
+
if (!autoSaveId) return;
|
|
56
|
+
clientStorage.setItem(getStorageKey(autoSaveId), JSON.stringify(sizes));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const PanelGroup = forwardRef<ImperativePanelGroupHandle, PanelGroupProps>(
|
|
60
|
+
function PanelGroup(
|
|
61
|
+
{ id, className, direction, autoSaveId, onLayout, children },
|
|
62
|
+
ref
|
|
63
|
+
) {
|
|
64
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
65
|
+
const containerSize = useSize(containerRef);
|
|
66
|
+
const containerWidth = containerSize?.width ?? 0;
|
|
67
|
+
|
|
68
|
+
const [panels, setPanels] = useState<Map<string, PanelState>>(new Map());
|
|
69
|
+
const panelsRef = useRef(panels);
|
|
70
|
+
panelsRef.current = panels;
|
|
71
|
+
|
|
72
|
+
const [resizing, setResizing] = useState<{
|
|
73
|
+
handleIndex: number;
|
|
74
|
+
startX: number;
|
|
75
|
+
startSizes: number[];
|
|
76
|
+
} | null>(null);
|
|
77
|
+
|
|
78
|
+
// Get panels sorted by order
|
|
79
|
+
const sortedPanels = useMemo(() => {
|
|
80
|
+
return Array.from(panels.values()).sort((a, b) => a.order - b.order);
|
|
81
|
+
}, [panels]);
|
|
82
|
+
|
|
83
|
+
// Calculate the flex panel (panel without defaultSize)
|
|
84
|
+
const flexPanelId = useMemo(() => {
|
|
85
|
+
for (const panel of sortedPanels) {
|
|
86
|
+
if (panel.defaultSize === undefined) {
|
|
87
|
+
return panel.id;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}, [sortedPanels]);
|
|
92
|
+
|
|
93
|
+
// Calculate sizes - fixed panels keep their size, flex panel gets remainder
|
|
94
|
+
const calculateLayout = useCallback(
|
|
95
|
+
(
|
|
96
|
+
panelStates: Map<string, PanelState>,
|
|
97
|
+
width: number
|
|
98
|
+
): Map<string, PanelState> => {
|
|
99
|
+
if (width === 0) return panelStates;
|
|
100
|
+
|
|
101
|
+
const sorted = Array.from(panelStates.values()).sort(
|
|
102
|
+
(a, b) => a.order - b.order
|
|
103
|
+
);
|
|
104
|
+
const flexPanel = sorted.find((p) => p.defaultSize === undefined);
|
|
105
|
+
|
|
106
|
+
let fixedSum = 0;
|
|
107
|
+
for (const panel of sorted) {
|
|
108
|
+
if (panel.id !== flexPanel?.id) {
|
|
109
|
+
fixedSum += panel.collapsed ? 0 : panel.size;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const flexSize = Math.max(0, width - fixedSum);
|
|
114
|
+
|
|
115
|
+
const newPanels = new Map(panelStates);
|
|
116
|
+
if (flexPanel) {
|
|
117
|
+
newPanels.set(flexPanel.id, {
|
|
118
|
+
...newPanels.get(flexPanel.id)!,
|
|
119
|
+
size: flexSize,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return newPanels;
|
|
124
|
+
},
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Recalculate layout when container width changes
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (containerWidth > 0 && panels.size > 0) {
|
|
131
|
+
setPanels((prev) => calculateLayout(prev, containerWidth));
|
|
132
|
+
}
|
|
133
|
+
}, [containerWidth, calculateLayout, panels.size]);
|
|
134
|
+
|
|
135
|
+
// Notify parent of layout changes
|
|
136
|
+
// eslint-disable-next-line @shopify/prefer-early-return
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (onLayout && sortedPanels.length > 0) {
|
|
139
|
+
const sizes = sortedPanels.map((p) => (p.collapsed ? 0 : p.size));
|
|
140
|
+
onLayout(sizes);
|
|
141
|
+
}
|
|
142
|
+
}, [onLayout, sortedPanels]);
|
|
143
|
+
|
|
144
|
+
const registerPanel = useCallback(
|
|
145
|
+
(panel: PanelRegistration) => {
|
|
146
|
+
setPanels((prev) => {
|
|
147
|
+
const savedSizes = loadSavedSizes(autoSaveId);
|
|
148
|
+
const savedSize = savedSizes?.[panel.id];
|
|
149
|
+
// Use saved size if available, otherwise check defaultCollapsed, then use defaultSize
|
|
150
|
+
const hasSavedSize = savedSize !== undefined;
|
|
151
|
+
const collapsed = hasSavedSize
|
|
152
|
+
? savedSize === 0 && !!panel.collapsible
|
|
153
|
+
: !!panel.defaultCollapsed && !!panel.collapsible;
|
|
154
|
+
const initialSize = hasSavedSize
|
|
155
|
+
? savedSize
|
|
156
|
+
: collapsed
|
|
157
|
+
? 0
|
|
158
|
+
: panel.defaultSize ?? 0;
|
|
159
|
+
|
|
160
|
+
const newPanels = new Map(prev);
|
|
161
|
+
newPanels.set(panel.id, {
|
|
162
|
+
...panel,
|
|
163
|
+
size: initialSize,
|
|
164
|
+
collapsed,
|
|
165
|
+
sizeBeforeCollapse: collapsed ? panel.defaultSize : undefined,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return calculateLayout(newPanels, containerWidth);
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
[autoSaveId, calculateLayout, containerWidth]
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const unregisterPanel = useCallback((panelId: string) => {
|
|
175
|
+
setPanels((prev) => {
|
|
176
|
+
const newPanels = new Map(prev);
|
|
177
|
+
newPanels.delete(panelId);
|
|
178
|
+
return newPanels;
|
|
179
|
+
});
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
const getPanelSize = useCallback(
|
|
183
|
+
(panelId: string): number => {
|
|
184
|
+
return panels.get(panelId)?.size ?? 0;
|
|
185
|
+
},
|
|
186
|
+
[panels]
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const getPanelIndex = useCallback(
|
|
190
|
+
(panelId: string): number => {
|
|
191
|
+
return sortedPanels.findIndex((p) => p.id === panelId);
|
|
192
|
+
},
|
|
193
|
+
[sortedPanels]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const setPanelSize = useCallback(
|
|
197
|
+
(panelId: string, size: number) => {
|
|
198
|
+
setPanels((prev) => {
|
|
199
|
+
const panel = prev.get(panelId);
|
|
200
|
+
if (!panel) return prev;
|
|
201
|
+
|
|
202
|
+
const clampedSize = Math.max(
|
|
203
|
+
panel.minSize ?? 0,
|
|
204
|
+
Math.min(size, panel.maxSize ?? Infinity)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const newPanels = new Map(prev);
|
|
208
|
+
newPanels.set(panelId, {
|
|
209
|
+
...panel,
|
|
210
|
+
size: clampedSize,
|
|
211
|
+
collapsed: clampedSize === 0,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return calculateLayout(newPanels, containerWidth);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Save to storage
|
|
218
|
+
const currentPanels = panelsRef.current;
|
|
219
|
+
const sizesToSave: Record<string, number> = {};
|
|
220
|
+
for (const [id, panel] of currentPanels) {
|
|
221
|
+
if (panel.defaultSize !== undefined) {
|
|
222
|
+
sizesToSave[id] = id === panelId ? size : panel.size;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
saveSizes(autoSaveId, sizesToSave);
|
|
226
|
+
},
|
|
227
|
+
[autoSaveId, calculateLayout, containerWidth]
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const expandPanel = useCallback(
|
|
231
|
+
(panelId: string) => {
|
|
232
|
+
setPanels((prev) => {
|
|
233
|
+
const panel = prev.get(panelId);
|
|
234
|
+
if (!panel || !panel.collapsed) return prev;
|
|
235
|
+
|
|
236
|
+
const newSize = panel.sizeBeforeCollapse ?? panel.defaultSize ?? 200;
|
|
237
|
+
const newPanels = new Map(prev);
|
|
238
|
+
newPanels.set(panelId, {
|
|
239
|
+
...panel,
|
|
240
|
+
size: newSize,
|
|
241
|
+
collapsed: false,
|
|
242
|
+
sizeBeforeCollapse: undefined,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Save to storage
|
|
246
|
+
const sizesToSave: Record<string, number> = {};
|
|
247
|
+
for (const [id, p] of newPanels) {
|
|
248
|
+
if (p.defaultSize !== undefined) {
|
|
249
|
+
sizesToSave[id] = p.size;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
saveSizes(autoSaveId, sizesToSave);
|
|
253
|
+
|
|
254
|
+
return calculateLayout(newPanels, containerWidth);
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
[autoSaveId, calculateLayout, containerWidth]
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const collapsePanel = useCallback(
|
|
261
|
+
(panelId: string) => {
|
|
262
|
+
setPanels((prev) => {
|
|
263
|
+
const panel = prev.get(panelId);
|
|
264
|
+
if (!panel || !panel.collapsible || panel.collapsed) return prev;
|
|
265
|
+
|
|
266
|
+
const newPanels = new Map(prev);
|
|
267
|
+
newPanels.set(panelId, {
|
|
268
|
+
...panel,
|
|
269
|
+
size: 0,
|
|
270
|
+
collapsed: true,
|
|
271
|
+
sizeBeforeCollapse: panel.size,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Save to storage
|
|
275
|
+
const sizesToSave: Record<string, number> = {};
|
|
276
|
+
for (const [id, p] of newPanels) {
|
|
277
|
+
if (p.defaultSize !== undefined) {
|
|
278
|
+
sizesToSave[id] = id === panelId ? 0 : p.size;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
saveSizes(autoSaveId, sizesToSave);
|
|
282
|
+
|
|
283
|
+
return calculateLayout(newPanels, containerWidth);
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
[autoSaveId, calculateLayout, containerWidth]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const isPanelExpanded = useCallback(
|
|
290
|
+
(panelId: string): boolean => {
|
|
291
|
+
const panel = panels.get(panelId);
|
|
292
|
+
return panel ? !panel.collapsed : false;
|
|
293
|
+
},
|
|
294
|
+
[panels]
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const startResize = useCallback(
|
|
298
|
+
(handleIndex: number, startX: number) => {
|
|
299
|
+
const startSizes = sortedPanels.map((p) => p.size);
|
|
300
|
+
setResizing({
|
|
301
|
+
handleIndex,
|
|
302
|
+
startX,
|
|
303
|
+
startSizes,
|
|
304
|
+
});
|
|
305
|
+
},
|
|
306
|
+
[sortedPanels]
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Handle mouse move during resize
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!resizing) return;
|
|
312
|
+
|
|
313
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
314
|
+
// Calculate delta from where the drag started
|
|
315
|
+
const delta = e.clientX - resizing.startX;
|
|
316
|
+
|
|
317
|
+
// Get the panels on either side of the handle
|
|
318
|
+
const leftPanelIndex = resizing.handleIndex;
|
|
319
|
+
const rightPanelIndex = resizing.handleIndex + 1;
|
|
320
|
+
const leftPanel = sortedPanels[leftPanelIndex];
|
|
321
|
+
const rightPanel = sortedPanels[rightPanelIndex];
|
|
322
|
+
|
|
323
|
+
if (!leftPanel || !rightPanel) return;
|
|
324
|
+
|
|
325
|
+
// Skip if either panel is the flex panel (it adjusts automatically)
|
|
326
|
+
const leftIsFixed = leftPanel.defaultSize !== undefined;
|
|
327
|
+
const rightIsFixed = rightPanel.defaultSize !== undefined;
|
|
328
|
+
|
|
329
|
+
if (!leftIsFixed && !rightIsFixed) return;
|
|
330
|
+
|
|
331
|
+
// Calculate intended sizes (before clamping)
|
|
332
|
+
const intendedLeftSize = resizing.startSizes[leftPanelIndex] + delta;
|
|
333
|
+
const intendedRightSize = resizing.startSizes[rightPanelIndex] - delta;
|
|
334
|
+
|
|
335
|
+
// Determine if panels should collapse (when dragged below half their minSize)
|
|
336
|
+
const leftMinSize = leftPanel.minSize ?? 0;
|
|
337
|
+
const rightMinSize = rightPanel.minSize ?? 0;
|
|
338
|
+
const collapseThreshold = 0.5; // Collapse when dragged below 50% of minSize
|
|
339
|
+
|
|
340
|
+
const shouldCollapseLeft =
|
|
341
|
+
leftIsFixed &&
|
|
342
|
+
leftPanel.collapsible &&
|
|
343
|
+
intendedLeftSize < leftMinSize * collapseThreshold;
|
|
344
|
+
const shouldCollapseRight =
|
|
345
|
+
rightIsFixed &&
|
|
346
|
+
rightPanel.collapsible &&
|
|
347
|
+
intendedRightSize < rightMinSize * collapseThreshold;
|
|
348
|
+
|
|
349
|
+
// Calculate final sizes
|
|
350
|
+
let newLeftSize = intendedLeftSize;
|
|
351
|
+
let newRightSize = intendedRightSize;
|
|
352
|
+
|
|
353
|
+
if (leftIsFixed) {
|
|
354
|
+
if (shouldCollapseLeft) {
|
|
355
|
+
newLeftSize = 0;
|
|
356
|
+
} else {
|
|
357
|
+
newLeftSize = Math.max(
|
|
358
|
+
leftMinSize,
|
|
359
|
+
Math.min(newLeftSize, leftPanel.maxSize ?? Infinity)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (rightIsFixed) {
|
|
364
|
+
if (shouldCollapseRight) {
|
|
365
|
+
newRightSize = 0;
|
|
366
|
+
} else {
|
|
367
|
+
newRightSize = Math.max(
|
|
368
|
+
rightMinSize,
|
|
369
|
+
Math.min(newRightSize, rightPanel.maxSize ?? Infinity)
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Update the fixed panel(s)
|
|
375
|
+
setPanels((prev) => {
|
|
376
|
+
const newPanels = new Map(prev);
|
|
377
|
+
if (leftIsFixed) {
|
|
378
|
+
const currentPanel = newPanels.get(leftPanel.id)!;
|
|
379
|
+
newPanels.set(leftPanel.id, {
|
|
380
|
+
...currentPanel,
|
|
381
|
+
size: newLeftSize,
|
|
382
|
+
collapsed: newLeftSize === 0,
|
|
383
|
+
sizeBeforeCollapse:
|
|
384
|
+
newLeftSize === 0 && !currentPanel.collapsed
|
|
385
|
+
? currentPanel.size
|
|
386
|
+
: currentPanel.sizeBeforeCollapse,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (rightIsFixed) {
|
|
390
|
+
const currentPanel = newPanels.get(rightPanel.id)!;
|
|
391
|
+
newPanels.set(rightPanel.id, {
|
|
392
|
+
...currentPanel,
|
|
393
|
+
size: newRightSize,
|
|
394
|
+
collapsed: newRightSize === 0,
|
|
395
|
+
sizeBeforeCollapse:
|
|
396
|
+
newRightSize === 0 && !currentPanel.collapsed
|
|
397
|
+
? currentPanel.size
|
|
398
|
+
: currentPanel.sizeBeforeCollapse,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return calculateLayout(newPanels, containerWidth);
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const handleMouseUp = () => {
|
|
406
|
+
// Save final sizes
|
|
407
|
+
const currentPanels = panelsRef.current;
|
|
408
|
+
const sizesToSave: Record<string, number> = {};
|
|
409
|
+
for (const [id, panel] of currentPanels) {
|
|
410
|
+
if (panel.defaultSize !== undefined) {
|
|
411
|
+
sizesToSave[id] = panel.size;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
saveSizes(autoSaveId, sizesToSave);
|
|
415
|
+
|
|
416
|
+
setResizing(null);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
document.addEventListener("mousemove", handleMouseMove);
|
|
420
|
+
document.addEventListener("mouseup", handleMouseUp);
|
|
421
|
+
|
|
422
|
+
return () => {
|
|
423
|
+
document.removeEventListener("mousemove", handleMouseMove);
|
|
424
|
+
document.removeEventListener("mouseup", handleMouseUp);
|
|
425
|
+
};
|
|
426
|
+
}, [
|
|
427
|
+
resizing,
|
|
428
|
+
sortedPanels,
|
|
429
|
+
autoSaveId,
|
|
430
|
+
calculateLayout,
|
|
431
|
+
containerWidth,
|
|
432
|
+
flexPanelId,
|
|
433
|
+
]);
|
|
434
|
+
|
|
435
|
+
useImperativeHandle(ref, () => ({
|
|
436
|
+
getLayout: () => sortedPanels.map((p) => p.size),
|
|
437
|
+
setLayout: (sizes: number[]) => {
|
|
438
|
+
setPanels((prev) => {
|
|
439
|
+
const newPanels = new Map(prev);
|
|
440
|
+
sortedPanels.forEach((panel, index) => {
|
|
441
|
+
if (sizes[index] !== undefined) {
|
|
442
|
+
newPanels.set(panel.id, {
|
|
443
|
+
...newPanels.get(panel.id)!,
|
|
444
|
+
size: sizes[index],
|
|
445
|
+
collapsed: sizes[index] === 0,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
return newPanels;
|
|
450
|
+
});
|
|
451
|
+
},
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
const contextValue: PanelGroupContextValue = useMemo(
|
|
455
|
+
() => ({
|
|
456
|
+
registerPanel,
|
|
457
|
+
unregisterPanel,
|
|
458
|
+
getPanelSize,
|
|
459
|
+
setPanelSize,
|
|
460
|
+
getPanelIndex,
|
|
461
|
+
expandPanel,
|
|
462
|
+
collapsePanel,
|
|
463
|
+
isPanelExpanded,
|
|
464
|
+
startResize,
|
|
465
|
+
containerWidth,
|
|
466
|
+
}),
|
|
467
|
+
[
|
|
468
|
+
registerPanel,
|
|
469
|
+
unregisterPanel,
|
|
470
|
+
getPanelSize,
|
|
471
|
+
setPanelSize,
|
|
472
|
+
getPanelIndex,
|
|
473
|
+
expandPanel,
|
|
474
|
+
collapsePanel,
|
|
475
|
+
isPanelExpanded,
|
|
476
|
+
startResize,
|
|
477
|
+
containerWidth,
|
|
478
|
+
]
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
return (
|
|
482
|
+
<PanelGroupContext.Provider value={contextValue}>
|
|
483
|
+
<div
|
|
484
|
+
ref={containerRef}
|
|
485
|
+
id={id}
|
|
486
|
+
className={cx("n-flex n-flex-row n-h-full n-w-full", className)}
|
|
487
|
+
style={{
|
|
488
|
+
cursor: resizing ? "col-resize" : undefined,
|
|
489
|
+
userSelect: resizing ? "none" : undefined,
|
|
490
|
+
}}
|
|
491
|
+
data-direction={direction}
|
|
492
|
+
>
|
|
493
|
+
{children}
|
|
494
|
+
</div>
|
|
495
|
+
</PanelGroupContext.Provider>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import { PanelGroupContextValue } from "./types";
|
|
3
|
+
|
|
4
|
+
export const PanelGroupContext = createContext<PanelGroupContextValue | null>(
|
|
5
|
+
null
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export function usePanelGroupContext(): PanelGroupContextValue {
|
|
9
|
+
const context = useContext(PanelGroupContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("Panel must be used within a PanelGroup");
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useContext, useRef } from "react";
|
|
4
|
+
import { cx } from "../../utils/classNames";
|
|
5
|
+
import { PanelGroupContext } from "./PanelGroupContext";
|
|
6
|
+
|
|
7
|
+
interface PanelResizeHandleProps {
|
|
8
|
+
className?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** The index of this handle (0 = between panels 0 and 1, 1 = between panels 1 and 2, etc.) */
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PanelResizeHandle({
|
|
15
|
+
className,
|
|
16
|
+
disabled,
|
|
17
|
+
index,
|
|
18
|
+
}: PanelResizeHandleProps) {
|
|
19
|
+
const context = useContext(PanelGroupContext);
|
|
20
|
+
const handleRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
const handleMouseDown = useCallback(
|
|
23
|
+
(e: React.MouseEvent) => {
|
|
24
|
+
if (disabled || !context) return;
|
|
25
|
+
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
context.startResize(index, e.clientX);
|
|
28
|
+
},
|
|
29
|
+
[context, disabled, index]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!context) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
ref={handleRef}
|
|
39
|
+
data-resize-handle
|
|
40
|
+
className={cx("n-relative", className)}
|
|
41
|
+
onMouseDown={handleMouseDown}
|
|
42
|
+
style={{
|
|
43
|
+
cursor: disabled ? undefined : "col-resize",
|
|
44
|
+
touchAction: "none",
|
|
45
|
+
zIndex: 10,
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{/* Invisible expanded hit area for easier dragging */}
|
|
49
|
+
<div
|
|
50
|
+
style={{
|
|
51
|
+
position: "absolute",
|
|
52
|
+
top: 0,
|
|
53
|
+
bottom: 0,
|
|
54
|
+
left: -4,
|
|
55
|
+
right: -4,
|
|
56
|
+
cursor: disabled ? undefined : "col-resize",
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|