Skip to content

Commit 2ddb7d5

Browse files
CGastrellclaude
andauthored
Admin pages: unify layout under jetpack-admin-page-layout mixin (#48109)
* base-styles: Add jetpack-admin-page-layout mixin Introduces @automattic/jetpack-base-styles/admin-page-layout, a SCSS mixin that standardizes Jetpack wp-admin pages: a viewport-pinned content column with the Jetpack header pinned at the top, a middle that scrolls internally, and JetpackFooter pinned at the bottom. No window-level scroll. Key mechanics: - Override WP core's "#wpbody-content { width: 100% }" (common.css) with "width: auto" so position: fixed + left + right: 0 can size the column correctly. Without this, width: 100% resolves to 100vw under fixed positioning and pushes the column past the viewport. - Honor the .folded body class unconditionally (user's persistent "Collapse menu" preference) and gate .auto-fold to (max-width: 960px), mirroring WP's own wp-admin.css scoping. This keeps a stale auto-fold class from mis-offsetting the column after a narrow to wide viewport resize. - The mobile media query matches #wpbody-content, .folded #wpbody-content, and .auto-fold #wpbody-content explicitly so "left: 0" wins via equal-specificity source order over the desktop fold rules. - Sidebar widths and viewport breakpoints live in SCSS variables; the admin-bar height stays as var(--wp-admin-bar-height, default) to keep the documented runtime-override path open. Exports the mixin via package.json so consumers can @use "@automattic/jetpack-base-styles/admin-page-layout" as *; Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * jetpack-components: Add stable jp-admin-page class to AdminPage Adds "jp-admin-page" as a second, non-CSS-Modules-hashed className on the AdminPage root in addition to the existing hashed styles["admin-page"] token. This gives global stylesheets and shared mixins (notably jetpack-admin-page-layout from @automattic/jetpack-base-styles) a stable selector to target without coupling to the hashed module name. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Protect: Adopt jetpack-admin-page-layout mixin Scopes the shared admin-page layout to body.jetpack_page_jetpack-protect so Protect uses the standard header-pinned / middle-scrolling / footer-pinned layout. No other visual changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Publicize: Adopt jetpack-admin-page-layout mixin Scopes the shared admin-page layout to body.jetpack_page_jetpack-social (the Social menu page is served by the Publicize package) so Social uses the standard header-pinned / middle-scrolling / footer-pinned layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * VideoPress: Adopt jetpack-admin-page-layout mixin Scopes the shared admin-page layout to body.jetpack_page_jetpack-videopress. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Backup: Adopt jetpack-admin-page-layout mixin Scopes the shared admin-page layout to body.jetpack_page_jetpack-backup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Newsletter: Adopt jetpack-admin-page-layout mixin Adds @automattic/jetpack-base-styles as a workspace dep and scopes the shared admin-page layout to body.jetpack_page_jetpack-newsletter. Keeps the existing dataviews imports in place. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Search: Drop inline JetpackFooter, adopt jetpack-admin-page-layout mixin Search rendered JetpackFooter twice: once via AdminPage's built-in slot, once as a sibling inside the dashboard. It suppressed the built-in slot with showFooter={false}, which conflicted with the shared mixin's footer-pinning rules. Drops the showFooter={false} flag and the inline <JetpackFooter /> so the footer lives inside AdminPage's flex column where the mixin can pin it. Adds dashboard/scss/admin-layout.scss to scope the mixin to body.jetpack_page_jetpack-search and imports it from dashboard/index.jsx. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Boost: Drop inline JetpackFooters, adopt jetpack-admin-page-layout mixin Boost rendered JetpackFooter directly inside each page (BoostAdminPage, card-page, settings-page, cache-debug-log, getting-started) and suppressed AdminPage's built-in footer with showFooter={false}. That left the footer outside AdminPage's flex column, so the shared mixin could not pin it. Drops the inline <JetpackFooter /> imports/renders and removes showFooter={false} so the footer lives inside AdminPage where the mixin pins it. Adds the body.jetpack_page_jetpack-boost scope in admin-style.scss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Jetpack: Adopt jetpack-admin-page-layout mixin for network admin Scopes the shared admin-page layout to both network-admin body classes: toplevel_page_jetpack-network-sites (Sites, the default view) jp-network_page_jetpack-network-settings (Settings submenu) Both share #jp-network-admin-root and branch on data-page. The Settings class prefix is derived from the menu *title* ("JP Network") sanitized to "jp-network", so it does NOT follow the usual "toplevel_page_*" convention — see the admin_print_scripts-jp-network_page_jetpack-network-settings hook in tools/docker/mu-plugins/0-sandbox.php for the canonical reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * changelog: Add entries for admin-page-layout work Covers all ten projects touched by the admin-page-layout mixin introduction and per-page adoption: - base-styles (new public mixin + export — minor, added) - components (AdminPage stable class — patch, changed) - protect (patch, changed) - publicize (patch, changed) - videopress (patch, changed) - backup (patch, changed) - newsletter (patch, changed; also adds workspace dep) - search (patch, changed; drops inline footer) - boost (patch, changed; drops inline footers) - jetpack (patch, enhancement — network admin) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6821c60 commit 2ddb7d5

31 files changed

Lines changed: 310 additions & 16 deletions

File tree

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Jetpack admin-page layout mixin.
2+
3+
// Standardizes the layout of a Jetpack wp-admin page so <AdminPage> controls
4+
// the full content area: fixed Jetpack header at the top, the middle section
5+
// scrolls internally, JetpackFooter pinned at the bottom. No window-level
6+
// scroll; nothing leaks outside the scope.
7+
8+
// Usage:
9+
10+
// @use "@automattic/jetpack-base-styles/admin-page-layout" as *;
11+
// :global {
12+
// body.jetpack_page_my-jetpack { @include jetpack-admin-page-layout; }
13+
// }
14+
15+
// Selector hooks this mixin relies on (all stable, not CSS-Modules hashed):
16+
17+
// wp-admin shell: #wpcontent, #wpbody-content, #wpfooter
18+
// <AdminPage> root: .jp-admin-page (added unconditionally by the
19+
// <AdminPage> component from
20+
// @automattic/jetpack-components)
21+
// @wordpress/admin-ui: .admin-ui-page, .admin-ui-page__header
22+
// <JetpackFooter>: .jetpack-footer
23+
24+
// Implementation note — `postcss-custom-properties` with `preserve: false`:
25+
// several consumer packages in this monorepo (publicize, videopress, search,
26+
// jetpack plugin) resolve `var()` to its fallback at build time. That rules
27+
// out *runtime-toggled* custom properties (you can't use a CSS var to flip
28+
// `<AdminPage>`'s margin between the legacy and the new layout, because the
29+
// fallback wins before the override can reach it). It does NOT rule out
30+
// `var(--name, <fallback>)` used purely for readability and forward-
31+
// compatibility on size values — the compiled output is identical to
32+
// inlining the fallback. This mixin uses `var(--wp-admin-bar-height, …)`
33+
// (mirroring `projects/packages/forms/src/dashboard/inbox/style.scss`) so
34+
// the intent stays legible, and so packages that don't run the `preserve:
35+
// false` plugin can override the var at runtime. Immutable sizes (sidebar
36+
// widths, viewport breakpoints) use SCSS variables — they can't change at
37+
// runtime and there's no need to pay the custom-property indirection.
38+
39+
// wp-admin chrome constants (see wp-admin.css).
40+
$jp-sidebar-width-expanded: 160px; // default sidebar
41+
$jp-sidebar-width-folded: 36px; // .folded preference OR .auto-fold class
42+
$jp-admin-bar-height-desktop: 32px;
43+
$jp-admin-bar-height-mobile: 46px;
44+
45+
// Viewport breakpoints WP itself uses:
46+
// ≤ 960px → WP toggles `.auto-fold` on <body>, narrows the sidebar to 36px
47+
// ≤ 782px → sidebar goes off-canvas (0) regardless of .folded/.auto-fold
48+
// We mirror these breakpoints because WP's body-class state is not always
49+
// in sync with the rendered viewport (the `auto-fold` class can stay stale
50+
// after a narrow → wide window resize).
51+
$jp-breakpoint-auto-fold: 960px;
52+
$jp-breakpoint-mobile: 782px;
53+
54+
@mixin jetpack-admin-page-layout {
55+
// ── wp-admin chrome resets ────────────────────────────────────────
56+
#wpcontent {
57+
padding-left: 0;
58+
}
59+
60+
// wp-admin's footer floats at the very bottom via `position: absolute`;
61+
// we replace it with a pinned JetpackFooter inside the scope.
62+
#wpfooter {
63+
display: none;
64+
}
65+
66+
// ── Viewport-pinned content column ────────────────────────────────
67+
// `position: fixed` pins the content column to the viewport. The
68+
// sidebar (`#adminmenumain`) is left untouched — it can grow past
69+
// viewport; the window will scroll for it without disturbing the
70+
// fixed content column. This is the pattern Calypso adopted in
71+
// wp-calypso#109899 for its own admin pages.
72+
73+
// We intentionally do NOT constrain `#adminmenuwrap` with
74+
// `overflow-y: auto` here: any overflow on a sidebar ancestor clips
75+
// the absolutely-positioned `.wp-submenu` flyouts that appear on
76+
// hover in both folded AND expanded modes (for parent menus whose
77+
// submenu isn't auto-expanded). Letting the window scroll for a tall
78+
// sidebar is the acceptable tradeoff.
79+
#wpbody-content {
80+
box-sizing: border-box;
81+
position: fixed;
82+
top: var(--wp-admin-bar-height, #{$jp-admin-bar-height-desktop});
83+
left: $jp-sidebar-width-expanded;
84+
right: 0;
85+
bottom: 0;
86+
// wp-admin core sets `width: 100%` on #wpbody-content (common.css).
87+
// With `position: fixed`, that resolves to 100% of the viewport and
88+
// overrides `right: 0`, making the column extend past the viewport
89+
// by the sidebar width. `width: auto` lets left+right size it.
90+
width: auto;
91+
padding-bottom: 0; // wp-admin default is 65px (clears #wpfooter).
92+
overflow: hidden;
93+
display: flex;
94+
flex-direction: column;
95+
}
96+
97+
// `.folded` is the user's persistent "Collapse menu" preference; WP
98+
// keeps it set at every viewport width, so we match it unconditionally.
99+
&.folded #wpbody-content {
100+
left: $jp-sidebar-width-folded;
101+
}
102+
103+
// `.auto-fold` is WP JS state, gated to narrow-desktop widths. Scope
104+
// our match to the same media range WP uses in wp-admin.css, otherwise
105+
// a stale `.auto-fold` class (WP doesn't always remove it on resize)
106+
// would mis-offset the column at wide viewports where the sidebar has
107+
// rebounded to 160px.
108+
@media (max-width: #{$jp-breakpoint-auto-fold}) {
109+
110+
&.auto-fold #wpbody-content {
111+
left: $jp-sidebar-width-folded;
112+
}
113+
}
114+
115+
// Every ancestor of <AdminPage> inside #wpbody-content must participate
116+
// in the flex chain, or the column collapses at the first non-flex
117+
// wrapper. Some pages mount their React tree with an extra wrapper in
118+
// between (e.g. ThemeProvider from @automattic/jetpack-components adds
119+
// a `display: block` div); `:has()` targets the whole chain regardless
120+
// of depth. `min-width: 0` + `min-height: 0` let flex items shrink on
121+
// both axes (defaults to content size which can cause overflow).
122+
#wpbody-content :has(.jp-admin-page) {
123+
flex: 1 1 auto;
124+
min-height: 0;
125+
min-width: 0;
126+
display: flex;
127+
flex-direction: column;
128+
}
129+
130+
// ── <AdminPage> root ──────────────────────────────────────────────
131+
// Neutralize the legacy -20px shift and make AdminPage a flex child
132+
// that fills its parent column.
133+
.jp-admin-page {
134+
margin-left: 0;
135+
display: flex;
136+
flex-direction: column;
137+
flex: 1 1 auto;
138+
min-height: 0;
139+
min-width: 0;
140+
}
141+
142+
// ── admin-ui Page internals ───────────────────────────────────────
143+
// admin-ui ships `.admin-ui-page` with `height: 100%`, which fills its
144+
// parent's computed height. Paired with our flex chain, the element
145+
// becomes a viewport-fitted flex column.
146+
.admin-ui-page {
147+
flex: 1 1 auto;
148+
min-height: 0;
149+
min-width: 0;
150+
display: flex;
151+
flex-direction: column;
152+
}
153+
154+
.admin-ui-page__header {
155+
flex-shrink: 0; // pinned at top of the flex column.
156+
}
157+
158+
// The scrollable middle. <AdminPage> does not pass `hasPadding` to
159+
// admin-ui's <Page>, so `.admin-ui-page__content` is not rendered —
160+
// children drop in as direct descendants of `.admin-ui-page`. Target
161+
// anything that isn't the header or the footer.
162+
163+
// `overflow: auto` (both axes) lets any child wider than the column
164+
// (dashboard grids with fixed column widths, wide tables, `100vw`
165+
// descendants) scroll horizontally inside the middle instead of
166+
// dragging the whole window into a horizontal scrollbar.
167+
.admin-ui-page > :not(.admin-ui-page__header):not(.jetpack-footer) {
168+
flex: 1 1 auto;
169+
min-height: 0;
170+
min-width: 0;
171+
overflow: auto;
172+
}
173+
174+
// ── <JetpackFooter> pinned at the bottom ─────────────────────────
175+
.jetpack-footer {
176+
flex-shrink: 0;
177+
}
178+
179+
// ── Mobile: admin bar grows to 46px; sidebar goes off-canvas ─────
180+
// Match `.folded` and `.auto-fold` explicitly: both outrank the bare
181+
// `#wpbody-content` selector on specificity, so the media query needs
182+
// equal specificity for `left: 0` to win over the desktop fold rules.
183+
@media (max-width: #{$jp-breakpoint-mobile}) {
184+
185+
#wpbody-content,
186+
&.folded #wpbody-content,
187+
&.auto-fold #wpbody-content {
188+
top: var(--wp-admin-bar-height, #{$jp-admin-bar-height-mobile});
189+
left: 0; // sidebar slides out of flow at this width.
190+
}
191+
192+
.jp-admin-page {
193+
margin-left: 0; // override legacy -10px @ ≤782px too.
194+
}
195+
}
196+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Added admin-page-layout mixin: a shared SCSS mixin that standardizes Jetpack wp-admin pages with a viewport-pinned content column (pinned header, scrolling middle, pinned footer). Consumed as `@use "@automattic/jetpack-base-styles/admin-page-layout"`.

projects/js-packages/base-styles/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"build-production-js": "echo 'Not implemented.'"
2020
},
2121
"exports": {
22+
"./admin-page-layout": "./admin-page-layout.scss",
2223
"./gutenberg-base-styles": "./gutenberg-base-styles.scss",
2324
"./package.json": "./package.json",
2425
"./root-variables": "./root-variables.scss",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
4+
AdminPage: added a stable, non-hashed `jp-admin-page` class on the component root so shared SCSS mixins and global stylesheets can target AdminPage without coupling to the hashed CSS-Modules className.

projects/js-packages/components/components/admin-page/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ const AdminPage: FC< AdminPageProps > = ( {
4747
restApi.setApiNonce( apiNonce );
4848
}, [ apiRoot, apiNonce ] );
4949

50-
const rootClassName = clsx( styles[ 'admin-page' ], className, {
50+
// `jp-admin-page` is a stable, non-hashed hook for global stylesheets and
51+
// shared SCSS mixins (notably `jetpack-admin-page-layout` in
52+
// @automattic/jetpack-base-styles). Do not rename.
53+
const rootClassName = clsx( styles[ 'admin-page' ], 'jp-admin-page', className, {
5154
[ styles.background ]: showBackground,
5255
[ styles[ 'without-bottom-border' ] ]: tabs || ! showBottomBorder,
5356
} );
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
4+
Adopt the shared Jetpack admin-page-layout mixin on the Backup admin page: pinned header, scrolling middle, pinned footer, no window-level scroll.

projects/packages/backup/src/js/components/Admin/style.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
@use "@automattic/jetpack-base-styles/style" as jpbs;
2+
@use "@automattic/jetpack-base-styles/admin-page-layout" as *;
23
@use "../masthead/calypso-mixins";
34

5+
body.jetpack_page_jetpack-backup {
6+
7+
@include jetpack-admin-page-layout;
8+
}
9+
410
.jp-header,
511
.jp-footer {
612
padding: 20px 0;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
4+
Adopt the shared Jetpack admin-page-layout mixin on the Newsletter admin page: pinned header, scrolling middle, pinned footer, no window-level scroll. Adds @automattic/jetpack-base-styles as a workspace dependency.

projects/packages/newsletter/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"dependencies": {
3535
"@automattic/jetpack-analytics": "workspace:*",
3636
"@automattic/jetpack-api": "workspace:*",
37+
"@automattic/jetpack-base-styles": "workspace:*",
3738
"@automattic/jetpack-components": "workspace:*",
3839
"@automattic/jetpack-connection": "workspace:*",
3940
"@automattic/jetpack-script-data": "workspace:*",

0 commit comments

Comments
 (0)