From b869587bbfaf93600fb019c5bd5621a34871f164 Mon Sep 17 00:00:00 2001
From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
Date: Fri, 21 Aug 2026 09:59:57 +0000
Subject: [PATCH 1/3] fix(configurator): size the shell by its container, not
the viewport
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The studio chrome decided its whole desktop-vs-mobile layout from the
browser viewport via Tailwind `md:` (@media 768px). When the configurator
is embedded in a container narrower than the viewport — the WP admin page
beside the ~160px admin menu, or the plugin's ~420px frontend overlay on a
desktop-width page — the viewport is still >=768px, so the full desktop
three-column layout (208px labelled rail + 360px panel + preview) was forced
into a box that could not hold it. Most visibly, SidebarNav rendered its
208px labelled rail inside the 420px overlay, squeezing the token panel to
~212px ('too narrow for its data').
Make the shell a CSS container (`@container`) and convert the layout-governing
breakpoints in App.svelte and SidebarNav.svelte from viewport `md:` to the
container variant `@3xl:` (48rem = 768px — the exact same threshold, now
measured against the space we're actually given). Standalone is unchanged
(container == viewport); embedded hosts now collapse to the compact rail /
mobile layout when they're narrow. Unlabelled panels/inputs were already
fluid, so no other files change.
Note: unnamed container queries don't fall back to the viewport, so a host
that mounts SidebarNav without an `@container` ancestor safely gets the
compact 56px rail.
---
configurator/src/App.svelte | 27 ++++++++++------
.../src/components/shell/SidebarNav.svelte | 31 +++++++++++++------
2 files changed, 39 insertions(+), 19 deletions(-)
diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte
index 38ec8a9d..291a72d7 100644
--- a/configurator/src/App.svelte
+++ b/configurator/src/App.svelte
@@ -354,7 +354,15 @@
{/snippet}
-
{ navigateTo(d); }}
@@ -408,7 +416,7 @@
on tool screens where the preview is hidden. On mobile it fills the row
(the icon rail already claims its own space). -->
{#each group.items as item (item.id)}
{@render navButton(item)}
{/each}
From b18c232d27d881a06d2476acff306fbcf89e45bb Mon Sep 17 00:00:00 2001
From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com>
Date: Fri, 21 Aug 2026 13:35:37 +0000
Subject: [PATCH 2/3] fix(configurator): close the mobile drawer when the shell
reaches desktop width
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Addresses review feedback on the container-query conversion: with the drawer
gated by `@3xl:hidden`, growing the shell past the 768px breakpoint only
CSS-hid an open category drawer — the aria-modal dialog stayed mounted and
use:drawerFocus never restored focus to the trigger. Observe the shell's own
box (the @container element) with a ResizeObserver and clear navDrawerOpen once
it reaches the desktop breakpoint, so the {#if} block unmounts, focus is
restored, and no hidden modal lingers. Matches the CSS threshold exactly
(DESKTOP_SHELL_PX = 768 = @3xl).
---
configurator/src/App.svelte | 28 +++++++++++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte
index 291a72d7..ab03f26d 100644
--- a/configurator/src/App.svelte
+++ b/configurator/src/App.svelte
@@ -53,6 +53,15 @@
let showPalette = $state(false);
// Mobile category drawer (replaces the cramped icon rail on narrow screens).
let navDrawerOpen = $state(false);
+ // The shell root — also our container-query context (see the wrapper below).
+ // A ResizeObserver on it reconciles the drawer when the *container* (not the
+ // viewport) crosses the desktop breakpoint: `@3xl:hidden` would otherwise only
+ // CSS-hide an open drawer, leaving a hidden aria-modal dialog mounted and its
+ // focus-restore (use:drawerFocus) un-run. Container width, not viewport, is
+ // what actually decides whether the persistent rail is showing.
+ let shellEl = $state(null);
+ // Keep in sync with the `@3xl` container breakpoint (48rem = 768px).
+ const DESKTOP_SHELL_PX = 768;
// When the drawer (a modal dialog) opens, move focus into it so keyboard users
// land inside the modal — the dialog's Escape handler then receives the event,
// and screen readers announce it. On close, focus returns to the trigger.
@@ -331,8 +340,25 @@
}
};
window.addEventListener("keydown", handler);
+
+ // Close the mobile drawer once the shell is wide enough to show the
+ // persistent rail. Without this, growing the container past the breakpoint
+ // (e.g. an embedded host resizing, or rotating to landscape) merely
+ // `@3xl:hidden`s an open drawer — the aria-modal dialog stays in the DOM and
+ // use:drawerFocus never restores focus to the trigger. Observing the shell's
+ // own box (our @container element) matches exactly what the CSS reacts to.
+ let ro: ResizeObserver | undefined;
+ if (shellEl && typeof ResizeObserver !== "undefined") {
+ ro = new ResizeObserver((entries) => {
+ const width = entries[0]?.contentRect.width ?? 0;
+ if (width >= DESKTOP_SHELL_PX && navDrawerOpen) navDrawerOpen = false;
+ });
+ ro.observe(shellEl);
+ }
+
return () => {
window.removeEventListener("keydown", handler);
+ ro?.disconnect();
if (saveStateTimer) clearTimeout(saveStateTimer);
if (importStatusTimer) clearTimeout(importStatusTimer);
};
@@ -362,7 +388,7 @@
desktop three-column layout into a box that can't hold it. The @3xl/… (48rem
= 768px) variants below query THIS element's width instead, so the same
768px threshold now measures the space we truly have. -->
-
+
Date: Fri, 21 Aug 2026 18:39:33 +0000
Subject: [PATCH 3/3] fix(configurator): rem-aware drawer breakpoint + keep
focus visible on close
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two review follow-ups on the drawer-reconcile logic:
- Derive the ResizeObserver threshold from the root font size (48rem, resolved
against document.documentElement) instead of a hardcoded 768px, so it tracks
the `@3xl` container breakpoint even when a host sets a non-16px root font
size (rem — and Tailwind container breakpoints — resolve against the root).
- When the breakpoint close hides the drawer, its original trigger is now
`@3xl:hidden` (offsetParent === null); restoring focus there would drop it to
. drawerFocus now restores to the trigger only if it's still visible,
otherwise moves focus to the visible desktop rail's current item.
---
configurator/src/App.svelte | 38 ++++++++++++++++++++++++++++++++-----
1 file changed, 33 insertions(+), 5 deletions(-)
diff --git a/configurator/src/App.svelte b/configurator/src/App.svelte
index ab03f26d..4c33c001 100644
--- a/configurator/src/App.svelte
+++ b/configurator/src/App.svelte
@@ -60,15 +60,43 @@
// focus-restore (use:drawerFocus) un-run. Container width, not viewport, is
// what actually decides whether the persistent rail is showing.
let shellEl = $state(null);
- // Keep in sync with the `@3xl` container breakpoint (48rem = 768px).
- const DESKTOP_SHELL_PX = 768;
+ // The desktop icon rail wrapper — used as a focus fallback when a breakpoint
+ // close hides the drawer's original trigger (see drawerFocus above).
+ let railEl = $state(null);
+ // Mirror of the `@3xl` container breakpoint, expressed in rem so it tracks the
+ // CSS even when a host sets a non-16px root font size. `rem` (and therefore
+ // Tailwind's container breakpoints) resolve against the *root* font size, so
+ // convert to px against that — a hardcoded 768 would drift from `@3xl` the
+ // moment `html { font-size }` isn't the 16px default.
+ const DESKTOP_SHELL_REM = 48;
+ function desktopShellPx(): number {
+ const root =
+ typeof document !== "undefined"
+ ? parseFloat(getComputedStyle(document.documentElement).fontSize)
+ : NaN;
+ return DESKTOP_SHELL_REM * (Number.isFinite(root) && root > 0 ? root : 16);
+ }
// When the drawer (a modal dialog) opens, move focus into it so keyboard users
// land inside the modal — the dialog's Escape handler then receives the event,
// and screen readers announce it. On close, focus returns to the trigger.
function drawerFocus(node: HTMLElement) {
const prev = document.activeElement as HTMLElement | null;
node.focus();
- return { destroy() { prev?.focus?.(); } };
+ return {
+ destroy() {
+ // Restore focus to whatever was focused before the drawer opened —
+ // normally the mobile trigger. But when the drawer closes because the
+ // shell reached desktop width (the ResizeObserver below), that trigger
+ // is now `@3xl:hidden` (offsetParent === null); focusing a hidden node
+ // drops focus to . In that case move focus to the visible desktop
+ // rail's current item instead so keyboard focus stays in the nav.
+ if (prev && prev.offsetParent !== null) { prev.focus?.(); return; }
+ const target =
+ railEl?.querySelector('[aria-current="page"]') ??
+ railEl?.querySelector("button");
+ target?.focus?.();
+ },
+ };
}
// Close on Escape and trap Tab/Shift+Tab inside the modal drawer so keyboard
// focus can't wander to the controls behind an aria-modal dialog.
@@ -351,7 +379,7 @@
if (shellEl && typeof ResizeObserver !== "undefined") {
ro = new ResizeObserver((entries) => {
const width = entries[0]?.contentRect.width ?? 0;
- if (width >= DESKTOP_SHELL_PX && navDrawerOpen) navDrawerOpen = false;
+ if (width >= desktopShellPx() && navDrawerOpen) navDrawerOpen = false;
});
ro.observe(shellEl);
}
@@ -430,7 +458,7 @@
-