feat: pause Today during quick capture (Closes #1076)
CI / lint (pull_request) Successful in 3m23s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 2m35s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 11:06:44 +00:00
1 parent d9749e3a6f
commit f8c2541d30
7 files changed
+304 -7

No files matched your search

+2
View File
@@ -841,6 +841,8 @@ textarea { resize: vertical; min-height: 120px; }
.create-issue-sheet.open { display:flex; }
.create-issue-panel { width:min(560px,100%); height:100dvh; overflow:auto; overflow-x:hidden; display:grid; align-content:start; gap:12px; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); background:#0b1526; border-left:1px solid #2a496e; }
.create-issue-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.today-capture-interruption { display:grid; gap:2px; padding:10px 12px; border:1px solid #3f6f9f; border-radius:10px; background:#10243b; }
.today-capture-interruption[hidden] { display:none; }
.create-issue-header button, .create-issue-actions button, .create-issue-capture-actions button { min-height:44px; }
.mobile-create-issue-nav { display:none; }
.create-issue-form { display:grid; gap:12px; }
+9 -4
View File
@@ -1851,6 +1851,7 @@
const item = todayMyWork.find(entry => todayWork.identity(entry) === identity);
return completeTodayItem(item);
},
onCapture:() => openCreateIssueSheet(),
});
todaySessionSync = attachTodaySessionHandoff({
fetchJson:fetchReviewJson, storage:localStorage, timer, qs,
@@ -4871,6 +4872,8 @@
creatingIssue = false;
issueCaptureModal().close({restore:!followUpSourceUpdate});
createIssueLauncher = null;
if (createAndStartRequested) timerView.transferCapture();
else timerView.finishCapture();
if (followUpSourceUpdate) {
const source = followUpSourceUpdate;
followUpSourceUpdate = null;
@@ -5941,7 +5944,6 @@
event.currentTarget.disabled = false;
}
});
qs('#new-issue').addEventListener('click', openCreateIssueSheet);
qs('#create-update-follow-up').addEventListener('click', async () => {
if (!selectedUpdate || !selectedUpdateDetail || !await ensureIssueCapture()) return;
const source = {item: selectedUpdate, detail: selectedUpdateDetail};
@@ -5982,16 +5984,19 @@
await unfiledCaptures.completeResume(rUC);
rUC = '';
}
const capture = timerView.finishCapture();
issueCapture.clearDraft();
qs('#create-issue-title').value = '';
qs('#create-issue-body').value = '';
createIssueAttachmentController.clear();
closeCreateIssueSheet(true, false);
qs('[data-work-filter="draft"]').click();
mobileTaskDock.select('queues');
if (!capture) {
qs('[data-work-filter="draft"]').click();
mobileTaskDock.select('queues');
}
refreshMyWorkView();
const savedCard = qs('[data-capture-id="' + CSS.escape(savedCapture.id) + '"]');
requestAnimationFrame(() => {
if (!capture) requestAnimationFrame(() => {
savedCard?.scrollIntoView({block:'nearest'});
savedCard?.focus({preventScroll:true});
});
+4
View File
@@ -964,6 +964,10 @@
<h3 id="create-issue-heading">Capture work</h3>
<button id="cancel-new-issue" type="button">Cancel</button>
</div>
<aside class="today-capture-interruption" id="today-capture-interruption" role="status" hidden>
<strong id="today-capture-interruption-label">Today paused</strong>
<span class="small">Capture this thought, then return without counting interruption time.</span>
</aside>
<nav class="mobile-create-issue-nav" aria-label="New issue sections">
<button type="button" data-create-issue-section="describe">Describe</button>
<button type="button" data-create-issue-section="evidence">Evidence</button>
+109 -3
View File
@@ -5,7 +5,7 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
};
const empty = () => ({
version:1, active_identity:'', entries:{}, away_at:null,
pending_interruption:null, attention_interruption:null, timed_break:null,
pending_interruption:null, attention_interruption:null, capture_interruption:null, timed_break:null,
});
const read = () => {
const ownerKey = key();
@@ -42,6 +42,12 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
typeof pending.resume === 'boolean' ?
{ identity:pending.identity, resume:pending.resume } : null;
};
const validCapture = state => {
const pending = state.capture_interruption;
return pending && typeof pending.identity === 'string' && pending.identity &&
typeof pending.resume === 'boolean' ?
{ identity:pending.identity, resume:pending.resume } : null;
};
const validBreak = state => {
const value = state.timed_break;
return value && typeof value.identity === 'string' && value.identity &&
@@ -188,6 +194,44 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
state.away_at = null;
return write(state) ? { identity:pending.identity, resumed:pending.resume } : null;
},
beginCapture() {
const state = read();
const existing = validCapture(state);
if (existing) return existing;
const identity = state.active_identity;
const entry = state.entries[identity];
if (!identity || !entry) return null;
const resume = Boolean(entry.running);
if (resume) settle(state);
state.away_at = null;
state.capture_interruption = { identity, resume };
return write(state) ? { ...state.capture_interruption } : null;
},
captureInterruption() {
return validCapture(read());
},
abandonCapture() {
const state = read();
const pending = validCapture(state);
if (!pending) return null;
state.capture_interruption = null;
state.away_at = null;
return write(state) ? { identity:pending.identity, resumed:false } : null;
},
returnFromCapture() {
const state = read();
const pending = validCapture(state);
const entry = pending && state.entries[pending.identity];
if (!pending || !entry) return null;
const resumed = pending.resume && state.active_identity === pending.identity;
if (resumed && !entry.running) {
entry.started_at = now();
entry.running = true;
}
state.capture_interruption = null;
state.away_at = null;
return write(state) ? { identity:pending.identity, resumed } : null;
},
markAway() {
const state = read();
const entry = state.entries[state.active_identity];
@@ -259,13 +303,20 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now(), onChange
};
}
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway, getItem, onReopen, onResume, onComplete }) {
function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRunway, getItem, onReopen, onResume, onComplete, onCapture }) {
let progress = null;
let runway = null;
let breakView = null;
if (typeof createTodayBreak === 'function') {
breakView = createTodayBreak({timer, qs:selector => queryAll(selector)[0], queryAll, onChange:() => render(), onResume:onResume || onReopen});
}
const captureView = createTodayCaptureInterruption({
timer,
banner:queryAll('#today-capture-interruption')[0],
label:queryAll('#today-capture-interruption-label')[0],
getItem,
onReturn:() => render(),
});
queryAll('[data-mobile-today-open]').forEach(button =>
button.addEventListener('click', () => {
const identity = timer.snapshot().identity;
@@ -366,7 +417,7 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
button.setAttribute('aria-pressed', String(!snapshot.running));
});
};
return {
const view = {
open(identity, active) {
const current = timer.snapshot();
if (active && current.identity !== identity) timer.activate(identity);
@@ -377,8 +428,28 @@ function createTodayTimerView({ timer, isActive, queryAll, formatEstimate, getRu
update(nextProgress, nextRunway) { progress = nextProgress; runway = nextRunway; render(); },
reset() { progress = null; runway = null; render(); },
toggle() { const state = timer.snapshot(); state.running ? timer.pause() : timer.resume(); render(); },
beginCapture() {
const result = captureView.open();
this.restoreCapture();
render();
return result;
},
restoreCapture() {
const result = captureView.restore();
const button = queryAll('#save-unfiled-issue')[0];
if (button) button.textContent = result ? 'Save & return to Today' : 'Save to Drafts';
return result;
},
finishCapture() { return captureView.finish(); },
transferCapture() { return captureView.transfer(); },
render,
};
queryAll('#new-issue').forEach(button => button.addEventListener('click', () => {
view.beginCapture();
onCapture?.();
}));
if (timer.captureInterruption) view.restoreCapture();
return view;
}
function createTodayBudgetReplan({ timer, openPlan }) {
@@ -430,9 +501,44 @@ function createTodayInterruptionPrompt({ timer, sheet, description, getItemLabel
};
}
function createTodayCaptureInterruption({ timer, banner, label, getItem, getItemLabel, onReturn }) {
banner ||= typeof document === 'undefined' ? null : document.querySelector('#today-capture-interruption');
label ||= typeof document === 'undefined' ? null : document.querySelector('#today-capture-interruption-label');
const render = pending => {
if (!banner || !label) return;
banner.hidden = !pending;
label.textContent = pending ? 'Today paused · ' +
String(getItem?.(pending.identity)?.title || getItemLabel?.(pending.identity) || 'Current Today item') : '';
};
return {
open() {
const pending = timer.beginCapture();
render(pending);
return pending;
},
restore() {
const pending = timer.captureInterruption();
render(pending);
return pending;
},
finish() {
const result = timer.returnFromCapture();
render(null);
if (result) onReturn?.(result);
return result;
},
transfer() {
const result = timer.abandonCapture();
render(null);
return result;
},
};
}
if (typeof module !== 'undefined' && module.exports) {
createTodayTimer.createView = createTodayTimerView;
createTodayTimer.createInterruptionPrompt = createTodayInterruptionPrompt;
createTodayTimer.createBudgetReplan = createTodayBudgetReplan;
createTodayTimer.createCaptureInterruption = createTodayCaptureInterruption;
module.exports = createTodayTimer;
}