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