fix(desktop): refine panel split physics

Signed-off-by: cynfria <yescynthia@gmail.com>
This commit is contained in:
cynfria
2026-08-27 16:47:10 -07:00
parent 0f8bde696d
commit a3aff2cd1d
2 changed files with 30 additions and 35 deletions

View File

@@ -1009,6 +1009,8 @@ function MessagesView() {
cell: HTMLElement;
x: number;
y: number;
detachedAtStart: boolean;
hasBroken: boolean;
} | null>(null);
const beginPanelDrag = (
@@ -1030,6 +1032,8 @@ function MessagesView() {
cell,
x: current.x,
y: current.y,
detachedAtStart: current.detached,
hasBroken: current.detached,
};
cell.classList.add("is-pulling", "is-active-drag");
};
@@ -1041,15 +1045,20 @@ function MessagesView() {
endPanelDrag(event);
return;
}
drag.x = Math.max(-340, Math.min(340, drag.originX + event.clientX - drag.startX));
drag.y = Math.max(-190, Math.min(190, drag.originY + event.clientY - drag.startY));
const rawX = Math.max(-340, Math.min(340, drag.originX + event.clientX - drag.startX));
const rawY = Math.max(-190, Math.min(190, drag.originY + event.clientY - drag.startY));
const rawDistance = Math.hypot(rawX, rawY);
if (!drag.detachedAtStart && rawDistance > 112) drag.hasBroken = true;
const resistance = drag.detachedAtStart || drag.hasBroken
? 1
: 0.76 + Math.min(rawDistance / 112, 1) * 0.12;
drag.x = rawX * resistance;
drag.y = rawY * resistance;
const distance = Math.hypot(drag.x, drag.y);
drag.cell.style.setProperty("--panel-x", `${drag.x}px`);
drag.cell.style.setProperty("--panel-y", `${drag.y}px`);
drag.cell.style.setProperty("--tether-distance", `${Math.min(distance, 126)}px`);
drag.cell.style.setProperty("--tether-angle", `${Math.atan2(drag.y, drag.x)}rad`);
drag.cell.style.setProperty("--tether-squeeze", String(Math.max(0.28, 1 - distance / 170)));
drag.cell.classList.toggle("is-breaking", distance > 94);
drag.cell.style.setProperty("--panel-tilt", `${Math.max(-1.2, Math.min(1.2, (event.clientX - drag.startX) * 0.012))}deg`);
drag.cell.classList.toggle("is-breaking", drag.hasBroken);
};
const endPanelDrag = (event: ReactPointerEvent<HTMLElement>) => {
@@ -1059,12 +1068,13 @@ function MessagesView() {
event.currentTarget.releasePointerCapture(event.pointerId);
}
const distance = Math.hypot(drag.x, drag.y);
const shouldAttach = distance < (layouts[drag.id].detached ? 54 : 72);
const shouldAttach = drag.detachedAtStart ? distance < 54 : !drag.hasBroken;
const next = shouldAttach
? { x: 0, y: 0, detached: false }
: { x: drag.x, y: drag.y, detached: true };
drag.cell.style.setProperty("--panel-x", `${next.x}px`);
drag.cell.style.setProperty("--panel-y", `${next.y}px`);
drag.cell.style.setProperty("--panel-tilt", "0deg");
drag.cell.classList.remove("is-pulling", "is-breaking", "is-active-drag");
setLayouts((current) => ({ ...current, [drag.id]: next }));
dragRef.current = null;
@@ -1073,6 +1083,7 @@ function MessagesView() {
const panelStyle = (id: MessagePanelId) => ({
"--panel-x": `${layouts[id].x}px`,
"--panel-y": `${layouts[id].y}px`,
"--panel-tilt": "0deg",
}) as CSSProperties;
const dragHandlers = (id: MessagePanelId) => ({
@@ -1084,9 +1095,8 @@ function MessagesView() {
return (
<section className="messages-stage" aria-label="Composable messages workspace">
<div className={`messages-board${Object.values(layouts).some((panel) => panel.detached) ? " has-detached-panel" : ""}`}>
<div className={`messages-board${Object.values(layouts).some((panel) => panel.detached) ? " has-detached-panel" : ""}${layouts.sidebar.detached ? " sidebar-is-detached" : ""}${layouts.chat.detached ? " chat-is-detached" : ""}${layouts.thread.detached ? " thread-is-detached" : ""}`}>
<div className={`message-panel-cell sidebar-cell${layouts.sidebar.detached ? " is-detached" : ""}`} style={panelStyle("sidebar")}>
<div className="liquid-tether" aria-hidden="true" />
<aside className="message-panel channel-panel">
<div className="panel-drag-handle channel-search-wrap" {...dragHandlers("sidebar")}>
<Search aria-hidden="true" size={18} strokeWidth={1.7} />
@@ -1104,7 +1114,6 @@ function MessagesView() {
</div>
<div className={`message-panel-cell chat-cell${layouts.chat.detached ? " is-detached" : ""}`} style={panelStyle("chat")}>
<div className="liquid-tether" aria-hidden="true" />
<article className="message-panel chat-panel">
<header className="panel-drag-handle chat-header" {...dragHandlers("chat")}>
<div><LockKeyhole aria-hidden="true" size={18} strokeWidth={1.7} /><strong>buzz-interface-squad</strong></div>
@@ -1120,7 +1129,6 @@ function MessagesView() {
</div>
<div className={`message-panel-cell thread-cell${layouts.thread.detached ? " is-detached" : ""}`} style={panelStyle("thread")}>
<div className="liquid-tether" aria-hidden="true" />
<aside className="message-panel thread-panel">
<header className="panel-drag-handle thread-header" {...dragHandlers("thread")}><div><PanelRightOpen aria-hidden="true" size={18} strokeWidth={1.7} /><strong>Thread</strong></div><button type="button" aria-label="Close thread"><X aria-hidden="true" size={18} strokeWidth={1.7} /></button></header>
<div className="thread-content">

View File

@@ -161,9 +161,8 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif
.message-panel-cell {
--panel-x: 0px;
--panel-y: 0px;
--tether-distance: 0px;
--tether-angle: 0rad;
--tether-squeeze: 1;
--panel-tilt: 0deg;
--panel-scale: 1;
position: relative;
min-width: 0;
min-height: 0;
@@ -180,7 +179,7 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif
overflow: hidden;
background: rgba(255,255,255,.94);
border: 1px solid rgba(255,255,255,.96);
transform: translate3d(var(--panel-x), var(--panel-y), 0);
transform: translate3d(var(--panel-x), var(--panel-y), 0) rotate(var(--panel-tilt)) scale(var(--panel-scale));
transition:
transform 480ms cubic-bezier(.22,1,.36,1),
border-radius 260ms ease-out,
@@ -190,33 +189,21 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif
.sidebar-cell .message-panel { border-radius: 28px 0 0 28px; }
.chat-cell .message-panel { border-radius: 0; border-left-color: rgba(38,38,38,.09); }
.thread-cell .message-panel { border-radius: 0 28px 28px 0; border-left-color: rgba(38,38,38,.09); }
.messages-board.sidebar-is-detached .chat-cell:not(.is-detached) .message-panel { border-radius: 28px 0 0 28px; }
.messages-board.thread-is-detached .chat-cell:not(.is-detached) .message-panel { border-radius: 0 28px 28px 0; }
.messages-board.sidebar-is-detached.thread-is-detached .chat-cell:not(.is-detached) .message-panel,
.messages-board.chat-is-detached .sidebar-cell:not(.is-detached) .message-panel,
.messages-board.chat-is-detached .thread-cell:not(.is-detached) .message-panel { border-radius: 28px; }
.message-panel-cell.is-detached .message-panel,
.message-panel-cell.is-breaking .message-panel {
border-radius: 28px;
box-shadow: 0 18px 44px rgba(53,76,86,.16), inset 0 1px 0 rgba(255,255,255,.9);
}
.message-panel-cell.is-pulling { --panel-scale: .997; }
.message-panel-cell.is-pulling.is-breaking { --panel-scale: 1; }
.message-panel-cell.is-pulling .message-panel { transition: border-radius 180ms ease-out, box-shadow 180ms ease-out; }
.panel-drag-handle { cursor: grab; touch-action: none; user-select: none; }
.message-panel-cell.is-pulling .panel-drag-handle { cursor: grabbing; }
.liquid-tether {
position: absolute;
z-index: 1;
left: 50%;
top: 50%;
width: calc(var(--tether-distance) + 88px);
height: 112px;
border-radius: 999px;
background: rgba(255,255,255,.92);
opacity: 0;
transform-origin: 0 50%;
transform: translateY(-50%) rotate(var(--tether-angle)) scaleY(var(--tether-squeeze));
transition: opacity 90ms ease, transform 120ms ease-out;
pointer-events: none;
filter: blur(1px);
}
.message-panel-cell.is-pulling .liquid-tether { opacity: 1; transition: none; }
.message-panel-cell.is-pulling.is-breaking .liquid-tether { opacity: 0; transform: translateY(-50%) rotate(var(--tether-angle)) scaleY(.08); transition: opacity 100ms ease-out, transform 160ms ease-out; }
.channel-panel { padding: 10px; }
.channel-search-wrap { display: flex; height: 42px; align-items: center; gap: 10px; padding: 0 12px; border-radius: 13px; background: rgba(243,243,243,.8); }
.channel-search-wrap svg { flex: 0 0 auto; color: rgba(38,38,38,.5); }
@@ -453,7 +440,7 @@ body { background: #e6f2f5; color: var(--ink); font-family: Inter, ui-sans-serif
@media (prefers-reduced-motion: reduce) {
.wrapped-labels { will-change: auto; }
.orbit-system, .task-detail-panel { transition-duration: 1ms; animation-duration: 1ms; }
.message-panel, .liquid-tether { transition: none; }
.message-panel { transition: none; }
.response-composer { transition: none; }
.response-composer textarea { animation: none; }
.project-name-pill { transition: none; }