Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e2b971e
Fixes sidebar animating to initial state
simonbs Jan 5, 2026
c7477e0
Simplifies sidebar handling
simonbs Jan 5, 2026
99c1cc6
Fixes linting errors
simonbs Jan 5, 2026
4c2fe98
Introduces useSessionStorageState
simonbs Jan 5, 2026
fa3000c
Fixes linting warnings
simonbs Jan 5, 2026
9a8f671
Update src/features/sidebar/data/useCloseSidebarOnSelection.ts
simonbs Jan 5, 2026
3b25bd1
Update src/features/sidebar/view/internal/secondary/Container.tsx
simonbs Jan 5, 2026
5b89d70
Update src/features/sidebar/view/internal/ClientSplitView.tsx
simonbs Jan 5, 2026
4c7fee0
Update src/features/sidebar/view/internal/secondary/Container.tsx
simonbs Jan 5, 2026
506a5e9
Uses cancelAnimationFrame()
simonbs Jan 5, 2026
8473e41
Generalizes useSidebarOpen and useDiffbarOpen
simonbs Jan 5, 2026
cbcae8b
Merge branch 'develop' into bugfix/sidebars-animating
simonbs Jan 6, 2026
8650e4f
Clean up
simonbs Jan 6, 2026
4f7154b
Fixes incorrect state
simonbs Jan 6, 2026
0f35cbc
Renames sidebarTransitionDuration
simonbs Jan 6, 2026
7e1b0b5
Simplifies transition state handling
simonbs Jan 6, 2026
64ea18c
Fixes linting error
simonbs Jan 6, 2026
428842e
Update useCloseSidebarOnSelection.ts
simonbs Jan 6, 2026
5a25165
Fix missing newline at end of Container.tsx
simonbs Jan 6, 2026
9043c81
Reduces changes
simonbs Jan 6, 2026
b788fc6
Reduces changes
simonbs Jan 6, 2026
4703c48
Reduces changes
simonbs Jan 6, 2026
3677dd3
Removes useContext from styled component
simonbs Jan 6, 2026
5847874
Improves formatting
simonbs Jan 6, 2026
6e613df
Fixes spacing
simonbs Jan 6, 2026
fb9afea
Adds comment
simonbs Jan 6, 2026
4f30cc2
Update src/features/sidebar/view/internal/useClientSplitViewTransitio…
simonbs Jan 6, 2026
fc42f7e
Merge pull request #648 from shapehq/bugfix/sidebars-animating
simonbs Jan 6, 2026
269b45a
Hide diff-related UI elements when diff feature is disabled
ulrikandersen Jan 6, 2026
e544e04
Merge pull request #651 from shapehq/bugfix/hide-diff-ui-when-disabled
ulrikandersen Jan 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/features/projects/view/toolbar/MobileToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Stack } from "@mui/material"
import Selector from "./Selector"
import { useProjectSelection } from "../../data"

const isDiffFeatureEnabled = process.env.NEXT_PUBLIC_ENABLE_DIFF_SIDEBAR === "true"

const MobileToolbar = () => {
const {
project,
Expand Down Expand Up @@ -31,7 +33,7 @@ const MobileToolbar = () => {
items={version.specifications.map(spec => ({
id: spec.id,
name: spec.name,
hasChanges: !!spec.diffURL
hasChanges: isDiffFeatureEnabled && !!spec.diffURL
}))}
selection={specification.id}
onSelect={selectSpecification}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faPenToSquare } from "@fortawesome/free-regular-svg-icons"
import { useProjectSelection } from "../../data"

const isDiffFeatureEnabled = process.env.NEXT_PUBLIC_ENABLE_DIFF_SIDEBAR === "true"

const TrailingToolbarItem = () => {
const {
project,
Expand Down Expand Up @@ -62,7 +64,7 @@ const TrailingToolbarItem = () => {
items={version.specifications.map(spec => ({
id: spec.id,
name: spec.name,
hasChanges: !!spec.diffURL
hasChanges: isDiffFeatureEnabled && !!spec.diffURL
}))}
selection={specification.id}
onSelect={selectSpecification}
Expand Down
4 changes: 3 additions & 1 deletion src/features/sidebar/view/SecondarySplitHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const SecondarySplitHeader = ({
<Box sx={{ position: "relative", flexGrow: 1, overflow: "hidden", minWidth: 0, height: 40 }}>
<Stack direction="row" alignItems="center" sx={{ position: "absolute", right: 0, top: "50%", transform: "translateY(-50%)", whiteSpace: "nowrap" }}>
{children}
<Divider orientation="vertical" flexItem sx={{ marginLeft: 0.5, marginRight: 0.5 }} />
{isDiffFeatureEnabled && (
<Divider orientation="vertical" flexItem sx={{ marginLeft: 0.5, marginRight: 0.5 }} />
)}
{mobileToolbar && (
<ToggleMobileToolbarButton
direction={isMobileToolbarVisible ? "up" : "down"}
Expand Down
57 changes: 39 additions & 18 deletions src/features/sidebar/view/internal/ClientSplitView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { isMac, useKeyboardShortcut, SidebarTogglableContext } from "@/common"
import { useSidebarOpen } from "../../data"
import useDiffbarOpen from "../../data/useDiffbarOpen"
import { useProjectSelection } from "@/features/projects/data"
import ClientSplitViewTransitionContext from "./ClientSplitViewTransitionContext"
import useClientSplitViewTransitionEnabled from "./useClientSplitViewTransitionEnabled"
import PrimaryContainer from "./primary/Container"
import SecondaryContainer from "./secondary/Container"
import RightContainer from "./tertiary/RightContainer"
Expand All @@ -21,6 +23,7 @@ const ClientSplitView = ({
children?: React.ReactNode
sidebarRight?: React.ReactNode
}) => {
const { isMounted, isTransitionsEnabled } = useClientSplitViewTransitionEnabled()
const [isSidebarOpen, setSidebarOpen] = useSidebarOpen()
const [isRightSidebarOpen, setRightSidebarOpen] = useDiffbarOpen()
const { specification } = useProjectSelection()
Expand Down Expand Up @@ -63,25 +66,43 @@ const ClientSplitView = ({
const diffWidth = 320

return (
<Stack direction="row" spacing={0} sx={{ width: "100%", height: "100%" }}>
<PrimaryContainer
width={sidebarWidth}
isOpen={isSidebarOpen}
onClose={() => setSidebarOpen(false)}
<ClientSplitViewTransitionContext.Provider value={{
isTransitionsEnabled
}}>
<Stack
direction="row"
spacing={0}
sx={{
width: "100%",
height: "100%",
visibility: isMounted ? "visible" : "hidden"
}}
>
{sidebar}
</PrimaryContainer>
<SecondaryContainer isSM={isSM} sidebarWidth={sidebarWidth} offsetContent={isSidebarOpen} diffWidth={diffWidth} offsetDiffContent={isRightSidebarOpen}>
{children}
</SecondaryContainer>
<RightContainer
width={diffWidth}
isOpen={isRightSidebarOpen}
onClose={() => setRightSidebarOpen(false)}
>
{sidebarRight}
</RightContainer>
</Stack>
<PrimaryContainer
width={sidebarWidth}
isOpen={isSidebarOpen}
onClose={() => setSidebarOpen(false)}
>
{sidebar}
</PrimaryContainer>
<SecondaryContainer
isSM={isSM}
sidebarWidth={sidebarWidth}
offsetContent={isSidebarOpen}
diffWidth={diffWidth}
offsetDiffContent={isRightSidebarOpen}
>
{children}
</SecondaryContainer>
<RightContainer
width={diffWidth}
isOpen={isRightSidebarOpen}
onClose={() => setRightSidebarOpen(false)}
>
{sidebarRight}
</RightContainer>
</Stack>
</ClientSplitViewTransitionContext.Provider>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client"

import { createContext } from "react"

type ClientSplitViewTransitionContextValue = {
isTransitionsEnabled: boolean
}

const ClientSplitViewTransitionContext = createContext<ClientSplitViewTransitionContextValue>({
isTransitionsEnabled: true
})

export default ClientSplitViewTransitionContext

9 changes: 7 additions & 2 deletions src/features/sidebar/view/internal/primary/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"use client"

import { useContext } from "react"
import { SxProps } from "@mui/system"
import { Drawer as MuiDrawer } from "@mui/material"
import { useTheme } from "@mui/material/styles"
import ClientSplitViewTransitionContext from "../ClientSplitViewTransitionContext"

const PrimaryContainer = ({
width,
Expand All @@ -17,7 +19,7 @@ const PrimaryContainer = ({
}) => {
return (
<>
<InnerPrimaryContainer
<InnerPrimaryContainer
variant="temporary"
width={width}
isOpen={isOpen}
Expand Down Expand Up @@ -60,12 +62,14 @@ const InnerPrimaryContainer = ({
children?: React.ReactNode
}) => {
const theme = useTheme()
const { isTransitionsEnabled } = useContext(ClientSplitViewTransitionContext)
return (
<MuiDrawer
variant={variant}
anchor="left"
open={isOpen}
onClose={onClose}
transitionDuration={isTransitionsEnabled ? undefined : 0}
ModalProps={{
keepMounted: keepMounted || false
}}
Expand All @@ -77,7 +81,8 @@ const InnerPrimaryContainer = ({
width: width,
boxSizing: "border-box",
borderRight: 0,
background: theme.palette.background.default
background: theme.palette.background.default,
...(isTransitionsEnabled ? {} : { transition: "none" })
}
}}
>
Expand Down
36 changes: 30 additions & 6 deletions src/features/sidebar/view/internal/secondary/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useContext } from "react"
import { SxProps } from "@mui/system"
import { Box, Stack } from "@mui/material"
import { styled } from "@mui/material/styles"
import CustomTopLoader from "@/common/ui/CustomTopLoader"
import ClientSplitViewTransitionContext from "../ClientSplitViewTransitionContext"

const SecondaryContainer = ({
sidebarWidth,
Expand All @@ -19,14 +21,15 @@ const SecondaryContainer = ({
isSM: boolean,
}) => {
const sx = { overflow: "hidden" }
const { isTransitionsEnabled } = useContext(ClientSplitViewTransitionContext)
return (
<>
<InnerSecondaryContainer

sidebarWidth={isSM ? sidebarWidth : 0}
isSidebarOpen={isSM ? offsetContent: false}
isSidebarOpen={isSM ? offsetContent: false}
diffWidth={isSM ? (diffWidth || 0) : 0}
isDiffOpen={isSM ? (offsetDiffContent || false) : false}
isTransitionsEnabled={isTransitionsEnabled}
sx={{ ...sx }}
>
{children}
Expand All @@ -43,18 +46,36 @@ interface WrapperStackProps {
readonly isSidebarOpen: boolean
readonly diffWidth: number
readonly isDiffOpen: boolean
readonly isTransitionsEnabled: boolean
}

const WrapperStack = styled(Stack, {
shouldForwardProp: (prop) => prop !== "isSidebarOpen" && prop !== "sidebarWidth" && prop !== "diffWidth" && prop !== "isDiffOpen"
})<WrapperStackProps>(({ theme, sidebarWidth, isSidebarOpen, diffWidth, isDiffOpen }) => {
shouldForwardProp: (prop) =>
prop !== "isSidebarOpen" &&
prop !== "sidebarWidth" &&
prop !== "diffWidth" &&
prop !== "isDiffOpen" &&
prop !== "isTransitionsEnabled",
})<WrapperStackProps>(
({ theme, sidebarWidth, isSidebarOpen, diffWidth, isDiffOpen, isTransitionsEnabled }) => {
const marginStyles = {
marginLeft: isSidebarOpen ? 0 : `-${sidebarWidth}px`,
marginRight: isDiffOpen ? 0 : `-${diffWidth}px`,
}

if (!isTransitionsEnabled) {
return {
transition: "none",
...marginStyles
}
}

return {
transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
marginLeft: isSidebarOpen ? 0 : `-${sidebarWidth}px`,
marginRight: isDiffOpen ? 0 : `-${diffWidth}px`,
...marginStyles,
...((isSidebarOpen || isDiffOpen) && {
transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.easeOut,
Expand All @@ -69,13 +90,15 @@ const InnerSecondaryContainer = ({
isSidebarOpen,
diffWidth,
isDiffOpen,
isTransitionsEnabled,
children,
sx
}: {
sidebarWidth: number
isSidebarOpen: boolean
diffWidth: number
isDiffOpen: boolean
isTransitionsEnabled: boolean
children: React.ReactNode
sx?: SxProps
}) => {
Expand All @@ -87,6 +110,7 @@ const InnerSecondaryContainer = ({
isSidebarOpen={isSidebarOpen}
diffWidth={diffWidth}
isDiffOpen={isDiffOpen}
isTransitionsEnabled={isTransitionsEnabled}
sx={{ ...sx, width: "100%", overflowY: "auto" }}

>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
'use client'

import { useContext } from "react"
import { SxProps } from "@mui/system"
import { Drawer as MuiDrawer } from "@mui/material"
import { useTheme } from "@mui/material/styles"
import ClientSplitViewTransitionContext from "../ClientSplitViewTransitionContext"

const RightContainer = ({
width,
Expand Down Expand Up @@ -60,12 +62,14 @@ const InnerRightContainer = ({
children?: React.ReactNode
}) => {
const theme = useTheme()
const { isTransitionsEnabled } = useContext(ClientSplitViewTransitionContext)
return (
<MuiDrawer
variant={variant}
anchor="right"
open={isOpen}
onClose={onClose}
transitionDuration={isTransitionsEnabled ? undefined : 0}
ModalProps={{
keepMounted: keepMounted || false
}}
Expand All @@ -77,7 +81,8 @@ const InnerRightContainer = ({
width: width,
boxSizing: "border-box",
borderLeft: 0,
background: theme.palette.background.default
background: theme.palette.background.default,
...(isTransitionsEnabled ? {} : { transition: "none" })
}
}}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client"

import { useEffect, useState } from "react"

export default function useClientSplitViewTransitionEnabled() {
const [isMounted, setMounted] = useState(false)
const [isTransitionsEnabled, setTransitionsEnabled] = useState(false)

useEffect(() => {
// Track first render to avoid showing default state.
const frame = window.requestAnimationFrame(() => {
setMounted(true)
})
return () => window.cancelAnimationFrame(frame)
}, [])

useEffect(() => {
if (!isMounted) {
return
}
// Enable transitions only after the first mounted paint.
const frame = window.requestAnimationFrame(() => {
setTransitionsEnabled(true)
})
return () => window.cancelAnimationFrame(frame)
}, [isMounted])

// NOTE (2026-01-06):

// There is a potential edge-case where the component unmounts between
// consecutive requestAnimationFrame calls. If that happens after setMounted(true)
// runs but before the second requestAnimationFrame fires, the second frame won't be
// cancelled by the first effect's cleanup.
// Quick mitigation idea: store both RAF IDs in refs (e.g. mountRef, transitionRef),
// clear the ref inside each RAF callback, and cancel any remaining non-null refs
// from a single unmount cleanup to guarantee no callback runs after unmount.
//
// This fix is not implemented because it would complicate the implementation
// and it's unclear how frequently the edge-case occurs in practice.
// Revisit if this issue is observed in production.

return { isMounted, isTransitionsEnabled }
}