GrSplitter

Пакет: @feugene/granularityядроГруппа: Утилиты

Берут, когда двум областям нужна общая граница.

Когда брать

  • двум областям нужна общая граница — дерево и контент, редактор и предпросмотр, список и деталь;
  • размер выбирает пользователь — граница двигается мышью и с клавиатуры, min/max держат края;
  • одну область сворачиваютcollapsible убирает панель целиком и возвращает обратно;
  • раскладка сохраняетсяv-model отдаёт размер, и его остаётся записать.

Когда взять другое

НужноБерите
Областей много и они на сеткеGrDashboard
Раскладка фиксированаCSS Grid и GrCard
Панель выезжает поверх контентаGrDrawer
Разделы переключаютсяGrTabs
Просто линия между блокамиGrDivider

Размер — доля, а не пиксели

modelValue — процент, который занимает первая панель. Проценты переживают смену ширины окна: раскладка, настроенная на широком мониторе, остаётся осмысленной на ноутбуке, а пиксельная ширина там же съела бы половину экрана.

Второе следствие важнее первого: доля известна до рендера, поэтому серверная разметка совпадает с клиентской и гидрация проходит чисто. Компонент меряет DOM ровно в одном месте — когда переводит координату указателя в проценты; ни клавиатура, ни Home/End, ни сворачивание замеров не требуют.

v-model не обязателен: без него сплиттер помнит размер сам (как collapsed у GrSidebar). change эмитится в конце жеста и на каждом шаге с клавиатуры — это точка, куда вешают сохранение раскладки.

Три панели — это вложение

Компонент держит ровно две панели и один разделитель. Раскладка «дерево | редактор / консоль» собирается вложением, как в редакторах кода:

<GrSplitter v-model="treeWidth">
  <template #start><FileTree /></template>
  <template #end>
    <GrSplitter v-model="consoleHeight" orientation="vertical">
      <template #start><Editor /></template>
      <template #end><Console /></template>
    </GrSplitter>
  </template>
</GrSplitter>

Вложенные сплиттеры независимы: каждый ведёт свою модель.

Границы панелей

min и max ограничивают первую панель, minEnd — вторую. Без minEnd вторую панель можно задавить в ноль, и вернуть её будет нечем.

При конфликте min и minEnd побеждает min: первая панель не уезжает под свой минимум, даже если вторая при этом окажется уже своего. Обратный порядок дал бы панель, из которой пользователь не может выбраться.

Клавиатура и мышь

Разделитель стоит в таб-порядке (role="separator", tabindex="0"):

  • / в горизонтальной раскладке, / в вертикальной — шаг;
  • Shift + стрелка — крупный шаг;
  • Home / End — к min / max;
  • Enter — свернуть и вернуть (при collapsible);
  • двойной клик — сброс к defaultSize;
  • перетаскивание вплотную к краю (ниже половины min) сворачивает панель мышью: двойной клик занят сбросом, а свернуть мышью чем-то надо.

Зона захвата шире видимой полосы: шестипиксельная мишень мышью не берётся, а делать ради этого полосу толще значило бы красить раскладку под удобство мыши.

`aria-orientation` инвертирован — и это правильно

orientation описывает раскладку: horizontal — панели стоят рядом. aria-orientation описывает сам разделитель, а панели, стоящие рядом, разделяет вертикальная полоса. Поэтому у горизонтальной раскладки aria-orientation="vertical", и наоборот.

Разделитель объявляет aria-valuenow/valuemin/valuemax в тех же процентах, что и модель, и ссылается на первую панель через aria-controls.

Свёрнутость — не ноль в модели

collapsed — отдельное состояние (v-model:collapsed), а не modelValue: 0. Прежний размер обязан пережить сворачивание, иначе Enter вернул бы панель не туда, откуда её свернули. В треке свёрнутая панель занимает 0 %, и aria-valuenow показывает 0 — то, что видно на экране.

Границы

  • GrResizable (ресайз блока за уголок) сюда не входит — другой сценарий и другой паттерн;
  • сохранение раскладки — задача приложения: v-model и change дают всё необходимое, а localStorage внутри компонента был бы состоянием, о котором приложение не знает;
  • snap-точек и «магнитов» нет — без сценария это лишняя механика поверх клампа;
  • своей физики касания нетpointer-события покрывают тач как есть.

Playground 12

Загружается…

Код
<GrSplitter />

Установка

npm i @feugene/granularity

Импорт

import { GrSplitter } from '@feugene/granularity/components/GrSplitter'

API

Props

PropTypeпо умолчаниюОписание
modelValuenumber | undefinedundefinedДоля первой панели в процентах. Поддерживает `v-model`; без него компонент помнит размер сам.
disabledboolean | undefinedfalseРазделитель не тянется, не фокусируется и не отвечает на клавиши.
ariaLabelstring | undefinedundefinedИмя разделителя для скринридера. Не задано — из локали.
maxnumber | undefined90Максимум первой панели в процентах.
orientation"horizontal" | "vertical" | undefinedundefined`horizontal` — панели рядом, `vertical` — одна над другой.
stepnumber | undefined1Шаг стрелок.
minnumber | undefined10Минимум первой панели в процентах.
collapsedboolean | undefinedfalseПервая панель свёрнута. Поддерживает `v-model:collapsed`.
defaultSizenumber | undefined50Стартовая доля и точка возврата по двойному клику.
minEndnumber | undefined10Минимум второй панели: без него её можно задавить в ноль.
bigStepnumber | undefined10Шаг стрелок с `Shift`.
collapsibleboolean | undefinedundefinedРазрешить сворачивание первой панели.

Slots

SlotTypeОписание
startanyПервая панель — та, чей размер задаёт модель.
endanyВторая панель: занимает остаток.

Events

EventTypeОписание
update:modelValue[value: number]
change[value: number]
update:collapsed[value: boolean]

Примеры 3

Дерево и контент

Размер первой панели — доля контейнера в процентах: она переживает смену ширины окна и рендерится на сервере без замеров. Граница тянется мышью и ходит с клавиатуры.

src/

components/

composables/

styles/

docs/

package.json

Тяните границу мышью или доведите до неё фокус клавишей Tab: стрелки двигают на процент, Shift + стрелка — на десять, Home и End упираются в границы.

Доля первой панели: 28 %

Basic
<script setup lang="ts">
import { ref } from 'vue'
import { GrSplitter } from '@feugene/granularity'

const size = ref(28)

const files = ['src/', '  components/', '  composables/', '  styles/', 'docs/', 'package.json']
</script>

<template>
  <div class="grid gap-3">
    <div class="h-64 overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]">
      <GrSplitter v-model="size" :min="15" :max="60" aria-label="Ширина дерева файлов">
        <template #start>
          <div class="h-full overflow-auto bg-[var(--gr-muted)] p-3" tabindex="0">
            <p
              v-for="file in files"
              :key="file"
              class="whitespace-pre text-[length:var(--gr-text-xs)] leading-[var(--gr-leading-relaxed)] text-[var(--gr-muted-fg)]"
            >{{ file }}</p>
          </div>
        </template>

        <template #end>
          <div class="h-full overflow-auto p-4">
            <p class="text-[length:var(--gr-text-sm)] text-[var(--gr-fg)]">
              Тяните границу мышью или доведите до неё фокус клавишей Tab: стрелки двигают на процент,
              Shift + стрелка — на десять, Home и End упираются в границы.
            </p>
          </div>
        </template>
      </GrSplitter>
    </div>

    <p class="text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
      Доля первой панели: {{ Math.round(size) }} %
    </p>
  </div>
</template>

Три панели — это вложение

Сплиттер держит две панели и один разделитель; редактор с консолью под деревом собирается вложенным вертикальным сплиттером — ровно так, как это делают редакторы кода.

Дерево
Редактор

$ yarn build

✓ built in 692ms

Nested
<script setup lang="ts">
import { ref } from 'vue'
import { GrSplitter } from '@feugene/granularity'

const treeWidth = ref(25)
const consoleHeight = ref(65)
</script>

<template>
  <div class="h-72 overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]">
    <GrSplitter v-model="treeWidth" :min="15" :max="50" aria-label="Ширина дерева">
      <template #start>
        <div class="h-full bg-[var(--gr-muted)] p-3 text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
          Дерево
        </div>
      </template>

      <template #end>
        <!-- Трёхпанельная раскладка — это вложение, а не третья панель у сплиттера. -->
        <GrSplitter v-model="consoleHeight" orientation="vertical" :min="30" aria-label="Высота консоли">
          <template #start>
            <div class="h-full p-4 text-[length:var(--gr-text-sm)] text-[var(--gr-fg)]">
              Редактор
            </div>
          </template>

          <template #end>
            <div class="h-full overflow-auto bg-[var(--gr-muted)] p-3 font-mono text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]" tabindex="0">
              <p>$ yarn build</p>
              <p>✓ built in 692ms</p>
            </div>
          </template>
        </GrSplitter>
      </template>
    </GrSplitter>
  </div>
</template>

Минимумы и сворачивание

min и minEnd не дают задавить ни одну из панелей. Enter сворачивает первую и возвращает её к прежнему размеру, двойной клик сбрасывает границу к defaultSize.

Фильтры
Enter на разделителе сворачивает панель и возвращает её туда же, откуда свернул. Двойной клик сбрасывает границу к значению по умолчанию, а перетаскивание вплотную к левому краю схлопывает панель мышью.

Доля панели: 30 %

Collapsible
<script setup lang="ts">
import { ref } from 'vue'
import { GrSplitter } from '@feugene/granularity'

const size = ref(30)
const collapsed = ref(false)
</script>

<template>
  <div class="grid gap-3">
    <div class="h-56 overflow-hidden rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]">
      <GrSplitter
        v-model="size"
        v-model:collapsed="collapsed"
        collapsible
        :min="20"
        :min-end="30"
        :default-size="30"
        aria-label="Ширина боковой панели"
      >
        <template #start>
          <div class="h-full bg-[var(--gr-muted)] p-3 text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
            Фильтры
          </div>
        </template>

        <template #end>
          <div class="h-full p-4 text-[length:var(--gr-text-sm)] text-[var(--gr-fg)]">
            Enter на разделителе сворачивает панель и возвращает её туда же, откуда свернул.
            Двойной клик сбрасывает границу к значению по умолчанию, а перетаскивание вплотную
            к левому краю схлопывает панель мышью.
          </div>
        </template>
      </GrSplitter>
    </div>

    <p class="text-[length:var(--gr-text-xs)] text-[var(--gr-muted-fg)]">
      {{ collapsed ? 'Панель свёрнута' : `Доля панели: ${Math.round(size)} %` }}
    </p>
  </div>
</template>

Доступность

Паттерн APG
window splitter
Клавиши
разделитель стоит в таб-порядке: / (в вертикальной раскладке /) — шаг, Shift+стрелка — крупный шаг, Home/End — к min/max, Enter — свернуть и вернуть панель (при collapsible). Свёрнутая панель на первое нажатие стрелки возвращается: двигать то, чего не видно, значит менять раскладку вслепую

Полный клавиатурный контракт пакета

Документация компонентаВсе компоненты