@riebeckite/plugin-garden-explorer 0.0.14 → 0.0.16
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/README.md +72 -19
- package/README_ja.md +46 -16
- package/dist/client.js +331 -50
- package/dist/components/garden-explorer.js +331 -50
- package/dist/index.d.ts +3 -2
- package/dist/index.js +362 -57
- package/dist/src/garden-explorer.d.ts +15 -0
- package/dist/src/garden-explorer.server.d.ts +3 -1
- package/package.json +4 -3
- package/style.css +63 -0
package/dist/index.js
CHANGED
|
@@ -3,13 +3,12 @@ import { createClientEntry, definePlugin } from "@riebeckite/core";
|
|
|
3
3
|
|
|
4
4
|
// src/garden-explorer.server.ts
|
|
5
5
|
import {
|
|
6
|
-
buildGraphEdges
|
|
7
|
-
isPublished
|
|
6
|
+
buildGraphEdges
|
|
8
7
|
} from "@riebeckite/core";
|
|
9
8
|
var MAX_BODY_LENGTH = 4e3;
|
|
10
9
|
function getGardenExplorerData(args) {
|
|
11
10
|
const graph = args.manifest.graph;
|
|
12
|
-
const publishedEntries =
|
|
11
|
+
const publishedEntries = args.manifest.discoverableEntries;
|
|
13
12
|
const publishedSlugs = new Set(publishedEntries.map((entry) => entry.slug));
|
|
14
13
|
const notes = publishedEntries.map(
|
|
15
14
|
(entry) => toGardenNote(entry, graph, publishedSlugs, args.resolveTitle)
|
|
@@ -18,9 +17,25 @@ function getGardenExplorerData(args) {
|
|
|
18
17
|
notes,
|
|
19
18
|
edges: buildGraphEdges(notes, publishedSlugs),
|
|
20
19
|
tags: buildTags(notes),
|
|
21
|
-
folders: buildFolders(notes)
|
|
20
|
+
folders: buildFolders(notes),
|
|
21
|
+
options: resolveGardenExplorerOptions(args.options)
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
function resolveGardenExplorerOptions(options = {}) {
|
|
25
|
+
return {
|
|
26
|
+
layout: resolveGraphLayout(options.layout),
|
|
27
|
+
depth: clampInteger(options.depth ?? 1, 0, 4),
|
|
28
|
+
showTags: options.showTags ?? true,
|
|
29
|
+
showFolders: options.showFolders ?? true,
|
|
30
|
+
nodeSize: clampNumber(options.nodeSize ?? 1, 0.6, 2),
|
|
31
|
+
linkDistance: clampNumber(options.linkDistance ?? 84, 36, 180),
|
|
32
|
+
repulsion: clampNumber(options.repulsion ?? 1800, 200, 8e3),
|
|
33
|
+
showLabels: options.showLabels ?? true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function resolveGraphLayout(layout) {
|
|
37
|
+
return layout === "radial" || layout === "force" ? layout : "force";
|
|
38
|
+
}
|
|
24
39
|
function toGardenNote(entry, graph, publishedSlugs, resolveTitle) {
|
|
25
40
|
const { outgoingSlugs, incomingSlugs } = graph.filterNeighbors(
|
|
26
41
|
entry.slug,
|
|
@@ -87,15 +102,47 @@ function getEntryDate(frontmatter) {
|
|
|
87
102
|
function toPlainText(html) {
|
|
88
103
|
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ").replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, " ").trim();
|
|
89
104
|
}
|
|
105
|
+
function clampInteger(value, min, max) {
|
|
106
|
+
return Math.round(clampNumber(value, min, max));
|
|
107
|
+
}
|
|
108
|
+
function clampNumber(value, min, max) {
|
|
109
|
+
if (!Number.isFinite(value)) return min;
|
|
110
|
+
return Math.min(Math.max(value, min), max);
|
|
111
|
+
}
|
|
90
112
|
|
|
91
113
|
// src/garden-explorer-page.tsx
|
|
92
114
|
import { escapeScriptJson } from "@riebeckite/core";
|
|
93
115
|
import { renderToString } from "hono/jsx/dom/server";
|
|
94
116
|
|
|
95
117
|
// components/garden-explorer.tsx
|
|
96
|
-
import {
|
|
118
|
+
import {
|
|
119
|
+
layoutForceGraph,
|
|
120
|
+
layoutRadialGraph,
|
|
121
|
+
shouldGuardForceLayout
|
|
122
|
+
} from "@riebeckite/core/client";
|
|
97
123
|
import { useEffect, useMemo, useState } from "hono/jsx";
|
|
98
124
|
|
|
125
|
+
// src/garden-explorer.ts
|
|
126
|
+
function getGardenExplorerLocalGraphNotes(notes, selectedSlug, depth) {
|
|
127
|
+
const noteBySlug = new Map(notes.map((note) => [note.slug, note]));
|
|
128
|
+
const visible = /* @__PURE__ */ new Set([selectedSlug]);
|
|
129
|
+
let frontier = [selectedSlug];
|
|
130
|
+
for (let level = 0; level < depth; level++) {
|
|
131
|
+
const nextFrontier = [];
|
|
132
|
+
for (const slug of frontier) {
|
|
133
|
+
const note = noteBySlug.get(slug);
|
|
134
|
+
if (!note) continue;
|
|
135
|
+
for (const neighbor of [...note.outgoing, ...note.backlinks]) {
|
|
136
|
+
if (!noteBySlug.has(neighbor) || visible.has(neighbor)) continue;
|
|
137
|
+
visible.add(neighbor);
|
|
138
|
+
nextFrontier.push(neighbor);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
frontier = nextFrontier;
|
|
142
|
+
}
|
|
143
|
+
return notes.filter((note) => visible.has(note.slug));
|
|
144
|
+
}
|
|
145
|
+
|
|
99
146
|
// src/search-notes.ts
|
|
100
147
|
var FIELD_WEIGHTS = {
|
|
101
148
|
slug: 64,
|
|
@@ -249,11 +296,12 @@ function LinkedNoteSection(props) {
|
|
|
249
296
|
}
|
|
250
297
|
|
|
251
298
|
// components/garden-explorer.tsx
|
|
252
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "hono/jsx/jsx-runtime";
|
|
299
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "hono/jsx/jsx-runtime";
|
|
253
300
|
var GRAPH_WIDTH = 900;
|
|
254
301
|
var GRAPH_HEIGHT = 620;
|
|
255
302
|
var MIN_SCALE = 0.6;
|
|
256
303
|
var MAX_SCALE = 2.4;
|
|
304
|
+
var DRAG_THRESHOLD_PX = 4;
|
|
257
305
|
function GardenExplorer(props) {
|
|
258
306
|
const noteBySlug = useMemo(
|
|
259
307
|
() => new Map(props.data.notes.map((note) => [note.slug, note])),
|
|
@@ -266,7 +314,13 @@ function GardenExplorer(props) {
|
|
|
266
314
|
() => props.data.notes[0]?.slug ?? ""
|
|
267
315
|
);
|
|
268
316
|
const [mobilePanel, setMobilePanel] = useState("graph");
|
|
317
|
+
const [graphMode, setGraphMode] = useState("local");
|
|
318
|
+
const [hoveredSlug, setHoveredSlug] = useState(null);
|
|
269
319
|
const [view, setView] = useState({ x: 0, y: 0, scale: 1 });
|
|
320
|
+
const [drag, setDrag] = useState(null);
|
|
321
|
+
const [suppressNodeNavigation, setSuppressNodeNavigation] = useState(false);
|
|
322
|
+
const [pinnedNodes, setPinnedNodes] = useState({});
|
|
323
|
+
const [forceLayoutApproved, setForceLayoutApproved] = useState(false);
|
|
270
324
|
useEffect(() => {
|
|
271
325
|
const params = new URLSearchParams(window.location.search);
|
|
272
326
|
const note = params.get("note");
|
|
@@ -291,8 +345,9 @@ function GardenExplorer(props) {
|
|
|
291
345
|
return true;
|
|
292
346
|
});
|
|
293
347
|
const searched = query ? searchGardenExplorerNotes(base, query) : base;
|
|
294
|
-
return searched
|
|
348
|
+
return searched;
|
|
295
349
|
}, [props.data.notes, query, selectedTag, selectedFolder]);
|
|
350
|
+
const listNotes = useMemo(() => filteredNotes.slice(0, 80), [filteredNotes]);
|
|
296
351
|
const visibleSlugSet = useMemo(
|
|
297
352
|
() => new Set(filteredNotes.map((note) => note.slug)),
|
|
298
353
|
[filteredNotes]
|
|
@@ -303,29 +358,140 @@ function GardenExplorer(props) {
|
|
|
303
358
|
),
|
|
304
359
|
[props.data.edges, visibleSlugSet]
|
|
305
360
|
);
|
|
361
|
+
const graphNotes = useMemo(() => {
|
|
362
|
+
if (graphMode === "local") {
|
|
363
|
+
return getGardenExplorerLocalGraphNotes(
|
|
364
|
+
filteredNotes,
|
|
365
|
+
selectedSlug,
|
|
366
|
+
props.data.options.depth
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const notes = filteredNotes;
|
|
370
|
+
const selectedNote2 = noteBySlug.get(selectedSlug);
|
|
371
|
+
if (selectedNote2 && !notes.some((n) => n.slug === selectedSlug)) {
|
|
372
|
+
return [selectedNote2, ...notes];
|
|
373
|
+
}
|
|
374
|
+
return notes;
|
|
375
|
+
}, [
|
|
376
|
+
filteredNotes,
|
|
377
|
+
graphMode,
|
|
378
|
+
selectedSlug,
|
|
379
|
+
props.data.options.depth,
|
|
380
|
+
noteBySlug
|
|
381
|
+
]);
|
|
382
|
+
const graphSlugSet = useMemo(
|
|
383
|
+
() => new Set(graphNotes.map((note) => note.slug)),
|
|
384
|
+
[graphNotes]
|
|
385
|
+
);
|
|
386
|
+
const graphEdges = useMemo(
|
|
387
|
+
() => visibleEdges.filter(
|
|
388
|
+
(edge) => graphSlugSet.has(edge.source) && graphSlugSet.has(edge.target)
|
|
389
|
+
),
|
|
390
|
+
[visibleEdges, graphSlugSet]
|
|
391
|
+
);
|
|
392
|
+
const graphKey = useMemo(
|
|
393
|
+
() => graphNotes.map((note) => note.slug).join("\0"),
|
|
394
|
+
[graphNotes]
|
|
395
|
+
);
|
|
396
|
+
const shouldGuard = shouldGuardForceLayout({
|
|
397
|
+
layout: props.data.options.layout,
|
|
398
|
+
mode: graphMode,
|
|
399
|
+
nodeCount: graphNotes.length,
|
|
400
|
+
approved: forceLayoutApproved
|
|
401
|
+
});
|
|
402
|
+
const baseGraphNodes = useMemo(() => {
|
|
403
|
+
if (shouldGuard) {
|
|
404
|
+
return layoutRadialGraph(graphNotes, {
|
|
405
|
+
width: GRAPH_WIDTH,
|
|
406
|
+
height: GRAPH_HEIGHT,
|
|
407
|
+
centerSlug: selectedSlug
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return layoutNodes(graphNotes, selectedSlug, props.data.options);
|
|
411
|
+
}, [shouldGuard, graphNotes, selectedSlug, props.data.options]);
|
|
306
412
|
const graphNodes = useMemo(
|
|
307
|
-
() =>
|
|
308
|
-
[
|
|
413
|
+
() => applyPinnedNodes(baseGraphNodes, pinnedNodes),
|
|
414
|
+
[baseGraphNodes, pinnedNodes]
|
|
309
415
|
);
|
|
310
|
-
const selectedNote = noteBySlug.get(selectedSlug) ??
|
|
416
|
+
const selectedNote = noteBySlug.get(selectedSlug) ?? listNotes[0] ?? null;
|
|
311
417
|
const relatedNotes = useMemo(
|
|
312
418
|
() => selectedNote ? getRelatedNotes(selectedNote, props.data.notes) : [],
|
|
313
419
|
[selectedNote, props.data.notes]
|
|
314
420
|
);
|
|
421
|
+
const emphasizedSlugs = useMemo(
|
|
422
|
+
() => getRelatedSlugSet(selectedSlug, hoveredSlug, graphEdges),
|
|
423
|
+
[selectedSlug, hoveredSlug, graphEdges]
|
|
424
|
+
);
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
if (filteredNotes.length === 0 || visibleSlugSet.has(selectedSlug)) return;
|
|
427
|
+
setSelectedSlug(listNotes[0]?.slug ?? "");
|
|
428
|
+
}, [filteredNotes, selectedSlug, visibleSlugSet, listNotes]);
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
setHoveredSlug(null);
|
|
431
|
+
setDrag(null);
|
|
432
|
+
setSuppressNodeNavigation(false);
|
|
433
|
+
setPinnedNodes({});
|
|
434
|
+
setForceLayoutApproved(false);
|
|
435
|
+
}, [graphKey, graphMode, props.data.options.layout]);
|
|
315
436
|
const selectNote = (slug) => {
|
|
316
437
|
setSelectedSlug(slug);
|
|
317
438
|
setMobilePanel("details");
|
|
318
439
|
};
|
|
440
|
+
const navigateToNote = (slug) => {
|
|
441
|
+
const note = noteBySlug.get(slug);
|
|
442
|
+
if (!note) return;
|
|
443
|
+
window.location.href = note.permalink;
|
|
444
|
+
};
|
|
319
445
|
const clearFilters = () => {
|
|
320
446
|
setQuery("");
|
|
321
447
|
setSelectedTag(null);
|
|
322
448
|
setSelectedFolder(null);
|
|
323
449
|
};
|
|
324
|
-
const
|
|
450
|
+
const approveForceLayout = () => {
|
|
451
|
+
setForceLayoutApproved(true);
|
|
452
|
+
};
|
|
453
|
+
const zoom = (delta, origin) => {
|
|
454
|
+
setView((current) => zoomView(current, delta, origin));
|
|
455
|
+
};
|
|
456
|
+
const toSvgPoint = (event) => {
|
|
457
|
+
const svg = event.currentTarget;
|
|
458
|
+
const rect = svg.getBoundingClientRect();
|
|
459
|
+
return {
|
|
460
|
+
x: (event.clientX - rect.left) / rect.width * GRAPH_WIDTH,
|
|
461
|
+
y: (event.clientY - rect.top) / rect.height * GRAPH_HEIGHT
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
const toGraphPoint = (event, current) => {
|
|
465
|
+
const point = toSvgPoint(event);
|
|
466
|
+
return {
|
|
467
|
+
x: (point.x - current.x) / current.scale,
|
|
468
|
+
y: (point.y - current.y) / current.scale
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
const startPan = (event) => {
|
|
472
|
+
const point = toSvgPoint(event);
|
|
473
|
+
setDrag({ kind: "pan", x: point.x, y: point.y });
|
|
474
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
475
|
+
};
|
|
476
|
+
const movePan = (event, currentDrag) => {
|
|
477
|
+
const point = toSvgPoint(event);
|
|
325
478
|
setView((current) => ({
|
|
326
479
|
...current,
|
|
327
|
-
|
|
480
|
+
x: current.x + point.x - currentDrag.x,
|
|
481
|
+
y: current.y + point.y - currentDrag.y
|
|
328
482
|
}));
|
|
483
|
+
setDrag({ kind: "pan", x: point.x, y: point.y });
|
|
484
|
+
};
|
|
485
|
+
const moveNode = (event, currentDrag) => {
|
|
486
|
+
const hasMoved = currentDrag.hasMoved || Math.hypot(
|
|
487
|
+
event.clientX - currentDrag.startClientX,
|
|
488
|
+
event.clientY - currentDrag.startClientY
|
|
489
|
+
) >= DRAG_THRESHOLD_PX;
|
|
490
|
+
if (!hasMoved) return;
|
|
491
|
+
const point = toGraphPoint(event, view);
|
|
492
|
+
setSuppressNodeNavigation(true);
|
|
493
|
+
setPinnedNodes((current) => ({ ...current, [currentDrag.slug]: point }));
|
|
494
|
+
if (!currentDrag.hasMoved) setDrag({ ...currentDrag, hasMoved: true });
|
|
329
495
|
};
|
|
330
496
|
return /* @__PURE__ */ jsxs2("div", { class: "garden-explorer rr-garden-explorer", "data-garden-explorer": true, children: [
|
|
331
497
|
/* @__PURE__ */ jsxs2("fieldset", { class: "garden-explorer__mobile-tabs", children: [
|
|
@@ -366,7 +532,7 @@ function GardenExplorer(props) {
|
|
|
366
532
|
}
|
|
367
533
|
)
|
|
368
534
|
] }),
|
|
369
|
-
/* @__PURE__ */ jsx2(
|
|
535
|
+
props.data.options.showTags ? /* @__PURE__ */ jsx2(
|
|
370
536
|
FilterList,
|
|
371
537
|
{
|
|
372
538
|
title: "Tags",
|
|
@@ -378,8 +544,8 @@ function GardenExplorer(props) {
|
|
|
378
544
|
selectedKey: selectedTag,
|
|
379
545
|
onSelect: (key) => setSelectedTag(key === selectedTag ? null : key)
|
|
380
546
|
}
|
|
381
|
-
),
|
|
382
|
-
/* @__PURE__ */ jsx2(
|
|
547
|
+
) : null,
|
|
548
|
+
props.data.options.showFolders ? /* @__PURE__ */ jsx2(
|
|
383
549
|
FilterList,
|
|
384
550
|
{
|
|
385
551
|
title: "Folders",
|
|
@@ -391,13 +557,21 @@ function GardenExplorer(props) {
|
|
|
391
557
|
selectedKey: selectedFolder,
|
|
392
558
|
onSelect: (key) => setSelectedFolder(key === selectedFolder ? null : key)
|
|
393
559
|
}
|
|
394
|
-
),
|
|
560
|
+
) : null,
|
|
395
561
|
/* @__PURE__ */ jsxs2("div", { class: "garden-explorer__section", children: [
|
|
396
|
-
/* @__PURE__ */
|
|
562
|
+
/* @__PURE__ */ jsxs2("p", { class: "garden-explorer__eyebrow", children: [
|
|
563
|
+
"Notes",
|
|
564
|
+
filteredNotes.length > listNotes.length && /* @__PURE__ */ jsxs2("span", { class: "garden-explorer__count-badge", children: [
|
|
565
|
+
"showing ",
|
|
566
|
+
listNotes.length,
|
|
567
|
+
" of ",
|
|
568
|
+
filteredNotes.length
|
|
569
|
+
] })
|
|
570
|
+
] }),
|
|
397
571
|
/* @__PURE__ */ jsx2(
|
|
398
572
|
NoteList,
|
|
399
573
|
{
|
|
400
|
-
notes:
|
|
574
|
+
notes: listNotes,
|
|
401
575
|
selectedSlug,
|
|
402
576
|
onSelect: selectNote
|
|
403
577
|
}
|
|
@@ -407,10 +581,53 @@ function GardenExplorer(props) {
|
|
|
407
581
|
/* @__PURE__ */ jsxs2("section", { class: panelClass("graph", mobilePanel), "aria-label": "Note graph", children: [
|
|
408
582
|
/* @__PURE__ */ jsxs2("div", { class: "garden-explorer__graph-toolbar", children: [
|
|
409
583
|
/* @__PURE__ */ jsxs2("span", { children: [
|
|
410
|
-
|
|
411
|
-
"
|
|
584
|
+
graphMode === "local" ? "Local" : "Global",
|
|
585
|
+
" graph ·",
|
|
586
|
+
" ",
|
|
587
|
+
graphNotes.length,
|
|
588
|
+
" notes",
|
|
589
|
+
graphMode === "global" && filteredNotes.length !== graphNotes.length && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
590
|
+
" (of ",
|
|
591
|
+
filteredNotes.length,
|
|
592
|
+
" filtered)"
|
|
593
|
+
] }),
|
|
594
|
+
shouldGuard && /* @__PURE__ */ jsxs2("span", { class: "garden-explorer__perf-warning", "aria-live": "polite", children: [
|
|
595
|
+
"⚠ Force layout may freeze this page (",
|
|
596
|
+
graphNotes.length,
|
|
597
|
+
" nodes)"
|
|
598
|
+
] }),
|
|
599
|
+
shouldGuard && !forceLayoutApproved && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
600
|
+
/* @__PURE__ */ jsx2("span", { class: "garden-explorer__perf-warning", children: "Force layout not run automatically." }),
|
|
601
|
+
/* @__PURE__ */ jsx2(
|
|
602
|
+
"button",
|
|
603
|
+
{
|
|
604
|
+
type: "button",
|
|
605
|
+
class: "garden-explorer__force-approve",
|
|
606
|
+
onClick: approveForceLayout,
|
|
607
|
+
children: "Run force layout anyway"
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
] })
|
|
412
611
|
] }),
|
|
413
612
|
/* @__PURE__ */ jsxs2("div", { children: [
|
|
613
|
+
/* @__PURE__ */ jsx2(
|
|
614
|
+
"button",
|
|
615
|
+
{
|
|
616
|
+
type: "button",
|
|
617
|
+
"aria-pressed": graphMode === "local",
|
|
618
|
+
onClick: () => setGraphMode("local"),
|
|
619
|
+
children: "Local"
|
|
620
|
+
}
|
|
621
|
+
),
|
|
622
|
+
/* @__PURE__ */ jsx2(
|
|
623
|
+
"button",
|
|
624
|
+
{
|
|
625
|
+
type: "button",
|
|
626
|
+
"aria-pressed": graphMode === "global",
|
|
627
|
+
onClick: () => setGraphMode("global"),
|
|
628
|
+
children: "Global"
|
|
629
|
+
}
|
|
630
|
+
),
|
|
414
631
|
/* @__PURE__ */ jsx2(
|
|
415
632
|
"button",
|
|
416
633
|
{
|
|
@@ -448,25 +665,44 @@ function GardenExplorer(props) {
|
|
|
448
665
|
"aria-label": "Interactive graph of notes and internal links",
|
|
449
666
|
onWheel: (event) => {
|
|
450
667
|
event.preventDefault();
|
|
451
|
-
zoom(event.deltaY > 0 ? -0.1 : 0.1);
|
|
668
|
+
zoom(event.deltaY > 0 ? -0.1 : 0.1, toSvgPoint(event));
|
|
669
|
+
},
|
|
670
|
+
onPointerDown: (event) => {
|
|
671
|
+
const target = event.target;
|
|
672
|
+
if (target.closest(".garden-graph__node")) return;
|
|
673
|
+
startPan(event);
|
|
452
674
|
},
|
|
453
675
|
onPointerMove: (event) => {
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
676
|
+
if (!drag) return;
|
|
677
|
+
if (drag.kind === "pan") {
|
|
678
|
+
movePan(event, drag);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
moveNode(event, drag);
|
|
682
|
+
},
|
|
683
|
+
onPointerUp: (event) => {
|
|
684
|
+
setDrag(null);
|
|
685
|
+
const svg = event.currentTarget;
|
|
686
|
+
if (svg.hasPointerCapture(event.pointerId)) {
|
|
687
|
+
svg.releasePointerCapture(event.pointerId);
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
onPointerCancel: () => {
|
|
691
|
+
setDrag(null);
|
|
692
|
+
setSuppressNodeNavigation(false);
|
|
460
693
|
},
|
|
461
694
|
children: /* @__PURE__ */ jsxs2("g", { transform: `translate(${view.x} ${view.y}) scale(${view.scale})`, children: [
|
|
462
|
-
|
|
695
|
+
graphEdges.map((edge) => {
|
|
463
696
|
const source = graphNodes.get(edge.source);
|
|
464
697
|
const target = graphNodes.get(edge.target);
|
|
465
698
|
if (!source || !target) return null;
|
|
699
|
+
const emphasized = emphasizedSlugs.has(edge.source) && emphasizedSlugs.has(edge.target);
|
|
466
700
|
return /* @__PURE__ */ jsx2(
|
|
467
701
|
"line",
|
|
468
702
|
{
|
|
469
703
|
class: "garden-graph__edge",
|
|
704
|
+
"data-muted": emphasizedSlugs.size > 0 && !emphasized ? "true" : "false",
|
|
705
|
+
"data-emphasized": emphasized ? "true" : "false",
|
|
470
706
|
x1: source.x,
|
|
471
707
|
y1: source.y,
|
|
472
708
|
x2: target.x,
|
|
@@ -475,33 +711,62 @@ function GardenExplorer(props) {
|
|
|
475
711
|
`${edge.source}-${edge.target}`
|
|
476
712
|
);
|
|
477
713
|
}),
|
|
478
|
-
|
|
714
|
+
graphNotes.map((note) => {
|
|
479
715
|
const node = graphNodes.get(note.slug);
|
|
480
716
|
if (!node) return null;
|
|
481
717
|
const selected = note.slug === selectedSlug;
|
|
482
|
-
|
|
483
|
-
|
|
718
|
+
const active = emphasizedSlugs.size === 0 || emphasizedSlugs.has(note.slug);
|
|
719
|
+
return /* @__PURE__ */ jsx2(
|
|
720
|
+
"g",
|
|
484
721
|
{
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
"
|
|
494
|
-
{
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
722
|
+
class: "garden-graph__node",
|
|
723
|
+
"data-muted": active ? "false" : "true",
|
|
724
|
+
onPointerEnter: () => setHoveredSlug(note.slug),
|
|
725
|
+
onPointerLeave: () => setHoveredSlug(null),
|
|
726
|
+
children: /* @__PURE__ */ jsxs2(
|
|
727
|
+
"a",
|
|
728
|
+
{
|
|
729
|
+
href: note.permalink,
|
|
730
|
+
"aria-label": `Open ${note.title}`,
|
|
731
|
+
onClick: (event) => {
|
|
732
|
+
event.preventDefault();
|
|
733
|
+
if (suppressNodeNavigation) {
|
|
734
|
+
setSuppressNodeNavigation(false);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
navigateToNote(note.slug);
|
|
738
|
+
},
|
|
739
|
+
children: [
|
|
740
|
+
/* @__PURE__ */ jsx2(
|
|
741
|
+
"circle",
|
|
742
|
+
{
|
|
743
|
+
cx: node.x,
|
|
744
|
+
cy: node.y,
|
|
745
|
+
r: selected ? 15 : node.radius * props.data.options.nodeSize,
|
|
746
|
+
"data-selected": selected ? "true" : "false",
|
|
747
|
+
onPointerDown: (event) => {
|
|
748
|
+
event.preventDefault();
|
|
749
|
+
event.stopPropagation();
|
|
750
|
+
setSelectedSlug(note.slug);
|
|
751
|
+
setSuppressNodeNavigation(false);
|
|
752
|
+
setDrag({
|
|
753
|
+
kind: "node",
|
|
754
|
+
slug: note.slug,
|
|
755
|
+
startClientX: event.clientX,
|
|
756
|
+
startClientY: event.clientY,
|
|
757
|
+
hasMoved: false
|
|
758
|
+
});
|
|
759
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
),
|
|
763
|
+
props.data.options.showLabels ? /* @__PURE__ */ jsx2("text", { x: node.x + 18, y: node.y + 5, children: note.title }) : null
|
|
764
|
+
]
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
},
|
|
768
|
+
note.slug
|
|
769
|
+
);
|
|
505
770
|
})
|
|
506
771
|
] })
|
|
507
772
|
}
|
|
@@ -518,13 +783,30 @@ function GardenExplorer(props) {
|
|
|
518
783
|
) : /* @__PURE__ */ jsx2("p", { class: "garden-explorer__empty", children: "Select a note to inspect links." }) })
|
|
519
784
|
] });
|
|
520
785
|
}
|
|
521
|
-
function layoutNodes(notes, selectedSlug) {
|
|
522
|
-
|
|
786
|
+
function layoutNodes(notes, selectedSlug, options) {
|
|
787
|
+
if (options.layout === "radial") {
|
|
788
|
+
return layoutRadialGraph(notes, {
|
|
789
|
+
width: GRAPH_WIDTH,
|
|
790
|
+
height: GRAPH_HEIGHT,
|
|
791
|
+
centerSlug: selectedSlug
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return layoutForceGraph(notes, {
|
|
523
795
|
width: GRAPH_WIDTH,
|
|
524
796
|
height: GRAPH_HEIGHT,
|
|
525
|
-
centerSlug: selectedSlug
|
|
797
|
+
centerSlug: selectedSlug,
|
|
798
|
+
linkDistance: options.linkDistance,
|
|
799
|
+
repulsion: options.repulsion
|
|
526
800
|
});
|
|
527
801
|
}
|
|
802
|
+
function applyPinnedNodes(layout, pinnedNodes) {
|
|
803
|
+
const next = new Map(layout);
|
|
804
|
+
for (const [slug, point] of Object.entries(pinnedNodes)) {
|
|
805
|
+
const current = next.get(slug);
|
|
806
|
+
if (current) next.set(slug, { ...current, x: point.x, y: point.y });
|
|
807
|
+
}
|
|
808
|
+
return next;
|
|
809
|
+
}
|
|
528
810
|
function getRelatedNotes(selectedNote, notes) {
|
|
529
811
|
const directSlugs = /* @__PURE__ */ new Set([
|
|
530
812
|
...selectedNote.outgoing,
|
|
@@ -538,12 +820,33 @@ function getRelatedNotes(selectedNote, notes) {
|
|
|
538
820
|
(a, b) => b.score - a.score || a.note.title.localeCompare(b.note.title, "ja")
|
|
539
821
|
).slice(0, 8).map((item) => item.note);
|
|
540
822
|
}
|
|
823
|
+
function getRelatedSlugSet(selectedSlug, hoveredSlug, edges) {
|
|
824
|
+
const origin = hoveredSlug ?? selectedSlug;
|
|
825
|
+
if (!origin) return /* @__PURE__ */ new Set();
|
|
826
|
+
const related = /* @__PURE__ */ new Set([origin]);
|
|
827
|
+
for (const edge of edges) {
|
|
828
|
+
if (edge.source === origin) related.add(edge.target);
|
|
829
|
+
if (edge.target === origin) related.add(edge.source);
|
|
830
|
+
}
|
|
831
|
+
return related;
|
|
832
|
+
}
|
|
541
833
|
function panelClass(panel, activePanel) {
|
|
542
834
|
return `garden-explorer__panel garden-explorer__panel--${panel}${panel === activePanel ? " garden-explorer__panel--active" : ""}`;
|
|
543
835
|
}
|
|
544
836
|
function clamp(value, min, max) {
|
|
545
837
|
return Math.min(Math.max(value, min), max);
|
|
546
838
|
}
|
|
839
|
+
function zoomView(current, delta, origin = { x: GRAPH_WIDTH / 2, y: GRAPH_HEIGHT / 2 }) {
|
|
840
|
+
const scale = clamp(current.scale + delta, MIN_SCALE, MAX_SCALE);
|
|
841
|
+
if (scale === current.scale) return current;
|
|
842
|
+
const graphX = (origin.x - current.x) / current.scale;
|
|
843
|
+
const graphY = (origin.y - current.y) / current.scale;
|
|
844
|
+
return {
|
|
845
|
+
x: origin.x - graphX * scale,
|
|
846
|
+
y: origin.y - graphY * scale,
|
|
847
|
+
scale
|
|
848
|
+
};
|
|
849
|
+
}
|
|
547
850
|
|
|
548
851
|
// src/garden-explorer-page.tsx
|
|
549
852
|
import { jsx as jsx3, jsxs as jsxs3 } from "hono/jsx/jsx-runtime";
|
|
@@ -553,7 +856,7 @@ function renderGardenExplorerPage(data, siteTitle) {
|
|
|
553
856
|
/* @__PURE__ */ jsxs3("header", { class: "garden-explorer-page__header", children: [
|
|
554
857
|
/* @__PURE__ */ jsx3("p", { class: "garden-explorer-page__eyebrow", children: siteTitle }),
|
|
555
858
|
/* @__PURE__ */ jsx3("h1", { class: "garden-explorer-page__title", children: "Garden Explorer" }),
|
|
556
|
-
/* @__PURE__ */ jsx3("p", { class: "garden-explorer-page__description", children: "
|
|
859
|
+
/* @__PURE__ */ jsx3("p", { class: "garden-explorer-page__description", children: "Explore your digital garden across graph, search, tags, folders, backlinks, and related notes." })
|
|
557
860
|
] }),
|
|
558
861
|
/* @__PURE__ */ jsx3("div", { "data-riebeckite-garden-explorer": true, children: /* @__PURE__ */ jsx3(GardenExplorer, { data }) }),
|
|
559
862
|
/* @__PURE__ */ jsx3(
|
|
@@ -571,7 +874,7 @@ function renderGardenExplorerPage(data, siteTitle) {
|
|
|
571
874
|
}
|
|
572
875
|
|
|
573
876
|
// index.ts
|
|
574
|
-
function gardenExplorerPlugin() {
|
|
877
|
+
function gardenExplorerPlugin(options = {}) {
|
|
575
878
|
return definePlugin({
|
|
576
879
|
name: "garden-explorer",
|
|
577
880
|
assets: [
|
|
@@ -586,6 +889,7 @@ function gardenExplorerPlugin() {
|
|
|
586
889
|
{
|
|
587
890
|
id: "garden-explorer",
|
|
588
891
|
paths: ["/explore"],
|
|
892
|
+
outputDependencies: [{ type: "global" }],
|
|
589
893
|
resolve: ({ pathname, manifest, config }) => {
|
|
590
894
|
if (pathname !== "/explore" || !config) return null;
|
|
591
895
|
const siteTitle = config.site.title;
|
|
@@ -598,7 +902,8 @@ function gardenExplorerPlugin() {
|
|
|
598
902
|
getGardenExplorerData({
|
|
599
903
|
manifest,
|
|
600
904
|
config,
|
|
601
|
-
resolveTitle: (slug, title) => typeof title === "string" && title.trim() ? title : slug
|
|
905
|
+
resolveTitle: (slug, title) => typeof title === "string" && title.trim() ? title : slug,
|
|
906
|
+
options
|
|
602
907
|
}),
|
|
603
908
|
siteTitle
|
|
604
909
|
)
|
|
@@ -15,6 +15,19 @@ export type GardenExplorerNote = GardenExplorerSearchFields & {
|
|
|
15
15
|
backlinks: string[];
|
|
16
16
|
};
|
|
17
17
|
export type GardenExplorerEdge = GraphEdge;
|
|
18
|
+
export type GardenExplorerGraphLayout = "force" | "radial";
|
|
19
|
+
export type GardenExplorerGraphMode = "local" | "global";
|
|
20
|
+
export type GardenExplorerOptions = {
|
|
21
|
+
layout: GardenExplorerGraphLayout;
|
|
22
|
+
depth: number;
|
|
23
|
+
showTags: boolean;
|
|
24
|
+
showFolders: boolean;
|
|
25
|
+
nodeSize: number;
|
|
26
|
+
linkDistance: number;
|
|
27
|
+
repulsion: number;
|
|
28
|
+
showLabels: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type GardenExplorerPluginOptions = Partial<GardenExplorerOptions>;
|
|
18
31
|
export type GardenExplorerTag = {
|
|
19
32
|
name: string;
|
|
20
33
|
count: number;
|
|
@@ -28,5 +41,7 @@ export type GardenExplorerData = {
|
|
|
28
41
|
edges: GardenExplorerEdge[];
|
|
29
42
|
tags: GardenExplorerTag[];
|
|
30
43
|
folders: GardenExplorerFolder[];
|
|
44
|
+
options: GardenExplorerOptions;
|
|
31
45
|
};
|
|
46
|
+
export declare function getGardenExplorerLocalGraphNotes(notes: GardenExplorerNote[], selectedSlug: string, depth: number): GardenExplorerNote[];
|
|
32
47
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ContentManifest, type ResolvedRiebeckiteConfig } from "@riebeckite/core";
|
|
2
|
-
import type { GardenExplorerData } from "./garden-explorer.js";
|
|
2
|
+
import type { GardenExplorerData, GardenExplorerOptions, GardenExplorerPluginOptions } from "./garden-explorer.js";
|
|
3
3
|
type TitleResolver = (slug: string, title: unknown) => string;
|
|
4
4
|
export declare function getGardenExplorerData(args: {
|
|
5
5
|
manifest: ContentManifest;
|
|
6
6
|
config: ResolvedRiebeckiteConfig;
|
|
7
7
|
resolveTitle: TitleResolver;
|
|
8
|
+
options?: GardenExplorerPluginOptions;
|
|
8
9
|
}): GardenExplorerData;
|
|
10
|
+
export declare function resolveGardenExplorerOptions(options?: GardenExplorerPluginOptions): GardenExplorerOptions;
|
|
9
11
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-garden-explorer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"hono": "^4.12.25",
|
|
31
|
-
"@riebeckite/core": "0.0.
|
|
31
|
+
"@riebeckite/core": "0.0.16"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"./style.css"
|
|
64
64
|
],
|
|
65
65
|
"scripts": {
|
|
66
|
-
"build": "node ../../../scripts/build_package.mjs"
|
|
66
|
+
"build": "node ../../../scripts/build_package.mjs",
|
|
67
|
+
"test": "node --import tsx --test \"test/*.test.ts\""
|
|
67
68
|
}
|
|
68
69
|
}
|