diff --git a/src/app/root/root.component.scss b/src/app/root/root.component.scss
index 9eb198417ad..2a6cdfb7848 100644
--- a/src/app/root/root.component.scss
+++ b/src/app/root/root.component.scss
@@ -14,3 +14,27 @@
top: 0;
}
}
+
+// Admin-sidebar left gutter, driven by the ds-admin-sidebar-* class from sidebarPaddingState$ rather
+// than the @slideSidebarPadding animation. The animation read the width from a browser-only store, so
+// the server emitted `padding-left: *` and the page moved sideways once the browser resolved it.
+// Resolving from the custom properties in CSS renders the same on both sides, with no hardcoded px.
+.outer-wrapper {
+ &.ds-admin-sidebar-hidden {
+ padding-left: 0;
+ }
+
+ &.ds-admin-sidebar-unpinned {
+ padding-left: var(--ds-admin-sidebar-fixed-element-width);
+ }
+
+ &.ds-admin-sidebar-pinned {
+ padding-left: var(--ds-admin-sidebar-total-width);
+ }
+
+ // Only genuine pin/unpin toggles slide: the class is added after the first paint, so the initial
+ // gutter never animates behind the anti-flicker overlay.
+ &.ds-admin-sidebar-animate {
+ transition: padding-left 300ms ease-in-out;
+ }
+}
diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts
index 8ce800f5f5f..2dac12579f2 100644
--- a/src/app/root/root.component.ts
+++ b/src/app/root/root.component.ts
@@ -3,6 +3,7 @@ import {
NgClass,
} from '@angular/common';
import {
+ AfterViewInit,
Component,
Inject,
Input,
@@ -38,7 +39,6 @@ import {
} from '../core/services/window.service';
import { ThemedFooterComponent } from '../footer/themed-footer.component';
import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component';
-import { slideSidebarPadding } from '../shared/animations/slide';
import { HostWindowService } from '../shared/host-window.service';
import { LiveRegionComponent } from '../shared/live-region/live-region.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
@@ -52,7 +52,6 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne
selector: 'ds-base-root',
templateUrl: './root.component.html',
styleUrls: ['./root.component.scss'],
- animations: [slideSidebarPadding],
imports: [
AsyncPipe,
LiveRegionComponent,
@@ -68,12 +67,25 @@ import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banne
TranslateModule,
],
})
-export class RootComponent implements OnInit {
+export class RootComponent implements OnInit, AfterViewInit {
theme: Observable = of({} as any);
isSidebarVisible$: Observable;
slideSidebarOver$: Observable;
collapsedSidebarWidth$: Observable;
expandedSidebarWidth$: Observable;
+
+ /**
+ * Which admin-sidebar gutter the outer wrapper should carry: 'hidden', 'unpinned' or 'pinned'.
+ * The width itself comes from CSS (see root.component.scss), so the server and the browser
+ * resolve it the same way.
+ */
+ sidebarPaddingState$: Observable;
+
+ /**
+ * Lets the gutter transition run only after the first paint, so the initial resolution does not
+ * animate. Off on the server and on the first render; only pin/unpin toggles slide.
+ */
+ gutterTransitionEnabled = false;
notificationOptions: INotificationBoardOptions;
models: any;
@@ -129,11 +141,27 @@ export class RootComponent implements OnInit {
startWith(true),
);
+ // A CSS class instead of the @slideSidebarPadding animation: that animation needed a concrete
+ // width from the browser-only CSS-variable store, so the server rendered `padding-left: *` and
+ // the page moved sideways once the browser resolved the real width.
+ this.sidebarPaddingState$ = combineLatestObservable([this.isSidebarVisible$, this.slideSidebarOver$]).pipe(
+ map(([visible, over]: [boolean, boolean]) => !visible ? 'hidden' : over ? 'unpinned' : 'pinned'),
+ );
+
if (this.router.url === getPageInternalServerErrorRoute()) {
this.shouldShowRouteLoader = false;
}
}
+ ngAfterViewInit(): void {
+ // Browser only; requestAnimationFrame does not exist under SSR.
+ if (typeof requestAnimationFrame === 'function') {
+ requestAnimationFrame(() => {
+ this.gutterTransitionEnabled = true;
+ });
+ }
+ }
+
skipToMainContent() {
const mainContent = document.getElementById('main-content');
if (mainContent) {
diff --git a/src/themes/custom/app/root/root.component.ts b/src/themes/custom/app/root/root.component.ts
index 2bdab293bf6..e3611b32a77 100644
--- a/src/themes/custom/app/root/root.component.ts
+++ b/src/themes/custom/app/root/root.component.ts
@@ -11,7 +11,6 @@ import { ThemedBreadcrumbsComponent } from '../../../../app/breadcrumbs/themed-b
import { ThemedFooterComponent } from '../../../../app/footer/themed-footer.component';
import { ThemedHeaderNavbarWrapperComponent } from '../../../../app/header-nav-wrapper/themed-header-navbar-wrapper.component';
import { RootComponent as BaseComponent } from '../../../../app/root/root.component';
-import { slideSidebarPadding } from '../../../../app/shared/animations/slide';
import { LiveRegionComponent } from '../../../../app/shared/live-region/live-region.component';
import { ThemedLoadingComponent } from '../../../../app/shared/loading/themed-loading.component';
import { NotificationsBoardComponent } from '../../../../app/shared/notifications/notifications-board/notifications-board.component';
@@ -23,7 +22,6 @@ import { SystemWideAlertBannerComponent } from '../../../../app/system-wide-aler
styleUrls: ['../../../../app/root/root.component.scss'],
// templateUrl: './root.component.html',
templateUrl: '../../../../app/root/root.component.html',
- animations: [slideSidebarPadding],
imports: [
AsyncPipe,
LiveRegionComponent,