[FEAT] Clean code

This commit is contained in:
2026-01-02 22:16:23 +01:00
parent 647083d5b9
commit 224d0d62fe
48 changed files with 83 additions and 384 deletions

View File

@@ -13,7 +13,6 @@
const breadcrumbs = computed<BreadcrumbItem[]>(() => {
const items: BreadcrumbItem[] = [{ name: 'Home', path: '/' }];
// Map route names to breadcrumb labels
const routeLabels: Record<string, string> = {
'blog-overview': 'Blog',
'blog-detail': 'Blog',
@@ -27,10 +26,8 @@
const routeName = route.name.toString();
if (routeName === 'blog-detail') {
// For blog detail pages, add Blog first, then the post title
items.push({ name: 'Blog', path: '/blog' });
// Get the post title from route meta or params if available
const postTitle = (route.meta.title as string) || 'Article';
items.push({ name: postTitle, path: route.path });
} else if (routeLabels[routeName]) {

View File

@@ -86,12 +86,10 @@
}
success.value = 'Thank you! Your feedback was sent.';
// Reset fields
name.value = '';
contact.value = '';
content.value = '';
// Optionally close after short delay
setTimeout(() => close(), 600);
} catch (e: any) {
console.error('Failed to send feedback:', e);

View File

@@ -63,7 +63,6 @@
if (input.files && input.files.length > 0) {
const files = Array.from(input.files);
emit('uploadSprites', files);
// Reset input value so uploading the same file again will trigger the event
if (fileInput.value) fileInput.value.value = '';
}
};

View File

@@ -170,13 +170,11 @@
const response = await fetch('/CHANGELOG.md');
const text = await response.text();
// Configure marked options
marked.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert line breaks to <br>
});
// Convert markdown to HTML
changelogHtml.value = await marked(text);
} catch (error) {
console.error('Failed to load changelog:', error);

View File

@@ -87,7 +87,6 @@
copied.value = false;
}, 2000);
} catch {
// Fallback for older browsers
const input = document.createElement('input');
input.value = shareUrl.value;
document.body.appendChild(input);
@@ -101,14 +100,12 @@
}
};
// Start sharing when modal opens
watch(
() => props.isOpen,
isOpen => {
if (isOpen) {
performShare();
} else {
// Reset state when closing
loading.value = false;
shareUrl.value = '';
error.value = '';

View File

@@ -225,7 +225,6 @@
(e: 'copySpriteToFrame', spriteId: string, targetLayerId: string, targetFrameIndex: number): void;
}>();
// Get settings from store
const settingsStore = useSettingsStore();
const gridContainerRef = ref<HTMLDivElement | null>(null);
@@ -242,6 +241,8 @@
};
};
const selectedSpriteIds = ref<Set<string>>(new Set());
const {
isDragging,
activeSpriteId,
@@ -266,6 +267,7 @@
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
selectedSpriteIds,
getMousePosition,
onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y),
onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex),
@@ -287,16 +289,13 @@
const contextMenuY = ref(0);
const contextMenuIndex = ref<number | null>(null);
const contextMenuSpriteId = ref<string | null>(null);
const selectedSpriteIds = ref<Set<string>>(new Set());
const replacingSpriteId = ref<string | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
// Copy to frame modal state
const showCopyToFrameModal = ref(false);
const copyTargetLayerId = ref(props.activeLayerId);
const copySpriteId = ref<string | null>(null);
// Clear selection when toggling multi-select mode
watch(
() => props.isMultiSelectMode,
() => {
@@ -304,7 +303,6 @@
}
);
// Use the new useGridMetrics composable for consistent calculations
const { gridMetrics: gridMetricsRef, getCellPosition: getCellPositionHelper } = useGridMetrics({
layers: toRef(props, 'layers'),
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
@@ -316,13 +314,11 @@
const gridMetrics = gridMetricsRef;
const totalCells = computed(() => {
// Use all layers regardless of visibility to keep canvas size stable
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
return Math.max(1, Math.ceil(maxLen / props.columns)) * props.columns;
});
const gridDimensions = computed(() => {
// Use all layers regardless of visibility to keep canvas size stable
const maxLen = Math.max(1, ...props.layers.map(l => l.sprites.length));
const rows = Math.max(1, Math.ceil(maxLen / props.columns));
return {
@@ -335,7 +331,6 @@
return getCellPositionHelper(index, props.columns, gridMetrics.value);
};
// Use the new useBackgroundStyles composable for consistent background styling
const {
backgroundColor: cellBackgroundColor,
backgroundImage: cellBackgroundImage,
@@ -353,17 +348,14 @@
const getCellBackgroundPosition = () => cellBackgroundPosition.value;
const startDrag = (event: MouseEvent) => {
// If the click originated from an interactive element (button, link, input), ignore drag handling
const target = event.target as HTMLElement;
if (target && target.closest('button, a, input, select, textarea')) {
return;
}
if (!gridContainerRef.value) return;
// Hide context menu if open
showContextMenu.value = false;
// Handle right-click for context menu
if ('button' in event && (event as MouseEvent).button === 2) {
event.preventDefault();
const pos = getMousePosition(event, props.zoom);
@@ -374,14 +366,11 @@
contextMenuSpriteId.value = clickedSprite?.id || null;
if (clickedSprite) {
// If the right-clicked sprite is not in the selection, clear selection and select just this one
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
selectedSpriteIds.value.clear();
selectedSpriteIds.value.add(clickedSprite.id);
}
// If it IS in the selection, keep the current selection (so we can apply action to all)
} else {
// Right click on empty space
selectedSpriteIds.value.clear();
}
@@ -392,37 +381,27 @@
return;
}
// Ignore non-left mouse buttons
if ('button' in event && (event as MouseEvent).button !== 0) return;
// Handle selection logic for left click
const pos = getMousePosition(event, props.zoom);
if (pos) {
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
if (clickedSprite) {
// Selection logic with multi-select mode check
if (event.ctrlKey || event.metaKey || props.isMultiSelectMode) {
// Toggle selection
if (selectedSpriteIds.value.has(clickedSprite.id)) {
selectedSpriteIds.value.delete(clickedSprite.id);
} else {
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
selectedSpriteIds.value.add(clickedSprite.id);
}
} else {
// Single select (but don't clear if dragging starts immediately?
// Usually standard behavior is to clear others unless shift/ctrl held)
if (!selectedSpriteIds.value.has(clickedSprite.id)) {
selectedSpriteIds.value.clear();
selectedSpriteIds.value.add(clickedSprite.id);
}
}
} else {
// Clicked on empty space
selectedSpriteIds.value.clear();
}
}
// Delegate to composable for actual drag handling
dragStart(event);
};
@@ -430,7 +409,6 @@
const latestEvent = ref<MouseEvent | null>(null);
const drag = (event: MouseEvent) => {
// Store the latest event and schedule a single animation frame update
latestEvent.value = event;
if (!pendingDrag.value) {
pendingDrag.value = true;
@@ -481,7 +459,6 @@
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Delete' || event.key === 'Backspace') {
// Don't delete if editing text/input
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') return;
if (selectedSpriteIds.value.size > 0) {
@@ -571,8 +548,6 @@
document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('keydown', handleKeyDown);
});
// Watch for background color changes
</script>
<style scoped></style>

View File

@@ -303,7 +303,6 @@
const previewContainerRef = ref<HTMLDivElement | null>(null);
// Get settings from store
const settingsStore = useSettingsStore();
const {
@@ -331,14 +330,12 @@
onDraw: () => {}, // No longer needed for canvas drawing
});
// Preview state
const isDraggable = ref(false);
const repositionAllLayers = ref(false);
const arrowKeyMovement = ref(false);
const showAllSprites = ref(false);
const isDragOver = ref(false);
// Context menu state
const showContextMenu = ref(false);
const contextMenuX = ref(0);
const contextMenuY = ref(0);
@@ -347,11 +344,9 @@
const fileInput = ref<HTMLInputElement | null>(null);
const replacingSpriteId = ref<string | null>(null);
// Copy to frame modal state
const showCopyToFrameModal = ref(false);
const copyTargetLayerId = ref(props.activeLayerId);
// Drag and drop for new sprites
const onDragOver = () => {
isDragOver.value = true;
};
@@ -373,10 +368,8 @@
};
const compositeFrames = computed<Sprite[]>(() => {
// Show frames from the active layer for the thumbnail list
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
if (!activeLayer) {
// Fallback to first visible layer if no active layer
const v = getVisibleLayers();
const len = maxFrames();
const arr: Sprite[] = [];
@@ -395,7 +388,6 @@
return layer.sprites[currentFrameIndex.value] || null;
});
// Use the new useGridMetrics composable for consistent calculations
const { gridMetrics, getCellPosition: getCellPositionHelper } = useGridMetrics({
layers: toRef(props, 'layers'),
negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'),
@@ -404,19 +396,16 @@
manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
});
// Helper function to get cell position (same as SpriteCanvas)
const getCellPosition = (index: number) => {
return getCellPositionHelper(index, props.columns, gridMetrics.value);
};
// Computed cell dimensions (for backward compatibility with existing code)
const cellDimensions = computed(() => ({
cellWidth: gridMetrics.value.maxWidth,
cellHeight: gridMetrics.value.maxHeight,
negativeSpacing: gridMetrics.value.negativeSpacing,
}));
// Use the new useBackgroundStyles composable for consistent background styling
const {
backgroundImage: previewBackgroundImage,
backgroundSize: previewBackgroundSize,
@@ -429,7 +418,6 @@
const getPreviewBackgroundImage = () => previewBackgroundImage.value;
// Dragging state
const isDragging = ref(false);
const activeSpriteId = ref<string | null>(null);
const activeLayerId = ref<string | null>(null);
@@ -438,7 +426,6 @@
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
// Drag functionality
const startDrag = (event: MouseEvent, sprite: Sprite, layerId: string) => {
if (!isDraggable.value || !previewContainerRef.value) return;
@@ -455,7 +442,6 @@
dragStartX.value = mouseX;
dragStartY.value = mouseY;
// Store initial positions for all sprites in this frame from all visible layers
allSpritesPosBeforeDrag.value.clear();
const visibleLayers = getVisibleLayers();
visibleLayers.forEach(layer => {
@@ -490,7 +476,6 @@
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
if (activeSpriteId.value === 'ALL_LAYERS') {
// Move all sprites in current frame from all visible layers
const visibleLayers = getVisibleLayers();
visibleLayers.forEach(layer => {
const sprite = layer.sprites[currentFrameIndex.value];
@@ -499,26 +484,21 @@
const originalPos = allSpritesPosBeforeDrag.value.get(sprite.id);
if (!originalPos) return;
// Calculate new position with constraints
let newX = Math.round(originalPos.x + deltaX);
let newY = Math.round(originalPos.y + deltaY);
// Constrain movement within expanded cell
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
});
} else {
// Move only the active layer sprite
const activeSprite = props.layers.find(l => l.id === activeLayerId.value)?.sprites[currentFrameIndex.value];
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
// Calculate new position with constraints and round to integers
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
// Constrain movement within expanded cell (allow negative values up to -negativeSpacing)
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - activeSprite.width, newX));
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
@@ -562,7 +542,6 @@
}
};
// Arrow key movement handler
const handleKeyDown = (event: KeyboardEvent) => {
if (!isDraggable.value || !arrowKeyMovement.value) return;
@@ -593,7 +572,6 @@
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
if (repositionAllLayers.value) {
// Move all sprites in current frame from all visible layers
const visibleLayers = getVisibleLayers();
visibleLayers.forEach(layer => {
const sprite = layer.sprites[currentFrameIndex.value];
@@ -602,14 +580,12 @@
let newX = Math.round(sprite.x + deltaX);
let newY = Math.round(sprite.y + deltaY);
// Constrain movement within expanded cell
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
});
} else {
// Move only the active layer sprite
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
if (!activeLayer) return;
@@ -619,7 +595,6 @@
let newX = Math.round(sprite.x + deltaX);
let newY = Math.round(sprite.y + deltaY);
// Constrain movement within expanded cell
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
@@ -627,7 +602,6 @@
}
};
// Lifecycle hooks
onMounted(() => {
window.addEventListener('keydown', handleKeyDown);
});
@@ -637,8 +611,6 @@
window.removeEventListener('keydown', handleKeyDown);
});
// Watchers - most canvas-related watchers removed
// Keep layer watchers to ensure reactivity
watch(
() => props.layers,
() => {},
@@ -650,7 +622,6 @@
);
watch(currentFrameIndex, () => {});
// Context menu functions
const openContextMenu = (event: MouseEvent, sprite: Sprite, layerId: string) => {
event.preventDefault();
contextMenuSpriteId.value = sprite.id;

View File

@@ -115,7 +115,6 @@
const settingsStore = useSettingsStore();
const splitter = useSpritesheetSplitter();
// State
const detectionMode = ref<DetectionMode>('grid');
const cellWidth = ref(64);
const cellHeight = ref(64);
@@ -126,12 +125,10 @@
const isProcessing = ref(false);
const imageElement = ref<HTMLImageElement | null>(null);
// Computed
const gridCols = computed(() => (imageElement.value && cellWidth.value > 0 ? Math.floor(imageElement.value.width / cellWidth.value) : 0));
const gridRows = computed(() => (imageElement.value && cellHeight.value > 0 ? Math.floor(imageElement.value.height / cellHeight.value) : 0));
// Load image and set initial cell size
watch(
() => props.imageUrl,
url => {
@@ -141,7 +138,6 @@
img.onload = () => {
imageElement.value = img;
// Set suggested cell size
const suggested = splitter.getSuggestedCellSize(img.width, img.height);
cellWidth.value = suggested.width;
cellHeight.value = suggested.height;
@@ -153,14 +149,12 @@
{ immediate: true }
);
// Regenerate preview when options change
watch([detectionMode, cellWidth, cellHeight, sensitivity, removeEmpty, preserveCellSize], () => {
if (imageElement.value) {
generatePreview();
}
});
// Generate preview
async function generatePreview() {
if (!imageElement.value) return;
@@ -190,7 +184,6 @@
}
}
// Actions
function cancel() {
emit('close');
}

View File

@@ -92,7 +92,6 @@
close();
} catch (e: any) {
error.value = e.message || 'An error occurred';
// Better PB error handling
if (e?.data?.message) error.value = e.data.message;
} finally {
loading.value = false;

View File

@@ -74,7 +74,6 @@
import { useProjectManager } from '@/composables/useProjectManager';
import { useToast } from '@/composables/useToast';
// Sub-components
import NavbarLogo from './navbar/NavbarLogo.vue';
import NavbarLinks from './navbar/NavbarLinks.vue';
import NavbarProjectActions from './navbar/NavbarProjectActions.vue';
@@ -144,7 +143,6 @@
} catch (error) {
addToast('Failed to save project', 'error');
console.error(error);
// Error handled in composable but kept here for toast
} finally {
isSaving.value = false;
}

View File

@@ -5,11 +5,7 @@
:key="link.path"
:to="link.path"
class="px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200"
:class="[
isActive(link.path)
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400'
]"
:class="[isActive(link.path) ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400']"
>
{{ link.name }}
</router-link>
@@ -17,22 +13,22 @@
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { useRoute } from 'vue-router';
const route = useRoute();
const route = useRoute();
const links = [
{ name: 'Home', path: '/' },
{ name: 'Blog', path: '/blog' },
{ name: 'About', path: '/about' },
{ name: 'FAQ', path: '/faq' },
{ name: 'Contact', path: '/contact' },
];
const links = [
{ name: 'Home', path: '/' },
{ name: 'Blog', path: '/blog' },
{ name: 'About', path: '/about' },
{ name: 'FAQ', path: '/faq' },
{ name: 'Contact', path: '/contact' },
];
const isActive = (path: string) => {
if (path === '/') {
return route.path === '/';
}
return route.path.startsWith(path);
};
const isActive = (path: string) => {
if (path === '/') {
return route.path === '/';
}
return route.path.startsWith(path);
};
</script>

View File

@@ -39,11 +39,7 @@
:key="link.path"
:to="link.path"
class="flex items-center gap-3 px-3 py-3 rounded-md text-base font-medium transition-all duration-200"
:class="[
isActive(link.path)
? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400'
: 'text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400'
]"
:class="[isActive(link.path) ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400' : 'text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-indigo-600 dark:hover:text-indigo-400']"
@click="$emit('close')"
>
<i :class="link.icon" class="w-5 text-center"></i> {{ link.name }}
@@ -163,6 +159,5 @@
const handleLogout = () => {
authStore.logout();
// emit('close'); // Optional: close menu on logout? Maybe better to keep open so they can login again if they want.
};
</script>

View File

@@ -15,9 +15,9 @@
<i class="fas fa-question-circle text-lg"></i>
</button>
</Tooltip>
<div class="h-4 w-px bg-gray-200 dark:bg-gray-700 ml-1.5"></div>
<DarkModeToggle />
</div>
</template>

View File

@@ -61,7 +61,6 @@
return width.value > 0 && height.value > 0 && columns.value > 0 && rows.value > 0;
});
// Reset to defaults when opened
watch(
() => props.isOpen,
val => {

View File

@@ -187,9 +187,7 @@
const sprites: any[] = [];
if (!project.data || !project.data.layers) return sprites;
// Iterate through layers to find sprites
for (const layer of project.data.layers as any[]) {
// Check if layer is visible (default to true if undefined)
if (layer.visible === false) continue;
if (layer.sprites && layer.sprites.length > 0) {

View File

@@ -76,59 +76,47 @@
const startPos = ref({ x: 0, y: 0 });
const startSize = ref({ width: 0, height: 0 });
// Add isFullScreen ref
const isFullScreen = ref(false);
const isMobile = ref(false);
// Add previous state storage for restoring from full screen
const previousState = ref({
position: { x: 0, y: 0 },
size: { width: 0, height: 0 },
});
// Check if device is mobile
const checkMobile = () => {
isMobile.value = window.innerWidth < 640; // sm breakpoint in Tailwind
// Auto fullscreen on mobile
if (isMobile.value && !isFullScreen.value) {
toggleFullScreen();
} else if (!isMobile.value && isFullScreen.value && autoFullScreened.value) {
// If we're no longer on mobile and were auto-fullscreened, exit fullscreen
toggleFullScreen();
autoFullScreened.value = false;
}
};
// Track if fullscreen was automatic (for mobile)
const autoFullScreened = ref(false);
// Add toggleFullScreen function
const toggleFullScreen = () => {
if (!isFullScreen.value) {
// Store current state before going full screen
previousState.value = {
position: { ...position.value },
size: { ...size.value },
};
// If toggling to fullscreen on mobile automatically, track it
if (isMobile.value) {
autoFullScreened.value = true;
}
} else {
// Restore previous state
position.value = { ...previousState.value.position };
size.value = { ...previousState.value.size };
}
isFullScreen.value = !isFullScreen.value;
};
// Unified start function for both drag and resize
const startAction = (event: MouseEvent | TouchEvent, action: 'drag' | 'resize') => {
if (isFullScreen.value) return;
// Extract the correct coordinates based on event type
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
@@ -211,7 +199,6 @@
position.value = { x: 0, y: 0 };
};
// Event handlers
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape' && props.isOpen) close();
};
@@ -223,7 +210,6 @@
}
};
// Add these new touch handling functions
const handleTouchStart = (event: TouchEvent) => {
if (isFullScreen.value) return;
if (event.touches.length === 1) {
@@ -237,7 +223,6 @@
handleMove(event);
};
// Lifecycle
watch(
() => props.isOpen,
newValue => {
@@ -250,7 +235,6 @@
window.addEventListener('resize', handleResize);
window.addEventListener('resize', checkMobile);
// Initial check for mobile
checkMobile();
if (props.isOpen) centerModal();

View File

@@ -27,7 +27,6 @@
import type { Toast } from '@/composables/useToast';
import { useToast } from '@/composables/useToast';
// Simple functional components for icons using h (avoid runtime compiler)
const SuccessIcon = {
render: () =>
h(

View File

@@ -42,30 +42,24 @@
let x = mouseX.value + offsetX;
let y = mouseY.value + offsetY;
// Get tooltip dimensions (estimate if not mounted yet)
const tooltipWidth = tooltipRef.value?.offsetWidth || 200;
const tooltipHeight = tooltipRef.value?.offsetHeight || 30;
// Screen boundaries
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
// Adjust horizontal position if too close to right edge
if (x + tooltipWidth + padding > screenWidth) {
x = mouseX.value - tooltipWidth - offsetX;
}
// Adjust horizontal position if too close to left edge
if (x < padding) {
x = padding;
}
// Adjust vertical position if too close to bottom edge
if (y + tooltipHeight + padding > screenHeight) {
y = mouseY.value - tooltipHeight - offsetY;
}
// Adjust vertical position if too close to top edge
if (y < padding) {
y = padding;
}