GrSplitter

  • ядро
  • Утилиты
  • API · 12 props
  • ⌨ клавиши

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

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

Обзор

Когда брать

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

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

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

Playground контролы выводятся из API, а не пишутся руками

Сцена

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

0 изменено· состояние — в адресе

Примеры 2 у каждого — живое демо и его код

  • Список переписок и открытое письмо

    Модель — доля списка в процентах: она переживает сужение окна, а сервер рисует то же деление, и гидрация проходит чисто. min и max держат список читаемым, min-end не даёт задавить письмо в ноль. change приходит в конце жеста и на каждом шаге стрелкой — место сохранить раскладку; двойной клик по границе возвращает default-size.

    Open · 4Layout: list 38%
    #4182 · Northwind TradersCard declined on renewal

    Our card was declined this morning, but the bank says. Can you check what happened on your side? We would like to keep the subscription active.

  • Таблицы, запрос и его результат

    Три панели — это два сплиттера, один внутри другого, у каждого своя модель. Список таблиц сворачивается через collapsible и v-model:collapsed — доля переживает сворачивание, и панель возвращается на место; свернуть можно и Enter на границе, и перетаскиванием к краю. orientation="vertical" делит редактор и результат, step и big-step задают шаг стрелок, а запертая раскладка — disabled.

    • billing · 6 tables
    • customers
    • invoices
    • invoice_lines
    • payments
    • refunds
    • subscriptions
    SELECT c.name, s.plan, SUM(i.total) AS billed
    FROM customers c
    JOIN subscriptions s ON s.customer_id = c.id
    JOIN invoices i ON i.customer_id = c.id
    WHERE i.issued_at >= DATE '2026-09-01'
    GROUP BY c.name, s.plan
    ORDER BY billed DESC;
    3 rows · 41 ms
    Northwind TradersBusiness€4,820.00
    Contoso LtdTeam€1,296.00
    Fabrikam GmbHTeam€1,104.00

API порождается из пакета · разойтись не может

Props

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

Слоты

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

События

EventСигнатураОписание
update:modelValue(event: "update:modelValue", value: number): void—
change(event: "change", value: number): void—
update:collapsed(event: "update:collapsed", value: boolean): void—

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrSplitterProps, GrSplitterEmits, GrSplitterOrientation } from '@feugene/granularity/components/GrSplitter'

Файлdist/types/components/GrSplitter/GrSplitter.vue.d.ts

GrSplitterPropsinterface

Пропы {@link GrSplitter}. Две панели и разделитель между ними. Размер первой панели — доля контейнера в процентах: она переживает смену ширины окна и рендерится на сервере без замеров.

interface GrSplitterProps {
    /** Доля первой панели в процентах. Поддерживает `v-model`; без него компонент помнит размер сам. */
    modelValue?: number;
    /** Стартовая доля и точка возврата по двойному клику. */
    defaultSize?: number;
    /** `horizontal` — панели рядом, `vertical` — одна над другой. */
    orientation?: GrSplitterOrientation;
    /** Минимум первой панели в процентах. */
    min?: number;
    /** Максимум первой панели в процентах. */
    max?: number;
    /** Минимум второй панели: без него её можно задавить в ноль. */
    minEnd?: number;
    /** Шаг стрелок. */
    step?: number;
    /** Шаг стрелок с `Shift`. */
    bigStep?: number;
    /** Разрешить сворачивание первой панели. */
    collapsible?: boolean;
    /** Первая панель свёрнута. Поддерживает `v-model:collapsed`. */
    collapsed?: boolean;
    /** Разделитель не тянется, не фокусируется и не отвечает на клавиши. */
    disabled?: boolean;
    /** Имя разделителя для скринридера. Не задано — из локали. */
    ariaLabel?: string;
}
GrSplitterEmitsinterface
interface GrSplitterEmits {
    /**
     * Новая доля первой панели в процентах — на каждом шаге жеста и клавиатуры. Свёртка модель
     * не обнуляет: доля остаётся точкой возврата.
     */
    (e: 'update:modelValue', value: number): void;
    /** Первая панель свёрнута или развёрнута: жестом к краю, `Enter` на разделителе или стрелкой. */
    (e: 'update:collapsed', value: boolean): void;
    /** Конец жеста и каждый шаг с клавиатуры: сюда вешают сохранение раскладки. */
    (e: 'change', value: number): void;
}

Файлdist/types/components/GrSplitter/splitterGeometry.d.ts

GrSplitterOrientationtype
type GrSplitterOrientation = typeof GR_SPLITTER_ORIENTATIONS[number];
GrSplitterBoundsinterface
interface GrSplitterBounds {
    /** Минимум первой панели. */
    min: number;
    /** Максимум первой панели. */
    max: number;
    /** Минимум второй панели: без него её можно задавить в ноль. */
    minEnd: number;
}
sizeFromPointerfunction

Доля, соответствующая позиции указателя. Вне контейнера — его края.

function sizeFromPointer(rect: {
    left: number;
    top: number;
    width: number;
    height: number;
}, clientX: number, clientY: number, orientation: GrSplitterOrientation): number;

Файлdist/types/components/GrSplitter/config.d.ts

grSplitterConfigconst
const grSplitterConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSplitter">;

Файлdist/types/components/GrSplitter/safelist.d.ts

grSplitterSafelistconst
const grSplitterSafelist: string[];

Файлdist/types/components/GrSplitter/defaults.d.ts

GrSplitterConfigurablePropsinterface

Пропы GrSplitter, настраиваемые глобально через componentDefaults. Только оформление раскладки: размер панели и её свёрнутость принадлежат конкретному экземпляру.

interface GrSplitterConfigurableProps {
    orientation: GrSplitterOrientation;
    collapsible: boolean;
}

Файлdist/types/components/GrSplitter/index.d.ts

GrSplitterInstancetype
type GrSplitterInstance = ComponentExposed<typeof GrSplitterComponent>;

Доступность

Паттерн APG

window splitter

Клавиши

Home / EndEnter

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

Использование из прозы библиотеки

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

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

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

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

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

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

vue
<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 — то, что видно на экране.

Свёрнутая панель обрезана и скрыта (overflow: hidden, visibility: hidden) и помечена inert: её содержимое не рисуется поверх второй панели, не объявляется скринридером и не попадает в порядок Tab. Раньше нулевая ширина оставляла содержимое видимым поверх соседа, а ссылки внутри — достижимыми вслепую. При разворачивании всё возвращается.

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