hd-garden 0.14.7 → 0.14.10
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.
- package/app/components/FeaturedTitle.tsx +23 -0
- package/app/components/MarkdownArticle.tsx +2 -0
- package/app/components/TableOfContents.tsx +51 -50
- package/app/globals.css +22 -10
- package/app/lib/i18n.ts +4 -0
- package/app/lib/toc-expansion.ts +49 -0
- package/app/page.tsx +2 -2
- package/app/raw.d.ts +5 -0
- package/app/styles/shell.css +3 -2
- package/editors/dist/{garden-typora-0.14.7.zip → garden-typora-0.14.10.zip} +0 -0
- package/editors/dist/typora/manifest.json +1 -1
- package/editors/dist/vscode/{garden-ui-0.14.7.vsix → garden-ui-0.14.10.vsix} +0 -0
- package/package.json +2 -2
- package/scripts/file-preview.mjs +3 -0
- package/scripts/release.py +367 -0
- package/scripts/release_executor.py +347 -0
- package/ui/garden/feature-title-fit.mjs +95 -0
- package/ui/garden/inline-math.css +19 -0
- package/ui/garden/rehype-keep-punctuation.mjs +47 -0
- package/ui/preview/dist/garden-ui.esm.js +1 -1
- package/ui/preview/dist/garden-ui.iife.js +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useLayoutEffect, useRef } from "react";
|
|
5
|
+
import { mountFeatureTitle } from "../../ui/garden/feature-title-fit.mjs";
|
|
6
|
+
import runtimeSource from "../../ui/garden/feature-title-fit.mjs?raw";
|
|
7
|
+
|
|
8
|
+
// Preserve one authored runtime across SSR/browser builds; function.toString()
|
|
9
|
+
// would be changed independently by server and client optimizers.
|
|
10
|
+
const FEATURE_TITLE_BOOT = `(()=>{${runtimeSource.replace(/^export /gm, "")}\nmountFeatureTitle(document.currentScript.parentElement);})();`;
|
|
11
|
+
|
|
12
|
+
type FeaturedTitleProps = { title: string; href: string; description?: string };
|
|
13
|
+
|
|
14
|
+
export function FeaturedTitle({ title, href, description }: FeaturedTitleProps) {
|
|
15
|
+
const heading = useRef<HTMLHeadingElement>(null);
|
|
16
|
+
useLayoutEffect(() => mountFeatureTitle(heading.current?.parentElement), [title, href, description]);
|
|
17
|
+
return <>
|
|
18
|
+
<h2 id="ft" ref={heading} data-feature-title=""><Link href={href}>{title}</Link></h2>
|
|
19
|
+
{description ? <p data-feature-summary="">{description}</p> : null}
|
|
20
|
+
<style data-feature-title-rules="">{""}</style>
|
|
21
|
+
<script data-feature-title-boot="" dangerouslySetInnerHTML={{ __html: FEATURE_TITLE_BOOT }} />
|
|
22
|
+
</>;
|
|
23
|
+
}
|
|
@@ -15,6 +15,7 @@ import Link from "next/link";
|
|
|
15
15
|
import ReactMarkdown from "react-markdown";
|
|
16
16
|
import rehypeRaw from "rehype-raw";
|
|
17
17
|
import rehypeKatex from "rehype-katex";
|
|
18
|
+
import {rehypeKeepPunctuation} from "../../ui/garden/rehype-keep-punctuation.mjs";
|
|
18
19
|
import rehypeSlug from "rehype-slug";
|
|
19
20
|
import rehypeHighlight from "rehype-highlight";
|
|
20
21
|
import { rehypeHeadingAnchors } from "../../ui/garden/heading-anchors.mjs";
|
|
@@ -254,6 +255,7 @@ export function MarkdownArticle({ content, lang, fieldTables, refDemos, handbook
|
|
|
254
255
|
rehypeRaw,
|
|
255
256
|
rehypeGardenBlocks,
|
|
256
257
|
rehypeKatex,
|
|
258
|
+
rehypeKeepPunctuation,
|
|
257
259
|
rehypeSlug,
|
|
258
260
|
// Not rehype-autolink-headings "wrap": a heading with a link in it would nest <a> in <a> (React #418).
|
|
259
261
|
[rehypeHeadingAnchors, { className: ["h-anchor"] }],
|
|
@@ -4,39 +4,28 @@ import {
|
|
|
4
4
|
useCallback,
|
|
5
5
|
useEffect,
|
|
6
6
|
useId,
|
|
7
|
+
useMemo,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
useSyncExternalStore,
|
|
10
11
|
type MouseEvent,
|
|
11
12
|
type ReactNode,
|
|
12
13
|
} from "react";
|
|
14
|
+
import { usePathname } from "next/navigation";
|
|
15
|
+
import { nestHeadings, expandedTocSections, toggleTocSection, type TocHeading, type TocNode, type TocExpansion } from "../lib/toc-expansion";
|
|
13
16
|
import { createPortal } from "react-dom";
|
|
14
17
|
import { TOC_PANEL_ID, TOC_CONTROL_ID, TOC_CLOSED_ID } from "../lib/reading-navigation";
|
|
15
|
-
import { X } from "@phosphor-icons/react";
|
|
18
|
+
import { CaretUp, CaretDown, X } from "@phosphor-icons/react";
|
|
16
19
|
import { ui, type UiLang } from "../lib/i18n";
|
|
17
20
|
|
|
18
|
-
type
|
|
21
|
+
type TocProps = { headings: TocHeading[]; label?: string; lang?: UiLang };
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (const heading of headings) {
|
|
27
|
-
const node: TocNode = { ...heading, children: [] };
|
|
28
|
-
while (stack.at(-1) && stack.at(-1)!.depth >= heading.depth) {
|
|
29
|
-
stack.pop();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const parent = stack.at(-1);
|
|
33
|
-
if (parent) parent.children.push(node);
|
|
34
|
-
else roots.push(node);
|
|
35
|
-
stack.push(node);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return roots;
|
|
39
|
-
};
|
|
23
|
+
// A route/content change starts a fresh reading session, including when React
|
|
24
|
+
// reuses the component during normal client navigation.
|
|
25
|
+
export function TableOfContents(props: TocProps) {
|
|
26
|
+
const pathname = usePathname();
|
|
27
|
+
return <ContentsSession key={`${pathname}:${JSON.stringify(props.headings)}`} {...props} />;
|
|
28
|
+
}
|
|
40
29
|
|
|
41
30
|
const getCssScrollPaddingTop = () => {
|
|
42
31
|
const value = window.getComputedStyle(document.documentElement).scrollPaddingTop;
|
|
@@ -131,22 +120,15 @@ function subscribeControlHost(changed: () => void) {
|
|
|
131
120
|
return () => observer.disconnect();
|
|
132
121
|
}
|
|
133
122
|
|
|
134
|
-
|
|
123
|
+
function ContentsSession({
|
|
135
124
|
headings,
|
|
136
125
|
label,
|
|
137
126
|
lang,
|
|
138
|
-
}: {
|
|
139
|
-
headings: Heading[];
|
|
140
|
-
label?: string;
|
|
141
|
-
/** Defaults to the site language; the bilingual handbook passes its own. */
|
|
142
|
-
lang?: UiLang;
|
|
143
|
-
}) {
|
|
127
|
+
}: TocProps) {
|
|
144
128
|
const t = ui(lang);
|
|
145
129
|
label ??= t.toc;
|
|
146
|
-
const [activeId, setActiveId] = useState(
|
|
147
|
-
const [
|
|
148
|
-
() => new Set(),
|
|
149
|
-
);
|
|
130
|
+
const [activeId, setActiveId] = useState<string>();
|
|
131
|
+
const [expansion, setExpansion] = useState<TocExpansion>({ mode: "follow" });
|
|
150
132
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
151
133
|
const panelId = TOC_PANEL_ID;
|
|
152
134
|
const controlHost = useSyncExternalStore(subscribeControlHost, readControlHost, serverControlHost);
|
|
@@ -157,7 +139,8 @@ export function TableOfContents({
|
|
|
157
139
|
const mobileCloseRef = useRef<HTMLButtonElement>(null);
|
|
158
140
|
const restoreMobileTriggerFocusRef = useRef(false);
|
|
159
141
|
const anchorCleanupRef = useRef<(() => void) | undefined>(undefined);
|
|
160
|
-
const tree = nestHeadings(headings);
|
|
142
|
+
const tree = useMemo(() => nestHeadings(headings), [headings]);
|
|
143
|
+
const expanded = useMemo(() => expandedTocSections(tree, expansion, activeId), [tree, expansion, activeId]);
|
|
161
144
|
|
|
162
145
|
const closeMobileToc = useCallback((restoreFocus = false) => {
|
|
163
146
|
restoreMobileTriggerFocusRef.current = restoreFocus;
|
|
@@ -250,9 +233,9 @@ export function TableOfContents({
|
|
|
250
233
|
const current = reachedPageEnd
|
|
251
234
|
? visibleHeadings.at(-1)?.id
|
|
252
235
|
: visibleHeadings.filter((heading) => heading.top <= readingProbe).at(-1)
|
|
253
|
-
?.id
|
|
236
|
+
?.id;
|
|
254
237
|
|
|
255
|
-
|
|
238
|
+
setActiveId((previous) => (previous === current ? previous : current));
|
|
256
239
|
};
|
|
257
240
|
const scheduleUpdate = () => {
|
|
258
241
|
if (!frame) frame = window.requestAnimationFrame(updateActiveHeading);
|
|
@@ -341,7 +324,7 @@ export function TableOfContents({
|
|
|
341
324
|
const activeItem = list?.querySelector<HTMLElement>(
|
|
342
325
|
'[aria-current="location"]',
|
|
343
326
|
);
|
|
344
|
-
if (!list || !activeItem) return;
|
|
327
|
+
if (!list || !activeItem || !activeItem.getClientRects().length) return;
|
|
345
328
|
|
|
346
329
|
const edge = 10;
|
|
347
330
|
const listBounds = list.getBoundingClientRect();
|
|
@@ -358,16 +341,20 @@ export function TableOfContents({
|
|
|
358
341
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
359
342
|
list.scrollTo({ top: nextTop, behavior: reduceMotion ? "auto" : "smooth" });
|
|
360
343
|
}
|
|
361
|
-
}, [activeId,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
344
|
+
}, [activeId, expanded, mobileOpen]);
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
const focused = document.activeElement;
|
|
348
|
+
if (!(focused instanceof HTMLElement) || !listRef.current?.contains(focused) || focused.getClientRects().length) return;
|
|
349
|
+
let section = focused.closest(".toc-section");
|
|
350
|
+
while (section) {
|
|
351
|
+
const link = section.querySelector<HTMLAnchorElement>(":scope > .toc-item-row > a");
|
|
352
|
+
if (link?.getClientRects().length) { link.focus({ preventScroll: true }); break; }
|
|
353
|
+
section = section.parentElement?.closest(".toc-section") ?? null;
|
|
354
|
+
}
|
|
355
|
+
}, [expanded]);
|
|
356
|
+
|
|
357
|
+
const toggleSection = (id: string) => setExpansion((current) => toggleTocSection(expandedTocSections(tree, current, activeId), id));
|
|
371
358
|
|
|
372
359
|
const navigateToHeading = (event: MouseEvent<HTMLAnchorElement>, id: string) => {
|
|
373
360
|
event.preventDefault();
|
|
@@ -390,7 +377,7 @@ export function TableOfContents({
|
|
|
390
377
|
};
|
|
391
378
|
|
|
392
379
|
const renderNode = (node: TocNode, path: number[]): ReactNode => {
|
|
393
|
-
const collapsed =
|
|
380
|
+
const collapsed = !expanded.has(node.id);
|
|
394
381
|
const childListId = `${listId}-${path.join("-")}`;
|
|
395
382
|
|
|
396
383
|
return (
|
|
@@ -422,7 +409,7 @@ export function TableOfContents({
|
|
|
422
409
|
) : null}
|
|
423
410
|
</div>
|
|
424
411
|
{node.children.length > 0 ? (
|
|
425
|
-
<ol id={childListId} className="toc-sublist"
|
|
412
|
+
<ol id={childListId} className="toc-sublist" role="list" data-collapsed={collapsed}>
|
|
426
413
|
{node.children.map((child, index) => renderNode(child, [...path, index]))}
|
|
427
414
|
</ol>
|
|
428
415
|
) : null}
|
|
@@ -465,9 +452,23 @@ export function TableOfContents({
|
|
|
465
452
|
id={panelId}
|
|
466
453
|
aria-label={label}
|
|
467
454
|
data-mobile-open={mobileOpen}
|
|
455
|
+
data-toc-mode={expansion.mode}
|
|
456
|
+
data-toc-active={activeId || ""}
|
|
468
457
|
>
|
|
469
458
|
<div className="toc-header">
|
|
470
459
|
<p>{label}</p>
|
|
460
|
+
{tree.some((node) => node.children.length > 0) ? <div className="toc-mode-controls">
|
|
461
|
+
<button
|
|
462
|
+
type="button"
|
|
463
|
+
data-toc-mode-toggle=""
|
|
464
|
+
aria-controls={listId}
|
|
465
|
+
aria-label={expansion.mode === "follow" ? t.tocExpandAll : t.tocResumeFollow}
|
|
466
|
+
title={expansion.mode === "follow" ? t.tocExpandAll : t.tocResumeFollow}
|
|
467
|
+
onClick={() => setExpansion({ mode: expansion.mode === "follow" ? "all" : "follow" })}
|
|
468
|
+
>
|
|
469
|
+
{expansion.mode === "follow" ? <CaretDown size={14} aria-hidden="true" /> : <CaretUp size={14} aria-hidden="true" />}
|
|
470
|
+
</button>
|
|
471
|
+
</div> : null}
|
|
471
472
|
<button
|
|
472
473
|
ref={mobileCloseRef}
|
|
473
474
|
className="toc-mobile-close"
|
|
@@ -481,7 +482,7 @@ export function TableOfContents({
|
|
|
481
482
|
<X size={18} aria-hidden="true" />
|
|
482
483
|
</a>
|
|
483
484
|
</div>
|
|
484
|
-
<ol id={listId} ref={listRef} className="toc-list">
|
|
485
|
+
<ol id={listId} ref={listRef} className="toc-list" role="list">
|
|
485
486
|
{tree.map((node, index) => renderNode(node, [index]))}
|
|
486
487
|
</ol>
|
|
487
488
|
</aside>
|
package/app/globals.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
2
|
@import "../ui/react/ui.css";
|
|
3
3
|
@import "../ui/compose/compose.css";
|
|
4
|
+
@import "../ui/garden/inline-math.css";
|
|
4
5
|
@import "./admin-ui.css";
|
|
5
6
|
|
|
6
7
|
:root {
|
|
@@ -1734,6 +1735,8 @@ button {
|
|
|
1734
1735
|
}
|
|
1735
1736
|
|
|
1736
1737
|
.toc-header > p {
|
|
1738
|
+
flex: 1 1 auto;
|
|
1739
|
+
min-width: 0;
|
|
1737
1740
|
margin: 0;
|
|
1738
1741
|
color: var(--accent);
|
|
1739
1742
|
font-size: 12px;
|
|
@@ -1754,10 +1757,13 @@ button {
|
|
|
1754
1757
|
|
|
1755
1758
|
.toc .toc-sublist {
|
|
1756
1759
|
margin: 0;
|
|
1757
|
-
padding: 0
|
|
1760
|
+
padding: 0;
|
|
1761
|
+
padding-inline-start: 1.25em;
|
|
1758
1762
|
list-style: none;
|
|
1759
1763
|
}
|
|
1760
1764
|
|
|
1765
|
+
.toc .toc-sublist[data-collapsed="true"] { display: none; }
|
|
1766
|
+
|
|
1761
1767
|
.toc-item-row {
|
|
1762
1768
|
display: flex;
|
|
1763
1769
|
align-items: flex-start;
|
|
@@ -1804,10 +1810,11 @@ button {
|
|
|
1804
1810
|
line-height: 1.6;
|
|
1805
1811
|
}
|
|
1806
1812
|
|
|
1807
|
-
.toc
|
|
1808
|
-
.toc
|
|
1809
|
-
|
|
1810
|
-
}
|
|
1813
|
+
.toc-list > .toc-section > .toc-item-row > a { font-weight: 600; }
|
|
1814
|
+
.toc-sublist > .toc-section > .toc-item-row > a { font-weight: 400; }
|
|
1815
|
+
.toc-mode-controls { display: flex; align-items: center; flex: 0 0 auto; margin-left: auto; }
|
|
1816
|
+
.toc-mode-controls button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; background: transparent; color: var(--muted); cursor: pointer; touch-action: manipulation; }
|
|
1817
|
+
.toc-mode-controls button:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
|
|
1811
1818
|
|
|
1812
1819
|
.toc a:hover {
|
|
1813
1820
|
color: var(--ink);
|
|
@@ -2557,6 +2564,7 @@ button {
|
|
|
2557
2564
|
overflow-wrap: normal;
|
|
2558
2565
|
}
|
|
2559
2566
|
|
|
2567
|
+
|
|
2560
2568
|
.markdown-body .footnotes {
|
|
2561
2569
|
margin-top: 64px;
|
|
2562
2570
|
padding-top: 22px;
|
|
@@ -3868,13 +3876,16 @@ html[data-theme="dark"] .search-footer {
|
|
|
3868
3876
|
padding-right: max(20px, env(safe-area-inset-right));
|
|
3869
3877
|
}
|
|
3870
3878
|
|
|
3871
|
-
.toc .toc-item-row > a { display: flex; align-items: center; min-height:
|
|
3872
|
-
.toc .toc-section-toggle { min-width:
|
|
3879
|
+
.toc .toc-item-row > a { display: flex; align-items: center; min-height: 40px; padding-block: 4px; }
|
|
3880
|
+
.toc .toc-section-toggle { min-width: 40px; min-height: 40px; }
|
|
3881
|
+
.toc .toc-mode-controls button { width: 44px; height: 44px; }
|
|
3873
3882
|
|
|
3874
|
-
.toc
|
|
3875
|
-
margin:
|
|
3883
|
+
.toc .toc-list .toc-section {
|
|
3884
|
+
margin: 2px 0;
|
|
3876
3885
|
font-size: 13px;
|
|
3886
|
+
line-height: 1.5;
|
|
3877
3887
|
}
|
|
3888
|
+
.toc-list > .toc-section + .toc-section { margin-top: 6px; }
|
|
3878
3889
|
|
|
3879
3890
|
.post-reading-page {
|
|
3880
3891
|
width: min(var(--content), calc(100% - 48px));
|
|
@@ -5393,9 +5404,10 @@ html[data-theme="dark"] .search-footer {
|
|
|
5393
5404
|
/* Without JavaScript every sub-list stays open and the collapse toggles do
|
|
5394
5405
|
nothing, so hide them at every width. */
|
|
5395
5406
|
@media (scripting: none) {
|
|
5396
|
-
.toc-section-toggle {
|
|
5407
|
+
.toc-section-toggle, .toc-mode-controls {
|
|
5397
5408
|
display: none;
|
|
5398
5409
|
}
|
|
5410
|
+
.toc .toc-sublist[data-collapsed="true"] { display: block; }
|
|
5399
5411
|
}
|
|
5400
5412
|
|
|
5401
5413
|
/* No JavaScript: the drawer button cannot open anything, so swap in plain
|
package/app/lib/i18n.ts
CHANGED
|
@@ -38,6 +38,8 @@ const zh = {
|
|
|
38
38
|
columnNewestFirst: (title: string | undefined) => `${title},从新到旧`,
|
|
39
39
|
toc: "本篇目录",
|
|
40
40
|
tocShort: "目录",
|
|
41
|
+
tocResumeFollow: "恢复跟读",
|
|
42
|
+
tocExpandAll: "全部展开",
|
|
41
43
|
tocPage: "本页目录",
|
|
42
44
|
open: "打开",
|
|
43
45
|
close: "关闭",
|
|
@@ -296,6 +298,8 @@ const en: typeof zh = {
|
|
|
296
298
|
columnNewestFirst: (title: string | undefined) => `${title}, newest first`,
|
|
297
299
|
toc: "Contents",
|
|
298
300
|
tocShort: "TOC",
|
|
301
|
+
tocResumeFollow: "Resume following",
|
|
302
|
+
tocExpandAll: "Expand all",
|
|
299
303
|
tocPage: "On this page",
|
|
300
304
|
open: "Open ",
|
|
301
305
|
close: "Close ",
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type TocHeading = { depth: number; text: string; id: string; no?: string };
|
|
2
|
+
export type TocNode = TocHeading & { children: TocNode[] };
|
|
3
|
+
export type TocExpansion =
|
|
4
|
+
| { mode: "follow" }
|
|
5
|
+
| { mode: "all" }
|
|
6
|
+
| { mode: "manual"; expanded: ReadonlySet<string> };
|
|
7
|
+
|
|
8
|
+
export function nestHeadings(headings: readonly TocHeading[]): TocNode[] {
|
|
9
|
+
const roots: TocNode[] = [], stack: TocNode[] = [];
|
|
10
|
+
for (const heading of headings) {
|
|
11
|
+
const node: TocNode = { ...heading, children: [] };
|
|
12
|
+
let parent = stack.at(-1);
|
|
13
|
+
while (parent && parent.depth >= node.depth) {
|
|
14
|
+
stack.pop();
|
|
15
|
+
parent = stack.at(-1);
|
|
16
|
+
}
|
|
17
|
+
if (parent) parent.children.push(node);
|
|
18
|
+
else roots.push(node);
|
|
19
|
+
stack.push(node);
|
|
20
|
+
}
|
|
21
|
+
return roots;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The nearest shallower heading is the parent even when heading ranks skip.
|
|
25
|
+
// Opening only this path exposes the current heading and its immediate children.
|
|
26
|
+
export function expandedTocSections(tree: readonly TocNode[], state: TocExpansion, activeId?: string): ReadonlySet<string> {
|
|
27
|
+
if (state.mode === "manual") return state.expanded;
|
|
28
|
+
const expanded = new Set<string>();
|
|
29
|
+
function visit(nodes: readonly TocNode[]): boolean {
|
|
30
|
+
let containsActive = false;
|
|
31
|
+
for (const node of nodes) {
|
|
32
|
+
const onPath = visit(node.children) || node.id === activeId;
|
|
33
|
+
if (node.children.length && (state.mode === "all" || onPath)) expanded.add(node.id);
|
|
34
|
+
containsActive ||= onPath;
|
|
35
|
+
}
|
|
36
|
+
return containsActive;
|
|
37
|
+
}
|
|
38
|
+
visit(tree);
|
|
39
|
+
return expanded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// An individual disclosure is an explicit manual browsing choice. Freeze the
|
|
43
|
+
// current visible branches instead of fighting the next scroll-spy update.
|
|
44
|
+
export function toggleTocSection(expanded: ReadonlySet<string>, id: string): TocExpansion {
|
|
45
|
+
const next = new Set(expanded);
|
|
46
|
+
if (next.has(id)) next.delete(id);
|
|
47
|
+
else next.add(id);
|
|
48
|
+
return { mode: "manual", expanded: next };
|
|
49
|
+
}
|
package/app/page.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
|
+
import { FeaturedTitle } from "./components/FeaturedTitle";
|
|
2
3
|
import { CoverImage } from "./components/CoverImage";
|
|
3
4
|
import { fullListDate, homeListDate } from "./lib/a2-meta";
|
|
4
5
|
import { columnStatusLabel, siteLang, ui } from "./lib/i18n";
|
|
@@ -65,8 +66,7 @@ export default function Home() {
|
|
|
65
66
|
<span>{t.homeLatestBadge}</span>
|
|
66
67
|
<time className="num" dateTime={day(feature.date)}>{fullListDate(feature.date, siteLang)}</time>
|
|
67
68
|
</p>
|
|
68
|
-
<
|
|
69
|
-
{feature.description ? <p>{feature.description}</p> : null}
|
|
69
|
+
<FeaturedTitle title={feature.title} href={postHref(feature)} description={feature.description} />
|
|
70
70
|
{feature.tags?.length ? (
|
|
71
71
|
<p className="tags">{feature.tags.map((tag) => <span key={tag}>#{tag}</span>)}</p>
|
|
72
72
|
) : null}
|
package/app/raw.d.ts
CHANGED
package/app/styles/shell.css
CHANGED
|
@@ -166,7 +166,8 @@ html[data-theme="dark"] .sh-rail{color:var(--muted)}
|
|
|
166
166
|
.sh-feature .cover{border-radius:var(--r-lg);aspect-ratio:auto;display:block;overflow:hidden;background:none}
|
|
167
167
|
.sh-feature .cover img{display:block;width:100%;height:auto}
|
|
168
168
|
.sh-feature .kicker{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:var(--fs-small);color:var(--muted);margin:0 0 12px}
|
|
169
|
-
.sh-feature
|
|
169
|
+
.sh-feature>div{min-width:0}
|
|
170
|
+
.sh-feature h2{font:700 max(1.875rem,1.125em)/1.35 var(--sans);color:var(--ink);margin:0;text-wrap:balance;overflow-wrap:anywhere}
|
|
170
171
|
.sh-feature h2 a{text-decoration:none}
|
|
171
172
|
.sh-feature h2 a::after{content:"";position:absolute;inset:0;border-radius:var(--r-lg)}
|
|
172
173
|
.sh-feature:hover h2 a{color:var(--blue);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:4px}
|
|
@@ -289,7 +290,7 @@ html[data-theme="dark"] .sh-pal-scrim{background:rgba(0,0,0,.55)}
|
|
|
289
290
|
.sh-intro{padding:44px 0 0}
|
|
290
291
|
.sh-intro p{font-size:var(--fs-body)}
|
|
291
292
|
.sh-feature{grid-template-columns:minmax(0,1fr);gap:20px;margin-top:36px}
|
|
292
|
-
.sh-feature h2{font-size:
|
|
293
|
+
.sh-feature h2{font-size:max(1.5rem,1.125em)}
|
|
293
294
|
.sh-section{margin-top:56px}
|
|
294
295
|
.sh-cols{grid-template-columns:minmax(0,1fr);gap:16px}
|
|
295
296
|
.sh-row,.sh-home-grid .sh-row{grid-template-columns:minmax(0,1fr) auto;gap:2px 12px;padding:16px 0 17px}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hd-garden",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
|
|
6
6
|
"license": "MIT",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"start": "node bin/garden.mjs start",
|
|
67
67
|
"admin:local": "node --env-file=.env.admin.local bin/garden.mjs start --host 0.0.0.0 --port 3000",
|
|
68
68
|
"admin:cloudflare": "npm run build && wrangler deploy --config dist/server/wrangler.json",
|
|
69
|
-
"test": "npm run build && node --experimental-strip-types --test tests/*.test.mjs",
|
|
69
|
+
"test": "npm run build && node --experimental-strip-types --test --test-concurrency=2 tests/*.test.mjs",
|
|
70
70
|
"ui:build": "node scripts/build-ui-preview.mjs",
|
|
71
71
|
"editors:build": "node editors/vscode/build.mjs && node editors/obsidian/build.mjs && node editors/typora/build.mjs",
|
|
72
72
|
"editors:pack": "node scripts/pack-editors.mjs",
|
package/scripts/file-preview.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import rehypeHighlight from "rehype-highlight";
|
|
17
17
|
import rehypeKatex from "rehype-katex";
|
|
18
|
+
import { rehypeKeepPunctuation } from "../ui/garden/rehype-keep-punctuation.mjs";
|
|
18
19
|
import rehypeRaw from "rehype-raw";
|
|
19
20
|
import rehypeSlug from "rehype-slug";
|
|
20
21
|
import rehypeStringify from "rehype-stringify";
|
|
@@ -104,6 +105,7 @@ export function renderFilePreview(markdown, { idPrefix = "gf-", lang = "zh" } =
|
|
|
104
105
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
105
106
|
.use(rehypeRaw)
|
|
106
107
|
.use(rehypeKatex)
|
|
108
|
+
.use(rehypeKeepPunctuation)
|
|
107
109
|
.use(rehypeSlug, { prefix: idPrefix })
|
|
108
110
|
.use(rehypeHighlight)
|
|
109
111
|
.use(rehypePreviewSafety)
|
|
@@ -190,6 +192,7 @@ export function previewStyleSheet() {
|
|
|
190
192
|
`:not(pre)>code{font:400 14px/1 ${mono};padding:3px 6px;border-radius:5px}`,
|
|
191
193
|
".table-wrap{overflow-x:auto}table{border-collapse:collapse}th,td{border:1px solid;padding:6px 10px}",
|
|
192
194
|
"a{color:inherit}.katex-html{display:none}",
|
|
195
|
+
fs.readFileSync(new URL("../ui/garden/inline-math.css", import.meta.url), "utf8"),
|
|
193
196
|
theme(light),
|
|
194
197
|
...rules.flatMap((sheet) => sheet.light),
|
|
195
198
|
`@media (prefers-color-scheme:dark){${theme(dark)}${rules.flatMap((sheet) => sheet.dark).join("")}}`,
|