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

+12 -12
View File
@@ -161,12 +161,12 @@ templ AudioReader(user User, metadata ReaderMetadata) {
class="text-xs rounded-md px-1.5 py-1"
style="background-color: var(--surface-hover); color: var(--text-primary); border: 1px solid var(--border);"
aria-label="Default playback speed"
@change="setDefaultSpeed(Number($event.target.value))"
@change="applyDefaultSpeedFromSelect($event)"
>
for _, rate := range SpeedOptions {
<option
value={ fmt.Sprintf("%g", rate) }
:selected={ fmt.Sprintf("Number(defaultSpeed) === %g", rate) }
:selected={ fmt.Sprintf("isDefaultSpeed(%g)", rate) }
>{ fmt.Sprintf("%g×", rate) }</option>
}
</select>
@@ -245,7 +245,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
:max="chaptersDuration() || 1"
:value="scrubbing ? scrubValue : currentTime"
class="relative w-full"
@input="scrubbing = true; scrubValue = Number($event.target.value)"
@input="onScrubInput($event)"
@change="commitScrub()"
aria-label="Seek position"
/>
@@ -272,7 +272,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
<div class="relative h-full">
<div class="absolute inset-x-0 top-1/2 h-px" style="background-color: var(--border);"></div>
<template x-for="tick in railTickList" :key="'rtick' + tick.start">
<div class="absolute top-1 bottom-1 w-px" :style="'left:' + ((tick.start - railFrom) / Math.max(1, railTo - railFrom) * 100) + '%;background-color:var(--text-secondary);opacity:0.5;'"></div>
<div class="absolute top-1 bottom-1 w-px" :style="railTickStyle(tick)"></div>
</template>
<div class="absolute top-0 bottom-0 w-0.5 -translate-x-1/2 rounded-full" :style="'left:' + railMarkerPct() + '%;background-color:var(--accent);'"></div>
<div class="absolute left-1 top-0.5 text-[10px] tabular-nums whitespace-nowrap" style="color: var(--text-primary);" x-text="fmtTime(scrubValue) + ' — ' + chapterTitleAt(scrubValue)"></div>
@@ -397,8 +397,8 @@ templ AudioReader(user User, metadata ReaderMetadata) {
type="button"
@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);'"
:class="isSkipActive(secs) ? 'font-semibold' : ''"
:style="isSkipActive(secs) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'"
x-text="secs + ' seconds'"
></button>
</template>
@@ -427,8 +427,8 @@ templ AudioReader(user User, metadata ReaderMetadata) {
type="button"
@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);'"
:class="isSpeedActive(rate) ? 'font-semibold' : ''"
:style="isSpeedActive(rate) ? 'background-color: var(--accent-muted); color: var(--text-primary);' : 'color: var(--text-secondary);'"
x-text="rate + '×' + (isDefaultSpeed(rate) ? ' · default' : '')"
></button>
</template>
@@ -512,7 +512,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
type="button"
@click="volumeOpen = !volumeOpen"
class="rounded-lg p-2 hover:bg-surface-hover transition-colors"
:aria-label="'Volume ' + Math.round(Number(volume) * 100) + '%'"
:aria-label="volumeLabel()"
style="color: var(--text-secondary);"
>
<span x-show="volumeIcon() === 'muted'" x-cloak>
@@ -576,7 +576,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
max="1"
step="0.05"
:value="muted ? 0 : volume"
@input="setVolume(Number($event.target.value))"
@input="setVolumeFromEvent($event)"
class="accent-current"
style="width: 7.5rem;"
aria-label="Volume"
@@ -659,8 +659,8 @@ templ AudioReader(user User, metadata ReaderMetadata) {
</button>
</div>
</div>
<div class="note-rich text-xs mt-1" x-show="b.notes && editingBookmarkId !== String(b.id)" x-html="bookmarkNoteHtml(b)"></div>
<div x-show="editingBookmarkId === String(b.id)" x-cloak class="mt-1">
<div class="note-rich text-xs mt-1" x-show="b.notes && !isBookmarkEditing(b)" x-html="bookmarkNoteHtml(b)"></div>
<div x-show="isBookmarkEditing(b)" x-cloak class="mt-1">
<textarea x-model="editingNote" rows="3" class="input text-sm resize-y" placeholder="Note (markdown)"></textarea>
<div class="flex justify-end gap-2 mt-1">
<button type="button" @click="editingBookmarkId = ''" class="btn btn-ghost text-xs px-2 py-1">Cancel</button>