mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: Toolbar Uniformity & SeriesDetail Redirect (#66)
This commit is contained in:
@@ -211,8 +211,8 @@
|
||||
|
||||
.content { flex: 1; overflow-y: auto; padding: 0 var(--sp-6) var(--sp-6); display: flex; flex-direction: column; gap: var(--sp-4); }
|
||||
|
||||
.icon-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); color: var(--text-muted); background: none; cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
.icon-btn:hover:not(:disabled):not(.active) { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); }
|
||||
.icon-btn { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
.icon-btn:hover:not(:disabled):not(.active) { color: var(--text-primary); border-color: var(--border-strong); }
|
||||
.icon-btn.active:hover:not(:disabled) { border-color: var(--accent); background: var(--accent-muted); }
|
||||
.icon-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
.icon-btn.loading { border-color: var(--accent-dim); color: var(--accent-fg); background: var(--accent-muted); }
|
||||
|
||||
@@ -323,8 +323,9 @@
|
||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||
.list { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: 1px; }
|
||||
.empty { display: flex; align-items: center; justify-content: center; flex: 1; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); }
|
||||
:global(.icon-btn) { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-md); color: var(--text-muted); transition: color var(--t-base), background var(--t-base); }
|
||||
:global(.icon-btn:hover:not(:disabled)) { color: var(--text-primary); background: var(--bg-raised); }
|
||||
:global(.icon-btn) { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
:global(.icon-btn:hover:not(:disabled)) { color: var(--text-primary); border-color: var(--border-strong); }
|
||||
:global(.icon-btn-active) { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||
.ext-panel { display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3) var(--sp-6); flex-shrink: 0; border-bottom: 1px solid var(--border-dim); background: var(--bg-raised); opacity: 1; }
|
||||
.ext-panel-anim { animation: panelSlide 0.18s cubic-bezier(0.16,1,0.3,1) both; }
|
||||
.panel-header { display: flex; align-items: center; padding-bottom: var(--sp-1); }
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
|
||||
const totalPages = $derived(Math.ceil(sortedChapters.length / CHAPTERS_PER_PAGE));
|
||||
const pageChapters = $derived(sortedChapters.slice((chapterPage - 1) * CHAPTERS_PER_PAGE, chapterPage * CHAPTERS_PER_PAGE));
|
||||
const readCount = $derived(chapters.filter(c => c.isRead).length);
|
||||
const totalCount = $derived(chapters.length);
|
||||
const readCount = $derived(sortedChapters.filter(c => c.isRead).length);
|
||||
const totalCount = $derived(sortedChapters.length);
|
||||
const progressPct = $derived(totalCount > 0 ? (readCount / totalCount) * 100 : 0);
|
||||
const downloadedCount = $derived(chapters.filter(c => c.isDownloaded).length);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
let manageOpen: boolean = $state(false);
|
||||
let genresExpanded: boolean = $state(false);
|
||||
let altOpen: boolean = $state(false);
|
||||
|
||||
const statusLabel = $derived(
|
||||
manga?.status ? manga.status.charAt(0) + manga.status.slice(1).toLowerCase() : null
|
||||
@@ -68,7 +69,9 @@
|
||||
!!store.settings.mangaPrefs?.[store.activeManga!.id]?.coverUrl
|
||||
);
|
||||
|
||||
function focusOnMount(node: HTMLElement) { node.focus(); }
|
||||
const altTitles = $derived(
|
||||
(manga as any)?.alternativeTitles ?? (manga as any)?.altTitles ?? []
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="sidebar">
|
||||
@@ -76,9 +79,9 @@
|
||||
<ArrowLeft size={13} weight="light" /> Back
|
||||
</button>
|
||||
|
||||
<div class="cover-wrap">
|
||||
<button class="cover-wrap" onclick={() => setPreviewManga(manga)}>
|
||||
<Thumbnail src={resolvedCover(store.activeManga!.id, store.activeManga!.thumbnailUrl)} alt={store.activeManga!.title} class="cover" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if loadingManga}
|
||||
<div class="meta-skeleton">
|
||||
@@ -88,12 +91,36 @@
|
||||
{:else}
|
||||
<div class="meta">
|
||||
<p class="title">{manga?.title}</p>
|
||||
|
||||
{#if manga?.author || manga?.artist}
|
||||
<p class="byline">{[manga?.author, manga?.artist].filter(Boolean).filter((v, i, a) => a.indexOf(v) === i).join(" · ")}</p>
|
||||
{/if}
|
||||
{#if statusLabel}
|
||||
<span class="status-badge" class:ongoing={manga?.status === "ONGOING"} class:ended={manga?.status !== "ONGOING"}>{statusLabel}</span>
|
||||
|
||||
<div class="badges">
|
||||
{#if statusLabel}
|
||||
<span class="badge" class:badge-ongoing={manga?.status === "ONGOING"} class:badge-ended={manga?.status !== "ONGOING"}>{statusLabel}</span>
|
||||
{/if}
|
||||
{#if manga?.source?.displayName ?? (manga as any)?.source?.name}
|
||||
<span class="badge badge-source">{manga?.source?.displayName ?? (manga as any)?.source?.name}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if altTitles.length > 0}
|
||||
<div class="alttitles-section">
|
||||
<button class="row-toggle" onclick={() => altOpen = !altOpen}>
|
||||
<span>Also known as</span>
|
||||
<CaretDown size={10} weight="light" style="transform:{altOpen ? 'rotate(180deg)' : 'rotate(0)'};transition:transform 0.15s ease;flex-shrink:0" />
|
||||
</button>
|
||||
{#if altOpen}
|
||||
<div class="alttitles-list">
|
||||
{#each altTitles as t}
|
||||
<p class="alttitle">{t}</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if manga?.genre?.length}
|
||||
<div class="genres">
|
||||
{#each (genresExpanded ? manga.genre : manga.genre.slice(0, 3)) as g}
|
||||
@@ -106,8 +133,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if manga?.description}
|
||||
<p class="desc">{manga.description}</p>
|
||||
<div class="desc-wrap">
|
||||
<p class="desc">{manga.description}</p>
|
||||
<button class="expand-toggle" onclick={() => setPreviewManga(manga)}>Read more</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -197,11 +228,13 @@
|
||||
padding: var(--sp-5);
|
||||
border-right: 1px solid var(--border-dim);
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-4);
|
||||
background: var(--bg-base);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar { display: none; }
|
||||
|
||||
.back {
|
||||
display: flex; align-items: center; gap: var(--sp-2);
|
||||
@@ -215,13 +248,17 @@
|
||||
width: 100%; aspect-ratio: 2/3; border-radius: var(--radius-md);
|
||||
overflow: hidden; background: var(--bg-raised);
|
||||
border: 1px solid var(--border-dim); flex-shrink: 0;
|
||||
cursor: pointer; transition: opacity var(--t-base);
|
||||
padding: 0;
|
||||
}
|
||||
.cover-wrap:hover { opacity: 0.88; }
|
||||
:global(.cover) { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.meta-skeleton { display: flex; flex-direction: column; gap: var(--sp-2); }
|
||||
.sk-line { border-radius: var(--radius-sm); }
|
||||
|
||||
.meta { display: flex; flex-direction: column; gap: var(--sp-3); }
|
||||
|
||||
.title {
|
||||
font-size: var(--text-base); font-weight: var(--weight-medium);
|
||||
color: var(--text-primary); line-height: var(--leading-snug);
|
||||
@@ -229,13 +266,33 @@
|
||||
}
|
||||
.byline { font-size: var(--text-xs); color: var(--text-muted); font-family: var(--font-ui); }
|
||||
|
||||
.status-badge {
|
||||
.badges { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
|
||||
.badge {
|
||||
display: inline-block; font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
padding: 2px 7px; border-radius: var(--radius-sm); width: fit-content;
|
||||
}
|
||||
.status-badge.ongoing { background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); }
|
||||
.status-badge.ended { background: var(--bg-raised); color: var(--text-faint); border: 1px solid var(--border-dim); }
|
||||
.badge-ongoing { background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); }
|
||||
.badge-ended { background: var(--bg-raised); color: var(--text-faint); border: 1px solid var(--border-dim); }
|
||||
.badge-source {
|
||||
background: var(--bg-raised); color: var(--text-faint); border: 1px solid var(--border-dim);
|
||||
text-transform: none; letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
|
||||
.alttitles-section { display: flex; flex-direction: column; gap: var(--sp-1); }
|
||||
.row-toggle {
|
||||
display: flex; align-items: center; justify-content: space-between; width: 100%;
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide); padding: 2px 0;
|
||||
transition: color var(--t-base);
|
||||
}
|
||||
.row-toggle:hover { color: var(--text-muted); }
|
||||
.alttitles-list { display: flex; flex-direction: column; gap: 3px; padding-top: var(--sp-1); }
|
||||
.alttitle {
|
||||
font-size: var(--text-2xs); color: var(--text-faint); font-family: var(--font-ui);
|
||||
line-height: var(--leading-snug); padding-left: var(--sp-1);
|
||||
border-left: 1px solid var(--border-dim);
|
||||
}
|
||||
|
||||
.genres { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
|
||||
.genre {
|
||||
@@ -253,10 +310,17 @@
|
||||
}
|
||||
.genre-toggle:hover { color: var(--accent-fg); border-color: var(--accent-dim); }
|
||||
|
||||
.desc-wrap { display: flex; flex-direction: column; gap: var(--sp-1); }
|
||||
.desc {
|
||||
font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-base);
|
||||
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.expand-toggle {
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint);
|
||||
letter-spacing: var(--tracking-wide); align-self: flex-start;
|
||||
transition: color var(--t-base);
|
||||
}
|
||||
.expand-toggle:hover { color: var(--accent-fg); }
|
||||
|
||||
.cta-section { display: flex; flex-direction: column; gap: var(--sp-2); }
|
||||
.read-btn {
|
||||
|
||||
@@ -32,32 +32,37 @@
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-top">
|
||||
<span class="heading">Tracking</span>
|
||||
<button class="icon-btn" onclick={onRefresh} disabled={loading} title="Refresh">
|
||||
<ArrowsClockwise size={14} weight="bold" class={loading ? "anim-spin" : ""} />
|
||||
</button>
|
||||
|
||||
{#if !loading && loggedIn.length > 0}
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="tab" class:active={activeTrackerId === "all"}
|
||||
onclick={() => onTrackerChange("all")}
|
||||
>
|
||||
All
|
||||
<span class="tab-count">{totalCount}</span>
|
||||
</button>
|
||||
{#each loggedIn as t}
|
||||
<button
|
||||
class="tab" class:active={activeTrackerId === t.id}
|
||||
onclick={() => onTrackerChange(t.id)}
|
||||
>
|
||||
<Thumbnail src={t.icon} alt={t.name} class="tab-icon" />
|
||||
{t.name}
|
||||
<span class="tab-count">{t.trackRecords.nodes.length}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="header-right">
|
||||
<button class="icon-btn" onclick={onRefresh} disabled={loading} title="Refresh">
|
||||
<ArrowsClockwise size={14} weight="bold" class={loading ? "anim-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !loading && loggedIn.length > 0}
|
||||
<div class="tracker-tabs">
|
||||
<button
|
||||
class="tracker-tab" class:active={activeTrackerId === "all"}
|
||||
onclick={() => onTrackerChange("all")}
|
||||
>
|
||||
All
|
||||
<span class="tab-count">{totalCount}</span>
|
||||
</button>
|
||||
{#each loggedIn as t}
|
||||
<button
|
||||
class="tracker-tab" class:active={activeTrackerId === t.id}
|
||||
onclick={() => onTrackerChange(t.id)}
|
||||
>
|
||||
<Thumbnail src={t.icon} alt={t.name} class="tab-icon" />
|
||||
{t.name}
|
||||
<span class="tab-count">{t.trackRecords.nodes.length}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="filter-row">
|
||||
<div class="search-wrap">
|
||||
<MagnifyingGlass size={13} weight="light" class="search-ico" />
|
||||
@@ -94,79 +99,40 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toolbar { flex-shrink: 0; border-bottom: 1px solid var(--border-dim); }
|
||||
.toolbar { flex-shrink: 0; }
|
||||
|
||||
.toolbar-top {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
display: flex; align-items: center; gap: var(--sp-4);
|
||||
padding: var(--sp-4) var(--sp-6);
|
||||
border-bottom: 1px solid var(--border-dim);
|
||||
}
|
||||
.heading {
|
||||
font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-normal);
|
||||
color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
}
|
||||
.icon-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 26px; height: 26px; border-radius: var(--radius-sm);
|
||||
color: var(--text-faint); background: none; border: none; cursor: pointer;
|
||||
transition: color var(--t-base), background var(--t-base);
|
||||
}
|
||||
.icon-btn:hover:not(:disabled) { color: var(--text-muted); background: var(--bg-raised); }
|
||||
.icon-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
.heading { font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-normal); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; flex-shrink: 0; }
|
||||
.header-right { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; flex-shrink: 0; }
|
||||
|
||||
.tracker-tabs {
|
||||
display: flex; align-items: center; gap: 1px;
|
||||
padding: 0 var(--sp-5); overflow-x: auto; scrollbar-width: none;
|
||||
}
|
||||
.tracker-tabs::-webkit-scrollbar { display: none; }
|
||||
.tabs { display: flex; align-items: center; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; overflow-x: auto; scrollbar-width: none; }
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.tab { display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid transparent; color: var(--text-faint); white-space: nowrap; cursor: pointer; flex-shrink: 0; transition: background var(--t-base), color var(--t-base), border-color var(--t-base); }
|
||||
.tab:hover { color: var(--text-muted); }
|
||||
.tab.active { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); }
|
||||
|
||||
.tracker-tab {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 8px 10px 7px;
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||
letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
||||
color: var(--text-faint); background: none; border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer; white-space: nowrap; margin-bottom: -1px;
|
||||
transition: color var(--t-base), border-color var(--t-base);
|
||||
}
|
||||
.tracker-tab:hover { color: var(--text-muted); }
|
||||
.tracker-tab.active { color: var(--text-secondary); border-bottom-color: var(--accent); }
|
||||
:global(.tab-icon) { width: 13px; height: 13px; border-radius: 2px; object-fit: contain; opacity: 0.8; }
|
||||
|
||||
.tab-count {
|
||||
font-size: 10px; padding: 1px 5px; border-radius: var(--radius-full);
|
||||
background: var(--bg-overlay); color: var(--text-faint); line-height: 15px;
|
||||
}
|
||||
.tracker-tab.active .tab-count { background: var(--accent-muted); color: var(--accent-fg); }
|
||||
.tab-count { font-size: var(--text-2xs); opacity: 0.6; }
|
||||
.tab.active .tab-count { opacity: 1; }
|
||||
|
||||
.filter-row {
|
||||
display: flex; align-items: center; gap: var(--sp-2);
|
||||
padding: var(--sp-2) var(--sp-5) var(--sp-3);
|
||||
}
|
||||
.search-wrap {
|
||||
flex: 1; display: flex; align-items: center; gap: var(--sp-2);
|
||||
background: var(--bg-raised); border: 1px solid var(--border-dim);
|
||||
border-radius: var(--radius-md); padding: 5px 10px;
|
||||
transition: border-color var(--t-base);
|
||||
}
|
||||
.icon-btn { display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); cursor: pointer; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
.icon-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
|
||||
.icon-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
.filter-row { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-6); border-bottom: 1px solid var(--border-dim); }
|
||||
.search-wrap { flex: 1; display: flex; align-items: center; gap: var(--sp-2); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px; transition: border-color var(--t-base); }
|
||||
.search-wrap:focus-within { border-color: var(--border-strong); }
|
||||
:global(.search-ico) { color: var(--text-faint); flex-shrink: 0; }
|
||||
.search-input {
|
||||
flex: 1; background: none; border: none; outline: none; min-width: 0;
|
||||
font-size: var(--text-sm); color: var(--text-primary);
|
||||
}
|
||||
.search-input { flex: 1; background: none; border: none; outline: none; min-width: 0; font-size: var(--text-sm); color: var(--text-primary); }
|
||||
.search-input::placeholder { color: var(--text-faint); }
|
||||
|
||||
.pill-select {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide);
|
||||
padding: 5px 22px 5px 9px; border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-dim); background: var(--bg-raised);
|
||||
color: var(--text-faint); outline: none; cursor: pointer; appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%23555' stroke-width='1.3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat; background-position: right 7px center;
|
||||
transition: border-color var(--t-base), color var(--t-base);
|
||||
}
|
||||
.pill-select { flex-shrink: 0; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 5px 22px 5px 9px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-faint); outline: none; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%23555' stroke-width='1.3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 7px center; transition: border-color var(--t-base), color var(--t-base); }
|
||||
.pill-select:hover { border-color: var(--border-strong); color: var(--text-muted); }
|
||||
.pill-select option { background: var(--bg-surface); color: var(--text-secondary); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user