@cstart/coldstart 2.2.9 → 2.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Embedded single-file notebook viewer (client). Generated from the
3
- * design source; do not hand-edit. `__DATA_JSON__` is replaced at build time
4
- * by `kb view` with the baked note data. Shipped as a string because tsc does
3
+ * design source; do not hand-edit. Two placeholders are substituted by
4
+ * `renderViewHtml` in `kb view`: `__TITLE__` (the tab title, repo-scoped) and
5
+ * `__DATA_JSON__` (the baked note data). Shipped as a string because tsc does
5
6
  * not copy non-.ts assets into dist.
6
7
  */
7
- export declare const VIEW_TEMPLATE = "<title>Coldstart Notebook \u2014 arches</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol\u2026\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Wikilink graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// \u2014 fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n const edgeCount = notes.reduce((s,n)=>s+n.outLinks.filter(l=>byId.has(l)).length,0);\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) \u2014 you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' \u00B7 '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors \u2014 verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">\u2014 queries that should surface this note</span><span class=\"chv\">\u203A</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id){linked.add(n.id);linked.add(l);edges.push([n.id,l]);}\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b])=>[idx.get(a),idx.get(b)]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n $('#graphHint').textContent=`${N} linked notes \u00B7 ${edges.length} links. Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} \u00B7 ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
8
+ export declare const VIEW_TEMPLATE = "<title>__TITLE__</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol\u2026\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Wikilink graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// \u2014 fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n const edgeCount = notes.reduce((s,n)=>s+n.outLinks.filter(l=>byId.has(l)).length,0);\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) \u2014 you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' \u00B7 '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors \u2014 verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">\u2014 queries that should surface this note</span><span class=\"chv\">\u203A</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id){linked.add(n.id);linked.add(l);edges.push([n.id,l]);}\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b])=>[idx.get(a),idx.get(b)]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n $('#graphHint').textContent=`${N} linked notes \u00B7 ${edges.length} links. Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} \u00B7 ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
8
9
  //# sourceMappingURL=view-template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"view-template.d.ts","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,w8sCAAs5sC,CAAC"}
1
+ {"version":3,"file":"view-template.d.ts","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,i7sCAAo4sC,CAAC"}
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Embedded single-file notebook viewer (client). Generated from the
3
- * design source; do not hand-edit. `__DATA_JSON__` is replaced at build time
4
- * by `kb view` with the baked note data. Shipped as a string because tsc does
3
+ * design source; do not hand-edit. Two placeholders are substituted by
4
+ * `renderViewHtml` in `kb view`: `__TITLE__` (the tab title, repo-scoped) and
5
+ * `__DATA_JSON__` (the baked note data). Shipped as a string because tsc does
5
6
  * not copy non-.ts assets into dist.
6
7
  */
7
- export const VIEW_TEMPLATE = "<title>Coldstart Notebook — arches</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol…\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Wikilink graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// — fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n const edgeCount = notes.reduce((s,n)=>s+n.outLinks.filter(l=>byId.has(l)).length,0);\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) — you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' · '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors — verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">— queries that should surface this note</span><span class=\"chv\">›</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id){linked.add(n.id);linked.add(l);edges.push([n.id,l]);}\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b])=>[idx.get(a),idx.get(b)]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n $('#graphHint').textContent=`${N} linked notes · ${edges.length} links. Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} · ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
8
+ export const VIEW_TEMPLATE = "<title>__TITLE__</title>\n<style>\n :root{\n --ground:#eaeef3; --surface:#ffffff; --rail:#f5f8fc; --sunk:#eef2f7;\n --ink:#161a22; --ink-soft:#39414f; --muted:#69748a; --faint:#9aa6b8;\n --line:#dce2ec; --line-soft:#e8edf3;\n --accent:#1f4fd8; --accent-soft:#e7ecfd; --accent-ink:#173bab;\n --fresh:#137a3b; --fresh-bg:#e4f4ea;\n --changed:#a85a06; --changed-bg:#fbeedd;\n --missing:#bb1436; --missing-bg:#fbe3e8;\n --unver:#5a6577; --unver-bg:#eceff4;\n --flow:#0b7285; --flow-bg:#dcf1f4;\n --file:#1f4fd8; --file-bg:#e7ecfd;\n --mono:ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,monospace;\n --sans:system-ui,-apple-system,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif;\n --sh:0 1px 2px rgba(20,28,45,.05),0 4px 16px rgba(20,28,45,.06);\n --r:9px;\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{margin:0;background:var(--ground);color:var(--ink);font-family:var(--sans);\n font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;\n text-rendering:optimizeLegibility;}\n button{font:inherit;color:inherit;cursor:pointer;border:0;background:none}\n ::selection{background:var(--accent-soft)}\n\n .app{display:grid;grid-template-rows:auto 1fr;height:100vh;overflow:hidden}\n\n /* ---- top bar ---- */\n .top{display:flex;align-items:center;gap:20px;padding:12px 20px;\n background:var(--surface);border-bottom:1px solid var(--line);z-index:5}\n .brand{display:flex;flex-direction:column;justify-content:center;min-width:0;gap:1px}\n .brand .kicker{font-family:var(--mono);font-size:9.5px;letter-spacing:.11em;\n text-transform:uppercase;color:var(--faint);font-weight:600;line-height:1}\n .brand .repo{display:inline-flex;align-items:center;gap:6px;font-size:17px;\n font-weight:680;letter-spacing:-.02em;color:var(--ink);line-height:1.1}\n .brand .repo::before{content:\"\";width:7px;height:7px;border-radius:50%;\n background:var(--accent);flex-shrink:0}\n .top .spacer{flex:1}\n .stat{display:flex;flex-direction:column;line-height:1.15}\n .stat b{font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:-.02em}\n .stat span{font-family:var(--mono);font-size:9.5px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint)}\n .fresh-meter{display:flex;align-items:center;gap:8px}\n .meter-bar{width:120px;height:8px;border-radius:5px;background:var(--sunk);\n overflow:hidden;display:flex}\n .meter-bar i{display:block;height:100%}\n .segbtns{display:flex;background:var(--sunk);border-radius:8px;padding:3px}\n .segbtns button{padding:5px 13px;border-radius:6px;font-size:12.5px;\n font-weight:500;color:var(--muted)}\n .segbtns button[aria-pressed=true]{background:var(--surface);color:var(--ink);\n box-shadow:0 1px 2px rgba(20,28,45,.12);font-weight:600}\n\n /* ---- body split ---- */\n .split{display:grid;grid-template-columns:340px 1fr;min-height:0}\n .split.graph-mode{grid-template-columns:1fr}\n .split.graph-mode .rail{display:none}\n\n /* ---- rail ---- */\n .rail{background:var(--rail);border-right:1px solid var(--line);\n display:flex;flex-direction:column;min-height:0}\n .rail .controls{padding:12px 12px 0;display:flex;flex-direction:column;gap:10px}\n .search{position:relative}\n .search input{width:100%;padding:9px 12px 9px 32px;border:1px solid var(--line);\n border-radius:8px;background:var(--surface);font-size:13px;outline:none}\n .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}\n .search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);\n width:15px;height:15px;stroke:var(--faint);fill:none;stroke-width:2}\n\n /* lens tabs (Files / Flows) */\n .lenses{display:flex;gap:4px}\n .lens{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;\n padding:9px 0;border-radius:9px;font-size:13px;font-weight:600;color:var(--muted);\n background:var(--sunk);border:1px solid transparent}\n .lens .ct{font-family:var(--mono);font-size:11px;font-weight:700;\n padding:1px 6px;border-radius:20px;background:rgba(105,116,138,.16);color:var(--muted);\n font-variant-numeric:tabular-nums}\n .lens[aria-pressed=true]{background:var(--surface);color:var(--ink);\n border-color:var(--line);box-shadow:var(--sh)}\n .lens.files[aria-pressed=true]{color:var(--file)} .lens.files[aria-pressed=true] .ct{background:var(--file-bg);color:var(--file)}\n .lens.flows[aria-pressed=true]{color:var(--flow)} .lens.flows[aria-pressed=true] .ct{background:var(--flow-bg);color:var(--flow)}\n .lens .ic{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8}\n\n /* status / freshness filter row */\n .statusrow{display:flex;align-items:center;gap:8px;padding:2px}\n .fchip{display:inline-flex;align-items:center;gap:7px;padding:5px 11px;border-radius:20px;\n font-size:12px;font-weight:600;border:1px solid var(--line);background:var(--surface);color:var(--muted)}\n .fchip .d{width:8px;height:8px;border-radius:50%;background:var(--fresh)}\n .fchip.has-stale{color:var(--changed);border-color:var(--changed);background:var(--changed-bg)}\n .fchip.has-stale .d{background:var(--changed)}\n .fchip[aria-pressed=true]{outline:2px solid var(--changed);outline-offset:1px}\n .fchip.all-fresh .d{background:var(--fresh)}\n .treetools{margin-left:auto;display:flex;gap:2px}\n .treetools button{font-family:var(--mono);font-size:10px;letter-spacing:.04em;\n text-transform:uppercase;color:var(--faint);padding:4px 7px;border-radius:6px}\n .treetools button:hover{color:var(--accent);background:var(--sunk)}\n\n .list{overflow-y:auto;padding:8px 8px 60px;flex:1}\n\n /* tree rows */\n .row{display:flex;align-items:center;gap:7px;width:100%;text-align:left;\n padding:5px 8px;border-radius:7px;min-height:30px}\n .row:hover{background:var(--sunk)}\n .row[aria-current=true]{background:var(--accent-soft)}\n .row[aria-current=true] .lbl{color:var(--accent-ink);font-weight:600}\n .chev{width:14px;height:14px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:2;\n transition:transform .12s}\n .row.open>.chev{transform:rotate(90deg)}\n .spacer-ic{width:14px;flex-shrink:0}\n .ficon{width:15px;height:15px;flex-shrink:0;stroke:var(--faint);fill:none;stroke-width:1.6}\n .row.dir .ficon{stroke:var(--accent);fill:var(--accent-soft)}\n .lbl{font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}\n .row.dir .lbl{font-weight:600;color:var(--ink-soft)}\n .row.dir .lbl .seg-dim{color:var(--faint);font-weight:500}\n .badge{font-family:var(--mono);font-size:10px;color:var(--faint);\n font-variant-numeric:tabular-nums;flex-shrink:0}\n .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n .dot.fresh{background:var(--fresh)} .dot.changed{background:var(--changed)}\n .dot.missing{background:var(--missing)} .dot.unverified{background:var(--unver)}\n .char{font-family:var(--mono);font-size:8.5px;font-weight:700;color:var(--faint);\n border:1px solid var(--line);border-radius:3px;padding:0 3px;flex-shrink:0}\n\n /* flow cards */\n .flowcard{display:block;width:100%;text-align:left;padding:11px 12px;border-radius:9px;\n border:1px solid var(--line-soft);background:var(--surface);margin-bottom:7px}\n .flowcard:hover{border-color:var(--flow);box-shadow:var(--sh)}\n .flowcard[aria-current=true]{border-color:var(--flow);background:var(--flow-bg)}\n .fc-top{display:flex;align-items:center;gap:8px;margin-bottom:6px}\n .fc-top .dot{margin-top:0}\n .fc-title{font-size:13px;font-weight:600;line-height:1.35;flex:1;overflow-wrap:anywhere}\n .fc-span{display:flex;flex-wrap:wrap;gap:4px}\n .fc-file{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--sunk);\n padding:1px 6px;border-radius:5px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .fc-more{font-family:var(--mono);font-size:10px;color:var(--faint);padding:1px 3px}\n .empty-note{color:var(--faint);font-size:12px;font-style:italic;padding:20px 10px}\n\n /* ---- reader ---- */\n .reader{overflow-y:auto;padding:0}\n .reader-inner{max-width:760px;margin:0 auto;padding:38px 40px 120px}\n .eyebrow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;\n font-family:var(--mono);font-size:11px;margin-bottom:14px}\n .eyebrow .id{color:var(--faint);overflow-wrap:anywhere}\n .eyebrow .sep{color:var(--line)}\n .kind{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:1px 5px;border-radius:4px}\n .kind.file{color:var(--file);background:var(--file-bg)}\n .kind.flow{color:var(--flow);background:var(--flow-bg)}\n h1.note-title{font-size:26px;line-height:1.22;letter-spacing:-.02em;\n font-weight:680;margin:0 0 16px;text-wrap:balance;overflow-wrap:anywhere}\n .metabar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;\n padding-bottom:18px;margin-bottom:22px;border-bottom:1px solid var(--line-soft)}\n .pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;\n font-weight:600;padding:3px 10px;border-radius:20px;font-family:var(--mono);\n letter-spacing:.02em}\n .pill .d{width:7px;height:7px;border-radius:50%}\n .pill.fresh{color:var(--fresh);background:var(--fresh-bg)} .pill.fresh .d{background:var(--fresh)}\n .pill.changed{color:var(--changed);background:var(--changed-bg)} .pill.changed .d{background:var(--changed)}\n .pill.missing{color:var(--missing);background:var(--missing-bg)} .pill.missing .d{background:var(--missing)}\n .pill.unverified{color:var(--unver);background:var(--unver-bg)} .pill.unverified .d{background:var(--unver)}\n .metabar .mi{font-size:12px;color:var(--muted)}\n .metabar .mi b{color:var(--ink-soft);font-variant-numeric:tabular-nums;font-weight:600}\n\n .sec-label{font-family:var(--mono);font-size:10px;letter-spacing:.09em;\n text-transform:uppercase;color:var(--faint);font-weight:600;margin:26px 0 10px}\n\n .anchor{border:1px solid var(--line);border-radius:var(--r);background:var(--surface);\n padding:11px 13px;margin-bottom:8px}\n .anchor-top{display:flex;align-items:center;gap:9px}\n .apath{font-family:var(--mono);font-size:12px;color:var(--ink);font-weight:600;\n overflow-wrap:anywhere;flex:1}\n .apath .p{color:var(--faint)}\n .astate{font-family:var(--mono);font-size:9px;letter-spacing:.06em;font-weight:700;\n text-transform:uppercase;padding:2px 6px;border-radius:4px;white-space:nowrap}\n .astate.fresh{color:var(--fresh);background:var(--fresh-bg)}\n .astate.changed{color:var(--changed);background:var(--changed-bg)}\n .astate.missing{color:var(--missing);background:var(--missing-bg)}\n .astate.unverified{color:var(--unver);background:var(--unver-bg)}\n .copy{font-family:var(--mono);font-size:10px;color:var(--faint);padding:2px 6px;\n border-radius:4px;border:1px solid var(--line)}\n .copy:hover{color:var(--accent);border-color:var(--accent)}\n .syms{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px}\n .sym{font-family:var(--mono);font-size:11px;background:var(--sunk);\n color:var(--ink-soft);padding:2px 7px;border-radius:5px;cursor:pointer;\n transition:background .1s,color .1s;position:relative}\n .sym:hover{background:var(--accent-soft);color:var(--accent-ink)}\n .sym.copied{background:var(--accent);color:#fff}\n .sym.copied::after{content:\"copied\";position:absolute;left:50%;bottom:calc(100% + 5px);\n transform:translateX(-50%);background:var(--ink);color:#fff;font-size:9px;\n padding:2px 6px;border-radius:4px;white-space:nowrap;letter-spacing:.04em}\n\n .aliases{border:1px solid var(--line-soft);border-radius:var(--r);\n background:var(--rail);overflow:hidden}\n .aliases summary{list-style:none;cursor:pointer;padding:9px 13px;\n display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}\n .aliases summary::-webkit-details-marker{display:none}\n .aliases summary b{color:var(--ink-soft);font-weight:600}\n .aliases summary .chv{margin-left:auto;transition:transform .15s}\n .aliases[open] summary .chv{transform:rotate(90deg)}\n .alias-cloud{display:flex;flex-wrap:wrap;gap:6px;padding:2px 13px 13px}\n .alias-cloud span{font-family:var(--mono);font-size:11px;color:var(--ink-soft);\n background:var(--surface);border:1px solid var(--line);padding:2px 8px;border-radius:12px}\n\n .md{font-size:14.5px;line-height:1.68;color:var(--ink-soft)}\n .md h2{font-size:13px;font-family:var(--mono);letter-spacing:.05em;\n text-transform:uppercase;color:var(--ink);font-weight:700;margin:30px 0 12px;\n padding-bottom:7px;border-bottom:1px solid var(--line-soft)}\n .md h3{font-size:15px;margin:22px 0 8px;color:var(--ink)}\n .md p{margin:0 0 14px}\n .md ul,.md ol{margin:0 0 14px;padding-left:22px}\n .md li{margin:0 0 8px}\n .md li::marker{color:var(--faint)}\n .md code{font-family:var(--mono);font-size:12.5px;background:var(--sunk);\n color:var(--accent-ink);padding:1px 5px;border-radius:5px;overflow-wrap:anywhere}\n .md strong{color:var(--ink);font-weight:650}\n .wl{font-family:var(--mono);font-size:12px;color:var(--accent);font-weight:600;\n background:var(--accent-soft);padding:1px 6px;border-radius:5px;\n text-decoration:none;overflow-wrap:anywhere}\n .wl:hover{background:var(--accent);color:#fff}\n .wl.dead{color:var(--muted);background:var(--sunk);cursor:default}\n\n .links-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:34px;\n padding-top:22px;border-top:1px solid var(--line-soft)}\n @media(max-width:640px){.links-grid{grid-template-columns:1fr}}\n .linkcol .sec-label{margin-top:0}\n .linkbtn{display:flex;align-items:center;gap:8px;width:100%;text-align:left;\n padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px}\n .linkbtn:hover{background:var(--sunk)}\n .linkbtn .kind{flex-shrink:0}\n .linkbtn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n\n .placeholder{display:flex;align-items:center;justify-content:center;height:100%;\n color:var(--faint);font-size:13px}\n\n /* ---- graph ---- */\n .graph-wrap{position:relative;overflow:hidden;background:\n radial-gradient(circle at 30% 20%, #f2f5fa, var(--ground))}\n .graph-wrap canvas{display:block;width:100%;height:100%}\n .graph-legend{position:absolute;top:16px;left:16px;background:var(--surface);\n border:1px solid var(--line);border-radius:var(--r);padding:12px 14px;\n box-shadow:var(--sh);font-size:12px;max-width:230px}\n .graph-legend .lg-t{font-family:var(--mono);font-size:10px;letter-spacing:.07em;\n text-transform:uppercase;color:var(--faint);margin-bottom:8px}\n .graph-legend .lg-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}\n .graph-legend .lg-row i{width:12px;height:12px;border-radius:50%;flex-shrink:0}\n .graph-legend .hint{color:var(--muted);margin-top:8px;line-height:1.4}\n .graph-tip{position:absolute;pointer-events:none;background:var(--ink);color:#fff;\n font-size:11.5px;padding:6px 10px;border-radius:7px;max-width:280px;opacity:0;\n transform:translate(-50%,-140%);transition:opacity .1s;box-shadow:var(--sh);z-index:3}\n .graph-tip .tk{font-family:var(--mono);font-size:9px;letter-spacing:.06em;\n text-transform:uppercase;opacity:.7}\n\n @media(max-width:860px){\n .split{grid-template-columns:1fr}\n .rail{position:absolute;inset:57px 0 0 0;z-index:4;width:min(340px,86vw);\n box-shadow:var(--sh)}\n .split:not(.rail-open) .rail{display:none}\n .split.graph-mode .rail{display:none}\n .reader-inner{padding:26px 20px 100px}\n .menu-btn{display:inline-flex!important}\n }\n .menu-btn{display:none;align-items:center;gap:6px}\n</style>\n\n<div class=\"app\">\n <header class=\"top\">\n <button class=\"menu-btn segbtns\" id=\"menuBtn\" style=\"padding:6px 10px\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M2 4h12M2 8h12M2 12h12\"/></svg></button>\n <div class=\"brand\">\n <span class=\"kicker\">Coldstart&nbsp;Notebook</span>\n <span class=\"repo\" id=\"repoName\">repo</span>\n </div>\n <div class=\"spacer\"></div>\n <div class=\"stat\"><b id=\"sTotal\">0</b><span>notes</span></div>\n <div class=\"stat\"><b id=\"sLinks\">0</b><span>links</span></div>\n <div class=\"fresh-meter\">\n <div class=\"meter-bar\" id=\"meterBar\"></div>\n <div class=\"stat\"><b id=\"sFresh\">0%</b><span>fresh</span></div>\n </div>\n <div class=\"segbtns\" role=\"tablist\">\n <button id=\"tabNotes\" aria-pressed=\"true\">Notes</button>\n <button id=\"tabGraph\" aria-pressed=\"false\">Graph</button>\n </div>\n </header>\n\n <div class=\"split\" id=\"split\">\n <aside class=\"rail\" id=\"rail\">\n <div class=\"controls\">\n <div class=\"search\">\n <svg viewBox=\"0 0 16 16\"><circle cx=\"7\" cy=\"7\" r=\"5\"/><path d=\"M11 11l3 3\"/></svg>\n <input id=\"q\" placeholder=\"Search title, alias, path, symbol…\" autocomplete=\"off\">\n </div>\n <div class=\"lenses\">\n <button class=\"lens files\" data-lens=\"files\" aria-pressed=\"true\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>\n Files <span class=\"ct\" id=\"ctFiles\">0</span>\n </button>\n <button class=\"lens flows\" data-lens=\"flows\" aria-pressed=\"false\">\n <svg class=\"ic\" viewBox=\"0 0 16 16\"><circle cx=\"3.5\" cy=\"3.5\" r=\"1.6\"/><circle cx=\"12.5\" cy=\"8\" r=\"1.6\"/><circle cx=\"3.5\" cy=\"12.5\" r=\"1.6\"/><path d=\"M5 3.9h4a2 2 0 0 1 2 2v.5M5 12.1h4a2 2 0 0 0 2-2v-.5\"/></svg>\n Flows <span class=\"ct\" id=\"ctFlows\">0</span>\n </button>\n </div>\n <div class=\"statusrow\">\n <button class=\"fchip all-fresh\" id=\"staleChip\" aria-pressed=\"false\"><span class=\"d\"></span><span id=\"staleLbl\">All fresh</span></button>\n <div class=\"treetools\" id=\"treeTools\">\n <button data-tt=\"expand\">Expand</button>\n <button data-tt=\"collapse\">Collapse</button>\n </div>\n </div>\n </div>\n <div class=\"list\" id=\"list\"></div>\n </aside>\n\n <main class=\"reader\" id=\"reader\">\n <div class=\"placeholder\">Select a note to read.</div>\n </main>\n\n <section class=\"graph-wrap\" id=\"graphWrap\" style=\"display:none\">\n <canvas id=\"graphCanvas\"></canvas>\n <div class=\"graph-legend\">\n <div class=\"lg-t\">Wikilink graph</div>\n <div class=\"lg-row\"><i style=\"background:var(--file)\"></i>file note</div>\n <div class=\"lg-row\"><i style=\"background:var(--flow)\"></i>flow note</div>\n <div class=\"hint\" id=\"graphHint\">Only linked notes shown. Click a node to open it.</div>\n </div>\n <div class=\"graph-tip\" id=\"graphTip\"></div>\n </section>\n </div>\n</div>\n\n<script>const DB = __DATA_JSON__;</script>\n<script>\n(function(){\n \"use strict\";\n const notes = DB.notes;\n const byId = new Map(notes.map(n=>[n.id,n]));\n const fileNotes = notes.filter(n=>n.type==='file');\n const flowNotes = notes.filter(n=>n.type==='flow');\n const $ = s=>document.querySelector(s);\n const esc = s=>String(s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));\n // clipboard: navigator.clipboard is unreliable on file:// — fall back to execCommand\n function copyText(t){\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(t).catch(()=>fallbackCopy(t));return;}\n }catch(e){}\n fallbackCopy(t);\n }\n function fallbackCopy(t){\n try{const ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';\n document.body.appendChild(ta);ta.focus();ta.select();document.execCommand('copy');document.body.removeChild(ta);}catch(e){}\n }\n const RANK={fresh:0,unverified:1,changed:2,missing:3};\n const isStale=n=>n.freshness==='changed'||n.freshness==='missing';\n\n let state = { lens:'files', q:'', selected:null, view:'notes', staleOnly:false, open:new Set() };\n\n // ---- header stats ----\n $('#repoName').textContent = DB.summary.repo;\n $('#sTotal').textContent = DB.summary.total;\n $('#ctFiles').textContent = fileNotes.length;\n $('#ctFlows').textContent = flowNotes.length;\n const edgeCount = notes.reduce((s,n)=>s+n.outLinks.filter(l=>byId.has(l)).length,0);\n $('#sLinks').textContent = edgeCount;\n const fr = DB.summary.byFreshness;\n const freshPct = Math.round(100*(fr.fresh||0)/DB.summary.total);\n $('#sFresh').textContent = freshPct+'%';\n const mb = $('#meterBar');\n const morder=[['fresh','--fresh'],['changed','--changed'],['unverified','--unver'],['missing','--missing']];\n mb.innerHTML = morder.filter(([k])=>fr[k]).map(([k,v])=>\n `<i style=\"width:${100*fr[k]/DB.summary.total}%;background:var(${v})\"></i>`).join('');\n\n // stale chip\n const staleCount = notes.filter(isStale).length;\n const chip=$('#staleChip');\n if(staleCount){\n chip.classList.remove('all-fresh');chip.classList.add('has-stale');\n $('#staleLbl').textContent=`${staleCount} need review`;\n }else{\n $('#staleLbl').textContent='All fresh';\n }\n\n // ---- filter helpers ----\n function matches(n,q){\n if(!q) return true;\n const hay=(n.title+' '+n.id+' '+n.aliases.join(' ')+' '+n.anchors.map(a=>a.path+' '+(a.symbols||[]).join(' ')).join(' ')).toLowerCase();\n return q.toLowerCase().split(/\\s+/).every(t=>hay.includes(t));\n }\n function passFilter(n){\n if(state.staleOnly && !isStale(n)) return false;\n return matches(n,state.q);\n }\n\n // ================= FILE TREE =================\n function buildTree(){\n const root={name:'',path:'',children:new Map(),notes:[],dir:true};\n for(const n of fileNotes){\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const segs=path.split('/');\n let cur=root;\n for(let i=0;i<segs.length-1;i++){\n const s=segs[i], p=segs.slice(0,i+1).join('/');\n if(!cur.children.has(s)) cur.children.set(s,{name:s,path:p,children:new Map(),notes:[],dir:true});\n cur=cur.children.get(s);\n }\n cur.notes.push(Object.assign({leaf:segs[segs.length-1]},n));\n }\n // compact single-child dir chains (VSCode style), never the root\n const compact=(node,isRoot)=>{\n for(const c of node.children.values()) compact(c,false);\n if(!isRoot){\n while(node.children.size===1 && node.notes.length===0){\n const only=[...node.children.values()][0];\n node.name = node.name+'/'+only.name;\n node.path = only.path; node.notes = only.notes; node.children = only.children;\n }\n }\n };\n compact(root,true);\n // rollups\n const roll=(node)=>{\n let worst='fresh',cnt=node.notes.length;\n for(const n of node.notes) if(RANK[n.freshness]>RANK[worst]) worst=n.freshness;\n for(const c of node.children.values()){roll(c); if(RANK[c.roll]>RANK[worst]) worst=c.roll; cnt+=c.count;}\n node.roll=worst; node.count=cnt;\n };\n roll(root);\n return root;\n }\n const TREE=buildTree();\n\n function nodeHasMatch(node){\n if(node.notes.some(passFilter)) return true;\n for(const c of node.children.values()) if(nodeHasMatch(c)) return true;\n return false;\n }\n function ancestorsOf(id){\n // dir paths that contain the selected file note\n const n=byId.get(id); if(!n||n.type!=='file') return new Set();\n const path=n.anchors[0]?n.anchors[0].path:n.title;\n const set=new Set(); const segs=path.split('/');\n for(let i=1;i<segs.length;i++) set.add(segs.slice(0,i).join('/'));\n return set;\n }\n const FOLDER_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M2 4.5h5l1.5 1.5H14v6.5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1z\"/></svg>';\n const FILE_SVG='<svg class=\"ficon\" viewBox=\"0 0 16 16\"><path d=\"M4 1.5h5L13 5v9a.5.5 0 0 1-.5.5h-9A.5.5 0 0 1 3 14V2a.5.5 0 0 1 1-.5z\"/><path d=\"M9 1.5V5h4\"/></svg>';\n\n function renderTreeNode(node,depth,active,out){\n const dirs=[...node.children.values()].filter(nodeHasMatch)\n .sort((a,b)=>a.name.localeCompare(b.name));\n const leaves=node.notes.filter(passFilter).sort((a,b)=>a.leaf.localeCompare(b.leaf));\n const pad=8+depth*15;\n for(const d of dirs){\n const open = active || state.open.has(d.path);\n const dimIdx=d.name.lastIndexOf('/');\n const nm = dimIdx>-1 ? `<span class=\"seg-dim\">${esc(d.name.slice(0,dimIdx+1))}</span>${esc(d.name.slice(dimIdx+1))}` : esc(d.name);\n out.push(`<button class=\"row dir ${open?'open':''}\" data-dir=\"${esc(d.path)}\" style=\"padding-left:${pad}px\">\n <svg class=\"chev\" viewBox=\"0 0 16 16\"><path d=\"M6 3l5 5-5 5\"/></svg>${FOLDER_SVG}\n <span class=\"lbl\">${nm}</span>\n <span class=\"badge\">${d.count}</span>\n ${d.roll!=='fresh'?`<span class=\"dot ${d.roll}\"></span>`:''}\n </button>`);\n if(open) renderTreeNode(d,depth+1,active,out);\n }\n for(const n of leaves){\n out.push(`<button class=\"row leaf\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\" style=\"padding-left:${pad+21}px\">\n ${FILE_SVG}\n <span class=\"lbl\">${esc(n.leaf)}</span>\n ${n.character?`<span class=\"char\" title=\"${esc(n.character)} note\">${n.character[0].toUpperCase()}</span>`:''}\n <span class=\"dot ${n.freshness}\"></span>\n </button>`);\n }\n }\n\n function renderFiles(){\n const active=!!(state.q||state.staleOnly);\n const out=[];\n renderTreeNode(TREE,0,active,out);\n $('#list').innerHTML = out.length?out.join(''):'<div class=\"empty-note\">No file notes match.</div>';\n }\n\n // ================= FLOWS =================\n function renderFlows(){\n const list=flowNotes.filter(passFilter)\n .sort((a,b)=>b.anchors.length-a.anchors.length||a.title.localeCompare(b.title));\n if(!list.length){$('#list').innerHTML='<div class=\"empty-note\">No flows match.</div>';return;}\n $('#list').innerHTML=list.map(n=>{\n const files=n.anchors.map(a=>a.path.split('/').pop());\n const shown=files.slice(0,4), extra=files.length-shown.length;\n return `<button class=\"flowcard\" data-id=\"${esc(n.id)}\" aria-current=\"${n.id===state.selected}\">\n <div class=\"fc-top\">\n <span class=\"dot ${n.freshness}\"></span>\n <span class=\"fc-title\">${esc(n.title)}</span>\n </div>\n <div class=\"fc-span\">\n ${shown.map(f=>`<span class=\"fc-file\">${esc(f)}</span>`).join('')}\n ${extra>0?`<span class=\"fc-more\">+${extra} more</span>`:''}\n </div>\n </button>`;\n }).join('');\n }\n\n function renderList(){\n $('#treeTools').style.visibility = state.lens==='files' ? 'visible':'hidden';\n if(state.lens==='files') renderFiles(); else renderFlows();\n }\n\n // ---- markdown mini-renderer ----\n function mdInline(t){\n t=esc(t);\n t=t.replace(/`([^`]+)`/g,(_,c)=>`<code>${c}</code>`);\n t=t.replace(/\\[\\[([^\\]]+)\\]\\]/g,(_,id)=>{\n const ok=byId.has(id);\n return `<a class=\"wl${ok?'':' dead'}\" ${ok?`data-goto=\"${esc(id)}\"`:''} title=\"${esc(id)}\">${ok?esc(byId.get(id).title):esc(id)}</a>`;\n });\n t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');\n return t;\n }\n function renderMd(src){\n const lines=src.split('\\n');\n let html='',i=0;\n const flush=(buf,tag)=>buf.length?`<${tag}>${buf.join('')}</${tag}>`:'';\n while(i<lines.length){\n let ln=lines[i];\n if(/^#\\s+/.test(ln)){i++;continue;}\n if(/^##\\s+/.test(ln)){html+=`<h2>${mdInline(ln.replace(/^##\\s+/,''))}</h2>`;i++;continue;}\n if(/^###\\s+/.test(ln)){html+=`<h3>${mdInline(ln.replace(/^###\\s+/,''))}</h3>`;i++;continue;}\n if(/^\\s*[-*]\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*[-*]\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*[-*]\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ul');continue;\n }\n if(/^\\s*\\d+\\.\\s+/.test(ln)){\n const buf=[];\n while(i<lines.length&&/^\\s*\\d+\\.\\s+/.test(lines[i])){buf.push(`<li>${mdInline(lines[i].replace(/^\\s*\\d+\\.\\s+/,''))}</li>`);i++;}\n html+=flush(buf,'ol');continue;\n }\n if(!ln.trim()){i++;continue;}\n const buf=[];\n while(i<lines.length&&lines[i].trim()&&!/^(#{1,3}\\s|\\s*[-*]\\s|\\s*\\d+\\.\\s)/.test(lines[i])){buf.push(lines[i]);i++;}\n html+=`<p>${mdInline(buf.join(' '))}</p>`;\n }\n return html;\n }\n function anchorHtml(a){\n const parts=a.path.split('/'); const file=parts.pop();\n const dir=parts.length?parts.join('/')+'/':'';\n return `<div class=\"anchor\">\n <div class=\"anchor-top\">\n <span class=\"apath\"><span class=\"p\">${esc(dir)}</span>${esc(file)}</span>\n <span class=\"astate ${a.state}\">${a.state}</span>\n <button class=\"copy\" data-copy=\"${esc(a.path)}\">copy</button>\n </div>\n ${(a.symbols&&a.symbols.length)?`<div class=\"syms\">${a.symbols.map(s=>`<button class=\"sym\" data-copysym=\"${esc(s)}\" title=\"click to copy symbol\">${esc(s)}</button>`).join('')}</div>`:''}\n </div>`;\n }\n function linkBtn(id){\n const n=byId.get(id);\n return `<button class=\"linkbtn\" data-goto=\"${esc(id)}\"><span class=\"kind ${n.type}\">${n.type.toUpperCase()}</span><span>${esc(n.title)}</span></button>`;\n }\n function renderNote(id){\n const n=byId.get(id); if(!n) return;\n state.selected=id;\n // reveal once: persist the file's ancestor folders so they stay open\n // (and prior folders stay open too) — you can still collapse any by hand\n if(n.type==='file') for(const p of ancestorsOf(id)) state.open.add(p);\n const outs=n.outLinks.filter(l=>byId.has(l));\n const r=$('#reader');\n r.innerHTML=`<div class=\"reader-inner\">\n <div class=\"eyebrow\">\n <span class=\"kind ${n.type}\">${n.type.toUpperCase()}${n.character?' · '+n.character:''}</span>\n <span class=\"sep\">/</span><span class=\"id\">${esc(n.id)}</span>\n </div>\n <h1 class=\"note-title\">${esc(n.title)}</h1>\n <div class=\"metabar\">\n <span class=\"pill ${n.freshness}\"><span class=\"d\"></span>${n.freshness}</span>\n <span class=\"mi\"><b>${n.anchors.length}</b> anchor${n.anchors.length===1?'':'s'}</span>\n <span class=\"mi\"><b>${n.edits}</b> edit${n.edits===1?'':'s'}</span>\n <span class=\"mi\">updated <b>${esc((n.updated||'').slice(0,10))}</b></span>\n </div>\n <div class=\"sec-label\">Anchors — verified code addresses</div>\n ${n.anchors.length?n.anchors.map(anchorHtml).join(''):'<div class=\"empty-note\">No anchors.</div>'}\n ${n.aliases.length?`<details class=\"aliases\" style=\"margin-top:14px\">\n <summary><b>${n.aliases.length}</b> search aliases <span style=\"color:var(--faint)\">— queries that should surface this note</span><span class=\"chv\">›</span></summary>\n <div class=\"alias-cloud\">${n.aliases.map(a=>`<span>${esc(a)}</span>`).join('')}</div>\n </details>`:''}\n <div class=\"sec-label\">Note</div>\n <div class=\"md\">${renderMd(n.body)}</div>\n <div class=\"links-grid\">\n <div class=\"linkcol\"><div class=\"sec-label\">Links to (${outs.length})</div>\n ${outs.length?outs.map(linkBtn).join(''):'<div class=\"empty-note\">No outgoing links.</div>'}</div>\n <div class=\"linkcol\"><div class=\"sec-label\">Referenced by (${n.backlinks.length})</div>\n ${n.backlinks.length?n.backlinks.map(linkBtn).join(''):'<div class=\"empty-note\">Not referenced.</div>'}</div>\n </div>\n </div>`;\n r.scrollTop=0;\n // switch lens to match the note type so it's visible in the rail\n if(n.type==='flow'&&state.lens!=='flows') setLens('flows');\n else if(n.type==='file'&&state.lens!=='files') setLens('files');\n else renderList();\n }\n\n // ---- events ----\n $('#q').addEventListener('input',e=>{state.q=e.target.value;renderList();});\n $('.lenses').addEventListener('click',e=>{const b=e.target.closest('.lens');if(b)setLens(b.dataset.lens);});\n function setLens(l){\n state.lens=l;\n document.querySelectorAll('.lens').forEach(x=>x.setAttribute('aria-pressed',x.dataset.lens===l));\n renderList();\n }\n chip.addEventListener('click',()=>{state.staleOnly=!state.staleOnly;chip.setAttribute('aria-pressed',state.staleOnly);renderList();});\n $('#treeTools').addEventListener('click',e=>{\n const b=e.target.closest('button');if(!b)return;\n if(b.dataset.tt==='collapse'){state.open.clear();}\n else{ // expand all dirs\n const walk=nd=>{for(const c of nd.children.values()){state.open.add(c.path);walk(c);}};walk(TREE);\n }\n renderFiles();\n });\n document.addEventListener('click',e=>{\n const dir=e.target.closest('[data-dir]');\n if(dir){const p=dir.dataset.dir;state.open.has(p)?state.open.delete(p):state.open.add(p);renderFiles();return;}\n const row=e.target.closest('[data-id]');\n if(row){switchView('notes');renderNote(row.dataset.id);closeRail();return;}\n const goto=e.target.closest('[data-goto]');\n if(goto){switchView('notes');renderNote(goto.dataset.goto);return;}\n const cs=e.target.closest('[data-copysym]');\n if(cs){copyText(cs.dataset.copysym);cs.classList.add('copied');setTimeout(()=>cs.classList.remove('copied'),750);return;}\n const cp=e.target.closest('[data-copy]');\n if(cp){copyText(cp.dataset.copy);cp.textContent='copied';setTimeout(()=>cp.textContent='copy',900);return;}\n });\n\n // ---- view switching ----\n function switchView(v){\n state.view=v;\n $('#tabNotes').setAttribute('aria-pressed',v==='notes');\n $('#tabGraph').setAttribute('aria-pressed',v==='graph');\n $('#split').classList.toggle('graph-mode',v==='graph');\n $('#reader').style.display=v==='graph'?'none':'';\n $('#graphWrap').style.display=v==='graph'?'':'none';\n if(v==='graph') drawGraph();\n }\n $('#tabNotes').onclick=()=>switchView('notes');\n $('#tabGraph').onclick=()=>switchView('graph');\n function closeRail(){$('#split').classList.remove('rail-open');}\n $('#menuBtn').onclick=()=>$('#split').classList.toggle('rail-open');\n\n // ================= GRAPH =================\n let G=null;\n function buildGraph(){\n const linked=new Set(); const edges=[];\n for(const n of notes) for(const l of n.outLinks) if(byId.has(l)&&l!==n.id){linked.add(n.id);linked.add(l);edges.push([n.id,l]);}\n const ids=[...linked]; const idx=new Map(ids.map((id,i)=>[id,i])); const N=ids.length;\n const nodes=ids.map((id,i)=>{const a=2*Math.PI*i/N;\n return {id,x:Math.cos(a)*260,y:Math.sin(a)*260,vx:0,vy:0,deg:0,type:byId.get(id).type,title:byId.get(id).title};});\n const E=edges.map(([a,b])=>[idx.get(a),idx.get(b)]);\n for(const [a,b] of E){nodes[a].deg++;nodes[b].deg++;}\n for(let it=0;it<320;it++){\n const k=it<220?1:0.4;\n for(let i=0;i<N;i++){let fx=0,fy=0;\n for(let j=0;j<N;j++){if(i===j)continue;\n let dx=nodes[i].x-nodes[j].x,dy=nodes[i].y-nodes[j].y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2);\n const rep=1600/d2; fx+=dx/d*rep; fy+=dy/d*rep;}\n fx-=nodes[i].x*0.012; fy-=nodes[i].y*0.012;\n nodes[i].vx=(nodes[i].vx+fx)*0.82; nodes[i].vy=(nodes[i].vy+fy)*0.82;}\n for(const [a,b] of E){let dx=nodes[b].x-nodes[a].x,dy=nodes[b].y-nodes[a].y,d=Math.sqrt(dx*dx+dy*dy)+0.01;\n const spring=(d-90)*0.02,ux=dx/d*spring,uy=dy/d*spring;\n nodes[a].vx+=ux;nodes[a].vy+=uy;nodes[b].vx-=ux;nodes[b].vy-=uy;}\n for(const nd of nodes){nd.x+=nd.vx*k;nd.y+=nd.vy*k;}\n }\n G={nodes,E,idx};\n $('#graphHint').textContent=`${N} linked notes · ${edges.length} links. Click a node to open it.`;\n }\n let cam={x:0,y:0,s:1},hover=-1;\n function drawGraph(){\n if(!G) buildGraph();\n const cv=$('#graphCanvas'),wrap=$('#graphWrap'),dpr=window.devicePixelRatio||1;\n const W=wrap.clientWidth,H=wrap.clientHeight;\n cv.width=W*dpr;cv.height=H*dpr;\n const ctx=cv.getContext('2d');ctx.setTransform(dpr,0,0,dpr,0,0);\n const xs=G.nodes.map(n=>n.x),ys=G.nodes.map(n=>n.y);\n const minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),pad=70;\n const s=Math.min((W-pad*2)/(maxX-minX||1),(H-pad*2)/(maxY-minY||1));\n cam.s=s;cam.x=W/2-(minX+maxX)/2*s;cam.y=H/2-(minY+maxY)/2*s;\n const T=(x,y)=>[x*cam.s+cam.x,y*cam.s+cam.y];\n ctx.clearRect(0,0,W,H);\n for(const [a,b] of G.E){const A=G.nodes[a],B=G.nodes[b],[ax,ay]=T(A.x,A.y),[bx,by]=T(B.x,B.y);\n const on=hover>-1&&(a===hover||b===hover);\n ctx.strokeStyle=on?'rgba(31,79,216,.55)':'rgba(105,116,138,.20)';ctx.lineWidth=on?1.8:1;\n ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.stroke();}\n const cs=getComputedStyle(document.documentElement);\n const cFile=cs.getPropertyValue('--file').trim(),cFlow=cs.getPropertyValue('--flow').trim();\n G.nodes.forEach((n,i)=>{const[x,y]=T(n.x,n.y),r=Math.max(4,Math.min(13,4+n.deg*1.6)),isH=i===hover;\n ctx.beginPath();ctx.arc(x,y,r+(isH?2:0),0,7);ctx.fillStyle=n.type==='flow'?cFlow:cFile;\n ctx.globalAlpha=hover>-1&&!isH&&!G.E.some(([a,b])=>(a===hover&&b===i)||(b===hover&&a===i))?0.35:1;\n ctx.fill();if(isH){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();}ctx.globalAlpha=1;});\n G._T=T;\n }\n const cv=$('#graphCanvas');\n function pick(mx,my){if(!G||!G._T)return -1;let best=-1,bd=18*18;\n G.nodes.forEach((n,i)=>{const[x,y]=G._T(n.x,n.y),d=(x-mx)**2+(y-my)**2,r=Math.max(4,Math.min(13,4+n.deg*1.6))+4;\n if(d<r*r&&d<bd){bd=d;best=i;}});return best;}\n cv.addEventListener('mousemove',e=>{const rect=cv.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,p=pick(mx,my),tip=$('#graphTip');\n if(p!==hover){hover=p;drawGraph();}\n if(p>-1){const n=G.nodes[p];tip.style.opacity=1;tip.style.left=mx+'px';tip.style.top=my+'px';\n tip.innerHTML=`<div class=\"tk\">${n.type} · ${n.deg} link${n.deg===1?'':'s'}</div>${esc(n.title)}`;cv.style.cursor='pointer';}\n else{tip.style.opacity=0;cv.style.cursor='default';}});\n cv.addEventListener('mouseleave',()=>{hover=-1;$('#graphTip').style.opacity=0;drawGraph();});\n cv.addEventListener('click',e=>{const rect=cv.getBoundingClientRect(),p=pick(e.clientX-rect.left,e.clientY-rect.top);\n if(p>-1){switchView('notes');renderNote(G.nodes[p].id);}});\n window.addEventListener('resize',()=>{if(state.view==='graph')drawGraph();});\n\n // ---- boot ----\n // pre-open the top two directory levels so the Files tree opens usefully\n (function preopen(node,depth){for(const c of node.children.values()){if(depth<2)state.open.add(c.path);preopen(c,depth+1);}})(TREE,0);\n // land on the richest FILE note; the Flows tab (badged) is one click away\n const seed=[...fileNotes].sort((a,b)=>(b.outLinks.length+b.backlinks.length)-(a.outLinks.length+a.backlinks.length))[0]\n || notes[0];\n renderList();\n if(seed) renderNote(seed.id);\n})();\n</script>\n";
8
9
  //# sourceMappingURL=view-template.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"view-template.js","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,m5sCAAm5sC,CAAC"}
1
+ {"version":3,"file":"view-template.js","sourceRoot":"","sources":["../../src/kb/view-template.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,i4sCAAi4sC,CAAC"}
package/dist/kb/view.d.ts CHANGED
@@ -30,7 +30,10 @@ export declare function buildViewData(root: string, generated: string): {
30
30
  backlinks: string[];
31
31
  }[];
32
32
  };
33
- /** Bake data into the template. `</` is neutralized so note text can't break the script tag. */
33
+ /** Bake data into the template. `</` is neutralized so note text can't break the
34
+ * script tag; the tab title is scoped to the repo already carried in the data
35
+ * (`summary.repo`, = basename(root)) so every notebook's view is named for its
36
+ * own repo rather than a hardcoded placeholder. */
34
37
  export declare function renderViewHtml(template: string, data: unknown): string;
35
38
  export interface ViewOptions {
36
39
  open?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AA4CjE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;mBA3BvC,UAAU;;;;;;mBAsBX,MAAM,EAAE;;EA2B5B;AAED,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAGtE;AAyBD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;0EAQ0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAYhF"}
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAkBA,KAAK,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AA4CjE,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;mBA3BvC,UAAU;;;;;;mBAsBX,MAAM,EAAE;;EA2B5B;AAOD;;;oDAGoD;AACpD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAStE;AAyBD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;0EAQ0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAYhF"}
package/dist/kb/view.js CHANGED
@@ -77,10 +77,23 @@ export function buildViewData(root, generated) {
77
77
  }
78
78
  return { summary: { total: notes.length, byType, byFreshness, repo: basename(root), generated }, notes };
79
79
  }
80
- /** Bake data into the template. `</` is neutralized so note text can't break the script tag. */
80
+ /** Escape the three chars that matter inside an HTML text node (here, <title>). */
81
+ function escapeHtml(s) {
82
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
83
+ }
84
+ /** Bake data into the template. `</` is neutralized so note text can't break the
85
+ * script tag; the tab title is scoped to the repo already carried in the data
86
+ * (`summary.repo`, = basename(root)) so every notebook's view is named for its
87
+ * own repo rather than a hardcoded placeholder. */
81
88
  export function renderViewHtml(template, data) {
82
89
  const json = JSON.stringify(data).replace(/<\//g, '<\\/');
83
- return template.replace('__DATA_JSON__', () => json);
90
+ const repo = data?.summary?.repo;
91
+ const title = typeof repo === 'string' && repo.trim()
92
+ ? `Coldstart Notebook — ${escapeHtml(repo)}`
93
+ : 'Coldstart Notebook';
94
+ return template
95
+ .replace('__TITLE__', () => title)
96
+ .replace('__DATA_JSON__', () => json);
84
97
  }
85
98
  /** Best-effort open in the OS default browser. Never throws. */
86
99
  function openInBrowser(file) {
@@ -1 +1 @@
1
- {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAI9C,MAAM,IAAI,GAA+B,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAE7F,kFAAkF;AAClF,SAAS,YAAY,CAAC,IAAgB;IACpC,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACvD,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACzB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC,CAAC,sCAAsC;IAChF,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;AACrB,CAAC;AAED,mFAAmF;AACnF,SAAS,YAAY,CAAC,IAAY,EAAE,IAAgB;IAClD,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAmB;KAC7B,CAAC,CAAC,CAAC;IACJ,IAAI,IAAI,GAAe,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;IAC/D,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;YAAE,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC;IAExE,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAExF,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;QACjC,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;QAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;QAC3B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC;QACtB,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,IAAI;QACf,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc;QAC3D,QAAQ;QACR,IAAI;QACJ,SAAS,EAAE,EAAc;KAC1B,CAAC;AACJ,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,SAAiB;IAC3D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC;SACvC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACtB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAE,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC;IAEvE,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,WAAW,GAA2B,EAAE,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3C,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC;AAC3G,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,IAAa;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1D,OAAO,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AACvD,CAAC;AAED,gEAAgE;AAChE,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,GAAG,GACP,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE;QAC7D,CAAC,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE;YAChF,CAAC,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACpD,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC,CAAC,6CAA6C,CAAC,CAAC;AAC3D,CAAC;AAED,wEAAwE;AACxE,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,YAAY,CAAC,EAAE,CAAC;YAC5D,aAAa,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC;QACtF,CAAC;IACH,CAAC;IAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC;AAC7B,CAAC;AAOD;;;;;;;;0EAQ0E;AAC1E,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,QAAgB,EAAE,IAAiB;IACtE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,YAAY,CAAC,CAAC;IACtD,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,CAAC;QACH,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC,CAAC,yCAAyC,CAAC,CAAC;IACrD,IAAI,CAAC,SAAS;QAAE,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC7C,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;QAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/kb/view.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstart/coldstart",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
4
4
  "mcpName": "io.github.AkashGoenka/coldstart",
5
5
  "publishConfig": {
6
6
  "access": "public"