From 7dbf4a331158d8c5c7126dd6e8a52497b61396f5 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 7 Nov 2026 15:33:13 +0000 Subject: [PATCH] Load the early theme script from a file so the CSP allows it Production's Content-Security-Policy has no script-src, so it falls back to default-src 'self' and blocks inline scripts. The inline head script that applied the saved theme never ran on prod, leaving the page on the system theme while the selector showed the saved choice. Move it to resources/js/theme-init.js, built by esbuild alongside app.js into a minified, source-mapped file loaded as a blocking script in the head. ThemeSelector now also applies the saved colour scheme, so the page and buttons can't disagree. Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- public/assets/js/app.js | 2 +- public/assets/js/app.js.br | Bin 1393 -> 1407 bytes public/assets/js/app.js.map | 4 ++-- public/assets/js/app.js.zst | Bin 1611 -> 1628 bytes public/assets/js/theme-init.js | 2 ++ public/assets/js/theme-init.js.br | Bin 0 -> 131 bytes public/assets/js/theme-init.js.map | 7 +++++++ public/assets/js/theme-init.js.zst | Bin 0 -> 160 bytes resources/js/theme-init.js | 12 ++++++++++++ resources/js/theme-selector.js | 4 ++++ resources/views/master.blade.php | 11 ++--------- 12 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 public/assets/js/theme-init.js create mode 100644 public/assets/js/theme-init.js.br create mode 100644 public/assets/js/theme-init.js.map create mode 100644 public/assets/js/theme-init.js.zst create mode 100644 resources/js/theme-init.js diff --git a/package.json b/package.json index b0ac9882..d71cbca7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lightningcss": "lightningcss --output-dir public/assets/css --sourcemap --bundle --minify resources/css/app.css", "fix-sourcemap": "./scripts/fix-sourcemap.sh", "build-css": "npm run lightningcss && npm run fix-sourcemap", - "build-js": "esbuild resources/js/app.js --bundle --minify --sourcemap --outfile=public/assets/js/app.js", + "build-js": "esbuild resources/js/app.js resources/js/theme-init.js --bundle --minify --sourcemap --outdir=public/assets/js", "compress": "./scripts/compress.sh", "build": "npm run lint && npm run build-css && npm run build-js && npm run compress" }, diff --git a/public/assets/js/app.js b/public/assets/js/app.js index f1b9a1a9..2992f727 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -1,2 +1,2 @@ -(()=>{var l=class{constructor(){}async register(){let e=await this.getCreateOptions(),t={challenge:this.base64URLStringToBuffer(e.challenge),rp:{id:e.rp.id,name:e.rp.name},user:{id:new TextEncoder().encode(window.atob(e.user.id)),name:e.user.name,displayName:e.user.displayName},pubKeyCredParams:e.pubKeyCredParams,excludeCredentials:[],authenticatorSelection:e.authenticatorSelection,timeout:6e4},r=await navigator.credentials.create({publicKey:t});if(!r)throw new Error("Error generating a passkey");let n={id:r.id?r.id:null,type:r.type?r.type:null,rawId:r.rawId?this.bufferToBase64URLString(r.rawId):null,response:{attestationObject:r.response.attestationObject?this.bufferToBase64URLString(r.response.attestationObject):null,clientDataJSON:r.response.clientDataJSON?this.bufferToBase64URLString(r.response.clientDataJSON):null}};if(!(await window.fetch("/admin/passkeys/register",{method:"POST",body:JSON.stringify(n),cache:"no-cache",headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Error saving the passkey");window.location.reload()}async getCreateOptions(){return await(await fetch("/admin/passkeys/register",{method:"GET"})).json()}async login(){let e=await this.getLoginData(),t=await navigator.credentials.get({publicKey:{challenge:this.base64URLStringToBuffer(e.challenge),userVerification:e.userVerification,timeout:6e4}});if(!t)throw new Error("Authentication failed");let r={id:t.id?t.id:"",type:t.type?t.type:"",rawId:t.rawId?this.bufferToBase64URLString(t.rawId):"",response:{authenticatorData:t.response.authenticatorData?this.bufferToBase64URLString(t.response.authenticatorData):"",clientDataJSON:t.response.clientDataJSON?this.bufferToBase64URLString(t.response.clientDataJSON):"",signature:t.response.signature?this.bufferToBase64URLString(t.response.signature):"",userHandle:t.response.userHandle?this.bufferToBase64URLString(t.response.userHandle):""}};if(!(await window.fetch("/login/passkey",{method:"POST",body:JSON.stringify(r),headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Login failed");window.location.assign("/admin")}async getLoginData(){return await(await fetch("/login/passkey",{method:"GET"})).json()}base64URLStringToBuffer(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,n=t.padEnd(t.length+r,"="),s=window.atob(n),i=new ArrayBuffer(s.length),o=new Uint8Array(i);for(let c=0;c{this.applyTheme(this.currentTheme==="light"?"system":"light")}),this.btnDark.addEventListener("click",()=>{this.applyTheme(this.currentTheme==="dark"?"system":"dark")})}applyTheme(e){let t=e==="light"||e==="dark"?e:"light dark",r=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light",n=document.documentElement,s=n.style.getPropertyValue("color-scheme");s===""&&(s="light dark");let i=!1;t!==s&&(t==="light dark"?i=s!==r:s==="light dark"?i=t!==r:i=!0);let o=()=>{this.currentTheme=e,n.style.setProperty("color-scheme",t),a.storeTheme(e),this.updateButtons(),this.status.textContent=e==="system"?"Theme set to system default":""};n.style.viewTransitionName="changing-theme",i&&document.startViewTransition?document.startViewTransition(o):o()}};var d=new l;document.querySelectorAll(".add-passkey").forEach(a=>{a.addEventListener("click",()=>{d.register()})});document.querySelectorAll(".login-passkey").forEach(a=>{a.addEventListener("click",()=>{d.login()})});var u=new h;u.setupEventListeners();})(); +(()=>{var l=class{constructor(){}async register(){let e=await this.getCreateOptions(),t={challenge:this.base64URLStringToBuffer(e.challenge),rp:{id:e.rp.id,name:e.rp.name},user:{id:new TextEncoder().encode(window.atob(e.user.id)),name:e.user.name,displayName:e.user.displayName},pubKeyCredParams:e.pubKeyCredParams,excludeCredentials:[],authenticatorSelection:e.authenticatorSelection,timeout:6e4},r=await navigator.credentials.create({publicKey:t});if(!r)throw new Error("Error generating a passkey");let n={id:r.id?r.id:null,type:r.type?r.type:null,rawId:r.rawId?this.bufferToBase64URLString(r.rawId):null,response:{attestationObject:r.response.attestationObject?this.bufferToBase64URLString(r.response.attestationObject):null,clientDataJSON:r.response.clientDataJSON?this.bufferToBase64URLString(r.response.clientDataJSON):null}};if(!(await window.fetch("/admin/passkeys/register",{method:"POST",body:JSON.stringify(n),cache:"no-cache",headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Error saving the passkey");window.location.reload()}async getCreateOptions(){return await(await fetch("/admin/passkeys/register",{method:"GET"})).json()}async login(){let e=await this.getLoginData(),t=await navigator.credentials.get({publicKey:{challenge:this.base64URLStringToBuffer(e.challenge),userVerification:e.userVerification,timeout:6e4}});if(!t)throw new Error("Authentication failed");let r={id:t.id?t.id:"",type:t.type?t.type:"",rawId:t.rawId?this.bufferToBase64URLString(t.rawId):"",response:{authenticatorData:t.response.authenticatorData?this.bufferToBase64URLString(t.response.authenticatorData):"",clientDataJSON:t.response.clientDataJSON?this.bufferToBase64URLString(t.response.clientDataJSON):"",signature:t.response.signature?this.bufferToBase64URLString(t.response.signature):"",userHandle:t.response.userHandle?this.bufferToBase64URLString(t.response.userHandle):""}};if(!(await window.fetch("/login/passkey",{method:"POST",body:JSON.stringify(r),headers:{"Content-Type":"application/json","X-CSRF-TOKEN":document.querySelector('meta[name="csrf-token"]').getAttribute("content")}})).ok)throw new Error("Login failed");window.location.assign("/admin")}async getLoginData(){return await(await fetch("/login/passkey",{method:"GET"})).json()}base64URLStringToBuffer(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,n=t.padEnd(t.length+r,"="),s=window.atob(n),i=new ArrayBuffer(s.length),o=new Uint8Array(i);for(let c=0;c{this.applyTheme(this.currentTheme==="light"?"system":"light")}),this.btnDark.addEventListener("click",()=>{this.applyTheme(this.currentTheme==="dark"?"system":"dark")})}applyTheme(e){let t=e==="light"||e==="dark"?e:"light dark",r=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light",n=document.documentElement,s=n.style.getPropertyValue("color-scheme");s===""&&(s="light dark");let i=!1;t!==s&&(t==="light dark"?i=s!==r:s==="light dark"?i=t!==r:i=!0);let o=()=>{this.currentTheme=e,n.style.setProperty("color-scheme",t),a.storeTheme(e),this.updateButtons(),this.status.textContent=e==="system"?"Theme set to system default":""};n.style.viewTransitionName="changing-theme",i&&document.startViewTransition?document.startViewTransition(o):o()}};var d=new l;document.querySelectorAll(".add-passkey").forEach(a=>{a.addEventListener("click",()=>{d.register()})});document.querySelectorAll(".login-passkey").forEach(a=>{a.addEventListener("click",()=>{d.login()})});var u=new h;u.setupEventListeners();})(); //# sourceMappingURL=app.js.map diff --git a/public/assets/js/app.js.br b/public/assets/js/app.js.br index b24963915b12c32a275c4b5590b660c0790746b1..9393eb4e2add3b7d42155eeebae858b49a9aab65 100644 GIT binary patch literal 1407 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 {\n this.applyTheme(this.currentTheme === 'light' ? 'system' : 'light');\n });\n\n this.btnDark.addEventListener('click', () => {\n this.applyTheme(this.currentTheme === 'dark' ? 'system' : 'dark');\n });\n }\n\n applyTheme(theme) {\n const selectedTheme = (theme === 'light' || theme === 'dark') ? theme : 'light dark';\n\n const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';\n const html = document.documentElement;\n let currentColorScheme = html.style.getPropertyValue('color-scheme');\n if (currentColorScheme === '') {\n currentColorScheme = 'light dark';\n }\n\n /* do we need to transition */\n let doTransition = false;\n\n if (selectedTheme !== currentColorScheme) {\n if (selectedTheme === 'light dark') {\n doTransition = currentColorScheme !== systemTheme;\n } else if (currentColorScheme === 'light dark') {\n doTransition = selectedTheme !== systemTheme;\n } else {\n doTransition = true;\n }\n }\n\n const applyChange = () => {\n this.currentTheme = theme;\n html.style.setProperty('color-scheme', selectedTheme);\n ThemeSelector.storeTheme(theme);\n\n this.updateButtons();\n this.status.textContent = theme === 'system' ? 'Theme set to system default' : '';\n };\n\n html.style.viewTransitionName = 'changing-theme';\n if (doTransition && document.startViewTransition) {\n document.startViewTransition(applyChange);\n } else {\n applyChange();\n }\n }\n}\n\nexport { ThemeSelector };\n", "import { Auth } from './auth.js';\nimport { ThemeSelector } from './theme-selector.js';\n\nlet auth = new Auth();\ndocument.querySelectorAll('.add-passkey').forEach((el) => {\n el.addEventListener('click', () => {\n auth.register();\n });\n});\n\ndocument.querySelectorAll('.login-passkey').forEach((el) => {\n el.addEventListener('click', () => {\n auth.login();\n });\n});\n\nlet themeSelector = new ThemeSelector();\nthemeSelector.setupEventListeners();\n"], - "mappings": "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", + "sourcesContent": ["class Auth {\n constructor() {}\n\n async register() {\n const createOptions = await this.getCreateOptions();\n\n const publicKeyCredentialCreationOptions = {\n challenge: this.base64URLStringToBuffer(createOptions.challenge),\n rp: {\n id: createOptions.rp.id,\n name: createOptions.rp.name,\n },\n user: {\n id: new TextEncoder().encode(window.atob(createOptions.user.id)),\n name: createOptions.user.name,\n displayName: createOptions.user.displayName,\n },\n pubKeyCredParams: createOptions.pubKeyCredParams,\n excludeCredentials: [],\n authenticatorSelection: createOptions.authenticatorSelection,\n timeout: 60000,\n };\n\n const credential = await navigator.credentials.create({\n publicKey: publicKeyCredentialCreationOptions\n });\n if (!credential) {\n throw new Error('Error generating a passkey');\n }\n\n const authenticatorAttestationResponse = {\n id: credential.id ? credential.id : null,\n type: credential.type ? credential.type : null,\n rawId: credential.rawId ? this.bufferToBase64URLString(credential.rawId) : null,\n response: {\n attestationObject: credential.response.attestationObject ? this.bufferToBase64URLString(credential.response.attestationObject) : null,\n clientDataJSON: credential.response.clientDataJSON ? this.bufferToBase64URLString(credential.response.clientDataJSON) : null,\n }\n };\n\n const registerCredential = await window.fetch('/admin/passkeys/register', {\n method: 'POST',\n body: JSON.stringify(authenticatorAttestationResponse),\n cache: 'no-cache',\n headers: {\n 'Content-Type': 'application/json',\n 'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'),\n },\n });\n\n if (!registerCredential.ok) {\n throw new Error('Error saving the passkey');\n }\n\n window.location.reload();\n }\n\n async getCreateOptions() {\n const response = await fetch('/admin/passkeys/register', {\n method: 'GET',\n });\n\n return await response.json();\n }\n\n async login() {\n const loginData = await this.getLoginData();\n\n const publicKeyCredential = await navigator.credentials.get({\n publicKey: {\n challenge: this.base64URLStringToBuffer(loginData.challenge),\n userVerification: loginData.userVerification,\n timeout: 60000,\n }\n });\n\n if (!publicKeyCredential) {\n throw new Error('Authentication failed');\n }\n\n const authenticatorAttestationResponse = {\n id: publicKeyCredential.id ? publicKeyCredential.id : '',\n type: publicKeyCredential.type ? publicKeyCredential.type : '',\n rawId: publicKeyCredential.rawId ? this.bufferToBase64URLString(publicKeyCredential.rawId) : '',\n response: {\n authenticatorData: publicKeyCredential.response.authenticatorData ? this.bufferToBase64URLString(publicKeyCredential.response.authenticatorData) : '',\n clientDataJSON: publicKeyCredential.response.clientDataJSON ? this.bufferToBase64URLString(publicKeyCredential.response.clientDataJSON) : '',\n signature: publicKeyCredential.response.signature ? this.bufferToBase64URLString(publicKeyCredential.response.signature) : '',\n userHandle: publicKeyCredential.response.userHandle ? this.bufferToBase64URLString(publicKeyCredential.response.userHandle) : '',\n },\n };\n\n const loginAttempt = await window.fetch('/login/passkey', {\n method: 'POST',\n body: JSON.stringify(authenticatorAttestationResponse),\n headers: {\n 'Content-Type': 'application/json',\n 'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'),\n },\n });\n\n if (!loginAttempt.ok) {\n throw new Error('Login failed');\n }\n\n window.location.assign('/admin');\n }\n\n async getLoginData() {\n const response = await fetch('/login/passkey', {\n method: 'GET',\n });\n\n return await response.json();\n }\n\n /**\n * Convert a base64 URL string to a buffer.\n *\n * Sourced from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/browser/src/helpers/base64URLStringToBuffer.ts#L8\n *\n * @param {string} base64URLString\n * @returns {ArrayBuffer}\n */\n base64URLStringToBuffer(base64URLString) {\n // Convert from Base64URL to Base64\n const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/');\n /**\n * Pad with '=' until it's a multiple of four\n * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding\n * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding\n * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding\n * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding\n */\n const padLength = (4 - (base64.length % 4)) % 4;\n const padded = base64.padEnd(base64.length + padLength, '=');\n // Convert to a binary string\n const binary = window.atob(padded);\n // Convert binary string to buffer\n const buffer = new ArrayBuffer(binary.length);\n const bytes = new Uint8Array(buffer);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return buffer;\n }\n\n /**\n * Convert a buffer to a base64 URL string.\n *\n * Sourced from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/browser/src/helpers/bufferToBase64URLString.ts#L7\n *\n * @param {ArrayBuffer} buffer\n * @returns {string}\n */\n bufferToBase64URLString(buffer) {\n const bytes = new Uint8Array(buffer);\n let str = '';\n for (const charCode of bytes) {\n str += String.fromCharCode(charCode);\n }\n const base64String = btoa(str);\n return base64String.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n }\n}\n\nexport { Auth };\n", "class ThemeSelector {\n constructor() {\n this.widget = document.querySelector('#theme-selector');\n this.btnLight = this.widget.querySelector('#btn-light');\n this.btnDark = this.widget.querySelector('#btn-dark');\n this.status = this.widget.querySelector('#theme-status');\n this.currentTheme = ThemeSelector.storedTheme();\n if (this.currentTheme !== 'system') {\n /* backstop in case theme-init.js didn't run, so the page always matches the buttons */\n document.documentElement.style.setProperty('color-scheme', this.currentTheme);\n }\n this.updateButtons();\n }\n\n static storedTheme() {\n try {\n const theme = localStorage.getItem('theme');\n return (theme === 'light' || theme === 'dark') ? theme : 'system';\n } catch {\n return 'system';\n }\n }\n\n static storeTheme(theme) {\n try {\n if (theme === 'system') {\n localStorage.removeItem('theme');\n } else {\n localStorage.setItem('theme', theme);\n }\n } catch {\n /* storage unavailable, the choice just won't persist */\n }\n }\n\n updateButtons() {\n this.btnLight.setAttribute('aria-pressed', String(this.currentTheme === 'light'));\n this.btnDark.setAttribute('aria-pressed', String(this.currentTheme === 'dark'));\n }\n\n setupEventListeners() {\n this.btnLight.addEventListener('click', () => {\n this.applyTheme(this.currentTheme === 'light' ? 'system' : 'light');\n });\n\n this.btnDark.addEventListener('click', () => {\n this.applyTheme(this.currentTheme === 'dark' ? 'system' : 'dark');\n });\n }\n\n applyTheme(theme) {\n const selectedTheme = (theme === 'light' || theme === 'dark') ? theme : 'light dark';\n\n const systemTheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';\n const html = document.documentElement;\n let currentColorScheme = html.style.getPropertyValue('color-scheme');\n if (currentColorScheme === '') {\n currentColorScheme = 'light dark';\n }\n\n /* do we need to transition */\n let doTransition = false;\n\n if (selectedTheme !== currentColorScheme) {\n if (selectedTheme === 'light dark') {\n doTransition = currentColorScheme !== systemTheme;\n } else if (currentColorScheme === 'light dark') {\n doTransition = selectedTheme !== systemTheme;\n } else {\n doTransition = true;\n }\n }\n\n const applyChange = () => {\n this.currentTheme = theme;\n html.style.setProperty('color-scheme', selectedTheme);\n ThemeSelector.storeTheme(theme);\n\n this.updateButtons();\n this.status.textContent = theme === 'system' ? 'Theme set to system default' : '';\n };\n\n html.style.viewTransitionName = 'changing-theme';\n if (doTransition && document.startViewTransition) {\n document.startViewTransition(applyChange);\n } else {\n applyChange();\n }\n }\n}\n\nexport { ThemeSelector };\n", "import { Auth } from './auth.js';\nimport { ThemeSelector } from './theme-selector.js';\n\nlet auth = new Auth();\ndocument.querySelectorAll('.add-passkey').forEach((el) => {\n el.addEventListener('click', () => {\n auth.register();\n });\n});\n\ndocument.querySelectorAll('.login-passkey').forEach((el) => {\n el.addEventListener('click', () => {\n auth.login();\n });\n});\n\nlet themeSelector = new ThemeSelector();\nthemeSelector.setupEventListeners();\n"], + "mappings": "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", "names": ["Auth", "createOptions", "publicKeyCredentialCreationOptions", "credential", "authenticatorAttestationResponse", "loginData", "publicKeyCredential", "base64URLString", "base64", "padLength", "padded", "binary", "buffer", "bytes", "i", "str", "charCode", "ThemeSelector", "_ThemeSelector", "theme", "selectedTheme", "systemTheme", "html", "currentColorScheme", "doTransition", "applyChange", "auth", "Auth", "el", "themeSelector", "ThemeSelector"] } diff --git a/public/assets/js/app.js.zst b/public/assets/js/app.js.zst index ae85c89d9a7cd53cd0f2bca3df62aa9e49956a45..641fc4180fe5da3ee51e5daae9faeb9c27bfb801 100644 GIT binary patch literal 1628 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 diff --git a/public/assets/js/theme-init.js b/public/assets/js/theme-init.js new file mode 100644 index 00000000..a8898291 --- /dev/null +++ b/public/assets/js/theme-init.js @@ -0,0 +1,2 @@ +(()=>{try{let e=localStorage.getItem("theme");(e==="light"||e==="dark")&&document.documentElement.style.setProperty("color-scheme",e)}catch{}})(); +//# sourceMappingURL=theme-init.js.map diff --git a/public/assets/js/theme-init.js.br b/public/assets/js/theme-init.js.br new file mode 100644 index 0000000000000000000000000000000000000000..6446006bc0d78843576da0ac63469d3ccc38fd27 GIT binary patch literal 131 zcmZ2@f|Wr%`Sq`cID@t2 z(eJjOHlHzDH*iY}=pm_AY{aon`sh6P#j;!gbIWMArO60`u$g6hq7yzd3LZtuz literal 0 HcmV?d00001 diff --git a/public/assets/js/theme-init.js.map b/public/assets/js/theme-init.js.map new file mode 100644 index 00000000..2bc745a0 --- /dev/null +++ b/public/assets/js/theme-init.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../resources/js/theme-init.js"], + "sourcesContent": ["/*\n * Apply a saved theme choice before first paint to avoid a flash of the system theme.\n * Loaded as a blocking external script (not inline) so it passes the site's CSP.\n */\ntry {\n const theme = localStorage.getItem('theme');\n if (theme === 'light' || theme === 'dark') {\n document.documentElement.style.setProperty('color-scheme', theme);\n }\n} catch {\n /* storage unavailable, fall back to the system theme */\n}\n"], + "mappings": "MAIA,GAAI,CACF,IAAMA,EAAQ,aAAa,QAAQ,OAAO,GACtCA,IAAU,SAAWA,IAAU,SACjC,SAAS,gBAAgB,MAAM,YAAY,eAAgBA,CAAK,CAEpE,MAAQ,CAER", + "names": ["theme"] +} diff --git a/public/assets/js/theme-init.js.zst b/public/assets/js/theme-init.js.zst new file mode 100644 index 0000000000000000000000000000000000000000..99aeb6ce430de603ae8cf858c8363fbde61e587e GIT binary patch literal 160 zcmdPcs{dDI*IX8cN1O_h0auxun|wa?ge%W_J$HkP`h2FyB^S<#aGExl7TyW2iN5Ki z#}}fV9XYvh)%rcm2MrpS?RMnObg93@YvowJ@blR#wr{pZN1DI4{&lqOx9_9yKK)sX z6C3(28DBE_9<=qkr{-VT3zlUs?;iels=7CB`MLjhJU>sB+jF1i@_a?1ed4SPdNcNH TE=W!*j+9FfcqVtr{cJY?Sj0_Y literal 0 HcmV?d00001 diff --git a/resources/js/theme-init.js b/resources/js/theme-init.js new file mode 100644 index 00000000..cba54c8e --- /dev/null +++ b/resources/js/theme-init.js @@ -0,0 +1,12 @@ +/* + * Apply a saved theme choice before first paint to avoid a flash of the system theme. + * Loaded as a blocking external script (not inline) so it passes the site's CSP. + */ +try { + const theme = localStorage.getItem('theme'); + if (theme === 'light' || theme === 'dark') { + document.documentElement.style.setProperty('color-scheme', theme); + } +} catch { + /* storage unavailable, fall back to the system theme */ +} diff --git a/resources/js/theme-selector.js b/resources/js/theme-selector.js index 582a7b4c..ee738872 100644 --- a/resources/js/theme-selector.js +++ b/resources/js/theme-selector.js @@ -5,6 +5,10 @@ class ThemeSelector { this.btnDark = this.widget.querySelector('#btn-dark'); this.status = this.widget.querySelector('#theme-status'); this.currentTheme = ThemeSelector.storedTheme(); + if (this.currentTheme !== 'system') { + /* backstop in case theme-init.js didn't run, so the page always matches the buttons */ + document.documentElement.style.setProperty('color-scheme', this.currentTheme); + } this.updateButtons(); } diff --git a/resources/views/master.blade.php b/resources/views/master.blade.php index cbac614e..0af7f507 100644 --- a/resources/views/master.blade.php +++ b/resources/views/master.blade.php @@ -25,15 +25,8 @@ @endif @stack('head') - + {{-- Blocking on purpose: applies a saved theme before first paint. Must stay external to satisfy the CSP --}} +
-- 2.56.0