diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index a5655fd..e1ede67 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -77,6 +77,14 @@ main { position: relative; z-index: 1; display: grid; grid-template-columns: 300
h2 { font-size: 13px; margin: 8px 0; text-transform: uppercase; letter-spacing: .08em; color: #94a3b8; }
a { color: #60a5fa; text-decoration: none; }
.muted { color: #94a3b8; }
+.empty-work-start { display:grid; gap:12px; margin:12px 0; padding:16px; border:1px solid #31577f; border-radius:14px; background:#0b1b2f; }
+.empty-work-start[hidden] { display:none; }
+.empty-work-start p { margin:6px 0 0; }
+.empty-work-start-actions { display:grid; grid-template-columns:minmax(0, 1fr) minmax(0, 1fr); gap:8px; }
+.empty-work-start-actions button { min-width:0; min-height:44px; white-space:normal; }
+@media (max-width:420px) {
+ .empty-work-start-actions { grid-template-columns:1fr; }
+}
.kv { display: grid; grid-template-columns: auto 1fr; gap: 6px 10px; }
.kv .label { color: #94a3b8; }
.kv .value { color: #e5e7eb; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index f4502f0..edc5fe5 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -49,11 +49,7 @@
attentionInterruption.hidden = !pending;
return pending;
}
- function openMobileWorkFallback() {
- qs('[data-work-filter="all"]').click();
- qs('#my-work').scrollIntoView({block:'start'});
- qs('#my-work').focus();
- }
+
function selectMobileQueue(name) {
if (name === 'attention' && workSession.checkpointed()) timer.beginAttention();
renderAttentionInterruption();
@@ -71,7 +67,7 @@
resumeToday: resumeTodaySession,
startToday: startTodaySession,
planToday: () => openPlanToday(mobileTaskButtons.work),
- openFallback: openMobileWorkFallback,
+ findWork: () => qs('#find-work').click(),
});
let mobileQueueCounts = {};
const mobileQueueLauncher = createMobileQueueLauncher({
@@ -122,6 +118,8 @@
},
});
mobileTaskDock.start();
+ qs('#empty-work-find').addEventListener('click', () => qs('#find-work').click());
+ qs('#empty-work-create').addEventListener('click', () => qs('#new-issue').click());
let liveMode = true;
let offlineWorkMode = false;
const WORK_FILTER_KEY = 'stackchain.my-work-filter.v1';
@@ -2433,6 +2431,10 @@
filterMyWork(activeMyWork, selectedWorkFilter, selectedWorkMilestone);
const incomplete = activeWorkStreams().some(stream => workPagination[stream]?.has_more);
const visible = findQueueItems(queueItems, queueFindQuery);
+ const emptyWorkStart = qs('#empty-work-start');
+ const showEmptyStart = selectedWorkFilter === 'all' && !queueFindQuery &&
+ !queueItems.length && !incomplete && hasContextSnapshot;
+ emptyWorkStart.hidden = !showEmptyStart;
updateQueueFinder(visible.length, queueItems.length, incomplete);
const selection = notificationSelection.snapshot();
const selectedIds = new Set(selection.ids);
@@ -2486,9 +2488,9 @@
return '' + selector + '' + contents + '' + readUpdate + markRead + planningActions + '';
}
return '' + selector + '' + contents + '' + markRead + planningActions + '';
- }).join('') : '
' + (incomplete ?
+ }).join('') : (showEmptyStart ? '' : '
' + (incomplete ?
'More work is available. Load the next page.' :
- 'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '
';
+ 'No ' + (selectedWorkFilter === 'attention' ? 'items need attention' : (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'update' ? 'unread updates' : (selectedWorkFilter === 'later' ? 'deferred work' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items'))))) + '.') + '
');
cardPlanning.wire();
document.querySelectorAll('[data-select-notification-id]').forEach(input => {
input.addEventListener('change', () => {
diff --git a/frontend/index.html b/frontend/index.html
index 3ad3071..9a0ff2d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -183,6 +183,16 @@
+
+
+
Ready for something new?
+
Find an available issue to own, or create the work that needs doing.
+
+
+
+
+
+
diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js
index 3d43982..506a8c5 100644
--- a/frontend/mobile-task-dock.js
+++ b/frontend/mobile-task-dock.js
@@ -69,6 +69,7 @@
resume: ['Resume', 'Resume Today'],
start: ['Start', 'Start Today'],
plan: ['Plan', 'Plan Today'],
+ find: ['Find', 'Find work'],
work: ['Work', 'Work'],
};
const workMode = labels[mode] ? mode : 'work';
diff --git a/frontend/mobile-work-entry.js b/frontend/mobile-work-entry.js
index af4aa47..56a1fa8 100644
--- a/frontend/mobile-work-entry.js
+++ b/frontend/mobile-work-entry.js
@@ -7,7 +7,7 @@
if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume';
if (options.getTodayCount() > 0) return 'start';
if (options.getEligibleCount() > 0) return 'plan';
- return 'work';
+ return 'find';
}
function open() {
@@ -16,7 +16,7 @@
else if (current === 'resume') options.resumeToday();
else if (current === 'start') options.startToday();
else if (current === 'plan') options.planToday();
- else options.openFallback();
+ else options.findWork();
return current;
}
diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py
index 1d09bcf..2d7574c 100644
--- a/tests/test_mobile_task_dock.py
+++ b/tests/test_mobile_task_dock.py
@@ -13,7 +13,7 @@ QUEUE_LAUNCHER = Path(__file__).resolve().parents[1] / "frontend" / "mobile-queu
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"
-def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_fallback():
+def test_mobile_work_entry_prioritizes_continue_resume_start_plan_then_find():
script = f"""
const createEntry = require({json.dumps(str(ENTRY))});
const state = {{active:true, resumable:true, today:2, eligible:3}};
@@ -27,7 +27,7 @@ const entry = createEntry({{
resumeToday: () => calls.push('resume'),
startToday: () => calls.push('start'),
planToday: () => calls.push('plan'),
- openFallback: () => calls.push('fallback'),
+ findWork: () => calls.push('find'),
}});
const modes = [];
modes.push(entry.open());
@@ -43,8 +43,8 @@ process.stdout.write(JSON.stringify({{modes, calls}}));
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == {
- "modes": ["continue", "resume", "start", "plan", "work"],
- "calls": ["continue", "resume", "start", "plan", "fallback"],
+ "modes": ["continue", "resume", "start", "plan", "find"],
+ "calls": ["continue", "resume", "start", "plan", "find"],
}
@@ -165,10 +165,10 @@ dock.updateWork('start', 0);
const starting = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
dock.updateWork('plan', 0);
const planning = {{ text:workLabel.textContent, label:work.attributes['aria-label'] }};
-dock.updateWork('work', 0);
+dock.updateWork('find', 0);
process.stdout.write(JSON.stringify({{
continuing, resuming, starting, planning,
- fallback:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }},
+ finding:{{ text:workLabel.textContent, label:work.attributes['aria-label'] }},
}}));
"""
result = subprocess.run(
@@ -184,7 +184,7 @@ process.stdout.write(JSON.stringify({{
"resuming": {"text": "Resume", "label": "Resume Today"},
"starting": {"text": "Start", "label": "Start Today"},
"planning": {"text": "Plan", "label": "Plan Today"},
- "fallback": {"text": "Work", "label": "Work"},
+ "finding": {"text": "Find", "label": "Find work"},
}
diff --git a/tests/test_my_work.py b/tests/test_my_work.py
index 3c52a4e..09f7a09 100644
--- a/tests/test_my_work.py
+++ b/tests/test_my_work.py
@@ -8,6 +8,22 @@ import pytest
from tests.dashboard_bundle import dashboard
+@pytest.mark.anyio
+async def test_empty_all_work_queue_offers_find_and_create_actions():
+ markup = (Path(__file__).resolve().parents[1] / "frontend" / "index.html").read_text()
+ source = await dashboard()
+
+ assert 'id="empty-work-start"' in markup
+ assert 'Ready for something new?' in markup
+ assert 'id="empty-work-find"' in markup
+ assert 'id="empty-work-create"' in markup
+ assert "qs('#empty-work-find').addEventListener('click'" in source
+ assert "qs('#find-work').click()" in source
+ assert "qs('#empty-work-create').addEventListener('click'" in source
+ assert "qs('#new-issue').click()" in source
+ assert "emptyWorkStart.hidden = !showEmptyStart" in source
+
+
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
NOTIFICATION_UNDO = Path(__file__).parents[1] / "frontend" / "notification-undo.js"
TODAY_TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.js"