web/app/collab/components/notes-sidebar.tsxblame
View source
0b4b5821"use client";
0b4b5822
0b4b5823import { useState, useRef } from "react";
0b4b5824import { useCollab } from "./collab-provider";
0b4b5825import type { CollabNote } from "../hooks/use-collab-socket";
0b4b5826
0b4b5827interface NotesSidebarProps {
0b4b5828 diagramId: string;
0b4b5829 diagramTitle: string;
0b4b58210}
0b4b58211
0b4b58212function timeAgo(ts: string): string {
0b4b58213 const secs = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
0b4b58214 if (secs < 60) return "just now";
0b4b58215 if (secs < 3600) return Math.floor(secs / 60) + "m ago";
0b4b58216 if (secs < 86400) return Math.floor(secs / 3600) + "h ago";
0b4b58217 return Math.floor(secs / 86400) + "d ago";
0b4b58218}
0b4b58219
0b4b58220function NoteCard({ note, onEdit, onDelete }: { note: CollabNote; onEdit: (text: string) => void; onDelete: () => void }) {
0b4b58221 const [editing, setEditing] = useState(false);
0b4b58222 const [editText, setEditText] = useState(note.text);
0b4b58223
0b4b58224 const handleSave = () => {
0b4b58225 if (editText.trim() && editText !== note.text) {
0b4b58226 onEdit(editText.trim());
0b4b58227 }
0b4b58228 setEditing(false);
0b4b58229 };
0b4b58230
0b4b58231 return (
0b4b58232 <div
0b4b58233 style={{
0b4b58234 padding: "8px 10px",
0b4b58235 borderBottom: "1px solid var(--border-subtle, var(--divide))",
0b4b58236 fontSize: "13px",
0b4b58237 position: "relative",
0b4b58238 transition: "background-color 0.12s",
0b4b58239 }}
0b4b58240 onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "var(--bg-hover)")}
0b4b58241 onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")}
0b4b58242 >
0b4b58243 <div className="flex items-center gap-1.5" style={{ marginBottom: "3px", fontSize: "10px", color: "var(--text-muted)", fontFamily: "'JetBrains Mono', Menlo, monospace" }}>
0b4b58244 <span style={{ fontWeight: 500, color: "var(--accent)" }}>{note.author}</span>
0b4b58245 <span>&middot;</span>
0b4b58246 <span>{timeAgo(note.timestamp)}</span>
0b4b58247 </div>
0b4b58248 {editing ? (
0b4b58249 <div>
0b4b58250 <textarea
0b4b58251 value={editText}
0b4b58252 onChange={(e) => setEditText(e.target.value)}
0b4b58253 onKeyDown={(e) => {
0b4b58254 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSave(); }
0b4b58255 if (e.key === "Escape") setEditing(false);
0b4b58256 }}
0b4b58257 className="w-full text-sm px-2 py-1"
0b4b58258 style={{
0b4b58259 backgroundColor: "var(--bg-input)",
0b4b58260 border: "1px solid var(--border)",
0b4b58261 color: "var(--text-primary)",
0b4b58262 resize: "vertical",
0b4b58263 minHeight: "2.5rem",
0b4b58264 outline: "none",
0b4b58265 font: "inherit",
0b4b58266 }}
0b4b58267 rows={2}
0b4b58268 autoFocus
0b4b58269 />
0b4b58270 <div className="flex gap-1 mt-1">
0b4b58271 <button
0b4b58272 onClick={handleSave}
0b4b58273 className="text-xs px-2 py-0.5"
0b4b58274 style={{ backgroundColor: "var(--accent)", color: "var(--accent-text)", border: "none", cursor: "pointer", font: "inherit" }}
0b4b58275 >
0b4b58276 Save
0b4b58277 </button>
0b4b58278 <button
0b4b58279 onClick={() => setEditing(false)}
0b4b58280 className="text-xs px-2 py-0.5"
0b4b58281 style={{ border: "1px solid var(--border)", background: "none", color: "var(--text-muted)", cursor: "pointer", font: "inherit" }}
0b4b58282 >
0b4b58283 Cancel
0b4b58284 </button>
0b4b58285 </div>
0b4b58286 </div>
0b4b58287 ) : (
0b4b58288 <div>
0b4b58289 <p style={{ color: "var(--text-primary)", whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: "12px", lineHeight: 1.5 }}>
0b4b58290 {note.text}
0b4b58291 </p>
0b4b58292 <div className="note-actions flex gap-2 mt-1">
0b4b58293 <button
0b4b58294 onClick={() => { setEditText(note.text); setEditing(true); }}
0b4b58295 className="text-xs"
0b4b58296 style={{ color: "var(--text-faint)", background: "none", border: "none", cursor: "pointer", font: "inherit" }}
0b4b58297 >
0b4b58298 Edit
0b4b58299 </button>
0b4b582100 <button
0b4b582101 onClick={onDelete}
0b4b582102 className="text-xs"
0b4b582103 style={{ color: "var(--text-faint)", background: "none", border: "none", cursor: "pointer", font: "inherit" }}
0b4b582104 >
0b4b582105 Delete
0b4b582106 </button>
0b4b582107 </div>
0b4b582108 </div>
0b4b582109 )}
0b4b582110 </div>
0b4b582111 );
0b4b582112}
0b4b582113
0b4b582114export function NotesSidebar({ diagramId, diagramTitle }: NotesSidebarProps) {
0b4b582115 const { notes, emit } = useCollab();
0b4b582116 const [text, setText] = useState("");
0b4b582117 const diagramNotes = notes[diagramId] ?? [];
0b4b582118
0b4b582119 const handleAdd = () => {
0b4b582120 if (!text.trim()) return;
0b4b582121 emit("add-note", { text: text.trim(), diagramId, diagramTitle });
0b4b582122 setText("");
0b4b582123 };
0b4b582124
0b4b582125 const handleEdit = (noteId: string, newText: string) => {
0b4b582126 emit("edit-note", { noteId, diagramId, text: newText });
0b4b582127 };
0b4b582128
0b4b582129 const handleDelete = (noteId: string) => {
0b4b582130 emit("delete-note", { noteId, diagramId });
0b4b582131 };
0b4b582132
0b4b582133 return (
0b4b582134 <div
0b4b582135 style={{
0b4b582136 width: "300px",
0b4b582137 minWidth: "180px",
0b4b582138 borderLeft: "1px solid var(--border)",
0b4b582139 background: "var(--bg-card)",
0b4b582140 display: "flex",
0b4b582141 flexDirection: "column",
0b4b582142 flexShrink: 0,
0b4b582143 overflow: "hidden",
0b4b582144 }}
0b4b582145 >
0b4b582146 <div
0b4b582147 style={{
0b4b582148 padding: "8px 12px",
0b4b582149 fontSize: "11px",
0b4b582150 fontWeight: 500,
0b4b582151 color: "var(--text-muted)",
0b4b582152 borderBottom: "1px solid var(--border-subtle, var(--divide))",
0b4b582153 fontFamily: "'JetBrains Mono', Menlo, monospace",
0b4b582154 textTransform: "uppercase",
0b4b582155 letterSpacing: "0.5px",
0b4b582156 }}
0b4b582157 >
0b4b582158 Notes
0b4b582159 </div>
0b4b582160 <div style={{ flex: 1, overflowY: "auto", padding: "6px" }}>
0b4b582161 {diagramNotes.length === 0 && (
0b4b582162 <p style={{ color: "var(--text-faint)", fontSize: "12px", fontStyle: "italic", textAlign: "center", padding: "32px 16px" }}>
0b4b582163 No notes yet for this diagram.
0b4b582164 Click on the diagram or type below to add one.
0b4b582165 </p>
0b4b582166 )}
0b4b582167 {diagramNotes.map((note) => (
0b4b582168 <NoteCard
0b4b582169 key={note.id}
0b4b582170 note={note}
0b4b582171 onEdit={(t) => handleEdit(note.id, t)}
0b4b582172 onDelete={() => handleDelete(note.id)}
0b4b582173 />
0b4b582174 ))}
0b4b582175 </div>
0b4b582176 <div style={{ borderTop: "1px solid var(--border-subtle, var(--divide))", padding: "8px", display: "flex", gap: "6px", alignItems: "flex-end" }}>
0b4b582177 <textarea
0b4b582178 value={text}
0b4b582179 onChange={(e) => setText(e.target.value)}
0b4b582180 onKeyDown={(e) => {
0b4b582181 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleAdd(); }
0b4b582182 }}
0b4b582183 placeholder="Add a note..."
0b4b582184 style={{
0b4b582185 flex: 1,
0b4b582186 backgroundColor: "var(--bg-input, var(--bg-inset))",
0b4b582187 border: "1px solid var(--border)",
0b4b582188 color: "var(--text-primary)",
0b4b582189 resize: "none",
0b4b582190 outline: "none",
0b4b582191 font: "inherit",
0b4b582192 fontSize: "13px",
0b4b582193 padding: "6px 8px",
0b4b582194 lineHeight: 1.4,
0b4b582195 }}
0b4b582196 rows={1}
0b4b582197 />
0b4b582198 <button
0b4b582199 onClick={handleAdd}
0b4b582200 disabled={!text.trim()}
0b4b582201 title="Send note (Enter)"
0b4b582202 style={{
0b4b582203 width: "32px",
0b4b582204 height: "32px",
0b4b582205 display: "flex",
0b4b582206 alignItems: "center",
0b4b582207 justifyContent: "center",
0b4b582208 background: text.trim() ? "var(--accent)" : "var(--bg-hover)",
0b4b582209 color: text.trim() ? "var(--accent-text)" : "var(--text-faint)",
0b4b582210 border: "none",
0b4b582211 borderRadius: "4px",
0b4b582212 cursor: text.trim() ? "pointer" : "default",
0b4b582213 flexShrink: 0,
0b4b582214 }}
0b4b582215 >
0b4b582216 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
0b4b582217 </button>
0b4b582218 </div>
0b4b582219 </div>
0b4b582220 );
0b4b582221}