feat: keep mobile conversation replies keyboard-safe (#323)
This commit is contained in:
1 parent
160753caf8
commit
21e0a49d3c
8 files changed
+289
-4
No files matched your search
@@ -310,6 +310,12 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
#cmd-results { flex:1; min-height:0; overflow-y:auto; max-height:none; overscroll-behavior:contain; padding-bottom:env(safe-area-inset-bottom); }
|
||||
.create-issue-panel { width:100%; border-left:0; padding:14px; }
|
||||
.pull-sheet-panel { width:100%; border-left:0; padding:14px; }
|
||||
.issue-sheet-panel.composer-keyboard-active,
|
||||
.pull-sheet-panel.composer-keyboard-active,
|
||||
.update-sheet-panel.composer-keyboard-active { position:fixed; top:var(--composer-viewport-top,0px); right:0; height:var(--composer-viewport-height,100dvh); overscroll-behavior:contain; padding-bottom:calc(14px + env(safe-area-inset-bottom)); }
|
||||
.composer-keyboard-active .issue-comment-composer,
|
||||
.composer-keyboard-active .pull-comment-composer,
|
||||
.composer-keyboard-active .update-reply { scroll-margin-block:12px; padding-bottom:env(safe-area-inset-bottom); }
|
||||
.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); }
|
||||
}
|
||||
.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; }
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
(function(){
|
||||
const qs = (s, el=document) => el.querySelector(s);
|
||||
const fmt = (d) => new Date(d).toLocaleString();
|
||||
const mobileComposerViewport = createMobileComposerViewport({
|
||||
viewport: window.visualViewport,
|
||||
mediaQuery: window.matchMedia('(max-width: 600px)'),
|
||||
entries: [
|
||||
{ panel:qs('#issue-sheet .issue-sheet-panel'), workspace:qs('.issue-comment-composer'), composer:qs('#issue-comment'), submit:qs('#send-issue-comment'), status:qs('#issue-comment-status') },
|
||||
{ panel:qs('#pull-sheet .pull-sheet-panel'), workspace:qs('.pull-comment-composer'), composer:qs('#pull-comment'), submit:qs('#send-pull-comment'), status:qs('#pull-comment-status') },
|
||||
{ panel:qs('#update-sheet .update-sheet-panel'), workspace:qs('.update-reply'), composer:qs('#update-reply'), submit:qs('#send-update-reply'), status:qs('#update-reply-status') },
|
||||
],
|
||||
});
|
||||
mobileComposerViewport.start();
|
||||
[
|
||||
[qs('.app-menu'), qs('#app-menu-toggle')],
|
||||
[qs('.work-settings'), qs('#work-settings-toggle')],
|
||||
@@ -515,6 +525,8 @@
|
||||
qs('#retry-work-route').addEventListener('click', () => workRoute.sync());
|
||||
|
||||
function closeOpenWorkSheets() {
|
||||
['#issue-sheet .issue-sheet-panel', '#pull-sheet .pull-sheet-panel', '#update-sheet .update-sheet-panel']
|
||||
.forEach(selector => mobileComposerViewport.close(qs(selector)));
|
||||
['#issue-sheet', '#pull-sheet', '#review-sheet', '#update-sheet'].forEach(selector =>
|
||||
qs(selector).classList.remove('open')
|
||||
);
|
||||
@@ -1206,6 +1218,7 @@
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
mobileComposerViewport.close(qs('#issue-sheet .issue-sheet-panel'));
|
||||
qs('#issue-sheet').classList.remove('open');
|
||||
selectedIssue = null;
|
||||
selectedIssueDetail = null;
|
||||
@@ -1336,6 +1349,7 @@
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
mobileComposerViewport.close(qs('#pull-sheet .pull-sheet-panel'));
|
||||
qs('#pull-sheet').classList.remove('open');
|
||||
selectedPull = null;
|
||||
selectedPullDetail = null;
|
||||
@@ -1749,6 +1763,7 @@
|
||||
workRoute.close();
|
||||
return;
|
||||
}
|
||||
mobileComposerViewport.close(qs('#update-sheet .update-sheet-panel'));
|
||||
qs('#update-sheet').classList.remove('open');
|
||||
selectedUpdate = null;
|
||||
if (restoreTrigger && updateTrigger?.isConnected) updateTrigger.focus();
|
||||
|
||||
@@ -536,6 +536,7 @@
|
||||
<script src="static/mobile-launch.js"></script>
|
||||
<script src="static/install-app.js"></script>
|
||||
<script src="static/mobile-search-viewport.js"></script>
|
||||
<script src="static/mobile-composer-viewport.js"></script>
|
||||
<script src="static/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,92 @@
|
||||
(function (root, factory) {
|
||||
const exported = factory();
|
||||
if (typeof module === 'object' && module.exports) module.exports = exported;
|
||||
else root.createMobileComposerViewport = exported;
|
||||
})(typeof self !== 'undefined' ? self : this, function () {
|
||||
return function createMobileComposerViewport(options) {
|
||||
const entries = options.entries || [];
|
||||
const viewport = options.viewport;
|
||||
const mediaQuery = options.mediaQuery;
|
||||
const documentRef = options.document || document;
|
||||
const schedule = options.schedule || (callback => requestAnimationFrame(callback));
|
||||
let active = null;
|
||||
let started = false;
|
||||
|
||||
function reveal() {
|
||||
if (!active) return;
|
||||
active.workspace.scrollIntoView({ block:'end', inline:'nearest' });
|
||||
}
|
||||
|
||||
function applyGeometry() {
|
||||
if (!active || !viewport || !mediaQuery.matches) return;
|
||||
active.panel.style.setProperty('--composer-viewport-top', `${viewport.offsetTop || 0}px`);
|
||||
active.panel.style.setProperty('--composer-viewport-height', `${viewport.height}px`);
|
||||
reveal();
|
||||
}
|
||||
|
||||
function onViewportChange() {
|
||||
schedule(applyGeometry);
|
||||
}
|
||||
|
||||
function activate(entry) {
|
||||
if (!viewport || !mediaQuery.matches) return;
|
||||
if (active === entry) {
|
||||
schedule(applyGeometry);
|
||||
return;
|
||||
}
|
||||
if (active) deactivate(active, false);
|
||||
active = entry;
|
||||
entry.scrollTop = entry.panel.scrollTop;
|
||||
entry.panel.classList?.add('composer-keyboard-active');
|
||||
viewport.addEventListener('resize', onViewportChange);
|
||||
viewport.addEventListener('scroll', onViewportChange);
|
||||
applyGeometry();
|
||||
}
|
||||
|
||||
function deactivate(entry, restore = true) {
|
||||
if (active !== entry) return;
|
||||
viewport?.removeEventListener('resize', onViewportChange);
|
||||
viewport?.removeEventListener('scroll', onViewportChange);
|
||||
entry.panel.classList?.remove('composer-keyboard-active');
|
||||
entry.panel.style.removeProperty('--composer-viewport-top');
|
||||
entry.panel.style.removeProperty('--composer-viewport-height');
|
||||
active = null;
|
||||
if (restore) schedule(() => { entry.panel.scrollTop = entry.scrollTop; });
|
||||
}
|
||||
|
||||
function bind(entry) {
|
||||
entry.onFocus = () => activate(entry);
|
||||
entry.onFocusOut = () => schedule(() => {
|
||||
if (!entry.workspace.contains(documentRef.activeElement)) deactivate(entry);
|
||||
});
|
||||
entry.composer.addEventListener('focus', entry.onFocus);
|
||||
entry.workspace.addEventListener('focusout', entry.onFocusOut);
|
||||
}
|
||||
|
||||
function unbind(entry) {
|
||||
entry.composer.removeEventListener('focus', entry.onFocus);
|
||||
entry.workspace.removeEventListener('focusout', entry.onFocusOut);
|
||||
}
|
||||
|
||||
return {
|
||||
start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
entries.forEach(bind);
|
||||
},
|
||||
close(panel) {
|
||||
const entry = entries.find(candidate => candidate.panel === panel);
|
||||
if (entry) deactivate(entry, false);
|
||||
},
|
||||
stop() {
|
||||
if (!started) return;
|
||||
if (active) deactivate(active, false);
|
||||
entries.forEach(unbind);
|
||||
started = false;
|
||||
},
|
||||
revealActive() {
|
||||
schedule(applyGeometry);
|
||||
},
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v35';
|
||||
const CACHE = 'stackchain-dashboard-shell-v36';
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const SHELL = [
|
||||
BASE,
|
||||
@@ -32,11 +32,13 @@ const SHELL = [
|
||||
BASE + 'static/pull-sheet.js',
|
||||
BASE + 'static/review-sheet.js',
|
||||
BASE + 'static/work-route.js',
|
||||
BASE + 'static/task-overlay-history.js',
|
||||
BASE + 'static/context-poller.js',
|
||||
BASE + 'static/mobile-task-dock.js',
|
||||
BASE + 'static/mobile-launch.js',
|
||||
BASE + 'static/install-app.js',
|
||||
BASE + 'static/mobile-search-viewport.js',
|
||||
BASE + 'static/mobile-composer-viewport.js',
|
||||
BASE + 'static/background-issue-sync.js',
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user