diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index eb1e9a2..5c3353c 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -189,7 +189,8 @@ textarea { resize: vertical; min-height: 120px; }
.milestone-lane { display:flex; align-items:center; gap:8px; min-width:min(100%,260px); }
.work-milestone-filter { min-width:180px; flex:1; padding:8px; border-radius:8px; border:1px solid #1f3a5f; background:#0b1526; color:var(--text); }
.my-work-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; }
-.draft-card { display:flex; flex-direction:column; gap:8px; min-width:0; }
+.draft-card { display:flex; flex-direction:column; gap:8px; min-width:0; scroll-margin-bottom:calc(76px + env(safe-area-inset-bottom)); }
+.draft-card:focus-visible { outline:3px solid #60a5fa; outline-offset:3px; border-color:#93c5fd; }
.draft-preview { color:var(--muted); overflow-wrap:anywhere; }
.draft-actions { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:8px; }
.draft-actions button { min-height:44px; width:100%; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 51b6813..8e4d7c7 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -2078,7 +2078,8 @@
(item.ownership ? '
' + escapeHtml(item.ownership) + '
' : '') : '';
const attempt = item.last_attempt_error ? 'Last attempt ' +
escapeHtml(fmt(item.last_attempt_at)) + ' · ' + escapeHtml(item.last_attempt_error) + '' : '';
- return '' +
+ const captureTarget = isUnfiled ? ' tabindex="-1" data-capture-id="' + escapeHtml(item.capture_id) + '"' : '';
+ return '' +
'' + escapeHtml(item.label) + (item.repository ? ' · ' + escapeHtml(item.repository) : '') + '' +
'' + escapeHtml(item.title) + '' +
'' + escapeHtml(item.preview || 'Unfinished draft') + '' + state + attempt +
@@ -4069,7 +4070,7 @@
body: qs('#create-issue-body').value.trim(),
attachment: await createIssueAttachmentController.serialize(),
};
- await unfiledCaptures.save(captureDraft);
+ const savedCapture = await unfiledCaptures.save(captureDraft);
if (resumedUnfiledCaptureId) {
await unfiledCaptures.completeResume(resumedUnfiledCaptureId);
resumedUnfiledCaptureId = '';
@@ -4079,8 +4080,15 @@
qs('#create-issue-body').value = '';
createIssueAttachmentController.clear();
closeCreateIssueSheet(true, false);
+ qs('[data-work-filter="draft"]').click();
+ mobileTaskDock.select('drafts');
refreshMyWorkView();
- qs('#my-work-action-status').textContent = 'Saved in Drafts · choose a repository after reconnecting.';
+ const savedCard = qs('[data-capture-id="' + CSS.escape(savedCapture.id) + '"]');
+ requestAnimationFrame(() => {
+ savedCard?.scrollIntoView({block:'nearest'});
+ savedCard?.focus({preventScroll:true});
+ });
+ qs('#my-work-action-status').textContent = 'Saved to Drafts. Choose a repository when you’re ready to file it.';
} catch (error) {
qs('#create-issue-capture-status').textContent = error.message;
qs('#create-issue-title').focus();
diff --git a/frontend/mobile-task-dock.js b/frontend/mobile-task-dock.js
index 173bf92..91fcf83 100644
--- a/frontend/mobile-task-dock.js
+++ b/frontend/mobile-task-dock.js
@@ -9,10 +9,12 @@
let wasHidden = false;
function select(name) {
+ if (!buttons[name]) return false;
Object.entries(buttons).forEach(([key, button]) => {
if (key === name) button.setAttribute('aria-current', 'page');
else button.removeAttribute('aria-current');
});
+ return true;
}
function refreshVisibility() {
@@ -68,5 +70,5 @@
else nav.removeAttribute('data-attention');
}
- return {start, refreshVisibility, updateAttention, updateWork};
+ return {start, select, refreshVisibility, updateAttention, updateWork};
});
diff --git a/tests/test_mobile_task_dock.py b/tests/test_mobile_task_dock.py
index f677fbc..0c28285 100644
--- a/tests/test_mobile_task_dock.py
+++ b/tests/test_mobile_task_dock.py
@@ -116,6 +116,40 @@ process.stdout.write(JSON.stringify({{
}
+def test_mobile_task_dock_programmatically_selects_destination_without_running_action():
+ script = f"""
+const createDock = require({json.dumps(str(DOCK))});
+const calls = [];
+const makeButton = () => ({{
+ attributes: {{}},
+ setAttribute(name, value) {{ this.attributes[name] = value; }},
+ removeAttribute(name) {{ delete this.attributes[name]; }},
+}});
+const buttons = {{work:makeButton(), drafts:makeButton()}};
+const dock = createDock({{
+ nav:{{}}, buttons,
+ actions:{{work:()=>calls.push('work'), drafts:()=>calls.push('drafts')}},
+}});
+const selected = dock.select('drafts');
+process.stdout.write(JSON.stringify({{
+ selected, calls,
+ work:buttons.work.attributes['aria-current'] || null,
+ drafts:buttons.drafts.attributes['aria-current'] || null,
+}}));
+"""
+ result = subprocess.run(
+ ["node", "-e", script], capture_output=True, text=True
+ )
+
+ assert result.returncode == 0, result.stderr
+ assert json.loads(result.stdout) == {
+ "selected": True,
+ "calls": [],
+ "work": None,
+ "drafts": "page",
+ }
+
+
def test_mobile_task_dock_keeps_today_label_separate_from_attention_count():
script = f"""
const createDock = require({json.dumps(str(DOCK))});
diff --git a/tests/test_unfiled_captures.py b/tests/test_unfiled_captures.py
index 436a3e3..8e4e766 100644
--- a/tests/test_unfiled_captures.py
+++ b/tests/test_unfiled_captures.py
@@ -151,6 +151,12 @@ async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume
assert "createUnfiledCaptures({" in html
assert "getCaptureLogin: () => String(lastContextSnapshot?.user?.login || '').trim()" in html
assert "unfiledCaptures.save(captureDraft)" in html
+ assert "const savedCapture = await unfiledCaptures.save(captureDraft)" in html
+ assert "mobileTaskDock.select('drafts')" in html
+ assert "data-capture-id=\"' + escapeHtml(item.capture_id) + '\"" in html
+ assert "requestAnimationFrame(() =>" in html
+ assert "savedCard?.scrollIntoView({block:'nearest'})" in html
+ assert "Saved to Drafts. Choose a repository when you’re ready to file it." in html
assert "await unfiledCaptures.resume(item.capture_id, activeFlushLogin)" in html
assert "await createIssueAttachmentController.serialize()" in html
assert "createUnfiledAttachmentStore()" in html
@@ -160,6 +166,8 @@ async def test_mobile_composer_exposes_cold_offline_save_and_account_safe_resume
assert "issueCapture.saveDraft(resumed)" in html
assert "item.kind === 'unfiled-issue'" in html
assert '.create-issue-actions button { min-height:44px;' in html
+ assert '.draft-card:focus-visible' in html
+ assert 'scroll-margin-bottom:calc(76px + env(safe-area-inset-bottom))' in html
assert '@media(max-width:320px)' in html