/* ─────────────────────────────────────────────────────────────
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-project-root.html', t:'Project Root', s:'根目录概念 · 三种模式 · 创建入口' },
{ f:'arch-tool-registry.html', t:'Tool Registry', s:'79 个工具的 JS 实现层' },
{ f:'arch-acp-bridge.html', t:'ACP Bridge', s:'LoopController ↔ agent-manager' },
{ f:'arch-git-versioning.html',t:'Git Versioning', s:'每项目 git repo + 自动 commit' },
]},
]},
{ 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}
›
${section.groups.map(g => {
const groupActive = g.pages.some(p => p.f === currentPage);
return `
`;
}).join('')}
`;
}).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 ? `← Previous${prev.t}` : ``) +
(next ? `Next →${next.t}` : ``);
}
// ── 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);
}
}
})();