sonance-brand-mcp 1.3.77 → 1.3.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,6 +64,7 @@ import {
64
64
  removePreviewStyles,
65
65
  injectPreviewStyles,
66
66
  extractPreviewCSSFromCode,
67
+ getActiveModalContent,
67
68
  } from "./utils";
68
69
  import { Section, ColorSwatch, SelectField } from "./components/common";
69
70
  import { InspectorOverlay } from "./components/InspectorOverlay";
@@ -126,6 +127,34 @@ export function SonanceDevTools() {
126
127
  const dragOffsetRef = useRef({ x: 0, y: 0 });
127
128
  const panelRef = useRef<HTMLDivElement>(null);
128
129
 
130
+ // Portal container state - ensures DevTool is always on top of modals
131
+ const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
132
+
133
+ // Create and manage dedicated portal container that stays on top
134
+ useEffect(() => {
135
+ // Create container if it doesn't exist
136
+ let container = document.getElementById('sonance-devtools-root');
137
+ if (!container) {
138
+ container = document.createElement('div');
139
+ container.id = 'sonance-devtools-root';
140
+ container.style.cssText = 'position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; isolation: isolate;';
141
+ document.body.appendChild(container);
142
+ }
143
+ setPortalContainer(container);
144
+
145
+ // MutationObserver to keep container as last child of body
146
+ // This ensures DevTool stays on top even when modals are dynamically added
147
+ const observer = new MutationObserver(() => {
148
+ if (container && container.nextSibling) {
149
+ document.body.appendChild(container);
150
+ }
151
+ });
152
+
153
+ observer.observe(document.body, { childList: true });
154
+
155
+ return () => observer.disconnect();
156
+ }, []);
157
+
129
158
  // Component-specific style overrides (for scalable, project-agnostic styling)
130
159
  // Key: component type (e.g., "card", "button-primary", "card:variant123"), Value: style overrides
131
160
  const [componentOverrides, setComponentOverrides] = useState<Record<string, ComponentStyle>>({});
@@ -767,14 +796,28 @@ export function SonanceDevTools() {
767
796
  return rect; // No scroll parent, use original rect
768
797
  };
769
798
 
799
+ // Helper: Detect active modal and filter elements to only those in the topmost layer
800
+ const activeModalContent = getActiveModalContent();
801
+ const isInActiveLayer = (el: Element): boolean => {
802
+ // Always exclude DevTools panel
803
+ if (el.closest("[data-sonance-devtools]")) return false;
804
+
805
+ // If a modal is active, only include elements inside the modal content
806
+ if (activeModalContent) {
807
+ return activeModalContent.contains(el) || el === activeModalContent;
808
+ }
809
+
810
+ return true;
811
+ };
812
+
770
813
  // Scan for tagged components
771
814
  if (inspectorEnabled || activeTab === "components") {
772
815
  // 1. Scan for explicitly tagged components
773
816
  const taggedComponents = document.querySelectorAll("[data-sonance-name]");
774
817
  taggedComponents.forEach((el) => {
775
- // Skip DevTools elements
776
- if (el.closest("[data-sonance-devtools]")) return;
777
-
818
+ // Skip elements outside the active layer (DevTools, behind modals)
819
+ if (!isInActiveLayer(el)) return;
820
+
778
821
  const name = el.getAttribute("data-sonance-name");
779
822
  if (name) {
780
823
  const rawRect = el.getBoundingClientRect();
@@ -820,9 +863,9 @@ export function SonanceDevTools() {
820
863
  Object.entries(genericSelectors).forEach(([genericName, selector]) => {
821
864
  const elements = document.querySelectorAll(selector);
822
865
  elements.forEach((el) => {
823
- // Skip DevTools elements
824
- if (el.closest("[data-sonance-devtools]")) return;
825
-
866
+ // Skip elements outside the active layer (DevTools, behind modals)
867
+ if (!isInActiveLayer(el)) return;
868
+
826
869
  const rawRect = el.getBoundingClientRect();
827
870
  const rect = getVisibleRect(el, rawRect);
828
871
  if (rect && rect.width > 0 && rect.height > 0) {
@@ -862,6 +905,9 @@ export function SonanceDevTools() {
862
905
  if (activeTab === "logos") {
863
906
  const images = document.querySelectorAll("img");
864
907
  images.forEach((img) => {
908
+ // Skip elements outside the active layer (DevTools, behind modals)
909
+ if (!isInActiveLayer(img)) return;
910
+
865
911
  const src = img.src || img.getAttribute("src") || "";
866
912
  const alt = img.alt || "";
867
913
 
@@ -916,9 +962,9 @@ export function SonanceDevTools() {
916
962
  const addedElements = new Set<Element>();
917
963
 
918
964
  textElements.forEach((el) => {
919
- // Skip elements that are part of the DevTools UI
920
- if (el.closest("[data-sonance-devtools]")) return;
921
-
965
+ // Skip elements outside the active layer (DevTools, behind modals)
966
+ if (!isInActiveLayer(el)) return;
967
+
922
968
  // Skip if this element is inside another text element we're already tracking
923
969
  // This prevents duplicate labels for nested structures
924
970
  const parentTextEl = el.parentElement?.closest("h1, h2, h3, h4, h5, h6, p, a, label, blockquote, figcaption, li");
@@ -2384,7 +2430,7 @@ export function SonanceDevTools() {
2384
2430
  <button
2385
2431
  onClick={() => setIsOpen(true)}
2386
2432
  className={cn(
2387
- "fixed bottom-6 right-6 z-[2147483646]",
2433
+ "fixed bottom-6 right-6 z-[2147483646] pointer-events-auto",
2388
2434
  "flex h-14 w-14 items-center justify-center",
2389
2435
  "rounded-full bg-[#333F48] text-white shadow-lg",
2390
2436
  "hover:scale-105 hover:shadow-xl",
@@ -2403,7 +2449,7 @@ export function SonanceDevTools() {
2403
2449
  ref={panelRef}
2404
2450
  data-sonance-devtools="true"
2405
2451
  className={cn(
2406
- "fixed bottom-6 right-6 z-[2147483647]",
2452
+ "fixed bottom-6 right-6 z-[2147483647] pointer-events-auto",
2407
2453
  "w-[360px] max-h-[80vh]",
2408
2454
  "bg-white rounded-lg shadow-2xl border border-gray-200",
2409
2455
  "flex flex-col overflow-hidden",
@@ -2637,6 +2683,9 @@ export function SonanceDevTools() {
2637
2683
  </div>
2638
2684
  );
2639
2685
 
2686
+ // Wait for portal container to be ready
2687
+ if (!portalContainer) return null;
2688
+
2640
2689
  return createPortal(
2641
2690
  <>
2642
2691
  {trigger}
@@ -2677,6 +2726,6 @@ export function SonanceDevTools() {
2677
2726
  />
2678
2727
  )}
2679
2728
  </>,
2680
- document.body
2729
+ portalContainer
2681
2730
  );
2682
2731
  }
@@ -329,6 +329,55 @@ export function extractPreviewCSSFromCode(
329
329
  return rules.join("\n");
330
330
  }
331
331
 
332
+ /**
333
+ * Finds the topmost modal/dialog content container if one is open.
334
+ * Returns the content element (not the backdrop) or null if no modal is active.
335
+ *
336
+ * Detection strategy:
337
+ * 1. Look for Radix UI dialogs with [data-state="open"]
338
+ * 2. Look for role="dialog" elements
339
+ * 3. Look for native <dialog open> elements
340
+ * 4. Look for custom Dialog with z-50 fixed positioning
341
+ * 5. Look for Drawer/Sheet overlays
342
+ */
343
+ export function getActiveModalContent(): Element | null {
344
+ if (typeof document === "undefined") return null;
345
+
346
+ // Priority-ordered list of modal content selectors
347
+ const contentSelectors = [
348
+ // Radix UI Dialog content
349
+ '[data-state="open"][role="dialog"]',
350
+ // Custom Dialog content (has data-sonance-name="dialog")
351
+ '.fixed.inset-0.z-50 [data-sonance-name="dialog"]',
352
+ '.fixed.inset-0.z-50 > .relative.z-10',
353
+ // Native dialog
354
+ 'dialog[open]',
355
+ // aria-modal content
356
+ '[aria-modal="true"]',
357
+ // Sheet/Drawer content
358
+ '[data-state="open"][data-sheet-content]',
359
+ '[data-state="open"][role="complementary"]',
360
+ ];
361
+
362
+ for (const selector of contentSelectors) {
363
+ const modal = document.querySelector(selector);
364
+ if (modal) {
365
+ // Return innermost dialog content, not the wrapper
366
+ return modal;
367
+ }
368
+ }
369
+
370
+ // Fallback: check for z-50 fixed full-screen overlays
371
+ const fixedOverlays = document.querySelectorAll('.fixed.inset-0.z-50');
372
+ for (const overlay of fixedOverlays) {
373
+ // Find the content within (skip if it's just a backdrop)
374
+ const content = overlay.querySelector('.relative.z-10, [data-sonance-name]');
375
+ if (content) return content;
376
+ }
377
+
378
+ return null;
379
+ }
380
+
332
381
  // Helper to check if a category should show scope options
333
382
  export function shouldShowScopeOptions(category: string): boolean {
334
383
  if (category === "all") return true; // Show for "all" selection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.77",
3
+ "version": "1.3.79",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",