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

View file

@ -1,29 +1,85 @@
function driveApp() {
window.driveApp = function driveApp() {
return {
current: 'All Files',
search: '',
current: "All Files",
search: "",
selectedFile: null,
navItems: [
{ name: 'All Files', icon: '📁' },
{ name: 'Recent', icon: '🕐' },
{ name: 'Starred', icon: '⭐' },
{ name: 'Shared', icon: '👥' },
{ name: 'Trash', icon: '🗑' }
{ name: "All Files", icon: "📁" },
{ name: "Recent", icon: "🕐" },
{ name: "Starred", icon: "⭐" },
{ name: "Shared", icon: "👥" },
{ name: "Trash", icon: "🗑" },
],
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: 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' }
{
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: 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() {
return this.files.filter(file =>
file.name.toLowerCase().includes(this.search.toLowerCase())
return this.files.filter((file) =>
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://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
defer
src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"
></script>
</head>
<body>

View file

@ -31,6 +31,29 @@ async function loadSectionHTML(path) {
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) {
const mainContent = document.getElementById("main-content");
@ -51,8 +74,8 @@ async function switchSection(section) {
try {
const htmlPath = sections[section];
console.log("Loading section:", section, "from", htmlPath);
// Resolve CSS path relative to the base directory.
const cssPath = getBasePath() + htmlPath.replace(".html", ".css");
const jsPath = getBasePath() + htmlPath.replace(".html", ".js");
// Preload chat CSS if the target is chat
if (section === "chat") {
@ -103,12 +126,44 @@ async function switchSection(section) {
loadingDiv.textContent = "Loading…";
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
const html = await loadSectionHTML(htmlPath);
// Create wrapper for the new section
const wrapper = document.createElement("div");
wrapper.id = `section-${section}`;
wrapper.className = "section";
// For Alpine sections, mark for manual initialization
if (isAlpineSection) {
wrapper.setAttribute("x-ignore", "");
}
wrapper.innerHTML = html;
// Hide any existing sections
@ -127,6 +182,28 @@ async function switchSection(section) {
container.appendChild(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
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}`);
// 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");
if (inputEl) {
inputEl.focus();
@ -201,18 +251,49 @@ function getInitialSection() {
// Default to chat if nothing matches
return section || "chat";
}
window.addEventListener("DOMContentLoaded", () => {
// Small delay to ensure all resources are loaded
setTimeout(() => {
console.log("DOM Content Loaded");
const initApp = () => {
const section = getInitialSection();
console.log(`Initializing app with section: ${section}`);
// Ensure valid section
if (!sections[section]) {
console.warn(`Invalid section: ${section}, defaulting to chat`);
window.location.hash = "#chat";
switchSection("chat");
} else {
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

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,78 +1,79 @@
function mailApp() {
window.mailApp = function mailApp() {
return {
currentFolder: 'Inbox',
currentFolder: "Inbox",
selectedMail: null,
folders: [
{ name: 'Inbox', icon: '📥', count: 4 },
{ name: 'Sent', icon: '📤', count: 0 },
{ name: 'Drafts', icon: '📝', count: 2 },
{ name: 'Starred', icon: '⭐', count: 0 },
{ name: 'Trash', icon: '🗑', count: 0 }
{ name: "Inbox", icon: "📥", count: 4 },
{ name: "Sent", icon: "📤", count: 0 },
{ name: "Drafts", icon: "📝", count: 2 },
{ name: "Starred", icon: "⭐", count: 0 },
{ name: "Trash", icon: "🗑", count: 0 },
],
mails: [
{
id: 1,
from: 'Sarah Johnson',
to: 'me@example.com',
subject: 'Q4 Project Update',
preview: 'Hi team, I wanted to share the latest updates on our Q4 projects...',
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>',
time: '10:30 AM',
date: 'Nov 15, 2025',
read: false
from: "Sarah Johnson",
to: "me@example.com",
subject: "Q4 Project Update",
preview:
"Hi team, I wanted to share the latest updates on our Q4 projects...",
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>",
time: "10:30 AM",
date: "Nov 15, 2025",
read: false,
},
{
id: 2,
from: 'Mike Chen',
to: 'me@example.com',
subject: 'Meeting Tomorrow',
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>',
time: '9:15 AM',
date: 'Nov 15, 2025',
read: false
from: "Mike Chen",
to: "me@example.com",
subject: "Meeting Tomorrow",
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>",
time: "9:15 AM",
date: "Nov 15, 2025",
read: false,
},
{
id: 3,
from: 'Emma Wilson',
to: 'me@example.com',
subject: 'Design Review 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>',
time: 'Yesterday',
date: 'Nov 14, 2025',
read: true
from: "Emma Wilson",
to: "me@example.com",
subject: "Design Review 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>",
time: "Yesterday",
date: "Nov 14, 2025",
read: true,
},
{
id: 4,
from: 'David Lee',
to: 'me@example.com',
subject: 'Budget Approval Needed',
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>',
time: 'Yesterday',
date: 'Nov 14, 2025',
read: false
}
from: "David Lee",
to: "me@example.com",
subject: "Budget Approval Needed",
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>",
time: "Yesterday",
date: "Nov 14, 2025",
read: false,
},
],
get filteredMails() {
return this.mails;
},
selectMail(mail) {
this.selectedMail = mail;
mail.read = true;
this.updateFolderCounts();
},
updateFolderCounts() {
const inbox = this.folders.find(f => f.name === 'Inbox');
const inbox = this.folders.find((f) => f.name === "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,77 +1,77 @@
function tasksApp() {
window.tasksApp = function tasksApp() {
return {
newTask: '',
filter: 'all',
newTask: "",
filter: "all",
tasks: [],
init() {
const saved = localStorage.getItem('tasks');
const saved = localStorage.getItem("tasks");
if (saved) {
try {
this.tasks = JSON.parse(saved);
} catch (e) {
console.error('Failed to load tasks:', e);
console.error("Failed to load tasks:", e);
this.tasks = [];
}
}
},
addTask() {
if (this.newTask.trim() === '') return;
if (this.newTask.trim() === "") return;
this.tasks.push({
id: Date.now(),
text: this.newTask.trim(),
completed: false,
createdAt: new Date().toISOString()
createdAt: new Date().toISOString(),
});
this.newTask = '';
this.newTask = "";
this.save();
},
toggleTask(id) {
const task = this.tasks.find(t => t.id === id);
const task = this.tasks.find((t) => t.id === id);
if (task) {
task.completed = !task.completed;
this.save();
}
},
deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id);
this.tasks = this.tasks.filter((t) => t.id !== id);
this.save();
},
clearCompleted() {
this.tasks = this.tasks.filter(t => !t.completed);
this.tasks = this.tasks.filter((t) => !t.completed);
this.save();
},
save() {
try {
localStorage.setItem('tasks', JSON.stringify(this.tasks));
localStorage.setItem("tasks", JSON.stringify(this.tasks));
} catch (e) {
console.error('Failed to save tasks:', e);
console.error("Failed to save tasks:", e);
}
},
get filteredTasks() {
if (this.filter === 'active') {
return this.tasks.filter(t => !t.completed);
if (this.filter === "active") {
return this.tasks.filter((t) => !t.completed);
}
if (this.filter === 'completed') {
return this.tasks.filter(t => t.completed);
if (this.filter === "completed") {
return this.tasks.filter((t) => t.completed);
}
return this.tasks;
},
get activeTasks() {
return this.tasks.filter(t => !t.completed).length;
return this.tasks.filter((t) => !t.completed).length;
},
get completedTasks() {
return this.tasks.filter(t => t.completed).length;
}
return this.tasks.filter((t) => t.completed).length;
},
};
}
};