Give the dock its own compact notes layout
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-windows (pull_request) Successful in 4m55s
Build App (Preview) / build-linux (pull_request) Successful in 5m29s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-windows (pull_request) Successful in 4m55s
Build App (Preview) / build-linux (pull_request) Successful in 5m29s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
The dock was showing `NotesPanel`, which is a master/detail layout: a column of titles beside an editor. The previous commit made that survive dock width; it did not make it right. At 352px the layout still spends roughly 356px of height on chrome — dock header, panel header, title strip, a button row that wraps, and a paragraph of help — before the body gets a pixel. So the dock now shows one note. The title field names what is open and the chevron beside it switches; New and Delete move into the overflow menu; the help text goes. Chrome drops to about 112px and the body takes the rest. The two surfaces are now different components, which contradicts a docstring I wrote — "shared so the two cannot drift into different behaviour". That claim was about behaviour, and behaviour was never in the layout: it is in `useNotes` for the cache and its write ordering, and now in `useNoteDraft`, extracted here so when a keystroke becomes a save is defined in exactly one place. Only the layout diverges. `NotesPanel.shared.test.tsx` gets stronger for it — it now mounts the dock panel and the tab panel together, which is what the app actually does, instead of the same component twice. `NoteSwitcher` is not `OverflowMenu` despite the shape being close: that keys items by label, and notes are addressed by id, so two untitled notes — the ordinary case — would collapse into one row. It is also not a `combobox`; an input plus a listbox button is two honest controls, where the role would owe active-descendant tracking and filtering that nothing here needs. `SendToAgentButton` picks up `useUnavailable` from #49, which is what its `disabled` plus explanatory `title` was already asking for. Four tests moved from `toBeDisabled()` to the new contract, and one of them — "does nothing for an empty note" — turned out never to have asserted that it does nothing. It does now, for click and for Enter, which is the guard the swap needs. It also gains `dropUp`, and that is load-bearing rather than cosmetic: the dock clips its own overflow, so a session menu opening downward from a button on the bottom edge is drawn outside the panel and never seen. 744 tests pass, 62 files. As before, jsdom has no layout engine: that the dock now reads as compact is not something the suite can tell you. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Note } from "../../lib/types";
|
||||
|
||||
/**
|
||||
* Draft text for the note being edited, committed when a field loses focus.
|
||||
*
|
||||
* This is the half the dock and the tab must never disagree on, so it lives
|
||||
* here rather than in either layout. The two surfaces differ in how they show
|
||||
* notes; they must not differ in when a keystroke becomes a save.
|
||||
*
|
||||
* The draft is "untouched" exactly while it still matches what was last copied
|
||||
* out of the store, which is what lets an edit made on the *other* surface
|
||||
* reach this one's editor without ever discarding half-typed text.
|
||||
*/
|
||||
export function useNoteDraft(
|
||||
selected: Note | null,
|
||||
saveNote: (note: Note) => Promise<unknown>,
|
||||
) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const seeded = useRef<{ id: string | null; title: string; body: string }>({
|
||||
id: null,
|
||||
title: "",
|
||||
body: "",
|
||||
});
|
||||
|
||||
// Re-seed on a change of note, and on a change to the *stored* text of the
|
||||
// note already open — the second case is the dock and the tab showing one
|
||||
// project at once.
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
seeded.current = { id: null, title: "", body: "" };
|
||||
setTitle("");
|
||||
setBody("");
|
||||
return;
|
||||
}
|
||||
const untouched =
|
||||
title === seeded.current.title && body === seeded.current.body;
|
||||
if (seeded.current.id !== selected.id || untouched) {
|
||||
seeded.current = {
|
||||
id: selected.id,
|
||||
title: selected.title,
|
||||
body: selected.body,
|
||||
};
|
||||
setTitle(selected.title);
|
||||
setBody(selected.body);
|
||||
}
|
||||
}, [selected?.id, selected?.title, selected?.body]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const commit = () => {
|
||||
if (!selected) return;
|
||||
// Reading is not editing: clicking through notes must not rewrite the file.
|
||||
if (title === selected.title && body === selected.body) return;
|
||||
// Mark the draft as matching what was just committed, so the store update
|
||||
// this save produces reads as "no change" rather than as a stale re-seed.
|
||||
seeded.current = { id: selected.id, title, body };
|
||||
void saveNote({ ...selected, title, body });
|
||||
};
|
||||
|
||||
return { title, body, setTitle, setBody, commit };
|
||||
}
|
||||
Reference in New Issue
Block a user