Refactor Dashboard Tab Functionality and Enhance Styles
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- 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.
This commit is contained in:
Tom Butcher 2026-09-18 23:45:50 +01:00
parent 0aee5b410f
commit 877db10c09
9 changed files with 350 additions and 113 deletions

View File

@ -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 {

View File

@ -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,13 +19,33 @@ const locationsEqual = (left, right) =>
(left.hash || '') === (right.hash || '')
)
const entryToLocation = (entry, fallbackLocation, tabId) => ({
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,
key: `tab-${tabId}`
})
state: fallbackLocation.state
})
const getTabCurrentEntry = (tab) =>
tab?.history?.[tab.historyIndex] || tab?.history?.[tab.history?.length - 1]
@ -41,18 +62,19 @@ const CachedTabRouteLayout = () => (
</TableStateProvider>
)
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(<CachedTabRouteLayout />),
[]
@ -62,6 +84,40 @@ const DashboardTabPane = memo(function DashboardTabPane({
[loc]
)
return (
<UNSAFE_LocationContext.Provider value={locationContextValue}>
<Routes location={loc}>{tabRoutes}</Routes>
</UNSAFE_LocationContext.Provider>
)
}, (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 (
<div
ref={(element) => registerTabPane(tabId, element)}
@ -73,10 +129,8 @@ const DashboardTabPane = memo(function DashboardTabPane({
aria-hidden={!isActive}
inert={!isActive}
>
<NavigationTabActiveContext.Provider value={isActive}>
<UNSAFE_LocationContext.Provider value={locationContextValue}>
<Routes location={loc}>{tabRoutes}</Routes>
</UNSAFE_LocationContext.Provider>
<NavigationTabActiveContext.Provider value={storeRef.current}>
<DashboardTabPaneRoutes loc={loc} />
</NavigationTabActiveContext.Provider>
</div>
)
@ -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 <Outlet />
}
const stackedTabs = activeTab
? [...tabs.filter((tab) => tab.id !== activeTabId), activeTab]
: tabs
return (
<div className='dashboard-tab-panes'>
{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)
}
return (
<DashboardTabPane

View File

@ -2,27 +2,38 @@ import { useLayoutEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { LoadingOutlined } from '@ant-design/icons'
import { useThemeContext } from '../context/ThemeContext'
const DashboardTabPreview = ({ src, loading = false }) => {
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 (
<div className='dashboard-tab-preview'>
<div
className={classNames('dashboard-tab-preview', {
'dark-mode': isDarkMode
})}
>
<div className='dashboard-tab-preview-stack'>
{src ? (
{usableSrc ? (
<img
src={src}
src={usableSrc}
alt=''
className='dashboard-tab-preview-svg dashboard-tab-preview-svg-current'
/>
@ -67,6 +82,7 @@ const DashboardTabPreview = ({ src, loading = false }) => {
DashboardTabPreview.propTypes = {
src: PropTypes.string,
srcTheme: PropTypes.oneOf(['dark', 'light']),
loading: PropTypes.bool
}

View File

@ -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={
<DashboardTabPreview src={previewSrc} loading={previewCapturing} />
<DashboardTabPreview
src={previewSrc}
srcTheme={previewTheme}
loading={previewCapturing}
/>
}
classNames={{ root: 'dashboard-tab-preview-popover' }}
styles={{ body: { padding: 0, overflow: 'hidden' } }}

View File

@ -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
const sync = () => {
if (!tabActiveStore.getSnapshot()) return
setCurrentObjectType(modelName)
if (objectId) {
setCurrentObject({ _id: objectId, _user: userProfile })
}
}
sync()
const unsubscribe = tabActiveStore.subscribe(sync)
return () => {
unsubscribe()
if (tabActiveStore.getSnapshot()) {
setCurrentObject(null)
setCurrentObjectType(null)
}
}
}, [
isTabActive,
modelName,
objectId,
setCurrentObject,
setCurrentObjectType,
tabActiveStore,
userProfile
])

View File

@ -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
const sync = () => {
if (!store.getSnapshot()) return
setCurrentObjectTools(tools, ownerIdRef.current)
}, [isTabActive, tools, setCurrentObjectTools])
}
sync()
return store.subscribe(sync)
}, [setCurrentObjectTools, store, tools])
useEffect(() => {
const ownerId = ownerIdRef.current

View File

@ -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 (
<NavigationTabsContext.Provider value={value}>
<NavigationTabMetaContext.Provider value={metaValue}>
<NavigationTabPreviewsContext.Provider value={previewsValue}>
{children}
</NavigationTabPreviewsContext.Provider>
</NavigationTabMetaContext.Provider>
</NavigationTabsContext.Provider>
)
}
@ -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
if (!isElectron || !title) return undefined
const sync = () => {
if (!store.getSnapshot()) return
setTabPage({ title, modelName })
}, [isElectron, isTabActive, modelName, setTabPage, title])
}
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

View File

@ -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(

View File

@ -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,30 +85,7 @@ export const waitForTabPreviewLayout = async (pane) => {
return pane.clientWidth >= 8 && pane.clientHeight >= 8
}
const preparePaneForCapture = (pane) => {
const previousInert = pane.inert
pane.classList.add(CAPTURE_CLASS)
pane.inert = false
void pane.offsetHeight
return () => {
pane.classList.remove(CAPTURE_CLASS)
pane.inert = previousInert
}
}
const tabPreviewPlugin = {
name: 'tab-preview-layout',
afterClone(ctx) {
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 resetCloneLayout = (element) => {
const styles = element.style
styles.position = 'relative'
styles.inset = 'auto'
@ -122,20 +101,78 @@ const tabPreviewPlugin = {
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 {
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
}
}
}
const tabPreviewPlugin = {
name: 'tab-preview-layout',
afterClone(ctx) {
const element = ctx.clone
if (!(element instanceof HTMLElement)) return
const pane = element.classList.contains('dashboard-tab-pane')
? element
: element.querySelector('.dashboard-tab-pane')
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()
}
}