Files
InkNote/frontend/src/NoteApp.jsx
T

1095 lines
89 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect, useMemo } from "react";
import * as sync from "./syncEngine.js";
const uid = () => Math.random().toString(36).slice(2, 10);
const now = () => new Date().toISOString();
const fmtDate = (d) => new Date(d).toLocaleDateString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});
const COLORS = ["#0d1117","#e94560","#0f3460","#533483","#e07c24","#21bf73","#fd5e53","#3282b8","#ff6b6b","#4ecdc4","#f7dc6f","#bb8fce","#85c1e9","#f8c471","#82e0aa","#f1948a","#aeb6bf","#ffffff","#f39c12","#000000"];
const PEN_SIZES = [1,2,3,5,8,12,20];
const COVERS = [
{bg:"linear-gradient(135deg,#0f0c29,#302b63,#24243e)",spine:"#1a1540",accent:"#6c63ff"},
{bg:"linear-gradient(135deg,#1a1a2e,#16213e,#0f3460)",spine:"#0a1628",accent:"#4fc3f7"},
{bg:"linear-gradient(135deg,#134e5e,#71b280)",spine:"#0e3d4a",accent:"#a5d6a7"},
{bg:"linear-gradient(135deg,#8e2de2,#4a00e0)",spine:"#3a0099",accent:"#ce93d8"},
{bg:"linear-gradient(135deg,#fc4a1a,#f7b733)",spine:"#c43d15",accent:"#ffe082"},
{bg:"linear-gradient(135deg,#c94b4b,#4b134f)",spine:"#3d0f40",accent:"#f48fb1"},
{bg:"linear-gradient(135deg,#11998e,#38ef7d)",spine:"#0d7a71",accent:"#b9f6ca"},
{bg:"linear-gradient(135deg,#373b44,#4286f4)",spine:"#2a2e35",accent:"#90caf9"},
{bg:"linear-gradient(135deg,#141e30,#243b55)",spine:"#0e1520",accent:"#78909c"},
{bg:"linear-gradient(135deg,#f12711,#f5af19)",spine:"#c01f0d",accent:"#fff59d"},
{bg:"linear-gradient(135deg,#2c3e50,#3498db)",spine:"#1e2b38",accent:"#81d4fa"},
{bg:"linear-gradient(135deg,#0f2027,#203a43,#2c5364)",spine:"#0a171c",accent:"#80cbc4"},
{bg:"linear-gradient(135deg,#aa4b6b,#6b6b83,#3b8d99)",spine:"#7a3550",accent:"#f48fb1"},
{bg:"linear-gradient(135deg,#1f1c2c,#928dab)",spine:"#15131f",accent:"#b39ddb"},
{bg:"linear-gradient(135deg,#654ea3,#eaafc8)",spine:"#4e3c7e",accent:"#f8bbd0"},
];
const DEFAULT_FOLDERS = [{id:"all",name:"All Notes",icon:"📋"},{id:"starred",name:"Starred",icon:"⭐"},{id:"trash",name:"Trash",icon:"🗑️"}];
const I = {
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="15" height="15" 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="15" height="15" 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="15" height="15" 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="15" height="15" 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>,
listOrd:<svg width="15" height="15" 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>,
check:<svg width="15" height="15" 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="15" height="15" 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="15" height="15" 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="15" height="15" 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="17" height="17" 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="17" height="17" 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>,
highlight:<svg width="15" height="15" 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>,
undo:<svg width="15" height="15" 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="15" height="15" 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="17" height="17" 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="17" height="17" 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>,
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>,
text:<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/></svg>,
draw:<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></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>,
move:<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>,
restore:<svg width="14" height="14" 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>,
grip:<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="5" r="2"/><circle cx="15" cy="5" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="19" r="2"/><circle cx="15" cy="19" r="2"/></svg>,
logout:<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>,
user:<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
x:<svg width="14" height="14" 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>,
save:<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>,
};
/* ═══════ Shared Components ═══════ */
function ConfirmDialog({title,message,confirmLabel="Delete",onConfirm,onCancel,dark}){
const T=useMemo(()=>({bg:dark?"#161b22":"#fff",border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",muted:dark?"#8b949e":"#656d76",danger:dark?"#f85149":"#cf222e"}),[dark]);
return(<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.5)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:2000,backdropFilter:"blur(4px)"}} onClick={onCancel}>
<div style={{background:T.bg,borderRadius:16,padding:26,width:380,maxWidth:"90vw",boxShadow:"0 20px 40px rgba(0,0,0,0.3)",border:`1px solid ${T.border}`}} onClick={e=>e.stopPropagation()}>
<div style={{fontSize:17,fontWeight:600,color:T.text,marginBottom:8,fontFamily:"'Newsreader',Georgia,serif"}}>{title}</div>
<div style={{fontSize:13,color:T.muted,lineHeight:1.6,marginBottom:22,fontFamily:"'DM Sans',sans-serif"}}>{message}</div>
<div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
<button onClick={onCancel} 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={onConfirm} style={{padding:"8px 20px",borderRadius:10,border:"none",background:T.danger,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:500}}>{confirmLabel}</button>
</div>
</div>
</div>);
}
function ContextMenu({x,y,items,onClose,dark}){
const T=useMemo(()=>({bg:dark?"#1c2128":"#fff",border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",hover:dark?"#262c36":"#f3f4f6",danger:dark?"#f85149":"#cf222e"}),[dark]);
useEffect(()=>{const h=()=>onClose();window.addEventListener("click",h);window.addEventListener("contextmenu",h);return()=>{window.removeEventListener("click",h);window.removeEventListener("contextmenu",h);};},[onClose]);
return(<div style={{position:"fixed",left:Math.min(x,window.innerWidth-220),top:Math.min(y,window.innerHeight-200),zIndex:3000,background:T.bg,border:`1px solid ${T.border}`,borderRadius:12,boxShadow:"0 8px 24px rgba(0,0,0,0.25)",padding:4,minWidth:190,fontFamily:"'DM Sans',sans-serif"}} onClick={e=>e.stopPropagation()}>
{items.map((item,i)=>item.divider?<div key={i} style={{height:1,background:T.border,margin:"4px 6px"}}/>:item.submenu?<SubMenu key={i} item={item} T={T} onClose={onClose}/>:(
<button key={i} onClick={()=>{item.action();onClose();}} style={{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"8px 12px",borderRadius:8,border:"none",cursor:"pointer",fontSize:13,fontWeight:500,background:"transparent",color:item.danger?T.danger:T.text,textAlign:"left"}}
onMouseEnter={e=>e.currentTarget.style.background=T.hover} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
{item.icon&&<span style={{display:"flex",opacity:0.7}}>{item.icon}</span>}{item.label}
</button>
))}
</div>);
}
function SubMenu({item,T,onClose}){
const [open,setOpen]=useState(false);
return(<div style={{position:"relative"}} onMouseEnter={()=>setOpen(true)} onMouseLeave={()=>setOpen(false)}>
<div style={{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"8px 12px",borderRadius:8,fontSize:13,fontWeight:500,color:T.text,cursor:"pointer",background:open?T.hover:"transparent"}}>
{item.icon&&<span style={{display:"flex",opacity:0.7}}>{item.icon}</span>}{item.label}<span style={{marginLeft:"auto",fontSize:10,opacity:0.5}}>▸</span>
</div>
{open&&<div style={{position:"absolute",left:"100%",top:0,background:T.bg,border:`1px solid ${T.border}`,borderRadius:10,boxShadow:"0 8px 20px rgba(0,0,0,0.2)",padding:4,minWidth:160,zIndex:10}}>
{item.submenu.map((sub,j)=><button key={j} onClick={()=>{sub.action();onClose();}} style={{display:"flex",alignItems:"center",gap:8,width:"100%",padding:"7px 12px",borderRadius:6,border:"none",cursor:"pointer",fontSize:12,fontWeight:500,background:"transparent",color:T.text,textAlign:"left"}}
onMouseEnter={e=>e.currentTarget.style.background=T.hover} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
{sub.colorDot&&<div style={{width:12,height:8,borderRadius:2,background:sub.colorDot,flexShrink:0}}/>}{sub.label}
</button>)}
</div>}
</div>);
}
function EditNotebookModal({notebook,onClose,onSave,dark}){
const [name,setName]=useState(notebook.name);const [sel,setSel]=useState(notebook.cover);
const T=useMemo(()=>({bg:dark?"#161b22":"#fff",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.55)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1000,backdropFilter:"blur(6px)"}} onClick={onClose}>
<div style={{background:T.bg,borderRadius:20,padding:30,width:460,maxWidth:"92vw",boxShadow:"0 24px 48px rgba(0,0,0,0.3)",border:`1px solid ${T.border}`}} onClick={e=>e.stopPropagation()}>
<h3 style={{margin:"0 0 22px",color:T.text,fontSize:18,fontWeight:600,fontFamily:"'Newsreader',Georgia,serif"}}>Edit Notebook</h3>
<input type="text" value={name} onChange={e=>setName(e.target.value)} autoFocus 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"}}
onKeyDown={e=>{if(e.key==="Enter"&&name.trim())onSave({name:name.trim(),cover:sel});}}/>
<div style={{marginBottom:22}}>
<div style={{fontSize:12,color:T.muted,marginBottom:8}}>Cover style</div>
<div style={{display:"grid",gridTemplateColumns:"repeat(5,1fr)",gap:8}}>
{COVERS.map((c,i)=><button key={i} onClick={()=>setSel(i)} style={{width:"100%",aspectRatio:"3/4",borderRadius:8,background:c.bg,border:sel===i?`3px solid ${T.active}`:"2px solid transparent",cursor:"pointer",position:"relative",overflow:"hidden"}}><div style={{position:"absolute",left:0,top:0,bottom:0,width:4,background:c.spine}}/></button>)}
</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())onSave({name:name.trim(),cover:sel});}} style={{padding:"8px 20px",borderRadius:10,border:"none",background:T.active,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:500}}>Save</button>
</div>
</div>
</div>);
}
function CreateNotebookModal({onClose,onCreate,dark}){
const [name,setName]=useState("");const [sel,setSel]=useState(0);
const T=useMemo(()=>({bg:dark?"#161b22":"#fff",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.55)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1000,backdropFilter:"blur(6px)"}} onClick={onClose}>
<div style={{background:T.bg,borderRadius:20,padding:30,width:460,maxWidth:"92vw",boxShadow:"0 24px 48px rgba(0,0,0,0.3)",border:`1px solid ${T.border}`}} onClick={e=>e.stopPropagation()}>
<h3 style={{margin:"0 0 22px",color:T.text,fontSize:18,fontWeight:600,fontFamily:"'Newsreader',Georgia,serif"}}>New Notebook</h3>
<input type="text" value={name} onChange={e=>setName(e.target.value)} placeholder="Notebook name..." autoFocus 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"}}
onKeyDown={e=>{if(e.key==="Enter"&&name.trim())onCreate({name:name.trim(),cover:sel});}}/>
<div style={{marginBottom:22}}>
<div style={{fontSize:12,color:T.muted,marginBottom:8}}>Choose cover style</div>
<div style={{display:"grid",gridTemplateColumns:"repeat(5,1fr)",gap:8}}>
{COVERS.map((c,i)=><button key={i} onClick={()=>setSel(i)} style={{width:"100%",aspectRatio:"3/4",borderRadius:8,background:c.bg,border:sel===i?`3px solid ${T.active}`:"2px solid transparent",cursor:"pointer",position:"relative",overflow:"hidden"}}><div style={{position:"absolute",left:0,top:0,bottom:0,width:4,background:c.spine}}/></button>)}
</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:sel});}} style={{padding:"8px 20px",borderRadius:10,border:"none",background:T.active,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:500}}>Create</button>
</div>
</div>
</div>);
}
/* ═══════ ACCOUNT MENU ═══════ */
function AccountMenu({user,onClose,onLogout,onUserUpdate,dark}){
const [tab,setTab]=useState("profile"); // profile | email | password
const [displayName,setDisplayName]=useState(user?.displayName||"");
const [newEmail,setNewEmail]=useState(user?.email||"");
const [emailPass,setEmailPass]=useState("");
const [curPass,setCurPass]=useState("");const [newPass,setNewPass]=useState("");const [newPass2,setNewPass2]=useState("");
const [msg,setMsg]=useState("");const [err,setErr]=useState("");const [saving,setSaving]=useState(false);
const T=useMemo(()=>({bg:dark?"#161b22":"#fff",border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",muted:dark?"#8b949e":"#656d76",accent:dark?"#388bfd":"#0969da",danger:dark?"#f85149":"#cf222e",success:dark?"#3fb950":"#1a7f37",inputBg:dark?"#0d1117":"#f6f8fa"}),[dark]);
const inp={width:"100%",padding:"10px 14px",borderRadius:10,border:`1px solid ${T.border}`,background:T.inputBg,color:T.text,fontSize:14,outline:"none",boxSizing:"border-box",marginBottom:10,fontFamily:"'DM Sans',sans-serif"};
const tbtn=(active)=>({padding:"7px 14px",borderRadius:8,border:"none",cursor:"pointer",fontSize:12,fontWeight:600,background:active?T.accent+"22":"transparent",color:active?T.accent:T.muted});
const clear=()=>{setMsg("");setErr("");};
const saveProfile=async()=>{if(!displayName.trim())return;clear();setSaving(true);
try{const u=await sync.updateProfile(displayName.trim());onUserUpdate(u);setMsg("Display name updated");}catch(e){setErr(e.message);}setSaving(false);};
const saveEmail=async()=>{if(!newEmail||!emailPass)return;clear();setSaving(true);
try{const u=await sync.updateEmail(newEmail,emailPass);onUserUpdate(u);setEmailPass("");setMsg("Email updated");}catch(e){setErr(e.message);}setSaving(false);};
const savePassword=async()=>{if(!curPass||!newPass)return;if(newPass!==newPass2){setErr("Passwords don't match");return;}clear();setSaving(true);
try{await sync.updatePassword(curPass,newPass);setCurPass("");setNewPass("");setNewPass2("");setMsg("Password changed");}catch(e){setErr(e.message);}setSaving(false);};
return(<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.55)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:2000,backdropFilter:"blur(6px)"}} onClick={onClose}>
<div style={{background:T.bg,borderRadius:20,padding:0,width:440,maxWidth:"92vw",boxShadow:"0 24px 48px rgba(0,0,0,0.3)",border:`1px solid ${T.border}`,overflow:"hidden"}} onClick={e=>e.stopPropagation()}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"18px 22px",borderBottom:`1px solid ${T.border}`}}>
<h3 style={{margin:0,fontSize:17,fontWeight:600,color:T.text,fontFamily:"'Newsreader',Georgia,serif"}}>Account</h3>
<button onClick={onClose} style={{display:"flex",alignItems:"center",justifyContent:"center",width:28,height:28,borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:T.muted}}>{I.x}</button>
</div>
<div style={{display:"flex",gap:4,padding:"12px 22px 0",borderBottom:`1px solid ${T.border}`}}>
<button onClick={()=>{setTab("profile");clear();}} style={tbtn(tab==="profile")}>Profile</button>
<button onClick={()=>{setTab("email");clear();}} style={tbtn(tab==="email")}>Email</button>
<button onClick={()=>{setTab("password");clear();}} style={tbtn(tab==="password")}>Password</button>
</div>
<div style={{padding:"18px 22px 22px"}}>
{msg&&<div style={{color:T.success,fontSize:12,marginBottom:10,padding:"8px 12px",borderRadius:8,background:T.success+"15"}}>{msg}</div>}
{err&&<div style={{color:T.danger,fontSize:12,marginBottom:10,padding:"8px 12px",borderRadius:8,background:T.danger+"12"}}>{err}</div>}
{tab==="profile"&&<>
<label style={{fontSize:12,color:T.muted,marginBottom:4,display:"block"}}>Display Name</label>
<input type="text" value={displayName} onChange={e=>setDisplayName(e.target.value)} style={inp} onKeyDown={e=>{if(e.key==="Enter")saveProfile();}}/>
<button onClick={saveProfile} disabled={saving} style={{width:"100%",padding:"10px",borderRadius:10,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:600,opacity:saving?0.6:1}}>{saving?"Saving...":"Save Changes"}</button>
</>}
{tab==="email"&&<>
<label style={{fontSize:12,color:T.muted,marginBottom:4,display:"block"}}>New Email</label>
<input type="email" value={newEmail} onChange={e=>setNewEmail(e.target.value)} style={inp}/>
<label style={{fontSize:12,color:T.muted,marginBottom:4,display:"block"}}>Confirm Password</label>
<input type="password" value={emailPass} onChange={e=>setEmailPass(e.target.value)} style={inp} onKeyDown={e=>{if(e.key==="Enter")saveEmail();}}/>
<button onClick={saveEmail} disabled={saving} style={{width:"100%",padding:"10px",borderRadius:10,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:600,opacity:saving?0.6:1}}>{saving?"Saving...":"Update Email"}</button>
</>}
{tab==="password"&&<>
<label style={{fontSize:12,color:T.muted,marginBottom:4,display:"block"}}>Current Password</label>
<input type="password" value={curPass} onChange={e=>setCurPass(e.target.value)} style={inp}/>
<label style={{fontSize:12,color:T.muted,marginBottom:4,display:"block"}}>New Password</label>
<input type="password" value={newPass} onChange={e=>setNewPass(e.target.value)} style={inp}/>
<label style={{fontSize:12,color:T.muted,marginBottom:4,display:"block"}}>Confirm New Password</label>
<input type="password" value={newPass2} onChange={e=>setNewPass2(e.target.value)} style={inp} onKeyDown={e=>{if(e.key==="Enter")savePassword();}}/>
<button onClick={savePassword} disabled={saving} style={{width:"100%",padding:"10px",borderRadius:10,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:600,opacity:saving?0.6:1}}>{saving?"Saving...":"Change Password"}</button>
</>}
</div>
<div style={{padding:"0 22px 18px"}}>
<button onClick={onLogout} style={{display:"flex",alignItems:"center",gap:8,width:"100%",padding:"10px",borderRadius:10,border:`1px solid ${T.danger}33`,background:T.danger+"08",cursor:"pointer",color:T.danger,fontSize:13,fontWeight:600,justifyContent:"center"}}>{I.logout} Sign Out</button>
</div>
</div>
</div>);
}
/* ═══════ AUTH SCREEN ═══════ */
function AuthScreen({onLogin,dark}){
const [isLogin,setIsLogin]=useState(true);
const [email,setEmail]=useState("");const [pass,setPass]=useState("");const [name,setName]=useState("");
const [err,setErr]=useState("");const [loading,setLoading]=useState(false);
const T=useMemo(()=>({bg:dark?"#0d1117":"#f6f8fa",card:dark?"#161b22":"#fff",border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",muted:dark?"#8b949e":"#656d76",accent:dark?"#388bfd":"#0969da",danger:dark?"#f85149":"#cf222e"}),[dark]);
const submit=async()=>{
if(!email||!pass){setErr("Email and password required");return;}
setLoading(true);setErr("");
try{
const user=isLogin?await sync.login(email,pass):await sync.register(email,pass,name||undefined);
onLogin(user);
}catch(e){setErr(e.message==="AUTH"?"Invalid credentials":e.message);}
finally{setLoading(false);}
};
return(<div style={{display:"flex",alignItems:"center",justifyContent:"center",minHeight:"100vh",background:T.bg,fontFamily:"'DM Sans',sans-serif"}}>
<div style={{background:T.card,borderRadius:24,padding:40,width:400,maxWidth:"90vw",boxShadow:"0 20px 60px rgba(0,0,0,0.15)",border:`1px solid ${T.border}`}}>
<div style={{display:"flex",alignItems:"center",gap:12,marginBottom:28}}>
<div style={{width:42,height:42,borderRadius:14,background:`linear-gradient(135deg,${T.accent},#a855f7)`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:22}}>✏️</div>
<div><div style={{fontSize:22,fontWeight:700,color:T.text,fontFamily:"'Newsreader',Georgia,serif"}}>InkNote</div>
<div style={{fontSize:12,color:T.muted}}>{isLogin?"Sign in to sync your notes":"Create your account"}</div></div>
</div>
{!isLogin&&<input type="text" value={name} onChange={e=>setName(e.target.value)} placeholder="Display name" style={{width:"100%",padding:"12px 16px",borderRadius:12,border:`1px solid ${T.border}`,background:T.bg,color:T.text,fontSize:14,outline:"none",marginBottom:12,boxSizing:"border-box"}}/>}
<input type="email" value={email} onChange={e=>setEmail(e.target.value)} placeholder="Email" autoFocus style={{width:"100%",padding:"12px 16px",borderRadius:12,border:`1px solid ${T.border}`,background:T.bg,color:T.text,fontSize:14,outline:"none",marginBottom:12,boxSizing:"border-box"}}/>
<input type="password" value={pass} onChange={e=>setPass(e.target.value)} placeholder="Password" style={{width:"100%",padding:"12px 16px",borderRadius:12,border:`1px solid ${T.border}`,background:T.bg,color:T.text,fontSize:14,outline:"none",marginBottom:16,boxSizing:"border-box"}} onKeyDown={e=>{if(e.key==="Enter")submit();}}/>
{err&&<div style={{color:T.danger,fontSize:12,marginBottom:12,padding:"8px 12px",borderRadius:8,background:T.danger+"12"}}>{err}</div>}
<button onClick={submit} disabled={loading} style={{width:"100%",padding:"12px",borderRadius:12,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:14,fontWeight:600,marginBottom:16,opacity:loading?0.6:1}}>
{loading?"...":(isLogin?"Sign In":"Create Account")}
</button>
<div style={{textAlign:"center",fontSize:13,color:T.muted}}>
{isLogin?"Don't have an account? ":"Already have an account? "}
<button onClick={()=>{setIsLogin(!isLogin);setErr("");}} style={{background:"none",border:"none",color:T.accent,cursor:"pointer",fontSize:13,fontWeight:600,textDecoration:"underline"}}>{isLogin?"Sign Up":"Sign In"}</button>
</div>
</div>
</div>);
}
/* ═══════ TASK LIST (proper React state, not contentEditable) ═══════ */
function TaskList({tasks,onChange,dark}){
const T=useMemo(()=>({border:dark?"#30363d":"#d0d7de",text:dark?"#e6edf3":"#1f2328",muted:dark?"#8b949e":"#656d76",active:dark?"#388bfd":"#0969da",faint:dark?"#484f58":"#afb8c1",done:dark?"#484f58":"#afb8c1"}),[dark]);
const inputRefs=useRef({});
const toggle=(i)=>{const t=[...tasks];t[i]={...t[i],done:!t[i].done};onChange(t);};
const setText=(i,text)=>{const t=[...tasks];t[i]={...t[i],text};onChange(t);};
const addAfter=(i)=>{const t=[...tasks];const nid=uid();t.splice(i+1,0,{id:nid,text:"",done:false});onChange(t);setTimeout(()=>inputRefs.current[nid]?.focus(),0);};
const remove=(i)=>{
if(tasks.length<=1&&tasks[0].text==="")return;
const t=[...tasks];t.splice(i,1);onChange(t);
if(t.length>0){const fi=Math.max(0,i-1);setTimeout(()=>{const el=inputRefs.current[t[fi]?.id];if(el){el.focus();el.selectionStart=el.value.length;}},0);}
};
const handleKey=(e,i)=>{
if(e.key==="Enter"){e.preventDefault();if(tasks[i].text===""){remove(i);}else{addAfter(i);}}
else if(e.key==="Backspace"&&tasks[i].text===""&&tasks.length>1){e.preventDefault();remove(i);}
};
if(!tasks||tasks.length===0)return null;
return(<div style={{padding:"0 24px 10px"}}>
<div style={{fontSize:11,fontWeight:600,color:T.muted,textTransform:"uppercase",letterSpacing:"0.06em",marginBottom:6}}>Tasks ({tasks.filter(t=>t.done).length}/{tasks.length})</div>
{tasks.map((task,i)=>(
<div key={task.id} style={{display:"flex",alignItems:"center",gap:8,padding:"4px 0",borderBottom:`1px solid ${dark?"#1c2128":"#f0f0f0"}`}}>
<input type="checkbox" checked={task.done} onChange={()=>toggle(i)} style={{width:16,height:16,accentColor:T.active,cursor:"pointer",flexShrink:0}}/>
<input ref={el=>inputRefs.current[task.id]=el} type="text" value={task.text} onChange={e=>setText(i,e.target.value)}
onKeyDown={e=>handleKey(e,i)} placeholder="Task..."
style={{flex:1,border:"none",background:"transparent",outline:"none",fontSize:14,color:task.done?T.done:T.text,textDecoration:task.done?"line-through":"none",fontFamily:"'DM Sans',sans-serif"}}/>
<button onClick={()=>remove(i)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:20,height:20,borderRadius:4,border:"none",background:"transparent",cursor:"pointer",color:T.faint,opacity:0.5,fontSize:12}}>×</button>
</div>
))}
<button onClick={()=>addAfter(tasks.length-1)} style={{display:"flex",alignItems:"center",gap:6,padding:"6px 0",border:"none",background:"transparent",cursor:"pointer",color:T.active,fontSize:12,fontWeight:500,marginTop:4}}>
{I.plus} <span>Add task</span>
</button>
</div>);
}
/* ═══════ NOTE EDITOR ═══════ */
function NoteEditor({note,onUpdate,dark}){
const [mode,setMode]=useState("text");
const editorRef=useRef(null);const canvasRef=useRef(null);const ctxRef=useRef(null);
const [isDrawing,setIsDrawing]=useState(false);
const [tool,setTool]=useState("pen");const [penColor,setPenColor]=useState(dark?"#e6edf3":"#1a1a2e");const [penSize,setPenSize]=useState(3);
const [undoStack,setUndoStack]=useState([]);const [redoStack,setRedoStack]=useState([]);
const [showColors,setShowColors]=useState(false);const [showSizes,setShowSizes]=useState(false);
const [selectedImg,setSelectedImg]=useState(null);
const [hideDrawing,setHideDrawing]=useState(false);
const [drawingBlend,setDrawingBlend]=useState({mode:"normal",opacity:1});
const [isMobileViewport,setIsMobileViewport]=useState(window.innerWidth<768);
const stageRef=useRef(null);
const saveTimerRef=useRef(null);
const dirtyRef=useRef(false);
const CW=1200,CH=1600; // Canonical drawing resolution shared by all devices.
const SAVE_DEBOUNCE_MS=600;
const [stageSize,setStageSize]=useState({w:CW,h:CH});
const parsed=useMemo(()=>{try{const p=JSON.parse(note.content);return{html:p.html||"",tasks:p.tasks||[]};}catch{return{html:note.content||"",tasks:[]};}},[note.content,note.id]);
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",faint:dark?"#484f58":"#afb8c1"}),[dark]);
useEffect(()=>{
const el=editorRef.current;
if(!el)return;
const prevNoteId=el.dataset.noteId;
const isNewNote=prevNoteId!==note.id;
if(isNewNote){
el.innerHTML=parsed.html;
el.dataset.noteId=note.id;
return;
}
// Late content arrival for the same note (mobile/network): fill only if editor is still empty.
const hasLocalText=(el.textContent||"").trim().length>0;
if(!hasLocalText && parsed.html){
el.innerHTML=parsed.html;
}
},[note.id,parsed.html]);
useEffect(()=>{
setHideDrawing(false);
const isLegacyRaster=typeof note.drawingData==="string"&&/^data:image\/(jpeg|jpg|webp)/i.test(note.drawingData);
if(!note.drawingData){setDrawingBlend({mode:"normal",opacity:1});return;}
let cancelled=false;
const img=new Image();
img.onload=()=>{
if(cancelled)return;
try{
const c=document.createElement("canvas");c.width=32;c.height=32;
const cx=c.getContext("2d",{willReadFrequently:true});if(!cx)throw new Error("ctx");
cx.drawImage(img,0,0,32,32);
const data=cx.getImageData(0,0,32,32).data;
let solid=0,black=0;
for(let i=0;i<data.length;i+=4){
const a=data[i+3];if(a<245)continue;solid++;
if(data[i]<16&&data[i+1]<16&&data[i+2]<16)black++;
}
const blackRatio=solid?black/solid:0;
if(blackRatio>0.9)setDrawingBlend({mode:"screen",opacity:0.62});
else if(isLegacyRaster)setDrawingBlend({mode:"screen",opacity:0.42});
else setDrawingBlend({mode:"normal",opacity:1});
}catch{
setDrawingBlend(isLegacyRaster?{mode:"screen",opacity:0.42}:{mode:"normal",opacity:1});
}
};
img.onerror=()=>{if(!cancelled)setHideDrawing(true);};
img.src=note.drawingData;
return()=>{cancelled=true;};
},[note.id,note.drawingData]);
// Keep one fixed-aspect drawing stage regardless of device dimensions.
useEffect(()=>{
const fitStage=()=>{
const host=stageRef.current;
if(!host)return;
const W=Math.max(1,host.clientWidth);
const mobile=window.innerWidth<768;
setIsMobileViewport(mobile);
const sidePad=mobile?10:28;
const desktopMax=760;
const minW=mobile?220:320;
const maxAllowed=Math.max(220,W-sidePad*2);
const targetW=mobile?maxAllowed:Math.min(desktopMax,maxAllowed);
const w=Math.floor(Math.max(minW,targetW));
const h=Math.floor((w*CH)/CW);
setStageSize({w,h});
};
fitStage();
if(typeof ResizeObserver!=="undefined"){
const ro=new ResizeObserver(fitStage);
if(stageRef.current)ro.observe(stageRef.current);
window.addEventListener("resize",fitStage);
return()=>{ro.disconnect();window.removeEventListener("resize",fitStage);};
}
window.addEventListener("resize",fitStage);
return()=>window.removeEventListener("resize",fitStage);
},[]);
const saveDrawingNow=()=>{
if(!canvasRef.current||!dirtyRef.current)return;
onUpdate({drawingData:canvasRef.current.toDataURL("image/png")});
dirtyRef.current=false;
};
const scheduleDrawingSave=()=>{
if(saveTimerRef.current)clearTimeout(saveTimerRef.current);
saveTimerRef.current=setTimeout(()=>{saveTimerRef.current=null;saveDrawingNow();},SAVE_DEBOUNCE_MS);
};
/* ── DRAWING: init canvas once on mode switch ── */
useEffect(()=>{
if(mode!=="draw")return;
const c=canvasRef.current;if(!c)return;
c.width=CW;c.height=CH;
const ctx=c.getContext("2d");ctx.lineCap="round";ctx.lineJoin="round";ctxRef.current=ctx;
ctx.clearRect(0,0,CW,CH);
dirtyRef.current=false;
if(note.drawingData){
const img=new Image();img.onload=()=>{
const t=document.createElement("canvas");t.width=CW;t.height=CH;
const tctx=t.getContext("2d",{willReadFrequently:true});if(!tctx){ctx.drawImage(img,0,0,CW,CH);return;}
tctx.drawImage(img,0,0,CW,CH);
try{
const sc=document.createElement("canvas");sc.width=40;sc.height=40;
const sctx=sc.getContext("2d",{willReadFrequently:true});if(!sctx)throw new Error("ctx");
sctx.drawImage(t,0,0,40,40);
const sample=sctx.getImageData(0,0,40,40).data;
let solid=0,black=0;
for(let i=0;i<sample.length;i+=4){
const a=sample[i+3];if(a<245)continue;solid++;
if(sample[i]<16&&sample[i+1]<16&&sample[i+2]<16)black++;
}
const blackRatio=solid?black/solid:0;
if(blackRatio>0.9){
const im=tctx.getImageData(0,0,CW,CH);const d=im.data;
for(let i=0;i<d.length;i+=4){
if(d[i]<18&&d[i+1]<18&&d[i+2]<18&&d[i+3]>245)d[i+3]=0;
}
tctx.putImageData(im,0,0);
}
}catch{}
ctx.drawImage(t,0,0,CW,CH);
};img.src=note.drawingData;
}
},[mode]);
/* Save drawing on mode switch and debounced stroke-end updates. */
const switchMode=(m)=>{
if(mode==="draw"&&m==="text"&&dirtyRef.current&&canvasRef.current){
if(saveTimerRef.current){clearTimeout(saveTimerRef.current);saveTimerRef.current=null;}
saveDrawingNow();
}
setMode(m);setShowColors(false);setShowSizes(false);setSelectedImg(null);
};
const pushUndo=()=>{if(!canvasRef.current)return;const d=canvasRef.current.toDataURL("image/png");setUndoStack(p=>[...p.slice(-15),d]);setRedoStack([]);};
const undo=()=>{if(!undoStack.length)return;const s=undoStack[undoStack.length-1];setUndoStack(p=>p.slice(0,-1));
setRedoStack(p=>[...p,canvasRef.current.toDataURL("image/png")]);
const i=new Image();i.onload=()=>{ctxRef.current.clearRect(0,0,CW,CH);ctxRef.current.drawImage(i,0,0,CW,CH);dirtyRef.current=true;scheduleDrawingSave();};i.src=s;};
const redo=()=>{if(!redoStack.length)return;const s=redoStack[redoStack.length-1];setRedoStack(p=>p.slice(0,-1));
setUndoStack(p=>[...p,canvasRef.current.toDataURL("image/png")]);
const i=new Image();i.onload=()=>{ctxRef.current.clearRect(0,0,CW,CH);ctxRef.current.drawImage(i,0,0,CW,CH);dirtyRef.current=true;scheduleDrawingSave();};i.src=s;};
const clearCanvas=()=>{pushUndo();ctxRef.current?.clearRect(0,0,CW,CH);dirtyRef.current=true;scheduleDrawingSave();};
const getPos=(e)=>{const c=canvasRef.current,r=c.getBoundingClientRect();const t=e.touches?e.touches[0]:e;return{x:(t.clientX-r.left)*(CW/r.width),y:(t.clientY-r.top)*(CH/r.height)};};
const startDraw=(e)=>{e.preventDefault();pushUndo();const p=getPos(e);setIsDrawing(true);
const ctx=ctxRef.current;ctx.beginPath();ctx.moveTo(p.x,p.y);
if(tool==="eraser"){ctx.globalCompositeOperation="destination-out";ctx.lineWidth=penSize*5;}
else if(tool==="highlighter"){ctx.globalCompositeOperation="source-over";ctx.strokeStyle=penColor+"44";ctx.lineWidth=penSize*6;}
else{ctx.globalCompositeOperation="source-over";ctx.strokeStyle=penColor;ctx.lineWidth=penSize;}};
const doDraw=(e)=>{if(!isDrawing)return;e.preventDefault();const p=getPos(e);ctxRef.current.lineTo(p.x,p.y);ctxRef.current.stroke();};
const stopDraw=(e)=>{if(!isDrawing)return;e?.preventDefault();setIsDrawing(false);ctxRef.current.closePath();ctxRef.current.globalCompositeOperation="source-over";dirtyRef.current=true;scheduleDrawingSave();};
useEffect(()=>()=>{if(saveTimerRef.current)clearTimeout(saveTimerRef.current);saveDrawingNow();},[]);
/* ── TEXT ── */
const saveContent=(html,tasks)=>{onUpdate({content:JSON.stringify({html,tasks})});};
const onHtmlChange=()=>{if(editorRef.current)saveContent(editorRef.current.innerHTML,parsed.tasks);};
const onTasksChange=(tasks)=>{saveContent(parsed.html,tasks);};
const addTaskList=()=>{if(parsed.tasks.length===0)onTasksChange([{id:uid(),text:"",done:false}]);};
const exec=(cmd,val=null)=>{document.execCommand(cmd,false,val);editorRef.current?.focus();onHtmlChange();};
/* ── IMAGE PASTE ── */
useEffect(()=>{
const el=editorRef.current;if(!el||mode!=="text")return;
const h=(e)=>{
const items=e.clipboardData?.items;if(!items)return;
for(const item of items){
if(item.type.startsWith("image/")){
e.preventDefault();
const blob=item.getAsFile();if(!blob)return;
const reader=new FileReader();
reader.onload=(ev)=>{
document.execCommand("insertHTML",false,
`<img src="${ev.target.result}" data-rid="1" style="width:50%;max-width:50%;height:auto;border-radius:8px;margin:8px 0;display:block;cursor:pointer" />`);
if(editorRef.current)saveContent(editorRef.current.innerHTML,parsed.tasks);
};
reader.readAsDataURL(blob);return;
}
}
};
el.addEventListener("paste",h);return()=>el.removeEventListener("paste",h);
},[mode,parsed.tasks]);
/* ── IMAGE SELECT + DRAG RESIZE ── */
useEffect(()=>{
const el=editorRef.current;if(!el||mode!=="text")return;
const onClick=(e)=>{
const img=e.target.closest("img[data-rid]");
setSelectedImg(img||null);
};
const onPointerDown=(e)=>{
const img=e.target.closest("img[data-rid]");if(!img)return;
const rect=img.getBoundingClientRect();
const px=e.clientX||(e.touches?.[0]?.clientX);
// Only start resize if clicking within 20px of right edge
if(px<rect.right-20)return;
e.preventDefault();e.stopPropagation();
setSelectedImg(img);
const startX=px,startW=img.offsetWidth,cW=el.offsetWidth-48;
const move=(ev)=>{
ev.preventDefault();
const mx=ev.clientX||(ev.touches?.[0]?.clientX);
const nw=Math.max(40,Math.min(cW,startW+(mx-startX)));
const pct=Math.round(nw/cW*100);
img.style.width=pct+"%";img.style.maxWidth=pct+"%";
};
const up=()=>{
document.removeEventListener("mousemove",move);document.removeEventListener("mouseup",up);
document.removeEventListener("touchmove",move);document.removeEventListener("touchend",up);
if(editorRef.current)saveContent(editorRef.current.innerHTML,parsed.tasks);
};
document.addEventListener("mousemove",move);document.addEventListener("mouseup",up);
document.addEventListener("touchmove",move,{passive:false});document.addEventListener("touchend",up);
};
// Cursor hint near right edge
const onMove=(e)=>{
const img=e.target.closest("img[data-rid]");if(!img){return;}
const r=img.getBoundingClientRect();
img.style.cursor=e.clientX>r.right-20?"ew-resize":"pointer";
};
el.addEventListener("click",onClick);
el.addEventListener("mousedown",onPointerDown);
el.addEventListener("touchstart",onPointerDown,{passive:false});
el.addEventListener("mousemove",onMove);
return()=>{el.removeEventListener("click",onClick);el.removeEventListener("mousedown",onPointerDown);
el.removeEventListener("touchstart",onPointerDown);el.removeEventListener("mousemove",onMove);};
},[mode,parsed.tasks]);
// Blue outline on selected image
useEffect(()=>{
const el=editorRef.current;if(!el)return;
el.querySelectorAll("img[data-rid]").forEach(img=>{
if(img===selectedImg){img.style.outline=`3px solid ${T.active}`;img.style.outlineOffset="2px";}
else{img.style.outline="none";img.style.outlineOffset="0";}
});
},[selectedImg,T.active]);
// Deselect on outside click
useEffect(()=>{if(!selectedImg)return;
const h=(e)=>{if(!editorRef.current?.contains(e.target)&&!e.target.closest("[data-imgtb]"))setSelectedImg(null);};
document.addEventListener("pointerdown",h);return()=>document.removeEventListener("pointerdown",h);
},[selectedImg]);
const setImgSize=(p)=>{if(!selectedImg)return;selectedImg.style.width=p+"%";selectedImg.style.maxWidth=p+"%";if(editorRef.current)saveContent(editorRef.current.innerHTML,parsed.tasks);};
const deleteImg=()=>{if(!selectedImg)return;selectedImg.remove();setSelectedImg(null);if(editorRef.current)saveContent(editorRef.current.innerHTML,parsed.tasks);};
/* ── RENDER ── */
const tb=(a)=>({display:"flex",alignItems:"center",justifyContent:"center",width:30,height:30,borderRadius:6,border:"none",cursor:"pointer",background:a?T.active+"22":"transparent",color:a?T.active:T.text,transition:"all .12s",flexShrink:0});
const mb=(m,ic,lb)=><button onClick={()=>switchMode(m)} style={{display:"flex",alignItems:"center",gap:6,padding:"6px 12px",borderRadius:8,fontSize:12,fontWeight:600,border:`1.5px solid ${mode===m?T.active:T.border}`,background:mode===m?T.active+"15":"transparent",color:mode===m?T.active:T.muted,cursor:"pointer",flexShrink:0}}>{ic}<span>{lb}</span></button>;
return(<div style={{display:"flex",flexDirection:"column",height:"100%",background:isMobileViewport?T.bg:(dark?"linear-gradient(180deg,#0f141b 0%,#0d1117 60%)":"linear-gradient(180deg,#f8fafd 0%,#eef3f8 70%)")}}>
{/* Main toolbar */}
<div style={{display:"flex",alignItems:"center",gap:6,padding:"7px 14px",borderBottom:`1px solid ${T.border}`,background:T.toolbar,flexWrap:"wrap",minHeight:46}}>
{mb("text",I.text,"Write")}{mb("draw",I.draw,"Draw")}
<div style={{width:1,height:22,background:T.border,margin:"0 2px",flexShrink:0}}/>
{mode==="text"?(<>
<button style={tb()} onClick={()=>exec("bold")}>{I.bold}</button>
<button style={tb()} onClick={()=>exec("italic")}>{I.italic}</button>
<button style={tb()} onClick={()=>exec("underline")}>{I.underline}</button>
<div style={{width:1,height:22,background:T.border,margin:"0 2px"}}/>
<button style={tb()} onClick={()=>exec("insertUnorderedList")}>{I.list}</button>
<button style={tb()} onClick={()=>exec("insertOrderedList")}>{I.listOrd}</button>
<button style={tb(parsed.tasks.length>0)} onClick={addTaskList} title="Add task list">{I.check}</button>
<div style={{width:1,height:22,background:T.border,margin:"0 2px"}}/>
<select onChange={e=>exec("formatBlock",e.target.value)} defaultValue="" style={{border:`1px solid ${T.border}`,borderRadius:6,padding:"3px 6px",background:T.toolbar,color:T.text,fontSize:11}}>
<option value="" disabled>Heading</option><option value="h1">H1</option><option value="h2">H2</option><option value="h3">H3</option><option value="p">Body</option>
</select>
</>):(<>
<button style={tb(tool==="pen")} onClick={()=>setTool("pen")}>{I.pen}</button>
<button style={tb(tool==="highlighter")} onClick={()=>setTool("highlighter")}>{I.highlight}</button>
<button style={tb(tool==="eraser")} onClick={()=>setTool("eraser")}>{I.eraser}</button>
<div style={{width:1,height:22,background:T.border,margin:"0 2px"}}/>
<button style={tb()} onClick={undo}>{I.undo}</button><button style={tb()} onClick={redo}>{I.redo}</button>
<div style={{width:1,height:22,background:T.border,margin:"0 2px"}}/>
<div style={{position:"relative"}}>
<button style={tb(showColors)} onClick={()=>{setShowColors(!showColors);setShowSizes(false);}}><div style={{width:18,height:18,borderRadius:"50%",background:penColor,border:`2px solid ${T.border}`}}/></button>
{showColors&&<div style={{position:"absolute",top:36,left:0,background:T.toolbar,border:`1px solid ${T.border}`,borderRadius:12,padding:8,display:"grid",gridTemplateColumns:"repeat(5,1fr)",gap:4,zIndex:100,boxShadow:"0 8px 20px rgba(0,0,0,.2)"}}>
{COLORS.map(c=><button key={c} onClick={()=>{setPenColor(c);setShowColors(false);}} style={{width:26,height:26,borderRadius:"50%",background:c,border:penColor===c?`3px solid ${T.active}`:`1px solid ${T.border}`,cursor:"pointer"}}/>)}
</div>}
</div>
<div style={{position:"relative"}}>
<button style={tb(showSizes)} onClick={()=>{setShowSizes(!showSizes);setShowColors(false);}}><div style={{width:Math.min(penSize*2,16),height:Math.min(penSize*2,16),borderRadius:"50%",background:T.text}}/></button>
{showSizes&&<div style={{position:"absolute",top:36,left:0,background:T.toolbar,border:`1px solid ${T.border}`,borderRadius:12,padding:8,display:"flex",flexDirection:"column",gap:4,zIndex:100,boxShadow:"0 8px 20px rgba(0,0,0,.2)"}}>
{PEN_SIZES.map(s=><button key={s} onClick={()=>{setPenSize(s);setShowSizes(false);}} style={{display:"flex",alignItems:"center",gap:8,padding:"3px 8px",borderRadius:6,border:"none",cursor:"pointer",background:penSize===s?T.active+"22":"transparent"}}>
<div style={{width:Math.min(s*2.5,20),height:Math.min(s*2.5,20),borderRadius:"50%",background:T.text}}/><span style={{fontSize:11,color:T.muted}}>{s}px</span></button>)}
</div>}
</div>
<div style={{flex:1}}/><button style={{...tb(),color:"#e5534b"}} onClick={clearCanvas}>{I.trash}</button>
</>)}
</div>
{/* Image toolbar — appears when an image is selected */}
{selectedImg&&mode==="text"&&<div data-imgtb="1" style={{display:"flex",alignItems:"center",justifyContent:"center",gap:6,padding:"6px 10px",background:T.toolbar,borderBottom:`1px solid ${T.border}`}}>
<span style={{fontSize:11,color:T.muted}}>Size:</span>
{[25,33,50,75,100].map(p=><button key={p} onClick={()=>setImgSize(p)} style={{padding:"3px 8px",borderRadius:6,border:`1px solid ${T.border}`,background:T.bg,color:T.text,fontSize:11,cursor:"pointer",fontWeight:500}}>{p}%</button>)}
<div style={{width:1,height:16,background:T.border}}/>
<button onClick={deleteImg} style={{padding:"3px 8px",borderRadius:6,border:"none",background:"#e5534b22",color:"#e5534b",fontSize:11,cursor:"pointer",fontWeight:600}}>Delete</button>
<span style={{fontSize:10,color:T.faint,marginLeft:2}}>· drag right edge to resize</span>
</div>}
{/* Content area */}
<div ref={stageRef} style={{flex:1,position:"relative",overflow:"hidden"}}>
<div style={{position:"absolute",inset:0,overflowY:"auto",WebkitOverflowScrolling:"touch",zIndex:1,padding:isMobileViewport?"10px 0 18px":"18px 0 28px"}}>
<div style={{position:"relative",width:stageSize.w,minHeight:stageSize.h,margin:isMobileViewport?"0 auto":"0 0 0 28px",background:T.bg,border:isMobileViewport?"none":`1px solid ${T.border}`,borderRadius:isMobileViewport?0:14,boxShadow:isMobileViewport?"none":"0 16px 36px rgba(0,0,0,0.14)"}}>
<div ref={editorRef} contentEditable={mode==="text"} suppressContentEditableWarning onInput={onHtmlChange}
style={{padding:"20px 24px",outline:"none",fontSize:15,lineHeight:1.7,color:T.text,minHeight:stageSize.h,width:"100%",fontFamily:"'Newsreader','Georgia',serif",pointerEvents:mode==="text"?"auto":"none"}}/>
{note.drawingData&&!hideDrawing&&<img src={note.drawingData} alt="" onError={()=>setHideDrawing(true)} style={{position:"absolute",left:0,top:0,width:"100%",height:stageSize.h,pointerEvents:"none",objectFit:"fill",zIndex:2,opacity:drawingBlend.opacity,mixBlendMode:drawingBlend.mode}}/>}
{mode==="draw"&&<canvas ref={canvasRef} onMouseDown={startDraw} onMouseMove={doDraw} onMouseUp={stopDraw} onMouseLeave={stopDraw} onTouchStart={startDraw} onTouchMove={doDraw} onTouchEnd={stopDraw}
style={{position:"absolute",left:0,top:0,width:"100%",height:stageSize.h,touchAction:"none",cursor:"crosshair",background:"transparent",zIndex:3}}/>}
{mode==="text"&&<TaskList tasks={parsed.tasks} onChange={onTasksChange} dark={dark}/>}
</div>
</div>
</div>
</div>);
}
/* ═══════ NOTEBOOK COVER ═══════ */
function NotebookCover({notebook,noteCount,onClick,onContextMenu,isActive,isDragOver,draggable,onDragStart,onDragEnd,onDragOver,onDrop}){
const cover=COVERS[notebook.cover%COVERS.length];const [hovered,setHovered]=useState(false);
return(<button onClick={onClick} onContextMenu={onContextMenu} draggable={draggable} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver} onDrop={onDrop} onMouseEnter={()=>setHovered(true)} onMouseLeave={()=>setHovered(false)}
style={{display:"flex",flexDirection:"column",alignItems:"center",gap:10,background:"none",border:"none",cursor:"pointer",transition:"transform .22s cubic-bezier(.4,0,.2,1),opacity .2s",transform:hovered?"translateY(-6px) scale(1.03)":isDragOver?"scale(1.06)":"none",opacity:isDragOver?0.6:1}}>
<div style={{position:"relative",width:156,height:210,perspective:800}}>
<div style={{position:"absolute",bottom:-5,left:10,right:10,height:14,background:"rgba(0,0,0,0.25)",borderRadius:"50%",filter:"blur(8px)",opacity:hovered?0.5:0.3}}/>
<div style={{position:"absolute",left:0,top:2,bottom:2,width:18,background:cover.spine,borderRadius:"4px 0 0 4px",boxShadow:"inset -2px 0 4px rgba(0,0,0,0.3)",zIndex:2}}>
{[12,18].map(t=><div key={t} style={{position:"absolute",top:t,left:3,right:3,height:1,background:"rgba(255,255,255,0.1)"}}/>)}
{[12,18].map(t=><div key={"b"+t} style={{position:"absolute",bottom:t,left:3,right:3,height:1,background:"rgba(255,255,255,0.1)"}}/>)}
</div>
<div style={{position:"absolute",right:2,top:8,bottom:8,width:5,background:"linear-gradient(90deg,#e8e0d4,#f5f0e8,#e8e0d4)",borderRadius:"0 2px 2px 0",zIndex:1}}/>
<div style={{position:"absolute",left:14,top:0,right:0,bottom:0,background:cover.bg,borderRadius:"2px 8px 8px 2px",boxShadow:isActive?`0 4px 20px rgba(0,0,0,0.3),0 0 0 3px ${cover.accent}88`:"0 4px 16px rgba(0,0,0,0.2)",overflow:"hidden",zIndex:3}}>
<div style={{position:"absolute",inset:0,background:"repeating-linear-gradient(45deg,transparent,transparent 30px,rgba(255,255,255,0.015) 30px,rgba(255,255,255,0.015) 60px)"}}/>
<div style={{position:"absolute",top:0,left:0,right:0,height:3,background:cover.accent,opacity:0.7}}/>
<div style={{position:"absolute",top:28,left:"50%",transform:"translateX(-50%)",width:50,height:50,borderRadius:"50%",border:`1.5px solid ${cover.accent}44`,display:"flex",alignItems:"center",justifyContent:"center"}}><div style={{width:30,height:30,borderRadius:"50%",border:`1px solid ${cover.accent}33`}}/></div>
<div style={{position:"absolute",top:90,left:8,right:8,display:"flex",flexDirection:"column",alignItems:"center",gap:4}}>
<div style={{width:"100%",padding:"6px 4px",borderTop:`1px solid ${cover.accent}33`,borderBottom:`1px solid ${cover.accent}33`,textAlign:"center"}}>
<span style={{color:"#fff",fontSize:notebook.name.length>12?12:14,fontWeight:600,fontFamily:"'Newsreader',Georgia,serif",textShadow:"0 1px 4px rgba(0,0,0,0.4)",display:"-webkit-box",WebkitLineClamp:2,WebkitBoxOrient:"vertical",overflow:"hidden",wordBreak:"break-word"}}>{notebook.name}</span>
</div>
<div style={{display:"flex",gap:3}}>{[0,1,2].map(i=><div key={i} style={{width:2.5,height:2.5,borderRadius:"50%",background:cover.accent+"55"}}/>)}</div>
</div>
<div style={{position:"absolute",bottom:0,left:0,right:0,height:3,background:cover.accent,opacity:0.7}}/>
</div>
</div>
<div style={{textAlign:"center",maxWidth:156}}>
<div style={{fontSize:13,fontWeight:600,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{notebook.name}</div>
<div style={{fontSize:11,opacity:0.5,marginTop:1}}>{noteCount} {noteCount===1?"note":"notes"}</div>
</div>
</button>);
}
/* ════════════════════════════ MAIN APP (offline-first) ════════════════════════════ */
export default function NoteApp(){
const [dark,setDark]=useState(true);
const [user,setUser]=useState(null);const [loading,setLoading]=useState(true);
const [sidebarOpen,setSidebarOpen]=useState(window.innerWidth>768);
const [activeFolder,setActiveFolder]=useState("dashboard");
const [activeNote,setActiveNote]=useState(null);
const [searchQuery,setSearchQuery]=useState("");
const [showNewNb,setShowNewNb]=useState(false);
const [syncStatus,setSyncStatus]=useState("synced");
const [ctxMenu,setCtxMenu]=useState(null);
const [confirmDialog,setConfirmDialog]=useState(null);
const [editingNb,setEditingNb]=useState(null);
const [showAccount,setShowAccount]=useState(false);
const [dragNbId,setDragNbId]=useState(null);const [dragOverNbId,setDragOverNbId]=useState(null);
const [notebooks,setNotebooks]=useState([]);const [notes,setNotes]=useState([]);
const [noteContents,setNoteContents]=useState({});
const [isMobile,setIsMobile]=useState(window.innerWidth<768);
const [mobileView,setMobileView]=useState("list"); // list | editor
const [installPrompt,setInstallPrompt]=useState(null);
const T=useMemo(()=>dark?{
bg:"#0d1117",sidebar:"#010409",sidebarActive:"#161b22",card:"#161b22",toolbar:"#161b22",border:"#30363d",borderLight:"#21262d",text:"#e6edf3",textSec:"#c9d1d9",muted:"#8b949e",faint:"#484f58",accent:"#388bfd",accentSoft:"#388bfd22",accentText:"#58a6ff",danger:"#f85149",success:"#3fb950",warning:"#d29922",inputBg:"#0d1117",dashBg:"#0d1117",
}:{
bg:"#f6f8fa",sidebar:"#ffffff",sidebarActive:"#eef2ff",card:"#ffffff",toolbar:"#ffffff",border:"#d0d7de",borderLight:"#e5e7eb",text:"#1f2328",textSec:"#424a53",muted:"#656d76",faint:"#afb8c1",accent:"#0969da",accentSoft:"#0969da18",accentText:"#0550ae",danger:"#cf222e",success:"#1a7f37",warning:"#9a6700",inputBg:"#f6f8fa",dashBg:"#f0f2f5",
},[dark]);
// ── Responsive ──
useEffect(()=>{const h=()=>{const m=window.innerWidth<768;setIsMobile(m);if(!m)setSidebarOpen(true);};window.addEventListener("resize",h);return()=>window.removeEventListener("resize",h);},[]);
// ── PWA install prompt ──
useEffect(()=>{
const h=(e)=>{e.preventDefault();setInstallPrompt(e);};
window.addEventListener("beforeinstallprompt",h);
return()=>window.removeEventListener("beforeinstallprompt",h);
},[]);
const handleInstall=async()=>{if(!installPrompt)return;installPrompt.prompt();const r=await installPrompt.userChoice;if(r.outcome==="accepted")setInstallPrompt(null);};
// ── Sync status listener ──
useEffect(()=>sync.onSyncStatus(setSyncStatus),[]);
// ── Data change listener — reload from IndexedDB ──
useEffect(()=>sync.onDataChange(async()=>{
setNotebooks(await sync.getNotebooks());
setNotes(await sync.getNotes());
// Keep loaded note content cached to avoid editor flicker/stalls on periodic sync
}),[]);
// ── Startup: restore session → load local data → sync ──
useEffect(()=>{(async()=>{
try{
const u=await sync.restoreSession();
if(u){setUser(u);await reloadLocal();
// Background sync if online
try{await sync.fullSync();await reloadLocal();}catch(e){if(e.message==="AUTH"){setUser(null);}}
}
}catch{}
setLoading(false);
})();},[]);
// Reload from IndexedDB
const reloadLocal=async()=>{
setNotebooks(await sync.getNotebooks());
setNotes(await sync.getNotes());
};
const onLogin=async(u)=>{setUser(u);
try{await sync.fullSync();}catch{}
await reloadLocal();
};
const onLogout=async()=>{await sync.logout();setUser(null);setNotes([]);setNotebooks([]);setNoteContents({});setActiveNote(null);setActiveFolder("dashboard");};
const activeNotes=useMemo(()=>notes.filter(n=>!n.trashed),[notes]);
const trashedNotes=useMemo(()=>notes.filter(n=>n.trashed),[notes]);
const filteredNotes=useMemo(()=>{
let list;
if(activeFolder==="trash")list=trashedNotes;
else if(activeFolder==="starred")list=activeNotes.filter(n=>n.starred);
else if(activeFolder==="all")list=activeNotes;
else if(activeFolder!=="dashboard")list=activeNotes.filter(n=>n.folderId===activeFolder);
else list=activeNotes;
if(searchQuery){const q=searchQuery.toLowerCase();list=list.filter(n=>n.title.toLowerCase().includes(q));}
return list.sort((a,b)=>new Date(b.updatedAt)-new Date(a.updatedAt));
},[notes,activeFolder,searchQuery,activeNotes,trashedNotes]);
const currentNoteMeta=activeNote?notes.find(n=>n.id===activeNote):null;
const currentNoteContent=activeNote?noteContents[activeNote]:null;
const activeNoteHasContent=!!currentNoteContent;
const currentNote=currentNoteMeta&&currentNoteContent?{...currentNoteMeta,...currentNoteContent}:null;
const currentNoteSafe=currentNote||(
currentNoteMeta
? {...currentNoteMeta,content:currentNoteContent?.content||"",drawingData:currentNoteContent?.drawingData||null}
: null
);
// Load note content on demand — fetches server-first on cache miss (page load = all cache empty)
useEffect(()=>{
if(!activeNote || activeNoteHasContent) return;
let cancelled=false;
sync.loadNoteContent(activeNote)
.then(c=>{ if(!cancelled) setNoteContents(p=>({...p,[activeNote]:c})); })
.catch(()=>{ if(!cancelled) setNoteContents(p=>({...p,[activeNote]:{content:"",drawingData:null}})); });
return ()=>{cancelled=true;};
},[activeNote,activeNoteHasContent]);
// Hard fallback: never stay in loading state forever.
useEffect(()=>{
if(!activeNote || activeNoteHasContent) return;
const t=setTimeout(()=>{
setNoteContents(p=>p[activeNote]?p:{...p,[activeNote]:{content:"",drawingData:null}});
},5000);
return ()=>clearTimeout(t);
},[activeNote,activeNoteHasContent]);
// Keep currently open note content fresh across devices.
useEffect(()=>{
if(!user||!activeNote)return;
const i=setInterval(()=>{
if(!sync.isOnline())return;
sync.loadNoteContent(activeNote,{forceRemote:true}).then(c=>{
setNoteContents(p=>{
const prev=p[activeNote];
if(prev&&prev.content===c.content&&prev.drawingData===c.drawingData)return p;
return {...p,[activeNote]:c};
});
}).catch(()=>{});
},6000);
return()=>clearInterval(i);
},[user,activeNote]);
// ── CRUD — all go through sync engine ──
const createNote=async()=>{
const fid=["all","starred","trash","dashboard"].includes(activeFolder)?(notebooks[0]?.id):activeFolder;if(!fid)return;
const note=await sync.createNote(fid);
setNotes(p=>[note,...p]);setNoteContents(p=>({...p,[note.id]:{content:"",drawingData:null}}));
setActiveNote(note.id);if(activeFolder==="dashboard")setActiveFolder(fid);
if(isMobile)setMobileView("editor");
};
const updateNote=async(id,u)=>{
if(u.title!==undefined){setNotes(p=>p.map(n=>n.id===id?{...n,title:u.title,updatedAt:now()}:n));sync.updateNoteTitle(id,u.title);}
if(u.content!==undefined){setNoteContents(p=>({...p,[id]:{...p[id],content:u.content}}));sync.updateNoteContent(id,u.content);}
if(u.drawingData!==undefined){setNoteContents(p=>({...p,[id]:{...p[id],drawingData:u.drawingData}}));sync.updateNoteDrawing(id,u.drawingData);}
};
const toggleStar=async(id)=>{const nv=await sync.toggleNoteStar(id);setNotes(p=>p.map(n=>n.id===id?{...n,starred:nv}:n));};
const trashNoteAction=(id)=>{const note=notes.find(n=>n.id===id);
setConfirmDialog({title:"Move to Trash",message:`Move "${note?.title}" to trash?`,confirmLabel:"Move to Trash",
onConfirm:async()=>{await sync.trashNote(id);setNotes(p=>p.map(n=>n.id===id?{...n,trashed:true}:n));
if(activeNote===id){setActiveNote(null);if(isMobile)setMobileView("list");}setConfirmDialog(null);}});
};
const restoreNote=async(id)=>{await sync.restoreNoteLocal(id);setNotes(p=>p.map(n=>n.id===id?{...n,trashed:false}:n));if(activeNote===id){setActiveNote(null);if(isMobile)setMobileView("list");}};
const permanentDeleteNote=(id)=>{const note=notes.find(n=>n.id===id);
setConfirmDialog({title:"Delete Permanently",message:`Permanently delete "${note?.title}"? This cannot be undone.`,
onConfirm:async()=>{await sync.permanentDeleteNoteLocal(id);setNotes(p=>p.filter(n=>n.id!==id));
if(activeNote===id){setActiveNote(null);if(isMobile)setMobileView("list");}setConfirmDialog(null);}});
};
const emptyTrash=()=>{
setConfirmDialog({title:"Empty Trash",message:`Permanently delete all ${trashedNotes.length} trashed notes?`,
onConfirm:async()=>{await sync.emptyTrashLocal();setNotes(p=>p.filter(n=>!n.trashed));setActiveNote(null);setConfirmDialog(null);}});
};
const moveNoteAction=async(id,newFolderId)=>{await sync.moveNote(id,newFolderId);setNotes(p=>p.map(n=>n.id===id?{...n,folderId:newFolderId}:n));};
const createNb=async({name,cover})=>{const nb=await sync.createNotebook({name,cover});setNotebooks(p=>[...p,nb]);setShowNewNb(false);setActiveFolder(nb.id);};
const updateNb=async(id,updates)=>{await sync.updateNotebook(id,updates);setNotebooks(p=>p.map(nb=>nb.id===id?{...nb,...updates}:nb));setEditingNb(null);};
const requestDeleteNb=(id)=>{const nb=notebooks.find(n=>n.id===id);const nc=activeNotes.filter(n=>n.folderId===id).length;
setConfirmDialog({title:"Delete Notebook",message:`Delete "${nb?.name}"${nc>0?` and trash its ${nc} notes`:""} permanently?`,
onConfirm:async()=>{await sync.deleteNotebookLocal(id);
setNotes(p=>p.map(n=>n.folderId===id?{...n,trashed:true}:n));setNotebooks(p=>p.filter(n=>n.id!==id));
if(activeFolder===id)setActiveFolder("dashboard");setConfirmDialog(null);}});
};
const handleNbDragStart=(e,id)=>{setDragNbId(id);e.dataTransfer.effectAllowed="move";};
const handleNbDragOver=(e,id)=>{e.preventDefault();if(dragNbId&&dragNbId!==id)setDragOverNbId(id);};
const handleNbDrop=async(e,targetId)=>{e.preventDefault();if(!dragNbId||dragNbId===targetId)return;
setNotebooks(prev=>{const a=[...prev];const fi=a.findIndex(n=>n.id===dragNbId);const ti=a.findIndex(n=>n.id===targetId);if(fi<0||ti<0)return prev;const[item]=a.splice(fi,1);a.splice(ti,0,item);
const ids=a.map(n=>n.id);sync.reorderNotebooks(ids);return a.map((n,i)=>({...n,sortOrder:i}));});
setDragNbId(null);setDragOverNbId(null);};
const handleNbDragEnd=()=>{setDragNbId(null);setDragOverNbId(null);};
const handleNbContextMenu=(e,nb)=>{e.preventDefault();e.stopPropagation();
setCtxMenu({x:e.clientX,y:e.clientY,items:[{label:"Edit",icon:I.edit,action:()=>setEditingNb(nb)},{divider:true},{label:"Delete Notebook",icon:I.trash,danger:true,action:()=>requestDeleteNb(nb.id)}]});};
const handleNoteContextMenu=(e,note)=>{e.preventDefault();e.stopPropagation();
if(note.trashed){setCtxMenu({x:e.clientX,y:e.clientY,items:[{label:"Restore",icon:I.restore,action:()=>restoreNote(note.id)},{divider:true},{label:"Delete Permanently",icon:I.trash,danger:true,action:()=>permanentDeleteNote(note.id)}]});}
else{const subs=notebooks.filter(nb=>nb.id!==note.folderId).map(nb=>({label:nb.name,colorDot:COVERS[nb.cover%COVERS.length].bg,action:()=>moveNoteAction(note.id,nb.id)}));
setCtxMenu({x:e.clientX,y:e.clientY,items:[{label:note.starred?"Unstar":"Star",icon:note.starred?I.starFill:I.star,action:()=>toggleStar(note.id)},...(subs.length>0?[{label:"Move to...",icon:I.move,submenu:subs}]:[]),{divider:true},{label:"Move to Trash",icon:I.trash,danger:true,action:()=>trashNoteAction(note.id)}]});}
};
useEffect(()=>{const l=document.createElement("link");l.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";l.rel="stylesheet";document.head.appendChild(l);},[]);
// ── Periodic sync (every 30s if online) ──
useEffect(()=>{const i=setInterval(()=>{if(sync.isOnline())sync.fullSync().then(()=>reloadLocal()).catch(()=>{});},30000);return()=>clearInterval(i);},[]);
// ── History API — browser/phone back button ──
const navStateRef=useRef({folder:"dashboard",note:null,view:"list"});
useEffect(()=>{
const cur=navStateRef.current;
if(cur.folder!==activeFolder||cur.note!==activeNote||cur.view!==mobileView){
const state={folder:activeFolder,note:activeNote,view:mobileView};
try{window.history.pushState(state,"");}catch{}
navStateRef.current=state;
}
},[activeFolder,activeNote,mobileView]);
useEffect(()=>{
const handler=(e)=>{
const s=e.state;
if(s){setActiveFolder(s.folder||"dashboard");setActiveNote(s.note||null);if(isMobile)setMobileView(s.view||"list");}
else{if(activeNote){setActiveNote(null);if(isMobile)setMobileView("list");}else if(activeFolder!=="dashboard"){setActiveFolder("dashboard");setActiveNote(null);}}
};
window.addEventListener("popstate",handler);return()=>window.removeEventListener("popstate",handler);
},[activeNote,activeFolder,isMobile]);
if(loading)return<div style={{display:"flex",alignItems:"center",justifyContent:"center",height:"100vh",background:dark?"#0d1117":"#f6f8fa",color:dark?"#e6edf3":"#1f2328",fontFamily:"'DM Sans',sans-serif",fontSize:16}}>Loading...</div>;
if(!user)return<AuthScreen onLogin={onLogin} dark={dark}/>;
const isTrashView=activeFolder==="trash";
/* ── Back button for mobile ── */
const backBtn=(label,onClick)=><button onClick={onClick} style={{display:"flex",alignItems:"center",gap:6,padding:"6px 10px",borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:T.accent,fontSize:13,fontWeight:600}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><polyline points="15 18 9 12 15 6"/></svg>{label}
</button>;
/* ── Sidebar ── */
const renderSidebar=()=>(
<div style={{position:isMobile?"fixed":"relative",zIndex:isMobile?900:1,width:isMobile?(sidebarOpen?"100vw":0):(sidebarOpen?260:0),minWidth:isMobile?0:(sidebarOpen?260:0),height:isMobile?"100vh":"auto",background:T.sidebar,borderRight:isMobile?"none":`1px solid ${T.border}`,display:"flex",flexDirection:"column",overflow:"hidden",transition:"all .25s cubic-bezier(.4,0,.2,1)",fontFamily:"'DM Sans',sans-serif"}}>
{isMobile&&sidebarOpen&&<div onClick={()=>setSidebarOpen(false)} style={{position:"absolute",inset:0,background:"rgba(0,0,0,0.4)",zIndex:-1}}/>}
<div style={{width:isMobile?"85vw":260,maxWidth:320,height:"100%",background:T.sidebar,display:"flex",flexDirection:"column",position:"relative",zIndex:1}}>
<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",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}}>{dark?I.sun:I.moon}</button>
<button onClick={()=>setShowAccount(true)} title="Account" style={{display:"flex",alignItems:"center",justifyContent:"center",width:32,height:32,borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:T.muted}}>{I.user}</button>
</div>
</div>
<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}}>{I.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>
<div style={{padding:"0 10px"}}>
<button onClick={()=>{setActiveFolder("dashboard");setActiveNote(null);if(isMobile){setSidebarOpen(false);setMobileView("list");}}} style={{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"9px 12px",borderRadius:10,border:"none",cursor:"pointer",fontSize:13,fontWeight:600,textAlign:"left",background:activeFolder==="dashboard"?T.sidebarActive:"transparent",color:activeFolder==="dashboard"?T.accentText:T.textSec}}><span style={{fontSize:15}}>📚</span><span>Dashboard</span></button>
</div>
<div style={{padding:"6px 10px 0"}}>
{DEFAULT_FOLDERS.map(f=>(
<button key={f.id} onClick={()=>{setActiveFolder(f.id);setActiveNote(null);if(isMobile){setSidebarOpen(false);setMobileView("list");}}} style={{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"8px 12px",borderRadius:10,border:"none",cursor:"pointer",fontSize:13,fontWeight:500,textAlign:"left",background:activeFolder===f.id?T.sidebarActive:"transparent",color:activeFolder===f.id?T.accentText:T.textSec}}>
<span style={{fontSize:14}}>{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}}>{activeNotes.length}</span>}
{f.id==="trash"&&trashedNotes.length>0&&<span style={{marginLeft:"auto",fontSize:11,color:T.danger,background:T.danger+"15",padding:"2px 7px",borderRadius:10}}>{trashedNotes.length}</span>}
</button>
))}
</div>
<div style={{padding:"14px 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={()=>setShowNewNb(true)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:22,height:22,borderRadius:6,border:"none",background:T.accentSoft,cursor:"pointer",color:T.accent}}>{I.plus}</button>
</div>
{notebooks.map(nb=>(
<div key={nb.id} draggable={!isMobile} onDragStart={e=>handleNbDragStart(e,nb.id)} onDragOver={e=>handleNbDragOver(e,nb.id)} onDrop={e=>handleNbDrop(e,nb.id)} onDragEnd={handleNbDragEnd} onContextMenu={e=>handleNbContextMenu(e,nb)}
style={{display:"flex",alignItems:"center",borderRadius:10,transition:"all .15s",background:dragOverNbId===nb.id?T.accentSoft:activeFolder===nb.id?T.sidebarActive:"transparent",borderTop:dragOverNbId===nb.id?`2px solid ${T.accent}`:"2px solid transparent"}}>
{!isMobile&&<span style={{padding:"0 2px 0 6px",color:T.faint,cursor:"grab",display:"flex",alignItems:"center",opacity:0.5}}>{I.grip}</span>}
<button onClick={()=>{setActiveFolder(nb.id);setActiveNote(null);if(isMobile){setSidebarOpen(false);setMobileView("list");}}} style={{display:"flex",alignItems:"center",gap:10,flex:1,padding:"7px 10px 7px "+(isMobile?"10px":"4px"),borderRadius:10,border:"none",cursor:"pointer",fontSize:13,fontWeight:500,textAlign:"left",background:"transparent",color:activeFolder===nb.id?T.accentText:T.textSec}}>
<div style={{width:20,height:14,borderRadius:3,background:COVERS[nb.cover%COVERS.length].bg,flexShrink:0,boxShadow:"0 1px 3px rgba(0,0,0,0.2)"}}/>
<span>{nb.name}</span>
<span style={{marginLeft:"auto",fontSize:11,color:T.faint}}>{activeNotes.filter(n=>n.folderId===nb.id).length}</span>
</button>
</div>
))}
</div>
<div style={{marginTop:"auto",padding:"12px 14px",borderTop:`1px solid ${T.borderLight}`}}>
{installPrompt&&<button onClick={handleInstall} style={{display:"flex",alignItems:"center",gap:8,width:"100%",padding:"8px 12px",borderRadius:10,border:`1px solid ${T.accent}44`,background:T.accentSoft,cursor:"pointer",color:T.accent,fontSize:12,fontWeight:600,marginBottom:8}}>
<svg width="14" height="14" 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>Install App</button>}
<div style={{display:"flex",alignItems:"center",gap:6,color:T.muted,fontSize:12}}>
<span style={{color:syncStatus==="synced"?T.success:syncStatus==="syncing"?T.warning:syncStatus==="offline"?"#8b949e":syncStatus==="pending"?T.warning:T.danger}}>●</span>
<span>{syncStatus==="synced"?"All synced":syncStatus==="syncing"?"Syncing...":syncStatus==="offline"?"Offline mode":syncStatus==="pending"?"Changes pending":"Sync error"}</span>
{!sync.isOnline()&&<span style={{fontSize:10,padding:"2px 6px",borderRadius:6,background:T.faint+"22",color:T.faint,marginLeft:"auto"}}>offline</span>}
</div>
</div>
</div>
</div>
);
/* ── Dashboard ── */
const renderDashboard=()=>(
<div style={{flex:1,display:"flex",flexDirection:"column",background:T.dashBg,fontFamily:"'DM Sans',sans-serif",overflow:"auto"}}>
<div style={{padding:isMobile?"20px 20px 16px":"28px 36px 20px",display:"flex",alignItems:"center",justifyContent:"space-between"}}>
<div style={{display:"flex",alignItems:"center",gap:10}}>
<button onClick={()=>setSidebarOpen(!sidebarOpen)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:36,height:36,borderRadius:10,border:"none",background:"transparent",cursor:"pointer",color:T.muted}}>{I.menu}</button>
<h1 style={{margin:0,fontSize:isMobile?20:24,fontWeight:700,color:T.text,fontFamily:"'Newsreader',Georgia,serif"}}>My Notebooks</h1>
</div>
</div>
<div style={{padding:isMobile?"0 16px 24px":"0 36px 32px",display:"grid",gridTemplateColumns:isMobile?"repeat(auto-fill,minmax(140px,1fr))":"repeat(auto-fill,minmax(180px,1fr))",gap:isMobile?"20px 16px":"32px 28px",justifyItems:"center"}}>
{notebooks.map(nb=>(
<NotebookCover key={nb.id} notebook={nb} noteCount={activeNotes.filter(n=>n.folderId===nb.id).length}
onClick={()=>{setActiveFolder(nb.id);setActiveNote(null);}} onContextMenu={e=>handleNbContextMenu(e,nb)}
isActive={activeFolder===nb.id} isDragOver={dragOverNbId===nb.id} draggable={!isMobile}
onDragStart={e=>handleNbDragStart(e,nb.id)} onDragEnd={handleNbDragEnd} onDragOver={e=>handleNbDragOver(e,nb.id)} onDrop={e=>handleNbDrop(e,nb.id)}/>
))}
<button onClick={()=>setShowNewNb(true)} style={{display:"flex",flexDirection:"column",alignItems:"center",gap:10,background:"none",border:"none",cursor:"pointer"}}>
<div style={{width:isMobile?130:156,height:isMobile?175:210,borderRadius:12,border:`2px dashed ${T.border}`,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:8,color:T.faint}}>
<div style={{width:40,height:40,borderRadius:"50%",background:T.accentSoft,display:"flex",alignItems:"center",justifyContent:"center",color:T.accent}}>{I.plus}</div>
<span style={{fontSize:12,fontWeight:500}}>Add Notebook</span>
</div>
</button>
</div>
<div style={{padding:isMobile?"0 16px 24px":"0 36px 36px"}}>
<h2 style={{fontSize:16,fontWeight:600,color:T.text,marginBottom:14,fontFamily:"'Newsreader',Georgia,serif"}}>Recent Notes</h2>
<div style={{display:"grid",gridTemplateColumns:isMobile?"1fr":"repeat(auto-fill,minmax(260px,1fr))",gap:12}}>
{activeNotes.slice(0,6).map(note=>{const nb=notebooks.find(b=>b.id===note.folderId);return(
<button key={note.id} onClick={()=>{setActiveFolder(note.folderId);setActiveNote(note.id);if(isMobile)setMobileView("editor");}}
style={{display:"flex",flexDirection:"column",padding:"14px 16px",borderRadius:12,border:`1px solid ${T.border}`,background:T.card,cursor:"pointer",textAlign:"left"}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6,width:"100%"}}>
{nb&&<div style={{width:14,height:10,borderRadius:2,background:COVERS[nb.cover%COVERS.length].bg,flexShrink:0}}/>}
<span style={{fontSize:13,fontWeight:600,color:T.text,flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{note.title}</span>
{note.starred&&<span style={{color:"#f0b429",fontSize:11}}>⭐</span>}
</div>
<div style={{display:"flex",alignItems:"center",gap:6,marginTop:6}}>
<span style={{fontSize:11,color:T.faint}}>{fmtDate(note.updatedAt)}</span>
{nb&&<span style={{fontSize:10,color:T.faint,background:T.accentSoft,padding:"1px 6px",borderRadius:6}}>{nb.name}</span>}
</div>
</button>
);})}
</div>
</div>
</div>
);
/* ── Note List ── */
const renderNoteList=()=>{
const fname=(()=>{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";})();
// On mobile, hide list when editor is open
if(isMobile&&mobileView==="editor"&&activeNote)return null;
return(
<div style={{width:isMobile?"100%":300,minWidth:isMobile?0:300,flex:isMobile?1:undefined,borderRight:isMobile?"none":`1px solid ${T.border}`,display:"flex",flexDirection:"column",background:T.bg,fontFamily:"'DM Sans',sans-serif"}}>
<div style={{padding:"14px 16px 10px",display:"flex",alignItems:"center",justifyContent:"space-between",borderBottom:`1px solid ${T.borderLight}`}}>
<div style={{display:"flex",alignItems:"center",gap:8}}>
<button onClick={()=>setSidebarOpen(!sidebarOpen)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:30,height:30,borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:T.muted}}>{I.menu}</button>
<h2 style={{margin:0,fontSize:16,fontWeight:600,color:T.text,fontFamily:"'Newsreader',Georgia,serif"}}>{fname}</h2>
</div>
<div style={{display:"flex",gap:4}}>
{!isTrashView&&<button onClick={createNote} title="New Note" style={{display:"flex",alignItems:"center",justifyContent:"center",width:32,height:32,borderRadius:10,border:"none",background:T.accent,cursor:"pointer",color:"#fff",boxShadow:`0 2px 8px ${T.accent}44`}}>{I.plus}</button>}
{isTrashView&&trashedNotes.length>0&&<button onClick={emptyTrash} title="Empty Trash" style={{display:"flex",alignItems:"center",justifyContent:"center",padding:"6px 12px",borderRadius:10,border:"none",background:T.danger,cursor:"pointer",color:"#fff",fontSize:12,fontWeight:600}}>Empty</button>}
</div>
</div>
<div style={{flex:1,overflowY:"auto",padding:6,WebkitOverflowScrolling:"touch"}}>
{filteredNotes.length===0?(
<div style={{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",height:"100%",color:T.faint,gap:10,padding:30}}>
<div style={{fontSize:36}}>{isTrashView?"🗑️":"📝"}</div><div style={{fontSize:13,textAlign:"center"}}>{isTrashView?"Trash is empty":"No notes yet"}</div>
{!isTrashView&&<button onClick={createNote} style={{display:"flex",alignItems:"center",gap:6,padding:"10px 20px",borderRadius:12,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:600,marginTop:8}}>{I.plus} New Note</button>}
</div>
):filteredNotes.map(note=>(
<button key={note.id} onClick={()=>{setActiveNote(note.id);if(isMobile)setMobileView("editor");}} onContextMenu={e=>handleNoteContextMenu(e,note)}
style={{display:"flex",flexDirection:"column",width:"100%",padding:"12px 14px",borderRadius:10,border:`1px solid ${activeNote===note.id?T.accent+"55":"transparent"}`,background:activeNote===note.id?T.accentSoft:"transparent",cursor:"pointer",marginBottom:2,textAlign:"left"}}>
<div style={{display:"flex",alignItems:"center",gap:7,marginBottom:3,width:"100%"}}>
<span style={{fontSize:13}}>{note.trashed?"🗑️":"📝"}</span>
<span style={{fontSize:13,fontWeight:600,color:T.text,flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{note.title}</span>
{note.starred&&!note.trashed&&<span style={{color:"#f0b429",fontSize:11}}>⭐</span>}
</div>
<div style={{fontSize:10,color:T.faint,marginTop:3}}>{fmtDate(note.updatedAt)}</div>
</button>
))}
</div>
</div>
);
};
/* ── Editor ── */
const renderEditor=()=>{
// On mobile, hide editor when list is shown
if(isMobile&&mobileView!=="editor")return null;
if(!currentNoteSafe)return isMobile?null:(
<div style={{flex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",background:T.bg,color:T.faint,fontFamily:"'DM Sans',sans-serif",gap:14}}>
<div style={{width:72,height:72,borderRadius:20,background:T.accentSoft,display:"flex",alignItems:"center",justifyContent:"center",fontSize:32}}>✏️</div>
<div style={{fontSize:15,fontWeight:500,color:T.muted}}>Select a note to edit</div>
{!isTrashView&&<button onClick={createNote} style={{display:"flex",alignItems:"center",gap:8,padding:"10px 22px",borderRadius:12,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:13,fontWeight:600,marginTop:6}}>{I.plus} New Note</button>}
</div>
);
if(isTrashView&&currentNoteMeta)return(
<div style={{flex:1,display:"flex",flexDirection:"column",background:T.bg,fontFamily:"'DM Sans',sans-serif"}}>
<div style={{display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:`1px solid ${T.borderLight}`}}>
{isMobile&&backBtn("Back",()=>{setActiveNote(null);setMobileView("list");})}
<span style={{fontSize:17,fontWeight:600,color:T.text,fontFamily:"'Newsreader',Georgia,serif",flex:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{currentNoteMeta.title}</span>
<button onClick={()=>restoreNote(currentNoteMeta.id)} style={{padding:"6px 14px",borderRadius:8,border:"none",background:T.accent,color:"#fff",cursor:"pointer",fontSize:12,fontWeight:600}}>Restore</button>
<button onClick={()=>permanentDeleteNote(currentNoteMeta.id)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:30,height:30,borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:T.danger}}>{I.trash}</button>
</div>
<div style={{flex:1,padding:"20px 24px",color:T.muted,fontStyle:"italic",fontSize:14}}>Trashed notes are read-only. Restore to edit.</div>
</div>
);
return(
<div style={{flex:1,display:"flex",flexDirection:"column",background:T.bg,fontFamily:"'DM Sans',sans-serif"}}>
<div style={{display:"flex",alignItems:"center",gap:8,padding:"8px 16px",borderBottom:`1px solid ${T.borderLight}`}}>
{isMobile&&backBtn("Back",()=>{setActiveNote(null);setMobileView("list");})}
<input type="text" value={currentNoteSafe.title} onChange={e=>updateNote(currentNoteSafe.id,{title:e.target.value})}
style={{flex:1,border:"none",background:"transparent",outline:"none",fontSize:isMobile?15:17,fontWeight:600,color:T.text,fontFamily:"'Newsreader',Georgia,serif",minWidth:0}}/>
<button onClick={()=>toggleStar(currentNoteSafe.id)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:30,height:30,borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:currentNoteSafe.starred?"#f0b429":T.faint}}>{currentNoteSafe.starred?I.starFill:I.star}</button>
<button onClick={()=>trashNoteAction(currentNoteSafe.id)} style={{display:"flex",alignItems:"center",justifyContent:"center",width:30,height:30,borderRadius:8,border:"none",background:"transparent",cursor:"pointer",color:T.danger}}>{I.trash}</button>
</div>
<div style={{flex:1,overflow:"hidden"}}>
<NoteEditor key={currentNoteSafe.id} note={currentNoteSafe} onUpdate={u=>updateNote(currentNoteSafe.id,u)} dark={dark}/>
</div>
</div>
);
};
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:5px}::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:${T.borderLight};border-radius:3px}
button:hover{opacity:0.88}
input::placeholder{color:${T.faint}}
[contenteditable]:empty:before{content:'Start writing...';color:${T.faint};pointer-events:none}
[draggable]{user-select:none}
@media(max-width:767px){
::-webkit-scrollbar{display:none}
body{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}
}
`}</style>
{renderSidebar()}
{activeFolder==="dashboard"?renderDashboard():(<>{renderNoteList()}{renderEditor()}</>)}
{showNewNb&&<CreateNotebookModal onClose={()=>setShowNewNb(false)} onCreate={createNb} dark={dark}/>}
{editingNb&&<EditNotebookModal notebook={editingNb} onClose={()=>setEditingNb(null)} onSave={u=>updateNb(editingNb.id,u)} dark={dark}/>}
{showAccount&&<AccountMenu user={user} onClose={()=>setShowAccount(false)} onLogout={()=>{setShowAccount(false);onLogout();}} onUserUpdate={u=>setUser(u)} dark={dark}/>}
{confirmDialog&&<ConfirmDialog title={confirmDialog.title} message={confirmDialog.message} confirmLabel={confirmDialog.confirmLabel} onConfirm={confirmDialog.onConfirm} onCancel={()=>setConfirmDialog(null)} dark={dark}/>}
{ctxMenu&&<ContextMenu x={ctxMenu.x} y={ctxMenu.y} items={ctxMenu.items} onClose={()=>setCtxMenu(null)} dark={dark}/>}
</div>
);
}