e6a89450da
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.
662 lines
46 KiB
HTML
662 lines
46 KiB
HTML
<!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; } }
|
||
@keyframes accOpen { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
|
||
.pp-scroll::-webkit-scrollbar { width: 0; height: 0; }
|
||
.pp-snap::-webkit-scrollbar { width: 0; height: 0; }
|
||
.pp-scroll, .pp-snap { scrollbar-width: none; -ms-overflow-style: none; }
|
||
.pp-root button { font-family: inherit; }
|
||
.pp-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;
|
||
}
|
||
.pp-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="pp-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 row -->
|
||
<div style="flex:none; padding:14px 16px 12px; display:flex; align-items:baseline; 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;">Pipeline</span>
|
||
<span style="font-family:var(--mono); font-size:12px; color:var(--t4);">{{ totalLabel }}</span>
|
||
</div>
|
||
<button onClick="{{ openSortSheet }}" style="flex:none; display:flex; align-items:center; gap:6px; height:30px; padding:0 12px; border-radius:999px; border:1px solid var(--border); background:var(--input); color:var(--t2); font-family:var(--mono); font-size:11px; font-weight:600; letter-spacing:0.04em; text-transform:uppercase; cursor:pointer;">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h11M3 12h7M3 18h4"></path><path d="M18 8v9m0 0 3-3m-3 3-3-3"></path></svg>
|
||
{{ sortLabel }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- ===== SWIPE MODE ===== -->
|
||
<sc-if value="{{ isSwipe }}" hint-placeholder-val="{{ true }}">
|
||
<!-- segmented stage control -->
|
||
<div class="pp-scroll" style="flex:none; overflow-x:auto; padding:0 16px 12px;">
|
||
<div style="display:inline-flex; gap:8px;">
|
||
<sc-for list="{{ segments }}" as="sg" hint-placeholder-count="6">
|
||
<button onClick="{{ sg.go }}" style="flex:none; cursor:pointer; display:flex; align-items:center; gap:8px; height:36px; padding:0 14px; border-radius:999px; border:1px solid {{ sg.border }}; background:{{ sg.bg }};">
|
||
<span style="font-family:var(--mono); font-size:12px; font-weight:600; letter-spacing:0.04em; text-transform:uppercase; color:{{ sg.text }};">{{ sg.label }}</span>
|
||
<span style="font-family:var(--mono); font-size:11px; font-weight:600; color:{{ sg.countText }}; background:{{ sg.countBg }}; min-width:18px; height:18px; border-radius:999px; display:inline-flex; align-items:center; justify-content:center; padding:0 5px;">{{ sg.count }}</span>
|
||
</button>
|
||
</sc-for>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- snap columns -->
|
||
<div class="pp-snap" ref="{{ snapRef }}" onScroll="{{ onSnapScroll }}" style="flex:1; min-height:0; display:flex; overflow-x:auto; overflow-y:hidden; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch;">
|
||
<sc-for list="{{ columns }}" as="col" hint-placeholder-count="6">
|
||
<div style="flex:none; width:100%; height:100%; scroll-snap-align:start; display:flex; flex-direction:column;">
|
||
<div style="flex:none; display:flex; align-items:center; justify-content:space-between; padding:4px 18px 12px;">
|
||
<span style="display:flex; align-items:center; gap:9px;">
|
||
<span style="font-family:var(--mono); font-size:13px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:4px 11px; border-radius:999px; background:{{ col.bg }}; color:{{ col.text }}; border:1px solid {{ col.border }};">{{ col.label }}</span>
|
||
<span style="font-family:var(--mono); font-size:12px; color:var(--t4);">{{ col.count }}</span>
|
||
</span>
|
||
<span style="font-family:var(--mono); font-size:13px; font-weight:600; color:{{ col.sumColor }};">{{ col.sum }}</span>
|
||
</div>
|
||
<div class="pp-scroll" style="flex:1; min-height:0; overflow-y:auto; padding:0 16px 18px; display:flex; flex-direction:column; gap:10px;">
|
||
<sc-for list="{{ col.cards }}" as="c" hint-placeholder-count="3">
|
||
<div style="background:var(--panel); border:1px solid var(--border); border-radius:10px; box-shadow:var(--shadow-card); overflow:hidden;">
|
||
<button onClick="{{ c.open }}" style="width:100%; text-align:left; cursor:pointer; background:none; border:none; padding:13px 14px 11px; display:flex; flex-direction:column; gap:9px; color:var(--t1);">
|
||
<span style="display:flex; align-items:flex-start; justify-content:space-between; gap:10px;">
|
||
<span style="display:flex; align-items:center; gap:6px; min-width:0;">
|
||
<sc-if value="{{ c.existing }}" hint-placeholder-val="{{ false }}"><span style="flex:none; color:var(--accent); font-size:12px; line-height:1;">★</span></sc-if>
|
||
<span style="font-size:16px; font-weight:600; line-height:1.25; overflow:hidden; text-overflow:ellipsis;">{{ c.name }}</span>
|
||
</span>
|
||
<sc-if value="{{ c.priority }}" hint-placeholder-val="{{ false }}"><span style="flex:none; font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:3px 7px; border-radius:4px; background:{{ priBg }}; color:{{ priText }};">Priority</span></sc-if>
|
||
</span>
|
||
<span style="display:flex; align-items:center; gap:10px;">
|
||
<span style="font-family:var(--mono); font-size:15px; font-weight:600; color:{{ c.amtColor }};">{{ c.amount }}</span>
|
||
<span style="width:3px; height:3px; border-radius:999px; background:var(--bstrong);"></span>
|
||
<span style="font-family:var(--mono); font-size:12px; color:var(--t4);">{{ c.last }}</span>
|
||
</span>
|
||
</button>
|
||
<div style="display:flex; border-top:1px solid var(--divider);">
|
||
<button onClick="{{ c.moveBack }}" disabled="{{ c.atStart }}" style="flex:1; cursor:pointer; background:none; border:none; border-right:1px solid var(--divider); height:40px; color:{{ c.backColor }}; font-family:var(--mono); font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; align-items:center; justify-content:center; gap:5px;">‹ {{ c.backLabel }}</button>
|
||
<button onClick="{{ c.moveFwd }}" disabled="{{ c.atEnd }}" style="flex:1; cursor:pointer; background:none; border:none; height:40px; color:{{ c.fwdColor }}; font-family:var(--mono); font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; align-items:center; justify-content:center; gap:5px;">{{ c.fwdLabel }} ›</button>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
<sc-if value="{{ col.empty }}" hint-placeholder-val="{{ false }}">
|
||
<div style="padding:40px 16px; text-align:center; color:var(--t4); font-size:13px; border:1px dashed var(--border); border-radius:10px;">No investors in this stage.</div>
|
||
</sc-if>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
<div style="flex:none; display:flex; align-items:center; justify-content:center; gap:9px; padding:8px 0 12px;">
|
||
<sc-for list="{{ dots }}" as="dt" hint-placeholder-count="4">
|
||
<button onClick="{{ dt.go }}" aria-label="Go to stage" style="background:none; border:none; cursor:pointer; padding:7px 3px; display:flex; align-items:center; justify-content:center;">
|
||
<sc-if value="{{ dt.active }}" hint-placeholder-val="{{ true }}"><span style="display:block; width:22px; height:6px; border-radius:999px; background:var(--accent);"></span></sc-if>
|
||
<sc-if value="{{ dt.inactive }}" hint-placeholder-val="{{ false }}"><span style="display:block; width:6px; height:6px; border-radius:999px; background:var(--bstrong);"></span></sc-if>
|
||
</button>
|
||
</sc-for>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- ===== ACCORDION MODE ===== -->
|
||
<sc-if value="{{ isAccordion }}" hint-placeholder-val="{{ false }}">
|
||
<div class="pp-scroll" style="flex:1; min-height:0; overflow-y:auto; padding:0 16px 20px; display:flex; flex-direction:column; gap:10px;">
|
||
<sc-for list="{{ sections }}" as="sec" hint-placeholder-count="6">
|
||
<div style="border:1px solid var(--border); border-radius:12px; background:var(--panel); overflow:hidden;">
|
||
<button onClick="{{ sec.toggle }}" style="width:100%; cursor:pointer; background:none; border:none; padding:14px 15px; display:flex; align-items:center; justify-content:space-between; gap:10px; color:var(--t1);">
|
||
<span style="display:flex; align-items:center; gap:10px; min-width:0;">
|
||
<span style="flex:none; color:var(--t3); font-size:12px; width:12px; transition:transform 150ms; transform:rotate({{ sec.rot }}deg);">▸</span>
|
||
<span style="font-family:var(--mono); font-size:13px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:4px 11px; border-radius:999px; background:{{ sec.bg }}; color:{{ sec.text }}; border:1px solid {{ sec.border }};">{{ sec.label }}</span>
|
||
<span style="font-family:var(--mono); font-size:12px; color:var(--t4);">{{ sec.count }}</span>
|
||
</span>
|
||
<span style="font-family:var(--mono); font-size:13px; font-weight:600; color:{{ sec.sumColor }};">{{ sec.sum }}</span>
|
||
</button>
|
||
<sc-if value="{{ sec.open }}" hint-placeholder-val="{{ false }}">
|
||
<div style="padding:0 12px 12px; display:flex; flex-direction:column; gap:9px; animation:accOpen 180ms ease;">
|
||
<sc-for list="{{ sec.cards }}" as="c" hint-placeholder-count="2">
|
||
<div style="background:var(--elev); border:1px solid var(--border); border-radius:10px; overflow:hidden;">
|
||
<button onClick="{{ c.open }}" style="width:100%; text-align:left; cursor:pointer; background:none; border:none; padding:12px 13px; display:flex; align-items:center; justify-content:space-between; gap:10px; color:var(--t1);">
|
||
<span style="display:flex; flex-direction:column; gap:5px; min-width:0;">
|
||
<span style="display:flex; align-items:center; gap:6px; min-width:0;"><sc-if value="{{ c.existing }}" hint-placeholder-val="{{ false }}"><span style="flex:none; color:var(--accent); font-size:11px; line-height:1;">★</span></sc-if><span style="font-size:15px; font-weight:600; line-height:1.2; overflow:hidden; text-overflow:ellipsis;">{{ c.name }}</span></span>
|
||
<span style="display:flex; align-items:center; gap:9px;">
|
||
<span style="font-family:var(--mono); font-size:13px; font-weight:600; color:{{ c.amtColor }};">{{ c.amount }}</span>
|
||
<span style="font-family:var(--mono); font-size:11px; color:var(--t4);">{{ c.last }}</span>
|
||
</span>
|
||
</span>
|
||
<sc-if value="{{ c.priority }}" hint-placeholder-val="{{ false }}"><span style="flex:none; font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:3px 7px; border-radius:4px; background:{{ priBg }}; color:{{ priText }};">Priority</span></sc-if>
|
||
</button>
|
||
<div style="display:flex; border-top:1px solid var(--divider);">
|
||
<button onClick="{{ c.moveBack }}" disabled="{{ c.atStart }}" style="flex:1; cursor:pointer; background:none; border:none; border-right:1px solid var(--divider); height:38px; color:{{ c.backColor }}; font-family:var(--mono); font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; align-items:center; justify-content:center;">‹ {{ c.backLabel }}</button>
|
||
<button onClick="{{ c.moveFwd }}" disabled="{{ c.atEnd }}" style="flex:1; cursor:pointer; background:none; border:none; height:38px; color:{{ c.fwdColor }}; font-family:var(--mono); font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; align-items:center; justify-content:center;">{{ c.fwdLabel }} ›</button>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
<sc-if value="{{ sec.empty }}" hint-placeholder-val="{{ false }}">
|
||
<div style="padding:18px 14px; text-align:center; color:var(--t4); font-size:13px;">No investors in this stage.</div>
|
||
</sc-if>
|
||
</div>
|
||
</sc-if>
|
||
</div>
|
||
</sc-for>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- detail / quick-move 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:88%; 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:flex-start; justify-content:space-between; gap:12px; padding:8px 0 4px; flex:none;">
|
||
<div style="display:flex; flex-direction:column; gap:7px; min-width:0;">
|
||
<span style="font-size:19px; font-weight:600; color:var(--t1);">{{ d.name }}</span>
|
||
<span style="display:flex; align-items:center; gap:8px;">
|
||
<sc-if value="{{ d.priority }}" hint-placeholder-val="{{ false }}"><span style="font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:3px 7px; border-radius:4px; background:{{ priBg }}; color:{{ priText }};">Priority</span></sc-if>
|
||
<sc-if value="{{ d.existing }}" hint-placeholder-val="{{ false }}"><span style="font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:3px 7px; border-radius:4px; background:#3b82c422; color:var(--accentlight);">Existing LP</span></sc-if>
|
||
<span style="font-family:var(--mono); font-size:12px; color:var(--t4);">Last contact {{ d.last }}</span>
|
||
</span>
|
||
</div>
|
||
<button onClick="{{ closeSheet }}" style="flex:none; background:none; border:none; color:var(--t3); font-size:22px; cursor:pointer; line-height:1; padding:0 4px;">×</button>
|
||
</div>
|
||
|
||
<div class="pp-scroll" style="overflow-y:auto; margin-top:8px;">
|
||
<div style="display:flex; gap:10px; margin:6px 0 4px;">
|
||
<div style="flex:1; background:var(--input); border:1px solid var(--border); border-radius:10px; padding:11px 13px; display:flex; flex-direction:column; gap:4px;">
|
||
<span style="font-family:var(--mono); font-size:10px; letter-spacing:0.06em; text-transform:uppercase; color:var(--t4);">Committed</span>
|
||
<span style="font-family:var(--mono); font-size:15px; font-weight:600; color:{{ d.amtColor }};">{{ d.amount }}</span>
|
||
</div>
|
||
<div style="flex:1; background:var(--input); border:1px solid var(--border); border-radius:10px; padding:11px 13px; display:flex; flex-direction:column; gap:4px;">
|
||
<span style="font-family:var(--mono); font-size:10px; letter-spacing:0.06em; text-transform:uppercase; color:var(--t4);">Contacts</span>
|
||
<span style="font-size:14px; color:var(--t2); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ d.contactLine }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3); margin:18px 0 9px;">Move stage</div>
|
||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||
<sc-for list="{{ d.stageOptions }}" as="so" hint-placeholder-count="6">
|
||
<button onClick="{{ so.pick }}" style="width:100%; cursor:pointer; height:46px; border-radius:8px; display:flex; align-items:center; justify-content:space-between; padding:0 14px; border:1px solid {{ so.rowBorder }}; background:{{ so.rowBg }};">
|
||
<span style="font-family:var(--mono); font-size:12px; font-weight:600; letter-spacing:0.04em; text-transform:uppercase; padding:4px 10px; border-radius:999px; background:{{ so.bg }}; color:{{ so.text }}; border:1px solid {{ so.border }};">{{ so.label }}</span>
|
||
<span style="color:var(--accent); font-size:15px; width:16px;">{{ so.check }}</span>
|
||
</button>
|
||
</sc-for>
|
||
</div>
|
||
|
||
<div style="display:flex; align-items:center; justify-content:space-between; margin:20px 0 10px;">
|
||
<span style="font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3);">Notes / communication</span>
|
||
<button onClick="{{ openLog }}" style="background:var(--elev); border:1px solid var(--border); border-radius:6px; padding:7px 12px; cursor:pointer; color:var(--accentlight); font-size:13px; min-height:36px;">+ Log</button>
|
||
</div>
|
||
<div style="display:flex; flex-direction:column;">
|
||
<sc-for list="{{ d.notes }}" as="n" hint-placeholder-count="2">
|
||
<div style="display:flex; gap:11px; padding-bottom:14px;">
|
||
<div style="flex:none; display:flex; flex-direction:column; align-items:center; gap:4px;">
|
||
<span style="width:9px; height:9px; border-radius:999px; background:var(--accent); margin-top:4px;"></span>
|
||
<span style="flex:1; width:1px; background:var(--border);"></span>
|
||
</div>
|
||
<div style="flex:1; min-width:0;">
|
||
<div style="display:flex; align-items:center; gap:8px;">
|
||
<span style="font-family:var(--mono); font-size:10px; font-weight:600; letter-spacing:0.05em; text-transform:uppercase; padding:2px 6px; border-radius:4px; background:{{ n.tagBg }}; color:{{ n.tagText }};">{{ n.type }}</span>
|
||
<span style="font-family:var(--mono); font-size:11px; color:var(--t4);">{{ n.date }}</span>
|
||
</div>
|
||
<div style="font-size:14px; color:var(--t2); margin-top:6px; line-height:1.45;">{{ n.summary }}</div>
|
||
</div>
|
||
</div>
|
||
</sc-for>
|
||
<sc-if value="{{ d.noNotes }}" hint-placeholder-val="{{ true }}">
|
||
<div style="font-size:13px; color:var(--t4); padding-bottom:6px;">No activity logged yet.</div>
|
||
</sc-if>
|
||
</div>
|
||
|
||
<div style="font-size:12px; color:var(--t4); margin-top:14px; line-height:1.45;">Stage moves and logged communications both write to the shared opportunities row — the same data the Grid edits.</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- log activity sheet (over detail) -->
|
||
<sc-if value="{{ logOpen }}" hint-placeholder-val="{{ false }}">
|
||
<div onClick="{{ closeLog }}" style="position:absolute; inset:0; z-index:65; 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 14px; flex:none;">
|
||
<span style="font-size:18px; font-weight:600; color:var(--t1);">Log communication</span>
|
||
<button onClick="{{ closeLog }}" style="background:none; border:none; color:var(--t3); font-size:22px; cursor:pointer; line-height:1; padding:0 4px;">×</button>
|
||
</div>
|
||
<div style="font-family:var(--mono); font-size:12px; color:var(--t4); margin:-4px 0 14px;">{{ logFor }}</div>
|
||
<div class="pp-scroll" style="overflow-y:auto;">
|
||
<div style="font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3); margin:0 0 8px;">Type</div>
|
||
<div style="display:flex; gap:8px;">
|
||
<sc-for list="{{ logTypes }}" as="lt" hint-placeholder-count="4">
|
||
<button onClick="{{ lt.pick }}" style="flex:1; height:42px; border-radius:7px; cursor:pointer; font-family:var(--mono); font-size:12px; font-weight:600; letter-spacing:0.04em; text-transform:uppercase; border:1px solid {{ lt.border }}; background:{{ lt.bg }}; color:{{ lt.text }};">{{ lt.label }}</button>
|
||
</sc-for>
|
||
</div>
|
||
<div style="font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3); margin:16px 0 8px;">Summary</div>
|
||
<input value="{{ logSummary }}" onInput="{{ onLogSummary }}" placeholder="Short headline" style="width:100%; height:46px; background:var(--input); border:1px solid var(--border); border-radius:8px; color:var(--t1); font-family:var(--sans); font-size:15px; padding:0 14px; outline:none; box-sizing:border-box;" />
|
||
<div style="font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3); margin:16px 0 8px;">Details</div>
|
||
<textarea value="{{ logDetails }}" onInput="{{ onLogDetails }}" rows="3" placeholder="Full context kept in communications history" style="width:100%; background:var(--input); border:1px solid var(--border); border-radius:8px; color:var(--t1); font-family:var(--sans); font-size:15px; padding:12px 14px; outline:none; resize:none; line-height:1.45; box-sizing:border-box;"></textarea>
|
||
<div style="display:flex; gap:10px; margin-top:20px;">
|
||
<button onClick="{{ closeLog }}" style="flex:1; height:48px; background:var(--elev); border:1px solid var(--border); border-radius:8px; color:var(--t2); font-size:15px; font-weight:500; cursor:pointer;">Cancel</button>
|
||
<button onClick="{{ saveLog }}" disabled="{{ logDisabled }}" style="flex:2; height:48px; border:none; border-radius:8px; color:{{ logBtnText }}; background:{{ logBtnBg }}; font-size:15px; font-weight:600; cursor:pointer;">Log it</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</sc-if>
|
||
|
||
<!-- sort sheet -->
|
||
<sc-if value="{{ sortSheet }}" hint-placeholder-val="{{ false }}">
|
||
<div onClick="{{ closeSortSheet }}" 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; 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="padding:8px 0 14px; font-size:18px; font-weight:600; color:var(--t1);">Sort within stage</div>
|
||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||
<sc-for list="{{ sortOptions }}" as="o" hint-placeholder-count="4">
|
||
<button onClick="{{ o.pick }}" style="width:100%; text-align:left; cursor:pointer; display:flex; align-items:center; justify-content:space-between; gap:10px; min-height:52px; padding:0 15px; border-radius:10px; border:1px solid {{ o.border }}; background:{{ o.bg }};">
|
||
<span style="display:flex; flex-direction:column; gap:2px;">
|
||
<span style="font-size:15px; font-weight:500; color:var(--t1);">{{ o.label }}</span>
|
||
<span style="font-family:var(--mono); font-size:11px; color:var(--t4);">{{ o.hint }}</span>
|
||
</span>
|
||
<sc-if value="{{ o.on }}" hint-placeholder-val="{{ false }}"><span style="color:var(--accent); font-size:15px;">✓</span></sc-if>
|
||
</button>
|
||
</sc-for>
|
||
</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="{"$preview":{"width":393,"height":812},"mode":{"editor":"enum","options":["swipe","accordion"],"default":"swipe","tsType":"'swipe'|'accordion'"},"theme":{"editor":"enum","options":["dark","light"],"default":"dark","tsType":"'dark'|'light'"}}">
|
||
class Component extends DCLogic {
|
||
constructor(props) {
|
||
super(props);
|
||
this._snap = null;
|
||
this.state = {
|
||
theme: props.theme === 'light' ? 'light' : 'dark',
|
||
active: 0,
|
||
sortKey: 'name',
|
||
sortSheet: false,
|
||
open: { 'diligence': true, 'commitment': true },
|
||
sheetId: null,
|
||
log: null,
|
||
toast: null,
|
||
investors: this.seed(),
|
||
};
|
||
}
|
||
|
||
componentDidMount() { if (window.T31Store) this._unsub = window.T31Store.subscribe(() => this.forceUpdate()); }
|
||
componentWillUnmount() { if (this._unsub) this._unsub(); }
|
||
|
||
seed() {
|
||
return [
|
||
{ id: 1, name: 'Northwall Capital', type: 'Investor', stage: 'committed', last: '2d ago', contacts: ['Dana Reyes', 'Per Holt'], amt: 2500000, notes: [['Email', 'Confirmed $2.5M allocation across funds', '2026-06-17'], ['Meeting', 'DD call — covered redemption terms', '2026-06-10']] },
|
||
{ id: 2, name: 'Brightseed Partners', type: 'Prospect', stage: 'meeting', last: '5d ago', contacts: ['Omar Said'], amt: 0, notes: [['Note', 'Intro from Polaris — warm', '2026-06-14']] },
|
||
{ id: 3, name: 'Cedarline Family Office', type: 'Investor', stage: 'funded', last: '1w ago', contacts: ['Lena Cho'], amt: 1200000, notes: [['Call', 'Wire received, fully funded', '2026-06-12']] },
|
||
{ id: 4, name: 'Vance & Co', type: 'Prospect', stage: 'outreach', last: '3d ago', contacts: ['Marcus Vance'], amt: 0, notes: [] },
|
||
{ id: 5, name: 'Polaris Endowment', type: 'Investor', stage: 'due diligence', last: 'yesterday', contacts: ['Ruth Almeida'], amt: 5000000, notes: [['Meeting', 'IC presentation went well', '2026-06-18'], ['Email', 'Sent data room access', '2026-06-15']] },
|
||
{ id: 7, name: 'Meridian Trust', type: 'Investor', stage: 'committed', last: '4d ago', contacts: ['Sofia Marin'], amt: 800000, notes: [['Note', 'Signed side letter', '2026-06-14']] },
|
||
{ id: 8, name: 'Atlas Ventures Fund', type: 'Prospect', stage: 'meeting', last: '6d ago', contacts: ['Will Tanaka'], amt: 0, notes: [] },
|
||
{ id: 10, name: 'Granite Bay LP', type: 'Investor', stage: 'funded', last: '1mo ago', contacts: ['Tom Becker'], amt: 3300000, notes: [] },
|
||
{ id: 11, name: 'Forsythe Holdings', type: 'Priority Target', stage: 'lead', last: '5w ago', contacts: [], amt: 0, notes: [] },
|
||
];
|
||
}
|
||
|
||
stages() { return ['lead', 'engaged', 'diligence', 'commitment']; }
|
||
shortStage(s) { return ({ 'lead': 'Lead', 'engaged': 'Engaged', 'diligence': 'Diligence', 'commitment': 'Commitment' })[s] || s; }
|
||
|
||
themePalette(theme) {
|
||
if (theme === 'light') return { t4: '#84909e', money: '#057a55' };
|
||
return { t4: '#70859b', money: '#6ee7b7' };
|
||
}
|
||
typeColors(t, theme) {
|
||
const light = theme === 'light';
|
||
if (t === 'Investor') return light ? { bg: '#10b9811f', text: '#057a55' } : { bg: '#10b98122', text: '#6ee7b7' };
|
||
if (t === 'Priority Target') return light ? { bg: '#e08e0922', text: '#a76a07' } : { bg: '#f59e0b22', text: '#fcd34d' };
|
||
return light ? { bg: '#3b82c41f', text: '#2266a0' } : { bg: '#3b82c422', text: '#93c5fd' };
|
||
}
|
||
stageColors(s, theme) {
|
||
const light = theme === 'light';
|
||
const dark = {
|
||
'lead': { bg: '#70859b22', text: '#8ea2b7', border: '#2635488a' },
|
||
'engaged': { bg: '#3b82c422', text: '#93c5fd', border: '#3b82c44d' },
|
||
'diligence': { bg: '#e0b3411f', text: '#e0b341', border: '#e0b3413d' },
|
||
'commitment': { bg: '#10b9811f', text: '#6ee7b7', border: '#10b9813d' },
|
||
};
|
||
const lite = {
|
||
'lead': { bg: '#5a6b7d14', text: '#5a6b7d', border: '#d6dde7' },
|
||
'engaged': { bg: '#3b82c416', text: '#2266a0', border: '#bcd2ea' },
|
||
'diligence': { bg: '#e0b34122', text: '#8a6c12', border: '#e4d29a' },
|
||
'commitment': { bg: '#10b98118', text: '#057a55', border: '#a9ddca' },
|
||
};
|
||
const map = light ? lite : dark;
|
||
return map[s] || (light ? { bg: '#5a6b7d12', text: '#84909e', border: '#d6dde7' } : { bg: '#1b2a3a', text: '#70859b', border: '#263548' });
|
||
}
|
||
noteTag(t, theme) {
|
||
const light = theme === 'light';
|
||
const dark = { 'Email': { bg: '#3b82c422', text: '#93c5fd' }, 'Call': { bg: '#10b98122', text: '#6ee7b7' }, 'Meeting': { bg: '#f59e0b1f', text: '#fcd34d' }, 'Note': { bg: '#1b2a3a', text: '#8ea2b7' } };
|
||
const lite = { 'Email': { bg: '#3b82c41a', text: '#2266a0' }, 'Call': { bg: '#10b9811a', text: '#057a55' }, 'Meeting': { bg: '#f59e0b1a', text: '#a76a07' }, 'Note': { bg: '#5a6b7d14', text: '#5a6b7d' } };
|
||
const map = light ? lite : dark;
|
||
return map[t] || map['Note'];
|
||
}
|
||
money(n) {
|
||
if (!n) return '$0';
|
||
if (n >= 1e6) return '$' + (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
|
||
if (n >= 1e3) return '$' + Math.round(n / 1e3) + 'K';
|
||
return '$' + n;
|
||
}
|
||
|
||
amt(i) { return window.T31Store ? window.T31Store.committed(i) : (i.amt || 0); }
|
||
sortCards(arr, key) {
|
||
const a = arr.slice();
|
||
if (key === 'amount') a.sort((x, y) => this.amt(y) - this.amt(x) || x.name.localeCompare(y.name));
|
||
else if (key === 'staleness') a.sort((x, y) => y.daysAgo - x.daysAgo || x.name.localeCompare(y.name));
|
||
else if (key === 'priority') a.sort((x, y) => (y.priority ? 1 : 0) - (x.priority ? 1 : 0) || x.name.localeCompare(y.name));
|
||
else a.sort((x, y) => x.name.localeCompare(y.name));
|
||
return a;
|
||
}
|
||
sortLabelFor(key) { return ({ name: 'Name', amount: 'Amount', staleness: 'Staleness', priority: 'Priority' })[key] || 'Name'; }
|
||
toast(msg) { this.setState({ toast: msg }); clearTimeout(this._tt); this._tt = setTimeout(() => this.setState({ toast: null }), 2000); }
|
||
moveStage(id, dir) {
|
||
const order = this.stages();
|
||
const inv = (window.T31Store ? window.T31Store.investors : []).find(i => i.id === id);
|
||
if (!inv) return;
|
||
const idx = order.indexOf(inv.stage);
|
||
const ni = Math.max(0, Math.min(order.length - 1, idx + dir));
|
||
if (window.T31Store) window.T31Store.updateInvestor(id, { stage: order[ni] });
|
||
this.toast('Moved to ' + this.shortStage(order[ni]));
|
||
}
|
||
setStage(id, stage) {
|
||
if (window.T31Store) window.T31Store.updateInvestor(id, { stage: stage });
|
||
this.toast('Moved to ' + this.shortStage(stage));
|
||
}
|
||
setLog(patch) { this.setState(s => ({ log: Object.assign({}, s.log, patch) })); }
|
||
saveLog() {
|
||
const lg = this.state.log; const id = this.state.sheetId;
|
||
if (!lg || !lg.summary.trim()) return;
|
||
const entry = [lg.type, lg.summary.trim(), '2026-06-19'];
|
||
if (window.T31Store) window.T31Store.logNote(id, entry);
|
||
this.setState({ log: null });
|
||
this.toast('Communication logged');
|
||
}
|
||
|
||
onSnapScroll(e) {
|
||
const el = e.target;
|
||
const w = el.clientWidth || 1;
|
||
const idx = Math.round(el.scrollLeft / w);
|
||
if (idx !== this.state.active) this.setState({ active: idx });
|
||
}
|
||
goSegment(idx, e) {
|
||
const root = e && e.currentTarget ? e.currentTarget.closest('.pp-root') : null;
|
||
const el = root ? root.querySelector('.pp-snap') : this._snap;
|
||
if (!el) return;
|
||
// Synchronous, snap-aligned jump — the only scroll path this runtime keeps.
|
||
// (smooth + rAF writes get clobbered by the reconciler here.) CSS scroll-behavior
|
||
// on the element gives a native glide where the engine supports it.
|
||
el.scrollLeft = idx * el.clientWidth;
|
||
this.setState({ active: idx });
|
||
}
|
||
|
||
cardModel(i, theme, p) {
|
||
const order = this.stages();
|
||
const idx = order.indexOf(i.stage);
|
||
const amt = this.amt(i);
|
||
const accentDim = theme === 'light' ? '#84909e' : '#46586c';
|
||
return {
|
||
id: i.id, name: i.name, priority: !!i.priority, existing: amt > 0,
|
||
amount: this.money(amt), amtColor: amt > 0 ? p.money : p.t4, last: i.daysAgo + 'd ago',
|
||
open: () => this.setState({ sheetId: i.id }),
|
||
atStart: idx <= 0, atEnd: idx >= order.length - 1,
|
||
backLabel: idx > 0 ? this.shortStage(order[idx - 1]) : 'Start',
|
||
fwdLabel: idx < order.length - 1 ? this.shortStage(order[idx + 1]) : 'End',
|
||
backColor: idx > 0 ? 'var(--t3)' : accentDim,
|
||
fwdColor: idx < order.length - 1 ? 'var(--accentlight)' : accentDim,
|
||
moveBack: () => this.moveStage(i.id, -1),
|
||
moveFwd: () => this.moveStage(i.id, 1),
|
||
};
|
||
}
|
||
|
||
renderVals() {
|
||
const s = this.state;
|
||
const theme = s.theme;
|
||
const p = this.themePalette(theme);
|
||
const order = this.stages();
|
||
const mode = this.props.mode === 'accordion' ? 'accordion' : 'swipe';
|
||
|
||
const byStage = {};
|
||
order.forEach(st => byStage[st] = []);
|
||
const list = window.T31Store ? window.T31Store.investors : [];
|
||
list.forEach(i => { if (byStage[i.stage]) byStage[i.stage].push(i); });
|
||
order.forEach(st => { byStage[st] = this.sortCards(byStage[st], s.sortKey); });
|
||
|
||
const grandTotal = list.reduce((a, i) => a + this.amt(i), 0);
|
||
const activeCount = list.filter(i => byStage[i.stage]).length;
|
||
const priC = theme === 'light' ? { bg: '#e08e0922', text: '#a76a07' } : { bg: '#f59e0b22', text: '#fcd34d' };
|
||
|
||
// segments (swipe)
|
||
const segments = order.map((st, idx) => {
|
||
const sc = this.stageColors(st, theme);
|
||
const on = idx === s.active;
|
||
return {
|
||
label: this.shortStage(st), count: String(byStage[st].length),
|
||
go: (e) => this.goSegment(idx, e),
|
||
bg: on ? sc.bg : 'var(--input)', border: on ? sc.border : 'var(--border)', text: on ? sc.text : 'var(--t3)',
|
||
countBg: on ? sc.border : 'var(--border)', countText: on ? sc.text : 'var(--t4)',
|
||
};
|
||
});
|
||
|
||
// columns (swipe)
|
||
const columns = order.map(st => {
|
||
const sc = this.stageColors(st, theme);
|
||
const sum = byStage[st].reduce((a, i) => a + this.amt(i), 0);
|
||
return {
|
||
label: this.shortStage(st), count: byStage[st].length + (byStage[st].length === 1 ? ' investor' : ' investors'),
|
||
bg: sc.bg, text: sc.text, border: sc.border,
|
||
sum: this.money(sum), sumColor: sum > 0 ? p.money : 'var(--t4)',
|
||
cards: byStage[st].map(i => this.cardModel(i, theme, p)),
|
||
empty: byStage[st].length === 0,
|
||
};
|
||
});
|
||
|
||
// sections (accordion)
|
||
const sections = order.map(st => {
|
||
const sc = this.stageColors(st, theme);
|
||
const sum = byStage[st].reduce((a, i) => a + this.amt(i), 0);
|
||
const isOpen = !!s.open[st];
|
||
return {
|
||
label: this.shortStage(st), count: byStage[st].length + (byStage[st].length === 1 ? ' investor' : ' investors'),
|
||
bg: sc.bg, text: sc.text, border: sc.border,
|
||
sum: this.money(sum), sumColor: sum > 0 ? p.money : 'var(--t4)',
|
||
open: isOpen, rot: isOpen ? 90 : 0,
|
||
toggle: () => this.setState(prev => ({ open: Object.assign({}, prev.open, { [st]: !prev.open[st] }) })),
|
||
cards: byStage[st].map(i => this.cardModel(i, theme, p)),
|
||
empty: byStage[st].length === 0,
|
||
};
|
||
});
|
||
|
||
// detail sheet
|
||
const sel = list.find(i => i.id === s.sheetId);
|
||
let d = null;
|
||
if (sel) {
|
||
const selAmt = this.amt(sel);
|
||
d = {
|
||
name: sel.name, priority: !!sel.priority, existing: selAmt > 0, last: sel.daysAgo + 'd ago',
|
||
amount: this.money(selAmt), amtColor: selAmt > 0 ? p.money : 'var(--t4)',
|
||
contactLine: sel.contacts.length ? (sel.contacts[0].name + (sel.contacts.length > 1 ? ' +' + (sel.contacts.length - 1) : '')) : 'None',
|
||
stageOptions: order.map(st => {
|
||
const sc = this.stageColors(st, theme);
|
||
const on = sel.stage === st;
|
||
return {
|
||
label: this.shortStage(st), bg: sc.bg, text: sc.text, border: sc.border,
|
||
rowBg: on ? 'var(--elev)' : 'var(--input)', rowBorder: on ? 'var(--bstrong)' : 'var(--border)',
|
||
check: on ? '✓' : '',
|
||
pick: () => this.setStage(sel.id, st),
|
||
};
|
||
}),
|
||
hasNote: sel.notes.length > 0, noNotes: sel.notes.length === 0,
|
||
notes: sel.notes.map(n => { const nt = this.noteTag(n[0], theme); return { type: n[0].toUpperCase(), tagBg: nt.bg, tagText: nt.text, date: n[2], summary: n[1] }; }),
|
||
};
|
||
}
|
||
|
||
// log-activity sheet (layered over detail)
|
||
const lg = s.log;
|
||
const logTypes = ['Note', 'Email', 'Call', 'Meeting'].map(t => {
|
||
const on = lg && lg.type === t; const tc = this.noteTag(t, theme);
|
||
return { label: t, pick: () => this.setLog({ type: t }),
|
||
bg: on ? tc.bg : 'var(--input)', border: on ? 'var(--bstrong)' : 'var(--border)', text: on ? tc.text : 'var(--t3)' };
|
||
});
|
||
const logDisabled = !lg || !lg.summary.trim();
|
||
|
||
const dots = order.map((st, idx) => ({
|
||
go: (e) => this.goSegment(idx, e),
|
||
active: idx === s.active,
|
||
inactive: idx !== s.active,
|
||
}));
|
||
|
||
const sortOptions = [['name', 'Name', 'A → Z'], ['amount', 'Committed', 'Most first'], ['staleness', 'Last contact', 'Most stale first'], ['priority', 'Priority', 'Flagged first']].map(o => ({
|
||
label: o[1], hint: o[2], on: s.sortKey === o[0],
|
||
bg: s.sortKey === o[0] ? 'var(--elev)' : 'var(--input)', border: s.sortKey === o[0] ? 'var(--bstrong)' : 'var(--border)',
|
||
pick: () => this.setState({ sortKey: o[0], sortSheet: false }),
|
||
}));
|
||
|
||
// bottom tabs
|
||
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 === 'pipeline' ? 'var(--accent)' : 'var(--t4)',
|
||
icon: this.tabIcon(t.key, t.key === 'pipeline'),
|
||
go: () => { if (window.T31Store) window.T31Store.setTab(t.key); },
|
||
}));
|
||
|
||
return {
|
||
themeAttr: theme, themeIcon: theme === 'light' ? '☾' : '☀',
|
||
priBg: priC.bg, priText: priC.text,
|
||
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,
|
||
totalLabel: activeCount + ' active · ' + this.money(grandTotal) + ' committed',
|
||
modeLabel: mode === 'swipe' ? 'Swipe stages' : 'Accordion',
|
||
isSwipe: mode === 'swipe', isAccordion: mode === 'accordion',
|
||
segments, columns, sections, tabs, dots,
|
||
sortLabel: this.sortLabelFor(s.sortKey), sortOptions,
|
||
openSortSheet: () => this.setState({ sortSheet: true }),
|
||
closeSortSheet: () => this.setState({ sortSheet: false }),
|
||
sortSheet: s.sortSheet,
|
||
snapRef: el => { this._snap = el; }, onSnapScroll: e => this.onSnapScroll(e),
|
||
sheetOpen: !!sel, d,
|
||
closeSheet: () => this.setState({ sheetId: null }),
|
||
openLog: () => this.setState({ log: { type: 'Note', summary: '', details: '' } }),
|
||
closeLog: () => this.setState({ log: null }),
|
||
logOpen: !!lg && !!sel,
|
||
logFor: sel ? 'For ' + sel.name : '',
|
||
logTypes,
|
||
logSummary: lg ? lg.summary : '', onLogSummary: e => this.setLog({ summary: e.target.value }),
|
||
logDetails: lg ? lg.details : '', onLogDetails: e => this.setLog({ details: e.target.value }),
|
||
logDisabled,
|
||
logBtnBg: logDisabled ? 'var(--elev)' : 'linear-gradient(#3b82c4,#2f6ea9)',
|
||
logBtnText: logDisabled ? 'var(--t4)' : '#fff',
|
||
saveLog: () => this.saveLog(),
|
||
stop: e => e.stopPropagation(),
|
||
toast: s.toast,
|
||
};
|
||
}
|
||
|
||
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>
|