Compare commits

...

2 Commits

Author SHA1 Message Date
f1d155529a Improved carousel logic.
All checks were successful
homepanel/HomePanel/pipeline/head This commit looks good
2026-02-13 01:26:12 +00:00
2fc93a0dc3 Increased polling interval for better performance. 2026-02-13 01:09:35 +00:00
7 changed files with 221 additions and 58 deletions

View File

@ -12,18 +12,45 @@
left: 0;
width: 100%;
height: 100%;
transition: opacity ease-in-out;
will-change: opacity;
pointer-events: none;
}
.dashboard-carousel-active {
.dashboard-carousel-item.dashboard-carousel-visible {
opacity: 1;
z-index: 1;
pointer-events: auto;
will-change: auto;
}
.dashboard-carousel-inactive {
.dashboard-carousel-item.dashboard-carousel-enter {
opacity: 0;
z-index: 0;
pointer-events: none;
z-index: 2;
animation: dashboardCarouselFadeIn var(--dashboard-carousel-duration, 1000ms)
ease-in-out forwards;
}
.dashboard-carousel-item.dashboard-carousel-exit {
opacity: 1;
z-index: 1;
animation: dashboardCarouselFadeOut var(--dashboard-carousel-duration, 1000ms)
ease-in-out forwards;
}
@keyframes dashboardCarouselFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dashboardCarouselFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

View File

@ -15,9 +15,30 @@ function DashboardCarousel({
onChange,
onSave,
}) {
const { currentPanelIndex, setCurrentPanel, panelSelectorVisible } = useDevice();
const { currentPanelIndex, setCurrentPanel, panelSelectorVisible } =
useDevice();
const intervalRef = useRef(null);
const currentIndexRef = useRef(currentPanelIndex);
const timeoutRef = useRef(null);
const prevIndexRef = useRef(-1); // -1 = not yet initialized
const isTransitioningRef = useRef(false);
// Two-slot flip-flop: only render the current and transitioning panels
const [slot1, setSlot1] = useState(null);
const [slot2, setSlot2] = useState(null);
const [activeSlot, setActiveSlot] = useState(1); // 1 or 2
const [isTransitioning, setIsTransitioning] = useState(false);
// Initial sync: populate slot when we have panels and prevIndex is uninitialized
useEffect(() => {
if (!panels?.length || currentPanelIndex >= panels.length) return;
if (prevIndexRef.current >= 0) return; // Already initialized
prevIndexRef.current = currentPanelIndex;
const panel = panels[currentPanelIndex];
setSlot1({ panel, index: currentPanelIndex });
setActiveSlot(1);
}, [panels, currentPanelIndex]);
// Keep ref in sync with context value
useEffect(() => {
@ -31,48 +52,110 @@ function DashboardCarousel({
}
}, [panels, currentPanelIndex, setCurrentPanel]);
// Sync slots when panels array changes (e.g. panel reordered or replaced)
useEffect(() => {
if (!panels || panels.length === 0) return;
const currentPanel = panels[currentPanelIndex];
if (!currentPanel) return;
setSlot1((prev) => {
if (prev?.index === currentPanelIndex) {
return { panel: currentPanel, index: currentPanelIndex };
}
return prev;
});
setSlot2((prev) => {
if (prev?.index === currentPanelIndex) {
return { panel: currentPanel, index: currentPanelIndex };
}
return prev;
});
}, [panels, currentPanelIndex]);
// Handle index changes - transition between panels using two-slot pattern
useEffect(() => {
if (!panels || panels.length === 0) return;
if (currentPanelIndex >= panels.length) return;
const newPanel = panels[currentPanelIndex];
// No change
if (currentPanelIndex === prevIndexRef.current) {
return;
}
// Already transitioning - queue for after transition completes
if (isTransitioningRef.current) {
return;
}
// Clear any pending timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
isTransitioningRef.current = true;
setIsTransitioning(true);
prevIndexRef.current = currentPanelIndex;
// Flip-flop: put new panel in inactive slot
if (activeSlot === 1) {
setSlot2({ panel: newPanel, index: currentPanelIndex });
} else {
setSlot1({ panel: newPanel, index: currentPanelIndex });
}
timeoutRef.current = setTimeout(() => {
if (activeSlot === 1) {
setSlot1(null);
setActiveSlot(2);
} else {
setSlot2(null);
setActiveSlot(1);
}
isTransitioningRef.current = false;
console.log("transitioning", isTransitioning);
setIsTransitioning(false);
timeoutRef.current = null;
}, transitionDuration);
}, [currentPanelIndex, panels, activeSlot, slot1, slot2, transitionDuration]);
useEffect(() => {
if (!panels || panels.length === 0) return;
// Clear any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// If only one panel, don't create interval
if (panels.length <= 1) {
setCurrentPanel(0);
return;
}
// Don't create interval if carousel is disabled
if (!carouselEnabled) {
return;
}
if (!carouselEnabled) return;
if (panelSelectorVisible) return;
// Pause carousel if panel selector is visible
if (panelSelectorVisible) {
return;
}
// Set up interval to cycle through panels
intervalRef.current = setInterval(() => {
const nextIndex = (currentIndexRef.current + 1) % panels.length;
setCurrentPanel(nextIndex);
}, holdTime);
// Cleanup on unmount or dependency change
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [panels, holdTime, setCurrentPanel, panelSelectorVisible, carouselEnabled]);
}, [
panels,
holdTime,
setCurrentPanel,
panelSelectorVisible,
carouselEnabled,
]);
// Handle onChange callback for individual dashboards
const handleDashboardChange = (index, updatedPanel) => {
if (onChange) {
// Create updated panels array
const updatedPanels = [...panels];
updatedPanels[index] = updatedPanel;
onChange(updatedPanels);
@ -86,10 +169,55 @@ function DashboardCarousel({
}
};
// Get CSS classes for each slot (like AnimatedOutlet)
const getSlot1Class = () => {
if (!slot1) return "";
if (isTransitioning && activeSlot === 1) return "dashboard-carousel-exit";
if (isTransitioning && activeSlot === 2) return "dashboard-carousel-enter";
if (activeSlot === 1) return "dashboard-carousel-visible";
return "";
};
const getSlot2Class = () => {
if (!slot2) return "";
if (isTransitioning && activeSlot === 2) return "dashboard-carousel-exit";
if (isTransitioning && activeSlot === 1) return "dashboard-carousel-enter";
if (activeSlot === 2) return "dashboard-carousel-visible";
return "";
};
if (!panels || panels.length === 0) {
return null;
}
// Single panel: render directly without transition machinery
if (panels.length === 1) {
return (
<div
className="dashboard-carousel"
style={{
width,
height,
position: "relative",
"--dashboard-carousel-duration": `${transitionDuration}ms`,
}}
>
<div className="dashboard-carousel-item dashboard-carousel-visible">
<Dashboard
panel={panels[0]}
width="100%"
height="100%"
visible
bordered={bordered}
editMode={editMode}
onChange={(updatedPanel) => handleDashboardChange(0, updatedPanel)}
onSave={(savedPanel) => handleDashboardSave(0, savedPanel)}
/>
</div>
</div>
);
}
return (
<div
className="dashboard-carousel"
@ -97,37 +225,45 @@ function DashboardCarousel({
width,
height,
position: "relative",
"--dashboard-carousel-duration": `${transitionDuration}ms`,
}}
>
{panels.map((panel, index) => {
const isActive = index === currentPanelIndex;
return (
<div
key={panel?.id || index}
className={`dashboard-carousel-item ${
isActive
? "dashboard-carousel-active"
: "dashboard-carousel-inactive"
}`}
style={{
transitionDuration: `${transitionDuration}ms`,
}}
>
{slot1 && (
<div className={`dashboard-carousel-item ${getSlot1Class()}`}>
<Dashboard
panel={panel}
panel={slot1.panel}
width="100%"
height="100%"
visible={isActive}
visible={activeSlot === 1 || isTransitioning}
bordered={bordered}
editMode={editMode}
onChange={(updatedPanel) =>
handleDashboardChange(index, updatedPanel)
handleDashboardChange(slot1.index, updatedPanel)
}
onSave={(savedPanel) =>
handleDashboardSave(slot1.index, savedPanel)
}
onSave={(savedPanel) => handleDashboardSave(index, savedPanel)}
/>
</div>
);
})}
)}
{slot2 && (
<div className={`dashboard-carousel-item ${getSlot2Class()}`}>
<Dashboard
panel={slot2.panel}
width="100%"
height="100%"
visible={activeSlot === 2 || isTransitioning}
bordered={bordered}
editMode={editMode}
onChange={(updatedPanel) =>
handleDashboardChange(slot2.index, updatedPanel)
}
onSave={(savedPanel) =>
handleDashboardSave(slot2.index, savedPanel)
}
/>
</div>
)}
</div>
);
}

View File

@ -47,7 +47,7 @@ function AnimatedCollapse({ visible, children }) {
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkHeight, 150);
intervalId = setInterval(checkHeight, 100);
}, 100);
return () => {

View File

@ -77,7 +77,7 @@ function DashboardPanelSelector({ isOpen, onClose, onPanelSelect, animated = tru
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkHeight, 150);
intervalId = setInterval(checkHeight, 100);
}, 100);
return () => {

View File

@ -81,8 +81,8 @@ export const Entity = ({
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkWidth, 10);
}, 10);
intervalId = setInterval(checkWidth, 100);
}, 100);
return () => {
clearTimeout(startId);

View File

@ -97,7 +97,7 @@ const LegacySlideView = forwardRef(
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkHeight, 150);
intervalId = setInterval(checkHeight, 100);
}, 100);
return () => {
@ -439,7 +439,7 @@ const ModernSlideView = forwardRef(
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkHeight, 150);
intervalId = setInterval(checkHeight, 100);
}, 100);
return () => {

View File

@ -98,8 +98,8 @@ export const Widget = ({
// Start polling after a short delay to ensure element is rendered
const startId = setTimeout(() => {
intervalId = setInterval(checkWidth, 10);
}, 10);
intervalId = setInterval(checkWidth, 100);
}, 100);
return () => {
clearTimeout(startId);