feat: open focused blockers from Today HUD (Closes #1070)
All checks were successful
CI / lint (pull_request) Successful in 2m41s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m14s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy 2026-08-18 08:25:27 +00:00
parent 027ded1f8f
commit f710b2ce1d
5 changed files with 135 additions and 13 deletions

View File

@ -290,6 +290,8 @@ textarea { resize: vertical; min-height: 120px; }
.today-progress-evidence .issue-evidence-note textarea { min-height:72px; }
.today-progress-blocker { display:grid; gap:8px; margin-top:14px; padding:12px; border:1px solid #875f2a; border-radius:12px; background:#21180d; }
.today-progress-blocker input, .today-progress-blocker button { box-sizing:border-box; width:100%; min-height:44px; }
.today-blocker-presets { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; }
.today-blocker-presets button { min-width:0; padding-inline:6px; }
.today-progress-blocker p { margin:0; }
.today-progress-blocker .today-progress-blocker-recovery { padding:8px; border-radius:8px; background:#3a270d; color:#fde68a; font-weight:700; }
.today-progress-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px; }
@ -297,6 +299,13 @@ textarea { resize: vertical; min-height: 120px; }
.today-progress-panel .voice-conversation { margin-top:10px; }
.today-progress-panel .voice-conversation-controls button,
.today-progress-panel .voice-conversation-review-actions button { min-height:44px; }
.today-progress-panel.blocker-mode .today-progress-activity,
.today-progress-panel.blocker-mode .voice-conversation,
.today-progress-panel.blocker-mode .today-progress-evidence,
.today-progress-panel.blocker-mode .today-progress-update-help,
.today-progress-panel.blocker-mode .today-progress-actions { display:none; }
.today-progress-panel.blocker-mode textarea { min-height:112px; }
@media(max-width:359px) { .today-blocker-presets { grid-template-columns:1fr; } }
@media(max-width:359px) { .today-progress-actions { grid-template-columns:1fr; } }
.today-recap-sheet { position:fixed; inset:0; z-index:88; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
.today-recap-sheet[hidden] { display:none; }
@ -1116,7 +1125,8 @@ textarea { resize: vertical; min-height: 120px; }
.mobile-today-hud [data-mobile-today-complete] { grid-area:complete; }
.mobile-today-hud [data-mobile-today-toggle] { grid-area:toggle; }
.mobile-today-hud button { min-height:44px; max-width:100%; }
.mobile-today-hud [data-mobile-today-update] { grid-column:1 / -1; width:100%; }
.mobile-today-hud [data-mobile-today-update] { grid-column:1; width:100%; }
.mobile-today-hud [data-mobile-today-blocked] { grid-column:2; width:100%; }
.mobile-today-hud [data-today-break-open] { grid-column:1 / -1; }
.mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; }

View File

@ -1537,6 +1537,7 @@
<div class="small" data-work-session-progress aria-live="polite"></div>
<button data-mobile-today-toggle data-work-session-timer-toggle type="button">Pause timer</button>
<button data-mobile-today-update type="button" hidden>Add update</button>
<button class="mobile-today-blocked" data-mobile-today-blocked type="button" hidden>Blocked</button>
<button data-today-break-open type="button">Take break</button>
<button data-work-session-adjust-plan type="button" hidden>Adjust remaining plan</button>
</section>
@ -1556,7 +1557,7 @@
<button id="load-older-today-progress-activity" type="button" hidden>Load older</button>
</div>
</section>
<label for="today-progress-body">Update</label>
<label id="today-progress-body-label" for="today-progress-body">Update</label>
<textarea id="today-progress-body" rows="5" maxlength="2000" placeholder="What changed, what you learned, or what comes next"></textarea>
<div class="mention-options" id="today-progress-body-mentions" role="listbox" aria-label="Teammates" hidden></div>
<div class="mention-status small" id="today-progress-body-mention-status" aria-live="polite"></div>
@ -1600,12 +1601,17 @@
</div>
</div>
</section>
<p class="small muted">Save privately on this device, or post to the exact Gitea item. Today keeps running either way.</p>
<p class="small muted today-progress-update-help">Save privately on this device, or post to the exact Gitea item. Today keeps running either way.</p>
<section class="today-progress-blocker" aria-labelledby="today-progress-blocker-title">
<strong id="today-progress-blocker-title">Blocked?</strong>
<p id="today-progress-blocker-recovery" class="today-progress-blocker-recovery small" role="status" hidden></p>
<label for="today-blocker-return-at">Return this item to Later at</label>
<input id="today-blocker-return-at" type="datetime-local" />
<div class="today-blocker-presets" aria-label="Return time choices">
<button data-today-blocker-preset="later-today" type="button">Later today</button>
<button data-today-blocker-preset="tomorrow" type="button">Tomorrow</button>
<button data-today-blocker-custom type="button">Choose time</button>
</div>
<button id="post-today-blocker" type="button">Post blocker &amp; move on</button>
<p class="small muted">Posts this update first, then defers the item and continues Today.</p>
</section>

View File

@ -309,11 +309,15 @@ function mountTodayProgressActivity(qs, fetchJson, actionSource = {}, render) {
return activity;
}
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {}, moveOn = null }) {
function createTodayProgressView({ progress, currentTarget, qs, photos, voice, activity, mentions, announce = () => {}, onAdmitted = () => {}, moveOn = null, now = () => new Date() }) {
const sheet = qs('#today-progress-sheet');
const panel = qs('.today-progress-panel');
const title = qs('#today-progress-title');
const body = qs('#today-progress-body');
const bodyLabel = qs('#today-progress-body-label');
const status = qs('#today-progress-status');
const launcher = qs('[data-mobile-today-update]');
const blockerLauncher = qs('[data-mobile-today-blocked]');
const blockerButton = qs('#post-today-blocker');
const blockerReturn = qs('#today-blocker-return-at');
const blockerRecovery = qs('#today-progress-blocker-recovery');
@ -342,6 +346,7 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a
const update = () => {
const target = currentTarget();
launcher.hidden = !target;
if (blockerLauncher) blockerLauncher.hidden = !target;
if (openedTarget && target?.identity !== openedTarget.identity) mentions?.dismiss?.();
};
const checkpoint = async () => {
@ -362,19 +367,24 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a
activity?.close?.();
mentions?.dismiss?.();
if (sheet.open) sheet.close();
panel?.classList?.remove('blocker-mode');
openedTarget = null;
};
launcher.addEventListener('click', async () => {
const open = async blockerMode => {
const target = currentTarget();
if (!target) return;
mentions?.dismiss?.();
openedTarget = target;
panel?.classList?.[blockerMode ? 'add' : 'remove']('blocker-mode');
if (title) title.textContent = blockerMode ? 'Report blocker' : 'Add progress update';
if (bodyLabel) bodyLabel.textContent = blockerMode ? 'Blocker description' : 'Update';
qs('#today-progress-target').textContent = target.label + (target.title ? ' · ' + target.title : '');
body.value = progress.load(target.identity);
showBlockerRecovery(progress.blockerRecovery?.(target.identity));
status.textContent = 'Restoring saved photo evidence…';
sheet.showModal();
if (!blockerMode) {
status.textContent = 'Restoring saved photo evidence…';
try {
await voice?.open?.(target.identity);
await photos?.open?.(target);
@ -382,8 +392,25 @@ function createTodayProgressView({ progress, currentTarget, qs, photos, voice, a
status.textContent = '';
}
catch (error) { status.textContent = error.message + ' You can retry by reopening this update.'; }
}
else status.textContent = '';
body.focus();
});
};
launcher.addEventListener('click', () => open(false));
blockerLauncher?.addEventListener('click', () => open(true));
const setReturn = preset => {
const date = new Date(now());
if (preset === 'later-today') date.setHours(date.getHours() + 4);
else {
date.setDate(date.getDate() + 1);
date.setHours(9, 0, 0, 0);
}
blockerReturn.value = localDateTime(date);
};
qs('[data-today-blocker-preset="later-today"]')?.addEventListener('click', () => setReturn('later-today'));
qs('[data-today-blocker-preset="tomorrow"]')?.addEventListener('click', () => setReturn('tomorrow'));
qs('[data-today-blocker-custom]')?.addEventListener('click', () => blockerReturn?.focus?.());
qs('#cancel-today-progress').addEventListener('click', async () => {
if (await checkpoint()) close();
});

View File

@ -87,6 +87,26 @@ def test_release_artifact_plans_hands_off_and_opens_next_mobile_issue(tmp_path:
if page.locator("#plan-today-sheet").is_visible():
page.locator("#cancel-plan-today").click()
expect(page.locator("[data-mobile-today-hud]")).to_be_visible()
blocked = page.locator("[data-mobile-today-blocked]")
expect(blocked).to_be_visible()
blocked_bounds = blocked.bounding_box()
assert blocked_bounds and blocked_bounds["height"] >= 44
blocked.click()
expect(page.locator("#today-progress-sheet")).to_be_visible()
expect(page.locator("#today-progress-title")).to_have_text("Report blocker")
expect(page.locator("#today-progress-target")).to_contain_text("Ship mobile capture")
expect(page.locator("#today-progress-body")).to_be_focused()
expect(page.locator("#today-progress-activity")).to_be_hidden()
expect(page.locator(".today-progress-evidence")).to_be_hidden()
for control in page.locator(".today-blocker-presets button, #post-today-blocker").all():
bounds = control.bounding_box()
assert bounds and bounds["height"] >= 44
page.locator('[data-today-blocker-preset="later-today"]').click()
assert page.locator("#today-blocker-return-at").input_value()
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
page.locator("#cancel-today-progress").click()
expect(page.locator("#today-progress-sheet")).to_be_hidden()
expect(page.locator("[data-mobile-today-hud]")).to_contain_text("Ship mobile capture")
fake.activity_comments = {
41: [{
"id": 701,

View File

@ -329,6 +329,65 @@ const view=createView({{
}
def test_blocked_hud_opens_a_focused_flow_with_quick_return_choices():
script = f"""
const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))});
class Element {{
constructor() {{
this.hidden=false;this.value='';this.textContent='';this.open=false;this.focused=false;this.listeners={{}};
const names=new Set();
this.classList={{add:name=>names.add(name),remove:name=>names.delete(name),contains:name=>names.has(name)}};
}}
addEventListener(name,callback) {{ this.listeners[name]=callback; }}
click() {{ return this.listeners.click?.(); }}
showModal() {{ this.open=true; }} close() {{ this.open=false; }} focus() {{ this.focused=true; }}
}}
const selectors=['#today-progress-sheet','.today-progress-panel','#today-progress-title','#today-progress-body',
'#today-progress-status','[data-mobile-today-update]','[data-mobile-today-blocked]',
'#today-progress-target','#cancel-today-progress','#save-today-progress','#post-today-progress',
'#today-blocker-return-at','#post-today-blocker','[data-today-blocker-preset="later-today"]',
'[data-today-blocker-preset="tomorrow"]','[data-today-blocker-custom]'];
const elements=Object.fromEntries(selectors.map(selector=>[selector,new Element()]));
const target={{identity:'issue:stackchain/dashboard:1070:',kind:'issue',repository:'stackchain/dashboard',number:1070,label:'#1070',title:'Focused blocker'}};
const calls=[];
const view=createView({{
progress:{{load:()=> 'Waiting for access',save:()=>true}},currentTarget:()=>target,qs:selector=>elements[selector],
voice:{{open:async()=>calls.push('voice')}},photos:{{open:async()=>calls.push('photos')}},
activity:{{open:async()=>calls.push('activity')}},now:()=>new Date('2026-08-18T10:15:00Z'),
}});
(async()=>{{
const availability={{update:elements['[data-mobile-today-update]'].hidden,blocked:elements['[data-mobile-today-blocked]'].hidden}};
await elements['[data-mobile-today-blocked]'].click();
const opened={{
title:elements['#today-progress-title'].textContent,target:elements['#today-progress-target'].textContent,
focused:elements['#today-progress-body'].focused,mode:elements['.today-progress-panel'].classList.contains('blocker-mode'),
calls:[...calls],sheet:elements['#today-progress-sheet'].open,
}};
await elements['[data-today-blocker-preset="later-today"]'].click();
const laterToday=elements['#today-blocker-return-at'].value;
await elements['[data-today-blocker-preset="tomorrow"]'].click();
const tomorrow=elements['#today-blocker-return-at'].value;
elements['#today-blocker-return-at'].focused=false;
await elements['[data-today-blocker-custom]'].click();
process.stdout.write(JSON.stringify({{availability,opened,laterToday,tomorrow,customFocused:elements['#today-blocker-return-at'].focused}}));
}})().catch(error=>{{console.error(error);process.exit(1);}});
"""
assert run_node(script) == {
"availability": {"update": False, "blocked": False},
"opened": {
"title": "Report blocker",
"target": "#1070 · Focused blocker",
"focused": True,
"mode": True,
"calls": [],
"sheet": True,
},
"laterToday": "2026-08-18T14:15",
"tomorrow": "2026-08-19T09:00",
"customFocused": True,
}
def test_progress_sheet_dismisses_mentions_when_opened_switched_or_closed():
script = f"""
const {{createView}}=require({json.dumps(str(TODAY_PROGRESS))});