diff --git a/frontend/src/components/ComponentPickerModal.css b/frontend/src/components/ComponentPickerModal.css index f2e63a14..e8c1d420 100644 --- a/frontend/src/components/ComponentPickerModal.css +++ b/frontend/src/components/ComponentPickerModal.css @@ -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; } diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx index 83250b6a..92e1b9c9 100644 --- a/frontend/src/components/ComponentPickerModal.tsx +++ b/frontend/src/components/ComponentPickerModal.tsx @@ -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 = ({ if (!isOpen) return null; - return ( + // Portal to : 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(
e.stopPropagation()}> {/* Header */} @@ -312,7 +315,8 @@ export const ComponentPickerModal: React.FC = ({ )}
-
+ , + document.body ); }; diff --git a/frontend/src/components/simulator/BoardOnCanvas.tsx b/frontend/src/components/simulator/BoardOnCanvas.tsx index 1fba266c..01024747 100644 --- a/frontend/src/components/simulator/BoardOnCanvas.tsx +++ b/frontend/src/components/simulator/BoardOnCanvas.tsx @@ -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). +
{boardEl} {/* Active board highlight ring */} @@ -231,6 +235,6 @@ export const BoardOnCanvas = ({ wrapperOffsetY={0} zoom={zoom} /> - +
); }; diff --git a/frontend/src/components/simulator/BoardPickerModal.tsx b/frontend/src/components/simulator/BoardPickerModal.tsx index 9022f9b4..26b21f6a 100644 --- a/frontend/src/components/simulator/BoardPickerModal.tsx +++ b/frontend/src/components/simulator/BoardPickerModal.tsx @@ -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 : escape the canvas subtree so no ancestor stacking + // context can pin the dialog below floating panels (e.g. the AI chat). + return createPortal(
@@ -147,6 +151,7 @@ export const BoardPickerModal = ({ isOpen, onClose, onSelectBoard }: BoardPicker Cancel
- + , + document.body ); }; diff --git a/frontend/src/components/simulator/PinOverlay.tsx b/frontend/src/components/simulator/PinOverlay.tsx index 217ed16e..62fbee3c 100644 --- a/frontend/src/components/simulator/PinOverlay.tsx +++ b/frontend/src/components/simulator/PinOverlay.tsx @@ -122,7 +122,11 @@ export const PinOverlay: React.FC = ({ 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) => { diff --git a/frontend/src/components/simulator/SimulatorCanvas.css b/frontend/src/components/simulator/SimulatorCanvas.css index 53c52e67..24d63131 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.css +++ b/frontend/src/components/simulator/SimulatorCanvas.css @@ -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 ───────────────────────────────── */ diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 2bf6f0f0..f2558acd 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -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', + }} > { 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', + }} >