This commit is contained in:
2026-03-28 00:09:47 -03:00
parent 886720c3ce
commit 49da927da0
3 changed files with 272 additions and 230 deletions

View File

@@ -0,0 +1,155 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = withDefaults(defineProps<{
/** Split direction */
direction?: 'horizontal' | 'vertical'
/** Initial size of the sized pane (px or flex ratio) */
initialSize?: number
/** Size mode: 'px' = sized pane fixed in pixels, 'ratio' = flex ratio */
sizeMode?: 'px' | 'ratio'
/** Which pane is sized: 'first' or 'second'. Default: 'first'. */
anchor?: 'first' | 'second'
/** Min size (px in px-mode, ratio in ratio-mode) */
min?: number
/** Max size (px in px-mode, ratio in ratio-mode) */
max?: number
/** Whether the divider is draggable */
resizable?: boolean
}>(), {
direction: 'horizontal',
initialSize: 1,
sizeMode: 'ratio',
anchor: 'first',
min: 0.1,
max: 10,
resizable: true,
})
const size = ref(props.initialSize)
const dragging = ref(false)
let startPos = 0
function onPointerDown(e: PointerEvent) {
if (!props.resizable) return
dragging.value = true
startPos = props.direction === 'horizontal' ? e.clientX : e.clientY
const el = e.target as HTMLElement
el.setPointerCapture(e.pointerId)
}
function onPointerMove(e: PointerEvent) {
if (!dragging.value) return
const currentPos = props.direction === 'horizontal' ? e.clientX : e.clientY
let delta = currentPos - startPos
startPos = currentPos
// Dragging right/down grows first pane, shrinks second.
// If anchor is 'second', invert so dragging grows the second pane.
if (props.anchor === 'second') delta = -delta
if (props.sizeMode === 'px') {
size.value = Math.max(props.min, Math.min(props.max, size.value + delta))
} else {
const scale = props.direction === 'horizontal' ? 0.01 : 0.02
size.value = Math.max(props.min, Math.min(props.max, size.value + delta * scale))
}
}
function onPointerUp() {
dragging.value = false
}
const isHorizontal = computed(() => props.direction === 'horizontal')
const sizedStyle = computed(() => {
if (props.sizeMode === 'px') {
return isHorizontal.value
? { width: size.value + 'px', flexShrink: '0' }
: { height: size.value + 'px', flexShrink: '0' }
}
return { flex: String(size.value) }
})
const flexStyle = computed(() => ({ flex: '1' }))
const firstStyle = computed(() => props.anchor === 'first' ? sizedStyle.value : flexStyle.value)
const secondStyle = computed(() => props.anchor === 'second' ? sizedStyle.value : flexStyle.value)
</script>
<template>
<div class="split-pane" :class="[direction]">
<div class="split-first" :style="firstStyle">
<slot name="first" />
</div>
<div
v-if="resizable"
class="split-divider"
:class="[direction, { dragging }]"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
/>
<div class="split-second" :style="secondStyle">
<slot name="second" />
</div>
</div>
</template>
<style scoped>
.split-pane {
display: flex;
width: 100%;
height: 100%;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.split-pane.horizontal {
flex-direction: row;
}
.split-pane.vertical {
flex-direction: column;
}
.split-first,
.split-second {
min-height: 0;
min-width: 0;
overflow: hidden;
}
/* Children must fill their pane */
.split-first > :deep(*),
.split-second > :deep(*) {
width: 100%;
height: 100%;
}
.split-divider {
flex-shrink: 0;
background: transparent;
transition: background 0.15s;
touch-action: none;
z-index: 10;
}
.split-divider:hover,
.split-divider.dragging {
background: var(--text-dim);
}
.split-divider.horizontal {
width: 4px;
cursor: col-resize;
margin: 0 -2px;
}
.split-divider.vertical {
height: 4px;
cursor: row-resize;
margin: -2px 0;
}
</style>

View File

@@ -8,6 +8,7 @@ export { useDataSource } from './composables/useDataSource'
export { default as Panel } from './components/Panel.vue'
export { default as LayoutGrid } from './components/LayoutGrid.vue'
export { default as ResizeHandle } from './components/ResizeHandle.vue'
export { default as SplitPane } from './components/SplitPane.vue'
// Renderers
export { default as LogRenderer } from './renderers/LogRenderer.vue'