sitedrift 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/viewer.css +8 -0
- package/assets/viewer.html +2 -2
- package/assets/viewer.js +73 -4
- package/package.json +1 -1
- package/src/frame-content.mjs +10 -1
- package/src/viewer.mjs +1 -1
package/assets/viewer.css
CHANGED
|
@@ -133,7 +133,11 @@ button:focus-visible, summary:focus-visible {
|
|
|
133
133
|
border-bottom: 1px solid var(--line);
|
|
134
134
|
}
|
|
135
135
|
.compact-side { min-inline-size: 0; display: flex; align-items: center; gap: 6px; }
|
|
136
|
+
.app.solo .compact-side { cursor: pointer; }
|
|
137
|
+
.compact-favicon { display: block; flex: 0 0 auto; inline-size: 16px; block-size: 16px; border-radius: 4px; }
|
|
138
|
+
.compact-copy { min-inline-size: 0; display: flex; align-items: baseline; gap: 7px; overflow: hidden; }
|
|
136
139
|
.compact-title { min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 650; }
|
|
140
|
+
.compact-origin { min-inline-size: 0; overflow: hidden; color: var(--muted); text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
|
|
137
141
|
.compact-controls { display: flex; align-items: center; gap: 8px; justify-self: center; }
|
|
138
142
|
|
|
139
143
|
.caret {
|
|
@@ -323,6 +327,10 @@ button[data-action="notes"] {
|
|
|
323
327
|
.status-err { color: #fff; background: var(--err); }
|
|
324
328
|
.compact-side .status-badge { block-size: 16px; }
|
|
325
329
|
|
|
330
|
+
@media (width <= 600px) {
|
|
331
|
+
.compact-origin { display: none; }
|
|
332
|
+
}
|
|
333
|
+
|
|
326
334
|
.meta-diff {
|
|
327
335
|
display: none;
|
|
328
336
|
align-items: center;
|
package/assets/viewer.html
CHANGED
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
<button class="caret" data-action="compact" title="Expand review chrome" aria-label="Expand review chrome">
|
|
106
106
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m3 10 5-5 5 5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
107
107
|
</button>
|
|
108
|
-
<div class="compact-side" data-compact-side="dev"><span class="pill dev">DEV</span><span class="compact-title" data-compact-title="dev">Loading…</span><span class="status-badge"></span></div>
|
|
108
|
+
<div class="compact-side" data-compact-side="dev"><span class="pill dev">DEV</span><img class="compact-favicon" data-compact-favicon="dev" alt=""><span class="compact-copy"><span class="compact-title" data-compact-title="dev">Loading…</span><span class="compact-origin" data-compact-origin="dev"></span></span><span class="status-badge"></span></div>
|
|
109
109
|
<div class="compact-controls">
|
|
110
110
|
<div class="modes" role="group" aria-label="View mode">
|
|
111
111
|
<button data-mode="split">Split</button>
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M15.5 6.5V3m0 0H12m3.5 0-2.2 2.2A6 6 0 1 0 15.8 11" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
121
121
|
</button>
|
|
122
122
|
</div>
|
|
123
|
-
<div class="compact-side" data-compact-side="live"><span class="pill live">LIVE</span><span class="compact-title" data-compact-title="live">Loading…</span><span class="status-badge"></span></div>
|
|
123
|
+
<div class="compact-side" data-compact-side="live"><span class="pill live">LIVE</span><img class="compact-favicon" data-compact-favicon="live" alt=""><span class="compact-copy"><span class="compact-title" data-compact-title="live">Loading…</span><span class="compact-origin" data-compact-origin="live"></span></span><span class="status-badge"></span></div>
|
|
124
124
|
<button class="icon" data-action="notes" title="Review notes" aria-label="Review notes">
|
|
125
125
|
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 3.5h10a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 15 13.5H9L5 17v-3.5A1.5 1.5 0 0 1 3.5 12V5A1.5 1.5 0 0 1 5 3.5Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
|
|
126
126
|
</button>
|
package/assets/viewer.js
CHANGED
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
const settleTimers = { dev: [], live: [] };
|
|
31
31
|
const frameState = { dev: { y: 0, max: 0 }, live: { y: 0, max: 0 } };
|
|
32
32
|
let order = params.get('swap') === '1' ? ['live', 'dev'] : ['dev', 'live'];
|
|
33
|
-
let syncScroll = queryOrStoredBool('scroll', 'site-compare-scroll',
|
|
33
|
+
let syncScroll = queryOrStoredBool('scroll', 'site-compare-scroll', !!config.hosted);
|
|
34
34
|
let scrollMode = params.get('scrollMode') || localStorage.getItem('site-compare-scroll-mode') || 'exact';
|
|
35
35
|
if (!['exact', 'ratio'].includes(scrollMode)) scrollMode = 'exact';
|
|
36
|
-
let mirrorLinks = queryOrStoredBool('mirror', 'site-compare-mirror',
|
|
36
|
+
let mirrorLinks = queryOrStoredBool('mirror', 'site-compare-mirror', !!config.hosted);
|
|
37
37
|
let mobileMode = (params.get('mode') || localStorage.getItem('site-compare-mode')) === 'mobile';
|
|
38
38
|
let compactMode = queryOrStoredBool('compact', 'site-compare-compact', !!config.hosted);
|
|
39
39
|
const storedView = localStorage.getItem('site-compare-view');
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
let dockMode = queryOrStoredBool('dock', 'site-compare-dock', true);
|
|
55
55
|
let scrollOwner = null;
|
|
56
56
|
const meta = { dev: null, live: null };
|
|
57
|
+
const statusDetails = { dev: {}, live: {} };
|
|
57
58
|
const apiHeaders = {
|
|
58
59
|
authorization: 'Bearer ' + config.token,
|
|
59
60
|
'content-type': 'application/json',
|
|
@@ -100,6 +101,31 @@
|
|
|
100
101
|
].filter(Boolean);
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
function formatMs(value) {
|
|
105
|
+
return Number.isFinite(value) && value >= 0 ? `${Math.round(value)} ms` : '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatBytes(value) {
|
|
109
|
+
if (!Number.isFinite(value) || value <= 0) return '';
|
|
110
|
+
if (value < 1024) return `${Math.round(value)} B`;
|
|
111
|
+
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
|
|
112
|
+
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function statusTitle(side, status) {
|
|
116
|
+
const detail = statusDetails[side];
|
|
117
|
+
const lines = [`${side.toUpperCase()} returned ${status || 'an error'}`];
|
|
118
|
+
if (detail.requestMs) lines.push(`Status check: ${formatMs(detail.requestMs)}`);
|
|
119
|
+
if (detail.response) lines.push(`Document response: ${formatMs(detail.response)}`);
|
|
120
|
+
if (detail.dom) lines.push(`DOM ready: ${formatMs(detail.dom)}`);
|
|
121
|
+
if (detail.load) lines.push(`Window load: ${formatMs(detail.load)}`);
|
|
122
|
+
const size = formatBytes(detail.transfer || detail.decoded);
|
|
123
|
+
if (size) lines.push(`${detail.transfer ? 'Transferred' : 'Decoded size'}: ${size}`);
|
|
124
|
+
if (detail.type) lines.push(`Content-Type: ${detail.type}`);
|
|
125
|
+
if (detail.cache) lines.push(`Cache-Control: ${detail.cache}`);
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
103
129
|
function setStatusBadge(side, status) {
|
|
104
130
|
const cls = status >= 200 && status < 300 ? 'status-ok'
|
|
105
131
|
: status >= 300 && status < 400 ? 'status-warn'
|
|
@@ -108,6 +134,8 @@
|
|
|
108
134
|
for (const badge of statusBadges(side)) {
|
|
109
135
|
badge.className = 'status-badge show ' + cls;
|
|
110
136
|
badge.textContent = text;
|
|
137
|
+
badge.title = statusTitle(side, status);
|
|
138
|
+
badge.setAttribute('aria-label', badge.title.replaceAll('\n', '. '));
|
|
111
139
|
}
|
|
112
140
|
}
|
|
113
141
|
|
|
@@ -115,15 +143,26 @@
|
|
|
115
143
|
for (const badge of statusBadges(side)) {
|
|
116
144
|
badge.className = 'status-badge';
|
|
117
145
|
badge.textContent = '';
|
|
146
|
+
badge.removeAttribute('title');
|
|
147
|
+
badge.removeAttribute('aria-label');
|
|
118
148
|
}
|
|
119
149
|
}
|
|
120
150
|
|
|
121
151
|
function fetchStatus(side, route) {
|
|
122
152
|
const url = statusUrl(side, route);
|
|
153
|
+
const started = performance.now();
|
|
123
154
|
const read = (method) => fetch(url, { method, cache: 'no-store', redirect: 'manual' });
|
|
124
155
|
read('HEAD')
|
|
125
156
|
.then((res) => (res.status === 405 || res.status === 501 ? read('GET') : res))
|
|
126
|
-
.then((res) =>
|
|
157
|
+
.then((res) => {
|
|
158
|
+
statusDetails[side] = {
|
|
159
|
+
...statusDetails[side],
|
|
160
|
+
requestMs: performance.now() - started,
|
|
161
|
+
type: res.headers.get('content-type') || '',
|
|
162
|
+
cache: res.headers.get('cache-control') || '',
|
|
163
|
+
};
|
|
164
|
+
setStatusBadge(side, res.status || (res.type === 'opaqueredirect' ? 302 : 0));
|
|
165
|
+
})
|
|
127
166
|
.catch(() => setStatusBadge(side, 0));
|
|
128
167
|
}
|
|
129
168
|
|
|
@@ -239,14 +278,26 @@
|
|
|
239
278
|
let canonicalPath = canonical;
|
|
240
279
|
try { canonicalPath = new URL(canonical).pathname; } catch {}
|
|
241
280
|
meta[side] = { title, description, canonicalPath, heading };
|
|
281
|
+
statusDetails[side] = { ...statusDetails[side], ...(source.timing || {}) };
|
|
242
282
|
label.querySelector('.page-heading').textContent = heading;
|
|
243
283
|
label.querySelector('.page-heading').title = title || heading;
|
|
244
284
|
updateDocTitle();
|
|
245
|
-
document.querySelector('[data-compact-title="' + side + '"]')
|
|
285
|
+
const compactTitle = document.querySelector('[data-compact-title="' + side + '"]');
|
|
286
|
+
compactTitle.textContent = heading;
|
|
287
|
+
compactTitle.title = title || heading;
|
|
288
|
+
const compactOrigin = document.querySelector('[data-compact-origin="' + side + '"]');
|
|
289
|
+
compactOrigin.textContent = new URL(config[side]).host + route;
|
|
290
|
+
compactOrigin.title = config[side] + route;
|
|
246
291
|
label.querySelector('.origin').textContent = config[side] + route;
|
|
247
292
|
const fav = label.querySelector('.favicon');
|
|
248
293
|
fav.onerror = () => { fav.onerror = null; fav.src = '/icon.svg'; };
|
|
249
294
|
fav.src = faviconSrc;
|
|
295
|
+
const compactFav = document.querySelector('[data-compact-favicon="' + side + '"]');
|
|
296
|
+
compactFav.onerror = () => {
|
|
297
|
+
compactFav.onerror = null;
|
|
298
|
+
compactFav.src = config.hosted ? '/__sitedrift/assets/icon.svg' : '/icon.svg';
|
|
299
|
+
};
|
|
300
|
+
compactFav.src = faviconSrc;
|
|
250
301
|
label.querySelector('.open-side').href = direct(side, route);
|
|
251
302
|
const card = label.querySelector('.seo-card');
|
|
252
303
|
const sourceRow = element('div', 'seo-source');
|
|
@@ -347,10 +398,14 @@
|
|
|
347
398
|
label.querySelector('.pill').textContent = side.toUpperCase();
|
|
348
399
|
label.querySelector('.page-heading').textContent = 'Loading…';
|
|
349
400
|
document.querySelector('[data-compact-title="' + side + '"]').textContent = 'Loading…';
|
|
401
|
+
document.querySelector('[data-compact-origin="' + side + '"]').textContent =
|
|
402
|
+
new URL(config[side]).host + route;
|
|
403
|
+
document.querySelector('[data-compact-favicon="' + side + '"]').removeAttribute('src');
|
|
350
404
|
label.querySelector('.origin').textContent = config[side] + route;
|
|
351
405
|
label.querySelector('.favicon').src = '/__' + side + '/favicon.ico';
|
|
352
406
|
label.querySelector('.open-side').href = direct(side, route);
|
|
353
407
|
meta[side] = null;
|
|
408
|
+
statusDetails[side] = {};
|
|
354
409
|
clearStatusBadge(side);
|
|
355
410
|
}
|
|
356
411
|
renderMetaDiff();
|
|
@@ -501,10 +556,16 @@
|
|
|
501
556
|
setLinkedScroll(side, frameState[side].y + delta);
|
|
502
557
|
} else if (message.type === 'navigate') {
|
|
503
558
|
go(message.route);
|
|
559
|
+
} else if (message.type === 'dismiss') {
|
|
560
|
+
closePopovers();
|
|
504
561
|
} else if (message.type === 'key') {
|
|
505
562
|
runFrameKey(message.key, side, message);
|
|
506
563
|
}
|
|
507
564
|
});
|
|
565
|
+
|
|
566
|
+
function closePopovers() {
|
|
567
|
+
for (const details of document.querySelectorAll('details[open]')) details.removeAttribute('open');
|
|
568
|
+
}
|
|
508
569
|
scrollButton.addEventListener('click', () => {
|
|
509
570
|
syncScroll = !syncScroll;
|
|
510
571
|
scrollButton.classList.toggle('active', syncScroll);
|
|
@@ -595,6 +656,14 @@
|
|
|
595
656
|
renderModes();
|
|
596
657
|
}
|
|
597
658
|
for (const button of modeButtons) button.addEventListener('click', () => setMode(button.dataset.mode));
|
|
659
|
+
for (const identity of document.querySelectorAll('.compact-side')) {
|
|
660
|
+
identity.addEventListener('click', () => {
|
|
661
|
+
if (viewMode !== 'solo') return;
|
|
662
|
+
focusSide = identity.dataset.compactSide === 'dev' ? 'live' : 'dev';
|
|
663
|
+
app.dataset.focus = focusSide;
|
|
664
|
+
renderModes();
|
|
665
|
+
});
|
|
666
|
+
}
|
|
598
667
|
for (const slider of overlaySliders) slider.addEventListener('input', () => {
|
|
599
668
|
if (viewMode !== 'overlay') setMode('overlay');
|
|
600
669
|
if (overlayBlend === 'difference') setOverlayBlend('opacity');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sitedrift",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Catch the drift between dev and live — frame your local site and production side-by-side on the same route, locked scroll, with a difference-blend overlay. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/frame-content.mjs
CHANGED
|
@@ -11,6 +11,14 @@ export function frameBridge(side, prefix = `/__${side}`) {
|
|
|
11
11
|
const title=(document.title||'').trim();
|
|
12
12
|
const description=q('meta[name="description"]')?.content?.trim()||'';
|
|
13
13
|
const canonical=q('link[rel="canonical"]')?.href||'';
|
|
14
|
+
const navigation=performance.getEntriesByType('navigation')[0];
|
|
15
|
+
const timing=navigation?{
|
|
16
|
+
response:Math.round(navigation.responseEnd),
|
|
17
|
+
dom:Math.round(navigation.domContentLoadedEventEnd),
|
|
18
|
+
load:Math.round(navigation.loadEventEnd||navigation.duration),
|
|
19
|
+
transfer:Number(navigation.transferSize)||0,
|
|
20
|
+
decoded:Number(navigation.decodedBodySize)||0
|
|
21
|
+
}:null;
|
|
14
22
|
const checks=[
|
|
15
23
|
['Title present',!!title],['Title 30–60 chars',title.length>=30&&title.length<=60,title.length+''],
|
|
16
24
|
['Meta description',!!description],['Description 70–160',description.length>=70&&description.length<=160,description.length+''],
|
|
@@ -23,7 +31,7 @@ export function frameBridge(side, prefix = `/__${side}`) {
|
|
|
23
31
|
['Images have alt',imgs.every((img)=>img.hasAttribute('alt')),imgs.filter((img)=>!img.hasAttribute('alt')).length+' missing']
|
|
24
32
|
].map(([label,ok,note])=>({label,ok,note}));
|
|
25
33
|
send('ready',{route:route(),meta:{title,description,canonical,heading:q('h1')?.textContent?.trim()||'',
|
|
26
|
-
siteName:q('meta[property="og:site_name"]')?.content?.trim()||'',icon:q('link[rel~="icon"]')?.href||'',checks}});
|
|
34
|
+
siteName:q('meta[property="og:site_name"]')?.content?.trim()||'',icon:q('link[rel~="icon"]')?.href||'',checks,timing}});
|
|
27
35
|
send('scroll',{y:scrollY,max:Math.max(0,root().scrollHeight-innerHeight)});
|
|
28
36
|
};
|
|
29
37
|
addEventListener('message',(event)=>{
|
|
@@ -43,6 +51,7 @@ export function frameBridge(side, prefix = `/__${side}`) {
|
|
|
43
51
|
if(linked&&!typing&&!event.metaKey&&!event.ctrlKey&&!event.altKey)send('key',{key:event.key,shift:event.shiftKey,y:scrollY,height:innerHeight,max:Math.max(0,root().scrollHeight-innerHeight)});
|
|
44
52
|
},true);
|
|
45
53
|
addEventListener('click',(event)=>{
|
|
54
|
+
send('dismiss');
|
|
46
55
|
if(!mirror||event.defaultPrevented||event.button!==0||event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)return;
|
|
47
56
|
const link=event.target.closest('a[href]');if(!link||link.target==='_blank'||link.hasAttribute('download'))return;
|
|
48
57
|
const url=new URL(link.href,location.href);if(url.origin!==location.origin||!url.pathname.startsWith(prefix))return;
|
package/src/viewer.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
|
|
3
3
|
// Bumped when the viewer assets change; busts the ?v= cache and reported in
|
|
4
4
|
// /health so the `site compare` wrapper knows when to restart the server.
|
|
5
|
-
export const VIEWER_VERSION =
|
|
5
|
+
export const VIEWER_VERSION = 30;
|
|
6
6
|
|
|
7
7
|
function readAsset(path) {
|
|
8
8
|
try {
|