Mobile Phase 6: app-wide light theme + [data-theme] toggle

Ship the light palette behind a :root[data-theme="light"] switch; dark
stays the default and brand identity. A pre-paint boot script applies
localStorage.venture_crm_theme (no flash, no prefers-color-scheme), and an
app-wide toggle lives in the desktop sidebar footer + the mobile top bar,
both driven by one theme state in App.

Method keeps dark mode byte-identical: :root grew to 44 themed color slots
whose dark values equal the original literals, then 319 hex literals were
migrated to var() across the JSX inline region and the <style> block. The
StageChip is now className-based (.stage-chip--{stage}); PIPELINE_STAGE_CHIP
is removed. Every light tint (stage/recency/note/priority/reminder/money)
uses the designer's exact values from the full Claude Design export
(store.js + the four *App.dc.html DCLogic palettes), now committed as
provenance under design/_imports/2026-06-19_zip-file/ (zip + screenshots
gitignored).

Mobile surfaces + chrome are fully var-based, so mobile light is complete.
Desktop light has known rough edges (bespoke <style> shades, the legacy
off-palette .badge-* family, dark-tuned shadows) folded into a new Phase 7
design-conformance pass.

Verified: render-smoke green; a jsdom interaction harness on the authed
shell exercised the toggle (boot-dark -> light+persist+relabel -> dark);
dark-identity, theme-parity, and no-undefined-var checks all green. Not yet
checked on a real phone/browser.
This commit is contained in:
Keysat
2026-06-19 16:38:30 -05:00
parent 7f711d1fae
commit e6a89450da
21 changed files with 5521 additions and 374 deletions
@@ -0,0 +1,474 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<script src="store.js"></script>
<style>
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.rm-scroll::-webkit-scrollbar { width: 0; height: 0; }
.rm-root button, .rm-root input, .rm-root textarea { font-family: inherit; }
.rm-root {
--sans:'IBM Plex Sans','Segoe UI',sans-serif; --mono:'IBM Plex Mono',monospace;
--grad1:#1a3c5e44; --grad2:#27496b33;
--base:#0b1118; --panel:#111a27; --elev:#152233; --input:#0d1622; --hover:#1b2a3a;
--border:#263548; --bstrong:#35506a; --divider:#1c2735;
--t1:#e5edf5; --t2:#c7d3e0; --t3:#8ea2b7; --t4:#70859b;
--accent:#3b82c4; --accentlight:#93c5fd; --danger:#e06c6c; --money:#6ee7b7;
--shadow-card:0 14px 26px rgba(2,12,24,0.28), inset 0 1px 0 #ffffff07;
--nav-bg:#0d1622cc;
}
.rm-root[data-theme="light"] {
--grad1:#3b82c41c; --grad2:#27496b10;
--base:#eaeef3; --panel:#ffffff; --elev:#f4f7fb; --input:#eef2f7; --hover:#e6ecf4;
--border:#d6dde7; --bstrong:#b6c3d4; --divider:#e8edf3;
--t1:#16202c; --t2:#33414f; --t3:#5a6b7d; --t4:#84909e;
--accent:#3b82c4; --accentlight:#1f6fb8; --danger:#c0322f; --money:#057a55;
--shadow-card:0 8px 20px rgba(40,70,110,0.10), inset 0 1px 0 #ffffff;
--nav-bg:#ffffffd9;
}
</style>
</helmet>
<div class="rm-root" data-theme="{{ themeAttr }}" style="position:absolute; inset:0; background:radial-gradient(900px 460px at 12% -8%, var(--grad1), transparent 60%), radial-gradient(760px 380px at 92% -2%, var(--grad2), transparent 58%), var(--base); display:flex; flex-direction:column; font-family:var(--sans); color:var(--t1); letter-spacing:0.01em; overflow:hidden;">
<!-- status bar -->
<div style="height:46px; flex:none; display:flex; align-items:flex-end; justify-content:space-between; padding:0 24px 6px; font-family:var(--mono); font-size:13px; color:var(--t2);">
<span>9:41</span>
<span style="display:flex; gap:6px; align-items:center; font-size:11px; letter-spacing:0.02em;">5G ▮▮▮▯ 84%</span>
</div>
<!-- top bar -->
<div style="flex:none; height:52px; display:flex; align-items:center; justify-content:space-between; padding:0 16px; border-bottom:1px solid var(--border);">
<span style="font-family:var(--mono); font-weight:600; font-size:15px; letter-spacing:0.04em; color:var(--t1);">·Ten31·</span>
<div style="display:flex; align-items:center; gap:10px;">
<button onClick="{{ toggleTheme }}" aria-label="Toggle theme" style="width:36px; height:36px; border-radius:999px; border:1px solid var(--border); background:var(--elev); color:var(--t3); font-size:15px; cursor:pointer; display:flex; align-items:center; justify-content:center; line-height:1;">{{ themeIcon }}</button>
<button onClick="{{ toggleAccount }}" aria-label="Account" style="width:36px; height:36px; border-radius:999px; border:1px solid var(--bstrong); background:var(--elev); color:var(--accentlight); font-family:var(--mono); font-weight:600; font-size:13px; cursor:pointer; display:flex; align-items:center; justify-content:center;">GG</button>
</div>
</div>
<!-- title + add -->
<div style="flex:none; padding:14px 16px 12px; display:flex; align-items:flex-start; justify-content:space-between; gap:10px;">
<div style="display:flex; flex-direction:column; gap:3px;">
<span style="font-size:21px; font-weight:600; letter-spacing:-0.01em;">Reminders</span>
<span style="font-family:var(--mono); font-size:12px; color:{{ summaryColor }};">{{ summary }}</span>
</div>
<button onClick="{{ openAdd }}" aria-label="Add reminder" style="flex:none; width:44px; height:44px; border-radius:10px; border:none; background:linear-gradient(#3b82c4,#2f6ea9); color:#fff; font-size:22px; font-weight:500; line-height:1; cursor:pointer; box-shadow:0 6px 14px rgba(12,40,68,0.35);">+</button>
</div>
<!-- list -->
<div class="rm-scroll" style="flex:1; min-height:0; overflow-y:auto; padding:2px 16px 20px; display:flex; flex-direction:column; gap:18px;">
<sc-for list="{{ sections }}" as="sec" hint-placeholder-count="3">
<div>
<div style="display:flex; align-items:center; gap:8px; padding:0 2px 9px;">
<span style="width:7px; height:7px; border-radius:999px; background:{{ sec.dot }};"></span>
<span style="font-family:var(--mono); font-size:11px; font-weight:600; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3);">{{ sec.label }}</span>
<span style="font-family:var(--mono); font-size:11px; color:var(--t4);">{{ sec.count }}</span>
</div>
<div style="display:flex; flex-direction:column; gap:9px;">
<sc-for list="{{ sec.items }}" as="r" hint-placeholder-count="2">
<div style="position:relative; overflow:hidden; border-radius:10px;">
<!-- snooze reveal (swipe right) -->
<div data-act="snooze" style="position:absolute; inset:0; display:flex; align-items:center; gap:9px; padding-left:20px; border-radius:10px; background:{{ snoozeBg }}; color:{{ snoozeFg }}; opacity:0; pointer-events:none;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"></circle><path d="M12 7v5l3 2"></path></svg>
<span style="font-family:var(--mono); font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase;">Snooze</span>
</div>
<!-- complete reveal (swipe left) -->
<div data-act="done" style="position:absolute; inset:0; display:flex; align-items:center; justify-content:flex-end; gap:9px; padding-right:20px; border-radius:10px; background:{{ doneBg }}; color:{{ doneFg }}; opacity:0; pointer-events:none;">
<span style="font-family:var(--mono); font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase;">Complete</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>
</div>
<!-- draggable card -->
<div onPointerDown="{{ r.dragStart }}" onPointerMove="{{ r.dragMove }}" onPointerUp="{{ r.dragEnd }}" onPointerCancel="{{ r.dragEnd }}" style="position:relative; background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:13px 14px; box-shadow:var(--shadow-card); display:flex; flex-direction:column; gap:5px; color:var(--t1); cursor:grab; touch-action:pan-y; user-select:none; transform:translateX(0);">
<span style="font-size:15px; font-weight:500; line-height:1.3;">{{ r.note }}</span>
<span style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<span style="font-size:12px; color:var(--t3);">{{ r.org }}</span>
<span style="font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:0.04em; text-transform:uppercase; padding:2px 8px; border-radius:999px; background:{{ r.chipBg }}; color:{{ r.chipText }}; border:1px solid {{ r.chipBorder }};">{{ r.dueText }}</span>
</span>
</div>
</div>
</sc-for>
</div>
</div>
</sc-for>
<sc-if value="{{ allClear }}" hint-placeholder-val="{{ false }}">
<div style="padding:40px 20px; text-align:center; display:flex; flex-direction:column; align-items:center; gap:10px;">
<span style="font-size:26px; color:var(--money);"></span>
<span style="font-size:15px; color:var(--t2); font-weight:500;">Inbox zero</span>
<span style="font-size:13px; color:var(--t4);">No open reminders. Nice.</span>
</div>
</sc-if>
<!-- completed -->
<sc-if value="{{ hasDone }}" hint-placeholder-val="{{ false }}">
<div>
<button onClick="{{ toggleCompleted }}" style="width:100%; background:none; border:none; cursor:pointer; display:flex; align-items:center; gap:8px; padding:0 2px 9px; color:var(--t3);">
<span style="font-size:12px; width:12px; transform:rotate({{ completedRot }}deg); transition:transform 150ms;"></span>
<span style="font-family:var(--mono); font-size:11px; font-weight:600; letter-spacing:0.08em; text-transform:uppercase;">Completed</span>
<span style="font-family:var(--mono); font-size:11px; color:var(--t4);">{{ doneCount }}</span>
</button>
<sc-if value="{{ completedOpen }}" hint-placeholder-val="{{ false }}">
<div style="display:flex; flex-direction:column; gap:9px;">
<sc-for list="{{ doneItems }}" as="r" hint-placeholder-count="1">
<div style="display:flex; align-items:center; gap:12px; background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:12px 13px; opacity:0.6;">
<button onClick="{{ r.toggle }}" aria-label="Reopen" style="flex:none; width:24px; height:24px; border-radius:999px; border:2px solid var(--accent); background:var(--accent); cursor:pointer; display:flex; align-items:center; justify-content:center; color:#fff; font-size:12px; line-height:1;"></button>
<button onClick="{{ r.open }}" style="flex:1; min-width:0; text-align:left; background:none; border:none; cursor:pointer; display:flex; flex-direction:column; gap:5px; color:var(--t1);">
<span style="font-size:15px; font-weight:500; line-height:1.3; text-decoration:line-through; color:var(--t3);">{{ r.note }}</span>
<span style="font-size:12px; color:var(--t4);">{{ r.org }}</span>
</button>
</div>
</sc-for>
</div>
</sc-if>
</div>
</sc-if>
</div>
<!-- bottom tab bar -->
<div style="flex:none; display:flex; border-top:1px solid var(--border); background:var(--nav-bg); backdrop-filter:blur(8px); padding-bottom:18px;">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="4">
<button onClick="{{ t.go }}" style="flex:1; background:none; border:none; cursor:pointer; height:56px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:5px; color:{{ t.color }};">
<span style="width:20px; height:20px; display:flex; align-items:center; justify-content:center;">{{ t.icon }}</span>
<span style="font-family:var(--mono); font-size:10px; letter-spacing:0.04em;">{{ t.label }}</span>
</button>
</sc-for>
</div>
<!-- account menu -->
<sc-if value="{{ accountMenu }}" hint-placeholder-val="{{ false }}">
<div onClick="{{ closeAccount }}" style="position:absolute; inset:0; z-index:40; animation:fadeIn 120ms ease;">
<div style="position:absolute; top:96px; right:16px; width:208px; background:var(--elev); border:1px solid var(--bstrong); border-radius:10px; box-shadow:0 24px 56px rgba(1,8,17,0.5); overflow:hidden;">
<div style="padding:14px 16px; border-bottom:1px solid var(--border);">
<div style="font-size:14px; font-weight:600; color:var(--t1);">Grant Gilliam</div>
<div style="font-size:12px; color:var(--t3); margin-top:2px;">grant@ten31.xyz</div>
</div>
<div style="padding:6px;">
<div style="padding:11px 12px; border-radius:7px; font-size:14px; color:var(--t2);">Profile</div>
<div style="padding:11px 12px; border-radius:7px; font-size:14px; color:var(--danger);">Log out</div>
</div>
</div>
</div>
</sc-if>
<!-- generic sheet -->
<sc-if value="{{ sheetOpen }}" hint-placeholder-val="{{ false }}">
<div onClick="{{ closeSheet }}" style="position:absolute; inset:0; z-index:60; background:rgba(4,9,16,0.55); animation:fadeIn 150ms ease; display:flex; flex-direction:column; justify-content:flex-end;">
<div onClick="{{ stop }}" style="background:var(--panel); border-top:1px solid var(--bstrong); border-radius:20px 20px 0 0; box-shadow:0 -24px 56px rgba(1,8,17,0.4); animation:sheetUp 280ms cubic-bezier(.2,.8,.2,1); padding:0 20px 26px; max-height:90%; display:flex; flex-direction:column;">
<div style="padding:10px 0 4px; display:flex; justify-content:center; flex:none;"><div style="width:38px; height:4px; border-radius:999px; background:var(--bstrong);"></div></div>
<div style="display:flex; align-items:center; justify-content:space-between; padding:8px 0 4px; flex:none;">
<span style="font-size:18px; font-weight:600; color:var(--t1);">{{ sheetTitle }}</span>
<button onClick="{{ closeSheet }}" style="background:none; border:none; color:var(--t3); font-size:22px; cursor:pointer; line-height:1; padding:0 4px;">×</button>
</div>
<div class="rm-scroll" style="overflow-y:auto; margin-top:8px;">
{{ sheetBody }}
</div>
</div>
</div>
</sc-if>
<!-- toast -->
<sc-if value="{{ toast }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; left:16px; right:16px; bottom:92px; z-index:70; background:var(--elev); border:1px solid var(--bstrong); border-radius:10px; box-shadow:0 10px 24px rgba(4,12,22,0.35); padding:13px 16px; font-size:14px; color:var(--t1); display:flex; align-items:center; gap:10px; animation:fadeIn 150ms ease;">
<span style="color:var(--money);"></span>{{ toast }}
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:393,&quot;height&quot;:812},&quot;theme&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;dark&quot;,&quot;light&quot;],&quot;default&quot;:&quot;dark&quot;,&quot;tsType&quot;:&quot;'dark'|'light'&quot;}}">
class Component extends DCLogic {
constructor(props) {
super(props);
this.today = new Date(2026, 5, 19); // Jun 19 2026
this.state = {
theme: props.theme === 'light' ? 'light' : 'dark',
accountMenu: false,
completedOpen: false,
sheet: null,
toast: null,
};
}
componentDidMount() { if (window.T31Store) this._unsub = window.T31Store.subscribe(() => this.forceUpdate()); }
componentWillUnmount() { if (this._unsub) this._unsub(); }
seed() {
return [
{ id: 1, note: 'Resend deck — bounced', org: 'Vance & Co', orgId: 4, due: '2026-06-18', done: false },
{ id: 2, note: 'Re-engage — cold 2 weeks', org: 'Hartman Group', orgId: 6, due: '2026-06-16', done: false },
{ id: 3, note: 'IC memo due', org: 'Polaris Endowment', orgId: 5, due: '2026-06-19', done: false },
{ id: 4, note: 'Follow up after intro call', org: 'Brightseed Partners', orgId: 2, due: '2026-06-19', done: false },
{ id: 5, note: 'Share data room link', org: 'Atlas Ventures Fund', orgId: 8, due: '2026-06-20', done: false },
{ id: 6, note: 'Countersign side letter', org: 'Meridian Trust', orgId: 7, due: '2026-06-21', done: false },
{ id: 7, note: 'Send Q2 update deck', org: 'Northwall Capital', orgId: 1, due: '2026-06-24', done: false },
{ id: 8, note: 'Quarterly check-in call', org: 'Cedarline Family Office', orgId: 3, due: '2026-07-08', done: false },
{ id: 9, note: 'Thank-you note post-wire', org: 'Granite Bay LP', orgId: 10, due: '2026-06-13', done: true },
];
}
investorList() {
if (window.T31Store) return window.T31Store.investors.map(i => ({ id: i.id, name: i.name }));
return [
{ id: 1, name: 'Northwall Capital' }, { id: 2, name: 'Brightseed Partners' }, { id: 3, name: 'Cedarline Family Office' },
{ id: 4, name: 'Vance & Co' }, { id: 5, name: 'Polaris Endowment' }, { id: 6, name: 'Hartman Group' },
{ id: 7, name: 'Meridian Trust' }, { id: 8, name: 'Atlas Ventures Fund' }, { id: 10, name: 'Granite Bay LP' },
];
}
parse(iso) { const p = iso.split('-'); return new Date(+p[0], +p[1] - 1, +p[2]); }
diffDays(iso) { return Math.round((this.parse(iso) - this.today) / 86400000); }
monthDay(iso) { const m = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const d = this.parse(iso); return m[d.getMonth()] + ' ' + d.getDate(); }
urgency(iso, theme) {
const light = theme === 'light';
const d = this.diffDays(iso);
const red = light ? { t: '#c0322f', bg: '#c0322f14', bd: '#e3b4b2' } : { t: '#f87171', bg: '#f8717118', bd: '#f8717140' };
const amber = light ? { t: '#8a6c12', bg: '#e0b34122', bd: '#e4d29a' } : { t: '#e0b341', bg: '#e0b3411f', bd: '#e0b3413d' };
const blue = light ? { t: '#1f6fb8', bg: '#3b82c416', bd: '#bcd2ea' } : { t: '#93c5fd', bg: '#3b82c422', bd: '#3b82c44d' };
const grey = light ? { t: '#5a6b7d', bg: '#5a6b7d12', bd: '#d6dde7' } : { t: '#8ea2b7', bg: '#1b2a3a', bd: '#263548' };
if (d < 0) return { bucket: 0, text: (-d) + 'd overdue', c: red };
if (d === 0) return { bucket: 1, text: 'Today', c: amber };
if (d === 1) return { bucket: 2, text: 'Tomorrow', c: blue };
if (d <= 7) return { bucket: 2, text: 'in ' + d + 'd', c: blue };
return { bucket: 3, text: this.monthDay(iso), c: grey };
}
toast(msg) { this.setState({ toast: msg }); clearTimeout(this._tt); this._tt = setTimeout(() => this.setState({ toast: null }), 2000); }
dragStart(e, id) {
const card = e.currentTarget;
card.style.transition = 'none';
this._drag = { id, x0: e.clientX, card, wrap: card.parentNode, dx: 0, moved: false, cap: false };
}
dragMove(e) {
const d = this._drag; if (!d) return;
let dx = e.clientX - d.x0;
if (Math.abs(dx) > 5) d.moved = true;
if (!d.cap && Math.abs(dx) > 6) { try { d.card.setPointerCapture(e.pointerId); } catch (_) {} d.cap = true; }
dx = Math.max(-160, Math.min(160, dx));
d.dx = dx;
d.card.style.transform = 'translateX(' + dx + 'px)';
const TH = 70;
const done = d.wrap.querySelector('[data-act="done"]');
const snooze = d.wrap.querySelector('[data-act="snooze"]');
if (dx < 0) { done.style.opacity = Math.min(1, -dx / TH); snooze.style.opacity = 0; }
else if (dx > 0) { snooze.style.opacity = Math.min(1, dx / TH); done.style.opacity = 0; }
else { done.style.opacity = 0; snooze.style.opacity = 0; }
}
dragEnd(e) {
const d = this._drag; if (!d) return; this._drag = null;
const dx = d.dx, TH = 70;
const done = d.wrap.querySelector('[data-act="done"]');
const snooze = d.wrap.querySelector('[data-act="snooze"]');
const clearBg = () => { done.style.opacity = 0; snooze.style.opacity = 0; };
d.card.style.transition = 'transform 200ms ease';
const S = window.T31Store;
const r = S ? S.reminders.find(x => x.id === d.id) : null;
const inv = (r && S) ? S.investorById(r.orgId) : null;
const orgName = inv ? inv.name : '';
if (dx <= -TH) {
d.card.style.transform = 'translateX(-110%)';
const card = d.card, id = d.id;
setTimeout(() => {
card.style.transition = 'none';
card.style.transform = 'translateX(0)';
clearBg();
this.toggleDone(id);
}, 190);
} else if (dx >= TH) {
d.card.style.transform = 'translateX(0)'; clearBg();
this.setState({ sheet: { kind: 'snooze', id: d.id, org: orgName } });
} else {
d.card.style.transform = 'translateX(0)'; clearBg();
if (r) this.setState({ sheet: { kind: 'edit', id: r.id, orgId: r.orgId, note: r.note, due: r.due, org: orgName } });
}
}
toggleDone(id) {
const S = window.T31Store; const r = S && S.reminders.find(x => x.id === id);
const nowDone = r ? !r.done : true;
if (S) S.toggleReminder(id);
this.toast(nowDone ? 'Marked done' : 'Reopened');
}
setSheet(patch) { this.setState(s => ({ sheet: Object.assign({}, s.sheet, patch) })); }
renderVals() {
const s = this.state;
const theme = s.theme;
const accent = '#3b82c4';
const S = window.T31Store;
const orgName = (id) => { const inv = S && S.investorById(id); return inv ? inv.name : ''; };
const allR = (S ? S.reminders : []).map(r => Object.assign({}, r, { org: orgName(r.orgId) }));
const open = allR.filter(r => !r.done);
const done = allR.filter(r => r.done);
const overdue = open.filter(r => this.diffDays(r.due) < 0).length;
const todayN = open.filter(r => this.diffDays(r.due) === 0).length;
let summary, summaryColor;
if (open.length === 0) { summary = 'All clear'; summaryColor = 'var(--money)'; }
else { summary = overdue + ' overdue · ' + todayN + ' today · ' + open.length + ' open'; summaryColor = overdue ? (theme === 'light' ? '#c0322f' : '#f87171') : 'var(--t4)'; }
const dots = ['#f87171', '#e0b341', accent, theme === 'light' ? '#84909e' : '#70859b'];
const labels = ['Overdue', 'Today', 'This week', 'Later'];
const buckets = [[], [], [], []];
open.forEach(r => { const u = this.urgency(r.due, theme); buckets[u.bucket].push({ r, u }); });
buckets.forEach(b => b.sort((a, z) => this.diffDays(a.r.due) - this.diffDays(z.r.due)));
const mkItem = ({ r, u }) => ({
note: r.note, org: r.org,
dueText: u.text, chipBg: u.c.bg, chipText: u.c.t, chipBorder: u.c.bd,
dragStart: (e) => this.dragStart(e, r.id),
dragMove: (e) => this.dragMove(e),
dragEnd: (e) => this.dragEnd(e),
});
const sections = [];
buckets.forEach((b, i) => { if (b.length) sections.push({ label: labels[i], count: String(b.length), dot: dots[i], items: b.map(mkItem) }); });
const doneItems = done.map(r => ({ note: r.note, org: r.org, toggle: () => this.toggleDone(r.id), open: () => this.setState({ sheet: { kind: 'edit', id: r.id, orgId: r.orgId, note: r.note, due: r.due, org: r.org } }) }));
const tabs = [
{ key: 'grid', label: 'Grid' }, { key: 'pipeline', label: 'Pipeline' },
{ key: 'reminders', label: 'Reminders' }, { key: 'contacts', label: 'Contacts' },
].map(t => ({
label: t.label, color: t.key === 'reminders' ? 'var(--accent)' : 'var(--t4)',
icon: this.tabIcon(t.key, t.key === 'reminders'),
go: () => { if (window.T31Store) window.T31Store.setTab(t.key); },
}));
const sheetBody = s.sheet ? this.buildSheet(s.sheet) : null;
return {
themeAttr: theme, themeIcon: theme === 'light' ? '☾' : '☀',
toggleTheme: () => { const t = theme === 'light' ? 'dark' : 'light'; if (window.T31Store) window.T31Store.setTheme(t); this.setState({ theme: t }); },
toggleAccount: () => this.setState(st => ({ accountMenu: !st.accountMenu })),
closeAccount: () => this.setState({ accountMenu: false }),
accountMenu: s.accountMenu,
summary, summaryColor,
sections, allClear: open.length === 0 && done.length === 0 ? false : open.length === 0,
tabs,
doneBg: theme === 'light' ? '#10b98118' : '#10b9812e', doneFg: theme === 'light' ? '#057a55' : '#6ee7b7',
snoozeBg: theme === 'light' ? '#e0b34120' : '#e0b3412e', snoozeFg: theme === 'light' ? '#8a6c12' : '#e0b341',
hasDone: done.length > 0, doneCount: String(done.length), doneItems,
completedOpen: s.completedOpen, completedRot: s.completedOpen ? 90 : 0,
toggleCompleted: () => this.setState(st => ({ completedOpen: !st.completedOpen })),
openAdd: () => this.setState({ sheet: { kind: 'add', orgId: null, q: '', note: '', due: '2026-06-20' } }),
sheetOpen: !!s.sheet, sheetTitle: s.sheet ? ({ add: 'New reminder', edit: 'Edit reminder', snooze: 'Snooze reminder' })[s.sheet.kind] : '',
sheetBody, closeSheet: () => this.setState({ sheet: null }), stop: e => e.stopPropagation(),
toast: s.toast,
};
}
duePresets() {
return [ ['Today', '2026-06-19'], ['Tomorrow', '2026-06-20'], ['In 3 days', '2026-06-22'], ['Next week', '2026-06-26'] ];
}
snoozePresets() {
return [ ['Tomorrow', '2026-06-20'], ['In 3 days', '2026-06-22'], ['1 week', '2026-06-26'], ['2 weeks', '2026-07-03'] ];
}
buildSheet(sh) {
const h = React.createElement;
const theme = this.state.theme;
const dark = theme !== 'light';
const T = { input: dark ? '#0d1622' : '#eef2f7', border: dark ? '#263548' : '#d6dde7', bstrong: dark ? '#35506a' : '#b6c3d4',
t1: dark ? '#e5edf5' : '#16202c', t2: dark ? '#c7d3e0' : '#33414f', t3: dark ? '#8ea2b7' : '#5a6b7d', t4: dark ? '#70859b' : '#84909e',
elev: dark ? '#152233' : '#f4f7fb', accentlight: dark ? '#93c5fd' : '#1f6fb8', danger: dark ? '#e06c6c' : '#c0322f' };
const inputStyle = { width: '100%', height: 46, background: T.input, border: '1px solid ' + T.border, borderRadius: 8, color: T.t1, fontFamily: 'var(--sans)', fontSize: 15, padding: '0 14px', outline: 'none', boxSizing: 'border-box' };
const label = (t) => h('div', { style: { fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.08em', textTransform: 'uppercase', color: T.t3, margin: '16px 0 8px' } }, t);
const dueChips = (sel, on) => h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, this.duePresets().map(d => {
const active = sel === d[1];
return h('button', { key: d[1], onClick: () => on(d[1]), style: { flex: '1 0 40%', height: 42, borderRadius: 7, cursor: 'pointer', fontSize: 13, fontWeight: 500, border: '1px solid ' + (active ? T.bstrong : T.border), background: active ? T.elev : T.input, color: active ? T.t1 : T.t3 } }, d[0] + ' · ' + this.monthDay(d[1]));
}));
if (sh.kind === 'edit') {
return h('div', null,
h('div', { style: { fontSize: 13, color: T.t3 } }, 'For ' + sh.org),
label('Reminder'),
h('input', { value: sh.note, onChange: e => this.setSheet({ note: e.target.value }), style: inputStyle, autoFocus: true }),
label('Due'),
dueChips(sh.due, v => this.setSheet({ due: v })),
h('button', { onClick: () => { if (window.T31Store) window.T31Store.updateReminder(sh.id, { note: sh.note, due: sh.due }); this.setState({ sheet: null }); this.toast('Reminder saved'); }, disabled: !sh.note.trim(), style: { width: '100%', height: 48, marginTop: 22, borderRadius: 8, border: 'none', background: !sh.note.trim() ? T.elev : 'linear-gradient(#3b82c4,#2f6ea9)', color: !sh.note.trim() ? T.t4 : '#fff', fontSize: 15, fontWeight: 600, cursor: 'pointer' } }, 'Save reminder'),
h('div', { style: { display: 'flex', gap: 10, marginTop: 10 } },
h('button', { onClick: () => { this.toggleDone(sh.id); this.setState({ sheet: null }); }, style: { flex: 1, height: 46, borderRadius: 8, border: '1px solid ' + T.bstrong, background: T.elev, color: T.t2, fontSize: 14, fontWeight: 500, cursor: 'pointer' } }, 'Mark done'),
h('button', { onClick: () => { if (window.T31Store) window.T31Store.deleteReminder(sh.id); this.setState({ sheet: null }); this.toast('Reminder deleted'); }, style: { flex: 'none', height: 46, padding: '0 18px', borderRadius: 8, border: '1px solid ' + (dark ? '#dc262655' : '#e3b4b2'), background: dark ? '#2a1416' : '#fbeceb', color: dark ? '#fca5a5' : '#c0322f', fontSize: 14, fontWeight: 500, cursor: 'pointer' } }, 'Delete')),
h('button', { onClick: () => { if (window.T31Store) window.T31Store.openInvestor(sh.orgId); }, style: { width: '100%', marginTop: 14, background: 'none', border: 'none', cursor: 'pointer', color: T.accentlight, fontSize: 13 } }, 'Open ' + sh.org + ' in Grid ')
);
}
if (sh.kind === 'snooze') {
return h('div', null,
h('div', { style: { fontSize: 13, color: T.t3, marginBottom: 4 } }, 'For ' + sh.org),
label('Snooze until'),
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } }, this.snoozePresets().map(d =>
h('button', { key: d[1], onClick: () => { if (window.T31Store) window.T31Store.updateReminder(sh.id, { due: d[1], done: false }); this.setState({ sheet: null }); this.toast('Snoozed to ' + this.monthDay(d[1])); }, style: { width: '100%', height: 50, borderRadius: 8, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', border: '1px solid ' + T.border, background: T.input, color: T.t1, fontSize: 15, fontWeight: 500 } },
h('span', null, d[0]),
h('span', { style: { fontFamily: 'var(--mono)', fontSize: 13, color: T.t4 } }, this.monthDay(d[1])))
))
);
}
// add
if (!sh.orgId) {
const qn = (sh.q || '').trim().toLowerCase();
let pool = this.investorList();
if (qn) pool = pool.filter(i => i.name.toLowerCase().includes(qn));
return h('div', null,
h('div', { style: { fontSize: 13, color: T.t3, marginBottom: 12 } }, 'Which investor is this reminder for?'),
h('input', { value: sh.q, onChange: e => this.setSheet({ q: e.target.value }), style: inputStyle, placeholder: 'Search investor…', autoFocus: true }),
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 12 } }, pool.length ? pool.map(i =>
h('button', { key: i.id, onClick: () => this.setSheet({ orgId: i.id, org: i.name }), style: { width: '100%', textAlign: 'left', cursor: 'pointer', background: T.input, border: '1px solid ' + T.border, borderRadius: 10, padding: '13px 14px', color: T.t1, fontSize: 15, fontWeight: 500 } }, i.name)
) : h('div', { style: { fontSize: 13, color: T.t4, padding: '16px 4px' } }, 'No matches.'))
);
}
return h('div', null,
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, background: T.input, border: '1px solid ' + T.border, borderRadius: 10, padding: '11px 13px' } },
h('span', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 } },
h('span', { style: { fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.06em', textTransform: 'uppercase', color: T.t4 } }, 'Reminder for'),
h('span', { style: { fontSize: 15, fontWeight: 600, color: T.t1 } }, sh.org)),
h('button', { onClick: () => this.setSheet({ orgId: null }), style: { flex: 'none', background: 'none', border: 'none', color: T.accentlight, fontSize: 13, cursor: 'pointer' } }, 'Change')),
label('Reminder'),
h('input', { value: sh.note, onChange: e => this.setSheet({ note: e.target.value }), style: inputStyle, placeholder: 'What needs doing?', autoFocus: true }),
label('Due'),
dueChips(sh.due, v => this.setSheet({ due: v })),
h('button', { onClick: () => { if (window.T31Store) window.T31Store.addReminder(sh.orgId, sh.note.trim(), sh.due); this.setState({ sheet: null }); this.toast('Reminder added'); }, disabled: !sh.note.trim(), style: { width: '100%', height: 48, marginTop: 22, borderRadius: 8, border: 'none', background: !sh.note.trim() ? T.elev : 'linear-gradient(#3b82c4,#2f6ea9)', color: !sh.note.trim() ? T.t4 : '#fff', fontSize: 15, fontWeight: 600, cursor: 'pointer' } }, 'Add reminder')
);
}
tabIcon(key, active) {
const c = active ? '#3b82c4' : (this.state.theme === 'light' ? '#84909e' : '#70859b');
const mk = (children) => React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 20 20', fill: 'none' }, children);
const r = (pp) => React.createElement('rect', pp);
const ln = (pp) => React.createElement('line', Object.assign({}, pp, { stroke: c, strokeWidth: 1.6, strokeLinecap: 'round' }));
if (key === 'grid') return mk([
r({ key: 1, x: 3, y: 3, width: 6, height: 6, rx: 1, stroke: c, strokeWidth: 1.6 }),
r({ key: 2, x: 11, y: 3, width: 6, height: 6, rx: 1, stroke: c, strokeWidth: 1.6 }),
r({ key: 3, x: 3, y: 11, width: 6, height: 6, rx: 1, stroke: c, strokeWidth: 1.6 }),
r({ key: 4, x: 11, y: 11, width: 6, height: 6, rx: 1, stroke: c, strokeWidth: 1.6 }),
]);
if (key === 'pipeline') return mk([
r({ key: 1, x: 3, y: 3, width: 4.5, height: 14, rx: 1, stroke: c, strokeWidth: 1.6 }),
r({ key: 2, x: 9.25, y: 3, width: 4.5, height: 10, rx: 1, stroke: c, strokeWidth: 1.6 }),
r({ key: 3, x: 15.5, y: 3, width: 1.5, height: 6, rx: 0.7, fill: c }),
]);
if (key === 'reminders') return mk([
React.createElement('circle', { key: 1, cx: 10, cy: 11, r: 6.2, stroke: c, strokeWidth: 1.6 }),
ln({ key: 2, x1: 10, y1: 11, x2: 10, y2: 7.5 }),
ln({ key: 3, x1: 10, y1: 11, x2: 12.4, y2: 12 }),
ln({ key: 4, x1: 7, y1: 3.4, x2: 4.4, y2: 5.4 }),
ln({ key: 5, x1: 13, y1: 3.4, x2: 15.6, y2: 5.4 }),
]);
return mk([
React.createElement('circle', { key: 1, cx: 10, cy: 7, r: 3.2, stroke: c, strokeWidth: 1.6 }),
React.createElement('path', { key: 2, d: 'M4 16.5c0-3 2.7-4.8 6-4.8s6 1.8 6 4.8', stroke: c, strokeWidth: 1.6, strokeLinecap: 'round' }),
]);
}
}
</script>
</body>
</html>