mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Finalized Svelte-5 Rewrite (Testing Phase)
This commit is contained in:
@@ -1,34 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { navPage, activeManga } from "../../store";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import Home from "../pages/Home.svelte";
|
||||
import Library from "../pages/Library.svelte";
|
||||
import SeriesDetail from "../pages/SeriesDetail.svelte";
|
||||
import History from "../pages/History.svelte";
|
||||
import Search from "../pages/Search.svelte";
|
||||
import Discover from "../pages/Discover.svelte";
|
||||
import Downloads from "../pages/Downloads.svelte";
|
||||
import Extensions from "../pages/Extensions.svelte";
|
||||
import { store } from "../../store/state.svelte";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import Home from "../pages/Home.svelte";
|
||||
import Library from "../pages/Library.svelte";
|
||||
import SeriesDetail from "../pages/SeriesDetail.svelte";
|
||||
import History from "../pages/History.svelte";
|
||||
import Search from "../pages/Search.svelte";
|
||||
import Discover from "../pages/Discover.svelte";
|
||||
import GenreDrillPage from "../pages/GenreDrillPage.svelte";
|
||||
import Downloads from "../pages/Downloads.svelte";
|
||||
import Extensions from "../pages/Extensions.svelte";
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Sidebar />
|
||||
<main class="main">
|
||||
{#if activeManga}
|
||||
{#if store.activeManga}
|
||||
<SeriesDetail />
|
||||
{:else if navPage === "home"}
|
||||
{:else if store.navPage === "home"}
|
||||
<Home />
|
||||
{:else if navPage === "library"}
|
||||
{:else if store.navPage === "library"}
|
||||
<Library />
|
||||
{:else if navPage === "search"}
|
||||
{:else if store.navPage === "search"}
|
||||
<Search />
|
||||
{:else if navPage === "history"}
|
||||
{:else if store.navPage === "history"}
|
||||
<History />
|
||||
{:else if navPage === "explore" || navPage === "sources"}
|
||||
{:else if (store.navPage === "explore" || store.navPage === "sources") && store.genreFilter}
|
||||
<GenreDrillPage />
|
||||
{:else if store.navPage === "explore" || store.navPage === "sources"}
|
||||
<Discover />
|
||||
{:else if navPage === "downloads"}
|
||||
{:else if store.navPage === "downloads"}
|
||||
<Downloads />
|
||||
{:else if navPage === "extensions"}
|
||||
{:else if store.navPage === "extensions"}
|
||||
<Extensions />
|
||||
{:else}
|
||||
<Home />
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ClockCounterClockwise, Trash, MagnifyingGlass, Play, Books, Fire, BookOpen, Clock, TrendUp } from "phosphor-svelte";
|
||||
import { thumbUrl } from "../../lib/client";
|
||||
import { history, readingStats, clearHistory, activeManga, activeChapterList, openReader } from "../../store";
|
||||
import type { HistoryEntry } from "../../store";
|
||||
import { store, clearHistory, openReader, setActiveManga } from "../../store/state.svelte";
|
||||
import type { HistoryEntry } from "../../store/state.svelte";
|
||||
|
||||
let search = "";
|
||||
let confirmClear = false;
|
||||
let search = $state("");
|
||||
let confirmClear = $state(false);
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function timeAgo(ts: number): string {
|
||||
const diff = Date.now() - ts, m = Math.floor(diff / 60000);
|
||||
if (m < 1) return "Just now";
|
||||
@@ -34,13 +33,18 @@
|
||||
return r === 0 ? `${h}h` : `${h}h ${r}m`;
|
||||
}
|
||||
|
||||
// ── Session grouping — collapses rapid same-manga reads ───────────────────────
|
||||
const SESSION_GAP_MS = 30 * 60 * 1000;
|
||||
|
||||
interface Session {
|
||||
mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
||||
latestChapterId: number; latestChapterName: string; latestPageNumber: number;
|
||||
firstChapterName: string; chapterCount: number; readAt: number;
|
||||
mangaId: number;
|
||||
mangaTitle: string;
|
||||
thumbnailUrl: string;
|
||||
latestChapterId: number;
|
||||
latestChapterName: string;
|
||||
latestPageNumber: number;
|
||||
firstChapterName: string;
|
||||
chapterCount: number;
|
||||
readAt: number;
|
||||
}
|
||||
|
||||
function buildSessions(entries: HistoryEntry[]): Session[] {
|
||||
@@ -53,28 +57,37 @@
|
||||
let j = i + 1;
|
||||
while (j < entries.length) {
|
||||
const next = entries[j];
|
||||
if (next.mangaId === anchor.mangaId && anchor.readAt - next.readAt <= SESSION_GAP_MS) { group.push(next); j++; }
|
||||
else break;
|
||||
if (next.mangaId === anchor.mangaId && anchor.readAt - next.readAt <= SESSION_GAP_MS) {
|
||||
group.push(next); j++;
|
||||
} else break;
|
||||
}
|
||||
const latest = group[0], oldest = group[group.length - 1];
|
||||
sessions.push({
|
||||
mangaId: latest.mangaId, mangaTitle: latest.mangaTitle, thumbnailUrl: latest.thumbnailUrl,
|
||||
latestChapterId: latest.chapterId, latestChapterName: latest.chapterName,
|
||||
latestPageNumber: latest.pageNumber, firstChapterName: oldest.chapterName,
|
||||
chapterCount: group.length, readAt: latest.readAt,
|
||||
mangaId: latest.mangaId,
|
||||
mangaTitle: latest.mangaTitle,
|
||||
thumbnailUrl: latest.thumbnailUrl,
|
||||
latestChapterId: latest.chapterId,
|
||||
latestChapterName: latest.chapterName,
|
||||
latestPageNumber: latest.pageNumber,
|
||||
firstChapterName: oldest.chapterName,
|
||||
chapterCount: group.length,
|
||||
readAt: latest.readAt,
|
||||
});
|
||||
i = j;
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
$: filtered = search.trim()
|
||||
? $history.filter(e => e.mangaTitle.toLowerCase().includes(search.toLowerCase()) || e.chapterName.toLowerCase().includes(search.toLowerCase()))
|
||||
: $history;
|
||||
const filtered = $derived(search.trim()
|
||||
? store..filter((e) =>
|
||||
e.mangaTitle.toLowerCase().includes(search.toLowerCase()) ||
|
||||
e.chapterName.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: store.);
|
||||
|
||||
$: sessions = buildSessions(filtered);
|
||||
const sessions = $derived(buildSessions(filtered));
|
||||
|
||||
$: groups = (() => {
|
||||
const groups = $derived.by(() => {
|
||||
const map = new Map<string, Session[]>();
|
||||
for (const s of sessions) {
|
||||
const l = dayLabel(s.readAt);
|
||||
@@ -82,12 +95,12 @@
|
||||
map.get(l)!.push(s);
|
||||
}
|
||||
return Array.from(map.entries()).map(([label, items]) => ({ label, items }));
|
||||
})();
|
||||
});
|
||||
|
||||
function resume(session: Session) {
|
||||
const ch = $activeChapterList.find(c => c.id === session.latestChapterId);
|
||||
if (ch && $activeChapterList.length > 0) openReader(ch, $activeChapterList);
|
||||
else activeManga.set({ id: session.mangaId, title: session.mangaTitle, thumbnailUrl: session.thumbnailUrl } as any);
|
||||
const ch = store..find((c) => c.id === session.latestChapterId);
|
||||
if (ch && store..length > 0) openReader(ch, );
|
||||
else setActiveManga({ id: session.mangaId, title: session.mangaTitle, thumbnailUrl: session.thumbnailUrl } as any);
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
@@ -98,18 +111,17 @@
|
||||
|
||||
<div class="root">
|
||||
|
||||
<!-- ── Header ──────────────────────────────────────────────────────────────── -->
|
||||
<div class="page-header">
|
||||
<span class="heading">History</span>
|
||||
<div class="header-right">
|
||||
<div class="search-wrap">
|
||||
<MagnifyingGlass size={12} class="search-icon" weight="light" />
|
||||
<input class="search" placeholder="Search history…" bind:value={search} />
|
||||
{#if search}<button class="search-clear" on:click={() => search = ""}>×</button>{/if}
|
||||
<input class="search" placeholder="Search store.…" bind:value={search} />
|
||||
{#if search}<button class="search-clear" onclick={() => search = ""}>×</button>{/if}
|
||||
</div>
|
||||
{#if $history.length > 0}
|
||||
<button class="clear-btn" class:confirm={confirmClear} on:click={handleClear}
|
||||
title={confirmClear ? "Click again to confirm" : "Clear history feed"}>
|
||||
{#if store..length > 0}
|
||||
<button class="clear-btn" class:confirm={confirmClear} onclick={handleClear}
|
||||
title={confirmClear ? "Click again to confirm" : "Clear store. feed"}>
|
||||
<Trash size={14} weight="light" />
|
||||
{#if confirmClear}<span class="clear-label">Confirm?</span>{/if}
|
||||
</button>
|
||||
@@ -117,46 +129,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Persistent stats bar — never cleared ────────────────────────────────── -->
|
||||
{#if $readingStats.totalChaptersRead > 0}
|
||||
{#if store..totalChaptersRead > 0}
|
||||
<div class="stats-bar">
|
||||
<div class="stat-group">
|
||||
<Fire size={13} weight="fill" class="stat-fire" />
|
||||
<span class="stat-val accent">{$readingStats.currentStreakDays}</span>
|
||||
<span class="stat-val accent">{store..currentStreakDays}</span>
|
||||
<span class="stat-label">day streak</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-group">
|
||||
<BookOpen size={13} weight="light" class="stat-icon-neutral" />
|
||||
<span class="stat-val">{$readingStats.totalChaptersRead}</span>
|
||||
<span class="stat-val">{store..totalChaptersRead}</span>
|
||||
<span class="stat-label">chapters</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-group">
|
||||
<Clock size={13} weight="light" class="stat-icon-neutral" />
|
||||
<span class="stat-val">{formatReadTime($readingStats.totalMinutesRead)}</span>
|
||||
<span class="stat-val">{formatReadTime(store..totalMinutesRead)}</span>
|
||||
<span class="stat-label">read time</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-group">
|
||||
<TrendUp size={13} weight="light" class="stat-icon-neutral" />
|
||||
<span class="stat-val">{$readingStats.totalMangaRead}</span>
|
||||
<span class="stat-val">{store..totalMangaRead}</span>
|
||||
<span class="stat-label">series</span>
|
||||
</div>
|
||||
<div class="stat-sep"></div>
|
||||
<div class="stat-group">
|
||||
<span class="stat-val muted">{$readingStats.longestStreakDays}d</span>
|
||||
<span class="stat-val muted">{store..longestStreakDays}d</span>
|
||||
<span class="stat-label">best streak</span>
|
||||
</div>
|
||||
<span class="stats-note">Stats are preserved when you clear the feed</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- ── Empty states ────────────────────────────────────────────────────────── -->
|
||||
{#if $history.length === 0}
|
||||
{#if store..length === 0}
|
||||
<div class="empty">
|
||||
<ClockCounterClockwise size={32} weight="light" class="empty-icon" />
|
||||
<p class="empty-text">No reading history</p>
|
||||
<p class="empty-text">No reading store.</p>
|
||||
<p class="empty-hint">Chapters you read will appear here</p>
|
||||
</div>
|
||||
{:else if sessions.length === 0}
|
||||
@@ -164,8 +174,6 @@
|
||||
<Books size={28} weight="light" class="empty-icon" />
|
||||
<p class="empty-text">No results for "{search}"</p>
|
||||
</div>
|
||||
|
||||
<!-- ── Timeline ────────────────────────────────────────────────────────────── -->
|
||||
{:else}
|
||||
<div class="timeline">
|
||||
{#each groups as { label, items }}
|
||||
@@ -176,17 +184,13 @@
|
||||
</div>
|
||||
<div class="session-list">
|
||||
{#each items as session (session.latestChapterId)}
|
||||
<button class="session-row" on:click={() => resume(session)}>
|
||||
|
||||
<!-- Cover -->
|
||||
<button class="session-row" onclick={() => resume(session)}>
|
||||
<div class="thumb-wrap">
|
||||
<img src={thumbUrl(session.thumbnailUrl)} alt={session.mangaTitle} class="thumb" />
|
||||
{#if session.chapterCount > 1}
|
||||
<span class="session-count">{session.chapterCount}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="session-info">
|
||||
<span class="session-title">{session.mangaTitle}</span>
|
||||
<span class="session-chapter">
|
||||
@@ -202,13 +206,10 @@
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Time + play -->
|
||||
<span class="session-time">{timeAgo(session.readAt)}</span>
|
||||
<div class="play-pill">
|
||||
<Play size={10} weight="fill" /> Resume
|
||||
</div>
|
||||
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -222,7 +223,6 @@
|
||||
<style>
|
||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; animation: fadeIn 0.14s ease both; }
|
||||
|
||||
/* ── Header ──────────────────────────────────────────────────────────────── */
|
||||
.page-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0;
|
||||
@@ -250,7 +250,6 @@
|
||||
.clear-btn.confirm { color: var(--color-error); background: var(--color-error-bg); border-color: var(--color-error); }
|
||||
.clear-label { font-size: var(--text-2xs); }
|
||||
|
||||
/* ── Stats bar — persisted, never clears ────────────────────────────────── */
|
||||
.stats-bar {
|
||||
display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
|
||||
padding: var(--sp-3) var(--sp-6); border-bottom: 1px solid var(--border-dim);
|
||||
@@ -266,7 +265,6 @@
|
||||
.stat-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
.stats-note { margin-left: auto; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); opacity: 0.5; letter-spacing: var(--tracking-wide); font-style: italic; }
|
||||
|
||||
/* ── Timeline ────────────────────────────────────────────────────────────── */
|
||||
.timeline { flex: 1; overflow-y: auto; padding: var(--sp-4) var(--sp-6); }
|
||||
|
||||
.day-group { margin-bottom: var(--sp-5); }
|
||||
@@ -285,7 +283,6 @@
|
||||
.session-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
|
||||
.session-row:hover .play-pill { opacity: 1; transform: translateX(0); }
|
||||
|
||||
/* Thumb */
|
||||
.thumb-wrap { position: relative; flex-shrink: 0; }
|
||||
.thumb { width: 38px; height: 54px; border-radius: var(--radius-sm); object-fit: cover; display: block; background: var(--bg-raised); border: 1px solid var(--border-dim); }
|
||||
.session-count {
|
||||
@@ -295,14 +292,12 @@
|
||||
padding: 1px 4px; border-radius: 6px; line-height: 1.4; pointer-events: none;
|
||||
}
|
||||
|
||||
/* Info */
|
||||
.session-info { flex: 1; display: flex; flex-direction: column; gap: 3px; overflow: hidden; min-width: 0; }
|
||||
.session-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.session-chapter { font-size: var(--text-xs); color: var(--text-muted); display: flex; align-items: center; gap: var(--sp-1); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ch-arrow { color: var(--text-faint); font-size: 10px; flex-shrink: 0; }
|
||||
.ch-page { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
|
||||
|
||||
/* Time & play */
|
||||
.session-time { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; white-space: nowrap; }
|
||||
.play-pill {
|
||||
display: flex; align-items: center; gap: 4px; flex-shrink: 0;
|
||||
@@ -313,7 +308,6 @@
|
||||
transition: opacity var(--t-base), transform var(--t-base);
|
||||
}
|
||||
|
||||
/* ── Empty ───────────────────────────────────────────────────────────────── */
|
||||
.empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2); }
|
||||
:global(.empty-icon) { color: var(--text-faint); }
|
||||
.empty-text { font-size: var(--text-base); color: var(--text-muted); }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { House, Books, MagnifyingGlass, ClockCounterClockwise, Compass, DownloadSimple, PuzzlePiece, GearSix } from "phosphor-svelte";
|
||||
import { navPage, activeManga, activeSource, libraryFilter, genreFilter, settingsOpen } from "../../store";
|
||||
import type { NavPage } from "../../store";
|
||||
import { store, setNavPage, setActiveManga, setActiveSource, setLibraryFilter, setGenreFilter, setSettingsOpen } from "../../store/state.svelte";
|
||||
import type { NavPage } from "../../store/state.svelte";
|
||||
|
||||
const TABS: { id: NavPage; label: string; icon: any }[] = [
|
||||
{ id: "home", label: "Home", icon: House },
|
||||
@@ -14,18 +14,18 @@
|
||||
];
|
||||
|
||||
function navigate(id: NavPage) {
|
||||
navPage = id;
|
||||
activeManga = null;
|
||||
genreFilter = "";
|
||||
if (id !== "explore") activeSource = null;
|
||||
store.navPage = id;
|
||||
store.activeManga = null;
|
||||
store.genreFilter = "";
|
||||
if (id !== "explore") store.activeSource = null;
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
navPage = "home";
|
||||
activeSource = null;
|
||||
activeManga = null;
|
||||
libraryFilter = "library";
|
||||
genreFilter = "";
|
||||
store.navPage = "home";
|
||||
store.activeSource = null;
|
||||
store.activeManga = null;
|
||||
store.libraryFilter = "library";
|
||||
store.genreFilter = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -35,14 +35,14 @@
|
||||
</button>
|
||||
<nav class="nav">
|
||||
{#each TABS as tab}
|
||||
<button class="tab" class:active={navPage === tab.id}
|
||||
<button class="tab" class:active={store.navPage === tab.id}
|
||||
title={tab.label} onclick={() => navigate(tab.id)}>
|
||||
<tab.icon size={18} weight="light" />
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="bottom">
|
||||
<button class="settings-btn" onclick={() => settingsOpen = true} title="Settings">
|
||||
<button class="settings-btn" onclick={() => store.settingsOpen = true} title="Settings">
|
||||
<GearSix size={18} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
let exiting = $state(false);
|
||||
let exitLock = false;
|
||||
|
||||
let fpsEl: HTMLSpanElement;
|
||||
let fpsEl = $state<HTMLSpanElement | undefined>(undefined);
|
||||
|
||||
function triggerExit(cb?: () => void) {
|
||||
if (exitLock) return;
|
||||
|
||||
@@ -6,17 +6,17 @@
|
||||
<div class="bar" data-tauri-drag-region>
|
||||
<span class="title" data-tauri-drag-region>Moku</span>
|
||||
<div class="controls">
|
||||
<button on:click={() => win.minimize()} title="Minimize" aria-label="Minimize">
|
||||
<button onclick={() => win.minimize()} title="Minimize" aria-label="Minimize">
|
||||
<svg width="10" height="1" viewBox="0 0 10 1">
|
||||
<line x1="0" y1="0.5" x2="10" y2="0.5" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button on:click={() => win.toggleMaximize()} title="Maximize" aria-label="Maximize">
|
||||
<button onclick={() => win.toggleMaximize()} title="Maximize" aria-label="Maximize">
|
||||
<svg width="9" height="9" viewBox="0 0 9 9">
|
||||
<rect x="0.75" y="0.75" width="7.5" height="7.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="close" on:click={() => win.close()} title="Close" aria-label="Close">
|
||||
<button class="close" onclick={() => win.close()} title="Close" aria-label="Close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from "svelte";
|
||||
import { toasts, dismissToast } from "../../store";
|
||||
import type { Toast } from "../../store";
|
||||
import { store, dismissToast } from "../../store/state.svelte";
|
||||
import type { Toast } from "../../store/state.svelte";
|
||||
|
||||
const timers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
@@ -12,9 +11,10 @@
|
||||
timers.set(t.id, setTimeout(() => dismissToast(t.id), dur));
|
||||
}
|
||||
|
||||
$: $toasts.forEach(schedule);
|
||||
|
||||
onDestroy(() => timers.forEach(clearTimeout));
|
||||
$effect(() => {
|
||||
store.toasts.forEach(schedule);
|
||||
return () => timers.forEach(clearTimeout);
|
||||
});
|
||||
|
||||
const icons: Record<Toast["kind"], string> = {
|
||||
success: "M9 12l2 2 4-4M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z",
|
||||
@@ -24,9 +24,9 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if $toasts.length}
|
||||
{#if store.toasts.length}
|
||||
<div class="toaster" aria-live="polite">
|
||||
{#each $toasts as t (t.id)}
|
||||
{#each store.toasts as t (t.id)}
|
||||
<div class="toast toast-{t.kind}" role="alert">
|
||||
<span class="icon">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
||||
@@ -38,7 +38,7 @@
|
||||
<p class="title">{t.title}</p>
|
||||
{#if t.body}<p class="sub">{t.body}</p>{/if}
|
||||
</div>
|
||||
<button class="close" on:click={() => dismissToast(t.id)} title="Dismiss">
|
||||
<button class="close" onclick={() => dismissToast(t.id)} title="Dismiss">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
|
||||
Reference in New Issue
Block a user