feat: add mobile review readiness inspector (#121)
This commit is contained in:
1 parent
329591ebd6
commit
83788c6b82
7 files changed
+391
-9
No files matched your search
+89
-5
@@ -61,15 +61,24 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.work-filter[aria-pressed="true"] { border-color:var(--accent); background:#1d4f7a; }
|
||||
.my-work-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; }
|
||||
.my-work-card { min-height: 44px; display:block; padding:12px; border:1px solid #1f3a5f; border-radius:12px; background:#0f1d33; color:var(--text); }
|
||||
.my-work-card.review-trigger { width:100%; text-align:left; font:inherit; }
|
||||
.my-work-card:hover { border-color:var(--accent); }
|
||||
.my-work-card-title { display:block; margin:5px 0; font-weight:650; }
|
||||
.my-work[data-stale="true"] { border-color:#fcd34d; }
|
||||
.review-sheet { position:fixed; inset:0; z-index:50; display:none; justify-content:flex-end; background:rgba(5,12,21,.72); backdrop-filter:blur(4px); }
|
||||
.review-sheet.open { display:flex; }
|
||||
.review-sheet-panel { width:min(560px,100%); height:100%; overflow:auto; padding:18px; background:#0b1526; border-left:1px solid #2a496e; }
|
||||
.review-sheet-header { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
||||
.review-sheet-body { white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||
.review-file, .review-history { padding:8px 0; border-bottom:1px solid #1b2d45; overflow-wrap:anywhere; }
|
||||
.review-action { min-height:44px; }
|
||||
@media (max-width: 600px) {
|
||||
header { align-items:flex-start; }
|
||||
.my-work { margin:0; }
|
||||
.my-work-list { grid-template-columns:1fr; }
|
||||
.work-filters { width:100%; }
|
||||
.work-filter { flex:1 1 calc(50% - 8px); }
|
||||
.review-sheet-panel { width:100%; border-left:0; padding:14px; }
|
||||
}
|
||||
.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; }
|
||||
@@ -189,12 +198,32 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="review-sheet" id="review-sheet" role="dialog" aria-modal="true" aria-labelledby="review-sheet-title">
|
||||
<section class="review-sheet-panel">
|
||||
<div class="review-sheet-header">
|
||||
<div>
|
||||
<div class="small" id="review-sheet-key"></div>
|
||||
<h3 id="review-sheet-title">Pull request review</h3>
|
||||
</div>
|
||||
<button class="review-action" id="close-review-sheet">Close</button>
|
||||
</div>
|
||||
<div id="review-sheet-status" class="small" aria-live="polite">Choose a review request.</div>
|
||||
<p class="review-sheet-body" id="review-sheet-body"></p>
|
||||
<div class="row"><span class="pill" id="review-ci-state">CI unknown</span><a id="open-review-gitea" href="#" target="_blank" rel="noopener noreferrer">Open in Gitea</a></div>
|
||||
<h2>Changed files</h2>
|
||||
<div id="review-files" class="muted"></div>
|
||||
<h2>Review history</h2>
|
||||
<div id="review-history" class="muted"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="footer">Creative AI-imbued UI • stackchain-dashboard</div>
|
||||
|
||||
<script src="static/markdown.js"></script>
|
||||
<script src="static/commands.js"></script>
|
||||
<script src="static/widgets.js"></script>
|
||||
<script src="static/my-work.js"></script>
|
||||
<script src="static/review-sheet.js"></script>
|
||||
<script src="static/context-poller.js"></script>
|
||||
<script>
|
||||
(function(){
|
||||
@@ -232,6 +261,16 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
}
|
||||
let lastMyWork = [];
|
||||
let hasContextSnapshot = false;
|
||||
let selectedReview = null;
|
||||
let reviewTrigger = null;
|
||||
|
||||
async function fetchReviewJson(url, options) {
|
||||
const response = await fetch(url, options);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(payload.error || 'Review request failed.');
|
||||
return payload;
|
||||
}
|
||||
const reviewController = createReviewController({ fetchJson: fetchReviewJson });
|
||||
|
||||
function setStatus(msg) { qs('#status').textContent = msg || 'Live'; }
|
||||
function setClock() { qs('#clock').textContent = fmt(new Date()); }
|
||||
@@ -300,14 +339,58 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
|
||||
function renderMyWork() {
|
||||
const visible = filterMyWork(lastMyWork, selectedWorkFilter);
|
||||
qs('#my-work-list').innerHTML = visible.length ? visible.map(item =>
|
||||
'<a class="my-work-card" href="' + escAttr(item.url) + '" target="_blank" rel="noopener noreferrer">' +
|
||||
qs('#my-work-list').innerHTML = visible.length ? visible.map(item => {
|
||||
const contents =
|
||||
'<span class="small">' + escapeHtml(item.key) + ' · ' + escapeHtml(item.kind === 'pull' ? 'PR' : 'Issue') + '</span>' +
|
||||
'<span class="my-work-card-title">' + escapeHtml(item.title) + '</span>' +
|
||||
'<span class="pill">' + escapeHtml(item.reason) + '</span>' +
|
||||
(item.updated_at ? '<span class="small"> · Updated ' + escapeHtml(fmt(item.updated_at)) + '</span>' : '') +
|
||||
'</a>'
|
||||
).join('') : '<div class="muted">No ' + (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items')) + '.</div>';
|
||||
(item.updated_at ? '<span class="small"> · Updated ' + escapeHtml(fmt(item.updated_at)) + '</span>' : '');
|
||||
if (item.is_review) {
|
||||
const index = lastMyWork.findIndex(candidate => candidate.key === item.key && candidate.kind === item.kind);
|
||||
return '<button class="my-work-card review-trigger" data-review-index="' + index + '">' + contents + '</button>';
|
||||
}
|
||||
return '<a class="my-work-card" href="' + escAttr(item.url) + '" target="_blank" rel="noopener noreferrer">' + contents + '</a>';
|
||||
}).join('') : '<div class="muted">No ' + (selectedWorkFilter === 'review' ? 'reviews' : (selectedWorkFilter === 'all' ? 'work' : selectedWorkFilter + ' items')) + '.</div>';
|
||||
document.querySelectorAll('[data-review-index]').forEach(button => {
|
||||
button.addEventListener('click', () => openReviewSheet(lastMyWork[Number(button.dataset.reviewIndex)], button));
|
||||
});
|
||||
}
|
||||
|
||||
async function openReviewSheet(item, trigger) {
|
||||
selectedReview = item;
|
||||
reviewTrigger = trigger;
|
||||
qs('#review-sheet').classList.add('open');
|
||||
qs('#review-sheet-key').textContent = item.key;
|
||||
qs('#review-sheet-title').textContent = item.title;
|
||||
qs('#review-sheet-status').textContent = 'Loading review details…';
|
||||
qs('#review-sheet-body').textContent = '';
|
||||
qs('#review-files').textContent = '';
|
||||
qs('#review-history').textContent = '';
|
||||
qs('#open-review-gitea').href = item.url;
|
||||
qs('#close-review-sheet').focus();
|
||||
try {
|
||||
const detail = await reviewController.load(selectedReview);
|
||||
if (selectedReview !== item) return;
|
||||
qs('#review-sheet-body').textContent = detail.body || 'No description provided.';
|
||||
qs('#review-ci-state').textContent = 'CI ' + (detail.ci_state || 'unknown');
|
||||
qs('#review-files').innerHTML = (detail.files || []).length ? detail.files.map(file =>
|
||||
'<div class="review-file"><strong>' + escapeHtml(file.filename || 'Unknown file') + '</strong><div class="small">' +
|
||||
escapeHtml(file.status || 'changed') + ' · +' + Number(file.additions || 0) + ' / −' + Number(file.deletions || 0) + '</div></div>'
|
||||
).join('') : '<div>No changed files reported.</div>';
|
||||
qs('#review-history').innerHTML = (detail.reviews || []).length ? detail.reviews.map(review =>
|
||||
'<div class="review-history"><strong>' + escapeHtml(review.user?.login || 'Reviewer') + '</strong> · ' +
|
||||
escapeHtml(review.state || 'commented') + (review.body ? '<div class="small">' + escapeHtml(review.body) + '</div>' : '') + '</div>'
|
||||
).join('') : '<div>No prior reviews.</div>';
|
||||
qs('#review-sheet-status').textContent = 'Ready to review · by ' + (detail.author || 'unknown author');
|
||||
} catch (error) {
|
||||
qs('#review-sheet-status').textContent = error.message + ' Use Open in Gitea or close and retry.';
|
||||
}
|
||||
}
|
||||
|
||||
function closeReviewSheet() {
|
||||
qs('#review-sheet').classList.remove('open');
|
||||
selectedReview = null;
|
||||
if (reviewTrigger?.isConnected) reviewTrigger.focus();
|
||||
}
|
||||
|
||||
function markMyWorkStale() {
|
||||
@@ -416,6 +499,7 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
qs('#cmd-input').addEventListener('input', (e) => renderCommands(e.target.value));
|
||||
document.addEventListener('keydown', (e) => { if ((e.metaKey||e.ctrlKey) && e.key==='k') { e.preventDefault(); qs('#cmd-palette').classList.toggle('open'); if(qs('#cmd-palette').classList.contains('open')){ qs('#cmd-input').focus(); renderCommands(''); } } });
|
||||
qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal'));
|
||||
qs('#close-review-sheet').addEventListener('click', closeReviewSheet);
|
||||
|
||||
const contextPoller = createContextPoller({
|
||||
fetchContext: fetchContextSnapshot,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
function createReviewController({ fetchJson }) {
|
||||
function endpoint(item) {
|
||||
const [owner, repo] = String(item.repository || '').split('/');
|
||||
if (!owner || !repo || !Number.isInteger(Number(item.number))) {
|
||||
throw new Error('This review link is invalid.');
|
||||
}
|
||||
return 'api/v1/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) +
|
||||
'/pulls/' + Number(item.number) + '/review';
|
||||
}
|
||||
|
||||
async function load(item) {
|
||||
return fetchJson(endpoint(item), { headers: { Accept: 'application/json' } });
|
||||
}
|
||||
|
||||
return { load };
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = createReviewController;
|
||||
}
|
||||
Reference in new issue
Block a user