feat: sidebar Kanban board + pages Markdown + boutons + et 📚 fonctionnels
- Sidebar: 'Kanban board' affiché avant les 4 sous-vues
- DB: nouvelle table 'pages' (workspace, title, content, parent_section)
- API: POST /board/api/pages, GET/PUT /board/api/pages/{id}
- UI: page_editor.html — éditeur Markdown avec preview live
- UI: boutons + créent une page Markdown, bouton 📚 ouvre la page
- Dashboard: sidebar_data utilise BDD pages + workspace
- board.py: _sidebar_data enrichi (Kanban board + pages DB)
This commit is contained in:
+32
-10
@@ -48,8 +48,8 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -60,8 +60,8 @@
|
||||
<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>
|
||||
<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 %}
|
||||
@@ -81,7 +81,8 @@
|
||||
<span class="chevron open">▶</span> Private
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" @click="addPage('private')" title="Add page">+</button>
|
||||
<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">
|
||||
@@ -92,11 +93,17 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -318,10 +325,25 @@
|
||||
this.contextMenu.visible = false;
|
||||
},
|
||||
|
||||
openInLibrary(id) { console.log('Open in library:', id); },
|
||||
openLibrary(id) {
|
||||
// Open the library view for this section/page
|
||||
console.log('Open library for:', id);
|
||||
window.location.href = '/pages/' + id.replace('page/', '');
|
||||
},
|
||||
openInLibrary(id) { this.openLibrary(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); },
|
||||
addPage(section) { this.newPage(section); },
|
||||
addSubPage(id) { this.newSubPage(id); },
|
||||
newPage(section) {
|
||||
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; });
|
||||
},
|
||||
newSubPage(parentId) {
|
||||
fetch(`/board/api/pages?title=New%20sub-page§ion=Private`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; });
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
{% extends "base.html" %}
|
||||
{% block page_icon %}📄{% endblock %}
|
||||
{% block page_title %}{{ page.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-title-area">
|
||||
<div class="page-title">
|
||||
<span class="page-icon-lg">📄</span>
|
||||
<h1 contenteditable="true"
|
||||
@blur="saveTitle($event.target.textContent)"
|
||||
x-data>{{ page.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data="pageEditor('{{ page.content|e }}')" style="padding: 0 24px 48px; max-width: 900px;">
|
||||
<!-- Toolbar -->
|
||||
<div style="display:flex; gap:4px; margin-bottom:12px; flex-wrap:wrap;">
|
||||
<button class="toolbar-btn" @click="insertMd('**', '**')" title="Bold">B</button>
|
||||
<button class="toolbar-btn" @click="insertMd('*', '*')" title="Italic"><i>I</i></button>
|
||||
<button class="toolbar-btn" @click="insertMd('# ', '')" title="Heading 1">H1</button>
|
||||
<button class="toolbar-btn" @click="insertMd('## ', '')" title="Heading 2">H2</button>
|
||||
<button class="toolbar-btn" @click="insertMd('- ', '')" title="Bullet list">•</button>
|
||||
<button class="toolbar-btn" @click="insertMd('1. ', '')" title="Numbered list">1.</button>
|
||||
<button class="toolbar-btn" @click="insertMd('> ', '')" title="Quote">"</button>
|
||||
<button class="toolbar-btn" @click="insertMd('`', '`')" title="Code"></></button>
|
||||
<button class="toolbar-btn" @click="insertMd('```\n', '\n```')" title="Code block">```</button>
|
||||
<button class="toolbar-btn" @click="insertMd('---\n', '')" title="Divider">—</button>
|
||||
<button class="toolbar-btn" @click="insertMd('[', '](url)')" title="Link">🔗</button>
|
||||
<button class="toolbar-btn" @click="insertMd('- [ ] ', '')" title="Todo">☐</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div style="display:flex; gap:0; min-height:400px; border:1px solid var(--border); border-radius:8px; overflow:hidden;">
|
||||
<!-- Markdown source -->
|
||||
<div style="flex:1; display:flex; flex-direction:column;">
|
||||
<div style="padding:4px 12px; background:var(--bg-tertiary); font-size:12px; color:var(--text-dim);">Markdown</div>
|
||||
<textarea x-ref="editor" x-model="content"
|
||||
@input="autoSave()"
|
||||
style="flex:1; width:100%; min-height:400px; background:var(--bg-secondary);
|
||||
border:none; padding:12px; color:var(--text-primary); font-family:var(--font-mono);
|
||||
font-size:13px; line-height:1.6; resize:none; outline:none; tab-size:2;"
|
||||
placeholder="Start writing in Markdown..."></textarea>
|
||||
</div>
|
||||
<!-- Preview -->
|
||||
<div style="flex:1; border-left:1px solid var(--border); display:flex; flex-direction:column;">
|
||||
<div style="padding:4px 12px; background:var(--bg-tertiary); font-size:12px; color:var(--text-dim);">Preview</div>
|
||||
<div style="flex:1; padding:12px; overflow-y:auto; font-size:14px; line-height:1.7; color:var(--text-primary);"
|
||||
x-html="renderedContent()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-top:8px; font-size:12px; color:var(--text-dim);">
|
||||
<span x-show="saved">✓ Saved</span>
|
||||
<span x-show="dirty && !saving">● Unsaved</span>
|
||||
<span x-text="wordCount + ' words'"></span>
|
||||
<span style="margin-left:auto;" x-text="'Last edited: ' + lastSaved"></span>
|
||||
</div>
|
||||
|
||||
<!-- Get started block (shown for empty pages) -->
|
||||
<div x-show="!content.trim()" style="margin-top:48px; text-align:center; color:var(--text-dim);">
|
||||
<div style="font-size:48px; margin-bottom:16px;">📝</div>
|
||||
<h3 style="color:var(--text-secondary); margin-bottom:8px;">Get started with</h3>
|
||||
<div style="display:flex; gap:12px; justify-content:center; flex-wrap:wrap;">
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('# ', '')">📄 Empty page</button>
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('- [ ] Task 1\n- [ ] Task 2\n', '')">✅ To-do list</button>
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('| Col 1 | Col 2 |\n|-------|-------|\n| | |\n', '')">📊 Table</button>
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('# Meeting Notes\n\n**Date:** \n**Attendees:** \n\n## Agenda\n\n- \n\n## Notes\n\n- \n\n## Action Items\n\n- [ ] \n', '')">🎤 Meeting Notes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Simple Markdown-to-HTML renderer
|
||||
function renderMarkdown(md) {
|
||||
let html = md
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
// Headers
|
||||
.replace(/^### (.+)$/gm, '<h3 style="font-size:16px; margin:12px 0 4px;">$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2 style="font-size:18px; margin:16px 0 4px;">$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1 style="font-size:22px; margin:20px 0 6px;">$1</h1>')
|
||||
// Bold/italic
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code style="background:var(--bg-tertiary); padding:1px 4px; border-radius:3px; font-size:12px;">$1</code>')
|
||||
// Code blocks
|
||||
.replace(/```[\s\S]*?```/g, (m) => '<pre style="background:var(--bg-tertiary); padding:12px; border-radius:4px; overflow-x:auto; font-size:12px;"><code>' + m.slice(3, -3).trim() + '</code></pre>')
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:var(--blue);">$1</a>')
|
||||
// Horizontal rules
|
||||
.replace(/^---$/gm, '<hr style="border:none; border-top:1px solid var(--border); margin:16px 0;">')
|
||||
// Blockquotes
|
||||
.replace(/^> (.+)$/gm, '<blockquote style="border-left:2px solid var(--blue); padding-left:12px; margin:8px 0; color:var(--text-secondary);">$1</blockquote>')
|
||||
// Lists
|
||||
.replace(/^- (.+)$/gm, '<li style="margin-left:20px;">$1</li>')
|
||||
.replace(/^\d+\. (.+)$/gm, '<li style="margin-left:20px;">$1</li>')
|
||||
// Checkboxes
|
||||
.replace(/^- \[ \] (.+)$/gm, '<div style="display:flex; align-items:center; gap:8px; margin:2px 0;"><input type="checkbox" style="accent-color:var(--blue);">$1</div>')
|
||||
.replace(/^- \[x\] (.+)$/gm, '<div style="display:flex; align-items:center; gap:8px; margin:2px 0;"><input type="checkbox" checked style="accent-color:var(--blue);"><span style="text-decoration:line-through; color:var(--text-dim);">$1</span></div>')
|
||||
// Paragraphs
|
||||
.replace(/\n\n/g, '<br><br>');
|
||||
return html;
|
||||
}
|
||||
|
||||
function pageEditor(initialContent) {
|
||||
return {
|
||||
content: initialContent || '',
|
||||
saved: true,
|
||||
dirty: false,
|
||||
saving: false,
|
||||
lastSaved: 'now',
|
||||
saveTimeout: null,
|
||||
renderedContent() { return renderMarkdown(this.content); },
|
||||
get wordCount() { return this.content.trim() ? this.content.trim().split(/\s+/).length : 0; },
|
||||
insertMd(before, after) {
|
||||
const el = this.$refs.editor;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
const selected = this.content.substring(start, end);
|
||||
const replacement = before + selected + after;
|
||||
this.content = this.content.substring(0, start) + replacement + this.content.substring(end);
|
||||
this.autoSave();
|
||||
el.focus();
|
||||
el.setSelectionRange(start + before.length, start + before.length + selected.length);
|
||||
},
|
||||
autoSave() {
|
||||
this.dirty = true;
|
||||
this.saved = false;
|
||||
clearTimeout(this.saveTimeout);
|
||||
this.saveTimeout = setTimeout(() => this.save(), 1500);
|
||||
},
|
||||
save() {
|
||||
this.saving = true;
|
||||
const title = document.querySelector('.page-title h1')?.textContent || 'New page';
|
||||
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(title)}&content=${encodeURIComponent(this.content)}`, { method: 'PUT' })
|
||||
.then(() => {
|
||||
this.saved = true;
|
||||
this.dirty = false;
|
||||
this.saving = false;
|
||||
this.lastSaved = new Date().toLocaleTimeString();
|
||||
});
|
||||
},
|
||||
saveTitle(newTitle) {
|
||||
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(newTitle)}`, { method: 'PUT' });
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user