Add vault graph poster to Obsidian Wrapped
- parse.worker collects edges, builds top-150-by-degree graph (links capped 600) - graphPoster.ts renders force-graph on off-screen canvas -> PNG (DS-2 colors), node names never leave the browser - WrappedClient: Карточка/Граф toggle, optional node labels, lazy-load force-graph - THIRD-PARTY-NOTICES for force-graph (MIT) and next/og satori/resvg (MPL-2.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Third-Party Notices
|
||||
|
||||
This product bundles third-party software. Their licenses and copyright
|
||||
notices are reproduced below as required.
|
||||
|
||||
## force-graph
|
||||
|
||||
Used client-side in the "Obsidian Wrapped" feature to render the vault graph
|
||||
poster on an off-screen canvas. The library and its node names never leave the
|
||||
browser.
|
||||
|
||||
- Version: 1.51.4
|
||||
- License: MIT
|
||||
- Copyright (c) 2018 Vasco Asturiano
|
||||
- https://github.com/vasturiano/force-graph
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Vasco Asturiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
force-graph depends on the d3-force family (ISC) by Mike Bostock for its
|
||||
force simulation, bundled transitively.
|
||||
|
||||
## next/og (Satori + resvg)
|
||||
|
||||
Server-side share-card PNG rendering (`ImageResponse`) uses Vercel's `next/og`,
|
||||
which bundles `satori` and `@resvg/resvg-wasm` (both MPL-2.0). These are part of
|
||||
the `next` dependency and distributed under Next.js's own license terms.
|
||||
|
||||
- satori — MPL-2.0 — https://github.com/vercel/satori
|
||||
- resvg-wasm — MPL-2.0 — https://github.com/yisibl/resvg-js
|
||||
Generated
+346
@@ -29,6 +29,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"disposable-email-domains": "^1.0.62",
|
||||
"force-graph": "^1.51.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
@@ -4522,6 +4523,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
@@ -5245,6 +5252,15 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/accessor-fn": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
|
||||
"integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
@@ -5730,6 +5746,16 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bezier-js": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
|
||||
"integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||
@@ -5916,6 +5942,18 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvas-color-tracker": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz",
|
||||
"integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinycolor2": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -6094,6 +6132,222 @@
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force-3d": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
||||
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-binarytree": "1",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-octree": "1",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-octree": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
|
||||
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||
@@ -7283,6 +7537,20 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/float-tooltip": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz",
|
||||
"integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-selection": "2 - 3",
|
||||
"kapsule": "^1.16",
|
||||
"preact": "10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -7299,6 +7567,32 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/force-graph": {
|
||||
"version": "1.51.4",
|
||||
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.4.tgz",
|
||||
"integrity": "sha512-TdJ2KbkoiDQ7NIRx8IPGD0mAXXpLhamS7c+b7W98b0MHG7lphnda1VOQX/98UDTsttIAdH4TcP0l0MauSnLK8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "18 - 25",
|
||||
"accessor-fn": "1",
|
||||
"bezier-js": "3 - 6",
|
||||
"canvas-color-tracker": "^1.3",
|
||||
"d3-array": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-force-3d": "2 - 3",
|
||||
"d3-scale": "1 - 4",
|
||||
"d3-scale-chromatic": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-zoom": "2 - 3",
|
||||
"float-tooltip": "^1.7",
|
||||
"index-array-by": "1",
|
||||
"kapsule": "^1.16",
|
||||
"lodash-es": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -7777,6 +8071,15 @@
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/index-array-by": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
|
||||
"integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@@ -7792,6 +8095,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -8369,6 +8681,18 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kapsule": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz",
|
||||
"integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -8735,6 +9059,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -10118,6 +10448,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.29.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz",
|
||||
"integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -11362,6 +11702,12 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"disposable-email-domains": "^1.0.62",
|
||||
"force-graph": "^1.51.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"lucide-react": "^1.7.0",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Stats, RawNote, Period, RunPayload } from "./_client/types";
|
||||
import { supportsFSA, readViaFSA, readViaInput, readViaDrop } from "./_client/reader";
|
||||
|
||||
type Screen = "intro" | "loading" | "result" | "error";
|
||||
type View = "card" | "graph";
|
||||
|
||||
const ARCHETYPE_RU: Record<string, string> = {
|
||||
architect: "🏛 Архитектор",
|
||||
@@ -21,14 +22,49 @@ export function WrappedClient({ userName }: { userName: string }) {
|
||||
const [error, setError] = useState("");
|
||||
const [shareUrl, setShareUrl] = useState("");
|
||||
const [sharing, setSharing] = useState(false);
|
||||
const [view, setView] = useState<View>("card");
|
||||
const [graphUrl, setGraphUrl] = useState("");
|
||||
const [graphLabels, setGraphLabels] = useState(false);
|
||||
const [graphBusy, setGraphBusy] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const notesRef = useRef<RawNote[]>([]);
|
||||
|
||||
// Сбросить отрисованный граф (при новых данных / смене периода).
|
||||
const resetGraph = useCallback(() => {
|
||||
setView("card");
|
||||
setGraphUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return "";
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Отрисовать граф-постер в офф-скрин Canvas → blob URL. force-graph грузим лениво.
|
||||
const renderGraph = useCallback(
|
||||
async (labels: boolean) => {
|
||||
if (!stats || !stats.graph.nodes.length) return;
|
||||
setGraphBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
const { renderGraphPoster } = await import("./_client/graphPoster");
|
||||
const blob = await renderGraphPoster(stats.graph, labels);
|
||||
setGraphUrl((prev) => {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return URL.createObjectURL(blob);
|
||||
});
|
||||
} catch {
|
||||
setError("Не удалось построить граф");
|
||||
}
|
||||
setGraphBusy(false);
|
||||
},
|
||||
[stats],
|
||||
);
|
||||
|
||||
const runWorker = useCallback((notes: RawNote[], p: Period) => {
|
||||
setScreen("loading");
|
||||
const worker = new Worker(new URL("./_client/parse.worker.ts", import.meta.url), { type: "module" });
|
||||
worker.onmessage = (e: MessageEvent<Stats>) => {
|
||||
setStats(e.data);
|
||||
resetGraph();
|
||||
setScreen("result");
|
||||
worker.terminate();
|
||||
};
|
||||
@@ -38,7 +74,7 @@ export function WrappedClient({ userName }: { userName: string }) {
|
||||
worker.terminate();
|
||||
};
|
||||
worker.postMessage({ notes, period: p });
|
||||
}, []);
|
||||
}, [resetGraph]);
|
||||
|
||||
const handleNotes = useCallback(
|
||||
(notes: RawNote[]) => {
|
||||
@@ -174,7 +210,7 @@ export function WrappedClient({ userName }: { userName: string }) {
|
||||
{screen === "result" && stats && (
|
||||
<div className="space-y-4">
|
||||
{/* переключатель периода */}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{(["year", "quarter", "all"] as Period[]).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
@@ -190,9 +226,68 @@ export function WrappedClient({ userName }: { userName: string }) {
|
||||
{PERIOD_RU[p]}
|
||||
</button>
|
||||
))}
|
||||
{/* переключатель вида: карточка / граф */}
|
||||
<div className="flex gap-2 ml-auto">
|
||||
{(["card", "graph"] as View[]).map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => {
|
||||
setView(v);
|
||||
if (v === "graph" && !graphUrl && !graphBusy) renderGraph(graphLabels);
|
||||
}}
|
||||
className="text-xs px-3 py-1.5"
|
||||
style={{
|
||||
border: "2px solid var(--foreground)",
|
||||
borderRadius: 2,
|
||||
background: view === v ? "var(--accent)" : "var(--background)",
|
||||
fontWeight: view === v ? 700 : 400,
|
||||
}}
|
||||
>
|
||||
{v === "card" ? "Карточка" : "Граф"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* граф-постер хранилища */}
|
||||
{view === "graph" && (
|
||||
<div className="card-aubade p-6 space-y-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="font-bold text-xl">Граф твоего хранилища</h2>
|
||||
<label className="text-xs flex items-center gap-1.5" style={{ color: "var(--muted-foreground)" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={graphLabels}
|
||||
onChange={(e) => { setGraphLabels(e.target.checked); renderGraph(e.target.checked); }}
|
||||
/>
|
||||
подписи узлов
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||
{stats.graph.nodes.length} самых связанных заметок из твоего хранилища. Имена остаются в браузере.
|
||||
</p>
|
||||
<div className="flex items-center justify-center" style={{ border: "2px solid var(--border)", borderRadius: 2, minHeight: 280, background: "#F5F5F0" }}>
|
||||
{graphBusy ? (
|
||||
<p className="text-sm py-12" style={{ color: "var(--muted-foreground)" }}>Строю граф…</p>
|
||||
) : graphUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={graphUrl} alt="Граф хранилища" className="w-full h-auto" style={{ borderRadius: 2 }} />
|
||||
) : stats.graph.nodes.length === 0 ? (
|
||||
<p className="text-sm py-12 px-4 text-center" style={{ color: "var(--muted-foreground)" }}>
|
||||
Слишком мало связей между заметками, чтобы построить граф.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{graphUrl && (
|
||||
<a href={graphUrl} download="obsidian-graph.png" className="btn-aubade text-sm inline-block">
|
||||
Скачать граф (PNG)
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DOM-карточка результата (ДС-2) */}
|
||||
{view === "card" && (
|
||||
<div className="card-aubade p-6">
|
||||
<h2 className="font-bold text-xl mb-1">Ты — {ARCHETYPE_RU[stats.archetype]}</h2>
|
||||
<p className="text-xs uppercase tracking-widest mb-4" style={{ color: "var(--muted-foreground)" }}>
|
||||
@@ -226,13 +321,14 @@ export function WrappedClient({ userName }: { userName: string }) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* действия */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<button onClick={share} disabled={sharing} className="btn-aubade btn-aubade-accent text-sm flex-1">
|
||||
{sharing ? "Создаю ссылку…" : "Поделиться карточкой"}
|
||||
</button>
|
||||
<button onClick={() => { setScreen("intro"); setStats(null); setShareUrl(""); }} className="btn-aubade text-sm flex-1">
|
||||
<button onClick={() => { setScreen("intro"); setStats(null); setShareUrl(""); resetGraph(); }} className="btn-aubade text-sm flex-1">
|
||||
Другое хранилище
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Граф-постер: force-graph (Canvas) в офф-скрин контейнере → PNG. ДС-2-окрас.
|
||||
// 100% client-side — имена узлов остаются в браузере, на сервер не уходят.
|
||||
|
||||
import ForceGraph from "force-graph";
|
||||
import type { GraphData } from "./types";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
const SIZE = 1080;
|
||||
const BG = "#F5F5F0";
|
||||
const NODE = "#323232";
|
||||
const LINK = "#AAAAAA";
|
||||
|
||||
export async function renderGraphPoster(graph: GraphData, labels: boolean): Promise<Blob> {
|
||||
const el = document.createElement("div");
|
||||
el.style.cssText = "position:fixed;left:-99999px;top:0;width:1080px;height:1080px;";
|
||||
document.body.appendChild(el);
|
||||
|
||||
try {
|
||||
const fg = (ForceGraph as any)()(el)
|
||||
.width(SIZE)
|
||||
.height(SIZE)
|
||||
.backgroundColor(BG)
|
||||
.graphData({
|
||||
nodes: graph.nodes.map((n) => ({ ...n })),
|
||||
links: graph.links.map((l) => ({ ...l })),
|
||||
})
|
||||
.nodeId("id")
|
||||
.nodeRelSize(labels ? 3 : 2.5)
|
||||
.nodeVal((n: any) => 1 + Math.sqrt(n.deg ?? 1))
|
||||
.nodeColor(() => NODE)
|
||||
.linkColor(() => LINK)
|
||||
.linkWidth(0.4)
|
||||
.enableNodeDrag(false)
|
||||
.enablePointerInteraction(false)
|
||||
.cooldownTime(3500);
|
||||
|
||||
if (labels) {
|
||||
fg.nodeCanvasObjectMode(() => "after").nodeCanvasObject((node: any, ctx: CanvasRenderingContext2D, scale: number) => {
|
||||
if ((node.deg ?? 0) >= 6) {
|
||||
const fontSize = 11 / scale;
|
||||
ctx.font = `${fontSize}px "Fira Mono", monospace`;
|
||||
ctx.fillStyle = NODE;
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(String(node.id), node.x + 2 + Math.sqrt(node.deg) / scale, node.y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// дождаться остановки симуляции (или таймаут)
|
||||
await new Promise<void>((resolve) => {
|
||||
let done = false;
|
||||
const finish = () => { if (!done) { done = true; resolve(); } };
|
||||
fg.onEngineStop(finish);
|
||||
setTimeout(finish, 5000);
|
||||
});
|
||||
|
||||
fg.zoomToFit(0, 50);
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const canvas = el.querySelector("canvas") as HTMLCanvasElement | null;
|
||||
if (!canvas) throw new Error("canvas not found");
|
||||
return await new Promise<Blob>((resolve, reject) =>
|
||||
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("toBlob failed"))), "image/png"),
|
||||
);
|
||||
} finally {
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Web Worker: парсит .md-тексты в агрегаты (off-main, без фриза UI).
|
||||
// Получает RawNote[], возвращает Stats. Тексты заметок дальше воркера не идут.
|
||||
|
||||
import type { RawNote, Stats, Period, Archetype } from "./types";
|
||||
import type { RawNote, Stats, Period, Archetype, GraphData } from "./types";
|
||||
|
||||
const WIKILINK = /\[\[([^\]|#]+)/g;
|
||||
const TAG = /(^|\s)#([\p{L}\d_/-]+)/gu;
|
||||
@@ -13,12 +13,22 @@ function filterByPeriod(notes: RawNote[], period: Period): RawNote[] {
|
||||
return notes.filter((n) => n.mtime >= cutoff);
|
||||
}
|
||||
|
||||
function buildGraph(degree: Map<string, number>, edges: { source: string; target: string }[]): GraphData {
|
||||
const TOP = 150;
|
||||
const top = [...degree.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOP);
|
||||
const nodes = top.map(([id, deg]) => ({ id, deg }));
|
||||
const ids = new Set(nodes.map((n) => n.id));
|
||||
const links = edges.filter((e) => ids.has(e.source) && ids.has(e.target)).slice(0, 600);
|
||||
return { nodes, links };
|
||||
}
|
||||
|
||||
function buildStats(
|
||||
notes: RawNote[],
|
||||
degree: Map<string, number>,
|
||||
tags: Map<string, number>,
|
||||
linkCount: number,
|
||||
period: Period,
|
||||
edges: { source: string; target: string }[],
|
||||
): Stats {
|
||||
const noteCount = notes.length;
|
||||
const topTags = [...tags.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tag, n]) => ({ tag, n }));
|
||||
@@ -57,7 +67,7 @@ function buildStats(
|
||||
if (linkDensity >= 3) achievements.push("Плотная сеть знаний");
|
||||
if (orphanRatio < 0.1 && noteCount >= 50) achievements.push("Почти нет сирот");
|
||||
|
||||
return { noteCount, linkCount, tagCount: tags.size, topTags, topLinked, orphans, busiestMonth, achievements, archetype, score, period };
|
||||
return { noteCount, linkCount, tagCount: tags.size, topTags, topLinked, orphans, busiestMonth, achievements, archetype, score, period, graph: buildGraph(degree, edges) };
|
||||
}
|
||||
|
||||
self.onmessage = (e: MessageEvent<{ notes: RawNote[]; period: Period }>) => {
|
||||
@@ -66,16 +76,21 @@ self.onmessage = (e: MessageEvent<{ notes: RawNote[]; period: Period }>) => {
|
||||
const degree = new Map<string, number>();
|
||||
const tags = new Map<string, number>();
|
||||
let linkCount = 0;
|
||||
const edges: { source: string; target: string }[] = [];
|
||||
for (const n of filtered) {
|
||||
const src = n.name.replace(/\.md$/i, "");
|
||||
for (const m of n.text.matchAll(WIKILINK)) {
|
||||
linkCount++;
|
||||
const t = m[1].trim();
|
||||
if (t) degree.set(t, (degree.get(t) ?? 0) + 1);
|
||||
if (t) {
|
||||
degree.set(t, (degree.get(t) ?? 0) + 1);
|
||||
edges.push({ source: src, target: t });
|
||||
}
|
||||
}
|
||||
for (const m of n.text.matchAll(TAG)) {
|
||||
const t = m[2];
|
||||
tags.set(t, (tags.get(t) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
self.postMessage(buildStats(filtered, degree, tags, linkCount, period));
|
||||
self.postMessage(buildStats(filtered, degree, tags, linkCount, period, edges));
|
||||
};
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
export type Archetype = "architect" | "gardener" | "librarian" | "pragmatist";
|
||||
export type Period = "year" | "quarter" | "all";
|
||||
|
||||
// Граф для постера (топ-N узлов по степени). Имена живут только в браузере.
|
||||
export interface GraphData {
|
||||
nodes: { id: string; deg: number }[];
|
||||
links: { source: string; target: string }[];
|
||||
}
|
||||
|
||||
export interface Stats {
|
||||
noteCount: number;
|
||||
linkCount: number;
|
||||
@@ -17,6 +23,7 @@ export interface Stats {
|
||||
archetype: Archetype;
|
||||
score: number; // 0..100
|
||||
period: Period;
|
||||
graph: GraphData; // топ-N узлов/рёбер для граф-постера (не уходит в БД)
|
||||
}
|
||||
|
||||
// Сырьё — НИКОГДА не покидает браузер.
|
||||
|
||||
Reference in New Issue
Block a user