feat(web): switch to the Alpine CSP build; drop 'unsafe-eval' from CSP
Standard Alpine evaluates every attribute expression through new Function(), which a strict Content-Security-Policy cannot allow. Swap to the official CSP-safe build (@alpinejs/csp), whose parser evaluates expressions against registered component scope without eval. script-src now carries only 'self' and 'unsafe-inline' (the latter still required by the pre-paint theme flash guard). Bindings rewritten out of the unsupported expression shapes: - 21 multi-statement sequences across admin, audio_reader, docs, header, index, and reader became single component methods (init/loadWatchStatus hooks, commitScrub, pickSkip, pickSpeed, pickDefaultSpeed, chooseSleepOption, chooseSleepClockMode, toggleFxInvert, openPageThumbs, submitNote, clearSearch, pickReadingTheme) - the header sidebar's inline x-data (openPanel/themeMode IIFE) became a registered sidebarShell component; openPanel itself moved to the header root so every sidebar panel shares one open state - the appearance-panel moved into the component init() - header init() now also seeds openPanel from localStorage (or the admin section on /admin pages) Alpine magics (, , , ) remain available — the CSP evaluator injects them into scope. The Playwright suite covers the converted surfaces end to end.
This commit is contained in:
1 parent
a730a62280
commit
eb57b7208b
15 files changed
+154
-23
No files matched your search
@@ -246,7 +246,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
:value="scrubbing ? scrubValue : currentTime"
|
||||
class="relative w-full"
|
||||
@input="scrubbing = true; scrubValue = Number($event.target.value)"
|
||||
@change="seekTo(scrubValue); scrubbing = false"
|
||||
@change="commitScrub()"
|
||||
aria-label="Seek position"
|
||||
/>
|
||||
<!-- Hover tooltip: time + chapter under the pointer -->
|
||||
@@ -395,7 +395,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<template x-for="secs in skipChoices" :key="'skip' + secs">
|
||||
<button
|
||||
type="button"
|
||||
@click="setSkip(secs); skipOpen = false"
|
||||
@click="pickSkip(secs)"
|
||||
class="w-full text-left px-3 py-1.5 rounded-md text-sm"
|
||||
:class="Number(skipSeconds) === secs ? 'font-semibold' : ''"
|
||||
:style="Number(skipSeconds) === secs ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'"
|
||||
@@ -425,7 +425,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<template x-for="rate in speeds" :key="'speed' + rate">
|
||||
<button
|
||||
type="button"
|
||||
@click="setSpeed(rate); speedOpen = false"
|
||||
@click="pickSpeed(rate)"
|
||||
class="w-full text-left px-3 py-1.5 rounded-md text-sm"
|
||||
:class="Number(speed) === rate ? 'font-semibold' : ''"
|
||||
:style="Number(speed) === rate ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'"
|
||||
@@ -437,7 +437,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<button
|
||||
type="button"
|
||||
x-show="!isDefaultSpeed(speed)"
|
||||
@click="setDefaultSpeed(speed); speedOpen = false"
|
||||
@click="pickDefaultSpeed(speed)"
|
||||
class="w-full text-left px-3 py-1.5 mt-1 rounded-md text-xs border-t"
|
||||
style="border-color: var(--border); color: var(--text-secondary);"
|
||||
x-text="'Make ' + speed + '× my default'"
|
||||
@@ -481,7 +481,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<template x-for="opt in sleepChoices" :key="opt.key">
|
||||
<button
|
||||
type="button"
|
||||
@click="chooseSleep(opt); sleepOpen = false"
|
||||
@click="chooseSleepOption(opt)"
|
||||
class="w-full text-left px-3 py-1.5 rounded-md text-sm"
|
||||
:class="isSleepActive(opt.key) ? 'font-semibold' : ''"
|
||||
:style="isSleepActive(opt.key) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'"
|
||||
@@ -500,7 +500,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="chooseSleepClock(); sleepOpen = false"
|
||||
@click="chooseSleepClockMode()"
|
||||
class="text-xs px-2.5 py-1 rounded-md font-semibold"
|
||||
:style="sleepArmedChoiceKey === 'clock' ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'background-color: var(--surface-hover); color: var(--text-secondary);'"
|
||||
>Arm</button>
|
||||
|
||||
Reference in new issue
Block a user