Skip to content

Commit d8d08a7

Browse files
feat: blue-grey look, lit cards, page glow, unclipped rings and a Today that fits the window (#59)
* feat: deep blue-grey look, lit cards, page glow and a Today page that fits the window * fix: share the card head, keep Today fitting the window, and clip hidden tables in cards --------- Co-authored-by: Atharva Acharya <acharyaatharva07@gmail.com>
1 parent 567e88b commit d8d08a7

14 files changed

Lines changed: 719 additions & 204 deletions

File tree

‎src-tauri/src/fitness.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ pub(crate) fn days(index: &Index, today: &str) -> rusqlite::Result<Vec<Day>> {
163163
.collect())
164164
}
165165

166+
/// The daily steps and protein goals: the plan's, or the defaults.
167+
pub(crate) fn daily_goals(index: &Index) -> rusqlite::Result<(i64, i64)> {
168+
Ok(
169+
plan(index)?.map_or((DEFAULT_STEPS_GOAL, DEFAULT_PROTEIN_GOAL), |p| {
170+
(p.steps_goal, p.protein_goal)
171+
}),
172+
)
173+
}
174+
166175
fn plan(index: &Index) -> rusqlite::Result<Option<PlanView>> {
167176
index
168177
.conn()

‎src-tauri/src/today.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub struct Today {
1717
pub applications: Option<i64>,
1818
pub gym: Option<String>,
1919
pub habits: Habits,
20+
/// Daily goals from the fitness plan (or its defaults), for the Today rings.
21+
pub steps_goal: i64,
22+
pub protein_goal: i64,
2023
pub follow_ups: Vec<FollowUp>,
2124
pub project: Option<Resume>,
2225
/// Notes the app could not read, so the owner can fix them.
@@ -120,6 +123,8 @@ pub fn today(index: &Index, date: &str) -> rusqlite::Result<Today> {
120123

121124
out.follow_ups = follow_ups(conn, date)?;
122125
out.project = resume(conn)?;
126+
(out.steps_goal, out.protein_goal) = crate::fitness::daily_goals(index)?;
127+
123128
out.parse_errors = conn.query_row(
124129
"SELECT COUNT(*) FROM notes WHERE error IS NOT NULL",
125130
[],
@@ -220,6 +225,8 @@ mod tests {
220225
}
221226
);
222227
assert_eq!(t.parse_errors, 1);
228+
// The fixture's Fitness/Plan.md sets both goals.
229+
assert_eq!((t.steps_goal, t.protein_goal), (8500, 160));
223230
}
224231

225232
#[test]

‎src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ export default function App() {
145145
expanded={railOpen}
146146
onToggle={toggleRail}
147147
/>
148+
{/* One soft glow in the page's own colour; it fades in on each page. */}
149+
<div key={route.id} className="page-glow" data-page={route.id} aria-hidden="true" />
148150
<main
149151
className="main"
150152
ref={setMain}

‎src/api.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ export interface Today {
6363
applications: number | null;
6464
gym: string | null;
6565
habits: Habits;
66+
/** Daily goals from the fitness plan, or its defaults. */
67+
stepsGoal: number;
68+
proteinGoal: number;
6669
followUps: FollowUp[];
6770
project: Resume | null;
6871
parseErrors: number;

‎src/components/Card.tsx‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,26 @@ export function Metric({
176176
</section>
177177
);
178178
}
179+
180+
/** A card's own title row: the title inside the card, an action on the right. */
181+
export function CardHead({ id, title, action }: { id: string; title: string; action?: ReactNode }) {
182+
return (
183+
<div className="card__head">
184+
<h2 id={id} className="card__title">
185+
{title}
186+
</h2>
187+
{action}
188+
</div>
189+
);
190+
}
191+
192+
export function MoreLink({ children, onClick }: { children: ReactNode; onClick: () => void }) {
193+
return (
194+
<button type="button" className="link-button card__action" onClick={onClick}>
195+
{children}
196+
<svg viewBox="0 0 12 12" aria-hidden="true" focusable="false">
197+
<path d="M4.5 2.5 8 6l-3.5 3.5" />
198+
</svg>
199+
</button>
200+
);
201+
}

‎src/components/Rings.tsx‎

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CSSProperties } from "react";
1+
import { useId, type CSSProperties } from "react";
22

33
/** One activity ring: progress towards a daily goal. */
44
export interface Ring {
@@ -14,8 +14,9 @@ export interface Ring {
1414
hint?: string;
1515
}
1616

17-
const STROKE = 14;
18-
const GAP = 4;
17+
/** Room around the rings for the stroke's round caps and the fill's glow, so
18+
* nothing is ever clipped at the edge of the drawing. */
19+
const PAD = 10;
1920

2021
/** Share of the goal reached, 0 to 1 (a ring is full at its goal). */
2122
export function progress(value: number, goal: number): number {
@@ -28,21 +29,55 @@ function legendValue(r: Ring, n: number): string {
2829
return `${Math.round(n).toLocaleString("en-GB")}${r.unit ?? ""}`;
2930
}
3031

31-
/** Concentric activity rings in the style of Apple Health. The legend
32-
* carries every value as text, so the drawing itself is decorative. */
33-
export function Rings({ rings, size = 168 }: { rings: Ring[]; size?: number }) {
32+
/** The plain number of a value, without its unit, for the large legend. */
33+
function bareValue(r: Ring, n: number): string {
34+
if (r.format) return r.format(n);
35+
return Math.round(n).toLocaleString("en-GB");
36+
}
37+
38+
/** Concentric activity rings in the style of Apple Health. Each ring sits in
39+
* a recessed track. The legend carries every value as text, so the drawing
40+
* itself is decorative. `hero` draws thicker rings with a large legend. */
41+
export function Rings({
42+
rings,
43+
size = 168,
44+
variant = "compact",
45+
}: {
46+
rings: Ring[];
47+
size?: number;
48+
variant?: "compact" | "hero";
49+
}) {
50+
const stroke = variant === "hero" ? 18 : 14;
51+
const gap = variant === "hero" ? 5 : 4;
3452
const centre = size / 2;
53+
const inset = `rings-inset-${useId().replace(/:/g, "")}`;
54+
const box = size + PAD * 2;
3555
return (
36-
<figure className="rings">
56+
<figure className={`rings rings--${variant}`}>
3757
<svg
3858
width={size}
3959
height={size}
40-
viewBox={`0 0 ${String(size)} ${String(size)}`}
60+
viewBox={`${String(-PAD)} ${String(-PAD)} ${String(box)} ${String(box)}`}
4161
role="presentation"
4262
focusable="false"
4363
>
64+
<defs>
65+
{/* A dark edge along the top of each track: the light comes from
66+
above, so the track reads as pressed into the card. */}
67+
<filter id={inset} x="-10%" y="-10%" width="120%" height="120%">
68+
<feOffset dy="1.5" in="SourceAlpha" result="offset" />
69+
<feComposite in="SourceAlpha" in2="offset" operator="out" result="edge" />
70+
<feGaussianBlur in="edge" stdDeviation="1" result="soft" />
71+
<feFlood floodColor="#000" floodOpacity="0.75" />
72+
<feComposite in2="soft" operator="in" result="shadow" />
73+
<feMerge>
74+
<feMergeNode in="SourceGraphic" />
75+
<feMergeNode in="shadow" />
76+
</feMerge>
77+
</filter>
78+
</defs>
4479
{rings.map((r, i) => {
45-
const radius = centre - STROKE / 2 - i * (STROKE + GAP);
80+
const radius = centre - stroke / 2 - i * (stroke + gap);
4681
if (radius <= 0) return null;
4782
const p = progress(r.value, r.goal);
4883
return (
@@ -53,13 +88,23 @@ export function Rings({ rings, size = 168 }: { rings: Ring[]; size?: number }) {
5388
cy={centre}
5489
r={radius}
5590
stroke={r.color}
91+
strokeWidth={stroke}
92+
/>
93+
<circle
94+
className="rings__well"
95+
cx={centre}
96+
cy={centre}
97+
r={radius}
98+
strokeWidth={stroke}
99+
filter={`url(#${inset})`}
56100
/>
57101
<circle
58102
className="rings__fill"
59103
cx={centre}
60104
cy={centre}
61105
r={radius}
62106
stroke={r.color}
107+
strokeWidth={stroke}
63108
pathLength={1}
64109
style={
65110
{
@@ -77,16 +122,31 @@ export function Rings({ rings, size = 168 }: { rings: Ring[]; size?: number }) {
77122
</svg>
78123
<figcaption>
79124
<ul className="rings__legend">
80-
{rings.map((r) => (
81-
<li key={r.key}>
82-
<span className="bar-chart__swatch" style={{ background: r.color }} />
83-
<span className="rings__name">{r.name}</span>
84-
<span className="rings__value">
85-
{legendValue(r, r.value)} / {legendValue(r, r.goal)}
86-
</span>
87-
{r.hint && <span className="rings__hint">{r.hint}</span>}
88-
</li>
89-
))}
125+
{rings.map((r) =>
126+
variant === "hero" ? (
127+
<li key={r.key}>
128+
<span className="rings__line">
129+
<span className="rings__name" style={{ color: r.color }}>
130+
{r.name}
131+
</span>
132+
{r.hint && <span className="rings__hint">{r.hint}</span>}
133+
</span>
134+
<span className="rings__big">
135+
<span className="rings__number">{bareValue(r, r.value)}</span>
136+
<span className="rings__of"> of {legendValue(r, r.goal)}</span>
137+
</span>
138+
</li>
139+
) : (
140+
<li key={r.key}>
141+
<span className="bar-chart__swatch" style={{ background: r.color }} />
142+
<span className="rings__name">{r.name}</span>
143+
<span className="rings__value">
144+
{legendValue(r, r.value)} / {legendValue(r, r.goal)}
145+
</span>
146+
{r.hint && <span className="rings__hint">{r.hint}</span>}
147+
</li>
148+
),
149+
)}
90150
</ul>
91151
</figcaption>
92152
</figure>

‎src/pages/EmptyStates.test.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const BLANK_TODAY: Today = {
1818
applications: null,
1919
gym: null,
2020
habits: { weighIn: false, steps: null, proteinG: null, mealsTracked: false, mobility: false },
21+
stepsGoal: 8000,
22+
proteinGoal: 170,
2123
followUps: [],
2224
project: null,
2325
parseErrors: 0,

‎src/pages/TodayPage.test.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const TODAY: Today = {
1414
applications: 4,
1515
gym: "Pull A",
1616
habits: { weighIn: true, steps: 10450, proteinG: 165, mealsTracked: true, mobility: false },
17+
stepsGoal: 8000,
18+
proteinGoal: 170,
1719
followUps: [
1820
{
1921
path: "Jobs/Companies/Example Security Ltd.md",
@@ -52,7 +54,11 @@ describe("TodayPage", () => {
5254
expect(screen.getByTitle("Since the last weigh-in")).toHaveTextContent("−0.2 kg");
5355
expect(within(screen.getByLabelText("Applied today")).getByText("4")).toBeInTheDocument();
5456
expect(within(screen.getByLabelText("Gym session")).getByText("Pull A")).toBeInTheDocument();
55-
expect(screen.getByText("4 / 5")).toBeInTheDocument();
57+
// Rings: habits done of five, then steps and protein against the plan's goals.
58+
expect(screen.getByText("of 5")).toBeInTheDocument();
59+
expect(screen.getByText("of 8,000")).toBeInTheDocument();
60+
expect(screen.getByText("Goal reached")).toBeInTheDocument();
61+
expect(screen.getByText("5 g to go")).toBeInTheDocument();
5662
});
5763

5864
it("lists overdue follow-ups and where to resume", async () => {

0 commit comments

Comments
 (0)