Files
ten31-database/design/_imports/2026-06-19_zip-file/Venture-CRM mobile redesign/RemindersApp.dc.html
T
Keysat e6a89450da 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.
2026-06-19 16:38:30 -05:00

475 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>