From 96f64e276cb204a8cbcbb9a6fe5caa6dfb2bf8c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 22:48:53 +0000 Subject: [PATCH 1/3] fix(configurator): note the "Link visited" override cannot preview live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browsers restrict :visited styling for privacy, so the preview's links (never visited) can't repaint with a --sf-color-link--visited override — it only takes effect on genuinely-visited links on the published site. The control was correct but read as broken; add a short inline note on the row so the behaviour is expected rather than surprising. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PtvNAKsrEE1mynifQbjSun --- configurator/src/components/panels/ColorsPanel.svelte | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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} From 74e86b7ab1c867dcd65f819abff05dbe7240d531 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 22:48:53 +0000 Subject: [PATCH 2/3] fix(configurator): isolate the embedded admin mount in a shadow dom When mounted into a host page's #slashed-admin-app (the WordPress plugin), the panel shared one document with every other admin plugin's CSS and JS. A competing reset or a broad rule like `* { pointer-events: none }` could leave it fully rendered but non-interactive, while the frontend overlay (already in a Shadow DOM) stayed fine on the same site. Mount the embedded panel inside a Shadow DOM too. This is CSS/DOM encapsulation, not a JS sandbox (an open shadow root shares the host realm), but it is what the failure needs: host styles no longer cross in, and Svelte's delegated listeners bind to the shadow-internal root so host document-level handlers can't preempt them. The panel stylesheet is preloaded, then linked inside the shadow once it loads (head styles don't cross the boundary); on stylesheet error or a stalled load we fall back to the previous light-DOM mount, which the plugin's head-enqueued app.css still styles. attachShadow is deferred until load success so a failed stylesheet can never trap the panel unstyled. Standalone (#app) is unchanged. embeddedCssUrl() trims its input so a whitespace-only value can't pass the same-origin check and mount without a stylesheet. Verified headlessly: under a hostile `* { pointer-events: none !important }` rule plus a clobbered window.wp, the shadow-isolated panel renders fully and responds to clicks; a 404 stylesheet falls back to a styled light-DOM mount. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PtvNAKsrEE1mynifQbjSun --- configurator/src/main.ts | 103 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 7 deletions(-) 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; From 8ba3eb69c1e6624f6417fe2d10b1fc83e914c669 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 07:03:24 +0000 Subject: [PATCH 3/3] fix(deps): resolve high-severity advisories via npm audit fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm audit flagged 5 high-severity advisories in transitive dev/build dependencies (ip-address, js-yaml, nanoid, undici, and one more). All are fixable without breaking changes, so `npm audit fix` bumps them to patched patch/minor versions. Only package-lock.json changes; no root version bump, so version-sync stays intact. `npm run build` still succeeds and `npm audit --audit-level=high` now reports 0 vulnerabilities — clearing the Dependency vulnerability audit CI gate. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PtvNAKsrEE1mynifQbjSun --- package-lock.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) 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": {