Skip to content

Commit 0032e12

Browse files
authored
Merge pull request #6 from KYJCASTER/post/dubstep-bass
feat(home): 扉页作为首页入场,向下平滑滑入原首页
2 parents 9a3fb23 + 74313d7 commit 0032e12

4 files changed

Lines changed: 342 additions & 285 deletions

File tree

src/app/frontispiece/page.tsx

Lines changed: 3 additions & 280 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type { Metadata } from "next"
2-
import Link from "next/link"
32

4-
import { getPublishedPosts, getAllSeries } from "@/lib/posts"
5-
import { computeReadingStats } from "@/lib/reading-time"
6-
import { WaxSeal } from "@/components/wax-seal"
3+
import { Frontispiece } from "@/components/frontispiece"
74
import { site } from "@/lib/site"
85

96
export const metadata: Metadata = {
@@ -12,284 +9,10 @@ export const metadata: Metadata = {
129
alternates: { canonical: "/frontispiece" },
1310
}
1411

15-
// Roman numerals up to 3999 — sufficient for years and post counts.
16-
function toRoman(n: number): string {
17-
if (n <= 0) return ""
18-
const map: [number, string][] = [
19-
[1000, "M"], [900, "CM"], [500, "D"], [400, "CD"],
20-
[100, "C"], [90, "XC"], [50, "L"], [40, "XL"],
21-
[10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"],
22-
]
23-
let result = ""
24-
let rem = n
25-
for (const [v, s] of map) {
26-
while (rem >= v) {
27-
result += s
28-
rem -= v
29-
}
30-
}
31-
return result
32-
}
33-
34-
const MONTH_LATIN = [
35-
"Ianuarius", "Februarius", "Martius", "Aprilis", "Maius", "Iunius",
36-
"Iulius", "Augustus", "September", "October", "November", "December",
37-
] as const
38-
3912
export default function FrontispiecePage() {
40-
const posts = getPublishedPosts()
41-
const series = getAllSeries()
42-
43-
// CJK-aware total word count — sum across the whole corpus.
44-
let totalWords = 0
45-
for (const p of posts) totalWords += computeReadingStats(p.content).totalWords
46-
47-
// Earliest post date drives the "since" line. Posts are date-DESC.
48-
const earliest = posts.length ? posts[posts.length - 1].date : ""
49-
const earliestYear = earliest ? parseInt(earliest.slice(0, 4), 10) : 0
50-
const earliestMonth = earliest ? parseInt(earliest.slice(5, 7), 10) : 0
51-
const earliestLabel = earliest
52-
? `${MONTH_LATIN[earliestMonth - 1]} ${toRoman(earliestYear)}`
53-
: ""
54-
55-
const currentYear = new Date().getFullYear()
56-
const yearsSpan = earliest
57-
? Math.max(1, currentYear - earliestYear + 1)
58-
: 0
59-
6013
return (
61-
<main className="relative min-h-screen pt-24 pb-20 px-5 sm:px-6 overflow-hidden">
62-
{/* Soft parchment vignettes — same vocabulary as the home hero */}
63-
<div
64-
aria-hidden
65-
className="absolute inset-x-0 top-0 h-[65vh] pointer-events-none"
66-
style={{
67-
backgroundImage:
68-
"radial-gradient(ellipse 50% 40% at 100% 0%, rgba(194, 65, 12, 0.05), transparent 60%), " +
69-
"radial-gradient(ellipse 45% 35% at 0% 100%, rgba(180, 83, 9, 0.04), transparent 65%)",
70-
}}
71-
/>
72-
73-
<article className="relative max-w-3xl mx-auto">
74-
{/* ============ Outer page frame ============
75-
A double rule with the upper-left flourish drawn in.
76-
The flourish is a single SVG path that strokes-in over a
77-
second, giving the impression of a hand sketching the
78-
border the moment the page is opened. */}
79-
<div
80-
className="relative border hairline-strong rounded-sm bg-card/40 px-8 sm:px-14 py-14 sm:py-20"
81-
style={{ outline: "1px solid var(--border)", outlineOffset: "6px" }}
82-
>
83-
{/* Corner flourish — top-left. Path is drawn with stroke-dash. */}
84-
<svg
85-
aria-hidden
86-
viewBox="0 0 80 80"
87-
className="absolute -top-1 -left-1 w-12 sm:w-16 text-primary/65"
88-
fill="none"
89-
>
90-
<path
91-
d="M 4 30 C 4 14, 14 4, 30 4 M 8 14 Q 14 8, 22 6 M 4 24 L 4 18 L 10 18"
92-
stroke="currentColor"
93-
strokeWidth="1.4"
94-
strokeLinecap="round"
95-
style={{
96-
strokeDasharray: 120,
97-
strokeDashoffset: 120,
98-
animation: "swash-draw 1.4s 0.3s var(--ease-out) forwards",
99-
}}
100-
/>
101-
</svg>
102-
{/* Mirrored flourish — bottom-right */}
103-
<svg
104-
aria-hidden
105-
viewBox="0 0 80 80"
106-
className="absolute -bottom-1 -right-1 w-12 sm:w-16 text-primary/65 rotate-180"
107-
fill="none"
108-
>
109-
<path
110-
d="M 4 30 C 4 14, 14 4, 30 4 M 8 14 Q 14 8, 22 6 M 4 24 L 4 18 L 10 18"
111-
stroke="currentColor"
112-
strokeWidth="1.4"
113-
strokeLinecap="round"
114-
style={{
115-
strokeDasharray: 120,
116-
strokeDashoffset: 120,
117-
animation: "swash-draw 1.4s 0.6s var(--ease-out) forwards",
118-
}}
119-
/>
120-
</svg>
121-
122-
{/* ============ Press imprint ============ */}
123-
<div className="text-center animate-fade-in-up">
124-
<p className="font-mono text-[11px] sm:text-xs uppercase tracking-[0.42em] text-muted">
125-
Kairos &nbsp;·&nbsp; Press
126-
</p>
127-
<div className="flex items-center justify-center gap-3 mt-2">
128-
<span aria-hidden className="block w-8 h-px bg-border-strong/60" />
129-
<span className="font-mono text-[10px] uppercase tracking-[0.32em] text-muted-light">
130-
Open Access · {toRoman(currentYear)}
131-
</span>
132-
<span aria-hidden className="block w-8 h-px bg-border-strong/60" />
133-
</div>
134-
</div>
135-
136-
{/* ============ The mark ============ */}
137-
<div
138-
className="flex justify-center my-12 sm:my-16 animate-fade-in-up"
139-
style={{ animationDelay: "0.18s" }}
140-
>
141-
<WaxSeal />
142-
</div>
143-
144-
{/* ============ Bilingual epigraph ============ */}
145-
<div
146-
className="text-center animate-fade-in-up"
147-
style={{ animationDelay: "0.34s" }}
148-
>
149-
<p className="serif italic text-2xl sm:text-3xl text-foreground tracking-tight leading-snug">
150-
Verba volant,
151-
<br className="sm:hidden" />
152-
<span className="sm:ml-2">scripta manent.</span>
153-
</p>
154-
<p className="serif text-base sm:text-lg text-muted mt-4 leading-relaxed">
155-
口舌之言易散,
156-
<br className="sm:hidden" />
157-
<span className="sm:ml-1">笔墨之迹长存。</span>
158-
</p>
159-
<p className="font-mono text-[10px] uppercase tracking-[0.3em] text-muted-light mt-5">
160-
— proverbium romanum
161-
</p>
162-
</div>
163-
164-
{/* ============ Volume colophon ============
165-
The "this volume contains" line — uses real corpus data.
166-
Numbers in serif tabular nums, labels in mono small-caps.
167-
Reads like the verso of a real book's title page. */}
168-
<div
169-
className="mt-14 sm:mt-16 grid grid-cols-3 gap-6 sm:gap-10 animate-fade-in-up"
170-
style={{ animationDelay: "0.5s" }}
171-
>
172-
<Stat
173-
label="Volumina"
174-
sub="篇"
175-
value={posts.length.toString()}
176-
roman={toRoman(posts.length)}
177-
/>
178-
<Stat
179-
label="Verba"
180-
sub="字"
181-
value={totalWords.toLocaleString("en-US")}
182-
roman={null}
183-
/>
184-
<Stat
185-
label="Anno"
186-
sub="年"
187-
value={yearsSpan.toString()}
188-
roman={toRoman(yearsSpan)}
189-
/>
190-
</div>
191-
192-
{/* ============ Provenance line ============ */}
193-
<div
194-
className="mt-14 sm:mt-16 flex flex-col items-center text-center gap-2 animate-fade-in-up"
195-
style={{ animationDelay: "0.66s" }}
196-
>
197-
<span aria-hidden className="block w-10 h-px bg-border-strong/60" />
198-
<p className="font-mono text-[10px] sm:text-[11px] uppercase tracking-[0.28em] text-muted-light leading-relaxed">
199-
Imprinted by {site.author} &nbsp;·&nbsp; Kaifeng, Henan
200-
</p>
201-
<p className="font-mono text-[10px] uppercase tracking-[0.28em] text-muted-light leading-relaxed">
202-
{earliestLabel
203-
? <>Since {earliestLabel} &nbsp;·&nbsp; {series.length} {series.length === 1 ? "Series" : "Series"}</>
204-
: <>Sub Astris Apertis</>}
205-
</p>
206-
</div>
207-
208-
{/* ============ "Turn the page" CTA ============ */}
209-
<div
210-
className="mt-14 sm:mt-16 flex flex-col items-center gap-4 animate-fade-in-up"
211-
style={{ animationDelay: "0.82s" }}
212-
>
213-
<Link
214-
href="/blog"
215-
className="group relative inline-flex items-center gap-3 serif italic text-lg text-foreground hover:text-primary transition-colors"
216-
>
217-
<span className="text-primary/70 group-hover:text-primary transition-colors">
218-
219-
</span>
220-
<span>翻开书页</span>
221-
<span aria-hidden className="font-mono not-italic text-xs uppercase tracking-[0.28em] text-muted-light group-hover:text-primary/80 transition-colors">
222-
Turn →
223-
</span>
224-
<span className="text-primary/70 group-hover:text-primary transition-colors">
225-
226-
</span>
227-
</Link>
228-
<p className="font-mono text-[10px] uppercase tracking-[0.24em] text-muted-light">
229-
or wander · &nbsp;
230-
<Link
231-
href="/archive"
232-
className="hover:text-foreground transition-colors"
233-
>
234-
/archive
235-
</Link>
236-
&nbsp;·&nbsp;
237-
<Link
238-
href="/series"
239-
className="hover:text-foreground transition-colors"
240-
>
241-
/series
242-
</Link>
243-
&nbsp;·&nbsp;
244-
<Link
245-
href="/curriculum"
246-
className="hover:text-foreground transition-colors"
247-
>
248-
/curriculum
249-
</Link>
250-
</p>
251-
</div>
252-
</div>
253-
254-
{/* Folio number — like the bottom of every right-hand page in
255-
an old book. Always reads "I" because this is page one. */}
256-
<p className="mt-6 text-center font-mono text-[10px] uppercase tracking-[0.32em] text-muted-light tabular-nums">
257-
— &nbsp; folio &nbsp;I&nbsp; —
258-
</p>
259-
</article>
14+
<main>
15+
<Frontispiece mode="page" />
26016
</main>
26117
)
26218
}
263-
264-
function Stat({
265-
label,
266-
sub,
267-
value,
268-
roman,
269-
}: {
270-
label: string
271-
sub: string
272-
value: string
273-
roman: string | null
274-
}) {
275-
return (
276-
<div className="text-center">
277-
<p className="font-mono text-[10px] uppercase tracking-[0.3em] text-muted-light mb-2">
278-
{label}
279-
</p>
280-
<p className="serif text-3xl sm:text-4xl font-semibold text-foreground tabular-nums leading-none">
281-
{value}
282-
</p>
283-
{roman ? (
284-
<p className="font-mono text-[10px] uppercase tracking-[0.3em] text-primary/65 mt-2 tabular-nums">
285-
{roman}
286-
</p>
287-
) : (
288-
<p className="font-mono text-[10px] uppercase tracking-[0.3em] text-muted-light/60 mt-2">
289-
·
290-
</p>
291-
)}
292-
<p className="serif italic text-xs text-muted mt-1">{sub}</p>
293-
</div>
294-
)
295-
}

src/app/globals.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,14 @@ html .shiki, html .shiki span { color: var(--shiki-light); background-color: tra
10041004
100% { transform: scale(1) rotate(0deg); }
10051005
}
10061006

1007+
/* Frontispiece scroll hint — a softly bobbing chevron beneath the
1008+
"folio I" line, suggesting there's more below. Loops gently so it
1009+
never demands attention; reduced-motion users get it stilled. */
1010+
@keyframes scroll-hint {
1011+
0%, 100% { transform: translateY(0); opacity: 0.4; }
1012+
50% { transform: translateY(4px); opacity: 0.8; }
1013+
}
1014+
10071015
/* Cover panel — monogram + corner stamp behave like an ink stamp:
10081016
resting state is a faint impression; on group hover the stamp gets
10091017
"pressed in" — slightly darker and rotated by a degree or two, like

src/app/page.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Frontispiece } from "@/components/frontispiece"
12
import { HeroSection } from "./sections/hero-section"
23
import { LatestPosts } from "./sections/latest-posts"
34
import { AboutPreview } from "./sections/about-preview"
@@ -6,11 +7,20 @@ import { SectionOrnament } from "@/components/section-ornament"
67
export default function Home() {
78
return (
89
<div className="min-h-screen">
9-
<HeroSection />
10-
<SectionOrnament />
11-
<LatestPosts />
12-
<SectionOrnament variant="rule" />
13-
<AboutPreview />
10+
{/* Frontispiece: occupies the first viewport as an entry curtain.
11+
Its "Turn ↓" CTA anchors to #beyond below, smooth-scrolling
12+
the reader into the home content. */}
13+
<Frontispiece mode="intro" />
14+
15+
{/* The fold beyond the frontispiece — the home page proper. The
16+
id is the anchor target for the frontispiece's Turn CTA. */}
17+
<div id="beyond" className="scroll-mt-20">
18+
<HeroSection />
19+
<SectionOrnament />
20+
<LatestPosts />
21+
<SectionOrnament variant="rule" />
22+
<AboutPreview />
23+
</div>
1424
</div>
1525
)
1626
}

0 commit comments

Comments
 (0)