GrSplitter
Берут, когда двум областям нужна общая граница.
Обзор
Когда брать
- двум областям нужна общая граница — дерево и контент, редактор и предпросмотр, список и деталь;
- размер выбирает пользователь — граница двигается мышью и с клавиатуры,
min/maxдержат края; - одну область сворачивают —
collapsibleубирает панель целиком и возвращает обратно; - раскладка сохраняется —
v-modelотдаёт размер, и его остаётся записать.
Когда взять другое
| Нужно | Берите |
|---|---|
| Областей много и они на сетке | GrDashboard |
| Раскладка фиксирована | CSS Grid и GrCard |
| Панель выезжает поверх контента | GrDrawer |
| Разделы переключаются | GrTabs |
| Просто линия между блоками | GrDivider |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrSplitter>Текст</GrSplitter>Примеры 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 msNorthwind Traders Business €4,820.00 Contoso Ltd Team €1,296.00 Fabrikam GmbH Team €1,104.00
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
disabled | boolean | undefined | false | Разделитель не тянется, не фокусируется и не отвечает на клавиши. |
modelValuev-model | number | undefined | undefined | Доля первой панели в процентах. Поддерживает `v-model`; без него компонент помнит размер сам. |
ariaLabel | string | undefined | undefined | Имя разделителя для скринридера. Не задано — из локали. |
max | number | undefined | 90 | Максимум первой панели в процентах. |
orientation | "horizontal" | "vertical" | undefined | undefined | `horizontal` — панели рядом, `vertical` — одна над другой. |
step | number | undefined | 1 | Шаг стрелок. |
min | number | undefined | 10 | Минимум первой панели в процентах. |
collapsedv-model | boolean | undefined | false | Первая панель свёрнута. Поддерживает `v-model:collapsed`. |
defaultSize | number | undefined | 50 | Стартовая доля и точка возврата по двойному клику. |
minEnd | number | undefined | 10 | Минимум второй панели: без него её можно задавить в ноль. |
bigStep | number | undefined | 10 | Шаг стрелок с `Shift`. |
collapsible | boolean | undefined | undefined | Разрешить сворачивание первой панели. |
Слоты
| Slot | Type | Описание |
|---|---|---|
start | any | Первая панель — та, чей размер задаёт модель. |
end | any | Вторая панель: занимает остаток. |
События
| 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
Пропы {@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;
}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
type GrSplitterOrientation = typeof GR_SPLITTER_ORIENTATIONS[number];interface GrSplitterBounds {
/** Минимум первой панели. */
min: number;
/** Максимум первой панели. */
max: number;
/** Минимум второй панели: без него её можно задавить в ноль. */
minEnd: number;
}Доля, соответствующая позиции указателя. Вне контейнера — его края.
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
const grSplitterConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrSplitter">;Файлdist/types/components/GrSplitter/safelist.d.ts
const grSplitterSafelist: string[];Файлdist/types/components/GrSplitter/defaults.d.ts
Пропы GrSplitter, настраиваемые глобально через componentDefaults.
Только оформление раскладки: размер панели и её свёрнутость принадлежат
конкретному экземпляру.
interface GrSplitterConfigurableProps {
orientation: GrSplitterOrientation;
collapsible: boolean;
}Файлdist/types/components/GrSplitter/index.d.ts
type GrSplitterInstance = ComponentExposed<typeof GrSplitterComponent>;Доступность
Паттерн APG
window splitter
Клавиши
Home / EndEnter
| Клавиша | Действие |
|---|---|
| — | разделитель стоит в таб-порядке: ←/→ (в вертикальной раскладке ↑/↓) — шаг |
| — | Shift+стрелка — крупный шаг |
| Home / End | к min/max |
| Enter | свернуть и вернуть панель (при collapsible). Свёрнутая панель на первое нажатие стрелки возвращается: двигать то, чего не видно, значит менять раскладку вслепую |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Размер — доля, а не пиксели
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 — то, что видно на экране.
Свёрнутая панель обрезана и скрыта (overflow: hidden, visibility: hidden) и
помечена inert: её содержимое не рисуется поверх второй панели, не
объявляется скринридером и не попадает в порядок Tab. Раньше нулевая ширина
оставляла содержимое видимым поверх соседа, а ссылки внутри — достижимыми
вслепую. При разворачивании всё возвращается.
Границы
GrResizable(ресайз блока за уголок) сюда не входит — другой сценарий и другой паттерн;- сохранение раскладки — задача приложения:
v-modelиchangeдают всё необходимое, аlocalStorageвнутри компонента был бы состоянием, о котором приложение не знает; - snap-точек и «магнитов» нет — без сценария это лишняя механика поверх клампа;
- своей физики касания нет —
pointer-события покрывают тач как есть.
Пропы этого примера