verstak/internal/gui/index.html.go

180 lines
13 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 is the GUI frontend served inline.
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>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#1a1a2e;color:#ccc;height:100vh;display:flex;overflow:hidden}
#sb{width:260px;min-width:200px;background:#16213e;border-right:1px solid #0f3460;display:flex;flex-direction:column;overflow:hidden}
#sb-hdr{padding:14px 16px;font-size:16px;font-weight:700;color:#e94560;border-bottom:1px solid #0f3460;display:flex;justify-content:space-between;align-items:center}
#sb-hdr button{background:#0f3460;color:#ccc;border:1px solid #1a4080;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:11px}
#sb-hdr button:hover{background:#1a4080;color:#fff}
#tree{flex:1;overflow-y:auto;padding:6px 0}
.ti{padding:5px 14px;cursor:pointer;display:flex;align-items:center;gap:6px;font-size:13px;border-radius:3px;margin:0 4px}
.ti:hover{background:#0f3460}
.ti.sel{background:#1a4080;color:#fff}
.ti .ic{width:14px;color:#e94560;text-align:center;font-size:13px}
.empty{padding:40px 14px;color:#555;font-size:13px;text-align:center}
#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
#mh{padding:10px 20px;border-bottom:1px solid #0f3460;display:flex;justify-content:space-between;align-items:center;gap:10px}
#mh h1{font-size:17px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#mh .acts{display:flex;gap:5px;flex-shrink:0}
#mh button{background:#0f3460;color:#ccc;border:1px solid #1a4080;border-radius:4px;padding:5px 11px;cursor:pointer;font-size:12px}
#mh button:hover{background:#1a4080;color:#fff}
#mh button.p{background:#e94560;border-color:#e94560}
#mh button.p:hover{background:#c73652}
#srch{padding:6px 20px;border-bottom:1px solid #0f3460}
#srch input{width:100%;padding:7px 12px;background:#0f0f23;color:#ccc;border:1px solid #0f3460;border-radius:5px;font-size:13px;outline:none}
#srch input:focus{border-color:#e94560}
#tabs{display:flex;padding:0 20px;border-bottom:1px solid #0f3460;gap:0;flex-shrink:0}
#tabs .t{padding:9px 16px;cursor:pointer;font-size:13px;color:#666;border-bottom:2px solid transparent}
#tabs .t:hover{color:#ddd}
#tabs .t.a{color:#e94560;border-bottom-color:#e94560}
#cnt{flex:1;overflow-y:auto;padding:20px}
.cg{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
.card{background:#16213e;border:1px solid #0f3460;border-radius:7px;padding:14px;cursor:pointer;transition:border-color .15s}
.card:hover{border-color:#e94560}
.card .ct{font-weight:600;margin-bottom:4px}
.card .cy{font-size:11px;color:#777;text-transform:uppercase}
.card .cm{font-size:11px;color:#555;margin-top:6px}
.bc{font-size:12px;color:#555;margin-bottom:10px}
#ed{display:none;position:fixed;inset:0;background:#1a1a2e;z-index:90;flex-direction:column}
#ed-hdr{padding:10px 20px;border-bottom:1px solid #0f3460;display:flex;justify-content:space-between;align-items:center}
#ed-hdr span{font-size:15px;font-weight:600}
#ed button{background:#0f3460;color:#ccc;border:1px solid #1a4080;border-radius:4px;padding:5px 12px;cursor:pointer;font-size:12px;margin-left:6px}
#ed button.p{background:#e94560;border-color:#e94560}
#ed-ta{flex:1;background:#0f0f23;color:#ccc;border:none;padding:20px;font-family:Consolas,monospace;font-size:13px;line-height:1.7;resize:none;outline:none}
.mo{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center}
.mo.on{display:flex}
.md{background:#16213e;border:1px solid #0f3460;border-radius:10px;padding:22px;width:400px;max-width:90vw}
.md h3{margin-bottom:14px;font-size:15px}
.md label{display:block;font-size:11px;color:#777;margin-bottom:3px;margin-top:10px}
.md input,.md select{width:100%;padding:7px 10px;background:#0f0f23;color:#ccc;border:1px solid #1a4080;border-radius:4px;font-size:13px;outline:none}
.md input:focus{border-color:#e94560}
.md .ma{display:flex;gap:8px;justify-content:flex-end;margin-top:18px}
.md button{padding:7px 14px;border-radius:4px;border:1px solid #1a4080;background:#0f3460;color:#ccc;cursor:pointer}
.md button.p{background:#e94560;border-color:#e94560}
#sr-res{display:none;position:absolute;background:#16213e;border:1px solid #0f3460;border-radius:6px;width:350px;max-height:300px;overflow-y:auto;z-index:50;box-shadow:0 4px 20px rgba(0,0,0,.4)}
#sr-res .sri{padding:8px 12px;cursor:pointer;font-size:13px;border-bottom:1px solid #0f3460}
#sr-res .sri:last-child{border-bottom:none}
#sr-res .sri:hover{background:#0f3460}
#sr-res .sri .srt{font-size:10px;color:#e94560;text-transform:uppercase}
ul{list-style:none}
</style>
</head>
<body>
<div id="sb">
<div id="sb-hdr"><span>&#9874; ВЕРСТАК</span><button onclick="showCM()">+</button></div>
<div id="tree"><div class="empty">Загрузка...</div></div>
</div>
<div id="main">
<div id="mh">
<h1 id="pt">Верстак</h1>
<div class="acts">
<button onclick="showCNM()">+ Заметка</button>
<button class="p" onclick="showCM()">+ Дело</button>
</div>
</div>
<div id="srch" style="position:relative">
<input id="si" placeholder="Поиск по делам..." autocomplete="off" oninput="handleSR(this.value)">
<div id="sr-res" style="top:38px;left:0"></div>
</div>
<div id="tabs">
<div class="t a" data-t="ov" onclick="switchTab(this)">Обзор</div>
<div class="t" data-t="nt" onclick="switchTab(this)">Заметки</div>
<div class="t" data-t="fl" onclick="switchTab(this)">Файлы</div>
</div>
<div id="cnt"><div class="empty">Выберите дело или создайте новое</div></div>
</div>
<div id="ed">
<div id="ed-hdr"><span id="et">Редактор</span><div><button onclick="closeED()">Закрыть</button><button class="p" onclick="saveNT()">Сохранить</button></div></div>
<textarea id="ed-ta" placeholder="Пишите в Markdown..."></textarea>
</div>
<div class="mo" id="cm">
<div class="md"><h3>Новое дело</h3>
<label>Тип</label><select id="ct"><option value="case">Дело</option><option value="folder">Папка</option><option value="space">Пространство</option><option value="recipe">Рецепт</option></select>
<label>Название</label><input id="cn" placeholder="Название...">
<div class="ma"><button onclick="closeCM()">Отмена</button><button class="p" onclick="submitCM()">Создать</button></div></div>
</div>
<div class="mo" id="cnm">
<div class="md"><h3>Новая заметка</h3>
<label>Название</label><input id="cnn" placeholder="Название...">
<div class="ma"><button onclick="closeCNM()">Отмена</button><button class="p" onclick="submitCNM()">Создать</button></div></div>
</div>
<script>
const A='';let cur='',editId='';
async function api(p,o){const r=await fetch(A+p,{headers:{'Content-Type':'application/json'},...o});if(!r.ok)throw Error(r.status);return r.json()}
function esc(s){let d=document.createElement('div');d.textContent=s;return d.innerHTML}
const ni={case:'&#9670;',folder:'&#9656;',space:'&#9678;',note:'&#9997;',recipe:'&#9672;',document:'&#128196;',file:'&#128206;',action:'&#9889;',secret:'&#128274;',worklog:'&#9201;',link:'&#128279;'};
async function loadTree(){
try{const items=await api('/api/nodes');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':'')+'" onclick="selN(\''+n.id+'\')"><span class="ic">'+(ni[n.type]||'&#9670;')+'</span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(n.title)+'</span></div></li>'}
h+='</ul>';t.innerHTML=h}
catch(e){document.getElementById('tree').innerHTML='<div class="empty">Ошибка</div>'}
}
async function selN(id){
cur=id;document.querySelectorAll('.ti').forEach(e=>e.classList.remove('sel'));
const d=event.currentTarget;if(d)d.classList.add('sel');
try{const d=await api('/api/nodes/'+id);document.getElementById('pt').textContent=d.node.title;renderOV(d)}
catch(e){document.getElementById('cnt').innerHTML='<div class="empty">Ошибка</div>'}
}
function renderOV(d){const ch=d.children||[],fl=d.files||[];
let h='<div class="bc">'+d.node.type+'</div>';
if(ch.length||fl.length){h+='<div class="cg">';
for(const c of ch){h+='<div class="card" onclick="selN(\''+c.id+'\')"><div class="ct">'+esc(c.title)+'</div><div class="cy">'+c.type+'</div></div>'}
for(const f of fl){h+='<div class="card"><div class="ct">'+esc(f.filename)+'</div><div class="cy">'+(f.mime||'file')+'</div><div class="cm">'+fsz(f.size)+'</div></div>'}
h+='</div>'}else{h+='<div class="empty" style="margin-top:60px">Пусто. Добавьте заметку или файл.</div>'}
document.getElementById('cnt').innerHTML=h
}
function fsz(b){if(b<1024)return b+' Б';if(b<1048576)return(b/1024).toFixed(1)+' КБ';return(b/1048576).toFixed(1)+' МБ'}
function switchTab(el){
document.querySelectorAll('#tabs .t').forEach(e=>e.classList.remove('a'));el.classList.add('a');
const t=el.dataset.t;if(t==='nt')loadNT();else if(t==='fl')loadFL();else if(cur)selN(cur);
}
async function loadNT(){
if(!cur){document.getElementById('cnt').innerHTML='<div class="empty">Выберите дело</div>';return}
const d=await api('/api/nodes/'+cur);const ns=(d.children||[]).filter(c=>c.type==='note');
if(!ns.length){document.getElementById('cnt').innerHTML='<div class="empty">Нет заметок</div>';return}
let h='<div class="cg">';for(const n of ns){h+='<div class="card" onclick="openNT(\''+n.id+'\')"><div class="ct">'+esc(n.title)+'</div><div class="cy">заметка</div></div>'}h+='</div>';
document.getElementById('cnt').innerHTML=h
}
async function loadFL(){
if(!cur){document.getElementById('cnt').innerHTML='<div class="empty">Выберите дело</div>';return}
const d=(await api('/api/nodes/'+cur)).files||[];
if(!d.length){document.getElementById('cnt').innerHTML='<div class="empty">Нет файлов</div>';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||'file')+'</div><div class="cm">'+fsz(f.size)+'</div></div>'}h+='</div>';
document.getElementById('cnt').innerHTML=h
}
async function openNT(id){editId=id;
try{const d=await api('/api/notes/'+id);document.getElementById('et').textContent='Заметка';document.getElementById('ed-ta').value=d.content||'';document.getElementById('ed').style.display='flex';document.getElementById('ed-ta').focus()}
catch(e){alert('Ошибка: '+e.message)}
}
function closeED(){document.getElementById('ed').style.display='none';editId=''}
async function saveNT(){if(!editId)return;try{await api('/api/notes/'+editId,{method:'PUT',body:JSON.stringify({content:document.getElementById('ed-ta').value})});closeED();if(cur)selN(cur)}catch(e){alert('Ошибка: '+e.message)}}
function showCM(){document.getElementById('cm').classList.add('on');setTimeout(()=>document.getElementById('cn').focus(),50)}
function closeCM(){document.getElementById('cm').classList.remove('on');document.getElementById('cn').value=''}
async function submitCM(){const t=document.getElementById('ct').value,title=document.getElementById('cn').value.trim();if(!title)return;try{await api('/api/nodes',{method:'POST',body:JSON.stringify({parent_id:cur,type:t,title})});closeCM();loadTree();if(cur)selN(cur)}catch(e){alert('Ошибка: '+e.message)}}
function showCNM(){document.getElementById('cnm').classList.add('on');setTimeout(()=>document.getElementById('cnn').focus(),50)}
function closeCNM(){document.getElementById('cnm').classList.remove('on');document.getElementById('cnn').value=''}
async function submitCNM(){const title=document.getElementById('cnn').value.trim();if(!title)return;try{const n=await api('/api/notes/'+(cur||''),{method:'POST',body:JSON.stringify({parent_id:cur,title})});closeCNM();loadTree();selN(n.id)}catch(e){alert('Ошибка: '+e.message)}}
let sT=null;
async function handleSR(q){clearTimeout(sT);const b=document.getElementById('sr-res');
if(!q||q.length<2){b.style.display='none';return}
sT=setTimeout(async()=>{try{const items=await api('/api/nodes');
const hits=items.filter(n=>n.title.toLowerCase().includes(q.toLowerCase()));
if(!hits.length){b.style.display='none';return}
let h='';for(const r of hits){h+='<div class="sri" onclick="b.style.display=\'none\';selN(\''+r.id+'\')"><div class="srt">'+r.type+'</div><div>'+esc(r.title)+'</div></div>'}
b.innerHTML=h;b.style.display='block'
}catch(e){b.style.display='none'}},200)}
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeED();closeCM();closeCNM();document.getElementById('sr-res').style.display='none'}});
loadTree();
</script>
</body>
</html>`