papyr-react 1.0.0
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/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/blocks/WorkspaceBlock/WorkspaceBlock.d.ts +79 -0
- package/dist/blocks/index.d.ts +2 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.ts +12 -0
- package/dist/components/Breadcrumbs/index.d.ts +2 -0
- package/dist/components/DoubleSidebarLayout/DoubleSidebarLayout.d.ts +49 -0
- package/dist/components/DoubleSidebarLayout/index.d.ts +2 -0
- package/dist/components/EmptyState/EmptyState.d.ts +31 -0
- package/dist/components/EmptyState/index.d.ts +2 -0
- package/dist/components/FileHierarchy/FileHierarchy.d.ts +19 -0
- package/dist/components/FileHierarchy/FileHierarchy.test.d.ts +1 -0
- package/dist/components/FileHierarchy/MemoizedFolderTree.d.ts +16 -0
- package/dist/components/FileHierarchy/index.d.ts +3 -0
- package/dist/components/FileHierarchy/types.d.ts +9 -0
- package/dist/components/FileHierarchy/utils.d.ts +5 -0
- package/dist/components/FileSearch/FileSearch.d.ts +27 -0
- package/dist/components/FileSearch/index.d.ts +2 -0
- package/dist/components/GraphView/GraphView.d.ts +19 -0
- package/dist/components/GraphView/index.d.ts +2 -0
- package/dist/components/HoverPreview/HoverPreview.d.ts +12 -0
- package/dist/components/HoverPreview/index.d.ts +2 -0
- package/dist/components/MiniGraph/MiniGraph.d.ts +8 -0
- package/dist/components/MiniGraph/MiniGraph.test.d.ts +1 -0
- package/dist/components/MiniGraph/index.d.ts +2 -0
- package/dist/components/NotePreview/NotePreviewContent.d.ts +14 -0
- package/dist/components/NotePreview/index.d.ts +2 -0
- package/dist/components/NoteViewer/NoteViewer.d.ts +15 -0
- package/dist/components/NoteViewer/NoteViewer.test.d.ts +1 -0
- package/dist/components/NoteViewer/index.d.ts +2 -0
- package/dist/components/SearchBar/SearchBar.d.ts +9 -0
- package/dist/components/SearchBar/index.d.ts +2 -0
- package/dist/components/SearchDropdown/SearchDropdown.d.ts +12 -0
- package/dist/components/SearchDropdown/index.d.ts +1 -0
- package/dist/components/SidebarLayout/SidebarLayout.d.ts +33 -0
- package/dist/components/SidebarLayout/index.d.ts +2 -0
- package/dist/components/TableOfContents/TableOfContents.d.ts +10 -0
- package/dist/components/TableOfContents/TableOfContents.test.d.ts +1 -0
- package/dist/components/TableOfContents/index.d.ts +1 -0
- package/dist/components/TagFilter/TagFilter.d.ts +9 -0
- package/dist/components/TagFilter/index.d.ts +2 -0
- package/dist/components/Toast/Toast.d.ts +11 -0
- package/dist/components/index.d.ts +16 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/useActiveNote.d.ts +27 -0
- package/dist/hooks/useActiveNote.test.d.ts +1 -0
- package/dist/hooks/useNotes.d.ts +3 -0
- package/dist/hooks/useRoutableActiveNote.d.ts +16 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3178 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/PapyrProvider.d.ts +24 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/style.css +1548 -0
- package/dist/styles/index.d.ts +0 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/graphUtils.d.ts +18 -0
- package/dist/utils/hydration.d.ts +8 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/linkUtils.d.ts +10 -0
- package/package.json +87 -0
- package/src/styles/index.ts +2 -0
- package/src/styles/theme.css +184 -0
- package/style.css +1 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3178 @@
|
|
|
1
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useEffect, useMemo, useCallback, useRef, useLayoutEffect, createContext, useContext, memo } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import { select } from 'd3-selection';
|
|
6
|
+
import { zoomIdentity, zoom } from 'd3-zoom';
|
|
7
|
+
import { drag } from 'd3-drag';
|
|
8
|
+
import { forceSimulation, forceLink, forceManyBody, forceCollide, forceCenter } from 'd3-force';
|
|
9
|
+
import { createRoot } from 'react-dom/client';
|
|
10
|
+
import { importSearchIndex, searchNotes } from 'papyr-core/runtime';
|
|
11
|
+
import { useFloating, autoUpdate, offset, flip, shift } from '@floating-ui/react';
|
|
12
|
+
|
|
13
|
+
const shell = "_shell_j3ea5_1";
|
|
14
|
+
const root = "_root_j3ea5_8";
|
|
15
|
+
const canvas$1 = "_canvas_j3ea5_16";
|
|
16
|
+
const fullscreenButton = "_fullscreenButton_j3ea5_27";
|
|
17
|
+
const fullscreenOverlay = "_fullscreenOverlay_j3ea5_49";
|
|
18
|
+
const fullscreenBackdrop = "_fullscreenBackdrop_j3ea5_58";
|
|
19
|
+
const fullscreenDialog = "_fullscreenDialog_j3ea5_65";
|
|
20
|
+
const fullscreenHeader = "_fullscreenHeader_j3ea5_77";
|
|
21
|
+
const fullscreenClose = "_fullscreenClose_j3ea5_86";
|
|
22
|
+
const fullscreenBody = "_fullscreenBody_j3ea5_94";
|
|
23
|
+
const fullscreenGraph = "_fullscreenGraph_j3ea5_99";
|
|
24
|
+
const emptyState$2 = "_emptyState_j3ea5_104";
|
|
25
|
+
const node$1 = "_node_j3ea5_115";
|
|
26
|
+
const isActive = "_isActive_j3ea5_120";
|
|
27
|
+
const label = "_label_j3ea5_124";
|
|
28
|
+
const styles$f = {
|
|
29
|
+
shell: shell,
|
|
30
|
+
root: root,
|
|
31
|
+
canvas: canvas$1,
|
|
32
|
+
fullscreenButton: fullscreenButton,
|
|
33
|
+
fullscreenOverlay: fullscreenOverlay,
|
|
34
|
+
fullscreenBackdrop: fullscreenBackdrop,
|
|
35
|
+
fullscreenDialog: fullscreenDialog,
|
|
36
|
+
fullscreenHeader: fullscreenHeader,
|
|
37
|
+
fullscreenClose: fullscreenClose,
|
|
38
|
+
fullscreenBody: fullscreenBody,
|
|
39
|
+
fullscreenGraph: fullscreenGraph,
|
|
40
|
+
emptyState: emptyState$2,
|
|
41
|
+
node: node$1,
|
|
42
|
+
isActive: isActive,
|
|
43
|
+
label: label
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const TEXT_FALLBACK_CHAR_WIDTH = 8.5;
|
|
47
|
+
const TEXT_FALLBACK_HEIGHT = 15;
|
|
48
|
+
const LABEL_MIN_WIDTH = 40;
|
|
49
|
+
const LABEL_GAP_Y = 8;
|
|
50
|
+
const LABEL_EXTRA_PAD_X = 8;
|
|
51
|
+
const LABEL_EXTRA_PAD_Y = 6;
|
|
52
|
+
const TEXT_STROKE_PAD = 2;
|
|
53
|
+
const AUTO_ZOOM_PADDING = 48;
|
|
54
|
+
const DEFAULT_ZOOM_EXTENT = [0.35, 4];
|
|
55
|
+
const LABEL_FONT_BASE_PX = 11;
|
|
56
|
+
const LABEL_FONT_DISPLAY_SCALE = 1;
|
|
57
|
+
const LABEL_FONT_SCALE_MIN = 0.8;
|
|
58
|
+
const LABEL_FONT_SCALE_MAX = 1.5;
|
|
59
|
+
const NODE_RADIUS_MIN = 5;
|
|
60
|
+
const NODE_RADIUS_MAX = 14;
|
|
61
|
+
const NODE_RADIUS_POWER = 0.5;
|
|
62
|
+
const NODE_RADIUS_SCALE = 1;
|
|
63
|
+
const NODE_RADIUS_MIN_SCALED = NODE_RADIUS_MIN * NODE_RADIUS_SCALE;
|
|
64
|
+
const hasNodePosition = (node) => {
|
|
65
|
+
return typeof node.x === "number" && Number.isFinite(node.x) && typeof node.y === "number" && Number.isFinite(node.y);
|
|
66
|
+
};
|
|
67
|
+
function getNodeLabelSize(d, labelScale = 1) {
|
|
68
|
+
const text = d.node.label ?? d.node.id;
|
|
69
|
+
const width = (d.textWidth ?? Math.max(text.length * TEXT_FALLBACK_CHAR_WIDTH, LABEL_MIN_WIDTH)) + TEXT_STROKE_PAD;
|
|
70
|
+
const height = (d.textHeight ?? TEXT_FALLBACK_HEIGHT) + TEXT_STROKE_PAD;
|
|
71
|
+
return { width: width * labelScale, height: height * labelScale };
|
|
72
|
+
}
|
|
73
|
+
function getNodeVisualExtent(d, labelScale = 1) {
|
|
74
|
+
const r = d.radius;
|
|
75
|
+
const { width, height } = getNodeLabelSize(d, labelScale);
|
|
76
|
+
const halfWidth = Math.max(r, width / 2) + LABEL_EXTRA_PAD_X;
|
|
77
|
+
const top = r + LABEL_EXTRA_PAD_Y;
|
|
78
|
+
const bottom = r + LABEL_GAP_Y + height + LABEL_EXTRA_PAD_Y;
|
|
79
|
+
return { halfWidth, top, bottom };
|
|
80
|
+
}
|
|
81
|
+
const clamp = (value, min, max) => {
|
|
82
|
+
if (value < min) {
|
|
83
|
+
return min;
|
|
84
|
+
}
|
|
85
|
+
if (value > max) {
|
|
86
|
+
return max;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
};
|
|
90
|
+
const computeGraphBounds = (nodes, labelScale = 1) => {
|
|
91
|
+
let minX = Infinity;
|
|
92
|
+
let maxX = -Infinity;
|
|
93
|
+
let minY = Infinity;
|
|
94
|
+
let maxY = -Infinity;
|
|
95
|
+
let hasAny = false;
|
|
96
|
+
for (const node of nodes) {
|
|
97
|
+
if (!hasNodePosition(node)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const { halfWidth, top, bottom } = getNodeVisualExtent(node, labelScale);
|
|
101
|
+
const nodeMinX = node.x - halfWidth;
|
|
102
|
+
const nodeMaxX = node.x + halfWidth;
|
|
103
|
+
const nodeMinY = node.y - top;
|
|
104
|
+
const nodeMaxY = node.y + bottom;
|
|
105
|
+
hasAny = true;
|
|
106
|
+
if (nodeMinX < minX) minX = nodeMinX;
|
|
107
|
+
if (nodeMaxX > maxX) maxX = nodeMaxX;
|
|
108
|
+
if (nodeMinY < minY) minY = nodeMinY;
|
|
109
|
+
if (nodeMaxY > maxY) maxY = nodeMaxY;
|
|
110
|
+
}
|
|
111
|
+
if (!hasAny) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return { minX, maxX, minY, maxY };
|
|
115
|
+
};
|
|
116
|
+
const NODE_COLOR_FALLBACK = "var(--papyr-graph-node, rgba(255, 255, 255, 0.85))";
|
|
117
|
+
const NODE_OUTLINE = "var(--papyr-graph-stroke, rgba(0, 0, 0, 0.25))";
|
|
118
|
+
const LINK_COLOR = "var(--papyr-graph-link, rgba(255, 255, 255, 0.25))";
|
|
119
|
+
const LINK_HIGHLIGHT_COLOR = "var(--papyr-graph-link-active, rgba(255, 255, 255, 0.6))";
|
|
120
|
+
const LINK_STROKE_WIDTH = 1;
|
|
121
|
+
const LINK_HIGHLIGHT_STROKE_WIDTH = 1.5;
|
|
122
|
+
const BASE_LINK_OPACITY = 0.7;
|
|
123
|
+
const DIMMED_LINK_OPACITY = 0.08;
|
|
124
|
+
const FOCUS_LINK_OPACITY = 0.7;
|
|
125
|
+
const HIGHLIGHT_LINK_OPACITY = 1;
|
|
126
|
+
const DIMMED_NODE_OPACITY = 0.4;
|
|
127
|
+
function normalizeGraph(graph, filterNode) {
|
|
128
|
+
if (!graph) {
|
|
129
|
+
return {
|
|
130
|
+
nodes: [],
|
|
131
|
+
links: [],
|
|
132
|
+
adjacency: /* @__PURE__ */ new Map()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const nodes = [];
|
|
136
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
137
|
+
for (const [, node] of graph.nodes) {
|
|
138
|
+
if (filterNode && !filterNode(node)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
nodes.push(node);
|
|
142
|
+
adjacency.set(node.id, /* @__PURE__ */ new Set());
|
|
143
|
+
}
|
|
144
|
+
if (nodes.length === 0) {
|
|
145
|
+
return { nodes, links: [], adjacency };
|
|
146
|
+
}
|
|
147
|
+
const allowed = new Set(nodes.map((node) => node.id));
|
|
148
|
+
const links = [];
|
|
149
|
+
for (const edge of graph.edges) {
|
|
150
|
+
const { source, target } = edge;
|
|
151
|
+
if (!allowed.has(source) || !allowed.has(target) || source === target) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
adjacency.get(source)?.add(target);
|
|
155
|
+
adjacency.get(target)?.add(source);
|
|
156
|
+
links.push({ source, target });
|
|
157
|
+
}
|
|
158
|
+
return { nodes, links, adjacency };
|
|
159
|
+
}
|
|
160
|
+
function getNeighborhood(graph, slug) {
|
|
161
|
+
const neighbors = /* @__PURE__ */ new Set([slug]);
|
|
162
|
+
graph.edges.forEach((edge) => {
|
|
163
|
+
if (edge.source === slug) {
|
|
164
|
+
neighbors.add(edge.target);
|
|
165
|
+
}
|
|
166
|
+
if (edge.target === slug) {
|
|
167
|
+
neighbors.add(edge.source);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return neighbors;
|
|
171
|
+
}
|
|
172
|
+
const resolveLinkEndpointId = (value) => {
|
|
173
|
+
if (typeof value === "string") {
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
if (typeof value === "number") {
|
|
177
|
+
return String(value);
|
|
178
|
+
}
|
|
179
|
+
return value.id;
|
|
180
|
+
};
|
|
181
|
+
const resolveLinkCoordinate = (value, axis) => {
|
|
182
|
+
if (typeof value === "object" && value !== null) {
|
|
183
|
+
return axis === "x" ? value.x ?? 0 : value.y ?? 0;
|
|
184
|
+
}
|
|
185
|
+
return 0;
|
|
186
|
+
};
|
|
187
|
+
const GraphCanvas = ({
|
|
188
|
+
graph,
|
|
189
|
+
className,
|
|
190
|
+
filterNode,
|
|
191
|
+
focusPredicate,
|
|
192
|
+
nodeColor,
|
|
193
|
+
zoomExtent,
|
|
194
|
+
focusNodeId = null,
|
|
195
|
+
onNodeSelect
|
|
196
|
+
}) => {
|
|
197
|
+
const containerRef = useRef(null);
|
|
198
|
+
const svgRef = useRef(null);
|
|
199
|
+
const zoomBehaviorRef = useRef(null);
|
|
200
|
+
const transformRef = useRef(zoomIdentity);
|
|
201
|
+
const simulationRef = useRef(null);
|
|
202
|
+
const nodeLookupRef = useRef(/* @__PURE__ */ new Map());
|
|
203
|
+
const hoveredNodeRef = useRef(null);
|
|
204
|
+
const pendingFocusRef = useRef(null);
|
|
205
|
+
const draggingNodeRef = useRef(null);
|
|
206
|
+
const labelLayerRef = useRef(null);
|
|
207
|
+
const autoZoomPendingRef = useRef(false);
|
|
208
|
+
const autoZoomFrameRef = useRef(null);
|
|
209
|
+
const autoZoomReadyRef = useRef(false);
|
|
210
|
+
const labelFontScaleRef = useRef(LABEL_FONT_DISPLAY_SCALE);
|
|
211
|
+
const [activeNode, setActiveNode] = useState(null);
|
|
212
|
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
|
213
|
+
useLayoutEffect(() => {
|
|
214
|
+
const element = containerRef.current;
|
|
215
|
+
if (!element) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const observer = new ResizeObserver((entries) => {
|
|
219
|
+
const entry = entries[0];
|
|
220
|
+
if (!entry) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const { width, height } = entry.contentRect;
|
|
224
|
+
setDimensions({
|
|
225
|
+
width: Math.max(200, Math.floor(width)),
|
|
226
|
+
height: Math.max(240, Math.floor(height))
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
observer.observe(element);
|
|
230
|
+
return () => observer.disconnect();
|
|
231
|
+
}, []);
|
|
232
|
+
const normalized = useMemo(() => normalizeGraph(graph, filterNode), [graph, filterNode]);
|
|
233
|
+
const hasGraph = normalized.nodes.length > 0;
|
|
234
|
+
const resolvedZoomExtent = zoomExtent ?? DEFAULT_ZOOM_EXTENT;
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
setActiveNode(focusNodeId ?? null);
|
|
237
|
+
}, [focusNodeId]);
|
|
238
|
+
const focusMatches = useMemo(() => {
|
|
239
|
+
if (!focusPredicate) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const matches = /* @__PURE__ */ new Set();
|
|
243
|
+
for (const node of normalized.nodes) {
|
|
244
|
+
if (focusPredicate(node)) {
|
|
245
|
+
matches.add(node.id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return matches;
|
|
249
|
+
}, [focusPredicate, normalized.nodes]);
|
|
250
|
+
const resolvedNodeColor = useCallback(
|
|
251
|
+
(node) => {
|
|
252
|
+
if (nodeColor) {
|
|
253
|
+
return nodeColor(node);
|
|
254
|
+
}
|
|
255
|
+
return NODE_COLOR_FALLBACK;
|
|
256
|
+
},
|
|
257
|
+
[nodeColor]
|
|
258
|
+
);
|
|
259
|
+
const { nodeWeights, maxWeight } = useMemo(() => {
|
|
260
|
+
const weights = /* @__PURE__ */ new Map();
|
|
261
|
+
let currentMax = 0;
|
|
262
|
+
for (const node of normalized.nodes) {
|
|
263
|
+
const metadataWeight = typeof node.metadata?.weight === "number" ? node.metadata.weight : null;
|
|
264
|
+
const degree = normalized.adjacency.get(node.id)?.size ?? 0;
|
|
265
|
+
const weight = metadataWeight ?? degree;
|
|
266
|
+
weights.set(node.id, weight);
|
|
267
|
+
if (weight > currentMax) {
|
|
268
|
+
currentMax = weight;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { nodeWeights: weights, maxWeight: currentMax };
|
|
272
|
+
}, [normalized]);
|
|
273
|
+
const resolveNodeRadius = useCallback(
|
|
274
|
+
(weight) => {
|
|
275
|
+
if (!maxWeight || !Number.isFinite(maxWeight) || maxWeight <= 0) {
|
|
276
|
+
return NODE_RADIUS_MIN_SCALED;
|
|
277
|
+
}
|
|
278
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
279
|
+
return NODE_RADIUS_MIN_SCALED;
|
|
280
|
+
}
|
|
281
|
+
const normalizedWeight = clamp(weight / maxWeight, 0, 1);
|
|
282
|
+
const eased = Math.pow(normalizedWeight, NODE_RADIUS_POWER);
|
|
283
|
+
const base = NODE_RADIUS_MIN + eased * (NODE_RADIUS_MAX - NODE_RADIUS_MIN);
|
|
284
|
+
return base * NODE_RADIUS_SCALE;
|
|
285
|
+
},
|
|
286
|
+
[maxWeight]
|
|
287
|
+
);
|
|
288
|
+
const nodeData = useMemo(() => {
|
|
289
|
+
return normalized.nodes.map((node) => {
|
|
290
|
+
const weight = nodeWeights.get(node.id) ?? 0;
|
|
291
|
+
return {
|
|
292
|
+
id: node.id,
|
|
293
|
+
node,
|
|
294
|
+
weight,
|
|
295
|
+
radius: resolveNodeRadius(weight),
|
|
296
|
+
color: resolvedNodeColor(node)
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
}, [normalized.nodes, nodeWeights, resolveNodeRadius, resolvedNodeColor]);
|
|
300
|
+
const linkData = useMemo(() => {
|
|
301
|
+
return normalized.links.map((link, index) => ({
|
|
302
|
+
source: link.source,
|
|
303
|
+
target: link.target,
|
|
304
|
+
uid: `${link.source}::${link.target}::${index}`
|
|
305
|
+
}));
|
|
306
|
+
}, [normalized.links]);
|
|
307
|
+
const handleDefaultNavigation = useCallback((node) => {
|
|
308
|
+
if (typeof window === "undefined") {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const permalink = typeof node.metadata?.permalink === "string" ? node.metadata.permalink : null;
|
|
312
|
+
const fallback = `#/note/${node.id}`;
|
|
313
|
+
const target = permalink && permalink.length > 0 ? permalink : fallback;
|
|
314
|
+
window.open(target, "_self");
|
|
315
|
+
}, []);
|
|
316
|
+
const focusOnNode = useCallback(
|
|
317
|
+
(datum) => {
|
|
318
|
+
if (!svgRef.current || !zoomBehaviorRef.current) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (!hasNodePosition(datum)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (!dimensions.width || !dimensions.height) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const currentScale = transformRef.current?.k ?? 1;
|
|
328
|
+
const tx = dimensions.width / 2 - currentScale * datum.x;
|
|
329
|
+
const ty = dimensions.height / 2 - currentScale * datum.y;
|
|
330
|
+
const nextTransform = zoomIdentity.translate(tx, ty).scale(currentScale);
|
|
331
|
+
transformRef.current = nextTransform;
|
|
332
|
+
select(svgRef.current).call(zoomBehaviorRef.current.transform, nextTransform);
|
|
333
|
+
return true;
|
|
334
|
+
},
|
|
335
|
+
[dimensions.height, dimensions.width]
|
|
336
|
+
);
|
|
337
|
+
const cancelAutoZoomFrame = useCallback(() => {
|
|
338
|
+
if (typeof window === "undefined") {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (autoZoomFrameRef.current !== null) {
|
|
342
|
+
window.cancelAnimationFrame(autoZoomFrameRef.current);
|
|
343
|
+
autoZoomFrameRef.current = null;
|
|
344
|
+
}
|
|
345
|
+
}, []);
|
|
346
|
+
const applyLabelFontSize = useCallback(
|
|
347
|
+
(zoomScale) => {
|
|
348
|
+
if (!labelLayerRef.current) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const safeScale = clamp(zoomScale, 0.15, 6);
|
|
352
|
+
const labelScale = clamp(
|
|
353
|
+
LABEL_FONT_DISPLAY_SCALE / safeScale,
|
|
354
|
+
LABEL_FONT_SCALE_MIN,
|
|
355
|
+
LABEL_FONT_SCALE_MAX
|
|
356
|
+
);
|
|
357
|
+
labelFontScaleRef.current = labelScale;
|
|
358
|
+
const fontSizePx = LABEL_FONT_BASE_PX * labelScale;
|
|
359
|
+
select(labelLayerRef.current).selectAll("text[data-label]").style("font-size", `${fontSizePx}px`);
|
|
360
|
+
},
|
|
361
|
+
[]
|
|
362
|
+
);
|
|
363
|
+
const applyAutoZoom = useCallback(() => {
|
|
364
|
+
if (!svgRef.current || !zoomBehaviorRef.current) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (!dimensions.width || !dimensions.height) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const bounds = computeGraphBounds(nodeLookupRef.current.values(), labelFontScaleRef.current);
|
|
371
|
+
if (!bounds) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const boundsWidth = bounds.maxX - bounds.minX;
|
|
375
|
+
const boundsHeight = bounds.maxY - bounds.minY;
|
|
376
|
+
if (!Number.isFinite(boundsWidth) || !Number.isFinite(boundsHeight)) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const innerWidth = Math.max(1, dimensions.width - AUTO_ZOOM_PADDING * 2);
|
|
380
|
+
const innerHeight = Math.max(1, dimensions.height - AUTO_ZOOM_PADDING * 2);
|
|
381
|
+
const rawScale = Math.min(
|
|
382
|
+
1,
|
|
383
|
+
innerWidth / Math.max(boundsWidth, 1),
|
|
384
|
+
innerHeight / Math.max(boundsHeight, 1)
|
|
385
|
+
);
|
|
386
|
+
if (!Number.isFinite(rawScale) || rawScale <= 0) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
const [providedMin, providedMax] = resolvedZoomExtent;
|
|
390
|
+
const minScale = Math.min(providedMin, rawScale);
|
|
391
|
+
const maxScale = providedMax;
|
|
392
|
+
const scale = clamp(rawScale, minScale, maxScale);
|
|
393
|
+
let targetX = (bounds.minX + bounds.maxX) / 2;
|
|
394
|
+
let targetY = (bounds.minY + bounds.maxY) / 2;
|
|
395
|
+
const focusId = pendingFocusRef.current ?? activeNode;
|
|
396
|
+
if (focusId) {
|
|
397
|
+
const focusNode = nodeLookupRef.current.get(focusId);
|
|
398
|
+
if (focusNode && hasNodePosition(focusNode)) {
|
|
399
|
+
targetX = focusNode.x;
|
|
400
|
+
targetY = focusNode.y;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const tx = dimensions.width / 2 - scale * targetX;
|
|
404
|
+
const ty = dimensions.height / 2 - scale * targetY;
|
|
405
|
+
const nextTransform = zoomIdentity.translate(tx, ty).scale(scale);
|
|
406
|
+
transformRef.current = nextTransform;
|
|
407
|
+
const behavior = zoomBehaviorRef.current;
|
|
408
|
+
behavior.scaleExtent([minScale, maxScale]);
|
|
409
|
+
select(svgRef.current).call(behavior.transform, nextTransform);
|
|
410
|
+
return true;
|
|
411
|
+
}, [activeNode, dimensions.height, dimensions.width, resolvedZoomExtent]);
|
|
412
|
+
const scheduleAutoZoom = useCallback(() => {
|
|
413
|
+
if (typeof window === "undefined") {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
autoZoomPendingRef.current = true;
|
|
417
|
+
if (autoZoomFrameRef.current !== null) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const tick = () => {
|
|
421
|
+
autoZoomFrameRef.current = null;
|
|
422
|
+
if (!autoZoomPendingRef.current) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const applied = applyAutoZoom();
|
|
426
|
+
if (applied) {
|
|
427
|
+
autoZoomPendingRef.current = false;
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
autoZoomFrameRef.current = window.requestAnimationFrame(tick);
|
|
431
|
+
};
|
|
432
|
+
autoZoomFrameRef.current = window.requestAnimationFrame(tick);
|
|
433
|
+
}, [applyAutoZoom]);
|
|
434
|
+
const flushPendingFocus = useCallback(() => {
|
|
435
|
+
const targetId = pendingFocusRef.current;
|
|
436
|
+
if (!targetId) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const node = nodeLookupRef.current.get(targetId);
|
|
440
|
+
if (!node) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const didFocus = focusOnNode(node);
|
|
444
|
+
if (didFocus) {
|
|
445
|
+
pendingFocusRef.current = null;
|
|
446
|
+
}
|
|
447
|
+
}, [focusOnNode]);
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
return () => {
|
|
450
|
+
autoZoomPendingRef.current = false;
|
|
451
|
+
cancelAutoZoomFrame();
|
|
452
|
+
};
|
|
453
|
+
}, [cancelAutoZoomFrame]);
|
|
454
|
+
useEffect(() => {
|
|
455
|
+
if (!hasGraph) {
|
|
456
|
+
autoZoomPendingRef.current = false;
|
|
457
|
+
cancelAutoZoomFrame();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (!autoZoomReadyRef.current) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
scheduleAutoZoom();
|
|
464
|
+
}, [
|
|
465
|
+
hasGraph,
|
|
466
|
+
scheduleAutoZoom,
|
|
467
|
+
cancelAutoZoomFrame,
|
|
468
|
+
dimensions.height,
|
|
469
|
+
dimensions.width,
|
|
470
|
+
resolvedZoomExtent
|
|
471
|
+
]);
|
|
472
|
+
const requestFocusNode = useCallback(
|
|
473
|
+
(nodeId) => {
|
|
474
|
+
pendingFocusRef.current = nodeId;
|
|
475
|
+
if (!nodeId) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
flushPendingFocus();
|
|
479
|
+
},
|
|
480
|
+
[flushPendingFocus]
|
|
481
|
+
);
|
|
482
|
+
const handleNodeSelection = useCallback(
|
|
483
|
+
(datum) => {
|
|
484
|
+
setActiveNode(datum.id);
|
|
485
|
+
const didFocus = focusOnNode(datum);
|
|
486
|
+
pendingFocusRef.current = didFocus ? null : datum.id;
|
|
487
|
+
if (onNodeSelect) {
|
|
488
|
+
onNodeSelect(datum.node);
|
|
489
|
+
} else {
|
|
490
|
+
handleDefaultNavigation(datum.node);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
[focusOnNode, handleDefaultNavigation, onNodeSelect]
|
|
494
|
+
);
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
requestFocusNode(focusNodeId ?? null);
|
|
497
|
+
if (simulationRef.current && focusNodeId) {
|
|
498
|
+
const sim = simulationRef.current;
|
|
499
|
+
if (sim.alpha() < 0.1) {
|
|
500
|
+
sim.alpha(0.3).restart();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}, [focusNodeId, requestFocusNode]);
|
|
504
|
+
const applyHighlight = useCallback(
|
|
505
|
+
(hoveredId) => {
|
|
506
|
+
if (!svgRef.current) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
const svg = select(svgRef.current);
|
|
510
|
+
const nodesSelection = svg.selectAll("circle[data-node]");
|
|
511
|
+
const linksSelection = svg.selectAll("line[data-link]");
|
|
512
|
+
const labelsSelection = svg.selectAll("text[data-label]");
|
|
513
|
+
const highlightId = hoveredId;
|
|
514
|
+
const hoveredSet = highlightId ? /* @__PURE__ */ new Set([
|
|
515
|
+
highlightId,
|
|
516
|
+
...Array.from(normalized.adjacency.get(highlightId) ?? [])
|
|
517
|
+
]) : null;
|
|
518
|
+
const focusSet = focusMatches && focusMatches.size > 0 ? focusMatches : null;
|
|
519
|
+
nodesSelection.classed(styles$f.isActive, (d) => d.id === activeNode).style("opacity", (d) => {
|
|
520
|
+
if (hoveredSet) {
|
|
521
|
+
return hoveredSet.has(d.id) ? 1 : DIMMED_NODE_OPACITY;
|
|
522
|
+
}
|
|
523
|
+
if (focusSet) {
|
|
524
|
+
return focusSet.has(d.id) ? 1 : DIMMED_NODE_OPACITY;
|
|
525
|
+
}
|
|
526
|
+
return 1;
|
|
527
|
+
});
|
|
528
|
+
linksSelection.style("opacity", (d) => {
|
|
529
|
+
const sourceId = resolveLinkEndpointId(d.source);
|
|
530
|
+
const targetId = resolveLinkEndpointId(d.target);
|
|
531
|
+
if (highlightId) {
|
|
532
|
+
return sourceId === highlightId || targetId === highlightId ? HIGHLIGHT_LINK_OPACITY : DIMMED_LINK_OPACITY;
|
|
533
|
+
}
|
|
534
|
+
if (focusSet) {
|
|
535
|
+
return focusSet.has(sourceId) && focusSet.has(targetId) ? FOCUS_LINK_OPACITY : DIMMED_LINK_OPACITY;
|
|
536
|
+
}
|
|
537
|
+
return BASE_LINK_OPACITY;
|
|
538
|
+
}).attr("stroke-width", hoveredId ? LINK_HIGHLIGHT_STROKE_WIDTH : LINK_STROKE_WIDTH).attr("stroke", (d) => {
|
|
539
|
+
const sourceId = resolveLinkEndpointId(d.source);
|
|
540
|
+
const targetId = resolveLinkEndpointId(d.target);
|
|
541
|
+
if (highlightId && (sourceId === highlightId || targetId === highlightId)) {
|
|
542
|
+
return LINK_HIGHLIGHT_COLOR;
|
|
543
|
+
}
|
|
544
|
+
return LINK_COLOR;
|
|
545
|
+
});
|
|
546
|
+
labelsSelection.style("opacity", (d) => {
|
|
547
|
+
if (hoveredSet) {
|
|
548
|
+
return hoveredSet.has(d.id) ? 1 : 0.4;
|
|
549
|
+
}
|
|
550
|
+
if (focusSet && focusSet.has(d.id)) {
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
return 0.7;
|
|
554
|
+
});
|
|
555
|
+
},
|
|
556
|
+
[activeNode, focusMatches, normalized.adjacency]
|
|
557
|
+
);
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
if (!svgRef.current) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const svg = select(svgRef.current);
|
|
563
|
+
const viewport = svg.select("g[data-viewport]");
|
|
564
|
+
const linkLayer = viewport.select("g[data-links]");
|
|
565
|
+
const nodeLayer = viewport.select("g[data-nodes]");
|
|
566
|
+
const labelLayer = viewport.select("g[data-labels]");
|
|
567
|
+
const nodes = nodeData.map((datum) => ({ ...datum }));
|
|
568
|
+
const links = linkData.map((link) => ({ ...link }));
|
|
569
|
+
nodeLookupRef.current = new Map(nodes.map((node) => [node.id, node]));
|
|
570
|
+
autoZoomReadyRef.current = false;
|
|
571
|
+
autoZoomPendingRef.current = false;
|
|
572
|
+
cancelAutoZoomFrame();
|
|
573
|
+
pendingFocusRef.current = focusNodeId ?? null;
|
|
574
|
+
if (focusNodeId) {
|
|
575
|
+
const focusNode = nodes.find((n) => n.id === focusNodeId);
|
|
576
|
+
if (focusNode) {
|
|
577
|
+
focusNode.fx = 0;
|
|
578
|
+
focusNode.fy = 0;
|
|
579
|
+
focusNode.x = 0;
|
|
580
|
+
focusNode.y = 0;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const linkSelection = linkLayer.selectAll("line[data-link]").data(links, (d) => d.uid);
|
|
584
|
+
linkSelection.exit().remove();
|
|
585
|
+
const linkEnter = linkSelection.enter().append("line").attr("data-link", "true").attr("stroke-width", LINK_STROKE_WIDTH).attr("stroke", LINK_COLOR).attr("stroke-linecap", "round");
|
|
586
|
+
const mergedLinks = linkEnter.merge(linkSelection);
|
|
587
|
+
labelLayerRef.current = labelLayer.node();
|
|
588
|
+
const labelSelection = labelLayer.selectAll("text[data-label]").data(nodes, (d) => d.id);
|
|
589
|
+
labelSelection.exit().remove();
|
|
590
|
+
const labelEnter = labelSelection.enter().append("text").attr("data-label", "true").attr("class", styles$f.label).attr("text-anchor", "middle").attr("alignment-baseline", "hanging").attr("dy", (d) => `${d.radius + 8}px`).style("opacity", 1).text((d) => d.node.label ?? d.node.id);
|
|
591
|
+
const mergedLabels = labelEnter.merge(labelSelection);
|
|
592
|
+
mergedLabels.style(
|
|
593
|
+
"font-size",
|
|
594
|
+
`${LABEL_FONT_BASE_PX * labelFontScaleRef.current}px`
|
|
595
|
+
);
|
|
596
|
+
requestAnimationFrame(() => {
|
|
597
|
+
mergedLabels.each(function(d) {
|
|
598
|
+
const textElement = this;
|
|
599
|
+
try {
|
|
600
|
+
const currentX = textElement.getAttribute("x");
|
|
601
|
+
const currentY = textElement.getAttribute("y");
|
|
602
|
+
if (!currentX || !currentY || currentX === "0" || currentY === "0") {
|
|
603
|
+
textElement.setAttribute("x", "0");
|
|
604
|
+
textElement.setAttribute("y", "0");
|
|
605
|
+
}
|
|
606
|
+
const bbox = textElement.getBBox();
|
|
607
|
+
const baseScale = labelFontScaleRef.current || 1;
|
|
608
|
+
d.textWidth = bbox.width / baseScale;
|
|
609
|
+
d.textHeight = bbox.height / baseScale;
|
|
610
|
+
} catch {
|
|
611
|
+
const text = d.node.label ?? d.node.id;
|
|
612
|
+
d.textWidth = text.length * TEXT_FALLBACK_CHAR_WIDTH;
|
|
613
|
+
d.textHeight = TEXT_FALLBACK_HEIGHT;
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
const nodeSelection = nodeLayer.selectAll("circle[data-node]").data(nodes, (d) => d.id);
|
|
618
|
+
nodeSelection.exit().remove();
|
|
619
|
+
const nodeEnter = nodeSelection.enter().append("circle").attr("data-node", "true").attr("class", styles$f.node).attr("fill", (d) => d.color).attr("stroke", NODE_OUTLINE).attr("stroke-width", 1).attr("r", (d) => d.radius).on("mouseenter", (_event, datum) => {
|
|
620
|
+
hoveredNodeRef.current = datum.id;
|
|
621
|
+
applyHighlight(datum.id);
|
|
622
|
+
}).on("mouseleave", (_event, datum) => {
|
|
623
|
+
if (draggingNodeRef.current === datum.id) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
hoveredNodeRef.current = null;
|
|
627
|
+
applyHighlight(null);
|
|
628
|
+
}).on("click", (event, datum) => {
|
|
629
|
+
event.stopPropagation();
|
|
630
|
+
handleNodeSelection(datum);
|
|
631
|
+
});
|
|
632
|
+
nodeEnter.append("title").text((d) => d.node.label ?? d.node.id);
|
|
633
|
+
const mergedNodes = nodeEnter.merge(nodeSelection);
|
|
634
|
+
const centerX = focusNodeId ? 0 : dimensions.width / 2;
|
|
635
|
+
const centerY = focusNodeId ? 0 : dimensions.height / 2 - 20;
|
|
636
|
+
const simulation = forceSimulation(nodes).force(
|
|
637
|
+
"link",
|
|
638
|
+
forceLink(links).id((d) => d.id).distance(() => 135).strength(0.18)
|
|
639
|
+
).force("charge", forceManyBody().strength(-320)).force(
|
|
640
|
+
"collision",
|
|
641
|
+
forceCollide().radius((d) => {
|
|
642
|
+
const { halfWidth, top, bottom } = getNodeVisualExtent(d, labelFontScaleRef.current);
|
|
643
|
+
return Math.max(halfWidth, top, bottom);
|
|
644
|
+
})
|
|
645
|
+
).force("center", forceCenter(centerX, centerY)).alphaDecay(0.07).alpha(0.9);
|
|
646
|
+
let tickCount = 0;
|
|
647
|
+
simulation.on("tick", () => {
|
|
648
|
+
tickCount += 1;
|
|
649
|
+
mergedLinks.attr("x1", (d) => resolveLinkCoordinate(d.source, "x")).attr("y1", (d) => resolveLinkCoordinate(d.source, "y")).attr("x2", (d) => resolveLinkCoordinate(d.target, "x")).attr("y2", (d) => resolveLinkCoordinate(d.target, "y"));
|
|
650
|
+
mergedNodes.attr("cx", (d) => d.x ?? 0).attr("cy", (d) => d.y ?? 0);
|
|
651
|
+
mergedLabels.attr("x", (d) => d.x ?? 0).attr("y", (d) => {
|
|
652
|
+
const y = d.y ?? 0;
|
|
653
|
+
const radius = d.radius;
|
|
654
|
+
return y + radius + LABEL_GAP_Y;
|
|
655
|
+
}).each(function(d) {
|
|
656
|
+
if (d.textWidth === void 0 || d.textHeight === void 0) {
|
|
657
|
+
const textElement = this;
|
|
658
|
+
try {
|
|
659
|
+
const bbox = textElement.getBBox();
|
|
660
|
+
d.textWidth = bbox.width;
|
|
661
|
+
d.textHeight = bbox.height;
|
|
662
|
+
} catch {
|
|
663
|
+
const text = d.node.label ?? d.node.id;
|
|
664
|
+
d.textWidth = Math.max(
|
|
665
|
+
text.length * TEXT_FALLBACK_CHAR_WIDTH,
|
|
666
|
+
LABEL_MIN_WIDTH
|
|
667
|
+
);
|
|
668
|
+
d.textHeight = TEXT_FALLBACK_HEIGHT;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
const alpha = simulation.alpha();
|
|
673
|
+
if (pendingFocusRef.current && tickCount >= 10 && alpha < 0.3) {
|
|
674
|
+
flushPendingFocus();
|
|
675
|
+
}
|
|
676
|
+
if (alpha < 0.02) {
|
|
677
|
+
simulation.stop();
|
|
678
|
+
if (pendingFocusRef.current) {
|
|
679
|
+
flushPendingFocus();
|
|
680
|
+
setTimeout(() => {
|
|
681
|
+
if (pendingFocusRef.current) {
|
|
682
|
+
flushPendingFocus();
|
|
683
|
+
}
|
|
684
|
+
}, 50);
|
|
685
|
+
setTimeout(() => {
|
|
686
|
+
if (pendingFocusRef.current) {
|
|
687
|
+
flushPendingFocus();
|
|
688
|
+
}
|
|
689
|
+
}, 200);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
simulationRef.current = simulation;
|
|
694
|
+
const dragBehavior = drag().on("start", (event) => {
|
|
695
|
+
if (!event.active) {
|
|
696
|
+
simulation.alphaTarget(0.3).restart();
|
|
697
|
+
}
|
|
698
|
+
draggingNodeRef.current = event.subject.id;
|
|
699
|
+
hoveredNodeRef.current = event.subject.id;
|
|
700
|
+
applyHighlight(event.subject.id);
|
|
701
|
+
event.subject.fx = event.subject.x;
|
|
702
|
+
event.subject.fy = event.subject.y;
|
|
703
|
+
}).on("drag", (event) => {
|
|
704
|
+
event.subject.fx = event.x;
|
|
705
|
+
event.subject.fy = event.y;
|
|
706
|
+
hoveredNodeRef.current = event.subject.id;
|
|
707
|
+
applyHighlight(event.subject.id);
|
|
708
|
+
}).on("end", (event) => {
|
|
709
|
+
if (!event.active) {
|
|
710
|
+
simulation.alphaTarget(0);
|
|
711
|
+
}
|
|
712
|
+
draggingNodeRef.current = null;
|
|
713
|
+
event.subject.fx = null;
|
|
714
|
+
event.subject.fy = null;
|
|
715
|
+
hoveredNodeRef.current = null;
|
|
716
|
+
applyHighlight(null);
|
|
717
|
+
});
|
|
718
|
+
mergedNodes.call(dragBehavior);
|
|
719
|
+
applyLabelFontSize(transformRef.current?.k ?? 1);
|
|
720
|
+
applyHighlight(hoveredNodeRef.current);
|
|
721
|
+
flushPendingFocus();
|
|
722
|
+
if (nodes.length === 0) {
|
|
723
|
+
autoZoomReadyRef.current = false;
|
|
724
|
+
autoZoomPendingRef.current = false;
|
|
725
|
+
cancelAutoZoomFrame();
|
|
726
|
+
} else {
|
|
727
|
+
const warmupIterations = Math.min(80, 16 + nodes.length * 2);
|
|
728
|
+
for (let i = 0; i < warmupIterations; i += 1) {
|
|
729
|
+
simulation.tick();
|
|
730
|
+
}
|
|
731
|
+
autoZoomReadyRef.current = true;
|
|
732
|
+
scheduleAutoZoom();
|
|
733
|
+
simulation.alpha(0.4).restart();
|
|
734
|
+
}
|
|
735
|
+
return () => {
|
|
736
|
+
simulation.stop();
|
|
737
|
+
};
|
|
738
|
+
}, [
|
|
739
|
+
dimensions.height,
|
|
740
|
+
dimensions.width,
|
|
741
|
+
handleNodeSelection,
|
|
742
|
+
linkData,
|
|
743
|
+
nodeData,
|
|
744
|
+
applyHighlight,
|
|
745
|
+
applyLabelFontSize,
|
|
746
|
+
flushPendingFocus,
|
|
747
|
+
focusNodeId,
|
|
748
|
+
cancelAutoZoomFrame,
|
|
749
|
+
scheduleAutoZoom
|
|
750
|
+
]);
|
|
751
|
+
useEffect(() => {
|
|
752
|
+
if (!svgRef.current) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const svg = select(svgRef.current);
|
|
756
|
+
svg.attr("width", dimensions.width || 600);
|
|
757
|
+
svg.attr("height", dimensions.height || 400);
|
|
758
|
+
if (simulationRef.current) {
|
|
759
|
+
const centerX = focusNodeId ? 0 : dimensions.width / 2;
|
|
760
|
+
const centerY = focusNodeId ? 0 : dimensions.height / 2 - 20;
|
|
761
|
+
simulationRef.current.force("center", forceCenter(centerX, centerY));
|
|
762
|
+
simulationRef.current.alpha(0.4).restart();
|
|
763
|
+
}
|
|
764
|
+
}, [dimensions.height, dimensions.width, focusNodeId]);
|
|
765
|
+
useEffect(() => {
|
|
766
|
+
if (!svgRef.current) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const svg = select(svgRef.current);
|
|
770
|
+
const viewport = svg.select("g[data-viewport]");
|
|
771
|
+
const behavior = zoom().scaleExtent(resolvedZoomExtent).on("zoom", (event) => {
|
|
772
|
+
transformRef.current = event.transform;
|
|
773
|
+
viewport.attr("transform", event.transform.toString());
|
|
774
|
+
applyLabelFontSize(event.transform.k);
|
|
775
|
+
});
|
|
776
|
+
zoomBehaviorRef.current = behavior;
|
|
777
|
+
svg.call(behavior);
|
|
778
|
+
return () => {
|
|
779
|
+
svg.on(".zoom", null);
|
|
780
|
+
};
|
|
781
|
+
}, [applyLabelFontSize, resolvedZoomExtent]);
|
|
782
|
+
useEffect(() => {
|
|
783
|
+
applyHighlight(hoveredNodeRef.current);
|
|
784
|
+
}, [applyHighlight]);
|
|
785
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: clsx(styles$f.root, className), "data-papyr-component": "GraphView", children: [
|
|
786
|
+
/* @__PURE__ */ jsx("svg", { ref: svgRef, className: styles$f.canvas, role: "img", "aria-label": "Publish graph", children: /* @__PURE__ */ jsxs("g", { "data-viewport": true, children: [
|
|
787
|
+
/* @__PURE__ */ jsx("g", { "data-links": true }),
|
|
788
|
+
/* @__PURE__ */ jsx("g", { "data-nodes": true }),
|
|
789
|
+
/* @__PURE__ */ jsx("g", { "data-labels": true })
|
|
790
|
+
] }) }),
|
|
791
|
+
!hasGraph && /* @__PURE__ */ jsx("div", { className: styles$f.emptyState, children: "No published links to explore yet." })
|
|
792
|
+
] });
|
|
793
|
+
};
|
|
794
|
+
const isBrowser = typeof document !== "undefined";
|
|
795
|
+
const GraphView = ({
|
|
796
|
+
graph,
|
|
797
|
+
className,
|
|
798
|
+
activeSlug = null,
|
|
799
|
+
filterNode,
|
|
800
|
+
focusPredicate,
|
|
801
|
+
nodeColor,
|
|
802
|
+
zoomExtent,
|
|
803
|
+
onNodeSelect,
|
|
804
|
+
showFullscreenToggle = true,
|
|
805
|
+
fullscreenTitle = "Full site graph"
|
|
806
|
+
}) => {
|
|
807
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
808
|
+
useEffect(() => {
|
|
809
|
+
if (!isFullscreen || !isBrowser) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const previous = document.body.style.overflow;
|
|
813
|
+
document.body.style.overflow = "hidden";
|
|
814
|
+
return () => {
|
|
815
|
+
document.body.style.overflow = previous;
|
|
816
|
+
};
|
|
817
|
+
}, [isFullscreen]);
|
|
818
|
+
const neighborhood = useMemo(() => {
|
|
819
|
+
if (!graph || !activeSlug) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
return getNeighborhood(graph, activeSlug);
|
|
823
|
+
}, [graph, activeSlug]);
|
|
824
|
+
const derivedFilter = useMemo(() => {
|
|
825
|
+
if (isFullscreen || !activeSlug || !neighborhood) {
|
|
826
|
+
return filterNode;
|
|
827
|
+
}
|
|
828
|
+
if (!filterNode) {
|
|
829
|
+
return (node) => neighborhood.has(node.id);
|
|
830
|
+
}
|
|
831
|
+
return (node) => filterNode(node) && neighborhood.has(node.id);
|
|
832
|
+
}, [filterNode, neighborhood, activeSlug, isFullscreen]);
|
|
833
|
+
const handleCloseFullscreen = useCallback(() => {
|
|
834
|
+
setIsFullscreen(false);
|
|
835
|
+
}, []);
|
|
836
|
+
const canToggleFullScreen = showFullscreenToggle && !!graph;
|
|
837
|
+
const mainGraph = /* @__PURE__ */ jsx(
|
|
838
|
+
GraphCanvas,
|
|
839
|
+
{
|
|
840
|
+
graph,
|
|
841
|
+
className,
|
|
842
|
+
filterNode: derivedFilter,
|
|
843
|
+
focusPredicate,
|
|
844
|
+
nodeColor,
|
|
845
|
+
zoomExtent,
|
|
846
|
+
focusNodeId: activeSlug ?? null,
|
|
847
|
+
onNodeSelect
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
851
|
+
/* @__PURE__ */ jsxs("div", { className: clsx(styles$f.shell, className), children: [
|
|
852
|
+
canToggleFullScreen && /* @__PURE__ */ jsx(
|
|
853
|
+
"button",
|
|
854
|
+
{
|
|
855
|
+
type: "button",
|
|
856
|
+
className: styles$f.fullscreenButton,
|
|
857
|
+
"aria-label": "Open full graph",
|
|
858
|
+
onClick: () => setIsFullscreen(true),
|
|
859
|
+
children: "⤢"
|
|
860
|
+
}
|
|
861
|
+
),
|
|
862
|
+
mainGraph
|
|
863
|
+
] }),
|
|
864
|
+
isFullscreen && isBrowser ? createPortal(
|
|
865
|
+
/* @__PURE__ */ jsxs("div", { className: styles$f.fullscreenOverlay, role: "dialog", "aria-modal": "true", children: [
|
|
866
|
+
/* @__PURE__ */ jsx("div", { className: styles$f.fullscreenBackdrop, onClick: handleCloseFullscreen }),
|
|
867
|
+
/* @__PURE__ */ jsxs("div", { className: styles$f.fullscreenDialog, children: [
|
|
868
|
+
/* @__PURE__ */ jsxs("header", { className: styles$f.fullscreenHeader, children: [
|
|
869
|
+
/* @__PURE__ */ jsx("span", { children: fullscreenTitle }),
|
|
870
|
+
/* @__PURE__ */ jsx(
|
|
871
|
+
"button",
|
|
872
|
+
{
|
|
873
|
+
type: "button",
|
|
874
|
+
className: styles$f.fullscreenClose,
|
|
875
|
+
onClick: handleCloseFullscreen,
|
|
876
|
+
children: "Close"
|
|
877
|
+
}
|
|
878
|
+
)
|
|
879
|
+
] }),
|
|
880
|
+
/* @__PURE__ */ jsx("div", { className: styles$f.fullscreenBody, children: /* @__PURE__ */ jsx(
|
|
881
|
+
GraphCanvas,
|
|
882
|
+
{
|
|
883
|
+
graph,
|
|
884
|
+
className: clsx(className, styles$f.fullscreenGraph),
|
|
885
|
+
filterNode,
|
|
886
|
+
focusPredicate,
|
|
887
|
+
nodeColor,
|
|
888
|
+
focusNodeId: activeSlug ?? null,
|
|
889
|
+
zoomExtent,
|
|
890
|
+
onNodeSelect
|
|
891
|
+
}
|
|
892
|
+
) })
|
|
893
|
+
] })
|
|
894
|
+
] }),
|
|
895
|
+
document.body
|
|
896
|
+
) : null
|
|
897
|
+
] });
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const frame = "_frame_187pj_1";
|
|
901
|
+
const header$2 = "_header_187pj_12";
|
|
902
|
+
const title$3 = "_title_187pj_19";
|
|
903
|
+
const subtitle = "_subtitle_187pj_27";
|
|
904
|
+
const badge = "_badge_187pj_34";
|
|
905
|
+
const canvas = "_canvas_187pj_48";
|
|
906
|
+
const glow = "_glow_187pj_56";
|
|
907
|
+
const edge = "_edge_187pj_60";
|
|
908
|
+
const node = "_node_187pj_65";
|
|
909
|
+
const emptyState$1 = "_emptyState_187pj_71";
|
|
910
|
+
const styles$e = {
|
|
911
|
+
frame: frame,
|
|
912
|
+
header: header$2,
|
|
913
|
+
title: title$3,
|
|
914
|
+
subtitle: subtitle,
|
|
915
|
+
badge: badge,
|
|
916
|
+
canvas: canvas,
|
|
917
|
+
glow: glow,
|
|
918
|
+
edge: edge,
|
|
919
|
+
node: node,
|
|
920
|
+
emptyState: emptyState$1
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const MiniGraph = ({ graph, className }) => {
|
|
924
|
+
const { nodes, edges, totalNodes, totalEdges } = useMemo(() => {
|
|
925
|
+
const MAX_NODES = 24;
|
|
926
|
+
const VIEWBOX_SIZE = 200;
|
|
927
|
+
const CENTER = VIEWBOX_SIZE / 2;
|
|
928
|
+
const graphNodes = Array.from(graph.nodes.values());
|
|
929
|
+
const sortedNodes = graphNodes.slice().sort((a, b) => (b.linkCount ?? 0) - (a.linkCount ?? 0)).slice(0, MAX_NODES);
|
|
930
|
+
const nodeIds = new Set(sortedNodes.map((node) => node.id));
|
|
931
|
+
const filteredEdges = graph.edges.filter(
|
|
932
|
+
(edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)
|
|
933
|
+
);
|
|
934
|
+
const maxLinks = Math.max(
|
|
935
|
+
1,
|
|
936
|
+
...sortedNodes.map((node) => node.linkCount ?? 0)
|
|
937
|
+
);
|
|
938
|
+
const orbitRadius = sortedNodes.length > 12 ? 70 : 62;
|
|
939
|
+
const positionedNodes = sortedNodes.map((node, index) => {
|
|
940
|
+
const angle = Math.PI * 2 * index / sortedNodes.length - Math.PI / 2;
|
|
941
|
+
const radius = 3 + (node.linkCount ?? 0) / maxLinks * 5;
|
|
942
|
+
return {
|
|
943
|
+
id: node.id,
|
|
944
|
+
label: node.label ?? node.id,
|
|
945
|
+
x: CENTER + orbitRadius * Math.cos(angle),
|
|
946
|
+
y: CENTER + orbitRadius * Math.sin(angle),
|
|
947
|
+
radius
|
|
948
|
+
};
|
|
949
|
+
});
|
|
950
|
+
const nodeLookup = new Map(positionedNodes.map((node) => [node.id, node]));
|
|
951
|
+
const positionedEdges = filteredEdges.map((edge) => ({
|
|
952
|
+
source: nodeLookup.get(edge.source) ?? null,
|
|
953
|
+
target: nodeLookup.get(edge.target) ?? null
|
|
954
|
+
})).filter((edge) => edge.source && edge.target).map((edge) => ({
|
|
955
|
+
source: edge.source,
|
|
956
|
+
target: edge.target
|
|
957
|
+
}));
|
|
958
|
+
return {
|
|
959
|
+
nodes: positionedNodes,
|
|
960
|
+
edges: positionedEdges,
|
|
961
|
+
totalNodes: graphNodes.length,
|
|
962
|
+
totalEdges: graph.edges.length
|
|
963
|
+
};
|
|
964
|
+
}, [graph]);
|
|
965
|
+
return /* @__PURE__ */ jsxs("div", { className: clsx(styles$e.frame, className), "data-papyr-component": "MiniGraph", children: [
|
|
966
|
+
/* @__PURE__ */ jsxs("div", { className: styles$e.header, children: [
|
|
967
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
968
|
+
/* @__PURE__ */ jsx("div", { className: styles$e.title, children: "Mini graph" }),
|
|
969
|
+
/* @__PURE__ */ jsxs("div", { className: styles$e.subtitle, children: [
|
|
970
|
+
totalNodes,
|
|
971
|
+
" notes • ",
|
|
972
|
+
totalEdges,
|
|
973
|
+
" links"
|
|
974
|
+
] })
|
|
975
|
+
] }),
|
|
976
|
+
/* @__PURE__ */ jsxs("span", { className: styles$e.badge, children: [
|
|
977
|
+
nodes.length,
|
|
978
|
+
"/",
|
|
979
|
+
totalNodes
|
|
980
|
+
] })
|
|
981
|
+
] }),
|
|
982
|
+
/* @__PURE__ */ jsxs(
|
|
983
|
+
"svg",
|
|
984
|
+
{
|
|
985
|
+
className: styles$e.canvas,
|
|
986
|
+
viewBox: "0 0 200 200",
|
|
987
|
+
role: "img",
|
|
988
|
+
"aria-label": `Mini graph with ${totalNodes} notes and ${totalEdges} links`,
|
|
989
|
+
children: [
|
|
990
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("radialGradient", { id: "papyr-mini-graph-glow", cx: "50%", cy: "45%", r: "60%", children: [
|
|
991
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", stopColor: "rgba(99, 102, 241, 0.35)" }),
|
|
992
|
+
/* @__PURE__ */ jsx("stop", { offset: "100%", stopColor: "rgba(99, 102, 241, 0)" })
|
|
993
|
+
] }) }),
|
|
994
|
+
/* @__PURE__ */ jsx(
|
|
995
|
+
"circle",
|
|
996
|
+
{
|
|
997
|
+
className: styles$e.glow,
|
|
998
|
+
cx: "100",
|
|
999
|
+
cy: "100",
|
|
1000
|
+
r: "78"
|
|
1001
|
+
}
|
|
1002
|
+
),
|
|
1003
|
+
/* @__PURE__ */ jsx("g", { className: styles$e.edges, children: edges.map((edge, index) => /* @__PURE__ */ jsx(
|
|
1004
|
+
"line",
|
|
1005
|
+
{
|
|
1006
|
+
className: styles$e.edge,
|
|
1007
|
+
x1: edge.source.x,
|
|
1008
|
+
y1: edge.source.y,
|
|
1009
|
+
x2: edge.target.x,
|
|
1010
|
+
y2: edge.target.y
|
|
1011
|
+
},
|
|
1012
|
+
`${edge.source.id}-${edge.target.id}-${index}`
|
|
1013
|
+
)) }),
|
|
1014
|
+
/* @__PURE__ */ jsx("g", { className: styles$e.nodes, children: nodes.map((node) => /* @__PURE__ */ jsx(
|
|
1015
|
+
"circle",
|
|
1016
|
+
{
|
|
1017
|
+
className: styles$e.node,
|
|
1018
|
+
cx: node.x,
|
|
1019
|
+
cy: node.y,
|
|
1020
|
+
r: node.radius,
|
|
1021
|
+
children: /* @__PURE__ */ jsx("title", { children: node.label })
|
|
1022
|
+
},
|
|
1023
|
+
node.id
|
|
1024
|
+
)) })
|
|
1025
|
+
]
|
|
1026
|
+
}
|
|
1027
|
+
),
|
|
1028
|
+
!nodes.length && /* @__PURE__ */ jsx("div", { className: styles$e.emptyState, children: "No connections yet." })
|
|
1029
|
+
] });
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
const container$8 = "_container_1vncr_1";
|
|
1033
|
+
const header$1 = "_header_1vncr_10";
|
|
1034
|
+
const breadcrumbs = "_breadcrumbs_1vncr_17";
|
|
1035
|
+
const title$2 = "_title_1vncr_21";
|
|
1036
|
+
const summary = "_summary_1vncr_26";
|
|
1037
|
+
const metaRow = "_metaRow_1vncr_33";
|
|
1038
|
+
const tagList = "_tagList_1vncr_43";
|
|
1039
|
+
const tag$2 = "_tag_1vncr_43";
|
|
1040
|
+
const tagEmpty = "_tagEmpty_1vncr_60";
|
|
1041
|
+
const metaList = "_metaList_1vncr_65";
|
|
1042
|
+
const metaItem = "_metaItem_1vncr_71";
|
|
1043
|
+
const content = "_content_1vncr_90";
|
|
1044
|
+
const codeBlockWrapper = "_codeBlockWrapper_1vncr_119";
|
|
1045
|
+
const codeBlockHeader = "_codeBlockHeader_1vncr_131";
|
|
1046
|
+
const codeBlockLanguage = "_codeBlockLanguage_1vncr_141";
|
|
1047
|
+
const copyButton = "_copyButton_1vncr_149";
|
|
1048
|
+
const codeBlockPre = "_codeBlockPre_1vncr_178";
|
|
1049
|
+
const codeBlockCode = "_codeBlockCode_1vncr_198";
|
|
1050
|
+
const linkedNotes = "_linkedNotes_1vncr_205";
|
|
1051
|
+
const linkedNotesHeader = "_linkedNotesHeader_1vncr_214";
|
|
1052
|
+
const linkedNotesTitle = "_linkedNotesTitle_1vncr_220";
|
|
1053
|
+
const linkedNotesCount = "_linkedNotesCount_1vncr_229";
|
|
1054
|
+
const linkedNotesList = "_linkedNotesList_1vncr_243";
|
|
1055
|
+
const linkedNotesItem = "_linkedNotesItem_1vncr_252";
|
|
1056
|
+
const linkedNoteLink = "_linkedNoteLink_1vncr_257";
|
|
1057
|
+
const styles$d = {
|
|
1058
|
+
container: container$8,
|
|
1059
|
+
header: header$1,
|
|
1060
|
+
breadcrumbs: breadcrumbs,
|
|
1061
|
+
title: title$2,
|
|
1062
|
+
summary: summary,
|
|
1063
|
+
metaRow: metaRow,
|
|
1064
|
+
tagList: tagList,
|
|
1065
|
+
tag: tag$2,
|
|
1066
|
+
tagEmpty: tagEmpty,
|
|
1067
|
+
metaList: metaList,
|
|
1068
|
+
metaItem: metaItem,
|
|
1069
|
+
content: content,
|
|
1070
|
+
codeBlockWrapper: codeBlockWrapper,
|
|
1071
|
+
codeBlockHeader: codeBlockHeader,
|
|
1072
|
+
codeBlockLanguage: codeBlockLanguage,
|
|
1073
|
+
copyButton: copyButton,
|
|
1074
|
+
codeBlockPre: codeBlockPre,
|
|
1075
|
+
codeBlockCode: codeBlockCode,
|
|
1076
|
+
linkedNotes: linkedNotes,
|
|
1077
|
+
linkedNotesHeader: linkedNotesHeader,
|
|
1078
|
+
linkedNotesTitle: linkedNotesTitle,
|
|
1079
|
+
linkedNotesCount: linkedNotesCount,
|
|
1080
|
+
linkedNotesList: linkedNotesList,
|
|
1081
|
+
linkedNotesItem: linkedNotesItem,
|
|
1082
|
+
linkedNoteLink: linkedNoteLink
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
function toForceGraphData(graph) {
|
|
1086
|
+
if (!graph) {
|
|
1087
|
+
return { nodes: [], links: [] };
|
|
1088
|
+
}
|
|
1089
|
+
const nodes = [];
|
|
1090
|
+
const links = [];
|
|
1091
|
+
for (const [, node] of graph.nodes) {
|
|
1092
|
+
nodes.push({
|
|
1093
|
+
id: node.id,
|
|
1094
|
+
label: node.label,
|
|
1095
|
+
value: node.linkCount
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
graph.edges.forEach((edge) => {
|
|
1099
|
+
links.push({
|
|
1100
|
+
source: edge.source,
|
|
1101
|
+
target: edge.target,
|
|
1102
|
+
value: 1
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
return { nodes, links };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function buildWikiLink(slug) {
|
|
1109
|
+
return `[[${slug}]]`;
|
|
1110
|
+
}
|
|
1111
|
+
function findNoteBySlug(notes, slug) {
|
|
1112
|
+
return notes.find((note) => note.slug === slug);
|
|
1113
|
+
}
|
|
1114
|
+
function parseNoteHash(hash) {
|
|
1115
|
+
if (!hash || !hash.startsWith("#")) {
|
|
1116
|
+
return { slug: null, anchor: null };
|
|
1117
|
+
}
|
|
1118
|
+
const fragment = hash.slice(1);
|
|
1119
|
+
if (!fragment) {
|
|
1120
|
+
return { slug: null, anchor: null };
|
|
1121
|
+
}
|
|
1122
|
+
if (fragment.startsWith("/note/")) {
|
|
1123
|
+
const rest = fragment.slice("/note/".length);
|
|
1124
|
+
const anchorIndex = rest.indexOf("#");
|
|
1125
|
+
const slugFragment = anchorIndex === -1 ? rest : rest.slice(0, anchorIndex);
|
|
1126
|
+
const anchorFragment = anchorIndex === -1 ? null : rest.slice(anchorIndex + 1);
|
|
1127
|
+
let slug = slugFragment || null;
|
|
1128
|
+
if (slug) {
|
|
1129
|
+
try {
|
|
1130
|
+
slug = decodeURIComponent(slug);
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
let anchor2 = anchorFragment && anchorFragment.length > 0 ? anchorFragment : null;
|
|
1135
|
+
if (anchor2) {
|
|
1136
|
+
try {
|
|
1137
|
+
anchor2 = decodeURIComponent(anchor2);
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return { slug, anchor: anchor2 };
|
|
1142
|
+
}
|
|
1143
|
+
if (fragment.startsWith("/")) {
|
|
1144
|
+
return { slug: null, anchor: null };
|
|
1145
|
+
}
|
|
1146
|
+
let anchor = fragment;
|
|
1147
|
+
if (anchor) {
|
|
1148
|
+
try {
|
|
1149
|
+
anchor = decodeURIComponent(anchor);
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return { slug: null, anchor };
|
|
1154
|
+
}
|
|
1155
|
+
function updateHashWithAnchor(anchor) {
|
|
1156
|
+
if (typeof window === "undefined" || typeof window.history === "undefined") {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const trimmed = anchor.trim();
|
|
1160
|
+
const currentHash = window.location.hash ?? "";
|
|
1161
|
+
const { slug } = parseNoteHash(currentHash);
|
|
1162
|
+
let nextHash;
|
|
1163
|
+
if (slug) {
|
|
1164
|
+
const encodedSlug = encodeURIComponent(slug);
|
|
1165
|
+
nextHash = trimmed ? `#/note/${encodedSlug}#${encodeURIComponent(trimmed)}` : `#/note/${encodedSlug}`;
|
|
1166
|
+
} else {
|
|
1167
|
+
nextHash = trimmed ? `#${encodeURIComponent(trimmed)}` : "#";
|
|
1168
|
+
}
|
|
1169
|
+
window.history.replaceState(null, "", nextHash);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function isHydratedSearchIndex(value) {
|
|
1173
|
+
const candidate = value;
|
|
1174
|
+
return Boolean(candidate.index && typeof candidate.index.search === "function");
|
|
1175
|
+
}
|
|
1176
|
+
function hydrateSearchIndex(value) {
|
|
1177
|
+
if (isHydratedSearchIndex(value)) {
|
|
1178
|
+
return value;
|
|
1179
|
+
}
|
|
1180
|
+
return importSearchIndex(value);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
const trigger = "_trigger_j6udx_1";
|
|
1184
|
+
const highlight$1 = "_highlight_j6udx_7";
|
|
1185
|
+
const panel = "_panel_j6udx_13";
|
|
1186
|
+
const styles$c = {
|
|
1187
|
+
trigger: trigger,
|
|
1188
|
+
highlight: highlight$1,
|
|
1189
|
+
panel: panel};
|
|
1190
|
+
|
|
1191
|
+
const HoverPreview = ({
|
|
1192
|
+
trigger,
|
|
1193
|
+
renderPreview,
|
|
1194
|
+
highlightTrigger = true
|
|
1195
|
+
}) => {
|
|
1196
|
+
const [open, setOpen] = useState(false);
|
|
1197
|
+
const { refs, floatingStyles } = useFloating({
|
|
1198
|
+
placement: "top",
|
|
1199
|
+
middleware: [offset(12), flip(), shift()],
|
|
1200
|
+
whileElementsMounted: autoUpdate
|
|
1201
|
+
});
|
|
1202
|
+
const handleKeyDown = useCallback((e) => {
|
|
1203
|
+
if (e.key === "Escape" && open) {
|
|
1204
|
+
setOpen(false);
|
|
1205
|
+
}
|
|
1206
|
+
}, [open]);
|
|
1207
|
+
return /* @__PURE__ */ jsxs(
|
|
1208
|
+
"span",
|
|
1209
|
+
{
|
|
1210
|
+
className: clsx(styles$c.trigger, highlightTrigger && styles$c.highlight),
|
|
1211
|
+
ref: refs.setReference,
|
|
1212
|
+
onMouseEnter: () => setOpen(true),
|
|
1213
|
+
onFocus: () => setOpen(true),
|
|
1214
|
+
onMouseLeave: () => setOpen(false),
|
|
1215
|
+
onBlur: () => setOpen(false),
|
|
1216
|
+
onKeyDown: handleKeyDown,
|
|
1217
|
+
"data-papyr-component": "HoverPreview",
|
|
1218
|
+
role: "button",
|
|
1219
|
+
"aria-haspopup": "dialog",
|
|
1220
|
+
"aria-expanded": open,
|
|
1221
|
+
children: [
|
|
1222
|
+
trigger,
|
|
1223
|
+
open ? /* @__PURE__ */ jsx(
|
|
1224
|
+
"div",
|
|
1225
|
+
{
|
|
1226
|
+
ref: refs.setFloating,
|
|
1227
|
+
style: floatingStyles,
|
|
1228
|
+
className: styles$c.panel,
|
|
1229
|
+
role: "dialog",
|
|
1230
|
+
"aria-modal": "false",
|
|
1231
|
+
"aria-label": "Preview",
|
|
1232
|
+
children: renderPreview()
|
|
1233
|
+
}
|
|
1234
|
+
) : null
|
|
1235
|
+
]
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
const container$7 = "_container_1raaj_1";
|
|
1241
|
+
const title$1 = "_title_1raaj_8";
|
|
1242
|
+
const excerpt = "_excerpt_1raaj_18";
|
|
1243
|
+
const empty = "_empty_1raaj_25";
|
|
1244
|
+
const tags = "_tags_1raaj_32";
|
|
1245
|
+
const tag$1 = "_tag_1raaj_32";
|
|
1246
|
+
const styles$b = {
|
|
1247
|
+
container: container$7,
|
|
1248
|
+
title: title$1,
|
|
1249
|
+
excerpt: excerpt,
|
|
1250
|
+
empty: empty,
|
|
1251
|
+
tags: tags,
|
|
1252
|
+
tag: tag$1
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
const MAX_PREVIEW_LENGTH = 220;
|
|
1256
|
+
const getPreviewText = (note) => {
|
|
1257
|
+
const source = (note.excerpt ?? note.description ?? "").trim();
|
|
1258
|
+
if (!source) {
|
|
1259
|
+
return "";
|
|
1260
|
+
}
|
|
1261
|
+
if (source.length <= MAX_PREVIEW_LENGTH) {
|
|
1262
|
+
return source;
|
|
1263
|
+
}
|
|
1264
|
+
return `${source.slice(0, MAX_PREVIEW_LENGTH - 1).trimEnd()}…`;
|
|
1265
|
+
};
|
|
1266
|
+
const NotePreviewContent = ({ note }) => {
|
|
1267
|
+
const previewText = getPreviewText(note);
|
|
1268
|
+
return /* @__PURE__ */ jsxs("div", { className: styles$b.container, children: [
|
|
1269
|
+
/* @__PURE__ */ jsx("h3", { className: styles$b.title, children: note.title ?? note.slug }),
|
|
1270
|
+
previewText ? /* @__PURE__ */ jsx("p", { className: styles$b.excerpt, children: previewText }) : /* @__PURE__ */ jsx("p", { className: styles$b.empty, children: "No preview available" }),
|
|
1271
|
+
note.tags?.length ? /* @__PURE__ */ jsx("ul", { className: styles$b.tags, role: "list", "aria-label": "Tags", children: note.tags.slice(0, 4).map((tag) => /* @__PURE__ */ jsxs("li", { className: styles$b.tag, children: [
|
|
1272
|
+
"#",
|
|
1273
|
+
tag
|
|
1274
|
+
] }, tag)) }) : null
|
|
1275
|
+
] });
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const list$1 = "_list_bm9au_1";
|
|
1279
|
+
const item$1 = "_item_bm9au_12";
|
|
1280
|
+
const separator = "_separator_bm9au_23";
|
|
1281
|
+
const link$1 = "_link_bm9au_27";
|
|
1282
|
+
const styles$a = {
|
|
1283
|
+
list: list$1,
|
|
1284
|
+
item: item$1,
|
|
1285
|
+
separator: separator,
|
|
1286
|
+
link: link$1
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
const Breadcrumbs = ({
|
|
1290
|
+
trail,
|
|
1291
|
+
className,
|
|
1292
|
+
separator = "›"
|
|
1293
|
+
}) => {
|
|
1294
|
+
if (!trail.length) {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
return /* @__PURE__ */ jsx("nav", { "aria-label": "Breadcrumb", "data-papyr-component": "Breadcrumbs", children: /* @__PURE__ */ jsx("ol", { className: clsx(styles$a.list, className), children: trail.map((item, index) => {
|
|
1298
|
+
const isLast = index === trail.length - 1;
|
|
1299
|
+
return /* @__PURE__ */ jsxs("li", { className: styles$a.item, "aria-current": isLast ? "page" : void 0, children: [
|
|
1300
|
+
item.href && !isLast ? /* @__PURE__ */ jsx("a", { className: styles$a.link, href: item.href, children: item.label }) : /* @__PURE__ */ jsx("span", { children: item.label }),
|
|
1301
|
+
!isLast && /* @__PURE__ */ jsx("span", { className: styles$a.separator, children: separator })
|
|
1302
|
+
] }, item.label);
|
|
1303
|
+
}) }) });
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
function useNotes(notes) {
|
|
1307
|
+
return useMemo(() => {
|
|
1308
|
+
return [...notes].sort((a, b) => a.title.localeCompare(b.title));
|
|
1309
|
+
}, [notes]);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const viewport = "_viewport_1tgm1_1";
|
|
1313
|
+
const toast = "_toast_1tgm1_13";
|
|
1314
|
+
const message = "_message_1tgm1_46";
|
|
1315
|
+
const closeButton = "_closeButton_1tgm1_52";
|
|
1316
|
+
const styles$9 = {
|
|
1317
|
+
viewport: viewport,
|
|
1318
|
+
toast: toast,
|
|
1319
|
+
message: message,
|
|
1320
|
+
closeButton: closeButton
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const ToastContainer = ({ toasts, onDismiss }) => {
|
|
1324
|
+
if (toasts.length === 0) {
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
return /* @__PURE__ */ jsx("div", { className: styles$9.viewport, role: "region", "aria-live": "polite", "aria-label": "Notifications", children: toasts.map((toast) => /* @__PURE__ */ jsxs("div", { className: styles$9.toast, role: "status", children: [
|
|
1328
|
+
/* @__PURE__ */ jsx("span", { className: styles$9.message, children: toast.message }),
|
|
1329
|
+
/* @__PURE__ */ jsx(
|
|
1330
|
+
"button",
|
|
1331
|
+
{
|
|
1332
|
+
type: "button",
|
|
1333
|
+
className: styles$9.closeButton,
|
|
1334
|
+
onClick: () => onDismiss(toast.id),
|
|
1335
|
+
"aria-label": "Dismiss notification",
|
|
1336
|
+
children: "Close"
|
|
1337
|
+
}
|
|
1338
|
+
)
|
|
1339
|
+
] }, toast.id)) });
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const PapyrContext = createContext(void 0);
|
|
1343
|
+
const PapyrProvider = ({
|
|
1344
|
+
notes,
|
|
1345
|
+
children,
|
|
1346
|
+
searchIndex,
|
|
1347
|
+
graph = null
|
|
1348
|
+
}) => {
|
|
1349
|
+
const sortedNotes = useNotes(notes);
|
|
1350
|
+
const memoizedGraph = useMemo(() => graph ?? null, [graph]);
|
|
1351
|
+
const [toasts, setToasts] = useState([]);
|
|
1352
|
+
const timersRef = useRef({});
|
|
1353
|
+
const dismissToast = useCallback((id) => {
|
|
1354
|
+
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
|
1355
|
+
if (typeof window !== "undefined") {
|
|
1356
|
+
const timerId = timersRef.current[id];
|
|
1357
|
+
if (typeof timerId === "number") {
|
|
1358
|
+
window.clearTimeout(timerId);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
delete timersRef.current[id];
|
|
1362
|
+
}, []);
|
|
1363
|
+
const showToast = useCallback(
|
|
1364
|
+
(message, options) => {
|
|
1365
|
+
const id = options?.id ?? `papyr-toast-${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
|
1366
|
+
const duration = options?.duration ?? 3e3;
|
|
1367
|
+
setToasts((prev) => [
|
|
1368
|
+
...prev.filter((toast) => toast.id !== id),
|
|
1369
|
+
{ id, message }
|
|
1370
|
+
]);
|
|
1371
|
+
if (typeof window !== "undefined" && duration > 0) {
|
|
1372
|
+
const existingTimeout = timersRef.current[id];
|
|
1373
|
+
if (typeof existingTimeout === "number") {
|
|
1374
|
+
window.clearTimeout(existingTimeout);
|
|
1375
|
+
}
|
|
1376
|
+
const timeoutId = window.setTimeout(() => dismissToast(id), duration);
|
|
1377
|
+
timersRef.current[id] = timeoutId;
|
|
1378
|
+
}
|
|
1379
|
+
return id;
|
|
1380
|
+
},
|
|
1381
|
+
[dismissToast]
|
|
1382
|
+
);
|
|
1383
|
+
const search = useCallback(
|
|
1384
|
+
(query, options) => {
|
|
1385
|
+
return searchNotes(query, searchIndex, options);
|
|
1386
|
+
},
|
|
1387
|
+
[searchIndex]
|
|
1388
|
+
);
|
|
1389
|
+
useEffect(() => {
|
|
1390
|
+
return () => {
|
|
1391
|
+
if (typeof window !== "undefined") {
|
|
1392
|
+
Object.values(timersRef.current).forEach((timerId) => {
|
|
1393
|
+
if (typeof timerId === "number") {
|
|
1394
|
+
window.clearTimeout(timerId);
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
timersRef.current = {};
|
|
1399
|
+
};
|
|
1400
|
+
}, []);
|
|
1401
|
+
const value = useMemo(
|
|
1402
|
+
() => ({
|
|
1403
|
+
notes: sortedNotes,
|
|
1404
|
+
graph: memoizedGraph,
|
|
1405
|
+
searchIndex,
|
|
1406
|
+
search,
|
|
1407
|
+
showToast,
|
|
1408
|
+
dismissToast
|
|
1409
|
+
}),
|
|
1410
|
+
[sortedNotes, memoizedGraph, searchIndex, search, showToast, dismissToast]
|
|
1411
|
+
);
|
|
1412
|
+
return /* @__PURE__ */ jsxs(PapyrContext.Provider, { value, children: [
|
|
1413
|
+
children,
|
|
1414
|
+
/* @__PURE__ */ jsx(ToastContainer, { toasts, onDismiss: dismissToast })
|
|
1415
|
+
] });
|
|
1416
|
+
};
|
|
1417
|
+
function usePapyr() {
|
|
1418
|
+
const context = useContext(PapyrContext);
|
|
1419
|
+
if (!context) {
|
|
1420
|
+
throw new Error("usePapyr must be used within a PapyrProvider");
|
|
1421
|
+
}
|
|
1422
|
+
return context;
|
|
1423
|
+
}
|
|
1424
|
+
function useOptionalPapyr() {
|
|
1425
|
+
return useContext(PapyrContext);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const NOTE_LINK_PREFIX = "#/note/";
|
|
1429
|
+
const buildNoteHash = (slug, fragment) => {
|
|
1430
|
+
const base = `${NOTE_LINK_PREFIX}${encodeURIComponent(slug)}`;
|
|
1431
|
+
return fragment && fragment.length > 0 ? `${base}#${fragment}` : base;
|
|
1432
|
+
};
|
|
1433
|
+
const cloneElementAttributes = (element, exclude = []) => {
|
|
1434
|
+
const excludeSet = new Set(exclude);
|
|
1435
|
+
return Array.from(element.attributes).filter((attr) => !excludeSet.has(attr.name)).map((attr) => ({ name: attr.name, value: attr.value }));
|
|
1436
|
+
};
|
|
1437
|
+
const InlinePreviewAnchor = ({
|
|
1438
|
+
attributes,
|
|
1439
|
+
href,
|
|
1440
|
+
innerHtml,
|
|
1441
|
+
onClick
|
|
1442
|
+
}) => {
|
|
1443
|
+
const anchorRef = useRef(null);
|
|
1444
|
+
useLayoutEffect(() => {
|
|
1445
|
+
const element = anchorRef.current;
|
|
1446
|
+
if (!element) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
attributes.forEach((attr) => {
|
|
1450
|
+
if (attr.name === "href") {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
element.setAttribute(attr.name, attr.value);
|
|
1454
|
+
});
|
|
1455
|
+
element.setAttribute("href", href);
|
|
1456
|
+
}, [attributes, href]);
|
|
1457
|
+
return /* @__PURE__ */ jsx(
|
|
1458
|
+
"a",
|
|
1459
|
+
{
|
|
1460
|
+
ref: anchorRef,
|
|
1461
|
+
href,
|
|
1462
|
+
onClick,
|
|
1463
|
+
dangerouslySetInnerHTML: { __html: innerHtml }
|
|
1464
|
+
}
|
|
1465
|
+
);
|
|
1466
|
+
};
|
|
1467
|
+
const safeDecode = (value) => {
|
|
1468
|
+
try {
|
|
1469
|
+
return decodeURIComponent(value);
|
|
1470
|
+
} catch {
|
|
1471
|
+
return value;
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
const NoteViewer = ({ note, emptyState, onNavigateNote }) => {
|
|
1475
|
+
const contentRef = useRef(null);
|
|
1476
|
+
const inlinePreviewCleanupRef = useRef([]);
|
|
1477
|
+
const papyr = useOptionalPapyr();
|
|
1478
|
+
const providerNotes = papyr?.notes ?? [];
|
|
1479
|
+
const linkedNotesHeadingId = note ? `papyr-linked-notes-${note.slug}` : "papyr-linked-notes";
|
|
1480
|
+
const metadataRecord = note?.metadata ?? {};
|
|
1481
|
+
const noteLookup = useMemo(() => {
|
|
1482
|
+
if (!providerNotes.length) {
|
|
1483
|
+
return {};
|
|
1484
|
+
}
|
|
1485
|
+
return providerNotes.reduce((acc, current) => {
|
|
1486
|
+
acc[current.slug] = current;
|
|
1487
|
+
return acc;
|
|
1488
|
+
}, {});
|
|
1489
|
+
}, [providerNotes]);
|
|
1490
|
+
const resolveLinkedSlug = useCallback(
|
|
1491
|
+
(rawTarget) => {
|
|
1492
|
+
if (!rawTarget) {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
const baseTarget = rawTarget.split("#")[0];
|
|
1496
|
+
if (noteLookup[baseTarget]) {
|
|
1497
|
+
return baseTarget;
|
|
1498
|
+
}
|
|
1499
|
+
return null;
|
|
1500
|
+
},
|
|
1501
|
+
[noteLookup]
|
|
1502
|
+
);
|
|
1503
|
+
const getPreviewData = useCallback((targetNote) => ({
|
|
1504
|
+
slug: targetNote.slug,
|
|
1505
|
+
title: targetNote.title,
|
|
1506
|
+
excerpt: targetNote.excerpt ?? null,
|
|
1507
|
+
description: targetNote.description ?? null,
|
|
1508
|
+
tags: targetNote.tags ?? []
|
|
1509
|
+
}), []);
|
|
1510
|
+
const linkedNotes = useMemo(() => {
|
|
1511
|
+
if (!note?.linksTo?.length) {
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1515
|
+
const previews = [];
|
|
1516
|
+
note.linksTo.forEach((target) => {
|
|
1517
|
+
const resolvedSlug = resolveLinkedSlug(target);
|
|
1518
|
+
if (!resolvedSlug || seen.has(resolvedSlug)) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const targetNote = noteLookup[resolvedSlug];
|
|
1522
|
+
if (!targetNote) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
seen.add(resolvedSlug);
|
|
1526
|
+
previews.push(getPreviewData(targetNote));
|
|
1527
|
+
});
|
|
1528
|
+
return previews;
|
|
1529
|
+
}, [note?.linksTo, noteLookup, resolveLinkedSlug, getPreviewData]);
|
|
1530
|
+
const breadcrumbTrail = useMemo(() => {
|
|
1531
|
+
if (!note) {
|
|
1532
|
+
return [];
|
|
1533
|
+
}
|
|
1534
|
+
const rawTrail = metadataRecord.breadcrumbs;
|
|
1535
|
+
if (Array.isArray(rawTrail)) {
|
|
1536
|
+
const normalized = rawTrail.map((item) => {
|
|
1537
|
+
if (typeof item === "string") {
|
|
1538
|
+
return { label: item };
|
|
1539
|
+
}
|
|
1540
|
+
if (item && typeof item === "object") {
|
|
1541
|
+
const label = "label" in item ? String(item.label ?? "") : "";
|
|
1542
|
+
if (!label) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
const href = typeof item.href === "string" ? item.href : void 0;
|
|
1546
|
+
return { label, href };
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
}).filter((item) => Boolean(item));
|
|
1550
|
+
if (normalized.length) {
|
|
1551
|
+
return normalized;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const pathSource = typeof metadataRecord.path === "string" ? metadataRecord.path : typeof metadataRecord.filePath === "string" ? metadataRecord.filePath : typeof metadataRecord.relativePath === "string" ? metadataRecord.relativePath : null;
|
|
1555
|
+
if (!pathSource) {
|
|
1556
|
+
return [];
|
|
1557
|
+
}
|
|
1558
|
+
const normalizedPath = pathSource.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1559
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
1560
|
+
if (!segments.length) {
|
|
1561
|
+
return [];
|
|
1562
|
+
}
|
|
1563
|
+
return segments.map((segment, index) => {
|
|
1564
|
+
const isLast = index === segments.length - 1;
|
|
1565
|
+
const stripped = isLast ? segment.replace(/\.(md|markdown)$/i, "") : segment;
|
|
1566
|
+
const label = stripped.replace(/[-_]/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1567
|
+
return { label };
|
|
1568
|
+
});
|
|
1569
|
+
}, [metadataRecord, note]);
|
|
1570
|
+
const metaItems = useMemo(() => {
|
|
1571
|
+
if (!note) {
|
|
1572
|
+
return [];
|
|
1573
|
+
}
|
|
1574
|
+
const items = [];
|
|
1575
|
+
if (note.createdAt) {
|
|
1576
|
+
items.push({ label: "Created", value: note.createdAt });
|
|
1577
|
+
}
|
|
1578
|
+
if (note.updatedAt) {
|
|
1579
|
+
items.push({ label: "Updated", value: note.updatedAt });
|
|
1580
|
+
}
|
|
1581
|
+
if (typeof note.wordCount === "number" && note.wordCount > 0) {
|
|
1582
|
+
items.push({ label: "Words", value: String(note.wordCount) });
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof note.readingTime === "number" && note.readingTime > 0) {
|
|
1585
|
+
items.push({ label: "Read", value: `${note.readingTime} min` });
|
|
1586
|
+
}
|
|
1587
|
+
return items;
|
|
1588
|
+
}, [note]);
|
|
1589
|
+
const dispatchHashChange = useCallback(() => {
|
|
1590
|
+
if (typeof window === "undefined") {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const event = typeof window.HashChangeEvent === "function" ? new HashChangeEvent("hashchange") : new Event("hashchange");
|
|
1594
|
+
window.dispatchEvent(event);
|
|
1595
|
+
}, []);
|
|
1596
|
+
const handleNoteLinkNavigation = useCallback(
|
|
1597
|
+
(event, slug, fragment) => {
|
|
1598
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.currentTarget?.target && event.currentTarget.target !== "_self") {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
event.preventDefault();
|
|
1602
|
+
if (typeof window === "undefined") {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (onNavigateNote) {
|
|
1606
|
+
onNavigateNote(slug, fragment ? { fragment } : void 0);
|
|
1607
|
+
}
|
|
1608
|
+
const targetHash = buildNoteHash(slug, fragment);
|
|
1609
|
+
if (window.location.hash !== targetHash) {
|
|
1610
|
+
window.history.pushState(null, "", targetHash);
|
|
1611
|
+
}
|
|
1612
|
+
dispatchHashChange();
|
|
1613
|
+
},
|
|
1614
|
+
[dispatchHashChange, onNavigateNote]
|
|
1615
|
+
);
|
|
1616
|
+
useEffect(() => {
|
|
1617
|
+
const container = contentRef.current;
|
|
1618
|
+
if (!container || !note) {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const cleanupFns = [];
|
|
1622
|
+
const codeBlocks = Array.from(container.querySelectorAll("pre"));
|
|
1623
|
+
codeBlocks.forEach((pre) => {
|
|
1624
|
+
if (pre.dataset.papyrEnhanced === "true") {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const code = pre.querySelector("code");
|
|
1628
|
+
if (!code) {
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const wrapper = document.createElement("div");
|
|
1632
|
+
wrapper.classList.add(styles$d.codeBlockWrapper);
|
|
1633
|
+
wrapper.setAttribute("data-papyr-code-wrapper", "true");
|
|
1634
|
+
const header = document.createElement("div");
|
|
1635
|
+
header.classList.add(styles$d.codeBlockHeader);
|
|
1636
|
+
const languageClass = Array.from(code.classList).find((cls) => cls.startsWith("language-"));
|
|
1637
|
+
const detectedLanguage = languageClass ? languageClass.replace("language-", "") : code.getAttribute("data-language") || void 0;
|
|
1638
|
+
const language = detectedLanguage && detectedLanguage.trim() || "code";
|
|
1639
|
+
const label = document.createElement("span");
|
|
1640
|
+
label.classList.add(styles$d.codeBlockLanguage);
|
|
1641
|
+
label.textContent = language.toUpperCase();
|
|
1642
|
+
header.appendChild(label);
|
|
1643
|
+
const button = document.createElement("button");
|
|
1644
|
+
button.type = "button";
|
|
1645
|
+
button.classList.add(styles$d.copyButton);
|
|
1646
|
+
button.setAttribute("data-papyr-copy-button", "true");
|
|
1647
|
+
button.setAttribute("aria-label", "Copy code block");
|
|
1648
|
+
button.title = "Copy code";
|
|
1649
|
+
let resetTimer;
|
|
1650
|
+
const setButtonState = (state, autoReset) => {
|
|
1651
|
+
window.clearTimeout(resetTimer);
|
|
1652
|
+
button.dataset.state = state;
|
|
1653
|
+
switch (state) {
|
|
1654
|
+
case "idle":
|
|
1655
|
+
button.textContent = "Copy";
|
|
1656
|
+
break;
|
|
1657
|
+
case "copied":
|
|
1658
|
+
button.textContent = "Copied ✓";
|
|
1659
|
+
if (autoReset) {
|
|
1660
|
+
resetTimer = window.setTimeout(() => setButtonState("idle", false), 2e3);
|
|
1661
|
+
}
|
|
1662
|
+
break;
|
|
1663
|
+
case "empty":
|
|
1664
|
+
button.textContent = "Empty";
|
|
1665
|
+
if (autoReset) {
|
|
1666
|
+
resetTimer = window.setTimeout(() => setButtonState("idle", false), 1500);
|
|
1667
|
+
}
|
|
1668
|
+
break;
|
|
1669
|
+
case "error":
|
|
1670
|
+
button.textContent = "Copy Failed";
|
|
1671
|
+
if (autoReset) {
|
|
1672
|
+
resetTimer = window.setTimeout(() => setButtonState("idle", false), 2e3);
|
|
1673
|
+
}
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
const fallbackCopy = (text) => {
|
|
1678
|
+
const textarea = document.createElement("textarea");
|
|
1679
|
+
textarea.value = text;
|
|
1680
|
+
textarea.setAttribute("readonly", "");
|
|
1681
|
+
textarea.style.position = "absolute";
|
|
1682
|
+
textarea.style.left = "-9999px";
|
|
1683
|
+
textarea.style.top = "0";
|
|
1684
|
+
textarea.style.opacity = "0";
|
|
1685
|
+
document.body.appendChild(textarea);
|
|
1686
|
+
try {
|
|
1687
|
+
textarea.focus({ preventScroll: true });
|
|
1688
|
+
} catch (focusError) {
|
|
1689
|
+
textarea.focus();
|
|
1690
|
+
}
|
|
1691
|
+
textarea.select();
|
|
1692
|
+
try {
|
|
1693
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
1694
|
+
} catch (selectionError) {
|
|
1695
|
+
}
|
|
1696
|
+
const successful = document.execCommand("copy");
|
|
1697
|
+
document.body.removeChild(textarea);
|
|
1698
|
+
if (!successful) {
|
|
1699
|
+
throw new Error("execCommand returned false");
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
const handleCopy = async (event) => {
|
|
1703
|
+
event.preventDefault();
|
|
1704
|
+
event.stopPropagation();
|
|
1705
|
+
const text = (code.innerText ?? code.textContent ?? "").replace(/\u00a0/g, " ");
|
|
1706
|
+
if (!text.trim()) {
|
|
1707
|
+
setButtonState("empty", true);
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
try {
|
|
1711
|
+
if (navigator.clipboard?.writeText) {
|
|
1712
|
+
await navigator.clipboard.writeText(text);
|
|
1713
|
+
} else {
|
|
1714
|
+
fallbackCopy(text);
|
|
1715
|
+
}
|
|
1716
|
+
setButtonState("copied", true);
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
console.warn("Failed to copy code block", error);
|
|
1719
|
+
try {
|
|
1720
|
+
fallbackCopy(text);
|
|
1721
|
+
setButtonState("copied", true);
|
|
1722
|
+
} catch (fallbackError) {
|
|
1723
|
+
console.warn("Fallback copy failed", fallbackError);
|
|
1724
|
+
setButtonState("error", true);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
setButtonState("idle", false);
|
|
1729
|
+
button.addEventListener("click", handleCopy);
|
|
1730
|
+
cleanupFns.push(() => {
|
|
1731
|
+
button.removeEventListener("click", handleCopy);
|
|
1732
|
+
window.clearTimeout(resetTimer);
|
|
1733
|
+
});
|
|
1734
|
+
header.appendChild(button);
|
|
1735
|
+
pre.parentElement?.insertBefore(wrapper, pre);
|
|
1736
|
+
wrapper.appendChild(header);
|
|
1737
|
+
wrapper.appendChild(pre);
|
|
1738
|
+
pre.classList.add(styles$d.codeBlockPre);
|
|
1739
|
+
code.classList.add(styles$d.codeBlockCode);
|
|
1740
|
+
pre.dataset.papyrEnhanced = "true";
|
|
1741
|
+
});
|
|
1742
|
+
const anchorLinks = Array.from(container.querySelectorAll("a"));
|
|
1743
|
+
const handleAnchorClick = (event) => {
|
|
1744
|
+
const anchor = event.currentTarget;
|
|
1745
|
+
const href = anchor.getAttribute("href") ?? "";
|
|
1746
|
+
if (!href.startsWith("#") || href.startsWith("#/")) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
const targetId = href.slice(1);
|
|
1750
|
+
if (!targetId) {
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
event.preventDefault();
|
|
1754
|
+
const targetElement = document.getElementById(targetId);
|
|
1755
|
+
if (targetElement) {
|
|
1756
|
+
targetElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1757
|
+
}
|
|
1758
|
+
updateHashWithAnchor(targetId);
|
|
1759
|
+
};
|
|
1760
|
+
anchorLinks.forEach((link) => {
|
|
1761
|
+
const href = link.getAttribute("href") ?? "";
|
|
1762
|
+
if (href.startsWith("#") && !href.startsWith("#/")) {
|
|
1763
|
+
link.addEventListener("click", handleAnchorClick);
|
|
1764
|
+
cleanupFns.push(() => {
|
|
1765
|
+
link.removeEventListener("click", handleAnchorClick);
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
if (typeof window !== "undefined") {
|
|
1770
|
+
const hash = window.location.hash ?? "";
|
|
1771
|
+
const { slug: hashSlug, anchor: hashAnchor } = parseNoteHash(hash);
|
|
1772
|
+
if (hashAnchor && (!hashSlug || hashSlug === note.slug)) {
|
|
1773
|
+
const targetElement = document.getElementById(hashAnchor);
|
|
1774
|
+
if (targetElement) {
|
|
1775
|
+
window.requestAnimationFrame(() => {
|
|
1776
|
+
targetElement.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return () => {
|
|
1782
|
+
cleanupFns.forEach((fn) => fn());
|
|
1783
|
+
};
|
|
1784
|
+
}, [note?.slug, note?.html]);
|
|
1785
|
+
useEffect(() => {
|
|
1786
|
+
if (typeof window === "undefined") {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
inlinePreviewCleanupRef.current.forEach((cleanup) => cleanup());
|
|
1790
|
+
inlinePreviewCleanupRef.current = [];
|
|
1791
|
+
const container = contentRef.current;
|
|
1792
|
+
if (!container || !note) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
const inlineLinks = Array.from(
|
|
1796
|
+
container.querySelectorAll('a[href^="#/note/"]')
|
|
1797
|
+
);
|
|
1798
|
+
inlineLinks.forEach((anchor) => {
|
|
1799
|
+
const href = anchor.getAttribute("href") ?? "";
|
|
1800
|
+
if (!href.startsWith(NOTE_LINK_PREFIX)) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
if (anchor.closest('[data-papyr-inline-preview="true"]')) {
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const rawTarget = href.slice(NOTE_LINK_PREFIX.length);
|
|
1807
|
+
const [rawSlugPart, ...fragmentParts] = rawTarget.split("#");
|
|
1808
|
+
const fragment = fragmentParts.length > 0 ? fragmentParts.join("#").trim() || void 0 : void 0;
|
|
1809
|
+
const decodedTarget = safeDecode(rawSlugPart);
|
|
1810
|
+
const resolvedSlug = resolveLinkedSlug(decodedTarget);
|
|
1811
|
+
if (!resolvedSlug) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const targetNote = noteLookup[resolvedSlug];
|
|
1815
|
+
if (!targetNote) {
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const parent = anchor.parentElement;
|
|
1819
|
+
if (!parent) {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const wrapper = document.createElement("span");
|
|
1823
|
+
wrapper.setAttribute("data-papyr-inline-preview", "true");
|
|
1824
|
+
parent.replaceChild(wrapper, anchor);
|
|
1825
|
+
const anchorHtml = anchor.innerHTML;
|
|
1826
|
+
const preservedAttributes = cloneElementAttributes(anchor, ["href"]);
|
|
1827
|
+
const previewData = getPreviewData(targetNote);
|
|
1828
|
+
const noteHref = buildNoteHash(resolvedSlug, fragment);
|
|
1829
|
+
const root = createRoot(wrapper);
|
|
1830
|
+
root.render(
|
|
1831
|
+
/* @__PURE__ */ jsx(
|
|
1832
|
+
HoverPreview,
|
|
1833
|
+
{
|
|
1834
|
+
highlightTrigger: false,
|
|
1835
|
+
trigger: /* @__PURE__ */ jsx(
|
|
1836
|
+
InlinePreviewAnchor,
|
|
1837
|
+
{
|
|
1838
|
+
attributes: preservedAttributes,
|
|
1839
|
+
href: noteHref,
|
|
1840
|
+
innerHtml: anchorHtml,
|
|
1841
|
+
onClick: (event) => handleNoteLinkNavigation(event, resolvedSlug, fragment)
|
|
1842
|
+
}
|
|
1843
|
+
),
|
|
1844
|
+
renderPreview: () => /* @__PURE__ */ jsx(NotePreviewContent, { note: previewData })
|
|
1845
|
+
}
|
|
1846
|
+
)
|
|
1847
|
+
);
|
|
1848
|
+
inlinePreviewCleanupRef.current.push(() => {
|
|
1849
|
+
root.unmount();
|
|
1850
|
+
wrapper.replaceWith(anchor);
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
return () => {
|
|
1854
|
+
inlinePreviewCleanupRef.current.forEach((cleanup) => cleanup());
|
|
1855
|
+
inlinePreviewCleanupRef.current = [];
|
|
1856
|
+
};
|
|
1857
|
+
}, [
|
|
1858
|
+
note?.slug,
|
|
1859
|
+
note?.html,
|
|
1860
|
+
resolveLinkedSlug,
|
|
1861
|
+
noteLookup,
|
|
1862
|
+
handleNoteLinkNavigation,
|
|
1863
|
+
getPreviewData
|
|
1864
|
+
]);
|
|
1865
|
+
const hasLinkedNotes = linkedNotes.length > 0;
|
|
1866
|
+
if (!note) {
|
|
1867
|
+
return emptyState ? /* @__PURE__ */ jsx(Fragment, { children: emptyState }) : null;
|
|
1868
|
+
}
|
|
1869
|
+
return /* @__PURE__ */ jsxs("article", { className: styles$d.container, "data-papyr-component": "NoteViewer", children: [
|
|
1870
|
+
/* @__PURE__ */ jsxs("header", { className: styles$d.header, children: [
|
|
1871
|
+
breadcrumbTrail.length > 0 && /* @__PURE__ */ jsx(Breadcrumbs, { trail: breadcrumbTrail, className: styles$d.breadcrumbs }),
|
|
1872
|
+
/* @__PURE__ */ jsx("h1", { className: styles$d.title, children: note.title }),
|
|
1873
|
+
note.description && /* @__PURE__ */ jsx("p", { className: styles$d.summary, children: note.description }),
|
|
1874
|
+
/* @__PURE__ */ jsxs("div", { className: styles$d.metaRow, children: [
|
|
1875
|
+
/* @__PURE__ */ jsx("div", { className: styles$d.tagList, children: note.tags?.length ? note.tags.map((tag) => /* @__PURE__ */ jsx("span", { className: styles$d.tag, children: tag }, tag)) : /* @__PURE__ */ jsx("span", { className: styles$d.tagEmpty, children: "No tags assigned" }) }),
|
|
1876
|
+
metaItems.length > 0 && /* @__PURE__ */ jsx("dl", { className: styles$d.metaList, children: metaItems.map((item) => /* @__PURE__ */ jsxs("div", { className: styles$d.metaItem, children: [
|
|
1877
|
+
/* @__PURE__ */ jsx("dt", { children: item.label }),
|
|
1878
|
+
/* @__PURE__ */ jsx("dd", { children: item.value })
|
|
1879
|
+
] }, item.label)) })
|
|
1880
|
+
] })
|
|
1881
|
+
] }),
|
|
1882
|
+
/* @__PURE__ */ jsx(
|
|
1883
|
+
"section",
|
|
1884
|
+
{
|
|
1885
|
+
ref: contentRef,
|
|
1886
|
+
className: styles$d.content,
|
|
1887
|
+
dangerouslySetInnerHTML: { __html: note.html }
|
|
1888
|
+
}
|
|
1889
|
+
),
|
|
1890
|
+
hasLinkedNotes ? /* @__PURE__ */ jsxs(
|
|
1891
|
+
"section",
|
|
1892
|
+
{
|
|
1893
|
+
className: styles$d.linkedNotes,
|
|
1894
|
+
"aria-labelledby": linkedNotesHeadingId,
|
|
1895
|
+
"data-papyr-component": "LinkedNotesPreview",
|
|
1896
|
+
children: [
|
|
1897
|
+
/* @__PURE__ */ jsxs("div", { className: styles$d.linkedNotesHeader, children: [
|
|
1898
|
+
/* @__PURE__ */ jsx("h2", { id: linkedNotesHeadingId, className: styles$d.linkedNotesTitle, children: "Linked notes" }),
|
|
1899
|
+
/* @__PURE__ */ jsx("span", { className: styles$d.linkedNotesCount, children: linkedNotes.length })
|
|
1900
|
+
] }),
|
|
1901
|
+
/* @__PURE__ */ jsx("ul", { className: styles$d.linkedNotesList, children: linkedNotes.map((related) => /* @__PURE__ */ jsx("li", { className: styles$d.linkedNotesItem, children: /* @__PURE__ */ jsx(
|
|
1902
|
+
HoverPreview,
|
|
1903
|
+
{
|
|
1904
|
+
highlightTrigger: false,
|
|
1905
|
+
trigger: /* @__PURE__ */ jsx(
|
|
1906
|
+
"a",
|
|
1907
|
+
{
|
|
1908
|
+
href: buildNoteHash(related.slug),
|
|
1909
|
+
className: styles$d.linkedNoteLink,
|
|
1910
|
+
onClick: (event) => handleNoteLinkNavigation(event, related.slug),
|
|
1911
|
+
children: related.title ?? related.slug
|
|
1912
|
+
}
|
|
1913
|
+
),
|
|
1914
|
+
renderPreview: () => /* @__PURE__ */ jsx(NotePreviewContent, { note: related })
|
|
1915
|
+
}
|
|
1916
|
+
) }, related.slug)) })
|
|
1917
|
+
]
|
|
1918
|
+
}
|
|
1919
|
+
) : null
|
|
1920
|
+
] });
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
const wrapper = "_wrapper_1suuy_1";
|
|
1924
|
+
const input$2 = "_input_1suuy_6";
|
|
1925
|
+
const styles$8 = {
|
|
1926
|
+
wrapper: wrapper,
|
|
1927
|
+
input: input$2
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1930
|
+
const SearchBar = ({
|
|
1931
|
+
query,
|
|
1932
|
+
onQueryChange,
|
|
1933
|
+
placeholder = "Search notes…",
|
|
1934
|
+
className
|
|
1935
|
+
}) => {
|
|
1936
|
+
const handleChange = (event) => {
|
|
1937
|
+
onQueryChange(event.target.value);
|
|
1938
|
+
};
|
|
1939
|
+
return /* @__PURE__ */ jsx("div", { className: clsx(styles$8.wrapper, className), "data-papyr-component": "SearchBar", children: /* @__PURE__ */ jsx(
|
|
1940
|
+
"input",
|
|
1941
|
+
{
|
|
1942
|
+
className: styles$8.input,
|
|
1943
|
+
value: query,
|
|
1944
|
+
onChange: handleChange,
|
|
1945
|
+
placeholder,
|
|
1946
|
+
type: "search",
|
|
1947
|
+
autoComplete: "off",
|
|
1948
|
+
"aria-label": "Search notes"
|
|
1949
|
+
}
|
|
1950
|
+
) });
|
|
1951
|
+
};
|
|
1952
|
+
|
|
1953
|
+
const container$6 = "_container_r50hu_1";
|
|
1954
|
+
const tag = "_tag_r50hu_7";
|
|
1955
|
+
const tagActive = "_tagActive_r50hu_30";
|
|
1956
|
+
const styles$7 = {
|
|
1957
|
+
container: container$6,
|
|
1958
|
+
tag: tag,
|
|
1959
|
+
tagActive: tagActive
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
const TagFilter = ({
|
|
1963
|
+
availableTags,
|
|
1964
|
+
selectedTags,
|
|
1965
|
+
onChange,
|
|
1966
|
+
className
|
|
1967
|
+
}) => {
|
|
1968
|
+
const toggle = (tag) => {
|
|
1969
|
+
if (selectedTags.includes(tag)) {
|
|
1970
|
+
onChange(selectedTags.filter((existing) => existing !== tag));
|
|
1971
|
+
} else {
|
|
1972
|
+
onChange([...selectedTags, tag]);
|
|
1973
|
+
}
|
|
1974
|
+
};
|
|
1975
|
+
if (!availableTags.length) {
|
|
1976
|
+
return null;
|
|
1977
|
+
}
|
|
1978
|
+
return /* @__PURE__ */ jsx(
|
|
1979
|
+
"div",
|
|
1980
|
+
{
|
|
1981
|
+
className: clsx(styles$7.container, className),
|
|
1982
|
+
"data-papyr-component": "TagFilter",
|
|
1983
|
+
role: "group",
|
|
1984
|
+
"aria-label": "Filter by tags",
|
|
1985
|
+
children: availableTags.map((tag) => /* @__PURE__ */ jsxs(
|
|
1986
|
+
"button",
|
|
1987
|
+
{
|
|
1988
|
+
type: "button",
|
|
1989
|
+
className: clsx(styles$7.tag, selectedTags.includes(tag) && styles$7.tagActive),
|
|
1990
|
+
onClick: () => toggle(tag),
|
|
1991
|
+
"aria-pressed": selectedTags.includes(tag),
|
|
1992
|
+
children: [
|
|
1993
|
+
"#",
|
|
1994
|
+
tag
|
|
1995
|
+
]
|
|
1996
|
+
},
|
|
1997
|
+
tag
|
|
1998
|
+
))
|
|
1999
|
+
}
|
|
2000
|
+
);
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
const container$5 = "_container_vip00_1";
|
|
2004
|
+
const input$1 = "_input_vip00_7";
|
|
2005
|
+
const dropdown = "_dropdown_vip00_24";
|
|
2006
|
+
const category = "_category_vip00_39";
|
|
2007
|
+
const categoryTitle = "_categoryTitle_vip00_43";
|
|
2008
|
+
const resultItem = "_resultItem_vip00_54";
|
|
2009
|
+
const resultItemSelected = "_resultItemSelected_vip00_67";
|
|
2010
|
+
const resultTitle = "_resultTitle_vip00_71";
|
|
2011
|
+
const resultExcerpt = "_resultExcerpt_vip00_75";
|
|
2012
|
+
const titleMatch = "_titleMatch_vip00_80";
|
|
2013
|
+
const headingMatch = "_headingMatch_vip00_84";
|
|
2014
|
+
const highlight = "_highlight_vip00_110";
|
|
2015
|
+
const styles$6 = {
|
|
2016
|
+
container: container$5,
|
|
2017
|
+
input: input$1,
|
|
2018
|
+
dropdown: dropdown,
|
|
2019
|
+
category: category,
|
|
2020
|
+
categoryTitle: categoryTitle,
|
|
2021
|
+
resultItem: resultItem,
|
|
2022
|
+
resultItemSelected: resultItemSelected,
|
|
2023
|
+
resultTitle: resultTitle,
|
|
2024
|
+
resultExcerpt: resultExcerpt,
|
|
2025
|
+
titleMatch: titleMatch,
|
|
2026
|
+
headingMatch: headingMatch,
|
|
2027
|
+
highlight: highlight
|
|
2028
|
+
};
|
|
2029
|
+
|
|
2030
|
+
const SearchDropdown = ({
|
|
2031
|
+
searchIndex,
|
|
2032
|
+
onResultClick,
|
|
2033
|
+
placeholder = "Search...",
|
|
2034
|
+
className,
|
|
2035
|
+
maxResults = 20,
|
|
2036
|
+
showCategories = true
|
|
2037
|
+
}) => {
|
|
2038
|
+
const [query, setQuery] = useState("");
|
|
2039
|
+
const [results, setResults] = useState([]);
|
|
2040
|
+
const [categorizedResults, setCategorizedResults] = useState({
|
|
2041
|
+
titleMatches: [],
|
|
2042
|
+
headingMatches: [],
|
|
2043
|
+
contentMatches: []
|
|
2044
|
+
});
|
|
2045
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2046
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
2047
|
+
const inputRef = useRef(null);
|
|
2048
|
+
const dropdownRef = useRef(null);
|
|
2049
|
+
useEffect(() => {
|
|
2050
|
+
if (!query.trim()) {
|
|
2051
|
+
setResults([]);
|
|
2052
|
+
setCategorizedResults({ titleMatches: [], headingMatches: [], contentMatches: [] });
|
|
2053
|
+
setIsOpen(false);
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const searchResults = searchNotes(query, searchIndex, {
|
|
2057
|
+
limit: maxResults,
|
|
2058
|
+
highlight: true,
|
|
2059
|
+
fuzzy: true,
|
|
2060
|
+
boost: { title: 5, headings: 3, content: 1, metadata: 2 }
|
|
2061
|
+
// Boost titles and headings
|
|
2062
|
+
});
|
|
2063
|
+
const titleMatches = searchResults.filter(
|
|
2064
|
+
(result) => result.matchedFields.includes("title")
|
|
2065
|
+
);
|
|
2066
|
+
const headingMatches = searchResults.filter(
|
|
2067
|
+
(result) => !result.matchedFields.includes("title") && result.matchedFields.includes("headings")
|
|
2068
|
+
);
|
|
2069
|
+
const contentMatches = searchResults.filter(
|
|
2070
|
+
(result) => !result.matchedFields.includes("title") && !result.matchedFields.includes("headings")
|
|
2071
|
+
);
|
|
2072
|
+
const orderedResults = showCategories ? [...titleMatches, ...headingMatches, ...contentMatches] : searchResults;
|
|
2073
|
+
setResults(orderedResults);
|
|
2074
|
+
setCategorizedResults({ titleMatches, headingMatches, contentMatches });
|
|
2075
|
+
setIsOpen(orderedResults.length > 0);
|
|
2076
|
+
setSelectedIndex(0);
|
|
2077
|
+
}, [query, searchIndex, maxResults, showCategories]);
|
|
2078
|
+
const handleKeyDown = (event) => {
|
|
2079
|
+
if (!isOpen || results.length === 0) return;
|
|
2080
|
+
switch (event.key) {
|
|
2081
|
+
case "ArrowDown":
|
|
2082
|
+
event.preventDefault();
|
|
2083
|
+
setSelectedIndex((prev) => (prev + 1) % results.length);
|
|
2084
|
+
break;
|
|
2085
|
+
case "ArrowUp":
|
|
2086
|
+
event.preventDefault();
|
|
2087
|
+
setSelectedIndex((prev) => (prev - 1 + results.length) % results.length);
|
|
2088
|
+
break;
|
|
2089
|
+
case "Enter":
|
|
2090
|
+
event.preventDefault();
|
|
2091
|
+
if (results[selectedIndex]) {
|
|
2092
|
+
onResultClick(results[selectedIndex].slug);
|
|
2093
|
+
setIsOpen(false);
|
|
2094
|
+
}
|
|
2095
|
+
break;
|
|
2096
|
+
case "Escape":
|
|
2097
|
+
setIsOpen(false);
|
|
2098
|
+
inputRef.current?.blur();
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
useEffect(() => {
|
|
2103
|
+
const handleClickOutside = (event) => {
|
|
2104
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target) && inputRef.current && !inputRef.current.contains(event.target)) {
|
|
2105
|
+
setIsOpen(false);
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2109
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
2110
|
+
}, []);
|
|
2111
|
+
const highlightText = (text, query2) => {
|
|
2112
|
+
if (!query2.trim()) return text;
|
|
2113
|
+
const regex = new RegExp(`(${query2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
|
2114
|
+
const parts = text.split(regex);
|
|
2115
|
+
return parts.map(
|
|
2116
|
+
(part, index) => regex.test(part) ? /* @__PURE__ */ jsx("mark", { className: styles$6.highlight, children: part }, index) : part
|
|
2117
|
+
);
|
|
2118
|
+
};
|
|
2119
|
+
const renderResult = (result, index) => {
|
|
2120
|
+
const isSelected = index === selectedIndex;
|
|
2121
|
+
const isTitleMatch = result.matchedFields.includes("title");
|
|
2122
|
+
const isHeadingMatch = !isTitleMatch && result.matchedFields.includes("headings");
|
|
2123
|
+
const headingHighlight = result.highlights?.find((highlight) => highlight.field === "headings");
|
|
2124
|
+
const excerptText = isHeadingMatch ? headingHighlight?.text ?? result.excerpt : result.excerpt;
|
|
2125
|
+
return /* @__PURE__ */ jsxs(
|
|
2126
|
+
"div",
|
|
2127
|
+
{
|
|
2128
|
+
id: `result-${result.slug}`,
|
|
2129
|
+
role: "option",
|
|
2130
|
+
"aria-selected": isSelected,
|
|
2131
|
+
className: clsx(
|
|
2132
|
+
styles$6.resultItem,
|
|
2133
|
+
isSelected && styles$6.resultItemSelected,
|
|
2134
|
+
isTitleMatch && styles$6.titleMatch,
|
|
2135
|
+
isHeadingMatch && styles$6.headingMatch
|
|
2136
|
+
),
|
|
2137
|
+
onClick: () => {
|
|
2138
|
+
onResultClick(result.slug);
|
|
2139
|
+
setIsOpen(false);
|
|
2140
|
+
},
|
|
2141
|
+
children: [
|
|
2142
|
+
/* @__PURE__ */ jsx("div", { className: styles$6.resultTitle, children: highlightText(result.title, query) }),
|
|
2143
|
+
excerptText && /* @__PURE__ */ jsx("div", { className: styles$6.resultExcerpt, children: highlightText(excerptText, query) })
|
|
2144
|
+
]
|
|
2145
|
+
},
|
|
2146
|
+
result.slug
|
|
2147
|
+
);
|
|
2148
|
+
};
|
|
2149
|
+
return /* @__PURE__ */ jsxs("div", { className: clsx(styles$6.container, className), children: [
|
|
2150
|
+
/* @__PURE__ */ jsx(
|
|
2151
|
+
"input",
|
|
2152
|
+
{
|
|
2153
|
+
ref: inputRef,
|
|
2154
|
+
type: "search",
|
|
2155
|
+
value: query,
|
|
2156
|
+
onChange: (e) => setQuery(e.target.value),
|
|
2157
|
+
onKeyDown: handleKeyDown,
|
|
2158
|
+
onFocus: () => query.trim() && setIsOpen(true),
|
|
2159
|
+
placeholder,
|
|
2160
|
+
className: styles$6.input,
|
|
2161
|
+
autoComplete: "off",
|
|
2162
|
+
"aria-label": "Search",
|
|
2163
|
+
role: "combobox",
|
|
2164
|
+
"aria-expanded": isOpen,
|
|
2165
|
+
"aria-controls": "search-results",
|
|
2166
|
+
"aria-activedescendant": isOpen && results[selectedIndex] ? `result-${results[selectedIndex].slug}` : void 0,
|
|
2167
|
+
"aria-autocomplete": "list"
|
|
2168
|
+
}
|
|
2169
|
+
),
|
|
2170
|
+
isOpen && results.length > 0 && /* @__PURE__ */ jsx("div", { ref: dropdownRef, className: styles$6.dropdown, id: "search-results", role: "listbox", children: showCategories ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2171
|
+
categorizedResults.titleMatches.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles$6.category, children: [
|
|
2172
|
+
/* @__PURE__ */ jsx("div", { className: styles$6.categoryTitle, children: "Notes" }),
|
|
2173
|
+
categorizedResults.titleMatches.map(
|
|
2174
|
+
(result, index) => renderResult(result, index)
|
|
2175
|
+
)
|
|
2176
|
+
] }),
|
|
2177
|
+
categorizedResults.headingMatches.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles$6.category, children: [
|
|
2178
|
+
/* @__PURE__ */ jsx("div", { className: styles$6.categoryTitle, children: "Headers" }),
|
|
2179
|
+
categorizedResults.headingMatches.map(
|
|
2180
|
+
(result, index) => renderResult(result, categorizedResults.titleMatches.length + index)
|
|
2181
|
+
)
|
|
2182
|
+
] }),
|
|
2183
|
+
categorizedResults.contentMatches.length > 0 && /* @__PURE__ */ jsxs("div", { className: styles$6.category, children: [
|
|
2184
|
+
/* @__PURE__ */ jsx("div", { className: styles$6.categoryTitle, children: "Content" }),
|
|
2185
|
+
categorizedResults.contentMatches.map(
|
|
2186
|
+
(result, index) => renderResult(
|
|
2187
|
+
result,
|
|
2188
|
+
categorizedResults.titleMatches.length + categorizedResults.headingMatches.length + index
|
|
2189
|
+
)
|
|
2190
|
+
)
|
|
2191
|
+
] })
|
|
2192
|
+
] }) : /* @__PURE__ */ jsx("div", { className: styles$6.category, children: results.map((result, index) => renderResult(result, index)) }) })
|
|
2193
|
+
] });
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
const container$4 = "_container_1dw3c_2";
|
|
2197
|
+
const containerCompact = "_containerCompact_1dw3c_7";
|
|
2198
|
+
const input = "_input_1dw3c_13";
|
|
2199
|
+
const inputCompact = "_inputCompact_1dw3c_37";
|
|
2200
|
+
const styles$5 = {
|
|
2201
|
+
container: container$4,
|
|
2202
|
+
containerCompact: containerCompact,
|
|
2203
|
+
input: input,
|
|
2204
|
+
inputCompact: inputCompact
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
const FileSearch = ({
|
|
2208
|
+
query,
|
|
2209
|
+
onChange,
|
|
2210
|
+
placeholder = "Search files...",
|
|
2211
|
+
className,
|
|
2212
|
+
compact = false
|
|
2213
|
+
}) => {
|
|
2214
|
+
const handleChange = (e) => {
|
|
2215
|
+
onChange(e.target.value);
|
|
2216
|
+
};
|
|
2217
|
+
return /* @__PURE__ */ jsx(
|
|
2218
|
+
"div",
|
|
2219
|
+
{
|
|
2220
|
+
className: clsx(
|
|
2221
|
+
styles$5.container,
|
|
2222
|
+
compact && styles$5.containerCompact,
|
|
2223
|
+
className
|
|
2224
|
+
),
|
|
2225
|
+
"data-papyr-component": "FileSearch",
|
|
2226
|
+
children: /* @__PURE__ */ jsx(
|
|
2227
|
+
"input",
|
|
2228
|
+
{
|
|
2229
|
+
type: "search",
|
|
2230
|
+
className: clsx(
|
|
2231
|
+
styles$5.input,
|
|
2232
|
+
compact && styles$5.inputCompact
|
|
2233
|
+
),
|
|
2234
|
+
placeholder,
|
|
2235
|
+
value: query,
|
|
2236
|
+
onChange: handleChange,
|
|
2237
|
+
autoComplete: "off",
|
|
2238
|
+
"aria-label": "Search files"
|
|
2239
|
+
}
|
|
2240
|
+
)
|
|
2241
|
+
}
|
|
2242
|
+
);
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2245
|
+
const fileHierarchy = "_fileHierarchy_15xp0_1";
|
|
2246
|
+
const header = "_header_15xp0_11";
|
|
2247
|
+
const sectionTitle = "_sectionTitle_15xp0_17";
|
|
2248
|
+
const searchContainer = "_searchContainer_15xp0_26";
|
|
2249
|
+
const scrollContainer = "_scrollContainer_15xp0_33";
|
|
2250
|
+
const tree = "_tree_15xp0_48";
|
|
2251
|
+
const treeItem = "_treeItem_15xp0_54";
|
|
2252
|
+
const folderHeader = "_folderHeader_15xp0_60";
|
|
2253
|
+
const folderHeaderClickable = "_folderHeaderClickable_15xp0_70";
|
|
2254
|
+
const folderName = "_folderName_15xp0_74";
|
|
2255
|
+
const chevron = "_chevron_15xp0_78";
|
|
2256
|
+
const chevronExpanded = "_chevronExpanded_15xp0_109";
|
|
2257
|
+
const children = "_children_15xp0_124";
|
|
2258
|
+
const noteItem = "_noteItem_15xp0_134";
|
|
2259
|
+
const noteButton = "_noteButton_15xp0_138";
|
|
2260
|
+
const noteLabel = "_noteLabel_15xp0_157";
|
|
2261
|
+
const noteButtonActive = "_noteButtonActive_15xp0_180";
|
|
2262
|
+
const noteButtonSearchMatch = "_noteButtonSearchMatch_15xp0_189";
|
|
2263
|
+
const emptyState = "_emptyState_15xp0_198";
|
|
2264
|
+
const styles$4 = {
|
|
2265
|
+
fileHierarchy: fileHierarchy,
|
|
2266
|
+
header: header,
|
|
2267
|
+
sectionTitle: sectionTitle,
|
|
2268
|
+
searchContainer: searchContainer,
|
|
2269
|
+
scrollContainer: scrollContainer,
|
|
2270
|
+
tree: tree,
|
|
2271
|
+
treeItem: treeItem,
|
|
2272
|
+
folderHeader: folderHeader,
|
|
2273
|
+
folderHeaderClickable: folderHeaderClickable,
|
|
2274
|
+
folderName: folderName,
|
|
2275
|
+
chevron: chevron,
|
|
2276
|
+
chevronExpanded: chevronExpanded,
|
|
2277
|
+
children: children,
|
|
2278
|
+
noteItem: noteItem,
|
|
2279
|
+
noteButton: noteButton,
|
|
2280
|
+
noteLabel: noteLabel,
|
|
2281
|
+
noteButtonActive: noteButtonActive,
|
|
2282
|
+
noteButtonSearchMatch: noteButtonSearchMatch,
|
|
2283
|
+
emptyState: emptyState
|
|
2284
|
+
};
|
|
2285
|
+
|
|
2286
|
+
const capitalizeFirstLetter = (value) => {
|
|
2287
|
+
if (!value) return value;
|
|
2288
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
2289
|
+
};
|
|
2290
|
+
const formatFolderName = (name) => capitalizeFirstLetter(name);
|
|
2291
|
+
const getNoteDisplayText = (slug, noteTitleMap) => {
|
|
2292
|
+
const title = noteTitleMap?.[slug] ?? slug;
|
|
2293
|
+
return capitalizeFirstLetter(title);
|
|
2294
|
+
};
|
|
2295
|
+
|
|
2296
|
+
const containsSlug = (node, slug) => {
|
|
2297
|
+
if (!slug) return false;
|
|
2298
|
+
if (node.notes.includes(slug)) return true;
|
|
2299
|
+
return node.children.some((child) => containsSlug(child, slug));
|
|
2300
|
+
};
|
|
2301
|
+
const MemoizedFolderTree = memo(({
|
|
2302
|
+
node,
|
|
2303
|
+
onNoteClick,
|
|
2304
|
+
renderNoteLink,
|
|
2305
|
+
activeSlug,
|
|
2306
|
+
noteTitleMap,
|
|
2307
|
+
depth = 0,
|
|
2308
|
+
isSearching = false,
|
|
2309
|
+
searchQuery = ""
|
|
2310
|
+
}) => {
|
|
2311
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
2312
|
+
const hasChildren = node.children.length > 0 || node.notes.length > 0;
|
|
2313
|
+
const toggleExpanded = () => {
|
|
2314
|
+
if (hasChildren) {
|
|
2315
|
+
setIsExpanded(!isExpanded);
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
const hasActiveDescendant = useMemo(() => containsSlug(node, activeSlug), [node, activeSlug]);
|
|
2319
|
+
useEffect(() => {
|
|
2320
|
+
if (hasActiveDescendant) {
|
|
2321
|
+
setIsExpanded(true);
|
|
2322
|
+
}
|
|
2323
|
+
}, [hasActiveDescendant]);
|
|
2324
|
+
const isOpen = isExpanded || isSearching;
|
|
2325
|
+
const normalizedSearchQuery = searchQuery.trim().toLowerCase();
|
|
2326
|
+
return /* @__PURE__ */ jsxs("li", { className: styles$4.treeItem, children: [
|
|
2327
|
+
/* @__PURE__ */ jsxs(
|
|
2328
|
+
"div",
|
|
2329
|
+
{
|
|
2330
|
+
className: clsx(
|
|
2331
|
+
styles$4.folderHeader,
|
|
2332
|
+
hasChildren && styles$4.folderHeaderClickable
|
|
2333
|
+
),
|
|
2334
|
+
onClick: toggleExpanded,
|
|
2335
|
+
role: hasChildren ? "button" : void 0,
|
|
2336
|
+
"aria-expanded": hasChildren ? isOpen ? true : false : void 0,
|
|
2337
|
+
children: [
|
|
2338
|
+
hasChildren && /* @__PURE__ */ jsx("span", { className: clsx(styles$4.chevron, isOpen && styles$4.chevronExpanded), children: "›" }),
|
|
2339
|
+
/* @__PURE__ */ jsx("span", { className: styles$4.folderName, children: formatFolderName(node.name) })
|
|
2340
|
+
]
|
|
2341
|
+
}
|
|
2342
|
+
),
|
|
2343
|
+
isOpen && hasChildren && /* @__PURE__ */ jsxs("ul", { className: styles$4.children, children: [
|
|
2344
|
+
node.children.map((child) => /* @__PURE__ */ jsx(
|
|
2345
|
+
MemoizedFolderTree,
|
|
2346
|
+
{
|
|
2347
|
+
node: child,
|
|
2348
|
+
onNoteClick,
|
|
2349
|
+
renderNoteLink,
|
|
2350
|
+
activeSlug,
|
|
2351
|
+
noteTitleMap,
|
|
2352
|
+
depth: depth + 1,
|
|
2353
|
+
isSearching,
|
|
2354
|
+
searchQuery
|
|
2355
|
+
},
|
|
2356
|
+
child.path
|
|
2357
|
+
)),
|
|
2358
|
+
node.notes.map((slug) => {
|
|
2359
|
+
const displayText = getNoteDisplayText(slug, noteTitleMap);
|
|
2360
|
+
const isSearchMatch = Boolean(normalizedSearchQuery) && displayText.toLowerCase().includes(normalizedSearchQuery);
|
|
2361
|
+
const isActive = slug === activeSlug;
|
|
2362
|
+
const handleClick = () => onNoteClick?.(slug);
|
|
2363
|
+
const renderContext = {
|
|
2364
|
+
onClick: handleClick,
|
|
2365
|
+
isActive,
|
|
2366
|
+
isSearchMatch
|
|
2367
|
+
};
|
|
2368
|
+
return /* @__PURE__ */ jsx("li", { className: styles$4.noteItem, children: renderNoteLink ? renderNoteLink(slug, renderContext) : /* @__PURE__ */ jsx(
|
|
2369
|
+
MemoizedNoteButton,
|
|
2370
|
+
{
|
|
2371
|
+
displayText,
|
|
2372
|
+
isActive,
|
|
2373
|
+
isSearchMatch,
|
|
2374
|
+
onClick: handleClick
|
|
2375
|
+
}
|
|
2376
|
+
) }, slug);
|
|
2377
|
+
})
|
|
2378
|
+
] })
|
|
2379
|
+
] });
|
|
2380
|
+
});
|
|
2381
|
+
const MemoizedNoteButton = memo(({ displayText, isActive, isSearchMatch, onClick }) => {
|
|
2382
|
+
return /* @__PURE__ */ jsx(
|
|
2383
|
+
"button",
|
|
2384
|
+
{
|
|
2385
|
+
type: "button",
|
|
2386
|
+
className: clsx(
|
|
2387
|
+
styles$4.noteButton,
|
|
2388
|
+
isActive && styles$4.noteButtonActive,
|
|
2389
|
+
isSearchMatch && styles$4.noteButtonSearchMatch
|
|
2390
|
+
),
|
|
2391
|
+
onClick,
|
|
2392
|
+
children: /* @__PURE__ */ jsx("span", { className: styles$4.noteLabel, children: displayText })
|
|
2393
|
+
}
|
|
2394
|
+
);
|
|
2395
|
+
});
|
|
2396
|
+
MemoizedFolderTree.displayName = "MemoizedFolderTree";
|
|
2397
|
+
MemoizedNoteButton.displayName = "MemoizedNoteButton";
|
|
2398
|
+
|
|
2399
|
+
const normalizeTree = (tree) => {
|
|
2400
|
+
if (Array.isArray(tree)) {
|
|
2401
|
+
return { nodes: tree, rootNotes: [] };
|
|
2402
|
+
}
|
|
2403
|
+
return {
|
|
2404
|
+
nodes: tree.children,
|
|
2405
|
+
rootNotes: [...tree.notes]
|
|
2406
|
+
};
|
|
2407
|
+
};
|
|
2408
|
+
const filterTreeBySearch = (tree, searchQuery, noteTitleMap) => {
|
|
2409
|
+
if (!searchQuery.trim()) return tree;
|
|
2410
|
+
const query = searchQuery.toLowerCase();
|
|
2411
|
+
return tree.map((node) => {
|
|
2412
|
+
const filteredNotes = node.notes.filter((slug) => {
|
|
2413
|
+
const displayText = getNoteDisplayText(slug, noteTitleMap);
|
|
2414
|
+
return displayText.toLowerCase().includes(query);
|
|
2415
|
+
});
|
|
2416
|
+
const filteredChildren = filterTreeBySearch(node.children, searchQuery, noteTitleMap);
|
|
2417
|
+
const hasMatches = filteredNotes.length > 0 || filteredChildren.length > 0;
|
|
2418
|
+
if (hasMatches) {
|
|
2419
|
+
return {
|
|
2420
|
+
...node,
|
|
2421
|
+
notes: filteredNotes,
|
|
2422
|
+
children: filteredChildren
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
return null;
|
|
2426
|
+
}).filter(Boolean);
|
|
2427
|
+
};
|
|
2428
|
+
const FileHierarchy = ({
|
|
2429
|
+
tree,
|
|
2430
|
+
className,
|
|
2431
|
+
onNoteClick,
|
|
2432
|
+
renderNoteLink,
|
|
2433
|
+
activeSlug,
|
|
2434
|
+
notes,
|
|
2435
|
+
searchable = false,
|
|
2436
|
+
searchIndex,
|
|
2437
|
+
searchPlaceholder = "Search files..."
|
|
2438
|
+
}) => {
|
|
2439
|
+
const [fallbackSearchQuery, setFallbackSearchQuery] = useState("");
|
|
2440
|
+
const noteTitleMap = useMemo(() => {
|
|
2441
|
+
if (!notes) return void 0;
|
|
2442
|
+
return notes.reduce((acc, note) => {
|
|
2443
|
+
acc[note.slug] = note.title;
|
|
2444
|
+
return acc;
|
|
2445
|
+
}, {});
|
|
2446
|
+
}, [notes]);
|
|
2447
|
+
const isFallbackSearchEnabled = searchable && !searchIndex;
|
|
2448
|
+
const isFallbackSearching = isFallbackSearchEnabled && Boolean(fallbackSearchQuery.trim());
|
|
2449
|
+
useEffect(() => {
|
|
2450
|
+
if (!isFallbackSearchEnabled) return;
|
|
2451
|
+
console.warn("[papyr] FileHierarchy rendered with `searchable` but no `searchIndex`. Falling back to tree-based filtering.");
|
|
2452
|
+
}, [isFallbackSearchEnabled]);
|
|
2453
|
+
useEffect(() => {
|
|
2454
|
+
if (searchIndex) {
|
|
2455
|
+
setFallbackSearchQuery("");
|
|
2456
|
+
}
|
|
2457
|
+
}, [searchIndex]);
|
|
2458
|
+
const handleSearchResultClick = useCallback((slug) => {
|
|
2459
|
+
onNoteClick?.(slug);
|
|
2460
|
+
}, [onNoteClick]);
|
|
2461
|
+
const normalizedTree = useMemo(() => normalizeTree(tree), [tree]);
|
|
2462
|
+
const filteredHierarchy = useMemo(() => {
|
|
2463
|
+
if (!isFallbackSearchEnabled) {
|
|
2464
|
+
return normalizedTree;
|
|
2465
|
+
}
|
|
2466
|
+
const filteredNodes = filterTreeBySearch(
|
|
2467
|
+
normalizedTree.nodes,
|
|
2468
|
+
fallbackSearchQuery,
|
|
2469
|
+
noteTitleMap
|
|
2470
|
+
);
|
|
2471
|
+
const query = fallbackSearchQuery.trim().toLowerCase();
|
|
2472
|
+
const filteredRootNotes = normalizedTree.rootNotes.filter((slug) => {
|
|
2473
|
+
if (!query) return true;
|
|
2474
|
+
const displayText = getNoteDisplayText(slug, noteTitleMap);
|
|
2475
|
+
return displayText.toLowerCase().includes(query);
|
|
2476
|
+
});
|
|
2477
|
+
return {
|
|
2478
|
+
nodes: filteredNodes,
|
|
2479
|
+
rootNotes: filteredRootNotes
|
|
2480
|
+
};
|
|
2481
|
+
}, [normalizedTree, isFallbackSearchEnabled, fallbackSearchQuery, noteTitleMap]);
|
|
2482
|
+
const treeToRender = filteredHierarchy.nodes;
|
|
2483
|
+
const rootNotesToRender = filteredHierarchy.rootNotes;
|
|
2484
|
+
const effectiveSearchQuery = isFallbackSearchEnabled ? fallbackSearchQuery : "";
|
|
2485
|
+
const isEmptyState = !treeToRender.length && !rootNotesToRender.length;
|
|
2486
|
+
const emptyStateMessage = isFallbackSearching ? "No results found" : "No files to display yet";
|
|
2487
|
+
return /* @__PURE__ */ jsxs("aside", { className: clsx(styles$4.fileHierarchy, className), "data-papyr-component": "FileHierarchy", "aria-label": "File navigation", children: [
|
|
2488
|
+
/* @__PURE__ */ jsx("div", { className: styles$4.header, children: /* @__PURE__ */ jsx("span", { className: styles$4.sectionTitle, children: "Files" }) }),
|
|
2489
|
+
searchable && /* @__PURE__ */ jsx("div", { className: styles$4.searchContainer, children: searchIndex ? /* @__PURE__ */ jsx(
|
|
2490
|
+
SearchDropdown,
|
|
2491
|
+
{
|
|
2492
|
+
searchIndex,
|
|
2493
|
+
onResultClick: handleSearchResultClick,
|
|
2494
|
+
placeholder: searchPlaceholder
|
|
2495
|
+
}
|
|
2496
|
+
) : /* @__PURE__ */ jsx(
|
|
2497
|
+
FileSearch,
|
|
2498
|
+
{
|
|
2499
|
+
query: fallbackSearchQuery,
|
|
2500
|
+
onChange: setFallbackSearchQuery,
|
|
2501
|
+
placeholder: searchPlaceholder,
|
|
2502
|
+
compact: true
|
|
2503
|
+
}
|
|
2504
|
+
) }),
|
|
2505
|
+
/* @__PURE__ */ jsx("div", { className: styles$4.scrollContainer, children: isEmptyState ? /* @__PURE__ */ jsx("div", { className: styles$4.emptyState, children: emptyStateMessage }) : /* @__PURE__ */ jsxs("ul", { className: styles$4.tree, children: [
|
|
2506
|
+
rootNotesToRender.map((slug) => {
|
|
2507
|
+
const displayText = getNoteDisplayText(slug, noteTitleMap);
|
|
2508
|
+
const isActive = slug === activeSlug;
|
|
2509
|
+
const isSearchMatch = effectiveSearchQuery.trim().length > 0 && displayText.toLowerCase().includes(effectiveSearchQuery.toLowerCase());
|
|
2510
|
+
const handleClick = () => onNoteClick?.(slug);
|
|
2511
|
+
const renderContext = {
|
|
2512
|
+
onClick: handleClick,
|
|
2513
|
+
isActive,
|
|
2514
|
+
isSearchMatch
|
|
2515
|
+
};
|
|
2516
|
+
return /* @__PURE__ */ jsx("li", { className: styles$4.noteItem, children: renderNoteLink ? renderNoteLink(slug, renderContext) : /* @__PURE__ */ jsx(
|
|
2517
|
+
"button",
|
|
2518
|
+
{
|
|
2519
|
+
type: "button",
|
|
2520
|
+
className: clsx(
|
|
2521
|
+
styles$4.noteButton,
|
|
2522
|
+
isActive && styles$4.noteButtonActive,
|
|
2523
|
+
isSearchMatch && styles$4.noteButtonSearchMatch
|
|
2524
|
+
),
|
|
2525
|
+
onClick: handleClick,
|
|
2526
|
+
children: displayText
|
|
2527
|
+
}
|
|
2528
|
+
) }, `root-${slug}`);
|
|
2529
|
+
}),
|
|
2530
|
+
treeToRender.map((node) => /* @__PURE__ */ jsx(
|
|
2531
|
+
MemoizedFolderTree,
|
|
2532
|
+
{
|
|
2533
|
+
node,
|
|
2534
|
+
onNoteClick,
|
|
2535
|
+
renderNoteLink,
|
|
2536
|
+
activeSlug,
|
|
2537
|
+
noteTitleMap,
|
|
2538
|
+
isSearching: isFallbackSearching,
|
|
2539
|
+
searchQuery: effectiveSearchQuery
|
|
2540
|
+
},
|
|
2541
|
+
node.path
|
|
2542
|
+
))
|
|
2543
|
+
] }) })
|
|
2544
|
+
] });
|
|
2545
|
+
};
|
|
2546
|
+
|
|
2547
|
+
const container$3 = "_container_f89dg_1";
|
|
2548
|
+
const icon = "_icon_f89dg_11";
|
|
2549
|
+
const emoji = "_emoji_f89dg_16";
|
|
2550
|
+
const title = "_title_f89dg_21";
|
|
2551
|
+
const description = "_description_f89dg_28";
|
|
2552
|
+
const actions = "_actions_f89dg_36";
|
|
2553
|
+
const styles$3 = {
|
|
2554
|
+
container: container$3,
|
|
2555
|
+
icon: icon,
|
|
2556
|
+
emoji: emoji,
|
|
2557
|
+
title: title,
|
|
2558
|
+
description: description,
|
|
2559
|
+
actions: actions
|
|
2560
|
+
};
|
|
2561
|
+
|
|
2562
|
+
const EmptyState = ({
|
|
2563
|
+
icon,
|
|
2564
|
+
title,
|
|
2565
|
+
description,
|
|
2566
|
+
children,
|
|
2567
|
+
className
|
|
2568
|
+
}) => {
|
|
2569
|
+
return /* @__PURE__ */ jsxs(
|
|
2570
|
+
"div",
|
|
2571
|
+
{
|
|
2572
|
+
className: clsx(styles$3.container, className),
|
|
2573
|
+
"data-papyr-component": "EmptyState",
|
|
2574
|
+
role: "status",
|
|
2575
|
+
"aria-label": title,
|
|
2576
|
+
children: [
|
|
2577
|
+
icon && /* @__PURE__ */ jsx("div", { className: styles$3.icon, "aria-hidden": "true", children: typeof icon === "string" ? /* @__PURE__ */ jsx("span", { className: styles$3.emoji, children: icon }) : icon }),
|
|
2578
|
+
/* @__PURE__ */ jsx("h2", { className: styles$3.title, children: title }),
|
|
2579
|
+
description && /* @__PURE__ */ jsx("p", { className: styles$3.description, children: description }),
|
|
2580
|
+
children && /* @__PURE__ */ jsx("div", { className: styles$3.actions, children })
|
|
2581
|
+
]
|
|
2582
|
+
}
|
|
2583
|
+
);
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
const container$2 = "_container_13ske_1";
|
|
2587
|
+
const skipLink$1 = "_skipLink_13ske_9";
|
|
2588
|
+
const containerFullHeight$1 = "_containerFullHeight_13ske_26";
|
|
2589
|
+
const containerAutoHeight$1 = "_containerAutoHeight_13ske_32";
|
|
2590
|
+
const sidebar$1 = "_sidebar_13ske_36";
|
|
2591
|
+
const sidebarScrollArea$1 = "_sidebarScrollArea_13ske_46";
|
|
2592
|
+
const sidebarScrollAreaFullHeight = "_sidebarScrollAreaFullHeight_13ske_52";
|
|
2593
|
+
const sidebarScrollAreaAutoHeight = "_sidebarScrollAreaAutoHeight_13ske_60";
|
|
2594
|
+
const sidebarScrollAreaHiddenScrollbars$1 = "_sidebarScrollAreaHiddenScrollbars_13ske_64";
|
|
2595
|
+
const main$1 = "_main_13ske_74";
|
|
2596
|
+
const mainScrollArea$1 = "_mainScrollArea_13ske_85";
|
|
2597
|
+
const mainScrollAreaFullHeight = "_mainScrollAreaFullHeight_13ske_94";
|
|
2598
|
+
const mainScrollAreaAutoHeight = "_mainScrollAreaAutoHeight_13ske_102";
|
|
2599
|
+
const styles$2 = {
|
|
2600
|
+
container: container$2,
|
|
2601
|
+
skipLink: skipLink$1,
|
|
2602
|
+
containerFullHeight: containerFullHeight$1,
|
|
2603
|
+
containerAutoHeight: containerAutoHeight$1,
|
|
2604
|
+
sidebar: sidebar$1,
|
|
2605
|
+
sidebarScrollArea: sidebarScrollArea$1,
|
|
2606
|
+
sidebarScrollAreaFullHeight: sidebarScrollAreaFullHeight,
|
|
2607
|
+
sidebarScrollAreaAutoHeight: sidebarScrollAreaAutoHeight,
|
|
2608
|
+
sidebarScrollAreaHiddenScrollbars: sidebarScrollAreaHiddenScrollbars$1,
|
|
2609
|
+
main: main$1,
|
|
2610
|
+
mainScrollArea: mainScrollArea$1,
|
|
2611
|
+
mainScrollAreaFullHeight: mainScrollAreaFullHeight,
|
|
2612
|
+
mainScrollAreaAutoHeight: mainScrollAreaAutoHeight
|
|
2613
|
+
};
|
|
2614
|
+
|
|
2615
|
+
const SidebarLayout = ({
|
|
2616
|
+
sidebar,
|
|
2617
|
+
main,
|
|
2618
|
+
className,
|
|
2619
|
+
sidebarWidth = 280,
|
|
2620
|
+
fullHeight = true
|
|
2621
|
+
}) => {
|
|
2622
|
+
const gridTemplateColumns = typeof sidebarWidth === "number" ? `${sidebarWidth}px 1fr` : `${sidebarWidth} 1fr`;
|
|
2623
|
+
const containerClassName = clsx(
|
|
2624
|
+
styles$2.container,
|
|
2625
|
+
fullHeight ? styles$2.containerFullHeight : styles$2.containerAutoHeight,
|
|
2626
|
+
className
|
|
2627
|
+
);
|
|
2628
|
+
const sidebarScrollClassName = clsx(
|
|
2629
|
+
styles$2.sidebarScrollArea,
|
|
2630
|
+
fullHeight ? styles$2.sidebarScrollAreaFullHeight : styles$2.sidebarScrollAreaAutoHeight,
|
|
2631
|
+
styles$2.sidebarScrollAreaHiddenScrollbars
|
|
2632
|
+
);
|
|
2633
|
+
const mainScrollClassName = clsx(
|
|
2634
|
+
styles$2.mainScrollArea,
|
|
2635
|
+
fullHeight ? styles$2.mainScrollAreaFullHeight : styles$2.mainScrollAreaAutoHeight
|
|
2636
|
+
);
|
|
2637
|
+
return /* @__PURE__ */ jsxs(
|
|
2638
|
+
"div",
|
|
2639
|
+
{
|
|
2640
|
+
className: containerClassName,
|
|
2641
|
+
style: { gridTemplateColumns },
|
|
2642
|
+
"data-papyr-component": "SidebarLayout",
|
|
2643
|
+
children: [
|
|
2644
|
+
/* @__PURE__ */ jsx("a", { href: "#main-content", className: styles$2.skipLink, children: "Skip to main content" }),
|
|
2645
|
+
/* @__PURE__ */ jsx("aside", { className: styles$2.sidebar, "aria-label": "Primary navigation", children: /* @__PURE__ */ jsx("div", { className: sidebarScrollClassName, children: sidebar }) }),
|
|
2646
|
+
/* @__PURE__ */ jsx("main", { id: "main-content", className: styles$2.main, children: /* @__PURE__ */ jsx("div", { className: mainScrollClassName, children: main }) })
|
|
2647
|
+
]
|
|
2648
|
+
}
|
|
2649
|
+
);
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2652
|
+
const container$1 = "_container_1ttbz_1";
|
|
2653
|
+
const skipLink = "_skipLink_1ttbz_9";
|
|
2654
|
+
const containerFullHeight = "_containerFullHeight_1ttbz_26";
|
|
2655
|
+
const containerAutoHeight = "_containerAutoHeight_1ttbz_32";
|
|
2656
|
+
const sidebar = "_sidebar_1ttbz_36";
|
|
2657
|
+
const sidebarScrollArea = "_sidebarScrollArea_1ttbz_51";
|
|
2658
|
+
const sidebarScrollAreaHiddenScrollbars = "_sidebarScrollAreaHiddenScrollbars_1ttbz_62";
|
|
2659
|
+
const main = "_main_1ttbz_72";
|
|
2660
|
+
const mainScrollArea = "_mainScrollArea_1ttbz_84";
|
|
2661
|
+
const styles$1 = {
|
|
2662
|
+
container: container$1,
|
|
2663
|
+
skipLink: skipLink,
|
|
2664
|
+
containerFullHeight: containerFullHeight,
|
|
2665
|
+
containerAutoHeight: containerAutoHeight,
|
|
2666
|
+
sidebar: sidebar,
|
|
2667
|
+
sidebarScrollArea: sidebarScrollArea,
|
|
2668
|
+
sidebarScrollAreaHiddenScrollbars: sidebarScrollAreaHiddenScrollbars,
|
|
2669
|
+
main: main,
|
|
2670
|
+
mainScrollArea: mainScrollArea
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
const toCssSize = (value) => typeof value === "number" ? `${value}px` : value;
|
|
2674
|
+
const DoubleSidebarLayout = ({
|
|
2675
|
+
leftSidebar,
|
|
2676
|
+
main,
|
|
2677
|
+
rightSidebar = null,
|
|
2678
|
+
leftWidth = 280,
|
|
2679
|
+
rightWidth = 480,
|
|
2680
|
+
fullHeight = true,
|
|
2681
|
+
hideSidebarScrollbars = true,
|
|
2682
|
+
className
|
|
2683
|
+
}) => {
|
|
2684
|
+
const containerClassName = clsx(
|
|
2685
|
+
styles$1.container,
|
|
2686
|
+
fullHeight ? styles$1.containerFullHeight : styles$1.containerAutoHeight,
|
|
2687
|
+
className
|
|
2688
|
+
);
|
|
2689
|
+
const sidebarScrollClassName = clsx(
|
|
2690
|
+
styles$1.sidebarScrollArea,
|
|
2691
|
+
hideSidebarScrollbars && styles$1.sidebarScrollAreaHiddenScrollbars
|
|
2692
|
+
);
|
|
2693
|
+
const leftStyle = { width: toCssSize(leftWidth) };
|
|
2694
|
+
const rightStyle = { width: toCssSize(rightWidth) };
|
|
2695
|
+
return /* @__PURE__ */ jsxs("div", { className: containerClassName, "data-papyr-component": "DoubleSidebarLayout", children: [
|
|
2696
|
+
/* @__PURE__ */ jsx("a", { href: "#main-content", className: styles$1.skipLink, children: "Skip to main content" }),
|
|
2697
|
+
/* @__PURE__ */ jsx("aside", { className: styles$1.sidebar, style: leftStyle, "aria-label": "File navigation", children: /* @__PURE__ */ jsx("div", { className: sidebarScrollClassName, children: leftSidebar }) }),
|
|
2698
|
+
/* @__PURE__ */ jsx("main", { id: "main-content", className: styles$1.main, children: /* @__PURE__ */ jsx("div", { className: styles$1.mainScrollArea, children: main }) }),
|
|
2699
|
+
rightSidebar ? /* @__PURE__ */ jsx("aside", { className: styles$1.sidebar, style: rightStyle, "aria-label": "Contextual sidebar", children: /* @__PURE__ */ jsx("div", { className: sidebarScrollClassName, children: rightSidebar }) }) : null
|
|
2700
|
+
] });
|
|
2701
|
+
};
|
|
2702
|
+
|
|
2703
|
+
const container = "_container_1uvfd_1";
|
|
2704
|
+
const list = "_list_1uvfd_13";
|
|
2705
|
+
const item = "_item_1uvfd_40";
|
|
2706
|
+
const itemActive = "_itemActive_1uvfd_48";
|
|
2707
|
+
const link = "_link_1uvfd_65";
|
|
2708
|
+
const linkActive = "_linkActive_1uvfd_84";
|
|
2709
|
+
const level1 = "_level1_1uvfd_91";
|
|
2710
|
+
const level2 = "_level2_1uvfd_102";
|
|
2711
|
+
const level3 = "_level3_1uvfd_106";
|
|
2712
|
+
const level4 = "_level4_1uvfd_110";
|
|
2713
|
+
const level5 = "_level5_1uvfd_114";
|
|
2714
|
+
const level6 = "_level6_1uvfd_118";
|
|
2715
|
+
const styles = {
|
|
2716
|
+
container: container,
|
|
2717
|
+
list: list,
|
|
2718
|
+
item: item,
|
|
2719
|
+
itemActive: itemActive,
|
|
2720
|
+
link: link,
|
|
2721
|
+
linkActive: linkActive,
|
|
2722
|
+
level1: level1,
|
|
2723
|
+
level2: level2,
|
|
2724
|
+
level3: level3,
|
|
2725
|
+
level4: level4,
|
|
2726
|
+
level5: level5,
|
|
2727
|
+
level6: level6
|
|
2728
|
+
};
|
|
2729
|
+
|
|
2730
|
+
const TableOfContents = ({
|
|
2731
|
+
note,
|
|
2732
|
+
className,
|
|
2733
|
+
title = "On this page",
|
|
2734
|
+
onNavigate
|
|
2735
|
+
}) => {
|
|
2736
|
+
const headings = note?.headings ?? [];
|
|
2737
|
+
const hasRenderableHeadings = Boolean(note && headings.length > 0);
|
|
2738
|
+
const [visibleHeadings, setVisibleHeadings] = useState(() => /* @__PURE__ */ new Set());
|
|
2739
|
+
const clearVisibleHeadings = useCallback(() => {
|
|
2740
|
+
setVisibleHeadings((prev) => {
|
|
2741
|
+
if (prev.size === 0) {
|
|
2742
|
+
return prev;
|
|
2743
|
+
}
|
|
2744
|
+
return /* @__PURE__ */ new Set();
|
|
2745
|
+
});
|
|
2746
|
+
}, []);
|
|
2747
|
+
const headingSignature = headings.map((heading) => heading.slug).join("|");
|
|
2748
|
+
useEffect(() => {
|
|
2749
|
+
if (typeof window === "undefined") {
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
if (!hasRenderableHeadings) {
|
|
2753
|
+
clearVisibleHeadings();
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
const targets = headings.map((heading) => {
|
|
2757
|
+
const element = document.getElementById(heading.slug);
|
|
2758
|
+
return element ? { slug: heading.slug, element } : null;
|
|
2759
|
+
}).filter((value) => Boolean(value));
|
|
2760
|
+
if (!targets.length) {
|
|
2761
|
+
clearVisibleHeadings();
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
const getScrollableParent = (element) => {
|
|
2765
|
+
let parent = element.parentElement;
|
|
2766
|
+
while (parent && parent !== document.body) {
|
|
2767
|
+
const style = window.getComputedStyle(parent);
|
|
2768
|
+
const overflowY = style.overflowY;
|
|
2769
|
+
const overflow = style.overflow;
|
|
2770
|
+
const isScrollable = /(auto|scroll)/.test(`${overflowY}${overflow}`);
|
|
2771
|
+
if (isScrollable && parent.scrollHeight > parent.clientHeight + 1) {
|
|
2772
|
+
return parent;
|
|
2773
|
+
}
|
|
2774
|
+
parent = parent.parentElement;
|
|
2775
|
+
}
|
|
2776
|
+
return window;
|
|
2777
|
+
};
|
|
2778
|
+
const getAbsoluteTop = (element) => element.getBoundingClientRect().top + window.scrollY;
|
|
2779
|
+
const computeVisibleSections = () => {
|
|
2780
|
+
const viewportTop = window.scrollY;
|
|
2781
|
+
const viewportBottom = viewportTop + window.innerHeight;
|
|
2782
|
+
const nextVisible = /* @__PURE__ */ new Set();
|
|
2783
|
+
targets.forEach((target, index) => {
|
|
2784
|
+
const sectionStart = getAbsoluteTop(target.element);
|
|
2785
|
+
const nextTarget = targets[index + 1];
|
|
2786
|
+
const sectionEnd = nextTarget ? getAbsoluteTop(nextTarget.element) : Number.POSITIVE_INFINITY;
|
|
2787
|
+
if (sectionStart < viewportBottom && sectionEnd > viewportTop) {
|
|
2788
|
+
nextVisible.add(target.slug);
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
setVisibleHeadings((prev) => {
|
|
2792
|
+
if (prev.size === nextVisible.size) {
|
|
2793
|
+
let identical = true;
|
|
2794
|
+
prev.forEach((slug) => {
|
|
2795
|
+
if (!nextVisible.has(slug)) {
|
|
2796
|
+
identical = false;
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
if (identical) {
|
|
2800
|
+
return prev;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
return nextVisible;
|
|
2804
|
+
});
|
|
2805
|
+
};
|
|
2806
|
+
let rafId = null;
|
|
2807
|
+
const scheduleUpdate = () => {
|
|
2808
|
+
if (rafId !== null) {
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
rafId = window.requestAnimationFrame(() => {
|
|
2812
|
+
rafId = null;
|
|
2813
|
+
computeVisibleSections();
|
|
2814
|
+
});
|
|
2815
|
+
};
|
|
2816
|
+
scheduleUpdate();
|
|
2817
|
+
const scrollContainers = Array.from(
|
|
2818
|
+
new Set(targets.map(({ element }) => getScrollableParent(element)))
|
|
2819
|
+
);
|
|
2820
|
+
scrollContainers.forEach((container) => {
|
|
2821
|
+
container.addEventListener("scroll", scheduleUpdate, { passive: true });
|
|
2822
|
+
});
|
|
2823
|
+
window.addEventListener("resize", scheduleUpdate);
|
|
2824
|
+
return () => {
|
|
2825
|
+
if (rafId !== null) {
|
|
2826
|
+
window.cancelAnimationFrame(rafId);
|
|
2827
|
+
}
|
|
2828
|
+
scrollContainers.forEach((container) => {
|
|
2829
|
+
container.removeEventListener("scroll", scheduleUpdate);
|
|
2830
|
+
});
|
|
2831
|
+
window.removeEventListener("resize", scheduleUpdate);
|
|
2832
|
+
};
|
|
2833
|
+
}, [clearVisibleHeadings, hasRenderableHeadings, headingSignature, headings]);
|
|
2834
|
+
const handleClick = (heading, event) => {
|
|
2835
|
+
event.preventDefault();
|
|
2836
|
+
setVisibleHeadings((prev) => {
|
|
2837
|
+
const next = new Set(prev);
|
|
2838
|
+
next.add(heading.slug);
|
|
2839
|
+
return next;
|
|
2840
|
+
});
|
|
2841
|
+
if (onNavigate) {
|
|
2842
|
+
onNavigate(heading);
|
|
2843
|
+
} else if (heading.slug && typeof window !== "undefined") {
|
|
2844
|
+
const element = document.getElementById(heading.slug);
|
|
2845
|
+
if (element) {
|
|
2846
|
+
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
2847
|
+
}
|
|
2848
|
+
updateHashWithAnchor(heading.slug);
|
|
2849
|
+
}
|
|
2850
|
+
};
|
|
2851
|
+
if (!hasRenderableHeadings || !note) {
|
|
2852
|
+
return null;
|
|
2853
|
+
}
|
|
2854
|
+
return /* @__PURE__ */ jsx(
|
|
2855
|
+
"nav",
|
|
2856
|
+
{
|
|
2857
|
+
className: clsx(styles.container, className),
|
|
2858
|
+
"aria-label": title ?? "Table of contents",
|
|
2859
|
+
"data-papyr-component": "TableOfContents",
|
|
2860
|
+
children: /* @__PURE__ */ jsx("ul", { className: styles.list, children: headings.map((heading) => /* @__PURE__ */ jsx(
|
|
2861
|
+
"li",
|
|
2862
|
+
{
|
|
2863
|
+
className: clsx(
|
|
2864
|
+
styles.item,
|
|
2865
|
+
styles[`level${heading.level}`],
|
|
2866
|
+
visibleHeadings.has(heading.slug) && styles.itemActive
|
|
2867
|
+
),
|
|
2868
|
+
children: /* @__PURE__ */ jsx(
|
|
2869
|
+
"a",
|
|
2870
|
+
{
|
|
2871
|
+
href: `#${heading.slug}`,
|
|
2872
|
+
className: clsx(styles.link, visibleHeadings.has(heading.slug) && styles.linkActive),
|
|
2873
|
+
onClick: (event) => handleClick(heading, event),
|
|
2874
|
+
"aria-current": visibleHeadings.has(heading.slug) ? "location" : void 0,
|
|
2875
|
+
children: heading.text
|
|
2876
|
+
}
|
|
2877
|
+
)
|
|
2878
|
+
},
|
|
2879
|
+
heading.slug
|
|
2880
|
+
)) })
|
|
2881
|
+
}
|
|
2882
|
+
);
|
|
2883
|
+
};
|
|
2884
|
+
|
|
2885
|
+
function useActiveNote(notes, initialSlug) {
|
|
2886
|
+
const [activeSlug, setActiveSlugState] = useState(initialSlug ?? null);
|
|
2887
|
+
const hasUserSelection = useRef(false);
|
|
2888
|
+
const prevInitialSlugRef = useRef(initialSlug);
|
|
2889
|
+
const { noteSetSignature, noteSlugSet } = useMemo(() => {
|
|
2890
|
+
const slugs = notes.map((note) => note.slug);
|
|
2891
|
+
const slugSet = new Set(slugs);
|
|
2892
|
+
const signature = [...slugSet].sort().join("\0");
|
|
2893
|
+
return { noteSetSignature: signature, noteSlugSet: slugSet };
|
|
2894
|
+
}, [notes]);
|
|
2895
|
+
const prevSignatureRef = useRef(noteSetSignature);
|
|
2896
|
+
useEffect(() => {
|
|
2897
|
+
const datasetChanged = prevSignatureRef.current !== noteSetSignature;
|
|
2898
|
+
const activeSlugMissing = activeSlug != null && !noteSlugSet.has(activeSlug);
|
|
2899
|
+
prevSignatureRef.current = noteSetSignature;
|
|
2900
|
+
if (datasetChanged || activeSlugMissing) {
|
|
2901
|
+
hasUserSelection.current = false;
|
|
2902
|
+
}
|
|
2903
|
+
if (activeSlugMissing) {
|
|
2904
|
+
setActiveSlugState(null);
|
|
2905
|
+
}
|
|
2906
|
+
}, [noteSetSignature, noteSlugSet, activeSlug]);
|
|
2907
|
+
useEffect(() => {
|
|
2908
|
+
const normalizedSlug = initialSlug ?? null;
|
|
2909
|
+
const prevSlug = prevInitialSlugRef.current ?? null;
|
|
2910
|
+
const initialSlugChanged = normalizedSlug !== prevSlug;
|
|
2911
|
+
prevInitialSlugRef.current = initialSlug;
|
|
2912
|
+
if (!hasUserSelection.current || initialSlugChanged) {
|
|
2913
|
+
if (normalizedSlug !== activeSlug) {
|
|
2914
|
+
setActiveSlugState(normalizedSlug);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}, [initialSlug, activeSlug]);
|
|
2918
|
+
const activeNote = useMemo(() => {
|
|
2919
|
+
if (!activeSlug) return null;
|
|
2920
|
+
return notes.find((note) => note.slug === activeSlug) ?? null;
|
|
2921
|
+
}, [notes, activeSlug]);
|
|
2922
|
+
const setActiveNote = useCallback((note) => {
|
|
2923
|
+
hasUserSelection.current = true;
|
|
2924
|
+
setActiveSlugState(note?.slug ?? null);
|
|
2925
|
+
}, []);
|
|
2926
|
+
const setActiveSlug = useCallback((slug) => {
|
|
2927
|
+
hasUserSelection.current = true;
|
|
2928
|
+
setActiveSlugState(slug);
|
|
2929
|
+
}, []);
|
|
2930
|
+
return {
|
|
2931
|
+
activeSlug,
|
|
2932
|
+
activeNote,
|
|
2933
|
+
setActiveSlug,
|
|
2934
|
+
setActiveNote
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function useRoutableActiveNote(notes, options) {
|
|
2939
|
+
const { getCurrentSlug, onSlugChange } = options;
|
|
2940
|
+
const papyr = useOptionalPapyr();
|
|
2941
|
+
const showToast = papyr?.showToast;
|
|
2942
|
+
const currentRouteSlug = getCurrentSlug();
|
|
2943
|
+
const currentRouteExists = currentRouteSlug !== null && notes.some((note) => note.slug === currentRouteSlug);
|
|
2944
|
+
const routeSlugRef = useRef(currentRouteSlug ?? null);
|
|
2945
|
+
const onSlugChangeRef = useRef(onSlugChange);
|
|
2946
|
+
const syncingRef = useRef(false);
|
|
2947
|
+
const lastMissingSlugRef = useRef(null);
|
|
2948
|
+
const lastValidSlugRef = useRef(currentRouteExists ? currentRouteSlug : null);
|
|
2949
|
+
useEffect(() => {
|
|
2950
|
+
onSlugChangeRef.current = onSlugChange;
|
|
2951
|
+
}, [onSlugChange]);
|
|
2952
|
+
const {
|
|
2953
|
+
activeSlug,
|
|
2954
|
+
activeNote,
|
|
2955
|
+
setActiveSlug: baseSetActiveSlug,
|
|
2956
|
+
setActiveNote: baseSetActiveNote
|
|
2957
|
+
} = useActiveNote(notes, currentRouteSlug ?? void 0);
|
|
2958
|
+
const notifyRouter = useCallback(
|
|
2959
|
+
(slug) => {
|
|
2960
|
+
routeSlugRef.current = slug ?? null;
|
|
2961
|
+
if (!onSlugChangeRef.current) {
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
const currentRouteSlug2 = getCurrentSlug();
|
|
2965
|
+
if (slug !== currentRouteSlug2) {
|
|
2966
|
+
onSlugChangeRef.current(slug);
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
[getCurrentSlug]
|
|
2970
|
+
);
|
|
2971
|
+
useEffect(() => {
|
|
2972
|
+
const routeSlug = getCurrentSlug();
|
|
2973
|
+
routeSlugRef.current = routeSlug ?? null;
|
|
2974
|
+
if (!routeSlug) {
|
|
2975
|
+
lastMissingSlugRef.current = null;
|
|
2976
|
+
lastValidSlugRef.current = null;
|
|
2977
|
+
if (activeSlug !== null) {
|
|
2978
|
+
syncingRef.current = true;
|
|
2979
|
+
baseSetActiveSlug(null);
|
|
2980
|
+
syncingRef.current = false;
|
|
2981
|
+
}
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
const hasNotes = notes.length > 0;
|
|
2985
|
+
const exists = hasNotes && notes.some((note) => note.slug === routeSlug);
|
|
2986
|
+
if (exists) {
|
|
2987
|
+
lastMissingSlugRef.current = null;
|
|
2988
|
+
lastValidSlugRef.current = routeSlug;
|
|
2989
|
+
if (routeSlug !== activeSlug) {
|
|
2990
|
+
syncingRef.current = true;
|
|
2991
|
+
baseSetActiveSlug(routeSlug);
|
|
2992
|
+
syncingRef.current = false;
|
|
2993
|
+
}
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
if (!hasNotes) {
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
if (lastMissingSlugRef.current !== routeSlug) {
|
|
3000
|
+
lastMissingSlugRef.current = routeSlug;
|
|
3001
|
+
showToast?.(`Note "${routeSlug}" not found.`, { duration: 3e3 });
|
|
3002
|
+
}
|
|
3003
|
+
if (lastValidSlugRef.current === routeSlug) {
|
|
3004
|
+
lastValidSlugRef.current = null;
|
|
3005
|
+
}
|
|
3006
|
+
const fallbackSlug = lastValidSlugRef.current;
|
|
3007
|
+
if (fallbackSlug !== routeSlug) {
|
|
3008
|
+
notifyRouter(fallbackSlug ?? null);
|
|
3009
|
+
}
|
|
3010
|
+
if (fallbackSlug !== activeSlug) {
|
|
3011
|
+
syncingRef.current = true;
|
|
3012
|
+
baseSetActiveSlug(fallbackSlug ?? null);
|
|
3013
|
+
syncingRef.current = false;
|
|
3014
|
+
}
|
|
3015
|
+
}, [getCurrentSlug, currentRouteSlug, notes, activeSlug, baseSetActiveSlug, notifyRouter, showToast]);
|
|
3016
|
+
const setActiveSlug = useCallback(
|
|
3017
|
+
(slug) => {
|
|
3018
|
+
baseSetActiveSlug(slug);
|
|
3019
|
+
if (!syncingRef.current) {
|
|
3020
|
+
notifyRouter(slug);
|
|
3021
|
+
}
|
|
3022
|
+
},
|
|
3023
|
+
[baseSetActiveSlug, notifyRouter]
|
|
3024
|
+
);
|
|
3025
|
+
const setActiveNote = useCallback(
|
|
3026
|
+
(note) => {
|
|
3027
|
+
baseSetActiveNote(note);
|
|
3028
|
+
if (!syncingRef.current) {
|
|
3029
|
+
notifyRouter(note?.slug ?? null);
|
|
3030
|
+
}
|
|
3031
|
+
},
|
|
3032
|
+
[baseSetActiveNote, notifyRouter]
|
|
3033
|
+
);
|
|
3034
|
+
return {
|
|
3035
|
+
activeSlug,
|
|
3036
|
+
activeNote,
|
|
3037
|
+
setActiveSlug,
|
|
3038
|
+
setActiveNote
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
const resolveSlot = (slot, context, fallback) => {
|
|
3043
|
+
if (typeof slot === "function") {
|
|
3044
|
+
return slot(context);
|
|
3045
|
+
}
|
|
3046
|
+
if (slot !== void 0) {
|
|
3047
|
+
return slot;
|
|
3048
|
+
}
|
|
3049
|
+
return fallback;
|
|
3050
|
+
};
|
|
3051
|
+
const WorkspaceBlock = ({
|
|
3052
|
+
notes,
|
|
3053
|
+
tree,
|
|
3054
|
+
searchIndex,
|
|
3055
|
+
graph = null,
|
|
3056
|
+
initialSlug = null,
|
|
3057
|
+
activeSlug: controlledActiveSlug,
|
|
3058
|
+
onActiveSlugChange,
|
|
3059
|
+
className,
|
|
3060
|
+
leftWidth,
|
|
3061
|
+
rightWidth,
|
|
3062
|
+
leftSidebar,
|
|
3063
|
+
main,
|
|
3064
|
+
rightSidebar,
|
|
3065
|
+
hideTableOfContents = false,
|
|
3066
|
+
onNoteChange,
|
|
3067
|
+
fileHierarchyProps,
|
|
3068
|
+
tableOfContentsProps,
|
|
3069
|
+
emptyState,
|
|
3070
|
+
hideSidebarScrollbars = true
|
|
3071
|
+
}) => {
|
|
3072
|
+
const isControlled = controlledActiveSlug !== void 0;
|
|
3073
|
+
const internalState = useActiveNote(notes, initialSlug);
|
|
3074
|
+
const activeSlug = isControlled ? controlledActiveSlug : internalState.activeSlug;
|
|
3075
|
+
const activeNote = useMemo(() => {
|
|
3076
|
+
if (isControlled) {
|
|
3077
|
+
if (!controlledActiveSlug) return null;
|
|
3078
|
+
return notes.find((n) => n.slug === controlledActiveSlug) ?? null;
|
|
3079
|
+
}
|
|
3080
|
+
return internalState.activeNote;
|
|
3081
|
+
}, [isControlled, controlledActiveSlug, notes, internalState.activeNote]);
|
|
3082
|
+
const setActiveSlug = useCallback(
|
|
3083
|
+
(slug) => {
|
|
3084
|
+
if (isControlled) {
|
|
3085
|
+
onActiveSlugChange?.(slug);
|
|
3086
|
+
} else {
|
|
3087
|
+
internalState.setActiveSlug(slug);
|
|
3088
|
+
}
|
|
3089
|
+
},
|
|
3090
|
+
[isControlled, onActiveSlugChange, internalState]
|
|
3091
|
+
);
|
|
3092
|
+
const setActiveNote = useCallback(
|
|
3093
|
+
(note) => {
|
|
3094
|
+
if (isControlled) {
|
|
3095
|
+
onActiveSlugChange?.(note?.slug ?? null);
|
|
3096
|
+
} else {
|
|
3097
|
+
internalState.setActiveNote(note);
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
[isControlled, onActiveSlugChange, internalState]
|
|
3101
|
+
);
|
|
3102
|
+
useEffect(() => {
|
|
3103
|
+
onNoteChange?.(activeNote);
|
|
3104
|
+
}, [activeNote, onNoteChange]);
|
|
3105
|
+
const context = useMemo(
|
|
3106
|
+
() => ({
|
|
3107
|
+
notes,
|
|
3108
|
+
searchIndex,
|
|
3109
|
+
graph,
|
|
3110
|
+
activeSlug,
|
|
3111
|
+
activeNote,
|
|
3112
|
+
setActiveSlug,
|
|
3113
|
+
setActiveNote
|
|
3114
|
+
}),
|
|
3115
|
+
[notes, searchIndex, graph, activeSlug, activeNote, setActiveSlug, setActiveNote]
|
|
3116
|
+
);
|
|
3117
|
+
const defaultFileHierarchy = /* @__PURE__ */ jsx(
|
|
3118
|
+
FileHierarchy,
|
|
3119
|
+
{
|
|
3120
|
+
tree,
|
|
3121
|
+
onNoteClick: setActiveSlug,
|
|
3122
|
+
activeSlug: activeSlug ?? void 0,
|
|
3123
|
+
notes,
|
|
3124
|
+
searchable: true,
|
|
3125
|
+
searchIndex,
|
|
3126
|
+
...fileHierarchyProps
|
|
3127
|
+
}
|
|
3128
|
+
);
|
|
3129
|
+
const defaultMain = /* @__PURE__ */ jsx(
|
|
3130
|
+
NoteViewer,
|
|
3131
|
+
{
|
|
3132
|
+
note: activeNote,
|
|
3133
|
+
onNavigateNote: setActiveSlug,
|
|
3134
|
+
emptyState: emptyState ?? /* @__PURE__ */ jsx(
|
|
3135
|
+
EmptyState,
|
|
3136
|
+
{
|
|
3137
|
+
icon: "📝",
|
|
3138
|
+
title: "No note selected",
|
|
3139
|
+
description: "Choose a note from the file hierarchy to start reading."
|
|
3140
|
+
}
|
|
3141
|
+
)
|
|
3142
|
+
}
|
|
3143
|
+
);
|
|
3144
|
+
const defaultRightSidebar = hideTableOfContents ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3145
|
+
graph && activeSlug && /* @__PURE__ */ jsx("div", { style: {
|
|
3146
|
+
height: "240px",
|
|
3147
|
+
flexShrink: 0,
|
|
3148
|
+
borderBottom: "1px solid var(--papyr-border, rgba(255, 255, 255, 0.1))"
|
|
3149
|
+
}, children: /* @__PURE__ */ jsx(
|
|
3150
|
+
GraphView,
|
|
3151
|
+
{
|
|
3152
|
+
graph,
|
|
3153
|
+
activeSlug,
|
|
3154
|
+
onNodeSelect: (node) => setActiveSlug(node.id),
|
|
3155
|
+
showFullscreenToggle: false
|
|
3156
|
+
}
|
|
3157
|
+
) }),
|
|
3158
|
+
/* @__PURE__ */ jsx(TableOfContents, { note: activeNote, ...tableOfContentsProps })
|
|
3159
|
+
] });
|
|
3160
|
+
const resolvedLeft = resolveSlot(leftSidebar, context, defaultFileHierarchy);
|
|
3161
|
+
const resolvedMain = resolveSlot(main, context, defaultMain);
|
|
3162
|
+
const resolvedRight = resolveSlot(rightSidebar, context, defaultRightSidebar);
|
|
3163
|
+
return /* @__PURE__ */ jsx(PapyrProvider, { notes, searchIndex, graph, children: /* @__PURE__ */ jsx(
|
|
3164
|
+
DoubleSidebarLayout,
|
|
3165
|
+
{
|
|
3166
|
+
className,
|
|
3167
|
+
leftSidebar: resolvedLeft,
|
|
3168
|
+
main: resolvedMain,
|
|
3169
|
+
rightSidebar: resolvedRight ?? void 0,
|
|
3170
|
+
leftWidth,
|
|
3171
|
+
rightWidth,
|
|
3172
|
+
hideSidebarScrollbars
|
|
3173
|
+
}
|
|
3174
|
+
) });
|
|
3175
|
+
};
|
|
3176
|
+
|
|
3177
|
+
export { Breadcrumbs, DoubleSidebarLayout, EmptyState, FileHierarchy, FileSearch, GraphView, HoverPreview, MiniGraph, NotePreviewContent, NoteViewer, PapyrProvider, SearchBar, SearchDropdown, SidebarLayout, TableOfContents, TagFilter, ToastContainer, WorkspaceBlock, buildWikiLink, findNoteBySlug, hydrateSearchIndex, parseNoteHash, toForceGraphData, updateHashWithAnchor, useActiveNote, useNotes, useOptionalPapyr, usePapyr, useRoutableActiveNote };
|
|
3178
|
+
//# sourceMappingURL=index.js.map
|