GrDashboardPalette
Machine-translated from the Russian original, not yet reviewed. Read the original
When to take it
- the set of widgets is chosen by the user — the dashboard is assembled from a catalogue rather than arriving ready;
- what is available depends on the role or on the plan — the list arrives as a prop, and what is unavailable simply does not enter it;
- the card of the catalogue is non-standard — the
itemslot draws an icon, a description, a preview; - there is nothing to add yet — the
emptyslot explains why, instead of an empty rectangle.
When to take something else
| Need | Take |
|---|---|
| Choose one option from a list | GrSelect |
| Find a command or an object by searching | GrCommandPalette |
| Show a list without adding | GrList |
| Explain that the dashboard is empty | GrEmptyState |
| The grid itself | GrDashboard |
Despite the name, this is not GrCommandPalette: there a command is searched for across the
whole application by typing, here a widget is chosen from a short known set.
Where an added widget goes is decided by the application
The component emits an intent with a description of the widget, and the coordinates are assigned by the consumer: only they know whether to add a new widget at the bottom, in the first free place or at the position the user started from. Putting it “somewhere” would mean spoiling the layout the person has just assembled.
Dragging is a reinforcement rather than a contract
A tile can be dragged onto the grid (draggable, on by default), but the “Add” button stays and
remains the only keyboard path. Make dragging the contract and the catalogue cannot be used from the
keyboard, and “accessibility later” never comes here.
Where it was dropped is reported by the grid with an itemDrop emit, and the putting is still done
by the application — exactly as with the button. A grid that does not listen to itemDrop will
show the underlay and will do nothing on release: either handle the event or switch draggable
off.
A press on the button itself does not become a drag: otherwise a hand that wavered by five pixels would give a drag instead of an addition.
Limits
A tile is not dragged with a finger. Moving by touch requires a touch-action: none on the tile,
and the catalogue is a vertical list, which on a phone is obliged to scroll. Taking the vertical axis
away from a scroller for the sake of a gesture that already has a working equivalent is a bad trade;
on touch input the button remains.
Install
npm i @feugene/granularity-dashboardImport
import { GrDashboardPalette } from '@feugene/granularity-dashboard/components/GrDashboardPalette'API
The API for this component has not been generated yet: the showcase generator only covers the core so far. Until it does, the reference lives in the package documentation.
Examples 1
Basic
<script setup lang="ts">
import { computed, ref } from 'vue'
import { GrButton, GrStatistic } from '@feugene/granularity'
import type { GrDashboardPaletteItem, GrDashboardResponsiveLayout } from '@feugene/granularity-dashboard'
import { addItem, removeItem } from '@feugene/granularity-dashboard'
/**
* Полный круг: добавить виджет из каталога, увидеть его в сетке, убрать обратно.
*
* Каталог раскладкой не владеет — он сообщает, что выбрали, а куда это положить,
* решает приложение. Здесь это две строки с `addItem` и `removeItem` из подпути
* `./layout`.
*/
const breakpoints = { lg: 520, md: 400, sm: 320, xs: 0 }
const cols = { lg: 12, md: 8, sm: 4, xs: 2 }
const moveOptions = { cols: 12 }
interface Widget extends GrDashboardPaletteItem {
kind: 'sessions' | 'errors' | 'revenue' | 'uptime'
}
const catalogue: Widget[] = [
{ id: 'sessions', kind: 'sessions', title: 'Сессии', description: 'За неделю', defaultSize: { w: 6, h: 2 } },
{ id: 'revenue', kind: 'revenue', title: 'Выручка', description: 'За квартал', defaultSize: { w: 6, h: 2 } },
{ id: 'errors', kind: 'errors', title: 'Ошибки', description: 'По часам', defaultSize: { w: 12, h: 2 } },
{ id: 'uptime', kind: 'uptime', title: 'Доступность', description: 'За 30 дней', defaultSize: { w: 6, h: 2 } },
]
const layout = ref<GrDashboardResponsiveLayout>({
lg: [{ id: 'sessions', x: 0, y: 0, w: 6, h: 2 }],
})
const placed = computed(() => layout.value.lg ?? [])
const placedIds = computed(() => new Set(placed.value.map(item => item.id)))
/** Добавленный виджет остаётся в каталоге, но выключенным: список не прыгает. */
const items = computed<GrDashboardPaletteItem[]>(() => catalogue.map(item => ({
...item,
disabled: placedIds.value.has(item.id),
})))
const widgetById = new Map(catalogue.map(item => [item.id, item]))
function add(item: GrDashboardPaletteItem): void {
const size = item.defaultSize ?? { w: 6, h: 2 }
layout.value = {
...layout.value,
lg: addItem(placed.value, { id: item.id, x: 0, y: 0, w: size.w, h: size.h }, moveOptions),
}
}
function remove(id: string): void {
layout.value = { ...layout.value, lg: removeItem(placed.value, id, moveOptions) }
}
const sessions = [980, 1010, 995, 1042, 1078, 1065, 1120, 1156, 1190, 1215, 1246, 1284]
const errors = [14, 11, 9, 12, 7, 6, 8, 5, 4, 6, 3, 2]
</script>
<template>
<div class="grid gap-4 lg:grid-cols-[280px_minmax(0,1fr)] lg:items-start">
<GrDashboardPalette :items="items" aria-label="Что можно добавить" @add="add" />
<GrDashboard
v-model:layout="layout"
mode="edit"
:breakpoints="breakpoints"
:cols="cols"
:row-height="72"
aria-label="Сборка дашборда"
>
<template #empty>
<p class="text-[var(--gr-muted-fg)]">
Дашборд пуст — добавьте виджет из каталога слева.
</p>
</template>
<GrDashboardItem
v-for="item in placed"
:key="item.id"
:item-id="item.id"
:title="widgetById.get(item.id)?.title"
overflow="hidden"
>
<template #editActions>
<GrButton
size="xs"
variant="ghost"
tone="danger"
:aria-label="`Убрать «${widgetById.get(item.id)?.title}»`"
@click="remove(item.id)"
>
Убрать
</GrButton>
</template>
<div class="flex h-full flex-col justify-center">
<GrSparkline v-if="item.id === 'sessions'" :data="sessions" />
<GrSparkline
v-else-if="item.id === 'errors'"
:data="errors"
variant="area"
color="var(--gr-chart-2)"
/>
<GrStatistic
v-else-if="item.id === 'revenue'"
:value="12.48"
:precision="2"
suffix=" млн ₽"
tone="success"
trend="up"
/>
<GrStatistic
v-else
:value="99.95"
:precision="2"
suffix=" %"
tone="success"
/>
</div>
</GrDashboardItem>
</GrDashboard>
</div>
</template>