@boobooking/dashboard-ui-components
| Install | |
|---|---|
npm install @boobooking/dashboard-ui-components@1.0.0 |
|
| Latest Version: | 1.0.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 21, 2026 |
| Links: | GitHub · npm |
@boobooking/dashboard-ui-components
Компоненты дашборда на Vue 3: фильтры и общие элементы интерфейса.
Установка
npm install @boobooking/dashboard-ui-components
import { RussianMobileFilter, SelectDateIntervalFilter } from '@boobooking/dashboard-ui-components'
import '@boobooking/dashboard-ui-components/style.css'
Стили пакета подключаются до стилей приложения.
Подключение к приложению
Плагин dashboardUi задаёт язык, переход по ссылкам пакета и адрес
страницы на экране. Все параметры необязательны; без плагина компоненты
русскоязычные, ссылки — обычные <a>, а PageCard закрывается только на
fallback-url.
import { dashboardUi } from '@boobooking/dashboard-ui-components'
import { router, usePage } from '@inertiajs/vue3'
createApp(App).use(dashboardUi, {
lang: 'en',
navigate: (href) => router.visit(href),
currentUrl: () => usePage().url,
})
lang—'ru'(по умолчанию) или'en'; другое значение — ошибка вapp.use.navigate(href)— переход приложения по адресу: для Inertiarouter.visit(href), для Vue Routerrouter.push(href). Компоненты рисуют настоящий<a href>и отдаютnavigateтолько обычный клик левой кнопкой; клики с Ctrl/Cmd/Shift/Alt, средней кнопкой и ссылки сtargetостаются браузеру. Безnavigateбраузер идёт по ссылке сам.currentUrl()— адрес страницы на экране, по немуPageCardзнает, откуда его открыли. Для Inertia —() => usePage().url(проверено на@inertiajs/vue33.6.1). Договор для другого роутера: функция читает реактивное состояние роутера; её значение меняется при каждом показе страницы с новым адресом, в том числе при замене записи истории, «Назад»/«Вперёд» и первой загрузке, и меняется до создания компонентов новой страницы; неуспешный или отменённый переход значения не меняет; пока роутер не готов, функция возвращаетnullилиundefined. У Vue Router, например, до первой навигацииcurrentRoute—START_LOCATIONсfullPath === '/', и его надо отдавать какnull. Плагин следит за адресом только в браузере и перестаёт при размонтировании приложения.
navigate получает адрес ровно таким, каким он лежит в компоненте:
пагинация DataTable отдаёт links Laravel как есть, а это абсолютные URL. С Vue
Router приложение само превращает их в маршрут, например
new URL(href).pathname + search, и только потом зовёт router.push.
Требования
Vue не ниже 3.5, ESM, сборщик, умеющий импорт CSS из пакета. Версия 3.5 нужна
ради useId(): им компоненты связывают свои id с aria-атрибутами, и две
копии компонента на одной странице не делят один id.
Tailwind, @tailwindcss/forms, Inertia и какие-либо токены в CSS приложения
пакету не нужны.
Меню DropdownButtonWithAction и HamburgerMenu, списки SelectSingleFilter,
SelectSingleForm, SelectMultipleFilter и SelectMultipleForm, календари
SelectDateFilter, SelectDateForm, SelectDateIntervalFilter,
SelectDateIntervalForm и подсказка TextPopover
стоят на Popover API браузера: Safari 17+, Chrome 114+, Firefox 125+. В браузерах
старше меню, списки, календари и подсказка видны в потоке всегда.
Цвета
У пакета один список цветов, и все компоненты с цветом берут его:
gray, green, yellow, red, indigo, purple. Цвет передаётся
именем; другое имя валидатор отклоняет, а компонент рисуется без цвета.
Смысл цвета — какой статус зелёный — задаёт проект своей картой
«значение → имя».
| Где | Вид цвета <тон> |
|---|---|
InfoPill, ActionPill |
фон -100, текст -800; у ActionPill при наведении текст -600 |
основная кнопка и стрелка DropdownButtonWithAction |
фон -100, рамка -300, текст -800, при наведении фон -200 |
пункт меню DropdownButtonWithAction и HamburgerMenu |
фон -100, текст -800, при наведении и в фокусе фон -200 |
Dot, полоска строки DataTable |
-500 |
Без цвета кнопка белая с серой рамкой, пункт меню серый, строка таблицы без полоски — это вид компонента по умолчанию, в список он не входит.
Компоненты
Пакет экспортирует двадцать шесть компонентов: Dot, RussianMobileFilter,
Search, SelectDateFilter, SelectDateForm, SelectDateIntervalFilter,
SelectDateIntervalForm,
SelectSingleFilter, SelectSingleForm, SelectMultipleFilter,
SelectMultipleForm, SelectSingleCards, SelectMultipleList, InfoPill,
ErrorMessages, FormInput, ActionPill,
ConfirmationModal, DropdownButtonWithAction, HamburgerMenu, DataTable,
NavigationMenuElement, PageTabs, PageCard, NotificationMessage,
TextPopover, функцию downloadFile и плагин dashboardUi. В архив пакета
(files: ["dist"]) исходники не попадают, поэтому контракт каждого
компонента — здесь и в playground, а не в исходном коде.
Ластики очистки фильтров идут в порядке Tab сразу после своего поля или
кнопки, в фокусе с клавиатуры вокруг крестика кольцо; нажимаются Enter и
пробелом. Крестики — закрытие PageCard и ластики очистки фильтров —
при нажатии коротко «вдавливаются», и действие выполняется после этого,
через 150 мс: иначе нажатия не было бы видно — ластик пропадает сразу
после очистки, крестик уводит со страницы. Так при любом нажатии: мышью,
касанием, Enter, пробелом, расширением клавиатурной навигации. Нажатия
во время анимации не считаются: одно нажатие — одно действие. При
«уменьшении движения» в системе — без анимации и сразу.
Dot
Индикатор-точка любого цвета из списка, с пульсацией или без.
<dot color="green" :with-pulse="isLive" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
color |
String |
обязателен | Имя из списка цветов (раздел «Цвета») |
withPulse |
Boolean |
false |
Добавляет анимацию пульсации |
Событий нет. Значения у Dot тоже нет — см. раздел
«Почему у Dot нет события changed».
RussianMobileFilter
Поле ввода российского мобильного номера с маской +7 (000) 000 00 00 и
дебаунсом перед отправкой.
<russian-mobile-filter v-model="phone" header="Телефон" @changed="reload" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
modelValue |
String |
"" |
Номер: цифры вместе с кодом страны, без «+» (например, 79031234567), либо пустая строка |
header |
String |
обязателен | Подпись поля |
isRequired |
Boolean |
false |
Пометка обязательности в заголовке |
isLoading |
Boolean |
false |
Индикатор загрузки в заголовке |
«+» — неудачный символ для URL, поэтому в параметре только цифры; плюс к номеру дописывает бэкенд.
События: update:modelValue — на каждое принятое маской изменение;
changed — через 400 мс паузы после ввода, начиная с третьей цифры (раньше
событие не летит вовсе), а также сразу — без дебаунса — при опустевшем поле,
по клику на ластик, по Enter и по уходу фокуса. Дублей не бывает: компонент
помнит последнее отправленное значение и не шлёт его повторно. При
размонтировании changed не летит, даже если поле было в фокусе: набранный,
но ещё не отправленный номер не отправляется, иначе запрос страницы, с
которой уходят, вернул бы на неё.
Search
Текстовое поле поиска с ластиком очистки.
<search v-model="query" header="Поиск" @changed="reload" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
modelValue |
String |
"" |
Текущий текст поиска |
header |
String |
обязателен | Подпись поля |
isRequired |
Boolean |
false |
Пометка обязательности в заголовке |
isLoading |
Boolean |
false |
Индикатор загрузки в заголовке |
События: update:modelValue и changed — оба на каждый ввод. Значение,
изменённое родителем, поле показывает сразу и без событий: ответный
update:modelValue вернул бы родителю его же значение.
FormInput
Поле формы с подписью над ним. Значение забирает форма: у поля нет
события changed.
<form-input v-model="name" label="Имя" :is-required="markNameRequired" />
<form-input v-model="email" label="Email" type="email" :is-required="markEmailRequired" />
<form-input v-model="password" label="Пароль" type="password" :is-required="markPasswordRequired" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
modelValue |
String |
"" |
Значение поля |
label |
String |
обязателен | Подпись над полем |
type |
String |
"text" |
"text", "email", "password", "tel", "url" или "search" |
isRequired |
Boolean |
false |
Красная точка у подписи |
Событие update:modelValue уходит на каждый ввод со строкой из поля как
есть: пробелы не обрезаются, это решение формы. Исключение — type="email"
и type="url": браузер сам убирает пробелы по краям значения, и Email из
одних пробелов приходит пустой строкой. Формы, которые проверяют
length > 0, оставляют у Email type="text". Значение, изменённое
родителем, поле показывает сразу и без события. Набор через IME
(composition) отдаёт значение по его окончании.
Используйте v-model без модификаторов: модификаторы (.trim, .number,
.lazy) не входят в API FormInput; пробелы форма обрезает сама при
отправке.
Другой type, не из списка, отклоняется предупреждением Vue, а поле
рисуется с type="text".
class и style ложатся на корень компонента, все остальные атрибуты
и слушатели (autocomplete, placeholder, name, disabled, @blur,
@keydown.enter) — на <input>. id поля создаётся сам, for подписи
указывает на него, поэтому две копии на странице не делят один id.
Атрибут id от формы заменяет созданный и в поле, и в for.
Красная точка значит «не заполнено»; когда её показывать, решает форма. Основной способ: точки появляются после попытки отправить, ввод в поле снимает свою, а стирание поля ставит точку снова (так делает watcher из примера).
watch: {
name() { this.markNameRequired = this.name.length === 0 },
},
methods: {
create() {
this.markNameRequired = this.name.length === 0
// то же для остальных полей
},
},
Способ «сразу», точка видна, пока поле пусто:
<form-input v-model="number" label="Номер" :is-required="number.trim() === ''" />
isRequired только показывает точку: атрибут required и aria-* полю
он не ставит.
SelectDateFilter
Одно поле даты под заголовком. Дата выбирается в календаре Pikaday, у поля — ластик очистки. Подсказки у поля нет. Pikaday загружается в браузере при монтировании компонента, а не при загрузке пакета, поэтому календарь появляется, когда загрузка закончится.
Высота 4.5rem и серая рамка встроены в фильтр, ширина поля 9rem: класс
h-18 border border-gray-200 ему не нужен. class и остальные атрибуты
ложатся на корень фильтра.
<select-date-filter header="Дата" v-model="day" @changed="reload"/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
modelValue |
String |
"" |
Выбранная дата |
header |
String |
обязателен | Подпись поля |
isRequired |
Boolean |
false |
Пометка обязательности в заголовке |
isLoading |
Boolean |
false |
Индикатор загрузки в заголовке |
lang |
String |
язык плагина | "ru" или "en"; действует на календарь и на подпись ластика для скринридера (у самого поля текста нет) |
События: update:modelValue (дата или пустая строка после ластика)
и changed приходят на каждый выбор пользователя: в том числе на повторный
выбор уже показанной даты, на набранную в поле дату, которая разобралась,
и на очистку ластиком. Когда значение меняет родитель, ни одно из событий
не приходит. Поля диапазона SelectDateIntervalFilter и
SelectDateIntervalForm устроены иначе: их changed приходит, только когда
новое значение отличается от переданного пропса.
Календарь, ластик и клавиатура — как у каждого из двух полей
SelectDateIntervalFilter (см. «SelectDateIntervalFilter»).
SelectDateForm
Одно поле даты в форме, в виде FormInput: подпись над полем, рамка поля
на всю ширину формы, высотой 38px, как у FormInput. Рамка скругляет и поле,
и кольцо фокуса в нём. Подсказки у поля нет.
<select-date-form label="Дата" v-model="day" :is-required="markDayRequired"/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
modelValue |
String |
"" |
Выбранная дата |
label |
String |
обязателен | Подпись над полем |
isRequired |
Boolean |
false |
Красная точка у подписи |
lang |
String |
язык плагина | "ru" или "en"; действует на календарь и на подпись ластика для скринридера (у самого поля текста нет) |
События: update:modelValue и changed — когда значение выбрал пользователь,
как у SelectDateFilter (см. «SelectDateFilter»); смена значения родителем
их не вызывает. Календарь, ластик и клавиатура — тоже как у него.
Подпись связана с полем для скринридера: кнопка календаря называется подписью и датой, поле ввода — подписью. Клик по подписи ничего не делает.
isRequired только показывает точку, как у FormInput. class, style,
все остальные атрибуты и слушатели ложатся на корень компонента (у FormInput
остальные атрибуты уходят на поле ввода).
SelectDateIntervalFilter
Два поля даты под одним заголовком — интервал дат «от» и «до». Дата выбирается в календаре Pikaday, у каждого поля — ластик очистки. Pikaday загружается в браузере при монтировании компонента, а не при загрузке пакета, поэтому календари появляются, когда загрузка закончится.
Высота 4.5rem и серая рамка встроены в фильтр: класс
h-18 border border-gray-200 ему не нужен. Ширина фильтра 18rem, «от» и «до»
по половине, между ними вертикальная линия. class и остальные атрибуты
ложатся на корень фильтра.
<select-date-interval-filter
header="Период"
:date-from="dateFrom"
:date-to="dateTo"
@update:date-from="dateFrom = $event"
@update:date-to="dateTo = $event"
@changed="reload"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
dateFrom |
String |
"" |
Начало интервала |
dateTo |
String |
"" |
Конец интервала |
header |
String |
обязателен | Подпись поля |
isRequired |
Boolean |
false |
Пометка обязательности в заголовке |
isLoading |
Boolean |
false |
Индикатор загрузки в заголовке |
lang |
String |
язык плагина | "ru" или "en"; действует и на подсказки полей, и на оба календаря |
События: update:dateFrom, update:dateTo — на каждый выбор даты
в соответствующем поле и на его очистку ластиком (пустой строкой);
changed — когда новое значение отличается от переданного пропса
(отдельно для каждой из двух дат). Повторный выбор уже показанной даты
событий не вызывает.
Клик по любому месту поля открывает его календарь. Календарь открывается в верхнем слое браузера (Popover API): его не обрезает обёртка с прокруткой. Он открывается под полем, левым краем по левому краю поля. Не поместившись снизу, он открывается вверх, если сверху места больше; не поместившись справа — правым краем по правому краю поля, если слева места больше. Выбор даты, клик вне календаря, Escape, прокрутка страницы и изменение размера окна закрывают календарь. Клик вне доходит до элемента под курсором, поэтому календарь «до» открывается тем же кликом, что закрывает «от».
С клавиатуры: в поле даты стрелка вниз, Enter и пробел открывают календарь. Кнопки календаря и списков в фокусе с клавиатуры обведены кольцом пакета. Пока календарь открыт, стрелки переводят фокус по дням: влево и вправо — на день, вверх и вниз — на неделю; с края месяца календарь листается, дата при этом не выбирается. Первая стрелка ставит фокус на выбранный день, без выбранного — на сегодня. Enter или пробел выбирают день под фокусом и стрелки месяцев — так же, как клик; Escape закрывает календарь. После закрытия фокус возвращается на поле. В поле можно перейти Tab и набрать дату.
SelectDateIntervalForm
Интервал дат в форме, в виде FormInput: подпись над полями, под ней «от»
и «до» одинаковой ширины на всю ширину формы. У каждого поля своя рамка, как
у FormInput, высотой 38px; рамка скругляет и поле, и кольцо фокуса в нём.
<select-date-interval-form
label="Период"
v-model:date-from="dateFrom"
v-model:date-to="dateTo"
:is-required="markPeriodRequired"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
dateFrom |
String |
"" |
Начало интервала |
dateTo |
String |
"" |
Конец интервала |
label |
String |
обязателен | Подпись над полями |
isRequired |
Boolean |
false |
Красная точка у подписи |
lang |
String |
язык плагина | "ru" или "en"; действует и на подсказки полей, и на оба календаря |
События те же, что у SelectDateIntervalFilter: update:dateFrom,
update:dateTo и changed. Календари, ластики, подсказки и клавиатура —
тоже как у него (см. «SelectDateIntervalFilter»).
Подпись связана с полями для скринридера: оба поля — группа, названная подписью, а каждое поле, как в фильтре, называется своей подсказкой «от» или «до» с датой. Клик по подписи ничего не делает.
isRequired только показывает точку, как у FormInput. class, style,
все остальные атрибуты и слушатели ложатся на корень компонента (у FormInput
остальные атрибуты уходят на поле ввода).
SelectSingleFilter
<select-single-filter
header="Вендор"
:items="[{ id: 'a', name: 'Первый' }]"
v-model="vendor"
@changed="reload"
/>
items — массив объектов { id, name }. v-model держит id выбранного или
null, если не выбрано. Не массив, в том числе null и undefined, — пустой
список; элементы-необъекты в массиве пропускаются.
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items |
Array |
обязателен | Массив { id, name } |
modelValue |
String | Number |
null |
id выбранного элемента или null |
header |
String |
обязателен | Подпись поля |
isRequired |
Boolean |
false |
Пометка обязательности в заголовке |
isLoading |
Boolean |
false |
Индикатор загрузки в заголовке |
События: update:modelValue и changed — при выборе элемента (повторный
клик по уже выбранному снимает выбор) и при очистке через ластик.
Высота 4.5rem и серая рамка встроены в фильтр: класс
h-18 border border-gray-200 ему не нужен. class и остальные атрибуты
ложатся на корень фильтра. Без пунктов кнопки нет, а заголовок, высота и рамка
остаются.
С клавиатуры: пока список открыт, стрелки вверх и вниз переводят фокус по пунктам и страницу не прокручивают. Отсчёт идёт от выбранного пункта: вниз — следующий за ним, вверх — предыдущий; без выбранного вниз — первый пункт, вверх — последний; с краёв — по кругу. Enter или пробел выбирают пункт под фокусом так же, как клик, Escape закрывает список. Tab и Shift+Tab ходят по пунктам по кругу и наружу не выводят, пока список открыт. После закрытия фокус возвращается на кнопку фильтра. У закрытого списка стрелки прокручивают страницу.
Наведение подсвечивает пункт, но фокус не двигает: фокус меняют клавиатура и клик. Пункт под мышью и пункт в фокусе с клавиатуры могут быть подсвечены одновременно; стрелки продолжают от пункта в фокусе.
Список открывается в верхнем слое браузера (Popover API): его не обрезает обёртка с прокруткой. Он открывается под полем, левым краем по левому краю поля. Не поместившись снизу, он открывается вверх, если сверху места больше; не поместившись справа — правым краем по правому краю поля, если слева места больше. Длинный список ограничен высотой до края окна и прокручивается внутри. Клик вне списка закрывает его и доходит до элемента под курсором: соседний фильтр открывается тем же кликом. Прокрутка страницы и изменение размера окна закрывают список, прокрутка внутри списка — нет. Открытие другого списка, календаря или меню закрывает предыдущее.
SelectSingleForm
Выбор одного пункта в форме, в виде FormInput: подпись над полем, рамка
поля на всю ширину формы, высотой 38px, как у FormInput. В рамке — кнопка
со списком и ластиком, та же, что у SelectSingleFilter.
<select-single-form
label="Вендор"
:items="vendors"
v-model="vendor"
:is-required="markVendorRequired"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items |
Array |
обязателен | Массив { id, name } |
modelValue |
String | Number |
null |
id выбранного элемента или null |
label |
String |
обязателен | Подпись над полем |
isRequired |
Boolean |
false |
Красная точка у подписи |
items, v-model, события, список и клавиатура — как у SelectSingleFilter
(см. «SelectSingleFilter»). Без пунктов кнопки нет, под подписью остаётся
пустая рамка.
Подпись связана с кнопкой для скринридера: кнопка называется подписью и выбранным пунктом, у пустого поля — подписью. Клик по подписи ничего не делает.
isRequired только показывает точку, как у FormInput. class, style,
все остальные атрибуты и слушатели ложатся на корень компонента (у FormInput
остальные атрибуты уходят на поле ввода).
SelectMultipleFilter
<select-multiple-filter
header="Бренд"
:items="[{ id: 'rc', name: 'Royal Canin' }, { id: 'eukanuba', name: 'Eukanuba' }]"
v-model="brands"
@changed="reload"
/>
<select-multiple-filter header="Мероприятие" :items="events" v-model="eventIds" with-search @changed="reload"/>
Тот же фильтр, что SelectSingleFilter, но выбрать можно несколько пунктов.
items — массив объектов { id, name }. v-model держит массив id
выбранных пунктов, пустой массив — ничего не выбрано. id — строка или число,
сравнение строгое.
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items |
Array |
обязателен | Массив { id, name } |
modelValue |
Array |
[] |
id выбранных пунктов |
header |
String |
обязателен | Подпись поля |
isRequired |
Boolean |
false |
Пометка обязательности в заголовке |
isLoading |
Boolean |
false |
Индикатор загрузки в заголовке |
withSearch |
Boolean |
false |
Поле поиска вверху списка |
lang |
String |
язык плагина | "ru" или "en" — надпись, поле поиска, «Ничего не найдено», подписи обоих ластиков |
События: update:modelValue — новый массив id; changed — после каждого
изменения выбора кликом по пункту или ластиком.
Клик по невыбранному пункту добавляет его id в конец массива, по выбранному —
убирает. Массив из пропа компонент не меняет: он отдаёт новый, порядок id не
сортирует. id, которых нет среди пунктов, при переключении других пунктов
остаются в массиве, в надписи не считаются и ластик сами не показывают;
ластик отдаёт [] — уходят и они. Не массив в items, в том числе null
и undefined, — пустой список, элементы-необъекты в нём пропускаются; null
и undefined в modelValue — ничего не выбрано.
На кнопке: пока выбранных среди items нет — пусто; один — его название;
несколько — «Выбрано: 3». Считаются все пункты, а не только найденные поиском.
Ластик виден, когда выбран хотя бы один пункт и список закрыт.
Выбор пункта — клик, Enter или пробел — закрывает список, фокус возвращается
на кнопку фильтра, а запрос поиска сбрасывается. changed уходит после
каждого выбора, и страница сразу перезапрашивает выдачу.
Список открывается так же, как у SelectSingleFilter: в верхнем слое браузера, место,
высота, прокрутка и способы закрытия те же, фокус после закрытия возвращается
на кнопку. Отличия: галочка стоит у каждого выбранного пункта; пункт
сообщает скринридеру, выбран ли он (aria-pressed).
Как и у SelectSingleFilter, без пунктов кнопки нет, а заголовок, высота
и рамка остаются. Пункты пропали при открытом списке — список закрывается,
запрос поиска сбрасывается, а вернувшиеся пункты появляются с закрытым списком.
С клавиатуры, пока список открыт, всё как у SelectSingleFilter: стрелки вверх
и вниз ведут фокус по пунктам по кругу, Enter или пробел переключают пункт и
закрывают список, Escape закрывает его без выбора. Если выбрано несколько
пунктов, отсчёт идёт от первого выбранного среди видимых. Наведение подсвечивает
пункт, но фокус не двигает: фокус меняют клавиатура и клик, пункт под мышью и
пункт в фокусе могут быть подсвечены одновременно.
С with-search над пунктами стоит поле поиска: при прокрутке длинного списка
оно остаётся на месте. Имя пункта должно содержать запрос, регистр не важен,
пробелы по краям запроса отбрасываются. Если совпадений нет, вместо пунктов
строка «Ничего не найдено». Выбранные пункты, скрытые поиском, остаются
выбранными. Запрос не входит в v-model; он сбрасывается при каждом закрытии
списка, в том числе при выборе пункта. Ластик в поле очищает запрос. При
открытии списка фокус встаёт в поле; при загрузке страницы фокус в поле не
уходит. Без Popover API список виден всегда, и фокус в поле сам не ставится.
Пробел в поле печатает пробел, Enter ничего не делает и форму не отправляет.
Поле — первая остановка стрелок: из него вниз — первый пункт, вверх — последний,
с краёв пунктов — обратно в поле. Без совпадений стрелки в поле ничего не
делают. Пока список открыт, Tab и Shift+Tab ходят по кругу внутри него: поле,
ластик поля, пока запрос не пуст, пункты; наружу, на страницу, фокус уходит
после закрытия списка. Ластик поля входит в Tab, но не в стрелки. Ластик на
кнопке фильтра идёт в Tab сразу после неё. Печатный символ и Backspace на
пункте, куда фокус перевели стрелки или Tab, уходят в поле, поэтому набор не
пропадает; мышь фокус на пункт не уводит. Пока список открыт, панель не
становится уже, чем при открытии, а поле не двигается, сколько бы пунктов ни
убрал поиск: открытая вниз панель становится ниже вместе с найденным, открытая
вверх не становится ниже, и пустое место остаётся под пунктами, у кнопки.
SelectMultipleForm
Выбор нескольких пунктов в форме, в виде FormInput: подпись над полем, рамка
поля на всю ширину формы, высотой 38px, как у FormInput. В рамке — кнопка
со списком, поиском и ластиком, та же, что у SelectMultipleFilter.
<select-multiple-form label="Бренд" :items="brands" v-model="brandIds"/>
<select-multiple-form label="Мероприятие" :items="events" v-model="eventIds" with-search :is-required="markEventsRequired"/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items |
Array |
обязателен | Массив { id, name } |
modelValue |
Array |
[] |
id выбранных пунктов |
label |
String |
обязателен | Подпись над полем |
isRequired |
Boolean |
false |
Красная точка у подписи |
withSearch |
Boolean |
false |
Поле поиска вверху списка |
lang |
String |
язык плагина | "ru" или "en" — надпись, поле поиска, «Ничего не найдено», подписи обоих ластиков |
items, v-model, события, надпись на кнопке, список, поиск и клавиатура —
как у SelectMultipleFilter (см. «SelectMultipleFilter»). Без пунктов кнопки
нет, под подписью остаётся пустая рамка.
Подпись связана с кнопкой для скринридера: кнопка называется подписью и надписью на кнопке, у пустого поля — подписью. Клик по подписи ничего не делает.
isRequired только показывает точку, как у FormInput. class, style,
все остальные атрибуты и слушатели ложатся на корень компонента (у FormInput
остальные атрибуты уходят на поле ввода).
SelectSingleCards
Выбор одного пункта карточками в форме: подпись над карточками, в каждой карточке кружок слева и надпись. У выбранной карточки синяя рамка, светло-синий фон и точка в кружке. Карточки стоят в несколько колонок на всю ширину формы.
<select-single-cards label="Бренд" :items="brands" v-model="brand" :is-required="markBrandRequired"/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items |
Array |
обязателен | Массив { id, name } |
modelValue |
String | Number | Boolean |
null |
id выбранного пункта или null |
label |
String |
обязателен | Подпись над карточками |
isRequired |
Boolean |
false |
Красная точка у подписи |
columns |
Number |
2 |
Число колонок, целое от 1; иначе — две колонки. Vue предупреждает о нецелом, меньшем 1 и нечисловом значении, но не о null |
Событий два: update:modelValue с id выбранного пункта и сразу за ним
changed, после каждого выбора пользователя. id уходит в событие как есть,
без приведения к строке: строки, числа и true / false сравниваются строго.
Надпись карточки — name.
Без пунктов (items пуст, null или не массив) видна только подпись, сетки нет;
элементы, не являющиеся объектами, пропускаются.
Клик в любом месте карточки выбирает её. Клик по уже выбранной карточке
ничего не меняет и событий не вызывает: выбор не снимается, «ничего не
выбрано» бывает только до первого выбора. Если modelValue не совпадает
ни с одним id, не выбрана ни одна карточка.
Если родитель не принял значение (не обновил modelValue в ответ на
update:modelValue), выбранной остаётся карточка, которую задаёт проп.
Колонки компонент раскладывает сам: пункты идут по колонкам в порядке массива,
сначала заполняется первая колонка сверху вниз, потом вторая и так далее.
Длины колонок различаются не больше чем на один пункт, лишние пункты достаются
первым колонкам. Для n пунктов и c колонок:
n |
c |
Колонки |
|---|---|---|
| 14 | 2 | 7, 7 |
| 14 | 3 | 5, 5, 4 |
| 4 | 3 | 2, 1, 1 |
| 2 | 2 | 1, 1 |
| 2 | 3 | 1, 1, пустая |
| 1 | 2 | 1, пустая |
Колонки одинаковой ширины, их число всегда равно columns: при n < c
пустые колонки остаются местом справа, и ширина карточек от числа пунктов не
зависит. Карточки одной строки одной высоты, по самой высокой: так надпись,
перенесённая на две строки, не делает соседнюю карточку ниже. Колонки не
перестраиваются под узкий экран. Порядок Tab и стрелок — порядок массива, а не
порядок строк на экране.
С клавиатуры карточки ведут себя как родная группа переключателей: Tab входит в группу один раз — на выбранную карточку, а если выбранной нет, на первую; следующий Tab уходит из группы. Стрелки переходят к соседней карточке в порядке массива и выбирают её, пробел выбирает карточку под фокусом. Кольцо фокуса — вокруг всей карточки, только при фокусе с клавиатуры.
Для скринридера сетка карточек — группа radiogroup, названная подписью;
каждая карточка называется своей надписью. Клик по подписи ничего не делает.
В режиме высокой контрастности (forced-colors: active) браузер заменяет цвета
системными, а выбранная карточка остаётся видна: точка в кружке — системным
цветом текста, кружок — цветом выделения. Кольцо фокуса в этом режиме — одна
линия, ореол браузер снимает.
isRequired только показывает точку, как у FormInput: атрибутов required
и aria-required компонент не ставит. class, style, все остальные атрибуты
и слушатели ложатся на корень компонента. Своих надписей у компонента нет,
поэтому lang нет. Вид не зависит от preflight и @tailwindcss/forms
приложения: кружок и точку рисует сам компонент.
SelectMultipleList
Выбор нескольких пунктов списком галочек в форме: подпись над списком, у каждого пункта квадрат с галочкой и надпись. Отмеченный квадрат закрашен индиго с белой галочкой. Пункты стоят в несколько колонок.
<select-multiple-list label="Участники" :items="participantTypes" v-model="participants" :columns="2"/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
items |
Array |
обязателен | Массив { id, name } |
modelValue |
Array |
[] |
id отмеченных пунктов |
label |
String |
обязателен | Подпись над списком |
isRequired |
Boolean |
false |
Красная точка у подписи |
columns |
Number |
2 |
Число колонок, целое от 1; иначе — две колонки. Vue предупреждает о нецелом, меньшем 1 и нечисловом значении, но не о null |
Событий два: update:modelValue с новым массивом и сразу за ним changed,
после каждого выбора пользователя. Отметка добавляет id в конец массива,
снятие убирает его; id, которых нет среди пунктов, остаются в значении.
modelValue, не являющийся массивом, значит «ничего не отмечено». Клик по
квадрату и по надписи отмечает или снимает пункт.
Без пунктов (items пуст, null или не массив) видна только подпись, списка нет;
элементы, не являющиеся объектами, пропускаются.
Если родитель не принял значение (не обновил modelValue), отметки остаются
такими, какими их задаёт проп.
Раскладка по колонкам, ширина колонок, высота строк и порядок — как у
SelectSingleCards (см. «SelectSingleCards»), в том числе таблица n / c.
С клавиатуры каждая галочка — отдельная остановка Tab в порядке массива, пробел отмечает и снимает. Кольцо фокуса — вокруг квадрата, только при фокусе с клавиатуры.
Для скринридера список — группа group, названная подписью; каждая галочка
называется своей надписью. Клик по подписи ничего не делает.
В режиме высокой контрастности (forced-colors: active) компонент показывает
родные галочки браузера в системных цветах: свой закрашенный квадрат браузер
перекрасил бы в цвет фона, и отмеченный пункт не отличался бы от неотмеченного.
Кольцо фокуса в этом режиме — одна линия, как у SelectSingleCards.
isRequired, class, style, остальные атрибуты и слушатели, отсутствие
lang и независимость вида от preflight и @tailwindcss/forms — как у
SelectSingleCards.
InfoPill
Пилюля, которая только показывает текст: статус, счётчик, флаг, пометку. Текст не переносится.
<info-pill text="доставлено" color="green" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
text |
String |
обязателен | Текст пилюли |
color |
String |
обязателен | Имя из списка цветов |
Событий нет.
ErrorMessages
Плашка со списком сообщений: жёлтая для предупреждений, красная для ошибок. Без сообщений не отрисовывается вовсе.
<error-messages :messages="errors" type="dangerous" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
messages |
Object | Array |
обязателен | Объект ошибок валидации (ключ — поле, значение — текст) или массив строк |
type |
String |
"warning" |
"warning" (жёлтая) или "dangerous" (красная) |
Событий нет.
ActionPill
Пилюля, которой запускают действие: скачать отчёт, запросить статусы. Что
делает действие, пилюля не знает — страница слушает click и сама передаёт
isLoading, пока действие идёт.
<action-pill
icon="download"
title="скачать xlsx"
loading-text="формируется отчёт"
:is-loading="exportLoading"
@click="exportOrders"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
title |
String |
обязателен | Подпись и всплывающая подсказка |
icon |
String |
обязателен | "download" или "refresh" |
color |
String |
"purple" |
Имя из списка цветов |
isLoading |
Boolean |
false |
Идёт работа |
loadingText |
String |
null |
Подпись на время работы; без неё остаётся title |
Событие: click без аргументов.
Пока идёт работа, пилюля погашена и недоступна — второй клик не проходит,
вместо иконки бегут стрелки часов, подпись — loadingText. При системной
настройке «уменьшить движение» стрелки стоят. Иконки пакета — download
и refresh; новое действие с новой иконкой — новая иконка в пакете.
downloadFile
Функция: скачивает файл запросом и отдаёт его браузеру. Страница знает,
когда файл пришёл, — так показывается работа на ActionPill.
import { downloadFile } from '@boobooking/dashboard-ui-components'
exportOrders() {
this.exportLoading = true;
downloadFile(this.exportUrl)
.catch(() => { this.exportFailure = "Попробуйте ещё раз"; })
.finally(() => { this.exportLoading = false; });
}
- Запрос —
GETс cookie сессии (credentials: 'same-origin'). - Promise отклоняется:
- при перенаправлении — обычно на страницу входа после истёкшей сессии:
у ошибки
redirectUrl— адрес, куда в итоге привёл сервер,status—null; - при ответе не
2xx:status— код ответа,redirectUrl—null; - при сетевой ошибке — ошибкой
fetch, без этих полей.
- при перенаправлении — обычно на страницу входа после истёкшей сессии:
у ошибки
- Имя файла — из
Content-Disposition(filename*=UTF-8''…, затемfilename), без него — последний сегмент пути адреса, без пути —download. - Таймаута нет; уход со страницы скачивание не прерывает. Файл до сохранения целиком в памяти вкладки.
Ссылка при истёкшей сессии привела бы браузер на страницу входа. Чтобы
страница вела себя так же, она сама переходит по redirectUrl: пакет
не импортирует ни Inertia, ни роутер. В приложении на Inertia:
import { router } from '@inertiajs/vue3'
exportOrders() {
this.exportFailure = "";
this.exportLoading = true;
downloadFile(this.exportUrl)
.catch((error) => {
if (error.redirectUrl) {
router.visit(error.redirectUrl);
return;
}
this.exportFailure = "Попробуйте ещё раз";
})
.finally(() => {
this.exportLoading = false;
});
}
ConfirmationModal
Модалка подтверждения действия: заголовок, текст, кнопка действия и кнопка отмены.
<confirmation-modal
:is-open="showDelete"
type="dangerous"
confirmation-heading="Удаление администратора"
confirmation-text="Это действие необратимо."
action-button-text="Удалить"
@action-canceled="showDelete = false"
@action-confirmed="deleteUser"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
isOpen |
Boolean |
false |
Открыта ли модалка |
type |
String |
"warning" |
"warning" (жёлтая) или "dangerous" (красная) |
confirmationHeading |
String |
"" |
Заголовок |
confirmationText |
String |
"" |
Текст под заголовком |
cancelButtonText |
String |
null |
Подпись кнопки отмены; без него — «Отмена» / «Cancel» по языку |
actionButtonText |
String |
обязателен | Подпись кнопки действия |
lang |
String |
язык плагина | "ru" или "en" |
События: actionConfirmed — по кнопке действия, actionCanceled — по кнопке
отмены и по Escape. Ни одна из кнопок модалку не закрывает — см. раздел
«Почему у ConfirmationModal нет v-model».
Панель модалки — белая, со скруглением rounded-lg, стоит по центру и
занимает max-w-lg. class и другие атрибуты на <confirmation-modal>
попадают на полноэкранный корень, а не на панель. Панель не обрезает
содержимое, поэтому блоки текста и кнопок со своим фоном скругляют внешние
углы сами — верхний rounded-t-lg, нижний rounded-b-lg.
Открытая модалка держит фокус: в первом кадре после открытия он встаёт на
кнопку отмены, Tab и Shift+Tab ходят по её кнопкам по кругу, Escape —
actionCanceled, как кнопка отмены. После закрытия фокус возвращается туда,
где был до открытия, если страница не поставила его в другое место и тот
элемент ещё на странице. Кадр нужен модалкам, которые открывает пункт меню:
меню закрывается позже в том же клике и возвращает фокус на свою кнопку.
DropdownButtonWithAction
Кнопка из списка действий actions. Первое действие — основная кнопка,
остальные — меню за стрелкой справа. Одно действие — кнопка без стрелки;
без действий компонент ничего не рисует.
<dropdown-button-with-action
v-model="menuIsOpen"
:actions="[
{ label: 'Редактировать', href: route('user.edit', { user: row.getId() }) },
{ label: 'Поменять пароль', href: route('user.password.edit', { user: row.getId() }) },
{ label: 'Удалить', color: 'red', onSelect: () => prepareUserDelete(row) },
]"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
actions |
Array |
[] |
Действия, см. ниже |
modelValue |
Boolean |
false |
Открыто ли меню |
lang |
String |
язык плагина | "ru" или "en" |
Действие — объект одной из двух форм: переход (href без onSelect) или
действие (onSelect без href). Поле со значением undefined считается
отсутствующим.
| Поле | Тип | Описание |
|---|---|---|
label |
String, обязательно |
Текст кнопки или пункта меню |
href |
String |
Переход: обычный клик уходит в navigate плагина dashboardUi, клик с Cmd/Ctrl/Shift и средней кнопкой — браузеру; без navigate — обычная ссылка |
onSelect |
Function |
Действие: вызывается без аргументов |
color |
String |
Имя из списка цветов; без поля — обычный |
Основная кнопка работает как пункт меню: переход — ссылка, действие —
кнопка, и onSelect вызывает сам компонент — по клику, Enter и пробелу.
Своего события клика у кнопки нет: странице не нужно ловить клик и
разбираться в разметке компонента.
Цвет первого действия красит основную кнопку вместе со стрелкой, цвет пункта меню — только этот пункт. Без цвета кнопка белая с серой рамкой, пункт — серый текст и серый фон при подсветке; с цветом — по разделу «Цвета».
Неверный список Vue отмечает предупреждением о пропе в режиме разработки,
а компонент не падает: элементы-необъекты пропускает, кнопка без функции
onSelect по клику ничего не вызывает, пункт без неё только закрывает
меню. null — пустой список.
Вид кнопки и пунктов задаёт компонент: страница передаёт данные, а не
разметку, и классов не даёт. Слота button и поля danger нет с версии
0.15.0 — см. «Обновление с 0.14»; слота actions — с 0.14.0, см.
«Обновление с 0.13».
Событие: update:modelValue — только когда меню открыл или закрыл сам
компонент или браузер: клик по стрелке, клик вне меню, Escape, клик
по пункту меню, прокрутка, изменение размера окна, а ещё пропажа открытого
меню, когда действий осталось меньше двух. Значение, пришедшее
от родителя, компонент принимает молча: ответное событие вернуло бы
родителю его же решение. Без v-model меню работает само по себе.
Меню открывается в верхнем слое браузера (Popover API): его не обрезает таблица с горизонтальной прокруткой, и оно стоит поверх страницы. Правый край меню — по правому краю стрелки; меню открывается под стрелкой, а если там не помещается — туда, где больше места, сужается у левого края окна и закрывается кликом по любому пункту, прокруткой страницы или таблицы и изменением размера окна. Открытие другого меню закрывает предыдущее.
Пока меню открыто, стрелки вверх и вниз переводят фокус по пунктам и страницу не прокручивают: вниз — на первый пункт, вверх — на последний, дальше — по соседним, с краёв — по кругу. Enter на пункте под фокусом выбирает его. Наведение подсвечивает пункт, но фокус не двигает: фокус меняют клавиатура и клик. Пункт под мышью и пункт в фокусе с клавиатуры могут быть подсвечены одновременно; стрелки продолжают от пункта в фокусе.
После закрытия меню фокус возвращается на стрелку, если он был внутри меню и после закрытия потерян. Фокус, который перешёл на другой элемент (например, в модалку, открытую пунктом), сохраняется.
HamburgerMenu
Кнопка ☰ с меню из пунктов пропа actions — меню профиля в шапке
дашборда. Кнопку, панель и пункты рисует компонент, страница передаёт
только пункты. Без пунктов кнопки нет.
<hamburger-menu :actions="menuActions" />
import { router } from '@inertiajs/vue3';
computed: {
menuActions() {
return [
{ label: 'Администраторы', href: this.route('users') },
{ label: 'Поменять пароль', href: this.route('my.password.edit') },
{ label: 'Выйти', onSelect: () => router.delete(this.route('logout')) },
];
},
},
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
actions |
Array |
[] |
Пункты меню — в том же формате, что у DropdownButtonWithAction |
lang |
String |
язык плагина | "ru" или "en" |
Пункты, их проверка и вид — как у DropdownButtonWithAction: переход —
{ label, href }, действие — { label, onSelect }, цвет —
color: имя из списка цветов. Выход запросом DELETE — действие:
Inertia пакет не импортирует, router.delete вызывает страница.
Событий и v-model нет: меню открывают и закрывают кнопка, клик вне меню,
Escape, клик по пункту, прокрутка и изменение размера окна. Меню
открывается в верхнем слое браузера (Popover API), правым краем по правому
краю кнопки, под ней, а если там не помещается — туда, где больше места.
Открытие другого меню закрывает предыдущее.
Это исключение из правила «состояние, которое компонент меняет сам, — через
v-model»: шапке не нужно ни открывать меню извне, ни знать, открыто ли
оно. Меню строк таблицы на DropdownButtonWithAction страница держит под
v-model, чтобы знать, какая строка выбрана; у меню профиля такой нужды нет.
v-model, переданный на <hamburger-menu>, в контракт не входит. Атрибуты
с <hamburger-menu> — class, style, data-* — ложатся на обёртку
кнопки.
Клавиатура и мышь — как в меню DropdownButtonWithAction: пока меню открыто,
стрелки вверх и вниз переводят фокус по пунктам, Enter выбирает пункт под
фокусом; наведение подсвечивает пункт, но фокус не двигает: фокус меняют
клавиатура и клик, а пункт под мышью и пункт в фокусе могут быть подсвечены
одновременно.
После закрытия меню фокус возвращается на кнопку меню, если он был внутри меню и после закрытия потерян. Фокус, который перешёл на другой элемент (например, в модалку, открытую пунктом), сохраняется.
DataTable
Список данных: бейдж «Найдено X: N», таблица в карточке с горизонтальной
прокруткой, пустое состояние и пагинация — по ответу Laravel API Resource
{ data, meta, links }. Особые ячейки — слотами.
<data-table
:rows="orderObjects"
:columns="columns"
row-key="uuid"
:meta="orders.meta"
:links="orders.links"
found-text="Найдено ордеров"
empty-text="Не найдено ордеров"
>
<template #results-actions>
<action-pill icon="download" title="скачать xlsx" :is-loading="exportLoading" @click="exportOrders" />
</template>
<template #cell-status="{ row }">
<info-pill :text="row.statusLabel" :color="row.statusColor" />
</template>
</data-table>
columns: [
{ key: 'phone', label: 'Телефон' },
{ key: 'vendor', label: 'Вендор / Сертификат', secondary: (row) => row.certificate },
{ key: 'amount', label: 'Номинал', align: 'right', muted: true },
{ key: 'status', label: 'Статус' },
{ key: 'cost', label: 'Стоимость', align: 'right', visible: isAdmin },
{ key: 'actions', align: 'right', narrow: true },
]
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
rows |
Array |
обязателен | Строки: обычные объекты или модели страницы |
columns |
Array |
обязателен | Столбцы, см. ниже |
rowKey |
String, Function |
обязателен | Имя поля ключа строки или row => ключ |
meta |
Object |
null |
{ from, to, total } пагинации Laravel; есть — внизу пагинация, а число в бейдже — total |
links |
Object |
null |
{ prev, next } пагинации Laravel |
foundText |
String |
null |
«Найдено ордеров» → бейдж «Найдено ордеров: 100», когда записи есть |
emptyText |
String |
null |
«Не найдено ордеров» — бейдж, когда записей нет |
variant |
String |
"page" |
"page" — отдельная карточка с тенью; "card" — таблица внутри карточки страницы, только линия сверху |
rowStripe |
Function |
null |
row => имя цвета | null — полоска слева в первой ячейке строки; строки полосатые всегда |
lang |
String |
язык плагина | "ru" или "en" — тексты пагинации |
Столбец:
| Поле | По умолчанию | Описание |
|---|---|---|
key |
обязателен | Имя слота cell-<key> и поле значения по умолчанию |
label |
"" |
Заголовок; пустой — у столбца действий |
value |
row => row[key] |
Основной текст ячейки. У моделей с геттерами — (row) => row.getName() |
secondary |
нет | row => текст — серая вторая строка |
muted |
false |
Основной текст серый — даты, статусы, суммы |
align |
"left" |
"left", "center" или "right" — для заголовка и ячеек |
wrap |
false |
Текст ячеек переносится |
narrow |
false |
Ширина по содержимому — столбцы действий и иконок |
visible |
true |
false — столбца нет вовсе: ни заголовка, ни ячеек, ни слота |
Скрытый столбец — только отображение: данные, которые пользователю видеть нельзя, сервер не отдаёт.
Слоты: #cell-<key>="{ row, index }" — своя разметка ячейки (отступы,
выравнивание и перенос ячейки остаются за компонентом);
#results-actions — рядом с бейджем, только когда записи есть.
Пагинация: строка «Показаны результаты X - Y из Z» есть, только когда
meta.from и meta.to — числа; «Предыдущая» и «Следующая» — только когда
links.prev и links.next — непустые строки (null, пустая строка и
отсутствующий ключ — кнопки нет). Пагинации нет вовсе, когда meta не
передан или meta.total не больше нуля.
Записей нет — только бейдж с emptyText, без таблицы и пагинации; без
текстов — ничего. Страница за последней (total больше нуля, rows пуст) —
бейдж и пагинация без таблицы. Строки выравниваются по верху. Широкая
таблица прокручивается в своей карточке. Ссылки пагинации — через
navigate плагина. Событий нет.
NavigationMenuElement
Пункт меню шапки: ссылка с подчёркиванием активного раздела.
<navigation-menu-element name="Заказы" :url="route('orders')" :is-active="isOrders" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
url |
String |
обязателен | Адрес раздела |
name |
String |
обязателен | Подпись |
isActive |
Boolean |
false |
Активный раздел |
Ссылка — через navigate плагина. Событий нет.
PageTabs
Табы второго уровня: ссылки на страницы одного раздела под шапкой — «Активные / Архив», «Ордера / Сертификаты / Sms».
<page-tabs :tabs="tabs" :active-key="activeTab" />
tabs() {
return [
{ key: 'active', name: 'Активные', url: route('events') },
{ key: 'archive', name: 'Архив', url: route('archive') },
];
},
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
tabs |
Array |
обязателен | [{ key, name, url }]: ключ, подпись и адрес таба — строки |
activeKey |
String |
null |
Ключ открытого таба |
lang |
String |
язык плагина | "ru" или "en" — имя навигации для скринридера |
Ключ сравнивается с activeKey строго, поэтому ключи — строки. Открытый
таб выделен фоном и aria-current="page"; если ключа нет в списке, не
выделен ни один. Вид одинаков на любой ширине: ссылки в строку, без
переноса. Отступы вокруг и кнопки рядом — разметка страницы.
Ссылки — через navigate плагина. Событий нет.
PageCard
Карточка отдельной страницы — формы или просмотра записи — по центру с крестиком, который ведёт туда, откуда карточку открыли.
<page-card class="px-6 pt-6" :fallback-url="route('users')">
<h3 class="pr-(--bb-closer-space) text-lg font-medium">Создание администратора</h3>
…поля формы…
</page-card>
Страница просмотра, у секций которой свои отступы:
<page-card :fallback-url="route('payments')">
<div class="px-6 py-5 border-b border-gray-200">
<h3 class="pr-(--bb-closer-space) text-lg font-medium">{{ name }}</h3>
</div>
<div class="overflow-hidden rounded-b-lg">…строки…</div>
</page-card>
Куда ведёт крестик. Плагин dashboardUi с currentUrl (см.
«Подключение к приложению») помнит адрес страницы, которая была на экране
перед карточкой, со всеми её фильтрами и номером страницы, и крестик
ведёт ровно туда. Адрес не попадает ни в адрес страницы, ни в хранилища
браузера, ни в историю: он живёт в памяти приложения. «Как было» — это
адрес: прокрутку, несохранённый ввод и раскрытые элементы
страницы-источника карточка не возвращает.
Если адреса нет — карточку открыли в новой вкладке, прямой ссылкой, после
F5 или в приложении без currentUrl, — крестик ведёт на fallback-url.
Без fallback-url крестика нет. Истории браузера карточка не касается.
Окна. Открытая карточка — окно. Карточка, открытая со страницы другой карточки (например, форма с таба), — вложенное окно: её крестик ведёт на страницу, с которой её открыли, а крестик внешней карточки — по-прежнему туда, откуда открыли её. Окно закрывается, когда показана страница, куда оно возвращает: крестиком, «Отменой», после успешного действия или кнопкой «Назад» браузера.
Карточка из нескольких страниц (табы) — постоянный макет этих страниц Inertia с общим ключом окна:
export default {
layout: (props) => [PageCard, {
width: '7xl',
windowKey: `group:${props.group.data.uuid}`,
fallbackUrl: route('groups'),
class: 'px-6 py-6',
}],
}
layout — стрелочная функция одного аргумента или кортеж: обычную
функцию Inertia примет за компонент. Пока соседние страницы объявляют тот
же макет, карточка не пересоздаётся, и смена таба, фильтров и страницы
таблицы окна не меняет. Ключ окна по умолчанию — путь страницы без query:
отдельной странице-карточке его задавать не нужно, а если роутер
переиспользовал её для другой записи (/users/1/edit → /users/2/edit),
у неё новое окно. Ключ меняется только вместе со сменой страницы: смена
без неё оставляет карточку в прежнем окне и пишет предупреждение
в консоль. Рецепт проверен на @inertiajs/vue3 3.6.1.
«Отмена» и закрытие из кода. close() делает то же, что крестик.
Он доступен пропом слота по умолчанию и методом компонента:
<page-card ref="card" v-slot="{ close }" :fallback-url="route('users')">
…
<button type="button" @click="close">Отмена</button>
</page-card>
// после успешного сохранения
this.$refs.card.close()
close() переходит через navigate плагина, без него — полной загрузкой
страницы. Без адреса возврата и fallback-url он ничего не делает.
Ширина и крестик. От width зависят ширина карточки и порог, от
которого крестик стоит снаружи, на 1rem правее её края; ниже порога —
в правом верхнем углу:
width |
Ширина | Крестик снаружи |
|---|---|---|
xl (по умолчанию) |
max-w-xl (36rem) |
от md |
4xl |
max-w-4xl (56rem) |
от lg |
7xl |
max-w-7xl (80rem) |
от 2xl |
Пороги рассчитаны на карточку по центру области на всю ширину страницы: в узкой колонке или рядом с боковой панелью места снаружи может не хватить.
Заголовок оставляет место под крестик правым отступом из переменной
--bb-closer-space: 3.5rem, пока крестик в углу, 0 — когда он снаружи
или его нет. В Tailwind это pr-(--bb-closer-space); без Tailwind 4 —
обычный CSS padding-right: var(--bb-closer-space), а в Tailwind 3 —
pr-[var(--bb-closer-space)]. Вне PageCard отступ равен нулю.
Атрибуты. Карточка — relative my-6 mx-auto bg-white shadow-xl rounded-lg, box-sizing: border-box. Отступов у неё нет: форма
передаёт их классом на корень, страница просмотра — не передаёт. На корень
попадают class, style, data-* и aria-* приложения; классы
приложения сильнее классов пакета. Остальные атрибуты, в том числе id,
не переносятся: в роли макета карточка получает атрибутами пропы
страницы. Пропы страницы с именами пропов карточки — lang, width,
fallbackUrl, windowKey — приходят ей пропами, а не атрибутами; кортеж
макета перекрывает только те, что задаёт сам. Ресет пакета на карточку и её содержимое не распространяется:
разметка страницы, в том числе поля @tailwindcss/forms, выглядит так же,
как вне карточки.
Карточка не обрезает содержимое, как у ConfirmationModal: выпадающие
списки и календари могут выходить за её край. Блок со своим фоном у края
карточки скругляет свои углы сам — например, нижний блок строк:
overflow-hidden rounded-b-lg.
Крестик доступен с клавиатуры: Tab, Enter, пробел; в порядке табуляции крестик идёт после содержимого карточки; при фокусе с клавиатуры вокруг него кольцо. Доступное имя — «Назад» / «Back».
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
width |
String |
"xl" |
"xl", "4xl" или "7xl" |
fallback-url |
String |
null |
куда закрываться без адреса возврата; "" — как null |
window-key |
String |
путь страницы | общий ключ окна карточки из нескольких страниц |
lang |
String |
язык плагина | "ru" или "en" |
Событий нет. Проп слота по умолчанию — close.
Ограничения:
- память плагина не переживает F5 и новую вкладку: тогда работает
fallback-url; - адреса сравниваются строкой: равнозначный адрес с другим порядком
параметров или с лишним
page=1— другой адрес; - если вернуться в карточку «Назад» или «Вперёд» после ухода из неё не через крестик, адрес возврата — страница, с которой пришли в этот раз;
- на одной странице — одна карточка.
NotificationMessage
Всплывающее уведомление: заголовок, текст, иконка по типу и крестик закрытия. Стоит в правом верхнем углу.
<notification-message
v-model="message"
type="confirmation"
notification-heading="Письмо отправлено"
/>
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
modelValue |
String |
"" |
Текст уведомления; пустая строка или null — уведомления нет |
type |
String |
"warning" |
"confirmation" (галочка, серый фон), "warning" (жёлтое) или "dangerous" (красное) |
notificationHeading |
String |
"" |
Заголовок |
lang |
String |
язык плагина | "ru" или "en" |
Уведомление показано, пока текст непуст. Крестик закрывает его сам — эмитит
update:modelValue с пустой строкой, поэтому странице достаточно v-model.
Других событий нет.
Для скринридера уведомление — живая область: role="alert" у dangerous,
role="status" у остальных. Область есть на странице всегда, а пустая
невидима и не перехватывает клики. Скринридер объявляет текст, который
появился после монтирования компонента; текст, уже стоявший при монтировании,
может остаться необъявленным. Роль задаётся типом, поэтому type стоит менять
только пока текст пуст — проще всего держать по экземпляру на каждый тип.
Показывайте одно уведомление за раз: все они стоят в одном углу. Подпись
крестика для скринридера — «Закрыть» / «Close». После закрытия с клавиатуры
фокус не возвращается и уходит на body страницы.
Корень — на весь экран (fixed inset-0), с ресетом пакета; слотов нет.
У корня нет z-index, поэтому среди позиционированных элементов он рисуется в
порядке DOM: ставьте компонент в разметке страницы последним. Под открытой
модалкой или выпадающим меню пакета (их подложки — z-20) уведомление скрыто,
а его крестик не нажимается, поэтому закрывайте модалку, прежде чем показать
уведомление. class и атрибуты попадают на корень на весь экран, так что
страница может поднять его своим классом, например class="z-30".
TextPopover
Иконка-глаз, по клику — подсказка со скрытым текстом: длинный текст SMS, значение ключа. Пустой текст — компонента нет.
<text-popover :text="row.message" />
| Проп | Тип | По умолчанию | Описание |
|---|---|---|---|
text |
String |
обязателен | Скрытый текст; null и "" — компонента нет |
modelValue |
Boolean |
false |
Открыта ли подсказка; v-model необязателен |
lang |
String |
язык плагина | "ru" или "en" |
Событие: update:modelValue — когда подсказка открылась или закрылась сама:
кнопкой, кликом вне, Escape, прокруткой, изменением размера окна, опустевшим
текстом. Значение, пришедшее от родителя, эхом не возвращается.
Подсказка открывается в верхнем слое браузера (Popover API): правым краем по правому краю иконки, шириной до 384 px, вниз, если помещается, иначе туда, где места больше; длинный текст прокручивается внутри, в том числе с клавиатуры. Пока подсказка открыта, остальная страница не выделяется: тройной клик и протяжка мышью выделяют только её текст. У левого края окна подсказка сужается до места слева от иконки, как и меню.
Языки
Тексты пакета есть на русском и английском. Язык компонента — проп lang, если
он задан, иначе язык плагина dashboardUi, иначе русский. Явно переданный текст
главнее языка: cancel-button-text у ConfirmationModal.
| Компонент | ru |
en |
|---|---|---|
ConfirmationModal — кнопка отмены |
Отмена | Cancel |
DropdownButtonWithAction — подпись стрелки для скринридера |
Открыть меню | Open menu |
HamburgerMenu — подпись кнопки для скринридера |
Открыть меню | Open menu |
SelectDateFilter, SelectDateForm, SelectDateIntervalFilter, SelectDateIntervalForm — месяцы, дни недели, кнопки календарей |
по-русски | in English |
SelectDateIntervalFilter, SelectDateIntervalForm — подсказки полей |
от / до | from / to |
Ластик Search, RussianMobileFilter, SelectSingleFilter и SelectSingleForm (язык плагина), SelectMultipleFilter и SelectMultipleForm (проп lang), SelectDateFilter, SelectDateForm, SelectDateIntervalFilter и SelectDateIntervalForm — подпись для скринридера |
Очистить | Clear |
SelectMultipleFilter, SelectMultipleForm — надпись при нескольких выбранных |
Выбрано: N | Selected: N |
SelectMultipleFilter, SelectMultipleForm — подсказка и имя поля поиска |
Поиск | Search |
SelectMultipleFilter, SelectMultipleForm — нет совпадений |
Ничего не найдено | Nothing found |
DataTable — пагинация |
Предыдущая / Следующая / Показаны результаты X - Y из Z | Previous / Next / Showing X - Y of Z results |
PageTabs — имя навигации для скринридера |
Закладки | Tabs |
PageCard — доступное имя крестика |
Назад | Back |
NotificationMessage — подпись крестика для скринридера |
Закрыть | Close |
TextPopover — подпись кнопки и имя области |
Показать текст / Полный текст | Show text / Full text |
Формат даты — дд.мм.гггг в обоих языках: это формат значения, а не текст.
Неделя начинается с понедельника. Календари SelectDateFilter, SelectDateForm,
SelectDateIntervalFilter и SelectDateIntervalForm берут язык при монтировании: смена языка после
монтирования в них не отражается.
Клавиатурная навигация
Кнопки и ссылки компонентов нажимаются расширениями клавиатурной навигации — Vimium, Vimari, Vimlike — по их подсказкам, как мышью; подсказка у каждой кнопки одна и стоит на всей кнопке. Ластики фильтров входят в порядок Tab и нажимаются Enter и пробелом; кольцо фокуса — вокруг крестика. После ластика, нажатого с клавиатуры, фокус переходит на поле или кнопку этого фильтра: ластик пропадает, и без этого фокус упал бы на страницу. При касании и клике мышью фокус остаётся как раньше.
Фокус у всех компонентов пакета выглядит одинаково, как у FormInput: линия
1px blue-300 по краю элемента и ореол 3px blue-200 с прозрачностью 50%.
В режиме высокой контрастности (forced-colors: active) ореола нет, а линия
отстоит от края элемента на 2px: на рамке того же системного цвета она бы
слилась с ней. Вид задан одной утилитой focus-ring в src/styles/index.css,
компоненты выбирают только момент. Поля, кроме SelectSingleCards и
SelectMultipleList, крестик NotificationMessage, кнопки
ConfirmationModal и DropdownButtonWithAction показывают фокус при любом
его появлении, в том числе после клика мышью. Карточки SelectSingleCards,
галочки SelectMultipleList, остальные кнопки и ссылки показывают фокус
только с клавиатуры. Стрелки месяцев и подпись месяца в календаре поля даты
берут тот же вид, когда фокус в них с клавиатуры.
Vimlike (Safari на iPhone, iPad и Mac) считает кнопкой любой элемент
с cursor: pointer и из вложенных таких элементов оставляет самый глубокий.
Курсор наследуется, поэтому в кнопке с курсором-рукой — своим или от правила
приложения button:not(:disabled) { cursor: pointer } — подсказки достались
бы тексту, иконке, у которой нет click(), и скрытой подписи, которую Vimlike
считает невидимой. Поэтому содержимое кнопок и ссылок пакета — текст, иконки,
скрытые подписи — с курсором default и не ловит указатель: мышь над ним
видит руку самой кнопки. По той же причине поле даты SelectDateFilter, SelectDateForm,
SelectDateIntervalFilter и SelectDateIntervalForm лежит не внутри кнопки календаря, а под ней: поле
внутри кнопки Vimlike считал бы кнопкой вместо неё. В поле по-прежнему можно
перейти Tab и набрать дату.
У карточки SelectSingleCards одна подсказка расширения на всей карточке,
у галочки SelectMultipleList — одна на пункт. Safari на Mac без настройки
Tab по всем объектам пропускает переключатели и галочки, как и кнопки пакета.
Подсказки Vimlike в открытых меню, списках SelectSingleFilter,
SelectSingleForm, SelectMultipleFilter и SelectMultipleForm и календарях
SelectDateFilter, SelectDateForm, SelectDateIntervalFilter
и SelectDateIntervalForm не видны: Vimlike рисует
их под верхним слоем браузера, в котором открываются эти панели. Vimium
выводит свои подсказки в верхний слой, и у него такой проблемы нет. В Vimlike
панель открывают подсказкой её кнопки, а дальше выбирают стрелками и Enter:
пункт списка или меню, день календаря.
SSR
Пакет загружается и рендерится на сервере (серверный рендеринг, например Inertia SSR), а серверная разметка гидратируется без расхождений. К API браузера компоненты обращаются только после монтирования, поэтому:
PageCardна сервере рисует крестик, только если заданfallback-url: адрес возврата известен только в браузере, и крестик, ведущий на него, появляется после монтирования;SelectDateFilter,SelectDateForm,SelectDateIntervalFilterиSelectDateIntervalFormзагружают Pikaday в браузере при монтировании: календари появляются, когда загрузка закончится;- остальные компоненты рендерятся на сервере так же, как в браузере.
Правила API пакета
Компонент, добавляемый в пакет, следует тем же правилам, что и уже имеющиеся:
- Значение компонента передаётся через
Related Packages
https://docs.github.com/en/get-started/writing-on-github/getting-started-with-wr...
Version History
| Version | Released | License |
|---|---|---|
| 1.0.0 | MIT | |
| 0.21.0 | MIT | |
| 0.20.0 | MIT | |
| 0.19.1 | MIT | |
| 0.19.0 | MIT | |
| 0.18.0 | MIT | |
| 0.17.1 | MIT | |
| 0.17.0 | MIT | |
| 0.16.3 | MIT | |
| 0.16.2 | MIT | |
| 0.16.1 | MIT | |
| 0.16.0 | MIT | |
| 0.15.1 | MIT | |
| 0.15.0 | MIT | |
| 0.14.0 | MIT | |
| 0.13.0 | MIT | |
| 0.12.0 | MIT | |
| 0.11.1 | MIT | |
| 0.11.0 | MIT | |
| 0.10.1 | MIT | |
| 0.10.0 | MIT | |
| 0.9.0 | MIT | |
| 0.8.0 | MIT | |
| 0.7.0 | MIT | |
| 0.6.0 | MIT | |
| 0.5.0 | MIT | |
| 0.4.0 | MIT | |
| 0.3.0 | MIT | |
| 0.2.1 | MIT | |
| 0.2.0 | MIT |
Showing the latest 30 of 31. See every release on npm