/* ───────────────────────────────────────────────────────────── LemonBoss · NewFunction PRD — Shared Shell JS v2 3-level sidebar (Section → Subgroup → Page) + 全局搜索 ───────────────────────────────────────────────────────────── */ (function(){ // ── Site map (3-level: section → subgroup → page) ── const SITE = [ { id:'overview', label:'OVERVIEW · 总览', groups:[ { label:'Introduction', pages:[ { f:'index.html', t:'封面 · Hero', s:'三层 Loop 一图速览' }, { f:'reading-guide.html', t:'阅读指南', s:'人 + Claude Code 两套读法' }, ]}, ]}, { id:'prd', label:'PRD · 产品需求', groups:[ { label:'Foundation', pages:[ { f:'prd-background.html', t:'背景 / 问题陈述', s:'为什么要做这个' }, { f:'prd-personas.html', t:'角色 · User Stories', s:'三类用户 + 故事卡' }, { f:'requirement.html', t:'原始需求 Verbatim', s:'人类原话 + 解析' }, ]}, { label:'Specification', pages:[ { f:'scope.html', t:'范围与原则', s:'in / out / 原则' }, { f:'prd-feature-tree.html',t:'功能树', s:'4 级树状结构' }, { f:'prd-user-flows.html', t:'用户流程图', s:'3 个核心 user flow' }, { f:'prd-success-metrics.html', t:'成功指标 · KPI', s:'P0 验收 + 长期指标' }, ]}, { label:'Reference', pages:[ { f:'prd-glossary.html', t:'术语表', s:'30 个核心词' }, ]}, ]}, { id:'design', label:'DESIGN · 系统设计', groups:[ { label:'Loops', pages:[ { f:'loops-overview.html', t:'三层 Loop 总览', s:'L1 ⊂ L2 ⊂ L3' }, { f:'loops-l1.html', t:'L1 · Unit Loop', s:'代码 · 单测 · 重试' }, { f:'loops-l2.html', t:'L2 · Integration', s:'集成 · 归因 · 回写' }, { f:'loops-l3.html', t:'L3 · Human', s:'新需求 · 增量重跑' }, ]}, { label:'Agents', pages:[ { f:'agents-overview.html', t:'六个 Agent 总览', s:'职责矩阵 · 调用关系' }, { f:'agents-pm.html', t:'PM Agent', s:'需求 → PRD' }, { f:'agents-architect.html',t:'Architect Agent', s:'PRD → SDD + units.json' }, { f:'agents-coder.html', t:'Coder Agent', s:'写前后端代码' }, { f:'agents-tester.html', t:'Tester Agent', s:'写 / 跑 Unit Test' }, { f:'agents-integrator.html',t:'Integrator Agent', s:'跑集成测试' }, { f:'agents-triager.html', t:'BugTriager Agent', s:'集成失败归因' }, ]}, ]}, { id:'sdd', label:'SDD · MVCP 设计', groups:[ { label:'Overview', pages:[ { f:'sdd-overview.html', t:'MVCP 总览', s:'四象限 + 机械化抽取' }, ]}, { label:'MVCP', pages:[ { f:'sdd-model.html', t:'M · Model', s:'15 条数据结构' }, { f:'sdd-view.html', t:'V · View', s:'11 个组件签名' }, { f:'sdd-control.html', t:'C · Control', s:'17 个 API / 服务方法' }, { f:'sdd-process.html', t:'P · Process', s:'9 个长进程 / 状态机' }, ]}, ]}, { id:'proto', label:'PROTOTYPE · 原型', groups:[ { label:'Pages', pages:[ { f:'ui-dashboard.html', t:'主看板 · Dashboard', s:'4 个核心区域' }, { f:'proto-feedback-modal.html', t:'反馈模态框', s:'+ NEW REQUIREMENT' }, { f:'proto-unit-drawer.html', t:'Unit 详情抽屉', s:'最近 3 次 run' }, { f:'proto-prd-diff.html', t:'PRD diff 面板', s:'section 级差异' }, ]}, { label:'System', pages:[ { f:'ui-design-system.html',t:'视觉设计语言', s:'色板 · 字号 · 排版' }, ]}, ]}, { id:'arch', label:'ARCHITECTURE · 架构', groups:[ { label:'Mapping', pages:[ { f:'arch-mapping.html', t:'对 LemonBoss 的映射', s:'REUSE / NEW / PATCH' }, { f:'arch-components.html', t:'三个新组件', s:'LoopController · DocStore · UnitRegistry' }, ]}, { label:'Runtime', pages:[ { f:'arch-workflow.html', t:'运行时工作流', s:'端到端时序图' }, { f:'data-layout.html', t:'文件空间布局', s:'data/companies/.../projects/' }, { f:'data-documents.html', t:'四类文档关系', s:'PRD / SDD / units / runs' }, ]}, { label:'Implementation', pages:[ { f:'arch-tool-registry.html', t:'Tool Registry', s:'79 个工具的 JS 实现层' }, { f:'arch-acp-bridge.html', t:'ACP Bridge', s:'LoopController ↔ agent-manager' }, ]}, ]}, { id:'api', label:'API · 接口规范', groups:[ { label:'Specs', pages:[ { f:'api.html', t:'REST 端点 + 骨架', s:'/api/devloop/*' }, { f:'api-sse.html', t:'SSE 事件总线', s:'15 类事件完整规格' }, { f:'api-contracts.html', t:'Unit Contract 规范', s:'契约 JSON schema' }, ]}, ]}, { id:'ops', label:'OPS · 运行视图', groups:[ { label:'Runtime Views', pages:[ { f:'ops-progress.html', t:'Progress', s:'项目实时进度' }, { f:'ops-loop-log.html', t:'Loop Log', s:'L1/L2/L3 时间线' }, { f:'ops-buglist.html', t:'Buglist', s:'跨 run bug 台账' }, { f:'ops-changelog.html', t:'Changelog', s:'版本变更流' }, ]}, ]}, { id:'plan', label:'PLAN · 实施与复现', groups:[ { label:'Implementation', pages:[ { f:'plan-migration.html', t:'最小改造路径', s:'P0 → P4 五阶段' }, ]}, { label:'Operations', pages:[ { f:'plan-cost-concurrency.html', t:'成本 & 并发预算', s:'Token / $ / 墙钟 / 并发上限' }, { f:'plan-code-lifecycle.html', t:'代码归宿', s:'生成代码留沙箱 / export / PR' }, ]}, { label:'Quality & Verification', pages:[ { f:'plan-risks.html', t:'风险与待决', s:'7 个风险点' }, { f:'plan-reproducibility.html', t:'Claude Code 复现指南', s:'一个 prompt 重建项目' }, ]}, ]}, ]; // Flat list for prev/next + search resolution const FLAT = SITE.flatMap(sec => sec.groups.flatMap(g => g.pages.map(p => ({ ...p, section:sec.id, sectionLabel:sec.label, group:g.label })) )); const currentPage = (document.body.dataset.page || 'index.html').trim(); // ── Build sidebar (3-level) ── const sidebar = document.getElementById('sidebar'); if (sidebar){ sidebar.classList.add('sidebar'); sidebar.innerHTML = ` LemonBoss · PRD NewFunction · v0.3 ${SITE.map((section, sIx) => { const sectionActive = section.groups.some(g => g.pages.some(p => p.f === currentPage)); return `
${String(sIx+1).padStart(2,'0')}${section.label} ›
`; }).join('')} `; // Level 1 (Section) toggle sidebar.querySelectorAll('.section-group > .group-head').forEach(head => { head.addEventListener('click', () => { const g = head.parentElement; g.dataset.open = g.dataset.open === 'true' ? 'false' : 'true'; }); }); // Level 2 (Subgroup) toggle sidebar.querySelectorAll('.subgroup > .sub-head').forEach(head => { head.addEventListener('click', () => { const g = head.parentElement; g.dataset.open = g.dataset.open === 'true' ? 'false' : 'true'; }); }); } // ── Prev / Next ── const idx = FLAT.findIndex(p => p.f === currentPage); const prev = idx > 0 ? FLAT[idx-1] : null; const next = idx >= 0 && idx < FLAT.length-1 ? FLAT[idx+1] : null; const navEl = document.querySelector('nav.prev-next'); if (navEl){ navEl.innerHTML = (prev ? `` : ``) + (next ? `` : ``); } // ── Tabs (page-level content) ── document.querySelectorAll('[data-tabs]').forEach(root => { const buttons = root.querySelectorAll('.tabbar button'); const panels = root.querySelectorAll('.tabpanel'); buttons.forEach((btn, i) => btn.addEventListener('click', () => { buttons.forEach(b => b.setAttribute('aria-selected','false')); panels.forEach(p => p.hidden = true); btn.setAttribute('aria-selected','true'); panels[i].hidden = false; })); }); // ── Search ── const shell = document.querySelector('.shell'); const main = document.querySelector('main.page'); const input = document.getElementById('sbSearchInput'); const clearB = document.getElementById('sbClearBtn'); const searchBox = document.querySelector('.sb-search'); let panel = null; let searchIndex = null; let indexLoading = null; function ensurePanel(){ if (panel) return panel; panel = document.createElement('aside'); panel.className = 'search-panel'; panel.innerHTML = `
Search Results
`; sidebar.parentElement.insertBefore(panel, main); panel.querySelector('#spClose').addEventListener('click', closeSearch); return panel; } async function loadIndex(){ if (searchIndex) return searchIndex; if (indexLoading) return indexLoading; indexLoading = fetch('assets/search-index.json') .then(r => r.ok ? r.json() : { entries: [] }) .then(j => { searchIndex = j.entries || []; return searchIndex; }) .catch(() => { searchIndex = []; return searchIndex; }); return indexLoading; } function highlight(text, query){ if (!query) return text; const re = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi'); return text.replace(re, '$1'); } function snippet(content, query, len=180){ if (!content) return ''; const lc = content.toLowerCase(); const ix = lc.indexOf(query.toLowerCase()); if (ix < 0) return content.slice(0, len) + (content.length>len?'…':''); const start = Math.max(0, ix - 40); const end = Math.min(content.length, ix + query.length + 140); return (start>0?'…':'') + content.slice(start, end) + (end p.f === f); } async function doSearch(q){ q = q.trim(); if (!q){ closeSearch(); return; } shell.classList.add('search-open'); ensurePanel(); const idx2 = await loadIndex(); const ql = q.toLowerCase(); const results = []; for (const e of idx2){ let hits = 0; const fields = [e.title, e.content, ...(e.headings||[])]; for (const f of fields) if (f && f.toLowerCase().includes(ql)) hits++; if (hits){ results.push({ ...e, hits, snippet: snippet(e.content, q) }); } } results.sort((a,b) => b.hits - a.hits); const panelEl = ensurePanel(); panelEl.querySelector('#spCount').textContent = results.length + ' 条'; const listEl = panelEl.querySelector('#spList'); if (results.length === 0){ listEl.innerHTML = `
没找到 "${q}"
试试别的关键词,或检查拼写
`; return; } listEl.innerHTML = results.slice(0, 50).map(r => { const meta = pageMeta(r.f) || { sectionLabel:'', group:'' }; const url = encodeURI(r.f) + '?q=' + encodeURIComponent(q); return `
${meta.sectionLabel} ${meta.group ? '· '+meta.group : ''} ${r.hits} hit${r.hits>1?'s':''}
${highlight(r.title, q)}
${highlight(r.snippet, q)}
`; }).join(''); listEl.querySelectorAll('.sp-result').forEach(el => { el.addEventListener('click', e => { if (e.target.tagName === 'A') return; window.location.href = el.dataset.href; }); }); } function closeSearch(){ shell.classList.remove('search-open'); if (input) { input.value=''; searchBox.classList.remove('has-text'); } } if (input){ let timer = null; input.addEventListener('input', e => { const v = e.target.value; searchBox.classList.toggle('has-text', !!v.trim()); clearTimeout(timer); timer = setTimeout(() => doSearch(v), 180); }); clearB.addEventListener('click', closeSearch); document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'k'){ e.preventDefault(); input.focus(); input.select(); } else if (e.key === 'Escape' && shell.classList.contains('search-open')){ closeSearch(); } }); } // ── On-page highlight if arrived from search (?q=...) ── const params = new URLSearchParams(location.search); const q = params.get('q'); if (q){ const article = document.querySelector('article.body') || document.body; const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT); const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); let firstHit = null; const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); for (const n of nodes){ if (n.parentElement && /SCRIPT|STYLE|MARK/i.test(n.parentElement.tagName)) continue; if (re.test(n.textContent)){ const span = document.createElement('span'); span.innerHTML = n.textContent.replace(new RegExp('('+q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')+')','gi'), '$1'); n.parentNode.replaceChild(span, n); if (!firstHit) firstHit = span; } } if (firstHit){ setTimeout(() => firstHit.scrollIntoView({ behavior:'smooth', block:'center' }), 100); } } })();