- {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()
}
}