@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
|
@@ -1,7 +1,32 @@
|
|
|
1
1
|
// components/garden-explorer.tsx
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
layoutForceGraph,
|
|
4
|
+
layoutRadialGraph,
|
|
5
|
+
shouldGuardForceLayout
|
|
6
|
+
} from "@riebeckite/core/client";
|
|
3
7
|
import { useEffect, useMemo, useState } from "hono/jsx";
|
|
4
8
|
|
|
9
|
+
// src/garden-explorer.ts
|
|
10
|
+
function getGardenExplorerLocalGraphNotes(notes, selectedSlug, depth) {
|
|
11
|
+
const noteBySlug = new Map(notes.map((note) => [note.slug, note]));
|
|
12
|
+
const visible = /* @__PURE__ */ new Set([selectedSlug]);
|
|
13
|
+
let frontier = [selectedSlug];
|
|
14
|
+
for (let level = 0; level < depth; level++) {
|
|
15
|
+
const nextFrontier = [];
|
|
16
|
+
for (const slug of frontier) {
|
|
17
|
+
const note = noteBySlug.get(slug);
|
|
18
|
+
if (!note) continue;
|
|
19
|
+
for (const neighbor of [...note.outgoing, ...note.backlinks]) {
|
|
20
|
+
if (!noteBySlug.has(neighbor) || visible.has(neighbor)) continue;
|
|
21
|
+
visible.add(neighbor);
|
|
22
|
+
nextFrontier.push(neighbor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
frontier = nextFrontier;
|
|
26
|
+
}
|
|
27
|
+
return notes.filter((note) => visible.has(note.slug));
|
|
28
|
+
}
|
|
29
|
+
|
|
5
30
|
// src/search-notes.ts
|
|
6
31
|
var FIELD_WEIGHTS = {
|
|
7
32
|
slug: 64,
|
|
@@ -155,11 +180,12 @@ function LinkedNoteSection(props) {
|
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
// components/garden-explorer.tsx
|
|
158
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "hono/jsx/jsx-runtime";
|
|
183
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "hono/jsx/jsx-runtime";
|
|
159
184
|
var GRAPH_WIDTH = 900;
|
|
160
185
|
var GRAPH_HEIGHT = 620;
|
|
161
186
|
var MIN_SCALE = 0.6;
|
|
162
187
|
var MAX_SCALE = 2.4;
|
|
188
|
+
var DRAG_THRESHOLD_PX = 4;
|
|
163
189
|
function GardenExplorer(props) {
|
|
164
190
|
const noteBySlug = useMemo(
|
|
165
191
|
() => new Map(props.data.notes.map((note) => [note.slug, note])),
|
|
@@ -172,7 +198,13 @@ function GardenExplorer(props) {
|
|
|
172
198
|
() => props.data.notes[0]?.slug ?? ""
|
|
173
199
|
);
|
|
174
200
|
const [mobilePanel, setMobilePanel] = useState("graph");
|
|
201
|
+
const [graphMode, setGraphMode] = useState("local");
|
|
202
|
+
const [hoveredSlug, setHoveredSlug] = useState(null);
|
|
175
203
|
const [view, setView] = useState({ x: 0, y: 0, scale: 1 });
|
|
204
|
+
const [drag, setDrag] = useState(null);
|
|
205
|
+
const [suppressNodeNavigation, setSuppressNodeNavigation] = useState(false);
|
|
206
|
+
const [pinnedNodes, setPinnedNodes] = useState({});
|
|
207
|
+
const [forceLayoutApproved, setForceLayoutApproved] = useState(false);
|
|
176
208
|
useEffect(() => {
|
|
177
209
|
const params = new URLSearchParams(window.location.search);
|
|
178
210
|
const note = params.get("note");
|
|
@@ -197,8 +229,9 @@ function GardenExplorer(props) {
|
|
|
197
229
|
return true;
|
|
198
230
|
});
|
|
199
231
|
const searched = query ? searchGardenExplorerNotes(base, query) : base;
|
|
200
|
-
return searched
|
|
232
|
+
return searched;
|
|
201
233
|
}, [props.data.notes, query, selectedTag, selectedFolder]);
|
|
234
|
+
const listNotes = useMemo(() => filteredNotes.slice(0, 80), [filteredNotes]);
|
|
202
235
|
const visibleSlugSet = useMemo(
|
|
203
236
|
() => new Set(filteredNotes.map((note) => note.slug)),
|
|
204
237
|
[filteredNotes]
|
|
@@ -209,29 +242,140 @@ function GardenExplorer(props) {
|
|
|
209
242
|
),
|
|
210
243
|
[props.data.edges, visibleSlugSet]
|
|
211
244
|
);
|
|
245
|
+
const graphNotes = useMemo(() => {
|
|
246
|
+
if (graphMode === "local") {
|
|
247
|
+
return getGardenExplorerLocalGraphNotes(
|
|
248
|
+
filteredNotes,
|
|
249
|
+
selectedSlug,
|
|
250
|
+
props.data.options.depth
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const notes = filteredNotes;
|
|
254
|
+
const selectedNote2 = noteBySlug.get(selectedSlug);
|
|
255
|
+
if (selectedNote2 && !notes.some((n) => n.slug === selectedSlug)) {
|
|
256
|
+
return [selectedNote2, ...notes];
|
|
257
|
+
}
|
|
258
|
+
return notes;
|
|
259
|
+
}, [
|
|
260
|
+
filteredNotes,
|
|
261
|
+
graphMode,
|
|
262
|
+
selectedSlug,
|
|
263
|
+
props.data.options.depth,
|
|
264
|
+
noteBySlug
|
|
265
|
+
]);
|
|
266
|
+
const graphSlugSet = useMemo(
|
|
267
|
+
() => new Set(graphNotes.map((note) => note.slug)),
|
|
268
|
+
[graphNotes]
|
|
269
|
+
);
|
|
270
|
+
const graphEdges = useMemo(
|
|
271
|
+
() => visibleEdges.filter(
|
|
272
|
+
(edge) => graphSlugSet.has(edge.source) && graphSlugSet.has(edge.target)
|
|
273
|
+
),
|
|
274
|
+
[visibleEdges, graphSlugSet]
|
|
275
|
+
);
|
|
276
|
+
const graphKey = useMemo(
|
|
277
|
+
() => graphNotes.map((note) => note.slug).join("\0"),
|
|
278
|
+
[graphNotes]
|
|
279
|
+
);
|
|
280
|
+
const shouldGuard = shouldGuardForceLayout({
|
|
281
|
+
layout: props.data.options.layout,
|
|
282
|
+
mode: graphMode,
|
|
283
|
+
nodeCount: graphNotes.length,
|
|
284
|
+
approved: forceLayoutApproved
|
|
285
|
+
});
|
|
286
|
+
const baseGraphNodes = useMemo(() => {
|
|
287
|
+
if (shouldGuard) {
|
|
288
|
+
return layoutRadialGraph(graphNotes, {
|
|
289
|
+
width: GRAPH_WIDTH,
|
|
290
|
+
height: GRAPH_HEIGHT,
|
|
291
|
+
centerSlug: selectedSlug
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return layoutNodes(graphNotes, selectedSlug, props.data.options);
|
|
295
|
+
}, [shouldGuard, graphNotes, selectedSlug, props.data.options]);
|
|
212
296
|
const graphNodes = useMemo(
|
|
213
|
-
() =>
|
|
214
|
-
[
|
|
297
|
+
() => applyPinnedNodes(baseGraphNodes, pinnedNodes),
|
|
298
|
+
[baseGraphNodes, pinnedNodes]
|
|
215
299
|
);
|
|
216
|
-
const selectedNote = noteBySlug.get(selectedSlug) ??
|
|
300
|
+
const selectedNote = noteBySlug.get(selectedSlug) ?? listNotes[0] ?? null;
|
|
217
301
|
const relatedNotes = useMemo(
|
|
218
302
|
() => selectedNote ? getRelatedNotes(selectedNote, props.data.notes) : [],
|
|
219
303
|
[selectedNote, props.data.notes]
|
|
220
304
|
);
|
|
305
|
+
const emphasizedSlugs = useMemo(
|
|
306
|
+
() => getRelatedSlugSet(selectedSlug, hoveredSlug, graphEdges),
|
|
307
|
+
[selectedSlug, hoveredSlug, graphEdges]
|
|
308
|
+
);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (filteredNotes.length === 0 || visibleSlugSet.has(selectedSlug)) return;
|
|
311
|
+
setSelectedSlug(listNotes[0]?.slug ?? "");
|
|
312
|
+
}, [filteredNotes, selectedSlug, visibleSlugSet, listNotes]);
|
|
313
|
+
useEffect(() => {
|
|
314
|
+
setHoveredSlug(null);
|
|
315
|
+
setDrag(null);
|
|
316
|
+
setSuppressNodeNavigation(false);
|
|
317
|
+
setPinnedNodes({});
|
|
318
|
+
setForceLayoutApproved(false);
|
|
319
|
+
}, [graphKey, graphMode, props.data.options.layout]);
|
|
221
320
|
const selectNote = (slug) => {
|
|
222
321
|
setSelectedSlug(slug);
|
|
223
322
|
setMobilePanel("details");
|
|
224
323
|
};
|
|
324
|
+
const navigateToNote = (slug) => {
|
|
325
|
+
const note = noteBySlug.get(slug);
|
|
326
|
+
if (!note) return;
|
|
327
|
+
window.location.href = note.permalink;
|
|
328
|
+
};
|
|
225
329
|
const clearFilters = () => {
|
|
226
330
|
setQuery("");
|
|
227
331
|
setSelectedTag(null);
|
|
228
332
|
setSelectedFolder(null);
|
|
229
333
|
};
|
|
230
|
-
const
|
|
334
|
+
const approveForceLayout = () => {
|
|
335
|
+
setForceLayoutApproved(true);
|
|
336
|
+
};
|
|
337
|
+
const zoom = (delta, origin) => {
|
|
338
|
+
setView((current) => zoomView(current, delta, origin));
|
|
339
|
+
};
|
|
340
|
+
const toSvgPoint = (event) => {
|
|
341
|
+
const svg = event.currentTarget;
|
|
342
|
+
const rect = svg.getBoundingClientRect();
|
|
343
|
+
return {
|
|
344
|
+
x: (event.clientX - rect.left) / rect.width * GRAPH_WIDTH,
|
|
345
|
+
y: (event.clientY - rect.top) / rect.height * GRAPH_HEIGHT
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
const toGraphPoint = (event, current) => {
|
|
349
|
+
const point = toSvgPoint(event);
|
|
350
|
+
return {
|
|
351
|
+
x: (point.x - current.x) / current.scale,
|
|
352
|
+
y: (point.y - current.y) / current.scale
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
const startPan = (event) => {
|
|
356
|
+
const point = toSvgPoint(event);
|
|
357
|
+
setDrag({ kind: "pan", x: point.x, y: point.y });
|
|
358
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
359
|
+
};
|
|
360
|
+
const movePan = (event, currentDrag) => {
|
|
361
|
+
const point = toSvgPoint(event);
|
|
231
362
|
setView((current) => ({
|
|
232
363
|
...current,
|
|
233
|
-
|
|
364
|
+
x: current.x + point.x - currentDrag.x,
|
|
365
|
+
y: current.y + point.y - currentDrag.y
|
|
234
366
|
}));
|
|
367
|
+
setDrag({ kind: "pan", x: point.x, y: point.y });
|
|
368
|
+
};
|
|
369
|
+
const moveNode = (event, currentDrag) => {
|
|
370
|
+
const hasMoved = currentDrag.hasMoved || Math.hypot(
|
|
371
|
+
event.clientX - currentDrag.startClientX,
|
|
372
|
+
event.clientY - currentDrag.startClientY
|
|
373
|
+
) >= DRAG_THRESHOLD_PX;
|
|
374
|
+
if (!hasMoved) return;
|
|
375
|
+
const point = toGraphPoint(event, view);
|
|
376
|
+
setSuppressNodeNavigation(true);
|
|
377
|
+
setPinnedNodes((current) => ({ ...current, [currentDrag.slug]: point }));
|
|
378
|
+
if (!currentDrag.hasMoved) setDrag({ ...currentDrag, hasMoved: true });
|
|
235
379
|
};
|
|
236
380
|
return /* @__PURE__ */ jsxs2("div", { class: "garden-explorer rr-garden-explorer", "data-garden-explorer": true, children: [
|
|
237
381
|
/* @__PURE__ */ jsxs2("fieldset", { class: "garden-explorer__mobile-tabs", children: [
|
|
@@ -272,7 +416,7 @@ function GardenExplorer(props) {
|
|
|
272
416
|
}
|
|
273
417
|
)
|
|
274
418
|
] }),
|
|
275
|
-
/* @__PURE__ */ jsx2(
|
|
419
|
+
props.data.options.showTags ? /* @__PURE__ */ jsx2(
|
|
276
420
|
FilterList,
|
|
277
421
|
{
|
|
278
422
|
title: "Tags",
|
|
@@ -284,8 +428,8 @@ function GardenExplorer(props) {
|
|
|
284
428
|
selectedKey: selectedTag,
|
|
285
429
|
onSelect: (key) => setSelectedTag(key === selectedTag ? null : key)
|
|
286
430
|
}
|
|
287
|
-
),
|
|
288
|
-
/* @__PURE__ */ jsx2(
|
|
431
|
+
) : null,
|
|
432
|
+
props.data.options.showFolders ? /* @__PURE__ */ jsx2(
|
|
289
433
|
FilterList,
|
|
290
434
|
{
|
|
291
435
|
title: "Folders",
|
|
@@ -297,13 +441,21 @@ function GardenExplorer(props) {
|
|
|
297
441
|
selectedKey: selectedFolder,
|
|
298
442
|
onSelect: (key) => setSelectedFolder(key === selectedFolder ? null : key)
|
|
299
443
|
}
|
|
300
|
-
),
|
|
444
|
+
) : null,
|
|
301
445
|
/* @__PURE__ */ jsxs2("div", { class: "garden-explorer__section", children: [
|
|
302
|
-
/* @__PURE__ */
|
|
446
|
+
/* @__PURE__ */ jsxs2("p", { class: "garden-explorer__eyebrow", children: [
|
|
447
|
+
"Notes",
|
|
448
|
+
filteredNotes.length > listNotes.length && /* @__PURE__ */ jsxs2("span", { class: "garden-explorer__count-badge", children: [
|
|
449
|
+
"showing ",
|
|
450
|
+
listNotes.length,
|
|
451
|
+
" of ",
|
|
452
|
+
filteredNotes.length
|
|
453
|
+
] })
|
|
454
|
+
] }),
|
|
303
455
|
/* @__PURE__ */ jsx2(
|
|
304
456
|
NoteList,
|
|
305
457
|
{
|
|
306
|
-
notes:
|
|
458
|
+
notes: listNotes,
|
|
307
459
|
selectedSlug,
|
|
308
460
|
onSelect: selectNote
|
|
309
461
|
}
|
|
@@ -313,10 +465,53 @@ function GardenExplorer(props) {
|
|
|
313
465
|
/* @__PURE__ */ jsxs2("section", { class: panelClass("graph", mobilePanel), "aria-label": "Note graph", children: [
|
|
314
466
|
/* @__PURE__ */ jsxs2("div", { class: "garden-explorer__graph-toolbar", children: [
|
|
315
467
|
/* @__PURE__ */ jsxs2("span", { children: [
|
|
316
|
-
|
|
317
|
-
"
|
|
468
|
+
graphMode === "local" ? "Local" : "Global",
|
|
469
|
+
" graph ·",
|
|
470
|
+
" ",
|
|
471
|
+
graphNotes.length,
|
|
472
|
+
" notes",
|
|
473
|
+
graphMode === "global" && filteredNotes.length !== graphNotes.length && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
474
|
+
" (of ",
|
|
475
|
+
filteredNotes.length,
|
|
476
|
+
" filtered)"
|
|
477
|
+
] }),
|
|
478
|
+
shouldGuard && /* @__PURE__ */ jsxs2("span", { class: "garden-explorer__perf-warning", "aria-live": "polite", children: [
|
|
479
|
+
"⚠ Force layout may freeze this page (",
|
|
480
|
+
graphNotes.length,
|
|
481
|
+
" nodes)"
|
|
482
|
+
] }),
|
|
483
|
+
shouldGuard && !forceLayoutApproved && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
484
|
+
/* @__PURE__ */ jsx2("span", { class: "garden-explorer__perf-warning", children: "Force layout not run automatically." }),
|
|
485
|
+
/* @__PURE__ */ jsx2(
|
|
486
|
+
"button",
|
|
487
|
+
{
|
|
488
|
+
type: "button",
|
|
489
|
+
class: "garden-explorer__force-approve",
|
|
490
|
+
onClick: approveForceLayout,
|
|
491
|
+
children: "Run force layout anyway"
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
] })
|
|
318
495
|
] }),
|
|
319
496
|
/* @__PURE__ */ jsxs2("div", { children: [
|
|
497
|
+
/* @__PURE__ */ jsx2(
|
|
498
|
+
"button",
|
|
499
|
+
{
|
|
500
|
+
type: "button",
|
|
501
|
+
"aria-pressed": graphMode === "local",
|
|
502
|
+
onClick: () => setGraphMode("local"),
|
|
503
|
+
children: "Local"
|
|
504
|
+
}
|
|
505
|
+
),
|
|
506
|
+
/* @__PURE__ */ jsx2(
|
|
507
|
+
"button",
|
|
508
|
+
{
|
|
509
|
+
type: "button",
|
|
510
|
+
"aria-pressed": graphMode === "global",
|
|
511
|
+
onClick: () => setGraphMode("global"),
|
|
512
|
+
children: "Global"
|
|
513
|
+
}
|
|
514
|
+
),
|
|
320
515
|
/* @__PURE__ */ jsx2(
|
|
321
516
|
"button",
|
|
322
517
|
{
|
|
@@ -354,25 +549,44 @@ function GardenExplorer(props) {
|
|
|
354
549
|
"aria-label": "Interactive graph of notes and internal links",
|
|
355
550
|
onWheel: (event) => {
|
|
356
551
|
event.preventDefault();
|
|
357
|
-
zoom(event.deltaY > 0 ? -0.1 : 0.1);
|
|
552
|
+
zoom(event.deltaY > 0 ? -0.1 : 0.1, toSvgPoint(event));
|
|
553
|
+
},
|
|
554
|
+
onPointerDown: (event) => {
|
|
555
|
+
const target = event.target;
|
|
556
|
+
if (target.closest(".garden-graph__node")) return;
|
|
557
|
+
startPan(event);
|
|
358
558
|
},
|
|
359
559
|
onPointerMove: (event) => {
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
560
|
+
if (!drag) return;
|
|
561
|
+
if (drag.kind === "pan") {
|
|
562
|
+
movePan(event, drag);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
moveNode(event, drag);
|
|
566
|
+
},
|
|
567
|
+
onPointerUp: (event) => {
|
|
568
|
+
setDrag(null);
|
|
569
|
+
const svg = event.currentTarget;
|
|
570
|
+
if (svg.hasPointerCapture(event.pointerId)) {
|
|
571
|
+
svg.releasePointerCapture(event.pointerId);
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
onPointerCancel: () => {
|
|
575
|
+
setDrag(null);
|
|
576
|
+
setSuppressNodeNavigation(false);
|
|
366
577
|
},
|
|
367
578
|
children: /* @__PURE__ */ jsxs2("g", { transform: `translate(${view.x} ${view.y}) scale(${view.scale})`, children: [
|
|
368
|
-
|
|
579
|
+
graphEdges.map((edge) => {
|
|
369
580
|
const source = graphNodes.get(edge.source);
|
|
370
581
|
const target = graphNodes.get(edge.target);
|
|
371
582
|
if (!source || !target) return null;
|
|
583
|
+
const emphasized = emphasizedSlugs.has(edge.source) && emphasizedSlugs.has(edge.target);
|
|
372
584
|
return /* @__PURE__ */ jsx2(
|
|
373
585
|
"line",
|
|
374
586
|
{
|
|
375
587
|
class: "garden-graph__edge",
|
|
588
|
+
"data-muted": emphasizedSlugs.size > 0 && !emphasized ? "true" : "false",
|
|
589
|
+
"data-emphasized": emphasized ? "true" : "false",
|
|
376
590
|
x1: source.x,
|
|
377
591
|
y1: source.y,
|
|
378
592
|
x2: target.x,
|
|
@@ -381,33 +595,62 @@ function GardenExplorer(props) {
|
|
|
381
595
|
`${edge.source}-${edge.target}`
|
|
382
596
|
);
|
|
383
597
|
}),
|
|
384
|
-
|
|
598
|
+
graphNotes.map((note) => {
|
|
385
599
|
const node = graphNodes.get(note.slug);
|
|
386
600
|
if (!node) return null;
|
|
387
601
|
const selected = note.slug === selectedSlug;
|
|
388
|
-
|
|
389
|
-
|
|
602
|
+
const active = emphasizedSlugs.size === 0 || emphasizedSlugs.has(note.slug);
|
|
603
|
+
return /* @__PURE__ */ jsx2(
|
|
604
|
+
"g",
|
|
390
605
|
{
|
|
391
|
-
|
|
392
|
-
"
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
"
|
|
400
|
-
{
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
606
|
+
class: "garden-graph__node",
|
|
607
|
+
"data-muted": active ? "false" : "true",
|
|
608
|
+
onPointerEnter: () => setHoveredSlug(note.slug),
|
|
609
|
+
onPointerLeave: () => setHoveredSlug(null),
|
|
610
|
+
children: /* @__PURE__ */ jsxs2(
|
|
611
|
+
"a",
|
|
612
|
+
{
|
|
613
|
+
href: note.permalink,
|
|
614
|
+
"aria-label": `Open ${note.title}`,
|
|
615
|
+
onClick: (event) => {
|
|
616
|
+
event.preventDefault();
|
|
617
|
+
if (suppressNodeNavigation) {
|
|
618
|
+
setSuppressNodeNavigation(false);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
navigateToNote(note.slug);
|
|
622
|
+
},
|
|
623
|
+
children: [
|
|
624
|
+
/* @__PURE__ */ jsx2(
|
|
625
|
+
"circle",
|
|
626
|
+
{
|
|
627
|
+
cx: node.x,
|
|
628
|
+
cy: node.y,
|
|
629
|
+
r: selected ? 15 : node.radius * props.data.options.nodeSize,
|
|
630
|
+
"data-selected": selected ? "true" : "false",
|
|
631
|
+
onPointerDown: (event) => {
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
event.stopPropagation();
|
|
634
|
+
setSelectedSlug(note.slug);
|
|
635
|
+
setSuppressNodeNavigation(false);
|
|
636
|
+
setDrag({
|
|
637
|
+
kind: "node",
|
|
638
|
+
slug: note.slug,
|
|
639
|
+
startClientX: event.clientX,
|
|
640
|
+
startClientY: event.clientY,
|
|
641
|
+
hasMoved: false
|
|
642
|
+
});
|
|
643
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
),
|
|
647
|
+
props.data.options.showLabels ? /* @__PURE__ */ jsx2("text", { x: node.x + 18, y: node.y + 5, children: note.title }) : null
|
|
648
|
+
]
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
},
|
|
652
|
+
note.slug
|
|
653
|
+
);
|
|
411
654
|
})
|
|
412
655
|
] })
|
|
413
656
|
}
|
|
@@ -424,13 +667,30 @@ function GardenExplorer(props) {
|
|
|
424
667
|
) : /* @__PURE__ */ jsx2("p", { class: "garden-explorer__empty", children: "Select a note to inspect links." }) })
|
|
425
668
|
] });
|
|
426
669
|
}
|
|
427
|
-
function layoutNodes(notes, selectedSlug) {
|
|
428
|
-
|
|
670
|
+
function layoutNodes(notes, selectedSlug, options) {
|
|
671
|
+
if (options.layout === "radial") {
|
|
672
|
+
return layoutRadialGraph(notes, {
|
|
673
|
+
width: GRAPH_WIDTH,
|
|
674
|
+
height: GRAPH_HEIGHT,
|
|
675
|
+
centerSlug: selectedSlug
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
return layoutForceGraph(notes, {
|
|
429
679
|
width: GRAPH_WIDTH,
|
|
430
680
|
height: GRAPH_HEIGHT,
|
|
431
|
-
centerSlug: selectedSlug
|
|
681
|
+
centerSlug: selectedSlug,
|
|
682
|
+
linkDistance: options.linkDistance,
|
|
683
|
+
repulsion: options.repulsion
|
|
432
684
|
});
|
|
433
685
|
}
|
|
686
|
+
function applyPinnedNodes(layout, pinnedNodes) {
|
|
687
|
+
const next = new Map(layout);
|
|
688
|
+
for (const [slug, point] of Object.entries(pinnedNodes)) {
|
|
689
|
+
const current = next.get(slug);
|
|
690
|
+
if (current) next.set(slug, { ...current, x: point.x, y: point.y });
|
|
691
|
+
}
|
|
692
|
+
return next;
|
|
693
|
+
}
|
|
434
694
|
function getRelatedNotes(selectedNote, notes) {
|
|
435
695
|
const directSlugs = /* @__PURE__ */ new Set([
|
|
436
696
|
...selectedNote.outgoing,
|
|
@@ -444,12 +704,33 @@ function getRelatedNotes(selectedNote, notes) {
|
|
|
444
704
|
(a, b) => b.score - a.score || a.note.title.localeCompare(b.note.title, "ja")
|
|
445
705
|
).slice(0, 8).map((item) => item.note);
|
|
446
706
|
}
|
|
707
|
+
function getRelatedSlugSet(selectedSlug, hoveredSlug, edges) {
|
|
708
|
+
const origin = hoveredSlug ?? selectedSlug;
|
|
709
|
+
if (!origin) return /* @__PURE__ */ new Set();
|
|
710
|
+
const related = /* @__PURE__ */ new Set([origin]);
|
|
711
|
+
for (const edge of edges) {
|
|
712
|
+
if (edge.source === origin) related.add(edge.target);
|
|
713
|
+
if (edge.target === origin) related.add(edge.source);
|
|
714
|
+
}
|
|
715
|
+
return related;
|
|
716
|
+
}
|
|
447
717
|
function panelClass(panel, activePanel) {
|
|
448
718
|
return `garden-explorer__panel garden-explorer__panel--${panel}${panel === activePanel ? " garden-explorer__panel--active" : ""}`;
|
|
449
719
|
}
|
|
450
720
|
function clamp(value, min, max) {
|
|
451
721
|
return Math.min(Math.max(value, min), max);
|
|
452
722
|
}
|
|
723
|
+
function zoomView(current, delta, origin = { x: GRAPH_WIDTH / 2, y: GRAPH_HEIGHT / 2 }) {
|
|
724
|
+
const scale = clamp(current.scale + delta, MIN_SCALE, MAX_SCALE);
|
|
725
|
+
if (scale === current.scale) return current;
|
|
726
|
+
const graphX = (origin.x - current.x) / current.scale;
|
|
727
|
+
const graphY = (origin.y - current.y) / current.scale;
|
|
728
|
+
return {
|
|
729
|
+
x: origin.x - graphX * scale,
|
|
730
|
+
y: origin.y - graphY * scale,
|
|
731
|
+
scale
|
|
732
|
+
};
|
|
733
|
+
}
|
|
453
734
|
export {
|
|
454
735
|
GardenExplorer as default
|
|
455
736
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import type { GardenExplorerPluginOptions } from "./src/garden-explorer.js";
|
|
1
2
|
export { default as GardenExplorer } from "./components/garden-explorer.js";
|
|
2
|
-
export type { GardenExplorerData, GardenExplorerEdge, GardenExplorerFolder, GardenExplorerNote, GardenExplorerTag, } from "./src/garden-explorer.js";
|
|
3
|
+
export type { GardenExplorerData, GardenExplorerEdge, GardenExplorerFolder, GardenExplorerNote, GardenExplorerOptions, GardenExplorerPluginOptions, GardenExplorerTag, } from "./src/garden-explorer.js";
|
|
3
4
|
export { getGardenExplorerData } from "./src/garden-explorer.server.js";
|
|
4
|
-
export declare function gardenExplorerPlugin(): import("@riebeckite/core").RiebeckitePlugin<unknown>;
|
|
5
|
+
export declare function gardenExplorerPlugin(options?: GardenExplorerPluginOptions): import("@riebeckite/core").RiebeckitePlugin<unknown>;
|