From 2796808759d76cd4c08a9c60268259c9f120f334 Mon Sep 17 00:00:00 2001 From: timmy Date: Thu, 13 Aug 2026 03:50:53 +0000 Subject: [PATCH] feat: surface mobile deadline urgency (Closes #705) --- frontend/dashboard.css | 2 ++ frontend/dashboard.js | 1 + frontend/index.html | 2 +- frontend/mobile-task-dock.js | 13 ++++++++++++- tests/test_mobile_task_dock.py | 32 ++++++++++++++++++++++++-------- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/frontend/dashboard.css b/frontend/dashboard.css index e1ede67..f103e24 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -591,6 +591,7 @@ textarea { resize: vertical; min-height: 120px; } .mobile-task-action[hidden] { display:none; } .mobile-task-action[aria-current="page"] { color:#bfdbfe; background:#17365a; outline:1px solid #31577f; } .mobile-task-count { min-width:18px; min-height:18px; padding:1px 5px; border-radius:999px; background:#31577f; font-size:11px; line-height:16px; } +.mobile-task-deadline { padding:1px 5px; border-radius:999px; color:#fef3c7; background:#92400e; font-size:10px; line-height:14px; } .attention-interruption:not([hidden]) { max-width:100%; display:flex; align-items:center; justify-content:space-between; gap:10px; margin:0 0 12px; padding:10px 12px; border:1px solid #31577f; border-radius:10px; background:#102641; } .attention-interruption button { min-height:44px; flex:0 0 auto; } @media (max-width: 600px) { @@ -668,6 +669,7 @@ textarea { resize: vertical; min-height: 120px; } .mobile-queue-list button > span:first-child { display:grid; gap:2px; } .mobile-queue-list small { color:var(--muted); } .mobile-queue-list [data-mobile-queue-count] { min-width:28px; padding:3px 8px; border-radius:999px; text-align:center; background:#1d426d; } + .mobile-queue-list [data-mobile-queue="agenda"][data-deadlines="true"] { border-color:#f59e0b; background:#30240f; box-shadow:inset 3px 0 #f59e0b; } .mobile-queue-list [data-recommended="true"] { border-color:#60a5fa; box-shadow:0 0 0 2px #60a5fa; } .mobile-today-hud { position:fixed; left:8px; right:8px; bottom:calc(56px + env(safe-area-inset-bottom)); z-index:44; display:grid; grid-template-columns:minmax(0,1fr) minmax(112px,auto); grid-template-areas:"summary complete" "progress toggle"; gap:4px 8px; max-width:100%; padding:8px; border:1px solid #31577f; border-radius:12px 12px 0 0; background:rgba(16,38,65,.98); box-shadow:0 -8px 24px rgba(0,0,0,.28); } .mobile-today-summary { grid-area:summary; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; font-weight:700; } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index e2d9d29..3bf330b 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -101,6 +101,7 @@ Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element]) ), queueBadge: qs('#mobile-queue-count'), + deadlineBadge: qs('#mobile-deadline-count'), onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name), overlays: mobileTaskOverlays, diff --git a/frontend/index.html b/frontend/index.html index 9a0ff2d..fe8bab5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -948,7 +948,7 @@ - + diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js index 506a8c5..e305b30 100644 --- a/frontend/mobile-task-dock.js +++ b/frontend/mobile-task-dock.js @@ -108,7 +108,18 @@ options.queueBadge.textContent = String(total); options.queueBadge.hidden = total === 0; } - buttons.queues?.setAttribute('aria-label', 'Queues, ' + total + ' items'); + if (options.deadlineBadge) { + options.deadlineBadge.textContent = normalized.agenda + ' due'; + options.deadlineBadge.hidden = normalized.agenda === 0; + } + if (options.queueRows?.agenda) { + if (normalized.agenda > 0) options.queueRows.agenda.setAttribute('data-deadlines', 'true'); + else options.queueRows.agenda.removeAttribute('data-deadlines'); + } + const deadlineLabel = normalized.agenda > 0 + ? ', ' + normalized.agenda + ' upcoming deadlines' + : ''; + buttons.queues?.setAttribute('aria-label', 'Queues, ' + total + ' items' + deadlineLabel); } return {start, select, refreshVisibility, updateAttention, updateQueues, updateWork}; diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py index 36a29f9..d9c0e72 100644 --- a/tests/test_mobile_task_dock.py +++ b/tests/test_mobile_task_dock.py @@ -293,18 +293,19 @@ sheet.showModal = function () {{ this.open = true; }}; sheet.close = function () {{ this.open = false; this.listeners.close?.(); }}; const close = new FakeElement(); const badge = new FakeElement(); -const rows = Object.fromEntries(['today','attention','update','later','draft','recaps'].map(name => [name, new FakeElement()])); +const deadline = new FakeElement(); +const rows = Object.fromEntries(['today','agenda','attention','update','later','draft','recaps'].map(name => [name, new FakeElement()])); const counts = Object.fromEntries(Object.keys(rows).map(name => [name, new FakeElement()])); const selected = []; const utilities = []; const dock = createDock({{ nav, buttons:{{queues}}, actions:{{queues:()=>{{}}}}, overlays:[sheet], - queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge, + queueSheet:sheet, queueClose:close, queueRows:rows, queueCounts:counts, queueBadge:badge, deadlineBadge:deadline, onSelectQueue:(name,row)=>name === 'recaps' ? utilities.push(row === rows.recaps ? 'recaps:trigger' : 'recaps:missing-trigger') : selected.push(name), observe() {{}}, }}); dock.start(); -dock.updateQueues({{today:2, attention:1, update:5, later:3, draft:4}}); +dock.updateQueues({{today:2, agenda:5, attention:1, update:5, later:3, draft:4}}); queues.click(); const opened = sheet.open; rows.update.click(); @@ -312,9 +313,18 @@ const closedAfterSelect = !sheet.open; queues.click(); rows.recaps.click(); const closedAfterUtility = !sheet.open; queues.click(); close.click(); +const populated = {{ + deadline:deadline.textContent, deadlineHidden:deadline.hidden, + agendaDue:rows.agenda.attributes['data-deadlines'] || null, + badgeLabel:queues.attributes['aria-label'], +}}; +dock.updateQueues({{today:0, agenda:0, attention:0, update:0, later:0, draft:0}}); process.stdout.write(JSON.stringify({{ opened, closedAfterSelect, closedAfterUtility, selected, utilities, - badge:badge.textContent, badgeLabel:queues.attributes['aria-label'], + badge:badge.textContent, populated, + clearedDeadlineHidden:deadline.hidden, + clearedAgendaDue:rows.agenda.attributes['data-deadlines'] || null, + clearedBadgeLabel:queues.attributes['aria-label'], counts:Object.fromEntries(Object.entries(counts).map(([name, node]) => [name, node.textContent])), updateLabel:rows.update.attributes['aria-label'], queueFocuses:queues.focuses, @@ -331,10 +341,13 @@ process.stdout.write(JSON.stringify({{ "selected": ["update"], "utilities": ["recaps:trigger"], # Updates are a drill-down within Attention and must not inflate the aggregate badge. - "badge": "10", - "badgeLabel": "Queues, 10 items", - "counts": {"today": "2", "attention": "1", "update": "5", "later": "3", "draft": "4", "recaps": "0"}, - "updateLabel": "Updates, 5 unread conversations", + "badge": "0", + "populated": {"badgeLabel": "Queues, 10 items, 5 upcoming deadlines", "deadline": "5 due", "deadlineHidden": False, "agendaDue": "true"}, + "clearedDeadlineHidden": True, + "clearedAgendaDue": None, + "clearedBadgeLabel": "Queues, 0 items", + "counts": {"today": "0", "agenda": "0", "attention": "0", "update": "0", "later": "0", "draft": "0", "recaps": "0"}, + "updateLabel": "Updates, 0 unread conversations", "queueFocuses": 3, "columnState": None, } @@ -436,6 +449,7 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher(): assert 'data-mobile-queue="recaps"' in html assert 'RecapsHistory' in html assert 'id="mobile-queue-count"' in html + assert 'id="mobile-deadline-count"' in html assert 'mobileTaskDock.updateQueues(counts)' in html assert '' in html assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html @@ -444,6 +458,8 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher(): assert '.mobile-queue-sheet' in html assert '.my-work-actions { display:none;' in html assert 'padding-bottom:calc(16px + env(safe-area-inset-bottom))' in html + assert '.mobile-task-deadline' in html + assert '[data-deadlines="true"]' in html assert '.mobile-task-dock[data-attention="true"]' not in html -- 2.43.0