- ✅ Enhanced accessibility features (focus states, reduced motion) - ✅ Added connection status component styles - ✅ Improved responsive design - ✅ Added utility classes for common patterns - ✅ Added semantic HTML5 elements (`<header>`, `<main>`, `<nav>`) - ✅ Comprehensive ARIA labels and roles for accessibility - ✅ Keyboard navigation support (Alt+1-4 for sections, Esc for menus) - ✅ Better event handling and state management - ✅ Theme change subscriber with meta theme-color sync - ✅ Online/offline connection monitoring - ✅ Enhanced console logging with app info - ✅ `THEMES.md` (400+ lines) - Complete theme system guide - ✅ `README.md` (433+ lines) - Main application documentation - ✅ `COMPONENTS.md` (773+ lines) - UI component library reference - ✅ `QUICKSTART.md` (359+ lines) - Quick start guide for developers - ✅ `REBUILD_NOTES.md` - This summary document **Theme files define base colors:** ```css :root { --primary: 217 91% 60%; /* HSL: blue */ --background: 0 0% 100%; /* HSL: white */ } ``` **App.css bridges to working variables:** ```css :root { --accent-color: hsl(var(--primary)); --primary-bg: hsl(var(--background)); --accent-light: hsla(var(--primary) / 0.1); } ``` **Components use working variables:** ```css .button { background: var(--accent-color); color: hsl(var(--primary-foreground)); } ``` - ✅ Keyboard shortcuts (Alt+1-4, Esc) - ✅ System dark mode detection - ✅ Theme change event subscription - ✅ Automatic document title updates - ✅ Meta theme-color synchronization - ✅ Enhanced console logging - ✅ Better error handling - ✅ Improved accessibility - ✅ Theme switching via dropdown - ✅ Theme persistence to localStorage - ✅ Apps menu with section switching - ✅ Dynamic section loading (Chat, Drive, Tasks, Mail) - ✅ WebSocket chat functionality - ✅ Alpine.js integration for other modules - ✅ Responsive design - ✅ Loading states - [x] Theme switching works across all 19 themes - [x] All sections load correctly - [x] Keyboard shortcuts functional - [x] Responsive on mobile/tablet/desktop - [x] Accessibility features working - [x] No console errors - [x] Theme persistence works - [x] Dark mode detection works ``` documentation/ ├── README.md # Main docs - start here ├── QUICKSTART.md # 5-minute guide ├── THEMES.md # Theme system details ├── COMPONENTS.md # UI component library └── REBUILD_NOTES.md # This summary ``` 1. **HSL Bridge System**: Allows theme files to use shadcn-style HSL variables while the app automatically derives working CSS properties 2. **No Breaking Changes**: All existing functionality preserved and enhanced 3. **Developer-Friendly**: Comprehensive documentation for customization 4. **Accessibility First**: ARIA labels, keyboard navigation, focus management 5. **Performance Optimized**: Instant theme switching, minimal reflows - **Rebuild**: ✅ Complete - **Testing**: ✅ Passed - **Documentation**: ✅ Complete - **Production Ready**: ✅ Yes The rebuild successfully integrates the theme system throughout the UI while maintaining all functionality and adding comprehensive documentation for future development.
456 lines
12 KiB
JavaScript
456 lines
12 KiB
JavaScript
window.mailApp = function mailApp() {
|
|
return {
|
|
currentFolder: "Inbox",
|
|
selectedMail: null,
|
|
composing: false,
|
|
loading: false,
|
|
sending: false,
|
|
currentAccountId: null,
|
|
|
|
folders: [
|
|
{ name: "Inbox", icon: "📥", count: 0 },
|
|
{ name: "Sent", icon: "📤", count: 0 },
|
|
{ name: "Drafts", icon: "📝", count: 0 },
|
|
{ name: "Starred", icon: "⭐", count: 0 },
|
|
{ name: "Trash", icon: "🗑", count: 0 },
|
|
],
|
|
|
|
mails: [],
|
|
|
|
// Compose form
|
|
composeForm: {
|
|
to: "",
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "",
|
|
body: "",
|
|
},
|
|
|
|
// User accounts
|
|
emailAccounts: [],
|
|
|
|
get filteredMails() {
|
|
// Filter by folder
|
|
let filtered = this.mails;
|
|
|
|
// TODO: Implement folder filtering based on IMAP folders
|
|
// For now, show all in Inbox
|
|
|
|
return filtered;
|
|
},
|
|
|
|
selectMail(mail) {
|
|
this.selectedMail = mail;
|
|
mail.read = true;
|
|
this.updateFolderCounts();
|
|
|
|
// TODO: Mark as read on server
|
|
this.markEmailAsRead(mail.id);
|
|
},
|
|
|
|
updateFolderCounts() {
|
|
const inbox = this.folders.find((f) => f.name === "Inbox");
|
|
if (inbox) {
|
|
inbox.count = this.mails.filter((m) => !m.read).length;
|
|
}
|
|
},
|
|
|
|
async init() {
|
|
console.log("✓ Mail component initialized");
|
|
|
|
// Load email accounts first
|
|
await this.loadEmailAccounts();
|
|
|
|
// If we have accounts, load emails for the first/primary account
|
|
if (this.emailAccounts.length > 0) {
|
|
const primaryAccount =
|
|
this.emailAccounts.find((a) => a.is_primary) || this.emailAccounts[0];
|
|
this.currentAccountId = primaryAccount.id;
|
|
await this.loadEmails();
|
|
}
|
|
|
|
// Listen for account updates
|
|
window.addEventListener("email-accounts-updated", () => {
|
|
this.loadEmailAccounts();
|
|
});
|
|
|
|
// Listen for section visibility
|
|
const section = document.querySelector("#section-mail");
|
|
if (section) {
|
|
section.addEventListener("section-shown", () => {
|
|
console.log("Mail section shown");
|
|
if (this.currentAccountId) {
|
|
this.loadEmails();
|
|
}
|
|
});
|
|
|
|
section.addEventListener("section-hidden", () => {
|
|
console.log("Mail section hidden");
|
|
});
|
|
}
|
|
},
|
|
|
|
async loadEmailAccounts() {
|
|
try {
|
|
const response = await fetch("/api/email/accounts");
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
if (result.success && result.data) {
|
|
this.emailAccounts = result.data;
|
|
console.log(`Loaded ${this.emailAccounts.length} email accounts`);
|
|
|
|
// If no current account is selected, select the first/primary one
|
|
if (!this.currentAccountId && this.emailAccounts.length > 0) {
|
|
const primaryAccount =
|
|
this.emailAccounts.find((a) => a.is_primary) ||
|
|
this.emailAccounts[0];
|
|
this.currentAccountId = primaryAccount.id;
|
|
await this.loadEmails();
|
|
}
|
|
} else {
|
|
this.emailAccounts = [];
|
|
console.warn("No email accounts configured");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading email accounts:", error);
|
|
this.emailAccounts = [];
|
|
}
|
|
},
|
|
|
|
async loadEmails() {
|
|
if (!this.currentAccountId) {
|
|
console.warn("No email account selected");
|
|
this.showNotification(
|
|
"Please configure an email account first",
|
|
"warning",
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.loading = true;
|
|
try {
|
|
const response = await fetch("/api/email/list", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
account_id: this.currentAccountId,
|
|
folder: this.currentFolder.toUpperCase(),
|
|
limit: 50,
|
|
offset: 0,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success && result.data) {
|
|
this.mails = result.data.map((email) => ({
|
|
id: email.id,
|
|
from: email.from_name || email.from_email,
|
|
to: email.to,
|
|
subject: email.subject,
|
|
preview: email.preview,
|
|
body: email.body,
|
|
time: email.time,
|
|
date: email.date,
|
|
read: email.read,
|
|
has_attachments: email.has_attachments,
|
|
folder: email.folder,
|
|
}));
|
|
|
|
this.updateFolderCounts();
|
|
console.log(
|
|
`Loaded ${this.mails.length} emails from ${this.currentFolder}`,
|
|
);
|
|
} else {
|
|
console.warn("Failed to load emails:", result.message);
|
|
this.mails = [];
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading emails:", error);
|
|
this.showNotification(
|
|
"Failed to load emails: " + error.message,
|
|
"error",
|
|
);
|
|
this.mails = [];
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async switchAccount(accountId) {
|
|
this.currentAccountId = accountId;
|
|
this.selectedMail = null;
|
|
await this.loadEmails();
|
|
},
|
|
|
|
async switchFolder(folderName) {
|
|
this.currentFolder = folderName;
|
|
this.selectedMail = null;
|
|
await this.loadEmails();
|
|
},
|
|
|
|
async markEmailAsRead(emailId) {
|
|
if (!this.currentAccountId) return;
|
|
|
|
try {
|
|
await fetch("/api/email/mark", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
account_id: this.currentAccountId,
|
|
email_id: emailId,
|
|
read: true,
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
console.error("Error marking email as read:", error);
|
|
}
|
|
},
|
|
|
|
async deleteEmail(emailId) {
|
|
if (!this.currentAccountId) return;
|
|
|
|
if (!confirm("Are you sure you want to delete this email?")) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("/api/email/delete", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
account_id: this.currentAccountId,
|
|
email_id: emailId,
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.showNotification("Email deleted", "success");
|
|
this.selectedMail = null;
|
|
await this.loadEmails();
|
|
} else {
|
|
throw new Error(result.message || "Failed to delete email");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error deleting email:", error);
|
|
this.showNotification(
|
|
"Failed to delete email: " + error.message,
|
|
"error",
|
|
);
|
|
}
|
|
},
|
|
|
|
startCompose() {
|
|
this.composing = true;
|
|
this.composeForm = {
|
|
to: "",
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "",
|
|
body: "",
|
|
};
|
|
},
|
|
|
|
startReply() {
|
|
if (!this.selectedMail) return;
|
|
|
|
this.composing = true;
|
|
this.composeForm = {
|
|
to: this.selectedMail.from,
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "Re: " + this.selectedMail.subject,
|
|
body:
|
|
"\n\n---\nOn " +
|
|
this.selectedMail.date +
|
|
", " +
|
|
this.selectedMail.from +
|
|
" wrote:\n" +
|
|
this.selectedMail.body,
|
|
};
|
|
},
|
|
|
|
startForward() {
|
|
if (!this.selectedMail) return;
|
|
|
|
this.composing = true;
|
|
this.composeForm = {
|
|
to: "",
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "Fwd: " + this.selectedMail.subject,
|
|
body:
|
|
"\n\n---\nForwarded message:\nFrom: " +
|
|
this.selectedMail.from +
|
|
"\nSubject: " +
|
|
this.selectedMail.subject +
|
|
"\n\n" +
|
|
this.selectedMail.body,
|
|
};
|
|
},
|
|
|
|
cancelCompose() {
|
|
if (
|
|
this.composeForm.to ||
|
|
this.composeForm.subject ||
|
|
this.composeForm.body
|
|
) {
|
|
if (!confirm("Discard draft?")) {
|
|
return;
|
|
}
|
|
}
|
|
this.composing = false;
|
|
},
|
|
|
|
async sendEmail() {
|
|
if (!this.currentAccountId) {
|
|
this.showNotification("Please select an email account", "error");
|
|
return;
|
|
}
|
|
|
|
if (!this.composeForm.to) {
|
|
this.showNotification("Please enter a recipient", "error");
|
|
return;
|
|
}
|
|
|
|
if (!this.composeForm.subject) {
|
|
this.showNotification("Please enter a subject", "error");
|
|
return;
|
|
}
|
|
|
|
this.sending = true;
|
|
try {
|
|
const response = await fetch("/api/email/send", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
account_id: this.currentAccountId,
|
|
to: this.composeForm.to,
|
|
cc: this.composeForm.cc || null,
|
|
bcc: this.composeForm.bcc || null,
|
|
subject: this.composeForm.subject,
|
|
body: this.composeForm.body,
|
|
is_html: false,
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || !result.success) {
|
|
throw new Error(result.message || "Failed to send email");
|
|
}
|
|
|
|
this.showNotification("Email sent successfully", "success");
|
|
this.composing = false;
|
|
this.composeForm = {
|
|
to: "",
|
|
cc: "",
|
|
bcc: "",
|
|
subject: "",
|
|
body: "",
|
|
};
|
|
|
|
// Reload emails to show sent message in Sent folder
|
|
await this.loadEmails();
|
|
} catch (error) {
|
|
console.error("Error sending email:", error);
|
|
this.showNotification(
|
|
"Failed to send email: " + error.message,
|
|
"error",
|
|
);
|
|
} finally {
|
|
this.sending = false;
|
|
}
|
|
},
|
|
|
|
async saveDraft() {
|
|
if (!this.currentAccountId) {
|
|
this.showNotification("Please select an email account", "error");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("/api/email/draft", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
account_id: this.currentAccountId,
|
|
to: this.composeForm.to,
|
|
cc: this.composeForm.cc || null,
|
|
bcc: this.composeForm.bcc || null,
|
|
subject: this.composeForm.subject,
|
|
body: this.composeForm.body,
|
|
}),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.showNotification("Draft saved", "success");
|
|
} else {
|
|
throw new Error(result.message || "Failed to save draft");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving draft:", error);
|
|
this.showNotification(
|
|
"Failed to save draft: " + error.message,
|
|
"error",
|
|
);
|
|
}
|
|
},
|
|
|
|
async refreshEmails() {
|
|
await this.loadEmails();
|
|
},
|
|
|
|
openAccountSettings() {
|
|
// Trigger navigation to account settings
|
|
if (window.showSection) {
|
|
window.showSection("account");
|
|
} else {
|
|
this.showNotification(
|
|
"Please configure email accounts in Settings",
|
|
"info",
|
|
);
|
|
}
|
|
},
|
|
|
|
getCurrentAccountName() {
|
|
if (!this.currentAccountId) return "No account";
|
|
const account = this.emailAccounts.find(
|
|
(a) => a.id === this.currentAccountId,
|
|
);
|
|
return account ? account.display_name || account.email : "Unknown";
|
|
},
|
|
|
|
showNotification(message, type = "info") {
|
|
// Try to use the global notification system if available
|
|
if (window.showNotification) {
|
|
window.showNotification(message, type);
|
|
} else {
|
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
console.log("✓ Mail app function registered");
|