hd-garden 0.5.1 → 0.5.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.
@@ -111,7 +111,7 @@ export default async function PostPage({
111
111
  <ArticleTags tags={post.tags} />
112
112
  {post.cover && (
113
113
  <figure className="article-hero article-cover cover-card cover-img">
114
- <CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={`${post.title} 封面`} sizes="(max-width: 920px) calc(100vw - 40px), 880px" priority />
114
+ <CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={`${post.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
115
115
  </figure>
116
116
  )}
117
117
  </header>
@@ -131,7 +131,7 @@ export default async function ColumnEntryPage({
131
131
  <ArticleTags tags={entry.tags} />
132
132
  {entry.cover && (
133
133
  <figure className="article-hero article-cover cover-img">
134
- <CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={`${entry.title} 封面`} sizes="(max-width: 920px) calc(100vw - 40px), 880px" priority />
134
+ <CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={`${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
135
135
  </figure>
136
136
  )}
137
137
  </header>
@@ -101,23 +101,32 @@ export function ArticleMeasure() {
101
101
  article.querySelectorAll<HTMLElement>(".fig").forEach((figure) => {
102
102
  const image = figure.querySelector("img");
103
103
  resetWide(figure);
104
+ // From here on the browser decides; the build-time breakout CSS steps aside.
105
+ figure.classList.remove("fig-wide");
104
106
  if (!image) return;
105
- // Back to CSS sizing: natural size, capped at the column.
106
- image.style.removeProperty("width");
107
- if (!image.naturalWidth) return;
108
- const attrWidth = Number(image.getAttribute("width")) || 0;
107
+ // Back to the unwidened size: the build-time width when the size is known
108
+ // (so an image that has not loaded keeps its box), else CSS sizing.
109
+ const baseWidth = Number(image.dataset.bw) || 0;
110
+ if (baseWidth) image.style.width = `${baseWidth}px`;
111
+ else image.style.removeProperty("width");
112
+ // Sizes read at build time let this run before the file arrives; otherwise wait for load.
113
+ const known = Number(image.dataset.nw) || 0;
114
+ const pixels = image.naturalWidth || known;
115
+ if (!pixels) return;
116
+ // width="" written by the build is the pixel size, not an author's choice.
117
+ const attrWidth = known ? Number(image.dataset.aw) || 0 : Number(image.getAttribute("width")) || 0;
109
118
  const breakout = figureBreakout({
110
119
  viewport,
111
120
  articleLeft: articleBox.left,
112
121
  articleRight: articleBox.right,
113
122
  figureClient: figure.clientWidth,
114
123
  imageClient: image.clientWidth,
115
- naturalWidth: image.naturalWidth,
124
+ naturalWidth: pixels,
116
125
  attrWidth,
117
126
  });
118
127
  widen(figure, breakout);
119
128
  // Only a widened image, or one with a width attribute, gets an inline width.
120
- const width = figureImageWidth({ naturalWidth: image.naturalWidth, attrWidth, widened: breakout.left > 0 });
129
+ const width = figureImageWidth({ naturalWidth: pixels, attrWidth, widened: breakout.left > 0 });
121
130
  if (width) image.style.width = `${width}px`;
122
131
  });
123
132
  placeToc();
@@ -20,12 +20,17 @@ import { MarkdownImage } from "./MarkdownImage";
20
20
  import { CodeCopy } from "./garden/chrome";
21
21
  import { CodeDiff } from "./garden/chrome";
22
22
  import { GardenBlock } from "./garden/GardenBlock";
23
- import { linkAssets } from "@garden/generated-content";
24
- import { siteConfig } from "@garden/site-config";
23
+ import { imageSizes, linkAssets } from "@garden/generated-content";
24
+ import { imageDimensions, lookupImageSize } from "../../ui/garden/image-dims.mjs";
25
+ import { siteConfig, siteLinks } from "@garden/site-config";
26
+ import { articleLink, sameSiteMatcher } from "../../ui/garden/same-site.mjs";
25
27
  import { LinkCardRefresh } from "./LinkCardRefresh";
26
28
  import { linkHost } from "../../scripts/link-meta-core.mjs";
27
29
  import { cardDate } from "../../ui/garden/card-refresh.mjs";
28
30
 
31
+ // Full URLs back to this site are internal links, written as root-relative paths.
32
+ const sameSite = sameSiteMatcher(siteLinks?.address ?? "", siteLinks?.basePath ?? "");
33
+
29
34
  /** The site's favicon, downloaded at build time; nothing when there is none. */
30
35
  function Favicon({ href }: { href: string }) {
31
36
  const src = linkAssets?.favicons?.[linkHost(href)];
@@ -221,7 +226,7 @@ export function MarkdownArticle({ content }: { content: string }) {
221
226
  return (
222
227
  <div className="markdown-body prose">
223
228
  <ReactMarkdown
224
- remarkPlugins={[...markdownPlugins, remarkLinkCard, remarkA2]}
229
+ remarkPlugins={[...markdownPlugins, [remarkLinkCard, { sameSite }], [remarkA2, { imageSize: (src: string) => lookupImageSize(imageSizes, src) }]]}
225
230
  rehypePlugins={[
226
231
  rehypeRaw,
227
232
  rehypeKatex,
@@ -237,11 +242,11 @@ export function MarkdownArticle({ content }: { content: string }) {
237
242
  const only = nodes.length === 1 && isValidElement<{ href?: string; children?: ReactNode }>(nodes[0]) ? nodes[0] : null;
238
243
  const href = only?.props.href;
239
244
  const text = only ? headingText(only.props.children).trim() : "";
240
- if (href && text === href && /^https?:\/\//iu.test(href)) return <LinkCard href={href} />;
245
+ if (href && text === href && /^https?:\/\//iu.test(href) && !sameSite(href)) return <LinkCard href={href} />;
241
246
  return <p {...domProps}>{children}</p>;
242
247
  },
243
- a: ({ href, children, ...props }) => {
244
- const external = Boolean(href && /^(https?:)?\/\//iu.test(href));
248
+ a: ({ href: written, children, ...props }) => {
249
+ const { href, external } = articleLink(written, sameSite);
245
250
  const domProps = { ...props };
246
251
  Reflect.deleteProperty(domProps, "node");
247
252
  return (
@@ -256,7 +261,11 @@ export function MarkdownArticle({ content }: { content: string }) {
256
261
  </a>
257
262
  );
258
263
  },
259
- img: MarkdownImage,
264
+ img: (props) => {
265
+ const size = lookupImageSize(imageSizes, typeof props.src === "string" ? props.src : undefined);
266
+ const dims = imageDimensions(props, size);
267
+ return <MarkdownImage {...props} {...dims} style={{ ...props.style, ...dims.style }} />;
268
+ },
260
269
  table: ({ children, ...props }) => (
261
270
  <div className="table-wrap" role="region" aria-label="表格,可横向滚动" tabIndex={0}>
262
271
  <table {...props}>{children}</table>
@@ -6,7 +6,7 @@ import { useEffect, useState, type ComponentPropsWithoutRef } from "react";
6
6
  type MarkdownImageProps = ComponentPropsWithoutRef<"img"> & { node?: unknown };
7
7
 
8
8
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
- export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImageProps) {
9
+ export function MarkdownImage({ alt = "", src, node, style, ...props }: MarkdownImageProps) {
10
10
  const [open, setOpen] = useState(false);
11
11
  const description = alt || "查看原图";
12
12
 
@@ -37,7 +37,7 @@ export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImagePr
37
37
  >
38
38
  {/* Native images preserve Typora-authored relative paths. */}
39
39
  {/* eslint-disable-next-line @next/next/no-img-element */}
40
- <img loading="lazy" alt={alt} src={src} {...props} />
40
+ <img loading="lazy" alt={alt} src={src} {...props} style={style} />
41
41
  </button>
42
42
 
43
43
  {open ? (
@@ -49,8 +49,9 @@ export function MarkdownImage({ alt = "", src, node, ...props }: MarkdownImagePr
49
49
  onClick={() => setOpen(false)}
50
50
  >
51
51
  <div className="markdown-image-lightbox-content" onClick={(event) => event.stopPropagation()}>
52
+ {/* The preview sizes itself; the article's inline width stays on the article image. */}
52
53
  {/* eslint-disable-next-line @next/next/no-img-element */}
53
- <img src={src} alt={alt} {...props} />
54
+ <img src={src} alt={alt} width={props.width} height={props.height} />
54
55
  <button
55
56
  type="button"
56
57
  className="markdown-image-lightbox-close"
@@ -12,7 +12,7 @@ import {
12
12
  } from "react";
13
13
  import type Fuse from "fuse.js";
14
14
  import { MagnifyingGlass, X } from "@phosphor-icons/react";
15
- import { searchFull, searchIndexUrl } from "@garden/site-config";
15
+ import { brand, searchFull, searchIndexUrl } from "@garden/site-config";
16
16
  import { createSearch } from "../../ui/garden/search-match.mjs";
17
17
  import { createIdbStore, loadFullIndex, saveDataOn, whenIdleAfterLoad } from "../../ui/garden/search-full.mjs";
18
18
  import { siteConfig } from "../site.config";
@@ -358,7 +358,11 @@ export function SiteHeader() {
358
358
  <>
359
359
  <header className="sh-header">
360
360
  <div className="sh-bar">
361
- <Link href="/" className="sh-word">{siteConfig.name}</Link>
361
+ <Link href="/" className="sh-word">
362
+ {/* Build-time SVG from brand.logo, cleaned of scripts and handlers. */}
363
+ {brand.logo ? <span className="sh-mark" aria-hidden="true" dangerouslySetInnerHTML={{ __html: brand.logo }} /> : null}
364
+ {siteConfig.name}
365
+ </Link>
362
366
  <nav className="sh-nav" aria-label="站点导航">
363
367
  {nav.map((item) => (
364
368
  <Link key={item.href} href={item.href} aria-current={current(item.href) ? "page" : undefined}>
@@ -372,7 +376,7 @@ export function SiteHeader() {
372
376
  <circle cx="9" cy="9" r="5.8" />
373
377
  <path d="M13.3 13.3 17 17" />
374
378
  </svg>
375
- <span className="lbl">搜索文章</span>
379
+ <span className="lbl">搜索</span>
376
380
  <kbd><span className="kbd-mod kbd-mac">⌘</span><span className="kbd-mod kbd-other">Ctrl</span>{" "}K</kbd>
377
381
  </button>
378
382
  <button className="sh-icon sh-search-m" type="button" onClick={openSearch} onPointerDown={prefetchSearch} aria-label="搜索文章">
@@ -59,12 +59,18 @@ declare module "@garden/generated-content" {
59
59
  };
60
60
  export type LinkAssets = { favicons: Record<string, string>; cards: Record<string, LinkCardMeta> };
61
61
  export const linkAssets: LinkAssets;
62
+ /** Intrinsic sizes of body images by src, read at build time. */
63
+ export const imageSizes: Record<string, { width: number; height: number }>;
62
64
  }
63
65
 
64
66
  declare module "@garden/site-config" {
65
67
  export const searchIndexUrl: string;
66
68
  /** The full-text index file and the hash its contents are cached under. */
67
69
  export const searchFull: { url: string; hash: string };
70
+ /** The site address (site.config.yaml `url`, else SITE_URL) and base path, for spotting links back to the site. */
71
+ export const siteLinks: { address: string; basePath: string };
72
+ /** Header logo as inline SVG ("" when hidden) and icon URLs, from brand in site.config.yaml. */
73
+ export const brand: { logo: string; favicon: string; appleTouchIcon: string };
68
74
  export const siteConfig: {
69
75
  name: string;
70
76
  tagline: string;
@@ -74,6 +80,8 @@ declare module "@garden/site-config" {
74
80
  pages: Record<string, Record<string, unknown>>;
75
81
  footer: string;
76
82
  linkPreview?: { worker?: string };
83
+ url?: string;
84
+ brand?: { logo?: string | false; favicon?: string; appleTouchIcon?: string | false };
77
85
  };
78
86
  export const githubUrl: string;
79
87
  }
package/app/globals.css CHANGED
@@ -126,15 +126,6 @@ button {
126
126
  width: max-content;
127
127
  }
128
128
 
129
- .brand-mark {
130
- width: 43px;
131
- height: 50px;
132
- display: grid;
133
- place-items: center;
134
- color: var(--faint);
135
- border-right: 1px solid var(--line-strong);
136
- }
137
-
138
129
  .brand strong {
139
130
  display: block;
140
131
  font-size: 20px;
@@ -2061,15 +2052,32 @@ button {
2061
2052
  text-decoration-thickness: 2px;
2062
2053
  }
2063
2054
 
2064
- /* A per-line border stays continuous across inline code and descenders. */
2065
- .markdown-body u {
2055
+ /* Highlighter for key claims, as in the A2 demo: a soft stroke under the lower part of the line. */
2056
+ .markdown-body u,
2057
+ .markdown-body mark,
2058
+ .markdown-body .hl {
2059
+ color: var(--ink);
2060
+ font-weight: 500;
2066
2061
  text-decoration: none;
2067
- border-bottom: 1px solid currentColor;
2068
- padding-bottom: 0.1em;
2062
+ border-bottom: 0;
2063
+ background: linear-gradient(to bottom, transparent 58%, var(--hl) 58%, var(--hl) 92%, transparent 92%);
2064
+ padding: 0 0.08em;
2069
2065
  -webkit-box-decoration-break: clone;
2070
2066
  box-decoration-break: clone;
2071
2067
  }
2072
2068
 
2069
+ .markdown-body u strong,
2070
+ .markdown-body u u,
2071
+ .markdown-body mark strong,
2072
+ .markdown-body mark u,
2073
+ .markdown-body .hl strong,
2074
+ .markdown-body .hl u {
2075
+ font-weight: 600;
2076
+ text-decoration: none;
2077
+ background: none;
2078
+ padding: 0;
2079
+ }
2080
+
2073
2081
  .markdown-body strong {
2074
2082
  color: var(--ink);
2075
2083
  }
@@ -4098,11 +4106,6 @@ html[data-theme="dark"] .article-references {
4098
4106
  display: none;
4099
4107
  }
4100
4108
 
4101
- .brand-mark {
4102
- width: 36px;
4103
- height: 44px;
4104
- }
4105
-
4106
4109
  .brand strong {
4107
4110
  font-size: 17px;
4108
4111
  }
package/app/layout.tsx CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  webmentionEndpoint,
12
12
  } from "./lib/site";
13
13
  import { githubUrl, siteConfig } from "./site.config";
14
+ import { brand } from "@garden/site-config";
14
15
 
15
16
  export async function generateMetadata(): Promise<Metadata> {
16
17
  const siteUrl = await getSiteUrl();
@@ -29,7 +30,8 @@ export async function generateMetadata(): Promise<Metadata> {
29
30
  },
30
31
  },
31
32
  icons: {
32
- icon: "/favicon.svg",
33
+ icon: [{ url: brand.favicon, type: brand.favicon.endsWith(".svg") ? "image/svg+xml" : undefined }],
34
+ ...(brand.appleTouchIcon ? { apple: [{ url: brand.appleTouchIcon, sizes: "180x180" }] } : {}),
33
35
  },
34
36
  robots: {
35
37
  index: true,
@@ -1,3 +1,4 @@
1
+ import { figureLayout } from "../../ui/garden/enhance.mjs";
1
2
  import { toString } from "mdast-util-to-string";
2
3
 
3
4
  const ALERT_TITLE = {
@@ -18,10 +19,10 @@ const LEGACY_KIND = {
18
19
 
19
20
  const ALERT_ICON = {
20
21
  note: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25"/><path d="M8 7.2v4"/><path d="M8 4.8v.1" stroke-width="2"/></svg>',
21
- tip: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 12.5h4M6.5 14.5h3M8 1.75a4.5 4.5 0 0 0-2.6 8.2c.4.3.6.7.6 1.2v.35h4v-.35c0-.5.2-.9.6-1.2A4.5 4.5 0 0 0 8 1.75Z"/></svg>',
22
- important: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.25 3.25a1 1 0 0 1 1-1h9.5a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7l-3 2.5v-2.5h-.75a1 1 0 0 1-1-1Z"/><path d="M8 4.75v3"/><path d="M8 9.4v.1" stroke-width="2"/></svg>',
23
- warning: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 1.75 13.25h12.5Z"/><path d="M8 6.5v3.25"/><path d="M8 11.6v.1" stroke-width="2"/></svg>',
24
- caution: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.3 1.75h5.4l3.55 3.55v5.4l-3.55 3.55H5.3l-3.55-3.55V5.3Z"/><path d="M8 4.75v3.75"/><path d="M8 10.9v.1" stroke-width="2"/></svg>',
22
+ tip: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.75a4.25 4.25 0 0 0-2.5 7.7c.4.3.6.7.6 1.2v.6h3.8v-.6c0-.5.2-.9.6-1.2A4.25 4.25 0 0 0 8 1.75z"/><path d="M6.2 13.6h3.6"/></svg>',
23
+ important: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2.25 3.25a1.5 1.5 0 0 1 1.5-1.5h8.5a1.5 1.5 0 0 1 1.5 1.5v6.5a1.5 1.5 0 0 1-1.5 1.5H7.5l-3 2.75v-2.75h-.75a1.5 1.5 0 0 1-1.5-1.5z"/><path d="M8 4.5v3"/><path d="M8 9.4v.1" stroke-width="2"/></svg>',
24
+ warning: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M7.13 2.4a1 1 0 0 1 1.74 0l5.6 9.9a1 1 0 0 1-.87 1.5H2.4a1 1 0 0 1-.87-1.5z"/><path d="M8 6.3v3.1"/><path d="M8 11.4v.1" stroke-width="2"/></svg>',
25
+ caution: '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.3 1.75h5.4l3.55 3.55v5.4l-3.55 3.55H5.3L1.75 10.7V5.3z"/><path d="M8 4.8v3.6"/><path d="M8 10.6v.1" stroke-width="2"/></svg>',
25
26
  };
26
27
 
27
28
  function meaningfulChildren(node) {
@@ -168,7 +169,7 @@ function figureCaption(image) {
168
169
  return "";
169
170
  }
170
171
 
171
- function asFigure(node) {
172
+ function asFigure(node, imageSize) {
172
173
  if (node?.type !== "paragraph" || node.data?.hName) return null;
173
174
  const children = meaningfulChildren(node);
174
175
  if (children.length !== 1 || children[0].type !== "image") return null;
@@ -186,23 +187,30 @@ function asFigure(node) {
186
187
  children: [{ type: "text", value: caption }],
187
188
  }]
188
189
  : [card];
190
+ // A known size decides the breakout now, so the figure does not jump when the image loads.
191
+ const size = imageSize(image.url);
192
+ const side = size ? figureLayout({ naturalWidth: size.width }).side : 0;
193
+ const hProperties = side > 0
194
+ ? { className: ["fig", "fig-wide"], style: `--wl:${side}px;--wr:${side}px` }
195
+ : { className: ["fig"] };
189
196
  return {
190
197
  type: "paragraph",
191
- data: { hName: "figure", hProperties: { className: ["fig"] } },
198
+ data: { hName: "figure", hProperties },
192
199
  children: figureChildren,
193
200
  };
194
201
  }
195
202
 
196
- function walk(node) {
203
+ function walk(node, imageSize) {
197
204
  if (!node || typeof node !== "object" || !Array.isArray(node.children)) return;
198
205
  if (node.type === "blockquote") rewriteAlert(node);
199
- node.children = node.children.map((child) => asFigure(child) ?? child);
200
- for (const child of node.children) walk(child);
206
+ node.children = node.children.map((child) => asFigure(child, imageSize) ?? child);
207
+ for (const child of node.children) walk(child, imageSize);
201
208
  }
202
209
 
203
210
  /** Demo alert cards and captioned figures. Article rendering only. */
204
- export function remarkA2() {
211
+ export function remarkA2(options = {}) {
212
+ const imageSize = typeof options?.imageSize === "function" ? options.imageSize : () => null;
205
213
  return (tree) => {
206
- walk(tree);
214
+ walk(tree, imageSize);
207
215
  };
208
216
  }
@@ -32,7 +32,7 @@ const meaningful = (children) =>
32
32
  children.filter((child) => !(child.type === "text" && !child.value.trim()) && child.type !== "break");
33
33
 
34
34
  /** Take the markers out of one parent's inline children; return the marked links. */
35
- function takeMarkers(parent, source) {
35
+ function takeMarkers(parent, source, sameSite) {
36
36
  const marked = [];
37
37
  const children = parent.children ?? [];
38
38
  for (let index = 0; index < children.length - 1; index += 1) {
@@ -41,13 +41,14 @@ function takeMarkers(parent, source) {
41
41
  if (link.type !== "link" || next.type !== "text" || !next.value.startsWith(MARKER)) continue;
42
42
  if (!isHttp(link.url) || !writtenAsLink(link, source)) continue;
43
43
  next.value = next.value.slice(MARKER.length);
44
- marked.push(link);
44
+ // A link back to this site is an ordinary internal link: the marker goes, no card.
45
+ if (!sameSite(link.url)) marked.push(link);
45
46
  }
46
47
  parent.children = children.filter((child) => !(child.type === "text" && child.value === ""));
47
48
  return marked;
48
49
  }
49
50
 
50
- function walk(parent, source) {
51
+ function walk(parent, source, sameSite) {
51
52
  const children = parent.children ?? [];
52
53
  for (let index = 0; index < children.length; index += 1) {
53
54
  const node = children[index];
@@ -55,14 +56,14 @@ function walk(parent, source) {
55
56
  if (node.type !== "paragraph") {
56
57
  if (node.children) {
57
58
  // Headings, table cells and the like: drop the marker, no card.
58
- if (node.type === "heading" || node.type === "tableCell") takeMarkers(node, source);
59
- walk(node, source);
59
+ if (node.type === "heading" || node.type === "tableCell") takeMarkers(node, source, sameSite);
60
+ walk(node, source, sameSite);
60
61
  }
61
62
  continue;
62
63
  }
63
64
  // Links nested in emphasis count too.
64
- const marked = takeMarkers(node, source);
65
- for (const child of node.children) if (child.children && child.type !== "link") marked.push(...takeMarkers(child, source));
65
+ const marked = takeMarkers(node, source, sameSite);
66
+ for (const child of node.children) if (child.children && child.type !== "link") marked.push(...takeMarkers(child, source, sameSite));
66
67
  if (!marked.length) continue;
67
68
  const rest = meaningful(node.children);
68
69
  if (marked.length === 1 && rest.length === 1 && rest[0] === marked[0]) {
@@ -75,9 +76,10 @@ function walk(parent, source) {
75
76
  }
76
77
  }
77
78
 
78
- export function remarkLinkCard() {
79
+ export function remarkLinkCard(options = {}) {
80
+ const sameSite = typeof options?.sameSite === "function" ? options.sameSite : () => null;
79
81
  return (tree, file) => {
80
82
  const source = typeof file?.value === "string" ? file.value : String(file?.value ?? "");
81
- walk(tree, source);
83
+ walk(tree, source, sameSite);
82
84
  };
83
85
  }
package/app/styles/a2.css CHANGED
@@ -27,7 +27,6 @@ body {
27
27
  color: var(--ink);
28
28
  }
29
29
 
30
- .brand-mark,
31
30
  .brand small {
32
31
  display: none;
33
32
  }
@@ -244,7 +243,8 @@ body {
244
243
  .cover-card.cover-img,
245
244
  .article-hero.article-cover {
246
245
  width: 100%;
247
- max-width: 880px;
246
+ /* Column plus half a gutter each side: centred, and clear of the TOC rail. */
247
+ max-width: calc(var(--col) + 72px);
248
248
  margin: 48px auto 0;
249
249
  aspect-ratio: auto;
250
250
  background: none;
@@ -466,15 +466,15 @@ html[data-theme="dark"] .markdown-body img.fav {
466
466
  }
467
467
 
468
468
  .markdown-body.prose {
469
- --al-note: #30302e;
470
- --al-note-bg: #f0eee6;
471
- --al-note-line: rgba(20, 20, 19, 0.18);
469
+ --al-note: #2c55b4;
470
+ --al-note-bg: #eef3fc;
471
+ --al-note-line: rgba(44, 85, 180, 0.22);
472
472
  --al-tip: #2d6a3e;
473
473
  --al-tip-bg: #edf5ef;
474
474
  --al-tip-line: rgba(45, 106, 62, 0.22);
475
- --al-important: #30302e;
476
- --al-important-bg: #f0eee6;
477
- --al-important-line: rgba(20, 20, 19, 0.14);
475
+ --al-important: #6a3fb5;
476
+ --al-important-bg: #f3effa;
477
+ --al-important-line: rgba(106, 63, 181, 0.22);
478
478
  --al-warning: #8a5300;
479
479
  --al-warning-bg: #fbf3e5;
480
480
  --al-warning-line: rgba(176, 108, 0, 0.26);
@@ -487,15 +487,15 @@ html[data-theme="dark"] .markdown-body img.fav {
487
487
  :root[data-theme="dark"] .prose,
488
488
  html[data-theme="dark"] .markdown-body.prose,
489
489
  html[data-theme="dark"] .prose {
490
- --al-note: #eeece7;
491
- --al-note-bg: rgba(238, 236, 231, 0.06);
492
- --al-note-line: rgba(238, 236, 231, 0.22);
490
+ --al-note: #93b2f4;
491
+ --al-note-bg: rgba(147, 178, 244, 0.09);
492
+ --al-note-line: rgba(147, 178, 244, 0.28);
493
493
  --al-tip: #8fcb9f;
494
494
  --al-tip-bg: rgba(143, 203, 159, 0.08);
495
495
  --al-tip-line: rgba(143, 203, 159, 0.26);
496
- --al-important: #eeece7;
497
- --al-important-bg: #30302e;
498
- --al-important-line: rgba(238, 236, 231, 0.14);
496
+ --al-important: #bfa5f0;
497
+ --al-important-bg: rgba(191, 165, 240, 0.09);
498
+ --al-important-line: rgba(191, 165, 240, 0.28);
499
499
  --al-warning: #e6b566;
500
500
  --al-warning-bg: rgba(230, 181, 102, 0.08);
501
501
  --al-warning-line: rgba(230, 181, 102, 0.28);
@@ -535,17 +535,10 @@ html[data-theme="dark"] .prose {
535
535
  --al-line: var(--al-tip-line);
536
536
  }
537
537
 
538
- /* IMPORTANT reads in ink: a dark bar on the left and a bold title. */
539
538
  .markdown-body .markdown-alert-important {
540
539
  --al: var(--al-important);
541
540
  --al-bg: var(--al-important-bg);
542
541
  --al-line: var(--al-important-line);
543
- border-left: 3px solid var(--al-important);
544
- padding-left: 18px;
545
- }
546
-
547
- .markdown-body .markdown-alert-important .markdown-alert-title {
548
- font-weight: 700;
549
542
  }
550
543
 
551
544
  .markdown-body .markdown-alert-warning {
@@ -737,6 +730,19 @@ html[data-theme="dark"] .markdown-body .markdown-alert code {
737
730
  }
738
731
  }
739
732
 
733
+ /* A breakout decided at build time from the image's known size. It holds the
734
+ figure at its final width before the image loads and before the script
735
+ measures; the script then takes over (and removes .fig-wide). Only where the
736
+ article column is a full 740px with room on both sides. */
737
+ @media (min-width: 1348px) {
738
+ .post-reading-page .markdown-body .fig-wide {
739
+ margin-left: calc(-1 * var(--wl, 0px));
740
+ margin-right: calc(-1 * var(--wr, 0px));
741
+ max-width: none;
742
+ width: auto;
743
+ }
744
+ }
745
+
740
746
  .toc {
741
747
  font-size: var(--fs-small);
742
748
  line-height: 1.5;
@@ -37,21 +37,28 @@ img,svg{max-width:100%}
37
37
  .sh-header{position:sticky;top:0;z-index:40;background:var(--paper);border-bottom:1px solid transparent;transition:border-color .2s ease}
38
38
  .sh-header.is-scrolled{border-bottom-color:var(--rule)}
39
39
  .sh-bar{max-width:var(--shell);margin:0 auto;height:var(--hdr);padding:0 32px;display:flex;align-items:center;gap:28px}
40
- .sh-word{font:600 var(--fs-lead)/1 var(--serif);color:var(--ink);text-decoration:none;white-space:nowrap;padding:8px 0}
40
+ .sh-word{display:inline-flex;align-items:center;gap:10px;font:600 var(--fs-lead)/1 var(--serif);color:var(--ink);text-decoration:none;white-space:nowrap;padding:8px 0}
41
+ .sh-mark{display:inline-flex;flex:none}
42
+ .sh-mark svg{display:block;height:26px;width:auto}
41
43
  .sh-nav{display:flex;gap:4px;margin-left:auto}
42
44
  .sh-nav a{font-size:15px;color:var(--soft);text-decoration:none;padding:8px 12px;border-radius:var(--r);transition:color .15s ease,background-color .15s ease}
43
45
  .sh-nav a:hover{color:var(--ink);background:var(--tint)}
44
46
  .sh-nav a[aria-current="page"],.sh-nav a[aria-current="true"]{color:var(--blue);font-weight:600}
45
47
  .sh-tools{display:flex;align-items:center;gap:8px}
46
- .sh-search{display:inline-flex;align-items:center;gap:10px;height:38px;padding:0 8px 0 12px;border:1px solid var(--rule);background:var(--card);color:var(--muted);border-radius:var(--r);font:400 var(--fs-small)/1 var(--sans);cursor:pointer;appearance:none;transition:border-color .15s ease,color .15s ease}
47
- .sh-search:hover{border-color:var(--rule-strong);color:var(--soft)}
48
+ .sh-search{display:inline-flex;align-items:center;gap:6px;height:38px;padding:8px 12px;border:0;background:transparent;color:var(--soft);border-radius:var(--r);font:400 15px/1 var(--sans);cursor:pointer;appearance:none;transition:color .15s ease,background-color .15s ease}
49
+ .sh-search:hover{color:var(--ink);background:var(--tint)}
50
+ .sh-search:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
48
51
  .sh-search svg,.sh-icon svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:none}
49
- .sh-search .lbl{min-width:84px;text-align:left}
52
+ .sh-search svg{width:16px;height:16px}
53
+ .sh-search .lbl{text-align:left}
54
+ /* The shortcut hint only where there is a keyboard and a precise pointer. */
55
+ .sh-search kbd{display:none;margin-left:4px;border:0;padding:0;background:none;font:400 12px/1 var(--sans);color:var(--muted);opacity:.7}
56
+ @media (hover:hover) and (pointer:fine){.sh-search kbd{display:inline}}
50
57
  kbd{font:500 12px/1 var(--sans);color:var(--muted);border:1px solid var(--rule);border-bottom-width:2px;border-radius:6px;padding:3px 6px;background:var(--paper)}
51
- .sh-search .kbd-mod{display:inline-block;min-width:2.5em;text-align:end}
58
+ .sh-search .kbd-mod{display:inline}
52
59
  .sh-search .kbd-other{display:none}
53
60
  html[data-mod="other"] .sh-search .kbd-mac{display:none}
54
- html[data-mod="other"] .sh-search .kbd-other{display:inline-block}
61
+ html[data-mod="other"] .sh-search .kbd-other{display:inline}
55
62
  .sh-search-m{color:var(--soft);opacity:1}
56
63
  .sh-search-m svg{opacity:1}
57
64
  .sh-icon{display:inline-grid;place-items:center;width:40px;height:40px;border:0;background:transparent;color:var(--soft);border-radius:var(--r);cursor:pointer;transition:background-color .15s ease,color .15s ease}
@@ -90,7 +90,7 @@ html[data-theme="dark"] {
90
90
  --accent: var(--blue);
91
91
  --accent-soft: var(--blue-tint);
92
92
  --selection: rgb(238 236 231 / 18%);
93
- --hl: #6b5a2a;
93
+ --hl: #5e4e17;
94
94
  --red: #f29aab;
95
95
  --red-text: #f29aab;
96
96
  --red-tint: rgb(242 154 171 / 12%);
package/bin/garden.mjs CHANGED
@@ -77,7 +77,7 @@ function ensureLink(from, to) {
77
77
  function ensurePublic() {
78
78
  const dest = path.join(siteRoot, "public");
79
79
  fs.mkdirSync(dest, { recursive: true });
80
- for (const name of ["favicon.svg", "og.png"]) {
80
+ for (const name of ["favicon.svg", "apple-touch-icon.png", "og.png"]) {
81
81
  const from = path.join(gardenPackageRoot, "public", name);
82
82
  const to = path.join(dest, name);
83
83
  if (fs.existsSync(from) && !fs.existsSync(to)) fs.copyFileSync(from, to);
@@ -355,6 +355,7 @@ function help() {
355
355
  garden new 创建文章草稿
356
356
  garden embed 拉取外链摘要
357
357
  garden links 重新拉取外链图标(build 时会自动做)
358
+ garden images 读取新的远程图片尺寸,写入 image-sizes.json(build 时会自动做)
358
359
  garden init [目录] 创建只含稿和配置的新站点
359
360
  garden check <文章.md> 检查一篇博文的硬性结构
360
361
  garden ui <子命令> Intelligent UI:scan、check、apply、prompt、schema、catalog
@@ -382,11 +383,18 @@ switch (command) {
382
383
  case "build":
383
384
  // Favicons are refetched on every build; failures fall back to link-assets.json.
384
385
  if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/link-assets.mjs");
386
+ // Remote image sizes: only new images are probed; the rest come from image-sizes.json.
387
+ if (process.env.GARDEN_OFFLINE !== "1") runNode("scripts/image-sizes.mjs");
385
388
  runVinext(["build", ...rest], { copyDist: true });
389
+ // Pages for old addresses listed under redirects in site.config.yaml (static export only).
390
+ if (process.env.STATIC_EXPORT === "1") runNode("scripts/redirects.mjs");
386
391
  break;
387
392
  case "links":
388
393
  runNode("scripts/link-assets.mjs");
389
394
  break;
395
+ case "images":
396
+ runNode("scripts/image-sizes.mjs");
397
+ break;
390
398
  case "start":
391
399
  runVinext(["start", ...rest]);
392
400
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "bin",
28
28
  "build",
29
29
  "public/favicon.svg",
30
+ "public/apple-touch-icon.png",
30
31
  "public/og.png",
31
32
  "public/images",
32
33
  "scripts",
@@ -81,6 +82,7 @@
81
82
  "fuse.js": "^7.5.0",
82
83
  "github-slugger": "^2.0.0",
83
84
  "gray-matter": "^4.0.3",
85
+ "image-size": "^2.0.4",
84
86
  "katex": "^0.16.47",
85
87
  "marked": "^16.4.2",
86
88
  "mdast-util-from-markdown": "^2.1.0",
Binary file
@@ -1,4 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
2
- <rect width="256" height="256" fill="#f8f8f6"/>
3
- <path fill="#202124" d="M216,208H168V184h4a68.07,68.07,0,0,0,68-68,28,28,0,0,0-56,0,12,12,0,0,1-12,12h-4V56a40,40,0,0,0-80,0V88H84A12,12,0,0,1,72,76a28,28,0,0,0-56,0,68.07,68.07,0,0,0,68,68h4v64H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM96,128H84A52.06,52.06,0,0,1,32,76a12,12,0,0,1,24,0,28,28,0,0,0,28,28H96a8,8,0,0,0,8-8V56a24,24,0,0,1,48,0v80a8,8,0,0,0,8,8h12a28,28,0,0,0,28-28,12,12,0,0,1,24,0,52.06,52.06,0,0,1-52,52H160a8,8,0,0,0-8,8v32H104V136A8,8,0,0,0,96,128Z"/>
4
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="1.5 1.25 28 28" fill="none" stroke="#1B1A18" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round"><style>@media (prefers-color-scheme: dark){svg{stroke:#EEECE7}}</style><path d="M10 27V10"/><path d="M10 10C10 5.5 13.5 3.5 18 3.5C18 7.5 15 10 10 10Z"/><path d="M10 19C10 15.5 12.5 13.5 15.5 13.5S21 15.5 21 19V27"/></svg>