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
10 changes: 8 additions & 2 deletions configurator/src/components/panels/ColorsPanel.svelte
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 configurator/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);
}
}

export default app;
30 changes: 15 additions & 15 deletions package-lock.json

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