Files
commitea/docs/design/ui_kits/app/Shell.js.txt
Christian LeDoux 7a5cacc54c Scaffold CommiTea: yarn workspaces, Electron shell, core label schema, design system
- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens
  (preflight off; tokens/base.css owns the reset); boots to a Reginald
  placeholder proving fonts/tokens/core wiring
- packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard)
  with pessimistic conflict resolution + 15 unit tests
- docs/design: full design handoff (tokens, 16 component contracts,
  interactive 14-screen prototype, Reginald voice rules)
- docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic
  scheduler + Monte Carlo, directive log)
- Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local
  store is a rebuildable cache over gitea REST/webhooks (better-sqlite3
  in main process, arriving in P1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:42:46 -04:00

114 lines
6.3 KiB
Plaintext

// App shell — left rail + content + agent panel, issue page, theme toggle
function Shell() {
const DS = window.CommiTeaDesignSystem_20e63b;
const { Icon, Switch } = DS;
const [view, setView] = React.useState('focus');
const [prevView, setPrevView] = React.useState('focus');
const [dark, setDark] = React.useState(false);
const [issue, setIssue] = React.useState(null);
const [offline, setOffline] = React.useState(false);
const [readIds, setReadIds] = React.useState([]);
const inboxUnread = window.CT_DATA.inbox.filter((n) => n.unread && !readIds.includes(n.id)).length;
const openIssue = (i) => {
setIssue(i);
if (view !== 'issue') setPrevView(view);
setView('issue');
};
React.useEffect(() => {
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
}, [dark]);
if (view === 'firstrun') {
return <window.OnboardingScreen onDone={(dest) => setView(dest)} />;
}
const NAV = [
{ id: 'standup', label: 'Standup', icon: 'sun' },
{ id: 'focus', label: 'Morning service', icon: 'coffee' },
{ id: 'inbox', label: 'Inbox', icon: 'bell', count: inboxUnread || null },
{ id: 'capture', label: 'Capture', icon: 'plus' },
{ id: 'board', label: 'The pot', icon: 'square-kanban' },
{ id: 'runway', label: 'Runway', icon: 'chart-line' },
{ id: 'directives', label: 'Directives', icon: 'flag' },
];
const NavItem = ({ item }) => {
const active = view === item.id || (view === 'issue' && prevView === item.id) || ((view === 'calibration' || view === 'milestone') && item.id === 'runway');
return (
<button
type="button"
onClick={() => setView(item.id)}
style={{
display: 'flex', alignItems: 'center', gap: 10, width: '100%',
font: `${active ? 600 : 500} 13.5px/1 var(--font-sans)`,
color: active ? 'var(--ink-1)' : 'var(--ink-2)',
background: active ? 'var(--paper-2)' : 'transparent',
border: 'none', borderRadius: 'var(--radius-2)',
padding: '9px 12px', cursor: 'pointer', textAlign: 'left',
transition: 'background var(--duration-fast) var(--ease-out)',
}}
>
<Icon name={item.icon} size={16} style={{ color: active ? 'var(--accent-text)' : 'var(--ink-3)' }} />
{item.label}
{item.count ? <span style={{ marginLeft: 'auto', font: '500 10.5px/16px var(--font-mono)', color: 'var(--accent-text)', background: 'var(--spruce-2)', borderRadius: 'var(--radius-round)', padding: '0 6px' }}>{item.count}</span> : null}
</button>
);
};
return (
<div style={{ display: 'flex', height: '100vh', minWidth: 1280, background: 'var(--surface-app)', overflow: 'hidden' }} data-screen-label={`app-${view}`}>
{/* left rail */}
<nav style={{
width: 208, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 4,
padding: '18px 12px 14px', borderRight: '1px solid var(--line-1)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '0 12px 16px' }}>
<img src="../../assets/logo-icon.png" width="24" height="24" alt="" style={{ borderRadius: 6, display: 'block' }} />
<span style={{ font: '400 21px/1 var(--font-serif-display)', color: 'var(--ink-1)' }}>
Commi<span style={{ color: 'var(--accent-text)' }}>Tea</span>
</span>
</div>
{NAV.map((n) => <NavItem key={n.id} item={n} />)}
<div style={{ borderTop: '1px solid var(--line-1)', margin: '10px 8px' }}></div>
<NavItem item={{ id: 'settings', label: 'Settings', icon: 'settings-2' }} />
<NavItem item={{ id: 'firstrun', label: 'First run', icon: 'play' }} />
<NavItem item={{ id: 'states', label: 'States', icon: 'circle-dashed' }} />
<div style={{ marginTop: 'auto', padding: '0 12px', display: 'flex', flexDirection: 'column', gap: 12 }}>
<button type="button" onClick={() => setOffline(!offline)} title="Toggle the connection (demo)" style={{
font: '400 11px var(--font-mono)', color: 'var(--ink-3)', display: 'flex', alignItems: 'center', gap: 6,
background: 'none', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left',
}}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: offline ? 'var(--danger)' : 'var(--ok)', display: 'inline-block' }}></span>
gitea.stephenmann.io
</button>
<Switch label={<span style={{ font: 'var(--text-caption)' }}>Evening service</span>} checked={dark} onChange={(e) => setDark(e.target.checked)} />
</div>
</nav>
{/* main */}
<main style={{ flex: 1, minWidth: 0, overflowY: 'auto', padding: '24px 28px' }}>
<div style={{ maxWidth: 1120, margin: '0 auto', height: view === 'board' ? '100%' : 'auto', display: 'flex', flexDirection: 'column', gap: 14 }}>
{offline ? <window.OfflineBanner /> : null}
{view === 'focus' ? <window.FocusScreen onOpenIssue={openIssue} /> : null}
{view === 'board' ? <window.BoardScreen onOpenIssue={openIssue} /> : null}
{view === 'runway' ? <window.RunwayScreen onOpenCalibration={() => setView('calibration')} onOpenMilestone={() => setView('milestone')} /> : null}
{view === 'capture' ? <window.CaptureScreen onDone={() => setView('focus')} /> : null}
{view === 'standup' ? <window.StandupScreen onBegin={() => setView('focus')} onOpenIssue={openIssue} /> : null}
{view === 'settings' ? <window.SettingsScreen dark={dark} setDark={setDark} /> : null}
{view === 'directives' ? <window.DirectivesScreen /> : null}
{view === 'issue' && issue ? <window.IssueScreen issue={issue} onBack={() => setView(prevView)} onOpenIssue={openIssue} /> : null}
{view === 'calibration' ? <window.CalibrationScreen onBack={() => setView('runway')} /> : null}
{view === 'milestone' ? <window.MilestoneScreen onBack={() => setView('runway')} onOpenIssue={openIssue} /> : null}
{view === 'states' ? <window.StatesScreen onCapture={() => setView('capture')} /> : null}
{view === 'inbox' ? <window.InboxScreen onOpenIssue={openIssue} onOpenDirectives={() => setView('directives')} readIds={readIds} setReadIds={setReadIds} /> : null}
</div>
</main>
<window.ChatPanel onOpenDirectives={() => setView('directives')} offline={offline} />
</div>
);
}
Object.assign(window, { Shell });