Skip to content

Commit 4adab63

Browse files
committed
Release v0.3.0
1 parent 510091e commit 4adab63

File tree

9 files changed

+447
-103
lines changed

9 files changed

+447
-103
lines changed

main.js

Lines changed: 221 additions & 79 deletions
Large diffs are not rendered by default.

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "discourse-graphs",
33
"name": "Discourse Graph",
4-
"version": "0.2.3",
4+
"version": "0.3.0",
55
"minAppVersion": "1.7.0",
66
"description": "Discourse Graph Plugin for Obsidian",
77
"author": "Discourse Graphs",

src/components/NodeTypeSettings.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ConfirmationModal } from "./ConfirmationModal";
88
import { getTemplateFiles, getTemplatePluginInfo } from "~/utils/templates";
99

1010
const generateTagPlaceholder = (format: string, nodeName?: string): string => {
11-
if (!format) return "Enter tag (e.g., clm-candidate or #clm-candidate)";
11+
if (!format) return "Enter tag (e.g., clm-candidate)";
1212

1313
// Extract the prefix before " - {content}" or " -{content}" or " -{content}" etc.
1414
const match = format.match(/^([A-Z]+)\s*-\s*\{content\}/i);
@@ -360,7 +360,7 @@ const NodeTypeSettings = () => {
360360
const modal = new ConfirmationModal(plugin.app, {
361361
title: "Delete Node Type",
362362
message: `Are you sure you want to delete the node type "${nodeType.name}"?`,
363-
onConfirm: () => handleDeleteNodeType(index),
363+
onConfirm: () => void handleDeleteNodeType(index),
364364
});
365365
modal.open();
366366
};
@@ -581,7 +581,7 @@ const NodeTypeSettings = () => {
581581
Cancel
582582
</button>
583583
<button
584-
onClick={handleSave}
584+
onClick={() => void handleSave()}
585585
className="mod-cta"
586586
disabled={
587587
Object.keys(errors).length > 0 ||

src/components/canvas/TldrawViewComponent.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { RelationsOverlay } from "./overlays/RelationOverlay";
5151
import { showToast } from "./utils/toastUtils";
5252
import { WHITE_LOGO_SVG } from "~/icons";
5353
import { CustomContextMenu } from "./CustomContextMenu";
54+
import { openFileInSidebar, openFileInNewTab } from "./utils/openFileUtils";
5455

5556
type TldrawPreviewProps = {
5657
store: TLStore;
@@ -235,8 +236,10 @@ export const TldrawPreviewComponent = ({
235236
isCreatingRelationRef.current = false;
236237
}
237238

238-
if (e.shiftKey) {
239+
// Handle Shift+Click (open in sidebar) or Cmd+Click (open in new tab)
240+
if (e.shiftKey || e.metaKey) {
239241
const now = Date.now();
242+
const openInNewTab = e.metaKey; // Cmd on Mac, Ctrl on other platforms
240243

241244
// Debounce to prevent double opening
242245
if (now - lastShiftClickRef.current < SHIFT_CLICK_DEBOUNCE_MS) {
@@ -295,21 +298,12 @@ export const TldrawPreviewComponent = ({
295298
return;
296299
}
297300

298-
const rightSplit = plugin.app.workspace.rightSplit;
299-
const rightLeaf = plugin.app.workspace.getRightLeaf(false);
300-
301-
if (rightLeaf) {
302-
if (rightSplit && rightSplit.collapsed) {
303-
rightSplit.expand();
304-
}
305-
await rightLeaf.openFile(linkedFile);
306-
plugin.app.workspace.setActiveLeaf(rightLeaf);
301+
// Open in sidebar (Shift+Click) or new tab (Cmd+Click)
302+
if (openInNewTab) {
303+
await openFileInNewTab(plugin.app, linkedFile);
307304
} else {
308-
const leaf = plugin.app.workspace.getLeaf("split", "vertical");
309-
await leaf.openFile(linkedFile);
310-
plugin.app.workspace.setActiveLeaf(leaf);
305+
await openFileInSidebar(plugin.app, linkedFile);
311306
}
312-
313307
editor.selectNone();
314308
})
315309
.catch((error) => {

src/components/canvas/shapes/DiscourseNodeShape.tsx

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TLBaseShape,
77
TLResizeInfo,
88
useEditor,
9+
useValue,
910
} from "tldraw";
1011
import type { App, TFile } from "obsidian";
1112
import { memo, createElement, useEffect } from "react";
@@ -18,6 +19,8 @@ import {
1819
import { resolveLinkedFileFromSrc } from "~/components/canvas/stores/assetStore";
1920
import { getNodeTypeById } from "~/utils/typeUtils";
2021
import { calcDiscourseNodeSize } from "~/utils/calcDiscourseNodeSize";
22+
import { openFileInSidebar } from "~/components/canvas/utils/openFileUtils";
23+
import { showToast } from "~/components/canvas/utils/toastUtils";
2124

2225
export type DiscourseNodeShape = TLBaseShape<
2326
"discourse-node",
@@ -140,6 +143,14 @@ const discourseNodeContent = memo(
140143
const { src, title, nodeTypeId } = shape.props;
141144
const nodeType = getNodeTypeById(plugin, nodeTypeId);
142145

146+
const isHovered = useValue(
147+
"is hovered",
148+
() => {
149+
return editor.getHoveredShapeId() === shape.id;
150+
},
151+
[editor, shape.id],
152+
);
153+
143154
useEffect(() => {
144155
const loadNodeData = async () => {
145156
if (!src) {
@@ -255,17 +266,89 @@ const discourseNodeContent = memo(
255266
nodeType?.keyImage,
256267
]);
257268

269+
const handleOpenInSidebar = async (): Promise<void> => {
270+
if (!src) {
271+
showToast({
272+
severity: "warning",
273+
title: "Cannot open node",
274+
description: "No source file linked",
275+
});
276+
return;
277+
}
278+
try {
279+
const linkedFile = await resolveLinkedFileFromSrc({
280+
app,
281+
canvasFile,
282+
src,
283+
});
284+
285+
if (!linkedFile) {
286+
showToast({
287+
severity: "warning",
288+
title: "Cannot open node",
289+
description: "Linked file not found",
290+
});
291+
return;
292+
}
293+
294+
await openFileInSidebar(app, linkedFile);
295+
editor.selectNone();
296+
} catch (error) {
297+
console.error("Error opening linked file:", error);
298+
showToast({
299+
severity: "error",
300+
title: "Error",
301+
description: "Failed to open linked file",
302+
});
303+
}
304+
};
305+
258306
return (
259307
<div
260308
style={{
261309
backgroundColor: nodeType?.color ?? "",
262310
}}
263-
// NOTE: These Tailwind classes (p-2, border-2, rounded-md, m-1, text-base, m-0, text-sm)
311+
// NOTE: These Tailwind classes (p-2, border-2, rounded-md, m-1, text-base, m-0, text-sm)
264312
// correspond to constants in nodeConstants.ts. If you change these classes, update the
265313
// constants and the measureNodeText function to keep measurements accurate.
266-
className="box-border flex h-full w-full flex-col items-start justify-start rounded-md border-2 p-2"
314+
className="relative box-border flex h-full w-full flex-col items-start justify-center rounded-md border-2 p-2"
267315
>
268-
<h1 className="m-1 text-base">{title || "..."}</h1>
316+
{isHovered && (
317+
<button
318+
onClick={(e) => {
319+
e.stopPropagation();
320+
void handleOpenInSidebar();
321+
}}
322+
onPointerDown={(e) => {
323+
e.stopPropagation();
324+
e.preventDefault();
325+
}}
326+
onPointerUp={(e) => {
327+
e.stopPropagation();
328+
}}
329+
className="absolute left-1 top-1 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded border border-black/10 bg-white/90 p-1 shadow-sm transition-all duration-200 hover:bg-white"
330+
style={{
331+
pointerEvents: "auto",
332+
}}
333+
title="Open in sidebar"
334+
>
335+
<svg
336+
xmlns="http://www.w3.org/2000/svg"
337+
width="16"
338+
height="16"
339+
viewBox="0 0 24 24"
340+
fill="none"
341+
stroke="currentColor"
342+
strokeWidth="2"
343+
strokeLinecap="round"
344+
strokeLinejoin="round"
345+
>
346+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
347+
<line x1="15" y1="3" x2="15" y2="21" />
348+
</svg>
349+
</button>
350+
)}
351+
<h1 className="m-0 text-base">{title || "..."}</h1>
269352
<p className="m-0 text-sm opacity-80">{nodeType?.name || ""}</p>
270353
{shape.props.imageSrc ? (
271354
<div className="mt-2 flex min-h-0 w-full flex-1 items-center justify-center overflow-hidden">
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { App, TFile } from "obsidian";
2+
3+
export const openFileInSidebar = async (app: App, file: TFile): Promise<void> => {
4+
const rightSplit = app.workspace.rightSplit;
5+
const rightLeaf = app.workspace.getRightLeaf(false);
6+
7+
if (rightLeaf) {
8+
if (rightSplit && rightSplit.collapsed) {
9+
rightSplit.expand();
10+
}
11+
await rightLeaf.openFile(file);
12+
app.workspace.setActiveLeaf(rightLeaf);
13+
} else {
14+
const leaf = app.workspace.getLeaf("split", "vertical");
15+
await leaf.openFile(file);
16+
app.workspace.setActiveLeaf(leaf);
17+
}
18+
};
19+
20+
export const openFileInNewTab = async (app: App, file: TFile): Promise<void> => {
21+
const leaf = app.workspace.getLeaf("tab");
22+
await leaf.openFile(file);
23+
app.workspace.setActiveLeaf(leaf);
24+
};
25+

src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ export const DEFAULT_NODE_TYPES: Record<string, DiscourseNode> = {
1313
name: "Claim",
1414
format: "CLM - {content}",
1515
color: "#7DA13E",
16-
tag: "#clm-candidate",
16+
tag: "clm-candidate",
1717
},
1818
Evidence: {
1919
id: generateUid("node"),
2020
name: "Evidence",
2121
format: "EVD - {content}",
2222
color: "#DB134A",
23-
tag: "#evd-candidate",
23+
tag: "evd-candidate",
2424
},
2525
};
2626
export const DEFAULT_RELATION_TYPES: Record<string, DiscourseRelationType> = {

src/utils/tagNodeHandler.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,11 +379,75 @@ export class TagNodeHandler {
379379
}
380380

381381
let hoverTimeout: number | null = null;
382+
let currentMouseY = 0;
383+
let currentMouseX = 0;
384+
385+
// Track mouse position to determine which part of multi-line tag is hovered
386+
const handleMouseMove = (e: MouseEvent) => {
387+
currentMouseY = e.clientY;
388+
currentMouseX = e.clientX;
389+
390+
// Update tooltip position if it's already visible
391+
if (this.currentTooltip) {
392+
updateTooltipPosition();
393+
}
394+
};
395+
396+
const getClosestRect = (): DOMRect => {
397+
const range = document.createRange();
398+
range.selectNodeContents(tagElement);
399+
const clientRects = range.getClientRects();
400+
401+
if (clientRects.length > 0) {
402+
// If tag spans multiple lines, find the rect closest to mouse position
403+
if (clientRects.length > 1) {
404+
let closestRect: DOMRect | null = null;
405+
let minDistance = Infinity;
406+
407+
for (let i = 0; i < clientRects.length; i++) {
408+
const r = clientRects.item(i);
409+
if (!r) continue;
410+
411+
// Calculate distance from mouse position to center of this rect
412+
const rectCenterY = r.top + r.height / 2;
413+
const rectCenterX = r.left + r.width / 2;
414+
const distanceY = Math.abs(currentMouseY - rectCenterY);
415+
const distanceX = Math.abs(currentMouseX - rectCenterX);
416+
// Weight Y distance more heavily since we care more about vertical proximity
417+
const distance = distanceY * 2 + distanceX;
418+
419+
if (distance < minDistance) {
420+
minDistance = distance;
421+
closestRect = r;
422+
}
423+
}
424+
425+
return (
426+
closestRect ||
427+
clientRects.item(clientRects.length - 1) ||
428+
tagElement.getBoundingClientRect()
429+
);
430+
} else {
431+
// Single line tag - use the only rect
432+
return clientRects.item(0) || tagElement.getBoundingClientRect();
433+
}
434+
}
435+
436+
return tagElement.getBoundingClientRect();
437+
};
438+
439+
const updateTooltipPosition = () => {
440+
if (!this.currentTooltip) return;
441+
442+
const rect = getClosestRect();
443+
this.currentTooltip.style.top = `${rect.top - TOOLTIP_OFFSET}px`;
444+
this.currentTooltip.style.left = `${rect.left + rect.width / 2}px`;
445+
};
382446

383447
const showTooltip = () => {
384448
if (this.currentTooltip) return;
385449

386-
const rect = tagElement.getBoundingClientRect();
450+
const rect = getClosestRect();
387451

388452
this.currentTooltip = document.createElement("div");
389453
this.currentTooltip.className = "discourse-tag-popover";
@@ -443,6 +507,8 @@ export class TagNodeHandler {
443507
hoverTimeout = window.setTimeout(showTooltip, HOVER_DELAY);
444508
});
445509

510+
tagElement.addEventListener("mousemove", handleMouseMove);
511+
446512
tagElement.addEventListener("mouseleave", (e) => {
447513
if (hoverTimeout) {
448514
clearTimeout(hoverTimeout);
@@ -456,6 +522,7 @@ export class TagNodeHandler {
456522
});
457523

458524
const cleanup = () => {
525+
tagElement.removeEventListener("mousemove", handleMouseMove);
459526
if (hoverTimeout) {
460527
clearTimeout(hoverTimeout);
461528
}

0 commit comments

Comments
 (0)