GrDashboardPalette

Package: @feugene/granularity-dashboardcompanionGroup: misc

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 item slot draws an icon, a description, a preview;
  • there is nothing to add yet — the empty slot explains why, instead of an empty rectangle.

When to take something else

NeedTake
Choose one option from a listGrSelect
Find a command or an object by searchingGrCommandPalette
Show a list without addingGrList
Explain that the dashboard is emptyGrEmptyState
The grid itselfGrDashboard

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-dashboard

Import

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

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>

Component documentationAll components