Skip to content

Widget CRUD System - Design Spec

Goal

Transform the reporting dashboard from a flat, auto-generated KPI card grid into a configurable widget dashboard where each metric card can be individually added, removed, reordered, and resized within a 12-column grid.

Architecture

The MetricPicker remains the "starter" for bulk-selecting metrics and creatives. Once in the report view, logged-in users can toggle Edit mode to fine-tune the layout: drag widgets between grid slots, resize them, remove individual cards, or add new metrics via an inline dropdown. Widget layout is persisted entirely in URL query params so shared links preserve the exact layout. Public/shared report links are always read-only.

Grid System

  • 12-column CSS Grid (consistent across the entire app)
  • Each widget occupies a position and colspan (how many columns it spans)
  • Min width: 2 columns. Max width: 12 columns
  • Default KPI card: 2 columns (up to 6 per row)
  • Default chart widget (phase 2): 6 columns
  • Responsive: collapses to fewer columns on smaller screens
  • The Reports list page also uses this 12-column grid (report cards = 4 columns each)

Modes

ModeWhoWhat
ViewEveryone (default)Clean report, no controls. What the customer sees.
EditLogged-in usersDnD, resize, add/remove, widget-type switching
PreviewLogged-in (from edit)Shows the report exactly as the customer will see it

Shared/public report links always render in View mode regardless of auth.

Edit Mode Controls

Per widget

  • Drag handle (top-left corner) -- grab and move to another grid position
  • X button (top-right corner) -- remove from report
  • Resize handle (bottom-right corner) -- drag to change colspan, snaps to grid columns

Global

  • "+ Add metric" -- last cell in the grid, opens a dropdown of available metrics (grouped by source, excludes already-added ones)
  • "Preview" -- toggles to View mode temporarily
  • "Done" -- exits edit mode, URL is already up to date

URL Persistence

Widget config encoded in ?layout= query parameter:

?layout=imp:2,b_clicks:2,ir:3,custom:scratch_done:2,start:4

Format: metricCode:colspan per widget, comma-separated.

  • Order in URL = order in grid (left-to-right, top-to-bottom)
  • Widgets not in layout but present in m (metrics) render with default colspan
  • Removed widgets are omitted from both layout and m
  • Phase 2 extends with widget type: imp:2:kpi,start:6:chart

UX Polish Requirements

  • No native browser dialogs -- all confirms, alerts, dropdowns must be custom components that match the app's visual style (no confirm(), alert(), prompt())
  • Integrated feel -- all dropdowns, popovers, and controls use the same design tokens (colors, shadows, border-radius) as the rest of the app
  • Delete actions use icon buttons (trash icon), not text buttons that overflow
  • Grid snapping -- widgets snap to column boundaries during drag/resize, no fractional positions

Data Flow (unchanged)

Widgets continue to use useReportTotals / useReportTimeSeries. The composables send metric[]=base and cache via TanStack Query. Adding/removing a widget only changes rendering, not data fetching -- all metric data is fetched regardless.

Phases

Phase 1 (current scope)

  • 12-column grid layout with position and colspan per widget
  • Edit mode with DnD, resize, add/remove
  • Preview button for customer view
  • URL persistence of layout
  • Auth gate (edit only for logged-in users)
  • Reports list page on the same 12-column grid
  • Custom confirm dialog (replace native confirm())
  • Delete as icon button (replace text overflow issue)

Phase 2 (later)

  • Widget type selector per metric (KPI card, line chart, bar chart, table)
  • Engagement funnel as an optional special widget
  • URL format extended: metricCode:colspan:widgetType

Phase 3 (later)

  • Rowspan (widgets spanning multiple rows)
  • Grouping related metrics
  • Saved layout templates

Tech Considerations

  • DnD library: Evaluate @vueuse/integrations (native drag), vue-draggable-plus, or @dnd-kit (if Vue 3 compatible). Prefer lightweight.
  • Grid placement: CSS Grid with grid-column: span N per widget. Position determined by DOM order (matches URL order).
  • Resize: Track mouse delta during drag, snap to nearest column boundary, update colspan.
  • Responsive: Use CSS media queries or container queries to reduce column count on small screens.

Internal documentation