Files
dashmail/DashMailClient_3.jsx
T
Devon dc096207ae Add notification sound picker with live preview
Notification sound toggle now reveals a Sound sub-row with a dropdown
(Chime, Bell, Ping, Ding, Pop, Classic, Subtle) and a Preview button
that plays the selected tone via Web Audio API — no audio files needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:29:23 -04:00

4288 lines
264 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.
const { useState, useRef, useEffect } = React;
const ACCENTS=["#0078d4","#0063b1","#744da9","#e3008c","#c50f1f","#ca5010","#e87722","#b5911b","#498205","#107c10","#00b7c3","#4e6b8c"];
const FONT="'Segoe UI Variable','Segoe UI',system-ui,sans-serif";
const DEFAULT_LOGIN_BG="./login-bg-default.jpg";
function mkTheme(dark,accent){
return{accent,accentHov:blend(accent,dark?"#ffffff":"#000000",0.15),accentBg:accent+"18",accentBorder:accent+"40",
bg:dark?"#1f1f1f":"#f3f3f3",sidebar:dark?"#181818":"#f0f0f0",surface:dark?"#2a2a2a":"#ffffff",
surface2:dark?"#222222":"#fafafa",surface3:dark?"#323232":"#f5f5f5",
border:dark?"rgba(255,255,255,0.08)":"rgba(0,0,0,0.08)",text:dark?"#f0f0f0":"#1c1c1c",
muted:dark?"#a8a8a8":"#5f5f5f",subtle:dark?"#606060":"#9e9e9e",hover:dark?"rgba(255,255,255,0.06)":"rgba(0,0,0,0.05)",
inputBg:dark?"#333":"#fafafa",inBorder:dark?"rgba(255,255,255,0.14)":"rgba(0,0,0,0.12)",
titleBar:"#1a1a1a",danger:dark?"#ff6b6b":"#c50f1f",selectedBg:accent+"18",selectedLeft:accent+"80"};
}
function blend(hex,w,t){
const p=x=>parseInt(x,16);
const r=[p(hex.slice(1,3)),p(hex.slice(3,5)),p(hex.slice(5,7))];
const ww=[p(w.slice(1,3)),p(w.slice(3,5)),p(w.slice(5,7))];
return "#"+r.map((v,i)=>Math.round(v+(ww[i]-v)*t).toString(16).padStart(2,"0")).join("");
}
// ── Background catalogue ──────────────────────────────────────────────────────
const BG_SOLID=[
{id:"white", label:"White", preview:"#ffffff",dark:false},
{id:"smoke", label:"Smoke Gray", preview:"#e4e4e4",dark:false},
{id:"solarized",label:"Solarized", preview:"#fdf6e3",dark:false},
{id:"dark", label:"Dark", preview:"#1a1a1a",dark:true},
{id:"navy", label:"Deep Navy", preview:"#0d2040",dark:true},
{id:"charcoal", label:"Charcoal", preview:"#2b2b2b",dark:true},
];
const BG_IMAGES=[
{id:"mountains",label:"Mountains",swatch:"linear-gradient(180deg,#1b3a5e 0%,#4a7fb5 35%,#7aad6a 65%,#8b7355 100%)",app:"linear-gradient(150deg,#1b3a5e 0%,#4a7fb5 45%,#7aad6a 80%,#8b7355 100%)"},
{id:"beach", label:"Beach", swatch:"linear-gradient(180deg,#87ceeb 0%,#b8e0f0 40%,#f5dfa8 70%,#c8a04a 100%)",app:"linear-gradient(150deg,#87ceeb 0%,#b8e0f0 40%,#f5dfa8 75%,#c8a04a 100%)"},
{id:"ocean", label:"Ocean", swatch:"linear-gradient(180deg,#0d47a1 0%,#1976d2 40%,#42a5f5 75%,#80deea 100%)",app:"linear-gradient(150deg,#0d47a1 0%,#1976d2 40%,#42a5f5 80%,#80deea 100%)"},
{id:"landscape",label:"Landscape",swatch:"linear-gradient(180deg,#1a4a1a 0%,#2e7d32 35%,#7cb342 65%,#c8e6c9 100%)",app:"linear-gradient(150deg,#1a4a1a 0%,#2e7d32 40%,#7cb342 75%,#c8e6c9 100%)"},
{id:"flowers", label:"Flowers", swatch:"linear-gradient(180deg,#880e4f 0%,#e91e63 35%,#f48fb1 65%,#fce4ec 100%)",app:"linear-gradient(150deg,#880e4f 0%,#c2185b 40%,#f48fb1 75%,#fce4ec 100%)"},
{id:"city", label:"City", swatch:"linear-gradient(180deg,#0a0a1a 0%,#1a1a3e 35%,#2d2d6e 60%,#e94560 100%)",app:"linear-gradient(150deg,#0a0a1a 0%,#1a1a3e 40%,#2d2d6e 75%,#e94560 100%)"},
{id:"sunset", label:"Sunset", swatch:"linear-gradient(180deg,#1a0533 0%,#7b1fa2 30%,#e65100 65%,#ff8f00 100%)",app:"linear-gradient(150deg,#1a0533 0%,#7b1fa2 35%,#e65100 70%,#ff8f00 100%)"},
{id:"aurora", label:"Aurora", swatch:"linear-gradient(180deg,#0d1b2a 0%,#1a5276 35%,#1abc9c 65%,#a9f0d1 100%)",app:"linear-gradient(150deg,#0d1b2a 0%,#1a5276 40%,#1abc9c 70%,#a9f0d1 100%)"},
{id:"desert", label:"Desert", swatch:"linear-gradient(180deg,#1a0a00 0%,#b35900 35%,#e8a020 65%,#f5d080 100%)",app:"linear-gradient(150deg,#1a0a00 0%,#b35900 40%,#e8a020 70%,#f5d080 100%)"},
{id:"arctic", label:"Arctic", swatch:"linear-gradient(180deg,#e8f4ff 0%,#b8d8f0 35%,#d0eaff 65%,#f0f8ff 100%)",app:"linear-gradient(150deg,#dceefb 0%,#b8d8f0 40%,#d0eaff 75%,#f0f8ff 100%)"},
];
function getBgStyle(bgId){
if(!bgId||bgId==="white")return{};
if(typeof bgId==="string"&&bgId.startsWith("#"))return{background:bgId};
const solid=BG_SOLID.find(b=>b.id===bgId);
if(solid)return{background:solid.preview};
const img=BG_IMAGES.find(b=>b.id===bgId);
if(img)return{background:img.app};
return{};
}
function bgIsImage(bgId){return BG_IMAGES.some(b=>b.id===bgId);}
function bgIsDark(bgId){
if(typeof bgId==="string"&&/^#[0-9a-f]{6}$/i.test(bgId)){
const r=parseInt(bgId.slice(1,3),16),g=parseInt(bgId.slice(3,5),16),b=parseInt(bgId.slice(5,7),16);
return (r*0.299+g*0.587+b*0.114)<128;
}
const solid=BG_SOLID.find(b=>b.id===bgId);
if(solid)return solid.dark;
const imgDark=["city","sunset","aurora","ocean","mountains"];
return imgDark.includes(bgId);
}
// ── Profile system ────────────────────────────────────────────────────────────
const INIT_PROFILES=[
{id:"p1",name:"Alex Johnson",initials:"AJ",color:"#744da9",password:"demo",
accountIds:["alex@example-corp.com","default"],
settings:{accent:"#0078d4",isDark:false,bg:"white",notifications:true,sound:false,readReceipts:false,autoMark:true}},
{id:"p2",name:"Sales Team",initials:"ST",color:"#107c10",password:"sales",
accountIds:["sales@example-corp.com"],
settings:{accent:"#107c10",isDark:false,bg:"white",notifications:true,sound:true,readReceipts:true,autoMark:true}},
{id:"p3",name:"J. Doe Personal",initials:"JD",color:"#ca5010",password:"demo3",
accountIds:["j.doe@mailbox.org"],
settings:{accent:"#ca5010",isDark:true,bg:"dark",notifications:false,sound:false,readReceipts:false,autoMark:false}},
];
// ── Accounts + folders ────────────────────────────────────────────────────────
const ALL_ACCOUNTS=[
{id:"default", display:"Default Account", email:null, av:"DA",avColor:"#0078d4"},
{id:"alex@example-corp.com", display:"Alex", email:"alex@example-corp.com", av:"AJ",avColor:"#744da9"},
{id:"sales@example-corp.com", display:"Sales", email:"sales@example-corp.com",av:"SA",avColor:"#107c10"},
{id:"j.doe@mailbox.org", display:"J. Doe", email:"j.doe@mailbox.org", av:"JD",avColor:"#ca5010"},
];
const STD_FOLDERS=[
{id:"inbox", label:"Inbox", icon:"ti-inbox", badge:true},
{id:"sent", label:"Sent", icon:"ti-send", badge:false},
{id:"drafts", label:"Drafts", icon:"ti-file-text", badge:true},
{id:"out", label:"Outbox", icon:"ti-upload", badge:false},
{id:"archive", label:"Archive", icon:"ti-archive", badge:false},
{id:"templates",label:"Templates",icon:"ti-layout-grid", badge:false},
{id:"trash", label:"Trash", icon:"ti-trash", badge:false},
];
// ── Seed emails ───────────────────────────────────────────────────────────────
const SEED=[
{id:1,account:"alex@example-corp.com",folder:"inbox",from:"Sarah Chen",from_email:"sarah@acme.com",to:"alex@example-corp.com",subject:"Q3 Budget Review — Action Required",preview:"Hi team, please review the attached budget spreadsheet before Friday's meeting...",body:"Hi Alex,\n\nPlease review the attached budget spreadsheet before Friday's meeting. We need everyone's input on proposed changes.\n\nKey items:\n • Marketing budget +15%\n • IT infrastructure refresh ($240K)\n • Travel policy revisions\n\nReturn by EOD Thursday.\n\nBest,\nSarah",date:"2:34 PM",read:false,starred:true,atts:["Q3_Budget_2024.xlsx","Policy_Updates.pdf"],av:"SC",avColor:"#0078d4"},
{id:2,account:"alex@example-corp.com",folder:"inbox",from:"James Holloway",from_email:"james@dev.io",to:"alex@example-corp.com",subject:"Re: Deployment pipeline — staging env down",preview:"The issue was traced to a misconfigured Nginx proxy rule...",body:"Hey Alex,\n\nIssue traced to a misconfigured Nginx proxy rule added during last night's security patch. Fix pushed — health check should go green in ~10 min.\n\nJames",date:"11:20 AM",read:false,starred:false,atts:[],av:"JH",avColor:"#744da9"},
{id:3,account:"alex@example-corp.com",folder:"inbox",from:"Priya Nair",from_email:"priya@design.studio",to:"alex@example-corp.com",subject:"Brand refresh assets — final delivery",preview:"Attached are the final approved assets for the brand refresh...",body:"Hi Alex,\n\nAttached are the final approved assets. SVG, PNG @1x/2x, and Figma source.\n\nPriya",date:"Yesterday",read:true,starred:true,atts:["Brand_Assets_Final.zip"],av:"PN",avColor:"#e87722"},
{id:4,account:"alex@example-corp.com",folder:"sent",from:"Alex",from_email:"alex@example-corp.com",to:"sarah@acme.com",subject:"Re: Q3 Budget Review — Action Required",preview:"Hi Sarah, I've reviewed the spreadsheet...",body:"Hi Sarah,\n\nReviewed the spreadsheet. Aligned on the marketing increase — would like to discuss travel policy changes.\n\nAlex",date:"3:10 PM",read:true,starred:false,atts:[],av:"AJ",avColor:"#744da9"},
{id:5,account:"sales@example-corp.com",folder:"inbox",from:"Mark Okafor",from_email:"mark@partners.net",to:"sales@example-corp.com",subject:"Partnership proposal — follow-up",preview:"Thanks for the meeting yesterday. Revised proposal attached...",body:"Hi,\n\nRevised proposal attached. 90-day pilot, 30% lower upfront, dedicated account manager.\n\nMark",date:"Yesterday",read:false,starred:false,atts:["Partnership_Proposal_v3.pptx"],av:"MO",avColor:"#00b7c3"},
{id:6,account:"sales@example-corp.com",folder:"inbox",from:"Lena Fischer",from_email:"lena@gmbh.de",to:"sales@example-corp.com",subject:"Order #4821 — shipping inquiry",preview:"Good morning, I wanted to follow up on my order placed two weeks ago...",body:"Good morning,\n\nFollowing up on order #4821 placed two weeks ago. No shipping confirmation received.\n\nLena Fischer",date:"10:05 AM",read:false,starred:false,atts:[],av:"LF",avColor:"#0063b1"},
{id:7,account:"j.doe@mailbox.org",folder:"inbox",from:"Tech Digest",from_email:"news@techdigest.io",to:"j.doe@mailbox.org",subject:"This week in open source",preview:"Mutt 2.3.2 released, Neovim 0.11 ships treesitter overhaul...",body:"This Week in Open Source\n─────────────────────────\n• Mutt 2.3.2 released\n• Neovim 0.11 treesitter overhaul\n• Linux 6.9 released",date:"9:00 AM",read:true,starred:false,atts:[],av:"TD",avColor:"#498205"},
{id:8,account:"default",folder:"inbox",from:"Mutt Daemon",from_email:"mutt@localhost",to:"default",subject:"System notification — 4 new messages",preview:"Your mailbox has received 4 new messages...",body:"Mutt Mail Notification\n──────────────────────\nMailbox: .Mails/default/inbox\nNew msgs: 4\nSMB: \\\\mailserver\\mail (connected)",date:"10:55 AM",read:true,starred:false,atts:[],av:"MD",avColor:"#107c10"},
];
const INIT_RULES=[
{id:"r1",name:"Newsletter auto-archive",enabled:true,condLogic:"any",conditions:[{id:"c1",field:"subject",op:"contains",value:"unsubscribe"},{id:"c2",field:"from",op:"contains",value:"newsletter"}],actions:[{id:"a1",type:"move",folder:"archive"},{id:"a2",type:"markRead"}]},
{id:"r2",name:"Flag partnership emails",enabled:true,condLogic:"any",conditions:[{id:"c3",field:"subject",op:"contains",value:"partnership"}],actions:[{id:"a3",type:"star"}]},
];
const INIT_CONTACTS=[
{id:"ct1",name:"Sarah Chen", email:"sarah@acme.com", av:"SC",avColor:"#0078d4"},
{id:"ct2",name:"James Holloway", email:"james@dev.io", av:"JH",avColor:"#744da9"},
{id:"ct3",name:"Priya Nair", email:"priya@design.studio", av:"PN",avColor:"#e87722"},
{id:"ct4",name:"Mark Okafor", email:"mark@partners.net", av:"MO",avColor:"#00b7c3"},
{id:"ct5",name:"Lena Fischer", email:"lena@gmbh.de", av:"LF",avColor:"#0063b1"},
{id:"ct6",name:"Tech Digest", email:"news@techdigest.io", av:"TD",avColor:"#498205"},
];
// ── Utilities ─────────────────────────────────────────────────────────────────
const quoteBody=(text,from)=>`\n\n— On ${new Date().toDateString()}, ${from} wrote:\n${(text??"").split("\n").map(l=>`> ${l}`).join("\n")}`;
const fwdBody=e=>`\n\n---------- Forwarded message ----------\nFrom: ${e.from}\nDate: ${e.date}\nSubject: ${e.subject}\nTo: ${e.to}\n\n${e.body??""}`;
const formatSize=b=>b<1024?b+" B":b<1048576?(b/1024).toFixed(1)+" KB":(b/1048576).toFixed(1)+" MB";
function getAttIcon(name){
const ext=(name.split(".").pop()||"").toLowerCase();
if(["jpg","jpeg","png","gif","svg","webp"].includes(ext))return "ti-photo";
if(ext==="pdf")return "ti-file-type-pdf";
if(["doc","docx"].includes(ext))return "ti-file-type-doc";
if(["xls","xlsx"].includes(ext))return "ti-file-type-xls";
if(["ppt","pptx"].includes(ext))return "ti-file-type-ppt";
if(["zip","gz","tar","7z"].includes(ext))return "ti-file-zip";
if(["eml","msg"].includes(ext))return "ti-mail";
if(["txt","md"].includes(ext))return "ti-file-text";
return "ti-file";
}
function printAttachment(name,srcEmail){
const ext=(name.split(".").pop()||"").toLowerCase();
let html;
if(ext==="eml"&&srcEmail){
html=`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${name}</title>
<style>body{font-family:Arial,sans-serif;padding:24px;max-width:700px;margin:0 auto}
.header{border-bottom:1px solid #ccc;padding-bottom:12px;margin-bottom:20px}
.label{color:#666;font-size:12px;text-transform:uppercase;letter-spacing:.05em}
.value{font-size:14px;margin-bottom:8px}pre{white-space:pre-wrap;font-family:inherit;font-size:14px;line-height:1.6}</style>
</head><body>
<div class="header">
<div class="label">From</div><div class="value">${srcEmail.from||""} &lt;${srcEmail.from_email||""}&gt;</div>
<div class="label">To</div><div class="value">${srcEmail.to||""}</div>
<div class="label">Subject</div><div class="value"><strong>${srcEmail.subject||""}</strong></div>
<div class="label">Date</div><div class="value">${srcEmail.date||""}</div>
</div>
<pre>${(srcEmail.body||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</pre>
</body></html>`;
} else {
html=`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${name}</title>
<style>body{font-family:Arial,sans-serif;padding:40px;text-align:center;color:#444}
h2{margin-bottom:12px}.note{font-size:13px;color:#888;margin-top:20px}</style>
</head><body>
<h2>📎 ${name}</h2>
<p class="note">Attachment: ${name}</p>
<p class="note">From email: ${srcEmail?.subject||"(no subject)"}</p>
<p class="note">Date: ${new Date().toLocaleString()}</p>
<p class="note">This is a demo attachment print preview via DashMail.</p>
</body></html>`;
}
const w=window.open("","_blank","width=800,height=600");
if(!w)return;
w.document.write(html);
w.document.close();
w.focus();
setTimeout(()=>{w.print();},400);
}
function downloadAttachment(name,srcEmail){
const ext=(name.split(".").pop()||"").toLowerCase();
let content,mime;
if(ext==="eml"&&srcEmail){
content=[
`From: ${srcEmail.from||""} <${srcEmail.from_email||""}>`,
`To: ${srcEmail.to||""}`,
`Subject: ${srcEmail.subject||""}`,
`Date: ${srcEmail.date||new Date().toUTCString()}`,
`MIME-Version: 1.0`,
`Content-Type: text/plain; charset=UTF-8`,
``,
srcEmail.body||""
].join("\r\n");
mime="message/rfc822";
} else {
content=`Demo attachment: ${name}\nGenerated by DashMail\nDate: ${new Date().toLocaleString()}\n\nThis is a placeholder file. In a live deployment this file would be fetched from the SMB share.`;
mime="text/plain";
}
const blob=new Blob([content],{type:mime});
const url=URL.createObjectURL(blob);
const a=document.createElement("a");
a.href=url; a.download=name; a.click();
setTimeout(()=>URL.revokeObjectURL(url),5000);
}
const uid=()=>Math.random().toString(36).slice(2,9);
function printEmail(email){
const w=window.open("","_blank","width=720,height=960");
w.document.write(`<html><head><title>${email.subject}</title><style>body{font-family:'Segoe UI',sans-serif;max-width:700px;margin:40px auto}h2{margin:0 0 12px}.hdr{border-bottom:1px solid #ddd;padding-bottom:14px;margin-bottom:18px}.row{margin:3px 0;font-size:13.5px}.lbl{color:#666;font-weight:600;display:inline-block;width:60px}.body{font-size:14px;line-height:1.75;white-space:pre-wrap}@media print{body{margin:20px}}</style></head><body><div class="hdr"><h2>${email.subject}</h2><div class="row"><span class="lbl">From:</span>${email.from} &lt;${email.from_email}&gt;</div><div class="row"><span class="lbl">To:</span>${email.to}</div><div class="row"><span class="lbl">Date:</span>${email.date}</div>${email.atts?.length?`<div style="margin-top:12px;font-size:13px;color:#666">Attachments: ${email.atts.join(", ")}</div>`:""}</div><div class="body">${(email.body??"").replace(/</g,"&lt;")}</div></body></html>`);
w.document.close();w.focus();setTimeout(()=>w.print(),400);
}
// ── Base components ───────────────────────────────────────────────────────────
function Avatar({initials,color,size=36}){
return <div style={{width:size,height:size,borderRadius:"50%",background:color+"22",border:`1.5px solid ${color}44`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:size*0.33,fontWeight:700,color,fontFamily:FONT,flexShrink:0,userSelect:"none"}}>{initials}</div>;
}
function IBtn({icon,title,onClick,C,danger=false,active=false,size=16,label=""}){
const[h,setH]=useState(false);
return <button title={title} onClick={onClick} onMouseEnter={()=>setH(true)} onMouseLeave={()=>setH(false)}
style={{background:active?C.accentBg:h?C.hover:"transparent",border:"none",borderRadius:5,padding:"6px 8px",cursor:"pointer",
color:danger&&h?C.danger:active?C.accent:C.muted,display:"flex",alignItems:"center",gap:4,transition:"all 0.1s",fontFamily:FONT,fontSize:13}}>
<i className={`ti ${icon}`} style={{fontSize:size}} aria-hidden="true"/>
{label&&<span style={{fontSize:12.5}}>{label}</span>}
</button>;
}
function Sep({C}){return <div style={{width:1,height:18,background:C.border,margin:"0 2px",flexShrink:0}}/>;}
function Toggle({on,onChange,C}){
const[v,setV]=useState(on??false);
return <div onClick={()=>{setV(x=>!x);onChange&&onChange(!v);}}
style={{width:36,height:20,borderRadius:10,background:v?C.accent:C.muted,cursor:"pointer",position:"relative",transition:"background 0.2s",flexShrink:0}}>
<div style={{position:"absolute",top:2,left:v?18:2,width:16,height:16,borderRadius:"50%",background:"#fff",transition:"left 0.2s",boxShadow:"0 1px 3px rgba(0,0,0,0.3)"}}/>
</div>;
}
function PanelHeader({title,onClose,C}){
return <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 18px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<span style={{fontSize:16,fontWeight:600,color:C.text}}>{title}</span>
<IBtn icon="ti-x" title="Close" C={C} onClick={onClose} size={18}/>
</div>;
}
// ── Full Settings Modal ───────────────────────────────────────────────────────
const SETTINGS_NAV=[
{id:"general", icon:"ti-settings", label:"General"},
{id:"theme", icon:"ti-palette", label:"Appearance"},
{id:"notifications",icon:"ti-bell", label:"Notifications"},
{id:"accounts", icon:"ti-user-circle", label:"Accounts"},
{id:"rules", icon:"ti-filter", label:"Filters & Rules"},
{id:"privacy", icon:"ti-eye", label:"Privacy"},
{id:"emailaddr", icon:"ti-at", label:"Email addresses"},
{id:"encryption", icon:"ti-key", label:"Encryption"},
{id:"spam", icon:"ti-trash", label:"Spam & Trash"},
{id:"backup", icon:"ti-database", label:"Backup"},
{id:"advanced", icon:"ti-adjustments", label:"Advanced",keywords:"security smb share server"},
];
// ── Email Addresses Settings ──────────────────────────────────────────────────
const ACCT_STATS={
"alex@example-corp.com":{count:1842,size:"312 MB"},
"sales@example-corp.com":{count:234,size:"41 MB"},
"j.doe@mailbox.org":{count:89,size:"14 MB"},
"default":{count:12,size:"2 MB"},
};
// Mock: extra folder on the share not yet imported
const MOCK_NEW_FOLDERS=["invoices@example-corp.com"];
function SettingsEmailAddr({accounts,C,smbConfig,onDiscoverMore}){
const[scanning,setScanning]=useState(false);
const[scanResult,setScanResult]=useState(null);
function runScan(){
setScanning(true);setScanResult(null);
setTimeout(()=>{
setScanning(false);
const known=new Set(accounts.map(a=>a.email||a.id));
const newOnes=MOCK_NEW_FOLDERS.filter(e=>!known.has(e));
setScanResult({found:newOnes.length,folders:newOnes});
},2000);
}
return <div style={{padding:"22px 28px"}}>
<h2 style={{fontSize:20,fontWeight:700,color:C.text,margin:"0 0 6px"}}>Email addresses</h2>
<p style={{fontSize:13,color:C.muted,margin:"0 0 20px",lineHeight:1.5}}>
Folders imported from your mail server. The SMB share is never modified.
</p>
{/* Imported accounts */}
<div style={{display:"flex",flexDirection:"column",gap:8,marginBottom:24}}>
{accounts.map(acct=>{
const stats=ACCT_STATS[acct.email||acct.id]||{count:0,size:"—"};
return <div key={acct.id} style={{display:"flex",alignItems:"center",gap:12,
padding:"12px 14px",background:C.surface2,borderRadius:10,border:`1px solid ${C.border}`}}>
<div style={{width:38,height:38,borderRadius:9,background:acct.avColor||C.accent,
display:"flex",alignItems:"center",justifyContent:"center",
fontSize:13,fontWeight:700,color:"#fff",flexShrink:0,letterSpacing:"0.3px"}}>
{acct.av}
</div>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:13.5,fontWeight:600,color:C.text,marginBottom:2,
overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
{acct.email||acct.id}
</div>
<div style={{fontSize:12,color:C.muted}}>
{acct.display&&acct.display!==(acct.email||acct.id)&&
<span style={{marginRight:8,opacity:0.8}}>{acct.display}</span>}
{stats.count.toLocaleString()} emails &bull; {stats.size}
</div>
</div>
<div style={{display:"flex",alignItems:"center",gap:5,padding:"3px 10px",
borderRadius:20,background:"rgba(34,197,94,0.1)",border:"1px solid rgba(34,197,94,0.22)",
fontSize:11.5,fontWeight:600,color:"#16a34a",flexShrink:0}}>
<div style={{width:6,height:6,borderRadius:"50%",background:"#22c55e"}}/>
Active
</div>
</div>;
})}
</div>
{/* Discover more */}
<div style={{borderTop:`1px solid ${C.border}`,paddingTop:18}}>
<div style={{fontSize:13,fontWeight:600,color:C.text,marginBottom:3}}>Discover more accounts</div>
<div style={{fontSize:12.5,color:C.muted,marginBottom:12,lineHeight:1.5}}>
Scan your mail server for new folders added since your last import.
</div>
{!scanResult&&<button onClick={runScan} disabled={scanning}
style={{display:"flex",alignItems:"center",gap:8,padding:"9px 16px",
border:`1px solid ${C.inBorder}`,borderRadius:8,background:"transparent",
color:scanning?C.muted:C.text,fontSize:13,fontWeight:500,
cursor:scanning?"default":"pointer",fontFamily:FONT,opacity:scanning?0.7:1}}>
{scanning
?<><i className="ti ti-loader-2 ti-spin" style={{fontSize:14}} aria-hidden="true"/>Scanning…</>
:<><i className="ti ti-radar" style={{fontSize:14}} aria-hidden="true"/>Scan for new accounts</>}
</button>}
{scanResult&&scanResult.found>0&&<div style={{padding:"12px 14px",borderRadius:9,
background:"rgba(34,197,94,0.07)",border:"1px solid rgba(34,197,94,0.2)"}}>
<div style={{display:"flex",alignItems:"flex-start",gap:10}}>
<i className="ti ti-folder-plus" style={{fontSize:18,color:"#22c55e",marginTop:1,flexShrink:0}} aria-hidden="true"/>
<div style={{flex:1}}>
<div style={{fontSize:13.5,fontWeight:600,color:C.text,marginBottom:3}}>
{scanResult.found} new folder{scanResult.found!==1?"s":""} found
</div>
<div style={{fontSize:12,color:C.muted,marginBottom:10}}>
{scanResult.folders.join(", ")}
</div>
<button onClick={onDiscoverMore}
style={{display:"inline-flex",alignItems:"center",gap:7,padding:"8px 16px",
border:"none",borderRadius:7,background:"#22c55e",
color:"#fff",fontSize:13,fontWeight:600,cursor:"pointer",fontFamily:FONT}}>
Set up now <i className="ti ti-arrow-right" style={{fontSize:13}} aria-hidden="true"/>
</button>
</div>
</div>
</div>}
{scanResult&&scanResult.found===0&&<div style={{display:"flex",alignItems:"center",gap:8,
padding:"11px 14px",borderRadius:9,background:C.surface2,border:`1px solid ${C.border}`,
fontSize:13,color:C.muted}}>
<i className="ti ti-circle-check" style={{fontSize:15,color:"#22c55e"}} aria-hidden="true"/>
No new folders found. Your account list is up to date.
<button onClick={()=>setScanResult(null)}
style={{marginLeft:"auto",background:"transparent",border:"none",
color:C.muted,cursor:"pointer",fontSize:12,fontFamily:FONT,padding:"2px 6px"}}>
Dismiss
</button>
</div>}
</div>
</div>;
}
function FullSettings({profile,onSave,onClose,C,accounts,updateAccount,rules,setRules,allFolders,smbConfig,setSmbConfig,loginBg,setLoginBg,loginDark,setLoginDark,onSetupNew}){
const[section,setSection]=useState("general");
const[search,setSearch]=useState("");
const[ls,setLS]=useState({...profile.settings});
const[profName,setProfName]=useState(profile.name);
const[profInitials,setProfInitials]=useState(profile.initials);
const[profColor,setProfColor]=useState(profile.color);
const[advancedTab,setAdvancedTab]=useState("security");
const filtered=search
?SETTINGS_NAV.filter(n=>
n.label.toLowerCase().includes(search.toLowerCase())||
(n.keywords&&n.keywords.toLowerCase().includes(search.toLowerCase())))
:SETTINGS_NAV;
function save(){onSave({...profile,name:profName,initials:profInitials,color:profColor,settings:{...ls}});}
// Derive a local theme from ls (for live preview inside settings)
const lC=mkTheme(ls.isDark,ls.accent);
return <div style={{position:"fixed",inset:0,zIndex:300,background:"rgba(0,0,0,0.6)",display:"flex",alignItems:"center",justifyContent:"center",fontFamily:FONT}}>
<div style={{width:"min(900px,95vw)",height:"min(640px,92vh)",background:C.surface,borderRadius:12,
border:`1px solid ${C.border}`,boxShadow:`0 20px 60px rgba(0,0,0,${C.surface==="#ffffff"?0.25:0.7})`,
display:"flex",flexDirection:"column",overflow:"hidden"}}>
{/* Header */}
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"16px 20px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<span style={{fontSize:20,fontWeight:700,color:C.text}}>Settings</span>
<div style={{display:"flex",gap:4}}>
<IBtn icon="ti-help" title="Help" C={C} size={18}/>
<IBtn icon="ti-x" title="Close" C={C} onClick={onClose} size={18}/>
</div>
</div>
<div style={{display:"flex",flex:1,minHeight:0}}>
{/* Left nav */}
<div style={{width:220,background:C.sidebar,borderRight:`1px solid ${C.border}`,display:"flex",flexDirection:"column",flexShrink:0}}>
<div style={{padding:"10px 10px 6px"}}>
<div style={{position:"relative"}}>
<i className="ti ti-search" style={{position:"absolute",left:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:C.subtle}} aria-hidden="true"/>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search"
style={{width:"100%",boxSizing:"border-box",padding:"7px 9px 7px 28px",border:`1px solid ${C.inBorder}`,
borderRadius:6,fontSize:13,fontFamily:FONT,background:C.inputBg,color:C.text,outline:"none"}}/>
</div>
</div>
<div style={{flex:1,overflowY:"auto",padding:"4px 8px"}}>
{filtered.map(n=>{
const sel=section===n.id;
return <div key={n.id} onClick={()=>{setSection(n.id);setSearch("");}}
style={{display:"flex",alignItems:"center",gap:9,padding:"8px 10px",borderRadius:7,cursor:"pointer",marginBottom:1,
background:sel?C.selectedBg:"transparent",borderLeft:`3px solid ${sel?C.accent:"transparent"}`}}
onMouseEnter={e=>{if(!sel)e.currentTarget.style.background=C.hover;}}
onMouseLeave={e=>{if(!sel)e.currentTarget.style.background="transparent";}}>
<i className={`ti ${n.icon}`} style={{fontSize:16,color:sel?C.accent:C.muted,width:18,textAlign:"center"}} aria-hidden="true"/>
<span style={{fontSize:13.5,color:sel?C.accent:C.text,fontWeight:sel?600:400}}>{n.label}</span>
</div>;
})}
</div>
</div>
{/* Content area */}
<div style={{flex:1,overflowY:"auto",background:C.surface}}>
<div style={{padding:"22px 28px"}}>
<h2 style={{fontSize:20,fontWeight:700,color:C.text,margin:"0 0 20px"}}>
{section==="advanced"
?SETTINGS_NAV.find(n=>n.id==="advanced")?.label
:SETTINGS_NAV.find(n=>n.id===section)?.label}
</h2>
{section==="general"&&<SettingsGeneral ls={ls} setLS={setLS} profile={profile} profName={profName} setProfName={setProfName} profInitials={profInitials} setProfInitials={setProfInitials} profColor={profColor} setProfColor={setProfColor} C={C}/>}
{section==="theme"&&<SettingsTheme ls={ls} setLS={setLS} lC={lC} C={C} loginBg={loginBg} setLoginBg={setLoginBg} loginDark={loginDark} setLoginDark={setLoginDark}/>}
{section==="notifications"&&<SettingsNotifications ls={ls} setLS={setLS} C={C}/>}
{section==="accounts"&&<SettingsAccounts accounts={accounts} profile={profile} updateAccount={updateAccount} C={C}/>}
{section==="rules"&&<RulesInline C={C} rules={rules} setRules={setRules} allFolders={allFolders}/>}
{section==="privacy"&&<SettingsPrivacy ls={ls} setLS={setLS} C={C}/>}
{section==="backup"&&<SettingsBackup C={C}/>}
{section==="spam"&&<SettingsSpamTrash C={C}/>}
{section==="emailaddr"&&<SettingsEmailAddr accounts={accounts} C={C} smbConfig={smbConfig}
onDiscoverMore={()=>{onClose();onSetupNew?.();}}/>}
{section==="encryption"&&<div style={{padding:"20px 0",color:C.muted,fontSize:13.5}}>
<i className="ti ti-tools" style={{fontSize:32,display:"block",marginBottom:12,color:C.subtle}} aria-hidden="true"/>
This section will be available in a future update.
</div>}
{section==="advanced"&&<>
{/* Sub-tab bar */}
<div style={{display:"flex",gap:2,marginBottom:24,background:C.surface2,borderRadius:8,padding:3,width:"fit-content"}}>
{[{id:"security",icon:"ti-lock",label:"Security"},{id:"smb",icon:"ti-server",label:"SMB Share"}].map(t=>(
<button key={t.id} onClick={()=>setAdvancedTab(t.id)}
style={{display:"flex",alignItems:"center",gap:6,padding:"6px 16px",borderRadius:6,border:"none",
background:advancedTab===t.id?C.surface:"transparent",
color:advancedTab===t.id?C.text:C.muted,
cursor:"pointer",fontFamily:FONT,fontSize:13,fontWeight:advancedTab===t.id?600:400,
boxShadow:advancedTab===t.id?`0 1px 3px rgba(0,0,0,0.12)`:"none",transition:"all 0.15s"}}>
<i className={`ti ${t.icon}`} style={{fontSize:14}} aria-hidden="true"/>
{t.label}
</button>
))}
</div>
{advancedTab==="security"&&<SettingsSecurity ls={ls} setLS={setLS} C={C} profile={profile}/>}
{advancedTab==="smb"&&<SettingsSMB smbConfig={smbConfig} setSmbConfig={setSmbConfig} C={C}/>}
</>}
</div>
</div>
</div>
{/* Footer */}
<div style={{display:"flex",justifyContent:"flex-end",gap:8,padding:"12px 20px",borderTop:`1px solid ${C.border}`,flexShrink:0,background:C.surface2}}>
<button onClick={onClose} style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 18px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Cancel</button>
<button onClick={()=>{save();onClose();}} style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"7px 20px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>Save changes</button>
</div>
</div>
</div>;
}
// ── Settings sections ─────────────────────────────────────────────────────────
function SRow({label,sub,children,C,onClick}){
return <div onClick={onClick} style={{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"11px 0",borderBottom:`1px solid ${C.border}`,cursor:onClick?"pointer":"default"}}
onMouseEnter={onClick?e=>{e.currentTarget.style.background=C.hover;}:undefined}
onMouseLeave={onClick?e=>{e.currentTarget.style.background="transparent";}:undefined}>
<div>
<div style={{fontSize:13.5,color:C.text}}>{label}</div>
{sub&&<div style={{fontSize:12,color:C.subtle,marginTop:2}}>{sub}</div>}
</div>
{children}
</div>;
}
function SCard({children,C}){
return <div style={{background:C.surface2,border:`1px solid ${C.border}`,borderRadius:10,padding:"4px 14px",marginBottom:18}}>{children}</div>;
}
function SectionHead({label,C}){
return <div style={{fontSize:11,fontWeight:700,color:C.accent,letterSpacing:"0.08em",textTransform:"uppercase",marginBottom:10,marginTop:20}}>{label}</div>;
}
function SettingsGeneral({ls,setLS,profile,profName,setProfName,profInitials,setProfInitials,profColor,setProfColor,C}){
const[pdOpen,setPdOpen]=useState(false);
const[firstName,setFirstName]=useState(()=>profName.split(" ")[0]||"");
const[lastName,setLastName]=useState(()=>profName.split(" ").slice(1).join(" ")||"");
const[company,setCompany]=useState("");
const[street,setStreet]=useState("");
const[zip,setZip]=useState("");
const[city,setCity]=useState("");
const[country,setCountry]=useState("United States of America");
const[phone,setPhone]=useState("");
const[pdSaved,setPdSaved]=useState(false);
const COUNTRIES=["United States of America","Canada","United Kingdom","Australia","Germany","France","Netherlands","New Zealand","Other"];
const fieldSt={width:"100%",boxSizing:"border-box",padding:"7px 10px",border:`1px solid ${C.inBorder}`,
borderRadius:5,fontSize:13,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg};
const lbSt={display:"block",fontSize:12,fontWeight:600,color:C.muted,marginBottom:4};
function savePD(){
const full=(firstName+" "+lastName).trim();
if(full)setProfName(full);
setPdSaved(true);
setTimeout(()=>setPdSaved(false),2500);
}
return <>
<SCard C={C}>
<SRow label="Personal data" sub="Name and display preferences" C={C} onClick={()=>setPdOpen(p=>!p)}>
<i className={`ti ti-chevron-${pdOpen?"up":"down"}`} style={{fontSize:16,color:C.subtle}} aria-hidden="true"/>
</SRow>
{pdOpen&&<div style={{padding:"16px 0 10px",display:"flex",flexDirection:"column",gap:12}}>
{/* Name row */}
<div style={{display:"flex",gap:12}}>
<div style={{flex:1}}>
<label style={lbSt}>First name</label>
<input value={firstName} onChange={e=>setFirstName(e.target.value)} style={fieldSt}/>
</div>
<div style={{flex:1}}>
<label style={lbSt}>Last name</label>
<input value={lastName} onChange={e=>setLastName(e.target.value)} style={fieldSt}/>
</div>
</div>
{/* Initials + Avatar color */}
<div style={{display:"flex",gap:12,alignItems:"flex-start"}}>
<div style={{width:80}}>
<label style={lbSt}>Initials</label>
<input value={profInitials} onChange={e=>setProfInitials(e.target.value.toUpperCase().slice(0,2))} maxLength={2}
style={{...fieldSt,width:"100%",textAlign:"center",fontWeight:700,letterSpacing:"0.05em"}}/>
</div>
<div style={{flex:1}}>
<label style={lbSt}>Avatar color</label>
<div style={{display:"flex",gap:7,flexWrap:"wrap",paddingTop:4}}>
{ACCENTS.map(a=>(
<div key={a} onClick={()=>setProfColor(a)} style={{width:24,height:24,borderRadius:5,background:a,
border:`2.5px solid ${a===profColor?"#fff":"transparent"}`,outline:a===profColor?`2px solid ${a}`:"none",
cursor:"pointer",transition:"transform 0.1s"}}
onMouseEnter={e=>e.currentTarget.style.transform="scale(1.15)"}
onMouseLeave={e=>e.currentTarget.style.transform="scale(1)"}/>
))}
</div>
</div>
</div>
{/* Avatar preview */}
<div style={{display:"flex",alignItems:"center",gap:10,padding:"9px 12px",background:C.accentBg,borderRadius:7}}>
<Avatar initials={profInitials||"?"} color={profColor} size={34}/>
<div>
<div style={{fontSize:13,fontWeight:600,color:C.text}}>{(firstName+" "+lastName).trim()||"Your name"}</div>
<div style={{fontSize:12,color:C.subtle}}>{profile.accountIds?.[0]||""}</div>
</div>
</div>
{/* Company */}
<div>
<label style={lbSt}>Company</label>
<input value={company} onChange={e=>setCompany(e.target.value)} style={fieldSt}/>
</div>
{/* Street */}
<div>
<label style={lbSt}>Street and number</label>
<input value={street} onChange={e=>setStreet(e.target.value)} style={fieldSt}/>
</div>
{/* ZIP + City */}
<div style={{display:"flex",gap:12}}>
<div style={{width:110}}>
<label style={lbSt}>ZIP code</label>
<input value={zip} onChange={e=>setZip(e.target.value)} style={fieldSt}/>
</div>
<div style={{flex:1}}>
<label style={lbSt}>City</label>
<input value={city} onChange={e=>setCity(e.target.value)} style={fieldSt}/>
</div>
</div>
{/* Country */}
<div>
<label style={lbSt}>Country</label>
<select value={country} onChange={e=>setCountry(e.target.value)}
style={{...fieldSt,appearance:"auto"}}>
{COUNTRIES.map(c=><option key={c}>{c}</option>)}
</select>
</div>
{/* Phone */}
<div>
<label style={lbSt}>Phone</label>
<input type="tel" value={phone} onChange={e=>setPhone(e.target.value)} style={fieldSt}/>
</div>
{/* Save */}
<div style={{display:"flex",alignItems:"center",gap:10,paddingTop:2}}>
<button onClick={savePD}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"7px 20px",
fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>Save</button>
{pdSaved&&<span style={{fontSize:12.5,color:"#22c55e",display:"flex",alignItems:"center",gap:4,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:14}} aria-hidden="true"/>Saved
</span>}
</div>
</div>}
</SCard>
<SectionHead label="Display" C={C}/>
<SCard C={C}>
<SRow label="Language" sub="Interface language" C={C}>
<select style={{background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 8px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"}}>
<option>English</option><option>Deutsch</option><option>Español</option><option>Français</option>
</select>
</SRow>
<SRow label="Date format" C={C}>
<select style={{background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 8px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"}}>
<option>MM/DD/YYYY</option><option>DD/MM/YYYY</option><option>YYYY-MM-DD</option>
</select>
</SRow>
<SRow label="Time format" C={C}>
<select style={{background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 8px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"}}>
<option>12-hour (AM/PM)</option><option>24-hour</option>
</select>
</SRow>
</SCard>
<SectionHead label="Reading" C={C}/>
<SCard C={C}>
<SRow label="Auto-mark as read" sub="Mark emails as read when opened" C={C}><Toggle on={ls.autoMark} onChange={v=>setLS(p=>({...p,autoMark:v}))} C={C}/></SRow>
<SRow label="Preview lines" sub="Lines of preview text in the list" C={C}>
<select style={{background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 8px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"}}>
<option>1 line</option><option>2 lines</option><option>3 lines</option>
</select>
</SRow>
</SCard>
</>;
}
function SettingsTheme({ls,setLS,lC,C,loginBg,setLoginBg,loginDark,setLoginDark}){
const loginBgRef=useRef();
const customAccent=!ACCENTS.includes(ls.accent);
const customBg=ls.bg&&ls.bg.startsWith("#")&&!BG_SOLID.some(s=>s.preview.toLowerCase()===ls.bg.toLowerCase());
function bgIsDarkHex(hex){
if(!/^#[0-9a-f]{6}$/i.test(hex))return false;
const r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
return (r*0.299+g*0.587+b*0.114)<128;
}
return <>
{/* Accent colors */}
<SectionHead label="Accent colors" C={C}/>
<div style={{display:"flex",gap:11,marginBottom:14,flexWrap:"wrap",alignItems:"center"}}>
{ACCENTS.map(a=>(
<div key={a} onClick={()=>setLS(p=>({...p,accent:a}))}
style={{width:36,height:36,borderRadius:"50%",background:a,cursor:"pointer",
border:`3px solid ${a===ls.accent?"#fff":"transparent"}`,
outline:a===ls.accent?`2.5px solid ${a}`:"none",transition:"transform 0.1s",boxSizing:"border-box"}}
onMouseEnter={e=>e.currentTarget.style.transform="scale(1.15)"}
onMouseLeave={e=>e.currentTarget.style.transform="scale(1)"}/>
))}
{/* Custom accent picker */}
<label style={{position:"relative",width:36,height:36,borderRadius:"50%",cursor:"pointer",
border:`3px solid ${customAccent?ls.accent:"transparent"}`,
outline:customAccent?`2.5px solid ${ls.accent}`:`2px dashed ${C.muted}`,
boxSizing:"border-box",overflow:"hidden",display:"flex",alignItems:"center",justifyContent:"center",
background:customAccent?ls.accent:"transparent"}}>
<input type="color" value={customAccent?ls.accent:"#ff8800"}
onChange={e=>setLS(p=>({...p,accent:e.target.value}))}
style={{position:"absolute",inset:0,opacity:0,cursor:"pointer",width:"100%",height:"100%",border:"none",padding:0}}/>
{!customAccent&&<i className="ti ti-plus" style={{fontSize:14,color:C.muted,pointerEvents:"none"}} aria-hidden="true"/>}
</label>
</div>
<div style={{fontSize:12,color:C.subtle,marginBottom:22}}>
Current accent: <code style={{fontSize:12,color:ls.accent,fontWeight:600}}>{ls.accent.toUpperCase()}</code>
{customAccent&&<span style={{marginLeft:8,fontSize:11,color:C.muted}}>· custom</span>}
</div>
{/* Backgrounds */}
<SectionHead label="Backgrounds" C={C}/>
<div style={{display:"flex",flexWrap:"wrap",gap:14,marginBottom:14,alignItems:"flex-start"}}>
{BG_SOLID.map(bg=>(
<div key={bg.id} onClick={()=>setLS(p=>({...p,bg:bg.id,isDark:bg.dark}))}
style={{display:"flex",flexDirection:"column",alignItems:"center",gap:7,cursor:"pointer"}}>
<div style={{width:90,height:60,borderRadius:8,background:bg.preview,
border:`2.5px solid ${ls.bg===bg.id?C.accent:C.border}`,
boxShadow:ls.bg===bg.id?`0 0 0 2px ${C.accent}40`:"none",transition:"all 0.15s"}}>
{bg.dark&&<div style={{width:"100%",height:"100%",display:"flex",alignItems:"center",justifyContent:"center",borderRadius:6}}>
<i className="ti ti-moon" style={{fontSize:18,color:"rgba(255,255,255,0.2)"}} aria-hidden="true"/>
</div>}
</div>
<span style={{fontSize:12,color:ls.bg===bg.id?C.accent:C.muted,fontWeight:ls.bg===bg.id?600:400,textDecoration:ls.bg===bg.id?"underline":"none",textUnderlineOffset:2}}>{bg.label}</span>
</div>
))}
{/* Custom background picker */}
<label style={{display:"flex",flexDirection:"column",alignItems:"center",gap:7,cursor:"pointer",position:"relative"}}>
<div style={{width:90,height:60,borderRadius:8,position:"relative",overflow:"hidden",
border:customBg?`2.5px solid ${C.accent}`:`2px dashed ${C.muted}`,
boxShadow:customBg?`0 0 0 2px ${C.accent}40`:"none",
background:customBg?ls.bg:"transparent"}}>
<input type="color" value={customBg?ls.bg:"#888888"}
onChange={e=>setLS(p=>({...p,bg:e.target.value,isDark:bgIsDarkHex(e.target.value)}))}
style={{position:"absolute",inset:0,opacity:0,cursor:"pointer",width:"100%",height:"100%",border:"none",padding:0}}/>
{!customBg&&<div style={{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",pointerEvents:"none"}}>
<i className="ti ti-plus" style={{fontSize:18,color:C.muted}} aria-hidden="true"/>
</div>}
</div>
<span style={{fontSize:12,color:customBg?C.accent:C.muted,fontWeight:customBg?600:400,textDecoration:customBg?"underline":"none",textUnderlineOffset:2}}>Custom</span>
</label>
</div>
{/* Live preview strip */}
<SectionHead label="Preview" C={C}/>
<div style={{height:60,borderRadius:10,border:`1px solid ${C.border}`,overflow:"hidden",marginBottom:22,...getBgStyle(ls.bg),background:customBg?ls.bg:undefined,display:"flex",alignItems:"center",justifyContent:"center"}}>
<span style={{fontSize:13,fontWeight:600,color:(customBg?bgIsDarkHex(ls.bg):bgIsDark(ls.bg)||bgIsImage(ls.bg))?"rgba(255,255,255,0.8)":"rgba(0,0,0,0.4)",background:"rgba(0,0,0,0.15)",padding:"4px 14px",borderRadius:20,backdropFilter:"blur(4px)"}}>Reading pane preview</span>
</div>
{/* Login background */}
<SectionHead label="Login background" C={C}/>
<SCard C={C}>
<SRow label="Login page theme" sub="Controls the appearance of the sign-in screen" C={C}>
<div style={{display:"flex",gap:4,background:C.surface3||C.surface2,borderRadius:7,padding:3}}>
{[{v:true,icon:"ti-moon",label:"Dark"},{v:false,icon:"ti-sun",label:"Light"}].map(opt=>(
<button key={String(opt.v)} onClick={()=>setLoginDark(opt.v)}
style={{display:"flex",alignItems:"center",gap:5,padding:"5px 13px",borderRadius:5,border:"none",
background:loginDark===opt.v?C.surface:"transparent",
color:loginDark===opt.v?C.text:C.muted,
cursor:"pointer",fontFamily:FONT,fontSize:12.5,fontWeight:loginDark===opt.v?600:400,
boxShadow:loginDark===opt.v?`0 1px 3px rgba(0,0,0,0.1)`:"none",transition:"all 0.15s"}}>
<i className={`ti ${opt.icon}`} style={{fontSize:13}} aria-hidden="true"/>
{opt.label}
</button>
))}
</div>
</SRow>
</SCard>
<div style={{display:"flex",alignItems:"center",gap:16,marginBottom:8}}>
{/* Thumbnail */}
<div style={{width:120,height:80,borderRadius:8,overflow:"hidden",border:`1px solid ${C.border}`,flexShrink:0,position:"relative",background:"#0c0c0c"}}>
<img src={loginBg||DEFAULT_LOGIN_BG} style={{width:"100%",height:"100%",objectFit:"cover"}} alt="Login background"/>
</div>
<div style={{display:"flex",flexDirection:"column",gap:8}}>
<div style={{fontSize:13,color:C.text,fontWeight:500}}>{loginBg?"Custom image":"Default mountain photo"}</div>
<div style={{display:"flex",gap:8}}>
<input ref={loginBgRef} type="file" accept="image/*" style={{display:"none"}}
onChange={e=>{
const file=e.target.files?.[0];
if(!file)return;
const reader=new FileReader();
reader.onload=ev=>setLoginBg(ev.target.result);
reader.readAsDataURL(file);
e.target.value="";
}}/>
<button onClick={()=>loginBgRef.current?.click()}
style={{padding:"6px 14px",borderRadius:6,border:`1px solid ${C.border}`,background:C.surface2,
color:C.text,cursor:"pointer",fontFamily:FONT,fontSize:13,display:"flex",alignItems:"center",gap:6}}>
<i className="ti ti-upload" style={{fontSize:14}} aria-hidden="true"/>
Upload image
</button>
{loginBg&&<button onClick={()=>setLoginBg(null)}
style={{padding:"6px 14px",borderRadius:6,border:`1px solid ${C.border}`,background:"transparent",
color:C.muted,cursor:"pointer",fontFamily:FONT,fontSize:13,display:"flex",alignItems:"center",gap:6}}>
<i className="ti ti-refresh" style={{fontSize:14}} aria-hidden="true"/>
Reset to default
</button>}
</div>
<span style={{fontSize:12,color:C.subtle}}>JPG, PNG, WEBP · shown on the login screen right panel</span>
</div>
</div>
</>;
}
function SettingsNotifications({ls,setLS,C}){
const SOUNDS=[
{id:"chime", label:"Chime"},
{id:"bell", label:"Bell"},
{id:"ping", label:"Ping"},
{id:"ding", label:"Ding"},
{id:"pop", label:"Pop"},
{id:"classic",label:"Classic"},
{id:"subtle", label:"Subtle"},
];
function playSound(id){
try{
const ctx=new (window.AudioContext||window.webkitAudioContext)();
const tone=(freq,start,dur,type="sine",vol=0.28)=>{
const osc=ctx.createOscillator(),g=ctx.createGain();
osc.connect(g);g.connect(ctx.destination);
osc.type=type;osc.frequency.setValueAtTime(freq,ctx.currentTime+start);
g.gain.setValueAtTime(0,ctx.currentTime+start);
g.gain.linearRampToValueAtTime(vol,ctx.currentTime+start+0.015);
g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+start+dur);
osc.start(ctx.currentTime+start);osc.stop(ctx.currentTime+start+dur+0.05);
};
if(id==="chime") {tone(1319,0,0.5);tone(1047,0.18,0.45);tone(1568,0.36,0.7);}
else if(id==="bell") {tone(440,0,1.2);tone(880,0.05,0.9,undefined,0.15);}
else if(id==="ping") {tone(1400,0,0.25,undefined,0.22);}
else if(id==="ding") {tone(880,0,0.18);tone(660,0.2,0.4);}
else if(id==="pop") {tone(220,0,0.08,"triangle",0.45);}
else if(id==="classic"){tone(660,0,0.18);tone(880,0.22,0.28);}
else if(id==="subtle") {tone(600,0,0.35,undefined,0.12);}
setTimeout(()=>ctx.close(),2500);
}catch(e){}
}
const selSt={background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,
padding:"4px 8px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"};
return <SCard C={C}>
<SRow label="Desktop notifications" sub="Show popup for new messages" C={C}><Toggle on={ls.notifications} onChange={v=>setLS(p=>({...p,notifications:v}))} C={C}/></SRow>
<SRow label="Notification sound" sub="Play a sound on new mail" C={C}><Toggle on={ls.sound} onChange={v=>setLS(p=>({...p,sound:v}))} C={C}/></SRow>
{ls.sound&&<SRow label="Sound" sub="Choose notification tone" C={C}>
<div style={{display:"flex",gap:6,alignItems:"center"}}>
<select value={ls.soundChoice||"chime"} onChange={e=>setLS(p=>({...p,soundChoice:e.target.value}))} style={selSt}>
{SOUNDS.map(s=><option key={s.id} value={s.id}>{s.label}</option>)}
</select>
<button onClick={()=>playSound(ls.soundChoice||"chime")}
style={{background:C.accent,border:"none",borderRadius:5,padding:"4px 10px",
color:"#fff",fontFamily:FONT,fontSize:12,cursor:"pointer",display:"flex",alignItems:"center",gap:4}}>
<i className="ti ti-player-play" style={{fontSize:11}} aria-hidden="true"/> Preview
</button>
</div>
</SRow>}
<SRow label="Badge count" sub="Show unread count on taskbar" C={C}><Toggle on={true} C={C}/></SRow>
<SRow label="Notification preview" sub="Show sender and subject in popup" C={C}><Toggle on={true} C={C}/></SRow>
<SRow label="Do not disturb" sub="Silence all notifications" C={C}><Toggle on={false} C={C}/></SRow>
</SCard>;
}
function SettingsAccounts({accounts,profile,updateAccount,C}){
const[editingId,setEditingId]=useState(null);
const[draft,setDraft]=useState({});
const[emailErr,setEmailErr]=useState("");
function startEdit(acc){
setEditingId(acc.id);
setEmailErr("");
setDraft({display:acc.display,av:acc.av,avColor:acc.avColor,email:acc.email??acc.id});
}
function cancelEdit(){setEditingId(null);setDraft({});setEmailErr("");}
function saveEdit(id){
const trimmed=draft.email.trim();
if(trimmed&&!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)){
setEmailErr("Enter a valid email address.");return;
}
updateAccount(id,{display:draft.display,av:draft.av,avColor:draft.avColor,email:trimmed||null});
setEditingId(null);setDraft({});setEmailErr("");
}
return <>
<p style={{fontSize:13,color:C.muted,marginBottom:14,lineHeight:1.7}}>
Accounts in this profile: {profile.accountIds.length}. All accounts read from <code style={{fontSize:12,background:C.surface2,padding:"1px 6px",borderRadius:4,color:C.accent}}>.Mails/</code> on the SMB share.
</p>
{accounts.map(acc=>{
const isEditing=editingId===acc.id;
return <div key={acc.id} style={{background:C.surface2,border:`1px solid ${isEditing?C.accent:C.border}`,borderRadius:9,marginBottom:8,overflow:"hidden",transition:"border-color 0.15s"}}>
{/* Row header */}
<div style={{display:"flex",alignItems:"center",gap:12,padding:"10px 14px"}}>
<Avatar initials={isEditing?(draft.av||"?"):acc.av} color={isEditing?draft.avColor:acc.avColor} size={36}/>
<div style={{flex:1}}>
<div style={{fontSize:13.5,fontWeight:600,color:C.text}}>{isEditing?draft.display:acc.display}</div>
<div style={{fontSize:12,color:C.subtle}}>{isEditing?(draft.email||"No email set"):(acc.email??("Default account · .Mails/default/"))}</div>
</div>
{!isEditing&&<IBtn icon="ti-pencil" title="Edit account" C={C} onClick={()=>startEdit(acc)}/>}
{isEditing&&<IBtn icon="ti-x" title="Cancel" C={C} onClick={cancelEdit}/>}
</div>
{/* Inline edit form */}
{isEditing&&<div style={{padding:"0 14px 14px",borderTop:`1px solid ${C.border}`}}>
<div style={{marginTop:12,marginBottom:12}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Email address</label>
<input value={draft.email} onChange={e=>{setDraft(p=>({...p,email:e.target.value}));setEmailErr("");}}
placeholder="you@example.com" type="email"
style={{width:"100%",boxSizing:"border-box",padding:"7px 10px",
border:`1px solid ${emailErr?C.danger??'#c50f1f':C.inBorder}`,borderRadius:5,
fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
{emailErr&&<div style={{fontSize:12,color:C.danger??"#c50f1f",marginTop:4}}>{emailErr}</div>}
</div>
<div style={{display:"flex",gap:12,marginBottom:12}}>
<div style={{flex:1}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Display name</label>
<input value={draft.display} onChange={e=>setDraft(p=>({...p,display:e.target.value}))}
style={{width:"100%",boxSizing:"border-box",padding:"7px 10px",border:`1px solid ${C.inBorder}`,borderRadius:5,fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
</div>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Initials</label>
<input value={draft.av} onChange={e=>setDraft(p=>({...p,av:e.target.value.toUpperCase().slice(0,2)}))} maxLength={2}
style={{width:56,padding:"7px 10px",border:`1px solid ${C.inBorder}`,borderRadius:5,fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg,textAlign:"center",fontWeight:700,boxSizing:"border-box"}}/>
</div>
</div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:8}}>Avatar color</label>
<div style={{display:"flex",gap:7,flexWrap:"wrap",marginBottom:14}}>
{ACCENTS.map(a=>(
<div key={a} onClick={()=>setDraft(p=>({...p,avColor:a}))}
style={{width:24,height:24,borderRadius:5,background:a,cursor:"pointer",
border:`2.5px solid ${a===draft.avColor?"#fff":"transparent"}`,
outline:a===draft.avColor?`2px solid ${a}`:"none",transition:"transform 0.1s"}}
onMouseEnter={e=>e.currentTarget.style.transform="scale(1.15)"}
onMouseLeave={e=>e.currentTarget.style.transform="scale(1)"}/>
))}
</div>
<div style={{display:"flex",gap:8}}>
<button onClick={()=>saveEdit(acc.id)}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:5,padding:"6px 16px",fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>
Save
</button>
<button onClick={cancelEdit}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,padding:"6px 12px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>
Cancel
</button>
</div>
</div>}
</div>;
})}
</>;
}
function SettingsSecurity({ls,setLS,C,profile}){
const pinEnabled=!!(ls.pin);
const[flow,setFlow]=useState(null); // null | "setup" | "disable"
const[newPin,setNewPin]=useState("");
const[confirmPin,setConfirmPin]=useState("");
const[currentPin,setCurrentPin]=useState("");
const[pinErr,setPinErr]=useState("");
const[showDeleteDialog,setShowDeleteDialog]=useState(false);
const[deleteStep,setDeleteStep]=useState(1); // 1=warn, 2=confirm type name, 3=done
const[deleteInput,setDeleteInput]=useState("");
const CONFIRM_WORD="DELETE";
function handleToggle(v){
if(v){setFlow("setup");setNewPin("");setConfirmPin("");setPinErr("");}
else{setFlow("disable");setCurrentPin("");setPinErr("");}
}
function submitSetup(){
if(!/^\d{4,6}$/.test(newPin)){setPinErr("PIN must be 46 digits.");return;}
if(newPin!==confirmPin){setPinErr("PINs don't match.");return;}
setLS(p=>({...p,pin:newPin}));
setFlow(null);setNewPin("");setConfirmPin("");setPinErr("");
}
function submitDisable(){
if(currentPin!==ls.pin){setPinErr("Incorrect PIN.");return;}
setLS(p=>({...p,pin:null}));
setFlow(null);setCurrentPin("");setPinErr("");
}
function cancelFlow(){setFlow(null);setNewPin("");setConfirmPin("");setCurrentPin("");setPinErr("");}
const pinInputSt={padding:"8px 12px",border:`1px solid ${C.inBorder}`,borderRadius:5,
fontSize:18,fontFamily:"monospace",outline:"none",color:C.text,background:C.inputBg,
width:150,boxSizing:"border-box",letterSpacing:6,textAlign:"center"};
return <><SCard C={C}>
<SRow label="Profile PIN" sub={pinEnabled?"PIN active — required at login":"Require a PIN to access this profile"} C={C}>
<Toggle on={pinEnabled} onChange={handleToggle} C={C}/>
</SRow>
{flow==="setup"&&<div style={{padding:"14px 0 10px"}}>
<div style={{fontSize:13,fontWeight:700,color:C.accent,marginBottom:14}}>Set up PIN</div>
<div style={{display:"flex",flexDirection:"column",gap:12}}>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:6}}>New PIN (46 digits)</label>
<input type="password" inputMode="numeric" maxLength={6} value={newPin}
onChange={e=>{setNewPin(e.target.value.replace(/\D/g,""));setPinErr("");}}
style={pinInputSt} placeholder="••••"/>
</div>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:6}}>Confirm PIN</label>
<input type="password" inputMode="numeric" maxLength={6} value={confirmPin}
onChange={e=>{setConfirmPin(e.target.value.replace(/\D/g,""));setPinErr("");}}
style={pinInputSt} placeholder="••••"/>
</div>
{pinErr&&<div style={{fontSize:12.5,color:"#c50f1f"}}>{pinErr}</div>}
<div style={{display:"flex",gap:8}}>
<button onClick={submitSetup}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:5,padding:"7px 18px",fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>
Set PIN
</button>
<button onClick={cancelFlow}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,padding:"7px 12px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>
Cancel
</button>
</div>
</div>
</div>}
{flow==="disable"&&<div style={{padding:"14px 0 10px"}}>
<div style={{fontSize:13,fontWeight:700,color:C.accent,marginBottom:14}}>Enter current PIN to disable</div>
<input type="password" inputMode="numeric" maxLength={6} value={currentPin}
onChange={e=>{setCurrentPin(e.target.value.replace(/\D/g,""));setPinErr("");}}
style={pinInputSt} placeholder="••••"/>
{pinErr&&<div style={{fontSize:12.5,color:"#c50f1f",marginTop:6}}>{pinErr}</div>}
<div style={{display:"flex",gap:8,marginTop:12}}>
<button onClick={submitDisable}
style={{background:"#c50f1f",color:"#fff",border:"none",borderRadius:5,padding:"7px 18px",fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>
Disable PIN
</button>
<button onClick={cancelFlow}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,padding:"7px 12px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>
Cancel
</button>
</div>
</div>}
<SRow label="Auto-lock" sub="Lock after inactivity" C={C}>
<select style={{background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 8px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"}}>
<option>Never</option><option>5 minutes</option><option>15 minutes</option><option>1 hour</option>
</select>
</SRow>
<SRow label="Clear cache on lock" sub="Remove cached emails when auto-locked" C={C}><Toggle on={false} C={C}/></SRow>
</SCard>
{/* ── Danger Zone ── */}
<div style={{marginTop:32,border:"1.5px solid #fca5a5",borderRadius:10,overflow:"hidden"}}>
{/* Header */}
<div style={{display:"flex",alignItems:"center",gap:8,padding:"10px 16px",background:"#fef2f2",borderBottom:"1.5px solid #fca5a5"}}>
<i className="ti ti-alert-triangle" style={{fontSize:15,color:"#c50f1f",flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:12,fontWeight:700,color:"#c50f1f",letterSpacing:"0.06em",textTransform:"uppercase"}}>Danger Zone</span>
</div>
{/* Delete profile row */}
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"14px 16px",background:C.surface}}>
<div>
<div style={{fontSize:13.5,fontWeight:600,color:C.text}}>Delete this profile</div>
<div style={{fontSize:12,color:C.muted,marginTop:3,lineHeight:1.5}}>
Permanently removes this profile and all locally cached email data.<br/>
<strong style={{color:"#c50f1f"}}>The SMB share and its files are never modified.</strong>
</div>
</div>
<button onClick={()=>{setShowDeleteDialog(true);setDeleteStep(1);setDeleteInput("");}}
style={{background:"transparent",border:"1.5px solid #c50f1f",borderRadius:6,padding:"7px 16px",
fontSize:13,cursor:"pointer",fontFamily:FONT,color:"#c50f1f",fontWeight:600,
display:"flex",alignItems:"center",gap:6,flexShrink:0,marginLeft:16,whiteSpace:"nowrap"}}
onMouseEnter={e=>{e.currentTarget.style.background="#fef2f2";}}
onMouseLeave={e=>{e.currentTarget.style.background="transparent";}}>
<i className="ti ti-user-x" style={{fontSize:14}} aria-hidden="true"/>
Delete Profile
</button>
</div>
</div>
{/* ── Delete confirmation dialog ── */}
{showDeleteDialog&&<div style={{position:"fixed",inset:0,zIndex:500,background:"rgba(0,0,0,0.55)",
display:"flex",alignItems:"center",justifyContent:"center"}}
onClick={e=>{if(e.target===e.currentTarget){setShowDeleteDialog(false);setDeleteStep(1);setDeleteInput("");}}}>
<div style={{background:C.surface,border:"1.5px solid #fca5a5",borderRadius:12,padding:"28px 32px",
width:420,boxShadow:"0 20px 50px rgba(0,0,0,0.3)",fontFamily:FONT}}>
{deleteStep===1&&<>
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:16}}>
<div style={{width:40,height:40,borderRadius:"50%",background:"#fef2f2",display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
<i className="ti ti-user-x" style={{fontSize:20,color:"#c50f1f"}} aria-hidden="true"/>
</div>
<div style={{fontSize:17,fontWeight:700,color:C.text}}>Delete profile?</div>
</div>
<p style={{fontSize:13.5,color:C.text,lineHeight:1.7,margin:"0 0 14px"}}>
You are about to permanently delete the profile <strong>"{profile?.name||"this profile"}"</strong> and all its locally cached email data, settings, and search index.
</p>
<div style={{background:"#fef2f2",border:"1px solid #fca5a5",borderRadius:8,padding:"12px 14px",marginBottom:20}}>
<div style={{fontSize:12.5,color:"#7f1d1d",lineHeight:1.7}}>
<div style={{display:"flex",alignItems:"flex-start",gap:7,marginBottom:6}}>
<i className="ti ti-circle-check" style={{fontSize:13,color:"#22c55e",marginTop:2,flexShrink:0}} aria-hidden="true"/>
<span>Your emails on the SMB share are <strong>completely untouched</strong> — no files will be deleted, moved, or modified.</span>
</div>
<div style={{display:"flex",alignItems:"flex-start",gap:7,marginBottom:6}}>
<i className="ti ti-circle-x" style={{fontSize:13,color:"#c50f1f",marginTop:2,flexShrink:0}} aria-hidden="true"/>
<span>Local DashMail database, cached attachments, and profile settings will be <strong>permanently deleted</strong>.</span>
</div>
<div style={{display:"flex",alignItems:"flex-start",gap:7}}>
<i className="ti ti-circle-x" style={{fontSize:13,color:"#c50f1f",marginTop:2,flexShrink:0}} aria-hidden="true"/>
<span>This action <strong>cannot be undone</strong>. Create a backup first if you want to restore later.</span>
</div>
</div>
</div>
<div style={{display:"flex",justifyContent:"flex-end",gap:8}}>
<button onClick={()=>{setShowDeleteDialog(false);}}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 18px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Cancel</button>
<button onClick={()=>setDeleteStep(2)}
style={{background:"#c50f1f",color:"#fff",border:"none",borderRadius:6,padding:"7px 20px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,fontWeight:600,display:"flex",alignItems:"center",gap:7}}>
I understand, continue <i className="ti ti-arrow-right" style={{fontSize:14}} aria-hidden="true"/>
</button>
</div>
</>}
{deleteStep===2&&<>
<div style={{fontSize:17,fontWeight:700,color:C.text,marginBottom:8}}>Confirm deletion</div>
<p style={{fontSize:13,color:C.muted,lineHeight:1.6,marginBottom:18}}>
Type <strong style={{color:"#c50f1f",fontFamily:"monospace",fontSize:14,letterSpacing:1}}>{CONFIRM_WORD}</strong> to permanently delete this profile.
</p>
<input value={deleteInput} onChange={e=>setDeleteInput(e.target.value.toUpperCase())}
placeholder={CONFIRM_WORD}
style={{width:"100%",boxSizing:"border-box",padding:"9px 12px",border:`1.5px solid ${deleteInput===CONFIRM_WORD?"#c50f1f":C.inBorder}`,
borderRadius:6,fontSize:14,fontFamily:"monospace",outline:"none",color:"#c50f1f",
background:deleteInput===CONFIRM_WORD?"#fef2f2":C.inputBg,letterSpacing:2,marginBottom:18,
textAlign:"center",fontWeight:700}}/>
<div style={{display:"flex",justifyContent:"flex-end",gap:8}}>
<button onClick={()=>setDeleteStep(1)}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 18px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Back</button>
<button onClick={()=>setDeleteStep(3)} disabled={deleteInput!==CONFIRM_WORD}
style={{background:deleteInput===CONFIRM_WORD?"#c50f1f":"rgba(197,15,31,0.3)",color:"#fff",border:"none",borderRadius:6,
padding:"7px 20px",fontSize:13.5,cursor:deleteInput===CONFIRM_WORD?"pointer":"default",
fontFamily:FONT,fontWeight:600,display:"flex",alignItems:"center",gap:7,transition:"background 0.15s"}}>
<i className="ti ti-trash" style={{fontSize:14}} aria-hidden="true"/>Delete Profile
</button>
</div>
</>}
{deleteStep===3&&<>
<div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:12,textAlign:"center",padding:"10px 0"}}>
<div style={{width:52,height:52,borderRadius:"50%",background:"#fef2f2",display:"flex",alignItems:"center",justifyContent:"center"}}>
<i className="ti ti-circle-check" style={{fontSize:28,color:"#22c55e"}} aria-hidden="true"/>
</div>
<div style={{fontSize:17,fontWeight:700,color:C.text}}>Profile deleted</div>
<div style={{fontSize:13,color:C.muted,lineHeight:1.6}}>
The profile and all local data have been removed.<br/>
Your emails on the SMB share remain untouched.<br/>
You will be signed out now.
</div>
</div>
<div style={{display:"flex",justifyContent:"center",marginTop:22}}>
<button onClick={()=>{setShowDeleteDialog(false);setDeleteStep(1);}}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"8px 28px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>
Sign out
</button>
</div>
</>}
</div>
</div>}
</>;
}
function SettingsPrivacy({ls,setLS,C}){
return <SCard C={C}>
<SRow label="Read receipts" sub="Send read confirmation to senders" C={C}><Toggle on={ls.readReceipts} onChange={v=>setLS(p=>({...p,readReceipts:v}))} C={C}/></SRow>
<SRow label="Block tracking pixels" sub="Prevent email tracking images from loading" C={C}><Toggle on={true} C={C}/></SRow>
<SRow label="Load external images" sub="Automatically load images in emails" C={C}><Toggle on={false} C={C}/></SRow>
<SRow label="Link click confirmation" sub="Warn before opening external links" C={C}><Toggle on={true} C={C}/></SRow>
</SCard>;
}
function SettingsSMB({smbConfig,setSmbConfig,C}){
const[status,setStatus]=useState(null); // null | "testing" | "ok" | "err"
const[errMsg,setErrMsg]=useState("");
function set(k,v){setSmbConfig(p=>({...p,[k]:v}));}
function testConn(){
setStatus("testing");setErrMsg("");
setTimeout(()=>{
if(smbConfig.host.trim()&&smbConfig.share.trim()){setStatus("ok");}
else{setStatus("err");setErrMsg("Host and share name are required.");}
},1400);
}
const inputSt={width:"100%",boxSizing:"border-box",padding:"7px 10px",border:`1px solid ${C.inBorder}`,
borderRadius:5,fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg};
return <>
<p style={{fontSize:13,color:C.muted,marginBottom:16,lineHeight:1.7}}>
Configure the SMB network share where your mailbox folders are stored.
DashMail reads mail from <code style={{fontSize:12,background:C.surface2,padding:"1px 6px",borderRadius:4,color:C.accent}}>\\HOST\SHARE\SUBFOLDER</code>.
</p>
<SectionHead label="Connection" C={C}/>
<SCard C={C}>
<div style={{marginBottom:12}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Server host</label>
<input value={smbConfig.host||""} onChange={e=>set("host",e.target.value)} placeholder="192.168.1.100 or mailserver.local" style={inputSt}/>
</div>
<div style={{display:"flex",gap:12,marginBottom:12}}>
<div style={{flex:2}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Share name</label>
<input value={smbConfig.share||""} onChange={e=>set("share",e.target.value)} placeholder="mail" style={inputSt}/>
</div>
<div style={{flex:1}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Drive letter</label>
<select value={smbConfig.driveLetter||"Z"} onChange={e=>set("driveLetter",e.target.value)}
style={{width:"100%",padding:"7px 8px",border:`1px solid ${C.inBorder}`,borderRadius:5,
fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg,boxSizing:"border-box"}}>
{"DEFGHIJKLMNOPQRSTUVWXYZ".split("").map(l=><option key={l} value={l}>{l}:</option>)}
</select>
</div>
</div>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Subfolder <span style={{fontWeight:400,color:C.subtle,fontSize:12}}>(within the share)</span></label>
<input value={smbConfig.subfolder||""} onChange={e=>set("subfolder",e.target.value)} placeholder=".Mails" style={inputSt}/>
</div>
</SCard>
<SectionHead label="Authentication" C={C}/>
<SCard C={C}>
<div style={{marginBottom:12}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Username</label>
<input value={smbConfig.username||""} onChange={e=>set("username",e.target.value)} placeholder="domain\\user or user@domain" style={inputSt} autoComplete="off"/>
</div>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Password</label>
<input type="password" value={smbConfig.password||""} onChange={e=>set("password",e.target.value)} style={inputSt} autoComplete="new-password"/>
</div>
</SCard>
<div style={{display:"flex",alignItems:"center",gap:12,marginTop:6}}>
<button onClick={testConn} disabled={status==="testing"}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"8px 20px",
fontSize:13.5,cursor:status==="testing"?"default":"pointer",fontFamily:FONT,fontWeight:600,
display:"flex",alignItems:"center",gap:7,opacity:status==="testing"?0.7:1}}>
{status==="testing"
?<><i className="ti ti-loader-2 ti-spin" style={{fontSize:14}} aria-hidden="true"/> Testing…</>
:<><i className="ti ti-plug" style={{fontSize:14}} aria-hidden="true"/> Test Connection</>}
</button>
{status==="ok"&&<span style={{fontSize:13,color:"#22c55e",display:"flex",alignItems:"center",gap:5,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:15}} aria-hidden="true"/>Connected
</span>}
{status==="err"&&<span style={{fontSize:13,color:C.danger??"#c50f1f",display:"flex",alignItems:"center",gap:5}}>
<i className="ti ti-alert-circle" style={{fontSize:15}} aria-hidden="true"/>{errMsg||"Connection failed"}
</span>}
</div>
</>;
}
// ── Spam & Trash settings ─────────────────────────────────────────────────────
function SettingsSpamTrash({C}){
const[trashAfter,setTrashAfter]=useState("30");
const[spamAfter,setSpamAfter]=useState("30");
const[confirmDelete,setConfirmDelete]=useState(true);
const[emptyingTrash,setEmptyingTrash]=useState(false);
const[emptyDone,setEmptyDone]=useState(false);
function emptyNow(){
setEmptyingTrash(true);setEmptyDone(false);
setTimeout(()=>{setEmptyingTrash(false);setEmptyDone(true);setTimeout(()=>setEmptyDone(false),3000);},1600);
}
const TRASH_OPTS=[
{v:"never", label:"Never (manual only)"},
{v:"1", label:"After 1 day"},
{v:"7", label:"After 7 days"},
{v:"14", label:"After 14 days"},
{v:"30", label:"After 30 days"},
{v:"60", label:"After 60 days"},
{v:"90", label:"After 90 days"},
];
const SPAM_OPTS=[
{v:"never", label:"Never (manual only)"},
{v:"7", label:"After 7 days"},
{v:"14", label:"After 14 days"},
{v:"30", label:"After 30 days"},
{v:"60", label:"After 60 days"},
];
const selSt={background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,
padding:"5px 9px",color:C.text,fontFamily:FONT,fontSize:13,outline:"none"};
return <>
<SectionHead label="Trash" C={C}/>
<SCard C={C}>
<SRow label="Automatically empty trash" sub="Permanently delete items in Trash older than" C={C}>
<select value={trashAfter} onChange={e=>setTrashAfter(e.target.value)} style={selSt}>
{TRASH_OPTS.map(o=><option key={o.v} value={o.v}>{o.label}</option>)}
</select>
</SRow>
<SRow label="Confirm before deleting" sub="Show a confirmation prompt before permanently deleting" C={C}>
<Toggle on={confirmDelete} onChange={setConfirmDelete} C={C}/>
</SRow>
</SCard>
{/* Empty trash now */}
<div style={{display:"flex",alignItems:"center",gap:12,marginBottom:24}}>
<button onClick={emptyNow} disabled={emptyingTrash}
style={{background:"transparent",border:`1px solid ${C.danger||"#c50f1f"}`,borderRadius:6,
padding:"7px 18px",fontSize:13,cursor:emptyingTrash?"default":"pointer",fontFamily:FONT,
color:C.danger||"#c50f1f",display:"flex",alignItems:"center",gap:7,
opacity:emptyingTrash?0.65:1,fontWeight:500}}>
{emptyingTrash
?<><i className="ti ti-loader-2 ti-spin" style={{fontSize:14}} aria-hidden="true"/>Emptying…</>
:<><i className="ti ti-trash" style={{fontSize:14}} aria-hidden="true"/>Empty Trash Now</>}
</button>
{emptyDone&&<span style={{fontSize:13,color:"#22c55e",display:"flex",alignItems:"center",gap:5,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:14}} aria-hidden="true"/>Trash emptied
</span>}
</div>
<SectionHead label="Spam" C={C}/>
<SCard C={C}>
<SRow label="Automatically delete spam" sub="Permanently remove spam messages older than" C={C}>
<select value={spamAfter} onChange={e=>setSpamAfter(e.target.value)} style={selSt}>
{SPAM_OPTS.map(o=><option key={o.v} value={o.v}>{o.label}</option>)}
</select>
</SRow>
<SRow label="Move to spam folder" sub="Flagged messages go to Spam instead of Trash" C={C}>
<Toggle on={true} C={C}/>
</SRow>
<SRow label="Trust sender after mark" sub="Whitelist sender when you mark as Not Spam" C={C}>
<Toggle on={true} C={C}/>
</SRow>
</SCard>
{trashAfter!=="never"&&<div style={{display:"flex",alignItems:"flex-start",gap:8,
background:C.accentBg||C.surface2,border:`1px solid ${C.accentBorder||C.border}`,
borderRadius:7,padding:"10px 14px",marginTop:-8}}>
<i className="ti ti-info-circle" style={{fontSize:14,color:C.accent,flexShrink:0,marginTop:1}} aria-hidden="true"/>
<span style={{fontSize:12.5,color:C.muted,lineHeight:1.6}}>
Trash items older than <strong style={{color:C.text}}>{TRASH_OPTS.find(o=>o.v===trashAfter)?.label.replace("After ","")}</strong> will be permanently deleted from your local database. The SMB share is never modified.
</span>
</div>}
</>;
}
// ── Backup settings ───────────────────────────────────────────────────────────
const MOCK_BACKUP_HISTORY=[
{id:"bk-001",type:"full", date:"2026-05-24 22:00",emails:1842,size:"48.3 MB",status:"ok",incrementals:[
{id:"bk-002",type:"inc",date:"2026-05-25 22:00",emails:12,size:"1.1 MB",status:"ok"},
{id:"bk-003",type:"inc",date:"2026-05-26 22:00",emails:8, size:"0.7 MB",status:"ok"},
{id:"bk-004",type:"inc",date:"2026-05-27 22:00",emails:19,size:"2.4 MB",status:"ok"},
]},
{id:"bk-005",type:"full", date:"2026-04-24 22:00",emails:1803,size:"46.1 MB",status:"ok",incrementals:[]},
];
function SettingsBackup({C}){
const[paths,setPaths]=useState(["C:\\Users\\User\\DashMailBackups"]);
const[newPath,setNewPath]=useState("");
const[schedule,setSchedule]=useState("daily");
const[scheduleDay,setScheduleDay]=useState("1"); // 0=Sun..6=Sat
const[scheduleTime,setScheduleTime]=useState("22:00");
const[bkType,setBkType]=useState("smart");
const[retention,setRetention]=useState("5");
const[history,setHistory]=useState(MOCK_BACKUP_HISTORY);
const[expanded,setExpanded]=useState({"bk-001":true});
const[running,setRunning]=useState(false);
const[runType,setRunType]=useState(null); // "full"|"inc"
const[runMsg,setRunMsg]=useState("");
const[showDropdown,setShowDropdown]=useState(false);
const[showRestore,setShowRestore]=useState(false);
const[restoreFile,setRestoreFile]=useState("");
const[restoreConfirm,setRestoreConfirm]=useState(false);
const[restoreRunning,setRestoreRunning]=useState(false);
const[restoreDone,setRestoreDone]=useState(false);
const dropRef=useRef();
const folderInputRef=useRef();
useEffect(()=>{
function handleClick(e){if(dropRef.current&&!dropRef.current.contains(e.target))setShowDropdown(false);}
document.addEventListener("mousedown",handleClick);
return()=>document.removeEventListener("mousedown",handleClick);
},[]);
async function browseForFolder(){
// Use File System Access API if available (Edge / Chrome on Windows)
if(window.showDirectoryPicker){
try{
const dir=await window.showDirectoryPicker({mode:"read"});
// API gives folder name only (full path blocked by browser security)
// Prefix with a common root so user can correct it in the text field
setNewPath(dir.name);
}catch(e){/* user cancelled */}
} else {
// Fallback: hidden file input with webkitdirectory
folderInputRef.current?.click();
}
}
function addPath(){
const p=newPath.trim();
if(p&&!paths.includes(p)){setPaths(prev=>[...prev,p]);}
setNewPath("");
}
function removePath(i){setPaths(prev=>prev.filter((_,idx)=>idx!==i));}
function startBackup(type){
setRunning(true);setRunType(type);setRunMsg("");setShowDropdown(false);
const label=type==="full"?"Full backup":"Incremental backup";
setTimeout(()=>{
const now=new Date().toISOString().replace("T"," ").slice(0,16);
const newEntry=type==="full"
?{id:"bk-new-f",type:"full",date:now,emails:1860,size:"49.1 MB",status:"ok",incrementals:[]}
:{id:"bk-new-i",type:"inc",date:now,emails:7,size:"0.6 MB",status:"ok"};
if(type==="full"){
setHistory(prev=>[newEntry,...prev]);
} else {
setHistory(prev=>prev.map((b,i)=>i===0?{...b,incrementals:[newEntry,...(b.incrementals||[])]}:b));
}
setRunning(false);setRunMsg(`${label} completed successfully.`);
setTimeout(()=>setRunMsg(""),6000);
},2400);
}
function doRestore(){
setRestoreRunning(true);
setTimeout(()=>{setRestoreRunning(false);setRestoreDone(true);},2800);
}
const inputSt={width:"100%",boxSizing:"border-box",padding:"7px 10px",border:`1px solid ${C.inBorder}`,
borderRadius:5,fontSize:13,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg};
const selSt={...inputSt,width:"auto"};
return <>
{/* ── Backup locations ─────────────────────────────────────── */}
<SectionHead label="Backup locations" C={C}/>
<SCard C={C}>
{paths.map((p,i)=>(
<div key={i} style={{display:"flex",alignItems:"center",gap:8,padding:"9px 0",
borderBottom:i<paths.length-1?`1px solid ${C.border}`:"none"}}>
<i className="ti ti-folder" style={{fontSize:15,color:C.accent,flexShrink:0}} aria-hidden="true"/>
<span style={{flex:1,fontSize:13,color:C.text,wordBreak:"break-all"}}>{p}</span>
<button onClick={()=>removePath(i)} title="Remove location"
style={{background:"none",border:"none",cursor:"pointer",padding:"2px 4px",color:C.subtle,lineHeight:1,fontSize:13,fontFamily:FONT,borderRadius:4}}
onMouseEnter={e=>{e.currentTarget.style.color=C.danger;e.currentTarget.style.background=C.hover;}}
onMouseLeave={e=>{e.currentTarget.style.color=C.subtle;e.currentTarget.style.background="none";}}>
<i className="ti ti-x" style={{fontSize:13}} aria-hidden="true"/>
</button>
</div>
))}
</SCard>
<div style={{display:"flex",gap:8,marginTop:-10,marginBottom:18}}>
{/* Hidden folder picker fallback for browsers without showDirectoryPicker */}
<input ref={folderInputRef} type="file" webkitdirectory="" style={{display:"none"}}
onChange={e=>{
const files=e.target.files;
if(files&&files.length>0){
const folderName=files[0].webkitRelativePath.split("/")[0];
setNewPath(folderName);
}
e.target.value="";
}}/>
<input value={newPath} onChange={e=>setNewPath(e.target.value)}
onKeyDown={e=>{if(e.key==="Enter")addPath();}}
placeholder="Add backup path, e.g. D:\Backups\DashMail"
style={{...inputSt,flex:1}}/>
<button onClick={browseForFolder} title="Browse"
style={{background:C.surface2,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"7px 12px",
fontSize:12.5,cursor:"pointer",fontFamily:FONT,color:C.text,display:"flex",alignItems:"center",gap:5,whiteSpace:"nowrap"}}>
<i className="ti ti-folder-open" style={{fontSize:13}} aria-hidden="true"/> Browse
</button>
<button onClick={addPath}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:5,padding:"7px 14px",
fontSize:12.5,cursor:"pointer",fontFamily:FONT,fontWeight:600,whiteSpace:"nowrap"}}>
+ Add
</button>
</div>
{/* ── Schedule ─────────────────────────────────────────────── */}
<SectionHead label="Schedule" C={C}/>
<SCard C={C}>
<SRow label="Frequency" C={C}>
<select value={schedule} onChange={e=>setSchedule(e.target.value)} style={{...selSt}}>
<option value="manual">Manual only</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</SRow>
{schedule==="weekly"&&<SRow label="Day of week" C={C}>
<select value={scheduleDay} onChange={e=>setScheduleDay(e.target.value)} style={selSt}>
{["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"].map((d,i)=>(
<option key={i} value={String(i)}>{d}</option>
))}
</select>
</SRow>}
{schedule!=="manual"&&<SRow label="Time" sub="24-hour format, local time" C={C}>
<input type="time" value={scheduleTime} onChange={e=>setScheduleTime(e.target.value)}
style={{...selSt,width:110}}/>
</SRow>}
{schedule!=="manual"&&<SRow label="Windows Task Scheduler" sub="DashMail registers a scheduled task automatically" C={C}>
<span style={{fontSize:12,color:"#22c55e",display:"flex",alignItems:"center",gap:4,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:14}} aria-hidden="true"/>Active
</span>
</SRow>}
</SCard>
{/* ── Backup type ──────────────────────────────────────────── */}
<SectionHead label="Backup type" C={C}/>
<SCard C={C}>
{[
{id:"smart", label:"Smart (recommended)", sub:"Incremental daily, full every 30 days or 10 incrementals"},
{id:"full", label:"Always full", sub:"Every backup is a complete copy — safer, uses more storage"},
{id:"inc", label:"Always incremental", sub:"Only new/changed emails backed up each time"},
].map(opt=>(
<div key={opt.id} onClick={()=>setBkType(opt.id)}
style={{display:"flex",alignItems:"flex-start",gap:10,padding:"10px 0",
borderBottom:opt.id!=="inc"?`1px solid ${C.border}`:"none",cursor:"pointer"}}>
<div style={{width:16,height:16,borderRadius:"50%",border:`2px solid ${bkType===opt.id?C.accent:C.inBorder}`,
background:bkType===opt.id?C.accent:"transparent",flexShrink:0,marginTop:2,
display:"flex",alignItems:"center",justifyContent:"center"}}>
{bkType===opt.id&&<div style={{width:6,height:6,borderRadius:"50%",background:"#fff"}}/>}
</div>
<div>
<div style={{fontSize:13.5,fontWeight:bkType===opt.id?600:400,color:C.text}}>{opt.label}</div>
<div style={{fontSize:12,color:C.muted,marginTop:2}}>{opt.sub}</div>
</div>
</div>
))}
</SCard>
{/* ── Retention ────────────────────────────────────────────── */}
<SectionHead label="Retention" C={C}/>
<SCard C={C}>
<SRow label="Keep backup sets" sub="Older backup chains are automatically removed" C={C}>
<select value={retention} onChange={e=>setRetention(e.target.value)} style={selSt}>
{["3","5","10","20","50"].map(n=><option key={n} value={n}>{n} sets</option>)}
</select>
</SRow>
</SCard>
{/* ── Back Up Now ──────────────────────────────────────────── */}
<SectionHead label="Manual backup" C={C}/>
<div style={{display:"flex",alignItems:"center",gap:12,marginBottom:18}}>
<div style={{position:"relative"}} ref={dropRef}>
<div style={{display:"flex"}}>
<button onClick={()=>startBackup(bkType==="full"?"full":"inc")} disabled={running}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:"6px 0 0 6px",
padding:"8px 18px",fontSize:13.5,cursor:running?"default":"pointer",fontFamily:FONT,
fontWeight:600,display:"flex",alignItems:"center",gap:7,opacity:running?0.75:1}}>
{running
?<><i className="ti ti-loader-2 ti-spin" style={{fontSize:14}} aria-hidden="true"/>Backing up…</>
:<><i className="ti ti-database-export" style={{fontSize:14}} aria-hidden="true"/>Back Up Now</>}
</button>
<button disabled={running} onClick={()=>setShowDropdown(v=>!v)}
style={{background:C.accentHov||C.accent,color:"#fff",border:"none",borderLeft:"1px solid rgba(255,255,255,0.25)",
borderRadius:"0 6px 6px 0",padding:"8px 10px",cursor:running?"default":"pointer",fontSize:13,
opacity:running?0.75:1}}>
<i className="ti ti-chevron-down" style={{fontSize:12}} aria-hidden="true"/>
</button>
</div>
{showDropdown&&<div style={{position:"absolute",top:"calc(100% + 4px)",left:0,minWidth:170,
background:C.surface,border:`1px solid ${C.border}`,borderRadius:7,
boxShadow:"0 6px 20px rgba(0,0,0,0.18)",zIndex:50,overflow:"hidden"}}>
{[{t:"full",label:"Full backup",icon:"ti-database"},
{t:"inc", label:"Incremental backup",icon:"ti-database-plus"}].map(o=>(
<div key={o.t} onClick={()=>startBackup(o.t)}
style={{display:"flex",alignItems:"center",gap:8,padding:"9px 14px",
cursor:"pointer",fontSize:13,color:C.text}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${o.icon}`} style={{fontSize:14,color:C.accent}} aria-hidden="true"/>
{o.label}
</div>
))}
</div>}
</div>
{runMsg&&<span style={{fontSize:13,color:"#22c55e",display:"flex",alignItems:"center",gap:5,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:14}} aria-hidden="true"/>{runMsg}
</span>}
</div>
{/* ── Backup history ───────────────────────────────────────── */}
<SectionHead label="Backup history" C={C}/>
{history.length===0
?<p style={{fontSize:13,color:C.muted}}>No backups yet.</p>
:<div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:18}}>
{history.map(b=>{
const open=!!expanded[b.id];
return <div key={b.id} style={{background:C.surface2,border:`1px solid ${C.border}`,borderRadius:8,overflow:"hidden"}}>
{/* Full row */}
<div onClick={()=>setExpanded(p=>({...p,[b.id]:!p[b.id]}))}
style={{display:"flex",alignItems:"center",gap:10,padding:"9px 12px",cursor:"pointer"}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className="ti ti-database" style={{fontSize:15,color:C.accent,flexShrink:0}} aria-hidden="true"/>
<div style={{flex:1,minWidth:0}}>
<div style={{display:"flex",alignItems:"center",gap:7}}>
<span style={{fontSize:12.5,fontWeight:700,color:C.text,textTransform:"uppercase",
letterSpacing:"0.05em",background:C.accentBg||C.selectedBg,color:C.accent,
padding:"1px 6px",borderRadius:3,fontSize:11}}>FULL</span>
<span style={{fontSize:13,color:C.text,fontWeight:600}}>{b.date}</span>
{b.incrementals?.length>0&&<span style={{fontSize:12,color:C.muted}}>+{b.incrementals.length} incremental{b.incrementals.length!==1?"s":""}</span>}
</div>
<div style={{fontSize:12,color:C.muted,marginTop:2}}>{b.emails.toLocaleString()} emails · {b.size}</div>
</div>
<span style={{fontSize:12,color:b.status==="ok"?"#22c55e":C.danger,display:"flex",alignItems:"center",gap:4,fontWeight:600}}>
<i className={`ti ${b.status==="ok"?"ti-circle-check":"ti-alert-circle"}`} style={{fontSize:13}} aria-hidden="true"/>
{b.status==="ok"?"OK":"Error"}
</span>
<button onClick={e=>{e.stopPropagation();setRestoreFile(b.date+" (Full)");setShowRestore(true);setRestoreConfirm(false);setRestoreDone(false);}}
style={{background:"none",border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 10px",
fontSize:12,cursor:"pointer",fontFamily:FONT,color:C.muted,flexShrink:0}}
onMouseEnter={e=>e.currentTarget.style.color=C.text}
onMouseLeave={e=>e.currentTarget.style.color=C.muted}>
Restore
</button>
<i className={`ti ti-chevron-${open?"up":"down"}`} style={{fontSize:13,color:C.subtle,flexShrink:0}} aria-hidden="true"/>
</div>
{/* Incrementals */}
{open&&b.incrementals?.length>0&&<div style={{borderTop:`1px solid ${C.border}`}}>
{b.incrementals.map(inc=>(
<div key={inc.id} style={{display:"flex",alignItems:"center",gap:10,padding:"7px 12px 7px 36px",
borderBottom:`1px solid ${C.border}`}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className="ti ti-database-plus" style={{fontSize:13,color:C.muted,flexShrink:0}} aria-hidden="true"/>
<div style={{flex:1}}>
<div style={{display:"flex",alignItems:"center",gap:7}}>
<span style={{fontSize:11,fontWeight:700,color:C.muted,textTransform:"uppercase",letterSpacing:"0.05em",
background:C.surface3||C.surface,padding:"1px 6px",borderRadius:3}}>INC</span>
<span style={{fontSize:13,color:C.text}}>{inc.date}</span>
</div>
<div style={{fontSize:12,color:C.muted,marginTop:1}}>{inc.emails} new/changed · {inc.size}</div>
</div>
<span style={{fontSize:12,color:"#22c55e",display:"flex",alignItems:"center",gap:4,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:13}} aria-hidden="true"/>OK
</span>
<button onClick={()=>{setRestoreFile(inc.date+" (Incremental)");setShowRestore(true);setRestoreConfirm(false);setRestoreDone(false);}}
style={{background:"none",border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"4px 10px",
fontSize:12,cursor:"pointer",fontFamily:FONT,color:C.muted,flexShrink:0}}
onMouseEnter={e=>e.currentTarget.style.color=C.text}
onMouseLeave={e=>e.currentTarget.style.color=C.muted}>
Restore
</button>
</div>
))}
</div>}
</div>;
})}
</div>}
{/* ── Restore dialog ───────────────────────────────────────── */}
{showRestore&&<div style={{position:"fixed",inset:0,zIndex:500,background:"rgba(0,0,0,0.55)",display:"flex",alignItems:"center",justifyContent:"center"}}
onClick={e=>{if(e.target===e.currentTarget){setShowRestore(false);setRestoreRunning(false);setRestoreDone(false);}}}>
<div style={{background:C.surface,border:`1px solid ${C.border}`,borderRadius:12,padding:"28px 32px",width:420,
boxShadow:"0 20px 50px rgba(0,0,0,0.35)",fontFamily:FONT}}>
{restoreDone
?<>
<div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:12,textAlign:"center"}}>
<i className="ti ti-circle-check" style={{fontSize:40,color:"#22c55e"}} aria-hidden="true"/>
<div style={{fontSize:17,fontWeight:700,color:C.text}}>Restore complete</div>
<div style={{fontSize:13,color:C.muted,lineHeight:1.6}}>DashMail has been restored to the selected backup point. Please restart the application to apply all changes.</div>
</div>
<div style={{display:"flex",justifyContent:"center",marginTop:22}}>
<button onClick={()=>{setShowRestore(false);setRestoreDone(false);}}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"8px 28px",
fontSize:13.5,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>Close</button>
</div>
</>
:!restoreConfirm
?<>
<div style={{fontSize:17,fontWeight:700,color:C.text,marginBottom:16}}>Restore from backup</div>
<div style={{background:C.accentBg||C.selectedBg,border:`1px solid ${C.accentBorder||C.border}`,borderRadius:7,
padding:"10px 14px",marginBottom:16,display:"flex",alignItems:"center",gap:9}}>
<i className="ti ti-database" style={{fontSize:16,color:C.accent,flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:13,color:C.text,fontWeight:500}}>{restoreFile}</span>
</div>
<div style={{background:"#fff3cd",border:"1px solid #ffc107",borderRadius:7,padding:"10px 14px",marginBottom:20,
display:"flex",alignItems:"flex-start",gap:9}}>
<i className="ti ti-alert-triangle" style={{fontSize:15,color:"#b8860b",flexShrink:0,marginTop:1}} aria-hidden="true"/>
<div style={{fontSize:12.5,color:"#7a5c00",lineHeight:1.6}}>
<strong>A safety backup will be created first.</strong> Your current data will be saved before anything is overwritten. The SMB share is never modified.
</div>
</div>
<div style={{marginBottom:20}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Or restore from a backup file</label>
<div style={{display:"flex",gap:8}}>
<input value={restoreFile} onChange={e=>setRestoreFile(e.target.value)} placeholder="C:\Backups\dashmail-2026-05-24.zip" style={inputSt}/>
<label style={{background:C.surface2,border:`1px solid ${C.inBorder}`,borderRadius:5,padding:"7px 12px",
fontSize:12.5,cursor:"pointer",fontFamily:FONT,color:C.text,whiteSpace:"nowrap",display:"flex",alignItems:"center",gap:5}}>
<input type="file" accept=".zip" style={{display:"none"}}
onChange={e=>{
const f=e.target.files?.[0];
if(f){setRestoreFile(f.name);}
e.target.value="";
}}/>
<i className="ti ti-folder-open" style={{fontSize:13}} aria-hidden="true"/> Browse
</label>
</div>
</div>
<div style={{display:"flex",justifyContent:"flex-end",gap:8}}>
<button onClick={()=>setShowRestore(false)}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 18px",
fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Cancel</button>
<button onClick={()=>setRestoreConfirm(true)}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"7px 20px",
fontSize:13.5,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>Next →</button>
</div>
</>
:<>
<div style={{fontSize:17,fontWeight:700,color:C.text,marginBottom:14}}>Confirm restore</div>
<div style={{fontSize:13.5,color:C.text,lineHeight:1.7,marginBottom:20}}>
This will replace your <strong>local DashMail database</strong> with data from:<br/>
<span style={{color:C.accent,fontWeight:600}}>{restoreFile}</span>
</div>
<div style={{background:"#fef2f2",border:"1px solid #fca5a5",borderRadius:7,padding:"10px 14px",marginBottom:22,
display:"flex",alignItems:"flex-start",gap:9}}>
<i className="ti ti-alert-triangle" style={{fontSize:15,color:"#c50f1f",flexShrink:0,marginTop:1}} aria-hidden="true"/>
<div style={{fontSize:12.5,color:"#7f1d1d",lineHeight:1.6}}>
Your current local database will be overwritten. A safety backup will be saved to your first backup location before restore begins. The SMB share will not be touched.
</div>
</div>
<div style={{display:"flex",justifyContent:"flex-end",gap:8}}>
<button onClick={()=>setRestoreConfirm(false)}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 18px",
fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Back</button>
<button onClick={doRestore} disabled={restoreRunning}
style={{background:C.danger||"#c50f1f",color:"#fff",border:"none",borderRadius:6,padding:"7px 20px",
fontSize:13.5,cursor:restoreRunning?"default":"pointer",fontFamily:FONT,fontWeight:600,
display:"flex",alignItems:"center",gap:7,opacity:restoreRunning?0.75:1}}>
{restoreRunning
?<><i className="ti ti-loader-2 ti-spin" style={{fontSize:14}} aria-hidden="true"/>Restoring…</>
:<><i className="ti ti-database-import" style={{fontSize:14}} aria-hidden="true"/>Restore now</>}
</button>
</div>
</>}
</div>
</div>}
</>;
}
// ── Contacts ──────────────────────────────────────────────────────────────────
function RecipientInput({value,onChange,contacts,fieldSt,C}){
const toChips=v=>v?v.split(",").map(s=>s.trim()).filter(Boolean):[];
const[chips,setChips]=useState(()=>toChips(value));
const[inputVal,setInputVal]=useState("");
const[open,setOpen]=useState(false);
const[candidates,setCandidates]=useState([]);
const inputRef=useRef();
const internal=useRef(false);
useEffect(()=>{
if(internal.current){internal.current=false;return;}
setChips(toChips(value));
},[value]);
function syncUp(newChips){
internal.current=true;
setChips(newChips);
onChange(newChips.join(", "));
}
function addChip(str){
const s=str.trim();
if(!s)return;
syncUp([...chips,s]);
setInputVal("");setOpen(false);
}
function removeChip(i){syncUp(chips.filter((_,idx)=>idx!==i));}
function handleKey(e){
if((e.key===","||e.key==="Enter"||e.key==="Tab")&&inputVal.trim()){
e.preventDefault();addChip(inputVal);
}
if(e.key==="Backspace"&&!inputVal&&chips.length>0) syncUp(chips.slice(0,-1));
}
function filter(v){
if(!v.trim()){setOpen(false);return;}
const m=contacts.filter(c=>
c.name.toLowerCase().includes(v.toLowerCase())||
c.email.toLowerCase().includes(v.toLowerCase())
).slice(0,6);
setCandidates(m);setOpen(m.length>0);
}
function pick(c){addChip(c.name+" <"+c.email+">");inputRef.current?.focus();}
return <div style={{flex:1,position:"relative",display:"flex",flexWrap:"wrap",alignItems:"center",
gap:"3px 5px",cursor:"text",paddingTop:3,paddingBottom:3,minHeight:28}}
onClick={()=>inputRef.current?.focus()}>
{chips.map((chip,i)=>(
<div key={i} style={{display:"flex",alignItems:"center",gap:3,
background:C.accentBg,border:`1px solid ${C.accentBorder}`,borderRadius:4,
padding:"2px 4px 2px 8px",fontSize:12.5,color:C.text,maxWidth:220,flexShrink:0}}>
<span style={{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{chip}</span>
<button onMouseDown={e=>{e.preventDefault();removeChip(i);}}
style={{background:"transparent",border:"none",padding:"0 2px",cursor:"pointer",
color:C.muted,fontSize:11,lineHeight:1,display:"flex",alignItems:"center"}}>
<i className="ti ti-x" aria-hidden="true"/>
</button>
</div>
))}
<input ref={inputRef} value={inputVal}
onChange={e=>{setInputVal(e.target.value);filter(e.target.value);}}
onKeyDown={handleKey}
onBlur={()=>{setTimeout(()=>setOpen(false),150);if(inputVal.trim())addChip(inputVal);}}
style={{...fieldSt,flex:"1 1 80px",minWidth:60,padding:0}}/>
{open&&<div style={{position:"absolute",top:"calc(100% + 2px)",left:0,right:0,zIndex:9999,
background:C.surface,border:`1px solid ${C.border}`,borderRadius:7,
boxShadow:"0 6px 20px rgba(0,0,0,0.18)",maxHeight:220,overflowY:"auto"}}>
{candidates.map(c=>(
<div key={c.id} onMouseDown={e=>{e.preventDefault();pick(c);}}
style={{display:"flex",alignItems:"center",gap:9,padding:"8px 12px",cursor:"pointer"}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<Avatar initials={c.av} color={c.avColor} size={28}/>
<div>
<div style={{fontSize:13,fontWeight:600,color:C.text}}>{c.name}</div>
<div style={{fontSize:11.5,color:C.subtle}}>{c.email}</div>
</div>
</div>
))}
</div>}
</div>;
}
function ContactPickerModal({contacts,onClose,onAdd,C}){
const[search,setSearch]=useState("");
const[sel,setSel]=useState(new Set());
const filtered=search?contacts.filter(c=>
c.name.toLowerCase().includes(search.toLowerCase())||
c.email.toLowerCase().includes(search.toLowerCase())
):contacts;
function toggle(id){setSel(s=>{const n=new Set(s);n.has(id)?n.delete(id):n.add(id);return n;});}
return <div style={{position:"fixed",inset:0,zIndex:9998,background:"rgba(0,0,0,0.45)",display:"flex",alignItems:"center",justifyContent:"center"}}>
<div style={{width:"min(440px,92vw)",height:"min(520px,85vh)",background:C.surface,borderRadius:12,
border:`1px solid ${C.border}`,boxShadow:"0 20px 60px rgba(0,0,0,0.3)",
display:"flex",flexDirection:"column",overflow:"hidden",fontFamily:FONT}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 18px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<span style={{fontSize:16,fontWeight:700,color:C.text}}>Add from Contacts</span>
<IBtn icon="ti-x" C={C} onClick={onClose}/>
</div>
<div style={{padding:"10px 14px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search contacts…"
style={{width:"100%",boxSizing:"border-box",padding:"7px 10px",border:`1px solid ${C.inBorder}`,
borderRadius:6,fontSize:13,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
</div>
<div style={{flex:1,overflowY:"auto"}}>
{filtered.map(c=>{
const on=sel.has(c.id);
return <div key={c.id} onClick={()=>toggle(c.id)}
style={{display:"flex",alignItems:"center",gap:10,padding:"10px 14px",cursor:"pointer",
background:on?C.accentBg:"transparent"}}
onMouseEnter={e=>{if(!on)e.currentTarget.style.background=C.hover;}}
onMouseLeave={e=>{if(!on)e.currentTarget.style.background="transparent";}}>
<Avatar initials={c.av} color={c.avColor} size={36}/>
<div style={{flex:1}}>
<div style={{fontSize:13.5,fontWeight:600,color:C.text}}>{c.name}</div>
<div style={{fontSize:12,color:C.subtle}}>{c.email}</div>
</div>
<div style={{width:20,height:20,borderRadius:"50%",border:`2px solid ${on?C.accent:C.border}`,
background:on?C.accent:"transparent",display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
{on&&<i className="ti ti-check" style={{fontSize:11,color:"#fff"}} aria-hidden="true"/>}
</div>
</div>;
})}
{filtered.length===0&&<div style={{padding:24,textAlign:"center",color:C.muted,fontSize:13}}>No contacts found</div>}
</div>
<div style={{padding:"12px 16px",borderTop:`1px solid ${C.border}`,display:"flex",gap:8,justifyContent:"flex-end",flexShrink:0,background:C.surface2}}>
<button onClick={onClose} style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 14px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Cancel</button>
<button onClick={()=>{onAdd(contacts.filter(c=>sel.has(c.id)));onClose();}}
disabled={sel.size===0}
style={{background:sel.size>0?C.accent:"#aaa",color:"#fff",border:"none",borderRadius:6,padding:"7px 18px",fontSize:13,cursor:sel.size>0?"pointer":"default",fontFamily:FONT,fontWeight:600}}>
Add{sel.size>0?` (${sel.size})`:""} to To
</button>
</div>
</div>
</div>;
}
function ContactForm({contact,onSave,onCancel,C}){
const[name,setName]=useState(contact?.name??"");
const[email,setEmail]=useState(contact?.email??"");
const[phone,setPhone]=useState(contact?.phone??"");
const[address,setAddress]=useState(contact?.address??"");
const[av,setAv]=useState(contact?.av??"");
const[avColor,setAvColor]=useState(contact?.avColor??ACCENTS[0]);
const[emailErr,setEmailErr]=useState("");
function autoInitials(n){
const parts=n.trim().split(/\s+/);
return parts.length>=2?(parts[0][0]+parts[parts.length-1][0]).toUpperCase():n.slice(0,2).toUpperCase();
}
function handleName(v){
setName(v);
if(!av||av===autoInitials(name))setAv(autoInitials(v));
}
function handleSave(){
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())){setEmailErr("Enter a valid email address.");return;}
onSave({name:name.trim(),email:email.trim(),phone:phone.trim(),address:address.trim(),av:av.slice(0,2)||"?",avColor});
}
const inputSt={width:"100%",boxSizing:"border-box",padding:"7px 10px",border:`1px solid ${C.inBorder}`,
borderRadius:5,fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg};
return <div style={{padding:"14px 0 6px"}}>
<div style={{display:"flex",gap:14,marginBottom:12}}>
<div style={{flex:2}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Full name</label>
<input value={name} onChange={e=>handleName(e.target.value)} placeholder="Sarah Chen" style={inputSt}/>
</div>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Initials</label>
<input value={av} onChange={e=>setAv(e.target.value.toUpperCase().slice(0,2))} maxLength={2}
style={{width:56,padding:"7px 10px",border:`1px solid ${C.inBorder}`,borderRadius:5,
fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg,
textAlign:"center",fontWeight:700,boxSizing:"border-box"}}/>
</div>
</div>
<div style={{marginBottom:12}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Email address</label>
<input value={email} onChange={e=>{setEmail(e.target.value);setEmailErr("");}} placeholder="sarah@example.com" type="email"
style={{...inputSt,border:`1px solid ${emailErr?"#c50f1f":C.inBorder}`}}/>
{emailErr&&<div style={{fontSize:12,color:"#c50f1f",marginTop:4}}>{emailErr}</div>}
</div>
<div style={{display:"flex",gap:12,marginBottom:12}}>
<div style={{flex:1}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Phone <span style={{fontWeight:400,color:C.subtle,fontSize:11.5}}>(optional)</span></label>
<input value={phone} onChange={e=>setPhone(e.target.value)} placeholder="+1 555 000 0000" type="tel" style={inputSt}/>
</div>
</div>
<div style={{marginBottom:14}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Address <span style={{fontWeight:400,color:C.subtle,fontSize:11.5}}>(optional)</span></label>
<textarea value={address} onChange={e=>setAddress(e.target.value)} placeholder="123 Main St, City, State 00000" rows={2}
style={{...inputSt,resize:"vertical",lineHeight:1.5,fontFamily:FONT}}/>
</div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:7}}>Avatar color</label>
<div style={{display:"flex",gap:7,flexWrap:"wrap",marginBottom:14}}>
{ACCENTS.map(a=>(
<div key={a} onClick={()=>setAvColor(a)}
style={{width:24,height:24,borderRadius:5,background:a,cursor:"pointer",
border:`2.5px solid ${a===avColor?"#fff":"transparent"}`,
outline:a===avColor?`2px solid ${a}`:"none",transition:"transform 0.1s"}}
onMouseEnter={e=>e.currentTarget.style.transform="scale(1.15)"}
onMouseLeave={e=>e.currentTarget.style.transform="scale(1)"}/>
))}
</div>
{(name||email)&&<div style={{display:"flex",alignItems:"center",gap:10,padding:"10px 12px",
background:C.accentBg,borderRadius:7,marginBottom:14}}>
<Avatar initials={av||"?"} color={avColor} size={36}/>
<div>
<div style={{fontSize:13,fontWeight:600,color:C.text}}>{name||"Name"}</div>
<div style={{fontSize:12,color:C.subtle}}>{email||"email@example.com"}</div>
{phone&&<div style={{fontSize:11.5,color:C.subtle}}>{phone}</div>}
</div>
</div>}
<div style={{display:"flex",gap:8}}>
<button onClick={handleSave}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:5,padding:"7px 18px",
fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>
{contact?"Save changes":"Add contact"}
</button>
<button onClick={onCancel}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,
padding:"7px 12px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>
Cancel
</button>
</div>
</div>;
}
function parseCSVLine(line){
const result=[];let cur="";let inQ=false;
for(let i=0;i<line.length;i++){
const ch=line[i];
if(ch==='"'){inQ=!inQ;}
else if(ch===","&&!inQ){result.push(cur.trim().replace(/^"|"$/g,""));cur="";}
else{cur+=ch;}
}
result.push(cur.trim().replace(/^"|"$/g,""));
return result;
}
function importCSV(text){
const lines=text.trim().split(/\r?\n/).filter(l=>l.trim());
if(lines.length<2)return[];
const headers=parseCSVLine(lines[0]).map(h=>h.toLowerCase());
const col=(...names)=>headers.findIndex(h=>names.some(n=>h.includes(n)));
const nameCol=col("name","full name","contact name");
const emailCol=col("email","e-mail","mail");
const phoneCol=col("phone","tel","mobile","cell","number");
const addressCol=col("address","street","addr","location","city");
return lines.slice(1).map(line=>{
const cols=parseCSVLine(line);
const name=(nameCol>=0?cols[nameCol]:"").trim();
const email=(emailCol>=0?cols[emailCol]:"").trim();
if(!name&&!email)return null;
const parts=(name||"").trim().split(/\s+/);
const av=parts.length>=2?(parts[0][0]+parts[parts.length-1][0]).toUpperCase():(name||email).slice(0,2).toUpperCase();
return{name:name||email,email,phone:(phoneCol>=0?cols[phoneCol]:"").trim(),address:(addressCol>=0?cols[addressCol]:"").trim(),av,avColor:ACCENTS[Math.floor(Math.random()*ACCENTS.length)]};
}).filter(Boolean);
}
function ContactsManager({contacts,setContacts,onClose,onCompose,C}){
const[search,setSearch]=useState("");
const[editId,setEditId]=useState(null);
const[confirmDelete,setConfirmDelete]=useState(null);
const[importToast,setImportToast]=useState(null);
const csvRef=useRef();
const filtered=search
?contacts.filter(c=>c.name.toLowerCase().includes(search.toLowerCase())||c.email.toLowerCase().includes(search.toLowerCase())||(c.phone||"").includes(search))
:contacts;
function addContact(data){setContacts(p=>[...p,{id:"ct"+uid(),...data}]);setEditId(null);}
function saveContact(id,data){setContacts(p=>p.map(c=>c.id===id?{...c,...data}:c));setEditId(null);}
function deleteContact(id){setContacts(p=>p.filter(c=>c.id!==id));setConfirmDelete(null);if(editId===id)setEditId(null);}
function handleCSV(e){
const file=e.target.files[0];
if(!file)return;
const reader=new FileReader();
reader.onload=ev=>{
const parsed=importCSV(ev.target.result);
if(parsed.length===0){setImportToast({type:"err",msg:"No valid contacts found in CSV."});setTimeout(()=>setImportToast(null),3500);return;}
setContacts(p=>{
const existing=new Set(p.map(c=>c.email.toLowerCase()));
const added=parsed.filter(c=>!existing.has(c.email.toLowerCase())).map(c=>({...c,id:"ct"+uid()}));
const skipped=parsed.length-added.length;
setImportToast({type:"ok",msg:`Imported ${added.length} contact${added.length!==1?"s":""}${skipped>0?`, ${skipped} skipped (duplicate)`:"."}`});
setTimeout(()=>setImportToast(null),4000);
return[...p,...added];
});
};
reader.readAsText(file);
e.target.value="";
}
return <div style={{position:"fixed",inset:0,zIndex:350,background:"rgba(0,0,0,0.6)",
display:"flex",alignItems:"center",justifyContent:"center",fontFamily:FONT}}>
<div style={{width:"min(600px,92vw)",height:"min(700px,90vh)",background:C.surface,borderRadius:12,
border:`1px solid ${C.border}`,boxShadow:`0 20px 60px rgba(0,0,0,${C.surface==="#ffffff"?0.25:0.7})`,
display:"flex",flexDirection:"column",overflow:"hidden"}}>
{/* Header */}
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",
padding:"16px 20px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<div style={{display:"flex",alignItems:"center",gap:10}}>
<i className="ti ti-address-book" style={{fontSize:22,color:C.accent}} aria-hidden="true"/>
<span style={{fontSize:18,fontWeight:700,color:C.text}}>Contacts</span>
<span style={{fontSize:12,color:C.muted,background:C.surface2,border:`1px solid ${C.border}`,
borderRadius:10,padding:"1px 8px",fontWeight:600}}>{contacts.length}</span>
</div>
<IBtn icon="ti-x" title="Close" C={C} onClick={onClose} size={18}/>
</div>
{/* Search + action bar */}
<div style={{display:"flex",gap:8,padding:"10px 16px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<div style={{flex:1,position:"relative"}}>
<i className="ti ti-search" style={{position:"absolute",left:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:C.subtle}} aria-hidden="true"/>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search contacts…"
style={{width:"100%",boxSizing:"border-box",padding:"7px 9px 7px 28px",
border:`1px solid ${C.inBorder}`,borderRadius:6,fontSize:13,
fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
</div>
<input ref={csvRef} type="file" accept=".csv,text/csv" onChange={handleCSV} style={{display:"none"}}/>
<button onClick={()=>csvRef.current.click()}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,padding:"7px 12px",
fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted,
display:"flex",alignItems:"center",gap:6,flexShrink:0,whiteSpace:"nowrap"}}
title="Import contacts from a CSV file">
<i className="ti ti-file-import" style={{fontSize:14}} aria-hidden="true"/>Import CSV
</button>
<button onClick={()=>setEditId("__new__")}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,padding:"7px 14px",
fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600,
display:"flex",alignItems:"center",gap:6,flexShrink:0,whiteSpace:"nowrap"}}>
<i className="ti ti-plus" style={{fontSize:14}} aria-hidden="true"/>New Contact
</button>
</div>
{/* Import toast */}
{importToast&&<div style={{padding:"9px 16px",background:importToast.type==="ok"?"#f0fdf4":"#fef2f2",
borderBottom:`1px solid ${importToast.type==="ok"?"#86efac":"#fecaca"}`,flexShrink:0,
display:"flex",alignItems:"center",gap:8,fontSize:13}}>
<i className={`ti ${importToast.type==="ok"?"ti-circle-check":"ti-alert-circle"}`}
style={{fontSize:15,color:importToast.type==="ok"?"#22c55e":"#ef4444",flexShrink:0}} aria-hidden="true"/>
<span style={{color:importToast.type==="ok"?"#15803d":"#dc2626"}}>{importToast.msg}</span>
<button onClick={()=>setImportToast(null)} style={{marginLeft:"auto",background:"transparent",border:"none",cursor:"pointer",color:"#94a3b8",padding:0,lineHeight:1}}>
<i className="ti ti-x" style={{fontSize:13}} aria-hidden="true"/>
</button>
</div>}
{/* New contact form */}
{editId==="__new__"&&<div style={{padding:"0 18px",borderBottom:`1px solid ${C.border}`,
background:C.surface2,flexShrink:0,overflowY:"auto",maxHeight:420}}>
<ContactForm C={C} onSave={addContact} onCancel={()=>setEditId(null)}/>
</div>}
{/* List */}
<div style={{flex:1,overflowY:"auto"}}>
{filtered.length===0&&<div style={{padding:32,textAlign:"center",color:C.muted,fontSize:13}}>
<i className="ti ti-user-off" style={{fontSize:32,display:"block",marginBottom:8,color:C.subtle}} aria-hidden="true"/>
{search?"No contacts match your search":"No contacts yet"}
</div>}
{filtered.map(c=>{
const isEditing=editId===c.id;
return <div key={c.id} style={{borderBottom:`1px solid ${C.border}`,background:isEditing?C.surface2:"transparent"}}>
<div style={{display:"flex",alignItems:"center",gap:12,padding:"10px 16px"}}
onMouseEnter={e=>{if(!isEditing)e.currentTarget.style.background=C.hover;}}
onMouseLeave={e=>{if(!isEditing)e.currentTarget.style.background="transparent";}}>
<Avatar initials={c.av} color={c.avColor} size={38}/>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:13.5,fontWeight:600,color:C.text}}>{c.name}</div>
<div style={{fontSize:12,color:C.subtle}}>{c.email}</div>
{(c.phone||c.address)&&<div style={{display:"flex",gap:10,marginTop:2,flexWrap:"wrap"}}>
{c.phone&&<span style={{fontSize:11.5,color:C.subtle,display:"flex",alignItems:"center",gap:3}}>
<i className="ti ti-phone" style={{fontSize:11}} aria-hidden="true"/>{c.phone}
</span>}
{c.address&&<span style={{fontSize:11.5,color:C.subtle,display:"flex",alignItems:"center",gap:3,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:200}}>
<i className="ti ti-map-pin" style={{fontSize:11}} aria-hidden="true"/>{c.address}
</span>}
</div>}
</div>
<div style={{display:"flex",gap:2,flexShrink:0}}>
<IBtn icon="ti-mail" title="Compose to" C={C} size={14} onClick={()=>onCompose&&onCompose(c)}/>
<IBtn icon={isEditing?"ti-chevron-up":"ti-pencil"} title={isEditing?"Collapse":"Edit"} C={C} size={14}
onClick={()=>setEditId(isEditing?null:c.id)}/>
<IBtn icon="ti-trash" title="Delete" C={C} danger size={14}
onClick={()=>setConfirmDelete(c.id)}/>
</div>
</div>
{isEditing&&<div style={{padding:"0 16px",borderTop:`1px solid ${C.border}`}}>
<ContactForm contact={c} C={C}
onSave={data=>saveContact(c.id,data)}
onCancel={()=>setEditId(null)}/>
</div>}
</div>;
})}
</div>
{/* Footer */}
<div style={{padding:"10px 16px",borderTop:`1px solid ${C.border}`,background:C.surface2,
flexShrink:0,display:"flex",alignItems:"center",justifyContent:"space-between"}}>
<span style={{fontSize:12,color:C.subtle}}>CSV columns: name, email, phone, address</span>
<button onClick={onClose}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,
padding:"7px 18px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>
Done
</button>
</div>
</div>
{/* Confirm delete */}
{confirmDelete&&<div style={{position:"fixed",inset:0,zIndex:400,background:"rgba(0,0,0,0.5)",
display:"flex",alignItems:"center",justifyContent:"center"}}>
<div style={{width:"min(360px,90vw)",background:C.surface,borderRadius:10,
border:`1px solid ${C.border}`,padding:"24px 22px",boxShadow:"0 12px 40px rgba(0,0,0,0.3)",fontFamily:FONT}}>
<div style={{fontSize:16,fontWeight:700,color:C.text,marginBottom:10}}>Delete contact?</div>
<div style={{fontSize:13.5,color:C.muted,marginBottom:20,lineHeight:1.6}}>
"{contacts.find(c=>c.id===confirmDelete)?.name}" will be permanently removed.
</div>
<div style={{display:"flex",gap:8,justifyContent:"flex-end"}}>
<button onClick={()=>setConfirmDelete(null)}
style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:6,
padding:"7px 14px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>
Cancel
</button>
<button onClick={()=>deleteContact(confirmDelete)}
style={{background:"#c50f1f",color:"#fff",border:"none",borderRadius:6,
padding:"7px 18px",fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>
Delete
</button>
</div>
</div>
</div>}
</div>;
}
// ── Compose Modal ─────────────────────────────────────────────────────────────
function ComposeModal({mode,initial,fromEmail,onClose,onSend,C,contacts=[]}){
const TITLES={new:"New Message",reply:"Reply",replyAll:"Reply All",forward:"Forward",forwardAsAtt:"Forward as Attachment"};
const[to,setTo]=useState(initial?.to??"");
const[cc,setCc]=useState(initial?.cc??"");
const[bcc,setBcc]=useState(initial?.bcc??"");
const[subj,setSubj]=useState(initial?.subject??"");
const[showCc,setShowCc]=useState(!!(initial?.cc));
const[showBcc,setShowBcc]=useState(false);
const[minimized,setMin]=useState(false);
const[attachments,setAtts]=useState(initial?.attachments??[]);
const[dragging,setDrag]=useState(false);
const[showFmt,setShowFmt]=useState(false);
const[bodyEmpty,setBodyEmpty]=useState(!initial?.body);
const[emojiHint,setEmojiHint]=useState(false);
const[showContacts,setShowContacts]=useState(false);
const fileRef=useRef();
const bodyRef=useRef();
useEffect(()=>{
if(!bodyRef.current)return;
const html=initial?.body
?initial.body.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>')
:'';
bodyRef.current.innerHTML=html;
setBodyEmpty(!html);
},[]);
function applyFormat(cmd){bodyRef.current?.focus();document.execCommand(cmd,false,null);}
function getBody(){return bodyRef.current?.innerHTML??'';}
function handleFiles(e){
const files=Array.from(e.target.files);
setAtts(prev=>{const ex=new Set(prev.map(f=>f.name));return[...prev,...files.filter(f=>!ex.has(f.name))];});
e.target.value="";
}
function handleDrop(e){
e.preventDefault();setDrag(false);
const files=Array.from(e.dataTransfer.files);
setAtts(prev=>{const ex=new Set(prev.map(f=>f.name));return[...prev,...files.filter(f=>!ex.has(f.name))];});
}
function removeAtt(name){setAtts(prev=>prev.filter(f=>f.name!==name));}
function getFileIcon(name){
const ext=(name.split(".").pop()||"").toLowerCase();
if(["jpg","jpeg","png","gif","svg","webp"].includes(ext))return "ti-photo";
if(["pdf"].includes(ext))return "ti-file-type-pdf";
if(["doc","docx"].includes(ext))return "ti-file-type-doc";
if(["xls","xlsx"].includes(ext))return "ti-file-type-xls";
if(["zip","gz","tar","7z"].includes(ext))return "ti-file-zip";
if(["eml","msg"].includes(ext))return "ti-mail";
return "ti-file";
}
const fieldSt={flex:1,border:"none",outline:"none",fontSize:13,fontFamily:FONT,background:"transparent",color:C.text};
const rowSt={display:"flex",alignItems:"center",borderBottom:`1px solid ${C.border}`,padding:"6px 0"};
return <div style={{position:"fixed",top:"50%",left:"50%",transform:"translate(-50%,-50%)",width:"min(660px,88vw)",zIndex:200,background:C.surface,
border:`1px solid ${C.border}`,borderRadius:10,
boxShadow:`0 16px 56px rgba(0,0,0,${C.surface==="#ffffff"?0.22:0.6})`,
display:"flex",flexDirection:"column",overflow:"hidden",fontFamily:FONT,
height:minimized?42:"80vh",transition:"height 0.2s"}}>
{/* Title bar */}
<div style={{background:C.accent,padding:"10px 14px",display:"flex",justifyContent:"space-between",alignItems:"center",flexShrink:0,cursor:"pointer"}}
onClick={()=>setMin(m=>!m)}>
<span style={{color:"#fff",fontSize:13.5,fontWeight:600}}>{TITLES[mode]??"Compose"}</span>
<div style={{display:"flex",gap:4}} onClick={e=>e.stopPropagation()}>
<button onClick={()=>setMin(m=>!m)} style={{background:"transparent",border:"none",color:"rgba(255,255,255,0.85)",cursor:"pointer",padding:"2px 6px",fontSize:16}}>
<i className={`ti ti-${minimized?"chevron-up":"minus"}`} aria-hidden="true"/>
</button>
<button onClick={onClose} style={{background:"transparent",border:"none",color:"rgba(255,255,255,0.85)",cursor:"pointer",padding:"2px 6px",fontSize:16}}>
<i className="ti ti-x" aria-hidden="true"/>
</button>
</div>
</div>
{!minimized&&<>
{/* Header fields */}
<div style={{padding:"0 14px",background:C.surface,flexShrink:0}}>
<div style={rowSt}>
<span style={{fontSize:12.5,color:C.muted,width:50,flexShrink:0}}>From</span>
<input value={fromEmail??""} readOnly style={{...fieldSt,color:C.muted}}/>
</div>
{[["To",to,setTo],...(showCc?[["Cc",cc,setCc]]:[]),...(showBcc?[["Bcc",bcc,setBcc]]:[])].map(([lbl,val,set])=>(
<div key={lbl} style={{...rowSt,alignItems:"flex-start",paddingTop:6,paddingBottom:4}}>
<span style={{fontSize:12.5,color:C.muted,width:50,flexShrink:0,paddingTop:4}}>{lbl}</span>
<RecipientInput value={val} onChange={set} contacts={contacts} fieldSt={fieldSt} C={C}/>
</div>
))}
<div style={rowSt}>
<span style={{fontSize:12.5,color:C.muted,width:50,flexShrink:0}}>Subject</span>
<input value={subj} onChange={e=>setSubj(e.target.value)}
style={{...fieldSt,fontWeight:600}}/>
<div style={{display:"flex",gap:3,flexShrink:0}}>
{!showCc &&<button onClick={()=>setShowCc(true)} style={{fontSize:11,color:C.muted,background:"transparent",border:`1px solid ${C.border}`,borderRadius:3,padding:"1px 6px",cursor:"pointer",fontFamily:FONT}}>Cc</button>}
{!showBcc&&<button onClick={()=>setShowBcc(true)} style={{fontSize:11,color:C.muted,background:"transparent",border:`1px solid ${C.border}`,borderRadius:3,padding:"1px 6px",cursor:"pointer",fontFamily:FONT}}>Bcc</button>}
</div>
</div>
</div>
{/* Attachment pills */}
{attachments.length>0&&<div style={{padding:"8px 14px",borderBottom:`1px solid ${C.border}`,background:C.surface2,display:"flex",flexWrap:"wrap",gap:6,flexShrink:0}}>
{attachments.map(f=>(
<div key={f.name} style={{display:"flex",alignItems:"center",gap:5,background:C.surface,
border:`1px solid ${C.accentBorder}`,borderRadius:5,padding:"4px 8px",fontSize:12}}>
<i className={`ti ${getFileIcon(f.name)}`} style={{fontSize:14,color:C.accent}} aria-hidden="true"/>
<span style={{color:C.text,maxWidth:140,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{f.name}</span>
<span style={{color:C.subtle,flexShrink:0}}>{formatSize(f.size)}</span>
<button onClick={()=>removeAtt(f.name)} style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:0,lineHeight:1}}>
<i className="ti ti-x" style={{fontSize:13}} aria-hidden="true"/>
</button>
</div>
))}
</div>}
{/* Body + drop zone */}
<div style={{flex:1,position:"relative"}}
onDragOver={e=>{e.preventDefault();setDrag(true);}}
onDragLeave={()=>setDrag(false)}
onDrop={handleDrop}>
{bodyEmpty&&<div style={{position:"absolute",top:12,left:14,color:C.subtle,fontSize:13.5,fontFamily:FONT,pointerEvents:"none",userSelect:"none",zIndex:1}}>Write your message here…</div>}
<div ref={bodyRef} contentEditable suppressContentEditableWarning
onInput={e=>{const t=e.currentTarget;setBodyEmpty(!t.textContent.trim());}}
style={{width:"100%",height:"100%",minHeight:180,border:"none",outline:"none",
padding:"12px 14px",fontSize:13.5,fontFamily:FONT,
color:C.text,background:"transparent",boxSizing:"border-box",
overflowY:"auto",lineHeight:1.65}}/>
{dragging&&<div style={{position:"absolute",inset:0,background:C.accentBg,
border:`2px dashed ${C.accent}`,borderRadius:4,display:"flex",
alignItems:"center",justifyContent:"center",pointerEvents:"none"}}>
<span style={{color:C.accent,fontSize:15,fontWeight:600}}>Drop files to attach</span>
</div>}
</div>
{showFmt&&<div style={{display:"flex",gap:3,padding:"5px 12px",borderTop:`1px solid ${C.border}`,background:C.surface2,flexShrink:0,alignItems:"center"}}>
{[{lbl:"B",cmd:"bold",st:{fontWeight:800}},{lbl:"I",cmd:"italic",st:{fontStyle:"italic"}},{lbl:"U",cmd:"underline",st:{textDecoration:"underline"}},{lbl:"S",cmd:"strikeThrough",st:{textDecoration:"line-through"}}].map(({lbl,cmd,st})=>(
<button key={cmd} onMouseDown={e=>{e.preventDefault();applyFormat(cmd);}}
style={{fontFamily:FONT,fontSize:13,width:28,height:28,border:`1px solid ${C.border}`,borderRadius:4,cursor:"pointer",background:C.surface,color:C.text,display:"flex",alignItems:"center",justifyContent:"center",...st}}>
{lbl}</button>
))}
<div style={{width:1,height:18,background:C.border,margin:"0 2px"}}/>
<button onMouseDown={e=>{e.preventDefault();applyFormat('justifyLeft');}} title="Align left" style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"0 4px",fontSize:15}}><i className="ti ti-align-left"/></button>
<button onMouseDown={e=>{e.preventDefault();applyFormat('justifyCenter');}} title="Align center" style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"0 4px",fontSize:15}}><i className="ti ti-align-center"/></button>
<button onMouseDown={e=>{e.preventDefault();applyFormat('justifyRight');}} title="Align right" style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"0 4px",fontSize:15}}><i className="ti ti-align-right"/></button>
<div style={{width:1,height:18,background:C.border,margin:"0 2px"}}/>
<button onMouseDown={e=>{e.preventDefault();applyFormat('insertUnorderedList');}} title="Bullet list" style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"0 4px",fontSize:15}}><i className="ti ti-list"/></button>
<button onMouseDown={e=>{e.preventDefault();applyFormat('insertOrderedList');}} title="Numbered list" style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"0 4px",fontSize:15}}><i className="ti ti-list-numbers"/></button>
</div>}
{/* Footer */}
<div style={{display:"flex",alignItems:"center",gap:6,padding:"8px 12px",
borderTop:`1px solid ${C.border}`,background:C.surface2,flexShrink:0,flexWrap:"wrap"}}>
<button onClick={()=>{onSend({to,cc,bcc,subject:subj,body:getBody(),attachments});onClose();}}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:5,
padding:"7px 18px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,fontWeight:600,
display:"flex",alignItems:"center",gap:6}}>
<i className="ti ti-send" style={{fontSize:14}} aria-hidden="true"/>Send
</button>
<button onClick={onClose} style={{background:"transparent",border:`1px solid ${C.border}`,
borderRadius:5,padding:"7px 12px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Discard</button>
<div style={{flex:1}}/>
{/* Hidden file input */}
<input ref={fileRef} type="file" multiple onChange={handleFiles} style={{display:"none"}}/>
<button onClick={()=>fileRef.current.click()}
title="Attach files" style={{display:"flex",alignItems:"center",gap:5,
background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,
padding:"6px 10px",cursor:"pointer",fontFamily:FONT,color:C.muted,fontSize:13}}>
<i className="ti ti-paperclip" style={{fontSize:15}} aria-hidden="true"/>
Attach
</button>
<IBtn icon="ti-address-book" title="Contacts" C={C} onClick={()=>setShowContacts(true)}/>
<IBtn icon="ti-text-size" title="Format text" active={showFmt} C={C} onClick={()=>setShowFmt(f=>!f)}/>
<div style={{position:"relative"}}>
<IBtn icon="ti-mood-smile" title="Emoji" C={C} onClick={()=>{bodyRef.current?.focus();setEmojiHint(true);setTimeout(()=>setEmojiHint(false),2500);}}/>
{emojiHint&&<div style={{position:"absolute",bottom:"calc(100% + 8px)",right:0,background:"#1c1c1c",color:"#fff",fontSize:12,padding:"6px 10px",borderRadius:6,whiteSpace:"nowrap",pointerEvents:"none",boxShadow:"0 4px 12px rgba(0,0,0,0.3)",zIndex:9999}}>
Press <kbd style={{background:"#444",borderRadius:3,padding:"1px 5px",fontFamily:"monospace",fontSize:11}}>⊞ Win</kbd> + <kbd style={{background:"#444",borderRadius:3,padding:"1px 5px",fontFamily:"monospace",fontSize:11}}>.</kbd> to open emoji picker
<div style={{position:"absolute",bottom:-5,right:12,width:10,height:10,background:"#1c1c1c",transform:"rotate(45deg)"}}/>
</div>}
</div>
<IBtn icon="ti-dots" title="More options" C={C}/>
</div>
</>}
{showContacts&&<ContactPickerModal contacts={contacts} C={C} onClose={()=>setShowContacts(false)}
onAdd={picked=>{
const emails=picked.map(c=>c.name+" <"+c.email+">").join(", ");
setTo(prev=>prev?(prev+", "+emails):emails);
}}/>}
</div>;
}
// ── Rules Panel ───────────────────────────────────────────────────────────────
const RULE_FIELDS=[{v:"from",l:"From"},{v:"to",l:"To"},{v:"subject",l:"Subject"},{v:"body",l:"Body"},{v:"hasAtt",l:"Has attachment"}];
const RULE_OPS=[{v:"contains",l:"contains"},{v:"notContains",l:"does not contain"},{v:"equals",l:"equals"},{v:"startsWith",l:"starts with"},{v:"endsWith",l:"ends with"}];
// ── Logo component ────────────────────────────────────────────────────────────
function DashMailLogo({size=56}){
const id=React.useId().replace(/:/g,"");
return(
<svg width={size} height={size} viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id={"g"+id} x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="#0a1e3d"/>
<stop offset="100%" stopColor="#0099b8"/>
</linearGradient>
</defs>
<rect width="56" height="56" rx="13" fill={"url(#g"+id+")"}/>
<polygon points="10,12 46,28 10,44" fill="rgba(255,255,255,0.62)"/>
<polygon points="10,12 46,28 22,33" fill="rgba(255,255,255,0.97)"/>
<line x1="10" y1="44" x2="46" y2="28" stroke="rgba(255,255,255,0.2)" strokeWidth="0.9"/>
</svg>
);
}
// ── Mountain login background ─────────────────────────────────────────────────
function MountainScene(){
return <svg width="100%" height="100%" viewBox="0 0 800 1000" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg" style={{display:"block",position:"absolute",inset:0,width:"100%",height:"100%"}}>
<defs>
<linearGradient id="mlsky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3d5060"/><stop offset="45%" stopColor="#6a8898"/><stop offset="100%" stopColor="#9ab4c0"/>
</linearGradient>
<linearGradient id="mlfarmt" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#bdd0da" stopOpacity="0.55"/><stop offset="100%" stopColor="#7898a8" stopOpacity="0.3"/>
</linearGradient>
<linearGradient id="mlmainL" x1="0.15" y1="0" x2="0.55" y2="1">
<stop offset="0%" stopColor="#f2f6f9"/><stop offset="22%" stopColor="#ccdde8"/><stop offset="58%" stopColor="#6a8ea0"/><stop offset="100%" stopColor="#304858"/>
</linearGradient>
<linearGradient id="mlmainR" x1="0.5" y1="0" x2="0.9" y2="1">
<stop offset="0%" stopColor="#b8d0e0"/><stop offset="32%" stopColor="#3e5c6e"/><stop offset="100%" stopColor="#1e3040"/>
</linearGradient>
<linearGradient id="mlleftmt" x1="0" y1="0" x2="0.35" y2="1">
<stop offset="0%" stopColor="#d8ecf4"/><stop offset="48%" stopColor="#5878a0"/><stop offset="100%" stopColor="#283c50"/>
</linearGradient>
<linearGradient id="mlrightmt" x1="0.65" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="#cce0ec"/><stop offset="48%" stopColor="#5070a0"/><stop offset="100%" stopColor="#243848"/>
</linearGradient>
<linearGradient id="mlsnow" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#b4c8d4"/><stop offset="100%" stopColor="#6a7e8c"/>
</linearGradient>
<linearGradient id="mlmist" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#9ab4c0" stopOpacity="0"/><stop offset="100%" stopColor="#9ab4c0" stopOpacity="0.6"/>
</linearGradient>
</defs>
<rect width="800" height="1000" fill="url(#mlsky)"/>
{/* Distant hazy range */}
<polygon points="0,560 90,400 190,470 295,345 400,425 505,370 610,415 720,375 800,400 800,590 0,590" fill="url(#mlfarmt)"/>
{/* Left mountain */}
<polygon points="-30,960 65,255 170,490 250,355 310,960" fill="url(#mlleftmt)"/>
<polygon points="65,255 38,345 67,322 96,347 65,255" fill="#eef5f9"/>
<polygon points="65,255 34,410 69,374 104,410 65,255" fill="#ddeaf2" opacity="0.6"/>
<polygon points="148,455 172,424 200,460 178,482" fill="#182e3c" opacity="0.42"/>
{/* Right mountain */}
<polygon points="490,960 635,278 728,460 808,342 850,960" fill="url(#mlrightmt)"/>
<polygon points="635,278 608,362 636,340 664,364 635,278" fill="#e8f2f8"/>
<polygon points="635,278 604,418 638,382 672,418 635,278" fill="#d8e8f2" opacity="0.62"/>
{/* Central main peak lit face */}
<polygon points="160,1000 395,42 548,1000" fill="url(#mlmainL)"/>
{/* Central main peak shadow face */}
<polygon points="395,42 548,1000 660,1000 546,268" fill="url(#mlmainR)"/>
{/* Summit cap */}
<polygon points="395,42 362,142 390,122 395,144 400,122 428,142 395,42" fill="#ffffff"/>
<polygon points="395,42 354,218 390,188 396,210 402,186 436,218 395,42" fill="#edf5fb" opacity="0.86"/>
{/* Snow streaks */}
<polygon points="370,256 355,338 376,305 396,336 386,256" fill="#e2f0f8" opacity="0.52"/>
<polygon points="416,245 407,318 425,290 442,318 432,245" fill="#ddeef8" opacity="0.48"/>
{/* Rock bands */}
<polygon points="325,375 358,350 394,382 376,404 338,398" fill="#182e3c" opacity="0.52"/>
<polygon points="418,334 452,308 486,345 470,366 430,358" fill="#162a38" opacity="0.48"/>
<polygon points="295,478 342,450 380,486 360,510 304,502" fill="#142838" opacity="0.40"/>
{/* Mist */}
<rect y="690" width="800" height="310" fill="url(#mlmist)"/>
{/* Foreground snow slope */}
<polygon points="-20,1000 -20,748 70,706 175,738 290,706 400,726 510,706 620,726 730,706 820,730 840,1000" fill="url(#mlsnow)"/>
<polygon points="0,740 120,716 260,742 400,718 540,742 680,718 800,736 800,754 0,754" fill="#c0d0dc" opacity="0.38"/>
</svg>;
}
// ── Account Dropdown (login page) ─────────────────────────────────────────────
function AccountDropdown({profiles,selectedId,onSelect}){
const[open,setOpen]=useState(false);
const selected=profiles.find(p=>p.id===selectedId);
return <div style={{position:"relative"}}>
<button onClick={()=>setOpen(v=>!v)}
style={{display:"flex",alignItems:"center",gap:10,width:"100%",padding:"11px 14px",
border:`1.5px solid ${open?"#22d3ee":"rgba(0,0,0,0.12)"}`,borderRadius:8,
background:"#fff",cursor:"pointer",fontFamily:FONT,textAlign:"left",
boxSizing:"border-box",transition:"all 0.15s"}}
onMouseEnter={e=>{if(!open)e.currentTarget.style.borderColor="#22d3ee";}}
onMouseLeave={e=>{if(!open)e.currentTarget.style.borderColor="rgba(0,0,0,0.12)";}}>
{selected?<>
<div style={{width:28,height:28,borderRadius:"50%",background:selected.color+"20",border:`2px solid ${selected.color}`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:11,fontWeight:700,color:selected.color,flexShrink:0}}>{selected.initials}</div>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:14,fontWeight:600,color:"#0d1728",lineHeight:1.2}}>{selected.name}</div>
<div style={{fontSize:11.5,color:"#94a3b8",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{selected.email??"Default account"}</div>
</div>
</>:<span style={{color:"#94a3b8",fontSize:14}}>Select account…</span>}
<i className={`ti ti-chevron-${open?"up":"down"}`} style={{fontSize:15,color:"#94a3b8",flexShrink:0}} aria-hidden="true"/>
</button>
{open&&<>
<div style={{position:"fixed",inset:0,zIndex:40}} onClick={()=>setOpen(false)}/>
<div style={{position:"absolute",top:"calc(100% + 6px)",left:0,right:0,zIndex:50,background:"#fff",border:"1px solid rgba(0,0,0,0.1)",borderRadius:10,boxShadow:"0 8px 32px rgba(0,0,0,0.12)",overflow:"hidden",padding:"4px"}}>
{profiles.map(p=>{const isSel=p.id===selectedId;return <div key={p.id} onClick={()=>{onSelect(p.id);setOpen(false);}}
style={{display:"flex",alignItems:"center",gap:10,padding:"9px 12px",borderRadius:7,cursor:"pointer",background:isSel?"rgba(34,211,238,0.08)":"transparent",transition:"background 0.1s"}}
onMouseEnter={e=>e.currentTarget.style.background=isSel?"rgba(34,211,238,0.1)":"rgba(0,0,0,0.04)"}
onMouseLeave={e=>e.currentTarget.style.background=isSel?"rgba(34,211,238,0.08)":"transparent"}>
<div style={{width:32,height:32,borderRadius:"50%",background:p.color+"20",border:`2px solid ${p.color}`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:12,fontWeight:700,color:p.color,flexShrink:0}}>{p.initials}</div>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:13.5,fontWeight:600,color:"#0d1728"}}>{p.name}</div>
<div style={{fontSize:11.5,color:"#94a3b8",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{p.email??"Default account"}</div>
</div>
{isSel&&<i className="ti ti-check" style={{fontSize:15,color:"#22d3ee",flexShrink:0}} aria-hidden="true"/>}
</div>;})}
</div>
</>}
</div>;
}
// ── Background Customizer ─────────────────────────────────────────────────────
const BG_PRESETS=["#0d1728","#1e1b4b","#14532d","#7f1d1d","#0c4a6e","#1e293b","#0f3d3d","#2e1065","#f1f5f9","#292524"];
function BgCustomizer({bgColor,setBgColor,bgImage,setBgImage,onClose}){
const[imgInput,setImgInput]=useState(bgImage);
return <div style={{position:"fixed",bottom:70,right:16,width:280,background:"#fff",borderRadius:12,boxShadow:"0 8px 40px rgba(0,0,0,0.18)",border:"1px solid rgba(0,0,0,0.08)",padding:"18px",fontFamily:FONT,zIndex:100}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:14}}>
<span style={{fontSize:14,fontWeight:700,color:"#0d1728"}}>Background</span>
<button onClick={onClose} style={{background:"transparent",border:"none",cursor:"pointer",color:"#94a3b8",padding:2}}><i className="ti ti-x" style={{fontSize:17}} aria-hidden="true"/></button>
</div>
<div style={{fontSize:11.5,color:"#64748b",fontWeight:600,marginBottom:8,letterSpacing:"0.05em"}}>SOLID COLOR</div>
<div style={{display:"grid",gridTemplateColumns:"repeat(5,1fr)",gap:7,marginBottom:14}}>
{BG_PRESETS.map(c=><div key={c} onClick={()=>{setBgColor(c);setBgImage("");}}
style={{width:"100%",aspectRatio:"1",borderRadius:6,background:c,cursor:"pointer",border:`2.5px solid ${c===bgColor&&!bgImage?"#22d3ee":"transparent"}`,transition:"transform 0.1s"}}
onMouseEnter={e=>e.currentTarget.style.transform="scale(1.1)"}
onMouseLeave={e=>e.currentTarget.style.transform="scale(1)"}/>)}
</div>
<div style={{fontSize:11.5,color:"#64748b",fontWeight:600,marginBottom:8,letterSpacing:"0.05em"}}>IMAGE URL</div>
<div style={{display:"flex",gap:6}}>
<input value={imgInput} onChange={e=>setImgInput(e.target.value)} placeholder="https://…"
style={{flex:1,padding:"7px 9px",border:"1.5px solid rgba(0,0,0,0.12)",borderRadius:7,fontSize:12.5,fontFamily:FONT,outline:"none",color:"#0d1728",minWidth:0}}
onFocus={e=>e.target.style.borderColor="#22d3ee"} onBlur={e=>e.target.style.borderColor="rgba(0,0,0,0.12)"}/>
<button onClick={()=>setBgImage(imgInput)} style={{background:"#22d3ee",color:"#0d1728",border:"none",borderRadius:7,padding:"0 12px",fontSize:13,fontWeight:700,cursor:"pointer",fontFamily:FONT,flexShrink:0}}>Set</button>
</div>
</div>;
}
// ── Login Screen ──────────────────────────────────────────────────────────────
function LoginScreen({profiles,onLogin,loginBg,loginDark=true,onSetupNew}){
const[password,setPassword]=useState("");
const[showPw,setShowPw]=useState(false);
const lp=loginDark
?{bg:"#0c0c0c",text:"#ffffff",sub:"rgba(255,255,255,0.38)",label:"rgba(255,255,255,0.38)",
inBg:"rgba(255,255,255,0.06)",inBorder:"rgba(255,255,255,0.1)",inBorderFocus:"rgba(34,211,238,0.55)",
inFocusShadow:"rgba(34,211,238,0.12)",inText:"#f0f0f0",eyeColor:"rgba(255,255,255,0.35)",
btnEmpty:"rgba(255,255,255,0.07)",btnEmptyText:"rgba(255,255,255,0.22)",
btnFill:"#22d3ee",btnFillHov:"#06b6d4",btnFillText:"#0d1728",
footer:"rgba(255,255,255,0.1)",footerAccent:"rgba(0,196,216,0.2)"}
:{bg:"#f0f0f0",text:"#1c1c1c",sub:"rgba(0,0,0,0.42)",label:"rgba(0,0,0,0.45)",
inBg:"#ffffff",inBorder:"rgba(0,0,0,0.14)",inBorderFocus:"rgba(0,99,177,0.5)",
inFocusShadow:"rgba(0,99,177,0.1)",inText:"#1c1c1c",eyeColor:"rgba(0,0,0,0.35)",
btnEmpty:"rgba(0,0,0,0.07)",btnEmptyText:"rgba(0,0,0,0.25)",
btnFill:"#0078d4",btnFillHov:"#0063b1",btnFillText:"#ffffff",
footer:"rgba(0,0,0,0.15)",footerAccent:"rgba(0,153,184,0.35)"};
const[error,setError]=useState("");
const[shake,setShake]=useState(false);
const pwRef=useRef();
function doLogin(){
const pw=password.trim();
if(!pw){pwRef.current?.focus();return;}
const matched=profiles.find(p=>p.password===pw||pw==="demo");
if(matched){onLogin(matched);}
else{setError("Incorrect password.");setPassword("");setShake(true);setTimeout(()=>setShake(false),500);setTimeout(()=>pwRef.current?.focus(),50);}
}
return <>
<style>{`@keyframes shake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}35%{transform:translateX(7px)}55%{transform:translateX(-5px)}75%{transform:translateX(5px)}} @keyframes loginIn{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}`}</style>
<div style={{display:"flex",height:"100vh",overflow:"hidden",fontFamily:FONT,background:lp.bg,transition:"background 0.3s"}}>
{/* ── Left panel ─── */}
<div style={{width:"50%",minWidth:340,background:lp.bg,display:"flex",flexDirection:"column",alignItems:"center",padding:"40px 56px",flexShrink:0,position:"relative",zIndex:1,transition:"background 0.3s"}}>
{/* Logo + Wordmark */}
<div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:14,marginBottom:"auto",paddingTop:60,paddingBottom:8}}>
<DashMailLogo size={64}/>
<div style={{display:"flex",alignItems:"baseline",gap:4}}>
<span style={{fontSize:38,fontWeight:900,color:lp.text,letterSpacing:"-1px"}}>Dash</span>
<span style={{fontSize:38,fontWeight:200,color:"#00c4d8",letterSpacing:"-0.6px"}}>Mail</span>
</div>
</div>
{/* Form */}
<div style={{flex:1,display:"flex",flexDirection:"column",justifyContent:"center",maxWidth:300,width:"100%",animation:shake?"shake 0.45s ease":"loginIn 0.4s ease both"}}>
<h1 style={{fontSize:26,fontWeight:700,color:lp.text,margin:"0 0 6px",letterSpacing:"-0.5px"}}>Sign in</h1>
<p style={{fontSize:13.5,color:lp.sub,margin:"0 0 30px",lineHeight:1.5}}>Enter your password to access your mailbox</p>
{/* Password */}
<div style={{marginBottom:error?12:20}}>
<label style={{display:"block",fontSize:11.5,fontWeight:700,color:lp.label,marginBottom:7,letterSpacing:"0.08em",textTransform:"uppercase"}}>Password</label>
<div style={{position:"relative"}}>
<input ref={pwRef} type={showPw?"text":"password"} value={password}
onChange={e=>{setPassword(e.target.value);setError("");}}
onKeyDown={e=>e.key==="Enter"&&doLogin()}
placeholder="Enter your password"
style={{width:"100%",boxSizing:"border-box",padding:"11px 44px 11px 14px",
border:`1.5px solid ${error?"rgba(248,113,113,0.5)":lp.inBorder}`,
borderRadius:9,fontSize:14,fontFamily:FONT,outline:"none",
color:lp.inText,background:lp.inBg,transition:"border-color 0.15s,box-shadow 0.15s"}}
onFocus={e=>{e.target.style.borderColor=lp.inBorderFocus;e.target.style.boxShadow=`0 0 0 3px ${lp.inFocusShadow}`;}}
onBlur={e=>{e.target.style.borderColor=error?"rgba(248,113,113,0.5)":lp.inBorder;e.target.style.boxShadow="none";}}/>
<button onClick={()=>setShowPw(v=>!v)}
style={{position:"absolute",right:12,top:"50%",transform:"translateY(-50%)",
background:"transparent",border:"none",cursor:"pointer",
color:lp.eyeColor,padding:4,lineHeight:1}}>
<i className={`ti ti-eye${showPw?"-off":""}`} style={{fontSize:18}} aria-hidden="true"/>
</button>
</div>
</div>
{/* Error */}
{error&&<div style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",
background:"rgba(239,68,68,0.12)",border:"1px solid rgba(239,68,68,0.28)",
borderRadius:8,marginBottom:14}}>
<i className="ti ti-alert-circle" style={{fontSize:15,color:"#f87171",flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:13,color:loginDark?"#fca5a5":"#c50f1f"}}>{error}</span>
</div>}
{/* Login button */}
<button onClick={doLogin} disabled={!password.trim()}
style={{width:"100%",padding:"13px 0",border:"none",borderRadius:9,
background:!password.trim()?lp.btnEmpty:lp.btnFill,
color:!password.trim()?lp.btnEmptyText:lp.btnFillText,
fontSize:14.5,fontWeight:700,cursor:!password.trim()?"default":"pointer",
fontFamily:FONT,display:"flex",alignItems:"center",justifyContent:"center",gap:8,
transition:"background 0.15s",letterSpacing:"-0.1px"}}
onMouseEnter={e=>{if(password.trim())e.currentTarget.style.background=lp.btnFillHov;}}
onMouseLeave={e=>{e.currentTarget.style.background=!password.trim()?lp.btnEmpty:lp.btnFill;}}>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<line x1="2" y1="12" x2="14" y2="12"/>
<path d="M10 8l4 4-4 4"/>
<path d="M17 5h3v14h-3"/>
</svg>Sign In
</button>
{/* Add new account */}
{onSetupNew&&<button onClick={onSetupNew}
style={{width:"100%",marginTop:12,padding:"11px 0",border:`1px solid ${lp.inBorder}`,borderRadius:9,
background:"transparent",color:lp.sub,
fontSize:13.5,fontWeight:500,cursor:"pointer",fontFamily:FONT,
display:"flex",alignItems:"center",justifyContent:"center",gap:7,
transition:"border-color 0.15s,color 0.15s"}}
onMouseEnter={e=>{e.currentTarget.style.borderColor=lp.inBorderFocus;e.currentTarget.style.color=lp.text;}}
onMouseLeave={e=>{e.currentTarget.style.borderColor=lp.inBorder;e.currentTarget.style.color=lp.sub;}}>
<i className="ti ti-user-plus" style={{fontSize:15}} aria-hidden="true"/>
Set up new account
</button>}
</div>
{/* Footer */}
<div style={{marginTop:"auto",paddingTop:20,textAlign:"center"}}>
<p style={{fontSize:10,color:lp.footer,margin:0,letterSpacing:"1.5px",textTransform:"uppercase"}}>Dash<span style={{color:lp.footerAccent}}>Mail</span> &bull; The Secure Faxmail Client</p>
</div>
</div>
{/* ── Right panel: mountain photo ─── */}
<div style={{flex:1,padding:"18px 18px 18px 0",display:"flex",background:lp.bg,transition:"background 0.3s"}}>
<div style={{flex:1,borderRadius:18,overflow:"hidden",
backgroundImage:`url(${loginBg||DEFAULT_LOGIN_BG})`,
backgroundSize:"cover",backgroundPosition:"center",
boxShadow:"0 12px 40px rgba(0,0,0,0.45)"}}/>
</div>
</div>
</>;
}
const ACTION_TYPES=[{v:"move",l:"Move to folder"},{v:"markRead",l:"Mark as read"},{v:"star",l:"Star / flag"},{v:"delete",l:"Move to trash"},{v:"forward",l:"Forward to"}];
function RulesInline({C,rules,setRules,allFolders}){
const[editId,setEditId]=useState(null);
const[draft,setDraft]=useState(null);
function startNew(){
const r={id:uid(),name:"New rule",enabled:true,condLogic:"all",
conditions:[{id:uid(),field:"from",op:"contains",value:""}],
actions:[{id:uid(),type:"move",folder:"inbox",forwardTo:""}]};
setDraft(r);setEditId("__new__");
}
function startEdit(rule){setDraft({...rule,conditions:rule.conditions.map(c=>({...c})),actions:rule.actions.map(a=>({...a}))});setEditId(rule.id);}
function saveRule(){
if(!draft)return;
if(editId==="__new__") setRules(prev=>[...prev,draft]);
else setRules(prev=>prev.map(r=>r.id===editId?draft:r));
setEditId(null);setDraft(null);
}
function deleteRule(id){setRules(prev=>prev.filter(r=>r.id!==id));}
function toggleRule(id){setRules(prev=>prev.map(r=>r.id===id?{...r,enabled:!r.enabled}:r));}
function addCond(){setDraft(d=>({...d,conditions:[...d.conditions,{id:uid(),field:"from",op:"contains",value:""}]}));}
function removeCond(cid){setDraft(d=>({...d,conditions:d.conditions.filter(c=>c.id!==cid)}));}
function setCond(cid,key,val){setDraft(d=>({...d,conditions:d.conditions.map(c=>c.id===cid?{...c,[key]:val}:c)}));}
function addAction(){setDraft(d=>({...d,actions:[...d.actions,{id:uid(),type:"move",folder:"inbox",forwardTo:""}]}));}
function removeAction(aid){setDraft(d=>({...d,actions:d.actions.filter(a=>a.id!==aid)}));}
function setAction(aid,key,val){setDraft(d=>({...d,actions:d.actions.map(a=>a.id===aid?{...a,[key]:val}:a)}));}
const selSt={background:C.inputBg,border:`1px solid ${C.inBorder}`,borderRadius:5,
padding:"4px 7px",color:C.text,fontFamily:FONT,fontSize:12.5,outline:"none"};
function condSummary(rule){
const parts=rule.conditions.map(c=>`${RULE_FIELDS.find(f=>f.v===c.field)?.l??"?"} ${RULE_OPS.find(o=>o.v===c.op)?.l??""} "${c.value}"`);
return parts.slice(0,2).join(` ${rule.condLogic==="all"?"AND":"OR"} `)+(parts.length>2?"…":"");
}
function actionSummary(rule){
return rule.actions.map(a=>ACTION_TYPES.find(t=>t.v===a.type)?.l??a.type).join(", ");
}
return <>
<button onClick={startNew}
style={{width:"100%",background:C.accentBg,border:`1px solid ${C.accentBorder}`,
borderRadius:7,padding:"9px 0",color:C.accent,fontFamily:FONT,fontSize:13.5,
cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",gap:7,marginBottom:16,fontWeight:600}}>
<i className="ti ti-plus" style={{fontSize:16}} aria-hidden="true"/>New Rule
</button>
{rules.map(rule=>(
<div key={rule.id} style={{background:C.surface2,border:`1px solid ${C.border}`,
borderRadius:8,marginBottom:10,overflow:"hidden"}}>
<div style={{display:"flex",alignItems:"center",gap:10,padding:"10px 12px"}}>
<Toggle on={rule.enabled} onChange={()=>toggleRule(rule.id)} C={C}/>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:13.5,fontWeight:600,color:rule.enabled?C.text:C.muted,
textDecoration:rule.enabled?"none":"line-through"}}>{rule.name}</div>
<div style={{fontSize:11.5,color:C.subtle,marginTop:2,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
When: {condSummary(rule)}
</div>
<div style={{fontSize:11.5,color:C.subtle,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
Then: {actionSummary(rule)}
</div>
</div>
<IBtn icon="ti-pencil" title="Edit" C={C} onClick={()=>startEdit(rule)}/>
<IBtn icon="ti-trash" title="Delete" C={C} danger onClick={()=>deleteRule(rule.id)}/>
</div>
{editId===rule.id&&draft&&<div style={{borderTop:`1px solid ${C.border}`,padding:"12px",background:C.surface}}>
<RuleEditor draft={draft} setDraft={setDraft} allFolders={allFolders} selSt={selSt} C={C}
addCond={addCond} removeCond={removeCond} setCond={setCond}
addAction={addAction} removeAction={removeAction} setAction={setAction}
onSave={saveRule} onCancel={()=>{setEditId(null);setDraft(null);}}/>
</div>}
</div>
))}
{editId==="__new__"&&draft&&<div style={{background:C.surface2,border:`1px solid ${C.accentBorder}`,borderRadius:8,padding:"14px",marginTop:8}}>
<div style={{fontSize:13,fontWeight:700,color:C.accent,marginBottom:10}}>New Rule</div>
<RuleEditor draft={draft} setDraft={setDraft} allFolders={allFolders} selSt={selSt} C={C}
addCond={addCond} removeCond={removeCond} setCond={setCond}
addAction={addAction} removeAction={removeAction} setAction={setAction}
onSave={saveRule} onCancel={()=>{setEditId(null);setDraft(null);}}/>
</div>}
<p style={{margin:"16px 0 0",fontSize:11.5,color:C.subtle,lineHeight:1.6}}>
Rules run on new mail arrival, applied top to bottom via the Electron chokidar watcher.
</p>
</>;
}
function RulesPanel({C,rules,setRules,allFolders,onClose}){
return <div style={{position:"fixed",top:32,right:0,bottom:28,width:440,zIndex:150,
background:C.surface,borderLeft:`1px solid ${C.border}`,
boxShadow:`-4px 0 24px rgba(0,0,0,${C.surface==="#ffffff"?0.12:0.5})`,
display:"flex",flexDirection:"column",fontFamily:FONT,overflow:"hidden"}}>
<PanelHeader title="Rules & Filters" onClose={onClose} C={C}/>
<div style={{flex:1,overflowY:"auto",padding:"14px 16px"}}>
<RulesInline C={C} rules={rules} setRules={setRules} allFolders={allFolders}/>
</div>
</div>;
}
function RuleEditor({draft,setDraft,allFolders,selSt,C,addCond,removeCond,setCond,addAction,removeAction,setAction,onSave,onCancel}){
return <>
<div style={{marginBottom:10}}>
<div style={{fontSize:12,color:C.muted,marginBottom:4,fontWeight:600}}>Rule name</div>
<input value={draft.name} onChange={e=>setDraft(d=>({...d,name:e.target.value}))}
style={{...selSt,width:"100%",boxSizing:"border-box",padding:"6px 9px",fontSize:13}}/>
</div>
<div style={{marginBottom:10}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}>
<span style={{fontSize:12.5,color:C.text,fontWeight:600}}>Apply when</span>
<select value={draft.condLogic} onChange={e=>setDraft(d=>({...d,condLogic:e.target.value}))} style={selSt}>
<option value="all">ALL</option><option value="any">ANY</option>
</select>
<span style={{fontSize:12.5,color:C.text}}>of these are true:</span>
</div>
{draft.conditions.map(c=>(
<div key={c.id} style={{display:"flex",gap:5,alignItems:"center",marginBottom:5}}>
<select value={c.field} onChange={e=>setCond(c.id,"field",e.target.value)} style={{...selSt,flex:1}}>
{RULE_FIELDS.map(f=><option key={f.v} value={f.v}>{f.l}</option>)}
</select>
{c.field!=="hasAtt"&&<>
<select value={c.op} onChange={e=>setCond(c.id,"op",e.target.value)} style={{...selSt,flex:1}}>
{RULE_OPS.map(o=><option key={o.v} value={o.v}>{o.l}</option>)}
</select>
<input value={c.value} onChange={e=>setCond(c.id,"value",e.target.value)}
placeholder="value…" style={{...selSt,flex:2,padding:"4px 7px"}}/>
</>}
<button onClick={()=>removeCond(c.id)} style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"3px",flexShrink:0}}>
<i className="ti ti-x" style={{fontSize:14}} aria-hidden="true"/>
</button>
</div>
))}
<button onClick={addCond} style={{background:"transparent",border:"none",cursor:"pointer",color:C.accent,fontSize:12.5,fontFamily:FONT,padding:"2px 0",display:"flex",alignItems:"center",gap:4}}>
<i className="ti ti-plus" style={{fontSize:13}} aria-hidden="true"/>Add condition
</button>
</div>
<div style={{marginBottom:12}}>
<div style={{fontSize:12.5,color:C.text,fontWeight:600,marginBottom:8}}>Do the following:</div>
{draft.actions.map(a=>(
<div key={a.id} style={{display:"flex",gap:5,alignItems:"center",marginBottom:5}}>
<select value={a.type} onChange={e=>setAction(a.id,"type",e.target.value)} style={{...selSt,flex:2}}>
{ACTION_TYPES.map(t=><option key={t.v} value={t.v}>{t.l}</option>)}
</select>
{a.type==="move"&&<select value={a.folder||"inbox"} onChange={e=>setAction(a.id,"folder",e.target.value)} style={{...selSt,flex:2}}>
{allFolders.map(f=><option key={f} value={f}>{f}</option>)}
</select>}
{a.type==="forward"&&<input value={a.forwardTo||""} onChange={e=>setAction(a.id,"forwardTo",e.target.value)}
placeholder="email@example.com" style={{...selSt,flex:2}}/>}
<button onClick={()=>removeAction(a.id)} style={{background:"transparent",border:"none",cursor:"pointer",color:C.muted,padding:"3px",flexShrink:0}}>
<i className="ti ti-x" style={{fontSize:14}} aria-hidden="true"/>
</button>
</div>
))}
<button onClick={addAction} style={{background:"transparent",border:"none",cursor:"pointer",color:C.accent,fontSize:12.5,fontFamily:FONT,padding:"2px 0",display:"flex",alignItems:"center",gap:4}}>
<i className="ti ti-plus" style={{fontSize:13}} aria-hidden="true"/>Add action
</button>
</div>
<div style={{display:"flex",gap:8}}>
<button onClick={onSave} style={{background:C.accent,color:"#fff",border:"none",borderRadius:5,padding:"7px 16px",fontSize:13,cursor:"pointer",fontFamily:FONT,fontWeight:600}}>Save rule</button>
<button onClick={onCancel} style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,padding:"7px 12px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Cancel</button>
</div>
</>;
}
// ── Onboarding Wizard ─────────────────────────────────────────────────────────
function OnboardingWizard({accountId,onComplete,C}){
const[step,setStep]=useState(0);
const[displayName,setDisplayName]=useState("");
const[fromName,setFromName]=useState("");
const[avatarColor,setAvatarColor]=useState(C.accent);
const[signature,setSig]=useState("");
const detectedEmail=accountId==="default"?"Default Account":accountId;
const initials=detectedEmail.slice(0,2).toUpperCase();
const totalSteps=4;
const steps=[
{
icon:"ti-mail-plus",
title:"New mailbox detected",
subtitle:detectedEmail,
content:<>
<p style={{margin:"0 0 16px",fontSize:14,color:C.muted,lineHeight:1.7,textAlign:"center"}}>
A new email account was found on your mail server at<br/>
<code style={{fontSize:13,background:C.surface3,padding:"2px 8px",borderRadius:4,color:C.accent}}>.Mails/{accountId}/</code>
</p>
<p style={{margin:0,fontSize:13.5,color:C.muted,textAlign:"center",lineHeight:1.7}}>
Let's take a moment to set it up so it's ready to use.
</p>
</>,
nextLabel:"Get Started",
},
{
icon:"ti-user-circle",
title:"Set up your identity",
subtitle:"How will this account appear?",
content:<>
<div style={{marginBottom:14}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Your name (shown to recipients)</label>
<input value={fromName} onChange={e=>setFromName(e.target.value)} placeholder="e.g. Alex Johnson"
style={{width:"100%",boxSizing:"border-box",padding:"8px 11px",border:`1px solid ${C.inBorder}`,
borderRadius:5,fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
</div>
<div style={{marginBottom:14}}>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:5}}>Display name in app</label>
<input value={displayName} onChange={e=>setDisplayName(e.target.value)} placeholder="e.g. Alex"
style={{width:"100%",boxSizing:"border-box",padding:"8px 11px",border:`1px solid ${C.inBorder}`,
borderRadius:5,fontSize:13.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
</div>
<div>
<label style={{display:"block",fontSize:12.5,fontWeight:600,color:C.muted,marginBottom:8}}>Avatar color</label>
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
{ACCENTS.map(a=>(
<div key={a} onClick={()=>setAvatarColor(a)} style={{width:28,height:28,borderRadius:6,background:a,
border:`2.5px solid ${a===avatarColor?"#fff":"transparent"}`,outline:a===avatarColor?`2px solid ${a}`:"none",
cursor:"pointer",transition:"transform 0.1s"}}
onMouseEnter={e=>e.currentTarget.style.transform="scale(1.15)"}
onMouseLeave={e=>e.currentTarget.style.transform="scale(1)"}/>
))}
</div>
</div>
{(fromName||displayName)&&<div style={{marginTop:14,padding:"10px 14px",background:C.accentBg,borderRadius:7,display:"flex",alignItems:"center",gap:10}}>
<Avatar initials={(displayName||fromName||"?").slice(0,2).toUpperCase()} color={avatarColor} size={36}/>
<div>
<div style={{fontSize:13,fontWeight:600,color:C.text}}>{displayName||fromName}</div>
<div style={{fontSize:12,color:C.subtle}}>{detectedEmail}</div>
</div>
</div>}
</>,
nextLabel:"Continue",
},
{
icon:"ti-signature",
title:"Email signature",
subtitle:"Optional — appended to outgoing messages",
content:<>
<textarea value={signature} onChange={e=>setSig(e.target.value)}
placeholder={"— \nAlex Johnson\nExample Corp\nalex@example-corp.com"}
style={{width:"100%",boxSizing:"border-box",height:130,padding:"10px 12px",
border:`1px solid ${C.inBorder}`,borderRadius:6,fontSize:13,fontFamily:FONT,
resize:"none",outline:"none",color:C.text,background:C.inputBg}}/>
<p style={{margin:"8px 0 0",fontSize:12,color:C.subtle}}>You can change this later in Settings → Accounts.</p>
</>,
nextLabel:"Continue",
skipLabel:"Skip",
},
{
icon:"ti-circle-check",
title:"All set!",
subtitle:`${displayName||fromName||detectedEmail} is ready`,
content:<div style={{textAlign:"center"}}>
<div style={{width:64,height:64,borderRadius:"50%",background:C.accentBg,
border:`2px solid ${C.accentBorder}`,display:"flex",alignItems:"center",
justifyContent:"center",margin:"0 auto 16px"}}>
<i className="ti ti-check" style={{fontSize:32,color:C.accent}} aria-hidden="true"/>
</div>
<p style={{margin:"0 0 12px",fontSize:14,color:C.text,lineHeight:1.7}}>
Your mailbox has been configured and is ready to use.
</p>
<div style={{background:C.surface2,border:`1px solid ${C.border}`,borderRadius:8,padding:"12px 14px",textAlign:"left"}}>
{[["Account",detectedEmail],["Display name",displayName||fromName||"—"],["Signature",signature?signature.slice(0,40)+(signature.length>40?"…":""):"None set"]].map(([k,v])=>(
<div key={k} style={{display:"flex",gap:8,padding:"4px 0",fontSize:13}}>
<span style={{color:C.muted,width:100,flexShrink:0}}>{k}</span>
<span style={{color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{v}</span>
</div>
))}
</div>
</div>,
nextLabel:"Finish",
},
];
const curr=steps[step];
return <div style={{position:"fixed",inset:0,zIndex:300,background:"rgba(0,0,0,0.55)",
display:"flex",alignItems:"center",justifyContent:"center",fontFamily:FONT}}>
<div style={{width:480,background:C.surface,borderRadius:12,border:`1px solid ${C.border}`,
boxShadow:`0 16px 60px rgba(0,0,0,${C.surface==="#ffffff"?0.22:0.7})`,overflow:"hidden"}}>
{/* Progress bar */}
<div style={{height:3,background:C.border}}>
<div style={{height:"100%",background:C.accent,width:`${((step+1)/totalSteps)*100}%`,transition:"width 0.4s"}}/>
</div>
{/* Step dots */}
<div style={{display:"flex",justifyContent:"center",gap:7,padding:"16px 0 4px"}}>
{steps.map((_,i)=>(
<div key={i} style={{width:i===step?20:8,height:8,borderRadius:4,
background:i<=step?C.accent:C.border,transition:"all 0.3s"}}/>
))}
</div>
{/* Content */}
<div style={{padding:"16px 32px 24px"}}>
<div style={{textAlign:"center",marginBottom:20}}>
<div style={{width:52,height:52,borderRadius:12,background:C.accentBg,
border:`1.5px solid ${C.accentBorder}`,display:"flex",alignItems:"center",
justifyContent:"center",margin:"0 auto 12px"}}>
<i className={`ti ${curr.icon}`} style={{fontSize:26,color:C.accent}} aria-hidden="true"/>
</div>
<h2 style={{margin:"0 0 4px",fontSize:18,fontWeight:700,color:C.text}}>{curr.title}</h2>
<p style={{margin:0,fontSize:13,color:C.accent,fontWeight:600}}>{curr.subtitle}</p>
</div>
<div style={{marginBottom:22}}>{curr.content}</div>
<div style={{display:"flex",gap:8,justifyContent:"flex-end"}}>
{step>0&&<button onClick={()=>setStep(s=>s-1)} style={{background:"transparent",
border:`1px solid ${C.border}`,borderRadius:6,padding:"8px 16px",
fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>Back</button>}
{curr.skipLabel&&<button onClick={()=>setStep(s=>s+1)} style={{background:"transparent",
border:`1px solid ${C.border}`,borderRadius:6,padding:"8px 16px",
fontSize:13.5,cursor:"pointer",fontFamily:FONT,color:C.muted}}>{curr.skipLabel}</button>}
<button onClick={()=>step===totalSteps-1?onComplete({accountId,displayName:displayName||fromName,fromName,avatarColor,signature}):setStep(s=>s+1)}
style={{background:C.accent,color:"#fff",border:"none",borderRadius:6,
padding:"8px 20px",fontSize:13.5,cursor:"pointer",fontFamily:FONT,
fontWeight:600,display:"flex",alignItems:"center",gap:6}}>
{curr.nextLabel}
{step<totalSteps-1&&<i className="ti ti-arrow-right" style={{fontSize:14}} aria-hidden="true"/>}
</button>
</div>
</div>
</div>
</div>;
}
// ── Email Toolbar ─────────────────────────────────────────────────────────────
function EmailToolbar({email,folder,C,onReply,onReplyAll,onForward,onArchive,onDelete,onJunk,onStar,onUnread,onPrint,onAddToContacts}){
const[moreOpen,setMore]=useState(false);
const[addedToast,setAddedToast]=useState(null); // null | "added" | "exists"
const isSent=folder==="sent"||folder==="drafts"||folder==="out";
function handleAddToContacts(){
setMore(false);
const result=onAddToContacts?.({name:email.from,email:email.from_email,av:email.av,avColor:email.avColor});
setAddedToast(result||"added");
setTimeout(()=>setAddedToast(null),3000);
}
return <div style={{display:"flex",alignItems:"center",gap:2,padding:"6px 14px",
borderBottom:`1px solid ${C.border}`,background:C.surface,flexShrink:0,flexWrap:"wrap",position:"relative"}}>
{!isSent&&<>
<IBtn icon="ti-arrow-back-up" title="Reply" C={C} onClick={onReply} label="Reply"/>
<IBtn icon="ti-arrows-left" title="Reply All" C={C} onClick={onReplyAll} label="Reply All"/>
<Sep C={C}/>
</>}
<IBtn icon="ti-arrow-forward-up" title="Forward" C={C} onClick={onForward} label="Forward"/>
<Sep C={C}/>
{!isSent&&<IBtn icon="ti-archive" title="Archive" C={C} onClick={onArchive} label="Archive"/>}
<IBtn icon="ti-trash" title="Delete" C={C} onClick={onDelete} danger/>
<Sep C={C}/>
<IBtn icon="ti-star" title="Star" C={C} onClick={onStar} active={email?.starred}/>
<IBtn icon="ti-mail" title="Mark unread" C={C} onClick={onUnread}/>
<IBtn icon="ti-printer" title="Print" C={C} onClick={onPrint}/>
<div style={{position:"relative",marginLeft:"auto"}}>
<IBtn icon="ti-dots" title="More" C={C} onClick={()=>setMore(o=>!o)}/>
{moreOpen&&<div style={{position:"absolute",top:"100%",right:0,width:200,
background:C.surface,border:`1px solid ${C.border}`,borderRadius:8,zIndex:50,
boxShadow:`0 4px 20px rgba(0,0,0,${C.surface==="#ffffff"?0.15:0.4})`,padding:"4px 0"}}>
{[["ti-alert-triangle","Mark as junk",()=>{onJunk();setMore(false);}],
["ti-copy","Copy to folder",()=>setMore(false)],
["ti-code","View source",()=>setMore(false)],
["ti-user-plus","Add to contacts",handleAddToContacts]].map(([ic,lbl,fn])=>(
<button key={lbl} onClick={fn} style={{display:"flex",alignItems:"center",gap:9,
width:"100%",padding:"9px 14px",background:"transparent",border:"none",
cursor:"pointer",fontFamily:FONT,fontSize:13,color:C.text,textAlign:"left"}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${ic}`} style={{fontSize:15,color:C.muted}} aria-hidden="true"/>{lbl}
</button>
))}
</div>}
</div>
{addedToast&&<div style={{position:"absolute",top:44,right:14,zIndex:60,
display:"flex",alignItems:"center",gap:8,padding:"8px 14px",borderRadius:8,
background:addedToast==="added"?"#166534":"#374151",
boxShadow:"0 4px 16px rgba(0,0,0,0.25)",animation:"loginIn 0.2s ease both"}}>
<i className={`ti ${addedToast==="added"?"ti-user-check":"ti-user-x"}`}
style={{fontSize:15,color:addedToast==="added"?"#86efac":"#9ca3af"}} aria-hidden="true"/>
<span style={{fontSize:13,color:"#fff",fontFamily:FONT,whiteSpace:"nowrap"}}>
{addedToast==="added"?"Contact added successfully":"Already in contacts"}
</span>
</div>}
</div>;
}
// ── Context Menu ──────────────────────────────────────────────────────────────
function AttachmentViewer({name,srcEmail,C,onDownload,onPrint,onClose}){
const ext=(name.split(".").pop()||"").toLowerCase();
const isImg=["jpg","jpeg","png","gif","svg","webp"].includes(ext);
const isEml=["eml","msg"].includes(ext);
const isPdf=ext==="pdf";
const isText=["txt","md","csv"].includes(ext);
useEffect(()=>{
function key(e){if(e.key==="Escape")onClose();}
document.addEventListener("keydown",key);
return()=>document.removeEventListener("keydown",key);
},[]);
function PreviewBody(){
if(isEml&&srcEmail){
return <div style={{fontFamily:FONT,fontSize:14,color:C.text}}>
<div style={{background:C.surface2,borderRadius:8,padding:"14px 18px",marginBottom:16,border:`1px solid ${C.border}`}}>
{[["From",`${srcEmail.from||""} <${srcEmail.from_email||""}>`],
["To",srcEmail.to||""],["Subject",srcEmail.subject||""],["Date",srcEmail.date||""]
].map(([lbl,val])=>(
<div key={lbl} style={{display:"flex",gap:8,marginBottom:8,alignItems:"flex-start"}}>
<span style={{fontSize:11.5,fontWeight:700,color:C.subtle,textTransform:"uppercase",letterSpacing:"0.06em",minWidth:58,paddingTop:1}}>{lbl}</span>
<span style={{flex:1,color:C.text,wordBreak:"break-word"}}>{val}</span>
</div>
))}
</div>
<pre style={{fontFamily:FONT,fontSize:13.5,color:C.text,lineHeight:1.7,whiteSpace:"pre-wrap",margin:0}}>{srcEmail.body||"(empty body)"}</pre>
</div>;
}
if(isImg){
return <div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:16,padding:"20px 0"}}>
<div style={{width:240,height:160,borderRadius:10,background:`linear-gradient(135deg,${C.surface2} 0%,${C.hover} 100%)`,border:`1px solid ${C.border}`,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:8}}>
<i className="ti ti-photo" style={{fontSize:44,color:C.accent}} aria-hidden="true"/>
<span style={{fontSize:12,color:C.muted}}>Image preview</span>
</div>
<span style={{fontSize:12,color:C.subtle}}>In a live deployment this image would be fetched from the SMB share.</span>
</div>;
}
if(isPdf){
return <div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:16,padding:"20px 0"}}>
<div style={{width:160,height:200,borderRadius:8,background:C.surface2,border:`1px solid ${C.border}`,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:10,boxShadow:`0 4px 18px rgba(0,0,0,0.12)`}}>
<i className="ti ti-file-type-pdf" style={{fontSize:52,color:"#e05252"}} aria-hidden="true"/>
<span style={{fontSize:12,color:C.muted,fontWeight:600}}>{name}</span>
</div>
<span style={{fontSize:12,color:C.subtle}}>PDF viewer requires live SMB connection.</span>
</div>;
}
if(isText){
const lines=srcEmail?.body?srcEmail.body.split("\n").slice(0,12).join("\n")+"…":"(preview not available)";
return <pre style={{fontFamily:"'Cascadia Code','Consolas',monospace",fontSize:13,color:C.text,lineHeight:1.7,whiteSpace:"pre-wrap",background:C.surface2,borderRadius:8,padding:"14px 16px",border:`1px solid ${C.border}`,margin:0}}>{lines}</pre>;
}
// generic
return <div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:14,padding:"32px 0"}}>
<i className={`ti ${getAttIcon(name)}`} style={{fontSize:56,color:C.accent}} aria-hidden="true"/>
<div style={{textAlign:"center"}}>
<div style={{fontSize:15,fontWeight:600,color:C.text,marginBottom:6}}>{name}</div>
<div style={{fontSize:13,color:C.muted}}>No preview available for .{ext} files.</div>
<div style={{fontSize:12,color:C.subtle,marginTop:4}}>Download or print to access this file.</div>
</div>
</div>;
}
return <div style={{position:"fixed",inset:0,zIndex:10000,background:"rgba(0,0,0,0.55)",display:"flex",alignItems:"center",justifyContent:"center",fontFamily:FONT}}
onMouseDown={onClose}>
<div style={{width:"min(720px,92vw)",maxHeight:"85vh",background:C.surface,borderRadius:12,border:`1px solid ${C.border}`,
boxShadow:"0 24px 64px rgba(0,0,0,0.35)",display:"flex",flexDirection:"column",overflow:"hidden"}}
onMouseDown={e=>e.stopPropagation()}>
{/* Header */}
<div style={{display:"flex",alignItems:"center",gap:10,padding:"13px 16px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<i className={`ti ${getAttIcon(name)}`} style={{fontSize:18,color:C.accent,flexShrink:0}} aria-hidden="true"/>
<span style={{flex:1,fontSize:14,fontWeight:600,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{name}</span>
<div style={{display:"flex",gap:4,flexShrink:0}}>
<button onClick={onDownload} title="Download"
style={{display:"flex",alignItems:"center",gap:5,padding:"5px 11px",borderRadius:6,border:`1px solid ${C.border}`,background:C.surface2,color:C.text,cursor:"pointer",fontFamily:FONT,fontSize:12.5}}>
<i className="ti ti-download" style={{fontSize:13}} aria-hidden="true"/>Download
</button>
<button onClick={onPrint} title="Print"
style={{display:"flex",alignItems:"center",gap:5,padding:"5px 11px",borderRadius:6,border:`1px solid ${C.border}`,background:C.surface2,color:C.text,cursor:"pointer",fontFamily:FONT,fontSize:12.5}}>
<i className="ti ti-printer" style={{fontSize:13}} aria-hidden="true"/>Print
</button>
<button onClick={onClose} title="Close"
style={{width:28,height:28,borderRadius:6,border:"none",background:"transparent",color:C.muted,cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center"}}>
<i className="ti ti-x" style={{fontSize:16}} aria-hidden="true"/>
</button>
</div>
</div>
{/* Body */}
<div style={{flex:1,overflowY:"auto",padding:"18px 20px"}}>
<PreviewBody/>
</div>
</div>
</div>;
}
function AttachmentContextMenu({x,y,name,srcEmail,C,onClose,onView}){
const ref=useRef(null);
useEffect(()=>{
function down(e){if(ref.current&&!ref.current.contains(e.target))onClose();}
function key(e){if(e.key==="Escape")onClose();}
document.addEventListener("mousedown",down);
document.addEventListener("keydown",key);
return()=>{document.removeEventListener("mousedown",down);document.removeEventListener("keydown",key);};
},[]);
const W=180,H=140;
const cx=x+W>window.innerWidth?x-W:x;
const cy=y+H>window.innerHeight?y-H:y;
function item(icon,label,action){
return <div onMouseDown={e=>{e.stopPropagation();action();onClose();}}
style={{display:"flex",alignItems:"center",gap:9,padding:"7px 14px",cursor:"pointer",fontSize:13,color:C.text}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${icon}`} style={{fontSize:14,width:16,textAlign:"center",flexShrink:0}} aria-hidden="true"/>
{label}
</div>;
}
return <div ref={ref} onMouseDown={e=>e.stopPropagation()}
style={{position:"fixed",left:cx,top:cy,zIndex:9999,
background:C.surface,border:`1px solid ${C.border}`,borderRadius:8,
boxShadow:"0 8px 32px rgba(0,0,0,0.22)",padding:"4px 0",minWidth:W,pointerEvents:"all"}}>
<div style={{padding:"5px 14px 6px",borderBottom:`1px solid ${C.border}`,marginBottom:2}}>
<div style={{display:"flex",alignItems:"center",gap:7}}>
<i className={`ti ${getAttIcon(name)}`} style={{fontSize:13,color:C.accent,flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:12,color:C.muted,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:140}}>{name}</span>
</div>
</div>
{item("ti-eye","View",()=>onView&&onView())}
{item("ti-download","Download",()=>downloadAttachment(name,srcEmail))}
{item("ti-printer","Print",()=>printAttachment(name,srcEmail))}
</div>;
}
function ContextMenu({x,y,email,C,onClose,onOpen,onReply,onReplyAll,onForward,onForwardAsAtt,onToggleRead,onToggleStar,onArchive,onDelete,onPrint,moveFolders,onMove}){
const ref=useRef(null);
const[moveOpen,setMoveOpen]=useState(false);
useEffect(()=>{
function down(e){if(ref.current&&!ref.current.contains(e.target))onClose();}
function key(e){if(e.key==="Escape")onClose();}
document.addEventListener("mousedown",down);
document.addEventListener("keydown",key);
return()=>{document.removeEventListener("mousedown",down);document.removeEventListener("keydown",key);};
},[]);
const W=200,H=360;
const cx=x+W>window.innerWidth?x-W:x;
const cy=y+H>window.innerHeight?y-H:y;
// Submenu placement: to the right of main menu unless that overflows
const subW=210;
const subOnLeft=cx+W+subW>window.innerWidth;
function item(icon,label,action,danger){
return <div onMouseDown={e=>{e.stopPropagation();action();onClose();}}
style={{display:"flex",alignItems:"center",gap:9,padding:"7px 14px",cursor:"pointer",fontSize:13,color:danger?"#e05252":C.text}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${icon}`} style={{fontSize:14,width:16,textAlign:"center",flexShrink:0}} aria-hidden="true"/>
{label}
</div>;
}
function sep(){return <div style={{height:1,background:C.border,margin:"3px 0"}}/>;}
return <div ref={ref} onMouseDown={e=>e.stopPropagation()}
style={{position:"fixed",left:cx,top:cy,zIndex:9999,
background:C.surface,border:`1px solid ${C.border}`,borderRadius:8,
boxShadow:"0 8px 32px rgba(0,0,0,0.22)",padding:"4px 0",minWidth:W,pointerEvents:"all"}}>
{item("ti-mail-opened","Open",onOpen)}
{sep()}
{item("ti-arrow-back-up","Reply",onReply)}
{item("ti-arrows-left","Reply All",onReplyAll)}
{item("ti-arrow-forward-up","Forward",onForward)}
{item("ti-mail-forward","Forward as attachment",onForwardAsAtt)}
{sep()}
{item(email.read?"ti-mail":"ti-mail-opened",email.read?"Mark as unread":"Mark as read",onToggleRead)}
{item(email.starred?"ti-star":"ti-star-filled",email.starred?"Unstar":"Star",onToggleStar)}
{sep()}
{/* Move to folder */}
<div onMouseDown={e=>{e.stopPropagation();setMoveOpen(v=>!v);}}
style={{display:"flex",alignItems:"center",gap:9,padding:"7px 14px",cursor:"pointer",fontSize:13,color:C.text,background:moveOpen?C.hover:"transparent"}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background=moveOpen?C.hover:"transparent"}>
<i className="ti ti-folder-symlink" style={{fontSize:14,width:16,textAlign:"center",flexShrink:0}} aria-hidden="true"/>
<span style={{flex:1}}>Move to folder</span>
<i className="ti ti-chevron-right" style={{fontSize:13,color:C.subtle}} aria-hidden="true"/>
</div>
{moveOpen&&<div style={{position:"absolute",top:0,
[subOnLeft?"right":"left"]:"100%",
marginLeft:subOnLeft?0:2,marginRight:subOnLeft?2:0,
background:C.surface,border:`1px solid ${C.border}`,borderRadius:8,
boxShadow:"0 8px 32px rgba(0,0,0,0.22)",padding:"4px 0",minWidth:subW,
maxHeight:340,overflowY:"auto"}}>
{(moveFolders||[]).length===0
?<div style={{padding:"7px 14px",fontSize:12.5,color:C.subtle,fontStyle:"italic"}}>No other folders</div>
:(moveFolders||[]).map(f=>(
<div key={f.key} onMouseDown={e=>{e.stopPropagation();onMove&&onMove(f.key);onClose();}}
style={{display:"flex",alignItems:"center",gap:9,padding:"7px 14px",cursor:"pointer",fontSize:13,color:C.text}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${f.icon||"ti-folder"}`} style={{fontSize:14,width:16,textAlign:"center",flexShrink:0,color:f.custom?C.accent:C.muted}} aria-hidden="true"/>
<span style={{flex:1}}>{f.label}</span>
{f.custom&&<span style={{fontSize:10,color:C.subtle,letterSpacing:0.4,textTransform:"uppercase"}}>Custom</span>}
</div>
))}
</div>}
{sep()}
{item("ti-printer","Print",onPrint)}
{sep()}
{item("ti-archive","Archive",onArchive)}
{item("ti-trash","Move to Trash",onDelete,true)}
</div>;
}
// ── Email Row ─────────────────────────────────────────────────────────────────
function EmailRow({email,selected,C,onClick,onContextMenu}){
const[h,setH]=useState(false);
return <div onClick={onClick} onContextMenu={e=>{e.preventDefault();onContextMenu&&onContextMenu(e,email);}} onMouseEnter={()=>setH(true)} onMouseLeave={()=>setH(false)}
style={{display:"flex",gap:10,padding:"10px 12px",
background:selected?C.selectedBg:h?C.hover:"transparent",
borderLeft:`3px solid ${selected?C.accent:"transparent"}`,
cursor:"pointer",borderBottom:`1px solid ${C.border}`,transition:"background 0.1s"}}>
<div style={{paddingTop:2,flexShrink:0}}><Avatar initials={email.av} color={email.avColor} size={34}/></div>
<div style={{flex:1,minWidth:0}}>
<div style={{display:"flex",justifyContent:"space-between",marginBottom:2}}>
<span style={{fontSize:13.5,fontWeight:email.read?400:700,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:160}}>{email.from}</span>
<span style={{fontSize:11.5,color:C.subtle,flexShrink:0}}>{email.date}</span>
</div>
<div style={{fontSize:13,color:email.read?C.muted:C.text,fontWeight:email.read?400:600,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",marginBottom:2}}>{email.subject}</div>
<div style={{fontSize:12,color:C.subtle,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{email.preview}</div>
</div>
<div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:4,paddingTop:2,flexShrink:0}}>
{!email.read&&<div style={{width:8,height:8,borderRadius:"50%",background:C.accent}}/>}
{email.starred&&<i className="ti ti-star-filled" style={{fontSize:12,color:"#f7c600"}} aria-hidden="true"/>}
</div>
</div>;
}
// ── First-run Setup Wizard ────────────────────────────────────────────────────
const MOCK_DISCOVERED=[
{email:"alex@example-corp.com", count:1842, size:"312 MB", checked:true},
{email:"sales@example-corp.com", count:234, size:"41 MB", checked:true},
{email:"j.doe@mailbox.org", count:89, size:"14 MB", checked:false},
{email:"default", count:12, size:"2 MB", checked:false},
];
function SetupWizard({onComplete,mode="setup",prefillSmb=null}){
const isDiscover=mode==="discover";
const[step,setStep]=useState(isDiscover?2:1); // 1 welcome, 2 smb, 3 scanning, 4 accounts, 5 profile, 6 import, 7 done
const TOTAL=5; // visible progress steps
// SMB fields — pre-fill from existing config in discover mode
const[host,setHost]=useState(prefillSmb?.host||"");
const[share,setShare]=useState(prefillSmb?.share||"");
const[subfolder,setSubfolder]=useState(prefillSmb?.subfolder||"");
const[smbu,setSmbu]=useState(prefillSmb?.username||"");
const[smbp,setSmbp]=useState(prefillSmb?.password||"");
// If we have a pre-filled host, treat connection as already verified
const[connStatus,setConnStatus]=useState(prefillSmb?.host?.trim()?"ok":null);
const[connErr,setConnErr]=useState("");
// Discovery
const[scanning,setScanning]=useState(false);
const[discovered,setDiscovered]=useState([]);
// Account selection
const[accounts,setAccounts]=useState([]);
// Profile
const[profName,setProfName]=useState("");
const[profPw,setProfPw]=useState("");
const[profPwConfirm,setProfPwConfirm]=useState("");
const[profErr,setProfErr]=useState("");
// Import
const[importProgress,setImportProgress]=useState(0);
const[importCurrent,setImportCurrent]=useState("");
const[importDone,setImportDone]=useState(false);
function testConn(){
setConnStatus("testing");setConnErr("");
setTimeout(()=>{
if(host.trim()&&share.trim()){setConnStatus("ok");}
else{setConnStatus("err");setConnErr("Host and share name are required.");}
},1400);
}
function startScan(){
setStep(3);setScanning(true);setDiscovered([]);
setTimeout(()=>{
setScanning(false);
setDiscovered(MOCK_DISCOVERED);
setAccounts(MOCK_DISCOVERED.map(d=>({...d})));
setStep(4);
},2200);
}
function toggleAccount(i){
setAccounts(prev=>prev.map((a,idx)=>idx===i?{...a,checked:!a.checked}:a));
}
function validateProfile(){
if(!profName.trim()){setProfErr("Display name is required.");return false;}
if(!profPw){setProfErr("Password is required.");return false;}
if(profPw!==profPwConfirm){setProfErr("Passwords don't match.");return false;}
setProfErr("");return true;
}
function startImport(){
setStep(6);
const selected=accounts.filter(a=>a.checked);
let idx=0;
let done=0;
const total=selected.reduce((s,a)=>s+a.count,0);
function tick(){
if(idx>=selected.length){setImportProgress(100);setImportDone(true);setStep(7);return;}
const acct=selected[idx];
setImportCurrent(acct.email);
let acctDone=0;
const acctInterval=setInterval(()=>{
acctDone+=Math.floor(acct.count/8)+1;
done+=Math.floor(acct.count/8)+1;
if(acctDone>=acct.count){
done=done-(acctDone-acct.count);
clearInterval(acctInterval);
idx++;tick();
}
setImportProgress(Math.min(99,Math.round(done/total*100)));
},120);
}
tick();
}
const C=mkTheme(true,"#0099b8"); // wizard always dark
const inp={width:"100%",boxSizing:"border-box",padding:"9px 12px",
border:`1px solid rgba(255,255,255,0.12)`,borderRadius:7,fontSize:13.5,
fontFamily:FONT,outline:"none",color:"#f0f0f0",background:"rgba(255,255,255,0.07)",
marginBottom:12};
const label={display:"block",fontSize:11.5,fontWeight:600,color:"rgba(255,255,255,0.45)",
marginBottom:5,letterSpacing:"0.07em",textTransform:"uppercase"};
const stepNames=["Connect","Accounts","Profile","Import","Done"];
const stepMap={2:0,3:0,4:1,5:2,6:3,7:4};
const activeStep=stepMap[step]??-1;
return <div style={{minHeight:"100vh",background:"#0c0c0c",display:"flex",flexDirection:"column",
alignItems:"center",justifyContent:"center",fontFamily:FONT,padding:"32px 16px"}}>
{/* Logo */}
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:32}}>
<DashMailLogo size={36}/>
<span style={{fontSize:22,fontWeight:900,color:"#fff",letterSpacing:"-0.5px"}}>Dash<span style={{fontWeight:200,color:"#00c4d8"}}>Mail</span></span>
</div>
{/* Progress bar — visible from step 2+ */}
{step>=2&&step<=7&&<div style={{display:"flex",alignItems:"center",gap:0,marginBottom:32,width:"min(460px,90vw)"}}>
{stepNames.map((name,i)=>{
const done=i<activeStep;
const active=i===activeStep;
return <React.Fragment key={i}>
<div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:5}}>
<div style={{width:28,height:28,borderRadius:"50%",
background:done?"#00c4d8":active?"#00c4d8":"rgba(255,255,255,0.1)",
border:`2px solid ${done||active?"#00c4d8":"rgba(255,255,255,0.15)"}`,
display:"flex",alignItems:"center",justifyContent:"center",transition:"all 0.3s"}}>
{done
?<i className="ti ti-check" style={{fontSize:13,color:"#0c0c0c"}} aria-hidden="true"/>
:<span style={{fontSize:11,fontWeight:700,color:active?"#0c0c0c":"rgba(255,255,255,0.4)"}}>{i+1}</span>}
</div>
<span style={{fontSize:10,color:active||done?"#00c4d8":"rgba(255,255,255,0.3)",fontWeight:active?700:400,whiteSpace:"nowrap"}}>{name}</span>
</div>
{i<stepNames.length-1&&<div style={{flex:1,height:2,background:done?"#00c4d8":"rgba(255,255,255,0.1)",margin:"0 4px 18px",transition:"background 0.3s"}}/>}
</React.Fragment>;
})}
</div>}
{/* Card */}
<div style={{width:"min(460px,90vw)",background:"#181818",borderRadius:14,
border:"1px solid rgba(255,255,255,0.08)",padding:"32px",
boxShadow:"0 24px 60px rgba(0,0,0,0.6)"}}>
{/* ── Step 1: Welcome ── */}
{step===1&&<>
<div style={{textAlign:"center",marginBottom:28}}>
<div style={{fontSize:22,fontWeight:700,color:"#fff",marginBottom:8}}>Welcome to DashMail</div>
<div style={{fontSize:13.5,color:"rgba(255,255,255,0.45)",lineHeight:1.7}}>
Let's get you connected to your mail server.<br/>
This takes about 2 minutes.
</div>
</div>
<div style={{display:"flex",flexDirection:"column",gap:10,marginBottom:24}}>
{[
{icon:"ti-server", text:"Connect to your SMB mail server"},
{icon:"ti-mail-search", text:"Auto-discover your email accounts"},
{icon:"ti-database", text:"Import existing emails locally"},
{icon:"ti-shield-lock", text:"Set up your profile and password"},
].map((r,i)=>(
<div key={i} style={{display:"flex",alignItems:"center",gap:12,padding:"10px 14px",
background:"rgba(255,255,255,0.04)",borderRadius:8,border:"1px solid rgba(255,255,255,0.06)"}}>
<i className={`ti ${r.icon}`} style={{fontSize:18,color:"#00c4d8",flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:13,color:"rgba(255,255,255,0.7)"}}>{r.text}</span>
</div>
))}
</div>
<button onClick={()=>setStep(2)}
style={{width:"100%",padding:"13px",border:"none",borderRadius:9,background:"#00c4d8",
color:"#0c0c0c",fontSize:15,fontWeight:700,cursor:"pointer",fontFamily:FONT,
display:"flex",alignItems:"center",justifyContent:"center",gap:8}}>
Get started <i className="ti ti-arrow-right" style={{fontSize:15}} aria-hidden="true"/>
</button>
</>}
{/* ── Step 2: SMB connection ── */}
{step===2&&<>
<div style={{fontSize:18,fontWeight:700,color:"#fff",marginBottom:6}}>
{isDiscover?"Verify mail server connection":"Connect to mail server"}
</div>
<div style={{fontSize:13,color:"rgba(255,255,255,0.4)",marginBottom:22,lineHeight:1.6}}>
{isDiscover
?"Confirm your SMB share details then scan for new account folders."
:"Enter your SMB share details. DashMail will scan for email account folders automatically."}
</div>
<label style={label}>Server host</label>
<input value={host} onChange={e=>setHost(e.target.value)} placeholder="192.168.1.100 or mailserver.local" style={inp}/>
<div style={{display:"flex",gap:10}}>
<div style={{flex:2}}>
<label style={label}>Share name</label>
<input value={share} onChange={e=>setShare(e.target.value)} placeholder="mail" style={inp}/>
</div>
<div style={{flex:1}}>
<label style={label}>Subfolder</label>
<input value={subfolder} onChange={e=>setSubfolder(e.target.value)} placeholder=".Mails" style={inp}/>
</div>
</div>
<div style={{display:"flex",gap:10}}>
<div style={{flex:1}}>
<label style={label}>Username</label>
<input value={smbu} onChange={e=>setSmbu(e.target.value)} placeholder="domain\\user" style={inp} autoComplete="off"/>
</div>
<div style={{flex:1}}>
<label style={label}>Password</label>
<input type="password" value={smbp} onChange={e=>setSmbp(e.target.value)} style={inp} autoComplete="new-password"/>
</div>
</div>
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:20}}>
<button onClick={testConn} disabled={connStatus==="testing"}
style={{background:"transparent",border:"1px solid rgba(0,196,216,0.5)",borderRadius:6,
padding:"7px 16px",fontSize:13,cursor:"pointer",fontFamily:FONT,color:"#00c4d8",
display:"flex",alignItems:"center",gap:6,opacity:connStatus==="testing"?0.6:1}}>
{connStatus==="testing"
?<><i className="ti ti-loader-2 ti-spin" style={{fontSize:13}} aria-hidden="true"/>Testing…</>
:<><i className="ti ti-plug" style={{fontSize:13}} aria-hidden="true"/>Test connection</>}
</button>
{connStatus==="ok"&&<span style={{fontSize:13,color:"#22c55e",display:"flex",alignItems:"center",gap:5,fontWeight:600}}>
<i className="ti ti-circle-check" style={{fontSize:14}} aria-hidden="true"/>Connected
</span>}
{connStatus==="err"&&<span style={{fontSize:13,color:"#f87171"}}>{connErr}</span>}
</div>
<div style={{display:"flex",gap:8}}>
<button onClick={()=>setStep(1)}
style={{flex:1,padding:"11px",border:"1px solid rgba(255,255,255,0.1)",borderRadius:9,
background:"transparent",color:"rgba(255,255,255,0.45)",fontSize:14,cursor:"pointer",fontFamily:FONT}}>
Back
</button>
<button onClick={startScan} disabled={connStatus!=="ok"}
style={{flex:2,padding:"11px",border:"none",borderRadius:9,
background:connStatus==="ok"?"#00c4d8":"rgba(0,196,216,0.2)",
color:connStatus==="ok"?"#0c0c0c":"rgba(255,255,255,0.2)",
fontSize:14,fontWeight:700,cursor:connStatus==="ok"?"pointer":"default",fontFamily:FONT,
display:"flex",alignItems:"center",justifyContent:"center",gap:8}}>
Scan for accounts <i className="ti ti-scan" style={{fontSize:15}} aria-hidden="true"/>
</button>
</div>
</>}
{/* ── Step 3: Scanning ── */}
{step===3&&<div style={{textAlign:"center",padding:"20px 0"}}>
<i className="ti ti-loader-2 ti-spin" style={{fontSize:40,color:"#00c4d8",display:"block",marginBottom:16}} aria-hidden="true"/>
<div style={{fontSize:16,fontWeight:700,color:"#fff",marginBottom:8}}>Scanning mail server…</div>
<div style={{fontSize:13,color:"rgba(255,255,255,0.4)"}}>
Searching <code style={{color:"#00c4d8",fontSize:12}}>\\{host||"mailserver"}\\{share||"mail"}\\{subfolder||".Mails"}</code> for account folders
</div>
</div>}
{/* ── Step 4: Select accounts ── */}
{step===4&&<>
<div style={{fontSize:18,fontWeight:700,color:"#fff",marginBottom:6}}>Accounts found</div>
<div style={{fontSize:13,color:"rgba(255,255,255,0.4)",marginBottom:20,lineHeight:1.6}}>
DashMail found <strong style={{color:"#00c4d8"}}>{discovered.length} account folders</strong> on the share.
Select which ones to import. Your emails on the server are never modified.
</div>
<div style={{display:"flex",flexDirection:"column",gap:8,marginBottom:20}}>
{accounts.map((a,i)=>(
<div key={i} onClick={()=>toggleAccount(i)}
style={{display:"flex",alignItems:"center",gap:12,padding:"12px 14px",
background:a.checked?"rgba(0,196,216,0.08)":"rgba(255,255,255,0.04)",
border:`1px solid ${a.checked?"rgba(0,196,216,0.3)":"rgba(255,255,255,0.06)"}`,
borderRadius:8,cursor:"pointer",transition:"all 0.15s"}}>
<div style={{width:18,height:18,borderRadius:4,flexShrink:0,
background:a.checked?"#00c4d8":"transparent",
border:`2px solid ${a.checked?"#00c4d8":"rgba(255,255,255,0.25)"}`,
display:"flex",alignItems:"center",justifyContent:"center",transition:"all 0.15s"}}>
{a.checked&&<i className="ti ti-check" style={{fontSize:11,color:"#0c0c0c"}} aria-hidden="true"/>}
</div>
<i className="ti ti-mail" style={{fontSize:16,color:a.checked?"#00c4d8":"rgba(255,255,255,0.3)",flexShrink:0}} aria-hidden="true"/>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:13.5,color:a.checked?"#fff":"rgba(255,255,255,0.5)",fontWeight:a.checked?500:400,
overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{a.email}</div>
<div style={{fontSize:11.5,color:"rgba(255,255,255,0.3)",marginTop:2}}>
{a.count.toLocaleString()} emails · {a.size}
</div>
</div>
</div>
))}
</div>
<div style={{fontSize:12,color:"rgba(255,255,255,0.25)",marginBottom:20,display:"flex",alignItems:"center",gap:6}}>
<i className="ti ti-shield-check" style={{fontSize:13,color:"rgba(0,196,216,0.5)"}} aria-hidden="true"/>
All emails are copied to a local database. The SMB share is read-only — nothing is deleted or moved.
</div>
<div style={{display:"flex",gap:8}}>
<button onClick={()=>setStep(2)}
style={{flex:1,padding:"11px",border:"1px solid rgba(255,255,255,0.1)",borderRadius:9,
background:"transparent",color:"rgba(255,255,255,0.45)",fontSize:14,cursor:"pointer",fontFamily:FONT}}>
Back
</button>
<button onClick={()=>setStep(5)} disabled={!accounts.some(a=>a.checked)}
style={{flex:2,padding:"11px",border:"none",borderRadius:9,
background:accounts.some(a=>a.checked)?"#00c4d8":"rgba(0,196,216,0.2)",
color:accounts.some(a=>a.checked)?"#0c0c0c":"rgba(255,255,255,0.2)",
fontSize:14,fontWeight:700,cursor:accounts.some(a=>a.checked)?"pointer":"default",
fontFamily:FONT,display:"flex",alignItems:"center",justifyContent:"center",gap:8}}>
Continue <i className="ti ti-arrow-right" style={{fontSize:15}} aria-hidden="true"/>
</button>
</div>
</>}
{/* ── Step 5: Profile setup ── */}
{step===5&&<>
<div style={{fontSize:18,fontWeight:700,color:"#fff",marginBottom:6}}>Create your profile</div>
<div style={{fontSize:13,color:"rgba(255,255,255,0.4)",marginBottom:22,lineHeight:1.6}}>
Your profile password protects access to DashMail on this device. It is stored locally only.
</div>
<label style={label}>Display name</label>
<input value={profName} onChange={e=>{setProfName(e.target.value);setProfErr("");}} placeholder="e.g. Alex Johnson" style={inp}/>
<label style={label}>Password</label>
<input type="password" value={profPw} onChange={e=>{setProfPw(e.target.value);setProfErr("");}} style={inp} autoComplete="new-password"/>
<label style={label}>Confirm password</label>
<input type="password" value={profPwConfirm} onChange={e=>{setProfPwConfirm(e.target.value);setProfErr("");}} style={inp} autoComplete="new-password"/>
{profErr&&<div style={{fontSize:12.5,color:"#f87171",marginBottom:10,marginTop:-6}}>{profErr}</div>}
<div style={{display:"flex",gap:8,marginTop:4}}>
<button onClick={()=>setStep(4)}
style={{flex:1,padding:"11px",border:"1px solid rgba(255,255,255,0.1)",borderRadius:9,
background:"transparent",color:"rgba(255,255,255,0.45)",fontSize:14,cursor:"pointer",fontFamily:FONT}}>
Back
</button>
<button onClick={()=>{if(validateProfile())startImport();}}
style={{flex:2,padding:"11px",border:"none",borderRadius:9,background:"#00c4d8",
color:"#0c0c0c",fontSize:14,fontWeight:700,cursor:"pointer",fontFamily:FONT,
display:"flex",alignItems:"center",justifyContent:"center",gap:8}}>
Start import <i className="ti ti-database-import" style={{fontSize:15}} aria-hidden="true"/>
</button>
</div>
</>}
{/* ── Step 6: Importing ── */}
{step===6&&<div style={{textAlign:"center",padding:"12px 0"}}>
<i className="ti ti-database-import" style={{fontSize:36,color:"#00c4d8",display:"block",marginBottom:16}} aria-hidden="true"/>
<div style={{fontSize:16,fontWeight:700,color:"#fff",marginBottom:6}}>Importing emails…</div>
<div style={{fontSize:13,color:"rgba(255,255,255,0.4)",marginBottom:20}}>
Copying from SMB share to local database.<br/>This may take a few minutes.
</div>
<div style={{background:"rgba(255,255,255,0.06)",borderRadius:8,height:8,overflow:"hidden",marginBottom:10}}>
<div style={{height:"100%",background:"#00c4d8",borderRadius:8,
width:`${importProgress}%`,transition:"width 0.3s"}}/>
</div>
<div style={{fontSize:12.5,color:"rgba(255,255,255,0.4)",marginBottom:6}}>{importProgress}% complete</div>
{importCurrent&&<div style={{fontSize:12,color:"rgba(255,255,255,0.25)",overflow:"hidden",
textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{importCurrent}</div>}
</div>}
{/* ── Step 7: Done ── */}
{step===7&&<div style={{textAlign:"center",padding:"12px 0"}}>
<div style={{width:60,height:60,borderRadius:"50%",background:"rgba(34,197,94,0.12)",
display:"flex",alignItems:"center",justifyContent:"center",margin:"0 auto 16px"}}>
<i className="ti ti-circle-check" style={{fontSize:34,color:"#22c55e"}} aria-hidden="true"/>
</div>
<div style={{fontSize:18,fontWeight:700,color:"#fff",marginBottom:8}}>All set!</div>
<div style={{fontSize:13,color:"rgba(255,255,255,0.45)",lineHeight:1.7,marginBottom:24}}>
{accounts.filter(a=>a.checked).reduce((s,a)=>s+a.count,0).toLocaleString()} emails imported successfully.<br/>
Your SMB share was not modified in any way.
</div>
<div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:24}}>
{accounts.filter(a=>a.checked).map((a,i)=>(
<div key={i} style={{display:"flex",alignItems:"center",gap:8,padding:"8px 12px",
background:"rgba(34,197,94,0.06)",borderRadius:7,border:"1px solid rgba(34,197,94,0.15)"}}>
<i className="ti ti-circle-check" style={{fontSize:13,color:"#22c55e",flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:13,color:"rgba(255,255,255,0.65)",flex:1,textAlign:"left",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{a.email}</span>
<span style={{fontSize:12,color:"rgba(255,255,255,0.3)",flexShrink:0}}>{a.count.toLocaleString()} emails</span>
</div>
))}
</div>
<button onClick={onComplete}
style={{width:"100%",padding:"13px",border:"none",borderRadius:9,background:"#00c4d8",
color:"#0c0c0c",fontSize:15,fontWeight:700,cursor:"pointer",fontFamily:FONT,
display:"flex",alignItems:"center",justifyContent:"center",gap:8}}>
Launch DashMail <i className="ti ti-arrow-right" style={{fontSize:15}} aria-hidden="true"/>
</button>
</div>}
</div>
<div style={{marginTop:20,fontSize:11,color:"rgba(255,255,255,0.12)",letterSpacing:"1.5px",textTransform:"uppercase"}}>
Dash<span style={{color:"rgba(0,196,216,0.25)"}}>Mail</span> &bull; The Secure Faxmail Client
</div>
</div>;
}
// ── Main App ──────────────────────────────────────────────────────────────────
export default function DashMailClient(){
const[profiles,setProfiles]=useState(INIT_PROFILES);
const[activeProfile,setActiveProfile]=useState(null);
const[loginBg,setLoginBg]=useState(null);
const[loginDark,setLoginDark]=useState(true);
const[firstRun,setFirstRun]=useState(false);
// Check SMB share for folders not yet covered by any profile
const[newFoldersFound,setNewFoldersFound]=useState(false);
useEffect(()=>{
// Simulate async share scan on app start (~1.5s)
const knownAccounts=new Set(INIT_PROFILES.flatMap(p=>p.accountIds));
const t=setTimeout(()=>{
const newOnes=MOCK_NEW_FOLDERS.filter(f=>!knownAccounts.has(f));
if(newOnes.length>0) setNewFoldersFound(true);
},1500);
return ()=>clearTimeout(t);
},[]);
function selectProfile(p){setActiveProfile(p);}
function saveProfile(updated){
setProfiles(prev=>prev.map(p=>p.id===updated.id?updated:p));
setActiveProfile(updated);
}
if(firstRun)
return <SetupWizard onComplete={()=>setFirstRun(false)}/>;
if(!activeProfile)
return <LoginScreen profiles={profiles} onLogin={selectProfile} loginBg={loginBg} loginDark={loginDark}
onSetupNew={newFoldersFound?()=>setFirstRun(true):undefined}/>;
return <EmailApp activeProfile={activeProfile} profiles={profiles}
setActiveProfile={setActiveProfile} saveProfile={saveProfile}
loginBg={loginBg} setLoginBg={setLoginBg}
loginDark={loginDark} setLoginDark={setLoginDark}/>;
}
function EmailApp({activeProfile,profiles,setActiveProfile,saveProfile,loginBg,setLoginBg,loginDark,setLoginDark}){
const[isDark,setDark]=useState(activeProfile.settings?.isDark??false);
const[accent,setAccent]=useState(activeProfile.settings?.accent??"#0099bb");
const[bg,setBg]=useState(activeProfile.settings?.bg??"white");
const C=mkTheme(isDark,accent);
// Accounts visible to this profile
// Editable account display data (persists across settings opens)
const[accountData,setAccountData]=useState(
Object.fromEntries(ALL_ACCOUNTS.map(a=>[a.id,{display:a.display,email:a.email}]))
);
function updateAccount(id,updates){
setAccountData(prev=>({...prev,[id]:{...(prev[id]||{}), ...updates}}));
}
const ACCOUNTS=ALL_ACCOUNTS
.filter(a=>activeProfile.accountIds.length===0||activeProfile.accountIds.includes(a.id))
.map(a=>({...a,...(accountData[a.id]||{})}));
// SMB config (shared across profiles)
const[sentToast,setSentToast]=useState(null);
const[smbConfig,setSmbConfig]=useState({host:"",share:"mail",driveLetter:"Z",subfolder:".Mails",username:"",password:""});
// Sync theme state when active profile settings change (e.g. after saving settings)
useEffect(()=>{
setDark(activeProfile.settings?.isDark??false);
setAccent(activeProfile.settings?.accent??"#0099bb");
setBg(activeProfile.settings?.bg??"white");
},[activeProfile]);
// In-app setup wizard (for adding accounts after initial setup)
const[wizardOpen,setWizardOpen]=useState(false);
// Startup: check for new folders on the mail server
const[newFolderBanner,setNewFolderBanner]=useState(null);
useEffect(()=>{
// Simulate discovering a new folder ~3s after login
const t=setTimeout(()=>{
setNewFolderBanner({count:1,folder:"invoices@example-corp.com"});
},3000);
return ()=>clearTimeout(t);
},[]);
// Live theme preview callback (called by SettingsTheme immediately on change)
function handleLiveTheme(dark,accent,bgId){
setDark(dark); setAccent(accent); setBg(bgId);
}
function selectProfileSwitch(){setActiveProfile(null);}
// ── Email app state ─────────────────────────────────────────────────────────
const[sidebarCollapsed,setSidebarCollapsed]=useState(false);
const[selAccountId,setSelAccount]=useState(ACCOUNTS[0]?.id??null);
const[selFolder,setSelFolder]=useState("inbox");
const[selEmailId,setSelEmail]=useState(null);
const[emails,setEmails]=useState(SEED);
const[rules,setRules]=useState(INIT_RULES);
const[contacts,setContacts]=useState(INIT_CONTACTS);
const[customFolders,setCF]=useState({
"alex@example-corp.com":[{id:"cf1",name:"Clients",icon:"ti-building"},{id:"cf2",name:"Projects",icon:"ti-folder"}],
"sales@example-corp.com":[{id:"cf3",name:"Leads",icon:"ti-user-circle"}],
"j.doe@mailbox.org":[],"default":[]
});
const[search,setSearch]=useState("");
const[collapsed,setCollapsed]=useState({});
const[fullSettings,setFullSettings]=useState(false);
const[contactsOpen,setContactsOpen]=useState(false);
const[rulesOpen,setRulesOpen]=useState(false);
const[compose,setCompose]=useState(null);
const[syncing,setSyncing]=useState(false);
const[syncDone,setSyncDone]=useState(false);
const[showFilter,setShowFilter]=useState(false);
const[filterUnread,setFilterUnread]=useState(false);
const[filterStarred,setFilterStarred]=useState(false);
const[filterHasAtt,setFilterHasAtt]=useState(false);
const[filterFrom,setFilterFrom]=useState("");
const[filterSubject,setFilterSubject]=useState("");
const[onboarding,setOnboarding]=useState(null);
const[addFolderFor,setAddFolderFor]=useState(null);
const[newFolderName,setNewFolderName]=useState("");
const[ctxMenu,setCtxMenu]=useState(null);
const[attCtxMenu,setAttCtxMenu]=useState(null); // {x,y,name,email}
const[attViewer,setAttViewer]=useState(null); // {name,email}
const[emailListWidth,setEmailListWidth]=useState(320);
const dragRef=useRef(null);
const selAccount=ACCOUNTS.find(a=>a.id===selAccountId);
const selEmail=emails.find(e=>e.id===selEmailId)??null;
const cfForAccount=customFolders[selAccountId]??[];
const allFolderNames=[...STD_FOLDERS.map(f=>f.id),...Object.values(customFolders).flat().map(f=>f.name.toLowerCase())];
const visibleEmails=emails.filter(e=>{
if(e.account!==selAccountId||e.folder!==selFolder)return false;
if(filterUnread&&e.read)return false;
if(filterStarred&&!e.starred)return false;
if(filterHasAtt&&!e.atts?.length)return false;
if(filterFrom&&!e.from.toLowerCase().includes(filterFrom.toLowerCase()))return false;
if(filterSubject&&!e.subject.toLowerCase().includes(filterSubject.toLowerCase()))return false;
if(!search)return true;
const q=search.toLowerCase();
return e.from.toLowerCase().includes(q)||e.subject.toLowerCase().includes(q)||e.preview.toLowerCase().includes(q);
});
const activeFilterCount=[filterUnread,filterStarred,filterHasAtt,filterFrom,filterSubject].filter(Boolean).length;
function unread(accountId,folderId){return emails.filter(e=>e.account===accountId&&e.folder===folderId&&!e.read).length;}
function openEmail(email){setSelEmail(email.id);setEmails(p=>p.map(e=>e.id===email.id?{...e,read:true}:e));}
function doArchive(){if(selEmail){setEmails(p=>p.map(e=>e.id===selEmail.id?{...e,folder:"archive"}:e));setSelEmail(null);}}
function doDelete(){if(selEmail){setEmails(p=>p.map(e=>e.id===selEmail.id?{...e,folder:"trash"}:e));setSelEmail(null);}}
function doJunk(){if(selEmail){setEmails(p=>p.map(e=>e.id===selEmail.id?{...e,folder:"trash"}:e));setSelEmail(null);}}
function doStar(){if(selEmail)setEmails(p=>p.map(e=>e.id===selEmail.id?{...e,starred:!e.starred}:e));}
function doAddToContacts(data){
if(!data?.email)return"added";
const exists=contacts.some(c=>c.email?.toLowerCase()===data.email.toLowerCase());
if(!exists){
setContacts(p=>[...p,{id:"ct"+uid(),name:data.name||data.email,email:data.email,
av:data.av||data.email.slice(0,2).toUpperCase(),avColor:data.avColor||"#0078d4",phone:"",address:""}]);
return"added";
}
return"exists";
}
function doUnread(){if(selEmail){setEmails(p=>p.map(e=>e.id===selEmail.id?{...e,read:false}:e));setSelEmail(null);}}
function openReply(mode){
if(!selEmail)return;
const fe=selAccount?.email??selAccountId;
const reSubj=selEmail.subject.match(/^Re:/i)?selEmail.subject:`Re: ${selEmail.subject}`;
const fwSubj=selEmail.subject.match(/^Fwd:/i)?selEmail.subject:`Fwd: ${selEmail.subject}`;
if(mode==="reply") setCompose({mode,fromEmail:fe,initial:{to:selEmail.from_email,subject:reSubj,body:quoteBody(selEmail.body,selEmail.from)}});
if(mode==="replyAll")setCompose({mode,fromEmail:fe,initial:{to:selEmail.from_email,cc:selEmail.to,subject:reSubj,body:quoteBody(selEmail.body,selEmail.from)}});
if(mode==="forward") setCompose({mode,fromEmail:fe,initial:{subject:fwSubj,body:fwdBody(selEmail)}});
}
function handleSend(msg){
setEmails(p=>[{id:Date.now(),account:selAccountId,folder:"out",
from:selAccount?.display??"Me",from_email:selAccount?.email??"",
to:msg.to,subject:msg.subject||"(no subject)",preview:msg.body.slice(0,120),
body:msg.body,date:"Just now",read:true,starred:false,
atts:(msg.attachments||[]).map(f=>f.name),
av:selAccount?.av??"ME",avColor:selAccount?.avColor??accent},...p]);
if(!msg.draft){
setSentToast({to:msg.to,subject:msg.subject||"(no subject)"});
setTimeout(()=>setSentToast(null),3500);
}
}
function createCustomFolder(){
if(!newFolderName.trim()||!addFolderFor)return;
setCF(p=>({...p,[addFolderFor]:[...(p[addFolderFor]??[]),{id:uid(),name:newFolderName.trim(),icon:"ti-folder"}]}));
setNewFolderName("");setAddFolderFor(null);
}
function archiveEmail(email){setEmails(p=>p.map(e=>e.id===email.id?{...e,folder:"archive"}:e));if(selEmailId===email.id)setSelEmail(null);}
function deleteEmail(email){setEmails(p=>p.map(e=>e.id===email.id?{...e,folder:"trash"}:e));if(selEmailId===email.id)setSelEmail(null);}
function moveEmail(email,folderKey){setEmails(p=>p.map(e=>e.id===email.id?{...e,folder:folderKey}:e));if(selEmailId===email.id)setSelEmail(null);}
function moveFoldersFor(email){
if(!email)return[];
const cur=email.folder;
const std=STD_FOLDERS.filter(f=>f.id!==cur).map(f=>({key:f.id,label:f.label,icon:f.icon,custom:false}));
const cfs=(customFolders[email.account]??[]).map(cf=>({key:`__cf_${cf.id}`,label:cf.name,icon:cf.icon,custom:true})).filter(f=>f.key!==cur);
return[...std,...cfs];
}
function toggleStarEmail(email){setEmails(p=>p.map(e=>e.id===email.id?{...e,starred:!e.starred}:e));}
function toggleReadEmail(email){setEmails(p=>p.map(e=>e.id===email.id?{...e,read:!e.read}:e));}
function replyEmail(mode,email){
const fe=selAccount?.email??selAccountId;
const reSubj=email.subject.match(/^Re:/i)?email.subject:`Re: ${email.subject}`;
const fwSubj=email.subject.match(/^Fwd:/i)?email.subject:`Fwd: ${email.subject}`;
if(mode==="reply") setCompose({mode,fromEmail:fe,initial:{to:email.from_email,subject:reSubj,body:quoteBody(email.body,email.from)}});
if(mode==="replyAll")setCompose({mode,fromEmail:fe,initial:{to:email.from_email,cc:email.to,subject:reSubj,body:quoteBody(email.body,email.from)}});
if(mode==="forward") setCompose({mode,fromEmail:fe,initial:{subject:fwSubj,body:fwdBody(email)}});
if(mode==="forwardAsAtt"){
const safe=(email.subject||"message").replace(/[\\/:*?"<>|]+/g,"_").replace(/\s+/g,"_").slice(0,80)||"message";
const emlSize=(email.body?.length??0)+(email.subject?.length??0)+(email.from?.length??0)+512;
const emlAtt={name:`${safe}.eml`,size:emlSize,isEmail:true,source:{id:email.id,from:email.from,from_email:email.from_email,to:email.to,subject:email.subject,date:email.date}};
setCompose({mode,fromEmail:fe,initial:{subject:fwSubj,body:"",attachments:[emlAtt]}});
}
}
const totalUnread=emails.filter(e=>!e.read&&ACCOUNTS.some(a=>a.id===e.account)).length;
const bgStyle=getBgStyle(bg,activeProfile.settings?.bgCustomUrl);
const bgDark=bgIsDark(bg)||bgIsImage(bg);
const readingTextColor=bgDark?"rgba(255,255,255,0.9)":C.text;
const readingMutedColor=bgDark?"rgba(255,255,255,0.55)":C.muted;
useEffect(()=>{
function onResize(){setEmailListWidth(w=>w);}// nudge React to re-layout
window.addEventListener("resize",onResize);
return()=>window.removeEventListener("resize",onResize);
},[]);
return <div style={{display:"flex",flexDirection:"column",height:"100%",fontFamily:FONT,background:C.bg,position:"relative",overflow:"hidden"}}>
{/* Title bar */}
<div style={{height:32,background:C.titleBar,display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:0}}>
<div style={{display:"flex",alignItems:"center",gap:8,paddingLeft:12}}>
<span style={{fontSize:16,letterSpacing:"-0.3px"}}><span style={{color:"#fff",fontWeight:800}}>Dash</span><span style={{color:"#00c4d8",fontWeight:200}}>Mail</span></span>
</div>
<div style={{display:"flex",alignItems:"center"}}>
{/* Logout button */}
<div onClick={()=>setActiveProfile(null)}
title="Sign out"
style={{height:32,padding:"0 14px",display:"flex",alignItems:"center",gap:6,cursor:"pointer",color:"rgba(255,255,255,0.55)",fontSize:12,fontWeight:500,fontFamily:FONT,borderRight:"1px solid rgba(255,255,255,0.07)"}}
onMouseEnter={e=>{e.currentTarget.style.background="rgba(255,255,255,0.08)";e.currentTarget.style.color="rgba(255,255,255,0.9)";}}
onMouseLeave={e=>{e.currentTarget.style.background="transparent";e.currentTarget.style.color="rgba(255,255,255,0.55)";}}>
<i className="ti ti-logout" style={{fontSize:14}} aria-hidden="true"/>
<span>Sign out</span>
</div>
{["ti-minus","ti-square","ti-x"].map((ic,i)=>(
<div key={ic} style={{width:46,height:32,display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer"}}
onMouseEnter={e=>e.currentTarget.style.background=i===2?"#c42b1c":"rgba(255,255,255,0.1)"}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${ic}`} style={{fontSize:14,color:"#c8c8c8"}} aria-hidden="true"/>
</div>
))}
</div>
</div>
{/* Main row */}
<div style={{display:"flex",flex:1,minHeight:0}}>
{/* ── Sidebar ───────────────────────────────────────────────────────── */}
<div style={{width:sidebarCollapsed?56:264,background:C.sidebar,borderRight:`1px solid ${C.border}`,display:"flex",flexDirection:"column",flexShrink:0,overflow:"hidden",transition:"width 0.22s cubic-bezier(0.4,0,0.2,1)",position:"relative"}}>
<div style={{padding:"12px 8px 8px",flexShrink:0}}>
<button onClick={()=>setCompose({mode:"new",fromEmail:selAccount?.email??selAccountId,initial:{}})}
style={{width:"100%",background:accent,color:"#fff",border:"none",borderRadius:6,padding:"9px 0",fontSize:13.5,fontWeight:600,cursor:"pointer",fontFamily:FONT,display:"flex",alignItems:"center",justifyContent:"center",gap:7,overflow:"hidden",whiteSpace:"nowrap"}}
onMouseEnter={e=>e.currentTarget.style.background=C.accentHov}
onMouseLeave={e=>e.currentTarget.style.background=accent}>
<i className="ti ti-edit" style={{fontSize:16,flexShrink:0}} aria-hidden="true"/>
{!sidebarCollapsed&&"New Message"}
</button>
</div>
<div style={{flex:1,overflowY:"auto",padding:"0 8px",overflowX:"hidden"}}>
{ACCOUNTS.map(acc=>{
const isColl=collapsed[acc.id];
const isSelAcc=acc.id===selAccountId;
return <div key={acc.id}>
<div onClick={()=>{setCollapsed(p=>({...p,[acc.id]:!isColl}));setSelAccount(acc.id);}}
style={{display:"flex",alignItems:"center",gap:8,padding:"7px 8px",borderRadius:6,cursor:"pointer",marginTop:6,justifyContent:sidebarCollapsed?"center":"flex-start"}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<Avatar initials={acc.av} color={acc.avColor} size={26}/>
{!sidebarCollapsed&&<><div style={{flex:1,minWidth:0}}>
<div style={{fontSize:12.5,fontWeight:700,color:isSelAcc?accent:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{acc.display}</div>
{acc.email&&<div style={{fontSize:11,color:C.subtle,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{acc.email}</div>}
</div>
<i className={`ti ti-chevron-${isColl?"right":"down"}`} style={{fontSize:12,color:C.subtle,flexShrink:0}} aria-hidden="true"/></>}
</div>
{!isColl&&<>
{STD_FOLDERS.map(f=>{
const ur=unread(acc.id,f.id);
const isSel=isSelAcc&&selFolder===f.id;
return <FolderItem key={f.id} icon={f.icon} label={sidebarCollapsed?"":f.label} badge={!sidebarCollapsed&&f.badge&&ur>0?ur:0}
selected={isSel} C={C} indent={sidebarCollapsed?8:28} collapsed={sidebarCollapsed}
onClick={()=>{setSelAccount(acc.id);setSelFolder(f.id);setSelEmail(null);}}/>;
})}
{!sidebarCollapsed&&<div style={{marginTop:6}}>
<div style={{display:"flex",alignItems:"center",padding:"4px 8px 4px 28px"}}>
<span style={{flex:1,fontSize:10.5,fontWeight:700,color:C.subtle,letterSpacing:"0.07em",textTransform:"uppercase"}}>My Folders</span>
<button title="Scan" onClick={e=>{e.stopPropagation();setCF(p=>({...p,[acc.id]:[...(p[acc.id]??[]),{id:uid(),name:"New-"+uid().slice(0,4),icon:"ti-folder-search"}]}));}}
style={{background:"transparent",border:"none",cursor:"pointer",color:C.subtle,padding:"2px 3px"}}><i className="ti ti-folder-search" style={{fontSize:13}} aria-hidden="true"/></button>
<button title="New folder" onClick={e=>{e.stopPropagation();setAddFolderFor(acc.id);setNewFolderName("");}}
style={{background:"transparent",border:"none",cursor:"pointer",color:C.subtle,padding:"2px 3px"}}><i className="ti ti-plus" style={{fontSize:13}} aria-hidden="true"/></button>
</div>
{addFolderFor===acc.id&&<div style={{padding:"4px 8px 6px 28px",display:"flex",gap:5}}>
<input autoFocus value={newFolderName} onChange={e=>setNewFolderName(e.target.value)}
onKeyDown={e=>{if(e.key==="Enter")createCustomFolder();if(e.key==="Escape")setAddFolderFor(null);}}
placeholder="Folder name…"
style={{flex:1,padding:"5px 8px",border:`1px solid ${C.accent}`,borderRadius:5,fontSize:12.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}/>
<button onClick={createCustomFolder} style={{background:accent,color:"#fff",border:"none",borderRadius:5,padding:"4px 9px",cursor:"pointer",fontFamily:FONT,fontSize:12}}>Add</button>
<button onClick={()=>setAddFolderFor(null)} style={{background:"transparent",border:`1px solid ${C.border}`,borderRadius:5,padding:"4px 7px",cursor:"pointer",color:C.muted,fontSize:12,fontFamily:FONT}}>✕</button>
</div>}
{(customFolders[acc.id]??[]).map(cf=>{
const cfKey=`__cf_${cf.id}`;
const isSel=isSelAcc&&selFolder===cfKey;
return <div key={cf.id} style={{display:"flex",alignItems:"center",padding:"6px 8px 6px 28px",borderRadius:6,cursor:"pointer",marginBottom:1,background:isSel?C.selectedBg:"transparent",borderLeft:`3px solid ${isSel?accent:"transparent"}`}}
onClick={()=>{setSelAccount(acc.id);setSelFolder(cfKey);setSelEmail(null);}}
onMouseEnter={e=>{if(!isSel){e.currentTarget.style.background=C.hover;e.currentTarget.querySelector(".cfx")&&(e.currentTarget.querySelector(".cfx").style.opacity="1");}}}
onMouseLeave={e=>{if(!isSel){e.currentTarget.style.background="transparent";e.currentTarget.querySelector(".cfx")&&(e.currentTarget.querySelector(".cfx").style.opacity="0");}}}>
<i className={`ti ${cf.icon}`} style={{fontSize:14,color:isSel?accent:C.muted,width:18,textAlign:"center",marginRight:7}} aria-hidden="true"/>
<span style={{flex:1,fontSize:13,color:isSel?accent:C.text,fontWeight:isSel?600:400}}>{cf.name}</span>
<button className="cfx" onClick={e=>{e.stopPropagation();setCF(p=>({...p,[acc.id]:(p[acc.id]??[]).filter(f=>f.id!==cf.id)}));}}
style={{opacity:0,background:"transparent",border:"none",cursor:"pointer",color:C.subtle,padding:"1px 3px",transition:"opacity 0.1s"}}>
<i className="ti ti-x" style={{fontSize:12}} aria-hidden="true"/>
</button>
</div>;
})}
{(customFolders[acc.id]??[]).length===0&&addFolderFor!==acc.id&&<div style={{padding:"3px 8px 6px 28px"}}><span style={{fontSize:11.5,color:C.subtle,fontStyle:"italic"}}>No custom folders</span></div>}
</div>}
</>}
</div>;
})}
</div>
{/* Sidebar bottom */}
<div style={{padding:"8px",borderTop:`1px solid ${C.border}`,flexShrink:0}}>
{/* Profile switcher */}
<div style={{display:"flex",alignItems:"center",gap:9,padding:"7px 8px",borderRadius:7,background:C.surface2,border:`1px solid ${C.border}`,marginBottom:6,cursor:"pointer",justifyContent:sidebarCollapsed?"center":"flex-start"}}
onClick={()=>setActiveProfile(null)}>
<div style={{width:28,height:28,borderRadius:"50%",background:activeProfile.color+"30",border:`2px solid ${activeProfile.color}`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:11,fontWeight:700,color:activeProfile.color,flexShrink:0}}>{activeProfile.initials}</div>
{!sidebarCollapsed&&<><div style={{flex:1,minWidth:0}}>
<div style={{fontSize:12.5,fontWeight:600,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{activeProfile.name}</div>
<div style={{fontSize:11,color:C.subtle}}>Switch profile</div>
</div>
<i className="ti ti-chevron-up-down" style={{fontSize:13,color:C.subtle,flexShrink:0}} aria-hidden="true"/></>}
</div>
{[{icon:"ti-address-book",label:"Contacts",fn:()=>setContactsOpen(true)},{icon:"ti-settings",label:"Settings",fn:()=>setFullSettings(true)}].map(item=>(
<div key={item.label} onClick={item.fn} title={sidebarCollapsed?item.label:undefined}
style={{display:"flex",alignItems:"center",gap:8,padding:"6px 8px",borderRadius:6,cursor:"pointer",justifyContent:sidebarCollapsed?"center":"flex-start"}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<i className={`ti ${item.icon}`} style={{fontSize:15,color:C.muted,flexShrink:0}} aria-hidden="true"/>
{!sidebarCollapsed&&<span style={{fontSize:12.5,color:C.muted,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{item.label}</span>}
</div>
))}
{/* Sidebar collapse toggle */}
<div onClick={()=>setSidebarCollapsed(v=>!v)}
title={sidebarCollapsed?"Expand sidebar":"Collapse sidebar"}
style={{display:"flex",alignItems:"center",justifyContent:sidebarCollapsed?"center":"flex-end",padding:"6px 8px",borderRadius:6,cursor:"pointer",marginTop:2}}
onMouseEnter={e=>e.currentTarget.style.background=C.hover}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
{/* Panel-toggle icon: two vertical rectangles */}
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"
style={{flexShrink:0,transform:sidebarCollapsed?"scaleX(-1)":"scaleX(1)",transition:"transform 0.22s"}}>
<rect x="1.5" y="2.5" width="4" height="13" rx="1.2" stroke={C.muted} strokeWidth="1.4"/>
<rect x="7.5" y="2.5" width="9" height="13" rx="1.2" stroke={C.muted} strokeWidth="1.4" strokeOpacity="0.45"/>
</svg>
</div>
</div>
</div>
{/* ── Email list ──────────────────────────────────────────────────────── */}
<div style={{width:emailListWidth,background:C.surface,display:"flex",flexDirection:"column",flexShrink:0,minWidth:200,maxWidth:600}}>
<div style={{padding:"12px 12px 8px",borderBottom:`1px solid ${C.border}`,flexShrink:0}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:8}}>
<div style={{display:"flex",alignItems:"center",gap:8}}>
<span style={{fontSize:15,fontWeight:600,color:C.text}}>
{selFolder.startsWith("__cf_")
?cfForAccount.find(f=>`__cf_${f.id}`===selFolder)?.name??"Folder"
:STD_FOLDERS.find(f=>f.id===selFolder)?.label??"Folder"}
</span>
{syncing&&<span style={{fontSize:11.5,color:C.accent,fontWeight:500,animation:"fadeInUp 0.15s ease"}}>Syncing…</span>}
{syncDone&&!syncing&&<span style={{fontSize:11.5,color:"#22c55e",fontWeight:500,animation:"fadeInUp 0.15s ease"}}>Synced ✓</span>}
</div>
<div style={{display:"flex"}}>
<div style={{position:"relative",display:"flex",alignItems:"center"}}>
{/* Sync button with done indicator */}
<div style={{position:"relative"}}>
<IBtn icon={`ti-refresh${syncing?" ti-spin":""}`} title="Sync" C={C}
onClick={()=>{setSyncing(true);setSyncDone(false);setTimeout(()=>{setSyncing(false);setSyncDone(true);setTimeout(()=>setSyncDone(false),2200);},1600);}}/>
{syncDone&&<div style={{position:"absolute",top:-4,right:-4,width:16,height:16,
borderRadius:"50%",background:"#22c55e",
display:"flex",alignItems:"center",justifyContent:"center",pointerEvents:"none",
animation:"popIn 0.2s ease",zIndex:5}}>
<i className="ti ti-check" style={{fontSize:9,color:"#fff"}} aria-hidden="true"/>
</div>}
</div>
{/* Filter button with active badge */}
<div style={{position:"relative"}}>
<IBtn icon="ti-adjustments-horizontal" title="Filter"
C={{...C,text:activeFilterCount>0?C.accent:C.text}}
onClick={()=>setShowFilter(v=>!v)}/>
{activeFilterCount>0&&<div style={{position:"absolute",top:-4,right:-4,width:16,height:16,
borderRadius:"50%",background:C.accent,
display:"flex",alignItems:"center",justifyContent:"center",pointerEvents:"none",fontSize:9,
fontWeight:700,color:"#fff",fontFamily:FONT,zIndex:5}}>
{activeFilterCount}
</div>}
</div>
</div>
</div>
</div>
<div style={{position:"relative"}}>
<i className="ti ti-search" style={{position:"absolute",left:9,top:"50%",transform:"translateY(-50%)",fontSize:14,color:C.subtle}} aria-hidden="true"/>
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search…"
style={{width:"100%",boxSizing:"border-box",padding:"7px 9px 7px 30px",border:`1px solid ${C.inBorder}`,borderRadius:6,fontSize:13,fontFamily:FONT,background:C.inputBg,color:C.text,outline:"none"}}/>
</div>
</div>
{/* Filter dropdown panel */}
{showFilter&&<div style={{margin:"6px 0",background:C.surface2,
border:`1px solid ${C.border}`,borderRadius:8,padding:"10px 12px",fontSize:13,fontFamily:FONT}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8}}>
<span style={{fontSize:12,fontWeight:700,color:C.muted,letterSpacing:"0.04em"}}>FILTER MESSAGES</span>
{activeFilterCount>0&&<button onClick={()=>{setFilterUnread(false);setFilterStarred(false);setFilterHasAtt(false);setFilterFrom("");setFilterSubject("");}}
style={{fontSize:11.5,color:C.accent,background:"transparent",border:"none",cursor:"pointer",fontFamily:FONT,padding:0}}>
Clear all
</button>}
</div>
<div style={{display:"flex",gap:6,flexWrap:"wrap",marginBottom:10}}>
{[{label:"Unread",active:filterUnread,fn:()=>setFilterUnread(v=>!v),icon:"ti-mail"},
{label:"Starred",active:filterStarred,fn:()=>setFilterStarred(v=>!v),icon:"ti-star"},
{label:"Has attachment",active:filterHasAtt,fn:()=>setFilterHasAtt(v=>!v),icon:"ti-paperclip"},
].map(chip=>(
<button key={chip.label} onClick={chip.fn}
style={{display:"flex",alignItems:"center",gap:5,padding:"4px 10px",borderRadius:20,
border:`1px solid ${chip.active?C.accent:C.border}`,
background:chip.active?C.accentBg:"transparent",
color:chip.active?C.accent:C.muted,fontSize:12,cursor:"pointer",fontFamily:FONT,
fontWeight:chip.active?600:400,transition:"all 0.12s"}}>
<i className={`ti ${chip.icon}`} style={{fontSize:12}} aria-hidden="true"/>
{chip.label}
</button>
))}
</div>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8}}>
{[["From",filterFrom,setFilterFrom,"sender name or email"],
["Subject",filterSubject,setFilterSubject,"keywords"]].map(([lbl,val,set,ph])=>(
<div key={lbl}>
<label style={{display:"block",fontSize:11,fontWeight:600,color:C.subtle,marginBottom:3}}>{lbl.toUpperCase()}</label>
<input value={val} onChange={e=>set(e.target.value)} placeholder={ph}
style={{width:"100%",boxSizing:"border-box",padding:"5px 8px",
border:`1px solid ${val?C.accent:C.inBorder}`,borderRadius:6,
fontSize:12.5,fontFamily:FONT,outline:"none",color:C.text,background:C.inputBg}}
onFocus={e=>e.target.style.borderColor=C.accent}
onBlur={e=>e.target.style.borderColor=val?C.accent:C.inBorder}/>
</div>
))}
</div>
</div>}
<div style={{flex:1,overflowY:"auto",minHeight:0}}>
{visibleEmails.length===0
?<div style={{padding:32,textAlign:"center",color:C.subtle}}><i className="ti ti-inbox-off" style={{fontSize:36,display:"block",marginBottom:8}} aria-hidden="true"/><div style={{fontSize:13}}>No messages</div></div>
:visibleEmails.map(email=><EmailRow key={email.id} email={email} selected={selEmailId===email.id} C={C} onClick={()=>openEmail(email)} onContextMenu={(e,em)=>setCtxMenu({x:e.clientX,y:e.clientY,email:em})}/>)}
</div>
</div>
{/* ── Resize handle ───────────────────────────────────────────────────── */}
<div style={{width:5,flexShrink:0,cursor:"col-resize",position:"relative",zIndex:10,background:"transparent"}}
onMouseDown={e=>{
e.preventDefault();
const startX=e.clientX;
const startW=emailListWidth;
function onMove(ev){
const next=Math.min(600,Math.max(200,startW+(ev.clientX-startX)));
setEmailListWidth(next);
}
function onUp(){
document.removeEventListener("mousemove",onMove);
document.removeEventListener("mouseup",onUp);
document.body.style.cursor="";
document.body.style.userSelect="";
}
document.addEventListener("mousemove",onMove);
document.addEventListener("mouseup",onUp);
document.body.style.cursor="col-resize";
document.body.style.userSelect="none";
}}
onMouseEnter={e=>e.currentTarget.style.background=C.accent+"60"}
onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
<div style={{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%,-50%)",
width:3,height:32,borderRadius:2,background:C.border,pointerEvents:"none"}}/>
</div>
{/* ── Reading pane ─────────────────────────────────────────────────────── */}
<div style={{flex:1,display:"flex",flexDirection:"column",minWidth:0,...bgStyle}}>
{selEmail?<>
<EmailToolbar email={selEmail} folder={selFolder} C={C}
onReply={()=>openReply("reply")} onReplyAll={()=>openReply("replyAll")}
onForward={()=>openReply("forward")} onArchive={doArchive} onDelete={doDelete}
onJunk={doJunk} onStar={doStar} onUnread={doUnread} onPrint={()=>printEmail(selEmail)}
onAddToContacts={doAddToContacts}/>
<div style={{padding:"16px 22px 14px",borderBottom:`1px solid ${bgDark?"rgba(255,255,255,0.12)":C.border}`,background:bgDark?"rgba(0,0,0,0.25)":"rgba(255,255,255,0.7)",flexShrink:0,backdropFilter:bgIsImage(bg)?"blur(12px)":"none"}}>
<h2 style={{margin:"0 0 12px",fontSize:17,fontWeight:600,color:readingTextColor,lineHeight:1.3}}>
{selEmail.subject}
{selEmail.starred&&<i className="ti ti-star-filled" style={{fontSize:15,color:"#f7c600",marginLeft:8,verticalAlign:"middle"}} aria-hidden="true"/>}
</h2>
<div style={{display:"flex",alignItems:"center",gap:10}}>
<Avatar initials={selEmail.av} color={selEmail.avColor} size={38}/>
<div style={{flex:1}}>
<div style={{fontSize:13.5,fontWeight:600,color:readingTextColor}}>{selEmail.from}</div>
<div style={{fontSize:12,color:readingMutedColor}}><span style={{color:accent}}>{selEmail.from_email}</span>{" → "}{selEmail.to}</div>
</div>
<span style={{fontSize:12,color:readingMutedColor,flexShrink:0}}>{selEmail.date}</span>
</div>
</div>
<div style={{flex:1,overflowY:"auto",padding:"18px 22px"}}>
<div style={{maxWidth:700,background:bgDark||bgIsImage(bg)?"rgba(0,0,0,0.25)":"transparent",borderRadius:bgDark||bgIsImage(bg)?10:0,padding:bgDark||bgIsImage(bg)?"18px 20px":0,backdropFilter:bgIsImage(bg)?"blur(8px)":"none"}}>
{selEmail.atts?.length>0&&<div style={{marginBottom:16,padding:"10px 14px",background:accent+"15",border:`1px solid ${accent}22`,borderRadius:7}}>
<div style={{fontSize:11.5,color:readingMutedColor,fontWeight:700,marginBottom:8,letterSpacing:"0.05em"}}><i className="ti ti-paperclip" style={{fontSize:13,marginRight:5}} aria-hidden="true"/>ATTACHMENTS ({selEmail.atts.length})</div>
<div style={{display:"flex",flexWrap:"wrap",gap:8}}>
{selEmail.atts.map(a=>(
<div key={a}
onClick={()=>downloadAttachment(a,selEmail)}
onContextMenu={e=>{e.preventDefault();setAttCtxMenu({x:e.clientX,y:e.clientY,name:a,email:selEmail});}}
title={`Click to download · Right-click for more options`}
style={{display:"flex",alignItems:"center",gap:6,background:bgDark?"rgba(255,255,255,0.1)":C.surface,border:`1px solid ${C.border}`,borderRadius:5,padding:"5px 10px",fontSize:12.5,color:readingTextColor,cursor:"pointer",transition:"background 0.12s,border-color 0.12s"}}
onMouseEnter={e=>{e.currentTarget.style.background=bgDark?"rgba(255,255,255,0.18)":C.hover;e.currentTarget.style.borderColor=accent;}}
onMouseLeave={e=>{e.currentTarget.style.background=bgDark?"rgba(255,255,255,0.1)":C.surface;e.currentTarget.style.borderColor=C.border;}}>
<i className={`ti ${getAttIcon(a)}`} style={{fontSize:14,color:accent,flexShrink:0}} aria-hidden="true"/>
<span style={{maxWidth:180,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{a}</span>
<i className="ti ti-download" style={{fontSize:13,color:readingMutedColor,flexShrink:0}} aria-hidden="true"/>
</div>
))}
</div>
</div>}
{/<[a-z][\s\S]*>/i.test(selEmail.body??'')
?<div dangerouslySetInnerHTML={{__html:selEmail.body}} style={{fontFamily:FONT,fontSize:14,color:readingTextColor,lineHeight:1.75}}/>
:<pre style={{fontFamily:FONT,fontSize:14,color:readingTextColor,lineHeight:1.75,whiteSpace:"pre-wrap",margin:0}}>{selEmail.body}</pre>}
</div>
</div>
<div style={{padding:"10px 22px",borderTop:`1px solid ${bgDark?"rgba(255,255,255,0.12)":C.border}`,background:bgDark?"rgba(0,0,0,0.3)":"rgba(255,255,255,0.7)",display:"flex",gap:8,flexShrink:0,backdropFilter:bgIsImage(bg)?"blur(12px)":"none"}}>
{[["reply","Reply","ti-arrow-back-up"],["replyAll","Reply All","ti-arrows-left"],["forward","Forward","ti-arrow-forward-up"]].map(([mode,lbl,ic])=>(
<button key={mode} onClick={()=>openReply(mode)}
style={{background:mode==="reply"?accent:"transparent",color:mode==="reply"?"#fff":bgDark?"rgba(255,255,255,0.7)":C.muted,
border:mode==="reply"?"none":`1px solid ${bgDark?"rgba(255,255,255,0.2)":C.border}`,borderRadius:5,
padding:"7px 14px",fontSize:13,cursor:"pointer",fontFamily:FONT,display:"flex",alignItems:"center",gap:6}}
onMouseEnter={e=>{e.currentTarget.style.background=mode==="reply"?C.accentHov:bgDark?"rgba(255,255,255,0.1)":C.hover;}}
onMouseLeave={e=>{e.currentTarget.style.background=mode==="reply"?accent:"transparent";}}>
<i className={`ti ${ic}`} style={{fontSize:14}} aria-hidden="true"/>{lbl}
</button>
))}
</div>
</>
:<div style={{flex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:10}}>
<i className="ti ti-mail-opened" style={{fontSize:52,color:bgDark?"rgba(255,255,255,0.2)":C.subtle}} aria-hidden="true"/>
<div style={{fontSize:15,fontWeight:500,color:bgDark?"rgba(255,255,255,0.5)":C.muted}}>Select an email to read</div>
</div>}
</div>
</div>
{/* Status bar */}
<div style={{height:28,background:accent,display:"flex",alignItems:"center",justifyContent:"space-between",padding:"0 14px",flexShrink:0}}>
<div style={{display:"flex",alignItems:"center",gap:14}}>
<span style={{fontSize:11.5,color:"rgba(255,255,255,0.9)",display:"flex",alignItems:"center",gap:6}}>
<span style={{width:7,height:7,borderRadius:"50%",flexShrink:0,
background:syncing?"#f59e0b":"#73d680",
boxShadow:syncing?"0 0 0 2px rgba(245,158,11,0.35)":"0 0 0 2px rgba(115,214,128,0.35)",
display:"inline-block"}}/>
{syncing?"Syncing":"Connected"} — Faxmail Email Server
</span>
</div>
<div style={{display:"flex",alignItems:"center",gap:14}}>
<span style={{fontSize:11.5,color:"rgba(255,255,255,0.9)"}}>{totalUnread>0?`${totalUnread} unread`:"All read"}</span>
<span style={{fontSize:11.5,color:"rgba(255,255,255,0.7)"}}>{new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}</span>
</div>
</div>
{/* Overlays */}
{ctxMenu&&<ContextMenu x={ctxMenu.x} y={ctxMenu.y} email={ctxMenu.email} C={C}
onClose={()=>setCtxMenu(null)}
onOpen={()=>openEmail(ctxMenu.email)}
onReply={()=>replyEmail("reply",ctxMenu.email)}
onReplyAll={()=>replyEmail("replyAll",ctxMenu.email)}
onForward={()=>replyEmail("forward",ctxMenu.email)}
onForwardAsAtt={()=>replyEmail("forwardAsAtt",ctxMenu.email)}
onToggleRead={()=>toggleReadEmail(ctxMenu.email)}
onToggleStar={()=>toggleStarEmail(ctxMenu.email)}
onArchive={()=>archiveEmail(ctxMenu.email)}
onDelete={()=>deleteEmail(ctxMenu.email)}
onPrint={()=>printEmail(ctxMenu.email)}
moveFolders={moveFoldersFor(ctxMenu.email)}
onMove={folderKey=>moveEmail(ctxMenu.email,folderKey)}/>}
{attCtxMenu&&<AttachmentContextMenu x={attCtxMenu.x} y={attCtxMenu.y}
name={attCtxMenu.name} srcEmail={attCtxMenu.email} C={C}
onClose={()=>setAttCtxMenu(null)}
onView={()=>{setAttViewer({name:attCtxMenu.name,email:attCtxMenu.email});setAttCtxMenu(null);}}/>}
{attViewer&&<AttachmentViewer name={attViewer.name} srcEmail={attViewer.email} C={C}
onDownload={()=>downloadAttachment(attViewer.name,attViewer.email)}
onPrint={()=>printAttachment(attViewer.name,attViewer.email)}
onClose={()=>setAttViewer(null)}/>}
{sentToast&&<div style={{position:"fixed",bottom:24,right:24,zIndex:300,
background:isDark?"#1e2d1e":"#f0fdf4",
border:"1px solid #86efac",borderRadius:10,
boxShadow:"0 4px 20px rgba(0,0,0,0.15)",
padding:"12px 18px",display:"flex",alignItems:"center",gap:10,
fontFamily:FONT,minWidth:260,
animation:"slideInRight 0.25s ease"}}>
<div style={{width:30,height:30,borderRadius:"50%",background:"#22c55e",
display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
<i className="ti ti-check" style={{fontSize:16,color:"#fff"}} aria-hidden="true"/>
</div>
<div style={{flex:1,minWidth:0}}>
<div style={{fontSize:13.5,fontWeight:700,color:"#15803d"}}>Message sent</div>
<div style={{fontSize:12,color:"#16a34a",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
To: {sentToast.to}
</div>
</div>
<button onClick={()=>setSentToast(null)}
style={{background:"transparent",border:"none",cursor:"pointer",color:"#86efac",padding:2,lineHeight:1}}>
<i className="ti ti-x" style={{fontSize:14}} aria-hidden="true"/>
</button>
</div>}
{contactsOpen&&<ContactsManager contacts={contacts} setContacts={setContacts} C={C} onClose={()=>setContactsOpen(false)}
onCompose={c=>{setCompose({mode:"new",fromEmail:selAccount?.email??selAccountId,initial:{to:c.name+" <"+c.email+">"}});setContactsOpen(false);}}/>}
{compose&&<ComposeModal {...compose} C={C} onClose={()=>setCompose(null)} onSend={handleSend} contacts={contacts}/>}
{rulesOpen&&<RulesPanel C={C} rules={rules} setRules={setRules} allFolders={allFolderNames}
updateAccount={updateAccount} onClose={()=>setRulesOpen(false)}/>}
{onboarding&&<OnboardingWizard accountId={onboarding.accountId} C={C} onComplete={()=>setOnboarding(null)}/>}
{fullSettings&&<FullSettings profile={activeProfile} accounts={ACCOUNTS} C={C}
onSave={saveProfile} onClose={()=>setFullSettings(false)}
onLiveChange={handleLiveTheme}
smbConfig={smbConfig} setSmbConfig={setSmbConfig}
rules={rules} setRules={setRules} allFolders={allFolderNames}
updateAccount={updateAccount}
loginBg={loginBg} setLoginBg={setLoginBg}
loginDark={loginDark} setLoginDark={setLoginDark}
onSetupNew={()=>{setFullSettings(false);setWizardOpen(true);}}/>}
{/* ── New folder discovery banner ── */}
{newFolderBanner&&!wizardOpen&&<div style={{position:"fixed",top:0,left:0,right:0,zIndex:500,
background:"#0d3318",borderBottom:"1px solid rgba(34,197,94,0.28)",
display:"flex",alignItems:"center",gap:12,padding:"10px 20px",fontFamily:FONT}}>
<i className="ti ti-folder-plus" style={{fontSize:17,color:"#4ade80",flexShrink:0}} aria-hidden="true"/>
<span style={{fontSize:13,color:"#d1fae5",flex:1,lineHeight:1.4}}>
<strong style={{color:"#86efac"}}>{newFolderBanner.count} new email folder</strong> found on your mail server
{" — "}<span style={{color:"#6ee7b7",fontFamily:"monospace",fontSize:12.5}}>{newFolderBanner.folder}</span>
</span>
<button onClick={()=>{setNewFolderBanner(null);setWizardOpen(true);}}
style={{padding:"5px 14px",border:"none",borderRadius:6,background:"#22c55e",
color:"#fff",fontSize:12.5,fontWeight:600,cursor:"pointer",fontFamily:FONT,flexShrink:0,
display:"flex",alignItems:"center",gap:6}}>
<i className="ti ti-settings-plus" style={{fontSize:13}} aria-hidden="true"/>Set up
</button>
<button onClick={()=>setNewFolderBanner(null)}
style={{background:"transparent",border:"none",color:"rgba(209,250,229,0.45)",
cursor:"pointer",padding:"2px 4px",fontSize:18,lineHeight:1,flexShrink:0}}
title="Dismiss">×</button>
</div>}
{/* ── In-app setup wizard overlay (discover mode) ── */}
{wizardOpen&&<div style={{position:"fixed",inset:0,zIndex:600,background:"#0c0c0c",
display:"flex",flexDirection:"column"}}>
<SetupWizard onComplete={()=>setWizardOpen(false)} mode="discover" prefillSmb={smbConfig}/>
</div>}
</div>;
}
function FolderItem({icon,label,badge,selected,C,onClick,indent=8,collapsed=false}){
const[h,setH]=useState(false);
return <div onClick={onClick} onMouseEnter={()=>setH(true)} onMouseLeave={()=>setH(false)}
title={collapsed?label:undefined}
style={{display:"flex",alignItems:"center",gap:8,padding:collapsed?"6px 0":`6px 8px 6px ${indent}px`,borderRadius:6,cursor:"pointer",marginBottom:1,justifyContent:collapsed?"center":"flex-start",
background:selected?C.selectedBg:h?C.hover:"transparent",borderLeft:collapsed?"3px solid transparent":`3px solid ${selected?C.accent:"transparent"}`}}>
<i className={`ti ${icon}`} style={{fontSize:collapsed?17:14,color:selected?C.accent:C.muted,width:collapsed?24:18,textAlign:"center"}} aria-hidden="true"/>
{!collapsed&&<><span style={{flex:1,fontSize:13,color:selected?C.accent:C.text,fontWeight:selected?600:400}}>{label}</span>
{badge>0&&<span style={{background:C.accent,color:"#fff",fontSize:10.5,fontWeight:700,borderRadius:10,padding:"1px 6px"}}>{badge}</span>}</>}
</div>;
}