diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1fd2ccd..e91d9b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- Archived-day editing (`ArchiveEditDialog`, `ArchivedTaskRow`) had several accessibility gaps and unmemoized re-render/re-parse patterns. The "Restore" button no longer grabs focus via `autoFocus` when the dialog opens, letting Radix's default dialog focus behavior take over instead of landing keyboard focus on a state-changing action before the user has read anything. The day-summary and per-task edit toggles now announce their expanded/collapsed state via `aria-expanded`/`aria-controls`. The tasks table's header cells gained `scope="col"` and the table itself an accessible name tied to the day being edited. Per-task delete now requires a same-row Confirm/Cancel step instead of removing the row on a single click, matching the destructive-action pattern used elsewhere (the task isn't persisted until "Save Changes," so this doesn't need the full undo-toast machinery). On the performance side, the day/task dirty-check (`tasksChanged`) and the `handleTaskSave`/`handleTaskDelete`/toggle-expand callbacks are now memoized (`useMemo`/`useCallback`), and `ArchivedTaskRow`/`MarkdownDisplay` are wrapped in `React.memo`, so expanding one task row no longer re-renders every sibling row or re-parses unrelated markdown
+ — `src/components/ArchiveEditDialog.tsx`, `src/components/ArchiveEditDialog.test.tsx`, `src/components/ArchivedTaskRow.tsx`, `src/components/ArchivedTaskRow.test.tsx`, `src/components/MarkdownDisplay.tsx`
- `endDay` persisted the exact clock-out timestamp instead of rounding it to the nearest 15 minutes the way `startDay` already rounds the day's start time, so the archived day's `endTime` (and the last task's mirrored `endTime`) kept its raw seconds-precise value while `ArchiveEditDialog`'s Tasks table and day-summary form independently rounded the same values for display — producing a visible mismatch between the "posted"/persisted end time and what the edit dialog showed. `endDay` now rounds the effective end time at the source, so archived data and its display agree
— `src/contexts/TimeTrackingContext.tsx`, `src/contexts/TimeTracking.test.tsx`
- PWA manifest was duplicated three ways: `public/manifest.json` was an orphaned copy nothing linked to, and VitePWA's `manifest` option in `vite.config.ts` generated a second `manifest.webmanifest` that got injected into `dist/index.html` alongside the hand-maintained ``, leaving two manifest links in the built page. The actually-used `site.webmanifest` also pointed at screenshot files that don't exist (`desktop-1.png`/`mobile-1.png`). Deleted the orphaned JSON, set `manifest: false` on the VitePWA plugin, and fixed `site.webmanifest`'s screenshot list to reference the real files
diff --git a/src/components/ArchiveEditDialog.tsx b/src/components/ArchiveEditDialog.tsx
index 595a3ba..0160e60 100644
--- a/src/components/ArchiveEditDialog.tsx
+++ b/src/components/ArchiveEditDialog.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo, useCallback } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
@@ -189,8 +189,18 @@ export const ArchiveEditDialog: React.FC = ({
// of whether the day-summary editor was ever opened. Tasks are compared
// against the rounded baseline (not the raw day.tasks) so the automatic
// last-task rounding alone doesn't look like an unsaved change.
- const tasksChanged =
- JSON.stringify(tasks) !== JSON.stringify(getRoundedTasks(day.tasks));
+ // getRoundedTasks is redefined every render; depending on day.tasks alone
+ // still recomputes exactly when the baseline actually changes.
+ const roundedBaselineTasks = useMemo(
+ () => getRoundedTasks(day.tasks),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [day.tasks],
+ );
+
+ const tasksChanged = useMemo(
+ () => JSON.stringify(tasks) !== JSON.stringify(roundedBaselineTasks),
+ [tasks, roundedBaselineTasks],
+ );
const hasChanges = dayFormIsDirty || tasksChanged;
const parseTimeInput = (timeStr: string, baseDate: Date): Date => {
@@ -305,18 +315,28 @@ export const ArchiveEditDialog: React.FC = ({
onClose();
};
- const handleTaskSave = (updatedTask: Task) => {
- const updatedTasks = tasks.map((t) =>
- t.id === updatedTask.id ? updatedTask : t,
- );
- setTasks(updatedTasks);
- setExpandedTaskId(null);
- };
+ const handleTaskSave = useCallback(
+ (updatedTask: Task) => {
+ const updatedTasks = tasks.map((t) =>
+ t.id === updatedTask.id ? updatedTask : t,
+ );
+ setTasks(updatedTasks);
+ setExpandedTaskId(null);
+ },
+ [tasks],
+ );
- const handleTaskDelete = (taskId: string) => {
- const updatedTasks = tasks.filter((t) => t.id !== taskId);
- setTasks(updatedTasks);
- };
+ const handleTaskDelete = useCallback(
+ (taskId: string) => {
+ const updatedTasks = tasks.filter((t) => t.id !== taskId);
+ setTasks(updatedTasks);
+ },
+ [tasks],
+ );
+
+ const handleToggleExpand = useCallback((id: string) => {
+ setExpandedTaskId((cur) => (cur === id ? null : id));
+ }, []);
const handleCancel = () => {
resetFormState();
@@ -344,7 +364,6 @@ export const ArchiveEditDialog: React.FC = ({
size="sm"
aria-label="Restore this day"
className="text-blue-11 hover:text-blue-12"
- autoFocus
>
Restore
@@ -430,6 +449,8 @@ export const ArchiveEditDialog: React.FC = ({
? "Close day summary editor"
: "Edit day summary"
}
+ aria-expanded={isSummaryEditing}
+ aria-controls="day-summary-editor-region"
>
@@ -440,7 +461,7 @@ export const ArchiveEditDialog: React.FC = ({
-
+
{isSummaryEditing ? (