diff --git a/web/css/app.css b/web/css/app.css
index bb3b851..f600b9b 100644
--- a/web/css/app.css
+++ b/web/css/app.css
@@ -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;
diff --git a/web/index.html b/web/index.html
index 30b8e96..bfd2283 100644
--- a/web/index.html
+++ b/web/index.html
@@ -224,6 +224,7 @@
+
diff --git a/web/js/app.js b/web/js/app.js
index 55cd322..57ac047 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -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 ${esc(t.title || 'Sem tÃtulo')} e todo o seu histórico serão apagados.`, 'Excluir');
+ `A nota ${esc(n.title || 'Sem tÃtulo')} 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);
diff --git a/web/sw.js b/web/sw.js
index c71a53c..7653c6b 100644
--- a/web/sw.js
+++ b/web/sw.js
@@ -1,7 +1,7 @@
/* Service Worker do Notas — caches versionados.
* BUMP de CACHE em todo deploy que mude shell/assets (o activate limpa os antigos).
*/
-const CACHE = 'notas-shell-v14';
+const CACHE = 'notas-shell-v15';
const SHELL = [
'/',
'/css/app.css',