hd-garden 0.8.0 → 0.8.2
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/.agents/skills/garden-ui/SKILL.md +2 -2
- package/.env.en.example +67 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/app/a2/[slug]/page.tsx +4 -3
- package/app/a2/page.tsx +4 -3
- package/app/about/page.tsx +8 -9
- package/app/admin/page.tsx +7 -6
- package/app/admin/preview/[...path]/page.tsx +2 -1
- package/app/admin/ui/blocks/page.tsx +2 -1
- package/app/admin/ui/catalog/page.tsx +2 -1
- package/app/admin/ui/evals/page.tsx +2 -1
- package/app/admin/ui/page.tsx +2 -1
- package/app/blog/[...path]/page.tsx +7 -6
- package/app/blog/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +8 -7
- package/app/columns/page.tsx +12 -8
- package/app/components/AdminUiShell.tsx +4 -3
- package/app/components/ArticleMeasure.tsx +6 -3
- package/app/components/ArticleReferences.tsx +4 -1
- package/app/components/ArticleTags.tsx +3 -2
- package/app/components/BlogArchive.tsx +20 -16
- package/app/components/Comments.tsx +16 -10
- package/app/components/ContentEmptyState.tsx +29 -28
- package/app/components/ExternalEmbed.tsx +15 -14
- package/app/components/GardenContentGuide.tsx +10 -2
- package/app/components/GardenDocsNav.tsx +4 -2
- package/app/components/GiscusComments.tsx +2 -1
- package/app/components/GithubLiveOverview.tsx +10 -8
- package/app/components/GithubProfile.tsx +31 -21
- package/app/components/HandbookLangSwitch.tsx +8 -4
- package/app/components/HandbookView.tsx +92 -9
- package/app/components/MarkdownArticle.tsx +31 -7
- package/app/components/MermaidDiagram.tsx +4 -3
- package/app/components/SiteChrome.tsx +3 -3
- package/app/components/SiteFooter.tsx +1 -1
- package/app/components/SiteHeader.tsx +13 -4
- package/app/components/ThoughtCard.tsx +16 -14
- package/app/components/UiBlock.tsx +3 -2
- package/app/components/Webmentions.tsx +11 -11
- package/app/components/WidgetChart.tsx +8 -6
- package/app/components/garden/FilePreview.tsx +2 -2
- package/app/components/garden/GardenBlock.tsx +7 -0
- package/app/components/garden/GardenFile.tsx +2 -2
- package/app/components/garden/RefDemo.tsx +62 -0
- package/app/components/garden/blocks.tsx +56 -8
- package/app/components/garden/patterns.tsx +497 -0
- package/app/garden/[...slug]/page.tsx +3 -2
- package/app/garden/en/page.tsx +7 -7
- package/app/garden/page.tsx +3 -2
- package/app/globals.css +49 -1
- package/app/lib/a2-meta.ts +26 -3
- package/app/lib/content.ts +2 -2
- package/app/lib/garden-docs-meta.ts +24 -0
- package/app/lib/github-id.mjs +8 -0
- package/app/lib/handbook-switch.mjs +12 -0
- package/app/lib/i18n.ts +401 -2
- package/app/lib/rehype-field-tables.mjs +47 -0
- package/app/lib/remark-a2.mjs +18 -8
- package/app/lib/remark-link-card.mjs +22 -0
- package/app/lib/remark-ref-demos.mjs +42 -0
- package/app/lib/tag-filter.ts +9 -2
- package/app/not-found.tsx +32 -4
- package/app/page.tsx +17 -14
- package/app/rss.xml/route.ts +2 -1
- package/app/styles/a2.css +338 -0
- package/app/styles/shell.css +15 -2
- package/app/tags/page.tsx +7 -5
- package/app/thoughts/[slug]/page.tsx +2 -1
- package/app/thoughts/page/[n]/page.tsx +1 -1
- package/app/thoughts/page.tsx +2 -1
- package/app/ui-parity/page.tsx +38 -22
- package/bin/garden.mjs +129 -19
- package/docs/handbook/ai-writing.md +2 -2
- package/docs/handbook/assets/reading-layout-en.svg +26 -0
- package/docs/handbook/components/card.md +43 -22
- package/docs/handbook/components/chart.md +10 -26
- package/docs/handbook/components/choice.md +42 -0
- package/docs/handbook/components/compare.md +32 -17
- package/docs/handbook/components/figure.md +33 -46
- package/docs/handbook/components/file.md +10 -15
- package/docs/handbook/components/html.md +10 -30
- package/docs/handbook/components/interactive.md +9 -35
- package/docs/handbook/components/list.md +65 -0
- package/docs/handbook/components/relations.md +95 -0
- package/docs/handbook/components/stat.md +10 -28
- package/docs/handbook/components/timeline.md +13 -28
- package/docs/handbook/components.md +210 -19
- package/docs/handbook/config.md +5 -3
- package/docs/handbook/deploy.md +1 -1
- package/docs/handbook/design.md +20 -12
- package/docs/handbook/en/ai-writing.md +2 -2
- package/docs/handbook/en/components/card.md +42 -21
- package/docs/handbook/en/components/chart.md +10 -26
- package/docs/handbook/en/components/choice.md +42 -0
- package/docs/handbook/en/components/compare.md +30 -15
- package/docs/handbook/en/components/figure.md +29 -42
- package/docs/handbook/en/components/file.md +9 -14
- package/docs/handbook/en/components/html.md +9 -29
- package/docs/handbook/en/components/interactive.md +8 -34
- package/docs/handbook/en/components/list.md +65 -0
- package/docs/handbook/en/components/relations.md +95 -0
- package/docs/handbook/en/components/stat.md +9 -27
- package/docs/handbook/en/components/timeline.md +9 -24
- package/docs/handbook/en/components.md +211 -19
- package/docs/handbook/en/config.md +32 -30
- package/docs/handbook/en/deploy.md +12 -12
- package/docs/handbook/en/design.md +19 -12
- package/docs/handbook/en/index.md +2 -2
- package/docs/handbook/en/reference/card.md +85 -0
- package/docs/handbook/en/reference/chart.md +68 -0
- package/docs/handbook/en/reference/choice.md +69 -0
- package/docs/handbook/en/reference/compare.md +59 -0
- package/docs/handbook/en/reference/figure.md +84 -0
- package/docs/handbook/en/reference/file/checklist.md +17 -0
- package/docs/handbook/en/reference/file.md +50 -0
- package/docs/handbook/en/reference/html.md +62 -0
- package/docs/handbook/en/reference/interactive.md +71 -0
- package/docs/handbook/en/reference/list.md +72 -0
- package/docs/handbook/en/reference/relations.md +113 -0
- package/docs/handbook/en/reference/stat.md +60 -0
- package/docs/handbook/en/reference/timeline.md +56 -0
- package/docs/handbook/en/reference.md +26 -0
- package/docs/handbook/en/start.md +2 -2
- package/docs/handbook/en/workflow.md +6 -5
- package/docs/handbook/en/writing.md +3 -2
- package/docs/handbook/index.md +1 -1
- package/docs/handbook/reference/card.md +85 -0
- package/docs/handbook/reference/chart.md +68 -0
- package/docs/handbook/reference/choice.md +69 -0
- package/docs/handbook/reference/compare.md +59 -0
- package/docs/handbook/reference/figure.md +84 -0
- package/docs/handbook/reference/file/checklist.md +17 -0
- package/docs/handbook/reference/file.md +50 -0
- package/docs/handbook/reference/html.md +62 -0
- package/docs/handbook/reference/interactive.md +71 -0
- package/docs/handbook/reference/list.md +72 -0
- package/docs/handbook/reference/relations.md +113 -0
- package/docs/handbook/reference/stat.md +60 -0
- package/docs/handbook/reference/timeline.md +56 -0
- package/docs/handbook/reference.md +26 -0
- package/docs/handbook/start.md +1 -1
- package/docs/handbook/workflow.md +6 -5
- package/docs/handbook/writing.md +3 -2
- package/next.config.ts +6 -0
- package/package.json +2 -1
- package/public/images/garden/ref-card-book.svg +1 -0
- package/public/images/garden/ref-card-lamp.svg +1 -0
- package/scripts/build-content.mjs +37 -16
- package/scripts/favicon-ico.mjs +9 -6
- package/scripts/file-preview.mjs +10 -7
- package/scripts/handbook-allowlist.mjs +18 -0
- package/scripts/handbook-facts.mjs +305 -11
- package/scripts/link-assets.mjs +7 -0
- package/scripts/new-post.mjs +18 -9
- package/scripts/prepare-pages-output.mjs +30 -18
- package/scripts/redirects.mjs +17 -6
- package/templates/garden-blocks.md +67 -0
- package/templates/garden-snippets.plist +61 -0
- package/templates/site/Makefile.en +45 -0
- package/templates/site/README.en.md +15 -0
- package/templates/site/site.config.en.yaml +55 -0
- package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
- package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
- package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
- package/templates/skills-en/garden-post-review/SKILL.md +63 -0
- package/templates/skills-en/garden-ui/SKILL.md +118 -0
- package/templates/workflows/pages.en.yml +195 -0
- package/templates/workflows/pages.yml +195 -0
- package/ui/garden/catalog.mjs +205 -4
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/file-block.mjs +8 -7
- package/ui/garden/html-lang.mjs +73 -1
- package/ui/garden/markdown-links.mjs +7 -4
- package/ui/garden/parse.mjs +45 -0
- package/ui/garden/relations.mjs +252 -0
- package/ui/garden/search-index.mjs +6 -4
- package/ui/garden/source-icon.mjs +54 -0
- package/ui/parity-fixtures.mjs +77 -0
- package/ui/preview/dist/garden-ui.esm.js +41 -4
- package/ui/preview/dist/garden-ui.iife.js +41 -4
- package/ui/prompt/rules.md +28 -1
- package/ui/react/components/chart.tsx +1 -1
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/lang.tsx +4 -0
- package/ui/react/parity.tsx +8 -6
- package/worker/index.ts +8 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* garden relations: 3 to 6 key elements on a circle and the directed links between them.
|
|
3
|
+
* Shared by the parser (validation), the renderer and the tests.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @typedef {{ from: number, to: number, dashed: boolean, both: boolean, label: string }} RelationEdge */
|
|
7
|
+
|
|
8
|
+
/** 1-based index or exact title -> 0-based index, -1 when it names nothing. */
|
|
9
|
+
export function relationIndex(value, titles) {
|
|
10
|
+
if (typeof value === "number") return Number.isInteger(value) && value >= 1 && value <= titles.length ? value - 1 : -1;
|
|
11
|
+
const text = String(value ?? "").trim();
|
|
12
|
+
if (/^\d+$/u.test(text)) return relationIndex(Number(text), titles);
|
|
13
|
+
return titles.indexOf(text);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @returns {RelationEdge[]} */
|
|
17
|
+
export function relationEdges(props, titles) {
|
|
18
|
+
return (Array.isArray(props?.links) ? props.links : [])
|
|
19
|
+
.map((link) => link ?? {})
|
|
20
|
+
.map((link) => ({
|
|
21
|
+
from: relationIndex(link.from, titles),
|
|
22
|
+
to: relationIndex(link.to, titles),
|
|
23
|
+
dashed: link.style === "dashed",
|
|
24
|
+
both: link.direction === "both",
|
|
25
|
+
label: typeof link.label === "string" ? link.label : "",
|
|
26
|
+
}))
|
|
27
|
+
.filter((link) => link.from >= 0 && link.to >= 0 && link.from !== link.to);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Each node's outgoing edges as text, for narrow screens and screen readers. A both-way edge counts for both ends. */
|
|
31
|
+
/** @param {RelationEdge[]} edges @param {string[]} titles @returns {{ mark: string, to: string, label: string, dashed: boolean }[][]} */
|
|
32
|
+
export function relationLines(edges, titles) {
|
|
33
|
+
/** @type {{ mark: string, to: string, label: string, dashed: boolean }[][]} */
|
|
34
|
+
const lines = titles.map(() => []);
|
|
35
|
+
for (const link of edges) {
|
|
36
|
+
const mark = link.both ? (link.dashed ? "⇠⇢" : "↔") : link.dashed ? "⇢" : "→";
|
|
37
|
+
lines[link.from].push({ mark, to: titles[link.to], label: link.label, dashed: link.dashed });
|
|
38
|
+
if (link.both) lines[link.to].push({ mark, to: titles[link.from], label: link.label, dashed: link.dashed });
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where each node goes: `center` (0-based, or -1) sits in the middle, the rest
|
|
45
|
+
* evenly on the outer ring, first on top, clockwise. Percent of the ring width;
|
|
46
|
+
* `s` is the sine of the angle (vertical position, -1 top .. 1 bottom), 0 for the centre.
|
|
47
|
+
*/
|
|
48
|
+
export function relationLayout(n, centre = -1, compact = false) {
|
|
49
|
+
const hasCentre = centre >= 0 && centre < n;
|
|
50
|
+
const outer = hasCentre ? n - 1 : n;
|
|
51
|
+
// With a node in the middle, narrower nodes leave the spokes room for arrows and labels.
|
|
52
|
+
// Compact (narrow container): the same ring, with nodes a larger share of the width.
|
|
53
|
+
// Wide screens get wider nodes so an English title fits in two lines without breaking a word.
|
|
54
|
+
const w = compact ? (hasCentre ? 26 : n <= 4 ? 34 : 28) : hasCentre ? 26 : n <= 4 ? 32 : 30;
|
|
55
|
+
// A narrow hub has a narrower middle node, so each spoke keeps a visible line between the boxes.
|
|
56
|
+
const cw = hasCentre ? (compact ? 22 : 24) : w;
|
|
57
|
+
const rx = 50 - w / 2 - (compact && hasCentre ? 0.5 : 1);
|
|
58
|
+
const ring = [];
|
|
59
|
+
let k = 0;
|
|
60
|
+
const spots = Array.from({ length: n }, (_, i) => {
|
|
61
|
+
if (i === centre && hasCentre) return { x: 50, s: 0, w: cw, ring: -1 };
|
|
62
|
+
const angle = -Math.PI / 2 + (k * 2 * Math.PI) / outer;
|
|
63
|
+
ring.push(i);
|
|
64
|
+
const spot = { x: 50 + rx * Math.cos(angle), s: Math.sin(angle), w, ring: k };
|
|
65
|
+
k += 1;
|
|
66
|
+
return spot;
|
|
67
|
+
});
|
|
68
|
+
return { spots, ring, outer, centre: hasCentre ? centre : -1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Vertical placement from the measured node heights: the smallest radius that keeps
|
|
73
|
+
* nodes apart, then the canvas is exactly as tall as the nodes need.
|
|
74
|
+
* Only nodes whose columns overlap need vertical room between them.
|
|
75
|
+
* `minRy` keeps the ring round enough for arrows and labels to have room.
|
|
76
|
+
* @param {{ s: number, x: number, w: number }[]} spots @param {number[]} heights @param {number} gap @param {number} minRy
|
|
77
|
+
*/
|
|
78
|
+
export function relationHeights(spots, heights, gap = 36, minRy = 60) {
|
|
79
|
+
let ry = minRy;
|
|
80
|
+
for (let i = 0; i < spots.length; i += 1) {
|
|
81
|
+
for (let j = i + 1; j < spots.length; j += 1) {
|
|
82
|
+
const ds = Math.abs(spots[i].s - spots[j].s);
|
|
83
|
+
if (ds < 1e-6) continue;
|
|
84
|
+
if (Math.abs(spots[i].x - spots[j].x) >= (spots[i].w + spots[j].w) / 2 + 2) continue;
|
|
85
|
+
ry = Math.max(ry, ((heights[i] + heights[j]) / 2 + gap) / ds);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
ry = Math.min(ry, Math.max(280, minRy));
|
|
89
|
+
const top = Math.min(...spots.map((spot, i) => spot.s * ry - heights[i] / 2));
|
|
90
|
+
const ys = spots.map((spot) => spot.s * ry - top);
|
|
91
|
+
const height = Math.max(...spots.map((spot, i) => ys[i] + heights[i] / 2));
|
|
92
|
+
return { ys, height, ry };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Intersection point of segments p1-p2 and q1-q2, or null. */
|
|
96
|
+
export function crossing(p1, p2, q1, q2) {
|
|
97
|
+
const d = (p2.x - p1.x) * (q2.y - q1.y) - (p2.y - p1.y) * (q2.x - q1.x);
|
|
98
|
+
if (Math.abs(d) < 1e-9) return null;
|
|
99
|
+
const t = ((q1.x - p1.x) * (q2.y - q1.y) - (q1.y - p1.y) * (q2.x - q1.x)) / d;
|
|
100
|
+
const u = ((q1.x - p1.x) * (p2.y - p1.y) - (q1.y - p1.y) * (p2.x - p1.x)) / d;
|
|
101
|
+
if (t <= 0.02 || t >= 0.98 || u <= 0.02 || u >= 0.98) return null;
|
|
102
|
+
return { x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y) };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Label positions: the midpoint, unless a crossing with another line (or an earlier
|
|
107
|
+
* label) is within `clear` px; then slide along the line to the nearest clear spot.
|
|
108
|
+
* @param {{ at: (t: number) => { x: number, y: number }, a: object, b: object, label: string }[]} lines
|
|
109
|
+
*/
|
|
110
|
+
export function placeLabels(lines, clear = 34) {
|
|
111
|
+
const points = [];
|
|
112
|
+
lines.forEach((line, i) => lines.forEach((other, j) => {
|
|
113
|
+
if (j <= i) return;
|
|
114
|
+
const hit = crossing(line.a, line.b, other.a, other.b);
|
|
115
|
+
if (hit) points.push(hit);
|
|
116
|
+
}));
|
|
117
|
+
const placed = [];
|
|
118
|
+
const steps = [0.5, 0.42, 0.58, 0.34, 0.66, 0.27, 0.73, 0.2, 0.8];
|
|
119
|
+
return lines.map((line) => {
|
|
120
|
+
if (!line.label) return null;
|
|
121
|
+
let best = line.at(0.5);
|
|
122
|
+
let bestRoom = -1;
|
|
123
|
+
for (const t of steps) {
|
|
124
|
+
const p = line.at(t);
|
|
125
|
+
const room = Math.min(Infinity, ...points.map((q) => Math.hypot(p.x - q.x, p.y - q.y)), ...placed.map((q) => Math.hypot(p.x - q.x, p.y - q.y)));
|
|
126
|
+
if (room >= clear) { best = p; bestRoom = room; break; }
|
|
127
|
+
if (room > bestRoom) { best = p; bestRoom = room; }
|
|
128
|
+
}
|
|
129
|
+
placed.push(best);
|
|
130
|
+
return best;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Hard floors after scaling (px). */
|
|
135
|
+
export const RELATION_NODE_FONT_MIN = 13;
|
|
136
|
+
export const RELATION_LABEL_FONT_MIN = 12;
|
|
137
|
+
/** Below this container width the graph uses the compact node size (same layout, scaled). */
|
|
138
|
+
export const RELATION_COMPACT_WIDTH = 560;
|
|
139
|
+
|
|
140
|
+
/** ①, ②, … ⑳ for edge labels that are moved under the graph. */
|
|
141
|
+
export function circled(i) {
|
|
142
|
+
return i < 20 ? String.fromCharCode(0x2460 + i) : `(${i + 1})`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Does a one-line label of `width` x `height` px fit on the segment a-b at point p?
|
|
147
|
+
* It must leave room for both arrowheads and must not touch any node box.
|
|
148
|
+
*/
|
|
149
|
+
export function labelFits({ width, height }, a, b, p, boxes, margin = 4) {
|
|
150
|
+
const length = Math.hypot(b.x - a.x, b.y - a.y);
|
|
151
|
+
// Measure along the line: the label's extent in the line's direction.
|
|
152
|
+
const ux = length ? Math.abs(b.x - a.x) / length : 1;
|
|
153
|
+
const uy = length ? Math.abs(b.y - a.y) / length : 0;
|
|
154
|
+
const along = width * ux + height * uy;
|
|
155
|
+
if (along + 2 * 14 > length) return false;
|
|
156
|
+
const box = { x: p.x - width / 2 - margin, y: p.y - height / 2 - margin, w: width + 2 * margin, h: height + 2 * margin };
|
|
157
|
+
return !boxes.some((n) => box.x < n.x + n.w && box.x + box.w > n.x && box.y < n.y + n.h && box.y + box.h > n.y);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** "① 依赖:A → B" / "① depends: A → B" */
|
|
161
|
+
export function legendLine(index, edge, titles, lang = "zh") {
|
|
162
|
+
const arrow = edge.both ? "↔" : "→";
|
|
163
|
+
const sep = lang === "en" ? ": " : ":";
|
|
164
|
+
return `${circled(index)} ${edge.label}${sep}${titles[edge.from]} ${arrow} ${titles[edge.to]}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Where a line from the centre of `box` towards `to` leaves the box, `gap` px outside it. */
|
|
168
|
+
export function edgePoint(box, to, gap = 6) {
|
|
169
|
+
const c = { x: box.x + box.w / 2, y: box.y + box.h / 2 };
|
|
170
|
+
const dx = to.x - c.x;
|
|
171
|
+
const dy = to.y - c.y;
|
|
172
|
+
if (!dx && !dy) return c;
|
|
173
|
+
const scale = Math.min(dx ? (box.w / 2 + gap) / Math.abs(dx) : Infinity, dy ? (box.h / 2 + gap) / Math.abs(dy) : Infinity);
|
|
174
|
+
return { x: c.x + dx * scale, y: c.y + dy * scale };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const BADGE_SIZE = 22;
|
|
178
|
+
|
|
179
|
+
const hits = (r, s) => r.x < s.x + s.w && r.x + r.w > s.x && r.y < s.y + s.h && r.y + r.h > s.y;
|
|
180
|
+
const rectAt = (p, w, h, pad) => ({ x: p.x - w / 2 - pad, y: p.y - h / 2 - pad, w: w + 2 * pad, h: h + 2 * pad });
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Place every edge mark so none overlaps a node or another mark.
|
|
184
|
+
* marks: [{ a, b, at(k), w, h }] with w/h of the text label, or w = 0 for "no label".
|
|
185
|
+
* A text label stays on its line (sliding along it) or, if no clear spot exists, becomes a badge.
|
|
186
|
+
* A badge sits on its line when clear, else beside it, else at the nearest clear spot in the frame.
|
|
187
|
+
* Returns [{ x, y, badge }] (null for marks without a label).
|
|
188
|
+
*/
|
|
189
|
+
export function placeMarks(marks, nodes, frame) {
|
|
190
|
+
const points = [];
|
|
191
|
+
marks.forEach((m, i) => marks.forEach((o, j) => {
|
|
192
|
+
if (j > i) { const hit = crossing(m.a, m.b, o.a, o.b); if (hit) points.push(hit); }
|
|
193
|
+
}));
|
|
194
|
+
const placed = [];
|
|
195
|
+
const free = (p, w, h, near = 0) => {
|
|
196
|
+
const r = rectAt(p, w, h, 0);
|
|
197
|
+
if (r.x < 0 || r.y < 0 || r.x + r.w > frame.w || r.y + r.h > frame.h) return false;
|
|
198
|
+
if (nodes.some((n) => hits(rectAt(p, w, h, 3), n))) return false;
|
|
199
|
+
if (placed.some((q) => hits(rectAt(p, w, h, 2), q))) return false;
|
|
200
|
+
return !points.some((c) => Math.hypot(c.x - p.x, c.y - p.y) < near);
|
|
201
|
+
};
|
|
202
|
+
const steps = [0.5, 0.42, 0.58, 0.34, 0.66, 0.27, 0.73, 0.2, 0.8];
|
|
203
|
+
return marks.map((m) => {
|
|
204
|
+
if (!m.w) return null;
|
|
205
|
+
const length = Math.hypot(m.b.x - m.a.x, m.b.y - m.a.y) || 1;
|
|
206
|
+
const nx = -(m.b.y - m.a.y) / length;
|
|
207
|
+
const ny = (m.b.x - m.a.x) / length;
|
|
208
|
+
let spot = null;
|
|
209
|
+
let badge = !!m.badge;
|
|
210
|
+
if (!badge) {
|
|
211
|
+
for (const near of [m.h + 14, 0]) {
|
|
212
|
+
spot = steps.map((k) => m.at(k)).find((p) => free(p, m.w, m.h, near)) ?? null;
|
|
213
|
+
if (spot) break;
|
|
214
|
+
}
|
|
215
|
+
if (!spot) badge = true;
|
|
216
|
+
}
|
|
217
|
+
const w = badge ? BADGE_SIZE : m.w;
|
|
218
|
+
const h = badge ? BADGE_SIZE : m.h;
|
|
219
|
+
if (badge) {
|
|
220
|
+
// On the line (unless it is too short to show its arrow too), then beside it, then anywhere clear.
|
|
221
|
+
const offsets = length < 64 ? [18, -18, 30, -30, 42, -42] : [0, 18, -18, 30, -30, 42, -42];
|
|
222
|
+
outer: for (const d of offsets) for (const near of d ? [0] : [24, 0]) for (const k of steps) {
|
|
223
|
+
const p = m.at(k);
|
|
224
|
+
const q = { x: p.x + nx * d, y: p.y + ny * d };
|
|
225
|
+
if (free(q, w, h, near)) { spot = q; break outer; }
|
|
226
|
+
}
|
|
227
|
+
if (!spot) {
|
|
228
|
+
const mid = m.at(0.5);
|
|
229
|
+
for (let r = 8; r < Math.max(frame.w, frame.h) && !spot; r += 6) {
|
|
230
|
+
for (let t = 0; t < 24; t++) {
|
|
231
|
+
const q = { x: mid.x + r * Math.cos((t * Math.PI) / 12), y: mid.y + r * Math.sin((t * Math.PI) / 12) };
|
|
232
|
+
if (free(q, w, h)) { spot = q; break; }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
spot ??= m.at(0.5);
|
|
237
|
+
}
|
|
238
|
+
placed.push(rectAt(spot, w, h, 0));
|
|
239
|
+
return { x: spot.x, y: spot.y, badge };
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Keyboard on a relations graph with an open details panel.
|
|
245
|
+
* @param {string} key
|
|
246
|
+
* @param {number | null} open index of the node whose details are open
|
|
247
|
+
* @returns {{ close: boolean, focus: number | null }} close the panel? which node gets focus back?
|
|
248
|
+
*/
|
|
249
|
+
export function relationKeyAction(key, open) {
|
|
250
|
+
if (key === "Escape" && open !== null && open >= 0) return { close: true, focus: open };
|
|
251
|
+
return { close: false, focus: null };
|
|
252
|
+
}
|
|
@@ -73,7 +73,9 @@ const columnKey = (item) => item.sourcePath ?? columnEntryPath(item);
|
|
|
73
73
|
* empty section. A column entry is only listed on its own when no post has
|
|
74
74
|
* the same source (a column-only page).
|
|
75
75
|
*/
|
|
76
|
-
function searchEntries({ posts = [], columns = [], thoughts = [] }, { length = EXCERPT_LENGTH } = {}) {
|
|
76
|
+
function searchEntries({ posts = [], columns = [], thoughts = [] }, { length = EXCERPT_LENGTH, lang = "zh" } = {}) {
|
|
77
|
+
const defaultColumnSection = lang === "en" ? "Columns" : "专栏";
|
|
78
|
+
const defaultThoughtSection = lang === "en" ? "Thoughts" : "随想";
|
|
77
79
|
const record = (item, extra) => {
|
|
78
80
|
const text = searchableText(item.content ?? "");
|
|
79
81
|
return {
|
|
@@ -89,14 +91,14 @@ function searchEntries({ posts = [], columns = [], thoughts = [] }, { length = E
|
|
|
89
91
|
const columnOf = new Map();
|
|
90
92
|
for (const item of columns) {
|
|
91
93
|
const key = columnKey(item);
|
|
92
|
-
if (postKeys.has(key) && !columnOf.has(key)) columnOf.set(key, item.columnTitle ??
|
|
94
|
+
if (postKeys.has(key) && !columnOf.has(key)) columnOf.set(key, item.columnTitle ?? defaultColumnSection);
|
|
93
95
|
}
|
|
94
96
|
return [
|
|
95
97
|
...posts.map((item) => ({ item, record: record(item, () => ({ section: columnOf.get(entryKey(item)) ?? "", path: `/blog/${item.path}` })) })),
|
|
96
98
|
...columns
|
|
97
99
|
.filter((item) => !postKeys.has(columnKey(item)))
|
|
98
|
-
.map((item) => ({ item, record: record(item, () => ({ section: item.columnTitle ??
|
|
99
|
-
...thoughts.map((item) => ({ item, record: record(item, (text) => ({ section:
|
|
100
|
+
.map((item) => ({ item, record: record(item, () => ({ section: item.columnTitle ?? defaultColumnSection, path: `/columns/${item.column}/${columnEntryPath(item)}` })) })),
|
|
101
|
+
...thoughts.map((item) => ({ item, record: record(item, (text) => ({ section: defaultThoughtSection, path: `/thoughts/${item.slug}`, description: excerpt(text, length) })) })),
|
|
100
102
|
];
|
|
101
103
|
}
|
|
102
104
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One icon per site, wherever the site appears (inline link, {source} chip,
|
|
3
|
+
* card sources): the favicon fetched at build time, else the first letter of
|
|
4
|
+
* the registrable domain's main label (en.wikipedia.org -> W, bbc.co.uk -> B).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Common two-part public suffixes; enough for a fallback letter.
|
|
8
|
+
const SECOND_LEVEL = new Set(["co", "com", "net", "org", "gov", "edu", "ac", "or", "ne", "go", "gob", "nic"]);
|
|
9
|
+
|
|
10
|
+
export function sourceHost(url) {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(url).hostname.toLowerCase().replace(/^www\./u, "");
|
|
13
|
+
} catch {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The main label of the registrable domain: en.wikipedia.org -> wikipedia. */
|
|
19
|
+
export function registrableLabel(host) {
|
|
20
|
+
const parts = String(host ?? "").toLowerCase().replace(/^www\./u, "").split(".").filter(Boolean);
|
|
21
|
+
if (parts.length < 2) return parts[0] ?? "";
|
|
22
|
+
if (/^\d+$/u.test(parts.at(-1))) return parts[0]; // IP address
|
|
23
|
+
const last = parts.at(-1);
|
|
24
|
+
const second = parts.at(-2);
|
|
25
|
+
if (parts.length >= 3 && last.length === 2 && SECOND_LEVEL.has(second)) return parts.at(-3);
|
|
26
|
+
return second;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function sourceLetter(host) {
|
|
30
|
+
const label = registrableLabel(host);
|
|
31
|
+
return label ? [...label][0].toUpperCase() : "·";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** { src } when there is an image, else { letter }. The fetched favicon wins over an author icon. */
|
|
35
|
+
export function sourceIcon(url, favicons, explicit) {
|
|
36
|
+
const host = sourceHost(url);
|
|
37
|
+
const fetched = favicons?.[host];
|
|
38
|
+
const src = (typeof fetched === "string" ? fetched : fetched?.file) || (typeof explicit === "string" && explicit ? explicit : "");
|
|
39
|
+
return src ? { src } : { letter: sourceLetter(host) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** URLs named as card sources inside ```garden card fences, so their favicons get fetched too. */
|
|
43
|
+
export function gardenSourceUrls(markdown) {
|
|
44
|
+
const urls = [];
|
|
45
|
+
const fence = /^```garden\s+card\b[^\n]*\n([\s\S]*?)^```/gmu;
|
|
46
|
+
for (const [, body] of String(markdown ?? "").matchAll(fence)) {
|
|
47
|
+
for (const line of body.split("\n")) {
|
|
48
|
+
// `url: …`, `source: …`, or a bare `- …` entry of a source list.
|
|
49
|
+
const m = line.match(/^\s*(?:-\s*)?(?:(?:url|source)\s*:\s*)?["']?(https?:\/\/[^\s"']+)/u);
|
|
50
|
+
if (m) urls.push(m[1]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return urls;
|
|
54
|
+
}
|
package/ui/parity-fixtures.mjs
CHANGED
|
@@ -74,3 +74,80 @@ fmt.Println(b)\`} />
|
|
|
74
74
|
`,
|
|
75
75
|
},
|
|
76
76
|
];
|
|
77
|
+
|
|
78
|
+
const enHeader = (id, caption) => `---
|
|
79
|
+
id: ${id}
|
|
80
|
+
caption: ${caption}
|
|
81
|
+
intent: |
|
|
82
|
+
Pixel parity check
|
|
83
|
+
---
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
export const PARITY_FIXTURES_EN = [
|
|
87
|
+
{
|
|
88
|
+
id: "parity-slider",
|
|
89
|
+
width: 768,
|
|
90
|
+
body: `${enHeader("parity-slider", "Longer sequences require more KV memory.")}
|
|
91
|
+
<State seq={4096} />
|
|
92
|
+
<Slider bind="seq" label="Sequence length" min={1024} max={32768} scale="log" format="thousands" unit="token" />
|
|
93
|
+
`,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "parity-chart",
|
|
97
|
+
width: 768,
|
|
98
|
+
body: `${enHeader("parity-chart", "Memory grows linearly with sequence length.")}
|
|
99
|
+
<State seq={4096} />
|
|
100
|
+
<Let mem={2 * 32 * 8 * 128 * seq * 2} />
|
|
101
|
+
<Chart sweep="seq" from={1024} to={32768} scale="log" height={220} xLabel="Sequence length" yLabel="KV memory">
|
|
102
|
+
<Line y={mem} label="KV memory" format="bytes" />
|
|
103
|
+
</Chart>
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "parity-steps",
|
|
108
|
+
width: 768,
|
|
109
|
+
body: `${enHeader("parity-steps", "Prefill writes K/V in one pass; Decode appends one row per step.")}
|
|
110
|
+
<Steps>
|
|
111
|
+
<Step title="Prefill">Write all K/V rows in one pass.</Step>
|
|
112
|
+
<Step title="Decode">Append a single row on this tick.</Step>
|
|
113
|
+
<Step title="Done">21 projections reduced to 7.</Step>
|
|
114
|
+
</Steps>
|
|
115
|
+
`,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "parity-grid",
|
|
119
|
+
width: 768,
|
|
120
|
+
body: `${enHeader("parity-grid", "Under ECB, identical plaintext blocks produce identical ciphertext blocks.")}
|
|
121
|
+
<Grid cols={["Block 1", "Block 2", "Block 3"]} legend={{ same: "Identical plaintext produces identical ciphertext" }}>
|
|
122
|
+
<Row label="Plaintext" cells={["A", "B", "A"]} tones={["same", "plain", "same"]} />
|
|
123
|
+
<Row label="ECB Ciphertext" cells={["X", "Y", "X"]} tones={["same", "plain", "same"]} notes={["Same as Block 3", "", "Same as Block 1"]} />
|
|
124
|
+
</Grid>
|
|
125
|
+
`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "parity-code",
|
|
129
|
+
width: 768,
|
|
130
|
+
body: `${enHeader("parity-code", "This step performs the accumulation.")}
|
|
131
|
+
<Code lang="go" highlight="2" code={\`a := 1
|
|
132
|
+
b := a + 1
|
|
133
|
+
fmt.Println(b)\`} />
|
|
134
|
+
`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "parity-diagram",
|
|
138
|
+
width: 768,
|
|
139
|
+
body: `${enHeader("parity-diagram", "Leaves SYN-SENT only after receiving SYN+ACK.")}
|
|
140
|
+
<Diagram highlight={["synsent"]} mermaid={\`flowchart LR
|
|
141
|
+
closed[CLOSED] --> synsent[SYN-SENT]
|
|
142
|
+
synsent --> established[ESTABLISHED]\`} />
|
|
143
|
+
`,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "parity-sandbox",
|
|
147
|
+
width: 768,
|
|
148
|
+
body: `${enHeader("parity-sandbox", "Custom interactions outside the catalog run in a networkless canvas.")}
|
|
149
|
+
<Sandbox title="Counter" html={"<p id=\\"n\\">0</p><button id=\\"b\\" type=\\"button\\">Add one</button>"} css="button{color:var(--blue)}" js="document.getElementById('b').onclick=()=>{const n=document.getElementById('n');n.textContent=String(Number(n.textContent)+1)}" />
|
|
150
|
+
`,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|