fix(web): complete the CSP-build migration across remaining surfaces

- audio reader: scrub/volume/speed/genre-tick bindings converted to
  component methods (onScrubInput, setVolumeFromEvent,
  applyDefaultSpeedFromSelect, volumeLabel, railTickStyle,
  isSkipActive/isSpeedActive) — these used Number/Math/String globals
  that the CSP evaluator cannot resolve
- fmtTime restored to the audioReader component: the unexport pass
  had ripped it from the Alpine.data object (time displays were dead)
- book detail: cover-upload click and banner edit validation moved to
  component methods (triggerCoverUpload, bannerEditHasText)
- admin/docs/index components: init() hooks for their x-init chains
- CSP header: 'unsafe-eval' dropped — the CSP build needs no eval
- e2e spec: replace test.info().annotate (unavailable in this
  Playwright version) with console logging
This commit is contained in:
John O'Keefe committed 2026-10-25 17:22:40 -04:00
1 parent eb57b7208b
commit 09545ca5e1
16 files changed
+249 -234

No files matched your search

+30 -4
View File
@@ -699,15 +699,40 @@ Alpine.data("audioReader", () => ({
this.chooseSleep(choice);
this.sleepOpen = false;
},
applyDefaultSpeedFromSelect(e: Event) {
this.setDefaultSpeed(Number((e.target as HTMLSelectElement).value));
},
isDefaultSpeed(rate: number): boolean {
return this.defaultSpeed === rate;
},
onScrubInput(e: Event) {
this.scrubbing = true;
this.scrubValue = Number((e.target as HTMLInputElement).value);
},
setVolumeFromEvent(e: Event) {
this.setVolume(Number((e.target as HTMLInputElement).value));
},
volumeLabel(): string {
return `Volume ${Math.round(Number(this.volume) * 100)}%`;
},
railTickStyle(tick: { start: number }): string {
const span = Math.max(1, this.railTo - this.railFrom);
return `left:${((tick.start - this.railFrom) / span) * 100}%;background-color:var(--text-secondary);opacity:0.5;`;
},
isSkipActive(secs: number): boolean {
return Number(this.skipSeconds) === secs;
},
isSpeedActive(rate: number): boolean {
return Number(this.speed) === rate;
},
isBookmarkEditing(b: { id: unknown }): boolean {
return this.editingBookmarkId === String(b.id);
},
chooseSleepClockMode() {
this.chooseSleepClock();
this.sleepOpen = false;
},
isDefaultSpeed(rate: number): boolean {
return Number(this.defaultSpeed) === rate;
},
setSkip(seconds: number) {
this.skipSeconds = seconds;
localStorage.setItem("audio_skip", String(seconds));
@@ -1455,4 +1480,5 @@ Alpine.data("audioReader", () => ({
this.pushProgress();
}, PROGRESS_INTERVAL_MS);
},
fmtTime,
}));
+8
View File
@@ -55,6 +55,10 @@ function hideMetadataEditor(): void {
if (modal) modal.classList.add("hidden");
}
function triggerCoverUpload(): void {
document.getElementById("cover-upload-input")?.click();
}
function collectFormData(editorTags: string[]): Record<string, unknown> {
const modal = document.getElementById("metadata-editor-modal");
if (!modal) return {};
@@ -217,6 +221,10 @@ Alpine.data("bookDetail", () => {
showProgressSyncModal,
hideProgressSyncModal,
hideNotesModal,
triggerCoverUpload,
bannerEditHasText(): boolean {
return (this.bannerEditText || "").trim().length > 0;
},
async toggleAnnotationsBanner() {
this.annotationsBannerOpen = !this.annotationsBannerOpen;
+3
View File
@@ -3608,6 +3608,9 @@ document.addEventListener("alpine:init", () => {
this.readingTheme = value;
this.applyTheme();
},
noteEditHasText(note: { editText?: string }): boolean {
return String(note.editText ?? "").trim().length > 0;
},
detectChromeDarkMode(): boolean {
const darkChromeThemes = [
"theme-tokyo-night",