diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index deaee06..a5655fd 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -576,6 +576,7 @@ textarea { resize: vertical; min-height: 120px; }
.pull-retry { min-height:44px; width:100%; margin-top:10px; }
.mobile-task-dock { display:none; }
.mobile-task-dock[hidden] { display:none; }
+
.mobile-today-hud { display:none; }
.mobile-today-hud[hidden], .mobile-today-hud[data-overlay-hidden="true"] { display:none; }
.mobile-task-action { min-width:0; min-height:44px; padding:6px 2px; border:0; border-radius:8px; background:transparent; display:grid; place-items:center; gap:2px; font-size:12px; }
@@ -649,6 +650,7 @@ textarea { resize: vertical; min-height: 120px; }
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
.mobile-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
+
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
@@ -658,6 +660,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-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; }
.mobile-today-hud [data-work-session-progress] { grid-area:progress; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 093466e..78084ab 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -73,12 +73,24 @@
planToday: () => openPlanToday(mobileTaskButtons.work),
openFallback: openMobileWorkFallback,
});
+ let mobileQueueCounts = {};
const mobileQueueLauncher = createMobileQueueLauncher({
openToday: () => mobileWorkEntry.open(),
selectFilter: selectMobileQueue,
firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit'),
announce: message => { qs('#my-work-action-status').textContent = message; },
+ getCounts: () => mobileQueueCounts,
+ openFindWork: () => qs('#find-work').click(),
});
+ function showMobileQueueCompletion(completedName) {
+ const next = mobileQueueLauncher.recommend();
+ qs('#mobile-queue-heading').textContent = completedName + ' cleared';
+ document.querySelectorAll('[data-mobile-queue]').forEach(row => row.removeAttribute('data-recommended'));
+ const row = qs('[data-mobile-queue="' + next.name + '"]');
+ row.setAttribute('data-recommended', 'true');
+ qs('#mobile-queue-sheet').showModal();
+ row.focus();
+ }
const mobileTaskDock = createMobileTaskDock({
nav: qs('#mobile-task-dock'),
sessionHud: qs('[data-mobile-today-hud]'),
@@ -93,7 +105,8 @@
Array.from(document.querySelectorAll('[data-mobile-queue-count]')).map(element => [element.dataset.mobileQueueCount, element])
),
queueBadge: qs('#mobile-queue-count'),
- onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name),
+ onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() :
+ name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name),
overlays: mobileTaskOverlays,
actions: {
work: () => mobileWorkEntry.open(),
@@ -882,7 +895,10 @@
onStatus: message => {
qs('#update-sheet-status').textContent = message;
qs('#retry-update-load').hidden = !message.startsWith('Could not load update.');
- if (message === 'Inbox cleared.') qs('#my-work-action-status').textContent = message;
+ if (message === 'Inbox cleared.') {
+ qs('#my-work-action-status').textContent = message;
+ showMobileQueueCompletion('Updates');
+ }
},
onClose: () => closeUpdateSheet(false),
});
@@ -2158,6 +2174,7 @@
const element = qs('[data-work-count="' + filter + '"]');
if (element) element.textContent = count;
});
+ mobileQueueCounts = counts;
mobileTaskDock.updateQueues(counts);
mobileTaskDock.updateWork(mobileWorkEntry.mode());
mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention);
diff --git a/frontend/index.html b/frontend/index.html
index 66fa1b4..0d5850d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -925,6 +925,7 @@
+
diff --git a/frontend/mobile-queue-launcher.js b/frontend/mobile-queue-launcher.js
index 8cf7faf..2dadf12 100644
--- a/frontend/mobile-queue-launcher.js
+++ b/frontend/mobile-queue-launcher.js
@@ -8,6 +8,21 @@
later: 'No deferred work is ready to open.',
draft: 'No drafts are ready to open.',
};
+ const continuation = [
+ ['attention', 'Start Attention'],
+ ['today', 'Continue Today'],
+ ['later', 'Start Later'],
+ ['draft', 'Open Drafts'],
+ ];
+
+ function recommend() {
+ const counts = options.getCounts ? options.getCounts() : {};
+ const match = continuation.find(([name]) => Number(counts[name]) > 0);
+ if (!match) return {name: 'find', count: 0, label: 'Find Work'};
+ const [name, label] = match;
+ const count = Math.max(0, Number(counts[name]) || 0);
+ return {name, count, label: label + ' (' + count + ')'};
+ }
function open(name) {
if (name === 'today') return options.openToday();
@@ -21,5 +36,14 @@
return 'opened';
}
- return { open };
+ function continueWork() {
+ const next = recommend();
+ if (next.name === 'find') {
+ options.openFindWork();
+ return 'find';
+ }
+ return open(next.name);
+ }
+
+ return { open, recommend, continueWork };
});
diff --git a/src/frontend_bundle.py b/src/frontend_bundle.py
index ce80df8..d8b53ac 100644
--- a/src/frontend_bundle.py
+++ b/src/frontend_bundle.py
@@ -28,7 +28,7 @@ FEATURE_SOURCES = {
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
"security-center": ("static/security-center.js",),
"today-timer": (
- "static/mobile-task-dock.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
+ "static/mobile-task-dock.js", "static/mobile-queue-launcher.js", "static/notification-undo.js", "static/today-timer.js", "static/today-recap.js",
"static/today-rollover.js", "static/later-work.js", "static/drafts.js", "static/unfiled-captures.js",
"static/draft-filing-session.js", "static/draft-capacity-dialog.js", "static/work-selection.js",
"static/today-work.js", "static/pick-work.js", "static/batch-find-work.js",
diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py
index 2d22ec3..1843bc8 100644
--- a/tests/test_mobile_task_dock.py
+++ b/tests/test_mobile_task_dock.py
@@ -368,6 +368,50 @@ process.stdout.write(JSON.stringify({{opened, empty, calls}}));
}
+def test_mobile_queue_launcher_recommends_and_revalidates_cross_queue_continuation():
+ script = f"""
+const createLauncher = require({json.dumps(str(QUEUE_LAUNCHER))});
+const calls = [];
+let counts = {{attention: 2, today: 1, later: 4, draft: 3}};
+const launcher = createLauncher({{
+ openToday: () => calls.push('today'),
+ selectFilter: name => calls.push('filter:' + name),
+ firstAction: name => (counts[name] || 0) ? {{click() {{ calls.push('open:' + name); }}}} : null,
+ announce: message => calls.push('announce:' + message),
+ getCounts: () => counts,
+ openFindWork: () => calls.push('find'),
+}});
+const first = launcher.recommend();
+counts = {{attention: 0, today: 0, later: 4, draft: 3}};
+const opened = launcher.continueWork();
+counts = {{attention: 0, today: 0, later: 0, draft: 0}};
+const fallback = launcher.recommend();
+launcher.continueWork();
+process.stdout.write(JSON.stringify({{first, opened, fallback, calls}}));
+"""
+ result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
+
+ assert result.returncode == 0, result.stderr
+ assert json.loads(result.stdout) == {
+ "first": {"name": "attention", "count": 2, "label": "Start Attention (2)"},
+ "opened": "opened",
+ "fallback": {"name": "find", "count": 0, "label": "Find Work"},
+ "calls": ["filter:later", "open:later", "find"],
+ }
+
+
+@pytest.mark.anyio
+async def test_dashboard_renders_accessible_mobile_queue_completion_handoff():
+ html = await dashboard()
+
+ assert 'id="mobile-queue-heading"' in html
+ assert 'data-mobile-queue="find"' in html
+ assert "mobileQueueLauncher.recommend()" in html
+ assert "showMobileQueueCompletion('Updates')" in html
+ assert "row.setAttribute('data-recommended', 'true')" in html
+ assert "row.focus()" in html
+
+
@pytest.mark.anyio
async def test_dashboard_renders_and_wires_mobile_queue_switcher():
html = await dashboard()
@@ -388,7 +432,7 @@ async def test_dashboard_renders_and_wires_mobile_queue_switcher():
assert '' in html
assert "const mobileQueueLauncher = createMobileQueueLauncher({" in html
assert "firstAction: name => qs('#my-work-list .my-work-card-main, #my-work-list .draft-resume, #my-work-list .draft-continue, #my-work-list .draft-edit')" in html
- assert "onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name)" in html
+ assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
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
@@ -434,7 +478,7 @@ async def test_dashboard_renders_and_wires_phone_safe_task_dock():
assert '' in html
assert "createMobileTaskDock({" in html
assert "work: () => mobileWorkEntry.open()" in html
- assert "onSelectQueue:name => name === 'recaps' ? qs('#open-today-recaps').click() : mobileQueueLauncher.open(name)" in html
+ assert "name === 'find' ? qs('#find-work').click() : mobileQueueLauncher.open(name)" in html
assert "qs('[data-work-filter=\"' + name + '\"]').click()" in html
assert "find: () => qs('#find-work').click()" in html
assert "new: () => qs('#new-issue').click()" in html