botui/ui/suite/base.html

716 lines
25 KiB
HTML
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.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}General Bots Suite{% endblock %}</title>
<!-- HTMX (local) -->
<script src="/js/vendor/htmx.min.js"></script>
<script src="/js/vendor/htmx-ws.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="/css/app.css" />
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: rgba(59, 130, 246, 0.1);
--bg: #0f172a;
--surface: #1e293b;
--surface-hover: #334155;
--border: #334155;
--text: #f8fafc;
--text-secondary: #94a3b8;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f8fafc;
--surface: #ffffff;
--surface-hover: #f1f5f9;
--border: #e2e8f0;
--text: #1e293b;
--text-secondary: #64748b;
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
height: 64px;
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 1000;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 1.125rem;
color: var(--text);
text-decoration: none;
}
.logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.header-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-btn {
padding: 0.5rem;
background: transparent;
border: none;
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
transition:
background 0.2s,
color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.header-btn:hover {
background: var(--surface-hover);
color: var(--text);
}
.user-avatar {
width: 32px;
height: 32px;
background: var(--primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
}
/* Apps Menu */
.apps-menu {
position: relative;
}
.apps-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1rem;
min-width: 320px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
z-index: 1001;
}
.apps-dropdown.show {
display: block;
}
.apps-dropdown-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.app-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
border-radius: 8px;
text-decoration: none;
color: var(--text);
transition: background 0.2s;
}
.app-item:hover {
background: var(--surface-hover);
}
.app-item.active {
background: var(--primary-light);
color: var(--primary);
}
.app-item-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.app-item span {
font-size: 0.75rem;
font-weight: 500;
}
/* Main Content */
.app-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#main-content {
flex: 1;
overflow: auto;
}
/* HTMX Indicators */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-flex;
}
.htmx-request.htmx-indicator {
display: inline-flex;
}
/* Spinner */
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Notifications */
.notifications-container {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 2000;
max-width: 400px;
}
.notification {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease-out;
}
.notification.success {
border-left: 4px solid var(--success);
}
.notification.error {
border-left: 4px solid var(--error);
}
.notification.warning {
border-left: 4px solid var(--warning);
}
.notification.info {
border-left: 4px solid var(--info);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Utility Classes */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Responsive */
@media (max-width: 768px) {
.logo span {
display: none;
}
.apps-dropdown {
right: -1rem;
min-width: 280px;
}
}
</style>
{% block head %}{% endblock %}
</head>
<body>
<!-- Header -->
<header class="app-header">
<div class="header-left">
<a href="/" class="logo">
<div class="logo-icon">🤖</div>
<span>General Bots</span>
</a>
</div>
<div class="header-right">
<!-- Apps Menu -->
<div class="apps-menu">
<button
class="header-btn"
id="apps-btn"
aria-label="Applications"
aria-expanded="false"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="currentColor"
>
<circle cx="5" cy="5" r="2"></circle>
<circle cx="12" cy="5" r="2"></circle>
<circle cx="19" cy="5" r="2"></circle>
<circle cx="5" cy="12" r="2"></circle>
<circle cx="12" cy="12" r="2"></circle>
<circle cx="19" cy="12" r="2"></circle>
<circle cx="5" cy="19" r="2"></circle>
<circle cx="12" cy="19" r="2"></circle>
<circle cx="19" cy="19" r="2"></circle>
</svg>
</button>
<nav class="apps-dropdown" id="apps-dropdown" role="menu">
<div class="apps-dropdown-title">Applications</div>
<div class="apps-grid">
<a
href="#chat"
class="app-item"
role="menuitem"
hx-get="/chat/chat.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#3b82f6,
#1d4ed8
);
"
>
💬
</div>
<span>Chat</span>
</a>
<a
href="#drive"
class="app-item"
role="menuitem"
hx-get="/drive/index.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#f59e0b,
#d97706
);
"
>
📁
</div>
<span>Drive</span>
</a>
<a
href="#tasks"
class="app-item"
role="menuitem"
hx-get="/tasks/tasks.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#22c55e,
#16a34a
);
"
>
</div>
<span>Tasks</span>
</a>
<a
href="#mail"
class="app-item"
role="menuitem"
hx-get="/mail/mail.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#ef4444,
#dc2626
);
"
>
✉️
</div>
<span>Mail</span>
</a>
<a
href="#calendar"
class="app-item"
role="menuitem"
hx-get="/calendar/calendar.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#a855f7,
#7c3aed
);
"
>
📅
</div>
<span>Calendar</span>
</a>
<a
href="#meet"
class="app-item"
role="menuitem"
hx-get="/meet/meet.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#06b6d4,
#0891b2
);
"
>
🎥
</div>
<span>Meet</span>
</a>
<a
href="#paper"
class="app-item"
role="menuitem"
hx-get="/paper/paper.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#eab308,
#ca8a04
);
"
>
📝
</div>
<span>Paper</span>
</a>
<a
href="#research"
class="app-item"
role="menuitem"
hx-get="/research/research.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#ec4899,
#db2777
);
"
>
🔍
</div>
<span>Research</span>
</a>
<a
href="#analytics"
class="app-item"
role="menuitem"
hx-get="/analytics/analytics.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#6366f1,
#4f46e5
);
"
>
📊
</div>
<span>Analytics</span>
</a>
</div>
</nav>
</div>
<!-- Theme Toggle -->
<button
class="header-btn"
id="theme-btn"
aria-label="Toggle theme"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
></path>
</svg>
</button>
<!-- User Avatar -->
<button class="user-avatar" aria-label="User menu">
{{ user_initial|default("U") }}
</button>
</div>
</header>
<!-- Main Content -->
<main class="app-main">
<div id="main-content">{% block content %}{% endblock %}</div>
</main>
<!-- Notifications Container -->
<div class="notifications-container" id="notifications"></div>
<script>
// Apps menu toggle
const appsBtn = document.getElementById("apps-btn");
const appsDropdown = document.getElementById("apps-dropdown");
appsBtn.addEventListener("click", (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle("show");
appsBtn.setAttribute("aria-expanded", isOpen);
});
document.addEventListener("click", (e) => {
if (
!appsDropdown.contains(e.target) &&
!appsBtn.contains(e.target)
) {
appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
}
});
// Close on escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
appsDropdown.classList.remove("show");
appsBtn.setAttribute("aria-expanded", "false");
}
});
// Keyboard shortcuts for apps
document.addEventListener("keydown", (e) => {
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
const shortcuts = {
1: "chat",
2: "drive",
3: "tasks",
4: "mail",
5: "calendar",
6: "meet",
};
if (shortcuts[e.key]) {
e.preventDefault();
const link = document.querySelector(
`a[href="#${shortcuts[e.key]}"]`,
);
if (link) link.click();
appsDropdown.classList.remove("show");
}
}
});
// Update active app in menu
document.body.addEventListener("htmx:afterSwap", (e) => {
if (e.detail.target.id === "main-content") {
const hash = window.location.hash || "#chat";
document.querySelectorAll(".app-item").forEach((item) => {
item.classList.toggle(
"active",
item.getAttribute("href") === hash,
);
});
}
});
// Theme toggle
const themeBtn = document.getElementById("theme-btn");
themeBtn.addEventListener("click", () => {
document.body.classList.toggle("light-theme");
localStorage.setItem(
"theme",
document.body.classList.contains("light-theme")
? "light"
: "dark",
);
});
// Restore theme
if (localStorage.getItem("theme") === "light") {
document.body.classList.add("light-theme");
}
// Notification helper
window.showNotification = function (
message,
type = "info",
duration = 5000,
) {
const container = document.getElementById("notifications");
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.innerHTML = `
<div class="notification-content">
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(notification);
if (duration > 0) {
setTimeout(() => notification.remove(), duration);
}
};
</script>
{% block scripts %}{% endblock %}
</body>
</html>