botserver/ui/suite/drive.html
Rodrigo Rodriguez (Pragmatismo) e68a12176d Add Suite app documentation, templates, and Askama config
- Add askama.toml for template configuration (ui/ directory)
- Add Suite app documentation with flow diagrams (SVG)
  - App launcher, chat flow, drive flow, tasks flow
  - Individual app docs: chat, drive, tasks, mail, etc.
- Add HTML templates for Suite apps
  - Base template with header and app launcher
  - Auth login page
  - Chat, Drive, Mail, Meet, Tasks templates
  - Partial templates for messages, sessions, notifications
- Add Extensions type to AppState for type-erased storage
- Add mTLS module for service-to-service authentication
- Update web handlers to use new template paths (suite/)
- Fix auth module to avoid axum-extra TypedHeader dependency
2025-11-30 21:00:48 -03:00

600 lines
17 KiB
HTML

{% extends "suite/base.html" %}
{% block title %}Drive - General Bots Suite{% endblock %}
{% block content %}
<div class="drive-container">
<!-- Sidebar -->
<aside class="drive-sidebar">
<button class="new-btn"
hx-get="/api/drive/upload-modal"
hx-target="#modal-container"
hx-swap="innerHTML">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New
</button>
<nav class="nav-section">
<div class="nav-item active"
hx-get="/api/drive/files?path=/"
hx-target="#file-list"
hx-swap="innerHTML">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
<span class="nav-item-label">My Drive</span>
</div>
<div class="nav-item"
hx-get="/api/drive/files?filter=shared"
hx-target="#file-list"
hx-swap="innerHTML">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span class="nav-item-label">Shared with me</span>
</div>
<div class="nav-item"
hx-get="/api/drive/files?filter=recent"
hx-target="#file-list"
hx-swap="innerHTML">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span class="nav-item-label">Recent</span>
</div>
<div class="nav-item"
hx-get="/api/drive/files?filter=starred"
hx-target="#file-list"
hx-swap="innerHTML">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
<span class="nav-item-label">Starred</span>
</div>
<div class="nav-item"
hx-get="/api/drive/files?filter=trash"
hx-target="#file-list"
hx-swap="innerHTML">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
<span class="nav-item-label">Trash</span>
</div>
</nav>
<div class="storage-info">
<div class="storage-header">
<span class="storage-label">Storage</span>
<span class="storage-value"
hx-get="/api/drive/storage"
hx-trigger="load"
hx-swap="innerHTML">
Loading...
</span>
</div>
<div class="storage-bar">
<div class="storage-bar-fill" style="width: 35%"></div>
</div>
</div>
</aside>
<!-- Main content -->
<main class="drive-main">
<!-- Toolbar -->
<div class="drive-toolbar">
<div class="breadcrumb" id="breadcrumb">
<div class="breadcrumb-item current">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
</svg>
My Drive
</div>
</div>
<div class="toolbar-actions">
<div class="view-toggle">
<button class="view-toggle-btn active" id="grid-view-btn" title="Grid view">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</button>
<button class="view-toggle-btn" id="list-view-btn" title="List view">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="8" y1="6" x2="21" y2="6"></line>
<line x1="8" y1="12" x2="21" y2="12"></line>
<line x1="8" y1="18" x2="21" y2="18"></line>
<line x1="3" y1="6" x2="3.01" y2="6"></line>
<line x1="3" y1="12" x2="3.01" y2="12"></line>
<line x1="3" y1="18" x2="3.01" y2="18"></line>
</svg>
</button>
</div>
<select class="sort-dropdown" id="sort-dropdown"
hx-get="/api/drive/files"
hx-target="#file-list"
hx-swap="innerHTML"
hx-include="[name='path']">
<option value="name">Name</option>
<option value="modified">Last modified</option>
<option value="size">File size</option>
<option value="type">Type</option>
</select>
</div>
</div>
<!-- File list -->
<div class="file-content">
<div class="file-grid" id="file-list"
hx-get="/api/drive/files?path=/"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Files loaded via HTMX -->
</div>
</div>
</main>
</div>
<!-- Modal container -->
<div id="modal-container"></div>
<!-- Upload zone overlay -->
<div class="upload-overlay" id="upload-overlay">
<div class="upload-zone-large">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<p>Drop files here to upload</p>
</div>
</div>
<style>
.drive-container {
display: grid;
grid-template-columns: 240px 1fr;
height: calc(100vh - 56px);
overflow: hidden;
}
/* Sidebar */
.drive-sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
padding: 16px 12px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.new-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 12px 16px;
background: var(--primary);
color: white;
border: none;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-bottom: 20px;
transition: all 0.2s;
}
.new-btn:hover {
background: var(--primary-hover);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.nav-section {
margin-bottom: 24px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
color: var(--text-secondary);
text-decoration: none;
cursor: pointer;
transition: all 0.15s;
}
.nav-item:hover {
background: var(--surface-hover);
color: var(--text);
}
.nav-item.active {
background: var(--primary-light);
color: var(--primary);
}
.nav-item-label {
flex: 1;
font-size: 14px;
}
.storage-info {
margin-top: auto;
padding: 16px;
background: var(--surface-hover);
border-radius: 12px;
}
.storage-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
}
.storage-label {
font-size: 13px;
color: var(--text-secondary);
}
.storage-value {
font-size: 13px;
font-weight: 500;
}
.storage-bar {
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.storage-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), #a855f7);
border-radius: 3px;
transition: width 0.3s;
}
/* Main content */
.drive-main {
display: flex;
flex-direction: column;
overflow: hidden;
}
.drive-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
border-bottom: 1px solid var(--border);
background: var(--surface);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 4px;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 6px;
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.breadcrumb-item:hover {
background: var(--surface-hover);
color: var(--text);
}
.breadcrumb-item.current {
color: var(--text);
font-weight: 500;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 12px;
}
.view-toggle {
display: flex;
background: var(--surface-hover);
border-radius: 6px;
padding: 2px;
}
.view-toggle-btn {
background: transparent;
border: none;
color: var(--text-secondary);
width: 32px;
height: 32px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.view-toggle-btn:hover {
color: var(--text);
}
.view-toggle-btn.active {
background: var(--surface);
color: var(--text);
}
.sort-dropdown {
background: var(--surface-hover);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
color: var(--text);
font-size: 13px;
cursor: pointer;
}
/* File content */
.file-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
.file-list {
display: flex;
flex-direction: column;
gap: 2px;
}
/* File card */
.file-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.file-card:hover {
background: var(--surface-hover);
border-color: var(--primary);
transform: translateY(-2px);
}
.file-card.selected {
border-color: var(--primary);
background: var(--primary-light);
}
.file-card-preview {
width: 100%;
aspect-ratio: 4/3;
background: var(--surface-hover);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
overflow: hidden;
}
.file-card-preview svg {
width: 48px;
height: 48px;
color: var(--text-secondary);
}
.file-card-preview.folder svg {
color: #eab308;
}
.file-card-preview.document svg {
color: var(--primary);
}
.file-card-preview.image svg {
color: #a855f7;
}
.file-card-name {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-card-meta {
font-size: 12px;
color: var(--text-secondary);
}
/* Upload overlay */
.upload-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.upload-overlay.visible {
display: flex;
}
.upload-zone-large {
border: 3px dashed var(--primary);
border-radius: 24px;
padding: 64px;
text-align: center;
color: var(--text);
}
.upload-zone-large svg {
margin-bottom: 16px;
color: var(--primary);
}
.upload-zone-large p {
font-size: 18px;
}
/* Responsive */
@media (max-width: 768px) {
.drive-container {
grid-template-columns: 1fr;
}
.drive-sidebar {
display: none;
}
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
}
</style>
<script>
// View toggle
const gridBtn = document.getElementById('grid-view-btn');
const listBtn = document.getElementById('list-view-btn');
const fileList = document.getElementById('file-list');
if (gridBtn && listBtn) {
gridBtn.addEventListener('click', function() {
gridBtn.classList.add('active');
listBtn.classList.remove('active');
fileList.className = 'file-grid';
});
listBtn.addEventListener('click', function() {
listBtn.classList.add('active');
gridBtn.classList.remove('active');
fileList.className = 'file-list';
});
}
// Drag and drop upload
const uploadOverlay = document.getElementById('upload-overlay');
let dragCounter = 0;
document.addEventListener('dragenter', function(e) {
e.preventDefault();
dragCounter++;
if (dragCounter === 1) {
uploadOverlay.classList.add('visible');
}
});
document.addEventListener('dragleave', function(e) {
e.preventDefault();
dragCounter--;
if (dragCounter === 0) {
uploadOverlay.classList.remove('visible');
}
});
document.addEventListener('dragover', function(e) {
e.preventDefault();
});
document.addEventListener('drop', function(e) {
e.preventDefault();
dragCounter = 0;
uploadOverlay.classList.remove('visible');
const files = e.dataTransfer.files;
if (files.length > 0) {
// Trigger file upload via HTMX
const formData = new FormData();
for (let file of files) {
formData.append('files', file);
}
htmx.ajax('POST', '/api/drive/upload', {
target: '#file-list',
swap: 'innerHTML',
values: formData
});
}
});
// File selection
document.addEventListener('click', function(e) {
const card = e.target.closest('.file-card');
if (card) {
if (e.ctrlKey || e.metaKey) {
card.classList.toggle('selected');
} else {
document.querySelectorAll('.file-card.selected').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
}
}
});
// Double-click to open folder/file
document.addEventListener('dblclick', function(e) {
const card = e.target.closest('.file-card');
if (card) {
const path = card.dataset.path;
const isFolder = card.dataset.type === 'folder';
if (isFolder) {
htmx.ajax('GET', `/api/drive/files?path=${encodeURIComponent(path)}`, {
target: '#file-list',
swap: 'innerHTML'
});
} else {
htmx.ajax('GET', `/api/drive/preview?path=${encodeURIComponent(path)}`, {
target: '#modal-container',
swap: 'innerHTML'
});
}
}
});
</script>
{% endblock %}