Files
InkNote/note-app.jsx
T
2026-02-17 19:25:52 +01:00

1116 lines
53 KiB
React

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: (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
),
search: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
),
bold: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg>,
italic: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>,
underline: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg>,
list: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor"/><circle cx="4" cy="12" r="1" fill="currentColor"/><circle cx="4" cy="18" r="1" fill="currentColor"/></svg>,
listOrdered: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><text x="2" y="8" fontSize="8" fill="currentColor" stroke="none" fontFamily="sans-serif">1</text><text x="2" y="14" fontSize="8" fill="currentColor" stroke="none" fontFamily="sans-serif">2</text><text x="2" y="20" fontSize="8" fill="currentColor" stroke="none" fontFamily="sans-serif">3</text></svg>,
checkbox: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 12l2 2 4-4"/></svg>,
trash: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>,
star: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>,
starFill: <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>,
pen: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg>,
eraser: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M20 20H7L3 16l9-9 8 8-4 4"/><path d="M6 11l8 8"/></svg>,
undo: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>,
redo: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>,
sun: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>,
moon: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>,
folder: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>,
back: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="15 18 9 12 15 6"/></svg>,
menu: <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>,
download: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>,
highlight: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M15.5 2.5L22 9l-6 6-7-7 6.5-5.5z"/><path d="M9 8l-5 5 4 4 5-5"/><path d="M4 13l-2 7 7-2"/></svg>,
notebook: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>,
settings: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
edit: <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>,
sync: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>,
image: <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>,
close: <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>,
};
/* ────────────────────── 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 (
<div style={{ display: "flex", flexDirection: "column", height: "100%", position: "relative" }}>
<div style={toolbarStyle}>
<button style={btnStyle(tool === "pen")} onClick={() => setTool("pen")} title="Pen">{Icons.pen}</button>
<button style={btnStyle(tool === "highlighter")} onClick={() => setTool("highlighter")} title="Highlighter">{Icons.highlight}</button>
<button style={btnStyle(tool === "eraser")} onClick={() => setTool("eraser")} title="Eraser">{Icons.eraser}</button>
<div style={{ width: 1, height: 24, background: T.border, margin: "0 4px" }} />
<button style={btnStyle(false)} onClick={undo} title="Undo" disabled={historyIdx <= 0}>{Icons.undo}</button>
<button style={btnStyle(false)} onClick={redo} title="Redo" disabled={historyIdx >= history.length - 1}>{Icons.redo}</button>
<div style={{ width: 1, height: 24, background: T.border, margin: "0 4px" }} />
{/* Color */}
<div style={{ position: "relative" }}>
<button
style={{ ...btnStyle(showColorPicker), padding: 0 }}
onClick={() => { setShowColorPicker(!showColorPicker); setShowSizePicker(false); }}
>
<div style={{ width: 20, height: 20, borderRadius: "50%", background: color, border: `2px solid ${T.border}` }} />
</button>
{showColorPicker && (
<div style={{
position: "absolute", top: 40, left: 0, background: T.toolbar, border: `1px solid ${T.border}`,
borderRadius: 12, padding: 10, display: "grid", gridTemplateColumns: "repeat(5, 1fr)", gap: 4,
zIndex: 100, boxShadow: "0 8px 24px rgba(0,0,0,0.15)"
}}>
{COLORS.map((c) => (
<button key={c} onClick={() => { setColor(c); setShowColorPicker(false); }} style={{
width: 28, height: 28, borderRadius: "50%", background: c, border: color === c ? `3px solid ${T.active}` : `1px solid ${T.border}`,
cursor: "pointer", transition: "transform 0.1s",
}} />
))}
</div>
)}
</div>
{/* Size */}
<div style={{ position: "relative" }}>
<button
style={btnStyle(showSizePicker)}
onClick={() => { setShowSizePicker(!showSizePicker); setShowColorPicker(false); }}
>
<div style={{ width: Math.min(size * 2, 18), height: Math.min(size * 2, 18), borderRadius: "50%", background: T.text }} />
</button>
{showSizePicker && (
<div style={{
position: "absolute", top: 40, left: 0, background: T.toolbar, border: `1px solid ${T.border}`,
borderRadius: 12, padding: 10, display: "flex", flexDirection: "column", gap: 6, zIndex: 100,
boxShadow: "0 8px 24px rgba(0,0,0,0.15)"
}}>
{PEN_SIZES.map((s) => (
<button key={s} onClick={() => { setSize(s); setShowSizePicker(false); }} style={{
display: "flex", alignItems: "center", gap: 10, padding: "4px 8px", borderRadius: 6,
border: "none", cursor: "pointer", background: size === s ? T.active + "22" : "transparent",
}}>
<div style={{ width: Math.min(s * 2.5, 22), height: Math.min(s * 2.5, 22), borderRadius: "50%", background: T.text }} />
<span style={{ fontSize: 12, color: T.muted }}>{s}px</span>
</button>
))}
</div>
)}
</div>
<div style={{ flex: 1 }} />
<button style={{ ...btnStyle(false), color: "#e5534b" }} onClick={clearCanvas} title="Clear Canvas">{Icons.trash}</button>
</div>
<div style={{ flex: 1, overflow: "hidden", position: "relative", cursor: tool === "eraser" ? "crosshair" : "crosshair" }}>
<canvas
ref={canvasRef}
onMouseDown={startDraw}
onMouseMove={draw}
onMouseUp={stopDraw}
onMouseLeave={stopDraw}
onTouchStart={startDraw}
onTouchMove={draw}
onTouchEnd={stopDraw}
style={{
width: "100%",
height: "100%",
touchAction: "none",
background: overlayMode ? "transparent" : (dark ? "#0d1117" : "#fff"),
}}
/>
</div>
</div>
);
}
/* ────────────────────── 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 (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div style={{
display: "flex", alignItems: "center", gap: 2, padding: "6px 10px",
background: T.toolbar, borderBottom: `1px solid ${T.border}`, flexWrap: "wrap",
}}>
<button style={btnStyle} onClick={() => exec("bold")} title="Bold">{Icons.bold}</button>
<button style={btnStyle} onClick={() => exec("italic")} title="Italic">{Icons.italic}</button>
<button style={btnStyle} onClick={() => exec("underline")} title="Underline">{Icons.underline}</button>
<div style={{ width: 1, height: 20, background: T.border, margin: "0 4px" }} />
<button style={btnStyle} onClick={() => exec("insertUnorderedList")} title="Bullet List">{Icons.list}</button>
<button style={btnStyle} onClick={() => exec("insertOrderedList")} title="Ordered List">{Icons.listOrdered}</button>
<button style={btnStyle} onClick={() => {
const cb = `<div style="display:flex;align-items:center;gap:8px;margin:4px 0"><input type="checkbox" style="width:16px;height:16px;accent-color:${T.active}"/> <span>Task item</span></div>`;
exec("insertHTML", cb);
}} title="Checkbox">{Icons.checkbox}</button>
<div style={{ width: 1, height: 20, background: T.border, margin: "0 4px" }} />
<select
onChange={(e) => exec("formatBlock", e.target.value)}
style={{ border: `1px solid ${T.border}`, borderRadius: 6, padding: "4px 8px", background: T.toolbar, color: T.text, fontSize: 12 }}
defaultValue=""
>
<option value="" disabled>Heading</option>
<option value="h1">Heading 1</option>
<option value="h2">Heading 2</option>
<option value="h3">Heading 3</option>
<option value="p">Paragraph</option>
</select>
</div>
<div
ref={editorRef}
contentEditable
suppressContentEditableWarning
onInput={handleInput}
style={{
flex: 1, padding: "20px 24px", outline: "none", fontSize: 15, lineHeight: 1.7,
color: T.text, overflowY: "auto", minHeight: 200,
fontFamily: "'Newsreader', 'Georgia', serif",
}}
/>
</div>
);
}
/* ────────────────────── 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 (
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
<div style={{
display: "flex", alignItems: "center", gap: 8, padding: "8px 14px",
background: T.toolbar, borderBottom: `1px solid ${T.border}`,
}}>
<button
onClick={() => setDrawMode(!drawMode)}
style={{
display: "flex", alignItems: "center", gap: 6, padding: "6px 14px",
borderRadius: 8, border: `1px solid ${drawMode ? T.active : T.border}`,
background: drawMode ? T.active + "15" : "transparent",
color: drawMode ? T.active : T.text, cursor: "pointer", fontSize: 13,
fontWeight: 500,
}}
>
{Icons.pen} <span>Annotate</span>
</button>
<div style={{ flex: 1 }} />
<button onClick={() => setPageNum(Math.max(1, pageNum - 1))} disabled={pageNum <= 1}
style={{ padding: "4px 10px", borderRadius: 6, border: `1px solid ${T.border}`, background: T.toolbar, color: T.text, cursor: "pointer" }}>
←
</button>
<span style={{ fontSize: 13, color: T.muted }}>Page {pageNum} / {totalPages}</span>
<button onClick={() => setPageNum(Math.min(totalPages, pageNum + 1))} disabled={pageNum >= totalPages}
style={{ padding: "4px 10px", borderRadius: 6, border: `1px solid ${T.border}`, background: T.toolbar, color: T.text, cursor: "pointer" }}>
→
</button>
</div>
<div style={{ flex: 1, overflow: "auto", background: T.bg, position: "relative" }}>
{pdfData ? (
<div style={{ position: "relative", width: "100%", height: "100%" }}>
<div style={{
display: "flex", alignItems: "center", justifyContent: "center",
padding: 40, color: T.muted, fontSize: 14,
flexDirection: "column", gap: 12,
}}>
<div style={{ fontSize: 48 }}>📄</div>
<div>PDF loaded — {pdfData.name}</div>
<div style={{ fontSize: 12, maxWidth: 400, textAlign: "center", lineHeight: 1.6 }}>
In a full deployment, PDF.js renders pages here. The annotation canvas overlays each page for freehand drawing, highlighting, and text annotations.
</div>
</div>
{drawMode && (
<div style={{ position: "absolute", inset: 0, top: 0 }}>
<DrawingCanvas
drawingData={annotations}
onSave={onAnnotationSave}
dark={dark}
overlayMode
/>
</div>
)}
</div>
) : (
<div style={{
display: "flex", alignItems: "center", justifyContent: "center", height: "100%",
flexDirection: "column", gap: 16, color: T.muted,
}}>
<div style={{ fontSize: 64 }}>📄</div>
<div style={{ fontSize: 15 }}>No PDF loaded</div>
<div style={{ fontSize: 13 }}>Import a PDF to view and annotate it</div>
</div>
)}
</div>
</div>
);
}
/* ────────────────────── 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 (
<div style={{
position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", display: "flex",
alignItems: "center", justifyContent: "center", zIndex: 1000, backdropFilter: "blur(4px)",
}} onClick={onClose}>
<div style={{
background: T.bg, borderRadius: 16, padding: 28, width: 420, maxWidth: "90vw",
boxShadow: "0 24px 48px rgba(0,0,0,0.25)", border: `1px solid ${T.border}`,
}} onClick={(e) => e.stopPropagation()}>
<h3 style={{ margin: "0 0 20px", color: T.text, fontSize: 18, fontWeight: 600, fontFamily: "'Newsreader', Georgia, serif" }}>
New Notebook
</h3>
<div style={{
width: "100%", height: 140, borderRadius: 12, background: COVER_GRADIENTS[selectedCover],
display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 20,
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
}}>
<span style={{ color: "#fff", fontSize: 20, fontWeight: 600, fontFamily: "'Newsreader', Georgia, serif", textShadow: "0 2px 8px rgba(0,0,0,0.3)" }}>
{name || "Notebook"}
</span>
</div>
<input
type="text" value={name} onChange={(e) => 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",
}}
/>
<div style={{ marginBottom: 20 }}>
<div style={{ fontSize: 13, color: T.muted, marginBottom: 8 }}>Choose cover</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(6, 1fr)", gap: 8 }}>
{COVER_GRADIENTS.map((g, i) => (
<button key={i} onClick={() => setSelectedCover(i)} style={{
width: "100%", aspectRatio: "3/2", borderRadius: 8, background: g, border: selectedCover === i ? `3px solid ${T.active}` : `2px solid transparent`,
cursor: "pointer", transition: "all 0.15s",
}} />
))}
</div>
</div>
<div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
<button onClick={onClose} style={{
padding: "8px 20px", borderRadius: 10, border: `1px solid ${T.border}`, background: "transparent",
color: T.text, cursor: "pointer", fontSize: 13, fontWeight: 500,
}}>Cancel</button>
<button onClick={() => { if (name.trim()) onCreate({ name: name.trim(), cover: selectedCover }); }} style={{
padding: "8px 20px", borderRadius: 10, border: "none", background: T.active,
color: "#fff", cursor: "pointer", fontSize: 13, fontWeight: 500,
}}>Create</button>
</div>
</div>
</div>
);
}
/* ────────────────────────── 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: "<h2>Welcome to InkNote ✨</h2><p>Your cross-platform note-taking companion.</p><p>Features include:</p><ul><li><b>Rich text</b> editing with formatting</li><li>Freehand <i>drawing</i> with pen & highlighter</li><li>PDF import &amp; annotation</li><li>Notebook organization with custom covers</li><li>Dark &amp; light themes</li></ul><p>Start creating! Click <b>+</b> to add a new note.</p>",
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: "<h3>Sprint Planning</h3><p>Discussed roadmap for Q3. Key points:</p><ol><li>Sync engine improvements</li><li>Mobile gesture support</li><li>PDF annotation export</li></ol>",
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 = () => (
<div style={{
width: sidebarOpen ? 260 : 0, minWidth: sidebarOpen ? 260 : 0,
background: T.sidebar, borderRight: `1px solid ${T.border}`,
display: "flex", flexDirection: "column", overflow: "hidden",
transition: "all 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
fontFamily: "'DM Sans', sans-serif",
}}>
{/* Logo area */}
<div style={{
padding: "20px 18px 12px", display: "flex", alignItems: "center", justifyContent: "space-between",
}}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={{
width: 32, height: 32, borderRadius: 10, background: `linear-gradient(135deg, ${T.accent}, #a855f7)`,
display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16,
}}>✏️</div>
<span style={{ fontWeight: 700, fontSize: 17, color: T.text, letterSpacing: "-0.02em" }}>InkNote</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<button onClick={() => setDark(!dark)} style={{
display: "flex", alignItems: "center", justifyContent: "center", width: 32, height: 32,
borderRadius: 8, border: "none", background: "transparent", cursor: "pointer",
color: T.muted, transition: "color 0.15s",
}}>
{dark ? Icons.sun : Icons.moon}
</button>
</div>
</div>
{/* Search */}
<div style={{ padding: "0 14px 12px" }}>
<div style={{
display: "flex", alignItems: "center", gap: 8, padding: "8px 12px",
borderRadius: 10, background: T.inputBg, border: `1px solid ${T.borderLight}`,
}}>
<span style={{ color: T.faint }}>{Icons.search}</span>
<input
type="text" value={searchQuery} onChange={(e) => 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",
}}
/>
</div>
</div>
{/* System folders */}
<div style={{ padding: "0 10px" }}>
{DEFAULT_FOLDERS.map((f) => (
<button key={f.id} onClick={() => { setActiveFolder(f.id); setActiveNote(null); }} style={{
display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "9px 12px",
borderRadius: 10, border: "none", cursor: "pointer", fontSize: 13, fontWeight: 500,
background: activeFolder === f.id ? T.sidebarActive : "transparent",
color: activeFolder === f.id ? T.accentText : T.textSecondary,
transition: "all 0.12s", fontFamily: "'DM Sans', sans-serif", textAlign: "left",
}}>
<span style={{ fontSize: 15 }}>{f.icon}</span>
<span>{f.name}</span>
{f.id === "all" && <span style={{ marginLeft: "auto", fontSize: 11, color: T.faint, background: T.accentSoft, padding: "2px 7px", borderRadius: 10 }}>{notes.length}</span>}
</button>
))}
</div>
{/* Notebooks */}
<div style={{ padding: "16px 10px 4px" }}>
<div style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "0 12px", marginBottom: 6,
}}>
<span style={{ fontSize: 11, fontWeight: 600, color: T.faint, textTransform: "uppercase", letterSpacing: "0.08em" }}>Notebooks</span>
<button onClick={() => setShowNewNotebook(true)} style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 22, height: 22, borderRadius: 6, border: "none",
background: T.accentSoft, cursor: "pointer", color: T.accent,
}}>
{Icons.plus}
</button>
</div>
{notebooks.map((nb) => (
<button key={nb.id} onClick={() => { setActiveFolder(nb.id); setActiveNote(null); }} style={{
display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "8px 12px",
borderRadius: 10, border: "none", cursor: "pointer", fontSize: 13, fontWeight: 500,
background: activeFolder === nb.id ? T.sidebarActive : "transparent",
color: activeFolder === nb.id ? T.accentText : T.textSecondary,
transition: "all 0.12s", fontFamily: "'DM Sans', sans-serif", textAlign: "left",
}}>
<div style={{
width: 22, height: 16, borderRadius: 4,
background: COVER_GRADIENTS[nb.cover], flexShrink: 0,
}} />
<span>{nb.name}</span>
<span style={{ marginLeft: "auto", fontSize: 11, color: T.faint }}>
{notes.filter((n) => n.folderId === nb.id).length}
</span>
</button>
))}
</div>
{/* Bottom actions */}
<div style={{ marginTop: "auto", padding: "12px 14px", borderTop: `1px solid ${T.borderLight}` }}>
<div style={{ display: "flex", alignItems: "center", gap: 6, color: T.muted, fontSize: 12 }}>
<span style={{ color: syncStatus === "synced" ? T.success : syncStatus === "syncing" ? T.warning : T.danger }}>●</span>
<span>{syncStatus === "synced" ? "All changes synced" : syncStatus === "syncing" ? "Syncing..." : "Offline"}</span>
</div>
</div>
</div>
);
/* ─── 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 (
<div style={{
width: 320, minWidth: 320, borderRight: `1px solid ${T.border}`,
display: "flex", flexDirection: "column", background: T.bg,
fontFamily: "'DM Sans', sans-serif",
}}>
{/* Header */}
<div style={{
padding: "16px 18px 12px", display: "flex", alignItems: "center", justifyContent: "space-between",
borderBottom: `1px solid ${T.borderLight}`,
}}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<button onClick={() => setSidebarOpen(!sidebarOpen)} style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, borderRadius: 8, border: "none",
background: "transparent", cursor: "pointer", color: T.muted,
}}>
{Icons.menu}
</button>
<h2 style={{ margin: 0, fontSize: 17, fontWeight: 600, color: T.text, fontFamily: "'Newsreader', Georgia, serif" }}>
{currentFolderName}
</h2>
</div>
<div style={{ display: "flex", gap: 4 }}>
<button onClick={() => createNote("text")} title="New Note" style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 34, height: 34, borderRadius: 10, border: "none",
background: T.accent, cursor: "pointer", color: "#fff",
boxShadow: `0 2px 8px ${T.accent}44`,
}}>
{Icons.plus}
</button>
<button onClick={() => createNote("drawing")} title="New Drawing" style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 34, height: 34, borderRadius: 10, border: `1px solid ${T.border}`,
background: "transparent", cursor: "pointer", color: T.text,
}}>
{Icons.pen}
</button>
<button onClick={() => fileInputRef.current?.click()} title="Import PDF" style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 34, height: 34, borderRadius: 10, border: `1px solid ${T.border}`,
background: "transparent", cursor: "pointer", color: T.text, fontSize: 16,
}}>
📄
</button>
<input ref={fileInputRef} type="file" accept=".pdf" onChange={importPDF} style={{ display: "none" }} />
</div>
</div>
{/* Note cards */}
<div style={{ flex: 1, overflowY: "auto", padding: "8px" }}>
{filteredNotes.length === 0 ? (
<div style={{
display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
height: "100%", color: T.faint, gap: 12, padding: 30,
}}>
<div style={{ fontSize: 40 }}>{activeFolder === "pdfs" ? "📄" : "📝"}</div>
<div style={{ fontSize: 14, textAlign: "center" }}>
{activeFolder === "pdfs" ? "No PDFs yet. Import one to get started." : "No notes yet. Create one to get started."}
</div>
</div>
) : (
filteredNotes.map((note) => (
<button
key={note.id}
onClick={() => {
setActiveNote(note.id);
setView(note.type === "drawing" ? "drawing" : note.type === "pdf" ? "pdf" : "notes");
}}
style={{
display: "flex", flexDirection: "column", width: "100%", padding: "14px 16px",
borderRadius: 12, border: `1px solid ${activeNote === note.id ? T.accent + "55" : "transparent"}`,
background: activeNote === note.id ? T.accentSoft : "transparent",
cursor: "pointer", marginBottom: 4, textAlign: "left",
transition: "all 0.12s", fontFamily: "'DM Sans', sans-serif",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4, width: "100%" }}>
<span style={{ fontSize: 14 }}>
{note.type === "drawing" ? "🎨" : note.type === "pdf" ? "📄" : "📝"}
</span>
<span style={{
fontSize: 14, fontWeight: 600, color: T.text, flex: 1,
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
}}>
{note.title}
</span>
{note.starred && <span style={{ color: "#f0b429", fontSize: 12 }}>⭐</span>}
</div>
{note.type === "text" && note.content && (
<div style={{
fontSize: 12, color: T.muted, lineHeight: 1.4,
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
maxWidth: "100%",
}}>
{note.content.replace(/<[^>]*>/g, "").slice(0, 80)}
</div>
)}
<div style={{ fontSize: 11, color: T.faint, marginTop: 6 }}>
{fmtDate(note.updatedAt)}
</div>
</button>
))
)}
</div>
</div>
);
};
/* ─── EDITOR AREA ─── */
const renderEditor = () => {
if (!currentNote) {
return (
<div style={{
flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
background: T.bg, color: T.faint, fontFamily: "'DM Sans', sans-serif", gap: 16,
}}>
<div style={{
width: 80, height: 80, borderRadius: 24, background: T.accentSoft,
display: "flex", alignItems: "center", justifyContent: "center", fontSize: 36,
}}>✏️</div>
<div style={{ fontSize: 16, fontWeight: 500, color: T.muted }}>Select a note to edit</div>
<div style={{ fontSize: 13, color: T.faint }}>Or create a new one</div>
<div style={{ display: "flex", gap: 10, marginTop: 8 }}>
<button onClick={() => createNote("text")} style={{
display: "flex", alignItems: "center", gap: 8, padding: "10px 20px",
borderRadius: 12, border: "none", background: T.accent, color: "#fff",
cursor: "pointer", fontSize: 13, fontWeight: 600, fontFamily: "'DM Sans', sans-serif",
}}>
{Icons.plus} New Note
</button>
<button onClick={() => createNote("drawing")} style={{
display: "flex", alignItems: "center", gap: 8, padding: "10px 20px",
borderRadius: 12, border: `1px solid ${T.border}`, background: "transparent",
color: T.text, cursor: "pointer", fontSize: 13, fontWeight: 600, fontFamily: "'DM Sans', sans-serif",
}}>
{Icons.pen} Drawing
</button>
</div>
</div>
);
}
return (
<div style={{
flex: 1, display: "flex", flexDirection: "column", background: T.bg,
fontFamily: "'DM Sans', sans-serif",
}}>
{/* Note header */}
<div style={{
display: "flex", alignItems: "center", gap: 8, padding: "10px 18px",
borderBottom: `1px solid ${T.borderLight}`,
}}>
<input
type="text"
value={currentNote.title}
onChange={(e) => {
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",
}}
/>
<button onClick={() => toggleStar(currentNote.id)} style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, borderRadius: 8, border: "none",
background: "transparent", cursor: "pointer",
color: currentNote.starred ? "#f0b429" : T.faint,
}}>
{currentNote.starred ? Icons.starFill : Icons.star}
</button>
<button onClick={() => deleteNote(currentNote.id)} style={{
display: "flex", alignItems: "center", justifyContent: "center",
width: 32, height: 32, borderRadius: 8, border: "none",
background: "transparent", cursor: "pointer", color: T.danger,
}}>
{Icons.trash}
</button>
</div>
{/* Editor content */}
<div style={{ flex: 1, overflow: "hidden" }}>
{currentNote.type === "drawing" ? (
<DrawingCanvas
drawingData={currentNote.drawingData}
onSave={(data) => updateNote(currentNote.id, { drawingData: data })}
dark={dark}
standalone
/>
) : currentNote.type === "pdf" ? (
<PDFViewer
pdfData={currentNote.pdfData}
annotations={currentNote.annotations}
onAnnotationSave={(data) => {
setPdfs((prev) => prev.map((p) => p.id === currentNote.id ? { ...p, annotations: data, updatedAt: now() } : p));
}}
dark={dark}
/>
) : (
<RichTextEditor
content={currentNote.content}
onChange={(html) => updateNote(currentNote.id, { content: html })}
dark={dark}
/>
)}
</div>
</div>
);
};
/* ─── MAIN RENDER ─── */
return (
<div style={{
display: "flex", height: "100vh", width: "100vw", overflow: "hidden",
background: T.bg, color: T.text, fontFamily: "'DM Sans', sans-serif",
}}>
<style>{`
@import url('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');
* { box-sizing: border-box; margin: 0; padding: 0; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: ${T.borderLight}; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: ${T.faint}; }
button:hover { opacity: 0.85; }
input::placeholder { color: ${T.faint}; }
[contenteditable]:empty:before {
content: 'Start writing...';
color: ${T.faint};
pointer-events: none;
}
`}</style>
{renderSidebar()}
{renderNoteList()}
{renderEditor()}
{showNewNotebook && (
<CreateNotebookModal
onClose={() => setShowNewNotebook(false)}
onCreate={createNotebook}
dark={dark}
/>
)}
</div>
);
}