import { useState, useRef, useEffect, useCallback, useMemo } from "react"; /* ─────────────────────── helpers ─────────────────────── */ const uid = () => Math.random().toString(36).slice(2, 10); const now = () => new Date().toISOString(); const fmtDate = (d) => { const dt = new Date(d); const opts = { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }; return dt.toLocaleDateString("en-US", opts); }; /* ─────────────────────── default data ─────────────────────── */ const DEFAULT_FOLDERS = [ { id: "all", name: "All Notes", icon: "📋", system: true }, { id: "starred", name: "Starred", icon: "⭐", system: true }, { id: "drawings", name: "Drawings", icon: "🎨", system: true }, { id: "pdfs", name: "PDFs", icon: "📄", system: true }, { id: "trash", name: "Trash", icon: "🗑️", system: true }, ]; const COLORS = [ "#1a1a2e", "#e94560", "#0f3460", "#16213e", "#533483", "#e07c24", "#21bf73", "#fd5e53", "#3282b8", "#ff6b6b", "#4ecdc4", "#f7dc6f", "#bb8fce", "#85c1e9", "#f8c471", "#82e0aa", "#f1948a", "#aeb6bf", "#ffffff", "#000000", ]; const PEN_SIZES = [1, 2, 3, 5, 8, 12, 20]; const COVER_GRADIENTS = [ "linear-gradient(135deg, #0f0c29, #302b63, #24243e)", "linear-gradient(135deg, #e44d26, #f16529, #ffd700)", "linear-gradient(135deg, #134e5e, #71b280)", "linear-gradient(135deg, #8e2de2, #4a00e0)", "linear-gradient(135deg, #fc4a1a, #f7b733)", "linear-gradient(135deg, #00b4db, #0083b0)", "linear-gradient(135deg, #2c3e50, #3498db)", "linear-gradient(135deg, #c94b4b, #4b134f)", "linear-gradient(135deg, #11998e, #38ef7d)", "linear-gradient(135deg, #373b44, #4286f4)", "linear-gradient(135deg, #141e30, #243b55)", "linear-gradient(135deg, #f12711, #f5af19)", ]; /* ────────────────────── Icons (inline SVG) ────────────────────── */ const Icons = { plus: ( ), search: ( ), bold: , italic: , underline: , list: , listOrdered: 123, checkbox: , trash: , star: , starFill: , pen: , eraser: , undo: , redo: , sun: , moon: , folder: , back: , menu: , download: , highlight: , notebook: , settings: , edit: , sync: , image: , close: , }; /* ────────────────────── Drawing Canvas Component ────────────────────── */ function DrawingCanvas({ drawingData, onSave, dark, standalone = false, overlayMode = false }) { const canvasRef = useRef(null); const ctxRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [tool, setTool] = useState("pen"); const [color, setColor] = useState("#1a1a2e"); const [size, setSize] = useState(3); const [history, setHistory] = useState([]); const [historyIdx, setHistoryIdx] = useState(-1); const [showColorPicker, setShowColorPicker] = useState(false); const [showSizePicker, setShowSizePicker] = useState(false); const lastPoint = useRef(null); const T = useMemo(() => ({ bg: dark ? "#0d1117" : "#ffffff", toolbar: dark ? "#161b22" : "#f6f8fa", border: dark ? "#30363d" : "#d0d7de", text: dark ? "#e6edf3" : "#1f2328", muted: dark ? "#8b949e" : "#656d76", active: dark ? "#388bfd" : "#0969da", hover: dark ? "#1c2128" : "#eef1f4", }), [dark]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const parent = canvas.parentElement; if (standalone || overlayMode) { canvas.width = parent.clientWidth; canvas.height = parent.clientHeight; } else { canvas.width = parent.clientWidth; canvas.height = Math.max(600, parent.clientHeight); } const ctx = canvas.getContext("2d"); ctx.lineCap = "round"; ctx.lineJoin = "round"; ctxRef.current = ctx; if (drawingData && drawingData.length > 0) { const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0); saveToHistory(); }; img.src = drawingData; } else { saveToHistory(); } }, []); const saveToHistory = () => { const canvas = canvasRef.current; if (!canvas) return; const data = canvas.toDataURL(); setHistory((prev) => { const next = prev.slice(0, historyIdx + 1 || prev.length); next.push(data); return next; }); setHistoryIdx((prev) => prev + 1); }; const undo = () => { if (historyIdx <= 0) return; const newIdx = historyIdx - 1; setHistoryIdx(newIdx); const img = new Image(); img.onload = () => { const ctx = ctxRef.current; ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); ctx.drawImage(img, 0, 0); }; img.src = history[newIdx]; }; const redo = () => { if (historyIdx >= history.length - 1) return; const newIdx = historyIdx + 1; setHistoryIdx(newIdx); const img = new Image(); img.onload = () => { const ctx = ctxRef.current; ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); ctx.drawImage(img, 0, 0); }; img.src = history[newIdx]; }; const getPos = (e) => { const canvas = canvasRef.current; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; if (e.touches) { return { x: (e.touches[0].clientX - rect.left) * scaleX, y: (e.touches[0].clientY - rect.top) * scaleY, }; } return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY, }; }; const startDraw = (e) => { e.preventDefault(); const pos = getPos(e); lastPoint.current = pos; setIsDrawing(true); const ctx = ctxRef.current; ctx.beginPath(); ctx.moveTo(pos.x, pos.y); if (tool === "eraser") { ctx.globalCompositeOperation = "destination-out"; ctx.lineWidth = size * 4; } else if (tool === "highlighter") { ctx.globalCompositeOperation = "source-over"; ctx.strokeStyle = color + "55"; ctx.lineWidth = size * 6; } else { ctx.globalCompositeOperation = "source-over"; ctx.strokeStyle = color; ctx.lineWidth = size; } }; const draw = (e) => { if (!isDrawing) return; e.preventDefault(); const pos = getPos(e); const ctx = ctxRef.current; ctx.lineTo(pos.x, pos.y); ctx.stroke(); lastPoint.current = pos; }; const stopDraw = (e) => { if (!isDrawing) return; e?.preventDefault(); setIsDrawing(false); ctxRef.current.closePath(); ctxRef.current.globalCompositeOperation = "source-over"; saveToHistory(); if (onSave) { onSave(canvasRef.current.toDataURL()); } }; const clearCanvas = () => { const ctx = ctxRef.current; ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); saveToHistory(); if (onSave) onSave(canvasRef.current.toDataURL()); }; const toolbarStyle = { display: "flex", alignItems: "center", gap: "4px", padding: "6px 10px", background: T.toolbar, borderBottom: `1px solid ${T.border}`, flexWrap: "wrap", position: overlayMode ? "absolute" : "relative", top: overlayMode ? 0 : undefined, left: overlayMode ? 0 : undefined, right: overlayMode ? 0 : undefined, zIndex: overlayMode ? 20 : undefined, borderRadius: overlayMode ? "0 0 8px 8px" : 0, }; const btnStyle = (active) => ({ display: "flex", alignItems: "center", justifyContent: "center", width: 34, height: 34, borderRadius: 8, border: "none", cursor: "pointer", background: active ? T.active + "22" : "transparent", color: active ? T.active : T.text, transition: "all 0.15s", }); return (
{/* Color */}
{showColorPicker && (
{COLORS.map((c) => (
)}
{/* Size */}
{showSizePicker && (
{PEN_SIZES.map((s) => ( ))}
)}
); } /* ────────────────────── Rich Text Editor ────────────────────── */ function RichTextEditor({ content, onChange, dark }) { const editorRef = useRef(null); const T = useMemo(() => ({ bg: dark ? "#0d1117" : "#ffffff", toolbar: dark ? "#161b22" : "#f6f8fa", border: dark ? "#30363d" : "#d0d7de", text: dark ? "#e6edf3" : "#1f2328", muted: dark ? "#8b949e" : "#656d76", active: dark ? "#388bfd" : "#0969da", }), [dark]); useEffect(() => { if (editorRef.current && editorRef.current.innerHTML !== content) { editorRef.current.innerHTML = content || ""; } }, [content]); const exec = (cmd, val = null) => { document.execCommand(cmd, false, val); editorRef.current?.focus(); if (onChange) onChange(editorRef.current.innerHTML); }; const handleInput = () => { if (onChange) onChange(editorRef.current.innerHTML); }; const btnStyle = { display: "flex", alignItems: "center", justifyContent: "center", width: 32, height: 32, borderRadius: 6, border: "none", cursor: "pointer", background: "transparent", color: T.text, transition: "all 0.15s", }; return (
); } /* ────────────────────── PDF Viewer with Annotation ────────────────────── */ function PDFViewer({ pdfData, annotations, onAnnotationSave, dark }) { const [drawMode, setDrawMode] = useState(false); const [pageNum, setPageNum] = useState(1); const [totalPages, setTotalPages] = useState(1); const T = useMemo(() => ({ bg: dark ? "#0d1117" : "#f6f8fa", toolbar: dark ? "#161b22" : "#ffffff", border: dark ? "#30363d" : "#d0d7de", text: dark ? "#e6edf3" : "#1f2328", muted: dark ? "#8b949e" : "#656d76", active: dark ? "#388bfd" : "#0969da", }), [dark]); return (
Page {pageNum} / {totalPages}
{pdfData ? (
📄
PDF loaded — {pdfData.name}
In a full deployment, PDF.js renders pages here. The annotation canvas overlays each page for freehand drawing, highlighting, and text annotations.
{drawMode && (
)}
) : (
📄
No PDF loaded
Import a PDF to view and annotate it
)}
); } /* ────────────────────── Create Notebook Modal ────────────────────── */ function CreateNotebookModal({ onClose, onCreate, dark }) { const [name, setName] = useState(""); const [selectedCover, setSelectedCover] = useState(0); const T = useMemo(() => ({ bg: dark ? "#161b22" : "#ffffff", border: dark ? "#30363d" : "#d0d7de", text: dark ? "#e6edf3" : "#1f2328", muted: dark ? "#8b949e" : "#656d76", active: dark ? "#388bfd" : "#0969da", }), [dark]); return (
e.stopPropagation()}>

New Notebook

{name || "Notebook"}
setName(e.target.value)} placeholder="Notebook name..." style={{ width: "100%", padding: "10px 14px", borderRadius: 10, border: `1px solid ${T.border}`, background: dark ? "#0d1117" : "#f6f8fa", color: T.text, fontSize: 14, outline: "none", marginBottom: 16, boxSizing: "border-box", }} />
Choose cover
{COVER_GRADIENTS.map((g, i) => (
); } /* ────────────────────────── MAIN APP ────────────────────────── */ export default function NoteApp() { const [dark, setDark] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true); const [activeFolder, setActiveFolder] = useState("all"); const [activeNote, setActiveNote] = useState(null); const [view, setView] = useState("notes"); // notes | drawing | pdf | settings const [searchQuery, setSearchQuery] = useState(""); const [showNewNotebook, setShowNewNotebook] = useState(false); const [syncStatus, setSyncStatus] = useState("synced"); // synced | syncing | offline const fileInputRef = useRef(null); const [notebooks, setNotebooks] = useState([ { id: "nb1", name: "Personal", cover: 0, createdAt: now() }, { id: "nb2", name: "Work", cover: 2, createdAt: now() }, { id: "nb3", name: "Ideas", cover: 4, createdAt: now() }, ]); const [notes, setNotes] = useState([ { id: "n1", type: "text", title: "Welcome to InkNote", folderId: "nb1", content: "

Welcome to InkNote ✨

Your cross-platform note-taking companion.

Features include:

  • Rich text editing with formatting
  • Freehand drawing with pen & highlighter
  • PDF import & annotation
  • Notebook organization with custom covers
  • Dark & light themes

Start creating! Click + to add a new note.

", starred: true, createdAt: "2025-06-10T10:00:00Z", updatedAt: now(), }, { id: "n2", type: "drawing", title: "Quick Sketch", folderId: "nb3", drawingData: null, starred: false, createdAt: "2025-06-11T08:30:00Z", updatedAt: now(), }, { id: "n3", type: "text", title: "Meeting Notes", folderId: "nb2", content: "

Sprint Planning

Discussed roadmap for Q3. Key points:

  1. Sync engine improvements
  2. Mobile gesture support
  3. PDF annotation export
", starred: false, createdAt: "2025-06-12T14:00:00Z", updatedAt: now(), }, ]); const [pdfs, setPdfs] = useState([]); // Theme const T = useMemo(() => dark ? { bg: "#0d1117", sidebar: "#010409", sidebarHover: "#161b22", sidebarActive: "#161b22", card: "#161b22", cardHover: "#1c2128", toolbar: "#161b22", border: "#30363d", borderLight: "#21262d", text: "#e6edf3", textSecondary: "#c9d1d9", muted: "#8b949e", faint: "#484f58", accent: "#388bfd", accentSoft: "#388bfd22", accentText: "#58a6ff", danger: "#f85149", dangerSoft: "#f8514922", success: "#3fb950", successSoft: "#3fb95022", warning: "#d29922", shadow: "0 8px 24px rgba(0,0,0,0.4)", inputBg: "#0d1117", } : { bg: "#f6f8fa", sidebar: "#ffffff", sidebarHover: "#f3f4f6", sidebarActive: "#eef2ff", card: "#ffffff", cardHover: "#f9fafb", toolbar: "#ffffff", border: "#d0d7de", borderLight: "#e5e7eb", text: "#1f2328", textSecondary: "#424a53", muted: "#656d76", faint: "#afb8c1", accent: "#0969da", accentSoft: "#0969da18", accentText: "#0550ae", danger: "#cf222e", dangerSoft: "#cf222e15", success: "#1a7f37", successSoft: "#1a7f3715", warning: "#9a6700", shadow: "0 8px 24px rgba(0,0,0,0.08)", inputBg: "#f6f8fa", }, [dark]); // Filtered notes const filteredNotes = useMemo(() => { let list = notes; if (activeFolder === "starred") list = list.filter((n) => n.starred); else if (activeFolder === "drawings") list = list.filter((n) => n.type === "drawing"); else if (activeFolder === "pdfs") return pdfs; else if (activeFolder === "trash") return []; else if (activeFolder !== "all") list = list.filter((n) => n.folderId === activeFolder); if (searchQuery) { const q = searchQuery.toLowerCase(); list = list.filter((n) => n.title.toLowerCase().includes(q) || (n.content || "").toLowerCase().includes(q)); } return list.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)); }, [notes, pdfs, activeFolder, searchQuery]); const currentNote = activeNote ? notes.find((n) => n.id === activeNote) || pdfs.find((p) => p.id === activeNote) : null; // Actions const createNote = (type = "text") => { const fid = ["all", "starred", "drawings", "pdfs", "trash"].includes(activeFolder) ? (notebooks[0]?.id || "nb1") : activeFolder; const note = { id: uid(), type, title: type === "drawing" ? "New Drawing" : "Untitled Note", folderId: fid, content: "", drawingData: null, starred: false, createdAt: now(), updatedAt: now(), }; setNotes((prev) => [note, ...prev]); setActiveNote(note.id); setView(type === "drawing" ? "drawing" : "notes"); simulateSync(); }; const updateNote = (id, updates) => { setNotes((prev) => prev.map((n) => n.id === id ? { ...n, ...updates, updatedAt: now() } : n)); simulateSync(); }; const deleteNote = (id) => { setNotes((prev) => prev.filter((n) => n.id !== id)); if (activeNote === id) setActiveNote(null); simulateSync(); }; const toggleStar = (id) => { setNotes((prev) => prev.map((n) => n.id === id ? { ...n, starred: !n.starred } : n)); }; const importPDF = (e) => { const file = e.target.files[0]; if (!file) return; const pdf = { id: uid(), type: "pdf", title: file.name, name: file.name, pdfData: { name: file.name, size: file.size }, annotations: null, starred: false, createdAt: now(), updatedAt: now(), }; setPdfs((prev) => [pdf, ...prev]); setActiveNote(pdf.id); setView("pdf"); setActiveFolder("pdfs"); simulateSync(); }; const createNotebook = ({ name, cover }) => { const nb = { id: uid(), name, cover, createdAt: now() }; setNotebooks((prev) => [...prev, nb]); setShowNewNotebook(false); setActiveFolder(nb.id); }; const simulateSync = () => { setSyncStatus("syncing"); setTimeout(() => setSyncStatus("synced"), 1200); }; // Font import useEffect(() => { const link = document.createElement("link"); link.href = "https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=DM+Sans:wght@400;500;600;700&display=swap"; link.rel = "stylesheet"; document.head.appendChild(link); }, []); /* ─── SIDEBAR ─── */ const renderSidebar = () => (
{/* Logo area */}
✏️
InkNote
{/* Search */}
{Icons.search} setSearchQuery(e.target.value)} placeholder="Search notes..." style={{ border: "none", background: "transparent", outline: "none", flex: 1, color: T.text, fontSize: 13, fontFamily: "'DM Sans', sans-serif", }} />
{/* System folders */}
{DEFAULT_FOLDERS.map((f) => ( ))}
{/* Notebooks */}
Notebooks
{notebooks.map((nb) => ( ))}
{/* Bottom actions */}
● {syncStatus === "synced" ? "All changes synced" : syncStatus === "syncing" ? "Syncing..." : "Offline"}
); /* ─── NOTE LIST ─── */ const renderNoteList = () => { const currentFolderName = (() => { const sf = DEFAULT_FOLDERS.find((f) => f.id === activeFolder); if (sf) return sf.name; const nb = notebooks.find((n) => n.id === activeFolder); return nb ? nb.name : "Notes"; })(); return (
{/* Header */}

{currentFolderName}

{/* Note cards */}
{filteredNotes.length === 0 ? (
{activeFolder === "pdfs" ? "📄" : "📝"}
{activeFolder === "pdfs" ? "No PDFs yet. Import one to get started." : "No notes yet. Create one to get started."}
) : ( filteredNotes.map((note) => ( )) )}
); }; /* ─── EDITOR AREA ─── */ const renderEditor = () => { if (!currentNote) { return (
✏️
Select a note to edit
Or create a new one
); } return (
{/* Note header */}
{ if (currentNote.type === "pdf") { setPdfs((prev) => prev.map((p) => p.id === currentNote.id ? { ...p, title: e.target.value } : p)); } else { updateNote(currentNote.id, { title: e.target.value }); } }} style={{ flex: 1, border: "none", background: "transparent", outline: "none", fontSize: 17, fontWeight: 600, color: T.text, fontFamily: "'Newsreader', Georgia, serif", }} />
{/* Editor content */}
{currentNote.type === "drawing" ? ( updateNote(currentNote.id, { drawingData: data })} dark={dark} standalone /> ) : currentNote.type === "pdf" ? ( { setPdfs((prev) => prev.map((p) => p.id === currentNote.id ? { ...p, annotations: data, updatedAt: now() } : p)); }} dark={dark} /> ) : ( updateNote(currentNote.id, { content: html })} dark={dark} /> )}
); }; /* ─── MAIN RENDER ─── */ return (
{renderSidebar()} {renderNoteList()} {renderEditor()} {showNewNotebook && ( setShowNewNotebook(false)} onCreate={createNotebook} dark={dark} /> )}
); }