diff --git a/package-lock.json b/package-lock.json
index d447e7a..59e5f20 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
"@sveltejs/adapter-static": "3.x",
"@sveltejs/kit": "2.x",
"@sveltejs/vite-plugin-svelte": "4.x",
+ "@types/d3": "^7.4.3",
"@types/he": "1.2.x",
"@types/node": "22.x",
"autoprefixer": "10.x",
@@ -1531,6 +1532,290 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+ "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -1538,6 +1823,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/he": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/he/-/he-1.2.3.tgz",
diff --git a/package.json b/package.json
index ca9cd67..b0f1151 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"@sveltejs/adapter-static": "3.x",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "4.x",
+ "@types/d3": "^7.4.3",
"@types/he": "1.2.x",
"@types/node": "22.x",
"autoprefixer": "10.x",
diff --git a/src/app.css b/src/app.css
index 011ebd9..314408a 100644
--- a/src/app.css
+++ b/src/app.css
@@ -190,6 +190,8 @@
.ul-leather li a {
@apply text-gray-800 hover:text-primary-400 dark:text-gray-300 dark:hover:text-primary-500;
}
+
+ /* Network visualization */
.network-link-leather {
@apply stroke-gray-400 fill-gray-400;
}
@@ -202,11 +204,23 @@
}
@layer components {
+ /* Legend */
.leather-legend {
- @apply flex-shrink-0 p-4 bg-primary-0 dark:bg-primary-1000 rounded-lg shadow
- border border-gray-200 dark:border-gray-800;
+ @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 rounded;
+ @apply shadow-none text-primary-1000 border border-s-4 bg-highlight border-primary-200 has-[:hover]:border-primary-700;
+ @apply dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500;
}
+
+ /* Tooltip */
.tooltip-leather {
- @apply bg-primary-0 dark:bg-primary-1000 text-gray-800 dark:text-gray-300;
+ @apply fixed p-4 rounded shadow-lg bg-primary-0 dark:bg-primary-1000
+ text-gray-800 dark:text-gray-300 border border-gray-200 dark:border-gray-700
+ transition-colors duration-200;
+ max-width: 400px;
+ z-index: 1000;
+ }
+
+ .leather-legend button {
+ @apply dark:text-white;
}
}
diff --git a/src/lib/components/EventLimitControl.svelte b/src/lib/components/EventLimitControl.svelte
index aafd91f..d8c28be 100644
--- a/src/lib/components/EventLimitControl.svelte
+++ b/src/lib/components/EventLimitControl.svelte
@@ -30,7 +30,7 @@
-
diff --git a/src/lib/components/EventRenderLevelLimit.svelte b/src/lib/components/EventRenderLevelLimit.svelte
index bbfdc87..3a7d8a8 100644
--- a/src/lib/components/EventRenderLevelLimit.svelte
+++ b/src/lib/components/EventRenderLevelLimit.svelte
@@ -29,16 +29,16 @@
-
-
+
+
-
-
Legend
-
- -
-
-
-
- I
-
- Index events (kind 30040) - Each with a unique pastel color
-
- -
-
-
- C
-
- Content events (kinds 30041, 30818) - Publication sections
-
- -
-
- Arrows indicate reading/sequence order
-
-
+
+
+
Legend
+
+
+
+ {#if expanded}
+
+
+ -
+
+
+ I
+
+
+ Index events (kind 30040) - Each with a unique pastel color
+
+
+
+ -
+
+
+ C
+
+
+ Content events (kinds 30041, 30818) - Publication sections
+
+
+
+ -
+
+ Arrows indicate reading/sequence order
+
+
+ {/if}
diff --git a/src/lib/navigator/EventNetwork/NodeTooltip.svelte b/src/lib/navigator/EventNetwork/NodeTooltip.svelte
index cb6f779..4aedc8e 100644
--- a/src/lib/navigator/EventNetwork/NodeTooltip.svelte
+++ b/src/lib/navigator/EventNetwork/NodeTooltip.svelte
@@ -1,20 +1,33 @@
+
+
+
+
+
Settings
+
+
+
+ {#if expanded}
+
+
+ Showing {count} events from {$networkFetchLimit} headers
+
+
+
+
+ {/if}
+
diff --git a/src/lib/navigator/EventNetwork/index.svelte b/src/lib/navigator/EventNetwork/index.svelte
index 6a7fa61..7742475 100644
--- a/src/lib/navigator/EventNetwork/index.svelte
+++ b/src/lib/navigator/EventNetwork/index.svelte
@@ -1,40 +1,84 @@
-
+
-
-
+
+ {#if hasError}
+
+
Error
+
{errorMessage}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{#if tooltipVisible && tooltipNode}
@@ -327,9 +585,8 @@
selected={tooltipNode.id === selectedNodeId}
x={tooltipX}
y={tooltipY}
- on:close={handleTooltipClose}
+ onclose={handleTooltipClose}
/>
{/if}
-
diff --git a/src/lib/navigator/EventNetwork/types.ts b/src/lib/navigator/EventNetwork/types.ts
index 276b871..db2d46b 100644
--- a/src/lib/navigator/EventNetwork/types.ts
+++ b/src/lib/navigator/EventNetwork/types.ts
@@ -1,35 +1,79 @@
+/**
+ * Type definitions for the Event Network visualization
+ *
+ * This module defines the core data structures used in the D3 force-directed
+ * graph visualization of Nostr events.
+ */
+
import type { NDKEvent } from "@nostr-dev-kit/ndk";
-export interface NetworkNode extends d3.SimulationNodeDatum {
- id: string;
- event?: NDKEvent;
- level: number;
- kind: number;
- title: string;
- content: string;
- author: string;
- type: "Index" | "Content";
- naddr?: string;
- nevent?: string;
- x?: number;
- y?: number;
- isContainer?: boolean;
+/**
+ * Base interface for nodes in a D3 force simulation
+ * Represents the physical properties of a node in the simulation
+ */
+export interface SimulationNodeDatum {
+ index?: number; // Node index in the simulation
+ x?: number; // X position
+ y?: number; // Y position
+ vx?: number; // X velocity
+ vy?: number; // Y velocity
+ fx?: number | null; // Fixed X position (when node is pinned)
+ fy?: number | null; // Fixed Y position (when node is pinned)
+}
+
+/**
+ * Base interface for links in a D3 force simulation
+ * Represents connections between nodes
+ */
+export interface SimulationLinkDatum
{
+ source: NodeType | string | number; // Source node or identifier
+ target: NodeType | string | number; // Target node or identifier
+ index?: number; // Link index in the simulation
}
-export interface NetworkLink extends d3.SimulationLinkDatum {
- source: NetworkNode;
- target: NetworkNode;
- isSequential: boolean;
+/**
+ * Represents a node in the event network visualization
+ * Extends the base simulation node with Nostr event-specific properties
+ */
+export interface NetworkNode extends SimulationNodeDatum {
+ id: string; // Unique identifier (event ID)
+ event?: NDKEvent; // Reference to the original NDK event
+ level: number; // Hierarchy level in the network
+ kind: number; // Nostr event kind (30040 for index, 30041/30818 for content)
+ title: string; // Event title
+ content: string; // Event content
+ author: string; // Author's public key
+ type: "Index" | "Content"; // Node type classification
+ naddr?: string; // NIP-19 naddr identifier
+ nevent?: string; // NIP-19 nevent identifier
+ isContainer?: boolean; // Whether this node is a container (index)
}
+/**
+ * Represents a link between nodes in the event network
+ * Extends the base simulation link with event-specific properties
+ */
+export interface NetworkLink extends SimulationLinkDatum {
+ source: NetworkNode; // Source node (overridden to be more specific)
+ target: NetworkNode; // Target node (overridden to be more specific)
+ isSequential: boolean; // Whether this link represents a sequential relationship
+}
+
+/**
+ * Represents the complete graph data for visualization
+ */
export interface GraphData {
- nodes: NetworkNode[];
- links: NetworkLink[];
+ nodes: NetworkNode[]; // All nodes in the graph
+ links: NetworkLink[]; // All links in the graph
}
+/**
+ * Represents the internal state of the graph during construction
+ * Used to track relationships and build the final graph
+ */
export interface GraphState {
- nodeMap: Map;
- links: NetworkLink[];
- eventMap: Map;
- referencedIds: Set;
-}
\ No newline at end of file
+ nodeMap: Map; // Maps event IDs to nodes
+ links: NetworkLink[]; // All links in the graph
+ eventMap: Map; // Maps event IDs to original events
+ referencedIds: Set; // Set of event IDs referenced by other events
+}
diff --git a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts
index 2ba8e90..34731b3 100644
--- a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts
+++ b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts
@@ -1,27 +1,100 @@
/**
- * D3 force simulation utilities for the event network
+ * D3 Force Simulation Utilities
+ *
+ * This module provides utilities for creating and managing D3 force-directed
+ * graph simulations for the event network visualization.
*/
import type { NetworkNode, NetworkLink } from "../types";
-import type { Simulation } from "d3";
import * as d3 from "d3";
+// Configuration
+const DEBUG = false; // Set to true to enable debug logging
+const GRAVITY_STRENGTH = 0.05; // Strength of global gravity
+const CONNECTED_GRAVITY_STRENGTH = 0.3; // Strength of gravity between connected nodes
+
+/**
+ * Debug logging function that only logs when DEBUG is true
+ */
+function debug(...args: any[]) {
+ if (DEBUG) {
+ console.log("[ForceSimulation]", ...args);
+ }
+}
+
+/**
+ * Type definition for D3 force simulation
+ * Provides type safety for simulation operations
+ */
+export interface Simulation {
+ nodes(): NodeType[];
+ nodes(nodes: NodeType[]): this;
+ alpha(): number;
+ alpha(alpha: number): this;
+ alphaTarget(): number;
+ alphaTarget(target: number): this;
+ restart(): this;
+ stop(): this;
+ tick(): this;
+ on(type: string, listener: (this: this) => void): this;
+ force(name: string): any;
+ force(name: string, force: any): this;
+}
+
/**
- * Updates a node's velocity
+ * Type definition for D3 drag events
+ * Provides type safety for drag operations
+ */
+export interface D3DragEvent {
+ active: number;
+ sourceEvent: any;
+ subject: Subject;
+ x: number;
+ y: number;
+ dx: number;
+ dy: number;
+ identifier: string | number;
+}
+
+/**
+ * Updates a node's velocity by applying a force
+ *
+ * @param node - The node to update
+ * @param deltaVx - Change in x velocity
+ * @param deltaVy - Change in y velocity
*/
export function updateNodeVelocity(
node: NetworkNode,
deltaVx: number,
deltaVy: number
) {
+ debug("Updating node velocity", {
+ nodeId: node.id,
+ currentVx: node.vx,
+ currentVy: node.vy,
+ deltaVx,
+ deltaVy
+ });
+
if (typeof node.vx === "number" && typeof node.vy === "number") {
node.vx = node.vx - deltaVx;
node.vy = node.vy - deltaVy;
+ debug("New velocity", { nodeId: node.id, vx: node.vx, vy: node.vy });
+ } else {
+ debug("Node velocity not defined", { nodeId: node.id });
}
}
/**
- * Applies a logarithmic gravity force to a node
+ * Applies a logarithmic gravity force pulling the node toward the center
+ *
+ * The logarithmic scale ensures that nodes far from the center experience
+ * stronger gravity, preventing them from drifting too far away.
+ *
+ * @param node - The node to apply gravity to
+ * @param centerX - X coordinate of the center
+ * @param centerY - Y coordinate of the center
+ * @param alpha - Current simulation alpha (cooling factor)
*/
export function applyGlobalLogGravity(
node: NetworkNode,
@@ -35,102 +108,128 @@ export function applyGlobalLogGravity(
if (distance === 0) return;
- const force = Math.log(distance + 1) * 0.05 * alpha;
+ const force = Math.log(distance + 1) * GRAVITY_STRENGTH * alpha;
updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
}
/**
* Applies gravity between connected nodes
+ *
+ * This creates a cohesive force that pulls connected nodes toward their
+ * collective center of gravity, creating more meaningful clusters.
+ *
+ * @param node - The node to apply connected gravity to
+ * @param links - All links in the network
+ * @param alpha - Current simulation alpha (cooling factor)
*/
export function applyConnectedGravity(
node: NetworkNode,
links: NetworkLink[],
alpha: number,
) {
+ // Find all nodes connected to this node
const connectedNodes = links
- .filter(
- (link) => link.source.id === node.id || link.target.id === node.id,
- )
- .map((link) => (link.source.id === node.id ? link.target : link.source));
+ .filter(link => link.source.id === node.id || link.target.id === node.id)
+ .map(link => link.source.id === node.id ? link.target : link.source);
if (connectedNodes.length === 0) return;
- const cogX = d3.mean(connectedNodes, (n) => n.x);
- const cogY = d3.mean(connectedNodes, (n) => n.y);
+ // Calculate center of gravity of connected nodes
+ const cogX = d3.mean(connectedNodes, (n: NetworkNode) => n.x);
+ const cogY = d3.mean(connectedNodes, (n: NetworkNode) => n.y);
if (cogX === undefined || cogY === undefined) return;
+ // Calculate force direction and magnitude
const dx = (node.x ?? 0) - cogX;
const dy = (node.y ?? 0) - cogY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance === 0) return;
- const force = distance * 0.3 * alpha;
+ // Apply force proportional to distance
+ const force = distance * CONNECTED_GRAVITY_STRENGTH * alpha;
updateNodeVelocity(node, (dx / distance) * force, (dy / distance) * force);
}
/**
* Sets up drag behavior for nodes
+ *
+ * This enables interactive dragging of nodes in the visualization.
+ *
+ * @param simulation - The D3 force simulation
+ * @param warmupClickEnergy - Alpha target when dragging starts (0-1)
+ * @returns D3 drag behavior configured for the simulation
*/
export function setupDragHandlers(
simulation: Simulation,
warmupClickEnergy: number = 0.9
) {
return d3
- .drag()
- .on(
- "start",
- (
- event: d3.D3DragEvent,
- d: NetworkNode,
- ) => {
- if (!event.active)
- simulation.alphaTarget(warmupClickEnergy).restart();
- d.fx = d.x;
- d.fy = d.y;
- },
- )
- .on(
- "drag",
- (
- event: d3.D3DragEvent,
- d: NetworkNode,
- ) => {
- d.fx = event.x;
- d.fy = event.y;
- },
- )
- .on(
- "end",
- (
- event: d3.D3DragEvent,
- d: NetworkNode,
- ) => {
- if (!event.active) simulation.alphaTarget(0);
- d.fx = null;
- d.fy = null;
- },
- );
+ .drag()
+ .on("start", (event: D3DragEvent, d: NetworkNode) => {
+ // Warm up simulation if it's cooled down
+ if (!event.active) {
+ simulation.alphaTarget(warmupClickEnergy).restart();
+ }
+ // Fix node position at current location
+ d.fx = d.x;
+ d.fy = d.y;
+ })
+ .on("drag", (event: D3DragEvent, d: NetworkNode) => {
+ // Update fixed position to mouse position
+ d.fx = event.x;
+ d.fy = event.y;
+ })
+ .on("end", (event: D3DragEvent, d: NetworkNode) => {
+ // Cool down simulation when drag ends
+ if (!event.active) {
+ simulation.alphaTarget(0);
+ }
+ // Release fixed position
+ d.fx = null;
+ d.fy = null;
+ });
}
/**
* Creates a D3 force simulation for the network
+ *
+ * @param nodes - Array of network nodes
+ * @param links - Array of network links
+ * @param nodeRadius - Radius of node circles
+ * @param linkDistance - Desired distance between linked nodes
+ * @returns Configured D3 force simulation
*/
export function createSimulation(
nodes: NetworkNode[],
links: NetworkLink[],
nodeRadius: number,
linkDistance: number
-) {
- return d3
- .forceSimulation(nodes)
- .force(
- "link",
- d3
- .forceLink(links)
- .id((d) => d.id)
- .distance(linkDistance * 0.1),
- )
- .force("collide", d3.forceCollide().radius(nodeRadius * 4));
-}
\ No newline at end of file
+): Simulation {
+ debug("Creating simulation", {
+ nodeCount: nodes.length,
+ linkCount: links.length,
+ nodeRadius,
+ linkDistance
+ });
+
+ try {
+ // Create the simulation with nodes
+ const simulation = d3
+ .forceSimulation(nodes)
+ .force(
+ "link",
+ d3.forceLink(links)
+ .id((d: NetworkNode) => d.id)
+ .distance(linkDistance * 0.1)
+ )
+ .force("collide", d3.forceCollide().radius(nodeRadius * 4));
+
+ debug("Simulation created successfully");
+ return simulation;
+ } catch (error) {
+ console.error("Error creating simulation:", error);
+ throw error;
+ }
+}
diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts
index d9daa58..4f27c1f 100644
--- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts
+++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts
@@ -1,17 +1,49 @@
+/**
+ * Network Builder Utilities
+ *
+ * This module provides utilities for building a network graph from Nostr events.
+ * It handles the creation of nodes and links, and the processing of event relationships.
+ */
+
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types";
import { nip19 } from "nostr-tools";
import { standardRelays } from "$lib/consts";
+// Configuration
+const DEBUG = false; // Set to true to enable debug logging
+const INDEX_EVENT_KIND = 30040;
+const CONTENT_EVENT_KIND = 30041;
+
+/**
+ * Debug logging function that only logs when DEBUG is true
+ */
+function debug(...args: any[]) {
+ if (DEBUG) {
+ console.log("[NetworkBuilder]", ...args);
+ }
+}
+
/**
* Creates a NetworkNode from an NDKEvent
+ *
+ * Extracts relevant information from the event and creates a node representation
+ * for the visualization.
+ *
+ * @param event - The Nostr event to convert to a node
+ * @param level - The hierarchy level of the node (default: 0)
+ * @returns A NetworkNode object representing the event
*/
export function createNetworkNode(
event: NDKEvent,
level: number = 0
): NetworkNode {
- const isContainer = event.kind === 30040;
+ debug("Creating network node", { eventId: event.id, kind: event.kind, level });
+
+ const isContainer = event.kind === INDEX_EVENT_KIND;
+ const nodeType = isContainer ? "Index" : "Content";
+ // Create the base node with essential properties
const node: NetworkNode = {
id: event.id,
event,
@@ -20,13 +52,16 @@ export function createNetworkNode(
title: event.getMatchingTags("title")?.[0]?.[1] || "Untitled",
content: event.content || "",
author: event.pubkey || "",
- kind: event.kind,
- type: event?.kind === 30040 ? "Index" : "Content",
+ kind: event.kind || CONTENT_EVENT_KIND, // Default to content event kind if undefined
+ type: nodeType,
};
+ // Add NIP-19 identifiers if possible
if (event.kind && event.pubkey) {
try {
const dTag = event.getMatchingTags("d")?.[0]?.[1] || "";
+
+ // Create naddr (NIP-19 address) for the event
node.naddr = nip19.naddrEncode({
pubkey: event.pubkey,
identifier: dTag,
@@ -34,6 +69,7 @@ export function createNetworkNode(
relays: standardRelays,
});
+ // Create nevent (NIP-19 event reference) for the event
node.nevent = nip19.neventEncode({
id: event.id,
relays: standardRelays,
@@ -47,50 +83,93 @@ export function createNetworkNode(
return node;
}
+/**
+ * Creates a map of event IDs to events for quick lookup
+ *
+ * @param events - Array of Nostr events
+ * @returns Map of event IDs to events
+ */
export function createEventMap(events: NDKEvent[]): Map {
+ debug("Creating event map", { eventCount: events.length });
+
const eventMap = new Map();
events.forEach((event) => {
if (event.id) {
eventMap.set(event.id, event);
}
});
+
+ debug("Event map created", { mapSize: eventMap.size });
return eventMap;
}
+/**
+ * Extracts an event ID from an 'a' tag
+ *
+ * @param tag - The tag array from a Nostr event
+ * @returns The event ID or null if not found
+ */
export function extractEventIdFromATag(tag: string[]): string | null {
return tag[3] || null;
}
/**
- * Generates a color for an event based on its ID
+ * Generates a deterministic color for an event based on its ID
+ *
+ * This creates visually distinct colors for different index events
+ * while ensuring the same event always gets the same color.
+ *
+ * @param eventId - The event ID to generate a color for
+ * @returns An HSL color string
*/
export function getEventColor(eventId: string): string {
+ // Use first 4 characters of event ID as a hex number
const num = parseInt(eventId.slice(0, 4), 16);
+ // Convert to a hue value (0-359)
const hue = num % 360;
+ // Use fixed saturation and lightness for pastel colors
const saturation = 70;
const lightness = 75;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
+/**
+ * Initializes the graph state from a set of events
+ *
+ * Creates nodes for all events and identifies referenced events.
+ *
+ * @param events - Array of Nostr events
+ * @returns Initial graph state
+ */
export function initializeGraphState(events: NDKEvent[]): GraphState {
+ debug("Initializing graph state", { eventCount: events.length });
+
const nodeMap = new Map();
const eventMap = createEventMap(events);
- // Create initial nodes
+ // Create initial nodes for all events
events.forEach((event) => {
if (!event.id) return;
const node = createNetworkNode(event);
nodeMap.set(event.id, node);
});
+ debug("Node map created", { nodeCount: nodeMap.size });
- // Build referenced IDs set
+ // Build set of referenced event IDs to identify root events
const referencedIds = new Set();
events.forEach((event) => {
- event.getMatchingTags("a").forEach((tag) => {
+ const aTags = event.getMatchingTags("a");
+ debug("Processing a-tags for event", {
+ eventId: event.id,
+ aTagCount: aTags.length
+ });
+
+ aTags.forEach((tag) => {
const id = extractEventIdFromATag(tag);
if (id) referencedIds.add(id);
});
});
+ debug("Referenced IDs set created", { referencedCount: referencedIds.size });
return {
nodeMap,
@@ -100,6 +179,18 @@ export function initializeGraphState(events: NDKEvent[]): GraphState {
};
}
+/**
+ * Processes a sequence of nodes referenced by an index event
+ *
+ * Creates links between the index and its content, and between sequential content nodes.
+ * Also processes nested indices recursively up to the maximum level.
+ *
+ * @param sequence - Array of nodes in the sequence
+ * @param indexEvent - The index event referencing the sequence
+ * @param level - Current hierarchy level
+ * @param state - Current graph state
+ * @param maxLevel - Maximum hierarchy level to process
+ */
export function processSequence(
sequence: NetworkNode[],
indexEvent: NDKEvent,
@@ -107,14 +198,15 @@ export function processSequence(
state: GraphState,
maxLevel: number,
): void {
+ // Stop if we've reached max level or have no nodes
if (level >= maxLevel || sequence.length === 0) return;
- // Set levels for sequence nodes
+ // Set levels for all nodes in the sequence
sequence.forEach((node) => {
node.level = level + 1;
});
- // Create initial link from index to first content
+ // Create link from index to first content node
const indexNode = state.nodeMap.get(indexEvent.id);
if (indexNode && sequence[0]) {
state.links.push({
@@ -124,7 +216,7 @@ export function processSequence(
});
}
- // Create sequential links
+ // Create sequential links between content nodes
for (let i = 0; i < sequence.length - 1; i++) {
const currentNode = sequence[i];
const nextNode = sequence[i + 1];
@@ -135,16 +227,27 @@ export function processSequence(
isSequential: true,
});
- processNestedIndex(currentNode, level + 1, state, maxLevel);
+ // Process nested indices recursively
+ if (currentNode.isContainer) {
+ processNestedIndex(currentNode, level + 1, state, maxLevel);
+ }
}
- // Process final node if it's an index
+ // Process the last node if it's an index
const lastNode = sequence[sequence.length - 1];
if (lastNode?.isContainer) {
processNestedIndex(lastNode, level + 1, state, maxLevel);
}
}
+/**
+ * Processes a nested index node
+ *
+ * @param node - The index node to process
+ * @param level - Current hierarchy level
+ * @param state - Current graph state
+ * @param maxLevel - Maximum hierarchy level to process
+ */
export function processNestedIndex(
node: NetworkNode,
level: number,
@@ -159,6 +262,14 @@ export function processNestedIndex(
}
}
+/**
+ * Processes an index event and its referenced content
+ *
+ * @param indexEvent - The index event to process
+ * @param level - Current hierarchy level
+ * @param state - Current graph state
+ * @param maxLevel - Maximum hierarchy level to process
+ */
export function processIndexEvent(
indexEvent: NDKEvent,
level: number,
@@ -167,6 +278,7 @@ export function processIndexEvent(
): void {
if (level >= maxLevel) return;
+ // Extract the sequence of nodes referenced by this index
const sequence = indexEvent
.getMatchingTags("a")
.map((tag) => extractEventIdFromATag(tag))
@@ -177,19 +289,53 @@ export function processIndexEvent(
processSequence(sequence, indexEvent, level, state, maxLevel);
}
+/**
+ * Generates a complete graph from a set of events
+ *
+ * This is the main entry point for building the network visualization.
+ *
+ * @param events - Array of Nostr events
+ * @param maxLevel - Maximum hierarchy level to process
+ * @returns Complete graph data for visualization
+ */
export function generateGraph(
events: NDKEvent[],
maxLevel: number
): GraphData {
+ debug("Generating graph", { eventCount: events.length, maxLevel });
+
+ // Initialize the graph state
const state = initializeGraphState(events);
- // Process root indices
- events
- .filter((e) => e.kind === 30040 && e.id && !state.referencedIds.has(e.id))
- .forEach((rootIndex) => processIndexEvent(rootIndex, 0, state, maxLevel));
+ // Find root index events (those not referenced by other events)
+ const rootIndices = events.filter(
+ (e) => e.kind === INDEX_EVENT_KIND && e.id && !state.referencedIds.has(e.id)
+ );
+
+ debug("Found root indices", {
+ rootCount: rootIndices.length,
+ rootIds: rootIndices.map(e => e.id)
+ });
+
+ // Process each root index
+ rootIndices.forEach((rootIndex) => {
+ debug("Processing root index", {
+ rootId: rootIndex.id,
+ aTags: rootIndex.getMatchingTags("a").length
+ });
+ processIndexEvent(rootIndex, 0, state, maxLevel);
+ });
- return {
+ // Create the final graph data
+ const result = {
nodes: Array.from(state.nodeMap.values()),
links: state.links,
};
-}
\ No newline at end of file
+
+ debug("Graph generation complete", {
+ nodeCount: result.nodes.length,
+ linkCount: result.links.length
+ });
+
+ return result;
+}
diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte
index f1f7d33..142eeb5 100644
--- a/src/routes/visualize/+page.svelte
+++ b/src/routes/visualize/+page.svelte
@@ -1,55 +1,91 @@
+
-
-
Publication Network
-
-
- {#if !loading && !error}
-
- {/if}
+
+
+
Publication Network
-
- {#if !loading && !error && showSettings}
-
-
-
-
- Visualization Settings
-
-
-
-
- Showing {events.length} events from {$networkFetchLimit} headers
-
-
-
-
-
-
- {/if}
-
+
{#if loading}
@@ -140,12 +143,14 @@
Loading...
+
{:else if error}
-
Error loading network: {error}
+
Error loading network:
+
{error}
+
{:else}
-
-
+
+
{/if}
diff --git a/src/styles/visualize.css b/src/styles/visualize.css
index 59360c9..1ff732d 100644
--- a/src/styles/visualize.css
+++ b/src/styles/visualize.css
@@ -1,6 +1,7 @@
@layer components {
+ /* Legend styles - specific to visualization */
.legend-list {
- @apply list-disc pl-5 space-y-2 text-gray-800 dark:text-gray-300;
+ @apply list-disc mt-2 space-y-2 text-gray-800 dark:text-gray-300;
}
.legend-item {
@@ -20,7 +21,92 @@
background-color: #d6c1a8;
}
+ .legend-circle.content {
+ background-color: var(--content-color, #d6c1a8);
+ }
+
+ :global(.dark) .legend-circle.content {
+ background-color: var(--content-color-dark, #FFFFFF);
+ }
+
.legend-letter {
- @apply absolute inset-0 flex items-center justify-center text-black text-xs;
+ @apply absolute inset-0 flex items-center justify-center text-black text-xs font-bold;
+ }
+
+ .legend-text {
+ @apply text-sm;
+ }
+
+ /* Network visualization styles - specific to visualization */
+ .network-container {
+ @apply flex flex-col w-full h-[calc(100vh-138px)] min-h-[400px] max-h-[900px];
+ }
+
+ .network-svg-container {
+ @apply relative sm:h-[100%];
+ }
+
+ .network-svg {
+ @apply w-full sm:h-[100%] border;
+ @apply border border-primary-200 has-[:hover]:border-primary-700 dark:bg-primary-1000 dark:border-primary-800 dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500 rounded;
+ }
+
+ .network-error {
+ @apply w-full p-4 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 rounded-lg mb-4;
+ }
+
+ .network-error-title {
+ @apply font-bold text-lg;
+ }
+
+ .network-error-retry {
+ @apply mt-2 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700;
+ }
+
+ .network-debug {
+ @apply mt-4 text-sm text-gray-500;
+ }
+
+ /* Zoom controls */
+ .network-controls {
+ @apply absolute bottom-4 right-4 flex flex-col gap-2 z-10;
+ }
+
+ .network-control-button {
+ @apply bg-white;
+ }
+
+ /* Tooltip styles - specific to visualization tooltips */
+ .tooltip-close-btn {
+ @apply absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600
+ rounded-full p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200;
+ }
+
+ .tooltip-content {
+ @apply space-y-2 pr-6;
+ }
+
+ .tooltip-title {
+ @apply font-bold text-base;
+ }
+
+ .tooltip-title-link {
+ @apply text-gray-800 hover:text-blue-600 dark:text-gray-200 dark:hover:text-blue-400;
+ }
+
+ .tooltip-metadata {
+ @apply text-gray-600 dark:text-gray-400 text-sm;
+ }
+
+ .tooltip-summary {
+ @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40;
+ }
+
+ .tooltip-content-preview {
+ @apply mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40;
+ }
+
+ .tooltip-help-text {
+ @apply mt-2 text-xs text-gray-500 dark:text-gray-400 italic;
}
}
diff --git a/src/types/d3.d.ts b/src/types/d3.d.ts
new file mode 100644
index 0000000..3d230f5
--- /dev/null
+++ b/src/types/d3.d.ts
@@ -0,0 +1,19 @@
+/**
+ * Type declarations for D3.js and related modules
+ *
+ * These declarations allow TypeScript to recognize D3 imports without requiring
+ * detailed type definitions. For a project requiring more type safety, consider
+ * using the @types/d3 package and its related sub-packages.
+ */
+
+// Core D3 library
+declare module 'd3';
+
+// Force simulation module for graph layouts
+declare module 'd3-force';
+
+// DOM selection and manipulation module
+declare module 'd3-selection';
+
+// Drag behavior module
+declare module 'd3-drag';