diff --git a/apps/desktop/src/components/ui/LightTooltip.vue b/apps/desktop/src/components/ui/LightTooltip.vue
index dd8510ee3..f93715385 100644
--- a/apps/desktop/src/components/ui/LightTooltip.vue
+++ b/apps/desktop/src/components/ui/LightTooltip.vue
@@ -1,5 +1,6 @@
@@ -255,7 +277,7 @@ watch(
@mouseleave="scheduleClose"
>
{{ text }}
-
+
diff --git a/apps/desktop/src/lib/__tests__/common/floatingViewportPosition.spec.ts b/apps/desktop/src/lib/__tests__/common/floatingViewportPosition.spec.ts
new file mode 100644
index 000000000..a2f32f174
--- /dev/null
+++ b/apps/desktop/src/lib/__tests__/common/floatingViewportPosition.spec.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from "vitest";
+import { floatingArrowOffset, floatingViewportShift } from "@/lib/common/floatingViewportPosition";
+
+describe("floating viewport position", () => {
+ it("moves a floating element inside each viewport edge", () => {
+ expect(floatingViewportShift({ left: -24, right: 296, top: 40, bottom: 80 }, { width: 800, height: 600 })).toEqual({ x: 32, y: 0 });
+ expect(floatingViewportShift({ left: 600, right: 840, top: 40, bottom: 80 }, { width: 800, height: 600 })).toEqual({ x: -48, y: 0 });
+ expect(floatingViewportShift({ left: 40, right: 240, top: -12, bottom: 28 }, { width: 800, height: 600 })).toEqual({ x: 0, y: 20 });
+ expect(floatingViewportShift({ left: 40, right: 240, top: 570, bottom: 620 }, { width: 800, height: 600 })).toEqual({ x: 0, y: -28 });
+ });
+
+ it("does not move an element that already fits", () => {
+ expect(floatingViewportShift({ left: 8, right: 792, top: 8, bottom: 592 }, { width: 800, height: 600 })).toEqual({ x: 0, y: 0 });
+ });
+
+ it("keeps the arrow pointing at the trigger after shifting the floating element", () => {
+ expect(floatingArrowOffset(320, 32)).toBe(128);
+ expect(floatingArrowOffset(320, -48)).toBe(208);
+ expect(floatingArrowOffset(40, 100)).toBe(8);
+ expect(floatingArrowOffset(40, -100)).toBe(32);
+ });
+});
diff --git a/apps/desktop/src/lib/common/floatingViewportPosition.ts b/apps/desktop/src/lib/common/floatingViewportPosition.ts
new file mode 100644
index 000000000..cd29da3ed
--- /dev/null
+++ b/apps/desktop/src/lib/common/floatingViewportPosition.ts
@@ -0,0 +1,29 @@
+export interface FloatingRect {
+ left: number;
+ right: number;
+ top: number;
+ bottom: number;
+}
+
+export interface FloatingViewport {
+ width: number;
+ height: number;
+}
+
+export function floatingViewportShift(rect: FloatingRect, viewport: FloatingViewport, margin = 8): { x: number; y: number } {
+ let x = 0;
+ let y = 0;
+
+ if (rect.left < margin) x = margin - rect.left;
+ else if (rect.right > viewport.width - margin) x = viewport.width - margin - rect.right;
+
+ if (rect.top < margin) y = margin - rect.top;
+ else if (rect.bottom > viewport.height - margin) y = viewport.height - margin - rect.bottom;
+
+ return { x, y };
+}
+
+export function floatingArrowOffset(length: number, shift: number, padding = 8): number {
+ const maximum = Math.max(padding, length - padding);
+ return Math.min(Math.max(padding, length / 2 - shift), maximum);
+}