- Layout 3 zones (sidebar 240px + topbar 44px + contenu) - Sidebar: sections hiérarchiques, hover states, menu contextuel - CSS design system Notion: #191919/#222/#333, Inter, coins 4-6px - Vues: Kanban (groupes+colonnes), Dashboard Notion-style - Templates: base.html refondu, card.html partial, board.html v2 - Backend: données sidebar + board Notion (groups/cards) - Docs: ROADMAP v0.4→v1.0, WORKLOAD.md, README - Backward compatible: conserve _issue_column pour api.py - Images de référence + resized (analyse 2026-07-08)
339 lines
13 KiB
HTML
339 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>FlowDeck</title>
|
|
<link rel="stylesheet" href="/static/css/app.css">
|
|
<script src="https://unpkg.com/[email protected]"></script>
|
|
<script src="https://unpkg.com/[email protected]" defer></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js" defer></script>
|
|
</head>
|
|
<body hx-headers='{"X-CSRF-Token":"__CSRF_PLACEHOLDER__"}'>
|
|
<div class="app-layout" x-data="appState()">
|
|
|
|
<!-- ═══════════ SIDEBAR ═══════════ -->
|
|
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }" id="sidebar">
|
|
|
|
<!-- Workspace header -->
|
|
<div class="sidebar-workspace">
|
|
<div class="workspace-info">
|
|
<div class="workspace-avatar">{{ workspaceInitial }}</div>
|
|
<span class="workspace-name">{{ workspaceName }}</span>
|
|
</div>
|
|
<div class="sidebar-actions">
|
|
<button class="sidebar-action-btn" @click="sidebarCollapsed = !sidebarCollapsed" title="Toggle sidebar">
|
|
«
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick actions -->
|
|
<div class="sidebar-nav">
|
|
<a href="/" class="sidebar-nav-item" :class="{ active: currentView === 'home' }">
|
|
<span class="nav-icon">🏠</span> Home
|
|
</a>
|
|
</div>
|
|
<div class="sidebar-nav" style="gap: 2px; padding-right: 6px;">
|
|
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">💬</button>
|
|
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">🎤</button>
|
|
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">📥</button>
|
|
<button class="sidebar-action-btn" style="width:auto; padding: 0 6px;">🔍</button>
|
|
</div>
|
|
|
|
<!-- Recents -->
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<div class="sidebar-section-title">
|
|
<span class="chevron open">▶</span> Recents
|
|
</div>
|
|
<div class="sidebar-section-actions">
|
|
<button class="section-action-btn" title="Open in Library">📚</button>
|
|
<button class="section-action-btn" title="More">⋯</button>
|
|
</div>
|
|
</div>
|
|
<ul class="sidebar-items">
|
|
{% for page in recent_pages %}
|
|
<li class="sidebar-item {% if page.active %}active{% endif %} {% if page.indent %}indent-{{ page.indent }}{% endif %}"
|
|
@click="navigateTo('{{ page.url }}')"
|
|
@contextmenu.prevent="showContextMenu($event, '{{ page.id }}', '{{ page.name }}')">
|
|
<span class="page-icon">{{ page.icon }}</span>
|
|
<span class="page-name">{{ page.name }}</span>
|
|
<span class="item-actions">
|
|
<button class="item-action-btn" @click.stop="openInLibrary('{{ page.id }}')">📚</button>
|
|
<button class="item-action-btn" @click.stop="showMore('{{ page.id }}')">⋯</button>
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
{% if not recent_pages %}
|
|
<li class="sidebar-item">
|
|
<span class="page-icon">📄</span>
|
|
<span class="page-name text-dim">No recent pages</span>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Favorites: Private -->
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<div class="sidebar-section-title">
|
|
<span class="chevron open">▶</span> Private
|
|
</div>
|
|
<div class="sidebar-section-actions">
|
|
<button class="section-action-btn" @click="addPage('private')" title="Add page">+</button>
|
|
</div>
|
|
</div>
|
|
<ul class="sidebar-items">
|
|
{% for page in private_pages %}
|
|
<li class="sidebar-item {% if page.active %}active{% endif %} {% if page.indent %}indent-{{ page.indent }}{% endif %}"
|
|
@click="navigateTo('{{ page.url }}')"
|
|
@contextmenu.prevent="showContextMenu($event, '{{ page.id }}', '{{ page.name }}')">
|
|
<span class="page-icon">{{ page.icon }}</span>
|
|
<span class="page-name">{{ page.name }}</span>
|
|
<span class="item-actions">
|
|
<button class="item-action-btn" @click.stop="showMore('{{ page.id }}')">⋯</button>
|
|
<button class="item-action-btn" @click.stop="addSubPage('{{ page.id }}')">+</button>
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Notion apps -->
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<div class="sidebar-section-title">
|
|
<span class="chevron open">▶</span> Notion apps
|
|
</div>
|
|
</div>
|
|
<ul class="sidebar-items">
|
|
<li class="sidebar-item"><span class="page-icon">📅</span><span class="page-name">Notion Calendar</span></li>
|
|
<li class="sidebar-item"><span class="page-icon">🖥️</span><span class="page-name">Notion Desktop</span></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Library -->
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<div class="sidebar-section-title">
|
|
<span class="chevron open">▶</span> Library
|
|
</div>
|
|
</div>
|
|
<ul class="sidebar-items">
|
|
<li class="sidebar-item"><span class="page-icon">✅</span><span class="page-name">My Tasks</span></li>
|
|
<li class="sidebar-item"><span class="page-icon">🛒</span><span class="page-name">Marketplace</span></li>
|
|
<li class="sidebar-item"><span class="page-icon">❓</span><span class="page-name">Help</span></li>
|
|
<li class="sidebar-item"><span class="page-icon">🗑️</span><span class="page-name">Trash</span></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="sidebar-footer">
|
|
<div class="sidebar-invite" x-show="showInvite" x-transition>
|
|
<div class="sidebar-invite-title">👥 Invite members</div>
|
|
<div class="sidebar-invite-desc">Collaborate with your team.</div>
|
|
<button class="sidebar-invite-close" @click="showInvite = false">✕</button>
|
|
</div>
|
|
<button class="sidebar-new-chat">
|
|
<span>💬 New chat</span>
|
|
<span class="shortcut">Ctrl+O</span>
|
|
</button>
|
|
<div style="display: flex; gap: 4px; margin-top: 4px;">
|
|
{% if user %}
|
|
<a href="/auth/logout" class="sidebar-new-chat" style="flex:1; justify-content:center;">🚪</a>
|
|
{% else %}
|
|
<a href="/auth/login" class="sidebar-new-chat" style="flex:1; justify-content:center;">🔑</a>
|
|
{% endif %}
|
|
<button class="sidebar-new-chat" style="width:36px; justify-content:center;" @click="toggleTheme()">🌓</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ═══════════ MAIN ═══════════ -->
|
|
<div class="main-wrapper">
|
|
|
|
<!-- Topbar -->
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<div class="topbar-breadcrumb">
|
|
<span>{{ workspaceName }}</span>
|
|
<span class="text-dim">/</span>
|
|
<span class="text-dim">{{ currentPage }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="topbar-center">
|
|
<div class="topbar-page-title">
|
|
<span class="page-icon">{{ pageIcon }}</span>
|
|
<span>{{ pageTitle }}</span>
|
|
</div>
|
|
<div class="topbar-page-status">
|
|
<span>🔒</span> Private
|
|
<span style="cursor:pointer;">▾</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="topbar-right">
|
|
<span class="text-xs text-dim">Edited {{ lastEdited }}</span>
|
|
<button class="topbar-btn">🔔</button>
|
|
<button class="topbar-btn">🕒</button>
|
|
<button class="topbar-btn">🔍</button>
|
|
<button class="topbar-btn share-btn">🔗 Share ▾</button>
|
|
<button class="topbar-btn">⭐</button>
|
|
<button class="topbar-btn">⋯</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Content -->
|
|
<div class="content-area" id="main-content">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ═══════════ CONTEXT MENU ═══════════ -->
|
|
<div class="context-menu" id="context-menu"
|
|
:class="{ visible: contextMenu.visible }"
|
|
:style="'left:' + contextMenu.x + 'px; top:' + contextMenu.y + 'px'"
|
|
@click.away="contextMenu.visible = false">
|
|
<div class="context-menu-section">
|
|
<div class="context-menu-label">Database</div>
|
|
</div>
|
|
<div class="context-menu-section">
|
|
<div class="context-menu-item" @click="contextAction('favorite')">
|
|
<span class="menu-icon">⭐</span> Remove from Favorites
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('recent')">
|
|
<span class="menu-icon">👁️</span> Remove from Recents
|
|
</div>
|
|
</div>
|
|
<div class="context-menu-section">
|
|
<div class="context-menu-item" @click="contextAction('copyLink')">
|
|
<span class="menu-icon">🔗</span> Copy link
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('duplicate')">
|
|
<span class="menu-icon">📋</span> Duplicate
|
|
<span class="menu-shortcut">▶</span>
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('rename')">
|
|
<span class="menu-icon">✏️</span> Rename
|
|
<span class="menu-shortcut">Ctrl+Shift+R</span>
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('moveTo')">
|
|
<span class="menu-icon">↗️</span> Move to
|
|
<span class="menu-shortcut">Ctrl+Shift+P</span>
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('delete')">
|
|
<span class="menu-icon">🗑️</span> Move to Trash
|
|
</div>
|
|
</div>
|
|
<div class="context-menu-section">
|
|
<div class="context-menu-item" @click="contextAction('openTab')">
|
|
<span class="menu-icon">🔲</span> Open in new tab
|
|
<span class="menu-shortcut">Ctrl+Shift+O</span>
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('openPeek')">
|
|
<span class="menu-icon">📑</span> Open in side peek
|
|
<span class="menu-shortcut">Alt+Click</span>
|
|
</div>
|
|
</div>
|
|
<div class="context-menu-section">
|
|
<div class="context-menu-item text-dim" style="font-size:11px; cursor:default;">
|
|
Last edited by {{ workspaceName }}
|
|
</div>
|
|
<div class="context-menu-item text-dim" style="font-size:11px; cursor:default;">
|
|
{{ lastEdited }}
|
|
</div>
|
|
<div class="context-menu-item" @click="contextAction('learnDb')">
|
|
<span class="menu-icon">❓</span> Learn about databases
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ TOOLTIP ═══════════ -->
|
|
<div class="tooltip" id="tooltip" x-ref="tooltip"></div>
|
|
|
|
<!-- ═══════════ CARD MODAL ═══════════ -->
|
|
<div id="card-modal" class="modal-overlay" style="display:none">
|
|
<div class="modal-container">
|
|
<div class="modal-header">
|
|
<h3 id="card-modal-title">Détail</h3>
|
|
<button onclick="closeModal()" class="modal-close">×</button>
|
|
</div>
|
|
<div id="card-modal-content" class="modal-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
// Inject CSRF token into HTMX headers
|
|
(function() {
|
|
const getCsrf = () => {
|
|
const m = document.cookie.match(/csrf_token=([^;]+)/);
|
|
return m ? m[1] : '';
|
|
};
|
|
document.body.setAttribute('hx-headers', JSON.stringify({'X-CSRF-Token': getCsrf()}));
|
|
document.addEventListener('htmx:configRequest', function(evt) {
|
|
evt.detail.headers['X-CSRF-Token'] = getCsrf();
|
|
});
|
|
})();
|
|
|
|
function appState() {
|
|
return {
|
|
sidebarCollapsed: false,
|
|
showInvite: true,
|
|
workspaceName: '{{ workspace_name }}',
|
|
workspaceInitial: '{{ workspace_initial }}',
|
|
currentPage: '{{ current_page }}',
|
|
pageIcon: '{% block page_icon %}📁{% endblock %}',
|
|
pageTitle: '{% block page_title %}FlowDeck{% endblock %}',
|
|
lastEdited: '{{ last_edited }}',
|
|
currentView: 'home',
|
|
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
|
contextActions: {},
|
|
|
|
navigateTo(url) {
|
|
window.location.href = url;
|
|
},
|
|
|
|
showContextMenu(event, pageId, pageName) {
|
|
this.contextMenu = {
|
|
visible: true,
|
|
x: event.clientX,
|
|
y: event.clientY,
|
|
pageId: pageId,
|
|
pageName: pageName
|
|
};
|
|
},
|
|
|
|
contextAction(action) {
|
|
const { pageId, pageName } = this.contextMenu;
|
|
console.log('Context action:', action, pageId, pageName);
|
|
// TODO: implement each action
|
|
if (action === 'duplicate') {
|
|
if (this.contextActions.duplicate) this.contextActions.duplicate(pageId);
|
|
}
|
|
this.contextMenu.visible = false;
|
|
},
|
|
|
|
openInLibrary(id) { console.log('Open in library:', id); },
|
|
showMore(id) { console.log('Show more:', id); },
|
|
addPage(section) { console.log('Add page to:', section); },
|
|
addSubPage(id) { console.log('Add sub-page to:', id); },
|
|
|
|
toggleTheme() {
|
|
const html = document.documentElement;
|
|
const current = html.getAttribute('data-theme');
|
|
html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
|
|
localStorage.setItem('theme', current === 'dark' ? 'light' : 'dark');
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
<script src="/static/js/app.js" defer></script>
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|