Files
ten31-database/design/_imports/2026-06-19_zip-file/Venture-CRM mobile redesign/PipelineApp.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

662 lines
46 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; } }
@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="{&quot;$preview&quot;:{&quot;width&quot;:393,&quot;height&quot;:812},&quot;mode&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;swipe&quot;,&quot;accordion&quot;],&quot;default&quot;:&quot;swipe&quot;,&quot;tsType&quot;:&quot;'swipe'|'accordion'&quot;},&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._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>