hd-garden 0.14.10 → 0.14.11

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.
@@ -1,20 +1,40 @@
1
+ "use client";
2
+
3
+ import { useLayoutEffect, useRef } from "react";
1
4
  import { splitTitle, titleLineScale } from "../../ui/garden/title.mjs";
5
+ import { mountArticleTitle } from "../../ui/garden/article-title-fit.mjs";
6
+ import fitRuntime from "../../ui/garden/feature-title-fit.mjs?raw";
7
+ import articleRuntime from "../../ui/garden/article-title-fit.mjs?raw";
8
+
9
+ // Preserve one authored runtime across SSR/browser builds; function.toString()
10
+ // would be changed independently by server and client optimizers.
11
+ const ARTICLE_TITLE_BOOT = `(()=>{${fitRuntime.replace(/^export /gm, "")}
12
+ ${articleRuntime.replace(/^import\s+[^;]+;\s*/u, "").replace(/^export /gm, "")}
13
+ mountArticleTitle(document.currentScript.parentElement);})();`;
2
14
 
3
15
  export function ArticleTitle({ title, className = "" }: { title: string; className?: string }) {
16
+ const heading = useRef<HTMLHeadingElement>(null);
4
17
  const split = splitTitle(title);
18
+ useLayoutEffect(() => mountArticleTitle(heading.current?.parentElement), [title]);
5
19
  if (!split.split) {
6
20
  return <h1 className={`p-name${className ? ` ${className}` : ""}`}>{title}</h1>;
7
21
  }
8
22
  const scale = titleLineScale(split.lines);
9
23
  return (
10
- <h1 className={`p-name h1-br${className ? ` ${className}` : ""}`} style={{ ["--l" as string]: scale }}>
11
- <span className="h1-line">{split.lines[0]}</span>
12
- <span className="vh">{split.separator}</span>
13
- <span className="h1-line">{split.lines[1]}</span>
14
- </h1>
24
+ <>
25
+ <h1
26
+ id="at"
27
+ ref={heading}
28
+ data-article-title=""
29
+ className={`p-name h1-br${className ? ` ${className}` : ""}`}
30
+ style={{ ["--l" as string]: scale }}
31
+ >
32
+ <span className="h1-line">{split.lines[0]}</span>
33
+ <span className="vh">{split.separator}</span>
34
+ <span className="h1-line">{split.lines[1]}</span>
35
+ </h1>
36
+ <style data-article-title-rules="">{""}</style>
37
+ <script data-article-title-boot="" dangerouslySetInnerHTML={{ __html: ARTICLE_TITLE_BOOT }} />
38
+ </>
15
39
  );
16
40
  }
17
-
18
- export function originalUrl(entry: { original?: unknown }) {
19
- return typeof entry.original === "string" && /^https?:\/\//iu.test(entry.original) ? entry.original : "";
20
- }
@@ -2,7 +2,8 @@ import Link from "next/link";
2
2
  import { CoverImage } from "./CoverImage";
3
3
  import { ArticleMeasure } from "./ArticleMeasure";
4
4
  import { ArticleReferences } from "./ArticleReferences";
5
- import { ArticleTitle, originalUrl } from "./ArticleTitle";
5
+ import { ArticleTitle } from "./ArticleTitle";
6
+ import { originalUrl } from "../../ui/garden/title.mjs";
6
7
  import { MarkdownArticle } from "./MarkdownArticle";
7
8
  import { TableOfContents } from "./TableOfContents";
8
9
  import { Comments } from "./Comments";
@@ -5,7 +5,7 @@
5
5
  "author": "hedon",
6
6
  "authorUrl": "https://github.com/hedon954",
7
7
  "repo": "hedon954/garden",
8
- "version": "0.14.10",
8
+ "version": "0.14.11",
9
9
  "minAppVersion": "1.5.0",
10
10
  "minCoreVersion": "2.10.52",
11
11
  "platforms": [
@@ -2532,6 +2532,15 @@
2532
2532
  min-width: 0;
2533
2533
  }
2534
2534
 
2535
+ /* Columnized centered rows stretch labels to 100% width, so align-items
2536
+ can center arrows but not text. Center direct labels only; explicit
2537
+ start/end and nested cards or prose stay as authored. */
2538
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-h:not([data-align="start"]):not([data-align="end"]),
2539
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-text:not([data-align="start"]):not([data-align="end"]),
2540
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-caption-inline:not([data-align="start"]):not([data-align="end"]) {
2541
+ text-align: center;
2542
+ }
2543
+
2535
2544
  .g-compose-arrow[data-dir="right"] {
2536
2545
  transform: none;
2537
2546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hd-garden",
3
- "version": "0.14.10",
3
+ "version": "0.14.11",
4
4
  "private": false,
5
5
  "description": "用 Markdown 写作、用 GitHub Pages 发布的个人博客引擎。",
6
6
  "license": "MIT",
@@ -1262,6 +1262,15 @@
1262
1262
  min-width: 0;
1263
1263
  }
1264
1264
 
1265
+ /* Columnized centered rows stretch labels to 100% width, so align-items
1266
+ can center arrows but not text. Center direct labels only; explicit
1267
+ start/end and nested cards or prose stay as authored. */
1268
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-h:not([data-align="start"]):not([data-align="end"]),
1269
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-text:not([data-align="start"]):not([data-align="end"]),
1270
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-caption-inline:not([data-align="start"]):not([data-align="end"]) {
1271
+ text-align: center;
1272
+ }
1273
+
1265
1274
  .g-compose-arrow[data-dir="right"] {
1266
1275
  transform: none;
1267
1276
  }
@@ -0,0 +1,97 @@
1
+ import { fitTitleSize } from "./feature-title-fit.mjs";
2
+
3
+ /** Fit a split article H1 so the remaining title prefers one second line. */
4
+ export function mountArticleTitle(root, chooseSize = fitTitleSize) {
5
+ if (!root) return;
6
+ const key = Symbol.for("garden.article-title-fit");
7
+ if (root[key]) { root[key].refresh(); return root[key].dispose; }
8
+ const heading = root.querySelector("h1.h1-br");
9
+ const lines = heading ? [...heading.querySelectorAll(".h1-line")] : [];
10
+ const sheet = root.querySelector("style[data-article-title-rules]")?.sheet;
11
+ if (!heading?.id || lines.length < 2 || !sheet || !window.ResizeObserver || !window.CSS?.escape) return;
12
+ const index = sheet.insertRule(`#${CSS.escape(heading.id)}.h1-br{}`, sheet.cssRules.length);
13
+ const rule = sheet.cssRules[index].style;
14
+ let frame = 0, disposed = false, signature = "", previous, readingFloor = 0;
15
+ const widest = (probe) => Math.max(...lines.map((line) => {
16
+ probe.textContent = line.textContent;
17
+ return probe.getBoundingClientRect().width;
18
+ }));
19
+ const fit = () => {
20
+ if (disposed || !heading.isConnected) return;
21
+ const priorSize = rule.fontSize, priorWrap = rule.whiteSpace;
22
+ rule.removeProperty("font-size");
23
+ rule.removeProperty("white-space");
24
+ const style = getComputedStyle(heading);
25
+ const parent = heading.parentElement || root;
26
+ const parentSize = parseFloat(getComputedStyle(parent).fontSize);
27
+ const rootSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
28
+ const article = heading.closest("article");
29
+ const bodyNode = article?.querySelector(".markdown-body") || article || document.documentElement;
30
+ const body = parseFloat(getComputedStyle(bodyNode).fontSize);
31
+ const dpr = window.devicePixelRatio, scale = window.visualViewport?.scale || 1;
32
+ const width = heading.getBoundingClientRect().width;
33
+ const text = lines.map((line) => line.textContent).join("\n");
34
+ const state = { text, parentSize, rootSize, body, dpr, scale };
35
+ if (previous?.text !== text) readingFloor = 0;
36
+ if (previous?.text === text) {
37
+ if (dpr < previous.dpr - .001 || scale < previous.scale - .001 || body < previous.body - .01 || rootSize < previous.rootSize - .01) readingFloor = 0;
38
+ if (dpr > previous.dpr + .001 || scale > previous.scale + .001 || body > previous.body + .01 || rootSize > previous.rootSize + .01) {
39
+ const rootGrowth = Math.max(1, rootSize / previous.rootSize);
40
+ readingFloor = Math.max(readingFloor, previous.size * rootGrowth / parentSize, previous.size / previous.parentSize);
41
+ }
42
+ }
43
+ const minimum = Math.max(body * 1.125, readingFloor * parentSize);
44
+ const maximum = Math.max(parseFloat(style.fontSize), minimum);
45
+ const nextSignature = JSON.stringify([text, width, body, parentSize, rootSize, minimum, maximum,
46
+ style.fontFamily, style.fontWeight, style.fontStyle, style.fontStretch, style.letterSpacing, style.fontFeatureSettings,
47
+ style.fontVariationSettings, style.fontSizeAdjust, style.fontKerning, style.wordSpacing, style.fontOpticalSizing, style.textTransform, dpr, scale]);
48
+ if (!(width > 0 && parentSize > 0 && body > 0) || nextSignature === signature) {
49
+ if (priorSize) rule.fontSize = priorSize;
50
+ if (priorWrap) rule.whiteSpace = priorWrap;
51
+ return;
52
+ }
53
+ const probe = document.createElement("span");
54
+ probe.style.cssText = "position:fixed;visibility:hidden;pointer-events:none;white-space:nowrap;width:max-content;max-width:none;inset:0 auto auto 0;";
55
+ for (const property of ["font", "fontKerning", "fontFeatureSettings", "fontVariationSettings", "fontStretch", "fontSizeAdjust", "fontOpticalSizing", "letterSpacing", "wordSpacing", "textTransform", "direction"]) probe.style[property] = style[property];
56
+ heading.append(probe);
57
+ let size;
58
+ try {
59
+ size = chooseSize({ width: Math.max(0, width - .5), minimum, maximum,
60
+ measure(value) { probe.style.fontSize = `${value}px`; return widest(probe); } });
61
+ } finally { probe.remove(); }
62
+ rule.fontSize = `${size / parentSize}em`;
63
+ const actual = parseFloat(getComputedStyle(heading).fontSize);
64
+ heading.append(probe);
65
+ try {
66
+ probe.style.fontSize = `${actual}px`;
67
+ if (widest(probe) <= Math.max(0, heading.getBoundingClientRect().width - .5)) rule.whiteSpace = "nowrap";
68
+ } finally { probe.remove(); }
69
+ signature = nextSignature;
70
+ previous = { ...state, size };
71
+ };
72
+ const refresh = () => { if (!disposed && !frame) frame = requestAnimationFrame(() => { frame = 0; fit(); }); };
73
+ fit();
74
+ const resize = new ResizeObserver(refresh);
75
+ resize.observe(heading);
76
+ if (heading.parentElement) resize.observe(heading.parentElement);
77
+ const mutations = new MutationObserver(refresh);
78
+ mutations.observe(heading, { childList: true, subtree: true, characterData: true });
79
+ for (const element of [document.documentElement, document.body]) mutations.observe(element, { attributes: true, attributeFilter: ["class", "style", "lang", "data-theme"] });
80
+ window.addEventListener("resize", refresh);
81
+ window.visualViewport?.addEventListener("resize", refresh);
82
+ const fontsChanged = () => { signature = ""; refresh(); };
83
+ document.fonts?.addEventListener("loadingdone", fontsChanged);
84
+ document.fonts?.ready.then(fontsChanged);
85
+ const dispose = () => {
86
+ disposed = true;
87
+ cancelAnimationFrame(frame);
88
+ resize.disconnect(); mutations.disconnect();
89
+ window.removeEventListener("resize", refresh);
90
+ window.visualViewport?.removeEventListener("resize", refresh);
91
+ document.fonts?.removeEventListener("loadingdone", fontsChanged);
92
+ if (sheet.cssRules[index]?.style === rule) sheet.deleteRule(index);
93
+ delete root[key];
94
+ };
95
+ root[key] = { refresh, dispose };
96
+ return dispose;
97
+ }
@@ -32,3 +32,7 @@ export function titleLineScale(lines) {
32
32
  const width = Math.max(...lines.map((line) => titleWidthUnits(line)), 0.01);
33
33
  return Number(width.toFixed(2));
34
34
  }
35
+
36
+ export function originalUrl(entry) {
37
+ return typeof entry?.original === "string" && /^https?:\/\//iu.test(entry.original) ? entry.original : "";
38
+ }
@@ -2532,6 +2532,15 @@
2532
2532
  min-width: 0;
2533
2533
  }
2534
2534
 
2535
+ /* Columnized centered rows stretch labels to 100% width, so align-items
2536
+ can center arrows but not text. Center direct labels only; explicit
2537
+ start/end and nested cards or prose stay as authored. */
2538
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-h:not([data-align="start"]):not([data-align="end"]),
2539
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-text:not([data-align="start"]):not([data-align="end"]),
2540
+ .g-compose-row:not([data-wrap="nowrap"])[data-align="center"] > .g-compose-caption-inline:not([data-align="start"]):not([data-align="end"]) {
2541
+ text-align: center;
2542
+ }
2543
+
2535
2544
  .g-compose-arrow[data-dir="right"] {
2536
2545
  transform: none;
2537
2546
  }