@boobooking/dashboard-ui-components

Компоненты дашборда на Vue 3
3,789
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
Maintainer: boobooking

@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) — переход приложения по адресу: для Inertia router.visit(href), для Vue Router router.push(href). Компоненты рисуют настоящий <a href> и отдают navigate только обычный клик левой кнопкой; клики с Ctrl/Cmd/Shift/Alt, средней кнопкой и ссылки с target остаются браузеру. Без navigate браузер идёт по ссылке сам.
  • currentUrl() — адрес страницы на экране, по нему PageCard знает, откуда его открыли. Для Inertia — () => usePage().url (проверено на @inertiajs/vue3 3.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

tailwind-config-viewer

View your Tailwind CSS config file...visually!

19,524,150 2,211
@addf/addf-vue-base-components

https://docs.github.com/en/get-started/writing-on-github/getting-started-with-wr...

2,166 0
modern-filters

Modern filter collection

489 9
@flexdash/node-red-flexdash

Node-RED nodes for the FlexDash dashboard

247,442 10
@alexochihua/exos-library-components

Componentes base Vue 3 y Tailwind

14,674

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