simplifying the chat interface.

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-20 18:29:55 -03:00
parent 564ad32417
commit 37a15ea9e0
9 changed files with 1283 additions and 195 deletions

View file

@ -1 +1,246 @@
a {} /* Drive Layout */
.drive-layout {
display: grid;
grid-template-columns: 250px 1fr 300px;
gap: 1rem;
padding: 1rem;
height: 100%;
width: 100%;
background: #ffffff;
color: #202124;
}
[data-theme="dark"] .drive-layout {
background: #1a1a1a;
color: #e8eaed;
}
.drive-sidebar,
.drive-main,
.drive-details {
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 12px;
overflow: hidden;
}
[data-theme="dark"] .drive-sidebar,
[data-theme="dark"] .drive-main,
[data-theme="dark"] .drive-details {
background: #202124;
border-color: #3c4043;
}
.drive-sidebar {
overflow-y: auto;
}
.drive-main {
display: flex;
flex-direction: column;
}
.drive-details {
overflow-y: auto;
}
/* Navigation Items */
.nav-item {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
margin: 0.25rem 0.5rem;
transition: all 0.2s;
color: #5f6368;
}
[data-theme="dark"] .nav-item {
color: #9aa0a6;
}
.nav-item:hover {
background: rgba(26, 115, 232, 0.08);
color: #1a73e8;
}
[data-theme="dark"] .nav-item:hover {
background: rgba(138, 180, 248, 0.08);
color: #8ab4f8;
}
.nav-item.active {
background: #e8f0fe;
color: #1a73e8;
font-weight: 500;
}
[data-theme="dark"] .nav-item.active {
background: #1e3a5f;
color: #8ab4f8;
}
/* File List */
.file-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.file-item {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 1rem;
cursor: pointer;
border-radius: 0.5rem;
border: 1px solid transparent;
transition: all 0.2s;
margin-bottom: 0.25rem;
}
.file-item:hover {
background: rgba(26, 115, 232, 0.08);
border-color: rgba(26, 115, 232, 0.2);
}
[data-theme="dark"] .file-item:hover {
background: rgba(138, 180, 248, 0.08);
border-color: rgba(138, 180, 248, 0.2);
}
.file-item.selected {
background: #e8f0fe;
border-color: #1a73e8;
}
[data-theme="dark"] .file-item.selected {
background: #1e3a5f;
border-color: #8ab4f8;
}
.file-icon {
font-size: 1.5rem;
flex-shrink: 0;
}
/* Headers */
h2,
h3 {
margin: 0;
padding: 0;
font-weight: 500;
}
/* Text Styles */
.text-xs {
font-size: 0.75rem;
}
.text-sm {
font-size: 0.875rem;
}
.text-gray {
color: #5f6368;
}
[data-theme="dark"] .text-gray {
color: #9aa0a6;
}
/* Inputs */
input[type="text"] {
font-family: inherit;
}
input[type="text"]:focus {
outline: none;
border-color: #1a73e8 !important;
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
[data-theme="dark"] input[type="text"]:focus {
border-color: #8ab4f8 !important;
box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.2);
}
/* Buttons */
button {
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
opacity: 0.9;
}
button:active {
transform: scale(0.98);
}
/* Scrollbar Styles */
.drive-sidebar::-webkit-scrollbar,
.file-list::-webkit-scrollbar,
.drive-details::-webkit-scrollbar {
width: 8px;
}
.drive-sidebar::-webkit-scrollbar-track,
.file-list::-webkit-scrollbar-track,
.drive-details::-webkit-scrollbar-track {
background: transparent;
}
.drive-sidebar::-webkit-scrollbar-thumb,
.file-list::-webkit-scrollbar-thumb,
.drive-details::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.3);
border-radius: 4px;
}
.drive-sidebar::-webkit-scrollbar-thumb:hover,
.file-list::-webkit-scrollbar-thumb:hover,
.drive-details::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.5);
}
[data-theme="dark"] .drive-sidebar::-webkit-scrollbar-thumb,
[data-theme="dark"] .file-list::-webkit-scrollbar-thumb,
[data-theme="dark"] .drive-details::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
[data-theme="dark"] .drive-sidebar::-webkit-scrollbar-thumb:hover,
[data-theme="dark"] .file-list::-webkit-scrollbar-thumb:hover,
[data-theme="dark"] .drive-details::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Responsive */
@media (max-width: 1024px) {
.drive-layout {
grid-template-columns: 200px 1fr 250px;
gap: 0.5rem;
padding: 0.5rem;
}
}
@media (max-width: 768px) {
.drive-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.drive-details {
display: none;
}
}
/* Alpine.js cloak */
[x-cloak] {
display: none !important;
}

View file

@ -1,32 +1,57 @@
<div class="drive-layout" x-data="driveApp()" x-cloak> <div class="drive-layout" x-data="driveApp()" x-cloak>
<div class="panel drive-sidebar"> <div class="drive-sidebar">
<div style="padding: 1rem; border-bottom: 1px solid #334155;"> <div
style="
padding: 1rem;
border-bottom: 1px solid var(--border, #e0e0e0);
"
>
<h3>General Bots Drive</h3> <h3>General Bots Drive</h3>
</div> </div>
<template x-for="item in navItems" :key="item.name"> <template x-for="item in navItems" :key="item.name">
<div class="nav-item" <div
class="nav-item"
:class="{ active: current === item.name }" :class="{ active: current === item.name }"
@click="current = item.name"> @click="current = item.name"
>
<span x-text="item.icon"></span> <span x-text="item.icon"></span>
<span x-text="item.name"></span> <span x-text="item.name"></span>
</div> </div>
</template> </template>
</div> </div>
<div class="panel drive-main"> <div class="drive-main">
<div style="padding: 1rem; border-bottom: 1px solid #334155;"> <div
style="
padding: 1rem;
border-bottom: 1px solid var(--border, #e0e0e0);
"
>
<h2 x-text="current"></h2> <h2 x-text="current"></h2>
<input type="text" x-model="search" placeholder="Search files..." <input
style="width: 100%; margin-top: 0.5rem; padding: 0.5rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.375rem; color: #e2e8f0;"> type="text"
x-model="search"
placeholder="Search files..."
style="
width: 100%;
margin-top: 0.5rem;
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-family: inherit;
"
/>
</div> </div>
<div class="file-list"> <div class="file-list">
<template x-for="file in filteredFiles" :key="file.id"> <template x-for="file in filteredFiles" :key="file.id">
<div class="file-item" <div
class="file-item"
:class="{ selected: selectedFile?.id === file.id }" :class="{ selected: selectedFile?.id === file.id }"
@click="selectedFile = file"> @click="selectedFile = file"
>
<span class="file-icon" x-text="file.icon"></span> <span class="file-icon" x-text="file.icon"></span>
<div style="flex: 1;"> <div style="flex: 1">
<div style="font-weight: 600;" x-text="file.name"></div> <div style="font-weight: 600" x-text="file.name"></div>
<div class="text-xs text-gray" x-text="file.date"></div> <div class="text-xs text-gray" x-text="file.date"></div>
</div> </div>
<div class="text-sm text-gray" x-text="file.size"></div> <div class="text-sm text-gray" x-text="file.size"></div>
@ -35,31 +60,64 @@
</div> </div>
</div> </div>
<div class="panel drive-details"> <div class="drive-details">
<template x-if="selectedFile"> <template x-if="selectedFile">
<div style="padding: 2rem;"> <div style="padding: 2rem">
<div style="text-align: center; margin-bottom: 2rem;"> <div style="text-align: center; margin-bottom: 2rem">
<div style="font-size: 4rem; margin-bottom: 1rem;" x-text="selectedFile.icon"></div> <div
style="font-size: 4rem; margin-bottom: 1rem"
x-text="selectedFile.icon"
></div>
<h3 x-text="selectedFile.name"></h3> <h3 x-text="selectedFile.name"></h3>
<p class="text-sm text-gray" x-text="selectedFile.type"></p> <p class="text-sm text-gray" x-text="selectedFile.type"></p>
</div> </div>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem">
<div class="text-sm" style="margin-bottom: 0.5rem;">Size</div> <div class="text-sm" style="margin-bottom: 0.5rem">
Size
</div>
<div class="text-gray" x-text="selectedFile.size"></div> <div class="text-gray" x-text="selectedFile.size"></div>
</div> </div>
<div style="margin-bottom: 1rem;"> <div style="margin-bottom: 1rem">
<div class="text-sm" style="margin-bottom: 0.5rem;">Modified</div> <div class="text-sm" style="margin-bottom: 0.5rem">
Modified
</div>
<div class="text-gray" x-text="selectedFile.date"></div> <div class="text-gray" x-text="selectedFile.date"></div>
</div> </div>
<div style="display: flex; gap: 0.5rem; margin-top: 2rem;"> <div style="display: flex; gap: 0.5rem; margin-top: 2rem">
<button style="flex: 1; padding: 0.75rem; background: #3b82f6; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Download</button> <button
<button style="flex: 1; padding: 0.75rem; background: #10b981; color: white; border: none; border-radius: 0.375rem; cursor: pointer;">Share</button> style="
flex: 1;
padding: 0.75rem;
background: #1a73e8;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
"
>
Download
</button>
<button
style="
flex: 1;
padding: 0.75rem;
background: #34a853;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
"
>
Share
</button>
</div> </div>
</div> </div>
</template> </template>
<template x-if="!selectedFile"> <template x-if="!selectedFile">
<div style="padding: 2rem; text-align: center; color: #64748b;"> <div style="padding: 2rem; text-align: center; color: #5f6368">
<div style="font-size: 4rem; margin-bottom: 1rem;">📄</div> <div style="font-size: 4rem; margin-bottom: 1rem">📄</div>
<p>Select a file to view details</p> <p>Select a file to view details</p>
</div> </div>
</template> </template>

View file

@ -1,29 +1,85 @@
function driveApp() { window.driveApp = function driveApp() {
return { return {
current: 'All Files', current: "All Files",
search: '', search: "",
selectedFile: null, selectedFile: null,
navItems: [ navItems: [
{ name: 'All Files', icon: '📁' }, { name: "All Files", icon: "📁" },
{ name: 'Recent', icon: '🕐' }, { name: "Recent", icon: "🕐" },
{ name: 'Starred', icon: '⭐' }, { name: "Starred", icon: "⭐" },
{ name: 'Shared', icon: '👥' }, { name: "Shared", icon: "👥" },
{ name: 'Trash', icon: '🗑' } { name: "Trash", icon: "🗑" },
], ],
files: [ files: [
{ id: 1, name: 'Project Proposal.pdf', type: 'PDF', icon: '📄', size: '2.4 MB', date: 'Nov 10, 2025' }, {
{ id: 2, name: 'Design Assets', type: 'Folder', icon: '📁', size: '—', date: 'Nov 12, 2025' }, id: 1,
{ id: 3, name: 'Meeting Notes.docx', type: 'Document', icon: '📝', size: '156 KB', date: 'Nov 14, 2025' }, name: "Project Proposal.pdf",
{ id: 4, name: 'Budget 2025.xlsx', type: 'Spreadsheet', icon: '📊', size: '892 KB', date: 'Nov 13, 2025' }, type: "PDF",
{ id: 5, name: 'Presentation.pptx', type: 'Presentation', icon: '📽', size: '5.2 MB', date: 'Nov 11, 2025' }, icon: "📄",
{ id: 6, name: 'team-photo.jpg', type: 'Image', icon: '🖼', size: '3.1 MB', date: 'Nov 9, 2025' }, size: "2.4 MB",
{ id: 7, name: 'source-code.zip', type: 'Archive', icon: '📦', size: '12.8 MB', date: 'Nov 8, 2025' }, date: "Nov 10, 2025",
{ id: 8, name: 'video-demo.mp4', type: 'Video', icon: '🎬', size: '45.2 MB', date: 'Nov 7, 2025' } },
{
id: 2,
name: "Design Assets",
type: "Folder",
icon: "📁",
size: "—",
date: "Nov 12, 2025",
},
{
id: 3,
name: "Meeting Notes.docx",
type: "Document",
icon: "📝",
size: "156 KB",
date: "Nov 14, 2025",
},
{
id: 4,
name: "Budget 2025.xlsx",
type: "Spreadsheet",
icon: "📊",
size: "892 KB",
date: "Nov 13, 2025",
},
{
id: 5,
name: "Presentation.pptx",
type: "Presentation",
icon: "📽",
size: "5.2 MB",
date: "Nov 11, 2025",
},
{
id: 6,
name: "team-photo.jpg",
type: "Image",
icon: "🖼",
size: "3.1 MB",
date: "Nov 9, 2025",
},
{
id: 7,
name: "source-code.zip",
type: "Archive",
icon: "📦",
size: "12.8 MB",
date: "Nov 8, 2025",
},
{
id: 8,
name: "video-demo.mp4",
type: "Video",
icon: "🎬",
size: "45.2 MB",
date: "Nov 7, 2025",
},
], ],
get filteredFiles() { get filteredFiles() {
return this.files.filter(file => return this.files.filter((file) =>
file.name.toLowerCase().includes(this.search.toLowerCase()) file.name.toLowerCase().includes(this.search.toLowerCase()),
); );
} },
};
}; };
}

View file

@ -288,6 +288,10 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script
defer
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
></script>
</head> </head>
<body> <body>

View file

@ -31,6 +31,29 @@ async function loadSectionHTML(path) {
return await response.text(); return await response.text();
} }
async function loadScript(jsPath) {
return new Promise((resolve, reject) => {
const existingScript = document.querySelector(`script[src="${jsPath}"]`);
if (existingScript) {
console.log(`Script already loaded: ${jsPath}`);
resolve();
return;
}
const script = document.createElement("script");
script.src = jsPath;
script.onload = () => {
console.log(`✓ Script loaded: ${jsPath}`);
resolve();
};
script.onerror = (err) => {
console.error(`✗ Script failed to load: ${jsPath}`, err);
reject(err);
};
document.body.appendChild(script);
});
}
async function switchSection(section) { async function switchSection(section) {
const mainContent = document.getElementById("main-content"); const mainContent = document.getElementById("main-content");
@ -51,8 +74,8 @@ async function switchSection(section) {
try { try {
const htmlPath = sections[section]; const htmlPath = sections[section];
console.log("Loading section:", section, "from", htmlPath); console.log("Loading section:", section, "from", htmlPath);
// Resolve CSS path relative to the base directory.
const cssPath = getBasePath() + htmlPath.replace(".html", ".css"); const cssPath = getBasePath() + htmlPath.replace(".html", ".css");
const jsPath = getBasePath() + htmlPath.replace(".html", ".js");
// Preload chat CSS if the target is chat // Preload chat CSS if the target is chat
if (section === "chat") { if (section === "chat") {
@ -103,12 +126,44 @@ async function switchSection(section) {
loadingDiv.textContent = "Loading…"; loadingDiv.textContent = "Loading…";
container.appendChild(loadingDiv); container.appendChild(loadingDiv);
// For Alpine sections, load JavaScript FIRST before HTML
const isAlpineSection = ["drive", "tasks", "mail"].includes(section);
if (isAlpineSection) {
console.log(`Loading JS before HTML for Alpine section: ${section}`);
await loadScript(jsPath);
// Wait for the component function to be registered
const appFunctionName = section + "App";
let retries = 0;
while (typeof window[appFunctionName] !== "function" && retries < 50) {
await new Promise((resolve) => setTimeout(resolve, 50));
retries++;
}
if (typeof window[appFunctionName] !== "function") {
console.error(`${appFunctionName} function not found after waiting!`);
throw new Error(
`Component function ${appFunctionName} not available`,
);
}
console.log(`✓ Component function registered: ${appFunctionName}`);
}
// Load HTML // Load HTML
const html = await loadSectionHTML(htmlPath); const html = await loadSectionHTML(htmlPath);
// Create wrapper for the new section // Create wrapper for the new section
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.id = `section-${section}`; wrapper.id = `section-${section}`;
wrapper.className = "section"; wrapper.className = "section";
// For Alpine sections, mark for manual initialization
if (isAlpineSection) {
wrapper.setAttribute("x-ignore", "");
}
wrapper.innerHTML = html; wrapper.innerHTML = html;
// Hide any existing sections // Hide any existing sections
@ -127,6 +182,28 @@ async function switchSection(section) {
container.appendChild(wrapper); container.appendChild(wrapper);
sectionCache[section] = wrapper; sectionCache[section] = wrapper;
// For Alpine sections, initialize after DOM insertion
if (isAlpineSection && window.Alpine) {
console.log(`Initializing Alpine for section: ${section}`);
// Remove x-ignore to allow Alpine to process
wrapper.removeAttribute("x-ignore");
// Small delay to ensure DOM is ready
await new Promise((resolve) => setTimeout(resolve, 50));
try {
window.Alpine.initTree(wrapper);
console.log(`✓ Alpine initialized for ${section}`);
} catch (err) {
console.error(`Error initializing Alpine for ${section}:`, err);
}
} else if (!isAlpineSection) {
// For non-Alpine sections (like chat), load JS after HTML
await loadScript(jsPath);
await new Promise((resolve) => setTimeout(resolve, 100));
}
// Dispatch a custom event to notify the section it's being shown // Dispatch a custom event to notify the section it's being shown
wrapper.dispatchEvent(new CustomEvent("section-shown")); wrapper.dispatchEvent(new CustomEvent("section-shown"));
@ -138,35 +215,8 @@ async function switchSection(section) {
); );
} }
// Then load JS after HTML is inserted (skip if already loaded)
// Resolve JS path relative to the base directory.
const jsPath = getBasePath() + htmlPath.replace(".html", ".js");
const existingScript = document.querySelector(`script[src="${jsPath}"]`);
if (!existingScript) {
// Create script and wait for it to load before initializing Alpine
const script = document.createElement("script");
script.src = jsPath;
script.defer = true;
// Wait for script to load before initializing Alpine
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.body.appendChild(script);
});
}
window.history.pushState({}, "", `#${section}`); window.history.pushState({}, "", `#${section}`);
// Start Alpine on first load, then just init the tree for new sections
if (typeof window.startAlpine === "function") {
window.startAlpine();
delete window.startAlpine;
} else if (window.Alpine) {
window.Alpine.initTree(mainContent);
}
const inputEl = document.getElementById("messageInput"); const inputEl = document.getElementById("messageInput");
if (inputEl) { if (inputEl) {
inputEl.focus(); inputEl.focus();
@ -201,18 +251,49 @@ function getInitialSection() {
// Default to chat if nothing matches // Default to chat if nothing matches
return section || "chat"; return section || "chat";
} }
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
// Small delay to ensure all resources are loaded console.log("DOM Content Loaded");
setTimeout(() => {
const initApp = () => {
const section = getInitialSection(); const section = getInitialSection();
console.log(`Initializing app with section: ${section}`);
// Ensure valid section // Ensure valid section
if (!sections[section]) { if (!sections[section]) {
console.warn(`Invalid section: ${section}, defaulting to chat`);
window.location.hash = "#chat"; window.location.hash = "#chat";
switchSection("chat"); switchSection("chat");
} else { } else {
switchSection(section); switchSection(section);
} }
}, 50); };
// Check if Alpine sections might be needed and wait for Alpine
const hash = window.location.hash.substring(1);
if (["drive", "tasks", "mail"].includes(hash)) {
console.log(`Waiting for Alpine to load for section: ${hash}`);
const waitForAlpine = () => {
if (window.Alpine) {
console.log("Alpine is ready");
setTimeout(initApp, 100);
} else {
console.log("Waiting for Alpine...");
setTimeout(waitForAlpine, 100);
}
};
// Also listen for alpine:init event
document.addEventListener("alpine:init", () => {
console.log("Alpine initialized via event");
});
waitForAlpine();
} else {
// For chat, don't need to wait for Alpine
setTimeout(initApp, 100);
}
}); });
// Handle browser back/forward navigation // Handle browser back/forward navigation

View file

@ -1 +1,357 @@
a {} /* Mail Layout */
.mail-layout {
display: grid;
grid-template-columns: 250px 350px 1fr;
gap: 1rem;
padding: 1rem;
height: 100%;
width: 100%;
background: #ffffff;
color: #202124;
}
[data-theme="dark"] .mail-layout {
background: #1a1a1a;
color: #e8eaed;
}
.mail-sidebar,
.mail-list,
.mail-content {
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 12px;
overflow: hidden;
}
[data-theme="dark"] .mail-sidebar,
[data-theme="dark"] .mail-list,
[data-theme="dark"] .mail-content {
background: #202124;
border-color: #3c4043;
}
.mail-sidebar {
overflow-y: auto;
}
.mail-list {
display: flex;
flex-direction: column;
overflow-y: auto;
}
.mail-content {
overflow-y: auto;
}
/* Folder Navigation */
.nav-item {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
border-radius: 0.5rem;
margin: 0.25rem 0.5rem;
transition: all 0.2s;
color: #5f6368;
}
[data-theme="dark"] .nav-item {
color: #9aa0a6;
}
.nav-item:hover {
background: rgba(26, 115, 232, 0.08);
color: #1a73e8;
}
[data-theme="dark"] .nav-item:hover {
background: rgba(138, 180, 248, 0.08);
color: #8ab4f8;
}
.nav-item.active {
background: #e8f0fe;
color: #1a73e8;
font-weight: 500;
}
[data-theme="dark"] .nav-item.active {
background: #1e3a5f;
color: #8ab4f8;
}
.nav-item .count {
margin-left: auto;
background: #1a73e8;
color: white;
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
}
[data-theme="dark"] .nav-item .count {
background: #8ab4f8;
color: #202124;
}
/* Mail Items */
.mail-item {
padding: 1rem;
cursor: pointer;
border-bottom: 1px solid #e0e0e0;
transition: all 0.2s;
position: relative;
}
[data-theme="dark"] .mail-item {
border-bottom-color: #3c4043;
}
.mail-item:hover {
background: rgba(26, 115, 232, 0.08);
}
[data-theme="dark"] .mail-item:hover {
background: rgba(138, 180, 248, 0.08);
}
.mail-item.unread {
background: #f8f9fa;
font-weight: 500;
}
[data-theme="dark"] .mail-item.unread {
background: #292a2d;
}
.mail-item.selected {
background: #e8f0fe;
border-left: 3px solid #1a73e8;
}
[data-theme="dark"] .mail-item.selected {
background: #1e3a5f;
border-left-color: #8ab4f8;
}
.mail-item-from {
font-size: 0.875rem;
margin-bottom: 0.25rem;
color: #202124;
}
[data-theme="dark"] .mail-item-from {
color: #e8eaed;
}
.mail-item.unread .mail-item-from {
font-weight: 600;
}
.mail-item-subject {
font-size: 0.875rem;
margin-bottom: 0.25rem;
color: #202124;
}
[data-theme="dark"] .mail-item-subject {
color: #e8eaed;
}
.mail-item-preview {
font-size: 0.75rem;
color: #5f6368;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0.25rem;
}
[data-theme="dark"] .mail-item-preview {
color: #9aa0a6;
}
.mail-item-time {
font-size: 0.75rem;
color: #5f6368;
}
[data-theme="dark"] .mail-item-time {
color: #9aa0a6;
}
/* Mail Content View */
.mail-content-view {
padding: 2rem;
}
.mail-content-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #5f6368;
text-align: center;
}
[data-theme="dark"] .mail-content-empty {
color: #9aa0a6;
}
.mail-content-empty .icon {
font-size: 4rem;
margin-bottom: 1rem;
}
/* Mail Header */
.mail-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e0e0e0;
}
[data-theme="dark"] .mail-header {
border-bottom-color: #3c4043;
}
.mail-subject {
font-size: 1.5rem;
font-weight: 500;
margin-bottom: 1rem;
color: #202124;
}
[data-theme="dark"] .mail-subject {
color: #e8eaed;
}
.mail-meta {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.875rem;
color: #5f6368;
}
[data-theme="dark"] .mail-meta {
color: #9aa0a6;
}
.mail-from {
font-weight: 500;
color: #202124;
}
[data-theme="dark"] .mail-from {
color: #e8eaed;
}
.mail-to {
font-size: 0.75rem;
}
.mail-date {
margin-left: auto;
}
/* Mail Body */
.mail-body {
line-height: 1.7;
color: #202124;
}
[data-theme="dark"] .mail-body {
color: #e8eaed;
}
.mail-body p {
margin-bottom: 1rem;
}
.mail-body p:last-child {
margin-bottom: 0;
}
/* Headers */
h2,
h3 {
margin: 0;
padding: 0;
font-weight: 500;
}
/* Scrollbar Styles */
.mail-sidebar::-webkit-scrollbar,
.mail-list::-webkit-scrollbar,
.mail-content::-webkit-scrollbar {
width: 8px;
}
.mail-sidebar::-webkit-scrollbar-track,
.mail-list::-webkit-scrollbar-track,
.mail-content::-webkit-scrollbar-track {
background: transparent;
}
.mail-sidebar::-webkit-scrollbar-thumb,
.mail-list::-webkit-scrollbar-thumb,
.mail-content::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.3);
border-radius: 4px;
}
.mail-sidebar::-webkit-scrollbar-thumb:hover,
.mail-list::-webkit-scrollbar-thumb:hover,
.mail-content::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.5);
}
[data-theme="dark"] .mail-sidebar::-webkit-scrollbar-thumb,
[data-theme="dark"] .mail-list::-webkit-scrollbar-thumb,
[data-theme="dark"] .mail-content::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
[data-theme="dark"] .mail-sidebar::-webkit-scrollbar-thumb:hover,
[data-theme="dark"] .mail-list::-webkit-scrollbar-thumb:hover,
[data-theme="dark"] .mail-content::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Alpine.js cloak */
[x-cloak] {
display: none !important;
}
/* Responsive */
@media (max-width: 1024px) {
.mail-layout {
grid-template-columns: 200px 300px 1fr;
gap: 0.5rem;
padding: 0.5rem;
}
}
@media (max-width: 768px) {
.mail-layout {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.mail-sidebar {
max-height: 200px;
}
.mail-content {
display: none;
}
.mail-item.selected + .mail-content {
display: block;
}
}

View file

@ -1,61 +1,62 @@
function mailApp() { window.mailApp = function mailApp() {
return { return {
currentFolder: 'Inbox', currentFolder: "Inbox",
selectedMail: null, selectedMail: null,
folders: [ folders: [
{ name: 'Inbox', icon: '📥', count: 4 }, { name: "Inbox", icon: "📥", count: 4 },
{ name: 'Sent', icon: '📤', count: 0 }, { name: "Sent", icon: "📤", count: 0 },
{ name: 'Drafts', icon: '📝', count: 2 }, { name: "Drafts", icon: "📝", count: 2 },
{ name: 'Starred', icon: '⭐', count: 0 }, { name: "Starred", icon: "⭐", count: 0 },
{ name: 'Trash', icon: '🗑', count: 0 } { name: "Trash", icon: "🗑", count: 0 },
], ],
mails: [ mails: [
{ {
id: 1, id: 1,
from: 'Sarah Johnson', from: "Sarah Johnson",
to: 'me@example.com', to: "me@example.com",
subject: 'Q4 Project Update', subject: "Q4 Project Update",
preview: 'Hi team, I wanted to share the latest updates on our Q4 projects...', preview:
body: '<p>Hi team,</p><p>I wanted to share the latest updates on our Q4 projects. We\'ve made significant progress on the main deliverables and are on track to meet our goals.</p><p>Please review the attached documents and let me know if you have any questions.</p><p>Best regards,<br>Sarah</p>', "Hi team, I wanted to share the latest updates on our Q4 projects...",
time: '10:30 AM', body: "<p>Hi team,</p><p>I wanted to share the latest updates on our Q4 projects. We've made significant progress on the main deliverables and are on track to meet our goals.</p><p>Please review the attached documents and let me know if you have any questions.</p><p>Best regards,<br>Sarah</p>",
date: 'Nov 15, 2025', time: "10:30 AM",
read: false date: "Nov 15, 2025",
read: false,
}, },
{ {
id: 2, id: 2,
from: 'Mike Chen', from: "Mike Chen",
to: 'me@example.com', to: "me@example.com",
subject: 'Meeting Tomorrow', subject: "Meeting Tomorrow",
preview: 'Don\'t forget about our meeting tomorrow at 2 PM...', preview: "Don't forget about our meeting tomorrow at 2 PM...",
body: '<p>Hi,</p><p>Don\'t forget about our meeting tomorrow at 2 PM to discuss the new features.</p><p>See you then!<br>Mike</p>', body: "<p>Hi,</p><p>Don't forget about our meeting tomorrow at 2 PM to discuss the new features.</p><p>See you then!<br>Mike</p>",
time: '9:15 AM', time: "9:15 AM",
date: 'Nov 15, 2025', date: "Nov 15, 2025",
read: false read: false,
}, },
{ {
id: 3, id: 3,
from: 'Emma Wilson', from: "Emma Wilson",
to: 'me@example.com', to: "me@example.com",
subject: 'Design Review Complete', subject: "Design Review Complete",
preview: 'The design review for the new dashboard is complete...', preview: "The design review for the new dashboard is complete...",
body: '<p>Hi,</p><p>The design review for the new dashboard is complete. Overall, the team is happy with the direction.</p><p>I\'ve made the requested changes and updated the Figma file.</p><p>Thanks,<br>Emma</p>', body: "<p>Hi,</p><p>The design review for the new dashboard is complete. Overall, the team is happy with the direction.</p><p>I've made the requested changes and updated the Figma file.</p><p>Thanks,<br>Emma</p>",
time: 'Yesterday', time: "Yesterday",
date: 'Nov 14, 2025', date: "Nov 14, 2025",
read: true read: true,
}, },
{ {
id: 4, id: 4,
from: 'David Lee', from: "David Lee",
to: 'me@example.com', to: "me@example.com",
subject: 'Budget Approval Needed', subject: "Budget Approval Needed",
preview: 'Could you please review and approve the Q1 budget?', preview: "Could you please review and approve the Q1 budget?",
body: '<p>Hi,</p><p>Could you please review and approve the Q1 budget when you get a chance?</p><p>It\'s attached to this email.</p><p>Thanks,<br>David</p>', body: "<p>Hi,</p><p>Could you please review and approve the Q1 budget when you get a chance?</p><p>It's attached to this email.</p><p>Thanks,<br>David</p>",
time: 'Yesterday', time: "Yesterday",
date: 'Nov 14, 2025', date: "Nov 14, 2025",
read: false read: false,
} },
], ],
get filteredMails() { get filteredMails() {
@ -69,10 +70,10 @@ function mailApp() {
}, },
updateFolderCounts() { updateFolderCounts() {
const inbox = this.folders.find(f => f.name === 'Inbox'); const inbox = this.folders.find((f) => f.name === "Inbox");
if (inbox) { if (inbox) {
inbox.count = this.mails.filter(m => !m.read).length; inbox.count = this.mails.filter((m) => !m.read).length;
}
} }
},
};
}; };
}

View file

@ -1 +1,288 @@
a {} /* Tasks Container */
.tasks-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
height: 100%;
overflow-y: auto;
background: #ffffff;
color: #202124;
}
[data-theme="dark"] .tasks-container {
background: #1a1a1a;
color: #e8eaed;
}
/* Task Input */
.task-input {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.task-input input {
flex: 1;
padding: 0.875rem 1rem;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
color: #202124;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s;
}
[data-theme="dark"] .task-input input {
background: #202124;
border-color: #3c4043;
color: #e8eaed;
}
.task-input input:focus {
outline: none;
border-color: #1a73e8;
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}
[data-theme="dark"] .task-input input:focus {
border-color: #8ab4f8;
box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.2);
}
.task-input input::placeholder {
color: #5f6368;
}
[data-theme="dark"] .task-input input::placeholder {
color: #9aa0a6;
}
.task-input button {
padding: 0.875rem 1.5rem;
background: #1a73e8;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
font-size: 1rem;
transition: all 0.2s;
white-space: nowrap;
}
.task-input button:hover {
background: #1557b0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.task-input button:active {
transform: scale(0.98);
}
/* Task List */
.task-list {
list-style: none;
padding: 0;
margin: 0;
}
.task-item {
padding: 1rem;
display: flex;
align-items: center;
gap: 1rem;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s;
}
[data-theme="dark"] .task-item {
background: #202124;
border-color: #3c4043;
}
.task-item:hover {
border-color: #1a73e8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
[data-theme="dark"] .task-item:hover {
border-color: #8ab4f8;
}
.task-item.completed {
opacity: 0.6;
}
.task-item.completed span {
text-decoration: line-through;
}
.task-item input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
accent-color: #1a73e8;
flex-shrink: 0;
}
.task-item span {
flex: 1;
font-size: 1rem;
line-height: 1.5;
}
.task-item button {
background: #ea4335;
color: white;
border: none;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
flex-shrink: 0;
}
.task-item button:hover {
background: #c5221f;
}
.task-item button:active {
transform: scale(0.95);
}
/* Task Filters */
.task-filters {
display: flex;
gap: 0.5rem;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e0e0e0;
flex-wrap: wrap;
}
[data-theme="dark"] .task-filters {
border-top-color: #3c4043;
}
.task-filters button {
padding: 0.5rem 1rem;
background: #f8f9fa;
color: #5f6368;
border: 1px solid #e0e0e0;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
font-weight: 500;
}
[data-theme="dark"] .task-filters button {
background: #202124;
color: #9aa0a6;
border-color: #3c4043;
}
.task-filters button:hover {
background: #e8f0fe;
color: #1a73e8;
border-color: #1a73e8;
}
[data-theme="dark"] .task-filters button:hover {
background: #1e3a5f;
color: #8ab4f8;
border-color: #8ab4f8;
}
.task-filters button.active {
background: #1a73e8;
color: white;
border-color: #1a73e8;
}
[data-theme="dark"] .task-filters button.active {
background: #8ab4f8;
color: #202124;
border-color: #8ab4f8;
}
.task-filters button:active {
transform: scale(0.98);
}
/* Stats */
.task-stats {
display: flex;
gap: 1rem;
margin-top: 1rem;
font-size: 0.875rem;
color: #5f6368;
}
[data-theme="dark"] .task-stats {
color: #9aa0a6;
}
.task-stats span {
display: flex;
align-items: center;
gap: 0.25rem;
}
/* Scrollbar */
.tasks-container::-webkit-scrollbar {
width: 8px;
}
.tasks-container::-webkit-scrollbar-track {
background: transparent;
}
.tasks-container::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.3);
border-radius: 4px;
}
.tasks-container::-webkit-scrollbar-thumb:hover {
background: rgba(128, 128, 128, 0.5);
}
[data-theme="dark"] .tasks-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
[data-theme="dark"] .tasks-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Headers */
h2 {
margin: 0 0 1.5rem 0;
font-size: 1.75rem;
font-weight: 500;
}
/* Alpine.js cloak */
[x-cloak] {
display: none !important;
}
/* Responsive */
@media (max-width: 768px) {
.tasks-container {
padding: 1rem;
}
.task-input {
flex-direction: column;
}
.task-input button {
width: 100%;
}
}

View file

@ -1,37 +1,37 @@
function tasksApp() { window.tasksApp = function tasksApp() {
return { return {
newTask: '', newTask: "",
filter: 'all', filter: "all",
tasks: [], tasks: [],
init() { init() {
const saved = localStorage.getItem('tasks'); const saved = localStorage.getItem("tasks");
if (saved) { if (saved) {
try { try {
this.tasks = JSON.parse(saved); this.tasks = JSON.parse(saved);
} catch (e) { } catch (e) {
console.error('Failed to load tasks:', e); console.error("Failed to load tasks:", e);
this.tasks = []; this.tasks = [];
} }
} }
}, },
addTask() { addTask() {
if (this.newTask.trim() === '') return; if (this.newTask.trim() === "") return;
this.tasks.push({ this.tasks.push({
id: Date.now(), id: Date.now(),
text: this.newTask.trim(), text: this.newTask.trim(),
completed: false, completed: false,
createdAt: new Date().toISOString() createdAt: new Date().toISOString(),
}); });
this.newTask = ''; this.newTask = "";
this.save(); this.save();
}, },
toggleTask(id) { toggleTask(id) {
const task = this.tasks.find(t => t.id === id); const task = this.tasks.find((t) => t.id === id);
if (task) { if (task) {
task.completed = !task.completed; task.completed = !task.completed;
this.save(); this.save();
@ -39,39 +39,39 @@ function tasksApp() {
}, },
deleteTask(id) { deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id); this.tasks = this.tasks.filter((t) => t.id !== id);
this.save(); this.save();
}, },
clearCompleted() { clearCompleted() {
this.tasks = this.tasks.filter(t => !t.completed); this.tasks = this.tasks.filter((t) => !t.completed);
this.save(); this.save();
}, },
save() { save() {
try { try {
localStorage.setItem('tasks', JSON.stringify(this.tasks)); localStorage.setItem("tasks", JSON.stringify(this.tasks));
} catch (e) { } catch (e) {
console.error('Failed to save tasks:', e); console.error("Failed to save tasks:", e);
} }
}, },
get filteredTasks() { get filteredTasks() {
if (this.filter === 'active') { if (this.filter === "active") {
return this.tasks.filter(t => !t.completed); return this.tasks.filter((t) => !t.completed);
} }
if (this.filter === 'completed') { if (this.filter === "completed") {
return this.tasks.filter(t => t.completed); return this.tasks.filter((t) => t.completed);
} }
return this.tasks; return this.tasks;
}, },
get activeTasks() { get activeTasks() {
return this.tasks.filter(t => !t.completed).length; return this.tasks.filter((t) => !t.completed).length;
}, },
get completedTasks() { get completedTasks() {
return this.tasks.filter(t => t.completed).length; return this.tasks.filter((t) => t.completed).length;
} },
};
}; };
}