Merge pull request 'Complete Today from the mobile session HUD' (#602) from timmy/601-mobile-hud-complete into main
This commit is contained in:
commit
8dc155e49a
|
|
@ -574,9 +574,11 @@ textarea { resize: vertical; min-height: 120px; }
|
||||||
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
||||||
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
|
.mobile-task-dock { position:fixed; inset:auto 0 0; z-index:45; display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:2px; padding:6px 8px; padding-bottom:env(safe-area-inset-bottom); border-top:1px solid #2a496e; background:rgba(11,21,38,.98); backdrop-filter:blur(12px); }
|
||||||
.mobile-task-dock[data-attention="true"] { grid-template-columns:repeat(6,minmax(0,1fr)); }
|
.mobile-task-dock[data-attention="true"] { grid-template-columns:repeat(6,minmax(0,1fr)); }
|
||||||
.mobile-today-hud { position:fixed; left:8px; right:8px; bottom:calc(56px + env(safe-area-inset-bottom)); z-index:44; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:4px 8px; max-width:100%; padding:8px; border:1px solid #31577f; border-radius:12px 12px 0 0; background:rgba(16,38,65,.98); box-shadow:0 -8px 24px rgba(0,0,0,.28); }
|
.mobile-today-hud { position:fixed; left:8px; right:8px; bottom:calc(56px + env(safe-area-inset-bottom)); z-index:44; display:grid; grid-template-columns:minmax(0,1fr) minmax(112px,auto); grid-template-areas:"summary complete" "progress toggle"; gap:4px 8px; max-width:100%; padding:8px; border:1px solid #31577f; border-radius:12px 12px 0 0; background:rgba(16,38,65,.98); box-shadow:0 -8px 24px rgba(0,0,0,.28); }
|
||||||
.mobile-today-summary { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; font-weight:700; }
|
.mobile-today-summary { grid-area:summary; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; font-weight:700; }
|
||||||
.mobile-today-hud [data-work-session-progress] { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
.mobile-today-hud [data-work-session-progress] { grid-area:progress; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
|
.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 button { min-height:44px; max-width:100%; }
|
||||||
.mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; }
|
.mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -955,6 +955,10 @@
|
||||||
selectTodayWork();
|
selectTodayWork();
|
||||||
workSession.reopen(todayMyWork.find(item => todayWork.identity(item) === identity));
|
workSession.reopen(todayMyWork.find(item => todayWork.identity(item) === identity));
|
||||||
},
|
},
|
||||||
|
onComplete: identity => {
|
||||||
|
const item = todayMyWork.find(entry => todayWork.identity(entry) === identity);
|
||||||
|
return completeTodayItem(item);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
renderAttentionInterruption();
|
renderAttentionInterruption();
|
||||||
qs('#return-to-today').addEventListener('click', () => {
|
qs('#return-to-today').addEventListener('click', () => {
|
||||||
|
|
|
||||||
|
|
@ -813,6 +813,7 @@
|
||||||
|
|
||||||
<section class="mobile-today-hud" data-mobile-today-hud aria-label="Active Today session" hidden>
|
<section class="mobile-today-hud" data-mobile-today-hud aria-label="Active Today session" hidden>
|
||||||
<button class="mobile-today-summary" data-mobile-today-open type="button" aria-label="Open current Today item"></button>
|
<button class="mobile-today-summary" data-mobile-today-open type="button" aria-label="Open current Today item"></button>
|
||||||
|
<button data-mobile-today-complete type="button" hidden>Done & next</button>
|
||||||
<div class="small" data-work-session-progress aria-live="polite"></div>
|
<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-toggle data-work-session-timer-toggle type="button">Pause timer</button>
|
||||||
<button data-work-session-adjust-plan type="button" hidden>Adjust remaining plan</button>
|
<button data-work-session-adjust-plan type="button" hidden>Adjust remaining plan</button>
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway, getItem, onReopen }) {
|
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway, getItem, onReopen, onComplete }) {
|
||||||
let progress = null;
|
let progress = null;
|
||||||
let runway = null;
|
let runway = null;
|
||||||
queryAll('[data-mobile-today-open]').forEach(button =>
|
queryAll('[data-mobile-today-open]').forEach(button =>
|
||||||
|
|
@ -212,6 +212,12 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
|
||||||
queryAll('[data-mobile-today-toggle]').forEach(button =>
|
queryAll('[data-mobile-today-toggle]').forEach(button =>
|
||||||
button.addEventListener('click', () => view.toggle())
|
button.addEventListener('click', () => view.toggle())
|
||||||
);
|
);
|
||||||
|
queryAll('[data-mobile-today-complete]').forEach(button =>
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
const identity = timer.snapshot().identity;
|
||||||
|
if (identity) onComplete?.(identity);
|
||||||
|
})
|
||||||
|
);
|
||||||
if (typeof document !== 'undefined') queryAll('.work-session-nav').forEach(nav => {
|
if (typeof document !== 'undefined') queryAll('.work-session-nav').forEach(nav => {
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
|
|
@ -247,6 +253,15 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
|
||||||
queryAll('[data-mobile-today-open]').forEach(element => {
|
queryAll('[data-mobile-today-open]').forEach(element => {
|
||||||
element.textContent = active ? String(getItem?.(snapshot.identity)?.title || 'Current Today item') : '';
|
element.textContent = active ? String(getItem?.(snapshot.identity)?.title || 'Current Today item') : '';
|
||||||
});
|
});
|
||||||
|
queryAll('[data-mobile-today-complete]').forEach(button => {
|
||||||
|
const itemTitle = String(getItem?.(snapshot.identity)?.title || 'current Today item');
|
||||||
|
const finalItem = Boolean(progress && progress.index >= progress.total);
|
||||||
|
button.hidden = !active;
|
||||||
|
button.textContent = finalItem ? 'Done & recap' : 'Done & next';
|
||||||
|
button.setAttribute('aria-label', finalItem ?
|
||||||
|
'Complete ' + itemTitle + ' and open Today recap' :
|
||||||
|
'Complete ' + itemTitle + ' and open next Today item');
|
||||||
|
});
|
||||||
if (typeof document !== 'undefined') document.body.classList.toggle('mobile-today-active', active);
|
if (typeof document !== 'undefined') document.body.classList.toggle('mobile-today-active', active);
|
||||||
if (!progress) return;
|
if (!progress) return;
|
||||||
const sourceRunway = getRunway?.(snapshot) || runway;
|
const sourceRunway = getRunway?.(snapshot) || runway;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard
|
||||||
|
|
||||||
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
|
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
|
||||||
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
|
ENTRY = Path(__file__).resolve().parents[1] / "frontend" / "mobile-work-entry.js"
|
||||||
|
TIMER = Path(__file__).resolve().parents[1] / "frontend" / "today-timer.js"
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_work_entry_prioritizes_continue_resume_start_then_fallback():
|
def test_mobile_work_entry_prioritizes_continue_resume_start_then_fallback():
|
||||||
|
|
@ -146,6 +147,67 @@ process.stdout.write(JSON.stringify({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_mobile_today_hud_completes_current_item_and_labels_final_item_for_recap():
|
||||||
|
script = f"""
|
||||||
|
const createView = require({json.dumps(str(TIMER))}).createView;
|
||||||
|
class FakeElement {{
|
||||||
|
constructor() {{ this.listeners = {{}}; this.hidden = true; this.textContent = ''; this.attributes = {{}}; }}
|
||||||
|
addEventListener(name, callback) {{ this.listeners[name] = callback; }}
|
||||||
|
click() {{ return this.listeners.click?.(); }}
|
||||||
|
setAttribute(name, value) {{ this.attributes[name] = value; }}
|
||||||
|
}}
|
||||||
|
const hud = new FakeElement();
|
||||||
|
const open = new FakeElement();
|
||||||
|
const toggle = new FakeElement();
|
||||||
|
const complete = new FakeElement();
|
||||||
|
const progress = new FakeElement();
|
||||||
|
const elements = {{
|
||||||
|
'[data-mobile-today-hud]':[hud],
|
||||||
|
'[data-mobile-today-open]':[open],
|
||||||
|
'[data-mobile-today-toggle]':[toggle],
|
||||||
|
'[data-mobile-today-complete]':[complete],
|
||||||
|
'[data-work-session-progress]':[progress],
|
||||||
|
'[data-work-session-adjust-plan]':[],
|
||||||
|
'[data-work-session-timer-toggle]':[toggle],
|
||||||
|
}};
|
||||||
|
let active = true;
|
||||||
|
const snapshot = {{identity:'issue:r:1:', elapsed_ms:0, running:true}};
|
||||||
|
const completed = [];
|
||||||
|
const view = createView({{
|
||||||
|
timer:{{snapshot:()=>snapshot,totalElapsed:()=>0,pause:()=>true,resume:()=>true}},
|
||||||
|
isActive:()=>active,
|
||||||
|
queryAll:selector=>elements[selector] || [],
|
||||||
|
formatEstimate:value=>value + 'm',
|
||||||
|
getItem:()=>({{title:'Ship mobile flow'}}),
|
||||||
|
onComplete:identity=>{{ completed.push(identity); return false; }},
|
||||||
|
}});
|
||||||
|
view.update({{index:1,total:2}}, null);
|
||||||
|
const next = {{hidden:complete.hidden,label:complete.textContent,aria:complete.attributes['aria-label']}};
|
||||||
|
complete.click();
|
||||||
|
const preservedAfterFailure = {{hudHidden:hud.hidden,identity:snapshot.identity}};
|
||||||
|
view.update({{index:2,total:2}}, null);
|
||||||
|
const recap = {{hidden:complete.hidden,label:complete.textContent}};
|
||||||
|
active = false; view.render();
|
||||||
|
process.stdout.write(JSON.stringify({{next,recap,completed,preservedAfterFailure,hiddenWhenInactive:complete.hidden}}));
|
||||||
|
"""
|
||||||
|
result = subprocess.run(
|
||||||
|
["node", "-e", script], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert json.loads(result.stdout) == {
|
||||||
|
"next": {
|
||||||
|
"hidden": False,
|
||||||
|
"label": "Done & next",
|
||||||
|
"aria": "Complete Ship mobile flow and open next Today item",
|
||||||
|
},
|
||||||
|
"recap": {"hidden": False, "label": "Done & recap"},
|
||||||
|
"completed": ["issue:r:1:"],
|
||||||
|
"preservedAfterFailure": {"hudHidden": False, "identity": "issue:r:1:"},
|
||||||
|
"hiddenWhenInactive": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_task_dock_exposes_independent_attention_action_and_six_column_state():
|
def test_mobile_task_dock_exposes_independent_attention_action_and_six_column_state():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createDock = require({json.dumps(str(DOCK))});
|
const createDock = require({json.dumps(str(DOCK))});
|
||||||
|
|
@ -231,10 +293,16 @@ async def test_dashboard_renders_phone_safe_today_session_hud_above_task_dock():
|
||||||
assert 'data-work-session-progress' in html
|
assert 'data-work-session-progress' in html
|
||||||
assert 'data-work-session-timer-toggle' in html
|
assert 'data-work-session-timer-toggle' in html
|
||||||
assert 'data-work-session-adjust-plan' in html
|
assert 'data-work-session-adjust-plan' in html
|
||||||
|
assert 'data-mobile-today-complete' in html
|
||||||
|
assert 'Done & next' in html
|
||||||
assert "getItem: identity => [...todayMyWork, ...activeMyWork].find" in html
|
assert "getItem: identity => [...todayMyWork, ...activeMyWork].find" in html
|
||||||
assert "onReopen: identity =>" in html
|
assert "onReopen: identity =>" in html
|
||||||
|
assert "onComplete: identity =>" in html
|
||||||
|
assert "completeTodayItem(item)" in html
|
||||||
assert "workSession.reopen(item)" in html
|
assert "workSession.reopen(item)" in html
|
||||||
assert '.mobile-today-hud { display:none;' in html
|
assert '.mobile-today-hud { display:none;' in html
|
||||||
|
assert 'grid-template-areas:' in html
|
||||||
|
assert '[data-mobile-today-complete] { grid-area:complete;' in html
|
||||||
assert 'bottom:calc(56px + env(safe-area-inset-bottom))' in html
|
assert 'bottom:calc(56px + env(safe-area-inset-bottom))' in html
|
||||||
assert '.mobile-today-hud button { min-height:44px;' in html
|
assert '.mobile-today-hud button { min-height:44px;' in html
|
||||||
assert '.mobile-task-action[hidden] { display:none;' in html
|
assert '.mobile-task-action[hidden] { display:none;' in html
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user