@window-splitter/solid 0.8.0

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 (48) hide show
  1. package/.storybook/main.ts +24 -0
  2. package/.storybook/preview.ts +14 -0
  3. package/.tshy/build.json +8 -0
  4. package/.tshy/esm.json +18 -0
  5. package/.turbo/turbo-build.log +5 -0
  6. package/.turbo/turbo-lint.log +5 -0
  7. package/CHANGELOG.md +66 -0
  8. package/README.md +54 -0
  9. package/dist/esm/SolidWIndowSplitter.d.ts +19 -0
  10. package/dist/esm/SolidWIndowSplitter.d.ts.map +1 -0
  11. package/dist/esm/SolidWIndowSplitter.jsx +506 -0
  12. package/dist/esm/SolidWIndowSplitter.jsx.map +1 -0
  13. package/dist/esm/SolidWindowSplitter.stories.d.ts +52 -0
  14. package/dist/esm/SolidWindowSplitter.stories.d.ts.map +1 -0
  15. package/dist/esm/SolidWindowSplitter.stories.jsx +470 -0
  16. package/dist/esm/SolidWindowSplitter.stories.jsx.map +1 -0
  17. package/dist/esm/SolidWindowSplitter.test.d.ts +2 -0
  18. package/dist/esm/SolidWindowSplitter.test.d.ts.map +1 -0
  19. package/dist/esm/SolidWindowSplitter.test.jsx +197 -0
  20. package/dist/esm/SolidWindowSplitter.test.jsx.map +1 -0
  21. package/dist/esm/context.d.ts +18 -0
  22. package/dist/esm/context.d.ts.map +1 -0
  23. package/dist/esm/context.jsx +38 -0
  24. package/dist/esm/context.jsx.map +1 -0
  25. package/dist/esm/index.d.ts +2 -0
  26. package/dist/esm/index.d.ts.map +1 -0
  27. package/dist/esm/index.js +2 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/mergeSolidAttributes.d.ts +6 -0
  30. package/dist/esm/mergeSolidAttributes.d.ts.map +1 -0
  31. package/dist/esm/mergeSolidAttributes.js +45 -0
  32. package/dist/esm/mergeSolidAttributes.js.map +1 -0
  33. package/dist/esm/package.json +3 -0
  34. package/doctor-storybook.log +29 -0
  35. package/eslint.config.js +10 -0
  36. package/index.html +11 -0
  37. package/package.json +104 -0
  38. package/rsbuild.config.js +25 -0
  39. package/src/SolidWIndowSplitter.tsx +683 -0
  40. package/src/SolidWindowSplitter.stories.tsx +689 -0
  41. package/src/SolidWindowSplitter.test.tsx +333 -0
  42. package/src/__snapshots__/SolidWindowSplitter.test.tsx.snap +56 -0
  43. package/src/context.tsx +58 -0
  44. package/src/index.ts +1 -0
  45. package/src/mergeSolidAttributes.ts +61 -0
  46. package/tsconfig.json +8 -0
  47. package/vite.config.js +6 -0
  48. package/vitest.config.ts +22 -0
@@ -0,0 +1,683 @@
1
+ import {
2
+ buildTemplate,
3
+ getCollapsiblePanelForHandleId,
4
+ getCursor,
5
+ getGroupSize,
6
+ getPanelGroupPercentageSizes,
7
+ getPanelGroupPixelSizes,
8
+ getPanelPercentageSize,
9
+ getPanelPixelSize,
10
+ groupMachine,
11
+ GroupMachineContextValue,
12
+ haveConstraintsChangedForPanel,
13
+ haveConstraintsChangedForPanelHandle,
14
+ initializePanel,
15
+ initializePanelHandleData,
16
+ isPanelData,
17
+ isPanelHandle,
18
+ PanelData,
19
+ parseUnit,
20
+ prepareItems,
21
+ prepareSnapshot,
22
+ } from "@window-splitter/state";
23
+ import {
24
+ Accessor,
25
+ children,
26
+ createDeferred,
27
+ createEffect,
28
+ createRenderEffect,
29
+ createRoot,
30
+ createSignal,
31
+ createUniqueId,
32
+ JSX,
33
+ on,
34
+ onCleanup,
35
+ onMount,
36
+ Ref,
37
+ splitProps,
38
+ } from "solid-js";
39
+ import {
40
+ GroupMachineProvider,
41
+ useMachineActor,
42
+ useGroupId,
43
+ useMachineState,
44
+ useInitialPrerenderContext,
45
+ } from "./context.jsx";
46
+ import {
47
+ PanelGroupHandle,
48
+ SharedPanelGroupProps,
49
+ PanelHandle,
50
+ SharedPanelProps,
51
+ SharedPanelResizerProps,
52
+ measureGroupChildren,
53
+ getPanelDomAttributes,
54
+ getPanelResizerDomAttributes,
55
+ move,
56
+ } from "@window-splitter/interface";
57
+ import { mergeSolidAttributes } from "./mergeSolidAttributes.js";
58
+
59
+ export interface PanelGroupProps
60
+ extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "style">,
61
+ SharedPanelGroupProps {
62
+ /** Imperative handle to control the group */
63
+ handle?: Ref<PanelGroupHandle>;
64
+ style?: JSX.CSSProperties;
65
+ }
66
+
67
+ export function PanelGroup(props: PanelGroupProps) {
68
+ const [, attrs] = splitProps(props, [
69
+ "orientation",
70
+ "autosaveStrategy",
71
+ "autosaveId",
72
+ "snapshot",
73
+ ]);
74
+ const [intiialValue, send, machineState, groupId] = createRoot(() => {
75
+ const defaultGroupId = `panel-group-${createUniqueId()}`;
76
+ const groupIdInit = props.autosaveId || props.id || defaultGroupId;
77
+ const orientation = props.orientation || "horizontal";
78
+ const autosaveStrategy = props.autosaveStrategy || "localStorage";
79
+
80
+ let snapshot: GroupMachineContextValue | undefined;
81
+
82
+ if (props.snapshot) {
83
+ snapshot = props.snapshot;
84
+ } else if (
85
+ typeof window !== "undefined" &&
86
+ props.autosaveId &&
87
+ autosaveStrategy === "localStorage"
88
+ ) {
89
+ const localSnapshot = localStorage.getItem(props.autosaveId);
90
+
91
+ if (localSnapshot) {
92
+ snapshot = JSON.parse(localSnapshot) as GroupMachineContextValue;
93
+ }
94
+ }
95
+
96
+ return [
97
+ ...groupMachine(
98
+ {
99
+ orientation,
100
+ groupId: groupIdInit,
101
+ autosaveStrategy: autosaveStrategy,
102
+ ...(snapshot ? prepareSnapshot(snapshot) : undefined),
103
+ },
104
+ (value) => {
105
+ setCurrentValue({ ...value });
106
+ }
107
+ ),
108
+ groupIdInit,
109
+ ];
110
+ });
111
+ const [currentValue, setCurrentValue] = createSignal(intiialValue);
112
+
113
+ const getContext = () => {
114
+ const context = currentValue?.() || intiialValue;
115
+ if (!context) throw new Error("No state");
116
+ return context;
117
+ };
118
+
119
+ // Prerender the children with the machine context
120
+ const [isInitialPrerender, setIsInitialPrerender] = createSignal(true);
121
+ const resolvedChildren = children(() => (
122
+ <GroupMachineProvider
123
+ groupId={groupId}
124
+ send={send}
125
+ state={getContext}
126
+ initialPrerender={isInitialPrerender}
127
+ >
128
+ {props.children}
129
+ </GroupMachineProvider>
130
+ ));
131
+ setIsInitialPrerender(false);
132
+
133
+ let elementRef: HTMLDivElement | undefined;
134
+
135
+ // Measure group size
136
+ onMount(() => {
137
+ const observer = new ResizeObserver(([entry]) => {
138
+ if (!entry) return;
139
+ if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
140
+ send({
141
+ type: "setSize",
142
+ size: entry.contentRect,
143
+ });
144
+ }
145
+ });
146
+
147
+ if (elementRef) {
148
+ observer.observe(elementRef);
149
+ }
150
+
151
+ onCleanup(() => observer.disconnect());
152
+ });
153
+
154
+ const childIds = createDeferred(() => {
155
+ const s = currentValue?.();
156
+ if (!s) throw new Error("No state");
157
+ return s.items.map((i) => i.id).join(",");
158
+ });
159
+
160
+ // Measure children size
161
+ createEffect(
162
+ on(childIds, () => {
163
+ const cleanup = measureGroupChildren(groupId, (childrenSizes) => {
164
+ send({ type: "setActualItemsSize", childrenSizes });
165
+ });
166
+
167
+ onCleanup(cleanup);
168
+ })
169
+ );
170
+
171
+ createRefContent(
172
+ () => props.handle,
173
+ () => ({
174
+ getId: () => groupId,
175
+ getPixelSizes: () => {
176
+ const s = currentValue?.();
177
+ if (!s) throw new Error("No state");
178
+ return getPanelGroupPixelSizes(s);
179
+ },
180
+ getPercentageSizes: () => {
181
+ const s = currentValue?.();
182
+ if (!s) throw new Error("No state");
183
+ return getPanelGroupPercentageSizes(s);
184
+ },
185
+ setSizes: (updates) => {
186
+ const context = currentValue?.();
187
+ if (!context) throw new Error("No state");
188
+
189
+ for (let index = 0; index < updates.length; index++) {
190
+ const item = context.items[index];
191
+ const update = updates[index];
192
+
193
+ if (item && isPanelData(item) && update) {
194
+ send({
195
+ type: "setPanelPixelSize",
196
+ panelId: item.id,
197
+ size: update,
198
+ });
199
+ }
200
+ }
201
+ },
202
+ getTemplate: () => {
203
+ const context = currentValue?.();
204
+ if (!context) throw new Error("No state");
205
+ return buildTemplate({ ...context, items: prepareItems(context) });
206
+ },
207
+ getState: () => (machineState.current === "idle" ? "idle" : "dragging"),
208
+ })
209
+ );
210
+
211
+ const getTemplate = () => {
212
+ const context = getContext();
213
+ const tempalte = buildTemplate(context);
214
+ return tempalte;
215
+ };
216
+
217
+ onMount(() => send({ type: "unlockGroup" }));
218
+ onCleanup(() => send({ type: "lockGroup" }));
219
+
220
+ return (
221
+ <div
222
+ ref={elementRef}
223
+ data-panel-group-wrapper
224
+ {...attrs}
225
+ style={{
226
+ display: "grid",
227
+ "grid-template-columns":
228
+ currentValue()?.orientation === "horizontal"
229
+ ? getTemplate()
230
+ : undefined,
231
+ "grid-template-rows":
232
+ currentValue()?.orientation === "vertical"
233
+ ? getTemplate()
234
+ : undefined,
235
+ height: "100%",
236
+ ...props.style,
237
+ }}
238
+ >
239
+ {resolvedChildren()}
240
+ </div>
241
+ );
242
+ }
243
+
244
+ function createRefContent<T extends Exclude<unknown, () => void>>(
245
+ getRef: () => Ref<T> | undefined,
246
+ createRef: () => T
247
+ ) {
248
+ createRenderEffect(() => {
249
+ const refProp = getRef();
250
+ if (typeof refProp !== "function") {
251
+ return () => {};
252
+ }
253
+ const refFunc = refProp as (value: T) => void;
254
+ refFunc(createRef());
255
+ });
256
+ }
257
+
258
+ export interface PanelProps
259
+ extends SharedPanelProps<Accessor<boolean | undefined>>,
260
+ Omit<JSX.HTMLAttributes<HTMLDivElement>, "onResize" | "style"> {
261
+ /** Imperative handle to control the panel */
262
+ handle?: Ref<PanelHandle>;
263
+ style?: JSX.CSSProperties;
264
+ }
265
+
266
+ export function Panel(props: PanelProps) {
267
+ const [, attrs] = splitProps(props, [
268
+ "min",
269
+ "max",
270
+ "id",
271
+ "collapsible",
272
+ "collapsed",
273
+ "collapsedSize",
274
+ "onCollapseChange",
275
+ "collapseAnimation",
276
+ "onResize",
277
+ "defaultCollapsed",
278
+ "default",
279
+ "isStaticAtRest",
280
+ ]);
281
+
282
+ const isInitialPrerender = useInitialPrerenderContext();
283
+ const send = useMachineActor();
284
+ const groupId = useGroupId();
285
+ const state = useMachineState();
286
+ const defaultId = createUniqueId();
287
+ const panelId = () => props.id || defaultId;
288
+
289
+ let dynamicPanelIsMounting = false;
290
+ let hasMounted = false;
291
+
292
+ const initPanel = (): PanelData =>
293
+ initializePanel({
294
+ id: panelId(),
295
+ min: props.min,
296
+ max: props.max,
297
+ collapsible: props.collapsible,
298
+ collapsed: props.collapsed?.(),
299
+ collapsedSize: props.collapsedSize,
300
+ onCollapseChange: { current: props.onCollapseChange },
301
+ collapseAnimation: props.collapseAnimation,
302
+ onResize: { current: props.onResize },
303
+ defaultCollapsed: props.defaultCollapsed,
304
+ default: props.default,
305
+ isStaticAtRest: props.isStaticAtRest,
306
+ });
307
+
308
+ createRoot(() => {
309
+ const panelData = initPanel();
310
+
311
+ if (send) {
312
+ const hasRegistered = state?.()?.items.find((i) => i.id === panelData.id);
313
+
314
+ if (!hasRegistered) {
315
+ if (isInitialPrerender()) {
316
+ send({ type: "registerPanel", data: panelData });
317
+ } else {
318
+ dynamicPanelIsMounting = true;
319
+ }
320
+ } else {
321
+ send?.({
322
+ type: "rebindPanelCallbacks",
323
+ data: {
324
+ id: panelData.id,
325
+ onCollapseChange: { current: props.onCollapseChange },
326
+ onResize: { current: props.onResize },
327
+ },
328
+ });
329
+ }
330
+ }
331
+
332
+ return panelData.id;
333
+ });
334
+
335
+ const panel = () => {
336
+ const currentPanel = state?.()?.items.find((i) => i.id === panelId());
337
+ if (!currentPanel || !isPanelData(currentPanel))
338
+ throw new Error("Panel not found");
339
+ return currentPanel;
340
+ };
341
+
342
+ const contraintChanged = createDeferred(() => {
343
+ const currentPanel = dynamicPanelIsMounting === false ? panel() : undefined;
344
+ if (!currentPanel || !isPanelData(currentPanel)) return;
345
+ return haveConstraintsChangedForPanel(initPanel(), currentPanel);
346
+ });
347
+
348
+ createEffect(
349
+ on(contraintChanged, () => {
350
+ if (!hasMounted) return;
351
+ send?.({
352
+ type: "updateConstraints",
353
+ data: initPanel(),
354
+ });
355
+ })
356
+ );
357
+
358
+ onMount(() => {
359
+ hasMounted = true;
360
+ if (!dynamicPanelIsMounting) return;
361
+
362
+ // get the index of the panel in it's group
363
+ const panelElement = document.getElementById(panelId());
364
+
365
+ if (!panelElement) return;
366
+
367
+ const groupElement = panelElement.closest(
368
+ `[data-panel-group-wrapper]`
369
+ ) as HTMLDivElement;
370
+
371
+ if (!groupElement || !panelElement) return;
372
+
373
+ const order = Array.from(groupElement.children).indexOf(panelElement);
374
+
375
+ if (typeof order !== "number") return;
376
+
377
+ send?.({
378
+ type: "registerDynamicPanel",
379
+ data: { ...initPanel(), order },
380
+ });
381
+ dynamicPanelIsMounting = false;
382
+ });
383
+
384
+ const panelData = () => {
385
+ const item = state?.()?.items.find((i) => i.id === panelId());
386
+ if (!item || !isPanelData(item)) return undefined;
387
+ return item;
388
+ };
389
+
390
+ const currentCollapsed = () => props.collapsed?.() || false;
391
+ createEffect(
392
+ on(currentCollapsed, () => {
393
+ const collapsed = props.collapsed?.() || false;
394
+
395
+ if (!panelData?.()?.collapseIsControlled) {
396
+ return;
397
+ }
398
+
399
+ if (collapsed) {
400
+ send?.({
401
+ type: "collapsePanel",
402
+ panelId: panelId(),
403
+ controlled: true,
404
+ });
405
+ } else {
406
+ send?.({ type: "expandPanel", panelId: panelId(), controlled: true });
407
+ }
408
+ })
409
+ );
410
+
411
+ createRefContent(
412
+ () => props.handle,
413
+ () => ({
414
+ getId: () => panelId(),
415
+ collapse: () => {
416
+ if (!panel().collapsible) return;
417
+ send?.({ type: "collapsePanel", panelId: panelId(), controlled: true });
418
+ },
419
+ isCollapsed: () => Boolean(panel().collapsible && panel().collapsed),
420
+ expand: () => {
421
+ if (!panel().collapsible) return;
422
+ send?.({ type: "expandPanel", panelId: panelId(), controlled: true });
423
+ },
424
+ isExpanded: () => Boolean(panel().collapsible && !panel().collapsed),
425
+ getPixelSize: () => {
426
+ const s = state?.();
427
+ if (!s) throw new Error("No state");
428
+ return getPanelPixelSize(s, panelId());
429
+ },
430
+ setSize: (size) => {
431
+ send?.({ type: "setPanelPixelSize", panelId: panelId(), size });
432
+ },
433
+ getPercentageSize: () => {
434
+ const s = state?.();
435
+ if (!s) throw new Error("No state");
436
+ return getPanelPercentageSize(s, panelId());
437
+ },
438
+ })
439
+ );
440
+
441
+ onCleanup(() => {
442
+ // We wait a frame because in Solid children unmount before their parent
443
+ // and we want to only unregister if just the panel is being removed, not
444
+ // the whole group. This frame allows for the parent to lock the machine.
445
+ requestAnimationFrame(() => {
446
+ send?.({ type: "unregisterPanel", id: panelId() });
447
+ });
448
+ });
449
+
450
+ const domAttributes = () => {
451
+ const currentPanel = panelData();
452
+
453
+ return getPanelDomAttributes({
454
+ groupId,
455
+ id: panelId(),
456
+ collapsible: currentPanel?.collapsible,
457
+ collapsed: currentPanel?.collapsed,
458
+ });
459
+ };
460
+
461
+ return (
462
+ <div
463
+ {...mergeSolidAttributes(attrs, domAttributes())}
464
+ style={{
465
+ "min-width": 0,
466
+ "min-height": 0,
467
+ overflow: "hidden",
468
+ ...props.style,
469
+ }}
470
+ />
471
+ );
472
+ }
473
+
474
+ export interface PanelResizerProps
475
+ extends SharedPanelResizerProps,
476
+ Omit<
477
+ JSX.HTMLAttributes<HTMLDivElement>,
478
+ "onDragStart" | "onDrag" | "onDragEnd" | "style"
479
+ > {
480
+ style?: JSX.CSSProperties;
481
+ }
482
+
483
+ export function PanelResizer(props: PanelResizerProps) {
484
+ const [, attrs] = splitProps(props, [
485
+ "size",
486
+ "id",
487
+ "onDragStart",
488
+ "onDrag",
489
+ "onDragEnd",
490
+ "disabled",
491
+ ]);
492
+
493
+ const isInitialPrerender = useInitialPrerenderContext();
494
+ const send = useMachineActor();
495
+ const state = useMachineState();
496
+ const defaultId = createUniqueId();
497
+ const handleId = () => props.id || defaultId;
498
+ const handle = () => {
499
+ const currentHandle = state?.()?.items.find((i) => i.id === handleId());
500
+ if (!currentHandle || !isPanelHandle(currentHandle))
501
+ throw new Error("Handle not found");
502
+ return currentHandle;
503
+ };
504
+
505
+ let dynamicPanelHandleIsMounting = false;
506
+ let hasMounted = false;
507
+
508
+ const initHandle = () =>
509
+ initializePanelHandleData({
510
+ size: props.size || "0px",
511
+ id: handleId(),
512
+ });
513
+
514
+ createRoot(() => {
515
+ if (send) {
516
+ const hasRegistered = state?.()?.items.find((i) => i.id === handleId());
517
+
518
+ if (!hasRegistered) {
519
+ if (isInitialPrerender()) {
520
+ send({
521
+ type: "registerPanelHandle",
522
+ data: initHandle(),
523
+ });
524
+ } else {
525
+ dynamicPanelHandleIsMounting = true;
526
+ }
527
+ }
528
+ }
529
+ });
530
+
531
+ const contraintChanged = createDeferred(() => {
532
+ const currentHandle =
533
+ dynamicPanelHandleIsMounting === false ? handle() : undefined;
534
+ if (!currentHandle || !isPanelHandle(currentHandle)) return;
535
+ return haveConstraintsChangedForPanelHandle(initHandle(), currentHandle);
536
+ });
537
+
538
+ createEffect(
539
+ on(contraintChanged, () => {
540
+ if (!hasMounted) return;
541
+ send?.({
542
+ type: "updateConstraints",
543
+ data: initHandle(),
544
+ });
545
+ })
546
+ );
547
+
548
+ onMount(() => {
549
+ hasMounted = true;
550
+ if (!dynamicPanelHandleIsMounting) return;
551
+
552
+ // get the index of the panel in it's group
553
+ const handleElement = document.getElementById(handleId());
554
+
555
+ if (!handleElement) return;
556
+
557
+ const groupElement = handleElement.closest(
558
+ `[data-panel-group-wrapper]`
559
+ ) as HTMLDivElement;
560
+
561
+ if (!groupElement || !handleElement) return;
562
+
563
+ const order = Array.from(groupElement.children).indexOf(handleElement);
564
+
565
+ if (typeof order !== "number") return;
566
+
567
+ send?.({
568
+ type: "registerPanelHandle",
569
+ data: { ...initHandle(), order },
570
+ });
571
+ dynamicPanelHandleIsMounting = false;
572
+ });
573
+
574
+ const { moveProps } = move({
575
+ onMoveStart: () => {
576
+ send?.({ type: "dragHandleStart", handleId: handleId() });
577
+ props.onDragStart?.();
578
+ document.body.style.cursor = cursor() || "auto";
579
+ },
580
+ onMove: (e) => {
581
+ send?.({ type: "dragHandle", handleId: handleId(), value: e });
582
+ props.onDrag?.();
583
+ },
584
+ onMoveEnd: () => {
585
+ send?.({ type: "dragHandleEnd", handleId: handleId() });
586
+ props.onDragEnd?.();
587
+ document.body.style.cursor = "auto";
588
+ },
589
+ });
590
+
591
+ const itemIndex = () =>
592
+ state?.()?.items.findIndex((item) => item.id === handleId()) || -1;
593
+ const activeDragHandleId = () => state?.()?.activeDragHandleId;
594
+ const isDragging = () => activeDragHandleId() === handleId();
595
+ const panelBeforeHandle = () => state?.()?.items[itemIndex() - 1];
596
+ const getCollapsiblePanel = () => {
597
+ const currentState = state?.();
598
+ if (!currentState) return undefined;
599
+
600
+ try {
601
+ return getCollapsiblePanelForHandleId(currentState, handleId());
602
+ } catch {
603
+ return undefined;
604
+ }
605
+ };
606
+ const panelAttributes = () => {
607
+ const panelBefore = panelBeforeHandle();
608
+ const currentState = state?.();
609
+
610
+ if (!panelBefore || !currentState || !isPanelData(panelBefore))
611
+ return { id: handleId };
612
+
613
+ return getPanelResizerDomAttributes({
614
+ groupId: currentState.groupId,
615
+ id: handleId(),
616
+ orientation: currentState.orientation,
617
+ isDragging: isDragging(),
618
+ activeDragHandleId: activeDragHandleId(),
619
+ disabled: props.disabled,
620
+ controlsId: panelBefore.id,
621
+ min: panelBefore.min,
622
+ max: panelBefore.max,
623
+ currentValue: panelBefore.currentValue,
624
+ groupSize: getGroupSize(currentState),
625
+ });
626
+ };
627
+
628
+ const cursor = () => {
629
+ if (props.disabled) return;
630
+ const currentState = state?.();
631
+ if (!currentState) return;
632
+ return getCursor(currentState);
633
+ };
634
+ const dimensions = () => {
635
+ const currentState = state?.();
636
+ if (!currentState) return {};
637
+ const unit = parseUnit(props.size || "0px");
638
+ return currentState.orientation === "horizontal"
639
+ ? { width: `${unit.value.toNumber()}px`, height: "100%" }
640
+ : { height: `${unit.value.toNumber()}px`, width: "100%" };
641
+ };
642
+
643
+ const onKeyDown = (e: KeyboardEvent) => {
644
+ const collapsiblePanel = getCollapsiblePanel();
645
+
646
+ if (e.key === "Enter" && collapsiblePanel) {
647
+ if (collapsiblePanel.collapsed) {
648
+ send?.({ type: "expandPanel", panelId: collapsiblePanel.id });
649
+ } else {
650
+ send?.({ type: "collapsePanel", panelId: collapsiblePanel.id });
651
+ }
652
+ }
653
+ };
654
+
655
+ onCleanup(() => {
656
+ // We wait a frame because in Solid children unmount before their parent
657
+ // and we want to only unregister if just the panel is being removed, not
658
+ // the whole group. This frame allows for the parent to lock the machine.
659
+ requestAnimationFrame(() => {
660
+ send?.({ type: "unregisterPanelHandle", id: handleId() });
661
+ });
662
+ });
663
+
664
+ return (
665
+ <div
666
+ {...mergeSolidAttributes(
667
+ attrs,
668
+ panelAttributes(),
669
+ props.disabled
670
+ ? {}
671
+ : mergeSolidAttributes(moveProps, { onKeyDown, tabIndex: 0 }),
672
+ {
673
+ style: {
674
+ cursor: cursor(),
675
+ ...dimensions(),
676
+ ...props.style,
677
+ },
678
+ }
679
+ )}
680
+ id={handleId()}
681
+ />
682
+ );
683
+ }