diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index fcc41c20..43452994 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -4022,43 +4022,36 @@ body.objectKanbanColumnResizing * {
}
.dashboard-tab-pane {
+ position: absolute;
+ top: 0;
+ left: 0;
display: flex;
flex-direction: column;
- height: 100%;
+ box-sizing: border-box;
width: 100%;
+ height: 100%;
+ min-width: 0;
min-height: 0;
}
.dashboard-tab-pane-active {
- position: relative;
- left: 0;
z-index: 1;
+ pointer-events: auto;
+ transform: none;
}
.dashboard-tab-pane-inactive {
- position: absolute;
- top: 0;
- left: -100000px;
z-index: 0;
+ overflow: hidden;
pointer-events: none;
+ visibility: hidden;
+ transform: translate(-100%, 0);
}
.dashboard-tab-pane-inactive.dashboard-tab-pane-capturing {
- position: fixed;
- top: 0;
- left: calc(-1 * var(--dashboard-tab-capture-width) - 48px);
- z-index: 2;
- box-sizing: border-box;
- width: var(--dashboard-tab-capture-width);
- height: var(--dashboard-tab-capture-height);
- min-width: var(--dashboard-tab-capture-width);
- min-height: var(--dashboard-tab-capture-height);
- max-width: var(--dashboard-tab-capture-width);
- max-height: var(--dashboard-tab-capture-height);
- overflow: hidden;
+ /* Stay translated off-screen so capture cannot cover the active tab.
+ Layout size stays 100% of the host for SnapDOM. */
visibility: visible;
- opacity: 1;
- pointer-events: none;
}
.dashboard-tab-preview-popover .ant-popover-inner {
@@ -4069,18 +4062,69 @@ body.objectKanbanColumnResizing * {
.dashboard-tab-preview {
position: relative;
width: 320px;
- height: var(--dashboard-tab-preview-height, 180px);
background: var(--tab-preview-render-background);
overflow: hidden;
padding: 8px;
}
-.dashboard-tab-preview-image {
+.dashboard-tab-preview-stack {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.dashboard-tab-preview-svg {
display: block;
width: 100%;
+ height: auto;
+ pointer-events: none;
+}
+
+.dashboard-tab-preview-svg-current {
+ position: relative;
+ z-index: 0;
+}
+
+.dashboard-tab-preview-svg-previous {
+ position: absolute;
+ inset: 0;
+ z-index: 1;
+ width: 100%;
height: 100%;
object-fit: cover;
object-position: top center;
+ filter: blur(8px);
+ opacity: 1;
+ transition:
+ opacity 280ms ease,
+ filter 280ms ease;
+}
+
+.dashboard-tab-preview-stack:has(.dashboard-tab-preview-loading):not(
+ :has(.dashboard-tab-preview-svg-current)
+ ) {
+ min-height: 120px;
+}
+
+.dashboard-tab-preview-svg-previous.is-revealing {
+ opacity: 0;
+ filter: blur(0);
+}
+
+.dashboard-tab-preview-loading {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+}
+
+.dashboard-tab-preview-loading-icon {
+ font-size: 16px;
+ color: var(--color-text-secondary, rgba(255, 255, 255, 0.92));
+ filter: drop-shadow(0 0 10px rgba(0, 0, 0, 0.45));
}
.dashboard-tab-preview-placeholder {
diff --git a/bun.lock b/bun.lock
index d9445e5c..93303303 100644
--- a/bun.lock
+++ b/bun.lock
@@ -43,6 +43,7 @@
"@tsparticles/slim": "^3.9.1",
"@uiw/react-codemirror": "^4.25.1",
"@vscode/sudo-prompt": "^9.3.2",
+ "@zumer/snapdom": "^3.0.0",
"antd": "^5.27.1",
"antd-style": "^3.7.1",
"axios": "^1.11.0",
@@ -57,7 +58,6 @@
"echarts-for-react": "^3.0.6",
"electron-store": "^11.0.2",
"gcode-preview": "3.0.0-alpha.6",
- "html2canvas-pro": "^2.4.3",
"json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0",
"lodash": "^4.17.23",
@@ -951,6 +951,8 @@
"@zeit/schemas": ["@zeit/schemas@2.36.0", "", {}, "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg=="],
+ "@zumer/snapdom": ["@zumer/snapdom@3.0.0", "", {}, "sha512-O+gqKYz+7545JNgG4Hzdy9/i+7/H38bDpF6tUOCJBqAUTm9npbaE3ZxuevFBcRi6cBLpvMqVYIjtywAZIP6OFQ=="],
+
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "3.0.2", "negotiator": "1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
@@ -1039,8 +1041,6 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
- "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
-
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.4", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ=="],
@@ -1201,8 +1201,6 @@
"css-color-keywords": ["css-color-keywords@1.0.0", "", {}, "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg=="],
- "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
-
"css-mediaquery": ["css-mediaquery@0.1.2", "", {}, "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "1.0.0", "css-what": "6.2.2", "domhandler": "5.0.3", "domutils": "3.2.2", "nth-check": "2.1.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
@@ -1579,8 +1577,6 @@
"html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
- "html2canvas-pro": ["html2canvas-pro@2.4.3", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-ov+EmxDc2abD2uKYpZdFWHkQkl3pu2fH0N98khT1Ojfqr3DJOuGyFIFTSfSyaWxddK9jSJu8sM7aUATxM26mDg=="],
-
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.2", "toidentifier": "1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
@@ -2527,8 +2523,6 @@
"terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "0.3.11", "acorn": "8.17.0", "commander": "2.20.3", "source-map-support": "0.5.21" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="],
- "text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
-
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
"three": ["three@0.179.1", "", {}, "sha512-5y/elSIQbrvKOISxpwXCR4sQqHtGiOI+MKLc3SsBdDXA2hz3Mdp3X59aUp8DyybMa34aeBwbFTpdoLJaUDEWSw=="],
@@ -2637,8 +2631,6 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
- "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
-
"validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
diff --git a/package.json b/package.json
index 95e281ea..08bbde66 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@tsparticles/slim": "^3.9.1",
"@uiw/react-codemirror": "^4.25.1",
"@vscode/sudo-prompt": "^9.3.2",
+ "@zumer/snapdom": "^3.0.0",
"antd": "^5.27.1",
"antd-style": "^3.7.1",
"axios": "^1.11.0",
@@ -61,7 +62,6 @@
"echarts-for-react": "^3.0.6",
"electron-store": "^11.0.2",
"gcode-preview": "3.0.0-alpha.6",
- "html2canvas-pro": "^2.4.3",
"json-schema-traverse": "^1.0.0",
"keycloak-js": "^26.2.0",
"lodash": "^4.17.23",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 982bbe58..ee9b4738 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -130,6 +130,9 @@ importers:
'@vscode/sudo-prompt':
specifier: ^9.3.2
version: 9.3.2
+ '@zumer/snapdom':
+ specifier: ^3.0.0
+ version: 3.0.0
antd:
specifier: ^5.27.1
version: 5.29.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -172,9 +175,6 @@ importers:
gcode-preview:
specifier: 3.0.0-alpha.6
version: 3.0.0-alpha.6
- html2canvas-pro:
- specifier: ^2.4.3
- version: 2.4.3
json-schema-traverse:
specifier: ^1.0.0
version: 1.0.0
@@ -2418,6 +2418,9 @@ packages:
'@zeit/schemas@2.36.0':
resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==}
+ '@zumer/snapdom@3.0.0':
+ resolution: {integrity: sha512-O+gqKYz+7545JNgG4Hzdy9/i+7/H38bDpF6tUOCJBqAUTm9npbaE3ZxuevFBcRi6cBLpvMqVYIjtywAZIP6OFQ==}
+
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
@@ -2638,10 +2641,6 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
- base64-arraybuffer@1.0.2:
- resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
- engines: {node: '>= 0.6.0'}
-
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -3017,9 +3016,6 @@ packages:
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
engines: {node: '>=4'}
- css-line-break@2.1.0:
- resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
-
css-mediaquery@0.1.2:
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
@@ -3969,10 +3965,6 @@ packages:
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
- html2canvas-pro@2.4.3:
- resolution: {integrity: sha512-ov+EmxDc2abD2uKYpZdFWHkQkl3pu2fH0N98khT1Ojfqr3DJOuGyFIFTSfSyaWxddK9jSJu8sM7aUATxM26mDg==}
- engines: {node: '>=16.0.0'}
-
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
@@ -6145,9 +6137,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
- text-segmentation@1.0.3:
- resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
-
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -6380,9 +6369,6 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- utrie@1.0.2:
- resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
-
validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
@@ -9014,6 +9000,8 @@ snapshots:
'@zeit/schemas@2.36.0': {}
+ '@zumer/snapdom@3.0.0': {}
+
abbrev@3.0.1: {}
accepts@2.0.0:
@@ -9337,8 +9325,6 @@ snapshots:
balanced-match@1.0.2: {}
- base64-arraybuffer@1.0.2: {}
-
base64-js@1.5.1: {}
baseline-browser-mapping@2.11.4: {}
@@ -9754,10 +9740,6 @@ snapshots:
css-color-keywords@1.0.0: {}
- css-line-break@2.1.0:
- dependencies:
- utrie: 1.0.2
-
css-mediaquery@0.1.2: {}
css-select@5.2.2:
@@ -11092,11 +11074,6 @@ snapshots:
html-url-attributes@3.0.1: {}
- html2canvas-pro@2.4.3:
- dependencies:
- css-line-break: 2.1.0
- text-segmentation: 1.0.3
-
http-cache-semantics@4.2.0: {}
http-errors@2.0.1:
@@ -13839,10 +13816,6 @@ snapshots:
source-map-support: 0.5.21
optional: true
- text-segmentation@1.0.3:
- dependencies:
- utrie: 1.0.2
-
text-table@0.2.0: {}
three@0.165.0: {}
@@ -14096,10 +14069,6 @@ snapshots:
util-deprecate@1.0.2: {}
- utrie@1.0.2:
- dependencies:
- base64-arraybuffer: 1.0.2
-
validate-npm-package-license@3.0.4:
dependencies:
spdx-correct: 3.2.0
diff --git a/src/components/Dashboard/common/DashboardTabPanes.jsx b/src/components/Dashboard/common/DashboardTabPanes.jsx
index 941b1582..18314d44 100644
--- a/src/components/Dashboard/common/DashboardTabPanes.jsx
+++ b/src/components/Dashboard/common/DashboardTabPanes.jsx
@@ -1,4 +1,5 @@
-import { useLayoutEffect, useRef } from 'react'
+import { memo, useLayoutEffect, useMemo, useRef } from 'react'
+import PropTypes from 'prop-types'
import classNames from 'classnames'
import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-router-dom'
import { TableStateProvider } from '../context/TableStateContext'
@@ -40,16 +41,65 @@ const CachedTabRouteLayout = () => (
)
+const arePanePropsEqual = (prev, next) =>
+ prev.tabId === next.tabId &&
+ prev.isActive === next.isActive &&
+ prev.registerTabPane === next.registerTabPane &&
+ prev.loc === next.loc
+
+const DashboardTabPane = memo(function DashboardTabPane({
+ tabId,
+ isActive,
+ loc,
+ registerTabPane
+}) {
+ const tabRoutes = useMemo(
+ () => wrapDashboardChildRoutes(),
+ []
+ )
+ const locationContextValue = useMemo(
+ () => ({ location: loc, navigationType: 'POP' }),
+ [loc]
+ )
+
+ return (
+
registerTabPane(tabId, element)}
+ data-tab-id={tabId}
+ className={classNames('dashboard-tab-pane', {
+ 'dashboard-tab-pane-active': isActive,
+ 'dashboard-tab-pane-inactive': !isActive
+ })}
+ aria-hidden={!isActive}
+ inert={!isActive}
+ >
+
+
+ {tabRoutes}
+
+
+
+ )
+}, arePanePropsEqual)
+
+DashboardTabPane.displayName = 'DashboardTabPane'
+
+DashboardTabPane.propTypes = {
+ tabId: PropTypes.string.isRequired,
+ isActive: PropTypes.bool,
+ loc: PropTypes.shape({
+ pathname: PropTypes.string,
+ search: PropTypes.string,
+ hash: PropTypes.string,
+ key: PropTypes.string,
+ state: PropTypes.any
+ }).isRequired,
+ registerTabPane: PropTypes.func.isRequired
+}
+
const DashboardTabPanes = () => {
const location = useLocation()
- const {
- tabs,
- activeTabId,
- isElectron,
- registerTabPane,
- previewCaptureLayout,
- reportPreviewCaptureLayout
- } = useNavigationTabs()
+ const { tabs, activeTabId, isElectron, registerTabPane } = useNavigationTabs()
const frozenRef = useRef(new Map())
const prevActiveIdRef = useRef(activeTabId)
const lastLocationRef = useRef(location)
@@ -80,35 +130,6 @@ const DashboardTabPanes = () => {
window.dispatchEvent(new Event('resize'))
}, [activeTabId, isElectron])
- useLayoutEffect(() => {
- if (!previewCaptureLayout?.tabId) return undefined
-
- const pane =
- document.querySelector(
- `.dashboard-tab-pane[data-tab-id="${CSS.escape(previewCaptureLayout.tabId)}"]`
- )
- if (!pane) return undefined
-
- void pane.offsetHeight
- window.dispatchEvent(new Event('resize'))
-
- let cancelled = false
- let secondFrame = 0
- const firstFrame = window.requestAnimationFrame(() => {
- secondFrame = window.requestAnimationFrame(() => {
- if (cancelled) return
- void pane.offsetHeight
- reportPreviewCaptureLayout(previewCaptureLayout.tabId, pane)
- })
- })
-
- return () => {
- cancelled = true
- window.cancelAnimationFrame(firstFrame)
- if (secondFrame) window.cancelAnimationFrame(secondFrame)
- }
- }, [previewCaptureLayout, reportPreviewCaptureLayout])
-
prevActiveIdRef.current = activeTabId
lastLocationRef.current = location
@@ -116,58 +137,32 @@ const DashboardTabPanes = () => {
return
}
- const capturingTabId = previewCaptureLayout?.tabId
- const capturingIndex = capturingTabId
- ? tabs.findIndex((tab) => tab.id === capturingTabId)
- : -1
- const renderTabs =
- capturingIndex >= 0 && capturingIndex < tabs.length - 1
- ? [...tabs.filter((tab) => tab.id !== capturingTabId), tabs[capturingIndex]]
- : tabs
+ const stackedTabs = activeTab
+ ? [...tabs.filter((tab) => tab.id !== activeTabId), activeTab]
+ : tabs
return (
- {renderTabs.map((tab) => {
+ {stackedTabs.map((tab) => {
const isActive = tab.id === activeTabId
- const isCapturing =
- !isActive && previewCaptureLayout?.tabId === tab.id
const loc =
isActive && !pendingRestoreRef.current
? 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 (
-
registerTabPane(tab.id, element)}
- data-tab-id={tab.id}
- className={classNames('dashboard-tab-pane', {
- 'dashboard-tab-pane-active': isActive,
- 'dashboard-tab-pane-inactive': !isActive,
- 'dashboard-tab-pane-capturing': isCapturing
- })}
- style={
- isCapturing
- ? {
- '--dashboard-tab-capture-width': `${previewCaptureLayout.width}px`,
- '--dashboard-tab-capture-height': `${previewCaptureLayout.height}px`
- }
- : undefined
- }
- aria-hidden={!isActive}
- inert={!isActive && !isCapturing}
- >
-
-
-
- {wrapDashboardChildRoutes()}
-
-
-
-
+ tabId={tab.id}
+ isActive={isActive}
+ loc={loc}
+ registerTabPane={registerTabPane}
+ />
)
})}
diff --git a/src/components/Dashboard/common/DashboardTabPreview.jsx b/src/components/Dashboard/common/DashboardTabPreview.jsx
index 694eeb22..0d01b0f3 100644
--- a/src/components/Dashboard/common/DashboardTabPreview.jsx
+++ b/src/components/Dashboard/common/DashboardTabPreview.jsx
@@ -1,28 +1,73 @@
+import { useLayoutEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
+import classNames from 'classnames'
+import { LoadingOutlined } from '@ant-design/icons'
-const DEFAULT_HEIGHT = 180
+const DashboardTabPreview = ({ src, loading = false }) => {
+ const lastSrcRef = useRef(src || null)
+ const [overlaySrc, setOverlaySrc] = useState(src || null)
+ const [revealing, setRevealing] = useState(false)
-const DashboardTabPreview = ({ src, height = DEFAULT_HEIGHT }) => {
- const previewHeight = typeof height === 'number' ? `${height}px` : height
+ useLayoutEffect(() => {
+ if (loading) {
+ const locked = lastSrcRef.current || src || null
+ lastSrcRef.current = locked
+ setOverlaySrc(locked)
+ setRevealing(false)
+ return
+ }
- if (!src) return null
+ if (src) {
+ lastSrcRef.current = src
+ }
+
+ setRevealing(true)
+ }, [loading, src])
+
+ const handleOverlayTransitionEnd = (event) => {
+ if (event.propertyName !== 'opacity' || !revealing) return
+ setOverlaySrc(null)
+ setRevealing(false)
+ }
return (
-
-

+
+
+ {src ? (
+

+ ) : null}
+ {overlaySrc ? (
+

+ ) : null}
+ {loading ? (
+
+
+
+ ) : null}
+
)
}
DashboardTabPreview.propTypes = {
src: PropTypes.string,
- height: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
+ loading: PropTypes.bool
}
export default DashboardTabPreview
diff --git a/src/components/Dashboard/common/DashboardTabs.jsx b/src/components/Dashboard/common/DashboardTabs.jsx
index 8411faa8..6a74f6c1 100644
--- a/src/components/Dashboard/common/DashboardTabs.jsx
+++ b/src/components/Dashboard/common/DashboardTabs.jsx
@@ -8,7 +8,7 @@ import PlusIcon from '../../Icons/PlusIcon'
import XMarkIcon from '../../Icons/XMarkIcon'
import HomeIcon from '../../Icons/HomeIcon'
import { getModelByName } from '../../../database/ObjectModels'
-import { useNavigationTabs } from '../context/NavigationTabsContext'
+import { useNavigationTabs, useTabPreview } from '../context/NavigationTabsContext'
import { getDesktopWindowId } from '../../../electrobun-bridge.js'
import { hasExternalTabDrag, writeTabDragData } from './tabDrag'
@@ -80,70 +80,31 @@ const DashboardTabItem = ({
onClose
}) => {
const { captureTabPreview } = useNavigationTabs()
+ const { src: previewSrc, capturing: previewCapturing } = useTabPreview(tab.id)
const [previewOpen, setPreviewOpen] = useState(false)
- const [previewSrc, setPreviewSrc] = useState(null)
- const previewHoverRef = useRef(false)
- const previewOpenRef = useRef(false)
- const previewRequestRef = useRef(0)
const previewTargetRef = useRef(null)
const model = tab.modelName ? getModelByName(tab.modelName) : null
const Icon = model?.icon || HomeIcon
- const closePreview = useCallback(() => {
- previewHoverRef.current = false
- previewOpenRef.current = false
- previewRequestRef.current += 1
- setPreviewOpen(false)
- setPreviewSrc(null)
- }, [])
-
- const startPreviewCapture = useCallback(() => {
- if (isSelected || isDragging) return
-
- previewHoverRef.current = true
- const requestId = previewRequestRef.current + 1
- previewRequestRef.current = requestId
- previewOpenRef.current = false
- setPreviewOpen(false)
- setPreviewSrc(null)
-
- void captureTabPreview(tab.id).then((dataUrl) => {
- if (
- !previewHoverRef.current ||
- previewRequestRef.current !== requestId ||
- !dataUrl
- ) {
- return
- }
- if (
- previewTargetRef.current &&
- !previewTargetRef.current.matches(':hover')
- ) {
- return
- }
- setPreviewSrc(dataUrl)
- previewOpenRef.current = true
- setPreviewOpen(true)
- })
- }, [captureTabPreview, isDragging, isSelected, tab.id])
-
const handlePreviewOpenChange = useCallback(
(nextOpen) => {
- if (!nextOpen) closePreview()
+ if (isSelected || isDragging) {
+ setPreviewOpen(false)
+ return
+ }
+ setPreviewOpen(nextOpen)
+ if (nextOpen) {
+ void captureTabPreview(tab.id)
+ }
},
- [closePreview]
+ [captureTabPreview, isDragging, isSelected, tab.id]
)
- const handlePreviewMouseLeave = useCallback(() => {
- if (previewOpenRef.current) return
- closePreview()
- }, [closePreview])
-
useEffect(() => {
if (isSelected || isDragging) {
- closePreview()
+ setPreviewOpen(false)
}
- }, [closePreview, isDragging, isSelected])
+ }, [isDragging, isSelected])
const tabButton = (