/** * OpenStation — Chromeless Overrides. * * CSS adjustments for legacy admin pages rendered inside OpenStation * iframes (chromeless mode). All rules are scoped to .os-chromeless * so they never affect the classic admin or the desktop shell. * * Plugin and theme developers: to add your own chromeless overrides, * enqueue a stylesheet on the 'openstation_chromeless_styles' action. * Your CSS just needs to target .os-chromeless as the body class. * * Example: * add_action( 'openstation_chromeless_styles', function() { * wp_enqueue_style( 'my-plugin-chromeless', plugin_dir_url( __FILE__ ) . 'chromeless.css' ); * } ); * * @since 0.1.0 */ /* --------------------------------------------------------------- * Hide the classic admin chrome elements. These would otherwise * reserve space (sidebar gutter, footer, admin bar) around the * chromeless page content. * --------------------------------------------------------------- */ .os-chromeless #adminmenuwrap, .os-chromeless #adminmenuback, .os-chromeless #wpfooter, .os-chromeless #wpadminbar, .os-chromeless .wp-responsive-toggle, .os-chromeless #collapse-menu { display: none !important; } /* --------------------------------------------------------------- * The iframe paints its own canvas. * * An admin page leaves whole regions unpainted — the gutter around * `#wpbody-content`, the strip behind `.subsubsub` and `.tablenav`, * everything below short content. In a normal admin tab the browser * canvas is white underneath, so nobody notices. Inside an iframe the * canvas is TRANSPARENT, and what shows through is the window element * behind it — `--os-window-bg`. * * That was invisible while the window body was white and became a * black band the moment the station's palette made it Obsidian. It is * a real coupling either way: the look of a wp-admin page must not * depend on the colour of the frame around it. * * So the chromeless document paints itself, in the colour the browser * canvas would have been. Deliberately a literal and deliberately NOT * a token: nothing about the shell's palette — or any desktop theme — * should reach a real admin page. **An admin page in a window renders * exactly as it does outside one.** * --------------------------------------------------------------- */ body.os-chromeless { background: #fff; } /* * _wp_admin_html_begin() adds the `wp-toolbar` class to whenever * is_admin_bar_showing() is true — which, in admin, is unconditional. * The class carries a `padding-top: var(--wp-admin--admin-bar--height)` * that would leave a 32px (or 46px) dead gap at the top of the iframe. * Zero it out inside chromeless iframes. * * We also rebind `--wp-admin--admin-bar--height` (and the derived * `--wp-admin--admin-bar--position-offset` from block-library) to 0px * inside chromeless so plugins that position UI relative to the admin * bar resolve their math against the iframe's actual chrome state. * * WooCommerce's activity-panel wrapper, the block editor's sticky * header, and several others use `top: var(--wp-admin--admin-bar--height)` * to clear the bar. Without this override they reserve a 32px (or 46px * on small screens) gap that no longer exists, producing visible jumps * on first paint and dead space at the top of the content area. */ html.wp-toolbar:has( body.os-chromeless ) { padding-top: 0 !important; --wp-admin--admin-bar--height: 0px; --wp-admin--admin-bar--position-offset: 0px; } /* Remove the sidebar gutter on #wpcontent and let the body fill the frame. */ .os-chromeless #wpcontent { margin-inline-start: 0 !important; padding-inline-start: 0 !important; } /* * WooCommerce sidebar-reservation override. * * `.woocommerce-layout__header` is a `position: fixed` bar WC * mounts on every wc-admin and wc-embedded page. Its width is * compiled from the SCSS source (header/style.scss) as a literal: * * position: fixed; * top: 32px; // $adminbar-height * width: calc(100% - 160px); // reserves classic sidebar * z-index: 1001; * * The 160px subtraction is the classic admin menu width — WC bakes * it in at build time so the header doesn't overlap the sidebar * in standard admin. Inside chromeless we hide the sidebar, but * the header keeps the reservation — so the header ends 160px * short of the iframe right edge, and `.woocommerce-layout__activity-panel-wrapper` * (`position: absolute; right: 0; top: 100%; transform: translateX(100%)`, * containing block = the fixed header) translates past the * header's right edge, NOT past the iframe's right edge. The * resulting visible strip is exactly 160px wide — the size of the * sidebar gap WC reserved for nothing. * * Reclaim the reservation: pin the header to full iframe width * inside chromeless. The activity panel then translates past the * iframe edge as WC's design intended, the gray strip disappears, * and the header's content uses the full window width. No * transform / overflow / visibility tricks needed. */ .os-chromeless .woocommerce-layout__header { width: 100% !important; } /* * MailPoet top-bar overlap fix. * * MailPoet mounts a 64px-tall brand bar on its screens as * `.wrap .mailpoet-top-bar { position: absolute; top: 0 }` — * anchored to `#wpbody` (core's `position: relative`), overlaying * whatever the page's first 64px contain. In classic admin nothing * is visibly covered only by accident of geometry: the in-flow * space above `.wrap` — the `#screen-meta-links` row plus `.wrap`'s * own top margin — happens to add up to more than the bar's height, * so the content starts below it (measured: `.wrap` at y=114 vs * bar bottom at y=96). * * Chromeless collapses exactly that space on purpose — screen-meta * links are hidden (the window title bar owns those buttons) and * `.wrap` margins are trimmed — which slides MailPoet's content up * underneath the overlaid bar. Symptom: the page's heading sits * half-hidden behind the white logo bar inside the window. * * Reserve the bar's height in flow instead. `:has()` scopes the * rule to precisely the pages that render the in-wrap bar — * whatever MailPoet screen shape it is, present or future — and to * nothing else. */ .os-chromeless .wrap:has( .mailpoet-top-bar ) { padding-top: 64px; } /* --------------------------------------------------------------- * Screen Meta (Screen Options / Help panels) * The toggle buttons (#screen-meta-links) are hidden because * the parent desktop shell adds its own buttons to the window * title bar. The panels themselves stay visible and functional — * they're toggled via postMessage from the parent. * * Collapse all margins on the hidden links and the panel container * so they don't leave a gap at the top of the iframe content. * --------------------------------------------------------------- */ .os-chromeless #screen-meta-links { display: none; margin: 0; } .os-chromeless #screen-meta { margin: 0; border: none; } /* --------------------------------------------------------------- * Wrap container * Remove the default left margin that accounts for the sidebar * which doesn't exist in chromeless mode. * --------------------------------------------------------------- */ .os-chromeless .wrap { margin: 0; } /* --------------------------------------------------------------- * Page title & header * The window title bar already shows the page title, so the * in-page

+ header separator (.wp-header-end) are redundant * inside chromeless iframes. Hide them so the window content * starts flush. * * `.page-title-action` (the "Add New" / "Add Order" button next to * the H1) stays VISIBLE by default — it's the only entry point to * the add-new flow on many plugin pages (WooCommerce Orders, custom * CPTs, plugin settings pages, etc.). A blanket hide would break * every third-party plugin page that has no submenu equivalent, so * the button is only removed where the window's own tab strip * demonstrably leads to the same place — see * `includes/render/chromeless-title-actions.php`, which emits one * `[href="…"]` rule per submenu tab URL of the current screen. * * Sites that want to hide it somewhere that rule deliberately * doesn't reach can add their own via the * `openstation_chromeless_styles` action — e.g. WooCommerce's "Add * order", which we keep because it points at `&action=new` and the * Orders tab doesn't: * * body.woocommerce_page_wc-orders .wrap > .page-title-action { * display: none; * } * * --------------------------------------------------------------- */ .os-chromeless .wrap > h1, .os-chromeless .wrap > h1.wp-heading-inline, .os-chromeless #wpbody-content > .wrap > h1, .os-chromeless .wrap > .wp-header-end { display: none; } /* * The H1 above it is hidden, so the button would float at the very * top of the iframe area without breathing room — give it a small * margin so it lands cleanly. * * `block` + `fit-content` instead of Core's `inline-block`: with the * H1 hidden, an inline button ends up on the same line as the * floated `.subsubsub` filter row and reads as one more filter link * ("All (6) | Published (4) | Trash (2) Add Post"). Its own row is * also what the screen already looks like when an admin notice is * showing. `clear` keeps it below anything floated before it. * * Themes.php's native "Add Theme" button lives in the submenu tab * strip via `openstation_inject_appearance_tabs` * (includes/themes-tabs.php), so its in-page page-title-action is * redundant on that one screen. The generic href-matching hide in * `includes/render/chromeless-title-actions.php` only matches exact * URLs, and the injected tab points at * `theme-install.php?browse=popular` while the button points at * plain `theme-install.php`. Hence the per-page rule below. */ .os-chromeless .wrap > .page-title-action { display: block; width: fit-content; margin-top: 12px; margin-bottom: 12px; clear: both; } .os-chromeless.themes-php .wrap > .page-title-action { display: none; } /* --------------------------------------------------------------- * WooCommerce "embed page" header overlay — page-scoped. * * Background: WC renders TWO layouts simultaneously on "connected" * pages like `wc-orders` — the PHP-rendered legacy `.wrap` (with * the h1 + the "Add order" `.page-title-action`) AND a React-mounted * `EmbedHeader` (`client/admin/client/header/embed.tsx`) that * `position: fixed`-overlays the page from the top. References: * - `src/Internal/Admin/Loader.php::embed_page_header` * - `includes/react-admin/connect-existing-pages.php` (registers * wc-orders as a connected page when HPOS is on) * - `client/admin/client/header/shared.tsx::useUpdateBodyMargin` * (the hook that pushes `#wpbody.style.marginTop` to make * room for the fixed header) * * The conflict, narrowly: * 1. The legacy `.wrap > .page-title-action` ("Add order") at * the top of the document body — primary add-new affordance. * 2. The React `EmbedHeader` overlays the top of the iframe with * a redundant `

Orders

` and an Activity Panel toggle. * * The fixed React header OBSCURES the legacy `.page-title-action` * button when we reset `#wpbody`'s margin-top. * * Scope: ONLY the pages where the dual rendering is genuinely a * problem — `wc-orders` (Orders list, the user-reported failure * mode) and `wc-orders--shop_order` (the trash view variant). Other * WC-admin pages (Analytics, Marketing, Customers, Coupons, * Products, Reports, …) DON'T render a competing legacy `.wrap` * with a `.page-title-action` button; their primary content IS the * React app. On those pages the Activity Panel + EmbedHeader are * the only header affordance the user has, so we keep them visible. * * If new HPOS-style connected pages emerge where the same dual * layout creates the same conflict, add their per-page body class * (`.woocommerce_page_`) to the selector — don't broaden the * rule to `.woocommerce-admin-page` (that would silently strip the * Activity Panel from every WC screen). * * @since 0.8.9 */ .os-chromeless.woocommerce_page_wc-orders .woocommerce-layout__header, .os-chromeless.woocommerce_page_wc-orders--shop_order .woocommerce-layout__header { display: none !important; } .os-chromeless.woocommerce_page_wc-orders #wpbody, .os-chromeless.woocommerce_page_wc-orders--shop_order #wpbody { margin-top: 0 !important; } /* --------------------------------------------------------------- * Revisions screen — page-scoped. * * "← Go to editor" is a back button for a screen the user navigated * into. Here they didn't: the editor is open in its own window * behind this one, and closing this window is the way back. * * The revision tooltip is positioned upward from the bottom of * `.revisions-control-frame` and clears the top of the viewport only * thanks to the screen H1, which chromeless hides. An iframe can't * overflow its box, so the space has to be given back: 48px covers * both slider modes. * * The padding goes on `.revisions`, not on the frame — the frame is * the positioned ancestor for the compare-mode checkbox and the * tooltip, so padding it would leave both behind at the old top edge. * --------------------------------------------------------------- */ .os-chromeless.revision-php .wrap > h1.long-header + a { display: none; } .os-chromeless.revision-php .revisions { padding-top: 48px; } /* --------------------------------------------------------------- * Dashboard welcome panel * The default 16px top margin pushes the panel down and creates a * visible gap at the top of the iframe. Collapse it in chromeless * mode so the panel sits flush with the top of the window body. * --------------------------------------------------------------- */ .os-chromeless #welcome-panel, .os-chromeless .welcome-panel { margin-top: 0.5em; } /* --------------------------------------------------------------- * Footer * The classic footer is not rendered in chromeless mode, * but some pages have bottom padding assuming it exists. * --------------------------------------------------------------- */ .os-chromeless #wpbody-content { margin: 0; padding: 0 8px 8px; float: none; width: auto; } /* --------------------------------------------------------------- * Block Editor — hide Gutenberg chrome that would break the window. * * The fullscreen-mode close button (the "W" logo top-left of the editor) * is an that navigates the iframe to a * non-chromeless URL — which re-renders the entire classic admin * inside our desktop window. The link interceptor in the chromeless * bridge catches it too, but hiding the button removes the visual * affordance so users never try to click "back to dashboard" inside * what looks like a self-contained window. * * The site editor's equivalent navigation affordances (site hub toggle, * "back to dashboard" link) get the same treatment. * * The welcome guide is intentionally NOT hidden anymore. Earlier * iterations both CSS-hid the dialog AND flipped `core/edit-post: * welcomeGuide` to `false` at the data layer on every chromeless * mount — but Gutenberg already persists the user's "Get started" * dismissal to user meta the moment they close it, so the override * was permanently stealing the one-time orientation tour from every * user who never got to see it. Modal's focus trap is Tab-only and * doesn't fight the shell. Let it run. (See git history for the * removed `openstation_chromeless_editor_preferences` override.) * --------------------------------------------------------------- */ .os-chromeless .edit-post-fullscreen-mode-close, .os-chromeless .edit-post-fullscreen-mode-close__view-mode-toggle, .os-chromeless .edit-site-navigation-link, .os-chromeless .edit-site-site-hub, .os-chromeless .edit-site-site-hub__toggle { display: none !important; } /* --------------------------------------------------------------- * Block Editor & Site Editor — full-bleed layouts. * Gutenberg owns its entire viewport (its own header bar, side * panels, etc.). Any padding around #wpbody-content crops the * editor and breaks its layout, so reset to zero on those pages * and let the editor render edge-to-edge. * --------------------------------------------------------------- */ .os-chromeless.block-editor-page #wpbody-content, .os-chromeless.site-editor-php #wpbody-content, .os-chromeless.is-fullscreen-mode #wpbody-content { padding: 0; } /* --------------------------------------------------------------- * "Boot" SPA pages (Font Library, Options → Connectors). * * These screens mount a React app into `.boot-layout-container` and * paint `#wpwrap` a dark `#1e1e1e` via inline