feat: add mobile pull-to-refresh for My Work (Closes #1002)
CI / lint (pull_request) Successful in 2m57s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 2m1s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 05:43:39 +00:00
1 parent a2606f1dc2
commit 386acda1dc
9 files changed
+356 -1

No files matched your search

+44
View File
@@ -1061,3 +1061,47 @@ textarea { resize: vertical; min-height: 120px; }
.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; }
@keyframes fadein { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
/* One-handed live-data recovery on the mobile Home surface. */
.mobile-pull-refresh {
display: none;
}
@media (max-width: 600px) {
.mobile-pull-refresh {
position: sticky;
top: calc(3.25rem + env(safe-area-inset-top));
z-index: 8;
display: block;
width: max-content;
max-width: calc(100vw - 2rem);
margin: 0 auto -2rem;
padding: .45rem .8rem;
border: 1px solid rgba(125, 211, 252, .45);
border-radius: 999px;
background: rgba(11, 18, 32, .96);
color: #dbeafe;
font-size: .78rem;
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .28);
transform: translateY(.5rem);
pointer-events: none;
}
.mobile-pull-refresh[hidden] {
display: none;
}
.mobile-pull-refresh[data-state="ready"],
.mobile-pull-refresh[data-state="success"] {
border-color: rgba(52, 211, 153, .7);
}
.mobile-pull-refresh[data-state="error"] {
border-color: rgba(251, 113, 133, .75);
}
}
@media (max-width: 600px) and (prefers-reduced-motion: no-preference) {
.mobile-pull-refresh {
transition: transform 120ms ease, border-color 120ms ease;
}
}
+1
View File
@@ -7183,6 +7183,7 @@
intervalMs: 8000,
});
function load() { return contextPoller.refresh({ force: true }); }
createMobilePullRefresh({refresh:load}).start();
const liveDataStatusSheet = qs('#live-data-status-sheet');
const liveDataStatusTrigger = qs('#open-live-data-status');
let latestLiveFreshness = {};
+2
View File
@@ -124,6 +124,7 @@
</div>
<main>
<div id="mobile-pull-refresh" class="mobile-pull-refresh" role="status" aria-live="polite" hidden></div>
<section class="panel my-work" id="my-work" tabindex="-1">
<div class="my-work-header">
<div>
@@ -1634,6 +1635,7 @@
<script src="static/context-poller.js"></script>
<script src="static/live-data-status.js"></script>
<script src="static/mobile-task-dock.js"></script>
<script src="static/mobile-pull-refresh.js"></script>
<script src="static/mobile-work-entry.js"></script>
<script src="static/mobile-queue-launcher.js"></script>
<script src="static/mobile-start-day.js"></script>
+88
View File
@@ -0,0 +1,88 @@
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory;
else root.createMobilePullRefresh = factory;
})(typeof self !== 'undefined' ? self : this, function createMobilePullRefresh(options) {
const surface = options.surface || document.querySelector('#my-work');
const indicator = options.indicator || document.querySelector('#mobile-pull-refresh');
const threshold = options.threshold || 64;
const isMobile = options.isMobile || (() => window.matchMedia('(max-width: 600px)').matches);
const getScrollY = options.getScrollY || (() => window.scrollY);
const isBlocked = options.isBlocked || (() => Array.from(options.overlays || document.querySelectorAll('[role="dialog"], #whiteboard-modal, #markdown-modal')).some(overlay =>
(overlay.checkVisibility ? overlay.checkVisibility() : !overlay.closest('[hidden]')) &&
(overlay.classList.contains('open') || overlay.getAttribute('aria-modal') === 'true')));
let gesture = null;
let inFlight = null;
function show(state, text) {
indicator.hidden = false;
indicator.dataset.state = state;
indicator.textContent = text;
}
function resetGesture() {
gesture = null;
}
function startsOnExcludedTarget(target) {
if (!target) return false;
if (target.closest?.('button, a, input, textarea, select, summary, [contenteditable="true"], [role="button"]')) return true;
for (let node = target; node && node !== surface; node = node.parentElement) {
if ((node.scrollWidth || 0) > (node.clientWidth || 0)) return true;
}
return false;
}
function onPointerDown(event) {
if (inFlight || event.pointerType !== 'touch' || !isMobile() || getScrollY() > 0 || isBlocked() || startsOnExcludedTarget(event.target)) return;
gesture = {id:event.pointerId, x:event.clientX, y:event.clientY, ready:false};
surface.setPointerCapture?.(event.pointerId);
}
function onPointerMove(event) {
if (!gesture || gesture.id !== event.pointerId) return;
const dx = event.clientX - gesture.x;
const dy = event.clientY - gesture.y;
if (dy <= 0 || Math.abs(dx) > dy) return resetGesture();
event.preventDefault();
gesture.ready = dy >= threshold;
show(gesture.ready ? 'ready' : 'pulling', gesture.ready ? 'Release to refresh' : 'Pull to refresh');
}
async function onPointerUp(event) {
if (!gesture || gesture.id !== event.pointerId) return;
const shouldRefresh = gesture.ready;
resetGesture();
surface.releasePointerCapture?.(event.pointerId);
if (!shouldRefresh || inFlight) return;
show('refreshing', 'Refreshing My Work…');
inFlight = Promise.resolve().then(options.refresh);
try {
await inFlight;
show('success', 'My Work is up to date');
} catch (_) {
show('error', 'Could not refresh · pull to retry');
} finally {
inFlight = null;
}
}
function onPointerCancel() {
resetGesture();
}
return {
start() {
surface.addEventListener('pointerdown', onPointerDown);
surface.addEventListener('pointermove', onPointerMove);
surface.addEventListener('pointerup', onPointerUp);
surface.addEventListener('pointercancel', onPointerCancel);
},
stop() {
surface.removeEventListener('pointerdown', onPointerDown);
surface.removeEventListener('pointermove', onPointerMove);
surface.removeEventListener('pointerup', onPointerUp);
surface.removeEventListener('pointercancel', onPointerCancel);
resetGesture();
},
};
});
+1
View File
@@ -86,6 +86,7 @@ const SHELL = [
BASE + 'static/mobile-search-preview-nav.js',
BASE + 'static/mobile-plan-today-nav.js',
BASE + 'static/mobile-find-work-nav.js',
BASE + 'static/mobile-pull-refresh.js',
BASE + 'static/checklist-conflict.js',
BASE + 'static/voice-transcript-store.js',
BASE + 'static/voice-issue-capture.js',