- Add sort_order column to pages table for drag & drop reordering
- Rewrite _sidebar_data with recursive _build_page_tree for nested hierarchy
- Add tree toggle buttons (▶ collapsed / ▼ expanded) on parent items
- Alpine.js expandedNodes state with toggleTreeNode() for expand/collapse
- Jinja2 recursive macro render_tree_item for proper nesting + indentation
- SortableJS integration on .sidebar-items for drag & drop repositioning
- New PUT /board/api/pages/{id}/move API for persisting moves
- Auto-set sort_order on page creation (max+1 for same parent)
- CSS: .tree-toggle, .tree-toggle-placeholder, grab cursor on page-icon
- 28/28 tests pass, all endpoints healthy
550 lines
21 KiB
HTML
550 lines
21 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()">
|
|
|
|
{# ─── Recursive tree macro ─── #}
|
|
{% macro render_tree_item(page, depth=0) %}
|
|
{% set indent_px = depth * 16 %}
|
|
<li class="sidebar-item {% if page.active %}active{% endif %}"
|
|
style="padding-left: {{ 8 + indent_px }}px;"
|
|
@click="navigateTo('{{ page.url }}')"
|
|
@contextmenu.prevent="showContextMenu($event, '{{ page.id }}', '{{ page.name }}')"
|
|
data-page-id="{{ page.id }}"
|
|
data-page-depth="{{ depth }}"
|
|
data-has-children="{{ 'true' if page.has_children else 'false' }}">
|
|
{# Toggle chevron — only visible for items with children #}
|
|
{% if page.has_children %}
|
|
<button class="tree-toggle"
|
|
@click.stop="toggleTreeNode('{{ page.id }}')"
|
|
:class="{ expanded: expandedNodes['{{ page.id }}'] }">▶</button>
|
|
{% else %}
|
|
<span class="tree-toggle-placeholder"></span>
|
|
{% endif %}
|
|
<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>
|
|
{% if page.has_children %}
|
|
<li class="tree-children-wrapper" x-show="expandedNodes['{{ page.id }}']" x-transition>
|
|
<ul class="sidebar-items" data-parent="{{ page.id }}">
|
|
{% for child in page.children %}
|
|
{{ render_tree_item(child, depth + 1) }}
|
|
{% endfor %}
|
|
</ul>
|
|
</li>
|
|
{% endif %}
|
|
{% endmacro %}
|
|
|
|
<!-- ═══════════ SIDEBAR ═══════════ -->
|
|
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }" id="sidebar"
|
|
x-init="$nextTick(() => initTreeSortable())">
|
|
|
|
<!-- 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" data-section="recents">
|
|
{% for page in recent_pages %}
|
|
{{ render_tree_item(page) }}
|
|
{% endfor %}
|
|
{% if not recent_pages %}
|
|
<li class="sidebar-item">
|
|
<span class="tree-toggle-placeholder"></span>
|
|
<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" data-section="private">
|
|
{% for page in private_pages %}
|
|
{{ render_tree_item(page) }}
|
|
{% endfor %}
|
|
{% if not private_pages %}
|
|
<li class="sidebar-item">
|
|
<span class="tree-toggle-placeholder"></span>
|
|
<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" data-section="favorites">
|
|
{% for page in favorite_pages %}
|
|
{{ render_tree_item(page) }}
|
|
{% endfor %}
|
|
{% if not favorite_pages %}
|
|
<li class="sidebar-item">
|
|
<span class="tree-toggle-placeholder"></span>
|
|
<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 x-text="workspaceName"></span>
|
|
<span class="text-dim">/</span>
|
|
<span class="text-dim" x-text="currentPage"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="topbar-center">
|
|
<div class="topbar-page-title">
|
|
<span class="page-icon" x-text="pageIcon"></span>
|
|
<span x-text="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 <span x-text="lastEdited"></span></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 <span x-text="workspaceName"></span>
|
|
</div>
|
|
<div class="context-menu-item text-dim" style="font-size:11px; cursor:default;">
|
|
<span x-text="lastEdited"></span>
|
|
</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 }}',
|
|
workspaceKey: '{{ workspace_key }}',
|
|
currentView: 'home',
|
|
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
|
contextActions: {},
|
|
showNewPageMenu: false,
|
|
|
|
// ── Tree state ──
|
|
expandedNodes: {},
|
|
|
|
isExpanded(id) {
|
|
return this.expandedNodes[id] || false;
|
|
},
|
|
toggleTreeNode(id) {
|
|
const current = this.expandedNodes[id] || false;
|
|
this.expandedNodes[id] = !current;
|
|
// Trigger Alpine.js reactivity by reassigning
|
|
this.expandedNodes = Object.assign({}, this.expandedNodes);
|
|
// If expanding, init SortableJS on the newly visible children
|
|
if (!current) {
|
|
this.$nextTick(() => this.initTreeSortable());
|
|
}
|
|
},
|
|
expandAll() {
|
|
document.querySelectorAll('[data-has-children="true"]').forEach(el => {
|
|
this.expandedNodes[el.dataset.pageId] = true;
|
|
});
|
|
this.expandedNodes = Object.assign({}, this.expandedNodes);
|
|
this.$nextTick(() => this.initTreeSortable());
|
|
},
|
|
|
|
// ── Drag & drop tree ──
|
|
treeSortables: [],
|
|
initTreeSortable() {
|
|
// Initialize SortableJS on every .sidebar-items list
|
|
const self = this;
|
|
document.querySelectorAll('.sidebar-items').forEach(function(el) {
|
|
if (el._sortableTree) return; // Already initialized
|
|
el._sortableTree = new Sortable(el, {
|
|
group: 'pages-tree',
|
|
handle: '.page-icon',
|
|
animation: 150,
|
|
ghostClass: 'sortable-ghost',
|
|
dragClass: 'sortable-drag',
|
|
fallbackOnBody: true,
|
|
swapThreshold: 0.5,
|
|
onEnd: function(evt) {
|
|
// Get the dragged item's page ID
|
|
const item = evt.item;
|
|
const pageId = item.dataset.pageId;
|
|
if (!pageId || !pageId.startsWith('page/')) return;
|
|
const dbId = pageId.replace('page/', '');
|
|
// Determine new parent from the target list's data-parent
|
|
const targetList = evt.to;
|
|
let newParentId = 0;
|
|
const parentAttr = targetList.dataset.parent;
|
|
if (parentAttr && parentAttr.startsWith('page/')) {
|
|
newParentId = parentAttr.replace('page/', '');
|
|
}
|
|
const newIndex = evt.newIndex;
|
|
// Call API to persist the move
|
|
const csrf = self.getCsrfToken();
|
|
fetch(`/board/api/pages/${dbId}/move?new_parent_id=${newParentId}&new_order=${newIndex}`, {
|
|
method: 'PUT',
|
|
headers: { 'X-CSRF-Token': csrf }
|
|
}).catch(e => console.error('Move failed:', e));
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
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;
|
|
const self = this;
|
|
const csrf = this.getCsrfToken();
|
|
|
|
switch(action) {
|
|
case 'favorite':
|
|
alert(`Toggled favorite for: ${pageName}`);
|
|
break;
|
|
case 'copyLink':
|
|
const url = window.location.origin + '/' + pageId;
|
|
navigator.clipboard.writeText(url).then(() => alert('Link copied!'));
|
|
break;
|
|
case 'duplicate':
|
|
fetch(`/board/api/pages?title=${encodeURIComponent(pageName + ' copy')}§ion=Private&project=${encodeURIComponent(this.workspaceKey)}`, {
|
|
method: 'POST', headers: { 'X-CSRF-Token': csrf }
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => { window.location.href = `/pages/${data.id}`; })
|
|
.catch(e => alert('Duplicate failed'));
|
|
break;
|
|
case 'rename':
|
|
const newName = prompt('New name:', pageName);
|
|
if (!newName) break;
|
|
const pageNum = pageId.replace('page/', '');
|
|
fetch(`/board/api/pages/${pageNum}?title=${encodeURIComponent(newName)}`, {
|
|
method: 'PUT', headers: { 'X-CSRF-Token': csrf }
|
|
}).then(() => location.reload());
|
|
break;
|
|
case 'moveTo':
|
|
const section = prompt('Move to section (Private, Recents, Favorites):', 'Private');
|
|
if (!section) break;
|
|
alert(`Moved to ${section}`);
|
|
break;
|
|
case 'delete':
|
|
if (!confirm(`Delete "${pageName}"?`)) break;
|
|
alert('Delete not yet implemented (pages go to Trash in DB)');
|
|
break;
|
|
case 'openTab':
|
|
window.open('/' + pageId, '_blank');
|
|
break;
|
|
case 'openPeek':
|
|
alert('Side peek not available in browser');
|
|
break;
|
|
}
|
|
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) {
|
|
const project = this.workspaceKey || '';
|
|
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}&project=${encodeURIComponent(project)}`, {
|
|
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) {
|
|
const project = this.workspaceKey || '';
|
|
const pid = parentId.replace('page/', '');
|
|
fetch(`/board/api/pages?title=New%20sub-page§ion=Private&project=${encodeURIComponent(project)}&parent_id=${pid}`, {
|
|
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>
|