Skip to content

Commit 876e8ec

Browse files
authored
feat: shortcuts for every screen-level control (#27)
## Summary Every control that appears once per screen now has a shortcut (rebindable in Ctrl+/, hinted on hover). Row-level buttons in lists stay reachable with Tab + Enter. | Screen | Control | Default | | --- | --- | --- | | Focus | Reset session | Alt+R | | Focus | Extend finished session | Alt+Shift+E | | Focus | Edit minutes | Alt+I | | Focus | Auto-shuffle animations on/off | Alt+Shift+A | | Focus | Next animation | Alt+Shift+C | | To-Dos / Habits / Goals / Tags | Show / hide archived | Alt+Shift+H | | To-Dos | Show / hide backlog | Alt+Shift+K | | Habits | Switch 7 / 30 days | Alt+Shift+D | | Habits, Focus summary | Previous / next period | Alt+Shift+← / → | | Focus summary | Next chart range (7→30→90→180→365 days) | Alt+Shift+P | | Boost | Public / private post | Alt+Shift+V | | Tags | Back | Alt+Shift+B | | About Me | Save | Ctrl+S | | Menu | Sounds / Notifications | Alt+Shift+S / Alt+Shift+O | Defaults avoid keys browsers keep for themselves (Alt+←/→ back/forward, Alt+D/E/F). Each handler is enabled only while its control is on screen. Ctrl+S is captured on About Me only, and saves only when there are unsaved edits. The Notifications logic is shared between the menu item and the shortcut. README shortcut table updated. ## Testing - Lint, typecheck, unit tests and build pass. - Every new shortcut exercised in the browser at 1440x900 with its visible effect confirmed (reset/extend states, minutes focus, shuffle flag, animation cycling, archived/backlog/range toggles, period navigation incl. disabled forward arrow, chart range cycle, visibility toggle, back, save, Sounds panel, Notifications dialog). All 16 appear in the Ctrl+/ panel. - Not reached: the Notifications "ask permission" branch (test browser had permission denied); it reuses the menu item's existing logic.
1 parent abfa01a commit 876e8ec

13 files changed

Lines changed: 160 additions & 25 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,25 @@ Press <kbd>Ctrl</kbd>+<kbd>/</kbd> to open the shortcut list. Click a key to cha
7474
| | Focus summary | <kbd>Alt</kbd>+<kbd>H</kbd> |
7575
| | About Me · Tags | <kbd>Alt</kbd>+<kbd>P</kbd> · <kbd>Alt</kbd>+<kbd>T</kbd> |
7676
| | Open / close menu | <kbd>Alt</kbd>+<kbd>M</kbd> |
77+
| | Back (Tags page) | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>B</kbd> |
7778
| Focus timer | Start / pause / resume | <kbd>Alt</kbd>+<kbd>S</kbd> |
7879
| | Switch focus / break | <kbd>Alt</kbd>+<kbd>B</kbd> |
7980
| | End or complete session | <kbd>Alt</kbd>+<kbd>X</kbd> |
8081
| | Edit focus goal · Choose animation | <kbd>Alt</kbd>+<kbd>G</kbd> · <kbd>Alt</kbd>+<kbd>A</kbd> |
8182
| | Add tag | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> |
83+
| | Reset session · Extend finished session | <kbd>Alt</kbd>+<kbd>R</kbd> · <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> |
84+
| | Edit minutes | <kbd>Alt</kbd>+<kbd>I</kbd> |
85+
| | Auto-shuffle animations on/off · Next animation | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>A</kbd> · <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>C</kbd> |
8286
| Lists | New item on the current page | <kbd>Alt</kbd>+<kbd>N</kbd> |
8387
| | Post to Boost · Attach media | <kbd>Ctrl</kbd>+<kbd>Enter</kbd> · <kbd>Alt</kbd>+<kbd>U</kbd> |
88+
| | Public / private post | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>V</kbd> |
89+
| | Show / hide archived · Show / hide backlog | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd> · <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>K</kbd> |
90+
| | Switch 7 / 30 days (Habits) | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd> |
91+
| | Previous · next period | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>←</kbd> · <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>→</kbd> |
92+
| | Next chart range (Focus summary) | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd> |
8493
| App | Edit display name | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd> |
94+
| | Save page (About Me) | <kbd>Ctrl</kbd>+<kbd>S</kbd> |
95+
| | Sounds · Notifications | <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> · <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>O</kbd> |
8596
| | Show or hide the shortcut list | <kbd>Ctrl</kbd>+<kbd>/</kbd> |
8697

8798
Shortcuts without <kbd>Ctrl</kbd>, <kbd>Alt</kbd> or <kbd>Meta</kbd> are ignored while you type in a field. Hover a button to see its current shortcut. On Linux, Chrome reserves <kbd>Alt</kbd>+<kbd>1</kbd>…<kbd>8</kbd> for switching tabs; rebind those if they clash.

‎src/components/boost/BoostFeed.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useEffect, useRef, useState, type DragEvent, type FormEvent } from "react";
44
import { Globe, Handshake, Loader2, Lock, Paperclip, Send, Sparkles, Trash2, X } from "../icons";
5-
import { useHotkey } from "../hotkeys/HotkeysProvider";
5+
import { hotkeyHint } from "../focus/GoalForm";
6+
import { useHotkey, useHotkeyLabel } from "../hotkeys/HotkeysProvider";
67
import { getDisplayName } from "../lib/profile";
78
import { getLocalUserId, localDb, type Row } from "../lib/local-db";
89
import { PostContent } from "./PostContent";
@@ -106,6 +107,8 @@ export function BoostFeed() {
106107
useHotkey("list.new", () => composerRef.current?.focus());
107108
useHotkey("boost.post", () => formRef.current?.requestSubmit());
108109
useHotkey("boost.attach", () => fileInputRef.current?.click());
110+
useHotkey("boost.visibility", () => setIsPublic((current) => !current), !loading);
111+
const visibilityCombo = useHotkeyLabel("boost.visibility");
109112
const dragDepthRef = useRef(0);
110113

111114
useEffect(() => {
@@ -313,6 +316,7 @@ export function BoostFeed() {
313316
<button
314317
type="button"
315318
onClick={() => setIsPublic(!isPublic)}
319+
title={hotkeyHint(isPublic ? "Make private" : "Make public", visibilityCombo)}
316320
className="flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors"
317321
>
318322
{isPublic ? <Globe className="w-4 h-4" /> : <Lock className="w-4 h-4" />}

‎src/components/focus/AnimationPicker.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export function AnimationPicker({
6666
onAutoShuffleToggle,
6767
}: AnimationPickerProps) {
6868
const toggleCombo = useHotkeyLabel("timer.animation");
69+
const shuffleCombo = useHotkeyLabel("timer.shuffle");
70+
const nextCombo = useHotkeyLabel("timer.nextAnimation");
6971
return (
7072
<div className="mb-8 px-2 sm:px-0 relative">
7173
<button
@@ -98,6 +100,7 @@ export function AnimationPicker({
98100
<button
99101
type="button"
100102
onClick={onAutoShuffleToggle}
103+
title={hotkeyHint("Auto-shuffle animations", shuffleCombo)}
101104
className={`w-full flex items-center justify-between p-3 rounded-lg border-2 transition-all ${
102105
autoShuffle ? "border-blue-500 bg-blue-900/30" : "border-gray-700 bg-gray-900 hover:border-gray-600"
103106
}`}
@@ -118,7 +121,7 @@ export function AnimationPicker({
118121
</div>
119122
</button>
120123

121-
<div className="grid grid-cols-3 gap-3">
124+
<div className="grid grid-cols-3 gap-3" title={hotkeyHint("Next animation", nextCombo)}>
122125
{ANIMATIONS.map(({ type, title }) => {
123126
const isSelected = selectedAnimation === type;
124127
return (

‎src/components/focus/FocusTimer.tsx‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ export const FocusTimer = forwardRef<FocusTimerHandle, FocusTimerProps>(function
192192
localStorage.setItem(SELECTED_ANIMATION_KEY, type);
193193
};
194194

195+
const selectNextAnimation = () => {
196+
const index = ANIMATIONS.findIndex((animation) => animation.type === selectedAnimation);
197+
selectAnimation(ANIMATIONS[(index + 1) % ANIMATIONS.length].type);
198+
};
199+
195200
const rerollAnimation = () => {
196201
if (autoShuffle) setSelectedAnimation(randomAnimationOtherThan(selectedAnimation));
197202
};
@@ -533,9 +538,16 @@ export const FocusTimer = forwardRef<FocusTimerHandle, FocusTimerProps>(function
533538
useHotkey("timer.end", endCurrent, isActive);
534539
useHotkey("timer.switch", switchMode, isActive);
535540
useHotkey("timer.animation", () => setShowPicker((visible) => !visible), isActive && isIdle);
541+
useHotkey("timer.shuffle", toggleAutoShuffle, isActive && isIdle);
542+
useHotkey("timer.nextAnimation", selectNextAnimation, isActive && isIdle);
543+
const isSessionComplete = !isIdle && !isOnBreak && focus.isCompleted;
544+
useHotkey("timer.reset", resetTimer, isActive && !isIdle && !isDirectBreak && !isOnBreak);
545+
useHotkey("timer.extend", () => void extendFocus(), isActive && isSessionComplete);
536546
const toggleCombo = useHotkeyLabel("timer.toggle");
537547
const endCombo = useHotkeyLabel("timer.end");
538548
const switchCombo = useHotkeyLabel("timer.switch");
549+
const resetCombo = useHotkeyLabel("timer.reset");
550+
const extendCombo = useHotkeyLabel("timer.extend");
539551
const inputForPhase = isOnBreak ? breakMinutes : inputMinutes;
540552
const frameState: FrameState = {
541553
isOnBreak,
@@ -631,7 +643,7 @@ export const FocusTimer = forwardRef<FocusTimerHandle, FocusTimerProps>(function
631643
/>
632644
</div>
633645

634-
{focus.isCompleted && !isOnBreak ? (
646+
{isSessionComplete ? (
635647
<div className="bg-green-900/20 border border-green-700/40 rounded-xl px-3 py-2.5 mb-2.5 text-left">
636648
<div className="flex items-center justify-center gap-1.5 mb-2.5">
637649
<CheckCircle2 className="w-4 h-4 text-green-400" />
@@ -654,6 +666,7 @@ export const FocusTimer = forwardRef<FocusTimerHandle, FocusTimerProps>(function
654666
<span className="text-gray-500 text-xs">min</span>
655667
<button
656668
onClick={extendFocus}
669+
title={hotkeyHint("Extend", extendCombo)}
657670
className="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded-md text-xs font-medium transition-colors"
658671
>
659672
Extend
@@ -731,7 +744,7 @@ export const FocusTimer = forwardRef<FocusTimerHandle, FocusTimerProps>(function
731744
<Play className="w-5 h-5" />
732745
Start Break
733746
</TimerButton>
734-
<TimerButton onClick={resetTimer} className={`${BUTTON_BASE} px-4 py-3 bg-gray-500 hover:bg-gray-600`}>
747+
<TimerButton onClick={resetTimer} title={hotkeyHint("Reset", resetCombo)} className={`${BUTTON_BASE} px-4 py-3 bg-gray-500 hover:bg-gray-600`}>
735748
<RotateCcw className="w-5 h-5" />
736749
Reset
737750
</TimerButton>
@@ -753,7 +766,7 @@ export const FocusTimer = forwardRef<FocusTimerHandle, FocusTimerProps>(function
753766
<Target className={ICON_SMALL} />
754767
<span>Done</span>
755768
</TimerButton>
756-
<TimerButton onClick={resetTimer} className={`${BUTTON_SMALL} bg-gray-500 hover:bg-gray-600`}>
769+
<TimerButton onClick={resetTimer} title={hotkeyHint("Reset", resetCombo)} className={`${BUTTON_SMALL} bg-gray-500 hover:bg-gray-600`}>
757770
<RotateCcw className={ICON_SMALL} />
758771
Reset
759772
</TimerButton>

‎src/components/focus/GoalForm.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ export function GoalForm({
276276
goalInput.current?.focus();
277277
goalInput.current?.select();
278278
}, isActive);
279+
const minutesInput = useRef<HTMLInputElement>(null);
280+
useHotkey("timer.minutes", () => {
281+
minutesInput.current?.focus();
282+
minutesInput.current?.select();
283+
}, isActive);
284+
const minutesCombo = useHotkeyLabel("timer.minutes");
279285
// -1 = typing a fresh goal; otherwise the index into goalHistory being previewed.
280286
const [historyIndex, setHistoryIndex] = useState(-1);
281287
const [draftGoal, setDraftGoal] = useState("");
@@ -327,6 +333,7 @@ export function GoalForm({
327333
Minutes
328334
</FieldLabel>
329335
<input
336+
ref={minutesInput}
330337
type="number"
331338
value={inputMinutes}
332339
onChange={(event) => onMinutesChange(event.target.value)}
@@ -335,6 +342,7 @@ export function GoalForm({
335342
}}
336343
min="1"
337344
max="120"
345+
title={hotkeyHint("Minutes", minutesCombo)}
338346
className={INPUT_CLASS}
339347
/>
340348
</div>

‎src/components/hotkeys/bindings.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,35 @@ export const HOTKEY_ACTIONS: readonly HotkeyAction[] = [
1919
{ id: "nav.aboutme", group: "Navigate", label: "About Me", defaultCombo: "Alt+P" },
2020
{ id: "nav.tags", group: "Navigate", label: "Tags", defaultCombo: "Alt+T" },
2121
{ id: "nav.menu", group: "Navigate", label: "Open / close menu", defaultCombo: "Alt+M" },
22+
{ id: "page.back", group: "Navigate", label: "Back", defaultCombo: "Alt+Shift+B" },
2223

2324
{ id: "timer.toggle", group: "Focus timer", label: "Start / pause / resume", defaultCombo: "Alt+S" },
2425
{ id: "timer.switch", group: "Focus timer", label: "Switch focus / break", defaultCombo: "Alt+B" },
2526
{ id: "timer.end", group: "Focus timer", label: "End or complete session", defaultCombo: "Alt+X" },
2627
{ id: "timer.goal", group: "Focus timer", label: "Edit focus goal", defaultCombo: "Alt+G" },
2728
{ id: "timer.animation", group: "Focus timer", label: "Choose animation", defaultCombo: "Alt+A" },
2829
{ id: "timer.addTag", group: "Focus timer", label: "Add tag", defaultCombo: "Alt+Shift+T" },
30+
{ id: "timer.reset", group: "Focus timer", label: "Reset session", defaultCombo: "Alt+R" },
31+
{ id: "timer.extend", group: "Focus timer", label: "Extend finished session", defaultCombo: "Alt+Shift+E" },
32+
{ id: "timer.minutes", group: "Focus timer", label: "Edit minutes", defaultCombo: "Alt+I" },
33+
{ id: "timer.shuffle", group: "Focus timer", label: "Auto-shuffle animations on/off", defaultCombo: "Alt+Shift+A" },
34+
{ id: "timer.nextAnimation", group: "Focus timer", label: "Next animation", defaultCombo: "Alt+Shift+C" },
2935

3036
{ id: "list.new", group: "Lists", label: "New item on current page", defaultCombo: "Alt+N" },
3137
{ id: "boost.post", group: "Lists", label: "Post to Boost", defaultCombo: "Ctrl+Enter" },
3238
{ id: "boost.attach", group: "Lists", label: "Attach media to post", defaultCombo: "Alt+U" },
39+
{ id: "boost.visibility", group: "Lists", label: "Public / private post", defaultCombo: "Alt+Shift+V" },
40+
{ id: "list.archived", group: "Lists", label: "Show / hide archived", defaultCombo: "Alt+Shift+H" },
41+
{ id: "list.backlog", group: "Lists", label: "Show / hide backlog", defaultCombo: "Alt+Shift+K" },
42+
{ id: "list.range", group: "Lists", label: "Switch 7 / 30 days", defaultCombo: "Alt+Shift+D" },
43+
{ id: "list.prev", group: "Lists", label: "Previous period", defaultCombo: "Alt+Shift+Left" },
44+
{ id: "list.next", group: "Lists", label: "Next period", defaultCombo: "Alt+Shift+Right" },
45+
{ id: "history.period", group: "Lists", label: "Next chart range", defaultCombo: "Alt+Shift+P" },
3346

3447
{ id: "app.displayName", group: "App", label: "Edit display name", defaultCombo: "Alt+Shift+N" },
48+
{ id: "page.save", group: "App", label: "Save page", defaultCombo: "Ctrl+S" },
49+
{ id: "app.sounds", group: "App", label: "Sounds", defaultCombo: "Alt+Shift+S" },
50+
{ id: "app.notifications", group: "App", label: "Notifications", defaultCombo: "Alt+Shift+O" },
3551
{ id: "app.fullPage", group: "App", label: "Open full page (extension popup)", defaultCombo: "Alt+Enter" },
3652
{ id: "app.shortcuts", group: "App", label: "Show or hide this list", defaultCombo: "Ctrl+/" },
3753
];

‎src/components/pages/AboutMePage.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4+
import { hotkeyHint } from "../focus/GoalForm";
5+
import { useHotkey, useHotkeyLabel } from "../hotkeys/HotkeysProvider";
46
import { Save } from "../icons";
57
import { localDb, type Row } from "../lib/local-db";
68

@@ -77,6 +79,12 @@ export function AboutMePage({ userId }: { userId: string }) {
7779
setSaving(false);
7880
};
7981

82+
// Always claimed on this page so Ctrl+S never opens the browser's "Save page" dialog here.
83+
useHotkey("page.save", () => {
84+
if (dirty && !saving) void save();
85+
});
86+
const saveCombo = useHotkeyLabel("page.save");
87+
8088
// Cmd/Ctrl+Enter saves from anywhere on the page.
8189
useEffect(() => {
8290
const onKeyDown = (e: KeyboardEvent) => {
@@ -142,6 +150,7 @@ export function AboutMePage({ userId }: { userId: string }) {
142150
<button
143151
onClick={() => void save()}
144152
disabled={saving}
153+
title={hotkeyHint("Save", saveCombo)}
145154
className="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 rounded-xl transition-colors flex items-center justify-center gap-2 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
146155
>
147156
<Save className="w-5 h-5" />

‎src/components/pages/GoalsPage.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useEffect, useRef, useState, type DragEvent, type FormEvent } from "react";
44
import { Archive, ArchiveRestore, Plus } from "../icons";
5-
import { useHotkey } from "../hotkeys/HotkeysProvider";
5+
import { hotkeyHint } from "../focus/GoalForm";
6+
import { useHotkey, useHotkeyLabel } from "../hotkeys/HotkeysProvider";
67
import { localDb, type ChangePayload } from "../lib/local-db";
78
import { GoalCard, type EditDraft } from "./goals/GoalCard";
89
import {
@@ -198,6 +199,8 @@ export function GoalsPage({ userId }: { userId: string }) {
198199

199200
const activeGoals = goals.filter((g) => !g.data.archived);
200201
const archivedGoals = goals.filter((g) => g.data.archived);
202+
useHotkey("list.archived", () => setShowArchived((current) => !current), archivedGoals.length > 0);
203+
const archivedCombo = useHotkeyLabel("list.archived");
201204

202205
const swapActive = (index: number, other: number) => {
203206
const reordered = [...activeGoals];
@@ -292,6 +295,7 @@ export function GoalsPage({ userId }: { userId: string }) {
292295
<div className="mt-8">
293296
<button
294297
onClick={() => setShowArchived(!showArchived)}
298+
title={hotkeyHint(showArchived ? "Hide archived" : "Show archived", archivedCombo)}
295299
className="text-sm text-gray-400 hover:text-white transition-colors flex items-center gap-2 mx-auto mb-4"
296300
>
297301
<Archive size={16} />

‎src/components/pages/HabitsPage.tsx‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent } from "react";
44
import { Archive, ArchiveRestore, Calendar, ChevronLeft, ChevronRight, Plus, Trash2 } from "../icons";
5-
import { useHotkey } from "../hotkeys/HotkeysProvider";
5+
import { hotkeyHint } from "../focus/GoalForm";
6+
import { useHotkey, useHotkeyLabel } from "../hotkeys/HotkeysProvider";
67
import { localDb } from "../lib/local-db";
78
import { HabitCard, type HabitEdit } from "./habits/HabitCard";
89
import { ColorPicker, FIELD_INPUT, FieldLabel, PRIMARY_BUTTON, SECONDARY_BUTTON } from "./habits/HabitFields";
@@ -118,11 +119,13 @@ function useHabitData(userId: string) {
118119

119120
function ArchivedHabits({ habits, onRestore, onDelete }: { habits: readonly Habit[]; onRestore(habit: Habit): void; onDelete(habit: Habit): void }) {
120121
const [open, setOpen] = useState(false);
122+
useHotkey("list.archived", () => setOpen((current) => !current), habits.length > 0);
123+
const archivedCombo = useHotkeyLabel("list.archived");
121124
if (habits.length === 0) return null;
122125
const iconButton = "text-gray-500 transition-colors p-2 rounded-lg hover:bg-gray-800 opacity-0 group-hover:opacity-100";
123126
return (
124127
<div className="mt-8">
125-
<button onClick={() => setOpen(!open)} className="text-sm text-gray-400 hover:text-white transition-colors flex items-center gap-2 mx-auto mb-4">
128+
<button onClick={() => setOpen(!open)} title={hotkeyHint(open ? "Hide archived" : "Show archived", archivedCombo)} className="text-sm text-gray-400 hover:text-white transition-colors flex items-center gap-2 mx-auto mb-4">
126129
<Archive size={16} />
127130
{open ? "Hide archived habits" : `Show archived habits (${habits.length})`}
128131
</button>
@@ -356,6 +359,16 @@ export function HabitsPage({ userId }: { userId: string }) {
356359
setViewMode(mode);
357360
setDayOffset(0);
358361
};
362+
// The period bar is only rendered while there are active habits.
363+
const hasPeriodBar = !loading && sortedActive.length > 0;
364+
const previousPeriod = () => setDayOffset(dayOffset + 1);
365+
const nextPeriod = () => dayOffset > 0 && setDayOffset(dayOffset - 1);
366+
useHotkey("list.prev", previousPeriod, hasPeriodBar);
367+
useHotkey("list.next", nextPeriod, hasPeriodBar);
368+
useHotkey("list.range", () => pickView(viewMode === "7days" ? "30days" : "7days"), hasPeriodBar);
369+
const prevCombo = useHotkeyLabel("list.prev");
370+
const nextCombo = useHotkeyLabel("list.next");
371+
const rangeCombo = useHotkeyLabel("list.range");
359372
const viewButton = (mode: ViewMode) => "px-3 py-1 text-sm rounded-md transition-colors " + (viewMode === mode ? "bg-blue-600 text-white" : "text-gray-400 hover:text-white");
360373

361374
if (loading) {
@@ -391,12 +404,12 @@ export function HabitsPage({ userId }: { userId: string }) {
391404
) : (
392405
<div className="space-y-4">
393406
<div className="flex items-center justify-between bg-gray-900 rounded-xl p-4 border border-gray-800">
394-
<button onClick={() => setDayOffset(dayOffset + 1)} className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors">
407+
<button onClick={previousPeriod} title={hotkeyHint("Previous period", prevCombo)} className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors">
395408
<ChevronLeft className="w-5 h-5" />
396409
</button>
397410
<div className="flex items-center gap-4">
398411
<span className="text-white font-medium">{rangeLabel(dayOffset, viewMode)}</span>
399-
<div className="flex bg-gray-800 rounded-lg p-1">
412+
<div className="flex bg-gray-800 rounded-lg p-1" title={hotkeyHint("Switch 7 / 30 days", rangeCombo)}>
400413
<button onClick={() => pickView("7days")} className={viewButton("7days")}>
401414
7 Days
402415
</button>
@@ -406,8 +419,9 @@ export function HabitsPage({ userId }: { userId: string }) {
406419
</div>
407420
</div>
408421
<button
409-
onClick={() => dayOffset > 0 && setDayOffset(dayOffset - 1)}
422+
onClick={nextPeriod}
410423
disabled={dayOffset === 0}
424+
title={hotkeyHint("Next period", nextCombo)}
411425
className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:text-gray-400"
412426
>
413427
<ChevronRight className="w-5 h-5" />

0 commit comments

Comments
 (0)