// App chrome — sidebar, top bar, persona switcher, search, notifications.
const NAV = [
{ id: 'home', label: 'Home', icon: 'home' },
{ id: 'portfolio', label: 'Portfolio', icon: 'portfolio' }, { id: 'initiatives', label: 'Initiatives', icon: 'initiatives' },
{ id: 'projects', label: 'Projects', icon: 'projects' },
{ id: 'ideas', label: 'Ideas', icon: 'ideas' },
{ id: 'components', label: 'Components', icon: 'components' },
{ id: 'tickets', label: 'Tickets', icon: 'tickets' },
{ id: 'messages', label: 'Messages', icon: 'bell' },
{ id: 'help', label: 'Help Board', icon: 'help' },
{ id: 'admin', label: 'Admin', icon: 'admin' },
];
// ── Sidebar ──────────────────────────────────────────────────────────
const Sidebar = ({ route, onNavigate, persona, counts = {} }) => {
const [menuOpen, setMenuOpen] = React.useState(false);
const menuRef = React.useRef(null);
React.useEffect(() => {
if (!menuOpen) return;
const handleClick = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target)) {
setMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [menuOpen]);
return (
);
};
// ── Top bar ──────────────────────────────────────────────────────────
const TopBar = ({ breadcrumbs = [], onSearch, onChat, onNew, onMessages, onProfile, persona, msgCount = 0 }) => {
return (
{/* breadcrumbs */}
{breadcrumbs.map((b, i) => (
{i > 0 && }
))}
{/* search */}
{/* chat */}
} onClick={onChat}>
Ask Claude
{/* new */}
} onClick={onNew}>
New
{/* notifications / messages */}
{/* user profile */}
);
};
// ── Page shell (used by every page) ─────────────────────────────────
const Page = ({ children, max = 1280, pad = true }) => (
{children}
);
const PageHeader = ({ kicker, title, subtitle, action, meta }) => (
{kicker && (
{kicker}
)}
{title}
{subtitle && (
{subtitle}
)}
{meta &&
{meta}
}
{action &&
{action}
}
);
const AppShell = ({ persona, route, goto, openChat, openCompose, setTweak, children }) => {
const crumbs = (() => {
if (!route || route.name === 'home') return [{ label: 'Home' }];
const cap = s => s[0].toUpperCase() + s.slice(1);
const out = [{ label: cap(route.name), onClick: () => goto(route.name) }];
if (route.id) {
let nm = route.id;
if (route.name === 'project' || route.name === 'projects') nm = (PROJ_BY_ID[route.id]||{}).name;
else if (route.name === 'initiative' || route.name === 'initiatives') nm = (INI_BY_ID[route.id]||{}).name;
else if (route.name === 'components') nm = (COMP_BY_ID[route.id]||{}).name;
else if (route.name === 'ideas') nm = (APM_IDEAS.find(i => i.id === route.id)||{}).title;
if (nm) out.push({ label: nm });
}
return out;
})();
const counts = {
tickets: APM_TICKETS.filter(t => t.status === 'open').length,
ideas: APM_IDEAS.filter(i => i.status === 'new').length,
help: APM_HELP.filter(h => h.status === 'open').length,
messages: APM_TICKETS.filter(t => t.assignee === persona.id).length
+ (persona.role === 'owner' ? 3 : 0),
};
return (
{}}
onChat={openChat}
onNew={openCompose}
onMessages={() => goto('messages')}
onProfile={() => goto('profile')}
persona={persona}
msgCount={counts.messages}
setTweak={setTweak}
/>
{children}
);
};
Object.assign(window, { Sidebar, TopBar, Page, PageHeader, NAV, AppShell });