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>
This commit is contained in:
113
docs/design/ui_kits/app/Shell.js.txt
Normal file
113
docs/design/ui_kits/app/Shell.js.txt
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 });
|
||||
Reference in New Issue
Block a user