From 798507fb55ef4262d380283ef96e032fa1ebb2ed Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Thu, 17 Sep 2026 16:42:36 -0400 Subject: [PATCH 01/31] Improve Plugin Guide and Theme Preview on narrow screens --- packages/plugin-api-map/src/product-map.tsx | 37 ++++++++++++++++---- packages/plugin-api-map/src/surface-card.tsx | 10 +++--- plugins/plugin-api-docs/app.tsx | 2 +- plugins/theme-preview/app.test.tsx | 8 ++--- plugins/theme-preview/app.tsx | 28 +++++++-------- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/packages/plugin-api-map/src/product-map.tsx b/packages/plugin-api-map/src/product-map.tsx index 7bdf462858..33d68fb08e 100644 --- a/packages/plugin-api-map/src/product-map.tsx +++ b/packages/plugin-api-map/src/product-map.tsx @@ -234,7 +234,7 @@ function SpatialFixture({ const cardFootprint = flowCard ? Math.max(probeReserve, cardReserveRef.current) : 0; - const availableHeight = viewport + const availableHeight = viewport && frame.clientWidth >= 640 ? viewport.clientHeight - (frame.getBoundingClientRect().top - viewport.getBoundingClientRect().top + @@ -298,12 +298,12 @@ function SpatialFixture({ ref={frameRef} data-guide-responsive-strategy="scale-together" data-guide-scale={geometry.scale.toFixed(4)} - className="w-full overflow-x-clip transition-[height] duration-300 ease-out" + className="w-full overflow-x-clip" style={scaled ? { height: geometry.height ?? undefined } : undefined} >
@@ -544,6 +544,7 @@ export function ProductMap({ }; const onKeyDown = (event: KeyboardEvent) => { + if (event.target instanceof HTMLSelectElement) return; if (event.key === "ArrowRight") { event.preventDefault(); show(index + 1); @@ -591,7 +592,7 @@ export function ProductMap({ const target = event.target; if (!(target instanceof Element)) return; if (target.closest('[role="dialog"]')) return; - if (target.closest('a[href^="#surface-"]')) return; + if (target.closest('a[href^="#surface-"], [data-guide-annotation-picker]')) return; card.close(); }; scope.addEventListener("pointerdown", onPointerDown); @@ -601,7 +602,7 @@ export function ProductMap({ return ( -
+
show(slideIndex)} aria-current={slideIndex === index ? "true" : undefined} className={cn( - "cursor-pointer whitespace-nowrap rounded-md px-2.5 py-1 text-xs transition-colors", + "cursor-pointer whitespace-nowrap rounded-md px-2.5 py-2.5 text-sm @2xl/guide:py-1 @2xl/guide:text-xs transition-colors", FOCUS_RING_CLASS, slideIndex === index ? "bg-surface-selected text-foreground" @@ -702,6 +703,28 @@ export function ProductMap({
+ {slides[index].id !== "headless" ? ( + + ) : null} + {cardNode ? (
{number === null ? ( @@ -156,7 +156,7 @@ export function SurfaceCard({ disabled={!target} aria-label={label} title={label} - className={`inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-state-hover hover:text-foreground disabled:cursor-default disabled:opacity-35 disabled:hover:bg-transparent disabled:hover:text-muted-foreground ${FOCUS_RING_CLASS}`} + className={`inline-flex size-9 @2xl/guide:size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-state-hover hover:text-foreground disabled:cursor-default disabled:opacity-35 disabled:hover:bg-transparent disabled:hover:text-muted-foreground ${FOCUS_RING_CLASS}`} > @@ -169,7 +169,7 @@ export function SurfaceCard({ onClick={onDismiss} aria-label="Close" title="Close annotation" - className={`inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-state-hover hover:text-foreground ${FOCUS_RING_CLASS}`} + className={`inline-flex size-9 @2xl/guide:size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-state-hover hover:text-foreground ${FOCUS_RING_CLASS}`} > @@ -187,7 +187,7 @@ export function SurfaceCard({ {(surface.firstParty && surface.firstParty.length > 0) || onCopyForAgent ? ( -
+
{surface.firstParty && surface.firstParty.length > 0 ? ( <> @@ -231,7 +231,7 @@ export function SurfaceCard({ type="button" onClick={() => void copyForAgent()} disabled={copyState === "copying"} - className={`ml-auto inline-flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-state-hover hover:text-foreground disabled:cursor-wait disabled:opacity-60 ${FOCUS_RING_CLASS}`} + className={`ml-auto inline-flex h-9 @2xl/guide:h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs font-medium text-muted-foreground transition-colors hover:bg-state-hover hover:text-foreground disabled:cursor-wait disabled:opacity-60 ${FOCUS_RING_CLASS}`} > { expect(dark.getAttribute("aria-pressed")).toBe("false"); }); - it("restacks the main areas on mobile with the read-only style sheet last", async () => { - const width = vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(480); + it.each([390, 600, 1199])("restacks the main areas at %ipx with the read-only style sheet last", async (panelWidth) => { + const width = vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(panelWidth); try { renderPreview({ themeCatalog: () => DEFAULT_CATALOG, setTheme: () => DEFAULT_CATALOG, }); - await waitFor(() => expect(document.querySelector("[data-tp-band=mobile]")).not.toBeNull()); + await waitFor(() => expect(document.querySelector(`[data-tp-band=${panelWidth < 600 ? "mobile" : "narrow"}]`)).not.toBeNull()); expect(screen.queryByRole("button", { name: /full style guide/i })).toBeNull(); - // The compact interaction areas stay together before the style sheet. + expect(document.querySelector("[data-tp-section=rail]")).toBeNull(); const areas = [...document.querySelectorAll("[data-tp-area]")].map((el) => el.getAttribute("data-tp-area")); expect(areas).toEqual(["mock", "overlays", "components", "stylesheet"]); expect(document.querySelector("[data-tp-style-readonly]")).not.toBeNull(); diff --git a/plugins/theme-preview/app.tsx b/plugins/theme-preview/app.tsx index ebb6639598..c780b55739 100644 --- a/plugins/theme-preview/app.tsx +++ b/plugins/theme-preview/app.tsx @@ -591,7 +591,7 @@ function FrameView({ view, composition, themeName, mode }: { view: View; composi return ( <> {sidebar ? : null} - + {infoPanel ? : null} ); @@ -1187,7 +1187,7 @@ function OverlaySpecimens() { ); } -function ComponentsSection() { +function ComponentsSection({ stacked = false }: { stacked?: boolean }) { const [search, setSearch] = useState(""); const [notify, setNotify] = useState(true); const [compact, setCompact] = useState(false); @@ -1198,7 +1198,7 @@ function ComponentsSection() { const toggleControls: CSSProperties = { display: "flex", flexDirection: "column", gap: 8 }; const compactLabel: CSSProperties = { ...TEXT_LABEL, minWidth: 0, fontSize: 11.5, lineHeight: "16px" }; return ( -
+

Buttons

@@ -1212,7 +1212,7 @@ function ComponentsSection() {

Badges

-
+
RunningAttention FailedMergedbranch
@@ -1639,6 +1639,7 @@ function PreviewPage({ subPath }: { subPath: string }) { const radii = useResolvedRadii(revision); const mobile = layout.band === "mobile"; const railWidth = SURFACE_RAIL_WIDTH; + const showRail = layout.band === "desktop"; const contentInset = contentInsetForWidth(layout.width); const displayThemeId = pendingSelection?.themeId ?? catalog.activeThemeId; const displayThemeName = catalog.themes.find((theme) => theme.id === displayThemeId)?.name ?? "Current theme"; @@ -1647,7 +1648,7 @@ function PreviewPage({ subPath }: { subPath: string }) {
- navigate.toPluginPanel("preview", { subPath: next })}> + navigate.toPluginPanel("preview", { subPath: next })}> {VIEWS.map((item) => ( @@ -1656,7 +1657,7 @@ function PreviewPage({ subPath }: { subPath: string }) { ))} -
+ {mobile ? null :
} {error ? {error} : null}
- {/* Layout system, level 2: the plugin window. One stage zone (mock + - at-a-glance rail on wider bands), then flow sections in taxonomy - order, all on the same max-width spine. On the mobile band the rail - content becomes the first flow section so nothing is lost, only - restacked. */}
@@ -1695,7 +1691,7 @@ function PreviewPage({ subPath }: { subPath: string }) {
- {mobile ? null : ( + {showRail ? (
- )} + ) : null}
- {(mobile + {(!showRail ? (["overlays", "components", "stylesheet"] as const) : (["stylesheet"] as const) ).map((area) => ( @@ -1718,7 +1714,7 @@ function PreviewPage({ subPath }: { subPath: string }) {
{area === "overlays" ? - : area === "components" ? + : area === "components" ? : }
From 2dcf3d14f5e87890c1e41b17d813d943e504b5d4 Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Thu, 17 Sep 2026 16:45:34 -0400 Subject: [PATCH 02/31] Update Guide overflow assertion for immediate responsive sizing --- packages/plugin-api-map/test/wireframes.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugin-api-map/test/wireframes.test.ts b/packages/plugin-api-map/test/wireframes.test.ts index fa1f8046cf..7eba0a9f33 100644 --- a/packages/plugin-api-map/test/wireframes.test.ts +++ b/packages/plugin-api-map/test/wireframes.test.ts @@ -103,9 +103,7 @@ describe("guide fixture boundaries", () => { it("scrolls only the one-line page list and clips off-stage fixture overflow", () => { const markup = renderToStaticMarkup(createElement(ProductMap)); - expect(markup).toContain( - "overflow-x-clip transition-[height] duration-300 ease-out", - ); + expect(markup).toContain("w-full overflow-x-clip"); expect(markup).toContain("mx-auto flex w-fit max-w-full items-center"); expect(markup).toContain("data-guide-page-list-scroll"); expect(markup).toContain("min-w-0 overflow-x-auto"); From 593ebb430093287f64205974cdb662e9393c4c13 Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Thu, 17 Sep 2026 16:48:47 -0400 Subject: [PATCH 03/31] Align badge layout assertion with responsive wrapping --- plugins/theme-preview/app.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/theme-preview/app.test.tsx b/plugins/theme-preview/app.test.tsx index 1991defdc5..132815ceb1 100644 --- a/plugins/theme-preview/app.test.tsx +++ b/plugins/theme-preview/app.test.tsx @@ -609,7 +609,7 @@ describe("Theme Preview", () => { } }); - it("keeps badges on one row and the component specimens evenly grouped", async () => { + it("wraps badges and keeps the component specimens evenly grouped", async () => { const width = vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(1280); try { renderPreview({ @@ -619,8 +619,8 @@ describe("Theme Preview", () => { await waitFor(() => expect(document.querySelector("[data-tp-band=desktop]")).not.toBeNull()); const badges = document.querySelector("[data-tp-badge-row]"); - expect(badges?.style.flexWrap).toBe("nowrap"); - expect(badges?.style.overflowX).toBe("auto"); + expect(badges?.style.flexWrap).toBe("wrap"); + expect(badges?.style.overflowX).toBe(""); const components = document.querySelector("[data-tp-components]"); expect(components?.style.gridTemplateColumns).toBe("repeat(2, minmax(0, 1fr))"); From ab0b99a49e0ca934b4b6a6e23f66c064728cf9c3 Mon Sep 17 00:00:00 2001 From: Bersabel Tadesse Date: Thu, 17 Sep 2026 17:23:25 -0400 Subject: [PATCH 04/31] Show mobile product layouts in plugin previews --- .../plugin-api-map/src/anatomy-manifest.json | 20 + packages/plugin-api-map/src/product-map.tsx | 168 ++-- packages/plugin-api-map/src/wireframes.tsx | 742 ++++++++++++------ .../plugin-api-map/test/mobile-guide.test.ts | 85 ++ plugins/theme-preview/app.test.tsx | 26 + plugins/theme-preview/app.tsx | 84 +- 6 files changed, 823 insertions(+), 302 deletions(-) create mode 100644 packages/plugin-api-map/test/mobile-guide.test.ts diff --git a/packages/plugin-api-map/src/anatomy-manifest.json b/packages/plugin-api-map/src/anatomy-manifest.json index 45abb631a3..527d81fbf8 100644 --- a/packages/plugin-api-map/src/anatomy-manifest.json +++ b/packages/plugin-api-map/src/anatomy-manifest.json @@ -10,6 +10,26 @@ "fork", "plugin-actions" ], + "mobileLayout": { + "sources": [ + { + "path": "packages/shared-ui/src/components/ui/hooks/use-compact-viewport.tsx", + "anchors": ["(max-width: 767px)"] + }, + { + "path": "apps/app/src/views/RootComposeSecondaryContent.tsx", + "anchors": ["RootComposeCompactHome composer={children}", "PluginHomepageSections"] + }, + { + "path": "apps/app/src/components/secondary-panel/CompactSecondaryPanelShelf.tsx", + "anchors": ["CompactSecondaryPanelShelf"] + }, + { + "path": "apps/app/src/components/ui/sidebar.tsx", + "anchors": ["SIDEBAR_WIDTH_MOBILE", "SIDEBAR_MOBILE_VIEWPORT_FRACTION"] + } + ] + }, "surfaceFixtures": { "command-palette-actions": { "groupId": "command-palette", diff --git a/packages/plugin-api-map/src/product-map.tsx b/packages/plugin-api-map/src/product-map.tsx index 33d68fb08e..78f9f76d40 100644 --- a/packages/plugin-api-map/src/product-map.tsx +++ b/packages/plugin-api-map/src/product-map.tsx @@ -234,14 +234,15 @@ function SpatialFixture({ const cardFootprint = flowCard ? Math.max(probeReserve, cardReserveRef.current) : 0; - const availableHeight = viewport && frame.clientWidth >= 640 - ? viewport.clientHeight - - (frame.getBoundingClientRect().top - - viewport.getBoundingClientRect().top + - viewport.scrollTop) - - cardFootprint - - 8 - : undefined; + const availableHeight = + viewport && frame.clientWidth >= 640 + ? viewport.clientHeight - + (frame.getBoundingClientRect().top - + viewport.getBoundingClientRect().top + + viewport.scrollTop) - + cardFootprint - + 8 + : undefined; const scale = spatialFixtureScale( frame.clientWidth, authoredWidth, @@ -328,20 +329,26 @@ function SpatialFixture({ ); } -function SlideContent({ group }: { group: SurfaceGroup }) { +function SlideContent({ + group, + mobile = false, +}: { + group: SurfaceGroup; + mobile?: boolean; +}) { switch (group.id) { case "app-shell": - return ; + return ; case "command-palette": - return ; + return ; case "composer": - return ; + return ; case "home": - return ; + return ; case "settings": - return ; + return ; case "extensions": - return ; + return ; case "headless": return ; } @@ -380,7 +387,17 @@ function CardReserveProbe({ group }: { group: SurfaceGroup }) { ); } -function Slide({ group }: { group: SurfaceGroup }) { +function Slide({ group, mobile }: { group: SurfaceGroup; mobile: boolean }) { + if (mobile && group.id !== "headless") { + return ( +
+ +
+ ); + } if (fixtureResponsiveStrategy(group) === "reflow") { return (
@@ -500,6 +517,28 @@ export function ProductMap({ const pageButtonRefs = useRef>([]); const card = useSurfaceCard(); const [hoverId, setHoverId] = useState(null); + const [selectedId, setSelectedId] = useState(null); + const [viewportMobile, setViewportMobile] = useState( + () => + typeof window !== "undefined" && + window.matchMedia?.("(max-width: 767px)").matches === true, + ); + const [displayMode, setDisplayMode] = useState<"mobile" | "desktop" | null>( + null, + ); + const mobile = + displayMode === null ? viewportMobile : displayMode === "mobile"; + useEffect(() => { + const query = window.matchMedia?.("(max-width: 767px)"); + if (!query) return; + const update = () => setViewportMobile(query.matches); + query.addEventListener("change", update); + return () => query.removeEventListener("change", update); + }, []); + const selectSurface = (id: string) => { + setSelectedId(id); + card.open(id); + }; const pageListEdges = useScrollEdges(pageListRef); const [index, setIndex] = useState(() => Math.max( @@ -529,6 +568,7 @@ export function ProductMap({ return; } card.close(); + setSelectedId(null); setHoverId(null); setIndex(next); onSlideChange?.(slides[next].id); @@ -540,7 +580,7 @@ export function ProductMap({ const target = slides.findIndex((slide) => slide.id === group.id); if (target === -1) return; if (target !== index) show(target); - card.open(id); + selectSurface(id); }; const onKeyDown = (event: KeyboardEvent) => { @@ -558,16 +598,18 @@ export function ProductMap({ () => ({ activeId: hoverId, setActiveId: setHoverId, - expandedId: card.openId, + expandedId: + card.openId ?? + (mobile ? (selectedId ?? slides[index].surfaces[0]?.id) : null), numberOf: (id: string) => SURFACE_NUMBERS.get(id) ?? null, - onSelect: card.open, + onSelect: selectSurface, pluginPageHref, renderPluginIcon, currentGroupId: slides[index].id, onGoToSurface: goToSurface, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [hoverId, card.openId, pluginPageHref, renderPluginIcon, index], + [hoverId, card.openId, pluginPageHref, renderPluginIcon, index, selectedId, mobile], ); const cardNode = openSurface ? ( @@ -592,7 +634,12 @@ export function ProductMap({ const target = event.target; if (!(target instanceof Element)) return; if (target.closest('[role="dialog"]')) return; - if (target.closest('a[href^="#surface-"], [data-guide-annotation-picker]')) return; + if ( + target.closest( + 'a[href^="#surface-"], [data-guide-annotation-picker], [data-guide-display-mode]', + ) + ) + return; card.close(); }; scope.addEventListener("pointerdown", onPointerDown); @@ -615,9 +662,35 @@ export function ProductMap({

- {slides[index].blurb} + {mobile && slides[index].id !== "headless" + ? "Choose an annotation to see where it appears on mobile." + : slides[index].blurb}

+
+ {(["mobile", "desktop"] as const).map((mode) => ( + + ))} +
show(index + 1)} />
+ {slides[index].id !== "headless" ? ( + + ) : null}
- +
))}
- {slides[index].id !== "headless" ? ( - - ) : null} - {cardNode ? (
void; }) { return ( <> - onTabSelect("browser-toolbar")} - /> - onTabSelect("code-renderers")} - /> - onTabSelect("thread-panel")} - /> - onTabSelect("file-opener")} - /> + {!only || only === "browser-toolbar" ? ( + onTabSelect("browser-toolbar")} + /> + ) : null} + {!only || only === "code-renderers" ? ( + onTabSelect("code-renderers")} + /> + ) : null} + {!only || only === "thread-panel" ? ( + onTabSelect("thread-panel")} + /> + ) : null} + {!only || only === "file-opener" ? ( + onTabSelect("file-opener")} + /> + ) : null} ); } -export function CommandPaletteWireframe() { +export function CommandPaletteWireframe({ + mobile = false, +}: { + mobile?: boolean; +}) { const [paletteOpen, setPaletteOpen] = useState(true); const [releasePanelOpen, setReleasePanelOpen] = useState(false); const restoreTimer = useRef(undefined); @@ -721,7 +735,7 @@ export function CommandPaletteWireframe() {
@@ -729,35 +743,42 @@ export function CommandPaletteWireframe() { data-guide-fixture="command-palette-thread" className="flex min-h-[500px] bg-background" > - + {mobile ? null : ( + + )} -
+
Ship release candidate @@ -773,9 +794,11 @@ export function CommandPaletteWireframe() { > Quick palette - - ⇧⌘P - + {mobile ? null : ( + + ⇧⌘P + + )}
@@ -809,7 +832,10 @@ export function CommandPaletteWireframe() { {releasePanelOpen ? (