Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
136 changes: 68 additions & 68 deletions SLASHED-for-WP/admin-app/.vendored-manifest.json

Large diffs are not rendered by default.

32 changes: 7 additions & 25 deletions SLASHED-for-WP/admin-app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,12 @@
},
];

const SEMANTIC_OVERRIDES = [
const SEMANTIC_OVERRIDES: { name: string; label: string; note?: string }[] = [
{ name: "--sf-color-link", label: "Link" },
{ name: "--sf-color-link--visited", label: "Link visited" },
// Browsers restrict :visited styling for privacy — the preview links here
// have never been visited, so this override can't repaint them live. It
// still applies on real, already-visited links on the published site.
{ name: "--sf-color-link--visited", label: "Link visited", note: "Only affects links already visited in the browser — the preview can't show it (a browser privacy rule), but it applies on your live site." },
{ name: "--sf-color-mark-bg", label: "Mark background" },
{ name: "--sf-color-mark-text", label: "Mark text" },
{ name: "--sf-color-code-bg", label: "Code background" },
Expand Down Expand Up @@ -1100,6 +1103,9 @@
onSet={(v) => onSet(s.name, v)}
onReset={() => onReset(s.name)}
/>
{#if s.note}
<div class="mt-1 text-[9px] text-slate-400 dark:text-slate-600 leading-relaxed">{s.note}</div>
{/if}
</div>
{/each}
</div>
Expand Down
103 changes: 96 additions & 7 deletions SLASHED-for-WP/admin-app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,104 @@ import '@framework-css/core/macros.css';
// tokens.components.css, so that's not imported separately here.
import '@framework-css/optional/components.css';

// Standalone mounts into #app; the WP plugin renders #slashed-admin-app.
const target =
document.getElementById('app') ?? document.getElementById('slashed-admin-app');
// An embedded host (the WP plugin) may hand us a same-origin URL to the panel
// stylesheet via window.slashedApp.cssUrl. Its presence is how we know to load
// the panel's own CSS into a shadow root (head styles don't cross the shadow
// boundary). Typed locally so this upstream entry stays framework-agnostic.
function embeddedCssUrl(): string | undefined {
const boot = (window as unknown as { slashedApp?: { cssUrl?: string } }).slashedApp;
// Trim before the empty check: a whitespace-only value would survive `!== ''`,
// then `new URL(' ', href)` resolves to the current document and passes the
// same-origin test — mounting in a shadow root with no panel stylesheet.
const url = typeof boot?.cssUrl === 'string' ? boot.cssUrl.trim() : '';
return url !== '' ? url : undefined;
}
function isSameOrigin(url: string): boolean {
try {
return new URL(url, window.location.href).origin === window.location.origin;
} catch {
return false;
}
}

let app: ReturnType<typeof mount> | undefined;
if (target) {
target.innerHTML = '';

function mountInto(root: HTMLElement) {
// Apply the persisted/OS-derived theme class before mounting so the first
// paint is never wrong-themed (no flash of the other mode).
bindThemeRoot(target);
app = mount(App, { target });
bindThemeRoot(root);
app = mount(App, { target: root });
}

// Standalone owns the whole document and mounts straight into #app (light DOM):
// no other app's CSS/JS shares the page, so isolation isn't needed and the
// build's own app.css already styles it.
const standaloneHost = document.getElementById('app');
// The WP plugin renders #slashed-admin-app inside the shared wp-admin document.
const wpHost = document.getElementById('slashed-admin-app');

if (standaloneHost) {
standaloneHost.innerHTML = '';
mountInto(standaloneHost);
} else if (wpHost) {
// Embedded in wp-admin: the panel shares one document with every other admin
// plugin's CSS and JS, and a competing reset or a broad rule like
// `* { pointer-events: none }` can leave it rendered but non-interactive.
// Mount it inside a Shadow DOM, the same encapsulation the frontend overlay
// relies on. This is CSS/DOM encapsulation, not a JS sandbox (an open shadow
// root shares the host realm) — but it's what the failure needs: host styles
// no longer cross into the panel, and Svelte's delegated event listeners bind
// to the shadow-internal root, so host document-level handlers can't preempt
// them. The panel CSS is linked *inside* the shadow (the plugin supplies a
// same-origin cssUrl) because head styles don't cross the boundary, and mount
// is deferred until it loads so the panel never flashes unstyled. Falls back
// to the previous light-DOM mount (styled by the plugin's head-loaded app.css)
// when Shadow DOM or a usable cssUrl isn't available.
const cssUrl = embeddedCssUrl();
wpHost.innerHTML = '';

if (typeof wpHost.attachShadow === 'function' && cssUrl && isSameOrigin(cssUrl)) {
// Preload the panel stylesheet in <head> first and only commit to a Shadow
// DOM once it actually loads. attachShadow() is irreversible, so attaching
// up-front would trap the panel unstyled if the stylesheet errors; on error
// or a stalled load we instead mount into the light DOM, which the plugin's
// head-enqueued app.css still styles.
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssUrl;

let settled = false;
let timer: ReturnType<typeof setTimeout>;
const mountShadow = () => {
if (settled) return;
settled = true;
clearTimeout(timer);
const shadow = wpHost.attachShadow({ mode: 'open' });
shadow.appendChild(link); // move the now-loaded stylesheet inside the shadow
// The App root is `w-full h-full`; give the shadow holder real dimensions
// to fill (the host #slashed-admin-app is sized by the plugin's admin CSS).
const holder = document.createElement('div');
holder.style.width = '100%';
holder.style.height = '100%';
shadow.appendChild(holder);
mountInto(holder);
};
const mountLight = () => {
if (settled) return;
settled = true;
clearTimeout(timer);
link.remove();
mountInto(wpHost);
};
// Stalled load: fall back to a styled light-DOM mount rather than hang on
// the host's "Loading…" text.
timer = setTimeout(mountLight, 5000);
link.addEventListener('load', mountShadow);
link.addEventListener('error', mountLight);
document.head.appendChild(link);
} else {
mountInto(wpHost);
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
export default app;
30 changes: 15 additions & 15 deletions SLASHED-for-WP/assets/admin-app/app.js

Large diffs are not rendered by default.

46 changes: 44 additions & 2 deletions SLASHED-for-WP/includes/class-frontend-configurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,39 @@
*/
class Slashed_Frontend_Configurator {

/**
* Attributes that opt the overlay's ES-module script out of third-party JS
* optimisers (LiteSpeed / SG Optimizer / WP Rocket / Perfmatters / Cloudflare
* Rocket Loader). Combining or delaying a native module breaks its scope and
* Svelte's delegated events, leaving the overlay rendered but unresponsive.
* See Slashed_Token_Page::NO_OPTIMIZE_ATTRS for the full rationale — kept as a
* local copy so this integration file has no cross-class coupling.
*/
const NO_OPTIMIZE_ATTRS = ' data-no-optimize="1" data-no-defer="1" data-no-delay="1" data-no-minify="1" data-nowprocket="1" data-cfasync="false"';

public function __construct() {
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ), 30 );
add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_node' ), 100 );
add_action( 'wp_footer', array( $this, 'render_container' ), 100 );
add_filter( 'wp_inline_script_attributes', array( $this, 'mark_inline_no_optimize' ), 10, 2 );
}

/**
* Carry the optimiser opt-out onto the overlay handle's inline scripts
* (the `__SLASHED_FW_VERSION__` global and the `slashedApp` hydration data).
*
* @param array $attributes Inline <script> tag attributes (includes `id`).
* @param string $data The inline script body (unused).
* @return array
*/
public function mark_inline_no_optimize( $attributes, $data ) {
$id = isset( $attributes['id'] ) ? (string) $attributes['id'] : '';
if ( 0 === strpos( $id, 'slashed-frontend-overlay-js' ) ) {
$attributes['data-no-optimize'] = '1';
$attributes['data-nowprocket'] = '1';
$attributes['data-cfasync'] = 'false';
}
return $attributes;
}

/**
Expand Down Expand Up @@ -149,7 +178,15 @@ public function render_container() {
if ( ! $this->should_load() ) {
return;
}
echo '<div id="slashed-frontend-overlay" style="position:fixed;right:0;top:var(--wp-admin--admin-bar--height,32px);width:min(420px,100vw);height:calc(100vh - var(--wp-admin--admin-bar--height,32px));z-index:99998;"></div>';
// pointer-events:none until the Svelte overlay mounts. This container is a
// full-height fixed layer (the whole viewport width on mobile), so while it
// sits empty — before mount, which plugin-main.ts defers until the panel
// CSS loads (up to 5s), and forever if the module is broken by a JS
// optimiser — a transparent hit-testing layer would silently swallow every
// click over its area. AppOverlay's syncHostBounds() sets pointer-events
// back to auto once mounted, and the panel/trigger re-enable it on their
// own surfaces, so the live editor stays fully interactive.
echo '<div id="slashed-frontend-overlay" style="position:fixed;right:0;top:var(--wp-admin--admin-bar--height,32px);width:min(420px,100vw);height:calc(100vh - var(--wp-admin--admin-bar--height,32px));z-index:99998;pointer-events:none;"></div>';
}

/**
Expand All @@ -164,7 +201,12 @@ public function mark_as_module( $tag, $handle, $src ) {
if ( 'slashed-frontend-overlay' !== $handle ) {
return $tag;
}
return preg_replace( '/<script(\b[^>]*)>/', '<script type="module"$1>', $tag, 1 );
return preg_replace(
'/<script(\b[^>]*)>/',
'<script type="module"' . self::NO_OPTIMIZE_ATTRS . '$1>',
$tag,
1
);
}

/**
Expand Down
56 changes: 55 additions & 1 deletion SLASHED-for-WP/includes/class-token-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function __construct() {
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_filter( 'admin_body_class', array( $this, 'add_body_class' ) );
add_filter( 'wp_inline_script_attributes', array( $this, 'mark_inline_no_optimize' ), 10, 2 );
}

/**
Expand Down Expand Up @@ -201,6 +202,14 @@ public function enqueue_assets( $hook_suffix ) {
'slashed-admin-app',
'slashedApp',
array(
// Same-origin URL to the panel stylesheet. main.ts uses its
// presence to mount the panel inside a Shadow DOM (linking this
// CSS inside the shadow) — CSS/DOM encapsulation that keeps other
// wp-admin plugins' styles and document-level event handlers from
// leaving the panel rendered but dead. app.css stays enqueued in
// <head> too, so main.ts's light-DOM fallback (no Shadow DOM /
// cross-origin URL / stylesheet load failure) is still styled.
'cssUrl' => esc_url_raw( $plugin_url . 'assets/admin-app/app.css' ),
'rest' => array(
'url' => esc_url_raw( rest_url( Slashed_REST_Controller::NAMESPACE ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
Expand Down Expand Up @@ -400,7 +409,52 @@ public function mark_as_module( $tag, $handle, $src ) {
if ( 'slashed-admin-app' !== $handle ) {
return $tag;
}
return preg_replace( '/<script(\b[^>]*)>/', '<script type="module"$1>', $tag, 1 );
return preg_replace(
'/<script(\b[^>]*)>/',
'<script type="module"' . self::NO_OPTIMIZE_ATTRS . '$1>',
$tag,
1
);
}

/**
* Attributes that opt the SPA scripts out of third-party JS optimisers.
*
* The configurator ships as a native ES module. Optimiser plugins that
* concatenate ("combine"), defer, or delay scripts routinely break modules:
* a combined module loses its module scope and Svelte's delegated event
* listeners never bind, so the panel renders but every control is dead — and
* the app's in-memory state is corrupted, so a save posts an empty override
* map (HTTP 200, "saved", yet the page reloads to defaults). Emitting the
* opt-out attributes each optimiser documents keeps the module intact:
* data-no-optimize — LiteSpeed Cache, SG Optimizer
* data-no-defer — LiteSpeed Cache (skip "Load JS Deferred")
* data-no-delay — LiteSpeed Cache / Perfmatters ("delay JS execution")
* data-no-minify — WP Rocket
* data-nowprocket — WP Rocket ("Delay JavaScript Execution")
* data-cfasync — Cloudflare Rocket Loader
* They are inert `data-*` attributes to a browser, so adding them is safe
* even when none of these plugins is present.
*/
const NO_OPTIMIZE_ATTRS = ' data-no-optimize="1" data-no-defer="1" data-no-delay="1" data-no-minify="1" data-nowprocket="1" data-cfasync="false"';

/**
* Carry the same optimiser opt-out onto the handle's inline scripts
* (the `__SLASHED_FW_VERSION__` global and the `slashedApp` hydration data),
* so an "inline JS combine" pass can't detach them from the module either.
*
* @param array $attributes Inline <script> tag attributes (includes `id`).
* @param string $data The inline script body (unused).
* @return array
*/
public function mark_inline_no_optimize( $attributes, $data ) {
$id = isset( $attributes['id'] ) ? (string) $attributes['id'] : '';
if ( 0 === strpos( $id, 'slashed-admin-app-js' ) ) {
$attributes['data-no-optimize'] = '1';
$attributes['data-nowprocket'] = '1';
$attributes['data-cfasync'] = 'false';
}
return $attributes;
}

public function render_page() {
Expand Down
Loading
Loading