From c98f2bdb1f406d15476ee2c3f63c2851d4cfe283 Mon Sep 17 00:00:00 2001
From: Jesse Baker <jesse.baker@acquia.com>
Date: Tue, 22 Apr 2025 13:59:49 +0100
Subject: [PATCH 1/6] Add client.css to astro build

---
 experience_builder.libraries.yml                   |  3 +++
 ui/lib/astro-hydration/README.md                   |  4 +++-
 ui/lib/astro-hydration/extract-hydration-script.js | 14 +++++++++++++-
 ui/lib/astro-hydration/src/pages/index.astro       |  1 +
 ui/lib/astro-hydration/src/styles/client.css       |  1 +
 5 files changed, 21 insertions(+), 2 deletions(-)
 create mode 100644 ui/lib/astro-hydration/src/styles/client.css

diff --git a/experience_builder.libraries.yml b/experience_builder.libraries.yml
index dd0e305094..2b01eb3c87 100644
--- a/experience_builder.libraries.yml
+++ b/experience_builder.libraries.yml
@@ -99,6 +99,9 @@ astro.client:
 astro.hydration:
   js:
     ui/lib/astro-hydration/dist/hydration.js: {}
+  css:
+    theme:
+      ui/lib/astro-hydration/dist/client.css: {}
   license:
     name: MIT
     url: https://raw.githubusercontent.com/withastro/astro/refs/heads/main/LICENSE
diff --git a/ui/lib/astro-hydration/README.md b/ui/lib/astro-hydration/README.md
index 3b7b0064fc..e98867fdf7 100644
--- a/ui/lib/astro-hydration/README.md
+++ b/ui/lib/astro-hydration/README.md
@@ -11,7 +11,9 @@ This directory is an Astro project and is defined as an [npm workspace](https://
 
 The build step `astro build` generates bundles for Preact (preact.module.js), hooks (hooks.module.js), etc. that the in-browser-editable components depend on.
 
-`client.js` and `hydration.js` are defined as libraries in `experience_builder.libraries.yml` to make them accessible to the JS component source plugin.
+`client.js`, `client.css`, and `hydration.js` are defined as libraries in `experience_builder.libraries.yml` to make them accessible to the JS component source plugin.
+
+The `client.css` file contains basic styling for Astro elements to ensure proper display using `display: contents`.
 
 ***The rest of the built files do not need to be defined as Drupal libraries because they are imported by `client.js`.**
 
diff --git a/ui/lib/astro-hydration/extract-hydration-script.js b/ui/lib/astro-hydration/extract-hydration-script.js
index a8cce89a22..c4fd40cb85 100644
--- a/ui/lib/astro-hydration/extract-hydration-script.js
+++ b/ui/lib/astro-hydration/extract-hydration-script.js
@@ -1,5 +1,6 @@
 /* Extract the <script/> contents from the Astro generated index.html file which contains
- * Astro's hydration code and write it to /astro-hydration/dist/hydration.js. */
+ * Astro's hydration code and write it to /astro-hydration/dist/hydration.js. 
+ * Also ensure client.css exists in the dist directory. */
 import fs from 'fs';
 import path from 'path';
 import { fileURLToPath } from 'url';
@@ -10,6 +11,9 @@ const __dirname = path.dirname(__filename);
 const indexPath = path.join(__dirname, 'dist', 'index.html');
 // Define the path for the output file
 const outputFilePath = path.join(__dirname, 'dist', 'hydration.js');
+// Define the path for the CSS file
+const cssSourcePath = path.join(__dirname, 'src', 'styles', 'client.css');
+const cssOutputPath = path.join(__dirname, 'dist', 'client.css');
 
 // Read the HTML file
 const html = fs.readFileSync(indexPath, 'utf8');
@@ -29,3 +33,11 @@ scriptContents = scriptContents.replace('{0:t=>', "{'raw':t=>t,0:t=>");
 
 // Write the script contents to hydration.js
 fs.writeFileSync(outputFilePath, scriptContents);
+
+// Check if client.css exists in the dist directory and copy from source if needed
+if (!fs.existsSync(cssOutputPath) || 
+    fs.readFileSync(cssSourcePath, 'utf8') !== fs.readFileSync(cssOutputPath, 'utf8')) {
+  // Copy the CSS file from source to dist
+  fs.copyFileSync(cssSourcePath, cssOutputPath);
+  console.log('Generated client.css in dist directory');
+}
diff --git a/ui/lib/astro-hydration/src/pages/index.astro b/ui/lib/astro-hydration/src/pages/index.astro
index c4bea6ccad..3baec753bd 100644
--- a/ui/lib/astro-hydration/src/pages/index.astro
+++ b/ui/lib/astro-hydration/src/pages/index.astro
@@ -1,4 +1,5 @@
 ---
 import Stub from '../components/Stub.jsx'
+import '../styles/client.css'
 ---
 <Stub client:only="preact"/>
diff --git a/ui/lib/astro-hydration/src/styles/client.css b/ui/lib/astro-hydration/src/styles/client.css
new file mode 100644
index 0000000000..e18bee310e
--- /dev/null
+++ b/ui/lib/astro-hydration/src/styles/client.css
@@ -0,0 +1 @@
+astro-island,astro-slot,astro-static-slot{display:contents}
\ No newline at end of file
-- 
GitLab


From d50161de573cb97a758af70f8246cec47fa84e32 Mon Sep 17 00:00:00 2001
From: Jesse Baker <jesse.baker@acquia.com>
Date: Tue, 22 Apr 2025 15:01:28 +0100
Subject: [PATCH 2/6] simplify claudes weirdly over complex check

---
 ui/lib/astro-hydration/extract-hydration-script.js | 11 ++---------
 1 file changed, 2 insertions(+), 9 deletions(-)

diff --git a/ui/lib/astro-hydration/extract-hydration-script.js b/ui/lib/astro-hydration/extract-hydration-script.js
index c4fd40cb85..8de45f7e81 100644
--- a/ui/lib/astro-hydration/extract-hydration-script.js
+++ b/ui/lib/astro-hydration/extract-hydration-script.js
@@ -1,5 +1,5 @@
 /* Extract the <script/> contents from the Astro generated index.html file which contains
- * Astro's hydration code and write it to /astro-hydration/dist/hydration.js. 
+ * Astro's hydration code and write it to /astro-hydration/dist/hydration.js.
  * Also ensure client.css exists in the dist directory. */
 import fs from 'fs';
 import path from 'path';
@@ -33,11 +33,4 @@ scriptContents = scriptContents.replace('{0:t=>', "{'raw':t=>t,0:t=>");
 
 // Write the script contents to hydration.js
 fs.writeFileSync(outputFilePath, scriptContents);
-
-// Check if client.css exists in the dist directory and copy from source if needed
-if (!fs.existsSync(cssOutputPath) || 
-    fs.readFileSync(cssSourcePath, 'utf8') !== fs.readFileSync(cssOutputPath, 'utf8')) {
-  // Copy the CSS file from source to dist
-  fs.copyFileSync(cssSourcePath, cssOutputPath);
-  console.log('Generated client.css in dist directory');
-}
+fs.copyFileSync(cssSourcePath, cssOutputPath);
-- 
GitLab


From b43fc4cfebdf0e673e140467399721bafd9cdbaf Mon Sep 17 00:00:00 2001
From: Jesse Baker <jesse.baker@acquia.com>
Date: Tue, 22 Apr 2025 16:03:47 +0100
Subject: [PATCH 3/6] Implement MR suggestions

---
 experience_builder.libraries.yml                         | 2 +-
 ui/lib/astro-hydration/extract-hydration-script.js       | 7 +------
 ui/lib/astro-hydration/{src => public}/styles/client.css | 0
 ui/lib/astro-hydration/src/pages/index.astro             | 1 -
 4 files changed, 2 insertions(+), 8 deletions(-)
 rename ui/lib/astro-hydration/{src => public}/styles/client.css (100%)

diff --git a/experience_builder.libraries.yml b/experience_builder.libraries.yml
index 2b01eb3c87..0dbe663df8 100644
--- a/experience_builder.libraries.yml
+++ b/experience_builder.libraries.yml
@@ -101,7 +101,7 @@ astro.hydration:
     ui/lib/astro-hydration/dist/hydration.js: {}
   css:
     theme:
-      ui/lib/astro-hydration/dist/client.css: {}
+      ui/lib/astro-hydration/dist/styles/client.css: {}
   license:
     name: MIT
     url: https://raw.githubusercontent.com/withastro/astro/refs/heads/main/LICENSE
diff --git a/ui/lib/astro-hydration/extract-hydration-script.js b/ui/lib/astro-hydration/extract-hydration-script.js
index 8de45f7e81..a8cce89a22 100644
--- a/ui/lib/astro-hydration/extract-hydration-script.js
+++ b/ui/lib/astro-hydration/extract-hydration-script.js
@@ -1,6 +1,5 @@
 /* Extract the <script/> contents from the Astro generated index.html file which contains
- * Astro's hydration code and write it to /astro-hydration/dist/hydration.js.
- * Also ensure client.css exists in the dist directory. */
+ * Astro's hydration code and write it to /astro-hydration/dist/hydration.js. */
 import fs from 'fs';
 import path from 'path';
 import { fileURLToPath } from 'url';
@@ -11,9 +10,6 @@ const __dirname = path.dirname(__filename);
 const indexPath = path.join(__dirname, 'dist', 'index.html');
 // Define the path for the output file
 const outputFilePath = path.join(__dirname, 'dist', 'hydration.js');
-// Define the path for the CSS file
-const cssSourcePath = path.join(__dirname, 'src', 'styles', 'client.css');
-const cssOutputPath = path.join(__dirname, 'dist', 'client.css');
 
 // Read the HTML file
 const html = fs.readFileSync(indexPath, 'utf8');
@@ -33,4 +29,3 @@ scriptContents = scriptContents.replace('{0:t=>', "{'raw':t=>t,0:t=>");
 
 // Write the script contents to hydration.js
 fs.writeFileSync(outputFilePath, scriptContents);
-fs.copyFileSync(cssSourcePath, cssOutputPath);
diff --git a/ui/lib/astro-hydration/src/styles/client.css b/ui/lib/astro-hydration/public/styles/client.css
similarity index 100%
rename from ui/lib/astro-hydration/src/styles/client.css
rename to ui/lib/astro-hydration/public/styles/client.css
diff --git a/ui/lib/astro-hydration/src/pages/index.astro b/ui/lib/astro-hydration/src/pages/index.astro
index 3baec753bd..c4bea6ccad 100644
--- a/ui/lib/astro-hydration/src/pages/index.astro
+++ b/ui/lib/astro-hydration/src/pages/index.astro
@@ -1,5 +1,4 @@
 ---
 import Stub from '../components/Stub.jsx'
-import '../styles/client.css'
 ---
 <Stub client:only="preact"/>
-- 
GitLab


From f079bbfe72a7618bd706ee3f22880c375311d9c1 Mon Sep 17 00:00:00 2001
From: Jesse Baker <jesse.baker@acquia.com>
Date: Thu, 24 Apr 2025 16:48:41 +0100
Subject: [PATCH 4/6] Refactor several functions to better take into
 consideration astro-* elements

---
 .../layout/preview/DataToHtmlMapContext.tsx   |   6 +-
 .../previewOverlay/ComponentOverlay.tsx       |   5 +-
 .../useRenderPreviewEmptyRegionPlaceholder.ts |   2 +-
 .../useRenderPreviewEmptySlotPlaceholders.ts  |   2 +-
 ui/src/hooks/useSyncPreviewElementSize.ts     |  63 ++--------
 .../{AnnotationMaps.ts => Annotations.ts}     |  20 +++-
 ui/src/utils/function-utils.ts                | 112 ++++++++++++++++--
 7 files changed, 129 insertions(+), 81 deletions(-)
 rename ui/src/types/{AnnotationMaps.ts => Annotations.ts} (85%)

diff --git a/ui/src/features/layout/preview/DataToHtmlMapContext.tsx b/ui/src/features/layout/preview/DataToHtmlMapContext.tsx
index c9e6758689..3c9a07e610 100644
--- a/ui/src/features/layout/preview/DataToHtmlMapContext.tsx
+++ b/ui/src/features/layout/preview/DataToHtmlMapContext.tsx
@@ -1,11 +1,7 @@
 import type { ReactNode } from 'react';
 import { createContext, useContext, useState, useCallback } from 'react';
 
-import type {
-  ComponentsMap,
-  RegionsMap,
-  SlotsMap,
-} from '@/types/AnnotationMaps';
+import type { ComponentsMap, RegionsMap, SlotsMap } from '@/types/Annotations';
 
 interface ComponentHtmlMapProviderProps {
   children: ReactNode;
diff --git a/ui/src/features/layout/previewOverlay/ComponentOverlay.tsx b/ui/src/features/layout/previewOverlay/ComponentOverlay.tsx
index 45b796edd3..4984f724eb 100644
--- a/ui/src/features/layout/previewOverlay/ComponentOverlay.tsx
+++ b/ui/src/features/layout/previewOverlay/ComponentOverlay.tsx
@@ -27,7 +27,7 @@ import { useNavigationUtils } from '@/hooks/useNavigationUtils';
 import useXbParams from '@/hooks/useXbParams';
 import ComponentDropZone from '@/features/layout/previewOverlay/ComponentDropZone';
 import { useDraggable } from '@dnd-kit/core';
-import type { StackDirection } from '@/types/AnnotationMaps';
+import type { StackDirection } from '@/types/Annotations';
 
 export interface ComponentOverlayProps {
   component: ComponentNode;
@@ -110,8 +110,7 @@ const ComponentOverlay: React.FC<ComponentOverlayProps> = (props) => {
         const newOffsets = {
           ...getDistanceBetweenElements(
             parentElementInsideIframe,
-            // @todo Potential bug: an element amongst the elementsInsideIframe array other than the first could be further to the top/left than the first element.
-            elementsInsideIframe.current[0],
+            elementsInsideIframe.current,
           ),
         };
 
diff --git a/ui/src/hooks/useRenderPreviewEmptyRegionPlaceholder.ts b/ui/src/hooks/useRenderPreviewEmptyRegionPlaceholder.ts
index 055756cb90..288ccf6785 100644
--- a/ui/src/hooks/useRenderPreviewEmptyRegionPlaceholder.ts
+++ b/ui/src/hooks/useRenderPreviewEmptyRegionPlaceholder.ts
@@ -1,7 +1,7 @@
 import { useEffect, useCallback } from 'react';
 import { useAppSelector } from '@/app/hooks';
 import { selectLayout } from '@/features/layout/layoutModelSlice';
-import type { RegionsMap } from '@/types/AnnotationMaps';
+import type { RegionsMap } from '@/types/Annotations';
 import { DEFAULT_REGION } from '@/features/ui/uiSlice';
 
 /**
diff --git a/ui/src/hooks/useRenderPreviewEmptySlotPlaceholders.ts b/ui/src/hooks/useRenderPreviewEmptySlotPlaceholders.ts
index b23c14ea47..f82ad8dc74 100644
--- a/ui/src/hooks/useRenderPreviewEmptySlotPlaceholders.ts
+++ b/ui/src/hooks/useRenderPreviewEmptySlotPlaceholders.ts
@@ -2,7 +2,7 @@ import { useEffect, useCallback } from 'react';
 import { useAppSelector } from '@/app/hooks';
 import { selectLayout } from '@/features/layout/layoutModelSlice';
 import { findSlotById } from '@/features/layout/layoutUtils';
-import type { SlotsMap, SlotInfo } from '@/types/AnnotationMaps';
+import type { SlotsMap, SlotInfo } from '@/types/Annotations';
 
 /**
  * This hook renders a placeholder div in each empty component slot on the page in the preview iFrame.
diff --git a/ui/src/hooks/useSyncPreviewElementSize.ts b/ui/src/hooks/useSyncPreviewElementSize.ts
index 04fa8ac7bc..8f6c9cba7f 100644
--- a/ui/src/hooks/useSyncPreviewElementSize.ts
+++ b/ui/src/hooks/useSyncPreviewElementSize.ts
@@ -1,4 +1,5 @@
 import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
+import { calculateBoundingRect, elemIsVisible } from '@/utils/function-utils';
 
 /**
  * This hook takes an HTML element or array of HTML elements and returns a state containing the elements' dimensions and position.
@@ -13,14 +14,6 @@ interface Rect {
   height: number;
 }
 
-function elemIsVisible(elem: HTMLElement) {
-  return !!(
-    elem.offsetWidth ||
-    elem.offsetHeight ||
-    elem.getClientRects().length
-  );
-}
-
 function findParentBody(element: HTMLElement) {
   let currentElement = element;
 
@@ -39,28 +32,14 @@ function findParentBody(element: HTMLElement) {
 function isElementObservable(element: HTMLElement) {
   const style = window.getComputedStyle(element);
 
-  // Check if the element is inline - inline elements to not fire resize events.
-  if (style.display === 'inline') {
+  // display: contents; elements (e.g. <astro-* />) to not fire resize events.
+  if (style.display === 'contents') {
     return false;
   }
 
   return elemIsVisible(element);
 }
 
-function getMaxOfArray(numArray: number[]) {
-  if (numArray.length === 0) {
-    return 0;
-  }
-  return Math.max.apply(null, numArray);
-}
-
-function getMinOfArray(numArray: number[]) {
-  if (numArray.length === 0) {
-    return 0;
-  }
-  return Math.min.apply(null, numArray);
-}
-
 function useSyncPreviewElementSize(input: HTMLElement[] | HTMLElement | null) {
   // Normalize the input to always be an array
   const elements = useMemo(() => {
@@ -82,40 +61,12 @@ function useSyncPreviewElementSize(input: HTMLElement[] | HTMLElement | null) {
   const elementsRef = useRef<HTMLElement[] | null>(null);
 
   const recalculateBorder = useCallback(() => {
-    const tops: number[] = [];
-    const lefts: number[] = [];
-    const rights: number[] = [];
-    const bottoms: number[] = [];
-
-    elementsRef.current?.forEach((el) => {
-      if (!el) {
-        return;
-      }
-      const rect = el.getBoundingClientRect();
-
-      // check the element is actually visible on the page - otherwise we end up incorrectly setting the minTop & minLeft to be 0 for hidden elements.
-      if (elemIsVisible(el)) {
-        tops.push(rect.top);
-        lefts.push(rect.left);
-        rights.push(rect.left + rect.width);
-        bottoms.push(rect.top + rect.height);
-      }
-    });
-
-    const minTop = getMinOfArray(tops);
-    const minLeft = getMinOfArray(lefts);
-    const newRect = {
-      top: minTop,
-      left: minLeft,
-      width: getMaxOfArray(rights) - minLeft,
-      height: getMaxOfArray(bottoms) - minTop,
-    };
+    const elements = elementsRef.current;
+    const newRect = calculateBoundingRect(elements);
 
-    if (elementsRef.current) {
+    if (newRect && elements) {
       requestAnimationFrame(() => {
         setElementRect((prevRect) => {
-          // Only update if the values have changed so the hook returns the same object preventing components that use
-          // it from re-rendering
           if (
             prevRect.top !== newRect.top ||
             prevRect.left !== newRect.left ||
@@ -165,7 +116,7 @@ function useSyncPreviewElementSize(input: HTMLElement[] | HTMLElement | null) {
 
     elementsRef.current?.forEach((element) => {
       /**
-       * <astro-island> elements (XB Code Components) are display: inline; and that means you can't observe them with
+       * <astro-island> elements (XB Code Components) are display: contents; and that means you can't observe them with
        * resizeObserver. Here, if the element we're syncing with can't be observed we traverse up the DOM to find the
        * first parent that can be and watch that instead
        */
diff --git a/ui/src/types/AnnotationMaps.ts b/ui/src/types/Annotations.ts
similarity index 85%
rename from ui/src/types/AnnotationMaps.ts
rename to ui/src/types/Annotations.ts
index 82c8990b60..64fb4c157c 100644
--- a/ui/src/types/AnnotationMaps.ts
+++ b/ui/src/types/Annotations.ts
@@ -2,11 +2,19 @@ export interface RegionInfo {
   elements: HTMLElement[];
   regionId: string;
 }
+
 export interface ComponentInfo {
   elements: HTMLElement[];
   componentUuid: string;
 }
 
+export interface SlotInfo {
+  element: HTMLElement;
+  componentUuid: string;
+  slotName: string;
+  stackDirection: StackDirection;
+}
+
 export type StackDirection =
   | 'vertical'
   | 'vertical-grid'
@@ -14,12 +22,12 @@ export type StackDirection =
   | 'horizontal-flex'
   | 'horizontal-grid';
 
-export interface SlotInfo {
-  element: HTMLElement;
-  componentUuid: string;
-  slotName: string;
-  stackDirection: StackDirection;
-}
+export type BoundingRect = {
+  top: number;
+  left: number;
+  width: number;
+  height: number;
+};
 
 export type RegionsMap = Record<string, RegionInfo>;
 export type ComponentsMap = Record<string, ComponentInfo>;
diff --git a/ui/src/utils/function-utils.ts b/ui/src/utils/function-utils.ts
index 9c2974427f..2df54f780f 100644
--- a/ui/src/utils/function-utils.ts
+++ b/ui/src/utils/function-utils.ts
@@ -4,7 +4,8 @@ import type {
   ComponentsMap,
   RegionsMap,
   StackDirection,
-} from '@/types/AnnotationMaps';
+  BoundingRect,
+} from '@/types/Annotations';
 import type { PendingChanges } from '@/services/pendingChangesApi';
 
 export function handleNonWorkingBtn(): void {
@@ -46,18 +47,25 @@ export function parseValue(
 }
 
 /**
- * Calculates the horizontal and vertical distance between two DOM elements.
+ * Calculates the horizontal and vertical distance between the first and second passed element||group of elements.
  *
- * @param el1 - The first DOM element.
- * @param el2 - The second DOM element.
+ * @param el1 - The first element or array of elements.
+ * @param el2 - The second element or array of elements.
  * @returns An object containing the horizontal and vertical distances between the elements.
  */
 export function getDistanceBetweenElements(
-  el1: Element,
-  el2: Element,
+  el1: HTMLElement | HTMLElement[],
+  el2: HTMLElement | HTMLElement[],
 ): { horizontalDistance: number; verticalDistance: number } {
-  const rect1 = el1.getBoundingClientRect();
-  const rect2 = el2.getBoundingClientRect();
+  const rect1 = calculateBoundingRect(el1);
+  const rect2 = calculateBoundingRect(el2);
+
+  if (rect1 === null || rect2 === null) {
+    return {
+      horizontalDistance: 0,
+      verticalDistance: 0,
+    };
+  }
 
   // Calculate the horizontal and vertical distances
   const dx = rect2.left - rect1.left;
@@ -69,6 +77,89 @@ export function getDistanceBetweenElements(
   };
 }
 
+/**
+ * Calculates the bounding rectangle that encompasses all the provided elements.
+ *
+ * @param elements - A single DOM element or an array of DOM elements.
+ * @returns The bounding rectangle with properties: top, left, width, and height.
+ *          Returns null if elements are not provided or invalid.
+ */
+export function calculateBoundingRect(
+  elements: HTMLElement | HTMLElement[] | null,
+): BoundingRect | null {
+  if (!elements) {
+    return null;
+  }
+
+  const elementsArray = Array.isArray(elements) ? elements : [elements];
+  const expandedElements: HTMLElement[] = [];
+
+  elementsArray.forEach((el) => {
+    if (!el) {
+      return;
+    }
+
+    // elements that are display: contents; (e.g <astro-*> elements) take on the size of their children so in that case,
+    // we add the direct children to the array instead.
+    const style = window.getComputedStyle(el);
+    if (style.display === 'contents') {
+      expandedElements.push(
+        ...Array.from(el.children).filter(
+          (child): child is HTMLElement => child.nodeType === Node.ELEMENT_NODE,
+        ),
+      );
+    } else {
+      expandedElements.push(el);
+    }
+  });
+
+  const tops: number[] = [];
+  const lefts: number[] = [];
+  const rights: number[] = [];
+  const bottoms: number[] = [];
+
+  expandedElements.forEach((el) => {
+    const rect = el.getBoundingClientRect();
+    if (elemIsVisible(el)) {
+      tops.push(rect.top);
+      lefts.push(rect.left);
+      rights.push(rect.left + rect.width);
+      bottoms.push(rect.top + rect.height);
+    }
+  });
+
+  const minTop = getMinOfArray(tops);
+  const minLeft = getMinOfArray(lefts);
+  return {
+    top: minTop,
+    left: minLeft,
+    width: getMaxOfArray(rights) - minLeft,
+    height: getMaxOfArray(bottoms) - minTop,
+  };
+}
+
+export function elemIsVisible(elem: HTMLElement) {
+  return !!(
+    elem.offsetWidth ||
+    elem.offsetHeight ||
+    elem.getClientRects().length
+  );
+}
+
+export function getMaxOfArray(numArray: number[]) {
+  if (numArray.length === 0) {
+    return 0;
+  }
+  return Math.max.apply(null, numArray);
+}
+
+export function getMinOfArray(numArray: number[]) {
+  if (numArray.length === 0) {
+    return 0;
+  }
+  return Math.min.apply(null, numArray);
+}
+
 /**
  * Finds empty slots and inserts a placeholder div in between the xb-slot-start/xb-slot-end comments to show the user
  * where they can drop things.
@@ -397,7 +488,10 @@ export function findInChanges(
 }
 
 function getStackingDirection(container: HTMLElement): StackDirection {
-  const style = getComputedStyle(container);
+  let style = getComputedStyle(container);
+  if (style.display === 'contents' && container.parentElement) {
+    style = getComputedStyle(container.parentElement);
+  }
   const display = style.display;
 
   if (display.includes('flex')) {
-- 
GitLab


From ea91318b7cf0f8f3ec8225b8cdba65ba26ca0bd9 Mon Sep 17 00:00:00 2001
From: Jesse Baker <jesse.baker@acquia.com>
Date: Fri, 25 Apr 2025 10:53:37 +0100
Subject: [PATCH 5/6] rename css, added header property to library yml

---
 experience_builder.libraries.yml                               | 3 ++-
 .../public/styles/{client.css => hydration.css}                | 0
 2 files changed, 2 insertions(+), 1 deletion(-)
 rename ui/lib/astro-hydration/public/styles/{client.css => hydration.css} (100%)

diff --git a/experience_builder.libraries.yml b/experience_builder.libraries.yml
index 0dbe663df8..649a41cd3e 100644
--- a/experience_builder.libraries.yml
+++ b/experience_builder.libraries.yml
@@ -97,11 +97,12 @@ astro.client:
     gpl-compatible: true
 
 astro.hydration:
+  header: true
   js:
     ui/lib/astro-hydration/dist/hydration.js: {}
   css:
     theme:
-      ui/lib/astro-hydration/dist/styles/client.css: {}
+      ui/lib/astro-hydration/dist/styles/hydration.css: {}
   license:
     name: MIT
     url: https://raw.githubusercontent.com/withastro/astro/refs/heads/main/LICENSE
diff --git a/ui/lib/astro-hydration/public/styles/client.css b/ui/lib/astro-hydration/public/styles/hydration.css
similarity index 100%
rename from ui/lib/astro-hydration/public/styles/client.css
rename to ui/lib/astro-hydration/public/styles/hydration.css
-- 
GitLab


From 7aa9935476aba8998fd8014575d8b505a103150d Mon Sep 17 00:00:00 2001
From: Jesse Baker <jesse.baker@acquia.com>
Date: Mon, 28 Apr 2025 10:51:15 +0100
Subject: [PATCH 6/6] typo

---
 ui/src/hooks/useSyncPreviewElementSize.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/src/hooks/useSyncPreviewElementSize.ts b/ui/src/hooks/useSyncPreviewElementSize.ts
index 8f6c9cba7f..265be7775a 100644
--- a/ui/src/hooks/useSyncPreviewElementSize.ts
+++ b/ui/src/hooks/useSyncPreviewElementSize.ts
@@ -32,7 +32,7 @@ function findParentBody(element: HTMLElement) {
 function isElementObservable(element: HTMLElement) {
   const style = window.getComputedStyle(element);
 
-  // display: contents; elements (e.g. <astro-* />) to not fire resize events.
+  // display: contents; elements (e.g. <astro-* />) do not fire resize events.
   if (style.display === 'contents') {
     return false;
   }
-- 
GitLab