diff --git a/configurator/src/components/panels/ColorsPanel.svelte b/configurator/src/components/panels/ColorsPanel.svelte
index 492ab7ae..b8ea4fcd 100644
--- a/configurator/src/components/panels/ColorsPanel.svelte
+++ b/configurator/src/components/panels/ColorsPanel.svelte
@@ -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" },
@@ -1100,6 +1103,9 @@
onSet={(v) => onSet(s.name, v)}
onReset={() => onReset(s.name)}
/>
+ {#if s.note}
+
{s.note}
+ {/if}
{/each}
diff --git a/configurator/src/main.ts b/configurator/src/main.ts
index 387068b4..880aebff 100644
--- a/configurator/src/main.ts
+++ b/configurator/src/main.ts
@@ -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 | 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 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;
+ 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;
diff --git a/package-lock.json b/package-lock.json
index 47df2406..3a0d2e60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2460,9 +2460,9 @@
}
},
"node_modules/fast-uri": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
- "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
+ "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
"dev": true,
"funding": [
{
@@ -2934,9 +2934,9 @@
}
},
"node_modules/ip-address": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
- "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
+ "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3171,9 +3171,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"dev": true,
"funding": [
{
@@ -3700,9 +3700,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
@@ -5044,9 +5044,9 @@
}
},
"node_modules/undici": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz",
- "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==",
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
+ "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
"dev": true,
"license": "MIT",
"engines": {