Skip to content

Commit d471be3

Browse files
committed
refine(visual-edits): polimento visual e UX do dropdown
- Remove tooltip (title) dos itens; ruído visual sem ganho real. - Centraliza ícone e título no header com tracking premium; ícone agora é cinza/muted, gradient sutil de fundo e ring fino para acabamento. - Adiciona botão "X" de fechar à direita do header. - Clique em um item NÃO fecha mais o menu: marca o item como ativo (anel/ring com primary) e mantém o dropdown aberto. Fecha com X, Escape ou clique fora. - Badge HTML </>: muda de laranja para roxo (alinhado ao tema premium). - Label HTML deixa de ser genérico (<div>/<span>) e passa a mostrar a primeira propriedade do style (ex.: "font-size", "background") ou o nome do primeiro atributo — bem mais informativo. - Centraliza o dropdown no canvas do preview (data-ve-anchor) em vez de aparecer no ponto exato do clique.
1 parent df524d0 commit d471be3

2 files changed

Lines changed: 106 additions & 67 deletions

File tree

src/components/HtmlPreview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export function HtmlPreview({
239239

240240
<div
241241
ref={canvasRef}
242+
data-ve-anchor="preview-canvas"
242243
className={`relative flex flex-1 items-center justify-center overflow-hidden bg-[hsl(var(--preview-bg))]${visualEditsEnabled ? ' ve-border-pulse' : ''}`}
243244
>
244245
{menuOpen && (

src/components/VisualEditsMenu.tsx

Lines changed: 105 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2-
import { MousePointerClick } from 'lucide-react';
2+
import { MousePointerClick, X } from 'lucide-react';
33
import type { Contributor } from '@/lib/daxParser/types';
44

55
export 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-zA-Z][a-zA-Z0-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-zA-Z][a-zA-Z0-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 = /\bstyle\s*=\s*['"]\s*([a-zA-Z-]+)/.exec(rest);
65+
if (styleMatch) return styleMatch[1];
66+
67+
const attrMatch = /\s+([a-zA-Z][a-zA-Z0-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+
7586
export 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

183219
interface 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

Comments
 (0)