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