feat: add mobile update workspace navigation (Closes #967)
CI / lint (pull_request) Successful in 2m25s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 1m3s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-16 13:42:19 +00:00
1 parent 3c86e02efe
commit 6c7b6ebd4d
7 files changed
+277 -14

No files matched your search

+7 -5
View File
@@ -469,26 +469,28 @@ textarea { resize: vertical; min-height: 120px; }
.issue-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
.issue-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; }
.issue-sheet-header button { min-height:44px; }
.mobile-issue-detail-nav, .mobile-pull-detail-nav { display:none; }
.mobile-issue-detail-nav, .mobile-pull-detail-nav, .mobile-update-detail-nav { display:none; }
@media (max-width:600px) {
.issue-sheet-panel, .pull-sheet-panel { padding-top:max(12px,env(safe-area-inset-top)); }
.mobile-issue-detail-nav, .mobile-pull-detail-nav {
.issue-sheet-panel, .pull-sheet-panel, .update-sheet-panel { padding-top:max(12px,env(safe-area-inset-top)); }
.mobile-issue-detail-nav, .mobile-pull-detail-nav, .mobile-update-detail-nav {
position:sticky; top:env(safe-area-inset-top); z-index:6;
display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:4px;
margin:8px -6px 12px; padding:6px;
background:rgba(11,21,38,.98); border-block:1px solid #2a496e;
}
.mobile-issue-detail-nav button, .mobile-pull-detail-nav button {
.mobile-issue-detail-nav button, .mobile-pull-detail-nav button, .mobile-update-detail-nav button {
min-width:0; min-height:44px; padding:4px; overflow-wrap:anywhere;
border-color:transparent; font-size:12px;
}
.mobile-issue-detail-nav button[aria-current="location"], .mobile-pull-detail-nav button[aria-current="location"] {
.mobile-issue-detail-nav button[aria-current="location"], .mobile-pull-detail-nav button[aria-current="location"], .mobile-update-detail-nav button[aria-current="location"] {
border-color:#60a5fa; background:#17365a; color:#fff;
}
#issue-overview, #issue-conversation, #issue-comment, #issue-planning { scroll-margin-top:72px; }
#pull-overview, #pull-conversation, #pull-comment, #pull-review { scroll-margin-top:72px; }
#update-conversation, #update-subject-context, #update-reply-workspace { scroll-margin-top:72px; }
}
@media (min-width:601px) { .mobile-issue-detail-nav, .mobile-pull-detail-nav { display:none; } }
@media (min-width:601px) { .mobile-update-detail-nav { display:none; } }
.completed-filed-actions { position:fixed; right:0; bottom:0; z-index:57; box-sizing:border-box; width:min(560px,100%); display:grid; grid-template-columns:minmax(0,1fr); align-items:center; gap:8px; margin:0; padding:10px 12px calc(10px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:12px 0 0; background:rgba(11,21,38,.98); overflow-wrap:anywhere; }
.completed-filed-actions[hidden] { display:none; }
.completed-filed-actions button { min-height:44px; min-width:0; }
+14
View File
@@ -1353,6 +1353,19 @@
panel: qs('#update-sheet .update-sheet-panel'),
jump: qs('#jump-update-new-activity'),
});
const mobileUpdateDetailNavigation = createMobileUpdateDetailNavigation({
root:qs('#update-sheet .update-sheet-panel'),
buttons:Object.fromEntries(Array.from(document.querySelectorAll('[data-update-section]')).map(button => [button.dataset.updateSection, button])),
targets:{
conversation:qs('#update-conversation'),
context:qs('#update-subject-context'),
reply:qs('#update-reply-workspace'),
},
replyComposer:qs('#update-reply'),
jumpToNewActivity:() => updateReadPosition.jump(),
prefersReducedMotion:() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
});
mobileUpdateDetailNavigation.start();
const issueDetailPosition = createWorkDetailPosition({ panel: qs('#issue-sheet .issue-sheet-panel') });
const pullDetailPosition = createWorkDetailPosition({ panel: qs('#pull-sheet .pull-sheet-panel') });
const reviewDetailPosition = createWorkDetailPosition({ panel: qs('#review-sheet .review-sheet-panel') });
@@ -1391,6 +1404,7 @@
void updateVoiceReply.open('update:' + item.notification_id);
selectedUpdateDetail = null;
updateReadPosition.open(String(item.notification_id));
mobileUpdateDetailNavigation.reset();
updateMentions.dismiss();
qs('#update-sheet').classList.add('open');
qs('.update-more-actions').open = false;
+18 -9
View File
@@ -1098,6 +1098,12 @@
</div>
</div>
<div id="update-triage-progress" class="update-triage-progress small" aria-live="polite" hidden></div>
<nav class="mobile-update-detail-nav" aria-label="Update sections">
<button type="button" data-update-section="activity">New activity</button>
<button type="button" data-update-section="conversation">Conversation</button>
<button type="button" data-update-section="context">Subject context</button>
<button type="button" data-update-section="reply">Reply</button>
</nav>
<div id="update-gesture-status" class="update-gesture-status small" aria-live="polite">Swipe right to keep unread or left to mark read. Buttons remain available.</div>
<div id="update-sheet-status" class="small" aria-live="polite">Choose an update.</div>
<button class="update-retry" id="retry-update-load" type="button" hidden>Retry loading update</button>
@@ -1105,18 +1111,20 @@
<span class="pill" id="update-subject-type">Update</span>
<span class="pill" id="update-subject-state"></span>
</div>
<h2>Full conversation</h2>
<div id="update-comments"></div>
<button class="update-new-activity-jump" id="jump-update-new-activity" type="button" hidden>Jump to new activity</button>
<button class="conversation-more" id="load-older-update-comments" type="button" hidden>Load older messages</button>
<div id="update-conversation-status" class="small" aria-live="assertive"></div>
<button class="update-retry" id="retry-update-conversation" type="button" hidden>Retry conversation</button>
<button class="conversation-actions-retry" id="retry-update-comment-actions" type="button" hidden>Retry comment tools</button>
<details>
<section id="update-conversation" tabindex="-1">
<h2>Full conversation</h2>
<div id="update-comments"></div>
<button class="update-new-activity-jump" id="jump-update-new-activity" type="button" hidden>Jump to new activity</button>
<button class="conversation-more" id="load-older-update-comments" type="button" hidden>Load older messages</button>
<div id="update-conversation-status" class="small" aria-live="assertive"></div>
<button class="update-retry" id="retry-update-conversation" type="button" hidden>Retry conversation</button>
<button class="conversation-actions-retry" id="retry-update-comment-actions" type="button" hidden>Retry comment tools</button>
</section>
<details id="update-subject-context">
<summary><h2>Subject context</h2></summary>
<div class="update-sheet-content muted markdown-content" id="update-subject-body"></div>
</details>
<section class="update-reply" aria-labelledby="update-reply-title">
<section class="update-reply" id="update-reply-workspace" aria-labelledby="update-reply-title">
<h2 id="update-reply-title">Reply</h2>
<label for="update-reply" class="small">Continue this issue or pull-request conversation</label>
<textarea id="update-reply" maxlength="10000" placeholder="Write a reply"></textarea>
@@ -1549,6 +1557,7 @@
<script src="static/push-notifications.js"></script>
<script src="static/issue-filing-receipt.js"></script>
<script src="static/mobile-issue-detail-nav.js"></script>
<script src="static/mobile-update-detail-nav.js"></script>
<script src="static/dashboard.js"></script>
</body>
</html>
+83
View File
@@ -0,0 +1,83 @@
function createMobileUpdateDetailNavigation(options) {
const buttons = options.buttons || {};
const targets = options.targets || {};
const listeners = new Map();
const prefersReducedMotion = options.prefersReducedMotion || (() => false);
const targetNames = new Map(Object.entries(targets).map(([name, target]) => [target, name]));
let observer = null;
function select(name) {
Object.entries(buttons).forEach(([key, button]) => {
if (!button) return;
if (key === name) button.setAttribute('aria-current', 'location');
else button.removeAttribute('aria-current');
});
}
function navigate(name) {
if (name === 'activity') {
if (options.jumpToNewActivity) options.jumpToNewActivity();
select(name);
return true;
}
const target = targets[name];
if (!target) return false;
if (name === 'context') target.open = true;
target.scrollIntoView({
block:'start',
behavior:prefersReducedMotion() ? 'auto' : 'smooth',
});
if (name === 'reply' && options.replyComposer) {
options.replyComposer.focus({ preventScroll:true });
}
select(name);
return true;
}
function reset() {
if (targets.context) targets.context.open = false;
select('activity');
}
return {
start() {
Object.entries(buttons).forEach(([name, button]) => {
if (!button || listeners.has(button)) return;
const listener = event => {
event.preventDefault();
navigate(name);
};
listeners.set(button, listener);
button.addEventListener('click', listener);
});
reset();
const observe = options.observe || ((handler, observedTargets) => {
if (typeof IntersectionObserver === 'undefined') return null;
const instance = new IntersectionObserver(handler, {
root:options.root || null,
rootMargin:'-20% 0px -60% 0px',
threshold:[0, 0.25, 0.5, 0.75, 1],
});
observedTargets.forEach(target => instance.observe(target));
return instance;
});
observer = observe(entries => {
const visible = entries
.filter(entry => entry.isIntersecting && targetNames.has(entry.target))
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (visible) select(targetNames.get(visible.target));
}, Array.from(targetNames.keys()).filter(Boolean));
},
stop() {
listeners.forEach((listener, button) => button.removeEventListener('click', listener));
listeners.clear();
if (observer) observer.disconnect();
observer = null;
},
navigate,
reset,
select,
};
}
if (typeof module !== 'undefined') module.exports = createMobileUpdateDetailNavigation;
+1
View File
@@ -78,6 +78,7 @@ const SHELL = [
BASE + 'static/issue-filing-review.js',
BASE + 'static/issue-sheet.js',
BASE + 'static/mobile-issue-detail-nav.js',
BASE + 'static/mobile-update-detail-nav.js',
BASE + 'static/checklist-conflict.js',
BASE + 'static/voice-transcript-store.js',
BASE + 'static/voice-issue-capture.js',