@noya-app/noya-designsystem 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +20 -12
  2. package/CHANGELOG.md +15 -0
  3. package/dist/chunk-D57E6H3M.mjs +36 -0
  4. package/dist/chunk-D57E6H3M.mjs.map +1 -0
  5. package/dist/chunk-FJ6ZGZIA.mjs +43 -0
  6. package/dist/chunk-FJ6ZGZIA.mjs.map +1 -0
  7. package/dist/emojis.d.mts +1 -0
  8. package/dist/emojis.d.ts +1 -0
  9. package/dist/emojis.js +31 -0
  10. package/dist/emojis.js.map +1 -0
  11. package/dist/emojis.mjs +8 -0
  12. package/dist/emojis.mjs.map +1 -0
  13. package/dist/index.css +1 -1
  14. package/dist/index.d.mts +142 -90
  15. package/dist/index.d.ts +142 -90
  16. package/dist/index.js +30559 -6412
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +35081 -10928
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +13 -17
  21. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
  22. package/src/components/Avatar.tsx +10 -10
  23. package/src/components/Banner.tsx +1 -1
  24. package/src/components/BaseToolbar.tsx +1 -1
  25. package/src/components/Button.tsx +2 -1
  26. package/src/components/Chip.tsx +1 -1
  27. package/src/components/ColorSwatchControl.tsx +22 -14
  28. package/src/components/CommandPalette.tsx +10 -6
  29. package/src/components/ContextMenu.tsx +249 -38
  30. package/src/components/Dialog.tsx +70 -67
  31. package/src/components/Drawer.tsx +56 -17
  32. package/src/components/DropdownMenu.tsx +305 -46
  33. package/src/components/EditableText.tsx +1 -1
  34. package/src/components/EmojiPicker.tsx +645 -0
  35. package/src/components/GridView.tsx +5 -1
  36. package/src/components/IVirtualizedList.tsx +21 -1
  37. package/src/components/InputField.tsx +2 -10
  38. package/src/components/InspectorContainer.tsx +4 -2
  39. package/src/components/Message.tsx +5 -16
  40. package/src/components/Popover.tsx +73 -107
  41. package/src/components/Progress.tsx +18 -18
  42. package/src/components/ScrollArea.tsx +66 -31
  43. package/src/components/ScrollableSidebar.tsx +1 -1
  44. package/src/components/SegmentedControl.tsx +166 -38
  45. package/src/components/SelectMenu.tsx +193 -101
  46. package/src/components/Slider.tsx +40 -38
  47. package/src/components/Switch.tsx +4 -4
  48. package/src/components/TextArea.tsx +1 -1
  49. package/src/components/Toast.tsx +99 -26
  50. package/src/components/Toolbar.tsx +114 -16
  51. package/src/components/Tooltip.tsx +18 -8
  52. package/src/components/Virtualized.tsx +193 -14
  53. package/src/components/VisuallyHidden.tsx +20 -0
  54. package/src/components/__tests__/Virtualized.math.test.ts +426 -1
  55. package/src/components/__tests__/Virtualized.test.tsx +129 -1
  56. package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
  57. package/src/components/internal/Menu.tsx +4 -0
  58. package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
  59. package/src/components/listView/ListViewRoot.tsx +5 -1
  60. package/src/components/resizablePanels/Panel.tsx +94 -0
  61. package/src/components/resizablePanels/PanelGroup.tsx +498 -0
  62. package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
  63. package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
  64. package/src/components/resizablePanels/index.ts +7 -0
  65. package/src/components/resizablePanels/types.ts +65 -0
  66. package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
  67. package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
  68. package/src/components/workspace/WorkspaceLayout.tsx +28 -58
  69. package/src/components/workspace/types.ts +6 -4
  70. package/src/contexts/DialogContext.tsx +15 -24
  71. package/src/emojis.ts +1 -0
  72. package/src/hooks/useTriggerToggle.ts +95 -0
  73. package/src/index.css +2 -0
  74. package/src/index.tsx +1 -2
  75. package/src/theme/proseTheme.ts +2 -0
  76. package/src/utils/mergeProps.ts +57 -0
  77. package/src/utils/skinTone.ts +90 -0
  78. package/tsup.config.ts +3 -1
  79. package/src/__tests__/workspaceLayout.test.ts +0 -281
  80. package/src/components/ScrollArea2.tsx +0 -76
  81. package/src/components/internal/MenuViewport.tsx +0 -178
  82. package/src/components/internal/SelectItem.tsx +0 -138
  83. 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
+ }
@@ -0,0 +1,7 @@
1
+ export { Panel } from "./Panel";
2
+ export { PanelGroup } from "./PanelGroup";
3
+ export { PanelResizeHandle } from "./PanelResizeHandle";
4
+ export type {
5
+ ImperativePanelGroupHandle,
6
+ ImperativePanelHandle,
7
+ } from "./types";