Add dynamic tooltip positioning using Floating UI (#18)

- Use @floating-ui/dom for intelligent tooltip placement
- Tooltips now shift/flip to stay within viewport bounds
- Collapse hidden tooltips to prevent horizontal scroll overflow
- Move tooltip setup to App.svelte for global coverage
This commit is contained in:
Christian Semmler 2026-02-02 17:00:24 -08:00 committed by GitHub
parent eed65faeac
commit a680bc67ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 86 additions and 26 deletions

23
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "isle.pizza",
"version": "1.0.0",
"dependencies": {
"@floating-ui/dom": "^1.7.5",
"three": "^0.182.0"
},
"devDependencies": {
@ -1892,6 +1893,28 @@
"node": ">=12"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
"integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz",
"integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
"dependencies": {
"@floating-ui/core": "^1.7.4",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",

View File

@ -11,6 +11,7 @@
"prepare:assets": "node scripts/prepare.js"
},
"dependencies": {
"@floating-ui/dom": "^1.7.5",
"three": "^0.182.0"
},
"devDependencies": {

View File

@ -1,5 +1,6 @@
<script>
import { onMount } from 'svelte';
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
import { currentPage, debugEnabled } from './stores.js';
import { registerServiceWorker, checkCacheStatus } from './core/service-worker.js';
import { setupCanvasEvents } from './core/emscripten.js';
@ -15,6 +16,54 @@
import DebugPanel from './lib/DebugPanel.svelte';
import CanvasWrapper from './lib/CanvasWrapper.svelte';
async function positionTooltip(trigger) {
const tooltip = trigger.querySelector('.tooltip-content');
if (!tooltip) return;
const { x, y } = await computePosition(trigger, tooltip, {
placement: 'top',
middleware: [
offset(8),
flip(),
shift({ padding: 8 })
]
});
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`
});
}
function setupTooltips() {
const isTouchDevice = window.matchMedia('(any-pointer: coarse)').matches;
// Touch devices: position and show on click
document.addEventListener('click', (e) => {
const trigger = e.target.closest('.tooltip-trigger');
if (trigger) {
e.preventDefault();
e.stopPropagation();
const wasActive = trigger.classList.contains('active');
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
if (!wasActive) {
positionTooltip(trigger);
trigger.classList.add('active');
}
} else {
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
}
});
// Desktop: position on hover
if (!isTouchDevice) {
document.addEventListener('mouseenter', (e) => {
const trigger = e.target.closest('.tooltip-trigger');
if (trigger) positionTooltip(trigger);
}, true);
}
}
onMount(async () => {
// Disable browser's automatic scroll restoration
if ('scrollRestoration' in history) {
@ -30,6 +79,9 @@
// Setup canvas events
setupCanvasEvents();
// Setup global tooltip positioning
setupTooltips();
// Initialize history state based on current page
const initialHash = window.location.hash;
if (initialHash) {

View File

@ -786,13 +786,13 @@ body {
.tooltip-content {
position: absolute;
bottom: 140%;
left: 50%;
transform: translateX(-50%);
width: 220px;
top: 0;
left: 0;
width: max-content;
max-width: 220px;
background-color: var(--color-bg-panel);
color: var(--color-text-light);
padding: 10px;
padding: 0;
border-radius: 5px;
font-size: 0.85em;
font-weight: normal;
@ -802,14 +802,18 @@ body {
z-index: 10000;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
pointer-events: none;
overflow: hidden;
max-height: 0;
}
.tooltip-trigger:hover>.tooltip-content,
.tooltip-trigger.active>.tooltip-content {
opacity: 1;
visibility: visible;
padding: 10px;
max-height: none;
overflow: visible;
}
.option-list {

View File

@ -101,28 +101,8 @@
}
});
}
// Setup tooltip handling for touch devices
if (isTouchDevice) {
setupTouchTooltips();
}
});
function setupTouchTooltips() {
document.addEventListener('click', (e) => {
const trigger = e.target.closest('.tooltip-trigger');
if (trigger) {
e.preventDefault();
e.stopPropagation();
const wasActive = trigger.classList.contains('active');
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
if (!wasActive) trigger.classList.add('active');
} else {
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
}
});
}
function getSiFiles() {
const hdMusic = document.getElementById('check-hd-music');
const widescreenBgs = document.getElementById('check-widescreen-bgs');