Add a per-project Notes tab with a send-to-agent action #48
@@ -30,15 +30,22 @@ 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"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{/* The live editor text, not `note.body` — what is on screen is what
|
{/* The live editor text, not `note.body` — what is on screen is what
|
||||||
gets sent. */}
|
gets sent. */}
|
||||||
<SendToAgentButton projectId={projectId} body={body} />
|
<SendToAgentButton projectId={projectId} body={body} />
|
||||||
@@ -46,6 +53,7 @@ export default function NoteEditor({
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={body}
|
value={body}
|
||||||
onChange={(e) => onBodyChange(e.target.value)}
|
onChange={(e) => onBodyChange(e.target.value)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user