@openparachute/vault 0.2.3 → 0.2.4
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/.claude/settings.local.json +31 -0
- package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +1 -0
- package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +2 -0
- package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +211 -0
- package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +59 -0
- package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +232 -0
- package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +182 -0
- package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +91 -0
- package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +70 -0
- package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +59 -0
- package/CHANGELOG.md +6 -0
- package/core/src/core.test.ts +12 -0
- package/core/src/notes.ts +4 -0
- package/core/src/types.ts +1 -0
- package/package.json +1 -1
- package/religions-abrahamic-filter.png +0 -0
- package/religions-buddhism-v2.png +0 -0
- package/religions-buddhism.png +0 -0
- package/religions-final.png +0 -0
- package/religions-v1.png +0 -0
- package/religions-v2.png +0 -0
- package/religions-zen.png +0 -0
- package/web/README.md +73 -0
- package/web/bun.lock +827 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +15 -0
- package/web/package.json +36 -0
- package/web/public/favicon.svg +1 -0
- package/web/public/icons.svg +24 -0
- package/web/src/App.tsx +149 -0
- package/web/src/Graph.tsx +200 -0
- package/web/src/NoteView.tsx +155 -0
- package/web/src/Sidebar.tsx +186 -0
- package/web/src/api.ts +21 -0
- package/web/src/index.css +50 -0
- package/web/src/main.tsx +10 -0
- package/web/src/types.ts +37 -0
- package/web/src/utils.ts +107 -0
- package/web/tsconfig.app.json +25 -0
- package/web/tsconfig.json +7 -0
- package/web/tsconfig.node.json +24 -0
- package/web/vite.config.ts +16 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
package/web/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Religions of the World</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Newsreader:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
package/web/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
14
|
+
"@types/d3": "^7.4.3",
|
|
15
|
+
"d3": "^7.9.0",
|
|
16
|
+
"react": "^19.2.4",
|
|
17
|
+
"react-dom": "^19.2.4",
|
|
18
|
+
"react-markdown": "^10.1.0",
|
|
19
|
+
"remark-gfm": "^4.0.1",
|
|
20
|
+
"tailwindcss": "^4.2.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@eslint/js": "^9.39.4",
|
|
24
|
+
"@types/node": "^24.12.2",
|
|
25
|
+
"@types/react": "^19.2.14",
|
|
26
|
+
"@types/react-dom": "^19.2.3",
|
|
27
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
28
|
+
"eslint": "^9.39.4",
|
|
29
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
30
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
31
|
+
"globals": "^17.4.0",
|
|
32
|
+
"typescript": "~6.0.2",
|
|
33
|
+
"typescript-eslint": "^8.58.0",
|
|
34
|
+
"vite": "^8.0.4"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
3
|
+
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
4
|
+
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
5
|
+
</symbol>
|
|
6
|
+
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
7
|
+
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
8
|
+
</symbol>
|
|
9
|
+
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
10
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
11
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
12
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
13
|
+
</symbol>
|
|
14
|
+
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
15
|
+
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
16
|
+
</symbol>
|
|
17
|
+
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
18
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
19
|
+
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
20
|
+
</symbol>
|
|
21
|
+
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
22
|
+
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
23
|
+
</symbol>
|
|
24
|
+
</svg>
|
package/web/src/App.tsx
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
+
import { fetchAllNotes } from "./api";
|
|
3
|
+
import { buildGraph, extractWikilinks } from "./utils";
|
|
4
|
+
import type { Note } from "./types";
|
|
5
|
+
import Graph from "./Graph";
|
|
6
|
+
import NoteView from "./NoteView";
|
|
7
|
+
import Sidebar from "./Sidebar";
|
|
8
|
+
|
|
9
|
+
export default function App() {
|
|
10
|
+
const [notes, setNotes] = useState<Note[]>([]);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
13
|
+
const [activeFilter, setActiveFilter] = useState<string | null>(null);
|
|
14
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
fetchAllNotes().then((data) => {
|
|
18
|
+
setNotes(data);
|
|
19
|
+
setLoading(false);
|
|
20
|
+
});
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const { nodes, edges } = useMemo(() => buildGraph(notes), [notes]);
|
|
24
|
+
|
|
25
|
+
const noteById = useMemo(() => {
|
|
26
|
+
const map = new Map<string, Note>();
|
|
27
|
+
for (const n of notes) map.set(n.id, n);
|
|
28
|
+
return map;
|
|
29
|
+
}, [notes]);
|
|
30
|
+
|
|
31
|
+
const pathToId = useMemo(() => {
|
|
32
|
+
const map = new Map<string, string>();
|
|
33
|
+
for (const n of notes) {
|
|
34
|
+
if (n.path) map.set(n.path, n.id);
|
|
35
|
+
}
|
|
36
|
+
return map;
|
|
37
|
+
}, [notes]);
|
|
38
|
+
|
|
39
|
+
const filteredNodes = useMemo(() => {
|
|
40
|
+
if (!activeFilter) return nodes;
|
|
41
|
+
return nodes.filter((n) => n.tags.includes(activeFilter));
|
|
42
|
+
}, [nodes, activeFilter]);
|
|
43
|
+
|
|
44
|
+
const filteredEdges = useMemo(() => {
|
|
45
|
+
const ids = new Set(filteredNodes.map((n) => n.id));
|
|
46
|
+
return edges.filter((e) => {
|
|
47
|
+
const s = typeof e.source === "string" ? e.source : e.source.id;
|
|
48
|
+
const t = typeof e.target === "string" ? e.target : e.target.id;
|
|
49
|
+
return ids.has(s) && ids.has(t);
|
|
50
|
+
});
|
|
51
|
+
}, [filteredNodes, edges]);
|
|
52
|
+
|
|
53
|
+
const searchResults = useMemo(() => {
|
|
54
|
+
if (!searchQuery.trim()) return [];
|
|
55
|
+
const q = searchQuery.toLowerCase();
|
|
56
|
+
return nodes.filter(
|
|
57
|
+
(n) =>
|
|
58
|
+
n.label.toLowerCase().includes(q) ||
|
|
59
|
+
n.path.toLowerCase().includes(q)
|
|
60
|
+
).slice(0, 20);
|
|
61
|
+
}, [nodes, searchQuery]);
|
|
62
|
+
|
|
63
|
+
const handleNodeClick = useCallback((nodeId: string) => {
|
|
64
|
+
setSelectedId(nodeId);
|
|
65
|
+
setSearchQuery("");
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const handleWikilinkClick = useCallback(
|
|
69
|
+
(path: string) => {
|
|
70
|
+
const id = pathToId.get(path);
|
|
71
|
+
if (id) setSelectedId(id);
|
|
72
|
+
},
|
|
73
|
+
[pathToId],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const selectedNote = selectedId ? noteById.get(selectedId) : null;
|
|
77
|
+
|
|
78
|
+
// Find connected notes for the sidebar "related" section
|
|
79
|
+
const connectedIds = useMemo(() => {
|
|
80
|
+
if (!selectedNote) return [];
|
|
81
|
+
const links = extractWikilinks(selectedNote.content);
|
|
82
|
+
const seen = new Set<string>();
|
|
83
|
+
return links
|
|
84
|
+
.map((p) => pathToId.get(p))
|
|
85
|
+
.filter((id): id is string => {
|
|
86
|
+
if (!id || id === selectedId || seen.has(id)) return false;
|
|
87
|
+
seen.add(id);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
}, [selectedNote, pathToId, selectedId]);
|
|
91
|
+
|
|
92
|
+
if (loading) {
|
|
93
|
+
return (
|
|
94
|
+
<div className="h-screen flex items-center justify-center bg-bg">
|
|
95
|
+
<div className="text-text-muted text-lg animate-pulse">
|
|
96
|
+
Loading the world's religions...
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="h-screen flex overflow-hidden bg-bg">
|
|
104
|
+
{/* Left sidebar */}
|
|
105
|
+
<Sidebar
|
|
106
|
+
nodes={nodes}
|
|
107
|
+
activeFilter={activeFilter}
|
|
108
|
+
onFilterChange={setActiveFilter}
|
|
109
|
+
searchQuery={searchQuery}
|
|
110
|
+
onSearchChange={setSearchQuery}
|
|
111
|
+
searchResults={searchResults}
|
|
112
|
+
onSelect={handleNodeClick}
|
|
113
|
+
selectedId={selectedId}
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
{/* Graph canvas */}
|
|
117
|
+
<div className="flex-1 relative">
|
|
118
|
+
<Graph
|
|
119
|
+
nodes={activeFilter ? filteredNodes : nodes}
|
|
120
|
+
edges={activeFilter ? filteredEdges : edges}
|
|
121
|
+
selectedId={selectedId}
|
|
122
|
+
onNodeClick={handleNodeClick}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
{/* Stats overlay */}
|
|
126
|
+
<div className="absolute bottom-4 left-4 text-xs text-text-muted space-x-4 pointer-events-none select-none">
|
|
127
|
+
<span>{notes.length} notes</span>
|
|
128
|
+
<span>{edges.length} connections</span>
|
|
129
|
+
{activeFilter && (
|
|
130
|
+
<span className="text-accent">
|
|
131
|
+
filtered: {activeFilter}
|
|
132
|
+
</span>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Note reading pane */}
|
|
138
|
+
{selectedNote && (
|
|
139
|
+
<NoteView
|
|
140
|
+
note={selectedNote}
|
|
141
|
+
connectedIds={connectedIds}
|
|
142
|
+
noteById={noteById}
|
|
143
|
+
onClose={() => setSelectedId(null)}
|
|
144
|
+
onNavigate={handleWikilinkClick}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import * as d3 from "d3";
|
|
3
|
+
import type { GraphNode, GraphEdge } from "./types";
|
|
4
|
+
import { nodeColor, nodeRadius } from "./utils";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
nodes: GraphNode[];
|
|
8
|
+
edges: GraphEdge[];
|
|
9
|
+
selectedId: string | null;
|
|
10
|
+
onNodeClick: (id: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function Graph({ nodes, edges, selectedId, onNodeClick }: Props) {
|
|
14
|
+
const svgRef = useRef<SVGSVGElement>(null);
|
|
15
|
+
const simRef = useRef<d3.Simulation<GraphNode, GraphEdge> | null>(null);
|
|
16
|
+
|
|
17
|
+
const onNodeClickRef = useRef(onNodeClick);
|
|
18
|
+
onNodeClickRef.current = onNodeClick;
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const svg = d3.select(svgRef.current!);
|
|
22
|
+
const width = svgRef.current!.clientWidth;
|
|
23
|
+
const height = svgRef.current!.clientHeight;
|
|
24
|
+
|
|
25
|
+
svg.selectAll("*").remove();
|
|
26
|
+
|
|
27
|
+
// Background
|
|
28
|
+
svg
|
|
29
|
+
.append("rect")
|
|
30
|
+
.attr("width", width)
|
|
31
|
+
.attr("height", height)
|
|
32
|
+
.attr("fill", "#0a0a0f");
|
|
33
|
+
|
|
34
|
+
const g = svg.append("g");
|
|
35
|
+
|
|
36
|
+
// Zoom
|
|
37
|
+
const zoom = d3
|
|
38
|
+
.zoom<SVGSVGElement, unknown>()
|
|
39
|
+
.scaleExtent([0.15, 5])
|
|
40
|
+
.on("zoom", (event) => {
|
|
41
|
+
g.attr("transform", event.transform);
|
|
42
|
+
});
|
|
43
|
+
svg.call(zoom);
|
|
44
|
+
|
|
45
|
+
// Center initial view
|
|
46
|
+
svg.call(
|
|
47
|
+
zoom.transform,
|
|
48
|
+
d3.zoomIdentity.translate(width / 2, height / 2).scale(0.8),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Copy nodes/edges so D3 can mutate them
|
|
52
|
+
const simNodes: GraphNode[] = nodes.map((n) => ({ ...n }));
|
|
53
|
+
const simEdges: GraphEdge[] = edges.map((e) => ({
|
|
54
|
+
source: typeof e.source === "string" ? e.source : e.source.id,
|
|
55
|
+
target: typeof e.target === "string" ? e.target : e.target.id,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Simulation
|
|
59
|
+
const sim = d3
|
|
60
|
+
.forceSimulation<GraphNode>(simNodes)
|
|
61
|
+
.force(
|
|
62
|
+
"link",
|
|
63
|
+
d3
|
|
64
|
+
.forceLink<GraphNode, GraphEdge>(simEdges)
|
|
65
|
+
.id((d) => d.id)
|
|
66
|
+
.distance(40)
|
|
67
|
+
.strength(0.3),
|
|
68
|
+
)
|
|
69
|
+
.force("charge", d3.forceManyBody().strength(-60).distanceMax(300))
|
|
70
|
+
.force("center", d3.forceCenter(0, 0).strength(0.05))
|
|
71
|
+
.force("collision", d3.forceCollide().radius((d: any) => nodeRadius(d) + 2))
|
|
72
|
+
.alphaDecay(0.02);
|
|
73
|
+
|
|
74
|
+
simRef.current = sim;
|
|
75
|
+
|
|
76
|
+
// Edges
|
|
77
|
+
const link = g
|
|
78
|
+
.append("g")
|
|
79
|
+
.selectAll("line")
|
|
80
|
+
.data(simEdges)
|
|
81
|
+
.join("line")
|
|
82
|
+
.attr("stroke", "#1e1e30")
|
|
83
|
+
.attr("stroke-width", 0.5)
|
|
84
|
+
.attr("stroke-opacity", 0.6);
|
|
85
|
+
|
|
86
|
+
// Nodes
|
|
87
|
+
const node = g
|
|
88
|
+
.append("g")
|
|
89
|
+
.selectAll("circle")
|
|
90
|
+
.data(simNodes)
|
|
91
|
+
.join("circle")
|
|
92
|
+
.attr("r", (d) => nodeRadius(d))
|
|
93
|
+
.attr("fill", (d) => nodeColor(d))
|
|
94
|
+
.attr("stroke", "none")
|
|
95
|
+
.attr("opacity", 0.9)
|
|
96
|
+
.style("cursor", "pointer")
|
|
97
|
+
.on("click", (_event, d) => {
|
|
98
|
+
onNodeClickRef.current(d.id);
|
|
99
|
+
})
|
|
100
|
+
.on("mouseenter", function (event, d) {
|
|
101
|
+
d3.select(this).attr("r", nodeRadius(d) * 1.6).attr("opacity", 1);
|
|
102
|
+
tooltip
|
|
103
|
+
.style("display", "block")
|
|
104
|
+
.style("left", event.pageX + 12 + "px")
|
|
105
|
+
.style("top", event.pageY - 8 + "px")
|
|
106
|
+
.text(d.label);
|
|
107
|
+
})
|
|
108
|
+
.on("mouseleave", function (_event, d) {
|
|
109
|
+
d3.select(this).attr("r", nodeRadius(d)).attr("opacity", 0.9);
|
|
110
|
+
tooltip.style("display", "none");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Drag
|
|
114
|
+
const drag = d3
|
|
115
|
+
.drag<SVGCircleElement, GraphNode>()
|
|
116
|
+
.on("start", (event, d) => {
|
|
117
|
+
if (!event.active) sim.alphaTarget(0.3).restart();
|
|
118
|
+
d.fx = d.x;
|
|
119
|
+
d.fy = d.y;
|
|
120
|
+
})
|
|
121
|
+
.on("drag", (event, d) => {
|
|
122
|
+
d.fx = event.x;
|
|
123
|
+
d.fy = event.y;
|
|
124
|
+
})
|
|
125
|
+
.on("end", (event, d) => {
|
|
126
|
+
if (!event.active) sim.alphaTarget(0);
|
|
127
|
+
d.fx = null;
|
|
128
|
+
d.fy = null;
|
|
129
|
+
});
|
|
130
|
+
(node as any).call(drag);
|
|
131
|
+
|
|
132
|
+
// Labels for large nodes
|
|
133
|
+
const labels = g
|
|
134
|
+
.append("g")
|
|
135
|
+
.selectAll("text")
|
|
136
|
+
.data(simNodes.filter((n) => n.type === "tradition" || n.type === "moc"))
|
|
137
|
+
.join("text")
|
|
138
|
+
.text((d) => d.label)
|
|
139
|
+
.attr("fill", (d) => nodeColor(d))
|
|
140
|
+
.attr("font-size", (d) => (d.type === "moc" ? "8px" : "7px"))
|
|
141
|
+
.attr("font-weight", 500)
|
|
142
|
+
.attr("font-family", "Inter, sans-serif")
|
|
143
|
+
.attr("text-anchor", "middle")
|
|
144
|
+
.attr("dy", (d) => nodeRadius(d) + 11)
|
|
145
|
+
.attr("opacity", 0.7)
|
|
146
|
+
.attr("pointer-events", "none");
|
|
147
|
+
|
|
148
|
+
// Tooltip
|
|
149
|
+
const tooltip = d3
|
|
150
|
+
.select("body")
|
|
151
|
+
.append("div")
|
|
152
|
+
.style("position", "absolute")
|
|
153
|
+
.style("display", "none")
|
|
154
|
+
.style("background", "#1a1a26")
|
|
155
|
+
.style("color", "#d4d4e0")
|
|
156
|
+
.style("padding", "4px 10px")
|
|
157
|
+
.style("border-radius", "4px")
|
|
158
|
+
.style("font-size", "12px")
|
|
159
|
+
.style("font-family", "Inter, sans-serif")
|
|
160
|
+
.style("pointer-events", "none")
|
|
161
|
+
.style("z-index", "100")
|
|
162
|
+
.style("border", "1px solid #2a2a3a");
|
|
163
|
+
|
|
164
|
+
sim.on("tick", () => {
|
|
165
|
+
link
|
|
166
|
+
.attr("x1", (d: any) => d.source.x)
|
|
167
|
+
.attr("y1", (d: any) => d.source.y)
|
|
168
|
+
.attr("x2", (d: any) => d.target.x)
|
|
169
|
+
.attr("y2", (d: any) => d.target.y);
|
|
170
|
+
|
|
171
|
+
node.attr("cx", (d) => d.x!).attr("cy", (d) => d.y!);
|
|
172
|
+
|
|
173
|
+
labels.attr("x", (d) => d.x!).attr("y", (d) => d.y!);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
sim.stop();
|
|
178
|
+
tooltip.remove();
|
|
179
|
+
};
|
|
180
|
+
}, [nodes, edges]);
|
|
181
|
+
|
|
182
|
+
// Highlight selected node
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!svgRef.current) return;
|
|
185
|
+
const svg = d3.select(svgRef.current);
|
|
186
|
+
svg.selectAll("circle").attr("stroke", (d: any) =>
|
|
187
|
+
d.id === selectedId ? "#ffffff" : "none",
|
|
188
|
+
).attr("stroke-width", (d: any) =>
|
|
189
|
+
d.id === selectedId ? 2 : 0,
|
|
190
|
+
);
|
|
191
|
+
}, [selectedId]);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<svg
|
|
195
|
+
ref={svgRef}
|
|
196
|
+
className="w-full h-full"
|
|
197
|
+
style={{ background: "#0a0a0f" }}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import Markdown from "react-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import type { Note } from "./types";
|
|
5
|
+
import { nodeColor } from "./utils";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
note: Note;
|
|
9
|
+
connectedIds: string[];
|
|
10
|
+
noteById: Map<string, Note>;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onNavigate: (path: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getType(tags: string[]): string {
|
|
16
|
+
for (const t of tags) if (t.startsWith("type/")) return t.slice(5);
|
|
17
|
+
return "other";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getFamily(tags: string[]): string {
|
|
21
|
+
for (const t of tags) if (t.startsWith("family/")) return t.slice(7);
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function NoteView({ note, connectedIds, noteById, onClose, onNavigate }: Props) {
|
|
26
|
+
const type = getType(note.tags);
|
|
27
|
+
const family = getFamily(note.tags);
|
|
28
|
+
|
|
29
|
+
// Convert wikilinks [[Path]] and [[Path|Label]] to markdown links
|
|
30
|
+
const processedContent = useMemo(() => {
|
|
31
|
+
return note.content.replace(
|
|
32
|
+
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
|
33
|
+
(_match, path, label) => `[${label || path.split("/").pop()}](wikilink:${encodeURIComponent(path)})`,
|
|
34
|
+
);
|
|
35
|
+
}, [note.content]);
|
|
36
|
+
|
|
37
|
+
const connected = connectedIds
|
|
38
|
+
.map((id) => noteById.get(id))
|
|
39
|
+
.filter((n): n is Note => !!n)
|
|
40
|
+
.slice(0, 15);
|
|
41
|
+
|
|
42
|
+
const familyLabel = family ? family.charAt(0).toUpperCase() + family.slice(1) : null;
|
|
43
|
+
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
|
|
44
|
+
|
|
45
|
+
const color = nodeColor({
|
|
46
|
+
id: note.id,
|
|
47
|
+
path: note.path ?? "",
|
|
48
|
+
label: "",
|
|
49
|
+
type,
|
|
50
|
+
family,
|
|
51
|
+
tags: note.tags,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className="w-[480px] min-w-[380px] h-screen bg-surface border-l border-border flex flex-col overflow-hidden">
|
|
56
|
+
{/* Header */}
|
|
57
|
+
<div className="flex items-start justify-between p-5 pb-3 border-b border-border">
|
|
58
|
+
<div className="flex-1 min-w-0">
|
|
59
|
+
<div className="flex items-center gap-2 mb-1.5 text-xs">
|
|
60
|
+
<span
|
|
61
|
+
className="inline-block w-2 h-2 rounded-full"
|
|
62
|
+
style={{ background: color }}
|
|
63
|
+
/>
|
|
64
|
+
<span className="text-text-muted">{typeLabel}</span>
|
|
65
|
+
{familyLabel && (
|
|
66
|
+
<>
|
|
67
|
+
<span className="text-text-muted opacity-40">/</span>
|
|
68
|
+
<span className="text-text-muted">{familyLabel}</span>
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
{note.path && (
|
|
73
|
+
<div className="text-text-muted text-xs font-mono truncate opacity-50">
|
|
74
|
+
{note.path}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
<button
|
|
79
|
+
onClick={onClose}
|
|
80
|
+
className="text-text-muted hover:text-text ml-3 mt-0.5 text-lg leading-none cursor-pointer"
|
|
81
|
+
>
|
|
82
|
+
×
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Content */}
|
|
87
|
+
<div className="flex-1 overflow-y-auto p-5">
|
|
88
|
+
<div className="note-content">
|
|
89
|
+
<Markdown
|
|
90
|
+
remarkPlugins={[remarkGfm]}
|
|
91
|
+
components={{
|
|
92
|
+
a({ href, children }) {
|
|
93
|
+
if (href?.startsWith("wikilink:")) {
|
|
94
|
+
const path = decodeURIComponent(href.slice(9));
|
|
95
|
+
return (
|
|
96
|
+
<a
|
|
97
|
+
href="#"
|
|
98
|
+
onClick={(e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
onNavigate(path);
|
|
101
|
+
}}
|
|
102
|
+
className="text-accent hover:underline cursor-pointer"
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</a>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return (
|
|
109
|
+
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
110
|
+
{children}
|
|
111
|
+
</a>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{processedContent}
|
|
117
|
+
</Markdown>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Connected notes */}
|
|
122
|
+
{connected.length > 0 && (
|
|
123
|
+
<div className="border-t border-border p-4 max-h-[200px] overflow-y-auto">
|
|
124
|
+
<div className="text-xs text-text-muted mb-2 font-medium uppercase tracking-wider">
|
|
125
|
+
Connected ({connected.length})
|
|
126
|
+
</div>
|
|
127
|
+
<div className="space-y-1">
|
|
128
|
+
{connected.map((n) => (
|
|
129
|
+
<button
|
|
130
|
+
key={n.id}
|
|
131
|
+
onClick={() => onNavigate(n.path ?? "")}
|
|
132
|
+
className="w-full text-left text-sm text-text hover:text-accent transition-colors truncate py-0.5 cursor-pointer"
|
|
133
|
+
>
|
|
134
|
+
<span
|
|
135
|
+
className="inline-block w-1.5 h-1.5 rounded-full mr-2 opacity-70"
|
|
136
|
+
style={{
|
|
137
|
+
background: nodeColor({
|
|
138
|
+
id: n.id,
|
|
139
|
+
path: n.path ?? "",
|
|
140
|
+
label: "",
|
|
141
|
+
type: getType(n.tags),
|
|
142
|
+
family: getFamily(n.tags),
|
|
143
|
+
tags: n.tags,
|
|
144
|
+
}),
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
{(n.path ?? n.id).split("/").pop()}
|
|
148
|
+
</button>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|