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,119 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNotes } from "../../hooks/useNotes";
|
||||
import { useNoteDraft } from "./useNoteDraft";
|
||||
import NoteSwitcher from "./NoteSwitcher";
|
||||
import SendToAgentButton from "./SendToAgentButton";
|
||||
import Button from "../ui/Button";
|
||||
import OverflowMenu from "../ui/OverflowMenu";
|
||||
import SaveIndicator from "../ui/SaveIndicator";
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes at dock width.
|
||||
*
|
||||
* Deliberately not `NotesPanel` in a narrower box. The tab can afford a column
|
||||
* of titles beside the editor; the dock cannot, and shrinking that layout
|
||||
* spends its height on chrome — a title strip, a wrapped button row and a
|
||||
* paragraph of help — for a body that ends up a few words wide.
|
||||
*
|
||||
* So the dock shows exactly one note. The title row names it and switches to
|
||||
* another, the actions that are not writing live in the overflow menu, and
|
||||
* everything left over is the body. Roughly 240px of height comes back.
|
||||
*
|
||||
* What the two surfaces share is the part that must not drift: `useNotes` for
|
||||
* the cache and its write ordering, and `useNoteDraft` for when a keystroke
|
||||
* becomes a save. Only the layout is different.
|
||||
*/
|
||||
export default function NotesDockPanel({ projectId }: Props) {
|
||||
const { notes, loading, saveState, createNote, saveNote, deleteNote } =
|
||||
useNotes(projectId);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
const selected = useMemo(
|
||||
() => notes.find((n) => n.id === selectedId) ?? notes[0] ?? null,
|
||||
[notes, selectedId],
|
||||
);
|
||||
|
||||
const { title, body, setTitle, setBody, commit } = useNoteDraft(
|
||||
selected,
|
||||
saveNote,
|
||||
);
|
||||
|
||||
const onCreate = async () => {
|
||||
const note = await createNote();
|
||||
if (note) setSelectedId(note.id);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<p className="p-4 text-xs text-[var(--text-secondary)]">Loading notes…</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-3 p-4">
|
||||
<p className="text-[13px] text-[var(--text-secondary)] text-center">
|
||||
Keep reminders here, and send any of them straight to a running Claude
|
||||
session.
|
||||
</p>
|
||||
<Button variant="primary" onClick={onCreate}>
|
||||
New note
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0">
|
||||
<div className="flex items-center gap-1 px-2 py-1.5 flex-shrink-0 border-b border-[var(--border-color)]">
|
||||
<div className="flex-1 min-w-0">
|
||||
<NoteSwitcher
|
||||
notes={notes}
|
||||
selectedId={selected.id}
|
||||
title={title}
|
||||
onTitleChange={setTitle}
|
||||
onCommit={commit}
|
||||
onSelect={setSelectedId}
|
||||
/>
|
||||
</div>
|
||||
{/* Renders nothing while idle, so it costs no width until it matters. */}
|
||||
<SaveIndicator state={saveState} />
|
||||
<OverflowMenu
|
||||
label="Note actions"
|
||||
items={[
|
||||
{ label: "New note", onSelect: () => void onCreate() },
|
||||
{
|
||||
label: "Delete note",
|
||||
danger: true,
|
||||
onSelect: () => void deleteNote(selected.id),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onBlur={commit}
|
||||
placeholder="Reminders, gotchas, a prompt worth keeping…"
|
||||
aria-label="Note body"
|
||||
className="flex-1 min-h-0 w-full px-3 py-2 bg-transparent text-[13px] text-[var(--text-primary)] resize-none font-mono"
|
||||
/>
|
||||
|
||||
<div className="px-2 py-2 flex-shrink-0 border-t border-[var(--border-color)]">
|
||||
{/* The live draft, not `selected.body` — what is on screen is what gets
|
||||
sent. `dropUp` because the dock clips its own overflow. */}
|
||||
<SendToAgentButton
|
||||
projectId={projectId}
|
||||
body={body}
|
||||
fullWidth
|
||||
dropUp
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user