Updated design

This commit is contained in:
2026-07-19 09:54:35 +02:00
parent e3a735acda
commit 804e7f90d1
36 changed files with 403 additions and 328 deletions
+1 -1
View File
@@ -6,5 +6,5 @@
<File Path="midrandbooks-uat.yml" />
<File Path="README.md" />
</Folder>
<Project Path="MidrandBookshop/MidrandBookshop.csproj" />
<Project Path="MidrandBooks/MidrandBooks.csproj" />
</Solution>
@@ -1,12 +1,38 @@
@namespace MidrandBooks.Components
<div id="animated-bg" class="fixed inset-0 -z-50 overflow-hidden bg-[#FDFCFB]">
<style>
.mesh-bg {
background-image: radial-gradient(at 0% 0%, rgba(245, 158, 11, 0.08) 0, transparent 60%),
radial-gradient(at 50% 0%, rgba(20, 184, 166, 0.06) 0, transparent 60%),
radial-gradient(at 100% 0%, rgba(249, 115, 22, 0.08) 0, transparent 60%);
background-color: #030712;
}
@@keyframes float-up {
0% {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
10% {
opacity: var(--float-opacity, 0.2);
}
90% {
opacity: var(--float-opacity, 0.2);
}
100% {
transform: translateY(-20vh) rotate(360deg);
opacity: 0;
}
}
</style>
<div id="animated-bg" class="fixed inset-0 -z-50 overflow-hidden mesh-bg">
<!-- Abstract Elegant Soft Warm Gradients -->
<div class="absolute -top-[30%] -left-[10%] h-[80%] w-[60%] rounded-full bg-amber-500/5 blur-[120px] animate-pulse"
style="animation-duration: 14s;" />
<div class="absolute -bottom-[20%] -right-[5%] h-[80%] w-[50%] rounded-full bg-violet-500/5 blur-[150px] animate-pulse"
<div class="absolute -bottom-[20%] -right-[5%] h-[80%] w-[50%] rounded-full bg-sky-500/4 blur-[150px] animate-pulse"
style="animation-duration: 20s;" />
<div class="absolute top-[40%] left-[30%] h-[300px] w-[300px] rounded-full bg-indigo-500/3 blur-[100px] animate-pulse"
<div class="absolute top-[40%] left-[30%] h-[300px] w-[300px] rounded-full bg-orange-400/4 blur-[100px] animate-pulse"
style="animation-duration: 16s;" />
<!-- Grid Pattern Overlay -->
@@ -117,7 +117,7 @@
<!-- Seek Bar -->
<div class="space-y-1">
<input type="range" min="0" max="@Duration" value="@Progress" @oninput="SeekChanged" class="w-full h-1 bg-white/10 rounded-lg appearance-none cursor-pointer" />
<input type="range" min="0" max="@Duration" value="@Progress" @oninput="SeekChanged" class="w-full h-1.5 bg-slate-700 accent-amber-500 rounded-lg appearance-none cursor-pointer transition-colors" />
<div class="flex justify-between text-[10px] font-mono text-slate-400">
<span>@FormatSeconds(Progress)</span>
<span>@FormatSeconds(Duration)</span>
@@ -171,7 +171,7 @@
</svg>
}
</button>
<input type="range" min="0" max="1" step="0.05" @bind="Volume" class="w-20 h-1 bg-white/10 rounded-lg appearance-none cursor-pointer" />
<input type="range" min="0" max="1" step="0.05" @bind="Volume" class="w-20 h-1.5 bg-slate-700 accent-amber-500 rounded-lg appearance-none cursor-pointer transition-colors" />
</div>
</div>
@@ -2,6 +2,16 @@
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
<style>
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
<div id="razor-book-grid" class="max-w-7xl mx-auto px-4 sm:px-6 py-6 space-y-6">
<!-- Filters Rail -->
<div class="flex flex-col md:flex-row gap-4 items-stretch md:items-center justify-between border-b border-slate-200/50 pb-5">
@@ -5,10 +5,10 @@
@if (IsOpen)
{
<div class="fixed inset-0 z-50 overflow-hidden bg-slate-950/60 backdrop-blur-sm flex justify-end">
<!-- Cart Drawer Panel -->
<div class="w-full max-w-md bg-white border-l border-slate-200 h-full shadow-2xl flex flex-col justify-between relative animate-in slide-in-from-right duration-200">
<!-- Header -->
<header class="p-5 border-b border-slate-100 flex items-center justify-between">
<div class="flex items-center gap-2">
@@ -275,7 +275,7 @@
private string CurrentStep { get; set; } = "cart"; // "cart", "details", "processing", "otp", "success"
private string PaymentMethod { get; set; } = "card"; // "card", "eft"
// Processing parameters
private string ProcessingStatus { get; set; } = string.Empty;
private int ProcessingProgress { get; set; } = 0;
@@ -329,7 +329,7 @@
CurrentStep = "processing";
ProcessingProgress = 15;
ProcessingStatus = "Establishing secure proxy to South African gateway nodes...";
await Task.Delay(1000);
ProcessingProgress = 45;
ProcessingStatus = "Securing socket with SSL 256-bit encryption...";
@@ -2,6 +2,18 @@
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
<style>
.reader-text-serif {
font-family: "Playfair Display", serif;
}
.reader-text-sans {
font-family: "Poppins", sans-serif;
}
.reader-text-mono {
font-family: "JetBrains Mono", monospace;
}
</style>
<div class="fixed inset-0 z-50 flex flex-col h-screen overflow-hidden @ThemeBgClass()">
<!-- Header Controls -->
@@ -47,7 +59,7 @@
<label class="text-[10px] font-mono font-bold uppercase block opacity-70">Theme</label>
<div class="flex bg-black/10 dark:bg-white/10 p-1 rounded-xl">
<button @onclick='() => SetTheme("sepia")' class="flex-1 py-1 text-xs font-bold rounded-lg @(Theme == "sepia" ? "bg-amber-100 text-amber-950" : "text-slate-500")">Sepia</button>
<button @onclick='() => SetTheme("dark")' class="flex-1 py-1 text-xs font-bold rounded-lg @(Theme == "dark" ? "bg-slate-850 text-white" : "text-slate-500")">Dark</button>
<button @onclick='() => SetTheme("dark")' class="flex-1 py-1 text-xs font-bold rounded-lg @(Theme == "dark" ? "bg-slate-800 text-white" : "text-slate-500")">Dark</button>
<button @onclick='() => SetTheme("light")' class="flex-1 py-1 text-xs font-bold rounded-lg @(Theme == "light" ? "bg-white text-slate-800" : "text-slate-500")">Light</button>
</div>
</div>
@@ -68,7 +80,7 @@
<span>Font Size</span>
<span>@(FontSize)px</span>
</div>
<input type="range" min="12" max="24" @bind="FontSize" class="w-full h-1 bg-slate-200 rounded-lg appearance-none cursor-pointer" />
<input type="range" min="12" max="24" @bind="FontSize" class="w-full h-1.5 bg-slate-300 dark:bg-slate-700 accent-amber-500 rounded-lg appearance-none cursor-pointer transition-colors" />
</div>
</div>
@@ -192,9 +204,9 @@
{
return Font switch
{
"sans" => "font-sans",
"mono" => "font-mono",
_ => "font-serif"
"sans" => "reader-text-sans",
"mono" => "reader-text-mono",
_ => "reader-text-serif"
};
}
@@ -26,7 +26,7 @@
var audioInCart = Cart.Any(i => i.Book.Id == book.Id && i.Format == "audiobook");
<!-- Main Carousel Card -->
<div class="relative overflow-hidden rounded-3xl border border-slate-800/40 bg-slate-900 shadow-2xl min-h-[480px] md:min-h-[380px] flex items-center">
<div class="relative overflow-hidden rounded-3xl border border-slate-800/80 bg-slate-900/95 backdrop-blur-md shadow-lg shadow-slate-950/25 min-h-[480px] md:min-h-[380px] flex items-center">
<!-- Dynamic Background Glow -->
<div class="absolute inset-0 bg-gradient-to-br @book.CoverColor opacity-25 blur-2xl scale-125 transition-all duration-1000 -z-10" />
@@ -1,5 +1,5 @@
@namespace MidrandBooks.Components.Layout
@using MidrandBooks.Components.Pages
@using Microsoft.AspNetCore.Components
@inject IJSRuntime JSRuntime
@if (ShowBanner || ShowPrivacyModal)
@@ -7,13 +7,13 @@
@if (ShowBanner)
{
<div id="cookie-acceptance-banner"
class="fixed bottom-4 left-4 z-40 max-w-sm w-[calc(100vw-2rem)] sm:w-[380px] bg-slate-900/95 backdrop-blur-md border border-white/10 rounded-2xl p-4 shadow-2xl flex flex-col gap-3 animate-in slide-in-from-bottom-8 fade-in duration-500">
class="fixed bottom-4 left-4 z-40 max-w-sm w-[calc(100vw-2rem)] sm:w-[380px] bg-white/90 backdrop-blur-xl border border-slate-200/80 rounded-2xl p-4 shadow-xl flex flex-col gap-3 animate-in slide-in-from-bottom-8 fade-in duration-500 hover:shadow-slate-200/50 transition-all">
<!-- Accent Line -->
<div class="absolute top-0 left-6 right-6 h-0.5 bg-gradient-to-r from-violet-500/0 via-violet-500/50 to-violet-500/0"></div>
<div class="absolute top-0 left-6 right-6 h-0.5 bg-gradient-to-r from-amber-500/0 via-amber-500/60 to-amber-500/0"></div>
<div class="flex items-start gap-3">
<!-- Visual Icon -->
<div class="h-9 w-9 rounded-xl bg-violet-500/10 border border-violet-500/20 text-violet-400 flex items-center justify-center flex-shrink-0">
<div class="h-9 w-9 rounded-xl bg-amber-50 border border-amber-200/60 text-amber-600 flex items-center justify-center flex-shrink-0">
<svg class="h-5 w-5 animate-pulse" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5" />
@@ -26,16 +26,16 @@
</div>
<div class="space-y-1">
<h4 class="text-xs font-mono font-bold uppercase tracking-wider text-white">
<h4 class="text-xs font-sans font-bold text-slate-800 tracking-tight">
Cookie &amp; POPIA Protocol
</h4>
<p class="text-[11px] text-slate-300 leading-relaxed font-medium">
We utilize essential identifiers to facilitate secure electronic catalog licensing, sandboxed media playbacks, and cryptographic PayFast gateway handshake handoffs under <strong class="text-violet-300">Lite Charms (Pty) Ltd</strong>.
<p class="text-[11px] text-slate-600 leading-relaxed font-medium">
We utilize essential identifiers to facilitate secure electronic catalog licensing, sandboxed media playbacks, and cryptographic PayFast gateway handshake handoffs under <strong class="text-amber-600 font-bold">Lite Charms (Pty) Ltd</strong>.
</p>
</div>
<button @onclick="DismissBanner"
class="text-slate-400 hover:text-white transition-colors cursor-pointer p-0.5"
class="text-slate-400 hover:text-slate-600 transition-colors cursor-pointer p-0.5"
aria-label="Dismiss banner">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -44,9 +44,9 @@
</div>
<!-- Action Links & Buttons -->
<div class="flex items-center justify-between mt-1 pt-2.5 border-t border-white/5">
<div class="flex items-center justify-between mt-1 pt-2.5 border-t border-slate-100">
<button @onclick="OpenPrivacyModal"
class="text-[11px] font-semibold text-violet-400 hover:text-violet-300 underline cursor-pointer flex items-center gap-1 focus:outline-none">
class="text-[11px] font-semibold text-amber-600 hover:text-amber-700 underline cursor-pointer flex items-center gap-1 focus:outline-none">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
@@ -55,7 +55,7 @@
<div class="flex items-center gap-2">
<button @onclick="AcceptCookies"
class="bg-violet-600 hover:bg-violet-500 text-white rounded-xl py-1.5 px-3.5 text-[10px] font-mono font-bold uppercase tracking-wider transition-all cursor-pointer shadow-lg shadow-violet-600/15 hover:scale-[1.02] active:scale-[0.98]">
class="bg-slate-900 hover:bg-slate-800 text-white rounded-xl py-1.5 px-3.5 text-[10px] font-mono font-bold uppercase tracking-wider transition-all cursor-pointer shadow shadow-slate-900/10 hover:scale-[1.02] active:scale-[0.98]">
Accept All
</button>
</div>
@@ -70,12 +70,12 @@
<div id="cookie-privacy-modal-content"
class="relative w-full max-w-5xl bg-slate-950 border border-white/10 rounded-3xl shadow-2xl overflow-hidden max-h-[90vh] flex flex-col animate-in zoom-in-95 duration-200">
<!-- Upper Highlight Bar -->
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-violet-500 via-indigo-500 to-violet-500" />
<div class="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-amber-500 via-amber-400 to-amber-600" />
<!-- Modal Header -->
<div class="flex items-center justify-between px-6 py-4 border-b border-white/10 bg-slate-900/40 relative z-10 flex-shrink-0">
<div class="flex items-center gap-2 text-white">
<svg class="h-5 w-5 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<svg class="h-5 w-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span class="font-mono text-xs font-bold uppercase tracking-wider text-slate-300">
@@ -94,7 +94,7 @@
<!-- Modal Scrollable Body -->
<div class="flex-1 overflow-y-auto bg-slate-950/50 p-4 sm:p-6 md:p-8">
<div class="max-w-4xl mx-auto -my-10">
<Privacy />
<MidrandBooks.Components.Privacy />
</div>
</div>
@@ -2,7 +2,6 @@
@namespace MidrandBooks.Components.Layout
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Components
@using MidrandBooks.Components.Pages
@using MidrandBooks.Models
<div class="relative min-h-screen flex flex-col justify-between selection:bg-violet-500/10 selection:text-violet-700">
@@ -1,4 +1,4 @@
@namespace MidrandBooks.Components.Layout
@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
@@ -0,0 +1,205 @@
@namespace MidrandBooks.Components.Layout
@using Microsoft.AspNetCore.Components
@inject IJSRuntime JSRuntime
<dialog id="components-reconnect-modal" data-nosnippet class="fixed inset-0 w-screen h-screen m-0 p-0 border-0 bg-transparent z-[99999] overflow-hidden flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300">
<!-- Dark Frosted Jacket Layer -->
<div class="absolute inset-0 bg-slate-950/60 backdrop-blur-md cursor-wait z-[1] animate-in fade-in duration-300"></div>
<!-- Centered Content Wrapper -->
<div class="reconnect-content relative z-[2] flex flex-col items-center gap-6 text-center text-slate-100 max-w-sm px-6">
<!-- Waveform Animation -->
<svg class="waveform-svg text-violet-400" viewBox="0 0 54 50" width="60" height="60" xmlns="http://www.w3.org/2000/svg">
<rect class="bar bar-1" x="2" y="15" width="4" height="20" rx="2" fill="currentColor" />
<rect class="bar bar-2" x="14" y="5" width="4" height="40" rx="2" fill="currentColor" />
<rect class="bar bar-3" x="26" y="20" width="4" height="10" rx="2" fill="currentColor" />
<rect class="bar bar-4" x="38" y="10" width="4" height="30" rx="2" fill="currentColor" />
<rect class="bar bar-5" x="50" y="18" width="4" height="14" rx="2" fill="currentColor" />
</svg>
<!-- Message Body -->
<div class="sync-message-body font-mono uppercase text-xs tracking-widest text-slate-300 space-y-2">
<span class="components-reconnect-first-attempt-visible hidden">
Losing signal...
</span>
<span class="components-reconnect-repeated-attempt-visible hidden">
reconnecting in <span id="components-seconds-to-next-attempt" class="font-bold text-violet-400">0</span>s...
</span>
<span class="components-reconnect-failed-visible text-rose-400 font-semibold hidden">
Playback disconnected.
</span>
<span class="components-pause-visible hidden">
Reading layout paused.
</span>
<span class="components-resume-failed-visible text-rose-400 font-semibold hidden">
State alignment broken.
</span>
</div>
<!-- Glass Action Buttons -->
<div class="sync-action-node flex items-center justify-center gap-3">
<button id="components-reconnect-button" class="btn-glass-action hidden px-4 py-2.5 bg-white/5 border border-white/10 hover:bg-white/10 text-white font-mono uppercase text-[10px] tracking-widest rounded-xl transition-all items-center gap-2 cursor-pointer shadow-lg hover:scale-102 hover:border-violet-500/30">
<span>Retry Sync</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67" />
</svg>
</button>
<button id="components-resume-button" class="btn-glass-action hidden px-4 py-2.5 bg-white/5 border border-white/10 hover:bg-white/10 text-white font-mono uppercase text-[10px] tracking-widest rounded-xl transition-all items-center gap-2 cursor-pointer shadow-lg hover:scale-102 hover:border-violet-500/30">
<span>Reload Session</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
</div>
</div>
</dialog>
<style>
/* --- Show the modal smoothly when Blazor attaches state classes --- */
#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
opacity: 1 !important;
pointer-events: all !important;
}
#components-reconnect-modal.components-reconnect-hide {
display: none !important;
}
/* --- SVG Waveform Animation --- */
#components-reconnect-modal .bar {
transform-origin: center;
animation: reconnectWavePulse 1.2s ease-in-out infinite alternate;
}
#components-reconnect-modal .bar-1 { animation-delay: 0.0s; }
#components-reconnect-modal .bar-2 { animation-delay: 0.3s; }
#components-reconnect-modal .bar-3 { animation-delay: 0.6s; }
#components-reconnect-modal .bar-4 { animation-delay: 0.2s; }
#components-reconnect-modal .bar-5 { animation-delay: 0.5s; }
@@keyframes reconnectWavePulse {
0% {
transform: scaleY(0.3);
opacity: 0.5;
}
100% {
transform: scaleY(1);
opacity: 1;
}
}
/* Stop the waveform if the connection completely fails */
#components-reconnect-modal.components-reconnect-failed .bar,
#components-reconnect-modal.components-reconnect-rejected .bar,
#components-reconnect-modal.components-reconnect-paused .bar {
animation: none !important;
transform: scaleY(0.1) !important;
opacity: 0.3 !important;
}
/* --- Display Mechanics Matrix Controllers --- */
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: inline-block !important;
}
#components-reconnect-modal.components-reconnect-failed .btn-glass-action,
#components-reconnect-modal.components-reconnect-paused .btn-glass-action,
#components-reconnect-modal.components-reconnect-resume-failed .btn-glass-action {
display: inline-flex !important;
}
</style>
<script>
(function() {
function initReconnectModal() {
const reconnectModal = document.getElementById("components-reconnect-modal");
if (!reconnectModal) {
setTimeout(initReconnectModal, 100);
return;
}
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
if (retryButton) {
retryButton.addEventListener("click", retry);
}
const resumeButton = document.getElementById("components-resume-button");
if (resumeButton) {
resumeButton.addEventListener("click", resume);
}
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
if (window.Blazor) {
const successful = await window.Blazor.reconnect();
if (!successful) {
const resumeSuccessful = await window.Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
}
} catch (err) {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
if (window.Blazor) {
const successful = await window.Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initReconnectModal);
} else {
initReconnectModal();
}
})();
</script>
@@ -1,4 +1,4 @@
@namespace MidrandBooks.Components.Pages
@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
@@ -1,4 +1,4 @@
@namespace MidrandBooks.Components.Pages
@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
<div id="error-razor" class="w-full max-w-xl mx-auto px-4 py-16 text-center space-y-6">
@@ -3,7 +3,7 @@
@using MidrandBooks.Models
<div id="library-view-razor" class="w-full max-w-7xl mx-auto px-4 sm:px-6 my-10 space-y-8 min-h-[500px]">
<!-- Header Section -->
<div class="flex flex-col sm:flex-row sm:items-end justify-between gap-4 border-b border-slate-100 pb-4">
<div>
@@ -22,8 +22,9 @@
<div class="flex bg-slate-100 border border-slate-200/60 p-1 rounded-xl self-start sm:self-auto">
@foreach (var format in Formats)
{
<button @onclick="() => FilterByFormat(format)"
class="px-4 py-1.5 rounded-lg text-xs font-semibold uppercase tracking-wider transition-all cursor-pointer @(ActiveFormat == format ? "bg-white text-slate-900 shadow-sm border border-slate-200/50" : "text-slate-500 hover:text-slate-800")">
<button
@onclick="() => FilterByFormat(format)"
class="px-4 py-1.5 rounded-lg text-xs font-semibold uppercase tracking-wider transition-all cursor-pointer @(ActiveFormat == format ? "bg-white text-slate-900 shadow-sm border border-slate-200/50" : "text-slate-500 hover:text-slate-800")">
@format
</button>
}
@@ -42,7 +43,7 @@
var isDownloading = DownloadingBookId == book.Id;
<div class="group relative flex gap-5 rounded-2xl p-4 border border-slate-100 bg-white/70 backdrop-blur-md shadow-sm hover:shadow-md transition-all">
<!-- Book Cover -->
<div @onclick="() => HandleSelectBook(book)" class="relative h-36 w-24 rounded-xl overflow-hidden shadow-sm bg-slate-100 flex-shrink-0 cursor-pointer">
@if (!string.IsNullOrEmpty(book.CoverUrl))
@@ -67,7 +68,7 @@
<span class="inline-block text-[8px] font-mono font-extrabold uppercase rounded px-1.5 py-0.5 @(isEbook ? "bg-indigo-50 text-indigo-700" : "bg-violet-50 text-violet-700")">
@item.Format
</span>
<!-- Offline Cache Trigger Button -->
@if (isEbook)
{
@@ -180,9 +181,9 @@
var matchedBook = Books.FirstOrDefault(b => b.Id == purchase.BookId);
if (matchedBook != null)
{
items.Add(new LibraryItem
{
Book = matchedBook,
items.Add(new LibraryItem
{
Book = matchedBook,
Format = purchase.Format,
CurrentPage = purchase.CurrentPage,
Progress = purchase.Progress
@@ -198,10 +199,10 @@
{
if (DownloadingBookId != null) return;
DownloadingBookId = bookId;
await Task.Delay(1500); // Simulate network latency
await OnCacheBookOffline.InvokeAsync(bookId);
DownloadingBookId = null;
}
@@ -1,4 +1,4 @@
@namespace MidrandBooks.Components.Pages
@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
<div id="notfound-razor" class="w-full max-w-xl mx-auto px-4 py-16 text-center space-y-6">
@@ -1,5 +1,5 @@
@page "/privacy"
@namespace MidrandBooks.Components.Pages
@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@inject IJSRuntime JSRuntime
@@ -1,5 +1,5 @@
@page "/terms"
@namespace MidrandBooks.Components.Pages
@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@inject IJSRuntime JSRuntime
@@ -15,6 +15,7 @@
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-800: oklch(44.4% 0.177 26.899);
--color-orange-400: oklch(75% 0.183 55.934);
--color-orange-900: oklch(40.8% 0.123 38.172);
--color-amber-50: oklch(98.7% 0.022 95.277);
--color-amber-100: oklch(96.2% 0.059 95.617);
@@ -34,11 +35,11 @@
--color-emerald-700: oklch(50.8% 0.118 165.612);
--color-emerald-950: oklch(26.2% 0.051 172.552);
--color-teal-950: oklch(27.7% 0.046 192.524);
--color-sky-500: oklch(68.5% 0.169 237.323);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-indigo-50: oklch(96.2% 0.018 272.314);
--color-indigo-500: oklch(58.5% 0.233 277.117);
--color-indigo-600: oklch(51.1% 0.262 276.966);
--color-indigo-700: oklch(45.7% 0.24 277.023);
--color-indigo-900: oklch(35.9% 0.144 278.697);
@@ -1035,6 +1036,12 @@
border-color: color-mix(in oklab, var(--color-amber-200) 50%, transparent);
}
}
.border-amber-200\/60 {
border-color: color-mix(in srgb, oklch(92.4% 0.12 95.746) 60%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-amber-200) 60%, transparent);
}
}
.border-amber-400 {
border-color: var(--color-amber-400);
}
@@ -1125,6 +1132,12 @@
border-color: color-mix(in oklab, var(--color-slate-800) 40%, transparent);
}
}
.border-slate-800\/80 {
border-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 80%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-slate-800) 80%, transparent);
}
}
.border-slate-900\/10 {
border-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1149,12 +1162,6 @@
border-color: color-mix(in oklab, var(--color-violet-500) 10%, transparent);
}
}
.border-violet-500\/20 {
border-color: color-mix(in srgb, oklch(60.6% 0.25 292.717) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-violet-500) 20%, transparent);
}
}
.border-violet-500\/30 {
border-color: color-mix(in srgb, oklch(60.6% 0.25 292.717) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1188,9 +1195,6 @@
.border-t-violet-600 {
border-top-color: var(--color-violet-600);
}
.bg-\[\#FDFCFB\] {
background-color: #FDFCFB;
}
.bg-amber-50 {
background-color: var(--color-amber-50);
}
@@ -1248,18 +1252,18 @@
.bg-indigo-50 {
background-color: var(--color-indigo-50);
}
.bg-indigo-500\/3 {
background-color: color-mix(in srgb, oklch(58.5% 0.233 277.117) 3%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-indigo-500) 3%, transparent);
}
}
.bg-indigo-600\/10 {
background-color: color-mix(in srgb, oklch(51.1% 0.262 276.966) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-indigo-600) 10%, transparent);
}
}
.bg-orange-400\/4 {
background-color: color-mix(in srgb, oklch(75% 0.183 55.934) 4%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-orange-400) 4%, transparent);
}
}
.bg-red-100 {
background-color: var(--color-red-100);
}
@@ -1281,6 +1285,12 @@
.bg-rose-600 {
background-color: var(--color-rose-600);
}
.bg-sky-500\/4 {
background-color: color-mix(in srgb, oklch(68.5% 0.169 237.323) 4%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-sky-500) 4%, transparent);
}
}
.bg-slate-50 {
background-color: var(--color-slate-50);
}
@@ -1302,6 +1312,15 @@
.bg-slate-200 {
background-color: var(--color-slate-200);
}
.bg-slate-300 {
background-color: var(--color-slate-300);
}
.bg-slate-700 {
background-color: var(--color-slate-700);
}
.bg-slate-800 {
background-color: var(--color-slate-800);
}
.bg-slate-800\/60 {
background-color: color-mix(in srgb, oklch(27.9% 0.041 260.031) 60%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1371,18 +1390,6 @@
.bg-violet-100 {
background-color: var(--color-violet-100);
}
.bg-violet-500\/5 {
background-color: color-mix(in srgb, oklch(60.6% 0.25 292.717) 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-violet-500) 5%, transparent);
}
}
.bg-violet-500\/10 {
background-color: color-mix(in srgb, oklch(60.6% 0.25 292.717) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-violet-500) 10%, transparent);
}
}
.bg-violet-500\/20 {
background-color: color-mix(in srgb, oklch(60.6% 0.25 292.717) 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1437,6 +1444,12 @@
background-color: color-mix(in oklab, var(--color-white) 70%, transparent);
}
}
.bg-white\/90 {
background-color: color-mix(in srgb, #fff 90%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 90%, transparent);
}
}
.bg-white\/95 {
background-color: color-mix(in srgb, #fff 95%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1455,6 +1468,17 @@
--tw-gradient-position: to top right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-amber-500 {
--tw-gradient-from: var(--color-amber-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-amber-500\/0 {
--tw-gradient-from: color-mix(in srgb, oklch(76.9% 0.188 70.08) 0%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-from: color-mix(in oklab, var(--color-amber-500) 0%, transparent);
}
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-amber-950 {
--tw-gradient-from: var(--color-amber-950);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -1492,16 +1516,18 @@
--tw-gradient-from: var(--color-slate-900);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-violet-500 {
--tw-gradient-from: var(--color-violet-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
.via-amber-400 {
--tw-gradient-via: var(--color-amber-400);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.from-violet-500\/0 {
--tw-gradient-from: color-mix(in srgb, oklch(60.6% 0.25 292.717) 0%, transparent);
.via-amber-500\/60 {
--tw-gradient-via: color-mix(in srgb, oklch(76.9% 0.188 70.08) 60%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-from: color-mix(in oklab, var(--color-violet-500) 0%, transparent);
--tw-gradient-via: color-mix(in oklab, var(--color-amber-500) 60%, transparent);
}
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-black\/10 {
--tw-gradient-via: color-mix(in srgb, #000 10%, transparent);
@@ -1511,18 +1537,16 @@
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-indigo-500 {
--tw-gradient-via: var(--color-indigo-500);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-violet-500\/50 {
--tw-gradient-via: color-mix(in srgb, oklch(60.6% 0.25 292.717) 50%, transparent);
.to-amber-500\/0 {
--tw-gradient-to: color-mix(in srgb, oklch(76.9% 0.188 70.08) 0%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-via: color-mix(in oklab, var(--color-violet-500) 50%, transparent);
--tw-gradient-to: color-mix(in oklab, var(--color-amber-500) 0%, transparent);
}
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-amber-600 {
--tw-gradient-to: var(--color-amber-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-emerald-950 {
--tw-gradient-to: var(--color-emerald-950);
@@ -1544,17 +1568,6 @@
--tw-gradient-to: transparent;
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-violet-500 {
--tw-gradient-to: var(--color-violet-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-violet-500\/0 {
--tw-gradient-to: color-mix(in srgb, oklch(60.6% 0.25 292.717) 0%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-gradient-to: color-mix(in oklab, var(--color-violet-500) 0%, transparent);
}
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.fill-amber-400 {
fill: var(--color-amber-400);
}
@@ -1997,6 +2010,9 @@
.placeholder-slate-400::placeholder {
color: var(--color-slate-400);
}
.accent-amber-500 {
accent-color: var(--color-amber-500);
}
.opacity-0 {
opacity: 0%;
}
@@ -2036,18 +2052,24 @@
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.shadow-slate-900\/10 {
--tw-shadow-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-900) 10%, transparent) var(--tw-shadow-alpha), transparent);
}
}
.shadow-slate-950\/25 {
--tw-shadow-color: color-mix(in srgb, oklch(12.9% 0.042 264.695) 25%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-950) 25%, transparent) var(--tw-shadow-alpha), transparent);
}
}
.shadow-violet-500\/15 {
--tw-shadow-color: color-mix(in srgb, oklch(60.6% 0.25 292.717) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-violet-500) 15%, transparent) var(--tw-shadow-alpha), transparent);
}
}
.shadow-violet-600\/15 {
--tw-shadow-color: color-mix(in srgb, oklch(54.1% 0.281 293.009) 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-violet-600) 15%, transparent) var(--tw-shadow-alpha), transparent);
}
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
@@ -2107,6 +2129,11 @@
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
}
.backdrop-blur-xl {
--tw-backdrop-blur: blur(var(--blur-xl));
-webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
}
.transition-all {
transition-property: all;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -2313,6 +2340,9 @@
.hover\:text-amber-400:hover {
color: var(--color-amber-400);
}
.hover\:text-amber-700:hover {
color: var(--color-amber-700);
}
.hover\:text-red-400:hover {
color: var(--color-red-400);
}
@@ -2350,6 +2380,14 @@
--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.hover\:shadow-slate-200\/50:hover {
--tw-shadow-color: color-mix(in srgb, oklch(92.9% 0.013 255.508) 50%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:shadow-slate-200\/50:hover {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-200) 50%, transparent) var(--tw-shadow-alpha), transparent);
}
}
}
.focus\:border-violet-500:focus {
border-color: var(--color-violet-500);
@@ -2583,6 +2621,9 @@
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-slate-700 {
background-color: var(--color-slate-700);
}
.dark\:bg-white\/5 {
background-color: color-mix(in srgb, #fff 5%, transparent);
}

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,219 +0,0 @@
@namespace MidrandBooks.Components.Layout
@inject IJSRuntime JSRuntime
<dialog id="components-reconnect-modal" data-nosnippet class="fixed inset-0 w-screen h-screen m-0 p-0 border-0 bg-transparent z-[99999] overflow-hidden flex items-center justify-center opacity-0 pointer-events-none transition-opacity duration-300">
<!-- Dark Frosted Jacket Layer -->
<div class="absolute inset-0 bg-slate-950/60 backdrop-blur-md cursor-wait z-[1] animate-in fade-in duration-300"></div>
<!-- Centered Content Wrapper -->
<div class="reconnect-content relative z-[2] flex flex-col items-center gap-6 text-center text-slate-100 max-w-sm px-6">
<!-- Waveform Animation -->
<svg class="waveform-svg text-violet-400" viewBox="0 0 54 50" width="60" height="60" xmlns="http://www.w3.org/2000/svg">
<rect class="bar bar-1" x="2" y="15" width="4" height="20" rx="2" fill="currentColor" />
<rect class="bar bar-2" x="14" y="5" width="4" height="40" rx="2" fill="currentColor" />
<rect class="bar bar-3" x="26" y="20" width="4" height="10" rx="2" fill="currentColor" />
<rect class="bar bar-4" x="38" y="10" width="4" height="30" rx="2" fill="currentColor" />
<rect class="bar bar-5" x="50" y="18" width="4" height="14" rx="2" fill="currentColor" />
</svg>
<!-- Message Body -->
<div class="sync-message-body font-mono uppercase text-xs tracking-widest text-slate-300 space-y-2">
<span class="components-reconnect-first-attempt-visible hidden">
Losing signal...
</span>
<span class="components-reconnect-repeated-attempt-visible hidden">
reconnecting in <span id="components-seconds-to-next-attempt" class="font-bold text-violet-400">0</span>s...
</span>
<span class="components-reconnect-failed-visible text-rose-400 font-semibold hidden">
Playback disconnected.
</span>
<span class="components-pause-visible hidden">
Reading layout paused.
</span>
<span class="components-resume-failed-visible text-rose-400 font-semibold hidden">
State alignment broken.
</span>
</div>
<!-- Glass Action Buttons -->
<div class="sync-action-node flex items-center justify-center gap-3">
<button id="components-reconnect-button" class="btn-glass-action hidden px-4 py-2.5 bg-white/5 border border-white/10 hover:bg-white/10 text-white font-mono uppercase text-[10px] tracking-widest rounded-xl transition-all items-center gap-2 cursor-pointer shadow-lg hover:scale-102 hover:border-violet-500/30">
<span>Retry Sync</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67" />
</svg>
</button>
<button id="components-resume-button" class="btn-glass-action hidden px-4 py-2.5 bg-white/5 border border-white/10 hover:bg-white/10 text-white font-mono uppercase text-[10px] tracking-widest rounded-xl transition-all items-center gap-2 cursor-pointer shadow-lg hover:scale-102 hover:border-violet-500/30">
<span>Reload Session</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
</button>
</div>
</div>
</dialog>
<style>
/* --- Show the modal smoothly when Blazor attaches state classes --- */
#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
opacity: 1 !important;
pointer-events: all !important;
}
#components-reconnect-modal.components-reconnect-hide {
display: none !important;
}
/* --- SVG Waveform Animation --- */
#components-reconnect-modal .bar {
transform-origin: center;
animation: reconnectWavePulse 1.2s ease-in-out infinite alternate;
}
#components-reconnect-modal .bar-1 {
animation-delay: 0.0s;
}
#components-reconnect-modal .bar-2 {
animation-delay: 0.3s;
}
#components-reconnect-modal .bar-3 {
animation-delay: 0.6s;
}
#components-reconnect-modal .bar-4 {
animation-delay: 0.2s;
}
#components-reconnect-modal .bar-5 {
animation-delay: 0.5s;
}
@@keyframes reconnectWavePulse {
0% {
transform: scaleY(0.3);
opacity: 0.5;
}
100% {
transform: scaleY(1);
opacity: 1;
}
}
/* Stop the waveform if the connection completely fails */
#components-reconnect-modal.components-reconnect-failed .bar,
#components-reconnect-modal.components-reconnect-rejected .bar,
#components-reconnect-modal.components-reconnect-paused .bar {
animation: none !important;
transform: scaleY(0.1) !important;
opacity: 0.3 !important;
}
/* --- Display Mechanics Matrix Controllers --- */
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: inline-block !important;
}
#components-reconnect-modal.components-reconnect-failed .btn-glass-action,
#components-reconnect-modal.components-reconnect-paused .btn-glass-action,
#components-reconnect-modal.components-reconnect-resume-failed .btn-glass-action {
display: inline-flex !important;
}
</style>
<script>
(function() {
function initReconnectModal() {
const reconnectModal = document.getElementById("components-reconnect-modal");
if (!reconnectModal) {
setTimeout(initReconnectModal, 100);
return;
}
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
if (retryButton) {
retryButton.addEventListener("click", retry);
}
const resumeButton = document.getElementById("components-resume-button");
if (resumeButton) {
resumeButton.addEventListener("click", resume);
}
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
if (window.Blazor) {
const successful = await window.Blazor.reconnect();
if (!successful) {
const resumeSuccessful = await window.Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
}
} catch (err) {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
if (window.Blazor) {
const successful = await window.Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initReconnectModal);
} else {
initReconnectModal();
}
})();
</script>