feat(ui): reordenar abas com arraste, botão 🗂 Arquivos na toolbar, lixeira na lista
- Abas: HTML5 drag-and-drop — arrastar uma aba sobre outra mostra marca de inserção (borda accent à esquerda/direita conforme o lado) e solta reordenando; soltar na área vazia da aba-barra move para o fim; ordem persiste em localStorage (passa a valer após o debounce de 400ms de persistTabs); ✕ da aba não inicia arraste (dragstart cancelado) - Toolbar: botão 🗂 Arquivos abre o gerenciador no modo normal (não seletor), equivalente ao Ctrl+J - Lista de notas (sidebar): botão 🗑 em cada linha abre o mesmo diálogo de confirmação do menu e exclui a nota mesmo sem aba aberta; se houver aba da nota, ela é fechada e a aba vizinha ativada; no mobile o botão fica sempre visível (alvo 32px, sem depender de hover) - deleteNote() agora delega para deleteNoteById(n) (reuso do fluxo com presença/persistTabs) - sw v15
This commit is contained in:
1 parent
82417d2e79
commit
1b9ce995ba
4 files changed
+121
-13
No files matched your search
@@ -278,7 +278,26 @@ body.sidebar-off #sidebar { display: none; }
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.n-main { flex: 1 1 auto; min-width: 0; }
|
||||
.n-del {
|
||||
flex: none;
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
color: var(--muted);
|
||||
padding: 4px;
|
||||
border-radius: 0;
|
||||
opacity: 0;
|
||||
transition: opacity .12s ease, color .12s ease;
|
||||
}
|
||||
#notes-list li:hover .n-del, #notes-list li:focus-within .n-del { opacity: 1; }
|
||||
.n-del:hover { color: var(--danger); }
|
||||
#notes-list li:hover { background: color-mix(in oklab, var(--panel-2), var(--fg) 9%); }
|
||||
#notes-list li.active { background: color-mix(in oklab, var(--panel), var(--accent) 24%); box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.n-title { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -593,12 +612,16 @@ body.no-gutter #gutter { display: none; }
|
||||
background: var(--panel-2);
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
user-select: none;
|
||||
color: var(--muted);
|
||||
padding: 6px 8px 6px 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
max-width: 240px;
|
||||
}
|
||||
.tab.dragging { opacity: .45; }
|
||||
.tab.drop-before { box-shadow: inset 2px 0 0 var(--accent); }
|
||||
.tab.drop-after { box-shadow: inset -2px 0 0 var(--accent); }
|
||||
.tab.active {
|
||||
background: var(--panel);
|
||||
color: var(--fg);
|
||||
@@ -730,6 +753,7 @@ body.no-gutter #gutter { display: none; }
|
||||
#menubar::-webkit-scrollbar { display: none; }
|
||||
.menu-btn { padding: 0 9px; }
|
||||
.menu-dropdown li { padding: 9px 14px; }
|
||||
.n-del { opacity: 1; min-height: 32px; min-width: 32px; }
|
||||
|
||||
#toolbar {
|
||||
height: auto;
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
<button type="button" class="btn btn-icon" id="btn-save" title="Salvar (Ctrl+S)">💾<span class="tb-label">Salvar</span></button>
|
||||
<button type="button" class="btn btn-icon" id="btn-export" title="Baixar arquivo">⬇<span class="tb-label">Baixar</span></button>
|
||||
<button type="button" class="btn btn-icon" id="btn-attach" title="Inserir anexo (📎)">📎<span class="tb-label">Anexo</span></button>
|
||||
<button type="button" class="btn btn-icon" id="btn-files" title="Gerenciador de arquivos (Ctrl+J)">🗂<span class="tb-label">Arquivos</span></button>
|
||||
</div>
|
||||
<span class="tb-sep"></span>
|
||||
<div class="tb-group" id="fmt-tools">
|
||||
|
||||
+95
-12
@@ -343,6 +343,36 @@
|
||||
activateTab(next.key);
|
||||
}
|
||||
|
||||
let dragTabKey = null;
|
||||
function clearTabDropMarks() {
|
||||
E.tabs.querySelectorAll('.drop-before, .drop-after').forEach((x) => x.classList.remove('drop-before', 'drop-after'));
|
||||
}
|
||||
function moveTab(srcKey, dstKey, before) {
|
||||
const from = state.tabs.findIndex((t) => t.key === srcKey);
|
||||
if (from < 0) return;
|
||||
const [tab] = state.tabs.splice(from, 1);
|
||||
let to = state.tabs.findIndex((t) => t.key === dstKey);
|
||||
if (to < 0) { state.tabs.splice(from, 0, tab); return; }
|
||||
state.tabs.splice(before ? to : to + 1, 0, tab);
|
||||
renderTabs();
|
||||
persistTabs();
|
||||
}
|
||||
// soltar na área vazia da aba-barra move para o fim
|
||||
E.tabs.addEventListener('dragover', (e) => { if (dragTabKey) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; } });
|
||||
E.tabs.addEventListener('drop', (e) => {
|
||||
if (!dragTabKey) return;
|
||||
e.preventDefault();
|
||||
const tab = state.tabs.find((t) => t.key === dragTabKey);
|
||||
if (tab) {
|
||||
state.tabs.splice(state.tabs.indexOf(tab), 1);
|
||||
state.tabs.push(tab);
|
||||
renderTabs();
|
||||
persistTabs();
|
||||
}
|
||||
dragTabKey = null;
|
||||
clearTabDropMarks();
|
||||
});
|
||||
|
||||
function renderTabs() {
|
||||
E.tabs.textContent = '';
|
||||
for (const t of state.tabs) {
|
||||
@@ -351,6 +381,7 @@
|
||||
el.className = 'tab' + (t.key === state.activeKey ? ' active' : '') + (t.dirty ? ' dirty' : '');
|
||||
el.dataset.key = t.key;
|
||||
el.title = (t.title || 'Sem título') + EXT[t.format] + (t.dirty ? ' — não salvo' : '');
|
||||
el.draggable = true;
|
||||
const label = document.createElement('span');
|
||||
label.className = 'tab-label';
|
||||
label.textContent = (t.title || 'Sem título') + EXT[t.format];
|
||||
@@ -371,6 +402,38 @@
|
||||
else activateTab(t.key);
|
||||
});
|
||||
el.addEventListener('auxclick', (e) => { if (e.button === 1) closeTab(t.key); });
|
||||
el.addEventListener('dragstart', (e) => {
|
||||
if (e.target === close) { e.preventDefault(); return; }
|
||||
dragTabKey = t.key;
|
||||
el.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
try { e.dataTransfer.setData('text/plain', t.key); } catch { /* ignore */ }
|
||||
});
|
||||
el.addEventListener('dragend', () => {
|
||||
dragTabKey = null;
|
||||
el.classList.remove('dragging');
|
||||
clearTabDropMarks();
|
||||
});
|
||||
el.addEventListener('dragover', (e) => {
|
||||
if (!dragTabKey || dragTabKey === t.key) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
const r = el.getBoundingClientRect();
|
||||
const before = e.clientX < r.left + r.width / 2;
|
||||
clearTabDropMarks();
|
||||
el.classList.add(before ? 'drop-before' : 'drop-after');
|
||||
});
|
||||
el.addEventListener('dragleave', () => el.classList.remove('drop-before', 'drop-after'));
|
||||
el.addEventListener('drop', (e) => {
|
||||
if (!dragTabKey || dragTabKey === t.key) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const r = el.getBoundingClientRect();
|
||||
moveTab(dragTabKey, t.key, e.clientX < r.left + r.width / 2);
|
||||
dragTabKey = null;
|
||||
clearTabDropMarks();
|
||||
});
|
||||
E.tabs.appendChild(el);
|
||||
}
|
||||
}
|
||||
@@ -476,6 +539,8 @@
|
||||
for (const n of state.notes) {
|
||||
const li = document.createElement('li');
|
||||
li.className = n.id === openId ? 'active' : '';
|
||||
const main = document.createElement('div');
|
||||
main.className = 'n-main';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'n-title';
|
||||
title.textContent = n.title || 'Sem título';
|
||||
@@ -490,8 +555,16 @@
|
||||
if (n.tags && n.tags.length) bits.push(n.tags.map((t) => '#' + t).join(' '));
|
||||
if (state.presence[n.id] && state.presence[n.id].length) bits.push('👥 aberta por outra sessão');
|
||||
sub.textContent = bits.join(' · ');
|
||||
li.appendChild(title);
|
||||
li.appendChild(sub);
|
||||
main.appendChild(title);
|
||||
main.appendChild(sub);
|
||||
li.appendChild(main);
|
||||
const del = document.createElement('button');
|
||||
del.type = 'button';
|
||||
del.className = 'n-del';
|
||||
del.textContent = '🗑';
|
||||
del.title = 'Excluir nota';
|
||||
del.addEventListener('click', (e) => { e.stopPropagation(); deleteNoteById(n); });
|
||||
li.appendChild(del);
|
||||
li.addEventListener('click', () => openNoteTab(n.id));
|
||||
E.notesList.appendChild(li);
|
||||
}
|
||||
@@ -1707,20 +1780,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNote() {
|
||||
const t = activeTab();
|
||||
if (!t || !t.id) { toast('Nenhuma nota aberta.'); return; }
|
||||
async function deleteNoteById(n) {
|
||||
const ok = await confirmModal('Excluir nota?',
|
||||
`A nota <b>${esc(t.title || 'Sem título')}</b> e todo o seu histórico serão apagados.`, 'Excluir');
|
||||
`A nota <b>${esc(n.title || 'Sem título')}</b> e todo o seu histórico serão apagados.`, 'Excluir');
|
||||
if (!ok) return;
|
||||
try {
|
||||
await api('DELETE', '/api/notes/' + t.id);
|
||||
await api('DELETE', '/api/notes/' + n.id);
|
||||
toast('Nota excluída.');
|
||||
const idx = state.tabs.indexOf(t);
|
||||
state.tabs.splice(idx, 1);
|
||||
sendPresence();
|
||||
if (!state.tabs.length) addTab(newTab());
|
||||
else activateTab(state.tabs[Math.min(idx, state.tabs.length - 1)].key);
|
||||
const tab = state.tabs.find((t) => t.id === n.id);
|
||||
if (tab) {
|
||||
const idx = state.tabs.indexOf(tab);
|
||||
state.tabs.splice(idx, 1);
|
||||
sendPresence();
|
||||
if (!state.tabs.length) addTab(newTab());
|
||||
else if (state.activeKey === tab.key) activateTab(state.tabs[Math.min(idx, state.tabs.length - 1)].key);
|
||||
else renderTabs();
|
||||
persistTabs();
|
||||
}
|
||||
loadNotes().catch(() => {});
|
||||
loadAggregates().catch(() => {});
|
||||
} catch (err) {
|
||||
@@ -1728,6 +1804,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNote() {
|
||||
const t = activeTab();
|
||||
if (!t || !t.id) { toast('Nenhuma nota aberta.'); return; }
|
||||
await deleteNoteById({ id: t.id, title: t.title });
|
||||
}
|
||||
|
||||
function exportNote() {
|
||||
const t = activeTab();
|
||||
if (!t || !t.id) { toast('Salve a nota antes de baixar.'); return; }
|
||||
@@ -1847,6 +1929,7 @@
|
||||
E.btnSave.addEventListener('click', () => save());
|
||||
E.btnExport.addEventListener('click', exportNote);
|
||||
$('#btn-attach').addEventListener('click', () => openFiles(true));
|
||||
$('#btn-files').addEventListener('click', () => openFiles(false));
|
||||
E.btnVersions.addEventListener('click', () => toggleVersionsPanel());
|
||||
E.btnSidebar.addEventListener('click', toggleSidebar);
|
||||
|
||||
|
||||
Reference in new issue
Block a user