11/**
22 * Cursors Component
33 *
4- * Displays cursors for all connected users in real-time.
4+ * Displays cursors for all connected users in real-time (Google Docs style) .
55 */
66
7+ import { useState } from "react" ;
78import type { CursorInfo } from "../hooks/useCursors" ;
89
910interface CursorsProps {
@@ -13,61 +14,79 @@ interface CursorsProps {
1314export function Cursors ( { cursors } : CursorsProps ) {
1415 return (
1516 < >
16- { Object . entries ( cursors ) . map ( ( [ userId , cursor ] ) => {
17- return (
18- < div
19- key = { userId }
20- style = { {
21- position : "fixed" ,
22- left : cursor . position . x ,
23- top : cursor . position . y ,
24- pointerEvents : "none" ,
25- zIndex : 9999 ,
26- transition : "left 0.15s ease-out, top 0.15s ease-out" ,
27- willChange : "left, top" ,
28- } }
29- >
30- { /* Cursor SVG */ }
31- < svg
32- width = "24"
33- height = "24"
34- viewBox = "0 0 24 24"
35- fill = "none"
36- style = { {
37- filter : `drop-shadow(0 0 2px rgba(0,0,0,0.3))` ,
38- } }
39- >
40- < path
41- d = "M5.65376 12.3673L10.6477 17.3612L8.77546 19.2335L3.78149 14.2395L5.65376 12.3673Z"
42- fill = { cursor . user_color }
43- />
44- < path
45- d = "M4.46164 14.2323L12.6866 6.00731L18.3264 11.647L10.1014 19.872L4.46164 14.2323Z"
46- fill = { cursor . user_color }
47- />
48- </ svg >
49-
50- { /* User name label */ }
51- < div
52- style = { {
53- position : "absolute" ,
54- left : "20px" ,
55- top : "0px" ,
56- backgroundColor : cursor . user_color ,
57- color : "white" ,
58- padding : "4px 8px" ,
59- borderRadius : "4px" ,
60- fontSize : "12px" ,
61- fontWeight : 500 ,
62- whiteSpace : "nowrap" ,
63- boxShadow : "0 2px 8px rgba(0,0,0,0.2)" ,
64- } }
65- >
66- { cursor . user_name }
67- </ div >
68- </ div >
69- ) ;
70- } ) }
17+ { Object . entries ( cursors ) . map ( ( [ userId , cursor ] ) => (
18+ < RemoteCursor key = { userId } cursor = { cursor } />
19+ ) ) }
7120 </ >
7221 ) ;
7322}
23+
24+ function RemoteCursor ( { cursor } : { cursor : CursorInfo } ) {
25+ const [ isHovered , setIsHovered ] = useState ( false ) ;
26+
27+ return (
28+ < div
29+ style = { {
30+ position : "fixed" ,
31+ left : cursor . position . x ,
32+ top : cursor . position . y ,
33+ zIndex : 9999 ,
34+ transition : "left 0.15s ease-out, top 0.15s ease-out" ,
35+ willChange : "left, top" ,
36+ } }
37+ onMouseEnter = { ( ) => setIsHovered ( true ) }
38+ onMouseLeave = { ( ) => setIsHovered ( false ) }
39+ >
40+ { /* Vertical cursor line (caret) */ }
41+ < div
42+ style = { {
43+ position : "absolute" ,
44+ left : 0 ,
45+ top : 0 ,
46+ width : "2px" ,
47+ height : "20px" ,
48+ backgroundColor : cursor . user_color ,
49+ pointerEvents : "auto" ,
50+ cursor : "default" ,
51+ } }
52+ />
53+
54+ { /* Top flag/header */ }
55+ < div
56+ style = { {
57+ position : "absolute" ,
58+ left : "2px" ,
59+ top : "-2px" ,
60+ width : "8px" ,
61+ height : "8px" ,
62+ backgroundColor : cursor . user_color ,
63+ borderRadius : "2px 2px 0 0" ,
64+ pointerEvents : "auto" ,
65+ cursor : "default" ,
66+ } }
67+ />
68+
69+ { /* User name label (shown on hover) */ }
70+ { isHovered && (
71+ < div
72+ style = { {
73+ position : "absolute" ,
74+ left : "10px" ,
75+ top : "-6px" ,
76+ backgroundColor : cursor . user_color ,
77+ color : "white" ,
78+ padding : "4px 8px" ,
79+ borderRadius : "4px" ,
80+ fontSize : "12px" ,
81+ fontWeight : 500 ,
82+ whiteSpace : "nowrap" ,
83+ boxShadow : "0 2px 8px rgba(0,0,0,0.2)" ,
84+ pointerEvents : "none" ,
85+ } }
86+ >
87+ { cursor . user_name }
88+ </ div >
89+ ) }
90+ </ div >
91+ ) ;
92+ }
0 commit comments