visualViewport.resize fires on pinch-zoom, not just window-size change. The previous viewport-override would re-assert initial-scale=1.0 on every pinch event, snapping the user's zoom back to 1.0 instantly. Restrict the listener to orientationchange + initial settle timers so pinch-zoom is left alone after page load. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
199 lines
10 KiB
HTML
199 lines
10 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" id="viewport-meta" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
<script>
|
|
// Android Chrome (multi-window, split-screen, freeform, certain WebView
|
|
// configs) can cache device-width at a smaller value than the actual
|
|
// window, leaving the page rendered into a small upper-left canvas
|
|
// while position:fixed elements pin to the much larger visual viewport.
|
|
// window.innerWidth returns the (broken) layout viewport, so re-asserting
|
|
// width=innerWidth changes nothing. visualViewport.width returns the
|
|
// actual window — re-assert against that on resize/orientation so Chrome
|
|
// recomputes the layout at the correct size.
|
|
(function () {
|
|
var meta = document.getElementById('viewport-meta');
|
|
if (!meta || typeof window === 'undefined') return;
|
|
var apply = function () {
|
|
var vv = window.visualViewport;
|
|
if (!vv) return;
|
|
var w = Math.round(vv.width);
|
|
var inner = window.innerWidth;
|
|
// Only override when layout viewport is meaningfully smaller than
|
|
// the visual viewport (the bug we're fixing). Otherwise keep the
|
|
// standard `width=device-width` so phones with the keyboard up etc.
|
|
// behave normally.
|
|
if (w > inner + 8) {
|
|
meta.setAttribute('content', 'width=' + w + ', initial-scale=1.0, viewport-fit=cover');
|
|
} else {
|
|
meta.setAttribute('content', 'width=device-width, initial-scale=1.0, viewport-fit=cover');
|
|
}
|
|
};
|
|
// Only fire on events that genuinely change the window — NOT on
|
|
// visualViewport.resize, which also fires on pinch-zoom and would
|
|
// snap the user's zoom back to 1.0 every gesture.
|
|
window.addEventListener('orientationchange', apply);
|
|
apply();
|
|
// Re-check shortly after load — Chrome's initial reading sometimes
|
|
// settles a frame or two later in multi-window.
|
|
setTimeout(apply, 100);
|
|
setTimeout(apply, 500);
|
|
setTimeout(apply, 1500);
|
|
})();
|
|
</script>
|
|
<script>
|
|
// Append ?vpdebug to the URL to show a fixed overlay with the live
|
|
// viewport / element dimensions. Diagnostic only — remove once the
|
|
// Android tablet cut-off issue is understood.
|
|
(function () {
|
|
if (!/[?&]vpdebug\b/.test(location.search)) return;
|
|
var box = document.createElement('div');
|
|
box.style.cssText =
|
|
'position:fixed;top:0;right:0;z-index:2147483647;background:#000;color:#0f0;font:11px/1.3 monospace;padding:6px 8px;border:1px solid #0f0;white-space:pre;pointer-events:none;max-width:60vw;';
|
|
var ensureAttached = function () {
|
|
if (document.body && box.parentNode !== document.body) {
|
|
document.body.appendChild(box);
|
|
} else if (document.body) {
|
|
// Force to last child so it wins paint order regardless of stacking ctx
|
|
if (document.body.lastChild !== box) {
|
|
document.body.appendChild(box);
|
|
}
|
|
}
|
|
};
|
|
var tick = 0;
|
|
var update = function () {
|
|
ensureAttached();
|
|
tick += 1;
|
|
var f = function (el) {
|
|
if (!el) return 'none';
|
|
var r = el.getBoundingClientRect();
|
|
return Math.round(r.width) + 'x' + Math.round(r.height) + ' @' + Math.round(r.left) + ',' + Math.round(r.top);
|
|
};
|
|
var vv = window.visualViewport;
|
|
var nav = document.querySelector('.mobile-nav-bar');
|
|
var navInfo = 'null';
|
|
if (nav) {
|
|
var cs = window.getComputedStyle(nav);
|
|
navInfo = f(nav) + ' disp=' + cs.display + ' vis=' + cs.visibility + ' z=' + cs.zIndex + ' op=' + cs.opacity;
|
|
}
|
|
var dialogs = document.querySelectorAll('[role="dialog"],.modal,.modal-overlay').length;
|
|
var focused = document.activeElement && document.activeElement.tagName;
|
|
var d = window.__mNavDebug || {};
|
|
var dbg = 'mode=' + d.mode + ' modal=' + d.modalOpen + ' kb=' + d.keyboardOpen + ' fv=' + d.footerVisible + ' v=' + d.view;
|
|
var pc = document.querySelector('.project-content');
|
|
var pcCls = pc ? pc.className.replace('project-content', '').trim() : 'none';
|
|
box.textContent =
|
|
'win ' + window.innerWidth + 'x' + window.innerHeight + '\n' +
|
|
'vv ' + (vv ? Math.round(vv.width) + 'x' + Math.round(vv.height) + ' s' + vv.scale.toFixed(2) + ' o' + Math.round(vv.offsetLeft) + ',' + Math.round(vv.offsetTop) : 'n/a') + '\n' +
|
|
'dpr ' + window.devicePixelRatio + '\n' +
|
|
'html ' + f(document.documentElement) + '\n' +
|
|
'body ' + f(document.body) + '\n' +
|
|
'root ' + f(document.getElementById('root')) + '\n' +
|
|
'board ' + f(document.getElementById('board')) + '\n' +
|
|
'mNav ' + navInfo + '\n' +
|
|
'mNavR ' + dbg + '\n' +
|
|
'pcCls ' + pcCls + '\n' +
|
|
'mq768 ' + window.matchMedia('(max-width: 768px)').matches + '\n' +
|
|
'dialogs ' + dialogs + ' focus ' + focused + '\n' +
|
|
'sx ' + window.scrollX + ' sy ' + window.scrollY + ' tick ' + tick + '\n' +
|
|
'docCW ' + document.documentElement.clientWidth + ' bodyCW ' + document.body.clientWidth + '\n' +
|
|
'docSW ' + document.documentElement.scrollWidth + ' bodySW ' + document.body.scrollWidth + '\n' +
|
|
'ua ' + (navigator.userAgent || '').slice(0, 80);
|
|
try {
|
|
localStorage.setItem('vpdebug:last', box.textContent);
|
|
localStorage.setItem('vpdebug:lastAt', String(Date.now()));
|
|
} catch (e) {}
|
|
};
|
|
var attach = function () {
|
|
if (!document.body) {
|
|
requestAnimationFrame(attach);
|
|
return;
|
|
}
|
|
document.body.appendChild(box);
|
|
update();
|
|
setInterval(update, 500);
|
|
window.addEventListener('resize', update);
|
|
window.addEventListener('orientationchange', update);
|
|
if (window.visualViewport) {
|
|
window.visualViewport.addEventListener('resize', update);
|
|
window.visualViewport.addEventListener('scroll', update);
|
|
}
|
|
};
|
|
attach();
|
|
})();
|
|
</script>
|
|
<title>Fusion</title>
|
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
<link rel="manifest" href="/manifest.json" />
|
|
<link
|
|
rel="preload"
|
|
href="/fonts/SymbolsNerdFontMono-Regular.ttf"
|
|
as="font"
|
|
type="font/ttf"
|
|
crossorigin
|
|
/>
|
|
<meta name="theme-color" content="#1a1a2e" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
<script>
|
|
// Theme initialization - runs before React to prevent flash
|
|
(function() {
|
|
try {
|
|
var mode = localStorage.getItem('kb-dashboard-theme-mode') || 'dark';
|
|
var colorTheme = localStorage.getItem('kb-dashboard-color-theme') || 'default';
|
|
var validThemes = ['default', 'ocean', 'forest', 'sunset', 'zen', 'berry', 'high-contrast', 'industrial', 'monochrome', 'slate', 'ash', 'graphite', 'silver', 'solarized', 'factory', 'ayu', 'one-dark', 'nord', 'dracula', 'gruvbox', 'tokyo-night', 'catppuccin-mocha', 'github-dark', 'everforest', 'rose-pine', 'kanagawa', 'night-owl', 'palenight', 'monokai-pro', 'slime', 'brutalist', 'neon-city', 'parchment', 'terminal', 'glass', 'horizon', 'vitesse', 'outrun', 'snazzy', 'porple', 'espresso', 'mars', 'poimandres', 'ember', 'rust', 'copper', 'foundry', 'carbon', 'sandstone', 'lagoon', 'frost', 'lavender', 'neon-bloom', 'sepia'];
|
|
if (!validThemes.includes(colorTheme)) {
|
|
colorTheme = 'default';
|
|
}
|
|
var fontScale = Number(localStorage.getItem('kb-dashboard-font-scale-pct') || '100');
|
|
if (!Number.isFinite(fontScale)) {
|
|
fontScale = 100;
|
|
}
|
|
fontScale = Math.min(125, Math.max(85, Math.round(fontScale)));
|
|
var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
var effectiveMode = mode === 'system' ? (systemDark ? 'dark' : 'light') : mode;
|
|
document.documentElement.setAttribute('data-theme', effectiveMode);
|
|
document.documentElement.setAttribute('data-color-theme', colorTheme);
|
|
document.documentElement.style.fontSize = fontScale + '%';
|
|
if (colorTheme !== 'default') {
|
|
var base = document.baseURI || (document.location && document.location.href) || '';
|
|
var themeDataUrl;
|
|
if (!base) {
|
|
themeDataUrl = '/theme-data.css';
|
|
} else if (base.indexOf('http://') === 0 || base.indexOf('https://') === 0) {
|
|
themeDataUrl = new URL('/theme-data.css', base).toString();
|
|
} else if (base.indexOf('file://') === 0) {
|
|
if (base.endsWith('/')) {
|
|
themeDataUrl = base.slice(0, -1) + '/theme-data.css';
|
|
} else {
|
|
var lastSlashIndex = base.lastIndexOf('/');
|
|
themeDataUrl = lastSlashIndex >= 0
|
|
? base.slice(0, lastSlashIndex) + '/theme-data.css'
|
|
: '/theme-data.css';
|
|
}
|
|
} else {
|
|
themeDataUrl = '/theme-data.css';
|
|
}
|
|
var existingLink = document.getElementById('theme-data');
|
|
if (existingLink && existingLink.tagName === 'LINK' && existingLink.href !== themeDataUrl) {
|
|
existingLink.href = themeDataUrl;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
document.documentElement.setAttribute('data-color-theme', 'default');
|
|
document.documentElement.style.fontSize = '100%';
|
|
}
|
|
})();
|
|
</script>
|
|
<script type="module" src="./main.tsx"></script>
|
|
<!-- fusion:view-preload -->
|
|
<link rel="stylesheet" href="/theme-data.css" id="theme-data" />
|
|
</head>
|
|
<body>
|
|
<div id="root"></div>
|
|
</body>
|
|
</html>
|