Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 67 additions & 43 deletions components/renderers/DashboardViewRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Card className="mb-3">
<CardHeader className="flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
<Card className="mb-3 p-4">
<View className="flex-row items-start justify-between">
<Text
className="min-h-[40px] flex-1 pr-2 text-sm font-medium text-muted-foreground"
numberOfLines={2}
>
{widget.title ?? widget.name}
</CardTitle>
<View className="rounded-lg bg-primary/10 p-2">
<Hash size={18} color="#1e40af" />
</Text>
<View className="rounded-lg bg-primary/10 p-1.5">
<Hash size={16} color="#1e40af" />
</View>
</CardHeader>
<CardContent>
{data?.isLoading ? (
<ActivityIndicator size="small" />
) : (
<>
<Text className="text-2xl font-bold text-card-foreground">
{formatMetricValue(widget, value === "—" ? undefined : value)}
</Text>
{trend && (
<View className="mt-2 flex-row items-center">
<View
className={`flex-row items-center rounded-full px-2.5 py-1 ${
isPositive ? "bg-emerald-50" : "bg-red-50"
</View>
{data?.isLoading ? (
<ActivityIndicator size="small" className="mt-3 self-start" />
) : (
<>
<Text
className="mt-1 text-2xl font-bold text-card-foreground"
numberOfLines={1}
adjustsFontSizeToFit
>
{formatMetricValue(widget, value === "—" ? undefined : value)}
</Text>
{trend && (
<View className="mt-2 flex-row">
<View
className={`flex-row items-center rounded-full px-2 py-0.5 ${
isPositive ? "bg-emerald-500/10" : "bg-red-500/10"
}`}
>
{isPositive ? (
<TrendingUp size={12} color="#059669" />
) : (
<TrendingDown size={12} color="#dc2626" />
)}
<Text
className={`ml-1 text-xs font-semibold ${
isPositive ? "text-emerald-600" : "text-red-600"
}`}
>
{isPositive ? (
<TrendingUp size={12} color="#059669" />
) : (
<TrendingDown size={12} color="#dc2626" />
)}
<Text
className={`ml-1 text-xs font-semibold ${
isPositive ? "text-emerald-700" : "text-red-600"
}`}
>
{trend}
</Text>
</View>
{trend}
</Text>
</View>
)}
</>
)}
</CardContent>
</View>
)}
</>
)}
</Card>
);
}
Expand Down Expand Up @@ -304,19 +312,36 @@ 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 = {},
isLoading = false,
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 <DashboardSkeleton />;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading