verstak/internal/gui/index.html.go

423 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package gui
// indexHTML — SPA-фронтенд Верстака (тёмная тема, левое дерево, вкладки).
// Весь UI в одном файле: CSS + HTML + JS. Wails-совместимость: структура
// готова к упаковке (нет external JS/CSS, fetch к /api/* через origin).
const indexHTML = `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Верстак</title>
<style>
/* ── reset & tokens ── */
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#13131f;--surface:#1b1b2e;--surface2:#24243a;
--border:#2a2a45;--border-hi:#3d3d5c;
--text:#e4e4ef;--text2:#b0b0c8;--text3:#8888a4;
--accent:#e94560;--accent2:#c73652;
--ok:#4caf50;--warn:#ff9800;
--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
--mono:"Fira Code","Consolas","SF Mono",monospace;
--sb-w:260px;--cnt-w:720px;
}
html,body{height:100%;overflow:hidden;font-family:var(--font);background:var(--bg);color:var(--text)}
button{font-family:inherit;cursor:pointer;border:none;font-size:inherit}
input,textarea,select{font-family:inherit;font-size:inherit;outline:none}
/* ── layout ── */
#app{display:flex;height:100vh}
#sb{width:var(--sb-w);min-width:200px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;flex-shrink:0}
#sb-hdr{padding:14px 16px;font-size:15px;font-weight:700;color:var(--accent);letter-spacing:.5px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;height:52px}
#sb-sections{padding:6px 0;border-bottom:1px solid var(--border)}
.sb-sect{padding:8px 16px 4px;font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:1px;font-weight:600}
.sb-item{padding:6px 14px 6px 22px;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);border-radius:3px;margin:1px 6px}
.sb-item:hover{background:var(--border);color:var(--text)}
.sb-item.sel{background:var(--border-hi);color:var(--text);font-weight:500}
.sb-item .ic{width:16px;text-align:center;font-size:13px}
.sb-item .badge{margin-left:auto;font-size:10px;background:var(--accent);color:#fff;padding:1px 6px;border-radius:10px;min-width:18px;text-align:center}
#tree{flex:1;overflow-y:auto;padding:6px 0}
#tree ul{list-style:none}
.ti{padding:5px 14px;cursor:pointer;display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text2);border-radius:3px;margin:0 4px}
.ti:hover{background:var(--border);color:var(--text)}
.ti.sel{background:var(--border-hi);color:var(--text)}
.ti .ic{width:14px;color:var(--accent);text-align:center;font-size:13px}
.ti .ar{width:12px;font-size:10px;color:var(--text3)}
.tc{margin-left:20px}
.empty{padding:40px 14px;color:var(--text3);font-size:13px;text-align:center}
/* ── main panel ── */
#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;background:var(--bg)}
#mh{padding:12px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:12px;height:52px}
#mh h1{font-size:17px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#mh .actions{display:flex;gap:6px;flex-shrink:0}
.btn{background:var(--surface);color:var(--text2);border:1px solid var(--border);border-radius:5px;padding:6px 13px;font-size:12px;display:inline-flex;align-items:center;gap:5px}
.btn:hover{background:var(--border);color:var(--text)}
.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn.primary:hover{background:var(--accent2)}
.btn.dd{position:relative}
.btn.dd .menu{display:none;position:absolute;top:calc(100% + 4px);right:0;background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:4px;min-width:180px;z-index:80;box-shadow:0 8px 30px rgba(0,0,0,.5)}
.btn.dd.open .menu{display:block}
.menu-item{padding:8px 14px;cursor:pointer;font-size:13px;color:var(--text2);border-radius:3px;display:flex;align-items:center;gap:8px}
.menu-item:hover{background:var(--border);color:var(--text)}
.menu-item .mi-icon{width:16px;text-align:center;color:var(--accent)}
.menu-sep{height:1px;background:var(--border);margin:4px 8px}
/* ── search ── */
#srch{padding:6px 24px;border-bottom:1px solid var(--border);position:relative}
#srch input{width:100%;padding:8px 14px;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:13px}
#srch input:focus{border-color:var(--accent)}
#sr-res{display:none;position:absolute;top:46px;left:24px;right:24px;background:var(--surface2);border:1px solid var(--border);border-radius:8px;max-height:320px;overflow-y:auto;z-index:60;box-shadow:0 8px 30px rgba(0,0,0,.5)}
#sr-res:has(.sri){display:block}
.sri{padding:9px 14px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:10px}
.sri:last-child{border-bottom:none}
.sri:hover{background:var(--border)}
.sri .srt{font-size:10px;color:var(--accent);text-transform:uppercase;min-width:50px;font-weight:600}
.sri .sr-title{color:var(--text)}
/* ── tabs ── */
#tabs{display:flex;padding:0 24px;border-bottom:1px solid var(--border);gap:0;flex-shrink:0}
.tab{padding:10px 18px;cursor:pointer;font-size:13px;color:var(--text3);border-bottom:2px solid transparent;white-space:nowrap}
.tab:hover{color:var(--text)}
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
/* ── content ── */
#cnt{flex:1;overflow-y:auto;padding:24px}
#cnt-in{max-width:var(--cnt-w);margin:0 auto;width:100%}
/* ── dashboard ── */
.dash{margin-bottom:32px}
.dash h2{font-size:15px;font-weight:600;margin-bottom:12px;color:var(--text)}
.dash-section{margin-bottom:24px}
.dash-section-title{font-size:12px;color:var(--text3);text-transform:uppercase;letter-spacing:.8px;margin-bottom:8px;font-weight:600}
.dash-desc{font-size:14px;color:var(--text2);line-height:1.5;margin-bottom:12px}
.qa-grid{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
.qa-btn{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:8px 14px;font-size:12px;color:var(--text2);display:flex;align-items:center;gap:6px}
.qa-btn:hover{border-color:var(--accent);color:var(--text)}
.cg{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px}
.card{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:14px;cursor:pointer;transition:border-color .15s,background .15s}
.card:hover{border-color:var(--accent);background:var(--surface2)}
.card .ct{font-weight:600;margin-bottom:4px;font-size:14px;color:var(--text)}
.card .cy{font-size:11px;color:var(--text3);text-transform:uppercase;letter-spacing:.5px}
.card .cm{font-size:11px;color:var(--text3);margin-top:6px}
.card .cc{font-size:12px;color:var(--text2);margin-top:8px;line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}
.bc{font-size:12px;color:var(--text3);margin-bottom:12px}
.tag{display:inline-block;font-size:10px;padding:2px 7px;border-radius:4px;background:var(--border-hi);color:var(--text2);margin-right:4px;margin-bottom:4px}
.tag.accent{background:var(--accent);color:#fff}
/* ── editor ── */
#ed{display:none;position:fixed;inset:0;background:var(--bg);z-index:90;flex-direction:column}
#ed-hdr{padding:12px 24px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;height:52px}
#ed-crumb{font-size:12px;color:var(--text3)}
#ed-title{font-size:15px;font-weight:600;margin-top:2px}
#ed-actions{display:flex;gap:8px}
#ed-ta{flex:1;background:var(--bg);color:var(--text);border:none;padding:32px calc((100% - var(--cnt-w)) / 2);font-family:var(--mono);font-size:14px;line-height:1.8;resize:none}
#ed-ta:focus{outline:none}
/* ── modals ── */
.mo{display:none;position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:100;align-items:center;justify-content:center}
.mo.on{display:flex}
.md{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;width:420px;max-width:90vw;box-shadow:0 12px 40px rgba(0,0,0,.5)}
.md h3{margin-bottom:16px;font-size:16px}
.md label{display:block;font-size:12px;color:var(--text3);margin-bottom:4px;margin-top:12px;font-weight:500}
.md input,.md select,.md textarea{width:100%;padding:8px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:5px;font-size:14px}
.md input:focus,.md select:focus{border-color:var(--accent)}
.md textarea{min-height:80px;resize:vertical;font-family:var(--mono);font-size:13px}
.md .ma{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}
.md .btn{padding:8px 16px}
/* ── scrollbar ── */
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border-hi);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--text3)}
</style>
</head>
<body>
<div id="app">
<!-- ══ sidebar ══ -->
<div id="sb">
<div id="sb-hdr"><span>&#9874; ВЕРСТАК</span><button class="btn primary" onclick="showAdd()" style="font-size:11px;padding:4px 10px">+ Добавить</button></div>
<div id="sb-sections">
<div class="sb-sect">Навигация</div>
<div class="sb-item sel" data-g="today" onclick="goSec(this)"><span class="ic">&#9724;</span> Сегодня</div>
<div class="sb-item" data-g="inbox" onclick="goSec(this)"><span class="ic">&#9776;</span> Неразобранное <span class="badge" style="display:none">0</span></div>
<div class="sb-sect">Дела</div>
<div class="sb-item" data-g="clients" onclick="goSec(this)"><span class="ic">&#9874;</span> Клиенты</div>
<div class="sb-item" data-g="projects" onclick="goSec(this)"><span class="ic">&#9881;</span> Проекты</div>
<div class="sb-item" data-g="recipes" onclick="goSec(this)"><span class="ic">&#9886;</span> Рецепты</div>
<div class="sb-item" data-g="docs" onclick="goSec(this)"><span class="ic">&#128196;</span> Документы</div>
<div class="sb-item" data-g="archive" onclick="goSec(this)"><span class="ic">&#128450;</span> Архив</div>
</div>
<div id="tree"><div class="empty">Дерево дел<br><small style="color:var(--text3)">появится автоматически</small></div></div>
</div>
<!-- ══ main ══ -->
<div id="main">
<div id="mh">
<h1 id="pt">Верстак</h1>
<div class="actions" id="mh-actions">
<button class="btn dd" id="add-btn" onclick="toggleAdd(event)">
+ Добавить &#9662;
<div class="menu" id="add-menu">
<div class="menu-item" onclick="doAdd('case')"><span class="mi-icon">&#9670;</span> Дело</div>
<div class="menu-item" onclick="doAdd('note')"><span class="mi-icon">&#9997;</span> Заметка</div>
<div class="menu-item" onclick="doAdd('file')"><span class="mi-icon">&#128196;</span> Файл</div>
<div class="menu-sep"></div>
<div class="menu-item" onclick="doAdd('action')"><span class="mi-icon">&#9889;</span> Действие</div>
<div class="menu-item" onclick="doAdd('worklog')"><span class="mi-icon">&#9201;</span> Запись работы</div>
</div>
</button>
</div>
</div>
<div id="srch">
<input id="si" placeholder="Поиск по делам, заметкам, файлам..." autocomplete="off" oninput="handleSR(this.value)">
<div id="sr-res"></div>
</div>
<div id="tabs">
<div class="tab active" data-t="ov" onclick="switchTab(this)">Обзор</div>
<div class="tab" data-t="notes" onclick="switchTab(this)">Заметки</div>
<div class="tab" data-t="files" onclick="switchTab(this)">Файлы</div>
<div class="tab" data-t="actions" onclick="switchTab(this)">Действия</div>
<div class="tab" data-t="worklog" onclick="switchTab(this)">Журнал</div>
<div class="tab" data-t="activity" onclick="switchTab(this)">Активность</div>
</div>
<div id="cnt"><div id="cnt-in"><div class="empty">Выберите раздел или дело</div></div></div>
</div>
</div>
<!-- ══ editor ══ -->
<div id="ed">
<div id="ed-hdr">
<div><div id="ed-crumb">Заметка</div><div id="ed-title">Редактор</div></div>
<div id="ed-actions">
<button class="btn" onclick="closeED()">Закрыть</button>
<button class="btn primary" onclick="saveNT()">Сохранить</button>
</div>
</div>
<textarea id="ed-ta" placeholder="Пишите в Markdown..."></textarea>
</div>
<!-- ══ modals ══ -->
<div class="mo" id="m-node">
<div class="md"><h3>Новое дело</h3>
<label for="mn-type">Тип</label><select id="mn-type"><option value="case">◇ Дело</option><option value="folder">▸ Папка</option><option value="space">◎ Пространство</option><option value="recipe">◈ Рецепт</option></select>
<label for="mn-title">Название</label><input id="mn-title" placeholder="Название...">
<div class="ma"><button class="btn" onclick="closeM('m-node')">Отмена</button><button class="btn primary" onclick="submitNode()">Создать</button></div></div>
</div>
<div class="mo" id="m-note">
<div class="md"><h3>Новая заметка</h3>
<label for="mn2-title">Название</label><input id="mn2-title" placeholder="Название заметки...">
<div class="ma"><button class="btn" onclick="closeM('m-note')">Отмена</button><button class="btn primary" onclick="submitNote()">Создать</button></div></div>
</div>
<div class="mo" id="m-file">
<div class="md"><h3>Файл</h3>
<label style="color:var(--text3);font-size:13px;line-height:1.5">MVP: добавление файлов через интерфейс пока недоступно. Используйте CLI:<br><code style="background:var(--bg);padding:2px 6px;border-radius:3px;font-size:12px;color:var(--accent)">verstak file add &lt;path&gt; --node &lt;id&gt;</code></label>
<div class="ma"><button class="btn primary" onclick="closeM('m-file')">Понятно</button></div></div>
</div>
<div class="mo" id="m-action">
<div class="md"><h3>Действие</h3>
<label style="color:var(--text3);font-size:13px;line-height:1.5">MVP: создание действий пока недоступно в GUI. Используйте CLI:<br><code style="background:var(--bg);padding:2px 6px;border-radius:3px;font-size:12px;color:var(--accent)">verstak action add</code></label>
<div class="ma"><button class="btn primary" onclick="closeM('m-action')">Понятно</button></div></div>
</div>
<div class="mo" id="m-worklog">
<div class="md"><h3>Запись работы</h3>
<label for="mw-hours">Время (мин)</label><input id="mw-hours" type="number" placeholder="120" min="1">
<label for="mw-text">Описание</label><textarea id="mw-text" placeholder="Что сделано..."></textarea>
<label><input type="checkbox" id="mw-approx" checked style="width:auto;margin-right:6px"> примерно</label>
<div class="ma"><button class="btn" onclick="closeM('m-worklog')">Отмена</button><button class="btn primary" onclick="submitWorklog()">Записать</button></div></div>
</div>
<script>
/* ── type labels ── */
const TL={case:'Дело',folder:'Папка',space:'Пространство',note:'Заметка',document:'Документ',file:'Файл',recipe:'Рецепт',action:'Действие',secret:'Секрет',worklog:'Журнал',link:'Ссылка'};
const NI={case:'&#9670;',folder:'&#9656;',space:'&#9678;',note:'&#9997;',recipe:'&#9672;',document:'&#128196;',file:'&#128206;',action:'&#9889;',secret:'&#128274;',worklog:'&#9201;',link:'&#128279;'};
/* ── state ── */
let cur='',sec='today',tab='ov';
/* ── api ── */
async function api(p,o){
const r=await fetch(location.origin+p,{headers:{'Content-Type':'application/json'},...o});
if(!r.ok)throw Error(r.status+' '+r.statusText);
return r.json();
}
function esc(s){let d=document.createElement('div');d.textContent=s;return d.innerHTML}
/* ── sidebar navigation ── */
function goSec(el){
document.querySelectorAll('.sb-item').forEach(e=>e.classList.remove('sel'));
el.classList.add('sel');sec=el.dataset.g;
const g=sec;
document.getElementById('pt').textContent=el.childNodes[2]?.textContent?.trim()||sectTitle(g);
if(g==='today')renderToday();else if(g==='inbox')renderInbox();
else loadSection(g);
}
function sectTitle(g){
return{clients:'Клиенты',projects:'Проекты',recipes:'Рецепты',docs:'Документы',archive:'Архив'}[g]||g;
}
async function loadSection(g){
map={'case':'clients','folder':'case'}; // map section to type filter
// For now just load all roots — filtering comes later
const items=await api('/api/nodes');
renderTree(items);
}
function renderTree(items){
const t=document.getElementById('tree');
if(!items.length){t.innerHTML='<div class="empty">Нет дел в разделе</div>';return}
let h='<ul>';for(const n of items){
h+='<li><div class="ti'+(cur===n.id?' sel':'')+'" data-id="'+n.id+'" onclick="selN(this)"><span class="ic">'+(NI[n.type]||'&#9670;')+'</span><span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(n.title)+'</span></div></li>';
}h+='</ul>';t.innerHTML=h;
}
/* ── node selection ── */
async function selN(el){
cur=el.dataset.id;document.querySelectorAll('.ti').forEach(e=>e.classList.remove('sel'));el.classList.add('sel');
try{
const d=await api('/api/nodes/'+cur);
document.getElementById('pt').textContent=d.node.title;
renderDash(d);
}catch(e){E('Ошибка загрузки')}
}
/* ═══ DASHBOARD (Обзор) ═══ */
function renderDash(d){
const n=d.node,ch=d.children||[],fl=d.files||[],notes=ch.filter(c=>c.type==='note'),sub=ch.filter(c=>c.type!=='note');
let h='';
// meta/desc row
if(n.type){h+='<div style="margin-bottom:16px"><span class="tag accent">'+TL[n.type]+'</span>';if(n.status)h+='<span class="tag">'+n.status+'</span>';h+='</div>'}
// quick actions
h+='<div class="dash-section"><div class="dash-section-title">Быстрые действия</div><div class="qa-grid">';
h+='<button class="qa-btn" onclick="openM(\'m-note\')">&#9997; Новая заметка</button>';
h+='<button class="qa-btn" onclick="openM(\'m-file\')">&#128196; Добавить файл</button>';
h+='<button class="qa-btn" onclick="openM(\'m-action\')">&#9889; Действие</button>';
h+='<button class="qa-btn" onclick="openM(\'m-worklog\')">&#9201; Записать время</button>';
h+='</div></div>';
// sub-items
if(sub.length){h+='<div class="dash-section"><div class="dash-section-title">Вложенные ('+sub.length+')</div><div class="cg">';
for(const c of sub)h+='<div class="card" onclick="selN(this)" data-id="'+c.id+'"><div class="ct">'+esc(c.title)+'</div><div class="cy">'+TL[c.type]+'</div></div>';
h+='</div></div>'}
// notes
if(notes.length){h+='<div class="dash-section"><div class="dash-section-title">Заметки ('+notes.length+')</div><div class="cg">';
for(const c of notes)h+='<div class="card" onclick="openNT(\''+c.id+'\')"><div class="ct">'+esc(c.title)+'</div><div class="cy">Заметка</div></div>';
h+='</div></div>'}
// files
if(fl.length){h+='<div class="dash-section"><div class="dash-section-title">Файлы ('+fl.length+')</div><div class="cg">';
for(const f of fl)h+='<div class="card"><div class="ct">'+esc(f.filename)+'</div><div class="cy">'+(TL['file']||f.mime||'Файл')+'</div><div class="cm">'+fsz(f.size)+'</div></div>';
h+='</div></div>'}
// empty
if(!sub.length&&!notes.length&&!fl.length){
h+='<div class="empty" style="margin-top:40px">Пусто. Начните с заметки или вложенного дела.</div>';
}
G('cnt-in').innerHTML=h;tab='ov';setTabs();
}
/* ═══ TABS ═══ */
function setTabs(){
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active',t.dataset.t===tab));
}
function switchTab(el){
tab=el.dataset.t;setTabs();
if(!cur){E('Выберите деле в дереве слева');return}
if(tab==='ov')selN(document.querySelector('.ti.sel')||{dataset:{id:cur},classList:{add(){}}});
else if(tab==='notes')loadNotesTab();
else if(tab==='files')loadFilesTab();
else if(tab==='actions')E('Действия — в разработке');
else if(tab==='worklog')E('Журнал — в разработке');
else if(tab==='activity')E('Активность — в разработке');
}
function E(msg){G('cnt-in').innerHTML='<div class="empty" style="margin-top:60px">'+msg+'</div>'}
async function loadNotesTab(){
if(!cur){E('Выберите дело');return}
const d=await api('/api/nodes/'+cur);
const ns=(d.children||[]).filter(c=>c.type==='note');
if(!ns.length){E('Нет заметок. <button class="btn primary" onclick="openM(\'m-note\')" style="margin-top:8px">Создать</button>');return}
let h='<div class="cg">';
for(const n of ns)h+='<div class="card" onclick="openNT(\''+n.id+'\')"><div class="ct">&#9997; '+esc(n.title)+'</div><div class="cy">Заметка</div></div>';
h+='</div>';G('cnt-in').innerHTML=h;
}
async function loadFilesTab(){
if(!cur){E('Выберите дело');return}
const d=(await api('/api/nodes/'+cur)).files||[];
if(!d.length){E('Нет файлов. <button class="btn primary" onclick="openM(\'m-file\')" style="margin-top:8px">Добавить</button>');return}
let h='<div class="cg">';
for(const f of d)h+='<div class="card"><div class="ct">'+esc(f.filename)+'</div><div class="cy">'+(f.mime||'Файл')+'</div><div class="cm">'+fsz(f.size)+'</div></div>';
h+='</div>';G('cnt-in').innerHTML=h;
}
/* ═══ EDITOR ═══ */
let editId='';
async function openNT(id){editId=id;
try{const d=await api('/api/notes/'+id);G('ed-crumb').textContent='Заметка';G('ed-title').textContent='Редактирование';G('ed-ta').value=d.content||'';G('ed').style.display='flex';G('ed-ta').focus()}
catch(e){alert('Ошибка: '+e.message)}
}
function closeED(){G('ed').style.display='none';editId=''}
async function saveNT(){if(!editId)return;try{await api('/api/notes/'+editId,{method:'PUT',body:JSON.stringify({content:G('ed-ta').value})});closeED();if(cur)selN({dataset:{id:cur},classList:{add(){}}})}catch(e){alert('Ошибка: '+e.message)}}
/* ═══ Navigation pages ═══ */
async function renderToday(){
const items=await api('/api/nodes');
let h='<div class="dash"><h2 style="font-size:22px;margin-bottom:4px">&#9724; Сегодня</h2>';
h+='<div style="color:var(--text3);font-size:14px;margin-bottom:24px">'+new Date().toLocaleDateString('ru',{weekday:'long',day:'numeric',month:'long'})+'</div>';
if(items.length){
h+='<div class="dash-section"><div class="dash-section-title">Ваши дела</div><div class="cg">';
for(const n of items.slice(0,6)){h+='<div class="card" data-id="'+n.id+'" onclick="selN(this)"><div class="ct">'+esc(n.title)+'</div><div class="cy">'+TL[n.type]+'</div></div>'}
h+='</div></div>';
} else {h+='<div class="empty">Дел пока нет. Создайте первое!</div>'}
h+='</div>';G('cnt-in').innerHTML=h;
}
async function renderInbox(){
const items=await api('/api/nodes');
let h='<div class="dash"><h2 style="font-size:22px;margin-bottom:4px">&#9776; Неразобранное</h2>';
h+='<div style="color:var(--text3);font-size:14px;margin-bottom:24px">Файлы и заметки без дела</div>';
if(items.length){
h+='<div class="cg">';for(const n of items){h+='<div class="card" data-id="'+n.id+'" onclick="selN(this)"><div class="ct">'+esc(n.title)+'</div><div class="cy">'+TL[n.type]+'</div></div>'}h+='</div>';
} else {h+='<div class="empty">Всё разобрано!</div>'}
h+='</div>';G('cnt-in').innerHTML=h;
}
/* ═══ modals ═══ */
function openM(id){G(id).classList.add('on');setTimeout(()=>{const i=G(id).querySelector('input');if(i)i.focus()},60)}
function closeM(id){G(id).classList.remove('on');G(id).querySelectorAll('input,textarea').forEach(e=>e.value='')}
function doAdd(kind){closeAdd();if(kind==='case')openM('m-node');else if(kind==='note'){if(!cur&&sec==='today'){E('Выберите дело слева для заметки');return}openM('m-note')}else if(kind==='file')openM('m-file');else if(kind==='action')openM('m-action');else if(kind==='worklog')openM('m-worklog')}
async function submitNode(){const t=G('mn-type').value,title=G('mn-title').value.trim();if(!title)return;try{const n=await api('/api/nodes',{method:'POST',body:JSON.stringify({parent_id:cur,type:t,title})});closeM('m-node');renderTree(await api('/api/nodes'));selN({dataset:{id:n.id},classList:{add(){}}})}catch(e){alert('Ошибка: '+e.message)}}
async function submitNote(){const title=G('mn2-title').value.trim();if(!title)return;try{const n=await api('/api/notes/'+(cur||''),{method:'POST',body:JSON.stringify({parent_id:cur,title})});closeM('m-note');renderTree(await api('/api/nodes'));selN({dataset:{id:n.id},classList:{add(){}}})}catch(e){alert('Ошибка: '+e.message)}}
async function submitWorklog(){const mins=parseInt(G('mw-hours').value,10)||0;const text=G('mw-text').value.trim();const approx=G('mw-approw')?.checked?1:0;try{await api('/api/worklog',{method:'POST',body:JSON.stringify({node_id:cur,minutes:mins,summary:text,approximate:approx})});closeM('m-worklog');alert('Записано!')}catch(e){alert('Ошибка: '+e.message)}}
/* ═══ add menu dd ═══ */
function toggleAdd(e){e.stopPropagation();G('add-btn').classList.toggle('open')}
document.addEventListener('click',()=>{G('add-btn').classList.remove('open')});
/* ═══ search ═══ */
let sT=null;
async function handleSR(q){clearTimeout(sT);const b=G('sr-res');
if(!q||q.length<2){b.innerHTML='';return}
sT=setTimeout(async()=>{try{let items=await api('/api/nodes');
const hits=items.filter(n=>n.title.toLowerCase().includes(q.toLowerCase()));
if(!hits.length){b.innerHTML='';return}
let h='';for(const r of hits){h+='<div class="sri" data-id="'+r.id+'" onclick="selN(this);b.innerHTML=\'\'"><span class="srt">'+TL[r.type]+'</span><span class="sr-title">'+esc(r.title)+'</span></div>'}
b.innerHTML=h
}catch(e){b.innerHTML=''}},200);
}
/* ═══ keyboard ═══ */
document.addEventListener('keydown',e=>{
if(e.key==='Escape'){closeED();document.querySelectorAll('.mo.on').forEach(m=>m.classList.remove('on'));G('sr-res').innerHTML=''}
if(e.key==='n'&&(e.ctrlKey||e.metaKey)){e.preventDefault();doAdd('case')}
if(e.key==='s'&&(e.ctrlKey||e.metaKey)&&editId){e.preventDefault();saveNT()}
});
/* ═══ util ═══ */
function G(id){return document.getElementById(id)}
function fsz(b){if(b==null||b===0)return'—';if(b<1024)return b+' Б';if(b<1048576)return(b/1024).toFixed(1)+' КБ';return(b/1048576).toFixed(1)+' МБ'}
/* ═══ init ═══ */
renderToday();
</script>
</body>
</html>`