fix(canvas): stack pins with their component + pickers above floating panels
Two stacking bugs: 1. Pin overlays used a global z-index 30 while component bodies sit at z 0-5, all in .canvas-world's single stacking context — so a covered component's pins painted on top of whatever covered it (arduino pins showing through a breadboard). Each component/board group is now a zero-size positioned wrapper that forms its own stacking context (boards z 0, components z 1, selected z 2): pins stay above their own body but are hidden together with it. .components-area becomes pointer-events: none so board pins/drag overlays (now trapped at z 0) keep receiving clicks through it; component groups re-enable their own. 2. The Add Component / board picker overlays (z 1000/2000) rendered behind the pro AI chat panel (z 8000). Both now portal to <body> at z 9000. Verified in-app: board drag, component drag, wire creation from board pin to LED, covered pins hidden (0/14 leak), covering component's pins still clickable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a2df942d74
commit
b6ce131b03
|
|
@ -12,7 +12,8 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
/* Above every floating panel, including the pro AI chat (8000/8001). */
|
||||
z-index: 9000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ComponentRegistry } from '../services/ComponentRegistry';
|
||||
import type { ComponentMetadata, ComponentCategory } from '../types/component-metadata';
|
||||
|
|
@ -153,7 +154,9 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
// Portal to <body>: the picker must escape the canvas subtree so no ancestor
|
||||
// stacking context can pin it below floating panels (e.g. the AI chat).
|
||||
return createPortal(
|
||||
<div className="component-picker-overlay" onClick={onClose}>
|
||||
<div className="component-picker-modal" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
|
|
@ -312,7 +315,8 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -158,7 +158,11 @@ export const BoardOnCanvas = ({
|
|||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
// Zero-size positioned wrapper: children keep their absolute canvas
|
||||
// coords, but board + pins now share ONE stacking context, so this
|
||||
// board's pins can never paint above a component/board covering it.
|
||||
// z 0 keeps every board below components (their groups use z 1/2).
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, zIndex: 0 }}>
|
||||
{boardEl}
|
||||
|
||||
{/* Active board highlight ring */}
|
||||
|
|
@ -231,6 +235,6 @@ export const BoardOnCanvas = ({
|
|||
wrapperOffsetY={0}
|
||||
zoom={zoom}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
|
|
@ -58,7 +59,9 @@ export const BoardPickerModal = ({ isOpen, onClose, onSelectBoard }: BoardPicker
|
|||
const { t } = useTranslation();
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
// Portal to <body>: escape the canvas subtree so no ancestor stacking
|
||||
// context can pin the dialog below floating panels (e.g. the AI chat).
|
||||
return createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
|
|
@ -67,7 +70,8 @@ export const BoardPickerModal = ({ isOpen, onClose, onSelectBoard }: BoardPicker
|
|||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2000,
|
||||
// Above every floating panel, including the pro AI chat (8000/8001).
|
||||
zIndex: 9000,
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
|
|
@ -147,6 +151,7 @@ export const BoardPickerModal = ({ isOpen, onClose, onSelectBoard }: BoardPicker
|
|||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -122,7 +122,11 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
|
|||
left: `${componentX + wrapperOffsetX}px`,
|
||||
top: `${componentY + wrapperOffsetY}px`,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 30, // Above wires (20) and components, below modals/dialogs (1000+)
|
||||
// Local to the owning component's stacking context (its wrapper in
|
||||
// SimulatorCanvas/BoardOnCanvas sets position + z-index): above the
|
||||
// component's own body/overlays only — a covering component hides
|
||||
// these pins along with the body.
|
||||
zIndex: 30,
|
||||
}}
|
||||
>
|
||||
{pins.map((pin, index) => {
|
||||
|
|
|
|||
|
|
@ -245,6 +245,11 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* Hit-test transparent: this full-canvas div sits later in the DOM than
|
||||
the boards, so with pointer events it would swallow clicks meant for
|
||||
board pins / board drag overlays (their z-index is local to each
|
||||
board's stacking context). Component groups re-enable their own. */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Zoom controls ───────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -1969,7 +1969,18 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
onMouseLeave={() =>
|
||||
setHoveredComponentId((curr) => (curr === component.id ? null : curr))
|
||||
}
|
||||
style={{ display: 'contents' }}
|
||||
// Zero-size positioned wrapper: children keep their absolute canvas
|
||||
// coords, but body + pins now share ONE stacking context, so this
|
||||
// component's pins can never paint above a component covering it.
|
||||
// pointerEvents re-enables hit-testing under .components-area's
|
||||
// pointer-events: none.
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: isSelected ? 2 : 1,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
<InstrumentComponent
|
||||
id={component.id}
|
||||
|
|
@ -2021,7 +2032,19 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
onMouseLeave={() =>
|
||||
setHoveredComponentId((curr) => (curr === component.id ? null : curr))
|
||||
}
|
||||
style={{ display: 'contents' }}
|
||||
// Zero-size positioned wrapper: children keep their absolute canvas
|
||||
// coords, but body + pins now share ONE stacking context, so this
|
||||
// component's pins can never paint above a component covering it.
|
||||
// Selected components (z 2) still raise above unselected ones (z 1).
|
||||
// pointerEvents re-enables hit-testing under .components-area's
|
||||
// pointer-events: none.
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: isSelected ? 2 : 1,
|
||||
pointerEvents: 'auto',
|
||||
}}
|
||||
>
|
||||
<DynamicComponent
|
||||
id={component.id}
|
||||
|
|
|
|||
Loading…
Reference in New Issue