Skip to content

Commit 458cfdc

Browse files
committed
#develop focus pulse
1 parent 2125ad9 commit 458cfdc

File tree

3 files changed

+176
-4
lines changed

3 files changed

+176
-4
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Tooltip, TooltipProvider } from '@repo/ui';
2+
import React from 'react';
3+
import type { specta } from '@/environment';
4+
import { categoryToColor, FocusCategoryTooltip } from '@/features/focus';
5+
6+
export const FocusPulse: React.FC<TFocusPulseProps> = (props) => {
7+
const { activities } = props;
8+
9+
const { focusedMs, neutralMs, distractingMs, score } = React.useMemo(() => {
10+
let focusedMs = 0;
11+
let neutralMs = 0;
12+
let distractingMs = 0;
13+
for (const a of activities) {
14+
const dur = a.endedAt - a.startedAt;
15+
if (a.category === 'focused') focusedMs += dur;
16+
else if (a.category === 'neutral') neutralMs += dur;
17+
else if (a.category === 'distracting') distractingMs += dur;
18+
}
19+
const totalMs = focusedMs + neutralMs + distractingMs;
20+
const score =
21+
totalMs === 0
22+
? 0
23+
: Math.min(100, Math.round(((focusedMs + neutralMs * 0.5) / totalMs) * 100));
24+
return { focusedMs, neutralMs, distractingMs, score };
25+
}, [activities]);
26+
27+
const size = 128;
28+
const center = size / 2;
29+
const r = 52;
30+
const strokeWidth = 14;
31+
const c = 2 * Math.PI * r;
32+
const totalMs = focusedMs + neutralMs + distractingMs;
33+
34+
const focusedLen = totalMs > 0 ? (focusedMs / totalMs) * c : 0;
35+
const neutralLen = totalMs > 0 ? (neutralMs / totalMs) * c : 0;
36+
const distractingLen = totalMs > 0 ? (distractingMs / totalMs) * c : 0;
37+
38+
const focusedActivities = React.useMemo(
39+
() => activities.filter((activity) => activity.category === 'focused'),
40+
[activities]
41+
);
42+
const neutralActivities = React.useMemo(
43+
() => activities.filter((activity) => activity.category === 'neutral'),
44+
[activities]
45+
);
46+
const distractingActivities = React.useMemo(
47+
() => activities.filter((activity) => activity.category === 'distracting'),
48+
[activities]
49+
);
50+
51+
// MARK: - UI
52+
53+
return (
54+
<TooltipProvider delay={300} closeDelay={50}>
55+
<div className="flex flex-col items-center gap-1.5">
56+
{/* Donut */}
57+
<div className="relative" style={{ width: size, height: size }}>
58+
<svg width={size} height={size}>
59+
{/* Background track */}
60+
<circle
61+
cx={center}
62+
cy={center}
63+
r={r}
64+
fill="none"
65+
strokeWidth={strokeWidth}
66+
className="text-base-100"
67+
stroke="currentColor"
68+
/>
69+
{/* Focused arc */}
70+
{focusedLen > 0 && (
71+
<Tooltip
72+
content={
73+
<FocusCategoryTooltip
74+
category="focused"
75+
durationMs={focusedMs}
76+
activities={focusedActivities}
77+
/>
78+
}
79+
side="left"
80+
positionerClassName="z-50"
81+
>
82+
<circle
83+
cx={center}
84+
cy={center}
85+
r={r}
86+
fill="none"
87+
strokeWidth={strokeWidth}
88+
stroke={categoryToColor('focused')}
89+
strokeDasharray={`${focusedLen} ${c}`}
90+
strokeDashoffset={0}
91+
transform={`rotate(-90 ${center} ${center})`}
92+
className="cursor-default transition-opacity hover:opacity-80"
93+
/>
94+
</Tooltip>
95+
)}
96+
{/* Neutral arc */}
97+
{neutralLen > 0 && (
98+
<Tooltip
99+
content={
100+
<FocusCategoryTooltip
101+
category="neutral"
102+
durationMs={neutralMs}
103+
activities={neutralActivities}
104+
/>
105+
}
106+
side="left"
107+
positionerClassName="z-50"
108+
>
109+
<circle
110+
cx={center}
111+
cy={center}
112+
r={r}
113+
fill="none"
114+
strokeWidth={strokeWidth}
115+
stroke={categoryToColor('neutral')}
116+
strokeDasharray={`${neutralLen} ${c}`}
117+
strokeDashoffset={-focusedLen}
118+
transform={`rotate(-90 ${center} ${center})`}
119+
className="cursor-default transition-opacity hover:opacity-80"
120+
/>
121+
</Tooltip>
122+
)}
123+
{/* Distracting arc */}
124+
{distractingLen > 0 && (
125+
<Tooltip
126+
content={
127+
<FocusCategoryTooltip
128+
category="distracting"
129+
durationMs={distractingMs}
130+
activities={distractingActivities}
131+
/>
132+
}
133+
side="left"
134+
positionerClassName="z-50"
135+
>
136+
<circle
137+
cx={center}
138+
cy={center}
139+
r={r}
140+
fill="none"
141+
strokeWidth={strokeWidth}
142+
stroke={categoryToColor('distracting')}
143+
strokeDasharray={`${distractingLen} ${c}`}
144+
strokeDashoffset={-(focusedLen + neutralLen)}
145+
transform={`rotate(-90 ${center} ${center})`}
146+
className="cursor-default transition-opacity hover:opacity-80"
147+
/>
148+
</Tooltip>
149+
)}
150+
</svg>
151+
{/* Score */}
152+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
153+
<span className="text-base-900 text-3xl font-bold tabular-nums">{score}</span>
154+
</div>
155+
</div>
156+
157+
{/* Label */}
158+
<span className="text-base-400 text-[11px]">focus score</span>
159+
</div>
160+
</TooltipProvider>
161+
);
162+
};
163+
164+
interface TFocusPulseProps {
165+
activities: specta.WindowActivityDto[];
166+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './ActivityBalanceChart';
2+
export * from './FocusPulse';
23
export * from './UsageSection';

apps/desktop/src/routes/window.activity.overview/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { unwrapOr } from 'tuple-result';
55
import { specta } from '@/environment';
66
import { usePlatform } from '@/hooks';
77
import { toTuple } from '@/lib';
8-
import { ActivityBalanceChart, UsageSection, type TUsageEntry } from './components';
8+
import { ActivityBalanceChart, FocusPulse, UsageSection, type TUsageEntry } from './components';
99

1010
export const Route = createFileRoute('/window/activity/overview/')({
1111
loader: async () => {
@@ -87,7 +87,7 @@ function RouteComponent() {
8787

8888
return (
8989
<>
90-
{/* Header — drag region only */}
90+
{/* Header */}
9191
<header
9292
data-tauri-drag-region
9393
className={cn('shrink-0 select-none', platform === 'macos' ? 'h-8' : 'h-2')}
@@ -115,8 +115,13 @@ function RouteComponent() {
115115
</div>
116116
</div>
117117

118-
{/* Activity balance chart */}
119-
<ActivityBalanceChart activities={activities} startOfDay={startedAt} />
118+
{/* Activity balance chart + Focus Pulse */}
119+
<div className="flex items-start gap-4">
120+
<div className="min-w-0 flex-1">
121+
<ActivityBalanceChart activities={activities} startOfDay={startedAt} />
122+
</div>
123+
<FocusPulse activities={activities} />
124+
</div>
120125

121126
{/* App usage */}
122127
{appUsage.length > 0 && (

0 commit comments

Comments
 (0)