diff --git a/components/renderers/DashboardViewRenderer.tsx b/components/renderers/DashboardViewRenderer.tsx index 955dd66..3da59a9 100644 --- a/components/renderers/DashboardViewRenderer.tsx +++ b/components/renderers/DashboardViewRenderer.tsx @@ -94,49 +94,57 @@ function MetricWidget({ const trend = data?.trend; const isPositive = trend?.startsWith("+"); + // Compact tile (p-4, not p-5) — these pack two-up on phones, so the title + // and headline must stay tight. Title is given a two-line floor so a + // single-line tile lines its value up with a wrapping neighbor in the row. return ( - - - + + + {widget.title ?? widget.name} - - - + + + - - - {data?.isLoading ? ( - - ) : ( - <> - - {formatMetricValue(widget, value === "—" ? undefined : value)} - - {trend && ( - - + {data?.isLoading ? ( + + ) : ( + <> + + {formatMetricValue(widget, value === "—" ? undefined : value)} + + {trend && ( + + + {isPositive ? ( + + ) : ( + + )} + - {isPositive ? ( - - ) : ( - - )} - - {trend} - - + {trend} + - )} - - )} - + + )} + + )} ); } @@ -304,11 +312,28 @@ function renderWidget( /* Main Component */ /* ------------------------------------------------------------------ */ -/** Breakpoint (dp) at which the dashboard switches to 2-column grid */ -const GRID_BREAKPOINT = 600; +/** Minimum width (dp) below which the grid collapses to a single column. */ +const SINGLE_COLUMN_MAX = 340; /** Gap between grid cells in pixels */ const GRID_GAP = 12; +/** Widget types compact enough to pack two-per-row; the rest span full width. */ +const COMPACT_TYPES = new Set(["metric", "kpi"]); + +/** + * How many columns a widget occupies. KPI/metric tiles are compact and pack + * two-up; charts, lists and tables need the full row to stay legible on a + * phone. An explicit `span` on a compact widget is still honored (capped to the + * column count); the layout-derived span only matters for the full-width types, + * which always fill the row regardless. + */ +function effectiveSpan(widget: DashboardWidgetMeta, numColumns: number): number { + if (numColumns === 1) return 1; + const type = widget.type ?? "metric"; + if (COMPACT_TYPES.has(type)) return Math.min(widget.span ?? 1, numColumns); + return numColumns; +} + export function DashboardViewRenderer({ dashboard, widgetData = {}, @@ -316,7 +341,7 @@ export function DashboardViewRenderer({ onWidgetPress: _onWidgetPress, }: DashboardViewRendererProps) { const { width: screenWidth } = useWindowDimensions(); - const numColumns = screenWidth >= GRID_BREAKPOINT ? 2 : 1; + const numColumns = screenWidth > SINGLE_COLUMN_MAX ? 2 : 1; if (isLoading) { return ; @@ -350,8 +375,7 @@ export function DashboardViewRenderer({ let currentSpan = 0; for (const widget of dashboard.widgets) { - // A widget can declare span: 2 to take the full width - const span = Math.min(widget.span ?? 1, numColumns); + const span = effectiveSpan(widget, numColumns); if (currentSpan + span > numColumns && currentRow.length > 0) { rows.push(currentRow); @@ -396,7 +420,7 @@ export function DashboardViewRenderer({ }} > {row.map((widget) => { - const span = Math.min(widget.span ?? 1, numColumns); + const span = effectiveSpan(widget, numColumns); const widgetWidth = numColumns === 1 ? availableWidth