feat: pause Today while handling Attention (Closes #595)
This commit is contained in:
1 parent
9f3855d779
commit
9629643a63
17 files changed
+153
-21
No files matched your search
@@ -506,6 +506,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.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[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)); }
|
||||
header { min-height:56px; max-height:64px; padding:6px 10px; align-items:center; gap:8px; background:rgba(11,21,38,.98); }
|
||||
@@ -522,6 +524,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.device-setup-action { width:100%; }
|
||||
.device-readiness-card:not([hidden]) { display:grid; gap:12px; margin-left:max(0px,env(safe-area-inset-left)); margin-right:max(0px,env(safe-area-inset-right)); }
|
||||
.my-work { margin:0; }
|
||||
.attention-interruption:not([hidden]) { position:sticky; top:64px; z-index:6; margin-inline:max(0px,env(safe-area-inset-left)) max(0px,env(safe-area-inset-right)); }
|
||||
.my-work-header { align-items:flex-start; }
|
||||
.my-work-actions { width:100%; flex-wrap:nowrap; }
|
||||
.my-work-actions button { min-height:44px; flex:1 1 0; padding-inline:6px; }
|
||||
|
||||
@@ -44,12 +44,21 @@
|
||||
Array.from(document.querySelectorAll('[data-mobile-task]')).map(button => [button.dataset.mobileTask, button])
|
||||
);
|
||||
const mobileTaskOverlays = Array.from(document.querySelectorAll('[role="dialog"], #whiteboard-modal, #markdown-modal'));
|
||||
const attentionInterruption = qs('#attention-interruption');
|
||||
function renderAttentionInterruption() {
|
||||
const pending = timer.attentionInterruption();
|
||||
attentionInterruption.hidden = !pending;
|
||||
return pending;
|
||||
}
|
||||
function openMobileWorkFallback() {
|
||||
qs('[data-work-filter="all"]').click();
|
||||
qs('#my-work').scrollIntoView({block:'start'});
|
||||
qs('#my-work').focus();
|
||||
}
|
||||
function openMobileAttention() {
|
||||
if (workSession.checkpointed()) timer.beginAttention();
|
||||
renderAttentionInterruption();
|
||||
timerView.render();
|
||||
qs('[data-work-filter="attention"]').click();
|
||||
qs('#my-work').scrollIntoView({block:'start'});
|
||||
qs('#my-work').focus();
|
||||
@@ -941,6 +950,17 @@
|
||||
queryAll: s => document.querySelectorAll(s),
|
||||
formatEstimate: formatPlanMinutes,
|
||||
});
|
||||
renderAttentionInterruption();
|
||||
qs('#return-to-today').addEventListener('click', () => {
|
||||
const returned = timer.returnFromAttention();
|
||||
renderAttentionInterruption();
|
||||
if (!returned) return;
|
||||
selectTodayWork();
|
||||
const item = todayMyWork.find(entry => todayWork.identity(entry) === returned.identity);
|
||||
if (item) workSession.reopen(item);
|
||||
else continueTodaySession();
|
||||
timerView.render();
|
||||
});
|
||||
const interruptionPrompt = createTodayInterruptionPrompt({
|
||||
timer,
|
||||
sheet: qs('#today-interruption-sheet'),
|
||||
|
||||
@@ -147,6 +147,10 @@
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<aside class="attention-interruption" id="attention-interruption" role="status" aria-live="polite" hidden>
|
||||
<span><strong>Today paused</strong> while handling Attention</span>
|
||||
<button id="return-to-today" type="button">Return to Today</button>
|
||||
</aside>
|
||||
<section class="device-readiness-card" id="device-readiness-card" aria-labelledby="device-readiness-heading" hidden>
|
||||
<div>
|
||||
<strong id="device-readiness-heading">Make this phone work-ready</strong>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v90';
|
||||
const CACHE = 'stackchain-dashboard-shell-v91';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
|
||||
+42
-1
@@ -3,7 +3,10 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
|
||||
const login = String(getLogin?.() || '').trim().toLowerCase();
|
||||
return login ? 'stackchain.today-timer.v1.' + encodeURIComponent(login) : '';
|
||||
};
|
||||
const empty = () => ({ version:1, active_identity:'', entries:{}, away_at:null, pending_interruption:null });
|
||||
const empty = () => ({
|
||||
version:1, active_identity:'', entries:{}, away_at:null,
|
||||
pending_interruption:null, attention_interruption:null,
|
||||
});
|
||||
const read = () => {
|
||||
const ownerKey = key();
|
||||
if (!ownerKey || !storage) return empty();
|
||||
@@ -32,6 +35,12 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
|
||||
Number.isFinite(pending.away_ms) && pending.away_ms >= 0 ?
|
||||
{ identity:pending.identity, away_ms:pending.away_ms } : null;
|
||||
};
|
||||
const validAttention = state => {
|
||||
const pending = state.attention_interruption;
|
||||
return pending && typeof pending.identity === 'string' && pending.identity &&
|
||||
typeof pending.resume === 'boolean' ?
|
||||
{ identity:pending.identity, resume:pending.resume } : null;
|
||||
};
|
||||
const settle = (state, at = now()) => {
|
||||
const entry = state.entries[state.active_identity];
|
||||
if (!entry?.running) return state;
|
||||
@@ -63,6 +72,7 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
|
||||
state.entries[identity] = entry;
|
||||
state.away_at = null;
|
||||
state.pending_interruption = null;
|
||||
state.attention_interruption = null;
|
||||
return write(state);
|
||||
},
|
||||
pause() {
|
||||
@@ -86,8 +96,39 @@ function createTodayTimer({ storage, getLogin, now = () => Date.now() }) {
|
||||
settle(state);
|
||||
state.away_at = null;
|
||||
state.pending_interruption = null;
|
||||
state.attention_interruption = null;
|
||||
return write(state);
|
||||
},
|
||||
beginAttention() {
|
||||
const state = read();
|
||||
const existing = validAttention(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.attention_interruption = { identity, resume };
|
||||
return write(state) ? { ...state.attention_interruption } : null;
|
||||
},
|
||||
attentionInterruption() {
|
||||
return validAttention(read());
|
||||
},
|
||||
returnFromAttention() {
|
||||
const state = read();
|
||||
const pending = validAttention(state);
|
||||
const entry = pending && state.entries[pending.identity];
|
||||
if (!pending || !entry) return null;
|
||||
state.active_identity = pending.identity;
|
||||
if (pending.resume && !entry.running) {
|
||||
entry.started_at = now();
|
||||
entry.running = true;
|
||||
}
|
||||
state.attention_interruption = null;
|
||||
state.away_at = null;
|
||||
return write(state) ? { identity:pending.identity, resumed:pending.resume } : null;
|
||||
},
|
||||
markAway() {
|
||||
const state = read();
|
||||
const entry = state.entries[state.active_identity];
|
||||
|
||||
Reference in new issue
Block a user