feat: guide first mobile task activation (Closes #1140)
This commit is contained in:
1 parent
8731682090
commit
304e4a8a24
11 files changed
+343
-1
No files matched your search
@@ -1169,6 +1169,15 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.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-queue-sheet { width:100%; max-width:none; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||
|
||||
.mobile-first-task { box-sizing:border-box; width:100%; max-width:none; max-height:100dvh; margin:auto 0 0; padding:0; border:0; border-radius:18px 18px 0 0; color:var(--text); background:#102641; }
|
||||
.mobile-first-task::backdrop { background:rgba(3,9,18,.78); }
|
||||
.mobile-first-task-panel { display:grid; gap:14px; padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
||||
.mobile-first-task-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
|
||||
.mobile-first-task-panel h2, .mobile-first-task-panel p { margin:0; }
|
||||
.mobile-first-task-actions { display:grid; gap:10px; }
|
||||
.mobile-first-task-actions button { min-height:48px; width:100%; }
|
||||
.mobile-first-task-actions button:disabled { opacity:.55; }
|
||||
|
||||
.mobile-queue-sheet::backdrop { background:rgba(3,9,18,.7); }
|
||||
.mobile-queue-panel { padding:16px; padding-bottom:calc(16px + env(safe-area-inset-bottom)); }
|
||||
.mobile-queue-panel header { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||
|
||||
@@ -140,6 +140,11 @@
|
||||
if (!sheet.open) qs('#mobile-queue-sheet').showModal();
|
||||
qs('#mobile-start-day-action').focus();
|
||||
}
|
||||
const mobileFirstTask = createMobileFirstTask({
|
||||
getLogin: () => confirmedOwnerLogin,
|
||||
hasWork: () => todayMyWork.length > 0 || activeMyWork.length > 0,
|
||||
});
|
||||
mobileFirstTask.start();
|
||||
const mobileWorkEntry = createMobileWorkEntry({
|
||||
isTodayActive: () => workSession.checkpointed(),
|
||||
isTodayResumable: () => workSession.resumable(),
|
||||
@@ -152,6 +157,8 @@
|
||||
startToday: startTodaySession,
|
||||
planToday: () => openPlanToday(mobileTaskButtons.work),
|
||||
prepareToday: openMobileStartDay,
|
||||
shouldActivate: () => mobileFirstTask.required(),
|
||||
openActivation: () => mobileFirstTask.open(),
|
||||
findWork: () => qs('#find-work').click(),
|
||||
});
|
||||
const mobileStartDay = createMobileStartDay({
|
||||
@@ -3225,6 +3232,7 @@
|
||||
});
|
||||
mobileStartDay.render();
|
||||
mobileTaskDock.updateQueues(counts);
|
||||
mobileFirstTask.refresh();
|
||||
mobileTaskDock.updateWork(mobileWorkEntry.mode());
|
||||
mobileTaskDock.updateAttention(countMyWork(activeMyWork).attention);
|
||||
const activeQueue = qs('[data-work-filter="' + selectedWorkFilter + '"]');
|
||||
|
||||
@@ -1829,6 +1829,22 @@
|
||||
</section>
|
||||
</dialog>
|
||||
|
||||
<dialog class="mobile-first-task" id="mobile-first-task" aria-labelledby="mobile-first-task-title">
|
||||
<section class="mobile-first-task-panel">
|
||||
<header>
|
||||
<div><p class="small muted">Welcome to Work</p><h2 id="mobile-first-task-title">Start your first task</h2></div>
|
||||
<button id="close-mobile-first-task" type="button">Close</button>
|
||||
</header>
|
||||
<p class="small">Choose work, start it, then return to Work whenever you want to continue.</p>
|
||||
<p id="mobile-first-task-status" class="small" role="status" aria-live="polite"></p>
|
||||
<div class="mobile-first-task-actions">
|
||||
<button id="mobile-first-task-find" type="button">Find a task</button>
|
||||
<button id="mobile-first-task-create" type="button">Create a task</button>
|
||||
<button id="mobile-first-task-setup" type="button">Make this phone work-ready</button>
|
||||
</div>
|
||||
</section>
|
||||
</dialog>
|
||||
|
||||
<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="find" type="button">Find</button>
|
||||
@@ -1933,6 +1949,7 @@
|
||||
<script src="static/mobile-today-command-bar.js"></script>
|
||||
<script src="static/mobile-task-dock.js"></script>
|
||||
<script src="static/mobile-pull-refresh.js"></script>
|
||||
<script src="static/mobile-first-task.js"></script>
|
||||
<script src="static/mobile-work-entry.js"></script>
|
||||
<script src="static/mobile-queue-launcher.js"></script>
|
||||
<script src="static/mobile-delivery-recovery.js"></script>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
(function (root, factory) {
|
||||
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||
else root.createMobileFirstTask = factory;
|
||||
})(typeof self !== 'undefined' ? self : this, function createMobileFirstTask(options) {
|
||||
const doc = typeof document !== 'undefined' ? document : null;
|
||||
const win = typeof window !== 'undefined' ? window : null;
|
||||
options = Object.assign({
|
||||
storage: typeof localStorage !== 'undefined' ? localStorage : null,
|
||||
isOnline: () => typeof navigator === 'undefined' || navigator.onLine,
|
||||
mediaQuery: win?.matchMedia('(max-width: 600px)') || {matches:false},
|
||||
eventTarget: win,
|
||||
sheet: doc?.querySelector('#mobile-first-task'),
|
||||
findButton: doc?.querySelector('#mobile-first-task-find'),
|
||||
createButton: doc?.querySelector('#mobile-first-task-create'),
|
||||
setupButton: doc?.querySelector('#mobile-first-task-setup'),
|
||||
closeButton: doc?.querySelector('#close-mobile-first-task'),
|
||||
status: doc?.querySelector('#mobile-first-task-status'),
|
||||
onFind: () => doc?.querySelector('#find-work')?.click(),
|
||||
onCreate: () => doc?.querySelector('#new-issue')?.click(),
|
||||
onSetup: () => doc?.querySelector('#open-device-setup')?.click(),
|
||||
}, options);
|
||||
const prefix = 'stackchain.first-task.v1:';
|
||||
|
||||
function account() {
|
||||
return String(options.getLogin?.() || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function key() {
|
||||
const login = account();
|
||||
return login ? prefix + login : '';
|
||||
}
|
||||
|
||||
function completed() {
|
||||
const currentKey = key();
|
||||
if (!currentKey) return false;
|
||||
try { return options.storage.getItem(currentKey) === 'complete'; }
|
||||
catch (_error) { return false; }
|
||||
}
|
||||
|
||||
function required() {
|
||||
return Boolean(options.mediaQuery.matches && account() && !options.hasWork() && !completed());
|
||||
}
|
||||
|
||||
function markComplete() {
|
||||
const currentKey = key();
|
||||
if (!currentKey) return;
|
||||
try { options.storage.setItem(currentKey, 'complete'); }
|
||||
catch (_error) {}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const online = options.isOnline();
|
||||
options.findButton.disabled = !online;
|
||||
options.status.textContent = online ?
|
||||
'Choose a task to claim or create one of your own.' :
|
||||
'You are offline. Create a task now and it will stay in Drafts until you reconnect.';
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (!required()) return false;
|
||||
render();
|
||||
if (!options.sheet.open) options.sheet.showModal();
|
||||
(options.findButton.disabled ? options.createButton : options.findButton).focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (account() && options.hasWork() && !completed()) {
|
||||
markComplete();
|
||||
if (options.sheet.open) options.sheet.close();
|
||||
return 'completed';
|
||||
}
|
||||
if (options.sheet.open) render();
|
||||
return required() ? 'required' : 'inactive';
|
||||
}
|
||||
|
||||
function handoff(callback, requiresOnline = false) {
|
||||
if (requiresOnline && !options.isOnline()) return;
|
||||
if (options.sheet.open) options.sheet.close();
|
||||
callback();
|
||||
}
|
||||
|
||||
function start() {
|
||||
options.findButton.addEventListener('click', () => handoff(options.onFind, true));
|
||||
options.createButton.addEventListener('click', () => handoff(options.onCreate));
|
||||
options.setupButton.addEventListener('click', () => handoff(options.onSetup));
|
||||
options.closeButton.addEventListener('click', () => {
|
||||
if (options.sheet.open) options.sheet.close();
|
||||
});
|
||||
options.eventTarget?.addEventListener('online', render);
|
||||
options.eventTarget?.addEventListener('offline', render);
|
||||
}
|
||||
|
||||
return {required, open, refresh, render, start};
|
||||
});
|
||||
@@ -12,6 +12,7 @@
|
||||
if (options.getTodayCount() > 0 && options.isTodayResumable()) return 'resume';
|
||||
if (options.getTodayCount() > 0) return 'start';
|
||||
if (options.getEligibleCount() > 0) return 'plan';
|
||||
if (options.shouldActivate?.()) return 'activate';
|
||||
return 'find';
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@
|
||||
else if (current === 'resume') options.resumeToday();
|
||||
else if (current === 'start') options.startToday();
|
||||
else if (current === 'plan') options.planToday();
|
||||
else if (current === 'activate') options.openActivation();
|
||||
else if (current === 'find') options.findWork();
|
||||
else options.queueLauncher.open(current);
|
||||
return current;
|
||||
|
||||
@@ -113,6 +113,7 @@ const SHELL = [
|
||||
BASE + 'static/live-data-status.js',
|
||||
BASE + 'static/mobile-today-command-bar.js',
|
||||
BASE + 'static/mobile-task-dock.js',
|
||||
BASE + 'static/mobile-first-task.js',
|
||||
BASE + 'static/mobile-work-entry.js',
|
||||
BASE + 'static/mobile-queue-launcher.js',
|
||||
BASE + 'static/mobile-delivery-recovery.js',
|
||||
|
||||
Reference in new issue
Block a user