diff --git a/AGENTS.md b/AGENTS.md index cc0af1a..7eea9ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,11 +107,11 @@ Subsystem rules live in `docs/guides/` and lazy-load in Claude Code via `.claude ## Current state -_**Box live at v0.1.0:94**; `main` ahead by mobile Phases 0–7 + P3b + drag-reorder + **8a + 8b + 8c + 8d** — **all deploy-pending** (no s9pk built). **The fundraising grid + email capture is the canonical system of record.** Active thread: **mobile-first redesign → Phase 8**, building to `design/phase8-conformance.md` (the 8a–8i spec, anchored on each `*.dc.html` DEFAULT `data-props` — NOT the `screenshots/` PNGs). **Plan (Grant, 2026-06-19): finish features first → then Grant device-tests + deploys** (nothing verified on a real phone). History: git log + `start9/0.4/startos/versions/`._ +_**Box live at v0.1.0:94**; `main` ahead by mobile Phases 0–7 + P3b + drag-reorder + **8a + 8b + 8c + 8d + 8e** — **all deploy-pending** (no s9pk built). **The fundraising grid + email capture is the canonical system of record.** Active thread: **mobile-first redesign → Phase 8**, building to `design/phase8-conformance.md` (the 8a–8i spec, anchored on each `*.dc.html` DEFAULT `data-props` — NOT the `screenshots/` PNGs). **Plan (Grant, 2026-06-19): finish features first → then Grant device-tests + deploys** (nothing verified on a real phone). History: git log + `start9/0.4/startos/versions/`._ - **Mobile redesign — 4 core surfaces built (Grid · Contacts · Pipeline · Reminders), each a rules-of-hooks-safe `useIsMobile()` → `Mobile*`/`Desktop*` pair (desktop untouched).** Foundation: bottom-tab bar + `:root` mobile vars; 4-stage enum; derived grid signals injected-on-GET/stripped-on-write at both points; mobile writes use **one-row endpoints only** (log-communication, pipeline link/stage, reminders, `update-row`) — never whole-grid PUT. -- **Phase 8 done so far (8a–8d):** cards re-authored (existing-LP **earmark**/avatar **ring**, PRIORITY/Existing-LP pills, 4-stage chip, recency; disposition badges dropped); **Contacts + Pipeline detail → drag-dismiss bottom sheets** (email-copy pill, Log/Email, Org card, stat tiles, inline move-stage, notes timeline); **Grid-detail notes timeline** + shared `LogCommunicationSheet`; **top-bar quick-log pencil** (`MobileQuickLog`, all tabs); **sort controls** on Grid/Pipeline/Contacts (shared `SortPill`/`SortSheet`; Contacts type-tabs dropped + Priority sort). Per-phase detail in git log + the Design convention's primitives list. +- **Phase 8 done so far (8a–8e):** cards re-authored (existing-LP **earmark**/avatar **ring**, PRIORITY/Existing-LP pills, 4-stage chip, recency; disposition badges dropped); **Contacts + Pipeline detail → drag-dismiss bottom sheets** (email-copy pill, Log/Email, Org card, stat tiles, inline move-stage, notes timeline); **Grid-detail notes timeline** + shared `LogCommunicationSheet`; **top-bar quick-log pencil** (`MobileQuickLog`, all tabs); **sort controls** on Grid/Pipeline/Contacts (shared `SortPill`/`SortSheet`; Contacts type-tabs dropped + Priority sort); **8e Reminders re-bucketed** — Active/Done/All tabs dropped → title + urgency **summary line** + gradient add; **4 buckets** Overdue/Today/This-week/Later each with a colored **dot**; urgency-colored **`DueChip`** pill (`--due-*` theme vars) replaces plain due text; collapsible **Completed** section (done+cancelled, strikethrough, tap-check reopens); card now note + org + due-chip (assignee dropped from card, still in edit sheet); **swipe-right → snooze preset sheet** (+1/+3/+7/+14d, dc-style) replacing the old fixed +7d; **add-flow investor picker** — a stacked searchable sheet over the canonical grid investors that writes **`source_row_id` → a real server-resolved `investor_id` link** (replaces the old free-text label that never actually linked; "team task" = no investor). Per-phase detail in git log + the Design convention's primitives list. - **Live (deployed):** W2 NL query (v94); W1 reminders (v93); grid Pipeline (v88); Matrix intake + Gmail capture (DWD) + daily digest; Thesis/Architect (dual-approval); outreach — all draft-only. -- **Tests:** **39/39 backend green** (`python3 backend/run_tests.py`; +`test_grid_comm_timeline.py` for the 8c timeline filter, +priority assertions in `test_contacts_grid_signals.py`), `py_compile` clean; 8c+8d surfaces interaction-verified via throwaway 375px jsdom harnesses (deleted after). -- **Next — Phase 8, in order, build to `design/phase8-conformance.md`:** **8e** reminders (due-chip + Overdue/Today/This-week/Later buckets + dots + snooze sheet + investor picker) → **8f** Pipeline card (earmark/Priority/recency + horizontal-scroll stage pills + dots) → **8g** add-investor stage+priority → **8h** loose ends (incl. Grid detail G4/G5/G6 stage-card/reminder-card/timeline; "Open-in-Grid" deep-link-to-investor) → **8i** shell SVG icons + `·Ten31·` wordmark. **Skip Pipeline accordion** (Grant). **Then (after feature-complete):** deploy P0–P8 + P3b in one s9pk (**authorize + version-bump first**) and device-test light/dark on a phone. +- **Tests:** **39/39 backend green** (`python3 backend/run_tests.py`; +`test_grid_comm_timeline.py` for the 8c timeline filter, +priority assertions in `test_contacts_grid_signals.py`), `py_compile` clean; 8c+8d+8e surfaces interaction-verified via throwaway 375px jsdom harnesses (deleted after). 8e was frontend-only — no backend touched, so the 39/39 count is unchanged (the reminders read path was untouched: the list endpoint with no `status` param already returns all statuses, which 8e now splits client-side). +- **Next — Phase 8, in order, build to `design/phase8-conformance.md`:** **8f** Pipeline card (earmark/Priority/recency + horizontal-scroll stage pills + dots) → **8g** add-investor stage+priority → **8h** loose ends (incl. Grid detail G4/G5/G6 stage-card/reminder-card/timeline; "Open-in-Grid" deep-link-to-investor) → **8i** shell SVG icons + `·Ten31·` wordmark. **Skip Pipeline accordion** (Grant). **Then (after feature-complete):** deploy P0–P8 + P3b in one s9pk (**authorize + version-bump first**) and device-test light/dark on a phone. - **Open / risks:** all mobile work + light theme **built but never deployed or device-tested** (smoke/jsdom only); `MobileDetailRow` now unused-but-retained (legacy-usage sweep); Pipeline detail "Committed" tile shows grid-committed not deal-expected (deal forecast in a footnote); W2 happy-path only; **Claude/Architect path unverified live on the box**; v2.0 reserve-asset spine **not canonical**; doc drift — `crm-overview.md`/`EVALUATION.md` still call `lp_profiles` live. diff --git a/frontend/index.html b/frontend/index.html index 0ed3b7c..29bbaef 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -93,6 +93,11 @@ /* reminder status badges (open/snoozed/done/cancelled) — light values derived, since the comp models reminder URGENCY rather than these four statuses */ --rem-open: #7fb0d3; --rem-snoozed: #b08fd3; --rem-done: #7fd3a3; --rem-cancelled: #70859b; + /* reminder due-chips — bg / text / border, by urgency bucket (comp RemindersApp urgency, :228-240) */ + --due-overdue-bg: #f8717118; --due-overdue-text: #f87171; --due-overdue-border: #f8717140; + --due-today-bg: #e0b3411f; --due-today-text: #e0b341; --due-today-border: #e0b3413d; + --due-week-bg: #3b82c422; --due-week-text: #93c5fd; --due-week-border: #3b82c44d; + --due-later-bg: #1b2a3a; --due-later-text: #8ea2b7; --due-later-border: #263548; /* --- Phase 7 (2026-06-19): leftover literals that didn't flip in P6 (DESIGN §8). --- */ --nav-bg: rgba(17, 26, 39, 0.92); /* mobile bottom-tab-bar */ --panel-grad-end: #101926; /* sidebar / header gradient bottom stop */ @@ -159,6 +164,10 @@ --chip-commitment-bg: #10b98118; --chip-commitment-text: #057a55; --chip-commitment-border: #a9ddca; --chip-default-bg: #5a6b7d12; --chip-default-text: #84909e; --chip-default-border: #d6dde7; --rem-open: #2266a0; --rem-snoozed: #7a4fa8; --rem-done: #057a55; --rem-cancelled: #84909e; + --due-overdue-bg: #c0322f14; --due-overdue-text: #c0322f; --due-overdue-border: #e3b4b2; + --due-today-bg: #e0b34122; --due-today-text: #8a6c12; --due-today-border: #e4d29a; + --due-week-bg: #3b82c416; --due-week-text: #1f6fb8; --due-week-border: #bcd2ea; + --due-later-bg: #5a6b7d12; --due-later-text: #5a6b7d; --due-later-border: #d6dde7; /* Phase 7 light overrides (accent-grad-end is theme-stable → dark only). */ --nav-bg: #ffffffd9; --panel-grad-end: #f4f7fb; @@ -2623,16 +2632,27 @@ .pipeline-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--border-strong); transition: background 0.15s ease; } .pipeline-dot.active { background: var(--accent); } - /* ─── Phase 5 — Reminders mobile surface (urgency-grouped list + swipe actions) ─────── - JS-gated to MobileReminders; reuses the .mobile-toolbar / .mobile-seg / .grid-new-btn / - BottomSheet patterns. Each row is a pointer-drag swipe: left reveals Done, right reveals - Snooze (threshold 70px); a tap (no drag) opens the edit sheet (BRIEF §3d). */ + /* ─── Reminders mobile surface (8e: urgency-bucketed list + due-chips + swipe actions) ─── + JS-gated to MobileReminders. Header = title + summary line + gradient add (dc :56-62); + four buckets Overdue/Today/This week/Later, each with a colored dot (dc :317-318); each + row is a pointer-drag swipe (left reveals Complete, right reveals Snooze, threshold 70px, + tap opens the edit sheet); terminal items live in a collapsible Completed section. */ + .rem-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 14px; } + .rem-header-titles { display: flex; flex-direction: column; gap: 3px; min-width: 0; } + .rem-title { font-size: var(--mobile-font-screen-title); font-weight: 600; letter-spacing: -0.01em; color: var(--text-primary); } + .rem-summary { font-family: 'IBM Plex Mono', monospace; font-size: 12px; } /* color set inline by urgency */ + .rem-add-btn { + flex: none; width: 44px; height: 44px; border-radius: 10px; border: none; + background: linear-gradient(180deg, var(--accent) 0%, var(--accent-strong) 100%); + color: #fff; font-size: 22px; font-weight: 500; line-height: 1; cursor: pointer; + } .reminder-group-header { - display: flex; align-items: baseline; gap: 8px; margin: 16px 2px 8px; + display: flex; align-items: center; gap: 8px; margin: 16px 2px 8px; color: var(--text-subtle); font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; } .reminder-group-header:first-child { margin-top: 4px; } + .reminder-group-dot { flex: none; width: 7px; height: 7px; border-radius: 999px; } .reminder-group-count { color: var(--text-subtle); font-weight: 500; } .reminder-row { position: relative; overflow: hidden; border-radius: var(--mobile-card-radius); margin-bottom: var(--mobile-card-gap); } .reminder-action { @@ -2642,18 +2662,72 @@ .reminder-action.done { justify-content: flex-end; background: rgba(16,185,129,0.16); color: var(--money); } .reminder-action.snooze { justify-content: flex-start; background: rgba(224,179,65,0.14); color: var(--due-soon); } .reminder-fg { - position: relative; z-index: 1; display: block; width: 100%; text-align: left; color: inherit; + position: relative; z-index: 1; display: flex; flex-direction: column; gap: 5px; + width: 100%; text-align: left; color: inherit; background: var(--bg-panel); border: 1px solid var(--border); - border-radius: var(--mobile-card-radius); padding: 12px 14px; cursor: pointer; + border-radius: var(--mobile-card-radius); padding: 13px 14px; cursor: pointer; box-shadow: 0 14px 26px rgba(2,12,24,0.28), inset 0 1px 0 #ffffff07; transition: transform 0.18s ease; touch-action: pan-y; } - .reminder-fg-head { display: flex; align-items: baseline; gap: 8px; } - .reminder-title { font-size: var(--mobile-font-card-title); font-weight: 600; color: var(--text-primary); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .reminder-status-tag { flex: none; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } - .reminder-meta { font-size: 12px; color: var(--text-muted); margin-top: 4px; } - .reminder-due { font-family: 'IBM Plex Mono', monospace; } - .reminder-details { font-size: 12px; color: var(--text-secondary); margin-top: 5px; white-space: pre-wrap; line-height: 1.45; } + .reminder-title { font-size: var(--mobile-font-body); font-weight: 500; line-height: 1.3; color: var(--text-primary); } + .reminder-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + .reminder-org { font-size: 12px; color: var(--text-muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + /* Urgency-colored due-chip pill (dc RemindersApp:91, urgency :228-240). */ + .due-chip { + flex: none; font-family: 'IBM Plex Mono', monospace; font-size: 10px; font-weight: 600; + letter-spacing: 0.04em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; + border: 1px solid transparent; + } + .due-chip--overdue { background: var(--due-overdue-bg); color: var(--due-overdue-text); border-color: var(--due-overdue-border); } + .due-chip--today { background: var(--due-today-bg); color: var(--due-today-text); border-color: var(--due-today-border); } + .due-chip--week { background: var(--due-week-bg); color: var(--due-week-text); border-color: var(--due-week-border); } + .due-chip--later { background: var(--due-later-bg); color: var(--due-later-text); border-color: var(--due-later-border); } + .reminder-details { font-size: 12px; color: var(--text-secondary); white-space: pre-wrap; line-height: 1.45; } + /* Collapsible Completed section at list end (dc :109-130). Done/cancelled rows, dimmed. */ + .rem-completed-toggle { + width: 100%; background: none; border: none; cursor: pointer; margin-top: 8px; + display: flex; align-items: center; gap: 8px; padding: 0 2px 9px; color: var(--text-subtle); + font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 600; + letter-spacing: 0.08em; text-transform: uppercase; + } + .rem-completed-caret { flex: none; width: 12px; font-size: 12px; transition: transform 0.15s ease; } + .rem-completed-caret.open { transform: rotate(90deg); } + .rem-done-row { + display: flex; align-items: center; gap: 12px; opacity: 0.6; + background: var(--bg-panel); border: 1px solid var(--border); + border-radius: var(--mobile-card-radius); padding: 12px 13px; margin-bottom: var(--mobile-card-gap); + } + .rem-done-check { + flex: none; width: 24px; height: 24px; border-radius: 999px; cursor: pointer; line-height: 1; + border: 2px solid var(--accent); background: var(--accent); color: #fff; + display: flex; align-items: center; justify-content: center; font-size: 12px; + } + .rem-done-check.cancelled { background: transparent; border-color: var(--danger-soft); color: var(--danger-soft); } + .rem-done-body { flex: 1; min-width: 0; text-align: left; background: none; border: none; cursor: pointer; display: flex; flex-direction: column; gap: 4px; color: inherit; } + .rem-done-note { font-size: var(--mobile-font-body); font-weight: 500; line-height: 1.3; text-decoration: line-through; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .rem-done-org { font-size: 12px; color: var(--text-subtle); } + /* Add-flow investor picker field (tap → stacked picker sheet, dc :416-428). Styled like an input. */ + .rem-investor-pick { + width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 8px; + height: var(--mobile-input-h); padding: 0 12px; cursor: pointer; text-align: left; + background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--mobile-control-radius); + color: var(--text-primary); font-size: var(--mobile-font-body); font-family: inherit; + } + .rem-investor-pick > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .rem-investor-pick-empty { color: var(--text-subtle); } + .rem-investor-pick-caret { flex: none; color: var(--text-muted); } + .rem-investor-list { margin-top: 12px; max-height: 46vh; overflow-y: auto; } + .rem-investor-hint { font-size: 13px; color: var(--text-subtle); padding: 16px 4px; } + /* Snooze preset rows (dc :408-412): label left, resolved date right. */ + .snooze-list { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; } + .snooze-row { + width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; + min-height: 50px; padding: 0 16px; cursor: pointer; font-family: inherit; + background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--mobile-control-radius); + color: var(--text-primary); font-size: var(--mobile-font-body); font-weight: 500; + } + .snooze-row:active { background: var(--bg-hover); } + .snooze-date { font-family: 'IBM Plex Mono', monospace; font-size: 13px; color: var(--text-subtle); } /* Visibility utilities — base = desktop; flipped under the breakpoint. */ .mobile-only { display: none; } @@ -4837,39 +4911,55 @@ const today = new Date(); today.setHours(0, 0, 0, 0); return Math.round((due - today) / 86400000); }; - const formatDueShort = (iso) => { - const delta = reminderDueDelta(iso); - if (delta == null) return 'No due date'; + // Short "Jun 24" month-day for a YYYY-MM-DD string (chip 'later' label + snooze/toast dates). + const reminderMonthDay = (iso) => { + if (!iso) return ''; const d = new Date(String(iso).slice(0, 10) + 'T00:00:00'); - const label = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); - if (delta === 0) return 'Due today'; - if (delta < 0) return `${label} · ${-delta}d overdue`; - if (delta <= 7) return `${label} · in ${delta}d`; - return `Due ${label}`; + if (Number.isNaN(d.getTime())) return ''; + return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); }; - // Urgency bucket: done/cancelled collapse to 'done'; open/snoozed split by due-date delta. + // Due-chip: urgency bucket + short label for the card pill (dc RemindersApp urgency, :228-240). + // key drives the .due-chip--{key} color; text is "3d overdue" / "Today" / "Tomorrow" / "in 4d" / "Jun 24". + const reminderDueChip = (iso) => { + const d = reminderDueDelta(iso); + if (d == null) return { key: 'later', text: 'No date' }; + if (d < 0) return { key: 'overdue', text: `${-d}d overdue` }; + if (d === 0) return { key: 'today', text: 'Today' }; + if (d === 1) return { key: 'week', text: 'Tomorrow' }; + if (d <= 7) return { key: 'week', text: `in ${d}d` }; + return { key: 'later', text: reminderMonthDay(iso) }; + }; + const DueChip = ({ iso }) => { + const { key, text } = reminderDueChip(iso); + return {text}; + }; + // Snooze presets relative to today (dc snoozePresets, :371-373) → [label, YYYY-MM-DD]. + const reminderSnoozePresets = () => { + const mk = (days) => { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); }; + return [['Tomorrow', mk(1)], ['In 3 days', mk(3)], ['1 week', mk(7)], ['2 weeks', mk(14)]]; + }; + // Active-reminder bucket (Overdue/Today/This week/Later, dc :318) — split by due-date delta. + // Terminal items (done/cancelled) are filtered out before bucketing and rendered separately. const reminderBucket = (r) => { - if (r.status === 'cancelled') return 'cancelled'; - if (r.status === 'done') return 'done'; const delta = reminderDueDelta(r.due_date); if (delta == null) return 'later'; if (delta < 0) return 'overdue'; - if (delta <= 7) return 'soon'; + if (delta === 0) return 'today'; + if (delta <= 7) return 'week'; return 'later'; }; + // Bucket dots (dc :317): red / amber / accent-blue / grey. Theme-bound so they flip in light. const REMINDER_BUCKETS = [ - { key: 'overdue', label: 'Overdue' }, - { key: 'soon', label: 'Due soon' }, - { key: 'later', label: 'Later' }, - { key: 'done', label: 'Done' }, - { key: 'cancelled', label: 'Cancelled' }, + { key: 'overdue', label: 'Overdue', dot: 'var(--due-overdue-text)' }, + { key: 'today', label: 'Today', dot: 'var(--due-today-text)' }, + { key: 'week', label: 'This week', dot: 'var(--accent)' }, + { key: 'later', label: 'Later', dot: 'var(--text-subtle)' }, ]; - const REMINDER_STATUS_COLOR = { open: 'var(--rem-open)', snoozed: 'var(--rem-snoozed)', done: 'var(--rem-done)', cancelled: 'var(--rem-cancelled)' }; // One reminder row: a pointer-drag swipe over a tappable foreground. Encapsulates its own // drag state so rows don't share a translate. Tap (no drag) → onTap; swipe-left past 70px → // onDone; swipe-right → onSnooze. Vertical-dominant drags are released to the list scroll. - const ReminderRow = ({ r, canSwipe, onTap, onDone, onSnooze }) => { + const ReminderRow = ({ r, onTap, onDone, onSnooze }) => { const [dx, setDx] = useState(0); const drag = useRef(null); const onPointerDown = (e) => { @@ -4885,7 +4975,7 @@ d.active = true; } d.moved = true; - if (canSwipe) setDx(Math.max(-120, Math.min(120, ddx))); + setDx(Math.max(-120, Math.min(120, ddx))); }; const end = (e) => { const d = drag.current; drag.current = null; setDx(0); @@ -4894,34 +4984,23 @@ // gesture (a 0-x would read as a big left-swipe and spuriously mark-done). Just snap back. if (e && e.type === 'pointercancel') return; if (!d.moved) { onTap(); return; } - // non-swipeable rows (done/cancelled) have no swipe action — recover a stray drag as a - // tap so the edit sheet (the only way to reopen them) stays reachable. - if (!canSwipe) { onTap(); return; } const ddx = e && typeof e.clientX === 'number' ? e.clientX - d.x : 0; if (ddx <= -70) onDone(); else if (ddx >= 70) onSnooze(); }; - const dueDelta = reminderDueDelta(r.due_date); - const dueColor = (r.status === 'open' || r.status === 'snoozed') - ? (dueDelta != null && dueDelta < 0 ? 'var(--danger-soft)' : dueDelta != null && dueDelta <= 7 ? 'var(--due-soon)' : 'var(--text-subtle)') - : 'var(--text-subtle)'; return (