Lay the notes panel out by its own width, not the window's
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 4s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m45s
Build App (Preview) / build-windows (pull_request) Successful in 4m58s
Build App (Preview) / build-linux (pull_request) Successful in 5m13s
Build App (Preview) / prune-previews (pull_request) Successful in 1s

The panel splits master/detail unconditionally: a 192px title column beside
the editor. That fits the Project Home tab and does not fit the dock. At the
dock's 352px default the editor gets 157px, and its action row wants ~200px,
so the Delete button lands outside the dock's `overflow-hidden` with no
scrollbar to reach it, and the textarea collapses to a two-word column.

The two surfaces differ in width while sharing a viewport, so this is a
container query rather than a `md:` breakpoint — a viewport query reads the
window and hands both surfaces the same answer, which is wrong for one of
them. Tailwind v4 has these in core; verified as real
`@container (min-width: 32rem)` rules in the built CSS, since a variant that
silently compiles to nothing looks identical in review.

The threshold is arithmetic: side by side needs the 192px list, an editor
wide enough for its own buttons (~280px), and the divider. `@lg` (512px) is
the first stop clearing ~473px. Below it the titles become a capped strip
above the editor, so the note being written keeps the height.

The action row now wraps, which is the part that holds at *any* width rather
than on one side of a threshold: the buttons are a group that does not shrink,
the title field shrinks to 96px, and past that the title takes one row and the
buttons the next. Nothing can be pushed out of the panel.

Not covered by the suite — jsdom has no layout engine, so 711 tests pass
before and after. This needs eyes on the dock at its minimum, default and
maximum widths.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm
This commit is contained in:
2026-09-02 11:34:02 -07:00
co-authored by Claude Opus 5
parent 2708772bf9
commit 0f3fff92f4
2 changed files with 37 additions and 14 deletions
+16 -8
View File
@@ -30,21 +30,29 @@ export default function NoteEditor({
}: Props) { }: Props) {
return ( return (
<div className="flex flex-col h-full min-h-0 gap-2 p-3"> <div className="flex flex-col h-full min-h-0 gap-2 p-3">
<div className="flex items-center gap-2"> {/* Wraps rather than overflows. The two buttons are a group with a fixed
appetite (~190px) and the title field can shrink only so far, so in a
narrow dock the title takes the first row and the buttons the second.
Without the wrap the group is simply clipped by the dock's
`overflow-hidden`, which puts Delete off-window with no scrollbar to
reach it. */}
<div className="flex flex-wrap items-center gap-2">
<input <input
value={title} value={title}
onChange={(e) => onTitleChange(e.target.value)} onChange={(e) => onTitleChange(e.target.value)}
onBlur={onCommit} onBlur={onCommit}
placeholder="Note title" placeholder="Note title"
aria-label="Note title" aria-label="Note title"
className="flex-1 min-w-0 px-2 h-8 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] text-[13px] text-[var(--text-primary)] focus:border-[var(--accent)] transition-colors" className="flex-1 min-w-24 px-2 h-8 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] text-[13px] text-[var(--text-primary)] focus:border-[var(--accent)] transition-colors"
/> />
{/* The live editor text, not `note.body` — what is on screen is what <div className="flex items-center gap-2 flex-shrink-0">
gets sent. */} {/* The live editor text, not `note.body` — what is on screen is what
<SendToAgentButton projectId={projectId} body={body} /> gets sent. */}
<Button variant="danger" onClick={onDelete} aria-label="Delete note"> <SendToAgentButton projectId={projectId} body={body} />
Delete <Button variant="danger" onClick={onDelete} aria-label="Delete note">
</Button> Delete
</Button>
</div>
</div> </div>
<textarea <textarea
value={body} value={body}
+21 -6
View File
@@ -14,9 +14,21 @@ const UNTITLED = "Untitled note";
* The notes surface itself, shared by the Project Home tab and the dock so the * The notes surface itself, shared by the Project Home tab and the dock so the
* two cannot drift into different behaviour. * two cannot drift into different behaviour.
* *
* Master/detail: titles on the left, one editor on the right. The editor holds * Master/detail: titles beside the editor when there is room, stacked above it
* draft text locally and commits on blur, which is how every other editable * when there is not. That is a **container** query, not a viewport one, because
* field in the app behaves (`ClaudeInstructionsEditor`, the Config tab). * the two surfaces differ in width while sharing a viewport — the dock opens at
* 352px and the tab is the width of the main area. A `md:` breakpoint would
* read the window and give both the same answer, which is the wrong answer for
* one of them.
*
* The threshold is arithmetic, not taste: side by side needs the 192px list,
* plus an editor wide enough for its own action row (~280px), plus the divider.
* Below ~473px the editor is narrower than its buttons, so `@lg` (512px) is the
* first stop that clears it.
*
* The editor holds draft text locally and commits on blur, which is how every
* other editable field in the app behaves (`ClaudeInstructionsEditor`, the
* Config tab).
*/ */
export default function NotesPanel({ projectId }: Props) { export default function NotesPanel({ projectId }: Props) {
const { notes, loading, saveState, createNote, saveNote, deleteNote } = const { notes, loading, saveState, createNote, saveNote, deleteNote } =
@@ -83,7 +95,7 @@ export default function NotesPanel({ projectId }: Props) {
} }
return ( return (
<div className="flex flex-col h-full min-h-0"> <div className="@container flex flex-col h-full min-h-0">
<div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-[var(--border-color)]"> <div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-[var(--border-color)]">
<Button variant="primary" onClick={onCreate}> <Button variant="primary" onClick={onCreate}>
New note New note
@@ -99,8 +111,11 @@ export default function NotesPanel({ projectId }: Props) {
</p> </p>
</div> </div>
) : ( ) : (
<div className="flex-1 min-h-0 flex"> <div className="flex-1 min-h-0 flex flex-col @lg:flex-row">
<ul className="w-48 flex-shrink-0 overflow-y-auto border-r border-[var(--border-color)] py-1"> {/* Stacked: a capped strip of titles above the editor, so the note
being written keeps most of the height. Side by side: a full-height
column of the fixed width the editor's arithmetic assumes. */}
<ul className="flex-shrink-0 overflow-y-auto py-1 max-h-32 border-b @lg:max-h-none @lg:w-48 @lg:border-b-0 @lg:border-r border-[var(--border-color)]">
{notes.map((n) => ( {notes.map((n) => (
<li key={n.id}> <li key={n.id}>
<button <button