diff --git a/.cursor/rules/code-review.mdc b/.cursor/rules/code-review.mdc
new file mode 100644
index 0000000..119054d
--- /dev/null
+++ b/.cursor/rules/code-review.mdc
@@ -0,0 +1,40 @@
+---
+description:
+globs:
+alwaysApply: false
+---
+You are an agentic AI assistant that quickly identifies key focus areas in a git diff so that a developer can efficiently review files within a change set in a logical order.
+
+## Interaction Workflow
+
+### Prerequisites
+
+- Determine which branch the developer is working on. If the branch is the main branch, tell the developer that you cannot perform a code review on the main branch, and end your turn.
+
+### Project Structure
+
+This project broadly follows a model-view-controller (MVC) pattern.
+
+- The Model consists primarily of Nostr relays, accessed via WebSocket APIs. The Model layer also includes data stored in the web browser.
+- The View is a reactive UI defined by SvelteKit pages and Svelte components.
+- The Controller layer is defined by various TypeScript modules that provide utility functions, classes, singletons, and other facilities that prepare data for the view layer or handle user-provided data for to be saved to the browser or relays.
+
+### Additional Context
+
+- The primary branch for this repo is called `master`.
+
+### Expected Output
+
+- The developer may leave comments on the reviewed changes via an external PR tool, such as GitHub or OneDev, so specify filenames and line numbers for each highlighted item of code.
+- Specify the context of highlighted items, such as function, class, or component names.
+- Always explain why an item is worth the developer's particular attention.
+- Keep the code diff surveys concise and to the point.
+
+### Code Review Order
+
+1. Obtain the diff of the current branch with the main branch. If necessary, ask the developer to provide the diff as context.
+2. Read the diff and associated commit messages, if available, and give the developer a brief summary of the changes and key items to note.
+3. Tell the developer you will provide a more detailed description of the changes.
+4. Tell the developer to review model-level changes first. These may include changes to API clients, changes to database access, and changes to cache or browser storage patterns.
+5. Next, point the developer's attention to changes in the controller/view controller layer. These may include changes to service classes, utility functions, and anything else that could be classified as "business logic".
+6. Finally, draw the developer's attention to view/UI changes. In this project, view changes will be almost entirely in `.svelte` component files.
\ No newline at end of file
diff --git a/doc/settings_panel.org b/doc/settings_panel.org
new file mode 100644
index 0000000..3730929
--- /dev/null
+++ b/doc/settings_panel.org
@@ -0,0 +1,124 @@
+* Settings Panel Documentation
+
+** Overview
+The settings panel controls how events are fetched and displayed in the visualization. It has several sections that work together to create an efficient and user-friendly experience.
+
+** Event Types Configuration
+
+*** Purpose
+Controls which types of Nostr events are fetched and how many of each type.
+
+*** Key Event Types
+- *Kind 30040* (Index Events): Publication indices
+- *Kind 30041* (Content Events): Publication content
+- *Kind 30818* (Content Events): Alternative content format
+- *Kind 30023* (Content Events): Alternative content format
+
+*** How Limits Work
+Each event kind has a limit number that controls different things:
+
+**** For Kind 0 (Profiles)
+- Limit controls how many profiles to fetch from discovered pubkeys
+- These profiles are used for:
+ - Displaying names instead of pubkeys
+ - Showing profile pictures in tooltips
+ - When "People" tag anchors are selected, this limit controls how many people anchors to display
+
+**** For Kind 3 (Follow Lists)
+- =limit = 1=: Only fetch the current user's follow list
+- =limit > 1=: Fetch the user's follow list PLUS (limit-1) follow lists from people they follow
+- The depth selector controls traversal:
+ - =Direct= (0): Just the immediate follows
+ - =2 degrees= (1): Follows of follows
+ - =3 degrees= (2): Three levels deep
+
+**** For Kind 30040/30041/30818
+- Limit controls maximum number of these events to fetch
+
+** Tag Anchors
+
+*** What Are Tag Anchors?
+Tag anchors are special nodes in the graph that act as gravity points for events sharing common attributes. They help organize the visualization by grouping related content.
+
+*** Tag Types Available
+- *Hashtags* (t): Groups events by hashtag
+- *Authors*: Groups events by author
+- *People* (p): Shows people from follow lists as anchor points
+- *Event References* (e): Groups events that reference each other
+- *Titles*: Groups events by title
+- *Summaries*: Groups events by summary
+
+*** How People Tag Anchors Work
+When "People" is selected as the tag type:
+
+1. The system looks at all loaded follow lists (kind 3 events)
+2. Extracts all pubkeys (people) from those follow lists
+3. Creates tag anchors for those people (up to the kind 0 limit)
+4. Connects each person anchor to:
+ - Events they authored (where pubkey matches)
+ - Events where they're mentioned in "p" tags
+
+*** Display Limiting and Auto-Disable
+- Tag anchors are created for ALL discovered tags
+- But only displayed up to the configured limit
+- When > 20 tag anchors exist, they're all auto-disabled
+- Users can selectively enable specific anchors
+- The legend becomes scrollable for many anchors
+
+*** "Only show people with publications" Checkbox
+When checked (default):
+- Only shows people who have events in the current visualization
+
+When unchecked:
+- Shows ALL people from follow lists, even if they have no events displayed
+- Useful for seeing your complete social graph
+
+** Display Limits Section
+
+*** Max Publication Indices (30040)
+Controls display filtering for publication indices after they're fetched.
+
+*** Max Events per Index
+Limits how many content events to show per publication index.
+
+*** Fetch if not found
+When enabled, automatically fetches missing referenced events.
+
+** Graph Traversal Section
+
+*** Search through already fetched
+When enabled, tag expansion only searches through events already loaded (more efficient).
+
+*** Append mode
+When enabled, new fetches add to the existing graph instead of replacing it.
+
+** Current Implementation Questions
+
+1. *Profile Fetching*: Should we fetch profiles for:
+ - Only event authors?
+ - All pubkeys in follow lists?
+ - All pubkeys mentioned anywhere?
+
+2. *People Tag Anchors*: Should they connect to:
+ - Only events where the person is tagged with "p"?
+ - Events they authored?
+ - Both?
+
+3. *Display Limits*: Should limits control:
+ - How many to fetch from relays?
+ - How many to display (fetch all, display subset)?
+ - Both with separate controls?
+
+4. *Auto-disable Threshold*: Is 20 the right number for auto-disabling tag anchors?
+
+** Ideal User Flow
+
+1. User loads the visualization
+2. Their follow list is fetched (kind 3, limit 1)
+3. Profiles are fetched for people they follow (kind 0, respecting limit)
+4. Publications are fetched (kind 30040/30041/30818)
+5. User enables "People" tag anchors
+6. Sees their follows as anchor points
+7. Can see which follows have authored content
+8. Can selectively enable/disable specific people
+9. Can increase limits to see more content/people
diff --git a/package-lock.json b/package-lock.json
index ad65282..c5b7295 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,7 +53,7 @@
"tailwindcss": "^3.4.17",
"tslib": "2.8.x",
"typescript": "^5.8.3",
- "vite": "^6.3.5",
+ "vite": "^7.0.5",
"vitest": "^3.1.3"
}
},
@@ -588,6 +588,7 @@
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
@@ -606,6 +607,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -618,6 +620,7 @@
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@@ -628,6 +631,7 @@
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@eslint/object-schema": "^2.1.6",
@@ -643,6 +647,7 @@
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
"integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -653,6 +658,7 @@
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
"integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@types/json-schema": "^7.0.15"
@@ -666,6 +672,7 @@
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"ajv": "^6.12.4",
@@ -690,6 +697,7 @@
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
"integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -703,6 +711,7 @@
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -713,6 +722,7 @@
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
"integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@eslint/core": "^0.15.1",
@@ -752,6 +762,7 @@
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18.0"
@@ -762,6 +773,7 @@
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
"integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@humanfs/core": "^0.19.1",
@@ -776,6 +788,7 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
"integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18"
@@ -790,6 +803,7 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=12.22"
@@ -804,6 +818,7 @@
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
+ "license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=18.18"
@@ -1548,6 +1563,7 @@
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.1.tgz",
"integrity": "sha512-mcWud3pYGPWM2Pphdj8G9Qiq24nZ8L4LB7coCUckUEy5Y7wOWGJ/enaZ4AtJTcSm5dNK1rIkBRoqt+ae4zlxcQ==",
"dev": true,
+ "license": "MIT",
"peerDependencies": {
"@sveltejs/kit": "^2.0.0"
}
@@ -1612,6 +1628,7 @@
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.0.tgz",
"integrity": "sha512-+U6lz1wvGEG/BvQyL4z/flyNdQ9xDNv5vrh+vWBWTHaebqT0c9RNggpZTo/XSPoHsSCWBlYaTlRX8pZ9GATXCw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0-next.1",
"debug": "^4.4.1",
@@ -1633,6 +1650,7 @@
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.0.tgz",
"integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"debug": "^4.4.1"
},
@@ -1967,19 +1985,22 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/@types/mathjax": {
"version": "0.0.40",
"resolved": "https://registry.npmjs.org/@types/mathjax/-/mathjax-0.0.40.tgz",
"integrity": "sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.1.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
- "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
+ "version": "24.0.15",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
+ "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
}
@@ -2144,6 +2165,7 @@
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
+ "license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -2153,6 +2175,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -2240,6 +2263,7 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
+ "license": "Python-2.0",
"peer": true
},
"node_modules/aria-query": {
@@ -2467,6 +2491,7 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
@@ -2568,6 +2593,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -2591,6 +2617,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -3102,6 +3129,7 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/deepmerge": {
@@ -3287,6 +3315,7 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -3300,6 +3329,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
@@ -3361,6 +3391,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.11.0.tgz",
"integrity": "sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.6.1",
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -3394,6 +3425,7 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz",
"integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=18"
},
@@ -3444,6 +3476,7 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
+ "license": "ISC",
"engines": {
"node": ">= 6"
}
@@ -3453,6 +3486,7 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -3469,6 +3503,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -3487,6 +3522,7 @@
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
@@ -3504,6 +3540,7 @@
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
+ "license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"estraverse": "^5.1.0"
@@ -3526,6 +3563,7 @@
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -3538,6 +3576,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -3553,6 +3592,7 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -3571,6 +3611,7 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/fast-glob": {
@@ -3604,6 +3645,7 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/fast-levenshtein": {
@@ -3611,6 +3653,7 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/fastq": {
@@ -3640,6 +3683,7 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"flat-cache": "^4.0.0"
@@ -3691,6 +3735,7 @@
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"locate-path": "^6.0.0",
@@ -3708,6 +3753,7 @@
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"flatted": "^3.2.9",
@@ -3722,6 +3768,7 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
+ "license": "ISC",
"peer": true
},
"node_modules/flowbite": {
@@ -3957,6 +4004,7 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
@@ -4072,6 +4120,7 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">= 4"
@@ -4082,6 +4131,7 @@
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"parent-module": "^1.0.0",
@@ -4099,6 +4149,7 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.8.19"
@@ -4304,6 +4355,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
@@ -4317,6 +4369,7 @@
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/json-schema-traverse": {
@@ -4324,6 +4377,7 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/json-stable-stringify-without-jsonify": {
@@ -4331,6 +4385,7 @@
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/jstransformer": {
@@ -4347,6 +4402,7 @@
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"json-buffer": "3.0.1"
@@ -4365,13 +4421,15 @@
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz",
"integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1",
@@ -4427,6 +4485,7 @@
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"p-locate": "^5.0.0"
@@ -4602,6 +4661,7 @@
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
+ "license": "MIT",
"peer": true
},
"node_modules/neo-async": {
@@ -4778,6 +4838,7 @@
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
@@ -4796,6 +4857,7 @@
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -4812,6 +4874,7 @@
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"p-limit": "^3.0.2"
@@ -4841,6 +4904,7 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"callsites": "^3.0.0"
@@ -5134,6 +5198,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"engines": {
"node": ">=18.0"
},
@@ -5160,6 +5225,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"engines": {
"node": ">=12.0"
},
@@ -5189,6 +5255,7 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">= 0.8.0"
@@ -5344,6 +5411,7 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
@@ -5505,6 +5573,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -5516,6 +5585,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
"engines": {
"node": ">=8.6"
},
@@ -5560,6 +5630,7 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
@@ -5667,6 +5738,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -5829,6 +5901,7 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
@@ -5994,6 +6067,7 @@
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
@@ -6009,6 +6083,7 @@
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
@@ -6022,6 +6097,7 @@
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.3.0.tgz",
"integrity": "sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"eslint-scope": "^8.2.0",
"eslint-visitor-keys": "^4.0.0",
@@ -6050,6 +6126,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -6370,6 +6447,7 @@
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
+ "license": "MIT",
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1"
@@ -6412,7 +6490,8 @@
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/unicode-emoji-modifier-base": {
"version": "1.0.0",
@@ -6465,6 +6544,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"punycode": "^2.1.0"
@@ -6476,23 +6556,24 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/vite": {
- "version": "6.3.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
- "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
+ "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
- "fdir": "^6.4.4",
+ "fdir": "^6.4.6",
"picomatch": "^4.0.2",
- "postcss": "^8.5.3",
- "rollup": "^4.34.9",
- "tinyglobby": "^0.2.13"
+ "postcss": "^8.5.6",
+ "rollup": "^4.40.0",
+ "tinyglobby": "^0.2.14"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -6501,14 +6582,14 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
- "less": "*",
+ "less": "^4.0.0",
"lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
@@ -6733,6 +6814,7 @@
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
@@ -6793,6 +6875,7 @@
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
@@ -6830,6 +6913,7 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
+ "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
diff --git a/src/app.css b/src/app.css
index 7a55d9d..4e2c9b2 100644
--- a/src/app.css
+++ b/src/app.css
@@ -201,6 +201,20 @@
.network-node-content {
@apply fill-primary-100;
}
+
+ /* Person link colors */
+ .person-link-signed {
+ @apply stroke-green-500;
+ }
+
+ .person-link-referenced {
+ @apply stroke-blue-400;
+ }
+
+ /* Person anchor node */
+ .person-anchor-node {
+ @apply fill-green-400 stroke-green-600;
+ }
}
/* Utilities can be applied via the @apply directive. */
diff --git a/src/lib/components/EventKindFilter.svelte b/src/lib/components/EventKindFilter.svelte
new file mode 100644
index 0000000..5f7b992
--- /dev/null
+++ b/src/lib/components/EventKindFilter.svelte
@@ -0,0 +1,204 @@
+
+
+
+
+ {#each $visualizationConfig.eventConfigs as ec}
+ {@const isEnabled = ec.enabled !== false}
+ {@const isLoaded = (eventCounts[ec.kind] || 0) > 0}
+ {@const borderColor = isLoaded ? 'border-green-500' : 'border-red-500'}
+
+ {/each}
+
+ {#if !showAddInput}
+
showAddInput = true}
+ class="gap-1"
+ >
+ +
+ Add Kind
+
+ {/if}
+
+
+
+ Reload
+
+
+
+ {#if showAddInput}
+
+ {
+ const value = (e.target as HTMLInputElement).value;
+ validateKind(value);
+ }}
+ />
+
+ Add
+
+ {
+ showAddInput = false;
+ newKind = '';
+ inputError = '';
+ }}
+ >
+ Cancel
+
+
+ {#if inputError}
+
+ {inputError}
+
+ {/if}
+ {/if}
+
+
+
+
+ Green border = Events loaded
+
+
+
+ Red border = Not loaded (click Reload to fetch)
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/components/EventLimitControl.svelte b/src/lib/components/EventLimitControl.svelte
deleted file mode 100644
index 9a32a56..0000000
--- a/src/lib/components/EventLimitControl.svelte
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
- Update
-
-
diff --git a/src/lib/components/EventTypeConfig.svelte b/src/lib/components/EventTypeConfig.svelte
new file mode 100644
index 0000000..4d7bfc7
--- /dev/null
+++ b/src/lib/components/EventTypeConfig.svelte
@@ -0,0 +1,274 @@
+
+
+
+
+ Showing {Object.values(eventCounts).reduce((a: any, b: any) => a + b, 0)} of {Object.values(eventCounts).reduce((a: any, b: any) => a + b, 0)} events
+
+
+
+
+
+
+ {#if showAddInput}
+
+ {
+ const validation = validateEventKind(e.currentTarget.value, existingKinds);
+ inputError = validation.error;
+ }}
+ />
+
+ Add
+
+ {
+ showAddInput = false;
+ newKind = '';
+ inputError = '';
+ }}
+ >
+ Cancel
+
+
+ {#if inputError}
+
+ {inputError}
+
+ {/if}
+ {:else}
+
showAddInput = true}
+ class="gap-1"
+ >
+ +
+ Add Event Type
+
+ {/if}
+
+
+
+
+ Reload
+
+
+
+
+
+
+ Green = Events loaded
+
+
+
+ Red = Not loaded (click Reload)
+
+
+
\ No newline at end of file
diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte
index 1ab1655..f2c986c 100644
--- a/src/lib/components/util/ArticleNav.svelte
+++ b/src/lib/components/util/ArticleNav.svelte
@@ -4,12 +4,14 @@
CaretLeftOutline,
CloseOutline,
GlobeOutline,
+ ChartOutline,
} from "flowbite-svelte-icons";
import { Button } from "flowbite-svelte";
import { publicationColumnVisibility } from "$lib/stores";
import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { onDestroy, onMount } from "svelte";
+ import { goto } from "$app/navigation";
let { publicationType, indexEvent } = $props<{
rootId: any;
@@ -102,6 +104,11 @@
}
}
+ function visualizePublication() {
+ const eventId = indexEvent.id;
+ goto(`/visualize?event=${eventId}`);
+ }
+
let unsubscribe: () => void;
onMount(() => {
window.addEventListener("scroll", handleScroll);
@@ -186,6 +193,16 @@
Discussion
{/if}
+
+ Visualize Publication
+
diff --git a/src/lib/consts.ts b/src/lib/consts.ts
index ef41e0d..90afa53 100644
--- a/src/lib/consts.ts
+++ b/src/lib/consts.ts
@@ -2,7 +2,7 @@
export const wikiKind = 30818;
export const indexKind = 30040;
-export const zettelKinds = [30041, 30818];
+export const zettelKinds = [30041, 30818, 30023];
export const communityRelays = [
"wss://theforest.nostr1.com",
@@ -29,18 +29,18 @@ export const secondaryRelays = [
export const anonymousRelays = [
"wss://freelay.sovbit.host",
- "wss://thecitadel.nostr1.com"
+ "wss://thecitadel.nostr1.com",
];
export const lowbandwidthRelays = [
"wss://theforest.nostr1.com",
"wss://thecitadel.nostr1.com",
- "wss://aggr.nostr.land"
+ "wss://aggr.nostr.land",
];
export const localRelays: string[] = [
"wss://localhost:8080",
- "wss://localhost:4869"
+ "wss://localhost:4869",
];
export enum FeedType {
diff --git a/src/lib/navigator/EventNetwork/Legend.svelte b/src/lib/navigator/EventNetwork/Legend.svelte
index b553cab..1c49fe6 100644
--- a/src/lib/navigator/EventNetwork/Legend.svelte
+++ b/src/lib/navigator/EventNetwork/Legend.svelte
@@ -1,14 +1,61 @@
-
-
-
+
Legend
-
+
{#if expanded}
{:else}
{/if}
-
+
{#if expanded}
-
-
- -
-
-
- I
-
+
+
+
+
+
Node Types
+
+ {#if nodeTypesExpanded}
+
+ {:else}
+
+ {/if}
+
-
Index events (kind 30040) - Each with a unique pastel color
-
+
+ {#if nodeTypesExpanded}
+
+
+ {#each Object.entries(eventCounts).sort(([a], [b]) => Number(a) - Number(b)) as [kindStr, count]}
+ {@const kind = Number(kindStr)}
+ {@const countNum = count as number}
+ {@const color = getEventKindColor(kind)}
+ {@const name = getEventKindName(kind)}
+ {#if countNum > 0}
+ -
+
+
+
+
+
+ {kind} - {name} ({countNum})
+
+
+ {/if}
+ {/each}
+
+
+ -
+
+
+ {#if starMode}
+ Radial connections from centers to related events
+ {:else}
+ Arrows indicate relationships and sequence
+ {/if}
+
+
+
+
+ {#if showPersonNodes && personAnchors.length > 0}
+ -
+
+
+ Authored by person
+
+
+ -
+
+
+ References person
+
+
+ {/if}
+
+ {/if}
+
-
-
-
-
-
- C
-
+
+
+
tagControlsExpanded = !tagControlsExpanded}>
+
Tag Anchor Controls
+
+ {#if tagControlsExpanded}
+
+ {:else}
+
+ {/if}
+
-
Content events (kinds 30041, 30818) - Publication sections
-
+
+ {#if tagControlsExpanded}
+
+
+
+ {
+ showTagAnchors = !showTagAnchors;
+ onTagSettingsChange();
+ }}
+ class="px-2 py-1 border border-gray-300 dark:border-gray-700 rounded text-xs font-medium cursor-pointer transition min-w-[3rem] hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-primary-500 {showTagAnchors ? 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:text-white dark:border-blue-600 dark:hover:bg-blue-700' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'}"
+ >
+ {showTagAnchors ? 'ON' : 'OFF'}
+
+ Show Tag Anchors
+
+
+ {#if showTagAnchors}
+
+
+
+
+
+
+ {/if}
+
+ {/if}
+
-
-
-
-
- Arrows indicate reading/sequence order
-
-
+
+ {#if showTags && tagAnchors.length > 0}
+
+
+
Active Tag Anchors: {tagAnchors[0].type}
+
+ {#if tagAnchorsExpanded}
+
+ {:else}
+
+ {/if}
+
+
+
+ {#if tagAnchorsExpanded}
+ {@const sortedAnchors = tagSortMode === 'count'
+ ? [...tagAnchors].sort((a, b) => b.count - a.count)
+ : [...tagAnchors].sort((a, b) => a.label.localeCompare(b.label))
+ }
+ {#if autoDisabledTags}
+
+ Note: All {tagAnchors.length} tags were auto-disabled to prevent graph overload. Click individual tags below to enable them.
+
+ {/if}
+
+
+
+
+
+ {#each sortedAnchors as anchor}
+ {@const tagId = `${anchor.type}-${anchor.label}`}
+ {@const isDisabled = disabledTags.has(tagId)}
+
onTagToggle(tagId)}
+ title={isDisabled ? `Click to show ${anchor.label}` : `Click to hide ${anchor.label}`}
+ >
+
+
+
+ {anchor.type === "t"
+ ? "#"
+ : anchor.type === "author"
+ ? "A"
+ : anchor.type.charAt(0).toUpperCase()}
+
+
+
+
+ {anchor.label.length > 25 ? anchor.label.slice(0, 22) + '...' : anchor.label}
+ {#if !isDisabled}
+ ({anchor.count})
+ {/if}
+
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
+
+
+
personVisualizerExpanded = !personVisualizerExpanded}>
+
Person Visualizer
+
+ {#if personVisualizerExpanded}
+
+ {:else}
+
+ {/if}
+
+
+
+ {#if personVisualizerExpanded}
+
+
+
+
+ {#if showPersonNodes && personAnchors.length > 0}
+
+
+ {#if totalPersonCount > displayedPersonCount}
+ Displaying {displayedPersonCount} of {totalPersonCount} people found:
+ {:else}
+ {personAnchors.length} people found:
+ {/if}
+
+
+
+
+
+
+ {#each personAnchors as person}
+ {@const isDisabled = disabledPersons.has(person.pubkey)}
+
{
+ if (showPersonNodes) {
+ onPersonToggle(person.pubkey);
+ }
+ }}
+ disabled={!showPersonNodes}
+ title={!showPersonNodes ? 'Enable "Show Person Nodes" first' : isDisabled ? `Click to show ${person.displayName || person.pubkey}` : `Click to hide ${person.displayName || person.pubkey}`}
+ >
+
+
+
+
+ {person.displayName || person.pubkey.slice(0, 8) + '...'}
+ {#if !isDisabled}
+
+ ({person.signedByCount || 0}s/{person.referencedCount || 0}r)
+
+ {/if}
+
+
+ {/each}
+
+ {:else if showPersonNodes}
+
+ No people found in the current events.
+
+ {/if}
+
+ {/if}
+
+
{/if}
diff --git a/src/lib/navigator/EventNetwork/NodeTooltip.svelte b/src/lib/navigator/EventNetwork/NodeTooltip.svelte
index ef455bf..42c72d2 100644
--- a/src/lib/navigator/EventNetwork/NodeTooltip.svelte
+++ b/src/lib/navigator/EventNetwork/NodeTooltip.svelte
@@ -8,6 +8,12 @@
import type { NetworkNode } from "./types";
import { onMount } from "svelte";
import { getMatchingTags } from "$lib/utils/nostrUtils";
+ import { getEventKindName } from "$lib/utils/eventColors";
+ import {
+ getDisplayNameSync,
+ replacePubkeysWithDisplayNames,
+ } from "$lib/utils/profileCache";
+ import {indexKind, zettelKinds, wikiKind} from "$lib/consts";
// Component props
let {
@@ -16,12 +22,14 @@
x,
y,
onclose,
+ starMode = false,
} = $props<{
node: NetworkNode; // The node to display information for
selected?: boolean; // Whether the node is selected (clicked)
x: number; // X position for the tooltip
y: number; // Y position for the tooltip
onclose: () => void; // Function to call when closing the tooltip
+ starMode?: boolean; // Whether we're in star visualization mode
}>();
// DOM reference and positioning
@@ -32,6 +40,9 @@
// Maximum content length to display
const MAX_CONTENT_LENGTH = 200;
+ // Publication event kinds (text/article based)
+ const PUBLICATION_KINDS = [wikiKind, indexKind, ...zettelKinds];
+
/**
* Gets the author name from the event tags
*/
@@ -39,7 +50,11 @@
if (node.event) {
const authorTags = getMatchingTags(node.event, "author");
if (authorTags.length > 0) {
- return authorTags[0][1];
+ return getDisplayNameSync(authorTags[0][1]);
+ }
+ // Fallback to event pubkey
+ if (node.event.pubkey) {
+ return getDisplayNameSync(node.event.pubkey);
}
}
return "Unknown";
@@ -71,6 +86,34 @@
return "View Publication";
}
+ /**
+ * Checks if this is a publication event
+ */
+ function isPublicationEvent(kind: number): boolean {
+ return PUBLICATION_KINDS.includes(kind);
+ }
+
+ /**
+ * Gets the appropriate URL for the event
+ */
+ function getEventUrl(node: NetworkNode): string {
+ if (isPublicationEvent(node.kind)) {
+ return `/publication?id=${node.id}`;
+ }
+ return `/events?id=${node.id}`;
+ }
+
+ /**
+ * Gets display text for the link
+ */
+ function getLinkText(node: NetworkNode): string {
+ if (isPublicationEvent(node.kind)) {
+ return node.title || "Untitled Publication";
+ }
+ // For arbitrary events, show event kind name
+ return node.title || `Event ${node.kind}`;
+ }
+
/**
* Truncates content to a maximum length
*/
@@ -145,39 +188,92 @@
- {node.type} (kind: {node.kind})
+ {#if isPublicationEvent(node.kind)}
+ {node.type} (kind: {node.kind})
+ {:else}
+ {getEventKindName(node.kind)}
+ {#if node.event?.created_at}
+ · {new Date(node.event.created_at * 1000).toLocaleDateString()}
+ {/if}
+ {/if}
-
+
- Author: {getAuthorTag(node)}
+ Pub Author: {getAuthorTag(node)}
-
- {#if node.isContainer && getSummaryTag(node)}
-
diff --git a/src/lib/navigator/EventNetwork/Settings.svelte b/src/lib/navigator/EventNetwork/Settings.svelte
index 2cff9e2..584834b 100644
--- a/src/lib/navigator/EventNetwork/Settings.svelte
+++ b/src/lib/navigator/EventNetwork/Settings.svelte
@@ -1,58 +1,136 @@
-
-
+
Settings
-
+
{#if expanded}
{:else}
{/if}
-
+
{#if expanded}
- Showing {count} events from {$networkFetchLimit} headers
+ Showing {count} of {totalCount} events
-
-
+
+
+
+
+ {#if eventTypesExpanded}
+
+ {/if}
+
+
+
+
+
+
+
+ {#if visualSettingsExpanded}
+
+
+
+
+
+ Toggle between star clusters (on) and linear sequence (off)
+ visualization
+
+
+
+
+
+ {/if}
+
{/if}
diff --git a/src/lib/navigator/EventNetwork/TagTable.svelte b/src/lib/navigator/EventNetwork/TagTable.svelte
new file mode 100644
index 0000000..fa02295
--- /dev/null
+++ b/src/lib/navigator/EventNetwork/TagTable.svelte
@@ -0,0 +1,82 @@
+
+
+
+{#if uniqueTags.length > 0}
+
+
+ {tagTypeLabels[selectedTagType] || 'Tags'}
+
+
+
+
+ | Tag |
+ Count |
+
+
+
+ {#each uniqueTags as tag}
+
+ | {tag.value} |
+ {tag.count} |
+
+ {/each}
+
+
+
+{:else}
+
+ No {tagTypeLabels[selectedTagType]?.toLowerCase() || 'tags'} found
+
+{/if}
+
+
\ No newline at end of file
diff --git a/src/lib/navigator/EventNetwork/index.svelte b/src/lib/navigator/EventNetwork/index.svelte
index ffbb44a..c9a8149 100644
--- a/src/lib/navigator/EventNetwork/index.svelte
+++ b/src/lib/navigator/EventNetwork/index.svelte
@@ -11,6 +11,16 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { levelsToRender } from "$lib/state";
import { generateGraph, getEventColor } from "./utils/networkBuilder";
+ import { getEventKindColor } from "$lib/utils/eventColors";
+ import {
+ generateStarGraph,
+ applyStarLayout,
+ } from "./utils/starNetworkBuilder";
+ import {
+ createStarSimulation,
+ applyInitialStarPositions,
+ createStarDragHandler,
+ } from "./utils/starForceSimulation";
import {
createSimulation,
setupDragHandlers,
@@ -22,7 +32,20 @@
import NodeTooltip from "./NodeTooltip.svelte";
import type { NetworkNode, NetworkLink } from "./types";
import Settings from "./Settings.svelte";
+ import {
+ enhanceGraphWithTags,
+ getTagAnchorColor,
+ } from "./utils/tagNetworkBuilder";
+ import {
+ extractUniquePersons,
+ createPersonAnchorNodes,
+ createPersonLinks,
+ extractPersonAnchorInfo,
+ } from "./utils/personNetworkBuilder";
import { Button } from "flowbite-svelte";
+ import { visualizationConfig } from "$lib/stores/visualizationConfig";
+ import { get } from "svelte/store";
+ import type { EventCounts } from "$lib/types";
// Type alias for D3 selections
type Selection = any;
@@ -45,9 +68,24 @@
}
// Component props
- let { events = [], onupdate } = $props<{
+ let {
+ events = [],
+ followListEvents = [],
+ totalCount = 0,
+ onupdate,
+ onclear = () => {},
+ onTagExpansionChange,
+ profileStats = { totalFetched: 0, displayLimit: 50 },
+ allEventCounts = {}
+ } = $props<{
events?: NDKEvent[];
+ followListEvents?: NDKEvent[];
+ totalCount?: number;
onupdate: () => void;
+ onclear?: () => void;
+ onTagExpansionChange?: (tags: string[]) => void;
+ profileStats?: { totalFetched: number; displayLimit: number };
+ allEventCounts?: EventCounts;
}>();
// Error state
@@ -81,10 +119,57 @@
let svgGroup: Selection;
let zoomBehavior: any;
let svgElement: Selection;
+
+ // Position cache to preserve node positions across updates
+ let nodePositions = new Map
();
// Track current render level
let currentLevels = $derived(levelsToRender);
+ // Star visualization state (default to true)
+ let starVisualization = $state(true);
+
+ // Tag anchors state
+ let showTagAnchors = $state(false);
+ let selectedTagType = $state("t"); // Default to hashtags
+ let tagAnchorInfo = $state([]);
+
+ // Store initial state to detect if component is being recreated
+ let componentId = Math.random();
+ debug("Component created with ID:", componentId);
+
+ // Event counts by kind - derived from events
+ let eventCounts = $derived.by(() => {
+ const counts: { [kind: number]: number } = {};
+ events.forEach((event: NDKEvent) => {
+ if (event.kind !== undefined) {
+ counts[event.kind] = (counts[event.kind] || 0) + 1;
+ }
+ });
+ return counts;
+ });
+
+ // Disabled tags state for interactive legend
+ let disabledTags = $state(new Set());
+
+ // Track if we've auto-disabled tags
+ let autoDisabledTags = $state(false);
+
+ // Maximum number of tag anchors before auto-disabling
+ const MAX_TAG_ANCHORS = 20;
+
+ // Person nodes state
+ let showPersonNodes = $state(false);
+ let personAnchorInfo = $state([]);
+ let disabledPersons = $state(new Set());
+ let showSignedBy = $state(true);
+ let showReferenced = $state(true);
+ let personMap = $state