feat: keep active Today visible on mobile (Closes #599)
CI / lint (pull_request) Successful in 1m28s
CI / build-release (pull_request) Successful in 7s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-11 23:29:50 +00:00
1 parent 4c13e13275
commit 96e7b35419
8 files changed
+167 -10

No files matched your search

+9
View File
@@ -503,13 +503,17 @@ textarea { resize: vertical; min-height: 120px; }
.pull-retry { min-height:44px; width:100%; margin-top:10px; }
.mobile-task-dock { display:none; }
.mobile-task-dock[hidden] { display:none; }
.mobile-today-hud { display:none; }
.mobile-today-hud[hidden], .mobile-today-hud[data-overlay-hidden="true"] { display:none; }
.mobile-task-action { min-width:0; min-height:44px; padding:6px 2px; border:0; border-radius:8px; background:transparent; display:grid; place-items:center; gap:2px; font-size:12px; }
.mobile-task-action[hidden] { display:none; }
.mobile-task-action[aria-current="page"] { color:#bfdbfe; background:#17365a; outline:1px solid #31577f; }
.mobile-task-count { min-width:18px; min-height:18px; padding:1px 5px; border-radius:999px; background:#31577f; font-size:11px; line-height:16px; }
.attention-interruption:not([hidden]) { max-width:100%; display:flex; align-items:center; justify-content:space-between; gap:10px; margin:0 0 12px; padding:10px 12px; border:1px solid #31577f; border-radius:10px; background:#102641; }
.attention-interruption button { min-height:44px; flex:0 0 auto; }
@media (max-width: 600px) {
body { padding-bottom:calc(66px + env(safe-area-inset-bottom)); }
body.mobile-today-active { padding-bottom:calc(166px + env(safe-area-inset-bottom)); }
header { min-height:56px; max-height:64px; padding:6px 10px; align-items:center; gap:8px; background:rgba(11,21,38,.98); }
.app-brand .muted, #clock { display:none; }
.app-live-status { margin-left:auto; }
@@ -570,6 +574,11 @@ textarea { resize: vertical; min-height: 120px; }
.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[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-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 button { min-height:44px; max-width:100%; }
.mobile-today-hud [data-work-session-adjust-plan] { grid-column:1 / -1; }
}
.obi { width:14px; height:14px; background: url('data:image/svg+xml;utf8,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22><rect width=%2224%22 height=%2224%22 rx=%226%22 fill=%22%230b1526%22/><circle cx=%2212%22 cy=%2212%22 r=%226%22 fill=%22%2360a5fa%22/></svg>') center/contain no-repeat; display:inline-block; }
.footer { padding: 12px; text-align: center; color:#4e6b8a; font-size:12px; }
+6
View File
@@ -74,6 +74,7 @@
});
const mobileTaskDock = createMobileTaskDock({
nav: qs('#mobile-task-dock'),
sessionHud: qs('[data-mobile-today-hud]'),
buttons: mobileTaskButtons,
workLabel: qs('#mobile-work-label'),
attentionBadge: qs('#mobile-attention-count'),
@@ -949,6 +950,11 @@
isActive: () => workSession.checkpointed(),
queryAll: s => document.querySelectorAll(s),
formatEstimate: formatPlanMinutes,
getItem: identity => [...todayMyWork, ...activeMyWork].find(item => todayWork.identity(item) === identity),
onReopen: identity => {
selectTodayWork();
workSession.reopen(todayMyWork.find(item => todayWork.identity(item) === identity));
},
});
renderAttentionInterruption();
qs('#return-to-today').addEventListener('click', () => {
+6
View File
@@ -811,6 +811,12 @@
</form>
</dialog>
<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>
<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-work-session-adjust-plan type="button" hidden>Adjust remaining plan</button>
</section>
<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">
<button class="mobile-task-action" data-mobile-task="work" type="button" aria-current="page"><span id="mobile-work-label">Work</span></button>
<button class="mobile-task-action" data-mobile-task="attention" type="button" aria-label="Attention, 0 items" hidden>Attention <span class="mobile-task-count" id="mobile-attention-count" hidden>0</span></button>
+4
View File
@@ -18,6 +18,10 @@
function refreshVisibility() {
const hidden = overlays.some(overlay => overlay.classList.contains('open'));
nav.hidden = hidden;
if (options.sessionHud) {
if (hidden) options.sessionHud.setAttribute('data-overlay-hidden', 'true');
else options.sessionHud.removeAttribute('data-overlay-hidden');
}
if (wasHidden && !hidden && launcher) launcher.focus();
wasHidden = hidden;
}
+25 -5
View File
@@ -200,9 +200,18 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
};
}
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway }) {
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway, getItem, onReopen }) {
let progress = null;
let runway = null;
queryAll('[data-mobile-today-open]').forEach(button =>
button.addEventListener('click', () => {
const identity = timer.snapshot().identity;
if (identity) onReopen?.(identity);
})
);
queryAll('[data-mobile-today-toggle]').forEach(button =>
button.addEventListener('click', () => view.toggle())
);
if (typeof document !== 'undefined') queryAll('.work-session-nav').forEach(nav => {
const button = document.createElement('button');
button.type = 'button';
@@ -232,8 +241,14 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
return hours ? hours + ':' + String(minutes).padStart(2, '0') + ':' + remainder : minutes + ':' + remainder;
};
const render = () => {
if (!progress) return;
const snapshot = timer.snapshot();
const active = Boolean(progress && isActive() && snapshot.identity);
queryAll('[data-mobile-today-hud]').forEach(element => { element.hidden = !active; });
queryAll('[data-mobile-today-open]').forEach(element => {
element.textContent = active ? String(getItem?.(snapshot.identity)?.title || 'Current Today item') : '';
});
if (typeof document !== 'undefined') document.body.classList.toggle('mobile-today-active', active);
if (!progress) return;
const sourceRunway = getRunway?.(snapshot) || runway;
const liveRunway = sourceRunway?.future_minutes !== undefined ? (() => {
const currentElapsed = Math.max(0, Math.ceil(snapshot.elapsed_ms / 60000));
@@ -273,10 +288,15 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
});
};
return {
open(identity, active) { active ? timer.activate(identity) : timer.stop(); render(); },
finish() { timer.stop(); progress = null; runway = null; },
open(identity, active) {
const current = timer.snapshot();
if (active && current.identity !== identity) timer.activate(identity);
else if (!active) timer.stop();
render();
},
finish() { timer.stop(); progress = null; runway = null; render(); },
update(nextProgress, nextRunway) { progress = nextProgress; runway = nextRunway; render(); },
reset() { progress = null; runway = null; },
reset() { progress = null; runway = null; render(); },
toggle() { const state = timer.snapshot(); state.running ? timer.pause() : timer.resume(); render(); },
render,
};