refactor(ui): standardize tailwind classes

This commit is contained in:
2026-04-15 01:19:31 +02:00
parent 2a7361f133
commit c9d3ca5cc6
14 changed files with 228 additions and 228 deletions

View File

@@ -17,7 +17,7 @@ templ AnimeCard(props AnimeCardProps) {
if props.CurrentNode {
<div class={ defaultString(props.Class, "min-w-0") }>
@animeCardPoster(props.ImageURL, props.Title, props.ImgClass)
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-[0.86rem] leading-[1.3] text-[var(--text)]") }>
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-sm leading-snug text-(--text)") }>
{ props.Title }
</div>
{ children... }
@@ -27,7 +27,7 @@ templ AnimeCard(props AnimeCardProps) {
@animeCardPoster(props.ImageURL, props.Title, props.ImgClass)
if props.Class != "notification-card" && props.Class != "schedule-card" {
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-[0.86rem] leading-[1.3] text-[var(--text)]") }>
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-sm leading-snug text-(--text)") }>
{ props.Title }
</div>
}
@@ -37,11 +37,11 @@ templ AnimeCard(props AnimeCardProps) {
}
templ animeCardPoster(imageURL, title, imgClass string) {
<div class="flex h-full w-full aspect-[2/3] max-h-[var(--poster-max-height)] justify-center overflow-hidden">
<div class="flex h-full w-full aspect-2/3 max-h-(--poster-max-height) justify-center overflow-hidden">
if imageURL != "" {
<img src={ imageURL } alt={ title } class={ defaultString(imgClass, "block h-full w-full object-cover object-center") } loading="lazy"/>
} else {
<div class="flex h-full w-full justify-center overflow-hidden text-[0] text-transparent">No image</div>
<div class="flex h-full w-full justify-center overflow-hidden text-transparent">No image</div>
}
</div>
}

View File

@@ -3,7 +3,7 @@ package ui
templ EmptyState(title string) {
<div class="py-4">
<div class="mb-2 text-base">{ title }</div>
<div class="text-sm text-[var(--text-muted)]">
<div class="text-sm text-(--text-muted)">
{ children... }
</div>
</div>

View File

@@ -1,10 +1,10 @@
package ui
templ LoadingIndicator(text string) {
<div class="inline-flex items-center gap-2 text-sm text-[var(--text-muted)]">
<div class="h-1.5 w-1.5 rounded-full bg-[var(--text-faint)]"></div>
<div class="h-1.5 w-1.5 rounded-full bg-[var(--text-faint)]"></div>
<div class="h-1.5 w-1.5 rounded-full bg-[var(--text-faint)]"></div>
<div class="inline-flex items-center gap-2 text-sm text-(--text-muted)">
<div class="h-1.5 w-1.5 rounded-full bg-(--text-faint)"></div>
<div class="h-1.5 w-1.5 rounded-full bg-(--text-faint)"></div>
<div class="h-1.5 w-1.5 rounded-full bg-(--text-faint)"></div>
<span>{ text }</span>
</div>
}

View File

@@ -8,17 +8,17 @@ type SortFilterOptions struct {
}
templ SortFilter(opts SortFilterOptions) {
<div class="mb-4 flex flex-wrap items-center gap-3 bg-[var(--panel)] p-3 max-[860px]:flex-col max-[860px]:items-start max-[860px]:gap-2">
<div class="mb-4 flex flex-wrap items-center gap-3 bg-(--panel) p-3 max-lg:flex-col max-lg:items-start max-lg:gap-2">
<div class="flex items-center gap-2">
<label for="sort-select" class="text-[0.78rem] text-[var(--text-muted)]">Sort by</label>
<select id="sort-select" class="h-[30px] bg-[var(--surface-select)] px-2 text-[0.78rem] text-[var(--text)]" onchange="document.getElementById('sort-input').value = this.value; document.getElementById('sort-form').submit()">
<label for="sort-select" class="text-xs text-(--text-muted)">Sort by</label>
<select id="sort-select" class="h-8 bg-(--surface-select) px-2 text-xs text-(--text)" onchange="document.getElementById('sort-input').value = this.value; document.getElementById('sort-form').submit()">
<option value="date" selected?={ opts.Sort == "date" }>Date added</option>
<option value="title" selected?={ opts.Sort == "title" }>Title</option>
</select>
</div>
<div class="flex items-center gap-2">
<label for="order-select" class="text-[0.78rem] text-[var(--text-muted)]">Order</label>
<select id="order-select" class="h-[30px] bg-[var(--surface-select)] px-2 text-[0.78rem] text-[var(--text)]" onchange="document.getElementById('order-input').value = this.value; document.getElementById('sort-form').submit()">
<label for="order-select" class="text-xs text-(--text-muted)">Order</label>
<select id="order-select" class="h-8 bg-(--surface-select) px-2 text-xs text-(--text)" onchange="document.getElementById('order-input').value = this.value; document.getElementById('sort-form').submit()">
<option value="desc" selected?={ opts.Order == "desc" }>Descending</option>
<option value="asc" selected?={ opts.Order == "asc" }>Ascending</option>
</select>

View File

@@ -7,43 +7,43 @@ import "strings"
templ AnimeDetails(anime jikan.Anime, currentStatus string) {
@Layout("mal - " + anime.DisplayTitle(), true) {
<div class="grid grid-cols-[minmax(0,1fr)_300px] items-start gap-5 max-[1040px]:grid-cols-[minmax(0,1fr)]">
<div class="grid items-start gap-5 xl:grid-cols-[minmax(0,1fr)_300px]">
<div class="grid min-w-0 gap-8">
<div class="grid grid-cols-[220px_minmax(0,1fr)] gap-5 max-[860px]:grid-cols-[minmax(0,1fr)]">
<div class="w-[220px] max-[860px]:w-[min(230px,58vw)]">
<div class="grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)]">
<div class="w-56">
if anime.ImageURL() != "" {
<img class="w-full" src={ anime.ImageURL() } alt={ anime.DisplayTitle() }/>
} else {
<div class="flex aspect-[2/3] max-h-[var(--poster-max-height)] w-full justify-center overflow-hidden text-[0] text-transparent">No image</div>
<div class="flex aspect-2/3 max-h-(--poster-max-height) w-full justify-center overflow-hidden text-transparent">No image</div>
}
</div>
<div>
<h1>{ anime.DisplayTitle() }</h1>
if anime.TitleJapanese != "" {
<p class="my-2 mb-3 text-[0.9rem] text-[var(--text-muted)]">{ anime.TitleJapanese }</p>
<p class="my-2 mb-3 text-sm text-(--text-muted)">{ anime.TitleJapanese }</p>
}
<div class="flex flex-wrap gap-2">
if anime.ShortRating() != "" {
<span class="text-[0.67rem] text-[var(--text-faint)]">{ anime.ShortRating() }</span>
<span class="text-xs text-(--text-faint)">{ anime.ShortRating() }</span>
}
if anime.Type != "" {
<span class="text-[0.67rem] text-[var(--text-faint)]">{ anime.Type }</span>
<span class="text-xs text-(--text-faint)">{ anime.Type }</span>
}
if anime.Episodes > 0 {
<span class="text-[0.67rem] text-[var(--text-faint)]">{ fmt.Sprintf("%d ep", anime.Episodes) }</span>
<span class="text-xs text-(--text-faint)">{ fmt.Sprintf("%d ep", anime.Episodes) }</span>
}
if anime.ShortDuration() != "" {
<span class="text-[0.67rem] text-[var(--text-faint)]">{ anime.ShortDuration() }</span>
<span class="text-xs text-(--text-faint)">{ anime.ShortDuration() }</span>
}
</div>
<div class="mt-3">
@WatchlistDropdown(anime.MalID, anime.Title, anime.TitleEnglish, anime.TitleJapanese, anime.ImageURL(), currentStatus, anime.Airing)
</div>
<section class="mt-4 max-w-[100ch]">
<section class="mt-4 max-w-4xl">
if anime.Synopsis != "" {
<p>{ anime.Synopsis }</p>
} else {
<p class="text-[var(--text-faint)]">No synopsis available.</p>
<p class="text-(--text-faint)">No synopsis available.</p>
}
</section>
</div>
@@ -61,95 +61,95 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
</div>
</section>
</div>
<aside class="sticky top-[74px] grid gap-4 bg-[var(--panel)] p-3 max-[1040px]:static">
<aside class="sticky top-20 grid gap-4 bg-(--panel) p-3 max-xl:static">
<div class="grid gap-3">
<h3 class="mb-2 text-[0.78rem] text-[var(--text-faint)]">Details</h3>
<h3 class="mb-2 text-xs text-(--text-faint)">Details</h3>
if anime.Aired.String != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Aired</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ anime.Aired.String }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Aired</span>
<span class="text-sm text-(--text-muted)">{ anime.Aired.String }</span>
</div>
}
if anime.Premiered() != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Premiered</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ anime.Premiered() }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Premiered</span>
<span class="text-sm text-(--text-muted)">{ anime.Premiered() }</span>
</div>
}
if anime.Status != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Status</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ anime.Status }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Status</span>
<span class="text-sm text-(--text-muted)">{ anime.Status }</span>
</div>
}
if anime.Duration != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Duration</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ anime.Duration }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Duration</span>
<span class="text-sm text-(--text-muted)">{ anime.Duration }</span>
</div>
}
if len(anime.Genres) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Genres</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ joinNames(anime.Genres) }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Genres</span>
<span class="text-sm text-(--text-muted)">{ joinNames(anime.Genres) }</span>
</div>
}
</div>
if hasExtraSidebarDetails(anime) {
<details class="grid gap-3">
<summary class="cursor-pointer text-[0.82rem] text-[var(--text-muted)]">More metadata</summary>
<summary class="cursor-pointer text-xs text-(--text-muted)">More metadata</summary>
if anime.TitleJapanese != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Japanese</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ anime.TitleJapanese }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Japanese</span>
<span class="text-sm text-(--text-muted)">{ anime.TitleJapanese }</span>
</div>
}
if len(anime.TitleSynonyms) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Synonyms</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ strings.Join(anime.TitleSynonyms, ", ") }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Synonyms</span>
<span class="text-sm text-(--text-muted)">{ strings.Join(anime.TitleSynonyms, ", ") }</span>
</div>
}
if len(anime.Studios) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Studios</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ joinNames(anime.Studios) }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Studios</span>
<span class="text-sm text-(--text-muted)">{ joinNames(anime.Studios) }</span>
</div>
}
if len(anime.Producers) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Producers</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ joinNames(anime.Producers) }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Producers</span>
<span class="text-sm text-(--text-muted)">{ joinNames(anime.Producers) }</span>
</div>
}
if anime.Source != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Source</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ anime.Source }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Source</span>
<span class="text-sm text-(--text-muted)">{ anime.Source }</span>
</div>
}
if len(anime.Demographics) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Demographics</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ joinNames(anime.Demographics) }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Demographics</span>
<span class="text-sm text-(--text-muted)">{ joinNames(anime.Demographics) }</span>
</div>
}
if len(anime.Themes) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Themes</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ joinNames(anime.Themes) }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Themes</span>
<span class="text-sm text-(--text-muted)">{ joinNames(anime.Themes) }</span>
</div>
}
if anime.Broadcast.String != "" {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Broadcast</span>
<span class="text-[0.84rem] text-[var(--text-muted)]" data-jst-text={ anime.Broadcast.String }>{ anime.Broadcast.String }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Broadcast</span>
<span class="text-sm text-(--text-muted)" data-jst-text={ anime.Broadcast.String }>{ anime.Broadcast.String }</span>
</div>
}
if len(anime.Streaming) > 0 {
<div class="mt-1 grid gap-1">
<span class="mt-[2px] text-[0.84rem] text-[var(--text-faint)]">Streaming</span>
<span class="text-[0.84rem] text-[var(--text-muted)]">{ joinStreamingNames(anime) }</span>
<span class="mt-0.5 text-sm text-(--text-faint)">Streaming</span>
<span class="text-sm text-(--text-muted)">{ joinStreamingNames(anime) }</span>
</div>
}
</details>
@@ -161,12 +161,12 @@ templ AnimeDetails(anime jikan.Anime, currentStatus string) {
templ AnimePending(id int) {
@Layout("mal - anime pending", true) {
<div class="grid grid-cols-[minmax(0,1fr)_300px] items-start gap-5 max-[1040px]:grid-cols-[minmax(0,1fr)]">
<div class="grid items-start gap-5 xl:grid-cols-[minmax(0,1fr)_300px]">
<div class="grid min-w-0 gap-8">
<section>
<h1>Anime data is being fetched</h1>
<p class="text-[0.9rem] text-[var(--text-muted)]">We could not load this anime right now. A background worker is retrying data fetch for anime #{ fmt.Sprintf("%d", id) }.</p>
<p class="text-[0.9rem] text-[var(--text-muted)]">Refresh this page in a few seconds.</p>
<p class="text-sm text-(--text-muted)">We could not load this anime right now. A background worker is retrying data fetch for anime #{ fmt.Sprintf("%d", id) }.</p>
<p class="text-sm text-(--text-muted)">Refresh this page in a few seconds.</p>
</section>
</div>
</div>
@@ -196,15 +196,15 @@ func joinStreamingNames(anime jikan.Anime) string {
templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, currentStatus string, airing bool) {
<div class="relative inline-block" id="watchlist-dropdown">
<button class="inline-flex h-8 cursor-pointer items-center gap-2 bg-[var(--panel-soft)] px-2 text-[0.8rem] text-[var(--text)]" onclick="toggleDropdown()" data-dropdown-trigger>
<button class="inline-flex h-8 cursor-pointer items-center gap-2 bg-(--panel-soft) px-2 text-xs text-(--text)" onclick="toggleDropdown()" data-dropdown-trigger>
if currentStatus != "" {
{ formatStatus(currentStatus) }
} else {
Add to watchlist
}
<span class="text-[0.64rem]">▾</span>
<span class="text-xs">▾</span>
</button>
<div class="invisible absolute left-0 top-[calc(100%+2px)] z-[110] min-w-[210px] bg-[var(--panel)] opacity-0 transition-opacity duration-150" data-dropdown-menu data-dropdown-open-classes="visible opacity-100" data-dropdown-closed-classes="invisible opacity-0">
<div class="invisible absolute left-0 top-full mt-0.5 z-50 min-w-52 bg-(--panel) opacity-0 transition-opacity duration-150" data-dropdown-menu data-dropdown-open-classes="visible opacity-100" data-dropdown-closed-classes="invisible opacity-0">
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "watching", currentStatus, airing)
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "completed", currentStatus, airing)
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "on_hold", currentStatus, airing)
@@ -212,7 +212,7 @@ templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "plan_to_watch", currentStatus, airing)
if currentStatus != "" {
<button
class="flex w-full cursor-pointer items-center justify-between bg-transparent px-[0.62rem] py-2 text-left text-[0.78rem] text-[var(--text-muted)] hover:bg-[var(--panel-soft)] hover:text-[var(--danger)]"
class="flex w-full cursor-pointer items-center justify-between bg-transparent px-2.5 py-2 text-left text-xs text-(--text-muted) hover:bg-(--panel-soft) hover:text-(--danger)"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d", animeID))) }
hx-target="#watchlist-dropdown"
hx-swap="outerHTML swap:150ms"
@@ -225,8 +225,8 @@ templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string
templ dropdownStatusOption(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, status string, currentStatus string, airing bool) {
<button
class={
"flex w-full cursor-pointer items-center justify-between bg-transparent px-[0.62rem] py-2 text-left text-[0.78rem] text-[var(--text-muted)] hover:bg-[var(--panel-soft)] hover:text-[var(--text)]",
templ.KV("bg-[var(--panel-soft)] text-[var(--text)]", status == currentStatus),
"flex w-full cursor-pointer items-center justify-between bg-transparent px-2.5 py-2 text-left text-xs text-(--text-muted) hover:bg-(--panel-soft) hover:text-(--text)",
templ.KV("bg-(--panel-soft) text-(--text)", status == currentStatus),
}
hx-post="/api/watchlist"
hx-vals={ fmt.Sprintf(`{"anime_id": "%d", "anime_title": "%s", "anime_title_english": "%s", "anime_title_japanese": "%s", "anime_image": "%s", "status": "%s", "airing": "%v"}`, animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, status, airing) }
@@ -256,7 +256,7 @@ func formatStatus(status string) string {
templ AnimeRelationsList(relations []jikan.RelationEntry) {
if len(relations) > 1 {
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3" id="relations-grid">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5" id="relations-grid">
for _, rel := range relations {
@ui.AnimeCard(ui.AnimeCardProps{
ID: rel.Anime.MalID,
@@ -268,13 +268,13 @@ templ AnimeRelationsList(relations []jikan.RelationEntry) {
CurrentNode: rel.IsCurrent,
}) {
if rel.Relation != "" && rel.Relation != "Current" {
<div class="mt-1 text-[0.76rem] text-[var(--text-faint)]">{ rel.Relation }</div>
<div class="mt-1 text-xs text-(--text-faint)">{ rel.Relation }</div>
}
}
}
</div>
} else {
<p class="text-[0.9rem] text-[var(--text-muted)]">No related anime found.</p>
<p class="text-sm text-(--text-muted)">No related anime found.</p>
}
}
@@ -288,7 +288,7 @@ func relationCardClass(rel jikan.RelationEntry) string {
templ AnimeRecommendations(recs []jikan.Anime) {
if len(recs) > 0 {
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, anime := range recs {
@ui.AnimeCard(ui.AnimeCardProps{
ID: anime.MalID,
@@ -301,7 +301,7 @@ templ AnimeRecommendations(recs []jikan.Anime) {
}
</div>
} else {
<p class="text-[0.9rem] text-[var(--text-muted)]">No recommendations available.</p>
<p class="text-sm text-(--text-muted)">No recommendations available.</p>
}
}

View File

@@ -2,29 +2,29 @@ package templates
templ Login(formError string, username string) {
@Layout("Login", false) {
<div class="w-full max-w-[560px]">
<div class="mx-auto w-full bg-[var(--panel)] p-6">
<h2 class="m-0 text-[1.4rem]">Sign in</h2>
<p class="my-3 mb-5 text-[0.95rem] text-[var(--text-muted)]">Enter your credentials to continue.</p>
<div class="w-full max-w-xl">
<div class="mx-auto w-full bg-(--panel) p-6">
<h2 class="m-0 text-2xl">Sign in</h2>
<p class="my-3 mb-5 text-sm text-(--text-muted)">Enter your credentials to continue.</p>
<form action="/login" method="POST" class="grid gap-4">
<div class="grid gap-1">
<label for="username">Username / Email</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
</div>
<div class="grid gap-1">
<label for="password">Password</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="password" name="password" required placeholder="Your password"/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="password" name="password" required placeholder="Your password"/>
</div>
<button type="submit" class="h-10 cursor-pointer border-0 bg-[var(--accent)] text-[0.9rem] font-semibold text-[var(--text-on-accent)] hover:brightness-95">Sign in</button>
<button type="submit" class="h-10 cursor-pointer border-0 bg-(--accent) text-sm font-semibold text-(--text-on-accent) hover:brightness-95">Sign in</button>
if formError != "" {
<p class="mt-2 text-[0.82rem] text-[var(--danger)]" role="alert" aria-live="polite">{ formError }</p>
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ formError }</p>
}
</form>
<p class="mt-5 mb-0 text-center text-[0.9rem] text-[var(--text-muted)]">
Don't have an account? <a class="text-[var(--accent)]" href="/register">Register</a>
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
Don't have an account? <a class="text-(--accent)" href="/register">Register</a>
</p>
<p class="mt-5 mb-0 text-center text-[0.9rem] text-[var(--text-muted)]">
Lost access? <a class="text-[var(--accent)]" href="/recover">Recover account</a>
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
Lost access? <a class="text-(--accent)" href="/recover">Recover account</a>
</p>
</div>
</div>
@@ -33,29 +33,29 @@ templ Login(formError string, username string) {
templ Register(formError string, username string) {
@Layout("Register", false) {
<div class="w-full max-w-[560px]">
<div class="mx-auto w-full bg-[var(--panel)] p-6">
<h2 class="m-0 text-[1.4rem]">Register</h2>
<p class="my-3 mb-5 text-[0.95rem] text-[var(--text-muted)]">Create a new account to track anime.</p>
<div class="w-full max-w-xl">
<div class="mx-auto w-full bg-(--panel) p-6">
<h2 class="m-0 text-2xl">Register</h2>
<p class="my-3 mb-5 text-sm text-(--text-muted)">Create a new account to track anime.</p>
<form action="/register" method="POST" class="grid gap-4">
<div class="grid gap-1">
<label for="username">Username / Email</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
</div>
<div class="grid gap-1">
<label for="password">Password</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="password" name="password" required placeholder="Minimum 12 chars"/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="password" name="password" required placeholder="Minimum 12 chars"/>
</div>
<p class="m-0 text-[0.75rem] leading-[1.4] text-[var(--text-faint)]">
<p class="m-0 text-xs leading-normal text-(--text-faint)">
Password must be at least 12 characters and include an uppercase letter, lowercase letter, number, and special character.
</p>
<button type="submit" class="h-10 cursor-pointer border-0 bg-[var(--accent)] text-[0.9rem] font-semibold text-[var(--text-on-accent)] hover:brightness-95">Create account</button>
<button type="submit" class="h-10 cursor-pointer border-0 bg-(--accent) text-sm font-semibold text-(--text-on-accent) hover:brightness-95">Create account</button>
if formError != "" {
<p class="mt-2 text-[0.82rem] text-[var(--danger)]" role="alert" aria-live="polite">{ formError }</p>
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ formError }</p>
}
</form>
<p class="mt-5 mb-0 text-center text-[0.9rem] text-[var(--text-muted)]">
Already have an account? <a class="text-[var(--accent)]" href="/login">Sign in</a>
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
Already have an account? <a class="text-(--accent)" href="/login">Sign in</a>
</p>
</div>
</div>
@@ -64,18 +64,18 @@ templ Register(formError string, username string) {
templ RegistrationRecoveryKey(recoveryKey string) {
@Layout("Save recovery key", false) {
<div class="w-full max-w-[560px]">
<div class="mx-auto w-full bg-[var(--panel)] p-6">
<div class="w-full max-w-xl">
<div class="mx-auto w-full bg-(--panel) p-6">
<h2>Save your recovery key</h2>
<p class="my-3 mb-5 text-[0.95rem] text-[var(--text-muted)]">Store this key somewhere safe. It is shown only once.</p>
<p class="my-3 mb-5 text-sm text-(--text-muted)">Store this key somewhere safe. It is shown only once.</p>
<div class="grid gap-2">
<p class="m-0 break-all border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] p-3 text-[0.86rem] text-[var(--text)]" id="registration-recovery-key">{ recoveryKey }</p>
<button type="button" class="min-w-0 justify-self-start border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-[0.72rem] py-[0.42rem] text-[0.8rem] leading-none text-[var(--text)] hover:bg-[var(--surface-input-focus)]" onclick="copyRecoveryKey('registration-recovery-key', 'registration-copy-feedback')">Copy key</button>
<p class="m-0 break-all border border-(--surface-search-focus-border) bg-(--surface-search) p-3 text-sm text-(--text)" id="registration-recovery-key">{ recoveryKey }</p>
<button type="button" class="min-w-0 justify-self-start border border-(--surface-search-focus-border) bg-(--surface-search) px-3 py-1.5 text-xs leading-none text-(--text) hover:bg-(--surface-input-focus)" onclick="copyRecoveryKey('registration-recovery-key', 'registration-copy-feedback')">Copy key</button>
</div>
<p class="mt-2 min-h-[1.1rem] text-[0.75rem] leading-[1.4] text-[var(--text-faint)]" id="registration-copy-feedback" aria-live="polite"></p>
<p class="m-0 text-[0.75rem] leading-[1.4] text-[var(--text-faint)]">If you lose your password, this key is the only way to recover your account without email.</p>
<p class="mt-5 mb-0 text-center text-[0.9rem] text-[var(--text-muted)]">
<a href="/" class="inline-flex min-h-10 items-center justify-center border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-4 text-[var(--text)] no-underline hover:bg-[var(--panel-soft)] hover:no-underline">I saved it, continue</a>
<p class="mt-2 min-h-5 text-xs leading-normal text-(--text-faint)" id="registration-copy-feedback" aria-live="polite"></p>
<p class="m-0 text-xs leading-normal text-(--text-faint)">If you lose your password, this key is the only way to recover your account without email.</p>
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
<a href="/" class="inline-flex min-h-10 items-center justify-center border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) no-underline hover:bg-(--panel-soft) hover:no-underline">I saved it, continue</a>
</p>
</div>
</div>
@@ -84,30 +84,30 @@ templ RegistrationRecoveryKey(recoveryKey string) {
templ Recover(formError string, username string, recoveryKey string) {
@Layout("Recover account", false) {
<div class="w-full max-w-[560px]">
<div class="mx-auto w-full bg-[var(--panel)] p-6">
<h2 class="m-0 text-[1.4rem]">Recover account</h2>
<p class="my-3 mb-5 text-[0.95rem] text-[var(--text-muted)]">Enter your username, recovery key, and a new password.</p>
<div class="w-full max-w-xl">
<div class="mx-auto w-full bg-(--panel) p-6">
<h2 class="m-0 text-2xl">Recover account</h2>
<p class="my-3 mb-5 text-sm text-(--text-muted)">Enter your username, recovery key, and a new password.</p>
<form action="/recover" method="POST" class="grid gap-4">
<div class="grid gap-1">
<label for="username">Username / Email</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="text" id="username" name="username" required placeholder="you@example.com" value={ username }/>
</div>
<div class="grid gap-1">
<label for="recovery_key">Recovery key</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="text" id="recovery_key" name="recovery_key" required value={ recoveryKey }/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="text" id="recovery_key" name="recovery_key" required value={ recoveryKey }/>
</div>
<div class="grid gap-1">
<label for="new_password">New password</label>
<input class="h-10 border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="new_password" name="new_password" required placeholder="Minimum 12 chars"/>
<input class="h-10 border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="new_password" name="new_password" required placeholder="Minimum 12 chars"/>
</div>
<button type="submit" class="h-10 cursor-pointer border-0 bg-[var(--accent)] text-[0.9rem] font-semibold text-[var(--text-on-accent)] hover:brightness-95">Reset password</button>
<button type="submit" class="h-10 cursor-pointer border-0 bg-(--accent) text-sm font-semibold text-(--text-on-accent) hover:brightness-95">Reset password</button>
if formError != "" {
<p class="mt-2 text-[0.82rem] text-[var(--danger)]" role="alert" aria-live="polite">{ formError }</p>
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ formError }</p>
}
</form>
<p class="mt-5 mb-0 text-center text-[0.9rem] text-[var(--text-muted)]">
Remembered your password? <a class="text-[var(--accent)]" href="/login">Sign in</a>
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
Remembered your password? <a class="text-(--accent)" href="/login">Sign in</a>
</p>
</div>
</div>
@@ -116,18 +116,18 @@ templ Recover(formError string, username string, recoveryKey string) {
templ RecoveryComplete(newRecoveryKey string) {
@Layout("Recovery complete", false) {
<div class="w-full max-w-[560px]">
<div class="mx-auto w-full bg-[var(--panel)] p-6">
<div class="w-full max-w-xl">
<div class="mx-auto w-full bg-(--panel) p-6">
<h2>Account recovered</h2>
<p class="my-3 mb-5 text-[0.95rem] text-[var(--text-muted)]">Your password was reset and your recovery key was rotated.</p>
<p class="my-3 mb-5 text-sm text-(--text-muted)">Your password was reset and your recovery key was rotated.</p>
<div class="grid gap-2">
<p class="m-0 break-all border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] p-3 text-[0.86rem] text-[var(--text)]" id="recovery-complete-key">{ newRecoveryKey }</p>
<button type="button" class="min-w-0 justify-self-start border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-[0.72rem] py-[0.42rem] text-[0.8rem] leading-none text-[var(--text)] hover:bg-[var(--surface-input-focus)]" onclick="copyRecoveryKey('recovery-complete-key', 'recovery-complete-feedback')">Copy key</button>
<p class="m-0 break-all border border-(--surface-search-focus-border) bg-(--surface-search) p-3 text-sm text-(--text)" id="recovery-complete-key">{ newRecoveryKey }</p>
<button type="button" class="min-w-0 justify-self-start border border-(--surface-search-focus-border) bg-(--surface-search) px-3 py-1.5 text-xs leading-none text-(--text) hover:bg-(--surface-input-focus)" onclick="copyRecoveryKey('recovery-complete-key', 'recovery-complete-feedback')">Copy key</button>
</div>
<p class="mt-2 min-h-[1.1rem] text-[0.75rem] leading-[1.4] text-[var(--text-faint)]" id="recovery-complete-feedback" aria-live="polite"></p>
<p class="m-0 text-[0.75rem] leading-[1.4] text-[var(--text-faint)]">Replace your old recovery key with this one.</p>
<p class="mt-5 mb-0 text-center text-[0.9rem] text-[var(--text-muted)]">
<a href="/login" class="inline-flex min-h-10 items-center justify-center border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-4 text-[var(--text)] no-underline hover:bg-[var(--panel-soft)] hover:no-underline">Go to login</a>
<p class="mt-2 min-h-5 text-xs leading-normal text-(--text-faint)" id="recovery-complete-feedback" aria-live="polite"></p>
<p class="m-0 text-xs leading-normal text-(--text-faint)">Replace your old recovery key with this one.</p>
<p class="mt-5 mb-0 text-center text-sm text-(--text-muted)">
<a href="/login" class="inline-flex min-h-10 items-center justify-center border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) no-underline hover:bg-(--panel-soft) hover:no-underline">Go to login</a>
</p>
</div>
</div>
@@ -136,75 +136,75 @@ templ RecoveryComplete(newRecoveryKey string) {
templ Account(username string, createdAt string, passwordError string, passwordSuccess string, recoveryError string, recoverySuccess string, recoveryKey string) {
@Layout("mal - account", true) {
<div class="mx-auto grid w-[min(720px,100%)] gap-4">
<section class="grid gap-3 bg-[var(--panel)] p-5">
<div class="mx-auto grid w-full max-w-3xl gap-4">
<section class="grid gap-3 bg-(--panel) p-5">
<h2>Account</h2>
<div class="grid gap-2">
<div class="grid gap-1">
<span class="text-[0.78rem] text-[var(--text-faint)]">Email / Username</span>
<span class="text-[0.95rem] text-[var(--text)]">{ username }</span>
<span class="text-xs text-(--text-faint)">Email / Username</span>
<span class="text-sm text-(--text)">{ username }</span>
</div>
<div class="grid gap-1">
<span class="text-[0.78rem] text-[var(--text-faint)]">Created</span>
<span class="text-[0.95rem] text-[var(--text)]">{ createdAt }</span>
<span class="text-xs text-(--text-faint)">Created</span>
<span class="text-sm text-(--text)">{ createdAt }</span>
</div>
</div>
</section>
<section class="grid gap-3 bg-[var(--panel)] p-5">
<section class="grid gap-3 bg-(--panel) p-5">
<h3>Change password</h3>
<form action="/account/password" method="POST" class="grid gap-3" onsubmit="return confirmDangerAction('Change your password now?')">
<div class="grid gap-1">
<label for="current_password">Current password</label>
<input class="h-10 w-full border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="current_password" name="current_password" required/>
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="current_password" name="current_password" required/>
</div>
<div class="grid gap-1">
<label for="new_password">New password</label>
<input class="h-10 w-full border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="new_password" name="new_password" required placeholder="Minimum 12 chars"/>
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="new_password" name="new_password" required placeholder="Minimum 12 chars"/>
</div>
<div class="grid gap-1">
<label for="confirm_new_password">Confirm new password</label>
<input class="h-10 w-full border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="confirm_new_password" name="confirm_new_password" required/>
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="confirm_new_password" name="confirm_new_password" required/>
</div>
<button type="submit" class="h-10 cursor-pointer border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-4 text-[var(--text)] hover:bg-[var(--panel-soft)]">Update password</button>
<button type="submit" class="h-10 cursor-pointer border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) hover:bg-(--panel-soft)">Update password</button>
if passwordError != "" {
<p class="mt-2 text-[0.82rem] text-[var(--danger)]" role="alert" aria-live="polite">{ passwordError }</p>
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ passwordError }</p>
}
if passwordSuccess != "" {
<p class="m-0 text-[0.82rem] text-[var(--accent)]" role="status" aria-live="polite">{ passwordSuccess }</p>
<p class="m-0 text-xs text-(--accent)" role="status" aria-live="polite">{ passwordSuccess }</p>
}
</form>
</section>
<section class="grid gap-3 bg-[var(--panel)] p-5">
<section class="grid gap-3 bg-(--panel) p-5">
<h3>Recovery key</h3>
<p class="m-0 text-[0.75rem] leading-[1.4] text-[var(--text-faint)]">To view a new recovery key, confirm your current password. This rotates your old key.</p>
<p class="m-0 text-xs leading-normal text-(--text-faint)">To view a new recovery key, confirm your current password. This rotates your old key.</p>
<form action="/account/recovery-key" method="POST" class="grid gap-3" onsubmit="return confirmDangerAction('Rotate recovery key now? Your old key will stop working.')">
<div class="grid gap-1">
<label for="recovery_password">Current password</label>
<input class="h-10 w-full border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 focus:border-[var(--surface-search-focus-border)] focus:outline-none" type="password" id="recovery_password" name="password" required/>
<input class="h-10 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 focus:border-(--surface-search-focus-border) focus:outline-none" type="password" id="recovery_password" name="password" required/>
</div>
<button type="submit" class="h-10 cursor-pointer border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-4 text-[var(--text)] hover:bg-[var(--panel-soft)]">Show new recovery key</button>
<button type="submit" class="h-10 cursor-pointer border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) hover:bg-(--panel-soft)">Show new recovery key</button>
if recoveryError != "" {
<p class="mt-2 text-[0.82rem] text-[var(--danger)]" role="alert" aria-live="polite">{ recoveryError }</p>
<p class="mt-2 text-xs text-(--danger)" role="alert" aria-live="polite">{ recoveryError }</p>
}
if recoverySuccess != "" {
<p class="m-0 text-[0.82rem] text-[var(--accent)]" role="status" aria-live="polite">{ recoverySuccess }</p>
<p class="m-0 text-xs text-(--accent)" role="status" aria-live="polite">{ recoverySuccess }</p>
}
</form>
if recoveryKey != "" {
<div class="grid gap-2">
<p class="m-0 break-all border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] p-3 text-[0.86rem] text-[var(--text)]" id="account-recovery-key">{ recoveryKey }</p>
<button type="button" class="min-w-0 justify-self-start border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-[0.72rem] py-[0.42rem] text-[0.8rem] leading-none text-[var(--text)] hover:bg-[var(--surface-input-focus)]" onclick="copyRecoveryKey('account-recovery-key', 'account-copy-feedback')">Copy key</button>
<p class="m-0 break-all border border-(--surface-search-focus-border) bg-(--surface-search) p-3 text-sm text-(--text)" id="account-recovery-key">{ recoveryKey }</p>
<button type="button" class="min-w-0 justify-self-start border border-(--surface-search-focus-border) bg-(--surface-search) px-3 py-1.5 text-xs leading-none text-(--text) hover:bg-(--surface-input-focus)" onclick="copyRecoveryKey('account-recovery-key', 'account-copy-feedback')">Copy key</button>
</div>
<p class="mt-2 min-h-[1.1rem] text-[0.75rem] leading-[1.4] text-[var(--text-faint)]" id="account-copy-feedback" aria-live="polite"></p>
<p class="mt-2 min-h-5 text-xs leading-normal text-(--text-faint)" id="account-copy-feedback" aria-live="polite"></p>
}
</section>
<section class="grid gap-3 bg-[var(--panel)] p-5">
<section class="grid gap-3 bg-(--panel) p-5">
<h3>Danger zone</h3>
<form action="/logout" method="POST" class="inline-flex" onsubmit="return confirmDangerAction('Log out of this account now?')">
<button type="submit" class="h-10 cursor-pointer border border-[var(--surface-search-focus-border)] bg-[var(--surface-search)] px-4 text-[var(--text)] hover:bg-[var(--panel-soft)]">Log out</button>
<button type="submit" class="h-10 cursor-pointer border border-(--surface-search-focus-border) bg-(--surface-search) px-4 text-(--text) hover:bg-(--panel-soft)">Log out</button>
</form>
</section>
</div>

View File

@@ -6,7 +6,7 @@ import "fmt"
templ Catalog() {
@Layout("mal - catalog", true) {
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3" id="catalog-content">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5" id="catalog-content">
<div class="col-span-full" hx-get="/api/catalog?page=1" hx-trigger="load" hx-swap="outerHTML">
@ui.LoadingIndicator("Loading catalog")
</div>
@@ -21,8 +21,8 @@ templ CatalogItems(animes []jikan.Anime, nextPage int, hasNext bool) {
templ CatalogPlaceholderItems(count int) {
for i := 0; i < count; i++ {
<div class="pointer-events-none min-w-0" aria-hidden="true">
<div class="aspect-[2/3] max-h-[var(--poster-max-height)] w-full animate-pulse bg-[var(--surface-search)]"></div>
<div class="mt-2 h-[0.9rem] w-4/5 animate-pulse bg-[var(--surface-search)]"></div>
<div class="aspect-2/3 max-h-(--poster-max-height) w-full animate-pulse bg-(--surface-search)"></div>
<div class="mt-2 h-4 w-4/5 animate-pulse bg-(--surface-search)"></div>
</div>
}
}

View File

@@ -9,35 +9,35 @@ templ Discover() {
<div class="grid gap-4">
<div class="grid gap-4">
<h1>Discover</h1>
<p class="m-0 text-[0.88rem] text-[var(--text-muted)]">Browse what's airing now and what is coming soon.</p>
<p class="m-0 text-sm text-(--text-muted)">Browse what's airing now and what is coming soon.</p>
</div>
<div class="flex flex-wrap gap-2 max-[680px]:flex-nowrap max-[680px]:overflow-x-auto max-[680px]:pb-1" data-tab-group="discover">
<div class="flex flex-wrap gap-2 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1" data-tab-group="discover">
<button
class="tab-trigger shrink-0 whitespace-nowrap bg-[var(--surface-tab-active)] px-[0.45rem] py-[0.24rem] text-[0.76rem] text-[var(--accent)]"
class="tab-trigger shrink-0 whitespace-nowrap bg-(--surface-tab-active) px-2 py-1 text-xs text-(--accent)"
type="button"
hx-get="/api/discover/airing?page=1"
hx-target="#discover-content"
hx-trigger="click"
data-tab-trigger
data-tab-active-classes="bg-[var(--surface-tab-active)] text-[var(--accent)]"
data-tab-inactive-classes="bg-[var(--panel-soft)] text-[var(--text-muted)]"
data-tab-active-classes="bg-(--surface-tab-active) text-(--accent)"
data-tab-inactive-classes="bg-(--panel-soft) text-(--text-muted)"
>
airing now
</button>
<button
class="tab-trigger shrink-0 whitespace-nowrap bg-[var(--panel-soft)] px-[0.45rem] py-[0.24rem] text-[0.76rem] text-[var(--text-muted)] hover:bg-[var(--surface-tab-hover)] hover:text-[var(--text)]"
class="tab-trigger shrink-0 whitespace-nowrap bg-(--panel-soft) px-2 py-1 text-xs text-(--text-muted) hover:bg-(--surface-tab-hover) hover:text-(--text)"
type="button"
hx-get="/api/discover/upcoming?page=1"
hx-target="#discover-content"
hx-trigger="click"
data-tab-trigger
data-tab-active-classes="bg-[var(--surface-tab-active)] text-[var(--accent)]"
data-tab-inactive-classes="bg-[var(--panel-soft)] text-[var(--text-muted)]"
data-tab-active-classes="bg-(--surface-tab-active) text-(--accent)"
data-tab-inactive-classes="bg-(--panel-soft) text-(--text-muted)"
>
upcoming
</button>
</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3" id="discover-content" hx-get="/api/discover/airing?page=1" hx-trigger="load">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5" id="discover-content" hx-get="/api/discover/airing?page=1" hx-trigger="load">
<div class="col-span-full">
@ui.LoadingIndicator("Loading discover")
</div>

View File

@@ -28,7 +28,7 @@ templ SearchResultsWrapper(query string, animes []jikan.Anime, nextPage int, has
Try a different search term.
}
} else {
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
@SearchItems(query, animes, nextPage, hasNext)
</div>
}

View File

@@ -17,33 +17,33 @@ templ Layout(title string, showHeader bool) {
<script src="/dist/timezone.js" defer></script>
<script src="/dist/auth.js" defer></script>
</head>
<body class="min-h-screen bg-[var(--bg)] text-[var(--text)] font-[var(--font)] text-[14px] leading-[1.45]">
<body class="min-h-screen bg-(--bg) text-(--text) font-(--font) text-sm leading-normal">
if showHeader {
<header class="sticky top-0 z-[100] bg-[var(--header)]">
<div class="mx-auto flex w-full max-w-[1580px] items-center gap-4 px-4 py-3 max-[860px]:flex-wrap max-[860px]:gap-3">
<div class="flex min-w-0 items-center gap-5 max-[860px]:w-full max-[860px]:flex-wrap max-[860px]:gap-3" data-search-root>
<a href="/" class="inline-flex items-center text-[var(--accent)]" aria-label="mal logo">
<header class="sticky top-0 z-100 bg-(--header)">
<div class="mx-auto flex w-full max-w-screen-2xl items-center gap-4 px-4 py-3 max-lg:flex-wrap max-lg:gap-3">
<div class="flex min-w-0 items-center gap-5 max-lg:w-full max-lg:flex-wrap max-lg:gap-3" data-search-root>
<a href="/" class="inline-flex items-center text-(--accent)" aria-label="mal logo">
@icons.LogoIcon("h-7 w-7")
</a>
<div class="flex flex-wrap gap-3 text-[0.85rem] max-[860px]:w-full max-[860px]:gap-2">
<a class="text-[var(--text-muted)] no-underline hover:text-[var(--text)] hover:no-underline" href="/">Catalog</a>
<a class="text-[var(--text-muted)] no-underline hover:text-[var(--text)] hover:no-underline" href="/discover">Discover</a>
<a class="text-[var(--text-muted)] no-underline hover:text-[var(--text)] hover:no-underline" href="/notifications">Notifications</a>
<a class="text-[var(--text-muted)] no-underline hover:text-[var(--text)] hover:no-underline" href="/watchlist">Watchlist</a>
<a class="text-[var(--text-muted)] no-underline hover:text-[var(--text)] hover:no-underline" href="/account">Account</a>
<div class="flex flex-wrap gap-3 text-sm max-lg:w-full max-lg:gap-2">
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/">Catalog</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/discover">Discover</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/notifications">Notifications</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/watchlist">Watchlist</a>
<a class="text-(--text-muted) no-underline hover:text-(--text) hover:no-underline" href="/account">Account</a>
</div>
</div>
<div class="relative ml-auto min-w-[240px] w-[min(420px,45vw)] max-[860px]:ml-0 max-[860px]:w-full" data-search-root>
<div class="relative ml-auto min-w-60 w-full max-w-md max-lg:ml-0" data-search-root>
<form action="/search" method="GET" class="w-full" id="search-form">
<input type="text" id="search-input" name="q" class="h-[34px] w-full border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 placeholder:text-[var(--text-faint)] focus:border-[var(--surface-search-focus-border)] focus:outline-none" placeholder="Search anime..." autocomplete="off"/>
<div id="search-dropdown" class="absolute inset-x-0 top-[calc(100%+2px)] z-[120] max-h-[min(70vh,560px)] overflow-y-auto bg-[var(--panel)]" data-search-results-container></div>
<input type="text" id="search-input" name="q" class="h-9 w-full border border-transparent bg-(--surface-search) px-3 text-(--text) transition-colors duration-120 placeholder:text-(--text-faint) focus:border-(--surface-search-focus-border) focus:outline-none" placeholder="Search anime..." autocomplete="off"/>
<div id="search-dropdown" class="absolute inset-x-0 top-full mt-0.5 z-50 max-h-screen overflow-y-auto bg-(--panel)" data-search-results-container></div>
</form>
</div>
</div>
</header>
}
<main class={
"mx-auto w-full max-w-[1580px] px-4 pt-5 pb-8 max-[860px]:px-3 max-[860px]:pb-6",
"mx-auto w-full max-w-screen-2xl px-4 pt-5 pb-8 max-lg:px-3 max-lg:pb-6",
templ.KV("flex min-h-screen items-center justify-center px-4 py-0", !showHeader),
}>
{ children... }

View File

@@ -2,11 +2,11 @@ package templates
templ NotFoundPage() {
@Layout("mal - not found", false) {
<section class="w-[min(780px,calc(100vw-(1.5rem*2)))] min-h-[72vh] mx-auto py-8 px-7 grid content-center justify-items-center gap-3 text-center">
<p class="m-0 text-[clamp(4rem,15vw,10rem)] tracking-[0.04em] leading-[0.9] text-[var(--text-muted)]">404</p>
<h1 class="m-0 text-[clamp(2rem,4vw,3rem)]">Page not found</h1>
<p class="text-[var(--text-muted)]">The page you requested does not exist, or it was moved.</p>
<p><a href="/" class="text-[1.05rem] text-[var(--accent)] no-underline hover:underline">Back to catalog</a></p>
<section class="w-full max-w-3xl min-h-dvh mx-auto grid content-center justify-items-center gap-3 px-7 py-8 text-center">
<p class="m-0 text-6xl leading-none tracking-wider text-(--text-muted) sm:text-7xl md:text-8xl lg:text-9xl">404</p>
<h1 class="m-0 text-3xl sm:text-4xl md:text-5xl">Page not found</h1>
<p class="text-(--text-muted)">The page you requested does not exist, or it was moved.</p>
<p><a href="/" class="text-base text-(--accent) no-underline hover:underline">Back to catalog</a></p>
</section>
}
}

View File

@@ -15,7 +15,7 @@ templ Notifications(watching []WatchingAnimeWithDetails, activeTab string) {
@Layout("mal - notifications", true) {
<div class="grid gap-4">
<h1>Notifications</h1>
<div class="mb-3 flex flex-wrap gap-2 max-[680px]:flex-nowrap max-[680px]:overflow-x-auto max-[680px]:pb-1">
<div class="mb-3 flex flex-wrap gap-2 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1">
<a href="/notifications?tab=tracking" class={ statusTabClass(activeTab == "tracking") }>Tracking</a>
<a href="/notifications?tab=sequels" class={ statusTabClass(activeTab == "sequels") }>Sequels</a>
</div>
@@ -25,13 +25,13 @@ templ Notifications(watching []WatchingAnimeWithDetails, activeTab string) {
@ui.LoadingIndicator("Syncing sequel graphs...")
</div>
} else {
<p class="m-0 text-[0.88rem] text-[var(--text-muted)]">Shows you're currently watching or planning to watch.</p>
<p class="m-0 text-sm text-(--text-muted)">Shows you're currently watching or planning to watch.</p>
if len(watching) == 0 {
@ui.EmptyState("No airing anime in your watching list.") {
<span class="text-[0.9rem] text-[var(--text-muted)]">Add currently airing shows to your watching list to see upcoming episodes here.</span>
<span class="text-sm text-(--text-muted)">Add currently airing shows to your watching list to see upcoming episodes here.</span>
}
} else {
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, item := range watching {
@NotificationCard(item)
}
@@ -56,7 +56,7 @@ func splitUpcomingSeasons(items []database.GetUpcomingSeasonsRow) (airing []data
templ UpcomingSeasonsList(upcomingSeasons []database.GetUpcomingSeasonsRow) {
if len(upcomingSeasons) == 0 {
@ui.EmptyState("No upcoming seasons for anime you've watched.") {
<span class="text-[0.9rem] text-[var(--text-muted)]">As you watch more shows, new seasons will appear here.</span>
<span class="text-sm text-(--text-muted)">As you watch more shows, new seasons will appear here.</span>
}
} else {
@renderSplitSeasons(upcomingSeasons)
@@ -67,9 +67,9 @@ templ renderSplitSeasons(upcomingSeasons []database.GetUpcomingSeasonsRow) {
if airing, upcoming := splitUpcomingSeasons(upcomingSeasons); true {
if len(airing) > 0 {
<section class="mb-4 grid gap-3">
<h2 class="m-0 text-[1.25rem] font-semibold leading-[1.2]">Airing now</h2>
<p class="m-0 text-[0.88rem] text-[var(--text-muted)]">These are the currently airing anime, but you're not tracking any of these.</p>
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3">
<h2 class="m-0 text-xl font-semibold leading-tight">Airing now</h2>
<p class="m-0 text-sm text-(--text-muted)">These are the currently airing anime, but you're not tracking any of these.</p>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, item := range airing {
@UpcomingSeasonCard(item)
}
@@ -79,9 +79,9 @@ templ renderSplitSeasons(upcomingSeasons []database.GetUpcomingSeasonsRow) {
if len(upcoming) > 0 {
<section class="grid gap-3">
<h2 class="m-0 text-[1.25rem] font-semibold leading-[1.2]">Announced & upcoming</h2>
<p class="m-0 text-[0.88rem] text-[var(--text-muted)]">Newly announced or upcoming seasons related to anime you've watched.</p>
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3">
<h2 class="m-0 text-xl font-semibold leading-tight">Announced & upcoming</h2>
<p class="m-0 text-sm text-(--text-muted)">Newly announced or upcoming seasons related to anime you've watched.</p>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, item := range upcoming {
@UpcomingSeasonCard(item)
}
@@ -99,15 +99,15 @@ templ UpcomingSeasonCard(item database.GetUpcomingSeasonsRow) {
Class: "notification-card min-w-0 flex flex-col bg-transparent text-inherit no-underline",
}) {
<div class="mt-2 grid gap-1 p-0" data-notification-content>
<div class="line-clamp-2 text-[0.86rem] leading-[1.3] text-[var(--text)]">
<div class="line-clamp-2 text-sm leading-snug text-(--text)">
{ displaySeasonTitle(item) }
</div>
<div class="flex flex-wrap gap-2">
if item.Status.Valid {
<span class="text-[0.67rem] text-[var(--text-faint)]">{ seasonStatusLabel(item.Status.String) }</span>
<span class="text-xs text-(--text-faint)">{ seasonStatusLabel(item.Status.String) }</span>
}
if strings.TrimSpace(item.PrequelTitle) != "" {
<span class="text-[0.67rem] text-[var(--text-faint)]">{ fmt.Sprintf("Sequel to %s", item.PrequelTitle) }</span>
<span class="text-xs text-(--text-faint)">{ fmt.Sprintf("Sequel to %s", item.PrequelTitle) }</span>
}
</div>
</div>
@@ -126,16 +126,16 @@ templ NotificationCard(item WatchingAnimeWithDetails) {
Class: "notification-card min-w-0 flex flex-col bg-transparent text-inherit no-underline",
}) {
<div class="mt-2 grid gap-1 p-0" data-notification-content>
<div class="line-clamp-2 text-[0.86rem] leading-[1.3] text-[var(--text)]">
<div class="line-clamp-2 text-sm leading-snug text-(--text)">
{ displayTitle(item.Entry) }
</div>
<div class="flex flex-wrap gap-2">
if item.Anime.Broadcast.String != "" {
<span class="text-[0.67rem] text-[var(--text-faint)]" data-jst-text={ item.Anime.Broadcast.String } data-broadcast-day={ item.Anime.Broadcast.Day } data-broadcast-time={ item.Anime.Broadcast.Time } data-broadcast-timezone={ item.Anime.Broadcast.Timezone }>{ item.Anime.Broadcast.String }</span>
<span class="text-[0.67rem] text-[var(--text-faint)]" data-next-airing="pending">Calculating next episode time...</span>
<span class="text-xs text-(--text-faint)" data-jst-text={ item.Anime.Broadcast.String } data-broadcast-day={ item.Anime.Broadcast.Day } data-broadcast-time={ item.Anime.Broadcast.Time } data-broadcast-timezone={ item.Anime.Broadcast.Timezone }>{ item.Anime.Broadcast.String }</span>
<span class="text-xs text-(--text-faint)" data-next-airing="pending">Calculating next episode time...</span>
}
if item.Anime.Episodes > 0 {
<span class="text-[0.67rem] text-[var(--text-faint)]">
<span class="text-xs text-(--text-faint)">
if item.Entry.CurrentEpisode.Valid {
{ fmt.Sprintf("%d / %d eps", item.Entry.CurrentEpisode.Int64, item.Anime.Episodes) }
} else {
@@ -143,7 +143,7 @@ templ NotificationCard(item WatchingAnimeWithDetails) {
}
</span>
} else if item.Entry.CurrentEpisode.Valid && item.Entry.CurrentEpisode.Int64 > 0 {
<span class="text-[0.67rem] text-[var(--text-faint)]">
<span class="text-xs text-(--text-faint)">
{ fmt.Sprintf("%d eps watched", item.Entry.CurrentEpisode.Int64) }
</span>
}
@@ -174,9 +174,9 @@ func seasonStatusLabel(status string) string {
}
func statusTabClass(active bool) string {
base := "shrink-0 whitespace-nowrap bg-[var(--panel-soft)] px-[0.45rem] py-[0.24rem] text-[0.76rem] text-[var(--text-muted)] no-underline hover:bg-[var(--surface-tab-hover)] hover:text-[var(--text)] hover:no-underline"
base := "shrink-0 whitespace-nowrap bg-(--panel-soft) px-2 py-1 text-xs text-(--text-muted) no-underline hover:bg-(--surface-tab-hover) hover:text-(--text) hover:no-underline"
if active {
return "shrink-0 whitespace-nowrap bg-[var(--surface-tab-active)] px-[0.45rem] py-[0.24rem] text-[0.76rem] text-[var(--accent)] no-underline hover:no-underline"
return "shrink-0 whitespace-nowrap bg-(--surface-tab-active) px-2 py-1 text-xs text-(--accent) no-underline hover:no-underline"
}
return base
}

View File

@@ -8,24 +8,24 @@ import (
templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentStatus string, sortBy string, sortOrder string) {
@Layout("mal - watchlist", true) {
<div class="mb-4 flex items-end justify-between gap-4 max-[860px]:flex-col max-[860px]:items-start">
<div class="mb-4 flex items-end justify-between gap-4 max-lg:flex-col max-lg:items-start">
<div class="grid gap-1">
<h2>Watchlist</h2>
<p class="m-0 text-[0.86rem] text-[var(--text-muted)]">Track what you're watching with less noise.</p>
<p class="m-0 text-sm text-(--text-muted)">Track what you're watching with less noise.</p>
</div>
<div class="flex flex-wrap items-center justify-end gap-2 max-[860px]:w-full max-[860px]:justify-start">
<a href="/api/watchlist/export" class="inline-flex min-w-16 items-center justify-center px-[0.45rem] py-[0.24rem] text-center text-[0.8rem] leading-[1.2] text-[var(--text-muted)] no-underline hover:text-[var(--accent)] hover:no-underline">Export</a>
<button class="inline-flex min-w-16 cursor-pointer items-center justify-center border-0 bg-transparent px-[0.45rem] py-[0.24rem] text-center text-[0.8rem] leading-[1.2] text-[var(--text-muted)] hover:text-[var(--accent)]" type="button" onclick="document.getElementById('import-file').click()">Import</button>
<div class="flex flex-wrap items-center justify-end gap-2 max-lg:w-full max-lg:justify-start">
<a href="/api/watchlist/export" class="inline-flex min-w-16 items-center justify-center px-2 py-1 text-center text-xs leading-tight text-(--text-muted) no-underline hover:text-(--accent) hover:no-underline">Export</a>
<button class="inline-flex min-w-16 cursor-pointer items-center justify-center border-0 bg-transparent px-2 py-1 text-center text-xs leading-tight text-(--text-muted) hover:text-(--accent)" type="button" onclick="document.getElementById('import-file').click()">Import</button>
<form id="import-form" hx-post="/api/watchlist/import" hx-encoding="multipart/form-data" class="hidden">
<input type="file" id="import-file" name="file" accept=".json" onchange="htmx.trigger('#import-form', 'submit')"/>
</form>
<div class="flex flex-wrap gap-2 max-[680px]:flex-nowrap max-[680px]:overflow-x-auto max-[680px]:pb-1">
<div class="flex flex-wrap gap-2 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1">
<a href={ templ.URL(watchlistURL("grid", currentStatus, sortBy, sortOrder)) } class={ tabClass(layout == "grid") }>Grid</a>
<a href={ templ.URL(watchlistURL("table", currentStatus, sortBy, sortOrder)) } class={ tabClass(layout == "table") }>Table</a>
</div>
</div>
</div>
<div class="mb-3 flex flex-wrap gap-2 max-[680px]:flex-nowrap max-[680px]:overflow-x-auto max-[680px]:pb-1">
<div class="mb-3 flex flex-wrap gap-2 max-md:flex-nowrap max-md:overflow-x-auto max-md:pb-1">
<a href={ templ.URL(watchlistURL(layout, "all", sortBy, sortOrder)) } class={ tabClass(currentStatus == "all") }>All</a>
<a href={ templ.URL(watchlistURL(layout, "watching", sortBy, sortOrder)) } class={ tabClass(currentStatus == "watching") }>Watching</a>
<a href={ templ.URL(watchlistURL(layout, "continuing", sortBy, sortOrder)) } class={ tabClass(currentStatus == "continuing") }>Continuing</a>
@@ -47,7 +47,7 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
}
} else {
if layout == "grid" {
<div class="grid grid-cols-[repeat(auto-fill,minmax(190px,1fr))] gap-4 max-[680px]:grid-cols-2 max-[680px]:gap-3">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:gap-4 lg:grid-cols-4 xl:grid-cols-5">
for _, entry := range entries {
<div class="group relative min-w-0" id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
@ui.AnimeCard(ui.AnimeCardProps{
@@ -56,7 +56,7 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
ImageURL: entry.ImageUrl,
})
<button
class="absolute right-2 top-2 h-[22px] w-[22px] cursor-pointer border-0 bg-[var(--overlay-subtle)] text-[var(--text-muted)] opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:text-[var(--danger)]"
class="absolute right-2 top-2 h-6 w-6 cursor-pointer border-0 bg-(--overlay-subtle) text-(--text-muted) opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:text-(--danger)"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID) }
hx-swap="delete"
@@ -65,30 +65,30 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
}
</div>
} else {
<table class="block w-full overflow-x-auto whitespace-nowrap bg-[var(--panel)] md:table md:overflow-visible md:whitespace-normal">
<table class="block w-full overflow-x-auto whitespace-nowrap bg-(--panel) md:table md:overflow-visible md:whitespace-normal">
<thead>
<tr>
<th class="p-[0.6rem]"></th>
<th class="p-[0.6rem] text-left text-[0.67rem] text-[var(--text-faint)]">Title</th>
<th class="p-[0.6rem]"></th>
<th class="p-2.5"></th>
<th class="p-2.5 text-left text-xs text-(--text-faint)">Title</th>
<th class="p-2.5"></th>
</tr>
</thead>
<tbody>
for _, entry := range entries {
<tr class="hover:bg-[var(--panel-soft)]" id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
<td class="p-[0.6rem]">
<tr class="hover:bg-(--panel-soft)" id={ fmt.Sprintf("watchlist-entry-%d", entry.AnimeID) }>
<td class="p-2.5">
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="aspect-[2/3] w-9 object-cover" loading="lazy"/>
<img src={ entry.ImageUrl } alt={ entry.DisplayTitle() } class="aspect-2/3 w-9 object-cover" loading="lazy"/>
</a>
</td>
<td class="p-[0.6rem] font-medium">
<td class="p-2.5 font-medium">
<a href={ templ.SafeURL(fmt.Sprintf("/anime/%d", entry.AnimeID)) }>
{ entry.DisplayTitle() }
</a>
</td>
<td class="w-[90px] p-[0.6rem]">
<td class="w-24 p-2.5">
<button
class="cursor-pointer border-0 bg-transparent p-0 text-[0.8rem] text-[var(--text-muted)] hover:text-[var(--danger)]"
class="cursor-pointer border-0 bg-transparent p-0 text-xs text-(--text-muted) hover:text-(--danger)"
hx-delete={ string(templ.URL(fmt.Sprintf("/api/watchlist/%d?from=watchlist", entry.AnimeID))) }
hx-target={ fmt.Sprintf("#watchlist-entry-%d", entry.AnimeID) }
hx-swap="delete"
@@ -104,9 +104,9 @@ templ Watchlist(entries []database.GetUserWatchListRow, layout string, currentSt
}
func tabClass(active bool) string {
base := "shrink-0 whitespace-nowrap bg-[var(--panel-soft)] px-[0.45rem] py-[0.24rem] text-[0.76rem] text-[var(--text-muted)] no-underline hover:bg-[var(--surface-tab-hover)] hover:text-[var(--text)] hover:no-underline"
base := "shrink-0 whitespace-nowrap bg-(--panel-soft) px-2 py-1 text-xs text-(--text-muted) no-underline hover:bg-(--surface-tab-hover) hover:text-(--text) hover:no-underline"
if active {
return "shrink-0 whitespace-nowrap bg-[var(--surface-tab-active)] px-[0.45rem] py-[0.24rem] text-[0.76rem] text-[var(--accent)] no-underline hover:no-underline"
return "shrink-0 whitespace-nowrap bg-(--surface-tab-active) px-2 py-1 text-xs text-(--accent) no-underline hover:no-underline"
}
return base
}

View File

@@ -36,18 +36,18 @@ const clearSearchResults = (): void => {
const buildSearchResultItem = (result: QuickSearchResult): HTMLAnchorElement => {
const item = document.createElement('a')
item.className = 'flex items-start gap-3 px-3 py-2 text-inherit no-underline hover:bg-[var(--panel-soft)] hover:no-underline'
item.className = 'flex items-start gap-3 px-3 py-2 text-inherit no-underline hover:bg-(--panel-soft) hover:no-underline'
item.setAttribute('href', '/anime/' + encodeURIComponent(String(result.id || '')))
if (isSafeImageUrl(result.image)) {
const img = document.createElement('img')
img.className = 'aspect-[2/3] w-[42px] shrink-0 object-cover bg-[var(--surface-thumb)]'
img.className = 'aspect-2/3 w-[42px] shrink-0 object-cover bg-(--surface-thumb)'
img.setAttribute('src', result.image || '')
img.setAttribute('alt', String(result.title || ''))
item.appendChild(img)
} else {
const noImage = document.createElement('div')
noImage.className = 'aspect-[2/3] w-[42px] shrink-0 bg-[var(--surface-thumb)] text-[0] text-transparent'
noImage.className = 'aspect-2/3 w-[42px] shrink-0 bg-(--surface-thumb) text-[0] text-transparent'
noImage.textContent = 'no image'
item.appendChild(noImage)
}
@@ -56,12 +56,12 @@ const buildSearchResultItem = (result: QuickSearchResult): HTMLAnchorElement =>
info.className = 'grid min-w-0 gap-px'
const itemTitle = document.createElement('div')
itemTitle.className = 'line-clamp-1 text-[0.86rem] leading-[1.3] text-[var(--text)]'
itemTitle.className = 'line-clamp-1 text-[0.86rem] leading-[1.3] text-(--text)'
itemTitle.textContent = String(result.title || '')
info.appendChild(itemTitle)
const itemType = document.createElement('div')
itemType.className = 'text-[0.67rem] text-[var(--text-faint)]'
itemType.className = 'text-[0.67rem] text-(--text-faint)'
itemType.textContent = String(result.type || '')
info.appendChild(itemType)
@@ -83,7 +83,7 @@ const renderQuickSearchResults = (query: string, results: QuickSearchResult[]):
searchResults.className = 'grid'
const title = document.createElement('div')
title.className = 'px-3 py-2 text-[0.68rem] text-[var(--text-faint)]'
title.className = 'px-3 py-2 text-[0.68rem] text-(--text-faint)'
title.textContent = 'Anime'
searchResults.appendChild(title)
@@ -92,7 +92,7 @@ const renderQuickSearchResults = (query: string, results: QuickSearchResult[]):
})
const viewAll = document.createElement('a')
viewAll.className = 'bg-[var(--surface-search-view-all)] px-3 py-2 text-center text-[0.8rem] text-[var(--text-muted)] no-underline hover:bg-[var(--panel-soft)] hover:text-[var(--text)] hover:no-underline'
viewAll.className = 'bg-(--surface-search-view-all) px-3 py-2 text-center text-[0.8rem] text-(--text-muted) no-underline hover:bg-(--panel-soft) hover:text-(--text) hover:no-underline'
viewAll.setAttribute('href', '/search?q=' + encodeURIComponent(query))
viewAll.textContent = 'View all results for ' + query
searchResults.appendChild(viewAll)