From 877db10c0958a690ad31fa86bf6ae5b27293362e Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Fri, 18 Sep 2026 23:45:50 +0100 Subject: [PATCH] Refactor Dashboard Tab Functionality and Enhance Styles - Introduced a new store for managing active tab states, improving the responsiveness of tab interactions. - Updated DashboardTabPanes and DashboardTabPreview components to utilize the new active state management, enhancing user experience. - Enhanced CSS styles for dashboard tab previews and panes, ensuring better layout handling and visual consistency across themes. - Improved tab preview capture logic to support dynamic theme changes, refining the overall rendering process. - Adjusted various components to streamline the integration of tab previews and active state management. --- assets/stylesheets/App.css | 65 +++++++-- .../Dashboard/common/DashboardTabPanes.jsx | 123 +++++++++++++----- .../Dashboard/common/DashboardTabPreview.jsx | 36 +++-- .../Dashboard/common/DashboardTabs.jsx | 12 +- src/components/Dashboard/common/ModelPage.jsx | 26 ++-- .../context/DashboardObjectToolsContext.jsx | 15 ++- .../context/NavigationTabsContext.jsx | 95 +++++++++++--- .../Dashboard/context/ThemeContext.jsx | 2 +- .../Dashboard/context/tabPreviewCapture.js | 89 +++++++++---- 9 files changed, 350 insertions(+), 113 deletions(-) diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index cacabb2e..8ffcf817 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -3647,7 +3647,8 @@ body.objectKanbanColumnResizing * { overflow: visible; margin: 5px 0 0 0; padding: 0 10px 0 10px; - border: 1px solid var(--color-list-view-tabs-border); + border: 1px solid + color-mix(in srgb, var(--color-header-border) 55%, transparent); border-radius: 12px; background: transparent; cursor: pointer; @@ -3739,7 +3740,7 @@ body.objectKanbanColumnResizing * { .dashboard-tab-item-outline { position: absolute; - inset: 0 0 20px 0; + inset: 0 0 21px 0; border: 1px solid var(--color-header-border); border-bottom: none; border-radius: 12px 12px 0 0; @@ -4047,38 +4048,84 @@ body.objectKanbanColumnResizing * { .dashboard-tab-pane-inactive { z-index: 0; overflow: hidden; - display: none; visibility: hidden; opacity: 0; + pointer-events: none; } .dashboard-tab-pane-inactive.dashboard-tab-pane-capturing { - /* Preview render: show and size the pane, but keep it off-screen. */ - display: flex; - flex-direction: column; + /* Preview render: keep layout, but park the pane off-screen. */ visibility: visible; opacity: 1; + pointer-events: none; transform: translate(-100%, 0); z-index: 0; } +.dashboard-tab-preview-capture-frame { + position: absolute; + inset: 0; + z-index: 0; + box-sizing: border-box; + display: flex; + flex-direction: column; + padding: 24px; + background: var(--tab-preview-render-background); + transform: translate(-100%, 0); + pointer-events: none; +} + +.dashboard-tab-preview-capture-frame > .dashboard-tab-pane { + position: relative; + inset: auto; + flex: 1 1 auto; + width: 100%; + height: auto; + min-height: 0; + transform: none; +} + +.dashboard-tab-preview-capture-frame + > .dashboard-tab-pane-inactive.dashboard-tab-pane-capturing { + transform: none; +} + +.dashboard-tab-preview-popover { + padding-top: 6px; +} + .dashboard-tab-preview-popover .ant-popover-inner { padding: 0; overflow: hidden; + background: transparent; } .dashboard-tab-preview { position: relative; width: 320px; - background: var(--tab-preview-render-background); + background: var(--layout-header-bg); overflow: hidden; - padding: 8px; + padding: 6px; + border: 1px solid transparent; + border-radius: 12px; +} + +.dark-mode.dashboard-tab-preview { + border: 1px solid var(--color-header-border); +} + +.dark-mode.dashboard-tab-preview:before { + content: ''; + position: absolute; + inset: 1px; + background: var(--tab-preview-render-background); } .dashboard-tab-preview-stack { position: relative; width: 100%; overflow: hidden; + border-radius: 7px; } .dashboard-tab-preview-svg { @@ -4111,7 +4158,7 @@ body.objectKanbanColumnResizing * { .dashboard-tab-preview-stack:has(.dashboard-tab-preview-loading):not( :has(.dashboard-tab-preview-svg-current) ) { - min-height: 120px; + min-height: 160px; } .dashboard-tab-preview-svg-previous.is-revealing { diff --git a/src/components/Dashboard/common/DashboardTabPanes.jsx b/src/components/Dashboard/common/DashboardTabPanes.jsx index 18314d44..85c82935 100644 --- a/src/components/Dashboard/common/DashboardTabPanes.jsx +++ b/src/components/Dashboard/common/DashboardTabPanes.jsx @@ -5,6 +5,7 @@ import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-route import { TableStateProvider } from '../context/TableStateContext' import { NavigationTabActiveContext, + createNavigationTabActiveStore, useNavigationTabs } from '../context/NavigationTabsContext' import { wrapDashboardChildRoutes } from './DashboardChildRoutes' @@ -18,14 +19,34 @@ const locationsEqual = (left, right) => (left.hash || '') === (right.hash || '') ) -const entryToLocation = (entry, fallbackLocation, tabId) => ({ - pathname: entry?.pathname || fallbackLocation.pathname, - search: entry?.search ?? fallbackLocation.search ?? '', - hash: entry?.hash ?? fallbackLocation.hash ?? '', - state: fallbackLocation.state, +const toTabLocation = (tabId, source) => ({ + pathname: source?.pathname || '/', + search: source?.search ?? '', + hash: source?.hash ?? '', + state: source?.state, key: `tab-${tabId}` }) +const reuseTabLocation = (tabId, previous, next) => { + if ( + previous && + locationsEqual(previous, next) && + previous.key === `tab-${tabId}` && + previous.state === next.state + ) { + return previous + } + return toTabLocation(tabId, next) +} + +const entryToLocation = (entry, fallbackLocation, tabId) => + toTabLocation(tabId, { + pathname: entry?.pathname || fallbackLocation.pathname, + search: entry?.search ?? fallbackLocation.search ?? '', + hash: entry?.hash ?? fallbackLocation.hash ?? '', + state: fallbackLocation.state + }) + const getTabCurrentEntry = (tab) => tab?.history?.[tab.historyIndex] || tab?.history?.[tab.history?.length - 1] @@ -41,18 +62,19 @@ const CachedTabRouteLayout = () => ( ) +const areLocationsEqual = (left, right) => + left === right || + (locationsEqual(left, right) && + left?.key === right?.key && + left?.state === right?.state) + const arePanePropsEqual = (prev, next) => prev.tabId === next.tabId && prev.isActive === next.isActive && prev.registerTabPane === next.registerTabPane && - prev.loc === next.loc + areLocationsEqual(prev.loc, next.loc) -const DashboardTabPane = memo(function DashboardTabPane({ - tabId, - isActive, - loc, - registerTabPane -}) { +const DashboardTabPaneRoutes = memo(function DashboardTabPaneRoutes({ loc }) { const tabRoutes = useMemo( () => wrapDashboardChildRoutes(), [] @@ -62,6 +84,40 @@ const DashboardTabPane = memo(function DashboardTabPane({ [loc] ) + return ( + + {tabRoutes} + + ) +}, (prev, next) => areLocationsEqual(prev.loc, next.loc)) + +DashboardTabPaneRoutes.displayName = 'DashboardTabPaneRoutes' + +DashboardTabPaneRoutes.propTypes = { + loc: PropTypes.shape({ + pathname: PropTypes.string, + search: PropTypes.string, + hash: PropTypes.string, + key: PropTypes.string, + state: PropTypes.any + }).isRequired +} + +const DashboardTabPane = memo(function DashboardTabPane({ + tabId, + isActive, + loc, + registerTabPane +}) { + const storeRef = useRef(null) + if (storeRef.current == null) { + storeRef.current = createNavigationTabActiveStore(isActive) + } + + useLayoutEffect(() => { + storeRef.current.setActive(isActive) + }, [isActive]) + return (
registerTabPane(tabId, element)} @@ -73,10 +129,8 @@ const DashboardTabPane = memo(function DashboardTabPane({ aria-hidden={!isActive} inert={!isActive} > - - - {tabRoutes} - + +
) @@ -99,7 +153,7 @@ DashboardTabPane.propTypes = { const DashboardTabPanes = () => { const location = useLocation() - const { tabs, activeTabId, isElectron, registerTabPane } = useNavigationTabs() + const { tabs, activeTabId, registerTabPane } = useNavigationTabs() const frozenRef = useRef(new Map()) const prevActiveIdRef = useRef(activeTabId) const lastLocationRef = useRef(location) @@ -109,14 +163,28 @@ const DashboardTabPanes = () => { const switchedTabs = Boolean(prevActiveId && activeTabId && prevActiveId !== activeTabId) if (switchedTabs) { - frozenRef.current.set(prevActiveId, lastLocationRef.current) + frozenRef.current.set( + prevActiveId, + reuseTabLocation( + prevActiveId, + frozenRef.current.get(prevActiveId), + lastLocationRef.current + ) + ) pendingRestoreRef.current = true } const activeTab = tabs.find((tab) => tab.id === activeTabId) if (activeTab && tabMatchesLocation(activeTab, location)) { pendingRestoreRef.current = false - frozenRef.current.set(activeTabId, location) + frozenRef.current.set( + activeTabId, + reuseTabLocation( + activeTabId, + frozenRef.current.get(activeTabId), + location + ) + ) } for (const tabId of [...frozenRef.current.keys()]) { @@ -125,11 +193,6 @@ const DashboardTabPanes = () => { } } - useLayoutEffect(() => { - if (!isElectron) return - window.dispatchEvent(new Event('resize')) - }, [activeTabId, isElectron]) - prevActiveIdRef.current = activeTabId lastLocationRef.current = location @@ -137,23 +200,17 @@ const DashboardTabPanes = () => { return } - const stackedTabs = activeTab - ? [...tabs.filter((tab) => tab.id !== activeTabId), activeTab] - : tabs - return (
- {stackedTabs.map((tab) => { + {tabs.map((tab) => { const isActive = tab.id === activeTabId const loc = isActive && !pendingRestoreRef.current - ? location + ? reuseTabLocation(tab.id, frozenRef.current.get(tab.id), location) : frozenRef.current.get(tab.id) || entryToLocation(getTabCurrentEntry(tab), location, tab.id) - if (!isActive && !frozenRef.current.has(tab.id)) { - frozenRef.current.set(tab.id, loc) - } + frozenRef.current.set(tab.id, loc) return ( { - const lastSrcRef = useRef(src || null) - const [overlaySrc, setOverlaySrc] = useState(src || null) +const DashboardTabPreview = ({ src, srcTheme, loading = false }) => { + const { isDarkMode } = useThemeContext() + const currentTheme = isDarkMode ? 'dark' : 'light' + const usableSrc = src && srcTheme === currentTheme ? src : null + const lastSrcRef = useRef(usableSrc) + const [overlaySrc, setOverlaySrc] = useState(usableSrc) const [revealing, setRevealing] = useState(false) useLayoutEffect(() => { + if (srcTheme !== currentTheme) { + lastSrcRef.current = null + setOverlaySrc(null) + setRevealing(false) + return + } + if (loading) { - const locked = lastSrcRef.current || src || null + const locked = lastSrcRef.current || usableSrc || null lastSrcRef.current = locked setOverlaySrc(locked) setRevealing(false) return } - if (src) { - lastSrcRef.current = src + if (usableSrc) { + lastSrcRef.current = usableSrc } setRevealing(true) - }, [loading, src]) + }, [currentTheme, loading, srcTheme, usableSrc]) const handleOverlayTransitionEnd = (event) => { if (event.propertyName !== 'opacity' || !revealing) return @@ -31,11 +42,15 @@ const DashboardTabPreview = ({ src, loading = false }) => { } return ( -
+
- {src ? ( + {usableSrc ? ( @@ -67,6 +82,7 @@ const DashboardTabPreview = ({ src, loading = false }) => { DashboardTabPreview.propTypes = { src: PropTypes.string, + srcTheme: PropTypes.oneOf(['dark', 'light']), loading: PropTypes.bool } diff --git a/src/components/Dashboard/common/DashboardTabs.jsx b/src/components/Dashboard/common/DashboardTabs.jsx index 6a74f6c1..e63f6efa 100644 --- a/src/components/Dashboard/common/DashboardTabs.jsx +++ b/src/components/Dashboard/common/DashboardTabs.jsx @@ -80,7 +80,11 @@ const DashboardTabItem = ({ onClose }) => { const { captureTabPreview } = useNavigationTabs() - const { src: previewSrc, capturing: previewCapturing } = useTabPreview(tab.id) + const { + src: previewSrc, + theme: previewTheme, + capturing: previewCapturing + } = useTabPreview(tab.id) const [previewOpen, setPreviewOpen] = useState(false) const previewTargetRef = useRef(null) const model = tab.modelName ? getModelByName(tab.modelName) : null @@ -156,7 +160,11 @@ const DashboardTabItem = ({ open={isDragging ? false : previewOpen} onOpenChange={handlePreviewOpenChange} content={ - + } classNames={{ root: 'dashboard-tab-preview-popover' }} styles={{ body: { padding: 0, overflow: 'hidden' } }} diff --git a/src/components/Dashboard/common/ModelPage.jsx b/src/components/Dashboard/common/ModelPage.jsx index 4e824b60..16feef04 100644 --- a/src/components/Dashboard/common/ModelPage.jsx +++ b/src/components/Dashboard/common/ModelPage.jsx @@ -7,7 +7,7 @@ import { getModelByName } from '../../../database/ObjectModels' import { getObjectIdFromSearch } from '../../../utils/modelActions' import { useActions } from '../context/ActionsContext' import { AuthContext } from '../context/AuthContext' -import { useIsNavigationTabActive } from '../context/NavigationTabsContext' +import { NavigationTabActiveContext } from '../context/NavigationTabsContext' import { useObjectNavigationTabPage } from '../hooks/useObjectNavigationTabPage' const ModelPage = ({ modelName, pageName }) => { @@ -17,25 +17,33 @@ const ModelPage = ({ modelName, pageName }) => { const { setCurrentObject, setCurrentObjectType } = useActions() const { userProfile } = useContext(AuthContext) const objectId = getObjectIdFromSearch(modelName, location.search) - const isTabActive = useIsNavigationTabActive() + const tabActiveStore = useContext(NavigationTabActiveContext) useObjectNavigationTabPage({ modelName, pageName, objectId }) useEffect(() => { - if (!isTabActive) return undefined - setCurrentObjectType(modelName) - if (objectId) { - setCurrentObject({ _id: objectId, _user: userProfile }) + const sync = () => { + if (!tabActiveStore.getSnapshot()) return + setCurrentObjectType(modelName) + if (objectId) { + setCurrentObject({ _id: objectId, _user: userProfile }) + } } + + sync() + const unsubscribe = tabActiveStore.subscribe(sync) return () => { - setCurrentObject(null) - setCurrentObjectType(null) + unsubscribe() + if (tabActiveStore.getSnapshot()) { + setCurrentObject(null) + setCurrentObjectType(null) + } } }, [ - isTabActive, modelName, objectId, setCurrentObject, setCurrentObjectType, + tabActiveStore, userProfile ]) diff --git a/src/components/Dashboard/context/DashboardObjectToolsContext.jsx b/src/components/Dashboard/context/DashboardObjectToolsContext.jsx index 2975fdb3..6dfa0246 100644 --- a/src/components/Dashboard/context/DashboardObjectToolsContext.jsx +++ b/src/components/Dashboard/context/DashboardObjectToolsContext.jsx @@ -7,7 +7,7 @@ import { useState } from 'react' import PropTypes from 'prop-types' -import { useIsNavigationTabActive } from './NavigationTabsContext' +import { NavigationTabActiveContext } from './NavigationTabsContext' const DashboardObjectToolsContext = createContext() @@ -60,7 +60,7 @@ export const useDashboardObjectToolsContext = () => { export const useDashboardObjectTools = (tools) => { const { setCurrentObjectTools, clearCurrentObjectTools } = useDashboardObjectToolsContext() - const isTabActive = useIsNavigationTabActive() + const store = useContext(NavigationTabActiveContext) const ownerIdRef = useRef(null) if (ownerIdRef.current == null) { @@ -68,9 +68,14 @@ export const useDashboardObjectTools = (tools) => { } useEffect(() => { - if (!isTabActive) return - setCurrentObjectTools(tools, ownerIdRef.current) - }, [isTabActive, tools, setCurrentObjectTools]) + const sync = () => { + if (!store.getSnapshot()) return + setCurrentObjectTools(tools, ownerIdRef.current) + } + + sync() + return store.subscribe(sync) + }, [setCurrentObjectTools, store, tools]) useEffect(() => { const ownerId = ownerIdRef.current diff --git a/src/components/Dashboard/context/NavigationTabsContext.jsx b/src/components/Dashboard/context/NavigationTabsContext.jsx index 66198ca7..7c1bd53b 100644 --- a/src/components/Dashboard/context/NavigationTabsContext.jsx +++ b/src/components/Dashboard/context/NavigationTabsContext.jsx @@ -5,22 +5,51 @@ import { useEffect, useMemo, useRef, - useState + useState, + useSyncExternalStore } from 'react' import PropTypes from 'prop-types' import { useLocation, useNavigate } from 'react-router-dom' import { ElectronContext } from './ElectronContext' import { getDesktopWindowId } from '../../../electrobun-bridge.js' import { captureTabPaneImage } from './tabPreviewCapture' +import { useThemeContext } from './ThemeContext' const NavigationTabsContext = createContext() +const NavigationTabMetaContext = createContext({ + setTabPage: () => {}, + isElectron: false +}) const NavigationTabPreviewsContext = createContext({ tabPreviews: {}, tabPreviewCapturing: {} }) // eslint-disable-next-line react-refresh/only-export-components -export const NavigationTabActiveContext = createContext(true) +export const createNavigationTabActiveStore = (initial = true) => { + let isActive = Boolean(initial) + const listeners = new Set() + return { + getSnapshot: () => isActive, + setActive: (next) => { + const value = Boolean(next) + if (isActive === value) return + isActive = value + listeners.forEach((listener) => listener()) + }, + subscribe: (listener) => { + listeners.add(listener) + return () => listeners.delete(listener) + } + } +} + +const defaultNavigationTabActiveStore = createNavigationTabActiveStore(true) + +// eslint-disable-next-line react-refresh/only-export-components +export const NavigationTabActiveContext = createContext( + defaultNavigationTabActiveStore +) const createTabId = () => `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` @@ -75,6 +104,7 @@ export const NavigationTabsProvider = ({ children }) => { completeTabDrop, cancelTabDrag } = useContext(ElectronContext) + const { isDarkMode } = useThemeContext() const [tabs, setTabs] = useState([]) const [activeTabId, setActiveTabId] = useState(null) @@ -88,8 +118,10 @@ export const NavigationTabsProvider = ({ children }) => { const tabPaneElsRef = useRef(new Map()) const captureQueueRef = useRef(Promise.resolve()) const captureInFlightRef = useRef(new Map()) + const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light') tabsRef.current = tabs activeTabIdRef.current = activeTabId + previewThemeRef.current = isDarkMode ? 'dark' : 'light' const restoreToEntry = useCallback( (entry) => { @@ -300,11 +332,14 @@ export const NavigationTabsProvider = ({ children }) => { try { const dataUrl = await captureTabPaneImage(pane) if (dataUrl) { - setTabPreviews((current) => - current[tabId] === dataUrl - ? current - : { ...current, [tabId]: dataUrl } - ) + const theme = previewThemeRef.current + setTabPreviews((current) => { + const existing = current[tabId] + if (existing?.src === dataUrl && existing?.theme === theme) { + return current + } + return { ...current, [tabId]: { src: dataUrl, theme } } + }) } return dataUrl } catch (error) { @@ -591,12 +626,21 @@ export const NavigationTabsProvider = ({ children }) => { }), [tabPreviewCapturing, tabPreviews] ) + const metaValue = useMemo( + () => ({ + setTabPage, + isElectron + }), + [isElectron, setTabPage] + ) return ( - - {children} - + + + {children} + + ) } @@ -617,26 +661,41 @@ export const useTabPreview = (tabId) => { const { tabPreviews, tabPreviewCapturing } = useContext( NavigationTabPreviewsContext ) + const preview = tabId ? tabPreviews[tabId] : null return { - src: tabId ? tabPreviews[tabId] || null : null, + src: preview?.src || null, + theme: preview?.theme || null, capturing: Boolean(tabId && tabPreviewCapturing[tabId]) } } // eslint-disable-next-line react-refresh/only-export-components export const useNavigationTabPage = ({ title, modelName } = {}) => { - const { setTabPage, isElectron } = useNavigationTabs() - const isTabActive = useContext(NavigationTabActiveContext) + const { setTabPage, isElectron } = useContext(NavigationTabMetaContext) + const store = useContext(NavigationTabActiveContext) useEffect(() => { - if (!isElectron || !title || !isTabActive) return - setTabPage({ title, modelName }) - }, [isElectron, isTabActive, modelName, setTabPage, title]) + if (!isElectron || !title) return undefined + + const sync = () => { + if (!store.getSnapshot()) return + setTabPage({ title, modelName }) + } + + sync() + return store.subscribe(sync) + }, [isElectron, modelName, setTabPage, store, title]) } // eslint-disable-next-line react-refresh/only-export-components -export const useIsNavigationTabActive = () => - useContext(NavigationTabActiveContext) +export const useIsNavigationTabActive = () => { + const store = useContext(NavigationTabActiveContext) + return useSyncExternalStore( + store.subscribe, + store.getSnapshot, + store.getSnapshot + ) +} NavigationTabsProvider.propTypes = { children: PropTypes.node.isRequired diff --git a/src/components/Dashboard/context/ThemeContext.jsx b/src/components/Dashboard/context/ThemeContext.jsx index 15840c45..9313a565 100644 --- a/src/components/Dashboard/context/ThemeContext.jsx +++ b/src/components/Dashboard/context/ThemeContext.jsx @@ -177,7 +177,7 @@ export const ThemeProvider = ({ children }) => { ) root.style.setProperty( '--tab-preview-render-background', - isDarkMode ? '#000000' : '#ffffff' + isDarkMode ? '#000000' : '#E9E9E9' ) root.style.setProperty('--color-text', isDarkMode ? '#ffffff' : '#000000') root.style.setProperty( diff --git a/src/components/Dashboard/context/tabPreviewCapture.js b/src/components/Dashboard/context/tabPreviewCapture.js index 0d6227dd..3107b904 100644 --- a/src/components/Dashboard/context/tabPreviewCapture.js +++ b/src/components/Dashboard/context/tabPreviewCapture.js @@ -2,6 +2,8 @@ import { snapdom } from '@zumer/snapdom' const FONT_TIMEOUT_MS = 180 const CAPTURE_CLASS = 'dashboard-tab-pane-capturing' +const CAPTURE_FRAME_CLASS = 'dashboard-tab-preview-capture-frame' +const CAPTURE_PADDING_PX = 24 const PREVIEW_EXCLUDE = [ '.ant-popover', @@ -83,15 +85,56 @@ export const waitForTabPreviewLayout = async (pane) => { return pane.clientWidth >= 8 && pane.clientHeight >= 8 } +const resetCloneLayout = (element) => { + const styles = element.style + styles.position = 'relative' + styles.inset = 'auto' + styles.left = '0' + styles.top = '0' + styles.right = 'auto' + styles.bottom = 'auto' + styles.zIndex = '2' + styles.display = 'flex' + styles.flexDirection = 'column' + styles.opacity = '1' + styles.visibility = 'visible' + styles.translate = 'none' + styles.transform = 'none' + styles.overflow = 'hidden' +} + const preparePaneForCapture = (pane) => { const previousInert = pane.inert + const parent = pane.parentNode + const nextSibling = pane.nextSibling + const frame = document.createElement('div') + + frame.className = CAPTURE_FRAME_CLASS + frame.style.padding = `${CAPTURE_PADDING_PX}px` + if (parent) { + parent.insertBefore(frame, pane) + frame.appendChild(pane) + } + pane.classList.add(CAPTURE_CLASS) pane.inert = false + void frame.offsetHeight void pane.offsetHeight - return () => { - pane.classList.remove(CAPTURE_CLASS) - pane.inert = previousInert + return { + captureTarget: pane.parentNode === frame ? frame : pane, + restore: () => { + if (pane.parentNode === frame) { + if (nextSibling && nextSibling.parentNode === parent) { + parent.insertBefore(pane, nextSibling) + } else if (parent) { + parent.appendChild(pane) + } + } + frame.remove() + pane.classList.remove(CAPTURE_CLASS) + pane.inert = previousInert + } } } @@ -101,41 +144,35 @@ const tabPreviewPlugin = { const element = ctx.clone if (!(element instanceof HTMLElement)) return - element.classList.remove( - 'dashboard-tab-pane-inactive', - CAPTURE_CLASS - ) - element.classList.add('dashboard-tab-pane-active') + const pane = element.classList.contains('dashboard-tab-pane') + ? element + : element.querySelector('.dashboard-tab-pane') - const styles = element.style - styles.position = 'relative' - styles.inset = 'auto' - styles.left = '0' - styles.top = '0' - styles.right = 'auto' - styles.bottom = 'auto' - styles.zIndex = '2' - styles.display = 'flex' - styles.flexDirection = 'column' - styles.opacity = '1' - styles.visibility = 'visible' - styles.translate = 'none' - styles.transform = 'none' - styles.overflow = 'hidden' + resetCloneLayout(element) + + if (!(pane instanceof HTMLElement)) return + + pane.classList.remove('dashboard-tab-pane-inactive', CAPTURE_CLASS) + pane.classList.add('dashboard-tab-pane-active') + resetCloneLayout(pane) + pane.style.flex = '1 1 auto' + pane.style.width = '100%' + pane.style.height = 'auto' + pane.style.minHeight = '0' } } export const captureTabPaneImage = async (pane) => { if (!pane) return null - const restorePane = preparePaneForCapture(pane) + const { captureTarget, restore } = preparePaneForCapture(pane) try { const ready = await waitForTabPreviewLayout(pane) if (!ready) return null const backgroundColor = getPreviewBackgroundColor(pane) - const jpg = await snapdom.toJpg(pane, { + const jpg = await snapdom.toJpg(captureTarget, { backgroundColor, scale: 1, embedFonts: 'auto', @@ -152,6 +189,6 @@ export const captureTabPaneImage = async (pane) => { console.error('Failed to capture tab preview', error) return null } finally { - restorePane() + restore() } }