From 179a6c1631b0852c410288c10393b72a36d3cc1a Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Mon, 23 Mar 2026 11:47:21 +0300 Subject: [PATCH 1/4] chore: install @floating-ui/react --- frontend/package-lock.json | 60 ++++++++++++++++++++++++++++++++++++++ frontend/package.json | 1 + 2 files changed, 61 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b3fea5c..af174bb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@floating-ui/react": "^0.27.19", "@react-oauth/google": "^0.13.4", "@supabase/supabase-js": "^2.98.0", "axios": "^1.13.5", @@ -934,6 +935,59 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4512,6 +4566,12 @@ "node": ">=8" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index f3b715c..afde04e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@floating-ui/react": "^0.27.19", "@react-oauth/google": "^0.13.4", "@supabase/supabase-js": "^2.98.0", "axios": "^1.13.5", From 0acd526b1de6f82e158f76462871d2c5f00243bc Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Mon, 23 Mar 2026 11:48:11 +0300 Subject: [PATCH 2/4] feat: refactor FloatingMenu to use @floating-ui/react for dynamic positioning --- .../components/study-room/FloatingMenu.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/study-room/FloatingMenu.tsx b/frontend/src/components/study-room/FloatingMenu.tsx index 1b51421..e765f37 100644 --- a/frontend/src/components/study-room/FloatingMenu.tsx +++ b/frontend/src/components/study-room/FloatingMenu.tsx @@ -1,18 +1,34 @@ import React from 'react'; import { cn } from '../../lib/utils'; import { aiActions, type ActionKey } from './constants'; +import { useFloating, shift, flip, offset, autoUpdate } from '@floating-ui/react'; interface FloatingMenuProps { - x: number; - y: number; + virtualElement: any; onAction: (action: ActionKey) => void; } -export const FloatingMenu: React.FC = ({ x, y, onAction }) => { +export const FloatingMenu: React.FC = ({ virtualElement, onAction }) => { + const { refs, floatingStyles } = useFloating({ + placement: 'bottom-start', + elements: { + reference: virtualElement, + }, + middleware: [ + offset(8), + flip({ fallbackPlacements: ['top-start', 'bottom-end', 'top-end'] }), + shift({ padding: 16 }) + ], + whileElementsMounted: autoUpdate, + }); + + if (!virtualElement) return null; + return (
{aiActions.map((action) => ( @@ -37,3 +53,4 @@ export const FloatingMenu: React.FC = ({ x, y, onAction }) =>
); }; + From 98437dfc8170277326c80cc3ec10389b80ea294a Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Mon, 23 Mar 2026 11:48:14 +0300 Subject: [PATCH 3/4] feat: improve text selection logic and integrate floating menu virtual element --- frontend/src/pages/StudyRoom.tsx | 46 +++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/frontend/src/pages/StudyRoom.tsx b/frontend/src/pages/StudyRoom.tsx index 45432a1..0699edc 100644 --- a/frontend/src/pages/StudyRoom.tsx +++ b/frontend/src/pages/StudyRoom.tsx @@ -125,7 +125,7 @@ const StudyRoom: React.FC = () => { }, [id]); /* ── Floating menu state ── */ - const [menuPosition, setMenuPosition] = useState<{ x: number; y: number } | null>(null); + const [virtualElement, setVirtualElement] = useState(null); const [pendingText, setPendingText] = useState(null); /* ── Panel width state (RAF‑throttled drag) ── */ @@ -173,19 +173,24 @@ const StudyRoom: React.FC = () => { /* ── Text selection handler ── */ const handleMouseUp = useCallback(() => { - const selection = window.getSelection(); - const text = selection?.toString().trim(); - - if (text && text.length > 0) { - const range = selection!.getRangeAt(0); - const rect = range.getBoundingClientRect(); - - const menuX = Math.max(8, rect.left + rect.width / 2 - 200); - const menuY = rect.bottom + 8; - - setPendingText(text); - setMenuPosition({ x: menuX, y: menuY }); - } + // Small timeout ensures selection is fully resolved and allows dismissing + setTimeout(() => { + const selection = window.getSelection(); + const text = selection?.toString().trim(); + + if (text && text.length > 0) { + // Deep copy the range so it doesn't get messed up when selection changes subtly + const range = selection!.getRangeAt(0).cloneRange(); + setPendingText(text); + setVirtualElement({ + getBoundingClientRect: () => range.getBoundingClientRect(), + getClientRects: () => range.getClientRects() + }); + } else { + setPendingText(null); + setVirtualElement(null); + } + }, 10); }, []); /* ── Click-outside to dismiss menu ── */ @@ -193,15 +198,15 @@ const StudyRoom: React.FC = () => { function handleClickOutside(e: MouseEvent) { const target = e.target as HTMLElement; if (target.closest('[class*="animate-in"]')) return; - setMenuPosition(null); + setVirtualElement(null); setPendingText(null); } - if (menuPosition) { + if (virtualElement) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } - }, [menuPosition]); + }, [virtualElement]); /* ── Handle action click ── */ async function handleAction(action: ActionKey) { @@ -217,7 +222,7 @@ const StudyRoom: React.FC = () => { }; setMessages((prev) => [...prev, userMessage]); - setMenuPosition(null); + setVirtualElement(null); setPendingText(null); setIsTyping(true); @@ -458,10 +463,9 @@ const StudyRoom: React.FC = () => {
{/* ── Floating Action Menu ── */} - {menuPosition && pendingText && ( + {virtualElement && pendingText && ( )} From 01830bbbab6808235f45cdd3388846ec30057beb Mon Sep 17 00:00:00 2001 From: Samuel-Tefera Date: Mon, 23 Mar 2026 11:50:53 +0300 Subject: [PATCH 4/4] feat: improve floating menu clamping for large text selections --- frontend/src/components/study-room/FloatingMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/study-room/FloatingMenu.tsx b/frontend/src/components/study-room/FloatingMenu.tsx index e765f37..057b757 100644 --- a/frontend/src/components/study-room/FloatingMenu.tsx +++ b/frontend/src/components/study-room/FloatingMenu.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { cn } from '../../lib/utils'; import { aiActions, type ActionKey } from './constants'; -import { useFloating, shift, flip, offset, autoUpdate } from '@floating-ui/react'; +import { useFloating, shift, flip, offset, autoUpdate, inline } from '@floating-ui/react'; interface FloatingMenuProps { virtualElement: any; @@ -15,9 +15,10 @@ export const FloatingMenu: React.FC = ({ virtualElement, onAc reference: virtualElement, }, middleware: [ + inline(), offset(8), flip({ fallbackPlacements: ['top-start', 'bottom-end', 'top-end'] }), - shift({ padding: 16 }) + shift({ padding: 16, crossAxis: true }) ], whileElementsMounted: autoUpdate, });