phase 5
This commit is contained in:
155
ui/framework/src/components/SplitPane.vue
Normal file
155
ui/framework/src/components/SplitPane.vue
Normal 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>
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user