mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 16:39:39 +02:00
- Add transparent overlay during resize to capture all mouse events - Disable iframe pointer-events while dragging to prevent event stealing - Make PanelControls always visible for better UX - Fix TypeScript type for dividerPosition state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
203 lines
4.5 KiB
Svelte
203 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* ResizeHandle Component
|
|
* Draggable divider for resizing split panels.
|
|
*/
|
|
|
|
import { DIVIDER_CONSTRAINTS } from '../types.js';
|
|
|
|
interface Props {
|
|
position: number;
|
|
onResize: (position: number) => void;
|
|
onReset: () => void;
|
|
onDragStateChange?: (isDragging: boolean) => void;
|
|
}
|
|
|
|
let { position, onResize, onReset, onDragStateChange }: Props = $props();
|
|
|
|
let isDragging = $state(false);
|
|
|
|
function setDragging(value: boolean) {
|
|
isDragging = value;
|
|
onDragStateChange?.(value);
|
|
}
|
|
let containerRef: HTMLElement | null = null;
|
|
|
|
function handleMouseDown(event: MouseEvent) {
|
|
event.preventDefault();
|
|
setDragging(true);
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
if (!containerRef) return;
|
|
|
|
const container = containerRef.closest('.split-pane-container');
|
|
if (!container) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const newPosition = ((e.clientX - rect.left) / rect.width) * 100;
|
|
|
|
const clamped = Math.max(
|
|
DIVIDER_CONSTRAINTS.MIN,
|
|
Math.min(DIVIDER_CONSTRAINTS.MAX, newPosition)
|
|
);
|
|
|
|
onResize(clamped);
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
setDragging(false);
|
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
};
|
|
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
}
|
|
|
|
function handleTouchStart(event: TouchEvent) {
|
|
event.preventDefault();
|
|
setDragging(true);
|
|
|
|
const handleTouchMove = (e: TouchEvent) => {
|
|
if (!containerRef || !e.touches[0]) return;
|
|
|
|
const container = containerRef.closest('.split-pane-container');
|
|
if (!container) return;
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
const newPosition = ((e.touches[0].clientX - rect.left) / rect.width) * 100;
|
|
|
|
const clamped = Math.max(
|
|
DIVIDER_CONSTRAINTS.MIN,
|
|
Math.min(DIVIDER_CONSTRAINTS.MAX, newPosition)
|
|
);
|
|
|
|
onResize(clamped);
|
|
};
|
|
|
|
const handleTouchEnd = () => {
|
|
setDragging(false);
|
|
document.removeEventListener('touchmove', handleTouchMove);
|
|
document.removeEventListener('touchend', handleTouchEnd);
|
|
};
|
|
|
|
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
document.addEventListener('touchend', handleTouchEnd);
|
|
}
|
|
|
|
function handleDoubleClick() {
|
|
onReset();
|
|
}
|
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
|
const step = event.shiftKey ? 10 : 2;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowLeft':
|
|
event.preventDefault();
|
|
onResize(Math.max(DIVIDER_CONSTRAINTS.MIN, position - step));
|
|
break;
|
|
case 'ArrowRight':
|
|
event.preventDefault();
|
|
onResize(Math.min(DIVIDER_CONSTRAINTS.MAX, position + step));
|
|
break;
|
|
case 'Home':
|
|
event.preventDefault();
|
|
onResize(DIVIDER_CONSTRAINTS.DEFAULT);
|
|
break;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div
|
|
bind:this={containerRef}
|
|
class="resize-handle"
|
|
class:dragging={isDragging}
|
|
role="separator"
|
|
aria-orientation="vertical"
|
|
aria-valuenow={position}
|
|
aria-valuemin={DIVIDER_CONSTRAINTS.MIN}
|
|
aria-valuemax={DIVIDER_CONSTRAINTS.MAX}
|
|
tabindex="0"
|
|
onmousedown={handleMouseDown}
|
|
ontouchstart={handleTouchStart}
|
|
ondblclick={handleDoubleClick}
|
|
onkeydown={handleKeyDown}
|
|
>
|
|
<div class="handle-line"></div>
|
|
<div class="handle-grip">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.resize-handle {
|
|
position: relative;
|
|
width: 6px;
|
|
cursor: col-resize;
|
|
background: transparent;
|
|
transition: background 0.15s ease;
|
|
z-index: 10;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.resize-handle:hover,
|
|
.resize-handle.dragging {
|
|
background: var(--color-primary, #3b82f6);
|
|
background: linear-gradient(
|
|
to bottom,
|
|
transparent 0%,
|
|
var(--color-primary, #3b82f6) 20%,
|
|
var(--color-primary, #3b82f6) 80%,
|
|
transparent 100%
|
|
);
|
|
}
|
|
|
|
.resize-handle:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.resize-handle:focus-visible {
|
|
background: var(--color-primary, #3b82f6);
|
|
}
|
|
|
|
.handle-line {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 50%;
|
|
width: 1px;
|
|
background: var(--color-border, rgba(255, 255, 255, 0.1));
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.resize-handle:hover .handle-line,
|
|
.resize-handle.dragging .handle-line {
|
|
background: transparent;
|
|
}
|
|
|
|
.handle-grip {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
|
|
.resize-handle:hover .handle-grip,
|
|
.resize-handle.dragging .handle-grip {
|
|
opacity: 1;
|
|
}
|
|
|
|
.handle-grip span {
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
}
|
|
</style>
|