Compare commits
21 Commits
ba60d49fc6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ee2eb4796e | |||
| d90442456b | |||
| b17930c10b | |||
| 1f3323834e | |||
| 7cdfb90b1b | |||
| f6c5be0ee4 | |||
| fc24ddb676 | |||
| cafb810155 | |||
| ae37f93508 | |||
| 4a4d6dd933 | |||
| 7bf2c4149d | |||
| 9d7a6757c9 | |||
| d03b061c1e | |||
| d768d108d6 | |||
| 7685c79705 | |||
| 4173aa69d3 | |||
| e2f9c9d556 | |||
| 5d226a2969 | |||
| a77cd17419 | |||
| d42cb46af8 | |||
| d6edcf1757 |
@@ -198,7 +198,10 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
|
||||
<SectionDivider>
|
||||
<ScrollReveal>
|
||||
<article className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-dark">
|
||||
<article
|
||||
data-toc-content={slug}
|
||||
className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-dark"
|
||||
>
|
||||
{post.feature_image && (
|
||||
<div className="-mx-4 mb-8 transition-all duration-500 sm:-mx-12 lg:-mx-20 group-[.toc-open]:lg:-mx-4">
|
||||
<Image
|
||||
|
||||
@@ -110,7 +110,10 @@ export default async function StaticPage({ params }: Props) {
|
||||
|
||||
<SectionDivider>
|
||||
<ScrollReveal>
|
||||
<article className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-dark">
|
||||
<article
|
||||
data-toc-content={slug}
|
||||
className="prose prose-lg prose-slate mx-auto max-w-none dark:prose-dark"
|
||||
>
|
||||
{page.feature_image && (
|
||||
<div className="-mx-4 mb-8 transition-all duration-500 sm:-mx-12 lg:-mx-20 group-[.toc-open]:lg:-mx-4">
|
||||
<Image
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useRef, FocusEvent, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
FiMenu,
|
||||
FiX,
|
||||
@@ -14,9 +15,12 @@ import {
|
||||
FiTag,
|
||||
FiServer,
|
||||
FiCpu,
|
||||
FiList
|
||||
FiList,
|
||||
FiChevronDown,
|
||||
FiChevronRight
|
||||
} from 'react-icons/fi';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export type IconKey =
|
||||
| 'home'
|
||||
@@ -47,9 +51,10 @@ const ICON_MAP: Record<IconKey, any> = {
|
||||
|
||||
export interface NavLinkItem {
|
||||
key: string;
|
||||
href: string;
|
||||
label?: string;
|
||||
href?: string;
|
||||
label: string;
|
||||
iconKey: IconKey;
|
||||
children?: NavLinkItem[];
|
||||
}
|
||||
|
||||
interface NavMenuProps {
|
||||
@@ -58,40 +63,243 @@ interface NavMenuProps {
|
||||
|
||||
export function NavMenu({ items }: NavMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||
const [expandedMobileItems, setExpandedMobileItems] = useState<string[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const closeTimer = useRef<number | null>(null);
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Lock body scroll when menu is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
// Close menu on route change
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
const toggle = () => setOpen((val) => !val);
|
||||
const close = () => setOpen(false);
|
||||
|
||||
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
|
||||
setActiveDropdown(null);
|
||||
}
|
||||
};
|
||||
|
||||
const clearCloseTimer = () => {
|
||||
if (closeTimer.current) {
|
||||
clearTimeout(closeTimer.current);
|
||||
closeTimer.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const openDropdown = (key: string) => {
|
||||
clearCloseTimer();
|
||||
setActiveDropdown(key);
|
||||
};
|
||||
|
||||
const scheduleCloseDropdown = () => {
|
||||
clearCloseTimer();
|
||||
closeTimer.current = window.setTimeout(() => setActiveDropdown(null), 180);
|
||||
};
|
||||
|
||||
const toggleMobileItem = (key: string) => {
|
||||
setExpandedMobileItems(prev =>
|
||||
prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]
|
||||
);
|
||||
};
|
||||
|
||||
const renderDesktopChild = (item: NavLinkItem) => {
|
||||
const Icon = ICON_MAP[item.iconKey] ?? FiFile;
|
||||
return item.href ? (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className="motion-link inline-flex items-center gap-2 rounded-xl px-3 py-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
onClick={close}
|
||||
>
|
||||
<Icon className="h-4 w-4 text-slate-400" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderMobileItem = (item: NavLinkItem, depth = 0) => {
|
||||
const Icon = ICON_MAP[item.iconKey] ?? FiFile;
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedMobileItems.includes(item.key);
|
||||
|
||||
if (hasChildren) {
|
||||
return (
|
||||
<div key={item.key} className="flex flex-col">
|
||||
<button
|
||||
onClick={() => toggleMobileItem(item.key)}
|
||||
className="flex w-full items-center justify-between rounded-xl px-4 py-3 text-base font-medium text-slate-700 transition-colors active:bg-slate-100 dark:text-slate-200 dark:active:bg-slate-800"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-5 w-5 text-slate-400" />
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
<FiChevronRight
|
||||
className={`h-5 w-5 text-slate-400 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={`grid transition-all duration-200 ease-in-out ${isExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="flex flex-col gap-1 pl-4 pt-1">
|
||||
{item.children!.map(child => renderMobileItem(child, depth + 1))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return item.href ? (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className="flex w-full items-center gap-3 rounded-xl px-4 py-3 text-base font-medium text-slate-700 transition-colors active:bg-slate-100 dark:text-slate-200 dark:active:bg-slate-800"
|
||||
onClick={close}
|
||||
>
|
||||
<Icon className="h-5 w-5 text-slate-400" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Menu Trigger */}
|
||||
<button
|
||||
type="button"
|
||||
className="sm:hidden inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition duration-180 ease-snappy hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
className="relative z-50 inline-flex h-10 w-10 items-center justify-center rounded-full text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200 dark:hover:bg-slate-800 sm:hidden"
|
||||
aria-label={open ? '關閉選單' : '開啟選單'}
|
||||
aria-expanded={open}
|
||||
onClick={toggle}
|
||||
>
|
||||
{open ? <FiX className="h-4 w-4" /> : <FiMenu className="h-4 w-4" />}
|
||||
<div className="relative h-5 w-5">
|
||||
<span
|
||||
className={`absolute left-0 top-1/2 h-0.5 w-5 -translate-y-1/2 bg-current transition-all duration-300 ease-snappy ${open ? 'rotate-45' : '-translate-y-1.5'
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`absolute left-0 top-1/2 h-0.5 w-5 -translate-y-1/2 bg-current transition-all duration-300 ease-snappy ${open ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`absolute left-0 top-1/2 h-0.5 w-5 -translate-y-1/2 bg-current transition-all duration-300 ease-snappy ${open ? '-rotate-45' : 'translate-y-1.5'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<nav
|
||||
className={`${open ? 'flex' : 'hidden'} flex-col gap-2 sm:flex sm:flex-row sm:items-center sm:gap-3`}
|
||||
|
||||
{/* Mobile Menu Overlay - Portaled */}
|
||||
{mounted && createPortal(
|
||||
<div
|
||||
className={`fixed inset-0 z-[100] flex flex-col bg-white/95 backdrop-blur-xl transition-all duration-300 ease-snappy dark:bg-gray-950/95 sm:hidden ${open ? 'visible opacity-100' : 'invisible opacity-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
{items.map((item) => (
|
||||
{/* Close button area */}
|
||||
<div className="flex items-center justify-end px-4 py-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full text-slate-600 transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
onClick={close}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<div className="relative h-5 w-5">
|
||||
<span className="absolute left-0 top-1/2 h-0.5 w-5 -translate-y-1/2 rotate-45 bg-current" />
|
||||
<span className="absolute left-0 top-1/2 h-0.5 w-5 -translate-y-1/2 -rotate-45 bg-current" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto flex flex-1 flex-col px-4 pb-8">
|
||||
<div className="flex flex-1 flex-col gap-2 overflow-y-auto pt-4">
|
||||
{items.map(item => renderMobileItem(item))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-8 text-center text-xs text-slate-400">
|
||||
<p>© {new Date().getFullYear()} All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* Desktop Menu */}
|
||||
<nav className="hidden sm:flex sm:items-center sm:gap-3">
|
||||
{items.map((item) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const Icon = ICON_MAP[item.iconKey] ?? FiFile;
|
||||
const isOpen = activeDropdown === item.key;
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className="group relative"
|
||||
onMouseEnter={() => openDropdown(item.key)}
|
||||
onMouseLeave={scheduleCloseDropdown}
|
||||
onFocus={() => openDropdown(item.key)}
|
||||
onBlur={handleBlur}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="motion-link type-nav inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-slate-600 hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5 text-slate-400 transition group-hover:text-accent" />
|
||||
<span>{item.label}</span>
|
||||
<FiChevronDown className="h-3 w-3 text-slate-400 transition group-hover:text-accent" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`absolute left-0 top-full z-50 hidden min-w-[12rem] rounded-2xl border border-slate-200 bg-white p-2 shadow-lg transition duration-200 ease-snappy dark:border-slate-800 dark:bg-slate-900 sm:block ${isOpen ? 'pointer-events-auto translate-y-2 opacity-100' : 'pointer-events-none translate-y-1 opacity-0'
|
||||
}`}
|
||||
role="menu"
|
||||
aria-label={item.label}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{item.children.map((child) => renderDesktopChild(child))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Icon = ICON_MAP[item.iconKey] ?? FiFile;
|
||||
|
||||
return item.href ? (
|
||||
<Link
|
||||
key={item.key}
|
||||
href={item.href}
|
||||
className="motion-link type-nav group relative inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-slate-600 hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 dark:text-slate-200"
|
||||
onClick={close}
|
||||
>
|
||||
{(() => {
|
||||
const Icon = ICON_MAP[item.iconKey] ?? FiFile;
|
||||
return <Icon className="h-3.5 w-3.5 text-slate-400 transition group-hover:text-accent" />;
|
||||
})()}
|
||||
<Icon className="h-3.5 w-3.5 text-slate-400 transition group-hover:text-accent" />
|
||||
<span>{item.label}</span>
|
||||
<span className="absolute inset-x-3 -bottom-0.5 h-px origin-left scale-x-0 bg-accent transition duration-180 ease-snappy group-hover:scale-x-100" aria-hidden="true" />
|
||||
</Link>
|
||||
))}
|
||||
) : null;
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { FiList, FiChevronRight } from 'react-icons/fi';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FiList, FiX } from 'react-icons/fi';
|
||||
import { PostToc } from './post-toc';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -11,17 +12,111 @@ function cn(...inputs: ClassValue[]) {
|
||||
}
|
||||
|
||||
export function PostLayout({ children, hasToc = true, contentKey }: { children: React.ReactNode; hasToc?: boolean; contentKey?: string }) {
|
||||
const [isTocOpen, setIsTocOpen] = useState(hasToc);
|
||||
const [isTocOpen, setIsTocOpen] = useState(false); // Default closed on mobile
|
||||
const [isDesktopTocOpen, setIsDesktopTocOpen] = useState(hasToc); // Separate state for desktop
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Lock body scroll when mobile TOC is open
|
||||
useEffect(() => {
|
||||
if (isTocOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isTocOpen]);
|
||||
|
||||
const mobileToc = hasToc && mounted
|
||||
? createPortal(
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-[1140] bg-black/40 backdrop-blur-sm transition-opacity duration-300 lg:hidden",
|
||||
isTocOpen ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
onClick={() => setIsTocOpen(false)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed bottom-0 left-0 right-0 z-[1150] flex max-h-[85vh] flex-col rounded-t-2xl border-t border-white/20 bg-white/95 shadow-2xl backdrop-blur-xl transition-transform duration-300 ease-snappy dark:border-white/10 dark:bg-slate-900/95 lg:hidden",
|
||||
isTocOpen ? "translate-y-0" : "translate-y-full"
|
||||
)}
|
||||
>
|
||||
{/* Handle / Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-200/50 px-6 py-4 dark:border-slate-700/50" onClick={() => setIsTocOpen(false)}>
|
||||
<div className="flex items-center gap-2 font-semibold text-slate-900 dark:text-slate-100">
|
||||
<FiList className="h-5 w-5 text-slate-500" />
|
||||
<span>目錄</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsTocOpen(false)}
|
||||
className="rounded-full p-1 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
>
|
||||
<FiX className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6">
|
||||
<PostToc
|
||||
contentKey={contentKey}
|
||||
onLinkClick={() => setIsTocOpen(false)}
|
||||
showTitle={false}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
const tocButton = hasToc && mounted ? (
|
||||
<button
|
||||
onClick={() => setIsTocOpen(true)}
|
||||
className={cn(
|
||||
"fixed bottom-6 right-16 z-40 flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white/90 px-3 text-sm font-medium text-slate-600 shadow-md backdrop-blur-sm transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900/90 dark:text-slate-300 dark:hover:bg-slate-800 lg:hidden",
|
||||
isTocOpen ? "opacity-0 pointer-events-none" : "opacity-100"
|
||||
)}
|
||||
aria-label="Open Table of Contents"
|
||||
>
|
||||
<FiList className="h-4 w-4" />
|
||||
<span>目錄</span>
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const desktopTocButton = hasToc && mounted ? (
|
||||
<button
|
||||
onClick={() => setIsDesktopTocOpen(!isDesktopTocOpen)}
|
||||
className={cn(
|
||||
"fixed bottom-6 right-16 z-40 hidden h-9 items-center gap-2 rounded-full border border-slate-200 bg-white/90 px-3 text-sm font-medium text-slate-600 shadow-md backdrop-blur-sm transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900/90 dark:text-slate-300 dark:hover:bg-slate-800 lg:flex",
|
||||
)}
|
||||
aria-label={isDesktopTocOpen ? "Close Table of Contents" : "Open Table of Contents"}
|
||||
>
|
||||
<FiList className="h-4 w-4" />
|
||||
<span>{isDesktopTocOpen ? '隱藏目錄' : '顯示目錄'}</span>
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className={cn(
|
||||
"group grid gap-8 transition-all duration-500 ease-snappy",
|
||||
isTocOpen && hasToc ? "lg:grid-cols-[1fr_16rem] toc-open" : "lg:grid-cols-[1fr_0rem]"
|
||||
isDesktopTocOpen && hasToc ? "lg:grid-cols-[1fr_16rem] toc-open" : "lg:grid-cols-[1fr_0rem]"
|
||||
)}>
|
||||
{/* Main Content Area */}
|
||||
<div className="min-w-0">
|
||||
<div className={cn("mx-auto transition-all duration-500 ease-snappy", isTocOpen && hasToc ? "max-w-3xl" : "max-w-4xl")}>
|
||||
<div className={cn("mx-auto transition-all duration-500 ease-snappy", isDesktopTocOpen && hasToc ? "max-w-3xl" : "max-w-4xl")}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,9 +124,9 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
||||
{/* Desktop Sidebar (TOC) */}
|
||||
<aside className="hidden lg:block">
|
||||
<div className="sticky top-24 h-[calc(100vh-6rem)] overflow-hidden">
|
||||
{isTocOpen && hasToc && (
|
||||
{isDesktopTocOpen && hasToc && (
|
||||
<div className="toc-sidebar h-full overflow-y-auto pr-2">
|
||||
<PostToc key={contentKey} />
|
||||
<PostToc contentKey={contentKey} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -39,32 +134,15 @@ export function PostLayout({ children, hasToc = true, contentKey }: { children:
|
||||
</div>
|
||||
|
||||
{/* Mobile TOC Overlay */}
|
||||
{isTocOpen && hasToc && (
|
||||
<div className="toc-mobile fixed bottom-24 right-4 z-40 w-72 rounded-2xl border border-white/20 bg-white/90 p-6 shadow-2xl backdrop-blur-xl dark:border-white/10 dark:bg-slate-900/90 lg:hidden">
|
||||
<div className="max-h-[60vh] overflow-y-auto">
|
||||
<PostToc key={contentKey} onLinkClick={() => setIsTocOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{mobileToc}
|
||||
|
||||
{/* Toggle Button (Glassmorphism Pill) */}
|
||||
{hasToc && (
|
||||
<button
|
||||
onClick={() => setIsTocOpen(!isTocOpen)}
|
||||
className={cn(
|
||||
"toc-button fixed bottom-8 right-8 z-50 flex items-center gap-2 rounded-full border border-white/20 bg-white/80 px-4 py-2.5 shadow-lg backdrop-blur-md hover:bg-white dark:border-white/10 dark:bg-slate-900/80 dark:hover:bg-slate-900",
|
||||
"text-sm font-medium text-slate-600 dark:text-slate-300",
|
||||
"lg:right-20" // Adjust position for desktop
|
||||
)}
|
||||
aria-label="Toggle Table of Contents"
|
||||
>
|
||||
{isTocOpen ? (
|
||||
<FiChevronRight className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<FiList className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{isTocOpen ? 'Hide' : 'Menu'}</span>
|
||||
</button>
|
||||
{/* Toggle Buttons - Rendered via Portal */}
|
||||
{mounted && createPortal(
|
||||
<>
|
||||
{tocButton}
|
||||
{desktopTocButton}
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { FiList } from 'react-icons/fi';
|
||||
|
||||
interface TocItem {
|
||||
@@ -10,17 +9,48 @@ interface TocItem {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
||||
export function PostToc({
|
||||
onLinkClick,
|
||||
contentKey,
|
||||
showTitle = true,
|
||||
className
|
||||
}: {
|
||||
onLinkClick?: () => void;
|
||||
contentKey?: string;
|
||||
showTitle?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const [items, setItems] = useState<TocItem[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const itemRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const [indicator, setIndicator] = useState({ top: 0, opacity: 0 });
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Clear items immediately when content changes
|
||||
setItems([]);
|
||||
setActiveId(null);
|
||||
itemRefs.current = {};
|
||||
|
||||
const containerSelector = contentKey
|
||||
? `[data-toc-content="${contentKey}"]`
|
||||
: '[data-toc-content]';
|
||||
const container = document.querySelector<HTMLElement>(containerSelector);
|
||||
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
let rafId1: number;
|
||||
let rafId2: number;
|
||||
|
||||
// Use double requestAnimationFrame to ensure DOM has been painted
|
||||
// This is more reliable than setTimeout for DOM updates
|
||||
rafId1 = requestAnimationFrame(() => {
|
||||
rafId2 = requestAnimationFrame(() => {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('article h2, article h3')
|
||||
container.querySelectorAll<HTMLElement>('h2, h3')
|
||||
);
|
||||
const mapped = headings
|
||||
.filter((el) => el.id)
|
||||
@@ -31,7 +61,7 @@ export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
||||
}));
|
||||
setItems(mapped);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
@@ -49,10 +79,18 @@ export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
||||
}
|
||||
);
|
||||
|
||||
headings.forEach((el) => observer.observe(el));
|
||||
headings.forEach((el) => observer?.observe(el));
|
||||
});
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [pathname]);
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId1);
|
||||
cancelAnimationFrame(rafId2);
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
}, [contentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId || !listRef.current) {
|
||||
@@ -98,11 +136,13 @@ export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="not-prose sticky top-20 text-slate-500 dark:text-slate-400">
|
||||
<nav className={`not-prose text-slate-500 dark:text-slate-400 ${className || 'sticky top-20'}`}>
|
||||
{showTitle && (
|
||||
<div className="mb-2 inline-flex items-center gap-2 font-semibold text-slate-700 dark:text-slate-200">
|
||||
<FiList className="h-4 w-4 text-slate-400" />
|
||||
目錄
|
||||
</div>
|
||||
)}
|
||||
<div className="relative pl-4">
|
||||
<span className="absolute left-1 top-0 h-full w-px bg-slate-200 dark:bg-slate-800" aria-hidden="true" />
|
||||
<span
|
||||
@@ -127,8 +167,7 @@ export function PostToc({ onLinkClick }: { onLinkClick?: () => void }) {
|
||||
<a
|
||||
href={`#${item.id}`}
|
||||
onClick={handleClick(item.id)}
|
||||
className={`line-clamp-2 inline-flex items-center pl-2 hover:text-blue-600 dark:hover:text-blue-400 ${
|
||||
item.id === activeId
|
||||
className={`line-clamp-2 inline-flex items-center py-1 pl-2 hover:text-blue-600 dark:hover:text-blue-400 ${item.id === activeId
|
||||
? 'text-blue-600 dark:text-blue-400 font-semibold'
|
||||
: ''
|
||||
}`}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export function ReadingProgress() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -29,19 +30,21 @@ export function ReadingProgress() {
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [mounted]);
|
||||
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-x-0 top-0 z-40 h-px bg-transparent">
|
||||
<div className="relative h-1 w-full overflow-visible">
|
||||
return createPortal(
|
||||
<div className="pointer-events-none fixed inset-x-0 top-0 z-[1200] h-1.5 bg-transparent">
|
||||
<div className="relative h-1.5 w-full overflow-visible">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 w-full origin-left rounded-full bg-gradient-to-r from-blue-500/70 via-sky-400/70 to-indigo-500/70 shadow-[0_0_8px_rgba(59,130,246,0.45)] transition-[transform,opacity] duration-300 ease-out dark:from-blue-400/70 dark:via-sky-300/70 dark:to-indigo-400/70"
|
||||
className="absolute inset-y-0 left-0 w-full origin-left rounded-full bg-gradient-to-r from-[rgba(124,58,237,0.9)] via-[rgba(167,139,250,0.9)] to-[rgba(14,165,233,0.8)] shadow-[0_0_12px_rgba(124,58,237,0.5)] transition-[transform,opacity] duration-300 ease-out"
|
||||
style={{ transform: `scaleX(${progress / 100})`, opacity: progress > 0 ? 1 : 0 }}
|
||||
>
|
||||
<span className="absolute -right-2 top-1/2 h-3 w-3 -translate-y-1/2 rounded-full bg-white/70 blur-[1px] dark:bg-slate-900/70" aria-hidden="true" />
|
||||
<span className="absolute -right-2 top-1/2 h-3 w-3 -translate-y-1/2 rounded-full bg-white/80 blur-[1px] dark:bg-slate-900/80" aria-hidden="true" />
|
||||
</div>
|
||||
<span className="absolute inset-x-0 top-2 h-px bg-gradient-to-r from-transparent via-blue-200/40 to-transparent blur-sm dark:via-blue-900/30" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,19 +21,60 @@ export function SiteHeader() {
|
||||
.slice()
|
||||
.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
|
||||
const navItems: NavLinkItem[] = [
|
||||
{ key: 'home', href: '/', label: '首頁', iconKey: 'home' },
|
||||
{ key: 'blog', href: '/blog', label: 'Blog', iconKey: 'blog' },
|
||||
...pages.map((page) => ({
|
||||
const findPage = (title: string) => pages.find((page) => page.title === title);
|
||||
|
||||
const aboutChildren = [
|
||||
{ title: '關於作者', label: '作者' },
|
||||
{ title: '關於本站', label: '本站' }
|
||||
]
|
||||
.map(({ title, label }) => {
|
||||
const page = findPage(title);
|
||||
if (!page) return null;
|
||||
return {
|
||||
key: page._id,
|
||||
href: page.url,
|
||||
label: page.title,
|
||||
label,
|
||||
iconKey: getIconForPage(page.title, page.slug)
|
||||
}))
|
||||
} satisfies NavLinkItem;
|
||||
})
|
||||
.filter(Boolean) as NavLinkItem[];
|
||||
|
||||
const deviceChildren = [
|
||||
{ title: '開發工作環境', label: '開發環境' },
|
||||
{ title: 'HomeLab', label: 'HomeLab' }
|
||||
]
|
||||
.map(({ title, label }) => {
|
||||
const page = findPage(title);
|
||||
if (!page) return null;
|
||||
return {
|
||||
key: page._id,
|
||||
href: page.url,
|
||||
label,
|
||||
iconKey: getIconForPage(page.title, page.slug)
|
||||
} satisfies NavLinkItem;
|
||||
})
|
||||
.filter(Boolean) as NavLinkItem[];
|
||||
|
||||
const navItems: NavLinkItem[] = [
|
||||
{ key: 'home', href: '/', label: '首頁', iconKey: 'home' },
|
||||
{
|
||||
key: 'about',
|
||||
href: aboutChildren[0]?.href,
|
||||
label: '關於',
|
||||
iconKey: 'user',
|
||||
children: aboutChildren
|
||||
},
|
||||
{
|
||||
key: 'devices',
|
||||
href: deviceChildren[0]?.href,
|
||||
label: '裝置',
|
||||
iconKey: 'device',
|
||||
children: deviceChildren
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-white/80 backdrop-blur transition-colors duration-200 ease-snappy dark:bg-gray-950/80">
|
||||
<header className="relative z-40 bg-white/80 backdrop-blur transition-colors duration-200 ease-snappy dark:bg-gray-950/80">
|
||||
<div className="container mx-auto flex items-center justify-between px-4 py-3 text-slate-900 dark:text-slate-100">
|
||||
<Link
|
||||
href="/"
|
||||
|
||||
@@ -9,20 +9,20 @@ interface TimelineWrapperProps {
|
||||
export function TimelineWrapper({ children, className }: TimelineWrapperProps) {
|
||||
const items = Children.toArray(children);
|
||||
return (
|
||||
<div className={clsx('relative pl-8', className)}>
|
||||
<div className={clsx('relative pl-6 md:pl-8', className)}>
|
||||
<span
|
||||
className="pointer-events-none absolute left-3 top-0 h-full w-[2px] rounded-full bg-blue-400 shadow-[0_0_10px_rgba(59,130,246,0.35)] dark:bg-cyan-300"
|
||||
className="pointer-events-none absolute left-2 top-0 h-full w-[2px] rounded-full bg-blue-400 shadow-[0_0_10px_rgba(59,130,246,0.35)] dark:bg-cyan-300 md:left-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className="pointer-events-none absolute left-3 top-0 h-full w-[8px] rounded-full bg-blue-500/15 blur-[14px]"
|
||||
className="pointer-events-none absolute left-2 top-0 h-full w-[8px] rounded-full bg-blue-500/15 blur-[14px] md:left-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{items.map((child, index) => (
|
||||
<div key={index} className="relative pl-6 sm:pl-8">
|
||||
<span className="pointer-events-none absolute left-0 top-1/2 h-px w-8 -translate-x-full -translate-y-1/2 bg-gradient-to-r from-transparent via-blue-300/80 to-transparent dark:via-cyan-200/80" aria-hidden="true" />
|
||||
<div key={index} className="relative pl-5 sm:pl-8">
|
||||
<span className="pointer-events-none absolute left-0 top-1/2 h-px w-5 -translate-x-full -translate-y-1/2 bg-gradient-to-r from-transparent via-blue-300/80 to-transparent dark:via-cyan-200/80 sm:w-8" aria-hidden="true" />
|
||||
{child}
|
||||
</div>
|
||||
))}
|
||||
|
||||
2
content
2
content
Submodule content updated: d976bb08e2...3f72ccb628
32
env
Normal file
32
env
Normal file
@@ -0,0 +1,32 @@
|
||||
# Public site metadata (safe to expose to browser)
|
||||
NEXT_PUBLIC_SITE_NAME="Gbanyan"
|
||||
NEXT_PUBLIC_SITE_TITLE="霍德爾之目"
|
||||
NEXT_PUBLIC_SITE_DESCRIPTION="醫學、科技與生活隨筆。"
|
||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
||||
NEXT_PUBLIC_SITE_AUTHOR="Gbanyan"
|
||||
NEXT_PUBLIC_SITE_TAGLINE="醫學、科技與生活的隨筆記錄。"
|
||||
NEXT_PUBLIC_POSTS_PER_PAGE="5"
|
||||
NEXT_PUBLIC_DEFAULT_LOCALE="zh-TW"
|
||||
NEXT_PUBLIC_SITE_AVATAR_URL="https://www.gravatar.com/avatar/53f2a6e011d5ececcd0d6e33e8e7329f60efcd23a5b77ba382273d5060a2cffe?s=160&d=identicon"
|
||||
NEXT_PUBLIC_SITE_ABOUT_SHORT="掙扎混亂過日子 \n 對平淡美好日常的期待即是救贖"
|
||||
|
||||
# Color scheme / accents
|
||||
NEXT_PUBLIC_COLOR_ACCENT="#2563eb"
|
||||
NEXT_PUBLIC_COLOR_ACCENT_SOFT="#dbeafe"
|
||||
NEXT_PUBLIC_COLOR_ACCENT_TEXT_LIGHT="#1d4ed8"
|
||||
NEXT_PUBLIC_COLOR_ACCENT_TEXT_DARK="#93c5fd"
|
||||
|
||||
# Social and profile
|
||||
NEXT_PUBLIC_TWITTER_HANDLE="@gbanyan"
|
||||
NEXT_PUBLIC_GITHUB_URL="https://github.com/gbanyan"
|
||||
NEXT_PUBLIC_LINKEDIN_URL=""
|
||||
NEXT_PUBLIC_EMAIL_CONTACT=""
|
||||
NEXT_PUBLIC_MASTODON_URL=""
|
||||
NEXT_PUBLIC_GITEA_URL=""
|
||||
|
||||
# SEO / Open Graph
|
||||
NEXT_PUBLIC_OG_DEFAULT_IMAGE="/assets/og-default.jpg"
|
||||
NEXT_PUBLIC_TWITTER_CARD_TYPE="summary_large_image"
|
||||
|
||||
# Analytics (public ID only)
|
||||
NEXT_PUBLIC_ANALYTICS_ID=""
|
||||
@@ -42,7 +42,7 @@ export const siteConfig = {
|
||||
},
|
||||
slugs: {}
|
||||
},
|
||||
ogImage: process.env.NEXT_PUBLIC_OG_DEFAULT_IMAGE || '/assets/og-default.jpg',
|
||||
ogImage: process.env.NEXT_PUBLIC_OG_DEFAULT_IMAGE || '/assets/og-default.png',
|
||||
twitterCard:
|
||||
(process.env.NEXT_PUBLIC_TWITTER_CARD_TYPE as
|
||||
| 'summary'
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||
|
||||
const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Image optimization configuration
|
||||
@@ -41,4 +35,4 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default withBundleAnalyzer(nextConfig);
|
||||
export default nextConfig;
|
||||
|
||||
291
package-lock.json
generated
291
package-lock.json
generated
@@ -15,11 +15,11 @@
|
||||
"contentlayer2": "^0.5.8",
|
||||
"gray-matter": "^4.0.3",
|
||||
"markdown-wasm": "^1.2.0",
|
||||
"next": "^16.0.3",
|
||||
"next": "^16.0.7",
|
||||
"next-contentlayer2": "^0.5.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
@@ -31,7 +31,6 @@
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^16.0.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
@@ -449,16 +448,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
|
||||
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@effect-ts/core": {
|
||||
"version": "0.60.5",
|
||||
"resolved": "https://registry.npmjs.org/@effect-ts/core/-/core-0.60.5.tgz",
|
||||
@@ -1956,20 +1945,10 @@
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/bundle-analyzer": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.0.3.tgz",
|
||||
"integrity": "sha512-6Xo8f8/ZXtASfTPa6TH1aUn+xDg9Pkyl1YHVxu+89cVdLH7MnYjxv3rPOfEJ9BwCZCU2q4Flyw5MwltfD2pGbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webpack-bundle-analyzer": "4.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz",
|
||||
"integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
|
||||
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -2000,9 +1979,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz",
|
||||
"integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
|
||||
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2016,9 +1995,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz",
|
||||
"integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
|
||||
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2032,9 +2011,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz",
|
||||
"integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
|
||||
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2048,9 +2027,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz",
|
||||
"integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
|
||||
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2064,9 +2043,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz",
|
||||
"integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
|
||||
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2080,9 +2059,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz",
|
||||
"integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
|
||||
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2096,9 +2075,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz",
|
||||
"integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
|
||||
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2112,9 +2091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz",
|
||||
"integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
|
||||
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2547,13 +2526,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
@@ -3429,19 +3401,6 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
@@ -4490,13 +4449,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -4644,13 +4596,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
|
||||
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
@@ -6068,22 +6013,6 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"duplexer": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||
@@ -6410,13 +6339,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
@@ -6851,16 +6773,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -8552,16 +8464,6 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -8622,12 +8524,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz",
|
||||
"integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==",
|
||||
"version": "16.0.7",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
|
||||
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.0.3",
|
||||
"@next/env": "16.0.7",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -8640,14 +8542,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.0.3",
|
||||
"@next/swc-darwin-x64": "16.0.3",
|
||||
"@next/swc-linux-arm64-gnu": "16.0.3",
|
||||
"@next/swc-linux-arm64-musl": "16.0.3",
|
||||
"@next/swc-linux-x64-gnu": "16.0.3",
|
||||
"@next/swc-linux-x64-musl": "16.0.3",
|
||||
"@next/swc-win32-arm64-msvc": "16.0.3",
|
||||
"@next/swc-win32-x64-msvc": "16.0.3",
|
||||
"@next/swc-darwin-arm64": "16.0.7",
|
||||
"@next/swc-darwin-x64": "16.0.7",
|
||||
"@next/swc-linux-arm64-gnu": "16.0.7",
|
||||
"@next/swc-linux-arm64-musl": "16.0.7",
|
||||
"@next/swc-linux-x64-gnu": "16.0.7",
|
||||
"@next/swc-linux-x64-musl": "16.0.7",
|
||||
"@next/swc-win32-arm64-msvc": "16.0.7",
|
||||
"@next/swc-win32-x64-msvc": "16.0.7",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -8922,16 +8824,6 @@
|
||||
"node": ">= 14.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"dev": true,
|
||||
"license": "(WTFPL OR MIT)",
|
||||
"bin": {
|
||||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -9469,9 +9361,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
|
||||
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -9479,16 +9371,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
|
||||
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.0"
|
||||
"react": "^19.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
@@ -10329,21 +10221,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||
@@ -10967,16 +10844,6 @@
|
||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-dump": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
|
||||
@@ -11516,44 +11383,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz",
|
||||
"integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "0.5.7",
|
||||
"acorn": "^8.0.4",
|
||||
"acorn-walk": "^8.0.0",
|
||||
"commander": "^7.2.0",
|
||||
"debounce": "^1.2.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"html-escaper": "^2.0.2",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"opener": "^1.5.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"sirv": "^2.0.3",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -11764,28 +11593,6 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"dev": "concurrently \"contentlayer2 dev\" \"next dev --turbo\"",
|
||||
"sync-assets": "node scripts/sync-assets.mjs",
|
||||
"build": "npm run sync-assets && contentlayer2 build && next build && npx pagefind --site .next && rm -rf public/_pagefind && cp -r .next/pagefind public/_pagefind",
|
||||
"build:analyze": "ANALYZE=true npm run build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"contentlayer": "contentlayer build"
|
||||
@@ -23,11 +22,11 @@
|
||||
"contentlayer2": "^0.5.8",
|
||||
"gray-matter": "^4.0.3",
|
||||
"markdown-wasm": "^1.2.0",
|
||||
"next": "^16.0.3",
|
||||
"next": "^16.0.7",
|
||||
"next-contentlayer2": "^0.5.8",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react": "^19.2.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
@@ -39,7 +38,6 @@
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^16.0.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/react": "^19.2.5",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 485 KiB |
@@ -13,9 +13,24 @@
|
||||
--font-weight-semibold: 600;
|
||||
--font-system-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', 'PingFang TC', 'PingFang SC', 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Segoe UI Variable Text', 'Segoe UI', 'Microsoft JhengHei', 'Microsoft YaHei', 'Noto Sans TC', 'Noto Sans SC', 'Noto Sans CJK TC', 'Noto Sans CJK SC', 'Source Han Sans TC', 'Source Han Sans SC', 'Roboto', 'Ubuntu', 'Cantarell', 'Inter', 'Helvetica Neue', Arial, sans-serif;
|
||||
|
||||
/* Ink + accent palette */
|
||||
--color-ink-strong: #0f172a;
|
||||
--color-ink-body: #1f2937;
|
||||
--color-ink-muted: #475569;
|
||||
--color-accent: #7c3aed;
|
||||
--color-accent-soft: #f4f0ff;
|
||||
|
||||
font-size: clamp(15px, 0.65vw + 11px, 19px);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-ink-strong: #e2e8f0;
|
||||
--color-ink-body: #cbd5e1;
|
||||
--color-ink-muted: #94a3b8;
|
||||
--color-accent: #a78bfa;
|
||||
--color-accent-soft: #1f1a3d;
|
||||
}
|
||||
|
||||
@media (min-width: 2560px) {
|
||||
:root {
|
||||
font-size: 20px;
|
||||
@@ -27,6 +42,7 @@ body {
|
||||
font-size: 1rem;
|
||||
line-height: var(--line-height-body);
|
||||
font-family: var(--font-system-sans);
|
||||
color: var(--color-ink-body);
|
||||
}
|
||||
|
||||
@keyframes timeline-scroll {
|
||||
@@ -95,17 +111,17 @@ body {
|
||||
.prose blockquote {
|
||||
@apply transition-transform transition-shadow duration-180 ease-snappy;
|
||||
border-left: 4px solid var(--color-accent, #2563eb);
|
||||
background: linear-gradient(135deg, rgba(37, 99, 235, 0.04), rgba(37, 99, 235, 0.08));
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.06), rgba(124, 58, 237, 0.1));
|
||||
padding: 1.2rem 1.5rem;
|
||||
font-style: italic;
|
||||
color: rgba(15, 23, 42, 0.75);
|
||||
color: rgba(15, 23, 42, 0.78);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dark .prose blockquote {
|
||||
background: linear-gradient(135deg, rgba(96, 165, 250, 0.12), rgba(96, 165, 250, 0.06));
|
||||
color: rgba(226, 232, 240, 0.8);
|
||||
border-left-color: rgba(96, 165, 250, 0.9);
|
||||
background: linear-gradient(135deg, rgba(167, 139, 250, 0.12), rgba(124, 58, 237, 0.08));
|
||||
color: rgba(226, 232, 240, 0.85);
|
||||
border-left-color: rgba(167, 139, 250, 0.9);
|
||||
}
|
||||
|
||||
.prose blockquote:hover {
|
||||
@@ -134,27 +150,32 @@ body {
|
||||
.prose {
|
||||
font-size: clamp(1rem, 0.2vw + 0.9rem, 1.2rem);
|
||||
line-height: var(--line-height-body);
|
||||
color: var(--color-ink-body);
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: clamp(2.2rem, 1.4rem + 2.2vw, 3.4rem);
|
||||
line-height: 1.25;
|
||||
color: var(--color-ink-strong);
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: clamp(1.8rem, 1.1rem + 1.6vw, 2.8rem);
|
||||
line-height: 1.3;
|
||||
color: var(--color-ink-strong);
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: clamp(1.4rem, 0.9rem + 1vw, 2rem);
|
||||
line-height: 1.35;
|
||||
color: var(--color-ink-strong);
|
||||
}
|
||||
|
||||
.prose p,
|
||||
.prose li {
|
||||
font-size: clamp(1rem, 0.2vw + 0.9rem, 1.15rem);
|
||||
line-height: var(--line-height-body);
|
||||
color: var(--color-ink-body);
|
||||
}
|
||||
|
||||
.prose small,
|
||||
|
||||
Reference in New Issue
Block a user