;s;
This commit is contained in:
@@ -6,43 +6,31 @@ export function BookCover({ className = "" }: { className?: string }) {
|
||||
className={`cover-float relative w-full max-w-[min(100%,300px)] sm:max-w-[320px] ${className}`}
|
||||
>
|
||||
<div
|
||||
className="absolute -inset-6 rounded-[2rem] bg-gradient-to-br from-teal-500/50 via-amber-400/40 to-sky-500/50 opacity-60 blur-2xl"
|
||||
className="cover-glow absolute -inset-6 rounded-[2rem] opacity-50 blur-2xl"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="relative aspect-[3/4] overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/25">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-br from-teal-800 via-teal-700 to-sky-900"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"repeating-linear-gradient(0deg, transparent, transparent 23px, rgba(255,255,255,0.06) 24px)",
|
||||
}}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="cover-card relative aspect-[3/4] overflow-hidden rounded-2xl shadow-2xl ring-1 ring-white/15">
|
||||
<div
|
||||
className="absolute inset-0 bg-[radial-gradient(ellipse_at_30%_12%,rgba(255,255,255,0.22),transparent_55%)]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-black/50 to-transparent"
|
||||
className="absolute inset-0 bg-[linear-gradient(160deg,transparent_35%,rgba(0,0,0,0.4))]"
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className="absolute left-0 top-0 h-full w-4 bg-gradient-to-r from-black/35 to-transparent"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="absolute right-5 top-6 opacity-20" aria-hidden>
|
||||
<div className="absolute right-5 top-6 opacity-15" aria-hidden>
|
||||
<GlobeMark className="h-24 w-24 text-white" />
|
||||
</div>
|
||||
<div className="relative flex h-full flex-col justify-between p-7 text-white sm:p-8">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-[10px] font-bold uppercase tracking-[0.28em] text-teal-200/90">
|
||||
<p className="text-[10px] font-bold uppercase tracking-[0.28em] text-white/75">
|
||||
Digital nomad
|
||||
</p>
|
||||
<span className="rounded-full border border-white/20 bg-white/10 px-2 py-0.5 font-mono text-[10px] text-white/80">
|
||||
<span className="rounded-full border border-white/25 bg-white/10 px-2 py-0.5 font-mono text-[10px] text-white/90">
|
||||
EN
|
||||
</span>
|
||||
</div>
|
||||
@@ -50,12 +38,12 @@ export function BookCover({ className = "" }: { className?: string }) {
|
||||
<h2 className="font-serif text-[1.7rem] font-medium leading-[1.1] tracking-tight sm:text-[1.9rem]">
|
||||
{ebookMeta.title}
|
||||
</h2>
|
||||
<p className="mt-3 text-xs leading-relaxed text-teal-100/85 sm:text-sm">
|
||||
<p className="mt-3 text-xs leading-relaxed text-white/80 sm:text-sm">
|
||||
{ebookMeta.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-mono text-[10px] tracking-wider text-white/50">
|
||||
<p className="font-mono text-[10px] tracking-wider text-white/55">
|
||||
48.8566° N · Work from anywhere
|
||||
</p>
|
||||
<p className="mt-2 text-xs font-medium text-white/70">
|
||||
@@ -70,9 +58,15 @@ export function BookCover({ className = "" }: { className?: string }) {
|
||||
|
||||
function GlobeMark({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="0.75">
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="0.75"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" />
|
||||
<path d="M2 12h20M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 014-10z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function ReadSidebar() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
className="flex h-12 items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface-solid)] px-4 text-sm font-semibold text-[var(--foreground)] shadow-lg"
|
||||
className="flex h-12 items-center gap-2 rounded-full border border-[var(--border)] bg-[var(--surface)] px-4 text-sm font-semibold text-[var(--foreground)] shadow-lg"
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-toc-panel"
|
||||
>
|
||||
@@ -120,7 +120,7 @@ export function ReadSidebar() {
|
||||
/>
|
||||
<div
|
||||
id="mobile-toc-panel"
|
||||
className="absolute bottom-0 left-0 right-0 max-h-[80vh] overflow-hidden rounded-t-3xl border border-[var(--border)] bg-[var(--surface-solid)] shadow-2xl"
|
||||
className="absolute bottom-0 left-0 right-0 max-h-[80vh] overflow-hidden rounded-t-3xl border border-[var(--border)] bg-[var(--surface)] shadow-2xl dark:bg-[var(--surface-elevated)]"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-[var(--border)] px-5 py-4">
|
||||
<p
|
||||
|
||||
@@ -14,7 +14,7 @@ export function SiteHeader({
|
||||
showBack = false,
|
||||
}: SiteHeaderProps) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 h-[var(--header-h)] border-b border-[var(--border)]/70 bg-[var(--background)]/80 backdrop-blur-xl">
|
||||
<header className="sticky top-0 z-50 h-[var(--header-h)] border-b border-[var(--border)] bg-[var(--surface)]/95 backdrop-blur-xl dark:bg-[var(--background)]/90">
|
||||
<div className="mx-auto flex h-full max-w-6xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
|
||||
{showBack ? (
|
||||
<Link
|
||||
@@ -29,7 +29,7 @@ export function SiteHeader({
|
||||
className={`group flex min-w-0 items-center gap-2.5 ${showBack ? "hidden sm:flex" : "flex"}`}
|
||||
>
|
||||
<span
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] text-white shadow-lg shadow-teal-500/20"
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] text-white shadow-md"
|
||||
aria-hidden
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
@@ -51,7 +51,7 @@ export function SiteHeader({
|
||||
<ThemeToggle />
|
||||
<Link
|
||||
href={ctaHref}
|
||||
className="hidden h-10 items-center justify-center rounded-full bg-gradient-to-r from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] px-5 text-sm font-semibold text-white shadow-md shadow-teal-500/20 transition-transform hover:scale-[1.02] active:scale-[0.98] sm:inline-flex"
|
||||
className="hidden h-10 items-center justify-center rounded-full bg-gradient-to-r from-[var(--gradient-1)] via-[var(--gradient-2)] to-[var(--gradient-3)] px-5 text-sm font-semibold text-white shadow-[var(--glow-sm)] transition-transform hover:scale-[1.02] active:scale-[0.98] sm:inline-flex"
|
||||
>
|
||||
{ctaLabel}
|
||||
</Link>
|
||||
|
||||
@@ -1,97 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
function getPreferredTheme(): Theme {
|
||||
if (typeof window === "undefined") return "light";
|
||||
const stored = localStorage.getItem("theme");
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
|
||||
/** Theme click handled by /public/theme.js — works without React hydration */
|
||||
export function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTheme(getPreferredTheme());
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
function toggle() {
|
||||
const next: Theme = theme === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
applyTheme(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
|
||||
className="theme-toggle group relative flex h-10 w-10 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface)]/80 text-[var(--foreground)] shadow-sm backdrop-blur-md transition-all hover:border-[var(--accent)]/40 hover:shadow-[var(--glow-sm)]"
|
||||
data-theme-toggle
|
||||
aria-pressed="false"
|
||||
aria-label="Switch to dark mode"
|
||||
title="Dark mode"
|
||||
className="inline-flex h-10 shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-full border-2 border-[var(--border-strong)] bg-[var(--surface)] px-3 text-[var(--foreground)] shadow-sm transition-[background-color,border-color,color] duration-200 hover:border-[var(--accent)]"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{mounted
|
||||
? theme === "dark"
|
||||
? "Light mode"
|
||||
: "Dark mode"
|
||||
: "Toggle theme"}
|
||||
<span data-theme-icon="sun" hidden aria-hidden>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm0-16v2m0 16v2M4.22 4.22l1.42 1.42m12.72 12.72 1.42 1.42M2 12h2m16 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||
</svg>
|
||||
</span>
|
||||
<span data-theme-icon="moon" aria-hidden>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3 7 7 0 1 0 21 14.5z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span data-theme-label className="text-xs font-bold tracking-wide">
|
||||
Dark
|
||||
</span>
|
||||
<SunIcon
|
||||
className={`absolute h-[18px] w-[18px] transition-all duration-300 ${
|
||||
mounted && theme === "dark"
|
||||
? "rotate-0 scale-100 opacity-100"
|
||||
: "rotate-90 scale-0 opacity-0"
|
||||
}`}
|
||||
/>
|
||||
<MoonIcon
|
||||
className={`absolute h-[18px] w-[18px] transition-all duration-300 ${
|
||||
mounted && theme !== "dark"
|
||||
? "rotate-0 scale-100 opacity-100"
|
||||
: "-rotate-90 scale-0 opacity-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SunIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.75"
|
||||
aria-hidden
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MoonIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.75"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M21 14.5A8.5 8.5 0 0 1 9.5 3 7 7 0 1 0 21 14.5z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user