GrDashboard
Берут, когда раскладку определяет пользователь.
Обзор
Когда брать
- раскладку определяет пользователь — аналитическая панель, рабочий стол, сводка под конкретную роль: набор и расположение виджетов принадлежат человеку, а не дизайнеру экрана;
- раскладка обязана пережить перезагрузку —
useDashboardLayoutкладёт её вlocalStorageили на сервер через интерфейс хранилища; - на ноутбуке и планшете нужно разное — раскладка хранится на каждый брейкпоинт, недостающая выводится из соседней сверху;
- виджеты разного размера —
minW/minH/maxW/maxHобъявляет сам виджет, потому что «этот график ниже трёх строк не читается» знает только он.
Когда взять другое
| Нужно | Берите |
|---|---|
| Раскладка фиксирована дизайном | CSS Grid и GrCard |
| Две области, границу двигает пользователь | GrSplitter |
| Порядок элементов в одном списке | GrSortableList |
| Разделы, переключаемые вкладками | GrTabs |
| Строки таблицы с изменяемым порядком колонок | GrDataTable |
| Откуда виджет берёт данные | granularity-datasource |
| Чем виджет рисует содержимое | granularity-charts / granularity-chrono |
Дашборд — не способ разложить статичный экран. Если пользователь ничего не двигает, сетка виджетов даёт лишний слой состояния, хранилище и режим редактирования там, где хватило бы CSS Grid.
Примеры 2 у каждого — живое демо и его код
Финансовая сводка, которая помнит раскладку
Включите «Edit», подвигайте и растяните виджеты, затем перезагрузите страницу:
useDashboardLayoutсохраняет раскладку на каждый брейкпоинт и восстанавливает её после монтирования, так что сервер и первый рендер совпадают. Разметка лишь называет виджеты поitem-id;minW/minHзадают сами виджеты, а брейкпоинты меряются по контейнеру — двенадцать колонок от 540 px, две ниже. «Reset» возвращает исходную раскладку.Finance overviewDefault layoutRevenue, October€48,210+12,4%Overdue€6,940-18,2%Cash in bank€182,400+3,1%Overdue invoices- Northwind TradersINV-0418 · 6 days late€1,284.50
- Contoso LtdINV-0415 · 9 days late€432.00
- Fabrikam InteriorsINV-0409 · 17 days late€3,210.00
- Litware IncINV-0402 · 24 days late€2,013.50
Cash runway14 monthsAt September's burn of €13,000 a month, before new contracts.Перенос виджетов между двумя досками
Перетащите виджет за ручку через край одной доски в другую.
transferableразрешает виджету уйти,droppable— прийти: это разные права. Источник убирает виджет сам, приёмник эмититitem-dropс ячейкой, брейкпоинтом и своими опциями раскладки, а страница кладёт его черезaddItem— туда, где его показывала подложка.My boardMy queue12 open · 3 waiting on customerSLA breaches2 today · both on chatSatisfaction94% · 312 ratings this weekTeam boardTeam backlog87 open · oldest 4 daysOn callLena until 18:00, then Omar
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
breakpoints | GrDashboardBreakpoints | undefined | GR_DASHBOARD_BREAKPOINTS | Пороги ширины контейнера, от которых начинается брейкпоинт. |
cols | number | GrDashboardCols | undefined | GR_DASHBOARD_COLS | Число колонок: мапой по брейкпоинтам или одним числом на всё. |
initialBreakpoint | string | undefined | "lg" | Брейкпоинт первого рендера. Ширины контейнера на сервере нет, поэтому и сервер, и первый клиентский рендер берут его — уточнение приходит после монтирования, и гидрация не расходится. |
rowHeight | number | undefined | undefined | Высота строки сетки, px. |
gap | number | undefined | undefined | Зазор между виджетами, px. |
mode | GrDashboardMode | undefined | undefined | `view` — сетка только показывает; `edit` — появляются ручки. |
draggable | boolean | undefined | undefined | Виджеты переносятся за ручку в режиме `edit`; виджет сужает правило своим пропом. Не задан — из `GrConfigProvider`, иначе `true`. |
resizable | boolean | undefined | undefined | Виджеты растягиваются уголком в режиме `edit`; виджет сужает правило своим пропом. Не задан — из `GrConfigProvider`, иначе `true`. |
compact | GrDashboardCompaction | undefined | undefined | Уплотнение после переноса: `vertical` тянет виджеты вверх, `horizontal` — влево, `both` — в обе стороны, `none` оставляет там, куда положили. Не задан — из `GrConfigProvider`, иначе `vertical`. |
preventCollision | boolean | undefined | undefined | Столкновение отменяет перемещение, а не толкает соседей. |
lazy | boolean | undefined | undefined | Содержимое виджета монтируется по попаданию в окно. |
droppable | boolean | undefined | undefined | Сетка принимает виджеты, перетаскиваемые из каталога. Работает только в `mode="edit"`. Проп нужен там, где сеток на странице несколько, а принимать должна одна. |
transferable | boolean | undefined | undefined | Из сетки можно утащить виджет в соседний дашборд. Отдельно от `droppable`: принимать и отдавать — разные разрешения. Архивный дашборд принимает виджеты, но своих не отдаёт. |
layoutобязательныйv-model | GrDashboardResponsiveLayout | — | Раскладка на каждый брейкпоинт. `v-model:layout`. |
ariaLabel | string | undefined | — | Имя группы-сетки. Не задано — из локали («Dashboard»). |
Слоты
| Slot | Type | Описание |
|---|---|---|
default | any | Виджеты `GrDashboardItem` — по одному на запись раскладки. |
empty | any | Что показать, пока в раскладке нет ни одного виджета. |
События
| Event | Сигнатура | Описание |
|---|---|---|
itemMove |
| — |
itemResize |
| — |
itemAutoResize |
| — |
layoutChange | (event: "layoutChange", value: GrDashboardLayout, breakpoint: string): void | — |
breakpointChange | (event: "breakpointChange", breakpoint: string, cols: number): void | — |
update:layout | (event: "update:layout", value: GrDashboardResponsiveLayout): void | — |
itemSettings | (event: "itemSettings", id: string): void | — |
itemDrop | (event: "itemDrop", event: GrDashboardDropEvent): void | — |
itemTransferOut | (event: "itemTransferOut", id: string, transfer: GrDashboardTransfer): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
breakpoint | string | Текущий брейкпоинт: до первого замера контейнера — `initialBreakpoint`. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrDashboardConfigurableProps, GrDashboardMode, GrDashboardItemBounds } from '@feugene/granularity-dashboard/components/GrDashboard'Файлdist/types/components/GrDashboard/config.d.ts
group — раскладка общих чанков: модули из components/shared/ сборка кладёт
в dist/groups/GrDashboardFrame/shared/, а не в безымянный dist/chunks/, и
по имени группы видно, чьё это общее.
На состав CSS это не влияет: классы извлекаются по графу бандла, и общий чанк
достаётся каждому компоненту, который до него дотягивается.
const grDashboardConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDashboard">;Файлdist/types/components/GrDashboard/defaults.d.ts
Пропы GrDashboard, настраиваемые глобально через
<GrConfigProvider :component-defaults="{ GrDashboard: { … } }">.
Только оформление раскладки и её правила. Сама раскладка глобальной
настройкой быть не может по определению — это состояние экземпляра.
mode исключение осмысленное: «вся админка только для просмотра» — ровно
та настройка, которую задают один раз на приложение.
interface GrDashboardConfigurableProps {
rowHeight: number;
gap: number;
mode: GrDashboardMode;
draggable: boolean;
resizable: boolean;
compact: GrDashboardCompaction;
preventCollision: boolean;
lazy: boolean;
droppable: boolean;
/** Из сетки можно утащить виджет в соседний дашборд. */
transferable: boolean;
}Файлdist/types/components/GrDashboard/grDashboardStyles.d.ts
type GrDashboardMode = 'view' | 'edit';Файлdist/types/components/shared/context.d.ts
Границы, объявленные самим виджетом. Раскладка приходит из приложения и про виджет знает только координаты; «этот график ниже трёх строк не читается» знает виджет. Поэтому границы едут не в раскладке, а регистрацией — и применяются к записи поверх неё.
interface GrDashboardItemBounds {
minW?: number;
minH?: number;
maxW?: number;
maxH?: number;
static?: boolean;
/**
* Запрет переноса и растягивания для одного виджета.
*
* Не задан — действует правило сетки. Отличие от `static`: тот про раскладку
* («не двигается и соседям не даёт себя двигать»), а эти два — про
* интерфейс, и каждый снимается по отдельности.
*/
draggable?: boolean;
resizable?: boolean;
/** Имя виджета для ручек и объявлений. */
title?: string;
}Всё, что нужно, чтобы положить брошенный виджет ровно туда, где стояла подложка.
breakpoint и options — не полнота ради полноты: без первого приложение
пишет в lg, когда пользователь на sm, а со своими опциями считает
раскладку не так, как её посчитала сетка, и получает не то, что было показано.
interface GrDashboardDropEvent {
transfer: GrDashboardTransfer;
/** Ячейка левого верхнего угла — та же, что показывала подложка. */
cell: GrDashboardCell;
breakpoint: GrDashboardBreakpoint;
options: GrDashboardMoveOptions;
}interface GrDashboardContext {
mode: ComputedRef<GrDashboardMode>;
cols: ComputedRef<number>;
draggable: ComputedRef<boolean>;
resizable: ComputedRef<boolean>;
lazy: ComputedRef<boolean>;
layout: ComputedRef<GrDashboardLayout>;
order: ComputedRef<string[]>;
itemFor: (id: string) => GrDashboardItemLayout | undefined;const GR_DASHBOARD_KEY: InjectionKey<GrDashboardContext>;Контекст дашборда, если виджет стоит внутри него.
null — легальный ответ: GrDashboardPalette и GrDashboardToolbar
работают и снаружи, через собственные пропы и эмиты.
function useGrDashboardContext(): GrDashboardContext | null;Файлdist/types/components/GrDashboard/safelist.d.ts
const grDashboardSafelist: string[];Файлdist/types/components/GrDashboard/GrDashboard.vue.d.ts
interface GrDashboardProps {
/** Раскладка на каждый брейкпоинт. `v-model:layout`. */
layout: GrDashboardResponsiveLayout;
/** Пороги ширины контейнера, от которых начинается брейкпоинт. */
breakpoints?: GrDashboardBreakpoints;
/** Число колонок: мапой по брейкпоинтам или одним числом на всё. */
cols?: GrDashboardCols | number;
/**
* Брейкпоинт первого рендера. Ширины контейнера на сервере нет, поэтому и
* сервер, и первый клиентский рендер берут его — уточнение приходит после
* монтирования, и гидрация не расходится.
*/
initialBreakpoint?: GrDashboardBreakpoint;
/** Высота строки сетки, px. */
rowHeight?: number;
/** Зазор между виджетами, px. */
gap?: number;
/** `view` — сетка только показывает; `edit` — появляются ручки. */
mode?: GrDashboardMode;
/**
* Виджеты переносятся за ручку в режиме `edit`; виджет сужает правило своим пропом. Не задан —
* из `GrConfigProvider`, иначе `true`.
*/
draggable?: boolean;
/**
* Виджеты растягиваются уголком в режиме `edit`; виджет сужает правило своим пропом. Не задан —
* из `GrConfigProvider`, иначе `true`.
*/
resizable?: boolean;
/**
* Уплотнение после переноса: `vertical` тянет виджеты вверх, `horizontal` — влево, `both` —
* в обе стороны, `none` оставляет там, куда положили. Не задан — из `GrConfigProvider`, иначе
* `vertical`.
*/
compact?: GrDashboardCompaction;
/** Столкновение отменяет перемещение, а не толкает соседей. */
preventCollision?: boolean;
/** Содержимое виджета монтируется по попаданию в окно. */
lazy?: boolean;
/**
* Сетка принимает виджеты, перетаскиваемые из каталога.
*
* Работает только в `mode="edit"`. Проп нужен там, где сеток на странице
* несколько, а принимать должна одна.
*/
droppable?: boolean;
/**
* Из сетки можно утащить виджет в соседний дашборд.
*
* Отдельно от `droppable`: принимать и отдавать — разные разрешения. Архивный
* дашборд принимает виджеты, но своих не отдаёт.
*/
transferable?: boolean;
/** Имя группы-сетки. Не задано — из локали («Dashboard»). */
ariaLabel?: string;
}interface GrDashboardEmits {
/**
* Раскладка изменилась (`v-model:layout`): приходит целиком, с новой записью текущего
* брейкпоинта.
*/
(e: 'update:layout', value: GrDashboardResponsiveLayout): void;
/**
* Раскладка текущего брейкпоинта изменилась — вместе с самим брейкпоинтом; приходит следом
* за `update:layout`.
*/
(e: 'layoutChange', value: GrDashboardLayout, breakpoint: GrDashboardBreakpoint): void;
/** Виджет переехал — указателем или стрелками: прежняя и новая запись раскладки. */
(e: 'itemMove', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
/**
* Пользователь изменил размер виджета — уголком, стрелками или в окне настроек: прежняя и новая
* запись раскладки.
*/
(e: 'itemResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
/**
* Виджет с `auto-height` подстроился под содержимое.
*
* Отдельно от `itemResize` потому, что это не действие пользователя:
* приложение, которое считает раскладку грязной по правкам, иначе спрашивало
* бы «сохранить изменения?» после загрузки данных в виджет.
*/
(e: 'itemAutoResize', id: string, from: GrDashboardItemLayout, to: GrDashboardItemLayout): void;
/** Брейкпоинт сменился по ширине контейнера — новый брейкпоинт и число его колонок. */
(e: 'breakpointChange', breakpoint: GrDashboardBreakpoint, cols: number): void;
/** Виджет попросил открыть свои настройки: нажата встроенная кнопка-шестерёнка. */
(e: 'itemSettings', id: string): void;
/**
* В сетку бросили виджет из каталога.
*
* Сетка сообщает **что и куда**, а кладёт приложение — тем же `addItem`, что
* и по кнопке каталога. Иначе в раскладке появился бы виджет, для которого
* приложение не рисовало разметки.
*/
(e: 'itemDrop', event: GrDashboardDropEvent): void;
/**
* Виджет уехал в другую сетку.
*
* Из своей раскладки сетка убирает его сама — удаление однозначно, разметки
* для него не нужно, в отличие от вставки. Событие говорит приложению, что
* произошло, и несёт ту же нагрузку, что уехала в `itemDrop` приёмника.
*/
(e: 'itemTransferOut', id: string, transfer: GrDashboardTransfer): void;
}Использование из прозы библиотеки
Сдвинуть виджет в пустоту по оси уплотнения нельзя
compact знает четыре режима: 'vertical' (по умолчанию) тянет виджеты вверх,
'horizontal' — влево, 'both' — в обе стороны, 'none' оставляет их там, куда
положили. Отпущенный в пустоту виджет уезжает обратно — вниз при 'vertical',
вправо при 'horizontal', в обоих направлениях при 'both'. Это не дефект, а
определение компактизации. Нужна свободная сетка с дырами — compact: 'none'.
Толчок при столкновении распространяется по цепочке и всегда вниз, в любом режиме: строк бесконечно много, а вправо мешает правый край сетки. В горизонтальных режимах это даёт сочетание, которое стоит увидеть один раз: толкнутый вниз сосед тут же придвигается влево.
Куда бросили — говорит сетка, кладёт приложение
Виджет, перетащенный из каталога, в раскладку сама сетка не пишет: она эмитит
itemDrop с ячейкой, брейкпоинтом и своими опциями раскладки, а addItem
вызывает потребитель — тем же способом, что и по кнопке каталога. Иначе в
раскладке появился бы виджет, для которого приложение не рисовало разметки.
Пока указатель над сеткой, подложка показывает место по-настоящему: предпросмотр
считается тем же addItem с теми же опциями, что уедут в событие. Поэтому
опции и едут в событие: посчитай приложение раскладку своими, результат разошёлся
бы с тем, что человек только что видел.
Столкновение со статикой отменяет перенос целиком
Подвинуть static-виджет нельзя по определению, а положить один поверх другого
значило бы молча испортить раскладку. То же делает preventCollision для любого
столкновения. Половинчатого исхода — «подвинулось насколько влезло» — здесь нет:
пользователь не увидел бы, что именно не поместилось.
itemAutoResize — не то же, что itemResize
Виджет с auto-height подстраивается под содержимое сам, и его высота меняется
без участия человека: приехали данные, раскрыли секцию, сменился язык. Такое
изменение уезжает в update:layout наравне с остальными — раскладка остаётся
единственной правдой, — но сопровождается отдельным событием.
| Событие | Когда |
|---|---|
itemResize | пользователь потянул уголок или нажал стрелку |
itemAutoResize | содержимое виджета попросило другую высоту |
Различать их приходится всякому, кто по правкам решает, грязна ли раскладка:
без этого «сохранить изменения?» всплывало бы после загрузки данных в виджет.
Подробности замера — ./GrDashboardItem.md.
itemTransferOut — виджет уехал к соседям
Два дашборда на странице обмениваются виджетами перетаскиванием: жест за ручку, ушедший за край своей сетки в чужую, становится переносом между ними.
| Событие | У кого | Когда |
|---|---|---|
itemDrop | приёмник | в сетку что-то положили — из каталога или из другого дашборда |
itemTransferOut | источник | виджет уехал; из своей раскладки сетка убрала его сама |
Отличать источники приёмник может по transfer.source ('palette' или
'dashboard') и transfer.from — идентификатору сетки, из которой виджет
приехал. Разбор — ../model.md.
Отдавать и принимать разрешается по отдельности: transferable у источника,
droppable у приёмника.
Пока виджет несут, источник держит его место: виджет прячется, на его ячейке
стоит подложка, соседи не уплотняются. Уплотнись источник сразу — всё ниже него,
вместе с приёмником, подъехало бы под указателем, и бросок пришёлся бы мимо.
Убирается виджет только после того, как приёмник его принял; решает это одна
точка — тот приёмник, что под указателем в момент отпускания. Отпускание мимо
всех сеток, Esc и обрыв указателя (pointercancel) сворачивают перенос, и
виджет возвращается на место.
Нулевая ширина — это не самый узкий экран
Скрытая вкладка, свёрнутая панель, display: none дают нулевую ширину
контейнера, и брейкпоинт от этого не переключается. Принять одно за другое
значило бы перевести дашборд на две колонки и записать эту раскладку в модель —
то есть потерять настройку пользователя из-за того, что он открыл соседнюю
вкладку.
Границы
Чего в пакете нет и не будет — записано, чтобы вопрос не всплывал заново.
- виртуализации сетки — виджетов на дашборде десятки, а не тысячи, и цена сложности не окупается;
- вложенных дашбордов — сетка в сетке даёт неразрешимый вопрос, чей жест обрабатывать;
- своей карточки вместо
GrCard— копия разошлась бы с карточками остального приложения; - конструктора отчётов с полями и агрегатами — это продукт, а не компонент.
Пропы этого примера