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:
John O'Keefe committed 2026-10-25 16:58:28 -04:00
1 parent a730a62280
commit eb57b7208b
15 files changed
+154 -23

No files matched your search

+5 -5
View File
@@ -554,7 +554,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
</div>
<div class="reader-tools-row" x-show="comicFlow !== 'webtoon'">
<span>Night mode</span>
<button @click="fxInvert = !fxInvert; applyFxFilter(true)" class="reader-popover-btn text-xs px-2" :class="fxInvert ? 'text-blue-400' : ''" x-text="fxInvert ? 'On' : 'Off'">Off</button>
<button @click="toggleFxInvert()" class="reader-popover-btn text-xs px-2" :class="fxInvert ? 'text-blue-400' : ''" x-text="fxInvert ? 'On' : 'Off'">Off</button>
</div>
<div class="reader-tools-row" x-show="isPDF && !treatAsComic">
<span>Pointer</span>
@@ -593,7 +593,7 @@ templ ReaderTOCDrawer() {
<button :class="tocTab === 'contents' ? 'active' : ''" @click="tocTab = 'contents'">
Contents <span class="reader-tab-count" x-text="tocItems.length">0</span>
</button>
<button :class="tocTab === 'pages' ? 'active' : ''" @click="tocTab = 'pages'; initPageThumbs()">
<button :class="tocTab === 'pages' ? 'active' : ''" @click="openPageThumbs()">
Pages <span class="reader-tab-count" x-text="pageThumbList.length || ''">0</span>
</button>
</div>
@@ -691,7 +691,7 @@ templ ReaderAnnotationsDrawer() {
x-text="noteCountLabel(newNoteText)"
></div>
<button
@click="addNote(newNoteText); newNoteText = ''"
@click="submitNote()"
:disabled="noteOverLimit(newNoteText)"
class="w-full py-1.5 mt-1 mb-3 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
@@ -820,7 +820,7 @@ templ ReaderSearchDrawer() {
/>
<button
x-show="searchQuery || searchGroups.length"
@click="searchQuery = ''; clearSearchResults()"
@click="clearSearch()"
class="p-2 rounded-lg hover:bg-gray-700 shrink-0"
title="Clear results"
aria-label="Clear results"
@@ -908,7 +908,7 @@ templ ReaderSettingsDrawer() {
<template x-for="sw in themeSwatches" :key="sw.value">
<button
type="button"
@click="readingTheme = sw.value; applyTheme()"
@click="pickReadingTheme(sw.value)"
class="theme-swatch"
:class="readingTheme === sw.value ? 'ring-2 ring-blue-500' : ''"
:style="'background-color:' + sw.bg + '; color:' + sw.fg"