@corvu/resizable
Version:
Unstyled, accessible and customizable UI primitives for SolidJS
1,523 lines (1,512 loc) • 55.5 kB
JSX
// src/context.ts
import { createContext, useContext } from "solid-js";
import {
createKeyedContext,
useKeyedContext
} from "@corvu/utils/create/keyedContext";
var ResizableContext = createContext();
var createResizableContext = (contextId) => {
if (contextId === void 0) return ResizableContext;
const context = createKeyedContext(
`resizable-${contextId}`
);
return context;
};
var useResizableContext = (contextId) => {
if (contextId === void 0) {
const context2 = useContext(ResizableContext);
if (!context2) {
throw new Error(
"[corvu]: Resizable context not found. Make sure to wrap Resizable components in <Resizable.Root>"
);
}
return context2;
}
const context = useKeyedContext(
`resizable-${contextId}`
);
if (!context) {
throw new Error(
`[corvu]: Resizable context with id "${contextId}" not found. Make sure to wrap Resizable components in <Resizable.Root contextId="${contextId}">`
);
}
return context;
};
var InternalResizableContext = createContext();
var createInternalResizableContext = (contextId) => {
if (contextId === void 0) return InternalResizableContext;
const context = createKeyedContext(
`resizable-internal-${contextId}`
);
return context;
};
var useInternalResizableContext = (contextId) => {
if (contextId === void 0) {
const context2 = useContext(InternalResizableContext);
if (!context2) {
throw new Error(
"[corvu]: Resizable context not found. Make sure to wrap Resizable components in <Resizable.Root>"
);
}
return context2;
}
const context = useKeyedContext(
`resizable-internal-${contextId}`
);
if (!context) {
throw new Error(
`[corvu]: Resizable context with id "${contextId}" not found. Make sure to wrap Resizable components in <Resizable.Root contextId="${contextId}">`
);
}
return context;
};
// src/Handle.tsx
import {
callEventHandler,
combineStyle
} from "@corvu/utils/dom";
import {
createEffect,
createMemo,
createSignal,
mergeProps,
on,
onCleanup,
Show,
splitProps
} from "solid-js";
import { Dynamic } from "@corvu/utils/dynamic";
// src/lib/utils.ts
var resolveSize = (size, rootSize) => {
if (typeof size === "number") {
return size;
}
if (!size.endsWith("px")) {
throw new Error(
`[corvu] Sizes must be a number or a string ending with 'px'. Got ${size}`
);
}
return fixToPrecision(parseFloat(size) / rootSize);
};
var splitPanels = (props) => {
const precedingPanels = props.panels.filter(
(panel) => !!(props.focusedElement.compareDocumentPosition(panel.data.element) & Node.DOCUMENT_POSITION_PRECEDING)
);
const followingPanels = props.panels.filter(
(panel) => !!(props.focusedElement.compareDocumentPosition(panel.data.element) & Node.DOCUMENT_POSITION_FOLLOWING)
);
return [precedingPanels, followingPanels];
};
var PRECISION = 6;
var fixToPrecision = (value) => parseFloat(value.toFixed(PRECISION));
// src/lib/cursor.ts
var globalCursorStyle = null;
var cursorStyleElement = null;
var globalResizeConstraints = 0;
var constraintToCursorMap = {
1: "e-resize",
2: "w-resize",
3: "ew-resize",
4: "s-resize",
8: "n-resize",
12: "ns-resize",
5: "se-resize",
9: "ne-resize",
6: "sw-resize",
10: "nw-resize"
};
var cachedCursorStyle = null;
var updateCursorStyle = () => {
if (!globalCursorStyle) {
if (cursorStyleElement) {
cachedCursorStyle = null;
cursorStyleElement.remove();
cursorStyleElement = null;
}
return;
}
let cursorStyle = constraintToCursorMap[globalResizeConstraints] ?? null;
if (cursorStyle === null) {
switch (globalCursorStyle) {
case "horizontal":
cursorStyle = "col-resize";
break;
case "vertical":
cursorStyle = "row-resize";
break;
case "both":
cursorStyle = "move";
break;
}
}
if (cursorStyle === cachedCursorStyle) return;
cachedCursorStyle = cursorStyle;
if (!cursorStyleElement) {
cursorStyleElement = document.createElement("style");
document.head.appendChild(cursorStyleElement);
}
cursorStyleElement.innerHTML = `*{cursor: ${cursorStyle}!important;}`;
};
var reportResizeConstraints = (orientation, constraints) => {
switch (orientation) {
case "horizontal":
if (constraints === 1) {
globalResizeConstraints |= 1;
globalResizeConstraints &= ~2;
} else if (constraints === 2) {
globalResizeConstraints |= 2;
globalResizeConstraints &= ~1;
} else if (constraints === 3) {
globalResizeConstraints |= 3;
} else {
globalResizeConstraints &= ~3;
}
break;
case "vertical":
if (constraints === 1) {
globalResizeConstraints |= 4;
globalResizeConstraints &= ~8;
} else if (constraints === 2) {
globalResizeConstraints |= 8;
globalResizeConstraints &= ~4;
} else if (constraints === 3) {
globalResizeConstraints |= 12;
} else {
globalResizeConstraints &= ~12;
}
break;
}
updateCursorStyle();
};
var resetResizeConstraints = () => {
globalResizeConstraints = 0;
updateCursorStyle();
};
var setGlobalCursorStyle = (cursorStyle) => {
globalCursorStyle = cursorStyle;
updateCursorStyle();
};
var handleResizeConstraints = (props) => {
if (fixToPrecision(props.distributablePercentage) !== fixToPrecision(props.desiredPercentage)) {
let constraints = null;
if (props.betweenCollapse === true) {
constraints = 3;
} else if (props.desiredPercentage < props.distributablePercentage && props.revertConstraints !== true || props.desiredPercentage > props.distributablePercentage && props.revertConstraints === true) {
constraints = 1;
} else {
constraints = 2;
}
reportResizeConstraints(props.orientation, constraints);
} else {
reportResizeConstraints(props.orientation, 0);
}
};
// src/lib/handleManager.ts
import { batch } from "solid-js";
import { some } from "@corvu/utils/reactivity";
var handles = [];
var dragStartPos = null;
var globalHovered = null;
var INTERSECTION_TOLERANCE = 1;
var equalsWithTolerance = (a, b) => Math.abs(a - b) <= INTERSECTION_TOLERANCE;
var registerHandle = (handle) => {
handles.push(handle);
for (const handle2 of handles) {
for (const compareHandle of handles) {
if (handle2.orientation === compareHandle.orientation || handle2.element === compareHandle.element) {
continue;
}
const handleRect = handle2.element.getBoundingClientRect();
const compareHandleRect = compareHandle.element.getBoundingClientRect();
if (handle2.orientation === "horizontal") {
if (handleRect.left > compareHandleRect.right || handleRect.right < compareHandleRect.left) {
continue;
}
}
if (handle2.orientation === "vertical") {
if (handleRect.top > compareHandleRect.bottom || handleRect.bottom < compareHandleRect.top) {
continue;
}
}
const isStartIntersection = handle2.orientation === "horizontal" ? equalsWithTolerance(handleRect.top, compareHandleRect.bottom) : equalsWithTolerance(handleRect.left, compareHandleRect.right);
const isEndIntersection = handle2.orientation === "horizontal" ? equalsWithTolerance(handleRect.bottom, compareHandleRect.top) : equalsWithTolerance(handleRect.right, compareHandleRect.left);
if (isStartIntersection) {
handle2.startIntersection.setHandle(compareHandle);
}
if (isEndIntersection) {
handle2.endIntersection.setHandle(compareHandle);
}
}
}
return {
onDragStart: (event, target) => onDragStart(handle, event, target),
onHoveredChange: (state) => {
globalHovered = state;
const dragging = !!dragStartPos;
let cursorStyle = null;
batch(() => {
switch (state) {
case "handle": {
const startHandle = handle.startIntersection.handle();
const endHandle = handle.endIntersection.handle();
if (!dragging) {
handle.setActive(true);
startHandle?.setActive(false);
endHandle?.setActive(false);
}
startHandle?.setHoveredAsIntersection(false);
endHandle?.setHoveredAsIntersection(false);
cursorStyle = handle.orientation;
break;
}
case "startIntersection": {
const startHandle = handle.startIntersection.handle();
if (!dragging) {
startHandle?.setActive(true);
}
startHandle?.setHoveredAsIntersection(true);
cursorStyle = "both";
break;
}
case "endIntersection": {
const endHandle = handle.endIntersection.handle();
if (!dragging) {
endHandle?.setActive(true);
}
endHandle?.setHoveredAsIntersection(true);
cursorStyle = "both";
break;
}
case null: {
const startHandle = handle.startIntersection.handle();
const endHandle = handle.endIntersection.handle();
if (!dragging && !handle.focused()) {
handle.setActive(false);
startHandle?.setActive(false);
endHandle?.setActive(false);
}
startHandle?.setHoveredAsIntersection(false);
endHandle?.setHoveredAsIntersection(false);
cursorStyle = null;
break;
}
}
if (!dragging && handle.handleCursorStyle()) {
setGlobalCursorStyle(cursorStyle);
}
});
}
};
};
var unregisterHandle = (handle) => {
handles.splice(handles.indexOf(handle), 1);
};
var onDragStart = (handle, event, target) => {
dragStartPos = { x: event.clientX, y: event.clientY };
batch(() => {
handle.setDragging(true);
if (target === "startIntersection") {
handle.startIntersection.handle()?.setDragging(true);
}
if (target === "endIntersection") {
handle.endIntersection.handle()?.setDragging(true);
}
});
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("touchmove", onTouchMove);
window.addEventListener("pointerup", onDragEnd);
window.addEventListener("touchend", onDragEnd);
window.addEventListener("contextmenu", onDragEnd);
};
var onPointerMove = (event) => onMove(event.clientX, event.clientY, event.altKey);
var onTouchMove = (event) => {
if (!event.touches[0]) return;
onMove(event.touches[0].clientX, event.touches[0].clientY, event.altKey);
};
var altKeyCache = false;
var onMove = (x, y, altKey) => {
if (!dragStartPos) return;
if (handles.some((handle) => handle.dragging() && handle.altKey === "only")) {
altKey = true;
}
if (handles.some((handle) => handle.dragging() && handle.altKey === false)) {
altKey = false;
}
if (altKeyCache !== altKey) {
dragStartPos = { x, y };
altKeyCache = altKey;
}
for (const handle of handles) {
if (!handle.dragging()) continue;
handle.onDrag(
handle.orientation === "horizontal" ? x - dragStartPos.x : y - dragStartPos.y,
altKey
);
}
};
var onDragEnd = (event) => {
batch(() => {
for (const handle of handles) {
if (!handle.dragging()) {
if (some(handle.hovered, handle.focused, handle.hoveredAsIntersection)) {
handle.setActive(true);
if (handle.handleCursorStyle()) {
setGlobalCursorStyle(handle.orientation);
}
}
} else {
handle.setDragging(false);
handle.onDragEnd(event);
if (!handle.hovered() && !handle.hoveredAsIntersection()) {
handle.setActive(false);
}
if (!globalHovered && handle.handleCursorStyle()) {
setGlobalCursorStyle(null);
}
}
}
});
resetResizeConstraints();
dragStartPos = null;
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("touchmove", onTouchMove);
window.removeEventListener("pointerup", onDragEnd);
window.removeEventListener("touchend", onDragEnd);
window.removeEventListener("contextmenu", onDragEnd);
};
// src/Handle.tsx
import { dataIf } from "@corvu/utils";
import { mergeRefs } from "@corvu/utils/reactivity";
var ResizableHandle = (props) => {
const defaultedProps = mergeProps(
{
startIntersection: true,
endIntersection: true,
altKey: true
},
props
);
const [localProps, otherProps] = splitProps(defaultedProps, [
"startIntersection",
"endIntersection",
"altKey",
"onHandleDragStart",
"onHandleDrag",
"onHandleDragEnd",
"contextId",
"ref",
"style",
"disabled",
"children",
"onMouseEnter",
"onMouseLeave",
"onKeyDown",
"onKeyUp",
"onFocus",
"onBlur",
"onPointerDown"
]);
const [ref, setRef] = createSignal(null);
const [hoveredAsIntersection, setHoveredAsIntersection] = createSignal(false);
const [hovered, setHovered] = createSignal(null);
const [focused, setFocused] = createSignal(false);
const [active, setActive] = createSignal(false);
const [dragging, setDragging] = createSignal(false);
const [startIntersection, setStartIntersection] = createSignal(
null
);
const [endIntersection, setEndIntersection] = createSignal(
null
);
const context = createMemo(
() => useInternalResizableContext(localProps.contextId)
);
const ariaInformation = createMemo(() => {
const handle = ref();
if (!handle) {
return void 0;
}
const panels = context().panels();
const [precidingPanels, followingPanels] = splitPanels({
panels,
focusedElement: handle
});
const ariaControls = precidingPanels[precidingPanels.length - 1]?.data.id;
const ariaValueMax = followingPanels.reduce(
(acc, panel) => acc - resolveSize(panel.data.minSize, context().rootSize()),
1
);
const ariaValueMin = precidingPanels.reduce(
(acc, panel) => acc + resolveSize(panel.data.minSize, context().rootSize()),
0
);
const ariaValueNow = precidingPanels.reduce(
(acc, panel) => acc + resolveSize(panel.size(), context().rootSize()),
0
);
return {
ariaControls,
ariaValueMax: fixToPrecision(ariaValueMax),
ariaValueMin: fixToPrecision(ariaValueMin),
ariaValueNow: fixToPrecision(ariaValueNow)
};
});
let globalHandleCallbacks = null;
createEffect(() => {
if (localProps.disabled === true) return;
const element = ref();
if (!element) return;
const globalHandle = {
element,
orientation: context().orientation(),
handleCursorStyle: context().handleCursorStyle,
altKey: localProps.altKey,
startIntersection: {
handle: startIntersection,
setHandle: (handle) => {
if (localProps.startIntersection !== true) return;
setStartIntersection(handle);
}
},
endIntersection: {
handle: endIntersection,
setHandle: (handle) => {
if (localProps.endIntersection !== true) return;
setEndIntersection(handle);
}
},
hovered,
focused,
hoveredAsIntersection,
setHoveredAsIntersection,
active,
setActive,
dragging,
setDragging,
onDrag: (delta, altKey) => {
if (localProps.onHandleDrag !== void 0) {
const dragEvent = new CustomEvent("drag", {
cancelable: true
});
localProps.onHandleDrag(dragEvent);
if (dragEvent.defaultPrevented) return;
}
context().onDrag(element, delta, altKey);
},
onDragEnd: (event) => {
localProps.onHandleDragEnd?.(event);
context().onDragEnd();
}
};
globalHandleCallbacks = registerHandle(globalHandle);
onCleanup(() => {
unregisterHandle(globalHandle);
globalHandleCallbacks = null;
});
});
createEffect(
on(hovered, () => {
globalHandleCallbacks?.onHoveredChange(hovered());
})
);
const onMouseEnter = (e) => {
if (callEventHandler(localProps.onMouseEnter, e) || localProps.disabled === true)
return;
setHovered("handle");
};
const onMouseLeave = (e) => {
if (callEventHandler(localProps.onMouseLeave, e)) return;
setHovered(null);
};
const onKeyDown = (e) => {
if (callEventHandler(localProps.onKeyDown, e) || dragging()) return;
const element = ref();
if (!element) return;
const altKey = localProps.altKey === "only" || localProps.altKey !== false && e.altKey;
context().onKeyDown(element, e, altKey);
};
const onKeyUp = (e) => {
if (callEventHandler(localProps.onKeyUp, e) || e.key !== "Tab") return;
setFocused(true);
};
const onFocus = (e) => {
if (callEventHandler(localProps.onFocus, e) || hovered()) return;
setFocused(true);
setActive(true);
};
const onBlur = (e) => {
if (callEventHandler(localProps.onBlur, e)) return;
setFocused(false);
if (hovered()) return;
setActive(false);
};
const onPointerDown = (e) => {
if (callEventHandler(localProps.onPointerDown, e)) return;
if (callEventHandler(localProps.onHandleDragStart, e)) return;
const targetElement = e.target;
let target = "handle";
if (targetElement.hasAttribute(
"data-corvu-resizable-handle-start-intersection"
)) {
target = "startIntersection";
}
if (targetElement.hasAttribute("data-corvu-resizable-handle-end-intersection")) {
target = "endIntersection";
}
globalHandleCallbacks?.onDragStart(e, target);
};
return <Dynamic
as="button"
ref={mergeRefs(setRef, localProps.ref)}
style={combineStyle(
{
position: "relative",
cursor: context().handleCursorStyle() ? "inherit" : void 0,
"touch-action": "none",
"flex-shrink": 0
},
localProps.style
)}
disabled={localProps.disabled}
onBlur={onBlur}
onFocus={onFocus}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onPointerDown={onPointerDown}
role="separator"
aria-controls={ariaInformation()?.ariaControls}
aria-orientation={context().orientation()}
aria-valuemax={ariaInformation()?.ariaValueMax}
aria-valuemin={ariaInformation()?.ariaValueMin}
aria-valuenow={ariaInformation()?.ariaValueNow}
data-active={dataIf(active())}
data-dragging={dataIf(dragging())}
data-orientation={context().orientation()}
data-corvu-resizable-handle=""
{...otherProps}
>
<Show when={startIntersection()}>
<div
data-corvu-resizable-handle-start-intersection
onMouseEnter={() => setHovered("startIntersection")}
onMouseLeave={(e) => {
if (ref()?.contains(e.relatedTarget) === true) {
setHovered("handle");
} else {
setHovered(null);
}
}}
style={{
position: "absolute",
"aspect-ratio": "1 / 1",
top: 0,
left: 0,
height: context().orientation() === "horizontal" ? void 0 : "100%",
width: context().orientation() === "horizontal" ? "100%" : void 0,
transform: context().orientation() === "horizontal" ? "translate3d(0, -100%, 0)" : "translate3d(-100%, 0, 0)",
"z-index": 1
}}
/>
</Show>
{localProps.children}
<Show when={endIntersection()}>
<div
data-corvu-resizable-handle-end-intersection
onMouseEnter={() => setHovered("endIntersection")}
onMouseLeave={(e) => {
if (ref()?.contains(e.relatedTarget) === true) {
setHovered("handle");
} else {
setHovered(null);
}
}}
style={{
position: "absolute",
"aspect-ratio": "1 / 1",
bottom: 0,
right: 0,
height: context().orientation() === "horizontal" ? void 0 : "100%",
width: context().orientation() === "horizontal" ? "100%" : void 0,
transform: context().orientation() === "horizontal" ? "translate3d(0, 100%, 0)" : "translate3d(100%, 0, 0)",
"z-index": 1
}}
/>
</Show>
</Dynamic>;
};
var Handle_default = ResizableHandle;
// src/Panel.tsx
import { combineStyle as combineStyle2 } from "@corvu/utils/dom";
import {
createEffect as createEffect2,
createMemo as createMemo2,
createSignal as createSignal2,
createUniqueId,
mergeProps as mergeProps2,
onCleanup as onCleanup2,
splitProps as splitProps2,
untrack
} from "solid-js";
import { dataIf as dataIf2, isFunction } from "@corvu/utils";
import { Dynamic as Dynamic2 } from "@corvu/utils/dynamic";
import createOnce from "@corvu/utils/create/once";
// src/panelContext.ts
import { createContext as createContext2, useContext as useContext2 } from "solid-js";
import {
createKeyedContext as createKeyedContext2,
useKeyedContext as useKeyedContext2
} from "@corvu/utils/create/keyedContext";
var ResizablePanelContext = createContext2();
var createResizablePanelContext = (contextId) => {
if (contextId === void 0) return ResizablePanelContext;
const context = createKeyedContext2(
`resizable-panel-${contextId}`
);
return context;
};
var useResizablePanelContext = (contextId) => {
if (contextId === void 0) {
const context2 = useContext2(ResizablePanelContext);
if (!context2) {
throw new Error(
"[corvu]: Resizable panel context not found. Make sure to call usePanelContext under <Resizable.Panel>"
);
}
return context2;
}
const context = useKeyedContext2(
`resizable-panel-${contextId}`
);
if (!context) {
throw new Error(
`[corvu]: Resizable context with id "${contextId}" not found. Make sure to call usePanelContext under <Resizable.Panel contextId="${contextId}">`
);
}
return context;
};
// src/Panel.tsx
import { mergeRefs as mergeRefs2 } from "@corvu/utils/reactivity";
var ResizablePanel = (props) => {
const defaultedProps = mergeProps2(
{
initialSize: null,
minSize: 0,
maxSize: 1,
collapsible: false,
collapsedSize: 0,
collapseThreshold: 0.05,
panelId: createUniqueId()
},
props
);
const [localProps, otherProps] = splitProps2(defaultedProps, [
"initialSize",
"minSize",
"maxSize",
"collapsible",
"collapsedSize",
"collapseThreshold",
"onResize",
"onCollapse",
"onExpand",
"contextId",
"panelId",
"ref",
"style",
"children"
]);
const [ref, setRef] = createSignal2(null);
const context = createMemo2(
() => useInternalResizableContext(localProps.contextId)
);
const [panelInstance, setPanelInstance] = createSignal2(
null
);
createEffect2(() => {
const element = ref();
if (!element) return;
const _context = context();
const instance = untrack(() => {
return _context.registerPanel({
id: localProps.panelId,
element,
initialSize: localProps.initialSize,
minSize: localProps.minSize,
maxSize: localProps.maxSize,
collapsible: localProps.collapsible,
collapsedSize: localProps.collapsedSize,
collapseThreshold: localProps.collapseThreshold,
onResize: localProps.onResize
});
});
setPanelInstance(instance);
onCleanup2(() => {
_context.unregisterPanel(instance.data.id);
});
});
const panelSize = () => {
const instance = panelInstance();
if (!instance) {
if (typeof localProps.initialSize === "number") {
return localProps.initialSize;
}
return 1;
}
return instance.size();
};
const collapsed = createMemo2((prev) => {
const instance = panelInstance();
if (localProps.collapsible !== true) {
return false;
}
const collapsed2 = instance ? instance.size() === resolveSize(localProps.collapsedSize, context().rootSize()) : false;
if (instance && prev !== collapsed2) {
if (collapsed2 && localProps.onCollapse !== void 0) {
localProps.onCollapse(instance.size());
} else if (!collapsed2 && localProps.onExpand !== void 0) {
localProps.onExpand(instance.size());
}
}
return collapsed2;
});
const resize2 = (size, strategy) => {
const instance = panelInstance();
if (!instance) {
return;
}
instance.resize(size, strategy ?? "both");
};
const collapse = (strategy) => {
const instance = panelInstance();
if (!instance) {
return;
}
instance.collapse(strategy ?? "both");
};
const expand = (strategy) => {
const instance = panelInstance();
if (!instance) {
return;
}
instance.expand(strategy ?? "both");
};
const childrenProps = {
get size() {
return panelSize();
},
get minSize() {
return localProps.minSize;
},
get maxSize() {
return localProps.maxSize;
},
get collapsible() {
return localProps.collapsible;
},
get collapsedSize() {
return localProps.collapsedSize;
},
get collapseThreshold() {
return localProps.collapseThreshold;
},
get collapsed() {
return collapsed();
},
resize: resize2,
collapse,
expand,
get panelId() {
return localProps.panelId;
}
};
const memoizedChildren = createOnce(() => localProps.children);
const resolveChildren = () => {
const children = memoizedChildren()();
if (isFunction(children)) {
return children(childrenProps);
}
return children;
};
const memoizedResizablePanel = createMemo2(() => {
const ResizablePanelContext2 = createResizablePanelContext(
localProps.contextId
);
return <ResizablePanelContext2.Provider
value={{
size: panelSize,
minSize: () => localProps.minSize,
maxSize: () => localProps.maxSize,
collapsible: () => localProps.collapsible,
collapsedSize: () => localProps.collapsedSize,
collapseThreshold: () => localProps.collapseThreshold,
collapsed,
resize: resize2,
collapse,
expand,
panelId: () => localProps.panelId
}}
>
<Dynamic2
as="div"
ref={mergeRefs2(setRef, localProps.ref)}
style={combineStyle2(
{
"flex-basis": panelSize() * 100 + "%"
},
localProps.style
)}
id={localProps.panelId}
data-collapsed={dataIf2(collapsed())}
data-expanded={dataIf2(
localProps.collapsible === true && !collapsed()
)}
data-orientation={context().orientation()}
data-corvu-resizable-panel=""
{...otherProps}
>
{untrack(() => resolveChildren())}
</Dynamic2>
</ResizablePanelContext2.Provider>;
});
return memoizedResizablePanel;
};
var Panel_default = ResizablePanel;
// src/Root.tsx
import {
combineStyle as combineStyle3,
sortByDocumentPosition
} from "@corvu/utils/dom";
import {
createEffect as createEffect3,
createMemo as createMemo3,
createSignal as createSignal3,
mergeProps as mergeProps3,
splitProps as splitProps3,
untrack as untrack2
} from "solid-js";
// src/lib/resize.ts
import "solid-js";
var getDistributablePercentage = (props) => {
let distributablePercentage = props.desiredPercentage >= 0 ? Infinity : -Infinity;
let newSizes = props.initialSizes;
for (const resizeAction of props.resizeActions) {
const desiredPercentage = resizeAction.negate !== true ? props.desiredPercentage : -props.desiredPercentage;
let [
distributedPercentagePreceding,
// eslint-disable-next-line prefer-const
distributedSizesPreceding,
// eslint-disable-next-line prefer-const
collapsedPreceding
] = distributePercentage({
desiredPercentage,
side: "preceding",
panels: resizeAction.precedingPanels,
initialSizes: newSizes,
initialSizesStartIndex: 0,
collapsible: props.collapsible,
rootSize: props.resizableData.rootSize
});
let [
distributedPercentageFollowing,
// eslint-disable-next-line prefer-const
distributedSizesFollowing,
// eslint-disable-next-line prefer-const
collapsedFollowing
] = distributePercentage({
desiredPercentage,
side: "following",
panels: resizeAction.followingPanels,
initialSizes: newSizes,
initialSizesStartIndex: resizeAction.precedingPanels.length,
collapsible: props.collapsible,
rootSize: props.resizableData.rootSize
});
if (resizeAction.negate === true) {
distributedPercentagePreceding = -distributedPercentagePreceding;
distributedPercentageFollowing = -distributedPercentageFollowing;
}
if (collapsedPreceding) {
distributedPercentageFollowing = distributedPercentagePreceding;
}
if (collapsedFollowing) {
distributedPercentagePreceding = distributedPercentageFollowing;
}
if (props.desiredPercentage >= 0) {
distributablePercentage = Math.min(
distributablePercentage,
Math.min(
distributedPercentagePreceding,
distributedPercentageFollowing
)
);
} else {
distributablePercentage = Math.max(
distributablePercentage,
Math.max(
distributedPercentagePreceding,
distributedPercentageFollowing
)
);
}
newSizes = [...distributedSizesPreceding, ...distributedSizesFollowing];
}
return distributablePercentage;
};
var distributePercentage = (props) => {
props.desiredPercentage = fixToPrecision(props.desiredPercentage);
const resizeDirection = getResizeDirection({
side: props.side,
desiredPercentage: props.desiredPercentage
});
let distributedPercentage = 0;
const distributedSizes = props.initialSizes.slice(
props.initialSizesStartIndex,
props.initialSizesStartIndex + props.panels.length
);
for (let i = props.side === "preceding" ? props.panels.length - 1 : 0; props.side === "preceding" ? i >= 0 : i < props.panels.length; props.side === "preceding" ? i-- : i++) {
const panel2 = props.panels[i];
const panelSize2 = props.initialSizes[i + props.initialSizesStartIndex];
const collapsedSize2 = resolveSize(
panel2.data.collapsedSize ?? 0,
props.rootSize
);
if (panel2.data.collapsible && panelSize2 === collapsedSize2) continue;
const availablePercentage2 = fixToPrecision(
props.desiredPercentage - distributedPercentage
);
if (availablePercentage2 === 0) break;
switch (resizeDirection) {
case "precedingDecreasing": {
const minSize2 = resolveSize(panel2.data.minSize, props.rootSize);
distributedSizes[i] = Math.max(minSize2, panelSize2 + availablePercentage2);
distributedPercentage += distributedSizes[i] - panelSize2;
break;
}
case "followingDecreasing": {
const minSize2 = resolveSize(panel2.data.minSize, props.rootSize);
distributedSizes[i] = Math.max(minSize2, panelSize2 - availablePercentage2);
distributedPercentage -= distributedSizes[i] - panelSize2;
break;
}
case "precedingIncreasing": {
const maxSize = resolveSize(panel2.data.maxSize, props.rootSize);
distributedSizes[i] = Math.min(maxSize, panelSize2 + availablePercentage2);
distributedPercentage += distributedSizes[i] - panelSize2;
break;
}
case "followingIncreasing": {
const maxSize = resolveSize(panel2.data.maxSize, props.rootSize);
distributedSizes[i] = Math.min(maxSize, panelSize2 - availablePercentage2);
distributedPercentage -= distributedSizes[i] - panelSize2;
break;
}
}
}
distributedPercentage = fixToPrecision(distributedPercentage);
if (!props.collapsible || distributedPercentage === props.desiredPercentage) {
return [distributedPercentage, distributedSizes, false];
}
const panelIndex = props.side === "preceding" ? props.panels.length - 1 : 0;
const panel = props.panels[panelIndex];
if (!panel.data.collapsible) {
return [distributedPercentage, distributedSizes, false];
}
const availablePercentage = fixToPrecision(
props.desiredPercentage - distributedPercentage
);
let collapsed = false;
const panelSize = props.initialSizes[panelIndex + props.initialSizesStartIndex];
const minSize = resolveSize(panel.data.minSize, props.rootSize);
const collapsedSize = resolveSize(
panel.data.collapsedSize ?? 0,
props.rootSize
);
const collapseThreshold = Math.min(
resolveSize(panel.data.collapseThreshold ?? 0, props.rootSize),
minSize - collapsedSize
);
const isCollapsed = panelSize === collapsedSize;
if (resizeDirection === "precedingDecreasing" && !isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) {
distributedPercentage -= distributedSizes[panelIndex] - panelSize;
distributedSizes[panelIndex] = collapsedSize;
distributedPercentage += distributedSizes[panelIndex] - panelSize;
collapsed = true;
} else if (resizeDirection === "precedingIncreasing" && isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) {
const minSize2 = resolveSize(panel.data.minSize, props.rootSize);
distributedSizes[panelIndex] = minSize2;
if (Math.abs(availablePercentage) >= minSize2 - collapsedSize) {
const maxSize = resolveSize(panel.data.maxSize, props.rootSize);
distributedSizes[panelIndex] = Math.min(
maxSize,
panelSize + availablePercentage
);
} else {
collapsed = true;
}
distributedPercentage += distributedSizes[panelIndex] - panelSize;
} else if (resizeDirection === "followingDecreasing" && !isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) {
distributedPercentage += distributedSizes[panelIndex] - panelSize;
distributedSizes[panelIndex] = collapsedSize;
distributedPercentage -= distributedSizes[panelIndex] - panelSize;
collapsed = true;
} else if (resizeDirection === "followingIncreasing" && isCollapsed && Math.abs(availablePercentage) >= collapseThreshold) {
const minSize2 = resolveSize(panel.data.minSize, props.rootSize);
distributedSizes[panelIndex] = minSize2;
if (Math.abs(availablePercentage) >= minSize2 - collapsedSize) {
const maxSize = resolveSize(panel.data.maxSize, props.rootSize);
distributedSizes[panelIndex] = Math.min(
maxSize,
panelSize - availablePercentage
);
} else {
collapsed = true;
}
distributedPercentage -= distributedSizes[panelIndex] - panelSize;
}
return [distributedPercentage, distributedSizes, collapsed];
};
var getResizeDirection = (props) => {
switch (props.side) {
case "preceding":
return props.desiredPercentage >= 0 ? "precedingIncreasing" : "precedingDecreasing";
case "following":
return props.desiredPercentage >= 0 ? "followingDecreasing" : "followingIncreasing";
}
};
var resize = (props) => {
let newSizes = props.initialSizes;
for (const resizeAction of props.resizeActions) {
const [, distributedSizesPreceding] = distributePercentage({
desiredPercentage: resizeAction.deltaPercentage,
side: "preceding",
panels: resizeAction.precedingPanels,
initialSizes: newSizes,
initialSizesStartIndex: 0,
collapsible: props.collapsible,
rootSize: props.resizableData.rootSize
});
const [, distributedSizesFollowing] = distributePercentage({
desiredPercentage: resizeAction.deltaPercentage,
side: "following",
panels: resizeAction.followingPanels,
initialSizes: newSizes,
initialSizesStartIndex: resizeAction.precedingPanels.length,
collapsible: props.collapsible,
rootSize: props.resizableData.rootSize
});
newSizes = [...distributedSizesPreceding, ...distributedSizesFollowing];
}
newSizes = newSizes.map(fixToPrecision);
const totalSize = newSizes.reduce((totalSize2, size) => totalSize2 + size, 0);
if (totalSize !== 1) {
const offset = totalSize - 1;
const offsetPerPanel = offset / newSizes.length;
newSizes = newSizes.map((size) => size - offsetPerPanel);
}
props.resizableData.setSizes(newSizes.map(fixToPrecision));
};
var resizePanel = (props) => {
let [precedingPanels, followingPanels] = splitPanels({
panels: props.panels,
focusedElement: props.panel.data.element
});
const panelIndex = props.panels.indexOf(props.panel);
if (panelIndex === 0) {
props.strategy = "following";
} else if (panelIndex === props.panels.length - 1) {
props.strategy = "preceding";
}
if (props.strategy === "both") {
const precedingPanelsIncluding = [...precedingPanels, props.panel];
const followingPanelsIncluding = [props.panel, ...followingPanels];
const distributablePercentage = getDistributablePercentage({
desiredPercentage: props.deltaPercentage / 2,
initialSizes: props.initialSizes,
collapsible: true,
resizeActions: [
{
precedingPanels: precedingPanelsIncluding,
followingPanels
},
{
precedingPanels,
followingPanels: followingPanelsIncluding,
negate: true
}
],
resizableData: {
rootSize: props.resizableData.rootSize
}
});
resize({
initialSizes: props.initialSizes,
collapsible: true,
resizeActions: [
{
precedingPanels: precedingPanelsIncluding,
followingPanels,
deltaPercentage: distributablePercentage
},
{
precedingPanels,
followingPanels: followingPanelsIncluding,
deltaPercentage: -distributablePercentage
}
],
resizableData: props.resizableData
});
} else {
precedingPanels = props.strategy === "preceding" ? precedingPanels : [...precedingPanels, props.panel];
followingPanels = props.strategy === "following" ? followingPanels : [props.panel, ...followingPanels];
if (props.strategy === "preceding") {
props.deltaPercentage = -props.deltaPercentage;
}
const distributablePercentage = getDistributablePercentage({
desiredPercentage: props.deltaPercentage,
initialSizes: props.initialSizes,
collapsible: props.collapsible,
resizeActions: [
{
precedingPanels,
followingPanels
}
],
resizableData: {
rootSize: props.resizableData.rootSize
}
});
resize({
initialSizes: props.initialSizes,
collapsible: true,
resizeActions: [
{
precedingPanels,
followingPanels,
deltaPercentage: distributablePercentage
}
],
resizableData: props.resizableData
});
}
};
var deltaResize = (props) => {
if (props.altKey && props.panels.length > 2) {
let panelIndex = props.panels.filter(
(panel2) => !!(props.handle.compareDocumentPosition(panel2.data.element) & Node.DOCUMENT_POSITION_PRECEDING)
).length - 1;
const isPrecedingHandle = panelIndex === 0;
if (isPrecedingHandle) {
panelIndex++;
props.deltaPercentage = -props.deltaPercentage;
}
const panel = props.panels[panelIndex];
const panelSize = props.initialSizes[panelIndex];
const minDelta = resolveSize(panel.data.minSize, props.resizableData.rootSize) - panelSize;
const maxDelta = resolveSize(panel.data.maxSize, props.resizableData.rootSize) - panelSize;
const cappedDeltaPercentage = Math.max(minDelta, Math.min(props.deltaPercentage * 2, maxDelta)) / 2;
const [precedingPanels, followingPanels] = splitPanels({
panels: props.panels,
focusedElement: panel.data.element
});
const precedingPanelsIncluding = [...precedingPanels, panel];
const followingPanelsIncluding = [panel, ...followingPanels];
const distributablePercentage = getDistributablePercentage({
desiredPercentage: cappedDeltaPercentage,
initialSizes: props.initialSizes,
collapsible: false,
resizeActions: [
{
precedingPanels: precedingPanelsIncluding,
followingPanels
},
{
precedingPanels,
followingPanels: followingPanelsIncluding,
negate: true
}
],
resizableData: {
rootSize: props.resizableData.rootSize
}
});
if (props.resizableData.handleCursorStyle === true) {
handleResizeConstraints({
orientation: props.resizableData.orientation,
desiredPercentage: props.deltaPercentage,
distributablePercentage,
revertConstraints: isPrecedingHandle
});
}
resize({
initialSizes: props.initialSizes,
collapsible: false,
resizeActions: [
{
precedingPanels: precedingPanelsIncluding,
followingPanels,
deltaPercentage: distributablePercentage
},
{
precedingPanels,
followingPanels: followingPanelsIncluding,
deltaPercentage: -distributablePercentage
}
],
resizableData: props.resizableData
});
} else {
const [precedingPanels, followingPanels] = splitPanels({
panels: props.panels,
focusedElement: props.handle
});
const distributablePercentage = getDistributablePercentage({
desiredPercentage: props.deltaPercentage,
initialSizes: props.initialSizes,
collapsible: true,
resizeActions: [
{
precedingPanels,
followingPanels
}
],
resizableData: {
rootSize: props.resizableData.rootSize
}
});
resize({
initialSizes: props.initialSizes,
collapsible: true,
resizeActions: [
{
precedingPanels,
followingPanels,
deltaPercentage: distributablePercentage
}
],
resizableData: props.resizableData
});
if (props.resizableData.handleCursorStyle) {
const fixedDesiredPercentage = fixToPrecision(props.deltaPercentage);
const fixedDistributablePercentage = fixToPrecision(
distributablePercentage
);
let betweenCollapse = false;
const precedingPanel = precedingPanels[precedingPanels.length - 1];
if (precedingPanel.data.collapsible) {
const precedingCollapsedSize = resolveSize(
precedingPanel.data.collapsedSize ?? 0,
props.resizableData.rootSize
);
if (precedingPanel.size() === precedingCollapsedSize && fixedDesiredPercentage > fixedDistributablePercentage || precedingPanel.size() !== precedingCollapsedSize && fixedDesiredPercentage < fixedDistributablePercentage) {
betweenCollapse = true;
}
}
const followingPanel = followingPanels[0];
if (followingPanel.data.collapsible) {
const followingCollapsedSize = resolveSize(
followingPanel.data.collapsedSize ?? 0,
props.resizableData.rootSize
);
if (followingPanel.size() === followingCollapsedSize && fixedDesiredPercentage < fixedDistributablePercentage || followingPanel.size() !== followingCollapsedSize && fixedDesiredPercentage > fixedDistributablePercentage) {
betweenCollapse = true;
}
}
handleResizeConstraints({
orientation: props.resizableData.orientation,
desiredPercentage: props.deltaPercentage,
distributablePercentage,
betweenCollapse
});
}
}
};
// src/Root.tsx
import { Dynamic as Dynamic3 } from "@corvu/utils/dynamic";
import { isFunction as isFunction2 } from "@corvu/utils";
import createControllableSignal from "@corvu/utils/create/controllableSignal";
import createOnce2 from "@corvu/utils/create/once";
import createSize from "@corvu/utils/create/size";
import { mergeRefs as mergeRefs3 } from "@corvu/utils/reactivity";
var ResizableRoot = (props) => {
const defaultedProps = mergeProps3(
{
orientation: "horizontal",
initialSizes: [],
keyboardDelta: 0.1,
handleCursorStyle: true
},
props
);
const [localProps, otherProps] = splitProps3(defaultedProps, [
"orientation",
"sizes",
"onSizesChange",
"initialSizes",
"keyboardDelta",
"handleCursorStyle",
"contextId",
"ref",
"style",
"children"
]);
const [sizes, setSizes] = createControllableSignal({
value: () => localProps.sizes,
initialValue: [],
onChange: localProps.onSizesChange
});
const [ref, setRef] = createSignal3(null);
const rootSize = createSize({
element: ref,
dimension: () => localProps.orientation === "horizontal" ? "width" : "height"
});
const [panels, setPanels] = createSignal3([]);
const sizesToIds = [];
const registerPanel = (panelData) => {
const _panels = panels();
const panelIndex = _panels.filter(
(panel2) => !!(panelData.element.compareDocumentPosition(panel2.data.element) & Node.DOCUMENT_POSITION_PRECEDING)
).length;
const idExists = sizesToIds[panelIndex] === void 0 || sizesToIds[panelIndex] === panelData.id;
const sizeExists = sizes()[panelIndex] !== void 0;
let panelSize = null;
if (panelData.initialSize !== null) {
panelSize = resolveSize(panelData.initialSize, rootSize());
} else if (localProps.initialSizes[panelIndex] !== void 0 && idExists) {
panelSize = resolveSize(localProps.initialSizes[panelIndex], rootSize());
}
panelSize = panelSize ?? 0.5;
setSizes((sizes2) => {
let newSizes = [...sizes2];
const previousTotalSize = newSizes.reduce(
(totalSize, size) => totalSize + size,
0
);
if ((idExists && !sizeExists || !idExists) && previousTotalSize === 1) {
const offsetPerPanel = panelSize / newSizes.length;
newSizes = newSizes.map((size) => size - offsetPerPanel);
}
if (idExists) {
if (!sizeExists) {
newSizes[panelIndex] = panelSize;
}
sizesToIds[panelIndex] = panelData.id;
} else {
newSizes.splice(panelIndex, 0, panelSize);
sizesToIds.splice(panelIndex, 0, panelData.id);
}
return newSizes;
});
const panelSizeMemo = createMemo3(() => {
const index = sizesToIds.indexOf(panelData.id);
return sizes()[index];
});
createEffect3(() => panelData.onResize?.(panelSizeMemo()));
const panel = {
data: panelData,
size: panelSizeMemo,
resize: (size, strategy) => resize2(sizesToIds.indexOf(panelData.id), size, strategy),
collapse: (strategy) => collapse(sizesToIds.indexOf(panelData.id), strategy),
expand: (strategy) => expand(sizesToIds.indexOf(panelData.id), strategy)
};
setPanels((panels2) => {
const newPanels = [...panels2];
newPanels.push(panel);
newPanels.sort(
(a, b) => sortByDocumentPosition(a.data.element, b.data.element)
);
return newPanels;
});
return panel;
};
const unregisterPanel = (id) => {
setPanels((panels2) => panels2.filter((panel) => panel.data.id !== id));
const panelSizeIndex = sizesToIds.indexOf(id);
sizesToIds.splice(panelSizeIndex, 1);
setSizes((sizes2) => {
let newSizes = [...sizes2];
newSizes.splice(panelSizeIndex, 1);
const totalSize = newSizes.reduce(
(totalSize2, size) => totalSize2 + size,
0
);
const offset = totalSize - 1;
const offsetPerPanel = offset / newSizes.length;
newSizes = newSizes.map((size) => size + offsetPerPanel);
return newSizes;
});
};
createEffect3(() => {
if (localProps.onSizesChange !== void 0) {
localProps.onSizesChange(sizes());
}
});
const resize2 = (panelIndex, size, strategy) => {
untrack2(() => {
const panel = panels()[panelIndex];
if (!panel) return;
const minSize = resolveSize(panel.data.minSize, rootSize());
const maxSize = resolveSize(panel.data.maxSize, rootSize());
const newSize = resolveSize(size, rootSize());
const allowedSize = Math.max(minSize, Math.min(newSize, maxSize));
const deltaPercentage = allowedSize - sizes()[panelIndex];
resizePanel({
deltaPercentage,
strategy: strategy ?? "both",
panel,
panels: panels(),
initialSizes: panels().map((panel2) => panel2.size()),
collapsible: false,
resizableData: {
rootSize: rootSize(),
orientation: localProps.orientation,
setSizes
}
});
});
};
const collapse = (panelIndex, strategy) => {
untrack2(() => {
const panel = panels()[panelIndex];
if (!panel) return;
const panelSize = sizes()[panelIndex];
const collapsedSize = resolveSize(
panel.data.collapsedSize ?? 0,
rootSize()
);
if (!panel.data.collapsible || panelSize === collapsedSize) return;
const deltaPercentage = collapsedSize - panelSize;
resizePanel({
deltaPercentage,
strategy: strategy ?? "both",
panel,
panels: panels(),
initialSizes: panels().map((panel2) => panel2.size()),
collapsible: true,
resizableData: {
rootSize: rootSize(),
orientation: localProps.orientation,
setSizes
}
});
});
};
const expand = (panelIndex, strategy) => {
untrack2(() => {
const panel = panels()[panelIndex];
if (!panel) return;
const panelSize = sizes()[panelIndex];
const collapsedSize = resolveSize(
panel.data.collapsedSize ?? 0,
rootSize()
);
if (!panel.data.collapsible || panelSize !== collapsedSize) return;
const minSize = resolveSize(panel.data.minSize, rootSize());
const deltaPercentage = minSize - panelSize;
resizePanel({
deltaPercentage,
strategy: strategy ?? "both",
panel,
panels: panels(),
i