26 KiB
Calendar-Date Memo Prefill — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: When the user picks a date in the activity calendar, the home memo editor pre-fills its createTime/updateTime to that date and exposes the existing TimestampPopover so the user can adjust before saving. Empty calendar dates also become clickable.
Architecture: Frontend-only. A new pure helper derives a Date from the displayTime filter; PagedMemoList reads MemoFilterContext and passes the derived Date into MemoEditor via a new defaultCreateTime prop; MemoEditor seeds its reducer state from the prop, renders the popover when the prop is set in create mode, and re-syncs on prop change. CalendarCell drops the count > 0 gate on click handling.
Tech Stack: React 18 + TypeScript, Vite 7, Vitest + jsdom + @testing-library/react, Tailwind v4, react-router. State via React Context + useReducer.
Spec: docs/superpowers/specs/2026-05-02-calendar-date-prefill-design.md
File map
Created
web/src/components/MemoEditor/utils/deriveDefaultCreateTime.ts— pure helper(filters, now?) => Date | undefined.web/tests/derive-default-create-time.test.ts— Vitest unit tests for the helper.web/tests/calendar-cell-empty-clickable.test.tsx— RTL test that count=0 in-month cells are clickable.
Modified
web/src/components/ActivityCalendar/CalendarCell.tsx— dropday.count > 0gate from click/interactivity/tooltip.web/src/components/MemoEditor/types/components.ts— adddefaultCreateTime?: DatetoMemoEditorProps.web/src/components/MemoEditor/hooks/useMemoInit.ts— accept optionaldefaultCreateTime; in create mode, dispatchSET_TIMESTAMPSto seed{ createTime, updateTime }.web/src/components/MemoEditor/index.tsx— accept the new prop, pass it touseMemoInit, add auseEffectthat re-syncs timestamps when the prop changes in create mode, renderTimestampPopoverwhen(!memo && state.timestamps.createTime).web/src/components/PagedMemoList/PagedMemoList.tsx— readuseMemoFilterContext, derivedefaultCreateTime, pass to<MemoEditor>at line ~155.
Task 1: Pure helper deriveDefaultCreateTimeFromFilters
Files:
- Create:
web/src/components/MemoEditor/utils/deriveDefaultCreateTime.ts - Test:
web/tests/derive-default-create-time.test.ts
The helper takes the filters array from MemoFilterContext plus an injectable now, finds any displayTime filter (value format YYYY-MM-DD, local-date — produced by useDateFilterNavigation which forwards the dayjs().format("YYYY-MM-DD") string from CalendarDayCell.date), and returns a Date of selected_date + now's hh:mm:ss. Returns undefined if no displayTime filter or the value is malformed.
- Step 1: Write the failing tests
Create web/tests/derive-default-create-time.test.ts:
import { describe, expect, it } from "vitest";
import { deriveDefaultCreateTimeFromFilters } from "@/components/MemoEditor/utils/deriveDefaultCreateTime";
import type { MemoFilter } from "@/contexts/MemoFilterContext";
describe("deriveDefaultCreateTimeFromFilters", () => {
const now = new Date(2026, 4, 2, 14, 32, 10); // 2026-05-02 14:32:10 local
it("returns undefined when no filters are set", () => {
expect(deriveDefaultCreateTimeFromFilters([], now)).toBeUndefined();
});
it("returns undefined when no displayTime filter is present", () => {
const filters: MemoFilter[] = [
{ factor: "tagSearch", value: "work" },
{ factor: "pinned", value: "true" },
];
expect(deriveDefaultCreateTimeFromFilters(filters, now)).toBeUndefined();
});
it("merges the displayTime date with the current local hh:mm:ss", () => {
const filters: MemoFilter[] = [{ factor: "displayTime", value: "2025-05-01" }];
const result = deriveDefaultCreateTimeFromFilters(filters, now);
expect(result).toBeDefined();
expect(result!.getFullYear()).toBe(2025);
expect(result!.getMonth()).toBe(4); // May (0-indexed)
expect(result!.getDate()).toBe(1);
expect(result!.getHours()).toBe(14);
expect(result!.getMinutes()).toBe(32);
expect(result!.getSeconds()).toBe(10);
});
it("ignores extra non-displayTime filters", () => {
const filters: MemoFilter[] = [
{ factor: "tagSearch", value: "work" },
{ factor: "displayTime", value: "2025-05-01" },
{ factor: "pinned", value: "true" },
];
const result = deriveDefaultCreateTimeFromFilters(filters, now);
expect(result?.getDate()).toBe(1);
});
it("returns undefined for a malformed YYYY-MM-DD value", () => {
const cases: MemoFilter[][] = [
[{ factor: "displayTime", value: "not-a-date" }],
[{ factor: "displayTime", value: "2025-13-40" }],
[{ factor: "displayTime", value: "" }],
[{ factor: "displayTime", value: "2025-5-1" }], // single-digit month/day
];
for (const filters of cases) {
expect(deriveDefaultCreateTimeFromFilters(filters, now)).toBeUndefined();
}
});
it("uses real `new Date()` when `now` is omitted", () => {
const filters: MemoFilter[] = [{ factor: "displayTime", value: "2025-05-01" }];
const before = new Date();
const result = deriveDefaultCreateTimeFromFilters(filters);
const after = new Date();
expect(result).toBeDefined();
// Time-of-day should fall between before and after (within 1s tolerance).
const resultTimeOnly = result!.getHours() * 3600 + result!.getMinutes() * 60 + result!.getSeconds();
const beforeTimeOnly = before.getHours() * 3600 + before.getMinutes() * 60 + before.getSeconds();
const afterTimeOnly = after.getHours() * 3600 + after.getMinutes() * 60 + after.getSeconds();
// Handle midnight rollover by allowing any value if before > after.
if (beforeTimeOnly <= afterTimeOnly) {
expect(resultTimeOnly).toBeGreaterThanOrEqual(beforeTimeOnly);
expect(resultTimeOnly).toBeLessThanOrEqual(afterTimeOnly);
}
});
});
- Step 2: Run tests to verify they fail
Run: cd web && pnpm test derive-default-create-time
Expected: FAIL — module @/components/MemoEditor/utils/deriveDefaultCreateTime does not exist.
- Step 3: Implement the helper
Create web/src/components/MemoEditor/utils/deriveDefaultCreateTime.ts:
import type { MemoFilter } from "@/contexts/MemoFilterContext";
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
/**
* Derive a default `createTime` for a new memo from the active memo filters.
* If a `displayTime:YYYY-MM-DD` filter is present, returns that local date
* combined with `now`'s wall-clock hh:mm:ss. Returns undefined otherwise or
* when the filter value is malformed.
*/
export function deriveDefaultCreateTimeFromFilters(
filters: MemoFilter[],
now: Date = new Date(),
): Date | undefined {
const dateFilter = filters.find((f) => f.factor === "displayTime");
if (!dateFilter) return undefined;
const match = DATE_RE.exec(dateFilter.value);
if (!match) return undefined;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
// Construct a local-time Date and verify the components round-trip
// (catches things like 2025-13-40 that JS would silently roll forward).
const candidate = new Date(year, month - 1, day, now.getHours(), now.getMinutes(), now.getSeconds());
if (
candidate.getFullYear() !== year ||
candidate.getMonth() !== month - 1 ||
candidate.getDate() !== day
) {
return undefined;
}
return candidate;
}
- Step 4: Run tests to verify they pass
Run: cd web && pnpm test derive-default-create-time
Expected: PASS — all 6 cases.
- Step 5: Run lint
Run: cd web && pnpm lint
Expected: PASS (TypeScript + Biome happy).
- Step 6: Commit
git add web/src/components/MemoEditor/utils/deriveDefaultCreateTime.ts web/tests/derive-default-create-time.test.ts
git commit -m "feat(memo-editor): add deriveDefaultCreateTimeFromFilters helper"
Task 2: Make empty calendar cells clickable
Files:
- Modify:
web/src/components/ActivityCalendar/CalendarCell.tsx - Test:
web/tests/calendar-cell-empty-clickable.test.tsx
CalendarCell currently gates handleClick, isInteractive, and shouldShowTooltip on day.count > 0. Drop those gates so any in-month cell is clickable when onClick is provided. Out-of-month cells (early-returned at line ~38) stay unclickable.
- Step 1: Write the failing test
Create web/tests/calendar-cell-empty-clickable.test.tsx:
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { CalendarCell } from "@/components/ActivityCalendar/CalendarCell";
import type { CalendarDayCell } from "@/components/ActivityCalendar/types";
const makeDay = (overrides: Partial<CalendarDayCell> = {}): CalendarDayCell => ({
date: "2025-05-01",
label: "1",
count: 0,
isCurrentMonth: true,
isToday: false,
isSelected: false,
...overrides,
});
describe("CalendarCell empty-day clickability", () => {
it("fires onClick for an in-month day with count=0", () => {
const onClick = vi.fn();
render(<CalendarCell day={makeDay()} maxCount={5} tooltipText="May 1, 2025" onClick={onClick} />);
const button = screen.getByRole("button", { name: /May 1, 2025/ });
fireEvent.click(button);
expect(onClick).toHaveBeenCalledWith("2025-05-01");
});
it("renders an empty in-month day as interactive (tabIndex 0, not aria-disabled)", () => {
render(<CalendarCell day={makeDay()} maxCount={5} tooltipText="May 1, 2025" onClick={() => {}} />);
const button = screen.getByRole("button", { name: /May 1, 2025/ });
expect(button).toHaveAttribute("tabindex", "0");
expect(button).toHaveAttribute("aria-disabled", "false");
});
it("still renders a populated in-month day as interactive", () => {
const onClick = vi.fn();
render(<CalendarCell day={makeDay({ count: 3 })} maxCount={5} tooltipText="May 1, 2025" onClick={onClick} />);
fireEvent.click(screen.getByRole("button", { name: /May 1, 2025/ }));
expect(onClick).toHaveBeenCalledWith("2025-05-01");
});
it("does not render out-of-month days as interactive (no role=button)", () => {
render(
<CalendarCell
day={makeDay({ isCurrentMonth: false })}
maxCount={5}
tooltipText="May 1, 2025"
onClick={() => {}}
/>,
);
expect(screen.queryByRole("button")).toBeNull();
});
});
- Step 2: Run test to verify it fails
Run: cd web && pnpm test calendar-cell-empty-clickable
Expected: FAIL — first two tests fail because the empty cell currently has tabindex="-1", aria-disabled="true", and onClick is not invoked.
- Step 3: Edit
CalendarCell.tsxto drop the count gate
Modify web/src/components/ActivityCalendar/CalendarCell.tsx. Three edits:
(a) Replace handleClick:
const handleClick = () => {
if (onClick) {
onClick(day.date);
}
};
(b) Replace isInteractive:
const isInteractive = Boolean(onClick);
(c) Replace shouldShowTooltip:
const shouldShowTooltip = tooltipText && !disableTooltip;
Leave the out-of-month early return (if (!day.isCurrentMonth) { ... }) untouched — out-of-month cells remain non-buttons.
- Step 4: Run test to verify it passes
Run: cd web && pnpm test calendar-cell-empty-clickable
Expected: PASS — all 4 cases.
- Step 5: Run the full test suite to catch regressions
Run: cd web && pnpm test
Expected: PASS. The existing activity-calendar-tooltip.test.ts covers getTooltipText (a separate utility) and shouldn't be affected.
- Step 6: Run lint
Run: cd web && pnpm lint
Expected: PASS.
- Step 7: Commit
git add web/src/components/ActivityCalendar/CalendarCell.tsx web/tests/calendar-cell-empty-clickable.test.tsx
git commit -m "feat(activity-calendar): allow clicking empty in-month dates"
Task 3: Add defaultCreateTime prop to MemoEditorProps
Files:
- Modify:
web/src/components/MemoEditor/types/components.ts
Type-only change. No tests at this step — TypeScript compiler is the gate. Subsequent tasks consume the prop.
- Step 1: Add the prop
Modify web/src/components/MemoEditor/types/components.ts. Replace the MemoEditorProps interface (lines 6–16) with:
export interface MemoEditorProps {
className?: string;
cacheKey?: string;
placeholder?: string;
/** Existing memo to edit. When provided, the editor initializes from it without fetching. */
memo?: Memo;
parentMemoName?: string;
autoFocus?: boolean;
/**
* Default `createTime` for a *new* memo (create mode only). When set, the
* editor seeds both `createTime` and `updateTime` to this value and renders
* the timestamp popover so the user can adjust before saving. Tracked live:
* if the prop changes after mount, the editor's timestamps re-sync. Ignored
* in edit mode (when `memo` is set).
*/
defaultCreateTime?: Date;
onConfirm?: (memoName: string) => void;
onCancel?: () => void;
}
- Step 2: Verify compilation
Run: cd web && pnpm lint
Expected: PASS (no consumer changes yet, prop is optional).
- Step 3: Commit
git add web/src/components/MemoEditor/types/components.ts
git commit -m "feat(memo-editor): add defaultCreateTime prop type"
Task 4: Seed editor timestamps in useMemoInit (create mode)
Files:
- Modify:
web/src/components/MemoEditor/hooks/useMemoInit.ts
useMemoInit currently handles edit mode (if (memo)) by calling memoService.fromMemo(memo) and actions.initMemo(...). In create mode it only restores cached content and sets default visibility — it never touches timestamps. Extend the create branch so that when defaultCreateTime is set, it dispatches SET_TIMESTAMPS with { createTime: defaultCreateTime, updateTime: defaultCreateTime }. This handles the initial mount case.
- Step 1: Update
UseMemoInitOptionsanduseMemoInit
Modify web/src/components/MemoEditor/hooks/useMemoInit.ts. Replace the entire file with:
import { useEffect, useRef, useState } from "react";
import type { Memo, Visibility } from "@/types/proto/api/v1/memo_service_pb";
import type { EditorRefActions } from "../Editor";
import { cacheService, memoService } from "../services";
import { useEditorContext } from "../state";
interface UseMemoInitOptions {
editorRef: React.RefObject<EditorRefActions | null>;
memo?: Memo;
cacheKey?: string;
username: string;
autoFocus?: boolean;
defaultVisibility?: Visibility;
defaultCreateTime?: Date;
}
export const useMemoInit = ({
editorRef,
memo,
cacheKey,
username,
autoFocus,
defaultVisibility,
defaultCreateTime,
}: UseMemoInitOptions) => {
const { actions, dispatch } = useEditorContext();
const initializedRef = useRef(false);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
if (initializedRef.current) return;
initializedRef.current = true;
const key = cacheService.key(username, cacheKey);
if (memo) {
const initialState = memoService.fromMemo(memo);
cacheService.clear(key);
dispatch(actions.initMemo(initialState));
} else {
const cachedContent = cacheService.load(key);
if (cachedContent) {
dispatch(actions.updateContent(cachedContent));
}
if (defaultVisibility !== undefined) {
dispatch(actions.setMetadata({ visibility: defaultVisibility }));
}
if (defaultCreateTime) {
dispatch(actions.setTimestamps({ createTime: defaultCreateTime, updateTime: defaultCreateTime }));
}
}
if (autoFocus) {
setTimeout(() => editorRef.current?.focus(), 100);
}
setIsInitialized(true);
}, [memo, cacheKey, username, autoFocus, defaultVisibility, defaultCreateTime, actions, dispatch, editorRef]);
return { isInitialized };
};
Notes:
-
The
defaultCreateTimedependency is added to the effect's deps to satisfy the linter, butinitializedRefensures the body runs only once. Live re-sync after mount is handled by a separate effect in Task 5. -
Edit mode is unchanged —
defaultCreateTimeis intentionally ignored whenmemois set. -
Step 2: Verify compilation
Run: cd web && pnpm lint
Expected: PASS.
- Step 3: Commit
git add web/src/components/MemoEditor/hooks/useMemoInit.ts
git commit -m "feat(memo-editor): seed timestamps from defaultCreateTime on init"
Task 5: Wire defaultCreateTime through MemoEditor and render popover
Files:
- Modify:
web/src/components/MemoEditor/index.tsx
Three changes:
- Destructure
defaultCreateTimefrom props. - Pass it into
useMemoInit. - Add a
useEffectthat dispatchessetTimestampswheneverdefaultCreateTimechanges after mount (live re-sync per design Q3-A). Skip whenmemois set. - Update the popover render condition so it shows in create mode too when timestamps are seeded.
- Step 1: Destructure the prop and pass it to
useMemoInit
In web/src/components/MemoEditor/index.tsx, update the MemoEditorImpl destructuring (around line 42):
const MemoEditorImpl: React.FC<MemoEditorProps> = ({
className,
cacheKey,
memo,
parentMemoName,
autoFocus,
placeholder,
defaultCreateTime,
onConfirm,
onCancel,
}) => {
And update the useMemoInit call (around line 71):
const { isInitialized } = useMemoInit({
editorRef,
memo,
cacheKey,
username: currentUser?.name ?? "",
autoFocus,
defaultVisibility,
defaultCreateTime,
});
- Step 2: Add the live re-sync effect
In the same file, add a new useEffect after useMemoInit (and after useAutoSave) that re-syncs timestamps when defaultCreateTime changes in create mode. Place it just before the existing useEffect that fetches AI settings (around line 80):
// Live-sync the draft's createTime/updateTime to the calendar-derived prop.
// Only applies in create mode; edit mode owns its own timestamps. Runs after
// initial mount (the seed value is set in useMemoInit), and again whenever
// the prop changes — e.g., when the user picks a different calendar date
// while the editor is open.
useEffect(() => {
if (memo) return;
if (!isInitialized) return;
dispatch(
actions.setTimestamps({
createTime: defaultCreateTime,
updateTime: defaultCreateTime,
}),
);
}, [defaultCreateTime, memo, isInitialized, actions, dispatch]);
Notes:
-
We pass
undefinedthrough when the prop becomes undefined (filter cleared) — this resets timestamps to undefined so the editor falls back to "server-stamped now" on save, exactly the pre-feature behavior. -
The
isInitializedguard avoids racing withuseMemoInit's one-shot seed. -
Step 3: Update the popover render condition
In the same file, find the existing block (around line 294):
{memoName && (
<div className="w-full -mb-1">
<TimestampPopover />
</div>
)}
Replace with:
{(memoName || (!memo && state.timestamps.createTime)) && (
<div className="w-full -mb-1">
<TimestampPopover />
</div>
)}
Now the popover renders in edit mode (unchanged) AND in create mode whenever a default timestamp has been seeded.
- Step 4: Verify compilation
Run: cd web && pnpm lint
Expected: PASS.
- Step 5: Run all tests
Run: cd web && pnpm test
Expected: PASS (no editor-specific tests added; existing tests continue to pass).
- Step 6: Commit
git add web/src/components/MemoEditor/index.tsx
git commit -m "feat(memo-editor): live-sync timestamps and reveal popover from defaultCreateTime"
Task 6: Wire calendar selection through PagedMemoList
Files:
- Modify:
web/src/components/PagedMemoList/PagedMemoList.tsx
The home memo editor is rendered at line ~155 of PagedMemoList.tsx. Read the current MemoFilterContext filters, derive the defaultCreateTime, and pass it to <MemoEditor>. Wrap in useMemo so the reference stays stable when filters don't change (avoids re-firing the live-sync effect).
- Step 1: Add the imports and derivation
Modify web/src/components/PagedMemoList/PagedMemoList.tsx. Add to the existing imports near the top:
import { useMemo } from "react";
import { useMemoFilterContext } from "@/contexts/MemoFilterContext";
import { deriveDefaultCreateTimeFromFilters } from "@/components/MemoEditor/utils/deriveDefaultCreateTime";
If useMemo is already imported from react in this file, merge into the existing import rather than duplicating.
Inside the component body (above the children JSX, near the top of the function), add:
const { filters } = useMemoFilterContext();
const defaultCreateTime = useMemo(
() => deriveDefaultCreateTimeFromFilters(filters),
[filters],
);
- Step 2: Pass the prop to
<MemoEditor>
Replace the existing line ~155:
{showMemoEditor ? <MemoEditor className="mb-2" cacheKey="home-memo-editor" placeholder={t("editor.any-thoughts")} /> : null}
with:
{showMemoEditor ? (
<MemoEditor
className="mb-2"
cacheKey="home-memo-editor"
placeholder={t("editor.any-thoughts")}
defaultCreateTime={defaultCreateTime}
/>
) : null}
- Step 3: Verify compilation
Run: cd web && pnpm lint
Expected: PASS.
- Step 4: Run all tests
Run: cd web && pnpm test
Expected: PASS.
- Step 5: Commit
git add web/src/components/PagedMemoList/PagedMemoList.tsx
git commit -m "feat(home): pass calendar-selected date as default createTime to memo editor"
Task 7: Manual smoke test
No code changes. Per CLAUDE.md: "For UI or frontend changes, start the dev server and use the feature in a browser before reporting the task as complete." Walk through the user-visible flows.
- Step 1: Start the backend and frontend
In one terminal:
go run ./cmd/memos --port 8081
In another:
cd web && pnpm dev
Open http://localhost:3001.
- Step 2: Smoke — populated past date
- Sign in. Confirm the activity calendar is visible (statistics view).
- Click a past date that already has memos.
- Confirm the URL gains
?filter=displayTime:YYYY-MM-DD. - Confirm the home memo editor shows the timestamp popover above the textarea, populated with the selected date.
- Type a memo and click Save.
- Clear the date filter (chip X). Reapply by clicking the same date.
- Confirm the new memo appears under that date.
- Step 3: Smoke — empty past date
- Pick a past date with zero memos in the calendar (lightest cell).
- Confirm it is now clickable, the URL filter applies, and the empty-state shows.
- Type and save a memo.
- Confirm the memo appears for that date and the calendar cell tints up.
- Step 4: Smoke — future date
- Click a future date in the current month.
- Confirm the popover shows that date.
- Save a memo. Confirm it appears under that date.
- Step 5: Smoke — clear filter mid-draft
- Pick May 1 (or any non-today date). Type some content, do not save.
- Click the filter chip X to clear the date filter.
- Confirm the popover disappears and the draft content is preserved (autoSave behavior).
- Save and confirm the memo gets a server-stamped "now" timestamp (i.e., appears under today).
- Step 6: Smoke — change filter mid-draft
- Pick May 1. Type content.
- Without saving, click May 3.
- Confirm the popover updates to May 3.
- Save. Confirm the memo appears under May 3.
- Step 7: Smoke — comment editor unaffected
- Open any memo's detail view (or open the comments thread).
- Confirm the reply editor does not show a timestamp popover.
- Confirm the date filter has no visible effect on the reply editor.
- Step 8: Smoke — edit mode unaffected
- Edit an existing memo (pencil icon).
- Confirm the existing timestamp popover still works exactly as before, regardless of any active calendar filter.
- Step 9: Smoke — empty-date click on Explore page
- Navigate to the Explore page (which also renders the calendar).
- Click an empty date.
- Confirm the URL filter applies and the empty-state shows. (No editor on Explore — that's correct.)
- Step 10: Record results
Note any unexpected behavior (especially: selection-ring contrast on the lowest-intensity background, mentioned as a flag in the spec). If the ring is too subtle, file a follow-up — not part of this plan.
Self-review
Spec coverage check:
| Spec requirement | Task |
|---|---|
| Empty calendar dates clickable | Task 2 |
| Editor shows TimestampPopover in create mode when filter active | Task 5 (popover condition) |
createTime = selected date + current local hh:mm:ss |
Task 1 (helper) + Task 4 (seed) |
updateTime mirrored to same value |
Task 4 (seed) + Task 5 (live sync) |
| Live-derived: filter change re-syncs timestamps | Task 5 (live-sync useEffect) |
| Filter cleared → undefined → server-stamped "now" | Task 5 (passes undefined through) + Task 6 (helper returns undefined) |
| Future dates allowed (no clamp) | Task 1 (no clamp in helper); confirmed in Task 7 step 4 |
| Comment editor unaffected | Task 6 wires only PagedMemoList; confirmed in Task 7 step 7 |
| Edit mode unaffected | Task 4 + 5 explicitly guard on memo; confirmed in Task 7 step 8 |
| Empty-date click on Explore/Archived/Profile | Task 2 (calendar-side change); confirmed in Task 7 step 9 |
| DST/timezone uses local time | Task 1 (new Date(y, m-1, d, h, mi, s)) |
| Helper unit tests | Task 1 |
CalendarCell empty-cell test |
Task 2 |
| Manual smoke | Task 7 |
All spec requirements have a task. No gaps.
Placeholder scan: No "TBD"/"TODO" steps. Every code step shows the actual code.
Type/name consistency check:
MemoFilteranduseMemoFilterContextexist at@/contexts/MemoFilterContext(verified during exploration).editorActions.setTimestampsexists instate/actions.ts:75and acceptsPartial<EditorState["timestamps"]>(verified). Calls in Tasks 4 and 5 match.state.timestamps.createTimeisDate | undefined(verifiedstate/types.ts:27-29). The popover render condition uses it as a truthy guard —Dateinstances are truthy,undefinedis falsy.useMemoFilterContext(alias used in PagedMemoList) is exported fromMemoFilterContext.tsx:151(verified).deriveDefaultCreateTimeFromFilterssignature is identical between Task 1 (definition) and Task 6 (consumer).defaultCreateTime: Date | undefinedflows consistently throughMemoEditorProps(Task 3) →MemoEditorImpldestructuring (Task 5) →useMemoInitoptions (Task 4) → reducer payloads.