diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index 980ddc6..74ec847 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -617,9 +617,21 @@ textarea { resize: vertical; min-height: 120px; }
.work-session-nav button { min-height:44px; width:100%; }
.find-work-sheet { position:fixed; inset:0; z-index:58; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
.find-work-sheet.open { display:flex; }
-.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
+.find-work-panel { width:min(560px,100%); height:100dvh; overflow:auto; overflow-x:hidden; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
.find-work-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.find-work-header-actions { display:flex; gap:8px; }
+.mobile-find-work-nav { position:sticky; top:0; z-index:4; display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:6px; padding:6px 0; background:#0b1526; }
+.mobile-find-work-nav button { min-height:44px; min-width:0; padding:6px; }
+.mobile-find-work-nav button[aria-current="step"] { border-color:#60a5fa; background:#17355b; color:#fff; }
+.mobile-find-work-nav button:disabled { opacity:.5; }
+.find-work-stage { display:grid; gap:12px; min-width:0; outline:none; }
+.find-work-stage[hidden] { display:none; }
+.find-work-review { padding:12px; border:1px solid #2a496e; border-radius:12px; background:#101f36; }
+.find-work-review > div:first-child, .find-work-review-list { display:grid; gap:8px; }
+.find-work-review-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:10px; align-items:center; padding:10px; border:1px solid #2a496e; border-radius:10px; overflow-wrap:anywhere; }
+.find-work-review-item > div { display:grid; gap:4px; min-width:0; }
+.find-work-review-item button, .find-work-stage-actions button { min-height:44px; }
+.find-work-stage-actions { position:sticky; bottom:0; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:10px 0 env(safe-area-inset-bottom); background:#101f36; }
.find-work-header button, .find-work-card button, .find-work-card a, .find-work-more { min-height:44px; }
.find-work-search { position:sticky; top:0; z-index:2; display:grid; gap:6px; padding:8px 0; background:#0b1526; }
.find-work-search > div { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 1cd6859..620c9cb 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -1095,6 +1095,7 @@
}
return offlineToday.warm(confirmedOwnerLogin, todayMyWork);
}
+ let findWorkNavigation = null;
const findWorkController = createFindWork({
fetchJson: fetchReviewJson,
onItems: renderAvailableIssues,
@@ -1109,9 +1110,20 @@
qs('#find-work-selection-status').textContent = state.count ?
state.count + ' issue' + (state.count === 1 ? '' : 's') + ' selected.' : 'No work selected.';
qs('#claim-selected-work').disabled = state.count === 0;
+ findWorkNavigation?.sync({ selectedCount:state.count });
renderAvailableIssues(findWorkController.items());
},
});
+
+ findWorkNavigation = createMobileFindWorkNavigation({
+ document,
+ history:window.history,
+ eventTarget:window,
+ controller:findWorkController,
+ todayWork,
+ openFit:openFindWorkEstimateReview,
+ });
+ findWorkNavigation.start();
let findWorkSearchTimer = null;
function updateFindWorkMatchStatus() {
@@ -2192,26 +2204,22 @@
));
}
- function closeFindWorkEstimateReview() {
- qs('#find-work-estimate-review').hidden = true;
- qs('#batch-find-work-actions').hidden = !findWorkController.selection().active;
- }
-
function openFindWorkEstimateReview(items) {
const plan = todayWork.planning();
const planned = Object.values(plan.estimates).reduce((sum, minutes) => sum + minutes, 0);
- qs('#find-work-estimate-summary').textContent = formatPlanMinutes(Math.max(0, plan.capacity_minutes - planned)) +
- ' remaining. Estimate selected work before assigning it.';
+ const estimatesRequired = Number.isInteger(plan.capacity_minutes) && plan.capacity_minutes > 0;
+ qs('#find-work-estimate-summary').textContent = estimatesRequired ?
+ formatPlanMinutes(Math.max(0, plan.capacity_minutes - planned)) +
+ ' remaining. Estimate selected work before assigning it.' :
+ items.length + ' selected. Confirm to assign and queue this batch in Today.';
qs('#find-work-estimate-list').innerHTML = items.map(item => {
const id = String(item.repository || '') + '#' + String(item.number || '');
return '
';
+ '' + escapeHtml(item.title || 'Untitled work') + '' +
+ (estimatesRequired ? '' : '') + '';
}).join('');
- qs('#batch-find-work-actions').hidden = true;
- qs('#find-work-estimate-review').hidden = false;
- qs('[data-find-work-estimate]')?.focus();
+ if (estimatesRequired) qs('[data-find-work-estimate]')?.focus();
}
function formatCalendarDueDate(value) {
@@ -4290,6 +4298,7 @@
}
findingWork = true;
qs('#find-work-sheet').classList.add('open');
+ findWorkNavigation.open(batchFindWork.pending() > 0);
const retainedItems = findWorkController.items();
if (retainedItems.length) renderAvailableIssues(retainedItems);
else qs('#find-work-list').textContent = '';
@@ -5635,15 +5644,13 @@
});
qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal'));
qs('#find-work').addEventListener('click', openFindWorkSheet);
- qs('#close-find-work').addEventListener('click', closeFindWorkSheet);
- qs('#select-find-work').addEventListener('click', () => findWorkController.startSelection());
+
qs('#fill-find-work-today').addEventListener('click', () => {
const remainingSlots = todayWork.limit - todayWork.read().length;
const outcome = findWorkController.fillSelection(remainingSlots);
qs('#find-work-status').textContent = remainingSlots < 1 ? 'Today is full.' :
outcome.selected ? outcome.selected + ' ranked work selected.' : 'No matching work.';
- if (outcome.selected && todayWork.planning().capacity_minutes !== null)
- openFindWorkEstimateReview(findWorkController.selectedItems());
+ if (outcome.selected) findWorkNavigation.go('review');
});
qs('#find-work-search-form').addEventListener('submit', event => event.preventDefault());
qs('#find-work-search').addEventListener('input', event => {
@@ -5669,33 +5676,7 @@
input.dispatchEvent(new Event('input', { bubbles:true }));
input.focus();
});
- qs('#cancel-find-work-selection').addEventListener('click', () => findWorkController.cancelSelection());
- qs('#claim-selected-work').addEventListener('click', async event => {
- if (todayWork.planning().capacity_minutes !== null) {
- openFindWorkEstimateReview(findWorkController.selectedItems());
- return;
- }
- event.currentTarget.disabled = true;
- const outcome = await batchFindWork.run(findWorkController.selectedItems());
- if (outcome.status === 'complete') {
- const assignedOnly = outcome.failed.filter(item => item.assigned).length;
- const unavailable = outcome.failed.length - assignedOnly;
- qs('#find-work-status').textContent = outcome.queued.length + ' queued' +
- (unavailable ? ' · ' + unavailable + ' unavailable' : '') +
- (assignedOnly ? ' · ' + assignedOnly + ' assigned but not queued' : '') + '.';
- if (outcome.failed.length) {
- renderAvailableIssues(findWorkController.items());
- event.currentTarget.disabled = false;
- } else {
- findWorkController.cancelSelection();
- }
- refreshMyWorkView();
- } else {
- event.currentTarget.disabled = false;
- }
- });
- qs('#cancel-find-work-estimates').addEventListener('click', closeFindWorkEstimateReview);
qs('#confirm-find-work-estimates').addEventListener('click', async event => {
event.currentTarget.disabled = true;
const outcome = await batchFindWork.run(findWorkController.selectedItems(), findWorkEstimateValues());
@@ -5706,10 +5687,18 @@
return;
}
if (outcome.status !== 'complete') return;
+ const assignedOnly = outcome.failed.filter(item => item.assigned).length;
+ const unavailable = outcome.failed.length - assignedOnly;
qs('#find-work-status').textContent = outcome.queued.length + ' queued' +
- (outcome.failed.length ? ' · ' + outcome.failed.length + ' unavailable.' : '.');
- closeFindWorkEstimateReview();
- if (!outcome.failed.length) findWorkController.cancelSelection();
+ (unavailable ? ' · ' + unavailable + ' unavailable' : '') +
+ (assignedOnly ? ' · ' + assignedOnly + ' assigned but not queued' : '') + '.';
+ if (!outcome.failed.length) {
+ findWorkNavigation.complete();
+ findWorkController.cancelSelection();
+ } else {
+ renderAvailableIssues(findWorkController.items());
+ findWorkNavigation.sync({ selectedCount:findWorkController.selection().count, recovery:true });
+ }
refreshMyWorkView();
});
qs('#load-more-available').addEventListener('click', async event => {
diff --git a/frontend/index.html b/frontend/index.html
index 2bcebe1..bc337f1 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -843,42 +843,60 @@
Open · unassigned
Find work
- Claim an available issue and continue it in My Work.
-
-
- Filter work
-
-
-
-
-
-
-
- Open Find Work to load available issues.
-
-
-
-
- Fit this work into Today
-
-
-
-
-
+
Choose available issues, review the batch, then fit it into Today.
+
+
+
+
+ Filter work
+
+
+
+
+
+
+
+ Open Find Work to load available issues.
+
+
+
+
+ No work selected.
+
+
+
+
+
+ Review selected work
+
+
+
+
+
+
+
+
+
+
Fit this work into Today
+
+
+
+
+
+
-
- No work selected.
-
-
-
@@ -1596,6 +1614,7 @@
+