Files
flowdeck/app/templates/base.html
T
bruno bed326ee9b
FlowDeck CI / test (push) Failing after 5s
FlowDeck CI / docker (push) Has been skipped
feat: menu New page (Empty/Database/Kanban) + slash commands / dans l'éditeur
- Topbar bouton New ▾ avec dropdown: Empty page, Database, Kanban board
- page_editor: commandes slash / (/h1, /h2, /list, /todo, /code, /table, /quote...)
- Fonctions JS: createEmptyPage, createDatabase, createBoard
- State showNewPageMenu dans Alpine.js
2026-07-08 15:26:36 -04:00

430 lines
17 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" @click="openLibrary('Recents')">📚</button>
<button class="section-action-btn" title="Add page" @click="newPage('Recents')">+</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="openLibrary('{{ page.id }}')">📚</button>
<button class="item-action-btn" @click.stop="newSubPage('{{ 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" title="Open in Library" @click="openLibrary('Private')">📚</button>
<button class="section-action-btn" title="Add page" @click="newPage('Private')">+</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="openLibrary('{{ page.id }}')">📚</button>
<button class="item-action-btn" @click.stop="newSubPage('{{ page.id }}')">+</button>
</span>
</li>
{% endfor %}
{% if not private_pages %}
<li class="sidebar-item">
<span class="page-icon">📄</span>
<span class="page-name text-dim">No pages yet</span>
</li>
{% endif %}
</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>
<!-- Favorites -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-title">
<span class="chevron open">▶</span> Favorites
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="Add page" @click="newPage('Favorites')">+</button>
</div>
</div>
<ul class="sidebar-items">
{% for page in favorite_pages %}
<li class="sidebar-item {% if page.active %}active{% 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="newSubPage('{{ page.id }}')">+</button>
</span>
</li>
{% endfor %}
{% if not favorite_pages %}
<li class="sidebar-item">
<span class="page-icon">⭐</span>
<span class="page-name text-dim">No favorites yet</span>
</li>
{% endif %}
</ul>
</div>
<!-- Shared -->
<div class="sidebar-section">
<div class="sidebar-section-header">
<div class="sidebar-section-title">
<span class="chevron">▶</span> Shared
</div>
</div>
<ul class="sidebar-items">
<li class="sidebar-item">
<span class="page-icon">👥</span>
<span class="page-name text-dim">No shared pages</span>
</li>
</ul>
</div>
<!-- Library button -->
<div class="sidebar-nav" style="margin-top: 8px;">
<a href="/library" class="sidebar-nav-item" @click.prevent="navigateTo('/library')">
<span class="nav-icon">📚</span> Library
</a>
</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" @click="showNewPageMenu = !showNewPageMenu" style="position:relative;">
New ▾
<div class="dropdown-panel" :class="{ visible: showNewPageMenu }" style="top:100%; right:0; left:auto; min-width:200px;" @click.stop>
<div class="dropdown-option" @click="createEmptyPage()"><span class="option-icon">📄</span> Empty page</div>
<div class="dropdown-option" @click="createDatabase()"><span class="option-icon">📊</span> Database</div>
<div class="dropdown-option" @click="createBoard()"><span class="option-icon">📋</span> Kanban board</div>
</div>
</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">&times;</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: {},
showNewPageMenu: false,
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;
},
openLibrary(id) {
window.location.href = '/library';
},
openInLibrary(id) { this.openLibrary(id); },
showMore(id) { console.log('Show more:', id); },
addPage(section) { this.newPage(section); },
addSubPage(id) { this.newSubPage(id); },
getCsrfToken() {
const m = document.cookie.match(/csrf_token=([^;]+)/);
return m ? m[1] : '';
},
newPage(section) {
fetch(`/board/api/pages?title=New%20page&section=${encodeURIComponent(section)}`, {
method: 'POST',
headers: { 'X-CSRF-Token': this.getCsrfToken() }
})
.then(r => { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
.then(data => { window.location.href = `/pages/${data.id}`; })
.catch(err => { alert('Failed to create page: ' + err.message); });
},
newSubPage(parentId) {
fetch(`/board/api/pages?title=New%20page&section=Private`, {
method: 'POST',
headers: { 'X-CSRF-Token': this.getCsrfToken() }
})
.then(r => { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
.then(data => { window.location.href = `/pages/${data.id}`; })
.catch(err => { alert('Failed to create sub-page: ' + err.message); });
},
createEmptyPage() {
this.showNewPageMenu = false;
this.newPage('Private');
},
createDatabase() {
this.showNewPageMenu = false;
alert('Database creation: use a Gitea repo as a database. Navigate to a project board from the dashboard.');
},
createBoard() {
this.showNewPageMenu = false;
window.location.href = '/board/bruno/flowdeck';
},
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>