frontend/insights: steady the daily pair on device
On a real build the first pass only held for a full pair: the window label overflowed beside an hours-long sleep number, the energy number read as a bare "6,5" with no sense of how it was, and a day could sum past the 15h a single record allows The window label now sits under the number, energy carries a level band, the sleep hero caps at "24h+", and the card splits its height between the title and the content so the data stays together instead of spreading
Pedro Lucas Porcellis porcellis@eletrotupi.com · 12 days ago
fb784c6226618a02a32f1f7d619d8844b6b623ef Browse filesParents
5a1373aChanged files
3
Additions
+35
Deletions
-16
frontend/components/insights/InsightMetricCard.tsx
1 1
import React from "react";
2 2
import { ActivityIndicator, StyleSheet, View } from "react-native";
3 3
import { Ionicons } from "@expo/vector-icons";
4 4
import { Text } from "@/components/ui/Text";
5 5
import { Card } from "@/components/ui/Cards";
13 13
iconColor?: string;
14 14
iconBackground?: string;
15 15
isLoading?: boolean;
16 16
emptyMessage?: string;
17 17
value?: string;
18 +
// Plain-language read of the number, e.g. "Baixa"
19 +
band?: string;
18 20
windowLabel?: string;
19 21
detail?: string;
20 22
barRatio?: number;
21 23
comparison?: string | null;
22 24
comparisonColor?: string;
23 25
}
24 26
25 27
// One card for the daily insight pair: a tinted icon beside the title, a hero
26 -
// number with its window, an optional bounded bar, and the comparison line
28 +
// number with its band and window, an optional bounded bar, and the comparison
27 29
export const InsightMetricCard: React.FC<InsightMetricCardProps> = ({
28 30
icon,
29 31
title,
30 32
iconColor = Colors.light.tint,
31 33
iconBackground = Colors.light.tint + "1A",
32 34
isLoading = false,
33 35
emptyMessage,
34 36
value,
37 +
band,
35 38
windowLabel,
36 39
detail,
37 40
barRatio,
38 41
comparison,
39 42
comparisonColor = Colors.light.textSecondary,
51 54
<ActivityIndicator size="small" color={iconColor} />
52 55
) : value == null ? (
53 56
<Text style={styles.empty}>{emptyMessage}</Text>
54 57
) : (
55 58
<>
56 -
<View style={styles.heroRow}>
57 -
<Text style={styles.value}>{value}</Text>
58 -
{windowLabel && <Text style={styles.window}>{windowLabel}</Text>}
59 -
</View>
59 +
<Text style={styles.value}>{value}</Text>
60 +
61 +
{band && <Text style={styles.band}>{band}</Text>}
60 62
61 63
{barRatio != null && (
62 64
<View style={styles.track}>
63 65
<View
64 66
style={[
70 72
]}
71 73
/>
72 74
</View>
73 75
)}
74 76
77 +
{windowLabel && <Text style={styles.window}>{windowLabel}</Text>}
78 +
75 79
{detail && <Text style={styles.detail}>{detail}</Text>}
76 80
77 81
{comparison && (
78 82
<Text style={[styles.comparison, { color: comparisonColor }]}>
79 83
{comparison}
86 90
);
87 91
88 92
const styles = StyleSheet.create({
89 93
container: {
90 94
flex: 1,
95 +
justifyContent: "space-between",
91 96
},
92 97
header: {
93 98
flexDirection: "row",
94 99
alignItems: "center",
95 100
gap: Spacing.inlineGapSm,
105 110
...Typography.bodyMd,
106 111
fontWeight: "700",
107 112
color: Colors.light.text,
108 113
},
109 114
body: {
110 -
marginTop: 10,
111 -
gap: 6,
112 -
},
113 -
heroRow: {
114 -
flexDirection: "row",
115 -
alignItems: "baseline",
116 -
gap: 6,
115 +
marginTop: Spacing.cardGap,
116 +
gap: Spacing.inlineGapSm,
117 117
},
118 118
value: {
119 119
...Typography.headlineXg,
120 120
fontSize: 28,
121 121
lineHeight: 32,
122 +
color: Colors.light.text,
123 +
},
124 +
band: {
125 +
...Typography.bodyMd,
126 +
fontWeight: "700",
122 127
color: Colors.light.text,
123 128
},
124 129
window: {
125 130
...Typography.labelSm,
126 131
fontWeight: "500",
148 153
...Typography.labelSm,
149 154
color: Colors.light.textSecondary,
150 155
opacity: 0.6,
151 156
},
152 157
});
frontend/components/ui/DailyEnergyWidget.tsx
1 1
import { InsightMetricCard } from "@/components/insights/InsightMetricCard";
2 2
import { useInsight } from "@/hooks/useInsights.queries";
3 3
import { Colors } from "@/constants/theme";
4 4
5 5
const ENERGY_SCALE_MAX = 10;
16 16
icon="flash"
17 17
title="Energia"
18 18
isLoading={isLoading}
19 19
emptyMessage="Não há dados disponíveis para o dia de hoje"
20 20
value={avgEnergy != null ? formatEnergy(avgEnergy) : undefined}
21 +
band={avgEnergy != null ? energyBand(avgEnergy) : undefined}
21 22
windowLabel={metadata?.windowLabel}
22 23
barRatio={
23 24
avgEnergy != null
24 25
? Math.max(0, Math.min(1, avgEnergy / ENERGY_SCALE_MAX))
25 26
: undefined
30 31
);
31 32
};
32 33
33 34
function formatEnergy(value: number): string {
34 35
return value.toFixed(1).replace(".", ",");
36 +
}
37 +
38 +
// Same low/high read as the mood history badge: alta from 7 up, baixa below 4
39 +
function energyBand(value: number): string {
40 +
if (value < 4) return "Baixa";
41 +
if (value < 7) return "Média";
42 +
43 +
return "Alta";
35 44
}
36 45
37 46
// Positive means more energy than yesterday; nothing to compare reads null
38 47
function formatEnergyDiff(diff: number | null): string | null {
39 48
if (diff == null) return null;
46 55
function energyDiffColor(diff: number | null): string {
47 56
if (diff == null || diff === 0) return Colors.light.textSecondary;
48 57
49 58
return diff > 0 ? Colors.light.tint : Colors.light.danger;
50 59
}
frontend/components/ui/DailySleepWidget.tsx
1 1
import { InsightMetricCard } from "@/components/insights/InsightMetricCard";
2 2
import { useInsight } from "@/hooks/useInsights.queries";
3 3
import { Colors } from "@/constants/theme";
4 4
5 5
export const DailySleepWidget = () => {
31 31
}
32 32
/>
33 33
);
34 34
};
35 35
36 +
// A single record is capped at 15h, but a day sums them, so guard the display
37 +
const MAX_SLEEP_HOURS = 24;
38 +
36 39
function formatHours(hours: number): string {
37 -
const h = Math.floor(hours);
38 -
const m = Math.round((hours - h) * 60);
40 +
if (hours >= MAX_SLEEP_HOURS) return `${MAX_SLEEP_HOURS}h+`;
41 +
42 +
const minutes = Math.round(hours * 60);
43 +
const h = Math.floor(minutes / 60);
39 44
40 -
if (m === 0) return `${h}h`;
45 +
if (minutes % 60 === 0) return `${h}h`;
41 46
42 -
return `${h}h ${m}min`;
47 +
return `${h}h ${minutes % 60}min`;
43 48
}