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