mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Completed Splash-Screen & Iniital Tauri Wire-Up
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Play, ArrowRight, BookOpen, Clock } from "phosphor-svelte";
|
||||
import Thumbnail from "@shared/manga/Thumbnail.svelte";
|
||||
import type { HistoryEntry } from "@store/state.svelte";
|
||||
import type { Manga } from "@types";
|
||||
import { timeAgo } from "../lib/homeHelpers";
|
||||
import { Play, ArrowRight, BookOpen, Clock } from 'phosphor-svelte'
|
||||
import { timeAgo } from '$lib/components/home/homeHelpers'
|
||||
import Thumbnail from '$lib/components/shared/manga/Thumbnail.svelte'
|
||||
import { resolvedCover } from '$lib/core/cover/coverResolver'
|
||||
import type { Manga } from '$lib/types'
|
||||
import type { ReadSession } from '$lib/types/history'
|
||||
|
||||
let {
|
||||
entries,
|
||||
@@ -12,15 +13,15 @@
|
||||
onviewhistory,
|
||||
onopenlibrary,
|
||||
}: {
|
||||
entries: HistoryEntry[];
|
||||
libraryManga: Manga[];
|
||||
onresume: (entry: HistoryEntry) => void;
|
||||
onviewhistory: () => void;
|
||||
onopenlibrary: () => void;
|
||||
} = $props();
|
||||
entries: ReadSession[]
|
||||
libraryManga: Manga[]
|
||||
onresume: (entry: ReadSession) => void
|
||||
onviewhistory: () => void
|
||||
onopenlibrary: () => void
|
||||
} = $props()
|
||||
|
||||
function thumbFor(entry: HistoryEntry): string {
|
||||
return libraryManga.find(m => m.id === entry.mangaId)?.thumbnailUrl ?? entry.thumbnailUrl ?? "";
|
||||
function coverFor(entry: ReadSession): string {
|
||||
return libraryManga.find(m => m.id === entry.mangaId)?.thumbnailUrl ?? entry.thumbnailUrl ?? ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,16 +37,16 @@
|
||||
|
||||
<div class="list">
|
||||
{#if entries.length > 0}
|
||||
{#each entries as entry (entry.chapterId)}
|
||||
{#each entries as entry (entry.id)}
|
||||
<button class="row" onclick={() => onresume(entry)}>
|
||||
<Thumbnail src={thumbFor(entry)} alt={entry.mangaTitle} class="row-thumb" />
|
||||
<Thumbnail src={resolvedCover(entry.mangaId, coverFor(entry))} alt={entry.mangaTitle} class="row-thumb" />
|
||||
<div class="row-info">
|
||||
<span class="row-title">{entry.mangaTitle}</span>
|
||||
<span class="row-sub">
|
||||
{entry.chapterName}{entry.pageNumber > 1 ? ` · p.${entry.pageNumber}` : ""}
|
||||
{entry.endChapterName}{entry.endPage > 1 ? ` · p.${entry.endPage}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<span class="row-time">{timeAgo(entry.readAt)}</span>
|
||||
<span class="row-time">{timeAgo(entry.endedAt)}</span>
|
||||
<span class="row-play"><Play size={10} weight="fill" /></span>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -75,34 +76,19 @@
|
||||
.section { border-top: 1px solid var(--border-dim); flex-shrink: 0; }
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: var(--sp-3) var(--sp-4) var(--sp-2);
|
||||
}
|
||||
.section-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
display: inline-flex; align-items: center; gap: var(--sp-2);
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
}
|
||||
.see-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide); text-transform: uppercase;
|
||||
color: var(--text-faint); background: none; border: none; cursor: pointer; padding: 0;
|
||||
transition: color var(--t-base);
|
||||
}
|
||||
.see-all:hover { color: var(--accent-fg); }
|
||||
@@ -110,54 +96,31 @@
|
||||
.list { display: flex; flex-direction: column; padding: 0 var(--sp-3); overflow: hidden; }
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
padding: 7px var(--sp-2);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid transparent;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
display: flex; align-items: center; gap: var(--sp-3);
|
||||
padding: 7px var(--sp-2); border-radius: var(--radius-md);
|
||||
border: 1px solid transparent; background: none;
|
||||
text-align: left; cursor: pointer; width: 100%;
|
||||
transition: background var(--t-fast), border-color var(--t-fast);
|
||||
}
|
||||
.row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
|
||||
.row:hover .row-play { opacity: 1; }
|
||||
|
||||
:global(.row-thumb) {
|
||||
width: 33px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--border-dim);
|
||||
width: 33px; height: 48px; border-radius: var(--radius-sm);
|
||||
object-fit: cover; flex-shrink: 0; border: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.row-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; min-width: 0; }
|
||||
.row-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-secondary);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.row-sub {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-ui); font-size: var(--text-sm); color: var(--text-muted);
|
||||
letter-spacing: var(--tracking-wide); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.row-time {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-ui); font-size: var(--text-sm);
|
||||
color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0;
|
||||
}
|
||||
.row-play { color: var(--accent-fg); flex-shrink: 0; opacity: 0; transition: opacity var(--t-base); }
|
||||
|
||||
@@ -170,29 +133,18 @@
|
||||
|
||||
.placeholder { position: relative; }
|
||||
.placeholder-overlay {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: -1px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: var(--sp-4);
|
||||
position: absolute; left: 0; right: 0; top: 0; bottom: -1px;
|
||||
display: flex; align-items: flex-end; justify-content: center; padding-bottom: var(--sp-4);
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.6) 100%);
|
||||
}
|
||||
.placeholder-cta {
|
||||
pointer-events: all;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: 7px 16px;
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.13);
|
||||
color: rgba(255,255,255,0.62);
|
||||
cursor: pointer;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||
padding: 7px 16px; border-radius: var(--radius-full);
|
||||
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.13);
|
||||
color: rgba(255,255,255,0.62); cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.placeholder-cta:hover { background: rgba(255,255,255,0.14); color: rgba(255,255,255,0.9); }
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Play, ArrowRight, ArrowLeft, BookOpen, Clock, ListBullets, PushPin, X as XIcon } from "phosphor-svelte";
|
||||
import type { Manga, Chapter } from "@types";
|
||||
import type { HistoryEntry } from "@store/state.svelte";
|
||||
import { store, setGenreFilter, setNavPage } from "@store/state.svelte";
|
||||
import { timeAgo } from "../lib/homeHelpers";
|
||||
import { Play, ArrowRight, ArrowLeft, BookOpen, Clock, ListBullets, PushPin, X as XIcon } from 'phosphor-svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { timeAgo } from '$lib/components/home/homeHelpers'
|
||||
import Thumbnail from '$lib/components/shared/manga/Thumbnail.svelte'
|
||||
import { resolvedCover } from '$lib/core/cover/coverResolver'
|
||||
import type { Manga } from '$lib/types'
|
||||
import type { Chapter } from '$lib/types'
|
||||
import type { ReadSession } from '$lib/types/history'
|
||||
|
||||
interface HeroSlot {
|
||||
kind: "continue" | "pinned" | "empty";
|
||||
entry?: HistoryEntry;
|
||||
manga?: Manga;
|
||||
slotIndex: number;
|
||||
kind: 'continue' | 'pinned' | 'empty'
|
||||
entry?: ReadSession
|
||||
manga?: Manga
|
||||
slotIndex: number
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -33,29 +36,29 @@
|
||||
onunpin,
|
||||
onviewall,
|
||||
}: {
|
||||
resolvedSlots: HeroSlot[];
|
||||
activeIdx: number;
|
||||
heroThumb: string;
|
||||
heroTitle: string;
|
||||
heroManga: Manga | null | undefined;
|
||||
heroEntry: HistoryEntry | null;
|
||||
heroMangaId: number | null;
|
||||
heroChapters: Chapter[];
|
||||
heroNewChapter: Chapter | null;
|
||||
loadingHeroChapters: boolean;
|
||||
resuming: boolean;
|
||||
onresume: () => void;
|
||||
onopenchapter: (ch: Chapter) => void;
|
||||
oncyclenext: () => void;
|
||||
oncycleprev: () => void;
|
||||
ongotoslot: (i: number) => void;
|
||||
onopenpicker: (i: 1 | 2 | 3) => void;
|
||||
onunpin: (i: 1 | 2 | 3) => void;
|
||||
onviewall: () => void;
|
||||
} = $props();
|
||||
resolvedSlots: HeroSlot[]
|
||||
activeIdx: number
|
||||
heroThumb: string
|
||||
heroTitle: string
|
||||
heroManga: Manga | null | undefined
|
||||
heroEntry: ReadSession | null
|
||||
heroMangaId: number | null
|
||||
heroChapters: Chapter[]
|
||||
heroNewChapter: Chapter | null
|
||||
loadingHeroChapters: boolean
|
||||
resuming: boolean
|
||||
onresume: () => void
|
||||
onopenchapter: (ch: Chapter) => void
|
||||
oncyclenext: () => void
|
||||
oncycleprev: () => void
|
||||
ongotoslot: (i: number) => void
|
||||
onopenpicker: (i: 1 | 2 | 3) => void
|
||||
onunpin: (i: 1 | 2 | 3) => void
|
||||
onviewall: () => void
|
||||
} = $props()
|
||||
|
||||
const activeSlot = $derived(resolvedSlots[activeIdx]);
|
||||
const TOTAL_SLOTS = 4;
|
||||
const activeSlot = $derived(resolvedSlots[activeIdx])
|
||||
const TOTAL_SLOTS = 4
|
||||
</script>
|
||||
|
||||
<div class="hero-stage">
|
||||
@@ -71,12 +74,12 @@
|
||||
<button
|
||||
class="hero-cover-col"
|
||||
onclick={onresume}
|
||||
disabled={resuming || activeSlot?.kind === "empty"}
|
||||
aria-label={heroTitle ? `Resume ${heroTitle}` : "No manga selected"}
|
||||
disabled={resuming || activeSlot?.kind === 'empty'}
|
||||
aria-label={heroTitle ? `Resume ${heroTitle}` : 'No manga selected'}
|
||||
>
|
||||
{#if heroThumb}
|
||||
<img src={heroThumb} alt={heroTitle} class="hero-cover" loading="eager" decoding="async" />
|
||||
{#if activeSlot?.kind === "continue"}
|
||||
<Thumbnail src={resolvedCover(heroMangaId ?? 0, heroThumb)} alt={heroTitle} class="hero-cover" loading="eager" />
|
||||
{#if activeSlot?.kind === 'continue'}
|
||||
<div class="cover-resume-hint"><Play size={20} weight="fill" /></div>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -85,12 +88,12 @@
|
||||
</button>
|
||||
|
||||
<div class="hero-details">
|
||||
{#if activeSlot?.kind === "empty"}
|
||||
{#if activeSlot?.kind === 'empty'}
|
||||
<p class="hero-empty-title">Nothing here yet</p>
|
||||
<p class="hero-empty-sub">
|
||||
{activeSlot.slotIndex === 0
|
||||
? "Read a manga to see it here"
|
||||
: "Pin a manga or keep reading to fill this slot"}
|
||||
? 'Read a manga to see it here'
|
||||
: 'Pin a manga or keep reading to fill this slot'}
|
||||
</p>
|
||||
{#if activeSlot.slotIndex !== 0}
|
||||
<button class="hero-cta" onclick={() => onopenpicker(activeSlot.slotIndex as 1 | 2 | 3)}>
|
||||
@@ -99,7 +102,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="hero-tags">
|
||||
{#if activeSlot?.kind === "continue"}
|
||||
{#if activeSlot?.kind === 'continue'}
|
||||
<span class="hero-tag hero-tag-reading"><Play size={8} weight="fill" /> Reading</span>
|
||||
{:else}
|
||||
<span class="hero-tag hero-tag-pinned"><PushPin size={8} weight="fill" /> Pinned</span>
|
||||
@@ -110,7 +113,7 @@
|
||||
{#each (heroManga?.genre ?? []).slice(0, 3) as g}
|
||||
<button
|
||||
class="hero-tag hero-tag-genre"
|
||||
onclick={() => { setGenreFilter(g); setNavPage("explore"); }}
|
||||
onclick={() => goto(`/browse?genre=${encodeURIComponent(g)}`)}
|
||||
>{g}</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -121,9 +124,9 @@
|
||||
{#if heroEntry}
|
||||
<p class="hero-progress">
|
||||
<Clock size={10} weight="light" />
|
||||
{heroEntry.chapterName}
|
||||
{#if heroEntry.pageNumber > 1}<span class="hero-prog-page"> · p.{heroEntry.pageNumber}</span>{/if}
|
||||
<span class="hero-prog-time">{timeAgo(heroEntry.readAt)}</span>
|
||||
{heroEntry.endChapterName}
|
||||
{#if heroEntry.endPage > 1}<span class="hero-prog-page"> · p.{heroEntry.endPage}</span>{/if}
|
||||
<span class="hero-prog-time">{timeAgo(heroEntry.endedAt)}</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -132,17 +135,17 @@
|
||||
{/if}
|
||||
|
||||
<div class="hero-actions">
|
||||
{#if activeSlot?.kind === "continue"}
|
||||
{#if activeSlot?.kind === 'continue'}
|
||||
<button class="hero-cta" onclick={onresume} disabled={resuming}>
|
||||
<Play size={11} weight="fill" />{resuming ? "Loading…" : "Resume"}
|
||||
<Play size={11} weight="fill" />{resuming ? 'Loading…' : 'Resume'}
|
||||
</button>
|
||||
{:else if heroManga}
|
||||
<button class="hero-cta" onclick={() => store.previewManga = heroManga!}>
|
||||
<button class="hero-cta" onclick={() => goto(`/series/${heroManga!.id}`)}>
|
||||
<BookOpen size={11} weight="light" /> View manga
|
||||
</button>
|
||||
{/if}
|
||||
{#if activeSlot?.slotIndex !== 0}
|
||||
{#if activeSlot?.kind === "pinned"}
|
||||
{#if activeSlot?.kind === 'pinned'}
|
||||
<button class="hero-cta-ghost" onclick={() => onunpin(activeSlot.slotIndex as 1 | 2 | 3)}>
|
||||
<XIcon size={10} weight="bold" /> Unpin
|
||||
</button>
|
||||
@@ -164,7 +167,7 @@
|
||||
<button
|
||||
class="hero-dot"
|
||||
class:active={activeIdx === i}
|
||||
class:pinned={slot.kind === "pinned"}
|
||||
class:pinned={slot.kind === 'pinned'}
|
||||
onclick={() => ongotoslot(i)}
|
||||
aria-label="Slot {i + 1}"
|
||||
></button>
|
||||
@@ -182,7 +185,7 @@
|
||||
<ListBullets size={11} weight="bold" /><span>Up Next</span>
|
||||
</div>
|
||||
|
||||
{#if activeSlot?.kind === "empty"}
|
||||
{#if activeSlot?.kind === 'empty'}
|
||||
<p class="hero-chapters-empty">No chapters to show</p>
|
||||
{:else if loadingHeroChapters}
|
||||
{#each Array(4) as _}
|
||||
@@ -198,7 +201,7 @@
|
||||
<p class="hero-chapters-empty">No chapters available</p>
|
||||
{:else}
|
||||
{#each heroChapters as ch (ch.id)}
|
||||
{@const isCurrent = heroEntry?.chapterId === ch.id}
|
||||
{@const isCurrent = heroEntry?.endChapterId === ch.id}
|
||||
<button
|
||||
class="chapter-row"
|
||||
class:chapter-row-current={isCurrent}
|
||||
@@ -208,14 +211,14 @@
|
||||
<span class="ch-num">Ch.{ch.chapterNumber % 1 === 0 ? Math.floor(ch.chapterNumber) : ch.chapterNumber}</span>
|
||||
<div class="ch-info">
|
||||
<span class="ch-name">{ch.name}</span>
|
||||
{#if isCurrent && heroEntry && heroEntry.pageNumber > 1}
|
||||
<span class="ch-meta">p.{heroEntry.pageNumber} · in progress</span>
|
||||
{#if isCurrent && heroEntry && heroEntry.endPage > 1}
|
||||
<span class="ch-meta">p.{heroEntry.endPage} · in progress</span>
|
||||
{:else if ch.isRead}
|
||||
<span class="ch-meta ch-read">Read</span>
|
||||
{:else if ch.uploadDate}
|
||||
<span class="ch-meta">
|
||||
{new Date(Number(ch.uploadDate) > 1e10 ? Number(ch.uploadDate) : Number(ch.uploadDate) * 1000)
|
||||
.toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
||||
.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -274,316 +277,178 @@
|
||||
background: var(--bg-raised);
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-right: 1px solid rgba(255,255,255,0.07);
|
||||
}
|
||||
.hero-cover-col:hover .hero-cover { filter: brightness(1.1) saturate(1.05); }
|
||||
.hero-cover-col:hover :global(.hero-cover) { filter: brightness(1.1) saturate(1.05); }
|
||||
.hero-cover-col:hover .cover-resume-hint { opacity: 1; }
|
||||
.hero-cover { width: 100%; height: 100%; object-fit: cover; display: block; transition: filter 0.22s ease; }
|
||||
:global(.hero-cover) { width: 100%; height: 100%; object-fit: cover; display: block; transition: filter 0.22s ease; }
|
||||
.hero-cover-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-overlay);
|
||||
color: var(--text-faint);
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--bg-overlay); color: var(--text-faint);
|
||||
}
|
||||
.cover-resume-hint {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.38);
|
||||
opacity: 0;
|
||||
transition: opacity 0.18s ease;
|
||||
pointer-events: none;
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; background: rgba(0,0,0,0.38);
|
||||
opacity: 0; transition: opacity 0.18s ease; pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-details {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative; z-index: 2;
|
||||
flex: 1; min-width: 0;
|
||||
padding: var(--sp-5) var(--sp-5) var(--sp-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
display: flex; flex-direction: column; gap: var(--sp-2);
|
||||
overflow: hidden;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-right: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
|
||||
.hero-tags { display: flex; flex-wrap: wrap; gap: 5px; flex-shrink: 0; }
|
||||
.hero-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 9px;
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
font-family: var(--font-ui); font-size: 9px;
|
||||
letter-spacing: var(--tracking-wide); text-transform: uppercase;
|
||||
padding: 3px 8px; border-radius: var(--radius-sm);
|
||||
background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.6);
|
||||
border: 1px solid rgba(255,255,255,0.13);
|
||||
}
|
||||
.hero-tag-reading { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); }
|
||||
.hero-tag-pinned { background: rgba(168, 132, 232, 0.18); color: #c4a8f0; border-color: rgba(168, 132, 232, 0.28); }
|
||||
.hero-tag-new { background: rgba(74, 222, 128, 0.15); color: #86efac; border-color: rgba(74, 222, 128, 0.25); }
|
||||
.hero-tag-pinned { background: rgba(168,132,232,0.18); color: #c4a8f0; border-color: rgba(168,132,232,0.28); }
|
||||
.hero-tag-new { background: rgba(74,222,128,0.15); color: #86efac; border-color: rgba(74,222,128,0.25); }
|
||||
.hero-tag-genre { cursor: pointer; transition: background 0.15s, color 0.15s; }
|
||||
.hero-tag-genre:hover { background: rgba(255, 255, 255, 0.18); color: rgba(255, 255, 255, 0.9); }
|
||||
.hero-tag-genre:hover { background: rgba(255,255,255,0.18); color: rgba(255,255,255,0.9); }
|
||||
|
||||
.hero-title {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--weight-semibold);
|
||||
color: #fff;
|
||||
line-height: var(--leading-tight);
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.55);
|
||||
letter-spacing: -0.01em;
|
||||
font-size: var(--text-xl); font-weight: var(--weight-semibold);
|
||||
color: #fff; line-height: var(--leading-tight); margin: 0; flex-shrink: 0;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
text-shadow: 0 2px 12px rgba(0,0,0,0.55); letter-spacing: -0.01em;
|
||||
}
|
||||
.hero-author {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs);
|
||||
color: rgba(255,255,255,0.45); letter-spacing: var(--tracking-wide); flex-shrink: 0;
|
||||
}
|
||||
.hero-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
display: flex; align-items: center; gap: 5px; flex-shrink: 0;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs);
|
||||
color: rgba(255,255,255,0.55); letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.hero-prog-page { color: rgba(255, 255, 255, 0.35); }
|
||||
.hero-prog-time { margin-left: auto; color: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
.hero-prog-page { color: rgba(255,255,255,0.35); }
|
||||
.hero-prog-time { margin-left: auto; color: rgba(255,255,255,0.3); }
|
||||
.hero-desc {
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255, 255, 255, 0.38);
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.hero-empty-title {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--weight-medium);
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
flex-shrink: 0;
|
||||
font-size: var(--text-xs); color: rgba(255,255,255,0.38); line-height: 1.6;
|
||||
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; flex-shrink: 0;
|
||||
}
|
||||
.hero-empty-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: rgba(255,255,255,0.48); flex-shrink: 0; }
|
||||
.hero-empty-sub {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255, 255, 255, 0.26);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
line-height: var(--leading-snug);
|
||||
font-family: var(--font-ui); font-size: var(--text-xs);
|
||||
color: rgba(255,255,255,0.26); letter-spacing: var(--tracking-wide); line-height: var(--leading-snug);
|
||||
}
|
||||
|
||||
.hero-actions { display: flex; gap: var(--sp-2); flex-shrink: 0; flex-wrap: wrap; margin-top: var(--sp-1); }
|
||||
|
||||
.hero-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: 7px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--accent-muted);
|
||||
border: 1px solid var(--accent-dim);
|
||||
color: var(--accent-fg);
|
||||
cursor: pointer;
|
||||
transition: filter var(--t-base);
|
||||
white-space: nowrap;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||
padding: 7px 18px; border-radius: var(--radius-md);
|
||||
background: var(--accent-muted); border: 1px solid var(--accent-dim); color: var(--accent-fg);
|
||||
cursor: pointer; transition: filter var(--t-base); white-space: nowrap;
|
||||
}
|
||||
.hero-cta:hover:not(:disabled) { filter: brightness(1.18); }
|
||||
.hero-cta:disabled { opacity: 0.5; cursor: default; }
|
||||
|
||||
.hero-cta-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: 7px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
white-space: nowrap;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||
padding: 7px 14px; border-radius: var(--radius-md);
|
||||
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.11);
|
||||
color: rgba(255,255,255,0.48); cursor: pointer;
|
||||
transition: background var(--t-base), color var(--t-base); white-space: nowrap;
|
||||
}
|
||||
.hero-cta-ghost:hover { background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.82); }
|
||||
.hero-cta-ghost:hover { background: rgba(255,255,255,0.12); color: rgba(255,255,255,0.82); }
|
||||
|
||||
.hero-nav-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
flex-shrink: 0;
|
||||
margin-top: auto;
|
||||
padding-top: var(--sp-3);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.07);
|
||||
display: flex; align-items: center; gap: var(--sp-2);
|
||||
flex-shrink: 0; margin-top: auto; padding-top: var(--sp-3);
|
||||
border-top: 1px solid rgba(255,255,255,0.07);
|
||||
}
|
||||
.hero-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.11);
|
||||
color: rgba(255,255,255,0.55); cursor: pointer; flex-shrink: 0;
|
||||
transition: background var(--t-base), color var(--t-base);
|
||||
}
|
||||
.hero-nav-btn:hover { background: rgba(255, 255, 255, 0.18); color: #fff; }
|
||||
.hero-nav-btn:hover { background: rgba(255,255,255,0.18); color: #fff; }
|
||||
.hero-dots { display: flex; gap: 5px; align-items: center; }
|
||||
.hero-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 5px; height: 5px; border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2); border: none; cursor: pointer; padding: 0;
|
||||
transition: background var(--t-base), transform var(--t-base), width var(--t-base);
|
||||
}
|
||||
.hero-dot:hover { background: rgba(255, 255, 255, 0.48); }
|
||||
.hero-dot:hover { background: rgba(255,255,255,0.48); }
|
||||
.hero-dot.active { background: #fff; width: 14px; border-radius: 3px; }
|
||||
.hero-dot.pinned { background: rgba(168, 132, 232, 0.5); }
|
||||
.hero-dot.pinned { background: rgba(168,132,232,0.5); }
|
||||
.hero-dot.pinned.active { background: #c4a8f0; }
|
||||
.hero-counter {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.28);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
margin-left: auto;
|
||||
font-family: var(--font-ui); font-size: 10px;
|
||||
color: rgba(255,255,255,0.28); letter-spacing: var(--tracking-wide); margin-left: auto;
|
||||
}
|
||||
|
||||
.hero-chapters {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: clamp(180px, 30%, 232px);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--sp-4) var(--sp-3);
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
position: relative; z-index: 2;
|
||||
width: clamp(180px, 30%, 232px); flex-shrink: 0;
|
||||
display: flex; flex-direction: column;
|
||||
padding: var(--sp-4) var(--sp-3); gap: 1px; overflow: hidden;
|
||||
}
|
||||
.hero-chapters-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
padding-bottom: var(--sp-2);
|
||||
margin-bottom: var(--sp-1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
|
||||
flex-shrink: 0;
|
||||
display: flex; align-items: center; gap: var(--sp-2);
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: rgba(255,255,255,0.35); letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
padding-bottom: var(--sp-2); margin-bottom: var(--sp-1);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.07); flex-shrink: 0;
|
||||
}
|
||||
.hero-chapters-empty {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255, 255, 255, 0.22);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: var(--sp-3) 0;
|
||||
font-family: var(--font-ui); font-size: var(--text-xs);
|
||||
color: rgba(255,255,255,0.22); letter-spacing: var(--tracking-wide); padding: var(--sp-3) 0;
|
||||
}
|
||||
|
||||
.chapter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
width: 100%;
|
||||
padding: 7px var(--sp-2);
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; gap: var(--sp-2); width: 100%;
|
||||
padding: 7px var(--sp-2); border-radius: var(--radius-sm);
|
||||
background: none; border: none; text-align: left; cursor: pointer;
|
||||
transition: background var(--t-fast);
|
||||
}
|
||||
.chapter-row:hover { background: rgba(255, 255, 255, 0.07); }
|
||||
.chapter-row-current { background: rgba(255, 255, 255, 0.1) !important; }
|
||||
.chapter-row:hover { background: rgba(255,255,255,0.07); }
|
||||
.chapter-row-current { background: rgba(255,255,255,0.1) !important; }
|
||||
|
||||
.ch-num {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: rgba(255, 255, 255, 0.32);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
flex-shrink: 0;
|
||||
min-width: 36px;
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: rgba(255,255,255,0.32); letter-spacing: var(--tracking-wide); flex-shrink: 0; min-width: 36px;
|
||||
}
|
||||
.chapter-row-current .ch-num { color: var(--accent-fg); }
|
||||
|
||||
.ch-info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; min-width: 0; }
|
||||
.ch-name {
|
||||
font-size: var(--text-xs);
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.chapter-row-read .ch-name { color: rgba(255, 255, 255, 0.32); }
|
||||
.ch-name { font-size: var(--text-xs); color: rgba(255,255,255,0.72); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chapter-row-read .ch-name { color: rgba(255,255,255,0.32); }
|
||||
.chapter-row-current .ch-name { color: #fff; font-weight: var(--weight-medium); }
|
||||
.ch-meta {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 9px;
|
||||
color: rgba(255, 255, 255, 0.26);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
}
|
||||
.ch-read { color: rgba(255, 255, 255, 0.18); }
|
||||
.ch-meta { font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.26); letter-spacing: var(--tracking-wide); }
|
||||
.ch-read { color: rgba(255,255,255,0.18); }
|
||||
:global(.ch-play-icon) { color: var(--accent-fg); flex-shrink: 0; }
|
||||
|
||||
.chapter-row-sk { display: flex; gap: var(--sp-2); padding: 7px var(--sp-2); align-items: center; }
|
||||
.sk-info { flex: 1; display: flex; flex-direction: column; gap: 4px; }
|
||||
.sk { background: rgba(255, 255, 255, 0.06); border-radius: var(--radius-sm); animation: pulse 1.6s ease-in-out infinite; }
|
||||
.sk { background: rgba(255,255,255,0.06); border-radius: var(--radius-sm); animation: pulse 1.6s ease-in-out infinite; }
|
||||
.sk-num { width: 32px; height: 10px; flex-shrink: 0; }
|
||||
.sk-name { height: 11px; width: 85%; }
|
||||
.sk-meta { height: 9px; width: 50%; }
|
||||
|
||||
.ch-view-all {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: auto;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: rgba(255, 255, 255, 0.28);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: var(--sp-2) var(--sp-2) 0;
|
||||
transition: color var(--t-base);
|
||||
display: flex; align-items: center; gap: 4px; margin-top: auto;
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: rgba(255,255,255,0.28); letter-spacing: var(--tracking-wide);
|
||||
background: none; border: none; cursor: pointer;
|
||||
padding: var(--sp-2) var(--sp-2) 0; transition: color var(--t-base);
|
||||
}
|
||||
.ch-view-all:hover { color: var(--accent-fg); }
|
||||
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes backdropIn { from { opacity: 0 } to { opacity: 1 } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 0.4 } 50% { opacity: 0.7 } }
|
||||
</style>
|
||||
@@ -1,244 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { ArrowLeft, ArrowRight, Sparkle } from "phosphor-svelte";
|
||||
import Thumbnail from "@shared/manga/Thumbnail.svelte";
|
||||
import type { Manga } from "@types";
|
||||
import type { HistoryEntry } from "@store/state.svelte";
|
||||
import { fetchRecommendations, topGenres } from "../lib/recommendations";
|
||||
import type { RecommendedManga } from "../lib/recommendations";
|
||||
import { Sparkle } from 'phosphor-svelte'
|
||||
import type { Manga } from '$lib/types'
|
||||
import type { ReadSession } from '$lib/types/history'
|
||||
|
||||
let {
|
||||
libraryManga,
|
||||
history,
|
||||
onopenrecommended,
|
||||
}: {
|
||||
libraryManga: Manga[];
|
||||
history: HistoryEntry[];
|
||||
onopenrecommended: (m: Manga) => void;
|
||||
} = $props();
|
||||
|
||||
const CARD_MIN_WIDTH = 100;
|
||||
const GAP = 12;
|
||||
const ROWS = 2;
|
||||
|
||||
let containerEl: HTMLDivElement | undefined = $state();
|
||||
let containerWidth = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
if (!containerEl) return;
|
||||
const ro = new ResizeObserver(([entry]) => {
|
||||
containerWidth = entry.contentRect.width;
|
||||
});
|
||||
ro.observe(containerEl);
|
||||
return () => ro.disconnect();
|
||||
});
|
||||
|
||||
const cols = $derived(containerWidth > 0 ? Math.max(1, Math.floor((containerWidth + GAP) / (CARD_MIN_WIDTH + GAP))) : 6);
|
||||
const visibleCount = $derived(cols * ROWS);
|
||||
const gridStyle = $derived(`grid-template-columns: repeat(${cols}, 1fr);`);
|
||||
|
||||
let allRecs: RecommendedManga[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let _ctrl: AbortController | null = null;
|
||||
|
||||
$effect(() => {
|
||||
const _history = history;
|
||||
const _library = libraryManga;
|
||||
if (!_history.length || !_library.length) { allRecs = []; return; }
|
||||
_ctrl?.abort();
|
||||
const ctrl = new AbortController();
|
||||
_ctrl = ctrl;
|
||||
loading = true;
|
||||
fetchRecommendations(_history, _library, ctrl.signal)
|
||||
.then(r => { if (!ctrl.signal.aborted) { allRecs = r; loading = false; } })
|
||||
.catch(() => { if (!ctrl.signal.aborted) loading = false; });
|
||||
});
|
||||
|
||||
const genres = $derived(topGenres(history, libraryManga));
|
||||
|
||||
let genreIdx = $state(0);
|
||||
|
||||
const activeGenre = $derived(genres[genreIdx] ?? null);
|
||||
|
||||
const visibleRecs = $derived(
|
||||
(activeGenre
|
||||
? allRecs.filter(r => r.matchedGenres.some(g => g.toLowerCase() === activeGenre.toLowerCase()))
|
||||
: allRecs
|
||||
).slice(0, visibleCount)
|
||||
);
|
||||
|
||||
function prev() { genreIdx = (genreIdx - 1 + genres.length) % genres.length; }
|
||||
function next() { genreIdx = (genreIdx + 1) % genres.length; }
|
||||
libraryManga: Manga[]
|
||||
history: ReadSession[]
|
||||
onopenrecommended: (m: Manga) => void
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div class="col">
|
||||
<div class="col-header">
|
||||
<span class="col-title">
|
||||
<Sparkle size={10} weight="bold" /> Recommended
|
||||
</span>
|
||||
{#if genres.length > 1}
|
||||
<div class="genre-switcher">
|
||||
<button class="nav-btn" onclick={prev}><ArrowLeft size={9} weight="bold" /></button>
|
||||
<span class="genre-label">{activeGenre}</span>
|
||||
<button class="nav-btn" onclick={next}><ArrowRight size={9} weight="bold" /></button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid-container" bind:this={containerEl}>
|
||||
{#if loading}
|
||||
<p class="empty-msg">Loading…</p>
|
||||
{:else if visibleRecs.length > 0}
|
||||
<div class="card-grid" style={gridStyle}>
|
||||
{#each visibleRecs as r (r.manga.id)}
|
||||
<button class="card" onclick={() => onopenrecommended(r.manga)}>
|
||||
<div class="card-cover-wrap">
|
||||
<Thumbnail src={r.manga.thumbnailUrl} alt={r.manga.title} class="card-cover" />
|
||||
<div class="card-gradient"></div>
|
||||
<div class="card-footer">
|
||||
<p class="card-title">{r.manga.title}</p>
|
||||
<p class="card-badge">{r.matchedGenres.slice(0, 2).join(" · ")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="empty-msg">No recommendations found</p>
|
||||
{/if}
|
||||
<span class="col-title"><Sparkle size={10} weight="bold" /> Recommended</span>
|
||||
</div>
|
||||
<p class="stub">Recommendations coming soon</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.col { display: flex; flex-direction: column; min-width: 0; height: 100%; }
|
||||
|
||||
.col-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--sp-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.col-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--sp-2); flex-shrink: 0; }
|
||||
.col-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.genre-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
.genre-label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
text-transform: uppercase;
|
||||
min-width: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
color: var(--text-faint);
|
||||
transition: color var(--t-base);
|
||||
}
|
||||
.nav-btn:hover { color: var(--accent-fg); }
|
||||
|
||||
.grid-container {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, auto);
|
||||
grid-auto-rows: 0;
|
||||
overflow: hidden;
|
||||
gap: var(--sp-3);
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.card:hover :global(.card-cover) { filter: brightness(1.1) saturate(1.05); transform: scale(1.02); }
|
||||
|
||||
.card-cover-wrap {
|
||||
position: relative;
|
||||
aspect-ratio: 2 / 3;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-dim);
|
||||
box-shadow: 0 2px 14px rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
:global(.card-cover) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: filter 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.card-gradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.08) 55%, transparent 75%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.card-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: var(--sp-2);
|
||||
pointer-events: none;
|
||||
}
|
||||
.card-title {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-medium);
|
||||
color: rgba(255,255,255,0.92);
|
||||
line-height: var(--leading-snug);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 1px 4px rgba(0,0,0,0.7);
|
||||
}
|
||||
.card-badge {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 9px;
|
||||
color: rgba(255,255,255,0.45);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
margin-top: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.empty-msg {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
padding: var(--sp-1) 0;
|
||||
display: inline-flex; align-items: center; gap: var(--sp-2);
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
}
|
||||
.stub { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
</style>
|
||||
@@ -1,20 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Fire, BookOpen, Clock, TrendUp, Bell, CalendarBlank } from "phosphor-svelte";
|
||||
import { formatReadTime } from "../lib/homeHelpers";
|
||||
import { Fire, BookOpen, Clock, TrendUp, Bell, CalendarBlank } from 'phosphor-svelte'
|
||||
import { formatReadTime } from '$lib/components/home/homeHelpers'
|
||||
import type { ReadingStats } from '$lib/types/history'
|
||||
|
||||
let {
|
||||
stats,
|
||||
updateCount,
|
||||
}: {
|
||||
stats: {
|
||||
currentStreakDays: number;
|
||||
totalChaptersRead: number;
|
||||
totalMinutesRead: number;
|
||||
totalMangaRead: number;
|
||||
longestStreakDays: number;
|
||||
};
|
||||
updateCount: number;
|
||||
} = $props();
|
||||
stats: ReadingStats
|
||||
updateCount: number
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div class="col">
|
||||
@@ -69,65 +64,39 @@
|
||||
|
||||
<style>
|
||||
.col { display: flex; flex-direction: column; min-width: 0; }
|
||||
|
||||
.col-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--sp-2);
|
||||
}
|
||||
.col-header { display: flex; align-items: center; justify-content: space-between; padding-bottom: var(--sp-2); }
|
||||
.col-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wider);
|
||||
text-transform: uppercase;
|
||||
display: inline-flex; align-items: center; gap: var(--sp-2);
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
}
|
||||
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--sp-3);
|
||||
display: flex; align-items: center; gap: var(--sp-3);
|
||||
background: var(--bg-raised); border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius-md); padding: var(--sp-3);
|
||||
transition: border-color var(--t-fast);
|
||||
}
|
||||
.card:hover { border-color: var(--border-base); }
|
||||
|
||||
.icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-sm);
|
||||
flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 32px; height: 32px; border-radius: var(--radius-sm); flex-shrink: 0;
|
||||
}
|
||||
.fire { background: rgba(251, 146, 60, 0.15); color: #fb923c; }
|
||||
.fire { background: rgba(251,146,60,0.15); color: #fb923c; }
|
||||
.accent { background: var(--accent-muted); color: var(--accent-fg); }
|
||||
.neutral { background: var(--bg-overlay); color: var(--text-faint); }
|
||||
.green { background: rgba(34, 197, 94, 0.12); color: #22c55e; }
|
||||
.green { background: rgba(34,197,94,0.12); color: #22c55e; }
|
||||
|
||||
.body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
||||
.val {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-lg, 1.05rem);
|
||||
font-weight: var(--weight-medium);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1;
|
||||
font-family: var(--font-ui); font-size: var(--text-lg, 1.05rem);
|
||||
font-weight: var(--weight-medium); color: var(--text-secondary); line-height: 1;
|
||||
}
|
||||
.label {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--text-2xs);
|
||||
color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide);
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
color: var(--text-faint); letter-spacing: var(--tracking-wide); white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user