hd-garden 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,8 @@ const focus = about.focus ?? [
19
19
  const cta = about.cta ?? { href: "/blog", label: "从博文开始" };
20
20
 
21
21
  export const metadata = {
22
- title: about.title,
23
- description: about.subtitle,
22
+ title: { absolute: siteConfig.name },
23
+ description: about.subtitle || siteConfig.description,
24
24
  };
25
25
 
26
26
  export default function AboutPage() {
package/app/blog/page.tsx CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Suspense } from "react";
2
- import { siteConfig } from "../site.config";
2
+ import { siteConfig, siteDocumentTitle } from "../site.config";
3
3
  import { BlogArchive } from "../components/BlogArchive";
4
4
 
5
5
  export const metadata = {
6
- title: siteConfig.pages.blog.title,
7
- description: siteConfig.pages.blog.subtitle,
6
+ title: siteDocumentTitle,
7
+ description: siteConfig.pages.blog.subtitle || siteConfig.description,
8
8
  };
9
9
 
10
10
  export default function BlogIndex() {
@@ -1,13 +1,14 @@
1
+ /* eslint-disable @next/next/no-img-element -- static GitHub Pages assets must keep their exact paths. */
1
2
  import Link from "next/link";
2
3
  import { ArrowRight } from "@phosphor-icons/react/ssr";
3
4
  import { columnHref, columns } from "../lib/content";
4
5
  import { PageIntro } from "../components/PageIntro";
5
6
  import { ContentEmptyState } from "../components/ContentEmptyState";
6
- import { siteConfig } from "../site.config";
7
+ import { siteConfig, siteDocumentTitle } from "../site.config";
7
8
 
8
9
  export const metadata = {
9
- title: siteConfig.pages.columns.title,
10
- description: siteConfig.pages.columns.subtitle,
10
+ title: siteDocumentTitle,
11
+ description: siteConfig.pages.columns.subtitle || siteConfig.description,
11
12
  };
12
13
 
13
14
  export default function ColumnsPage() {
@@ -33,35 +34,45 @@ export default function ColumnsPage() {
33
34
  />
34
35
 
35
36
  {grouped.length > 0 ? <div className="column-feature-list">
36
- {grouped.map(({ slug, entries, first }, index) => (
37
- <Link
38
- href={columnHref(first)}
39
- className="column-feature"
40
- key={slug}
41
- >
42
- <div className="column-feature-copy">
43
- <span>
44
- {first.columnStatus ?? "持续更新"} · {entries.length} 篇 ·{" "}
45
- {first.topic ?? "主题写作"}
46
- </span>
47
- <h2>{first.columnTitle}</h2>
48
- <p>
49
- {first.columnDescription ??
50
- first.description ??
51
- "沿着一条清晰的阅读路径,把一个主题持续写深。"}
52
- </p>
53
- <strong>
54
- 从第一篇开始
55
- <ArrowRight size={17} />
56
- </strong>
57
- </div>
58
- <div className="column-feature-index">
59
- <span>主题专栏</span>
60
- <strong>{String(index + 1).padStart(2, "0")}</strong>
61
- <small>{slug.replaceAll("-", " ").toUpperCase()}</small>
62
- </div>
63
- </Link>
64
- ))}
37
+ {grouped.map(({ slug, entries, first }, index) => {
38
+ const cover = first.columnCover
39
+ ? { src: first.columnCover, alt: first.columnCoverAlt ?? `${first.columnTitle} 专栏封面` }
40
+ : undefined;
41
+ return (
42
+ <Link
43
+ href={columnHref(first)}
44
+ className="column-feature"
45
+ key={slug}
46
+ >
47
+ <div className="column-feature-copy">
48
+ <span>
49
+ {first.columnStatus ?? "持续更新"} · {entries.length} 篇 ·{" "}
50
+ {first.topic ?? "主题写作"}
51
+ </span>
52
+ <h2>{first.columnTitle}</h2>
53
+ <p>
54
+ {first.columnDescription ??
55
+ first.description ??
56
+ "沿着一条清晰的阅读路径,把一个主题持续写深。"}
57
+ </p>
58
+ <strong>
59
+ 从第一篇开始
60
+ <ArrowRight size={17} />
61
+ </strong>
62
+ </div>
63
+ <div className="column-feature-index">
64
+ {cover && (
65
+ <img className="column-feature-cover" src={cover.src} alt={cover.alt} />
66
+ )}
67
+ <div className="column-feature-index-label">
68
+ <span>主题专栏</span>
69
+ <strong>{String(index + 1).padStart(2, "0")}</strong>
70
+ <small>{slug.replaceAll("-", " ").toUpperCase()}</small>
71
+ </div>
72
+ </div>
73
+ </Link>
74
+ );
75
+ })}
65
76
  </div> : <ContentEmptyState kind="columns" />}
66
77
  </main>
67
78
  );
@@ -2,7 +2,6 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { useSearchParams } from "next/navigation";
5
- import { useEffect, useState } from "react";
6
5
  import { ArrowLeft, ArrowRight, PushPin } from "@phosphor-icons/react";
7
6
  import { formatDate, postHref, posts, topics } from "../lib/content";
8
7
  import { ContentEmptyState } from "./ContentEmptyState";
@@ -19,19 +18,13 @@ export const archiveHref = (topic?: string, page?: number) => {
19
18
 
20
19
  export function BlogArchive() {
21
20
  const searchParams = useSearchParams();
22
- const [ready, setReady] = useState(false);
23
-
24
- useEffect(() => {
25
- setReady(true);
26
- }, []);
27
-
28
- const requestedTopic = ready ? (searchParams.get("topic") ?? "") : "";
21
+ const requestedTopic = searchParams.get("topic") ?? "";
29
22
  const selectedTopic = topics.includes(requestedTopic) ? requestedTopic : undefined;
30
23
  const filteredPosts = selectedTopic
31
24
  ? posts.filter((post) => post.topic === selectedTopic)
32
25
  : posts;
33
26
  const pageCount = Math.max(1, Math.ceil(filteredPosts.length / pageSize));
34
- const parsedPage = Number.parseInt((ready ? searchParams.get("page") : null) ?? "1", 10);
27
+ const parsedPage = Number.parseInt(searchParams.get("page") ?? "1", 10);
35
28
  const currentPage = Math.min(
36
29
  pageCount,
37
30
  Math.max(1, Number.isFinite(parsedPage) ? parsedPage : 1),
@@ -11,6 +11,7 @@ import rehypeSlug from "rehype-slug";
11
11
  import rehypeAutolinkHeadings from "rehype-autolink-headings";
12
12
  import rehypeHighlight from "rehype-highlight";
13
13
  import { MermaidDiagram } from "./MermaidDiagram";
14
+ import { MarkdownImage } from "./MarkdownImage";
14
15
  import { ExternalEmbed } from "./ExternalEmbed";
15
16
  import { WidgetChart } from "./WidgetChart";
16
17
 
@@ -106,11 +107,7 @@ export function MarkdownArticle({ content }: { content: string }) {
106
107
  </a>
107
108
  );
108
109
  },
109
- img: ({ alt, ...props }) => (
110
- // Native images keep Typora-authored relative paths intact.
111
- // eslint-disable-next-line @next/next/no-img-element
112
- <img loading="lazy" alt={alt ?? ""} {...props} />
113
- ),
110
+ img: MarkdownImage,
114
111
  code: CodeBlock,
115
112
  pre: ({ children, ...props }) => {
116
113
  const child = Children.only(children);
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type ComponentPropsWithoutRef } from "react";
4
+
5
+ type MarkdownImageProps = ComponentPropsWithoutRef<"img">;
6
+
7
+ export function MarkdownImage({ alt = "", src, ...props }: MarkdownImageProps) {
8
+ const [open, setOpen] = useState(false);
9
+ const description = alt || "查看原图";
10
+
11
+ useEffect(() => {
12
+ if (!open) return undefined;
13
+
14
+ const closeOnEscape = (event: KeyboardEvent) => {
15
+ if (event.key === "Escape") setOpen(false);
16
+ };
17
+ document.addEventListener("keydown", closeOnEscape);
18
+ document.body.classList.add("image-preview-open");
19
+
20
+ return () => {
21
+ document.removeEventListener("keydown", closeOnEscape);
22
+ document.body.classList.remove("image-preview-open");
23
+ };
24
+ }, [open]);
25
+
26
+ if (!src) return null;
27
+
28
+ return (
29
+ <>
30
+ <button
31
+ type="button"
32
+ className="markdown-image-trigger"
33
+ onClick={() => setOpen(true)}
34
+ aria-label={`放大查看:${description}`}
35
+ >
36
+ {/* Native images preserve Typora-authored relative paths. */}
37
+ {/* eslint-disable-next-line @next/next/no-img-element */}
38
+ <img loading="lazy" alt={alt} src={src} {...props} />
39
+ </button>
40
+
41
+ {open ? (
42
+ <div
43
+ className="markdown-image-lightbox"
44
+ role="dialog"
45
+ aria-modal="true"
46
+ aria-label={description}
47
+ onClick={() => setOpen(false)}
48
+ >
49
+ <div className="markdown-image-lightbox-content" onClick={(event) => event.stopPropagation()}>
50
+ {/* eslint-disable-next-line @next/next/no-img-element */}
51
+ <img src={src} alt={alt} {...props} />
52
+ <button
53
+ type="button"
54
+ className="markdown-image-lightbox-close"
55
+ onClick={() => setOpen(false)}
56
+ aria-label="关闭图片预览"
57
+ >
58
+ 关闭
59
+ </button>
60
+ </div>
61
+ </div>
62
+ ) : null}
63
+ </>
64
+ );
65
+ }
@@ -7,7 +7,7 @@ import { TableOfContents } from "../components/TableOfContents";
7
7
  import { siteConfig } from "../site.config";
8
8
 
9
9
  export const metadata = {
10
- title: `Garden · ${siteConfig.pages.garden.title}`,
10
+ title: "Garden",
11
11
  description:
12
12
  "一个以 Markdown 为内容源、通过 GitHub Pages 发布的个人博客框架,支持博文、随想、专栏、搜索、RSS 与多媒体。",
13
13
  };
package/app/globals.css CHANGED
@@ -250,6 +250,11 @@ button {
250
250
  margin-top: 72px;
251
251
  }
252
252
 
253
+ .home-thought-list {
254
+ display: grid;
255
+ gap: 24px;
256
+ }
257
+
253
258
  .section-heading {
254
259
  min-height: 40px;
255
260
  margin-bottom: 21px;
@@ -1443,7 +1448,7 @@ button {
1443
1448
  }
1444
1449
 
1445
1450
  .archive-row.with-cover {
1446
- grid-template-columns: 128px minmax(0, 1fr) 20px;
1451
+ grid-template-columns: 160px minmax(0, 1fr) 20px;
1447
1452
  }
1448
1453
 
1449
1454
  .archive-row:hover {
@@ -1469,10 +1474,9 @@ button {
1469
1474
  }
1470
1475
 
1471
1476
  .archive-cover {
1472
- width: 128px;
1473
- height: 84px;
1477
+ width: 160px;
1478
+ height: auto;
1474
1479
  display: block;
1475
- object-fit: cover;
1476
1480
  border: 1px solid var(--line);
1477
1481
  }
1478
1482
 
@@ -1752,23 +1756,54 @@ button {
1752
1756
  }
1753
1757
 
1754
1758
  .column-feature-index {
1759
+ position: relative;
1760
+ overflow: hidden;
1761
+ background: var(--ink);
1762
+ color: var(--paper);
1763
+ }
1764
+
1765
+ .column-feature-cover {
1766
+ width: 100%;
1767
+ height: 100%;
1768
+ display: block;
1769
+ object-fit: cover;
1770
+ object-position: center;
1771
+ transition: transform 280ms ease;
1772
+ }
1773
+
1774
+ .column-feature:hover .column-feature-cover,
1775
+ .column-feature:focus-visible .column-feature-cover {
1776
+ transform: scale(1.035);
1777
+ }
1778
+
1779
+ .column-feature-index::after {
1780
+ content: "";
1781
+ position: absolute;
1782
+ inset: 0;
1783
+ background: linear-gradient(180deg, rgb(16 18 23 / 20%), rgb(16 18 23 / 8%) 48%, rgb(16 18 23 / 60%));
1784
+ pointer-events: none;
1785
+ }
1786
+
1787
+ .column-feature-index-label {
1788
+ position: absolute;
1789
+ z-index: 1;
1790
+ inset: 0;
1755
1791
  padding: 28px;
1756
1792
  display: flex;
1757
1793
  flex-direction: column;
1758
1794
  justify-content: space-between;
1759
- background: var(--ink);
1760
- color: var(--paper);
1761
1795
  }
1762
1796
 
1763
- .column-feature-index span,
1764
- .column-feature-index small {
1797
+ .column-feature-index-label span,
1798
+ .column-feature-index-label small {
1765
1799
  color: var(--faint);
1766
1800
  font-size: 9px;
1767
1801
  letter-spacing: 0.08em;
1768
1802
  }
1769
1803
 
1770
- .column-feature-index strong {
1804
+ .column-feature-index-label strong {
1771
1805
  align-self: center;
1806
+ color: white;
1772
1807
  font-size: 82px;
1773
1808
  line-height: 1;
1774
1809
  }
@@ -1805,10 +1840,9 @@ button {
1805
1840
 
1806
1841
  .article-hero img {
1807
1842
  width: 100%;
1808
- aspect-ratio: 16 / 8.5;
1843
+ height: auto;
1809
1844
  display: block;
1810
- object-fit: cover;
1811
- border: 1px solid var(--line);
1845
+ border: 0;
1812
1846
  }
1813
1847
 
1814
1848
  .post-reading-page .article-summary-row {
@@ -1854,14 +1888,6 @@ button {
1854
1888
  margin: 28px 0 34px;
1855
1889
  }
1856
1890
 
1857
- .article-cover img {
1858
- width: 100%;
1859
- aspect-ratio: 16 / 8.5;
1860
- display: block;
1861
- object-fit: cover;
1862
- border: 1px solid var(--line);
1863
- }
1864
-
1865
1891
  .article-header h1 {
1866
1892
  margin: 0;
1867
1893
  font-size: clamp(32px, 4.2vw, 48px);
@@ -2107,6 +2133,35 @@ button {
2107
2133
  padding-left: 25px;
2108
2134
  }
2109
2135
 
2136
+ .markdown-body ul {
2137
+ list-style: disc outside;
2138
+ }
2139
+
2140
+ .markdown-body ol {
2141
+ list-style: decimal outside;
2142
+ }
2143
+
2144
+ .markdown-body ul ul {
2145
+ list-style-type: circle;
2146
+ }
2147
+
2148
+ .markdown-body ul ul ul {
2149
+ list-style-type: square;
2150
+ }
2151
+
2152
+ .markdown-body ol ol {
2153
+ list-style-type: lower-alpha;
2154
+ }
2155
+
2156
+ .markdown-body ol ol ol {
2157
+ list-style-type: lower-roman;
2158
+ }
2159
+
2160
+ .markdown-body ul.contains-task-list {
2161
+ padding-left: 0;
2162
+ list-style: none;
2163
+ }
2164
+
2110
2165
  .markdown-body li {
2111
2166
  margin: 7px 0;
2112
2167
  }
@@ -2498,7 +2553,68 @@ button {
2498
2553
  padding-block: 0;
2499
2554
  }
2500
2555
 
2501
- .markdown-body img {
2556
+ .markdown-image-trigger {
2557
+ display: block;
2558
+ max-width: 100%;
2559
+ margin: 34px auto;
2560
+ padding: 0;
2561
+ border: 0;
2562
+ background: transparent;
2563
+ cursor: zoom-in;
2564
+ }
2565
+
2566
+ .markdown-image-trigger img {
2567
+ display: block;
2568
+ max-width: 100%;
2569
+ height: auto;
2570
+ margin: 0;
2571
+ }
2572
+
2573
+ .markdown-image-lightbox {
2574
+ position: fixed;
2575
+ z-index: 100;
2576
+ inset: 0;
2577
+ display: grid;
2578
+ place-items: center;
2579
+ padding: 32px;
2580
+ background: color-mix(in srgb, var(--ink) 82%, transparent);
2581
+ cursor: zoom-out;
2582
+ }
2583
+
2584
+ .markdown-image-lightbox-content {
2585
+ position: relative;
2586
+ max-width: min(1440px, 100%);
2587
+ max-height: 100%;
2588
+ }
2589
+
2590
+ .markdown-image-lightbox-content img {
2591
+ display: block;
2592
+ max-width: 100%;
2593
+ max-height: calc(100vh - 64px);
2594
+ width: auto;
2595
+ height: auto;
2596
+ margin: 0;
2597
+ object-fit: contain;
2598
+ cursor: default;
2599
+ }
2600
+
2601
+ .markdown-image-lightbox-close {
2602
+ position: absolute;
2603
+ top: 12px;
2604
+ right: 12px;
2605
+ padding: 7px 10px;
2606
+ border: 1px solid color-mix(in srgb, white 60%, transparent);
2607
+ background: color-mix(in srgb, var(--ink) 72%, transparent);
2608
+ color: white;
2609
+ font-size: 12px;
2610
+ cursor: pointer;
2611
+ }
2612
+
2613
+ .image-preview-open {
2614
+ overflow: hidden;
2615
+ }
2616
+
2617
+ .markdown-body > img {
2502
2618
  max-width: 100%;
2503
2619
  height: auto;
2504
2620
  margin: 34px auto;
@@ -3975,12 +4091,12 @@ button {
3975
4091
  }
3976
4092
 
3977
4093
  .archive-row.with-cover {
3978
- grid-template-columns: 96px minmax(0, 1fr) 16px;
4094
+ grid-template-columns: 112px minmax(0, 1fr) 16px;
3979
4095
  }
3980
4096
 
3981
4097
  .archive-cover {
3982
- width: 96px;
3983
- height: 68px;
4098
+ width: 112px;
4099
+ height: auto;
3984
4100
  }
3985
4101
 
3986
4102
  .archive-meta {
@@ -4023,15 +4139,15 @@ button {
4023
4139
  }
4024
4140
 
4025
4141
  .column-feature-index {
4026
- min-height: 130px;
4027
- display: grid;
4028
- grid-template-columns: 1fr auto;
4029
- align-items: center;
4142
+ aspect-ratio: 4 / 5;
4143
+ min-height: 0;
4030
4144
  }
4031
4145
 
4032
- .column-feature-index strong {
4033
- grid-column: 2;
4034
- grid-row: 1 / 3;
4146
+ .column-feature-index-label {
4147
+ padding: 20px;
4148
+ }
4149
+
4150
+ .column-feature-index-label strong {
4035
4151
  font-size: 58px;
4036
4152
  }
4037
4153
 
package/app/page.tsx CHANGED
@@ -9,10 +9,10 @@ import { columnHref, columns, formatDate, postHref, posts } from "./lib/content"
9
9
  import { ThoughtCard } from "./components/ThoughtCard";
10
10
  import { PageIntro } from "./components/PageIntro";
11
11
  import { getPublishedThoughts } from "./lib/public-thoughts";
12
- import { githubUrl, siteConfig } from "./site.config";
12
+ import { githubUrl, siteConfig, siteDocumentTitle } from "./site.config";
13
13
 
14
14
  export const metadata = {
15
- title: `${siteConfig.name} · ${siteConfig.tagline}`,
15
+ title: siteDocumentTitle,
16
16
  description: siteConfig.description,
17
17
  };
18
18
 
@@ -33,3 +33,6 @@ export type SiteConfig = {
33
33
  /** 站点身份来自根目录 site.config.yaml,由 garden sync 生成。 */
34
34
  export const siteConfig = raw as SiteConfig;
35
35
  export { githubUrl };
36
+
37
+ /** 列表页浏览器标题用站点名;页面大标题仍用 pages.*.title。 */
38
+ export const siteDocumentTitle = { absolute: siteConfig.name };
@@ -2,11 +2,11 @@ import { getPublishedThoughts } from "../lib/public-thoughts";
2
2
  import { ThoughtCard } from "../components/ThoughtCard";
3
3
  import { ContentEmptyState } from "../components/ContentEmptyState";
4
4
  import { PageIntro } from "../components/PageIntro";
5
- import { siteConfig } from "../site.config";
5
+ import { siteConfig, siteDocumentTitle } from "../site.config";
6
6
 
7
7
  export const metadata = {
8
- title: siteConfig.pages.thoughts.title,
9
- description: siteConfig.pages.thoughts.subtitle,
8
+ title: siteDocumentTitle,
9
+ description: siteConfig.pages.thoughts.subtitle || siteConfig.description,
10
10
  };
11
11
 
12
12
  export default function ThoughtsPage() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",
@@ -14,6 +14,6 @@
14
14
  "pages:prepare": "garden pages:prepare"
15
15
  },
16
16
  "dependencies": {
17
- "hd-garden": "^0.3.1"
17
+ "hd-garden": "^0.3.3",
18
18
  }
19
19
  }