| @@ -1,6 +1,12 @@ | ||
| 1 | +import { | |
| 2 | + CANVAS_PANE_WIDTH, | |
| 3 | + CanvasPane, | |
| 4 | + DOT_GRID, | |
| 5 | + useCanvasSeat, | |
| 6 | +} from '@agent/components/Canvas'; | |
| 1 | 7 | import { usePortal } from '@agent/hooks/usePortal'; |
| 2 | -import { useGlobalStore } from '@agent/state/global'; | |
| 8 | +import { DESKTOP_MIN_WIDTH, useGlobalStore } from '@agent/state/global'; | |
| 3 | 9 | import { createPortal, useEffect, useRef } from '@wordpress/element'; |
| 4 | 10 | import { __ } from '@wordpress/i18n'; |
| 5 | 11 | import { close, Icon } from '@wordpress/icons'; |
| 6 | 12 | import { motion } from 'framer-motion'; |
| @@ -5,9 +11,10 @@ | ||
| 5 | 11 | import { close, Icon } from '@wordpress/icons'; |
| 6 | 12 | import { motion } from 'framer-motion'; |
| 7 | 13 | import { OptionsPopover } from '../OptionsPopover'; |
| 8 | 14 | |
| 9 | -const SIDEBAR_WIDTH = 384; // 96 * 4 (w-96) | |
| 15 | +const SIDEBAR_WIDTH = 384; | |
| 16 | +const CANVAS_WIDTH = SIDEBAR_WIDTH + CANVAS_PANE_WIDTH; | |
| 10 | 17 | const FRAME_WIDTH = 8; // border-8 |
| 11 | 18 | const ANIMATE_TIME = 300; |
| 12 | 19 | |
| 13 | 20 | export const SidebarLayout = ({ children }) => { |
| @@ -13,8 +20,10 @@ | ||
| 13 | 20 | export const SidebarLayout = ({ children }) => { |
| 14 | 21 | const mountNode = usePortal('extendify-agent-sidebar-mount'); |
| 15 | 22 | const frameNode = usePortal('extendify-agent-border-frame-mount'); |
| 16 | 23 | const { open, setOpen } = useGlobalStore(); |
| 24 | + const seat = useCanvasSeat(); | |
| 25 | + const beside = seat === 'beside'; | |
| 17 | 26 | useLayoutShift(open); |
| 18 | 27 | |
| 19 | 28 | const closeAgent = () => { |
| 20 | 29 | setOpen(false); |
| @@ -47,11 +56,12 @@ | ||
| 47 | 56 | borderRadius: 0, |
| 48 | 57 | }, |
| 49 | 58 | }; |
| 50 | 59 | |
| 60 | + // Unclipped, the 9999px shadow covers the notification bar. | |
| 51 | 61 | const frameBar = frameNode |
| 52 | 62 | ? createPortal( |
| 53 | - <div className="fixed inset-0 pointer-events-none z-high"> | |
| 63 | + <div className="fixed top-0 left-0 right-0 bottom-(--extendify-notification-bar-height,0px) overflow-hidden pointer-events-none z-high"> | |
| 54 | 64 | <motion.div |
| 55 | 65 | className="absolute overflow-hidden" |
| 56 | 66 | initial={false} |
| 57 | 67 | animate={open ? 'open' : 'closed'} |
| @@ -77,49 +87,70 @@ | ||
| 77 | 87 | frameNode, |
| 78 | 88 | ) |
| 79 | 89 | : null; |
| 80 | 90 | |
| 91 | + // app/Agent/Skeleton.php mirrors this markup for the deferred mount; edit both. | |
| 81 | 92 | const sidebar = mountNode |
| 82 | 93 | ? createPortal( |
| 83 | 94 | <motion.div |
| 84 | - style={{ width: SIDEBAR_WIDTH }} | |
| 85 | - className=" fixed top-0 bottom-0 left-0 w-96 flex-col z-higher border-transparent border-8" | |
| 95 | + className={`fixed top-0 bottom-[var(--extendify-notification-bar-height,0px)] left-0 border-transparent border-8 ${ | |
| 96 | + // An emptied panel belongs under the scrim with the rest of the page. | |
| 97 | + seat === 'modal' ? 'z-high' : 'z-higher' | |
| 98 | + }`} | |
| 86 | 99 | id="extendify-agent-sidebar" |
| 100 | + data-extendify-agent-panel | |
| 101 | + data-extendify-agent-emptied={seat === 'modal' ? '' : undefined} | |
| 87 | 102 | initial={false} |
| 88 | 103 | inert={open ? undefined : ''} |
| 89 | - animate={{ x: open ? 0 : -SIDEBAR_WIDTH }} | |
| 104 | + animate={{ | |
| 105 | + x: open ? 0 : -SIDEBAR_WIDTH, | |
| 106 | + width: beside ? CANVAS_WIDTH : SIDEBAR_WIDTH, | |
| 107 | + }} | |
| 90 | 108 | transition={{ duration: ANIMATE_TIME / 1000, ease: 'easeInOut' }} |
| 91 | 109 | > |
| 92 | - <div className="h-full flex flex-col shadow-lg rounded-2xl overflow-hidden bg-white"> | |
| 93 | - <div className="group flex shrink-0 items-center justify-between overflow-hidden bg-banner-main text-banner-text"> | |
| 94 | - <div className="flex h-full grow items-center justify-between gap-1 p-0 py-2.5"> | |
| 95 | - <div className="flex h-5 px-4 max-w-36 overflow-hidden"> | |
| 96 | - <img | |
| 97 | - className="max-h-full max-w-full object-contain" | |
| 98 | - src={window.extSharedData.partnerLogo} | |
| 99 | - alt={window.extSharedData.partnerName} | |
| 100 | - /> | |
| 110 | + <div | |
| 111 | + className={`relative h-full flex shadow-lg rounded-2xl overflow-hidden ${beside ? 'bg-gray-50' : 'bg-white'}`} | |
| 112 | + style={beside ? DOT_GRID : undefined} | |
| 113 | + > | |
| 114 | + <div | |
| 115 | + className="relative z-10 h-full flex shrink-0 flex-col rounded-2xl overflow-hidden bg-white shadow-lg" | |
| 116 | + style={{ width: SIDEBAR_WIDTH - FRAME_WIDTH * 2 }} | |
| 117 | + > | |
| 118 | + <div className="group flex shrink-0 items-center justify-between overflow-hidden bg-banner-main text-banner-text"> | |
| 119 | + <div className="flex h-full grow items-center justify-between gap-1 p-0 py-2.5"> | |
| 120 | + <div className="flex h-5 px-4 max-w-36 overflow-hidden"> | |
| 121 | + <img | |
| 122 | + className="max-h-full max-w-full object-contain" | |
| 123 | + src={window.extSharedData.partnerLogo} | |
| 124 | + alt={window.extSharedData.partnerName} | |
| 125 | + /> | |
| 126 | + </div> | |
| 101 | 127 | </div> |
| 128 | + <div className="flex gap-1 h-full items-center p-2"> | |
| 129 | + {seat ? null : ( | |
| 130 | + <> | |
| 131 | + <OptionsPopover /> | |
| 132 | + <button | |
| 133 | + type="button" | |
| 134 | + className="relative z-10 flex justify-center h-6 w-6 items-center border-0 bg-banner-main text-banner-text outline-hidden ring-design-main focus:shadow-none focus:outline-hidden focus-visible:outline-design-main focus:ring-2 hover:opacity-80 rounded-sm" | |
| 135 | + onClick={closeAgent} | |
| 136 | + > | |
| 137 | + <Icon | |
| 138 | + className="pointer-events-none fill-current leading-none" | |
| 139 | + icon={close} | |
| 140 | + size={18} | |
| 141 | + /> | |
| 142 | + <span className="sr-only"> | |
| 143 | + {__('Close window', 'extendify-local')} | |
| 144 | + </span> | |
| 145 | + </button> | |
| 146 | + </> | |
| 147 | + )} | |
| 148 | + </div> | |
| 102 | 149 | </div> |
| 103 | - <div className="flex gap-1 h-full items-center p-2"> | |
| 104 | - <OptionsPopover /> | |
| 105 | - <button | |
| 106 | - type="button" | |
| 107 | - className="relative z-10 flex justify-center h-6 w-6 items-center border-0 bg-banner-main text-banner-text outline-hidden ring-design-main focus:shadow-none focus:outline-hidden focus-visible:outline-design-main focus:ring-2 hover:opacity-80 rounded-sm" | |
| 108 | - onClick={closeAgent} | |
| 109 | - > | |
| 110 | - <Icon | |
| 111 | - className="pointer-events-none fill-current leading-none" | |
| 112 | - icon={close} | |
| 113 | - size={18} | |
| 114 | - /> | |
| 115 | - <span className="sr-only"> | |
| 116 | - {__('Close window', 'extendify-local')} | |
| 117 | - </span> | |
| 118 | - </button> | |
| 119 | - </div> | |
| 150 | + {open ? children : null} | |
| 120 | 151 | </div> |
| 121 | - {open ? children : null} | |
| 152 | + <CanvasPane seat="beside" /> | |
| 122 | 153 | </div> |
| 123 | 154 | </motion.div>, |
| 124 | 155 | mountNode, |
| 125 | 156 | ) |
| @@ -193,9 +224,10 @@ | ||
| 193 | 224 | |
| 194 | 225 | const applyScaling = () => { |
| 195 | 226 | if (!siteBlocks) return; |
| 196 | 227 | |
| 197 | - if (open) { | |
| 228 | + // Unmount leaves these styles, so scaling below the breakpoint is permanent. | |
| 229 | + if (open && window.innerWidth >= DESKTOP_MIN_WIDTH) { | |
| 198 | 230 | // Capture before `position: fixed` zeroes window.scrollY. |
| 199 | 231 | // Fall through to savedScroll.current so resize / strict-mode |
| 200 | 232 | // re-runs don't clobber it with the now-pinned scrollY (0). |
| 201 | 233 | const stashed = isFirstApply ? consumeStashedScroll() : 0; |
| @@ -205,17 +237,20 @@ | ||
| 205 | 237 | const scale = (viewportWidth - SIDEBAR_WIDTH) / viewportWidth; |
| 206 | 238 | |
| 207 | 239 | // Subtract 40 because translateY(40px) below pushes the element down. |
| 208 | 240 | const scaledHeight = (window.innerHeight - 40) / scale; |
| 241 | + // Divided by the scale because this height is in pre-scale units. | |
| 242 | + const height = | |
| 243 | + `calc(${scaledHeight}px - ` + | |
| 244 | + `var(--extendify-notification-bar-height, 0px) / ${scale})`; | |
| 209 | 245 | |
| 210 | 246 | Object.assign(siteBlocks.style, { |
| 211 | 247 | transformOrigin: 'top left', |
| 212 | 248 | transform: `translateX(${SIDEBAR_WIDTH}px) translateY(40px) scale(${scale})`, |
| 213 | - height: `${scaledHeight}px`, | |
| 249 | + height, | |
| 214 | 250 | overflowY: 'auto', |
| 215 | 251 | // `auto` so the scrollTop below is instant; `smooth` would animate from 0. |
| 216 | 252 | scrollBehavior: 'auto', |
| 217 | - colorScheme: 'light', | |
| 218 | 253 | }); |
| 219 | 254 | // Force layout so scrollTop respects the new height/overflow. |
| 220 | 255 | void siteBlocks.scrollHeight; |
| 221 | 256 | siteBlocks.scrollTop = savedScroll.current; |
| @@ -240,9 +275,8 @@ | ||
| 240 | 275 | height: '', |
| 241 | 276 | overflowY: '', |
| 242 | 277 | maxWidth: '100vw', |
| 243 | 278 | scrollBehavior: '', |
| 244 | - colorScheme: '', | |
| 245 | 279 | }); |
| 246 | 280 | if (stickyHeader) { |
| 247 | 281 | stickyHeader.style.removeProperty( |
| 248 | 282 | '--wp-admin--admin-bar--position-offset', |
| @@ -278,11 +312,8 @@ | ||
| 278 | 312 | firstRun.current = false; |
| 279 | 313 | } |
| 280 | 314 | |
| 281 | 315 | const raf = requestAnimationFrame(() => { |
| 282 | - const fw = open ? `${FRAME_WIDTH}px` : '0px'; | |
| 283 | - const ml = open ? `${SIDEBAR_WIDTH}px` : '0px'; | |
| 284 | - | |
| 285 | 316 | applyScaling(); |
| 286 | 317 | |
| 287 | 318 | // External contract: no in-repo listener by design — lets host-page |
| 288 | 319 | // consumers react to the agent reflowing the viewport. |
| @@ -291,20 +322,11 @@ | ||
| 291 | 322 | detail: { open }, |
| 292 | 323 | }), |
| 293 | 324 | ); |
| 294 | 325 | |
| 295 | - if (wpadminbar) { | |
| 296 | - Object.assign(wpadminbar.style, { | |
| 297 | - marginTop: fw, | |
| 298 | - marginRight: fw, | |
| 299 | - marginBottom: '0px', | |
| 300 | - marginLeft: ml, | |
| 301 | - borderRadius: open ? '8px 8px 0 0' : '0', | |
| 302 | - maxWidth: open | |
| 303 | - ? `calc(100% - ${SIDEBAR_WIDTH + FRAME_WIDTH}px)` | |
| 304 | - : '100%', | |
| 305 | - }); | |
| 306 | - } | |
| 326 | + // agent.css carries the bar offsets, so they drop with the | |
| 327 | + // breakpoint rather than with this component. | |
| 328 | + document.documentElement.classList.toggle('extendify-agent-docked', open); | |
| 307 | 329 | }); |
| 308 | 330 | |
| 309 | 331 | window.addEventListener('resize', applyScaling); |
| 310 | 332 | |