Appearance
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
| Mode | Who | What |
|---|---|---|
| View | Everyone (default) | Clean report, no controls. What the customer sees. |
| Edit | Logged-in users | DnD, resize, add/remove, widget-type switching |
| Preview | Logged-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:41
Format: metricCode:colspan per widget, comma-separated.
- Order in URL = order in grid (left-to-right, top-to-bottom)
- Widgets not in
layoutbut present inm(metrics) render with default colspan - Removed widgets are omitted from both
layoutandm - 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 Nper 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.