fix(ui): the inspector remounted its preview on every render, so it flickered

The preview effect still depended on componentMetadata BY IDENTITY. For
components that is stable (the registry hands back the same object), but for
BOARDS the canvas builds the metadata inline per render, so every parent
re-render - which a pointer moving over the canvas causes - tore the preview
element down and rebuilt it. The dialog visibly resized and snapped back.

It now keys on a digest of the metadata (id, tagName, defaults) and reads the
object through a ref, joining propsKey and pinsKey which were fixed the same
way earlier. Nothing rebuilds unless something real changed.
This commit is contained in:
David Montero Crespo 2026-08-04 04:03:45 +02:00
parent d74df3323f
commit a1307f117d
1 changed files with 17 additions and 3 deletions

View File

@ -188,6 +188,17 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
.map(([k, v]) => `${k}=${typeof v === 'string' && v.length > 32 ? v.length : String(v)}`)
.join('|');
const pinsKey = pinInfo.map((p) => `${p.name}:${p.x},${p.y}`).join('|');
// componentMetadata is stable for components (the registry hands back the
// same object) but NOT for boards, whose metadata the canvas builds inline
// per render — depending on its identity remounted the preview on every
// parent render and the dialog flickered under the pointer.
const metaKey = `${componentMetadata.id}|${componentMetadata.tagName}|${Object.entries(
componentMetadata.defaultValues ?? {},
)
.map(([k, v]) => `${k}=${String(v).slice(0, 24)}`)
.join(',')}`;
const metaRef = useRef(componentMetadata);
metaRef.current = componentMetadata;
// ── Live preview: mount the real element, measure, lay out pins ──────────
useEffect(() => {
@ -197,7 +208,7 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
setLayout(null);
host.textContent = '';
const tag = previewTagName || componentMetadata.tagName;
const tag = previewTagName || metaRef.current.tagName;
if (!tag || !customElements.get(tag)) {
// Unregistered tag (or none): the header thumbnail already shows the
// art, so the preview just reports there is nothing live to mount.
@ -211,7 +222,7 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
// art and, for flip, the pinInfo itself), then the metadata defaults for
// whatever the instance does not carry.
for (const [k, v] of Object.entries({
...componentMetadata.defaultValues,
...metaRef.current.defaultValues,
...componentProperties,
})) {
try {
@ -267,9 +278,12 @@ export const PartInspectorDialog: React.FC<PartInspectorDialogProps> = ({
// (a colour edit, an LED flip) changes the digest and repaints; a bare
// re-render does not.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [componentMetadata, propsKey, pinsKey, previewTagName]);
}, [metaKey, propsKey, pinsKey, previewTagName]);
// ── Position: viewport coords, clamped, portaled to body ─────────────────
// NOTE the guard inside place(): setDialogPos only fires on a real move.
// Without it the ResizeObserver below and this effect feed each other and
// the dialog jitters.
// Re-run on every SIZE change, not just on the inputs we can name. The
// datasheet arrives asynchronously (loadDoc) and the markdown can be long,
// so a one-shot measure placed the dialog while it was still short and it