Skip to main content

Layout primitives

Split Panels code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/split-panels.blade.php

@props([
    'orientation' => 'horizontal',
    'initial' => 50,
    'min' => 20,
    'max' => 80,
    'height' => 'h-96',
])

@php
    /*
    | Resizable split panels with a draggable divider (no dependencies). Drag the
    | handle to resize the two panes; `initial`, `min` and `max` are percentages.
    |
    | orientation: horizontal (side-by-side) | vertical (stacked)
    |
    | Usage:
    |   <x-ui.split-panels :initial="40" class="rounded-2xl border border-gray-200 dark:border-gray-800">
    |       <x-slot:start>...</x-slot:start>
    |       <x-slot:end>...</x-slot:end>
    |   </x-ui.split-panels>
    */
    $vertical = $orientation === 'vertical';
@endphp

<div
    x-data="{
        pos: {{ (int) $initial }},
        min: {{ (int) $min }},
        max: {{ (int) $max }},
        vertical: @js($vertical),
        dragging: false,
        start() {
            this.dragging = true;
            document.body.style.userSelect = 'none';
            document.body.style.cursor = this.vertical ? 'row-resize' : 'col-resize';
        },
        move(event) {
            if (! this.dragging) { return; }
            const rect = this.$el.getBoundingClientRect();
            const raw = this.vertical
                ? ((event.clientY - rect.top) / rect.height) * 100
                : ((event.clientX - rect.left) / rect.width) * 100;
            this.pos = Math.min(this.max, Math.max(this.min, raw));
        },
        stop() {
            this.dragging = false;
            document.body.style.userSelect = '';
            document.body.style.cursor = '';
        },
    }"
    @pointermove.window="move($event)"
    @pointerup.window="stop()"
    {{ $attributes->merge(['class' => 'relative flex w-full overflow-hidden '.($vertical ? 'flex-col' : 'flex-row').' '.$height]) }}
>
    <div
        class="min-h-0 min-w-0 overflow-auto"
        :style="vertical ? `height: ${pos}%` : `width: ${pos}%`"
    >
        {{ $start ?? '' }}
    </div>

    <div
        role="separator"
        :aria-orientation="vertical ? 'horizontal' : 'vertical'"
        @pointerdown.prevent="start()"
        class="group relative flex shrink-0 items-center justify-center bg-gray-200 transition hover:bg-brand-400 dark:bg-gray-800 dark:hover:bg-brand-500"
        :class="[
            vertical ? 'h-1.5 w-full cursor-row-resize' : 'w-1.5 cursor-col-resize',
            dragging ? (vertical ? 'bg-brand-500' : 'bg-brand-500') : '',
        ]"
    >
        <span
            class="rounded-full bg-white/70 dark:bg-gray-500/70"
            :class="vertical ? 'h-1 w-8' : 'h-8 w-1'"
        ></span>
    </div>

    <div class="min-h-0 min-w-0 flex-1 overflow-auto">
        {{ $end ?? '' }}
    </div>
</div>