11import { useEffect , useLayoutEffect , useRef , useState } from 'react' ;
2- import { MousePointerClick } from 'lucide-react' ;
2+ import { MousePointerClick , X } from 'lucide-react' ;
33import type { Contributor } from '@/lib/daxParser/types' ;
44
55export interface VisualEditsMenuState {
@@ -28,7 +28,7 @@ function TypeBadge({ kind }: { kind: BadgeKind }) {
2828 if ( kind === 'html' ) {
2929 return (
3030 < span
31- className = { `${ base } bg-orange -100 text-orange -600 border-orange -200 dark:bg-orange -900/40 dark:text-orange-400 dark:border-orange -700` }
31+ className = { `${ base } bg-purple -100 text-purple -600 border-purple -200 dark:bg-purple -900/40 dark:text-purple-300 dark:border-purple -700` }
3232 >
3333 { '</>' }
3434 </ span >
@@ -52,18 +52,27 @@ function TypeBadge({ kind }: { kind: BadgeKind }) {
5252 ) ;
5353}
5454
55- function labelOf ( c : Contributor ) : string {
56- if ( c . kind === 'var' ) return c . name ;
57- if ( c . kind === 'html' ) {
58- // Mostra só o nome da tag para não poluir com atributos inline
59- const m = / ^ < ( [ a - z A - Z ] [ a - z A - Z 0 - 9 - ] * ) / . exec ( c . text ) ;
60- if ( m ) return `<${ m [ 1 ] } >` ;
61- }
62- return c . text ;
55+ // Para fragmentos HTML, em vez do genérico <div>/<span>, mostra a primeira
56+ // propriedade do style (ex.: "font-size", "background") ou o nome do primeiro
57+ // atributo, caindo de volta para a tag se nada for encontrado.
58+ function htmlLabel ( text : string ) : string {
59+ const tagMatch = / ^ < ( [ a - z A - Z ] [ a - z A - Z 0 - 9 - ] * ) / . exec ( text ) ;
60+ if ( ! tagMatch ) return text ;
61+ const tag = tagMatch [ 1 ] ;
62+ const rest = text . slice ( tagMatch [ 0 ] . length ) ;
63+
64+ const styleMatch = / \b s t y l e \s * = \s * [ ' " ] \s * ( [ a - z A - Z - ] + ) / . exec ( rest ) ;
65+ if ( styleMatch ) return styleMatch [ 1 ] ;
66+
67+ const attrMatch = / \s + ( [ a - z A - Z ] [ a - z A - Z 0 - 9 - ] * ) \s * = / . exec ( rest ) ;
68+ if ( attrMatch ) return attrMatch [ 1 ] ;
69+
70+ return `<${ tag } >` ;
6371}
6472
65- function tooltipOf ( c : Contributor ) : string | undefined {
66- if ( c . kind === 'var' ) return c . snippet ;
73+ function labelOf ( c : Contributor ) : string {
74+ if ( c . kind === 'var' ) return c . name ;
75+ if ( c . kind === 'html' ) return htmlLabel ( c . text ) ;
6776 return c . text ;
6877}
6978
@@ -72,10 +81,19 @@ function locOf(c: Contributor): { start: number; end: number } {
7281 return c . loc ;
7382}
7483
84+ const TRECHO_KEY = '__trecho__' ;
85+
7586export function VisualEditsMenu ( { menu, onSelect, onClose } : Props ) {
7687 const ref = useRef < HTMLDivElement | null > ( null ) ;
7788 const [ adjusted , setAdjusted ] = useState < { left : number ; top : number } | null > ( null ) ;
89+ const [ activeKey , setActiveKey ] = useState < string | null > ( null ) ;
90+
91+ // Reset feedback quando o menu abre para um novo elemento.
92+ useEffect ( ( ) => {
93+ setActiveKey ( null ) ;
94+ } , [ menu ?. clickedLoc . start , menu ?. clickedLoc . end ] ) ;
7895
96+ // Centraliza o menu no canvas do preview (e mantém dentro do viewport).
7997 useLayoutEffect ( ( ) => {
8098 if ( ! menu || ! ref . current ) {
8199 setAdjusted ( null ) ;
@@ -84,8 +102,18 @@ export function VisualEditsMenu({ menu, onSelect, onClose }: Props) {
84102 const rect = ref . current . getBoundingClientRect ( ) ;
85103 const vw = window . innerWidth ;
86104 const vh = window . innerHeight ;
87- let left = menu . x ;
88- let top = menu . y ;
105+
106+ let cx = menu . x ;
107+ let cy = menu . y ;
108+ const anchor = document . querySelector < HTMLElement > ( '[data-ve-anchor="preview-canvas"]' ) ;
109+ if ( anchor ) {
110+ const ar = anchor . getBoundingClientRect ( ) ;
111+ cx = ar . left + ar . width / 2 ;
112+ cy = ar . top + ar . height / 2 ;
113+ }
114+
115+ let left = cx - rect . width / 2 ;
116+ let top = cy - rect . height / 2 ;
89117 if ( left + rect . width > vw - VIEWPORT_PAD ) left = vw - rect . width - VIEWPORT_PAD ;
90118 if ( top + rect . height > vh - VIEWPORT_PAD ) top = vh - rect . height - VIEWPORT_PAD ;
91119 if ( left < VIEWPORT_PAD ) left = VIEWPORT_PAD ;
@@ -129,75 +157,85 @@ export function VisualEditsMenu({ menu, onSelect, onClose }: Props) {
129157 } ;
130158
131159 return (
132- < >
133- < div
134- ref = { ref }
135- style = { menuStyle }
136- className = "rounded-lg border border-border bg-white dark:bg-zinc-900 shadow-[var(--shadow-popover)] overflow-hidden"
137- role = "menu"
138- >
139- < div className = "flex items-center gap-2 px-3 py-1.5 border-b border-border" >
140- < MousePointerClick className = "h-3.5 w-3.5 text-primary" />
141- < span className = "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground" >
142- Elemento selecionado
143- </ span >
144- </ div >
145- < div className = "py-1" >
146- { menu . items . length > 0 && (
147- < div className = "max-h-72 overflow-y-auto" >
148- { menu . items . map ( ( c , i ) => {
149- const loc = locOf ( c ) ;
150- return (
151- < MenuItem
152- key = { `${ c . kind } -${ loc . start } -${ i } ` }
153- kind = { c . kind }
154- label = { labelOf ( c ) }
155- line = { c . line }
156- tooltip = { tooltipOf ( c ) }
157- onClick = { ( ) => {
158- onSelect ( loc . start , loc . end ) ;
159- onClose ( ) ;
160- } }
161- />
162- ) ;
163- } ) }
164- </ div >
165- ) }
166- { menu . items . length > 0 && < div className = "my-1 h-px bg-border" /> }
167- < MenuItem
168- kind = "html"
169- label = "Trecho HTML final"
170- line = { menu . clickedLine }
171- highlight
172- onClick = { ( ) => {
173- onSelect ( menu . clickedLoc . start , menu . clickedLoc . end ) ;
174- onClose ( ) ;
175- } }
176- />
177- </ div >
160+ < div
161+ ref = { ref }
162+ style = { menuStyle }
163+ className = "rounded-xl border border-border bg-white dark:bg-zinc-900 shadow-[var(--shadow-popover)] overflow-hidden ring-1 ring-black/5 dark:ring-white/10"
164+ role = "menu"
165+ >
166+ < div className = "relative flex items-center justify-center gap-2 px-3 py-2 border-b border-border bg-gradient-to-b from-muted/40 to-transparent" >
167+ < MousePointerClick className = "h-3.5 w-3.5 text-muted-foreground/70" />
168+ < span className = "text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground" >
169+ Elemento selecionado
170+ </ span >
171+ < button
172+ type = "button"
173+ onClick = { onClose }
174+ aria-label = "Fechar"
175+ className = "absolute right-1.5 top-1/2 -translate-y-1/2 inline-flex h-5 w-5 items-center justify-center rounded text-muted-foreground/60 hover:text-foreground hover:bg-accent transition-colors"
176+ >
177+ < X className = "h-3 w-3" />
178+ </ button >
179+ </ div >
180+ < div className = "py-1" >
181+ { menu . items . length > 0 && (
182+ < div className = "max-h-72 overflow-y-auto" >
183+ { menu . items . map ( ( c , i ) => {
184+ const loc = locOf ( c ) ;
185+ const key = `${ c . kind } -${ loc . start } -${ i } ` ;
186+ return (
187+ < MenuItem
188+ key = { key }
189+ kind = { c . kind }
190+ label = { labelOf ( c ) }
191+ line = { c . line }
192+ active = { activeKey === key }
193+ onClick = { ( ) => {
194+ onSelect ( loc . start , loc . end ) ;
195+ setActiveKey ( key ) ;
196+ } }
197+ />
198+ ) ;
199+ } ) }
200+ </ div >
201+ ) }
202+ { menu . items . length > 0 && < div className = "my-1 h-px bg-border" /> }
203+ < MenuItem
204+ kind = "html"
205+ label = "Trecho HTML final"
206+ line = { menu . clickedLine }
207+ highlight
208+ active = { activeKey === TRECHO_KEY }
209+ onClick = { ( ) => {
210+ onSelect ( menu . clickedLoc . start , menu . clickedLoc . end ) ;
211+ setActiveKey ( TRECHO_KEY ) ;
212+ } }
213+ />
178214 </ div >
179- </ >
215+ </ div >
180216 ) ;
181217}
182218
183219interface ItemProps {
184220 kind : BadgeKind ;
185221 label : string ;
186222 line : number ;
187- tooltip ?: string ;
188223 highlight ?: boolean ;
224+ active ?: boolean ;
189225 onClick : ( ) => void ;
190226}
191227
192- function MenuItem ( { kind, label, line, tooltip, highlight, onClick } : ItemProps ) {
228+ function MenuItem ( { kind, label, line, highlight, active, onClick } : ItemProps ) {
229+ const bg = active
230+ ? 'bg-primary/10 ring-1 ring-inset ring-primary/30'
231+ : highlight
232+ ? 'bg-primary/5 hover:bg-accent'
233+ : 'hover:bg-accent' ;
193234 return (
194235 < button
195236 type = "button"
196237 onClick = { onClick }
197- title = { tooltip }
198- className = { `w-full flex items-center gap-2 px-2.5 py-1 text-xs hover:bg-accent transition-colors ${
199- highlight ? 'bg-primary/5' : ''
200- } `}
238+ className = { `w-full flex items-center gap-2 px-2.5 py-1 text-xs transition-colors ${ bg } ` }
201239 role = "menuitem"
202240 >
203241 < TypeBadge kind = { kind } />
0 commit comments