Skip to content

Commit a680bc6

Browse files
authored
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
1 parent eed65fa commit a680bc6

File tree

5 files changed

+86
-26
lines changed

5 files changed

+86
-26
lines changed

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"prepare:assets": "node scripts/prepare.js"
1212
},
1313
"dependencies": {
14+
"@floating-ui/dom": "^1.7.5",
1415
"three": "^0.182.0"
1516
},
1617
"devDependencies": {

src/App.svelte

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script>
22
import { onMount } from 'svelte';
3+
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
34
import { currentPage, debugEnabled } from './stores.js';
45
import { registerServiceWorker, checkCacheStatus } from './core/service-worker.js';
56
import { setupCanvasEvents } from './core/emscripten.js';
@@ -15,6 +16,54 @@
1516
import DebugPanel from './lib/DebugPanel.svelte';
1617
import CanvasWrapper from './lib/CanvasWrapper.svelte';
1718
19+
async function positionTooltip(trigger) {
20+
const tooltip = trigger.querySelector('.tooltip-content');
21+
if (!tooltip) return;
22+
23+
const { x, y } = await computePosition(trigger, tooltip, {
24+
placement: 'top',
25+
middleware: [
26+
offset(8),
27+
flip(),
28+
shift({ padding: 8 })
29+
]
30+
});
31+
32+
Object.assign(tooltip.style, {
33+
left: `${x}px`,
34+
top: `${y}px`
35+
});
36+
}
37+
38+
function setupTooltips() {
39+
const isTouchDevice = window.matchMedia('(any-pointer: coarse)').matches;
40+
41+
// Touch devices: position and show on click
42+
document.addEventListener('click', (e) => {
43+
const trigger = e.target.closest('.tooltip-trigger');
44+
if (trigger) {
45+
e.preventDefault();
46+
e.stopPropagation();
47+
const wasActive = trigger.classList.contains('active');
48+
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
49+
if (!wasActive) {
50+
positionTooltip(trigger);
51+
trigger.classList.add('active');
52+
}
53+
} else {
54+
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
55+
}
56+
});
57+
58+
// Desktop: position on hover
59+
if (!isTouchDevice) {
60+
document.addEventListener('mouseenter', (e) => {
61+
const trigger = e.target.closest('.tooltip-trigger');
62+
if (trigger) positionTooltip(trigger);
63+
}, true);
64+
}
65+
}
66+
1867
onMount(async () => {
1968
// Disable browser's automatic scroll restoration
2069
if ('scrollRestoration' in history) {
@@ -30,6 +79,9 @@
3079
// Setup canvas events
3180
setupCanvasEvents();
3281
82+
// Setup global tooltip positioning
83+
setupTooltips();
84+
3385
// Initialize history state based on current page
3486
const initialHash = window.location.hash;
3587
if (initialHash) {

src/app.css

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -786,13 +786,13 @@ body {
786786

787787
.tooltip-content {
788788
position: absolute;
789-
bottom: 140%;
790-
left: 50%;
791-
transform: translateX(-50%);
792-
width: 220px;
789+
top: 0;
790+
left: 0;
791+
width: max-content;
792+
max-width: 220px;
793793
background-color: var(--color-bg-panel);
794794
color: var(--color-text-light);
795-
padding: 10px;
795+
padding: 0;
796796
border-radius: 5px;
797797
font-size: 0.85em;
798798
font-weight: normal;
@@ -802,14 +802,18 @@ body {
802802
z-index: 10000;
803803
opacity: 0;
804804
visibility: hidden;
805-
transition: opacity 0.2s, visibility 0.2s;
806805
pointer-events: none;
806+
overflow: hidden;
807+
max-height: 0;
807808
}
808809

809810
.tooltip-trigger:hover>.tooltip-content,
810811
.tooltip-trigger.active>.tooltip-content {
811812
opacity: 1;
812813
visibility: visible;
814+
padding: 10px;
815+
max-height: none;
816+
overflow: visible;
813817
}
814818

815819
.option-list {

src/lib/ConfigurePage.svelte

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,28 +101,8 @@
101101
}
102102
});
103103
}
104-
105-
// Setup tooltip handling for touch devices
106-
if (isTouchDevice) {
107-
setupTouchTooltips();
108-
}
109104
});
110105
111-
function setupTouchTooltips() {
112-
document.addEventListener('click', (e) => {
113-
const trigger = e.target.closest('.tooltip-trigger');
114-
if (trigger) {
115-
e.preventDefault();
116-
e.stopPropagation();
117-
const wasActive = trigger.classList.contains('active');
118-
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
119-
if (!wasActive) trigger.classList.add('active');
120-
} else {
121-
document.querySelectorAll('.tooltip-trigger.active').forEach(t => t.classList.remove('active'));
122-
}
123-
});
124-
}
125-
126106
function getSiFiles() {
127107
const hdMusic = document.getElementById('check-hd-music');
128108
const widescreenBgs = document.getElementById('check-widescreen-bgs');

0 commit comments

Comments
 (0)