diff --git a/packages/plugin-api-map/src/anatomy-manifest.json b/packages/plugin-api-map/src/anatomy-manifest.json index 45abb631a3f..527d81fbf8c 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 7bdf462858a..7ce8d83b5dd 100644 --- a/packages/plugin-api-map/src/product-map.tsx +++ b/packages/plugin-api-map/src/product-map.tsx @@ -10,6 +10,8 @@ import { } from "react"; import { Icon } from "@bb/shared-ui/icon"; import { HugeiconsIcon } from "@hugeicons/react"; +import ComputerIcon from "@hugeicons/core-free-icons/ComputerIcon"; +import SmartPhone01Icon from "@hugeicons/core-free-icons/SmartPhone01Icon"; import { cn } from "./cn"; import { SurfaceCard, useSurfaceCard } from "./surface-card"; @@ -52,6 +54,77 @@ export const SURFACE_NUMBERS: ReadonlyMap = new Map( ), ); +type GuideSlide = Omit & { + id: string; + groupId: SurfaceGroup["id"]; + appShellScene?: "navigation" | "conversation" | "panel"; + homePanel?: boolean; +}; + +const DESKTOP_SLIDES: GuideSlide[] = SURFACE_GROUPS.map((group) => ({ + ...group, + groupId: group.id, +})); +const MOBILE_SLIDES: GuideSlide[] = DESKTOP_SLIDES.flatMap((group) => { + if (group.id === "composer") { + return [{ + ...group, + blurb: "Plugins can add banners, actions, providers, and rich text to the prompt box.", + }]; + } + if (group.id === "app-shell") { + return [ + { + ...group, + title: "Sidebar", + blurb: "Plugins can add navigation, thread status, and footer controls.", + appShellScene: "navigation" as const, + surfaces: group.surfaces.filter((surface) => [ + "sidebar-navigation", "nav-panel", "thread-row-status", "thread-list", "sidebar-footer", + ].includes(surface.id)), + }, + { + ...group, + id: "app-shell-thread", + title: "Thread", + blurb: "Plugins can extend the thread header, conversation, and composer.", + appShellScene: "conversation" as const, + surfaces: group.surfaces.filter((surface) => [ + "thread-header", "timeline-renderers", "message-directives", "message-actions", + "pending-interaction", "app-overlay", "content-scripts", + ].includes(surface.id)), + }, + { + ...group, + id: "app-shell-panel", + title: "Side panel", + blurb: "Explore plugin controls and content in the side panel’s tabs.", + appShellScene: "panel" as const, + surfaces: group.surfaces.filter((surface) => [ + "code-renderers", "browser-toolbar", "thread-panel", "file-opener", + ].includes(surface.id)), + }, + ]; + } + if (group.id === "home") { + return [ + { + ...group, + surfaces: group.surfaces.filter((surface) => surface.id === "homepage-section"), + }, + { + ...group, + id: "home-actions", + title: "New thread actions", + blurb: "Plugins can add an action to the new-thread panel launcher.", + homePanel: true, + surfaces: group.surfaces.filter((surface) => surface.id === "new-thread-panel"), + }, + ]; + } + return [group]; +}); + export function annotationNeighbors( surfaces: readonly PluginSurface[], currentId: string, @@ -118,7 +191,7 @@ function PlatformCard({ surface }: { surface: PluginSurface }) { ); } -function PlatformSlide({ group }: { group: SurfaceGroup }) { +function PlatformSlide({ group }: { group: GuideSlide }) { return (
{(group.sections ?? []).map((section) => { @@ -181,9 +254,11 @@ const FIXTURE_WIDTH_BANDS: Record< function SpatialFixture({ band, + maxScale = MAX_FIXTURE_SCALE, children, }: { band?: { min: number; max: number }; + maxScale?: number; children: ReactNode; }) { const frameRef = useRef(null); @@ -234,22 +309,26 @@ function SpatialFixture({ const cardFootprint = flowCard ? Math.max(probeReserve, cardReserveRef.current) : 0; - const availableHeight = viewport - ? viewport.clientHeight - - (frame.getBoundingClientRect().top - - viewport.getBoundingClientRect().top + - viewport.scrollTop) - - cardFootprint - - 8 - : undefined; - const scale = spatialFixtureScale( - frame.clientWidth, - authoredWidth, - availableHeight, - authoredHeight, + const availableHeight = + viewport && frame.clientWidth >= 640 + ? viewport.clientHeight - + (frame.getBoundingClientRect().top - + viewport.getBoundingClientRect().top + + viewport.scrollTop) - + cardFootprint - + 8 + : undefined; + const scale = Math.min( + maxScale, + spatialFixtureScale( + frame.clientWidth, + authoredWidth, + availableHeight, + authoredHeight, + ), ); const scaled = Math.abs(scale - 1) >= 0.0001; - const height = scaled ? authoredHeight * scale : null; + const height = authoredHeight * scale; const width = scaled ? authoredWidth : null; const offsetX = scaled ? (frame.clientWidth - authoredWidth) / 2 : 0; setGeometry((current) => @@ -290,16 +369,16 @@ function SpatialFixture({ observer.disconnect(); cardObserver?.disconnect(); }; - }, []); + }, [maxScale]); - const scaled = geometry.height !== null; + const scaled = geometry.width !== null; 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 ; } @@ -350,7 +435,8 @@ function SlideContent({ group }: { group: SurfaceGroup }) { const PROBE_NOOP = () => {}; const PROBE_COPY = async () => false; -function CardReserveProbe({ group }: { group: SurfaceGroup }) { +function CardReserveProbe({ group }: { group: GuideSlide }) { + const { numberOf } = useSurfaceMap(); return (
+ +
+ ); + } if (fixtureResponsiveStrategy(group) === "reflow") { return (
@@ -390,8 +494,11 @@ function Slide({ group }: { group: SurfaceGroup }) { } return ( <> - - + + @@ -438,7 +545,7 @@ function PanButton({ disabled={disabled} aria-label={`${direction === "previous" ? "Previous" : "Next"} surface`} className={cn( - "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-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground", + "inline-flex size-10 @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-40 disabled:hover:bg-transparent disabled:hover:text-muted-foreground", FOCUS_RING_CLASS, )} > @@ -480,6 +587,36 @@ function useStageHeight( return { height, animate }; } +function MobileCardFlow({ children }: { children: ReactNode }) { + const frameRef = useRef(null); + const contentRef = useRef(null); + + useBrowserLayoutEffect(() => { + const frame = frameRef.current; + const content = contentRef.current; + if (!frame || !content) return; + const measure = () => { + frame.style.height = `${content.getBoundingClientRect().height}px`; + }; + measure(); + const observer = new ResizeObserver(measure); + observer.observe(content); + return () => observer.disconnect(); + }, []); + + return ( +
+
+ {children} +
+
+ ); +} + export function ProductMap({ pluginPageHref, renderPluginIcon, @@ -493,23 +630,65 @@ export function ProductMap({ onSlideChange?: (slideId: string) => void; onCopyForAgent?: (surface: PluginSurface) => Promise; }) { - const slides = SURFACE_GROUPS; const containerRef = useRef(null); const slideRefs = useRef>([]); const pageListRef = useRef(null); const pageButtonRefs = useRef>([]); + const touchStart = useRef<{ x: number; y: number } | null>(null); + const suppressClickUntil = useRef(0); const card = useSurfaceCard(); const [hoverId, setHoverId] = useState(null); - const pageListEdges = useScrollEdges(pageListRef); - const [index, setIndex] = useState(() => - Math.max( - 0, - slides.findIndex((slide) => slide.id === initialSlideId), - ), + 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"; + const slides = mobile ? MOBILE_SLIDES : DESKTOP_SLIDES; + const numbers = useMemo(() => new Map( + slides.filter((slide) => slide.groupId !== "headless").flatMap((slide) => + slide.surfaces.map((surface, index) => [surface.id, index + 1] as const), + ), + ), [slides]); + 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 [slideId, setSlideId] = useState(initialSlideId ?? "app-shell"); + const previousMobile = useRef(mobile); + useBrowserLayoutEffect(() => { + if (previousMobile.current === mobile) return; + previousMobile.current = mobile; + if (!mobile || !card.openId) return; + const destination = slides.find((slide) => + slide.surfaces.some((surface) => surface.id === card.openId), + ); + if (destination && destination.id !== slideId) { + setSlideId(destination.id); + onSlideChange?.(destination.id); + } + }, [mobile, card.openId, slides, slideId, onSlideChange]); + const selectedSlide = MOBILE_SLIDES.find((slide) => slide.id === slideId); + const index = Math.max(0, slides.findIndex((slide) => + slide.id === slideId || (!mobile && slide.id === selectedSlide?.groupId), + )); const stage = useStageHeight(index, slideRefs); useEffect(() => { + if (viewportMobile) return; const list = pageListRef.current; const button = pageButtonRefs.current[index]; if (!list || !button) return; @@ -519,31 +698,38 @@ export function ProductMap({ const rightDelta = buttonRect.right - listRect.right; if (leftDelta < 0) list.scrollLeft += leftDelta; else if (rightDelta > 0) list.scrollLeft += rightDelta; - }, [index]); + }, [index, viewportMobile]); const openSurface = card.openId ? SURFACES_BY_ID.get(card.openId) : undefined; const carets = panCarets(index, slides.length); const show = (next: number) => { - if (next < 0 || next >= slides.length) { + if (next === index || next < 0 || next >= slides.length) { return; } card.close(); + setSelectedId(null); setHoverId(null); - setIndex(next); + setSlideId(slides[next].id); onSlideChange?.(slides[next].id); }; const goToSurface = (id: string) => { const group = GROUP_BY_SURFACE_ID.get(id); if (!group) return; - const target = slides.findIndex((slide) => slide.id === group.id); + const target = slides.findIndex((slide) => + slide.surfaces.some((surface) => surface.id === id), + ); if (target === -1) return; if (target !== index) show(target); - card.open(id); + selectSurface(id); }; const onKeyDown = (event: KeyboardEvent) => { + if (event.target instanceof HTMLSelectElement) return; + if (event.target instanceof Element && event.target.closest('[role="dialog"]')) { + return; + } if (event.key === "ArrowRight") { event.preventDefault(); show(index + 1); @@ -557,29 +743,45 @@ export function ProductMap({ () => ({ activeId: hoverId, setActiveId: setHoverId, - expandedId: card.openId, - numberOf: (id: string) => SURFACE_NUMBERS.get(id) ?? null, - onSelect: card.open, + expandedId: card.openId ?? selectedId, + numberOf: (id: string) => numbers.get(id) ?? null, + onSelect: selectSurface, pluginPageHref, renderPluginIcon, - currentGroupId: slides[index].id, + currentGroupId: slides[index].groupId, onGoToSurface: goToSurface, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [hoverId, card.openId, pluginPageHref, renderPluginIcon, index], + [ + hoverId, + card.openId, + pluginPageHref, + renderPluginIcon, + index, + selectedId, + mobile, + slides, + numbers, + ], ); const cardNode = openSurface ? ( - +
+ +
) : null; useEffect(() => { if (card.openId === null) return; @@ -591,7 +793,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-"]')) return; + if ( + target.closest( + 'a[href^="#surface-"], [data-guide-display-mode]', + ) + ) + return; card.close(); }; scope.addEventListener("pointerdown", onPointerDown); @@ -601,7 +808,7 @@ export function ProductMap({ return ( -
+
-
- show(index - 1)} - /> +
+
+ show(index - 1)} + /> +
{ + suppressClickUntil.current = 0; + const touch = event.touches[0]; + touchStart.current = + viewportMobile && event.touches.length === 1 && touch + ? { x: touch.clientX, y: touch.clientY } + : null; + }} + onTouchCancelCapture={() => { + touchStart.current = null; + }} + onTouchEndCapture={(event) => { + const start = touchStart.current; + touchStart.current = null; + const touch = event.changedTouches[0]; + if (!start || !touch) return; + const dx = touch.clientX - start.x; + const dy = touch.clientY - start.y; + if (Math.abs(dx) < 30 || Math.abs(dx) <= Math.abs(dy)) + return; + suppressClickUntil.current = Date.now() + 500; + show(index + (dx < 0 ? 1 : -1)); + }} + onClickCapture={(event) => { + if (Date.now() >= suppressClickUntil.current) return; + suppressClickUntil.current = 0; + event.preventDefault(); + event.stopPropagation(); + }} + > +
    + {slides.map((entry, slideIndex) => ( + + ))} +
+
+ show(index + 1)} + /> +
-
    - {slides.map((entry, slideIndex) => ( -
  • - -
  • - ))} -
+ {(["mobile", "desktop"] as const).map((mode) => ( + + ))}
- show(index + 1)} - />
- +
))}
- {cardNode ? ( -
- {cardNode} -
- ) : null} + {viewportMobile ? {cardNode} : cardNode}
diff --git a/packages/plugin-api-map/src/surface-card.tsx b/packages/plugin-api-map/src/surface-card.tsx index a582ae18936..0adcacae0df 100644 --- a/packages/plugin-api-map/src/surface-card.tsx +++ b/packages/plugin-api-map/src/surface-card.tsx @@ -11,7 +11,7 @@ import { type SurfaceReference, } from "./annotation"; import { pluginIcon, surfaceIcon } from "./plugin-icons"; -import { UsedByList } from "./used-by"; +import { UsedByList, UsedByPager } from "./used-by"; import { SurfaceMapContext } from "./wireframes"; export function SurfaceCard({ @@ -21,12 +21,14 @@ export function SurfaceCard({ onCopyForAgent, navigation, probe = false, + mobile = false, }: { surface: PluginSurface; number: number | null; onDismiss: () => void; onCopyForAgent?: (surface: PluginSurface) => Promise; probe?: boolean; + mobile?: boolean; navigation?: { previous: PluginSurface | null; next: PluginSurface | null; @@ -34,6 +36,7 @@ export function SurfaceCard({ }; }) { const cardRef = useRef(null); + const ExampleList = mobile ? UsedByPager : UsedByList; const surfaceMap = useContext(SurfaceMapContext); const pluginPageHref = surfaceMap?.pluginPageHref; const icon = surfaceIcon(surface.id); @@ -98,14 +101,14 @@ export function SurfaceCard({ cardRef.current?.scrollIntoView({ block: "nearest", behavior: "smooth" }); }, 350); return () => window.clearTimeout(timer); - }, [surface.id, probe]); + }, [probe]); return (
{number === null ? ( @@ -156,7 +159,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 +172,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,13 +190,14 @@ export function SurfaceCard({ {(surface.firstParty && surface.firstParty.length > 0) || onCopyForAgent ? ( -
+
{surface.firstParty && surface.firstParty.length > 0 ? ( - <> - +
+ Used by - { const icon = pluginIcon(plugin); @@ -207,37 +211,44 @@ export function SurfaceCard({ className="size-3.5 shrink-0 text-subtle-foreground" /> ) : null)} - {plugin} + {mobile ? {plugin} : plugin} ); return href ? ( {body} ) : ( - + {body} ); }} /> - +
) : null} {onCopyForAgent ? (
); } + +export function UsedByPager({ + items, + renderItem, +}: { + items: readonly string[]; + renderItem: (item: string) => ReactNode; +}) { + const [index, setIndex] = useState(0); + const currentIndex = Math.min(index, items.length - 1); + const item = items[currentIndex]; + if (!item) return null; + + return ( +
{ + if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return; + event.preventDefault(); + event.stopPropagation(); + setIndex(Math.max(0, Math.min( + items.length - 1, + currentIndex + (event.key === "ArrowLeft" ? -1 : 1), + ))); + }} + > + {items.length > 1 ? ( + + ) : null} +
+ Example {currentIndex + 1} of {items.length}: + {renderItem(item)} +
+ {items.length > 1 ? ( + + ) : null} +
+ ); +} diff --git a/packages/plugin-api-map/src/wireframes.tsx b/packages/plugin-api-map/src/wireframes.tsx index 5a0010177bc..e4ed74eb9c7 100644 --- a/packages/plugin-api-map/src/wireframes.tsx +++ b/packages/plugin-api-map/src/wireframes.tsx @@ -51,12 +51,12 @@ export const APP_SHELL_MARKS = [ "thread-list", "sidebar-footer", "thread-header", - "browser-toolbar", "timeline-renderers", "message-directives", "message-actions", "pending-interaction", "code-renderers", + "browser-toolbar", "thread-panel", "file-opener", "app-overlay", @@ -67,9 +67,9 @@ export const COMMAND_PALETTE_MARKS = ["command-palette-actions"] as const; export const COMPOSER_MARKS = [ "composer-banners", + "composer-state", "mention-provider", "composer-rich-text", - "composer-state", "composer-plus-menu", "provider-picker", "composer-actions", @@ -151,6 +151,7 @@ function Mark({ className, chip = "corner", showChip = true, + target = false, onActivate, children, }: { @@ -159,6 +160,7 @@ function Mark({ className?: string; chip?: AnnotationChipPlacement; showChip?: boolean; + target?: boolean; onActivate?: () => void; children?: ReactNode; }) { @@ -168,6 +170,7 @@ function Mark({ return ( selectAnnotation(event, id, onSelect, onActivate)} @@ -191,6 +194,7 @@ const RELEASE_DEMO_MS = 2400; function CommandPaletteActionMark({ onRun }: { onRun: () => void }) { const id = "command-palette-actions"; + const { onSelect } = useSurfaceMap(); const { outlined } = useEngagement(id); const hover = useAnnotationHover(id); @@ -200,7 +204,10 @@ function CommandPaletteActionMark({ onRun }: { onRun: () => void }) { type="button" role="option" aria-selected="true" - onClick={onRun} + onClick={() => { + onRun(); + onSelect?.(id); + }} {...hover} data-guide-fixture="command-palette-action" className={cn( @@ -242,6 +249,7 @@ function RegionMark({ onSelect ? (event) => { event.preventDefault(); + event.stopPropagation(); onSelect(id); } : undefined @@ -276,6 +284,7 @@ function MeasuredBadge({ align = "center", flush = false, onActivate, + clipTo, }: { id: string; label: string; @@ -284,6 +293,7 @@ function MeasuredBadge({ align?: "start" | "center" | "end"; flush?: boolean; onActivate?: () => void; + clipTo?: string; }) { const { numberOf, onSelect } = useSurfaceMap(); const { active } = useEngagement(id); @@ -313,6 +323,12 @@ function MeasuredBadge({ node = node.offsetParent instanceof HTMLElement ? node.offsetParent : null; } + node = element.parentElement; + while (node) { + x -= node.scrollLeft; + y -= node.scrollTop; + node = node.parentElement; + } return { x, y }; }; const measure = () => { @@ -337,6 +353,15 @@ function MeasuredBadge({ width: target.offsetWidth, height: target.offsetHeight, }; + const clip = clipTo ? scope.querySelector(clipTo) : null; + if (clip) { + const clipOrigin = layoutOrigin(clip); + const left = targetOrigin.x - clipOrigin.x; + if (left < 0 || left + target.offsetWidth > clip.clientWidth) { + setPosition(null); + return; + } + } const centerY = local.top + local.height / 2 - chipBox / 2; const anchoredY = align === "start" @@ -401,8 +426,12 @@ function MeasuredBadge({ "[data-guide-responsive-strategy]", ); if (scaleWrapper) observer.observe(scaleWrapper); - return () => observer.disconnect(); - }, [anchor, at, align, flush]); + scope.addEventListener("scroll", measure, true); + return () => { + observer.disconnect(); + scope.removeEventListener("scroll", measure, true); + }; + }, [anchor, at, align, flush, clipTo]); return ( selectAnnotation(event, id, onSelect, onActivate)} {...hover} className={cn("pointer-events-auto absolute z-50", FOCUS_RING_CLASS)} - style={position ?? undefined} + style={position ?? { visibility: "hidden" }} > void; -}) { +function RightPanelTabLaneBadges({ mobile }: { mobile: boolean }) { + const clipTo = mobile ? '[data-guide-fixture="right-panel-tab-strip"]' : undefined; return ( <> - onTabSelect("browser-toolbar")} - /> + onTabSelect("code-renderers")} /> onTabSelect("thread-panel")} /> onTabSelect("file-opener")} /> ); } -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 +751,7 @@ export function CommandPaletteWireframe() {
@@ -729,35 +759,42 @@ export function CommandPaletteWireframe() { data-guide-fixture="command-palette-thread" className="flex min-h-[500px] bg-background" > - + {mobile ? null : ( + + )} -
+
Ship release candidate @@ -773,9 +810,11 @@ export function CommandPaletteWireframe() { > Quick palette - - ⇧⌘P - + {mobile ? null : ( + + ⇧⌘P + + )}
@@ -809,7 +848,10 @@ export function CommandPaletteWireframe() { {releasePanelOpen ? (