Fix Obsidian review issues and ship fully free

- Bump minAppVersion to 1.7.2 (covers addAction/revealLeaf) + add versions.json
- Obsidian API hygiene: createEl/createDiv, typed d3 callbacks, drop builtin-modules
- Add release workflow with GitHub artifact attestation
- Remove node-count gate and all Pro/Locked/Upgrade wording
- Bump version to 0.1.2
This commit is contained in:
yashmurty 2026-05-31 21:50:34 +09:00
parent 7540b03186
commit 5c7c0bef20
13 changed files with 82 additions and 79 deletions

45
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,45 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
permissions:
contents: write # create the GitHub release
id-token: write # required for attestation signing
attestations: write # write artifact attestations
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Generate artifact attestations
uses: actions/attest-build-provenance@v2
with:
subject-path: |
main.js
manifest.json
styles.css
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
gh release create "$TAG" \
--title="$TAG" \
--generate-notes \
main.js manifest.json styles.css

View file

@ -88,10 +88,11 @@ photo: attachments/me.jpg
| Edge opacity | Opacity of relationship lines | 0.4 |
| Show closeness ring | Color node ring by closeness score | On |
## Pro (Coming Soon)
## Roadmap
- Unlimited people (free tier: 20 interactive nodes)
- Export graph as PNG/SVG
Ideas planned for future releases:
- Export graph as SVG (PNG export is already available from the view toolbar)
- Filter view by tag or company
- Color themes

View file

@ -1,6 +1,6 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { builtinModules } from "node:module";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@ -30,7 +30,8 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
...builtinModules,
...builtinModules.map((m) => `node:${m}`),
],
format: "cjs",
target: "es2018",

View file

@ -1,8 +1,8 @@
{
"id": "people-graph",
"name": "People Graph",
"version": "0.1.1",
"minAppVersion": "1.0.0",
"version": "0.1.2",
"minAppVersion": "1.7.2",
"description": "A visual force-directed graph of people and friends in your vault with photo nodes, closeness-based positioning, and company clustering.",
"author": "yashmurty",
"isDesktopOnly": false

18
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-people-graph",
"version": "0.1.0",
"version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-people-graph",
"version": "0.1.0",
"version": "0.1.2",
"license": "MIT",
"dependencies": {
"d3": "^7.9.0"
@ -14,7 +14,6 @@
"devDependencies": {
"@types/d3": "^7.4.3",
"@types/node": "^22.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.0",
"obsidian": "latest",
"tslib": "2.8.1",
@ -807,19 +806,6 @@
"@types/estree": "*"
}
},
"node_modules/builtin-modules": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-people-graph",
"version": "0.1.1",
"version": "0.1.2",
"description": "A visual force-directed graph of people and friends in your vault",
"main": "main.js",
"scripts": {
@ -13,7 +13,6 @@
"devDependencies": {
"@types/d3": "^7.4.3",
"@types/node": "^22.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.24.0",
"obsidian": "latest",
"tslib": "2.8.1",

View file

@ -38,8 +38,7 @@ export class PeopleGraphView extends ItemView {
return;
}
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
const a = createEl("a", { href: url });
a.download = "people-graph.png";
a.click();
URL.revokeObjectURL(url);

View file

@ -26,7 +26,7 @@ export async function exportGraphAsPng(container: HTMLElement): Promise<Blob | n
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement("canvas");
const canvas = createEl("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d")!;

View file

@ -1,5 +1,5 @@
import * as d3 from "d3";
import { Notice, TFile } from "obsidian";
import { TFile } from "obsidian";
import type { App } from "obsidian";
import type { PersonNode, PeopleGraphSettings } from "../types";
@ -47,7 +47,7 @@ export function renderGraph(
container.empty();
if (people.length === 0) {
const msg = container.createEl("div", { cls: "people-graph-empty" });
const msg = container.createDiv({ cls: "people-graph-empty" });
msg.createEl("h4", { text: "No people found" });
msg.createEl("p", {
text: `Looking for notes where frontmatter field "${settings.personField}" = "${settings.personValue}".`,
@ -141,8 +141,8 @@ export function renderGraph(
const zoom = d3
.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.2, 5])
.on("zoom", (event) => {
g.attr("transform", event.transform);
.on("zoom", (event: d3.D3ZoomEvent<SVGSVGElement, unknown>) => {
g.attr("transform", event.transform.toString());
});
svg.call(zoom);
@ -184,18 +184,18 @@ export function renderGraph(
.call(
d3
.drag<SVGGElement, SimNode>()
.on("start", (event, d) => {
.on("start", (event: d3.D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
if (d.isCenter) return;
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
})
.on("drag", (event, d) => {
.on("drag", (event: d3.D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
if (d.isCenter) return;
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
.on("end", (event: d3.D3DragEvent<SVGGElement, SimNode, SimNode>, d) => {
if (d.isCenter) return;
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
@ -203,17 +203,7 @@ export function renderGraph(
}),
);
// Free tier: dim nodes beyond limit (skip center node)
const maxFree = settings.maxFreeNodes;
const nodeElements = nodeSelection.nodes();
let personIndex = 0;
nodeSelection.each((d, i) => {
if (d.isCenter) return;
if (personIndex >= maxFree) {
d3.select(nodeElements[i]).attr("opacity", 0.3);
}
personIndex++;
});
// Render node visuals
nodeSelection.each((d, i) => {
@ -299,30 +289,22 @@ export function renderGraph(
});
// Labels (skip center — it has its own)
let labelIndex = 0;
const nonCenterNodes = nodeSelection.filter((d) => !d.isCenter);
// Name
nonCenterNodes
.append("text")
.text((d) => {
const i = labelIndex++;
return i >= maxFree ? "Locked" : d.person.name;
})
.text((d) => d.person.name)
.attr("text-anchor", "middle")
.attr("dy", NODE_RADIUS + 14)
.attr("font-size", "11px")
.attr("fill", "#333333");
// Company
let companyIndex = 0;
nonCenterNodes
.filter((d) => !!(d.person.company))
.append("text")
.text((d) => {
const i = companyIndex++;
return i >= maxFree ? "" : d.person.company!;
})
.text((d) => d.person.company!)
.attr("text-anchor", "middle")
.attr("dy", NODE_RADIUS + 27)
.attr("font-size", "9px")
@ -335,22 +317,16 @@ export function renderGraph(
.attr("class", "people-graph-tooltip");
nodeSelection
.on("mouseenter", (event, d) => {
.on("mouseenter", (_event, d) => {
if (d.isCenter) return;
const nodeIndex = nodes.indexOf(d) - 1;
let lines: string[];
if (nodeIndex >= maxFree) {
lines = ["Upgrade to Pro to unlock this node"];
} else {
lines = [d.person.name];
if (d.person.company) lines.push(`Company: ${d.person.company}`);
if (d.person.role) lines.push(`Role: ${d.person.role}`);
lines.push(`Closeness: ${d.person.closeness}/10`);
}
const lines = [d.person.name];
if (d.person.company) lines.push(`Company: ${d.person.company}`);
if (d.person.role) lines.push(`Role: ${d.person.role}`);
lines.push(`Closeness: ${d.person.closeness}/10`);
tooltip.html(lines.join("<br>")).style("display", "block");
})
.on("mousemove", (event) => {
.on("mousemove", (event: MouseEvent) => {
const rect = container.getBoundingClientRect();
tooltip
.style("left", `${event.clientX - rect.left + 12}px`)
@ -360,16 +336,9 @@ export function renderGraph(
tooltip.style("display", "none");
});
// Click to open note (or show upgrade prompt for locked nodes)
// Click to open the person's note
nodeSelection.on("click", (_event, d) => {
if (d.isCenter) return;
const nodeIndex = nodes.indexOf(d) - 1; // -1 to skip center node
if (nodeIndex >= maxFree) {
new Notice(
`Upgrade to Pro to unlock all people nodes (currently limited to ${maxFree}).`,
);
return;
}
const file = app.vault.getAbstractFileByPath(d.person.id);
if (file instanceof TFile) {
void app.workspace.getLeaf("tab").openFile(file);

View file

@ -20,7 +20,7 @@ export async function indexPeople(app: App, settings: PeopleGraphSettings): Prom
const cache = app.metadataCache.getFileCache(file);
if (!cache?.frontmatter) continue;
const fm = cache.frontmatter;
const fm = cache.frontmatter as Record<string, unknown>;
if (!matchesPersonFilter(fm, settings.personField, settings.personValue)) continue;
const name = fm.name ?? file.basename;

View file

@ -34,7 +34,8 @@ export default class PeopleGraphPlugin extends Plugin {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const data = (await this.loadData()) as Partial<PeopleGraphSettings> | null;
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
}
async saveSettings() {

View file

@ -22,7 +22,6 @@ export interface PeopleGraphSettings {
edgeOpacity: number;
showClosenessRing: boolean;
centerLabel: string;
maxFreeNodes: number;
}
export const DEFAULT_SETTINGS: PeopleGraphSettings = {
@ -36,5 +35,4 @@ export const DEFAULT_SETTINGS: PeopleGraphSettings = {
edgeOpacity: 0.4,
showClosenessRing: true,
centerLabel: "You",
maxFreeNodes: 20,
};

4
versions.json Normal file
View file

@ -0,0 +1,4 @@
{
"0.1.1": "1.0.0",
"0.1.2": "1.7.2"
}