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

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

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

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

Verified: render-smoke green; a jsdom interaction harness on the authed
shell exercised the toggle (boot-dark -> light+persist+relabel -> dark);
dark-identity, theme-parity, and no-undefined-var checks all green. Not yet
checked on a real phone/browser.
This commit is contained in:
Keysat
2026-06-19 16:38:30 -05:00
parent 7f711d1fae
commit e6a89450da
21 changed files with 5521 additions and 374 deletions
@@ -0,0 +1,145 @@
/* Ten31 CRM — shared client store (single source of truth across the four mobile surfaces).
Mirrors the server model: investors carry stage/notes/funds/contacts/priority/last_activity;
reminders are their own collection keyed by investor. One canonical copy so a stage move,
logged communication, or reminder edit on any tab is reflected on every other tab.
Installed as a window singleton so it survives child-surface remounts within the shell. */
(function () {
if (window.T31Store) return;
var C = function (name, email) { return { name: name, email: email }; };
function seedInvestors() {
return [
{ id: 1, name: 'Northwall Capital', priority: true, stage: 'commitment', daysAgo: 2,
contacts: [C('Dana Reyes', 'dana@northwall.com'), C('Per Holt', 'per@northwall.com')],
funds: [['Ten31 Terahash', 1500000], ['Sats and Stats', 600000], ['Join the Fold', 400000]],
views: ['Main Fundraising', 'All Investors'],
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', priority: true, stage: 'engaged', daysAgo: 5,
contacts: [C('Omar Said', 'omar@brightseed.vc')], funds: [['Ten31 Terahash', 0]],
views: ['Main Fundraising', 'Follow-up List'], notes: [['Note', 'Intro from Polaris — warm', '2026-06-14']] },
{ id: 3, name: 'Cedarline Family Office', priority: false, stage: 'commitment', daysAgo: 7,
contacts: [C('Lena Cho', 'lena@cedarline.com')], funds: [['Ten31 Terahash', 800000], ['Pawn to F4', 400000]],
views: ['Main Fundraising', 'All Investors', 'Fund II investors'], notes: [['Call', 'Wire received, fully funded', '2026-06-12']] },
{ id: 4, name: 'Vance & Co', priority: false, stage: 'engaged', daysAgo: 3,
contacts: [C('Marcus Vance', 'mv@vanceco.com')], funds: [['Ten31 Terahash', 0]],
views: ['Main Fundraising', 'Follow-up List'], notes: [] },
{ id: 5, name: 'Polaris Endowment', priority: true, stage: 'diligence', daysAgo: 1,
contacts: [C('Ruth Almeida', 'ralmeida@polaris.org')], funds: [['Ten31 Terahash', 3000000], ['Sats and Stats', 2000000]],
views: ['Main Fundraising', 'All Investors', 'Follow-up List', 'Fund II investors'],
notes: [['Meeting', 'IC presentation went well', '2026-06-18'], ['Email', 'Sent data room access', '2026-06-15']] },
{ id: 6, name: 'Hartman Group', priority: false, stage: null, daysAgo: 14,
contacts: [], funds: [['Ten31 Terahash', 0]], views: ['Main Fundraising'], notes: [] },
{ id: 7, name: 'Meridian Trust', priority: false, stage: 'commitment', daysAgo: 4,
contacts: [C('Sofia Marin', 'sofia@meridiantrust.com')], funds: [['Ten31 Terahash', 800000]],
views: ['Main Fundraising', 'All Investors'], notes: [['Note', 'Signed side letter', '2026-06-14']] },
{ id: 8, name: 'Atlas Ventures Fund', priority: false, stage: 'engaged', daysAgo: 6,
contacts: [C('Will Tanaka', 'will@atlasvf.com')], funds: [['Ten31 Terahash', 0]],
views: ['Main Fundraising'], notes: [] },
{ id: 9, name: 'K. Whitfield', priority: false, stage: null, daysAgo: 21,
contacts: [C('Kira Whitfield', 'kira@whitfield.io')], funds: [],
views: ['Graveyard'], notes: [['Note', 'No allocation — parked', '2026-05-28']] },
{ id: 10, name: 'Granite Bay LP', priority: false, stage: 'commitment', daysAgo: 30,
contacts: [C('Tom Becker', 'tom@granitebay.com')], funds: [['Ten31 Terahash', 2000000], ['Sats and Stats', 1300000]],
views: ['Main Fundraising', 'All Investors', 'Fund II investors'], notes: [] },
{ id: 11, name: 'Forsythe Holdings', priority: false, stage: 'lead', daysAgo: 35,
contacts: [], funds: [], views: ['Graveyard'], notes: [] }
];
}
function seedReminders() {
return [
{ id: 1, note: 'Resend deck — bounced', orgId: 4, due: '2026-06-18', done: false },
{ id: 2, note: 'Re-engage — cold 2 weeks', orgId: 6, due: '2026-06-16', done: false },
{ id: 3, note: 'IC memo due', orgId: 5, due: '2026-06-19', done: false },
{ id: 4, note: 'Follow up after intro call', orgId: 2, due: '2026-06-19', done: false },
{ id: 5, note: 'Share data room link', orgId: 8, due: '2026-06-20', done: false },
{ id: 6, note: 'Countersign side letter', orgId: 7, due: '2026-06-21', done: false },
{ id: 7, note: 'Send Q2 update deck', orgId: 1, due: '2026-06-24', done: false },
{ id: 8, note: 'Quarterly check-in call', orgId: 3, due: '2026-07-08', done: false },
{ id: 9, note: 'Thank-you note post-wire', orgId: 10, due: '2026-06-13', done: true }
];
}
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var store = {
investors: seedInvestors(),
reminders: seedReminders(),
tab: 'grid',
theme: 'dark',
focusInvestorId: null,
today: new Date(2026, 5, 19),
_subs: [],
subscribe: function (fn) {
this._subs.push(fn);
var self = this;
return function () { self._subs = self._subs.filter(function (f) { return f !== fn; }); };
},
_notify: function () { this._subs.slice().forEach(function (f) { try { f(); } catch (e) {} }); },
setTab: function (t) { this.tab = t; this._notify(); },
openInvestor: function (id) { this.focusInvestorId = id; this.tab = 'grid'; this._notify(); },
setTheme: function (t) { this.theme = t; this._notify(); },
toggleTheme: function () { this.theme = this.theme === 'light' ? 'dark' : 'light'; this._notify(); },
// ----- investor mutations -----
updateInvestor: function (id, patch) {
this.investors = this.investors.map(function (i) { return i.id === id ? Object.assign({}, i, patch) : i; });
this._notify();
},
logNote: function (id, entry) {
this.investors = this.investors.map(function (i) { return i.id === id ? Object.assign({}, i, { notes: [entry].concat(i.notes), daysAgo: 0 }) : i; });
this._notify();
},
addInvestor: function (inv) {
var id = this.investors.reduce(function (m, i) { return Math.max(m, i.id); }, 0) + 1;
var ni = Object.assign({ id: id, priority: false, stage: 'lead', daysAgo: 0, contacts: [], funds: [['Ten31 Terahash', 0]], views: ['Main Fundraising'], notes: [] }, inv);
this.investors = [ni].concat(this.investors);
this._notify();
return id;
},
// ----- reminder mutations -----
addReminder: function (orgId, note, due) {
var id = this.reminders.reduce(function (m, r) { return Math.max(m, r.id); }, 0) + 1;
this.reminders = [{ id: id, note: note, orgId: orgId, due: due, done: false }].concat(this.reminders);
this._notify();
return id;
},
updateReminder: function (id, patch) {
this.reminders = this.reminders.map(function (r) { return r.id === id ? Object.assign({}, r, patch) : r; });
this._notify();
},
deleteReminder: function (id) {
this.reminders = this.reminders.filter(function (r) { return r.id !== id; });
this._notify();
},
toggleReminder: function (id) {
this.reminders = this.reminders.map(function (r) { return r.id === id ? Object.assign({}, r, { done: !r.done }) : r; });
this._notify();
},
reminderFor: function (orgId) {
// the soonest open reminder for an investor (used by the Grid detail)
var open = this.reminders.filter(function (r) { return r.orgId === orgId && !r.done; });
open.sort(function (a, b) { return a.due < b.due ? -1 : 1; });
return open[0] || null;
},
// ----- derived helpers -----
investorById: function (id) { return this.investors.find(function (i) { return i.id === id; }); },
committed: function (i) { return (i.funds || []).reduce(function (a, f) { return a + f[1]; }, 0); },
money: function (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;
},
parseDate: function (iso) { var p = iso.split('-'); return new Date(+p[0], +p[1] - 1, +p[2]); },
diffDays: function (iso) { return Math.round((this.parseDate(iso) - this.today) / 86400000); },
monthDay: function (iso) { var d = this.parseDate(iso); return MONTHS[d.getMonth()] + ' ' + d.getDate(); }
};
window.T31Store = store;
})();