verstak/internal/gui/static/index.html

260 lines
14 KiB
HTML
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.

<!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:#e0e0e0;height:100vh;display:flex;overflow:hidden}
#sb{width:280px;background:#16213e;border-right:1px solid #0f3460;display:flex;flex-direction:column;overflow:hidden;flex-shrink:0}
#sb-hdr{padding:16px;font-size:18px;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:#e0e0e0;border:1px solid #1a4080;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:12px}
#sb-hdr button:hover{background:#1a4080}
#tree{flex:1;overflow-y:auto;padding:8px 0}
#tree ul{list-style:none}
#tree li{user-select:none}
.ti{padding:6px 16px;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:13px;border-radius:4px;margin:1px 6px}
.ti:hover{background:#0f3460}
.ti.sel{background:#1a4080;color:#fff}
.ti .ic{width:16px;text-align:center;color:#e94560;font-size:14px}
.ti .ar{width:12px;font-size:10px;color:#555}
.tc{margin-left:18px;border-left:1px dashed #0f3460}
#main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
#mh{padding:14px 24px;border-bottom:1px solid #0f3460;display:flex;justify-content:space-between;align-items:center;gap:12px}
#mh h1{font-size:18px;font-weight:600;white-space:overflow:hidden;text-overflow:ellipsis}
#mh .acts{display:flex;gap:6px;flex-shrink:0}
#mh button{background:#0f3460;color:#e0e0e0;border:1px solid #1a4080;border-radius:4px;padding:6px 12px;cursor:pointer;font-size:12px}
#mh button:hover{background:#1a4080}
#mh button.p{background:#e94560;border-color:#e94560;color:#fff}
#mh button.p:hover{background:#c73652}
#tabs{display:flex;padding:0 24px;border-bottom:1px solid #0f3460;flex-shrink:0}
#tabs .t{padding:10px 18px;cursor:pointer;font-size:13px;color:#888;border-bottom:2px solid transparent;white-space:nowrap}
#tabs .t:hover{color:#e0e0e0}
#tabs .t.a{color:#e94560;border-bottom-color:#e94560}
#srch{padding:8px 24px}
#srch input{width:100%;padding:8px 12px;background:#0f0f23;color:#e0e0e0;border:1px solid #0f3460;border-radius:6px;font-size:13px;outline:none}
#srch input:focus{border-color:#e94560}
#sr-res{display:none;position:absolute;top:100px;left:50%;transform:translateX(-50%);width:400px;max-width:90vw;background:#16213e;border:1px solid #0f3460;border-radius:8px;max-height:350px;overflow-y:auto;z-index:60}
#sr-res .sri{padding:10px 14px;cursor:pointer;border-bottom:1px solid #0f3460;font-size:13px}
#sr-res .sri:hover{background:#0f3460}
#sr-res .sri .srt{font-size:10px;color:#e94560;text-transform:uppercase}
#cnt{flex:1;overflow-y:auto;padding:24px}
.bc{font-size:12px;color:#666;margin-bottom:12px}
.empty{display:flex;align-items:center;justify-content:center;height:100%;color:#555;font-size:15px}
.cg{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px}
.card{background:#16213e;border:1px solid #0f3460;border-radius:8px;padding:14px;cursor:pointer;transition:border-color .15s}
.card:hover{border-color:#e94560}
.card .ct{font-weight:600;margin-bottom:4px;font-size:14px}
.card .cy{font-size:11px;color:#888;text-transform:uppercase}
.card .cm{font-size:11px;color:#555;margin-top:6px}
#ed{display:none;position:fixed;inset:0;background:#1a1a2e;z-index:90;flex-direction:column}
#ed-hdr{padding:12px 24px;border-bottom:1px solid #0f3460;display:flex;justify-content:space-between;align-items:center}
#ed-hdr span{font-size:16px;font-weight:600}
#ed button{background:#0f3460;color:#e0e0e0;border:1px solid #1a4080;border-radius:4px;padding:6px 14px;cursor:pointer;font-size:12px}
#ed button.p{background:#e94560;border-color:#e94560}
#ed-ta{flex:1;background:#0f0f23;color:#e0e0e0;border:none;padding:24px;font-family:"Fira Code",Consolas,monospace;font-size:14px;line-height:1.7;resize:none;outline:none}
#ed-ftr{padding:10px 24px;border-top:1px solid #0f3460;display:flex;gap:8px}
.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:12px;padding:24px;width:420px;max-width:90vw}
.md h3{margin-bottom:16px;font-size:16px}
.md label{display:block;font-size:12px;color:#888;margin-bottom:4px;margin-top:12px}
.md input,.md select{width:100%;padding:8px 12px;background:#0f0f23;color:#e0e0e0;border:1px solid #1a4080;border-radius:4px;font-size:14px;outline:none}
.md input:focus{border-color:#e94560}
.md .ma{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}
.md button{padding:8px 16px;border-radius:4px;border:1px solid #1a4080;background:#0f3460;color:#e0e0e0;cursor:pointer}
.md button.p{background:#e94560;border-color:#e94560;color:#fff}
</style>
</head>
<body>
<div id="sb">
<div id="sb-hdr"><span>ВЕРСТАК</span><button onclick="showCM()">+</button></div>
<div id="tree"><div class="empty" style="padding:30px 14px;font-size:13px">Пусто. Создайте первое дело.</div></div>
</div>
<div id="main">
<div id="mh"><h1 id="pt">Верстак</h1><div class="acts">
<button onclick="showCNM()">+ Заметка</button>
<button onclick="showCM()">+ Дело</button>
<button onclick="document.getElementById('fi').click()">+ Файл</button>
<input type="file" id="fi" style="display:none" onchange="handleFA(event)">
</div></div>
<div id="tabs"><div class="t a" onclick="switchTab(this,'ov')">Обзор</div><div class="t" onclick="switchTab(this,'nt')">Заметки</div><div class="t" onclick="switchTab(this,'fl')">Файлы</div></div>
<div id="srch"><input id="si" placeholder="Поиск по делам и заметкам..." autocomplete="off" oninput="handleSR(this.value)"></div>
<div id="sr-res"></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 id="ed-ftr"></div>
</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="Название..." autofocus>
<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="Название заметки..." autofocus>
<div class="ma"><button onclick="closeCNM()">Отмена</button><button class="p" onclick="submitCNM()">Создать</button></div></div>
</div>
<script>
const API='';
let cur='',editId='',tab='ov';
async function api(p,opt){
const r=await fetch(API+p,{headers:{'Content-Type':'application/json'},...opt});
if(!r.ok)throw new Error(r.status);
return r.json();
}
// tree
async function loadTree(){
try{
const items=await api('/api/nodes');
const t=document.getElementById('tree');
if(!items.length){t.innerHTML='<div class="empty" style="padding:30px 14px;font-size:13px">Пусто. Создайте первое дело.</div>';return}
t.innerHTML=renderL(items,'');
}catch(e){document.getElementById('tree').innerHTML='<div class="empty" style="padding:30px 14px">Ошибка загрузки</div>'}
}
function renderL(items,pid){
let h='<ul'+(pid?' class="tc"':'')+'>';
for(const n of items){
h+='<li><div class="ti'+(cur===n.id?' sel':'')+'" data-id="'+n.id+'" onclick="selN(\''+n.id+'\')">';
h+='<span class="ar">'+('▸')+'</span><span class="ic">'+nic(n.type)+'</span>';
h+='<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+esc(n.title)+'</span></div></li>';
}
h+='</ul>';return h;
}
function nic(t){const i={case:'◇',folder:'▸',space:'◎',note:'📝',document:'📄',file:'📎',recipe:'◈',action:'⚡',secret:'🔒',worklog:'⏱',link:'🔗'};return i[t]||'◇'}
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
async function selN(id){
cur=id;
document.querySelectorAll('.ti').forEach(e=>e.classList.toggle('sel',e.dataset.id===id));
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:40px">Пусто. Добавьте заметку или файл.</div>';
}
document.getElementById('cnt').innerHTML=h;
}
function fsz(b){if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';return(b/1048576).toFixed(1)+' MB'}
function switchTab(el,t){
document.querySelectorAll('#tabs .t').forEach(e=>e.classList.remove('a'));
el.classList.add('a');tab=t;
if(t==='ov'&&cur)selN(cur);else if(t==='nt')loadNT();else if(t==='fl')loadFL();
}
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);
const fl=d.files||[];
if(!fl.length){document.getElementById('cnt').innerHTML='<div class="empty">Нет файлов</div>';return}
let h='<div class="cg">';
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>';document.getElementById('cnt').innerHTML=h;
}
// editor
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'}
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)}
}
// modals
function showCM(){document.getElementById('cm').classList.add('on');document.getElementById('cn').focus()}
function closeCM(){document.getElementById('cm').classList.remove('on');document.getElementById('cn').value=''}
async function submitCM(){
const type=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,title})});closeCM();loadTree();if(cur)selN(cur)}
catch(e){alert('Ошибка: '+e.message)}
}
function showCNM(){document.getElementById('cnm').classList.add('on');document.getElementById('cnn').focus()}
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);switchTab(document.querySelectorAll('#tabs .t')[1],'nt')}
catch(e){alert('Ошибка: '+e.message)}
}
function handleFA(ev){const f=ev.target.files[0];if(!f)return;alert('MVP: загрузка файлов будет в следующей версии. Файл: '+f.name);ev.target.value=''}
// search
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{
// For MVP, fetch all roots and filter locally
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="selN(\''+r.id+'\');b.style.display=\'none\'"><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);
}
// keyboard shortcuts
document.addEventListener('keydown',e=>{
if(e.key==='Escape'){closeED();closeCM();closeCNM();document.getElementById('sr-res').style.display='none'}
if(e.key==='n'&&(e.ctrlKey||e.metaKey)){e.preventDefault();showCM()}
});
loadTree();
</script>
</body>
</html>