@@ -527,6 +527,15 @@ export function AnnotationLayer(props: {
527527 onMouseMove = { handleMouseMove }
528528 onMouseUp = { handleMouseUp }
529529 >
530+ < style > { `
531+ .text-hover-overlay {
532+ transition: fill 0.15s, stroke 0.15s;
533+ }
534+ .group:hover .text-hover-overlay {
535+ fill: rgba(59, 130, 246, 0.05);
536+ stroke: rgba(59, 130, 246, 0.4);
537+ }
538+ ` } </ style >
530539 < For each = { annotations } >
531540 { ( ann ) => (
532541 < g
@@ -558,6 +567,7 @@ export function AnnotationLayer(props: {
558567 "line-height" : "1" ,
559568 } }
560569 ref = { ( el ) => {
570+ el . textContent = ann . text ?? "" ;
561571 setTimeout ( ( ) => {
562572 el . focus ( ) ;
563573 const range = document . createRange ( ) ;
@@ -568,11 +578,11 @@ export function AnnotationLayer(props: {
568578 } ) ;
569579 } }
570580 onInput = { ( e ) => {
571- const text = e . currentTarget . innerText ;
581+ const text = e . currentTarget . textContent ?? "" ;
572582 setAnnotations ( ( a ) => a . id === ann . id , "text" , text ) ;
573583 } }
574584 onBlur = { ( e ) => {
575- const text = e . currentTarget . innerText ;
585+ const text = e . currentTarget . textContent ?? "" ;
576586
577587 if ( ! text . trim ( ) ) {
578588 if ( textSnapshot ) projectHistory . push ( textSnapshot ) ;
@@ -598,16 +608,38 @@ export function AnnotationLayer(props: {
598608 e . currentTarget . blur ( ) ;
599609 }
600610 } }
601- >
602- { ann . text }
603- </ div >
611+ />
604612 </ foreignObject >
605613 </ Show >
606614
607615 < Show when = { textEditingId ( ) !== ann . id } >
608616 < RenderAnnotation annotation = { ann } />
609617 </ Show >
610618
619+ { /* Text hover overlay - only shown when not selected */ }
620+ < Show
621+ when = {
622+ ann . type === "text" &&
623+ selectedAnnotationId ( ) !== ann . id &&
624+ ! textEditingId ( ) &&
625+ activeTool ( ) === "select"
626+ }
627+ >
628+ < rect
629+ x = { ann . x - handleSize ( ) * 0.3 }
630+ y = { ann . y - handleSize ( ) * 0.3 }
631+ width = { Math . abs ( ann . width ) + handleSize ( ) * 0.6 }
632+ height = { Math . abs ( ann . height ) + handleSize ( ) * 0.6 }
633+ fill = "transparent"
634+ stroke = "transparent"
635+ stroke-width = { 2 }
636+ rx = { 4 }
637+ ry = { 4 }
638+ class = "text-hover-overlay"
639+ style = { { "pointer-events" : "all" } }
640+ />
641+ </ Show >
642+
611643 < Show when = { selectedAnnotationId ( ) === ann . id && ! textEditingId ( ) } >
612644 < SelectionHandles
613645 annotation = { ann }
@@ -733,86 +765,121 @@ function SelectionHandles(props: {
733765} ) {
734766 const half = createMemo ( ( ) => props . handleSize / 2 ) ;
735767
768+ const isText = ( ) => props . annotation . type === "text" ;
769+ const isArrow = ( ) => props . annotation . type === "arrow" ;
770+
771+ const padding = createMemo ( ( ) => ( isText ( ) ? props . handleSize * 0.3 : 0 ) ) ;
772+
773+ const selectionRect = createMemo ( ( ) => {
774+ const ann = props . annotation ;
775+ const p = padding ( ) ;
776+ return {
777+ x : Math . min ( ann . x , ann . x + ann . width ) - p ,
778+ y : Math . min ( ann . y , ann . y + ann . height ) - p ,
779+ width : Math . abs ( ann . width ) + p * 2 ,
780+ height : Math . abs ( ann . height ) + p * 2 ,
781+ } ;
782+ } ) ;
783+
784+ const cornerHandles = ( ) => {
785+ if ( isText ( ) ) {
786+ return [
787+ { id : "nw" , x : 0 , y : 0 } ,
788+ { id : "ne" , x : 1 , y : 0 } ,
789+ { id : "sw" , x : 0 , y : 1 } ,
790+ { id : "se" , x : 1 , y : 1 } ,
791+ ] ;
792+ }
793+ return [
794+ { id : "nw" , x : 0 , y : 0 } ,
795+ { id : "n" , x : 0.5 , y : 0 } ,
796+ { id : "ne" , x : 1 , y : 0 } ,
797+ { id : "w" , x : 0 , y : 0.5 } ,
798+ { id : "e" , x : 1 , y : 0.5 } ,
799+ { id : "sw" , x : 0 , y : 1 } ,
800+ { id : "s" , x : 0.5 , y : 1 } ,
801+ { id : "se" , x : 1 , y : 1 } ,
802+ ] ;
803+ } ;
804+
736805 return (
737806 < Show
738- when = { props . annotation . type === "arrow" }
807+ when = { ! isArrow ( ) }
739808 fallback = {
740809 < g >
741- < For
742- each = { [
743- { id : "nw" , x : 0 , y : 0 } ,
744- { id : "n" , x : 0.5 , y : 0 } ,
745- { id : "ne" , x : 1 , y : 0 } ,
746- { id : "w" , x : 0 , y : 0.5 } ,
747- { id : "e" , x : 1 , y : 0.5 } ,
748- { id : "sw" , x : 0 , y : 1 } ,
749- { id : "s" , x : 0.5 , y : 1 } ,
750- { id : "se" , x : 1 , y : 1 } ,
751- ] }
752- >
753- { ( handle ) => (
754- < Handle
755- x = {
756- props . annotation . x +
757- handle . x * props . annotation . width -
758- half ( )
759- }
760- y = {
761- props . annotation . y +
762- handle . y * props . annotation . height -
763- half ( )
764- }
765- size = { props . handleSize }
766- cursor = { `${ handle . id } -resize` }
767- onMouseDown = { ( e ) =>
768- props . onResizeStart ( e , props . annotation . id , handle . id )
769- }
770- />
771- ) }
772- </ For >
810+ < Handle
811+ cx = { props . annotation . x }
812+ cy = { props . annotation . y }
813+ r = { half ( ) }
814+ cursor = "crosshair"
815+ isText = { false }
816+ onMouseDown = { ( e ) =>
817+ props . onResizeStart ( e , props . annotation . id , "start" )
818+ }
819+ />
820+ < Handle
821+ cx = { props . annotation . x + props . annotation . width }
822+ cy = { props . annotation . y + props . annotation . height }
823+ r = { half ( ) }
824+ cursor = "crosshair"
825+ isText = { false }
826+ onMouseDown = { ( e ) =>
827+ props . onResizeStart ( e , props . annotation . id , "end" )
828+ }
829+ />
773830 </ g >
774831 }
775832 >
776833 < g >
777- < Handle
778- x = { props . annotation . x - half ( ) }
779- y = { props . annotation . y - half ( ) }
780- size = { props . handleSize }
781- cursor = "crosshair"
782- onMouseDown = { ( e ) =>
783- props . onResizeStart ( e , props . annotation . id , "start" )
784- }
785- />
786- < Handle
787- x = { props . annotation . x + props . annotation . width - half ( ) }
788- y = { props . annotation . y + props . annotation . height - half ( ) }
789- size = { props . handleSize }
790- cursor = "crosshair"
791- onMouseDown = { ( e ) =>
792- props . onResizeStart ( e , props . annotation . id , "end" )
793- }
794- />
834+ < Show when = { isText ( ) } >
835+ < rect
836+ x = { selectionRect ( ) . x }
837+ y = { selectionRect ( ) . y }
838+ width = { selectionRect ( ) . width }
839+ height = { selectionRect ( ) . height }
840+ fill = "rgba(59, 130, 246, 0.1)"
841+ stroke = "#3b82f6"
842+ stroke-width = { 2 }
843+ rx = { 4 }
844+ ry = { 4 }
845+ style = { { "pointer-events" : "none" } }
846+ />
847+ </ Show >
848+ < For each = { cornerHandles ( ) } >
849+ { ( handle ) => (
850+ < Handle
851+ cx = { selectionRect ( ) . x + handle . x * selectionRect ( ) . width }
852+ cy = { selectionRect ( ) . y + handle . y * selectionRect ( ) . height }
853+ r = { half ( ) }
854+ cursor = { `${ handle . id } -resize` }
855+ isText = { isText ( ) }
856+ onMouseDown = { ( e ) =>
857+ props . onResizeStart ( e , props . annotation . id , handle . id )
858+ }
859+ />
860+ ) }
861+ </ For >
795862 </ g >
796863 </ Show >
797864 ) ;
798865}
799866
800867function Handle ( props : {
801- x : number ;
802- y : number ;
803- size : number ;
868+ cx : number ;
869+ cy : number ;
870+ r : number ;
804871 cursor : string ;
872+ isText : boolean ;
805873 onMouseDown : ( e : MouseEvent ) => void ;
806874} ) {
807875 return (
808- < rect
809- x = { props . x }
810- y = { props . y }
811- width = { props . size }
812- height = { props . size }
813- fill = "white"
814- stroke = "#00A0FF"
815- stroke-width = { 1 }
876+ < circle
877+ cx = { props . cx }
878+ cy = { props . cy }
879+ r = { props . r }
880+ fill = { props . isText ? "#3b82f6" : "white" }
881+ stroke = { props . isText ? "white" : "#3b82f6" }
882+ stroke-width = { props . isText ? 1.5 : 1 }
816883 class = "cursor-pointer"
817884 style = { {
818885 "pointer-events" : "all" ,
0 commit comments