Refine storyline rail to minimalist arrow layout

This commit is contained in:
2025-11-18 16:54:35 +08:00
parent df10c8b751
commit 91dec52db6

View File

@@ -1,6 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import type { Post } from 'contentlayer/generated'; import type { Post } from 'contentlayer/generated';
import { siteConfig } from '@/lib/config';
interface Props { interface Props {
current: Post; current: Post;
@@ -12,8 +11,8 @@ interface StationConfig {
key: 'older' | 'current' | 'newer'; key: 'older' | 'current' | 'newer';
label: string; label: string;
post?: Post; post?: Post;
hint: string;
rel?: 'prev' | 'next'; rel?: 'prev' | 'next';
subtitle: string;
} }
export function PostStorylineNav({ current, newer, older }: Props) { export function PostStorylineNav({ current, newer, older }: Props) {
@@ -22,32 +21,33 @@ export function PostStorylineNav({ current, newer, older }: Props) {
key: 'older', key: 'older',
label: '上一站', label: '上一站',
post: older, post: older,
hint: older ? `發表於 ${formatDate(older.published_at)}` : '沒有更早的文章', subtitle: older ? '回顧這篇' : '到達起點',
rel: 'prev' rel: 'prev'
}, },
{ {
key: 'current', key: 'current',
label: '你在這裡', label: '你在這裡',
post: current, post: current,
hint: current.published_at ? formatDate(current.published_at) : '草稿' subtitle: '正在閱讀'
}, },
{ {
key: 'newer', key: 'newer',
label: '下一站', label: '下一站',
post: newer, post: newer,
hint: newer ? `發表於 ${formatDate(newer.published_at)}` : '還沒有新文章', subtitle: newer ? '繼續前往' : '尚無新章',
rel: 'next' rel: 'next'
} }
]; ];
return ( return (
<nav aria-label="文章導覽" className="relative mt-10"> <nav aria-label="文章導覽" className="relative mt-10">
<div className="relative overflow-hidden rounded-[32px] border border-slate-200/70 bg-gradient-to-r from-white via-slate-50 to-white p-6 shadow-lg dark:border-slate-800/70 dark:from-slate-900 dark:via-slate-900/80 dark:to-slate-900"> <div className="relative overflow-hidden rounded-[32px] border border-slate-200/70 bg-gradient-to-r from-white via-slate-50 to-white px-6 py-10 shadow-lg dark:border-slate-800/70 dark:from-slate-900 dark:via-slate-900/80 dark:to-slate-900">
<div className="pointer-events-none absolute inset-0 opacity-60"> <div className="pointer-events-none absolute inset-x-10 top-1/2 hidden -translate-y-1/2 items-center gap-3 text-slate-300 dark:text-slate-600 md:flex">
<div className="absolute left-6 right-6 top-1/2 hidden h-px -translate-y-1/2 bg-gradient-to-r from-transparent via-blue-500/30 to-transparent md:block" /> <span className="h-0 w-0 border-y-[7px] border-y-transparent border-r-[14px] border-r-current" />
<div className="absolute left-1/2 top-1/2 hidden h-56 w-56 -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/15 blur-3xl dark:bg-blue-400/15 md:block" /> <span className="flex-1 border-t border-dashed border-current" />
<span className="h-0 w-0 rotate-180 border-y-[7px] border-y-transparent border-r-[14px] border-r-current" />
</div> </div>
<div className="relative grid gap-6 md:grid-cols-3"> <div className="relative grid gap-8 md:grid-cols-3">
{stations.map((station) => ( {stations.map((station) => (
<Station key={station.key} station={station} /> <Station key={station.key} station={station} />
))} ))}
@@ -58,23 +58,17 @@ export function PostStorylineNav({ current, newer, older }: Props) {
} }
function Station({ station }: { station: StationConfig }) { function Station({ station }: { station: StationConfig }) {
const { post, label, hint, key, rel } = station; const { post, label, rel, subtitle, key } = station;
const isCurrent = key === 'current'; const isCurrent = key === 'current';
const base = `group flex flex-col gap-3 rounded-3xl px-4 py-5 transition duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 dark:focus-visible:ring-blue-300/70 ${ const capsule = (
isCurrent
? 'bg-white/80 shadow-md ring-1 ring-blue-200/60 dark:bg-slate-900/80 dark:ring-blue-500/20'
: 'bg-white/60 shadow-sm hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-900/70'
}`;
const circle = (
<span <span
className={`flex h-12 w-12 items-center justify-center rounded-full border text-xs font-semibold uppercase tracking-[0.3em] ${ className={`flex h-14 w-14 items-center justify-center rounded-full border-2 text-[11px] font-semibold uppercase tracking-[0.35em] transition duration-300 ${
isCurrent isCurrent
? 'border-blue-500 bg-blue-500 text-white shadow-[0_0_0_8px_rgba(59,130,246,0.15)]' ? 'border-blue-500 bg-blue-500 text-white shadow-[0_0_20px_rgba(59,130,246,0.35)]'
: post : post
? 'border-slate-300 bg-white text-slate-500 dark:border-slate-700 dark:bg-slate-900' ? 'border-slate-300 bg-white text-slate-500 dark:border-slate-700 dark:bg-slate-900'
: 'border-dashed border-slate-300 text-slate-300 dark:border-slate-700' : 'border-dashed border-slate-300 text-slate-400 dark:border-slate-700'
}`} }`}
> >
{label === '下一站' ? 'NEXT' : label === '上一站' ? 'PREV' : 'NOW'} {label === '下一站' ? 'NEXT' : label === '上一站' ? 'PREV' : 'NOW'}
@@ -83,40 +77,33 @@ function Station({ station }: { station: StationConfig }) {
if (!post) { if (!post) {
return ( return (
<div className={`${base} cursor-default opacity-60`}> <div className="flex flex-col items-center gap-3 text-center text-slate-400">
{circle} {capsule}
<p className="text-sm text-slate-500 dark:text-slate-400">{hint}</p> <p className="text-xs tracking-[0.35em] uppercase">{label}</p>
<p className="text-base font-semibold text-slate-400">{subtitle}</p>
</div> </div>
); );
} }
return ( return (
<Link href={post.url} rel={rel} className={`${base} text-slate-900 dark:text-slate-100`}> <Link
<div className="flex items-center gap-4"> href={post.url}
{circle} rel={rel}
<div className="text-xs uppercase tracking-[0.35em] text-slate-400 dark:text-slate-500"> className="group flex flex-col items-center gap-3 text-center text-slate-900 transition duration-300 hover:-translate-y-1 hover:text-blue-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 dark:text-slate-50"
{label} >
</div> <span className="relative flex items-center">
</div> {capsule}
<p className="text-lg font-semibold leading-snug tracking-tight text-slate-900 transition-colors group-hover:text-blue-600 dark:text-slate-50 dark:group-hover:text-blue-300"> <span className={`absolute inset-x-2 top-full mt-2 h-1 rounded-full transition ${isCurrent ? 'bg-blue-500/70' : 'bg-transparent group-hover:bg-blue-400/60'}`} />
</span>
<p className="text-[11px] uppercase tracking-[0.4em] text-slate-400 dark:text-slate-500">
{label}
</p>
<p className="text-lg font-semibold leading-snug tracking-tight">
{post.title} {post.title}
</p> </p>
<div className="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400"> <span className="text-xs text-slate-500 transition group-hover:text-blue-500 dark:text-slate-400">
<span>{hint}</span> {subtitle}
<span className="text-sm font-medium text-blue-600 opacity-0 transition group-hover:opacity-100 dark:text-blue-400"> </span>
{label === '下一站' ? '→ 前往' : label === '上一站' ? '回顧 ←' : '正在閱讀'}
</span>
</div>
</Link> </Link>
); );
} }
function formatDate(input?: string | Date) {
if (!input) return '';
const date = input instanceof Date ? input : new Date(input);
return date.toLocaleDateString(siteConfig.defaultLocale ?? 'zh-TW', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}