feat(web): updated tab, inline errors, card craft

Adds the Updated tab (series with an unread chapter) with its own empty
state, so "what's new" no longer means scanning the whole list.

A failed favourite/chapter/delete previously looked like an ignored tap:
htmx does not swap on a non-2xx response and nothing else said so. Errors
now surface inline on the card. The two per-card panels (chapter edit,
delete confirm) also close each other.

Mobile fixes, all measured at 360/390px rather than eyeballed:
.chapter-form and .confirm-row now wrap so the hint and the prompt take
their own row, and the number input shrinks (flex-basis 0) instead of
holding its ~20ch intrinsic width. Before this the Save button sat off
screen — 97px of page overflow at 390px, 127px at 360px — which broke
correcting a chapter number on the one device class this is built for.

Emoji glyphs replaced with inline SVG on currentColor: each rendered in a
different face at a different weight and colour, ignoring the card's own
type and colour system. No icon font or library.

Series with no scraped og:image render a title-initial monogram instead
of a blank rectangle that read as a cover still loading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 00:29:34 +07:00
parent 62772e1eaa
commit b2aa814982
8 changed files with 366 additions and 37 deletions
+8 -1
View File
@@ -25,6 +25,9 @@
<a role="tab" href="/?tab=all" class="{{if eq .Tab "all"}}active{{end}}"
hx-get="/ui/list?tab=all" hx-target="#list" hx-swap="innerHTML"
hx-push-url="/?tab=all" hx-on::after-request="setActiveTab(this)">All</a>
<a role="tab" href="/?tab=new" class="tab-new {{if eq .Tab "new"}}active{{end}}"
hx-get="/ui/list?tab=new" hx-target="#list" hx-swap="innerHTML"
hx-push-url="/?tab=new" hx-on::after-request="setActiveTab(this)">Updated</a>
<a role="tab" href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
hx-get="/ui/list?tab=fav" hx-target="#list" hx-swap="innerHTML"
hx-push-url="/?tab=fav" hx-on::after-request="setActiveTab(this)">Favourites</a>
@@ -36,7 +39,11 @@
<div class="recent-strip">
{{range .Recent}}
<a class="recent-card" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">{{end}}
<span class="recent-cover">
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
{{else}}<span class="cover-blank" aria-hidden="true">{{.Initial}}</span>{{end}}
{{if .HasNewChapter}}<span class="new-ribbon new-ribbon-sm">New</span>{{end}}
</span>
<span class="recent-title">{{.Title}}</span>
<span class="recent-chapter">Ch {{.LastChapter}}</span>
</a>
+42 -10
View File
@@ -1,37 +1,69 @@
{{define "card"}}
<article class="card" id="card-{{.Key}}" data-title="{{.Title}}">
<a class="cover" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">{{end}}
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
{{else}}<span class="cover-blank" aria-hidden="true">{{.Initial}}</span>{{end}}
{{if .HasNewChapter}}<span class="new-ribbon">New</span>{{end}}
</a>
<div class="body">
<h3 class="title">{{.Title}}</h3>
<p class="meta">
<span class="site site-{{.Site}}">{{.Site}}</span>
<span class="chapter">Ch {{.LastChapter}}</span>
{{if .HasNewChapter}}<span class="new">NEW {{.LatestChapter}}</span>{{end}}
{{if .HasNewChapter}}<span class="new-chapter">Ch {{.LatestChapter}} out</span>{{end}}
</p>
<div class="actions">
<a class="primary" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">Continue</a>
<a class="primary" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
title="Continue reading" aria-label="Continue reading">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path d="M8 4.8a1 1 0 0 1 1.52-.85l10 7.2a1 1 0 0 1 0 1.7l-10 7.2A1 1 0 0 1 8 19.2z"/>
</svg>
</a>
<button class="icon {{if .Favorite}}on{{end}}"
title="Favourite" aria-label="Toggle favourite"
hx-post="/ui/bookmarks/{{.Key}}/favorite"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML">
{{if .Favorite}}★{{else}}☆{{end}}
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
<svg viewBox="0 0 24 24" fill="{{if .Favorite}}currentColor{{else}}none{{end}}"
stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"
aria-hidden="true" focusable="false">
<path d="M12 3.7l2.63 5.33 5.87.86-4.25 4.14 1 5.85L12 17.31l-5.25 2.57 1-5.85L3.5 9.89l5.87-.86z"/>
</svg>
</button>
<button class="icon" title="Set chapter" aria-label="Set chapter"
onclick="toggleChapterForm('{{.Key}}')">✎</button>
onclick="toggleChapterForm('{{.Key}}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 20h4L19.3 8.7a1.5 1.5 0 0 0 0-2.12l-1.88-1.88a1.5 1.5 0 0 0-2.12 0L4 16z"/>
<path d="M13.8 7.2l3 3"/>
</svg>
</button>
<button class="icon danger" title="Remove" aria-label="Remove"
hx-delete="/ui/bookmarks/{{.Key}}"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-confirm="Remove {{.Title}} from the list?">🗑</button>
onclick="toggleConfirmRow('{{.Key}}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4.5 6.8h15M9.8 6.8V4.6h4.4v2.2M7.3 6.8l.9 12.6h7.6l.9-12.6"/>
</svg>
</button>
</div>
<form class="chapter-form" id="chapter-form-{{.Key}}" hidden
hx-post="/ui/bookmarks/{{.Key}}/chapter"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML">
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
{{if .LatestChapter}}<p class="hint">Latest known: Ch {{.LatestChapter}}</p>{{end}}
<input name="chapter" type="number" step="0.1" min="0"
value="{{.LastChapterNum}}" aria-label="Chapter number" required>
<button type="submit">Save</button>
</form>
<div class="confirm-row" id="confirm-row-{{.Key}}" hidden>
<span>Remove this?</span>
<button class="danger-solid"
hx-delete="/ui/bookmarks/{{.Key}}"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">Remove</button>
<button type="button" onclick="toggleConfirmRow('{{.Key}}')">Cancel</button>
</div>
<p class="error-inline" hidden></p>
</div>
</article>
{{end}}
+4
View File
@@ -1,6 +1,10 @@
{{define "list"}}
{{if .Items}}
{{range .Items}}{{template "card" .}}{{end}}
{{else if eq .Tab "fav"}}
<p class="empty">No favourites yet. Star a series to pin it here.</p>
{{else if eq .Tab "new"}}
<p class="empty">Nothing new. Every series is caught up to its latest chapter.</p>
{{else}}
<p class="empty">
Nothing here yet. Bookmarks appear once the userscript records a chapter.