feat: add search functionality
All checks were successful
Deploy eolas-app / deploy (push) Successful in 1m45s

This commit is contained in:
Thomas Bishop 2025-12-11 19:17:37 +00:00
parent 01b915552c
commit 7f9e227f6b
17 changed files with 1188 additions and 248 deletions

424
package-lock.json generated
View file

@ -9,10 +9,10 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-collapsible": "^1.1.7", "@radix-ui/react-collapsible": "^1.1.7",
"@radix-ui/react-dialog": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
@ -20,6 +20,7 @@
"@react-sigma/core": "^5.0.4", "@react-sigma/core": "^5.0.4",
"@react-sigma/layout-forceatlas2": "^5.0.6", "@react-sigma/layout-forceatlas2": "^5.0.6",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@tanstack/react-form": "^1.27.1",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0", "@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
@ -37,6 +38,7 @@
"react-resizable-panels": "^3.0.4", "react-resizable-panels": "^3.0.4",
"react-router": "^7.7.0", "react-router": "^7.7.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"shiki": "^3.9.1", "shiki": "^3.9.1",
@ -1175,21 +1177,22 @@
} }
}, },
"node_modules/@radix-ui/react-dialog": { "node_modules/@radix-ui/react-dialog": {
"version": "1.1.10", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.10.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
"integrity": "sha512-m6pZb0gEM5uHPSb+i2nKKGQi/HMSVjARMsLMWQfKDP+eJ6B+uqryHnXhpnohTWElw+vEcMk/o4wJODtdRKHwqg==", "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/primitive": "1.1.2", "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2", "@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.7", "@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-guards": "1.1.3",
"@radix-ui/react-focus-scope": "1.1.4", "@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1", "@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.6", "@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.3", "@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.0", "@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4", "aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3" "react-remove-scroll": "^2.6.3"
@ -1209,21 +1212,107 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
"version": "1.2.0", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-compose-refs": "1.1.2" "@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" "@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/react": { "@types/react": {
"optional": true "optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
} }
} }
}, },
@ -1269,9 +1358,10 @@
} }
}, },
"node_modules/@radix-ui/react-focus-guards": { "node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@ -1283,12 +1373,13 @@
} }
}, },
"node_modules/@radix-ui/react-focus-scope": { "node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.4", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1" "@radix-ui/react-use-callback-ref": "1.1.1"
}, },
"peerDependencies": { "peerDependencies": {
@ -1306,6 +1397,29 @@
} }
} }
}, },
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-hover-card": { "node_modules/@radix-ui/react-hover-card": {
"version": "1.1.15", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
@ -1514,12 +1628,12 @@
} }
}, },
"node_modules/@radix-ui/react-label": { "node_modules/@radix-ui/react-label": {
"version": "2.1.7", "version": "2.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.1.3" "@radix-ui/react-primitive": "2.1.4"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -1537,12 +1651,12 @@
} }
}, },
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-slot": "1.2.3" "@radix-ui/react-slot": "1.2.4"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -1559,6 +1673,24 @@
} }
} }
}, },
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz",
@ -1731,12 +1863,12 @@
} }
}, },
"node_modules/@radix-ui/react-separator": { "node_modules/@radix-ui/react-separator": {
"version": "1.1.4", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
"integrity": "sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w==", "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.1.0" "@radix-ui/react-primitive": "2.1.4"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -1753,6 +1885,47 @@
} }
} }
}, },
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@ -2904,6 +3077,51 @@
"vite": "^5.2.0 || ^6" "vite": "^5.2.0 || ^6"
} }
}, },
"node_modules/@tanstack/devtools-event-client": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.3.5.tgz",
"integrity": "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/form-core": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.27.1.tgz",
"integrity": "sha512-hPM+0tUnZ2C2zb2TE1lar1JJ0S0cbnQHlUwFcCnVBpMV3rjtUzkoM766gUpWrlmTGCzNad0GbJ0aTxVsjT6J8g==",
"license": "MIT",
"dependencies": {
"@tanstack/devtools-event-client": "^0.3.5",
"@tanstack/pacer": "^0.15.3",
"@tanstack/store": "^0.7.7"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/pacer": {
"version": "0.15.4",
"resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.15.4.tgz",
"integrity": "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==",
"license": "MIT",
"dependencies": {
"@tanstack/devtools-event-client": "^0.3.2",
"@tanstack/store": "^0.7.5"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "5.83.0", "version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
@ -2924,6 +3142,28 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@tanstack/react-form": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.27.1.tgz",
"integrity": "sha512-HKP0Ew2ae9AL5vU1PkJ+oAC2p+xBtA905u0fiNLzlfn1vLkBxenfg5L6TOA+rZITHpQsSo10tqwc5Yw6qn8Mpg==",
"license": "MIT",
"dependencies": {
"@tanstack/form-core": "1.27.1",
"@tanstack/react-store": "^0.8.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@tanstack/react-start": {
"optional": true
}
}
},
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "5.83.0", "version": "5.83.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
@ -2958,6 +3198,34 @@
"react": "^18 || ^19" "react": "^18 || ^19"
} }
}, },
"node_modules/@tanstack/react-store": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz",
"integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==",
"license": "MIT",
"dependencies": {
"@tanstack/store": "0.8.0",
"use-sync-external-store": "^1.6.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/react-store/node_modules/@tanstack/store": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz",
"integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-table": { "node_modules/@tanstack/react-table": {
"version": "8.21.3", "version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
@ -2978,6 +3246,16 @@
"react-dom": ">=16.8" "react-dom": ">=16.8"
} }
}, },
"node_modules/@tanstack/store": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz",
"integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/table-core": { "node_modules/@tanstack/table-core": {
"version": "8.21.3", "version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
@ -4641,6 +4919,31 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hast-util-raw": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
"integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/unist": "^3.0.0",
"@ungap/structured-clone": "^1.0.0",
"hast-util-from-parse5": "^8.0.0",
"hast-util-to-parse5": "^8.0.0",
"html-void-elements": "^3.0.0",
"mdast-util-to-hast": "^13.0.0",
"parse5": "^7.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0",
"web-namespaces": "^2.0.0",
"zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-html": { "node_modules/hast-util-to-html": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
@ -4691,6 +4994,25 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/hast-util-to-parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
"integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"devlop": "^1.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0",
"web-namespaces": "^2.0.0",
"zwitch": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hast-util-to-text": { "node_modules/hast-util-to-text": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
@ -6687,6 +7009,21 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/rehype-raw": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz",
"integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-raw": "^9.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": { "node_modules/remark-gfm": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
@ -7362,6 +7699,15 @@
} }
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vfile": { "node_modules/vfile": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",

View file

@ -11,10 +11,10 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-collapsible": "^1.1.7", "@radix-ui/react-collapsible": "^1.1.7",
"@radix-ui/react-dialog": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-separator": "^1.1.4", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tabs": "^1.1.12",
@ -22,6 +22,7 @@
"@react-sigma/core": "^5.0.4", "@react-sigma/core": "^5.0.4",
"@react-sigma/layout-forceatlas2": "^5.0.6", "@react-sigma/layout-forceatlas2": "^5.0.6",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@tanstack/react-form": "^1.27.1",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0", "@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
@ -39,6 +40,7 @@
"react-resizable-panels": "^3.0.4", "react-resizable-panels": "^3.0.4",
"react-router": "^7.7.0", "react-router": "^7.7.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"shiki": "^3.9.1", "shiki": "^3.9.1",

View file

@ -2,30 +2,34 @@
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap");
code { code {
font-family: "JetBrains Mono"; font-family: "JetBrains Mono";
} }
pre > code { pre>code {
padding: 0.5rem; padding: 0.5rem;
font-size: 14px; font-size: 14px;
} }
h2 > code { h2>code {
font-size: 1rem; font-size: 1rem;
} }
.btn[data-state="active"] { .btn[data-state="active"] {
box-shadow: none !important; box-shadow: none !important;
} }
button[data-state="active"] { button[data-state="active"] {
--tw-shadow: none; --tw-shadow: none;
--tw-shadow-colored: none; --tw-shadow-colored: none;
box-shadow: box-shadow:
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.sigma-container { .sigma-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
}
match {
color: "red" !important;
} }

View file

@ -1,5 +1,6 @@
import { SidebarTrigger } from "./ui/sidebar" import { SidebarTrigger } from "./ui/sidebar"
import { Separator } from "./ui/separator" import { Separator } from "./ui/separator"
import Search from "@/containers/Search"
export default function AppHeader({ pageTitle }: { pageTitle: string }) { export default function AppHeader({ pageTitle }: { pageTitle: string }) {
return ( return (
@ -7,7 +8,7 @@ export default function AppHeader({ pageTitle }: { pageTitle: string }) {
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6"> <div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" /> <Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
<h1 className="text-base font-medium">{pageTitle}</h1> <Search />
</div> </div>
</header> </header>
) )

View file

@ -12,7 +12,7 @@ export default function BodyLink({ link, children }) {
const cachedEntry = queryClient.getQueryData([`entry_${path}`]) const cachedEntry = queryClient.getQueryData([`entry_${path}`])
if (cachedEntry) { if (cachedEntry) {
setEntryExists(true) setEntryExists(true)
console.info("INFO: Entry exists in cache.") // console.info("INFO: Entry exists in cache.")
} else { } else {
try { try {
const remoteEntry = await queryClient.fetchQuery({ const remoteEntry = await queryClient.fetchQuery({
@ -21,9 +21,9 @@ export default function BodyLink({ link, children }) {
}) })
setEntryExists(true) setEntryExists(true)
console.info("INFO: Entry exists on remote.") // console.info("INFO: Entry exists on remote.")
} catch (error) { } catch (error) {
console.log(`INFO: Could not fetch entry ${path} ${error}`) // console.log(`INFO: Could not fetch entry ${path} ${error}`)
setEntryExists(false) setEntryExists(false)
} }
} }

View file

@ -0,0 +1,37 @@
import ReactMarkdown from "react-markdown"
import rehypeRaw from "rehype-raw"
import remarkGfm from "remark-gfm"
import remarkMath from "remark-math"
import { Link } from "react-router"
const stripMarkdownLinks = (text) => {
return text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
}
export default function EntriesSearchResult({ entry, match, searchParams }) {
return (
<Link to={`/entries/${entry}?highlight=${encodeURIComponent(searchParams)}`}>
{" "}
<div className="text-sm mt-1 mb-3 p-2 bg-muted hover:bg-sidebar">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeRaw]}
components={{
mark: ({ children }) => (
<span className="dark:bg-[#4c1d95] bg-[#ddd6fe]">{children}</span>
),
code: ({ children }) => (
<code className="rounded bg-mutedfont-mono text-[12px]">{children}</code>
),
pre: ({ children }) => (
<code className="rounded bg-muted font-mono text-[12px]">{children}</code>
),
}}
>
{stripMarkdownLinks(match)}
</ReactMarkdown>
<div className="font-semibold">{`${entry.replace(/_/g, " ")}`}</div>
</div>
</Link>
)
}

View file

@ -4,26 +4,9 @@ import CodeBlock from "@/components/CodeBlock"
import remarkMath from "remark-math" import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex" import rehypeKatex from "rehype-katex"
import "katex/dist/katex.min.css" import "katex/dist/katex.min.css"
import { Skeleton } from "@/components/ui/skeleton"
import BodyLink from "./BodyLink" import BodyLink from "./BodyLink"
import EntryLoadingSkeleton from "./EntryLoadingSkeleton"
const EntryLoadingSkeleton = () => { import { useSearchParams } from "react-router"
return (
<div className="space-y-2 max-w-2xl p-4 lg:p-6">
{/*
<Skeleton className="h-[400px] md:h-[800px] max-w-2xl rounded-none" />
*/}
<Skeleton className="h-4 max-w-full" />
<Skeleton className="h-4 md:max-w-xl max-w-[300px]" />
<Skeleton className="h-4 md:max-w-[400px] max-w-[250px]" />
<Skeleton className="h-4 md:max-w-[300px] max-w-[200px]" />
<Skeleton className="h-4 md:max-w-[200px] max-w-[150px]" />
<Skeleton className="h-4 md:max-w-[100px] max-w-[100px]" />
</div>
)
}
const ImagePreprocessor = (src) => { const ImagePreprocessor = (src) => {
const filename = src.src.split("/").pop() const filename = src.src.split("/").pop()
@ -31,73 +14,108 @@ const ImagePreprocessor = (src) => {
return <img src={s3RootUrl + filename} /> return <img src={s3RootUrl + filename} />
} }
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const highlighter = (children, highlight) => {
if (!highlight || typeof children !== "string") return children
const words = highlight.trim().split(/\s+/)
const pattern = words.length > 1 ? escapeRegex(highlight) : escapeRegex(words[0])
const regex = new RegExp(`\\b(${pattern})\\b`, "gi")
const parts = children.split(regex)
return parts.map((part, i) =>
regex.test(part) ? (
<mark key={i} className="dark:bg-[#4c1d95] dark:text-white bg-[#ddd6fe]">
{part}
</mark>
) : (
part
),
)
}
export default function EntryBody({ body, isLoading }) { export default function EntryBody({ body, isLoading }) {
const [searchParams] = useSearchParams()
const highlight = searchParams.get("highlight")
if (isLoading) { if (isLoading) {
return <EntryLoadingSkeleton /> return <EntryLoadingSkeleton />
} else }
return ( return (
<div className="max-w-2xl p-4 lg:p-6"> <div className="max-w-2xl p-4 lg:p-6">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]} rehypePlugins={[rehypeKatex]}
components={{ components={{
h1: () => null, h1: () => null,
h2: ({ children }) => ( h2: ({ children }) => (
<h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h2> <h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">
), {highlighter(children, highlight)}
h3: ({ children }) => ( </h2>
<h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h3> ),
), h3: ({ children }) => (
h4: ({ children }) => ( <h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">
<h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h4> {highlighter(children, highlight)}
), </h3>
p: ({ children }) => ( ),
<p className="leading-[1.5] mb-4 not-first:mt-4">{children}</p> h4: ({ children }) => (
), <h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">
ul: ({ children }) => ( {highlighter(children, highlight)}
<ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul> </h4>
), ),
ol: ({ children }) => ( p: ({ children }) => (
<ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol> <p className="leading-[1.5] mb-4 not-first:mt-4">
), {highlighter(children, highlight)}
table: ({ children }) => ( </p>
<table className="w-full mb-4 text-sm">{children}</table> ),
), ul: ({ children }) => <ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul>,
tr: ({ children }) => ( ol: ({ children }) => (
<tr className="even:bg-muted m-0 border-t p-0">{children}</tr> <ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol>
), ),
th: ({ children }) => ( li: ({ children }) => (
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"> <li className="list-disc ml-10 mb-4 space-y-1">
{children} {highlighter(children, highlight)}
</th> </li>
), ),
td: ({ children }) => ( table: ({ children }) => <table className="w-full mb-4 text-sm">{children}</table>,
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"> tr: ({ children }) => (
{children} <tr className="even:bg-muted m-0 border-t p-0">
</td> {highlighter(children, highlight)}
), </tr>
blockquote: ({ children }) => ( ),
<blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground"> th: ({ children }) => (
{children} <th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
</blockquote> {highlighter(children, highlight)}
), </th>
pre: ({ children }) => { ),
const child = children.props td: ({ children }) => (
return <CodeBlock className={child.className}>{child.children}</CodeBlock> <td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
}, {highlighter(children, highlight)}
code: ({ children }) => ( </td>
<code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm"> ),
{children} blockquote: ({ children }) => (
</code> <blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground">
), {highlighter(children, highlight)}
img: ({ src }) => <ImagePreprocessor src={src} />, </blockquote>
a: ({ href, children }) => { ),
return <BodyLink link={href} children={children} /> pre: ({ children }) => {
}, const child = children.props
}} return <CodeBlock className={child.className}>{child.children}</CodeBlock>
> },
{body} code: ({ children }) => (
</ReactMarkdown> <code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
</div> {children}
) </code>
),
img: ({ src }) => <ImagePreprocessor src={src} />,
a: ({ href, children }) => {
return <BodyLink link={href} children={children} />
},
}}
>
{body}
</ReactMarkdown>
</div>
)
} }

View file

@ -0,0 +1,18 @@
import { Skeleton } from "@/components/ui/skeleton"
export default function EntryLoadingSkeleton() {
return (
<div className="space-y-2 max-w-2xl p-4 lg:p-6">
{/*
<Skeleton className="h-[400px] md:h-[800px] max-w-2xl rounded-none" />
*/}
<Skeleton className="h-4 max-w-full" />
<Skeleton className="h-4 md:max-w-xl max-w-[300px]" />
<Skeleton className="h-4 md:max-w-[400px] max-w-[250px]" />
<Skeleton className="h-4 md:max-w-[300px] max-w-[200px]" />
<Skeleton className="h-4 md:max-w-[200px] max-w-[150px]" />
<Skeleton className="h-4 md:max-w-[100px] max-w-[100px]" />
</div>
)
}

View file

@ -0,0 +1,21 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table"
import { useQueryClient } from "@tanstack/react-query"
export default function SearchHistory({ }) {
const queryClient = useQueryClient()
const history = queryClient.getQueriesData({ queryKey: ["search_results"] })
console.log(history)
return (
<Table className="mt-2">
{/* <TableHeader> */}
{/* <TableRow> */}
{/* <TableHead>Search term</TableHead> */}
{/* </TableRow> */}
{/* </TableHeader> */}
<TableBody>
<TableRow>
<TableCell>Paid</TableCell>
</TableRow>
</TableBody>
</Table>
)
}

View file

@ -0,0 +1,42 @@
import { Field } from "./ui/field"
import { Input } from "./ui/input"
import { Kbd } from "./ui/kbd"
export default function SearchInput({ form, inputRef }) {
return (
<>
<form className="w-full">
<form.Field
name="search"
children={(field) => {
return (
<Field>
<div className="relative">
<Input
ref={inputRef}
className="rounded-none [&::-webkit-search-cancel-button]:hidden"
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
form.handleSubmit()
}
}}
type="search"
placeholder="Search"
/>
<div className="absolute right-5 top-1 pointer-events-none">
{field.state.value ? <Kbd>Enter</Kbd> : <Kbd>Ctrl + K</Kbd>}
</div>
</div>
</Field>
)
}}
/>
</form>
</>
)
}

View file

@ -0,0 +1,109 @@
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from "./ui/sheet"
import { Badge } from "./ui/badge"
import EntriesSearchResult from "./EntriesSearchResult"
import { Link } from "react-router"
import { Skeleton } from "./ui/skeleton"
export default function SearchResults({
form,
entriesResults,
tagResults,
sheetOpen,
loading,
error,
setSheetOpen,
searchParams,
setSearchParams,
}) {
return (
<Sheet
open={sheetOpen}
onOpenChange={(open) => {
setSheetOpen(open)
if (!open) {
form.reset()
setSearchParams(null)
}
}}
>
<SheetContent>
<SheetHeader className="border-b bg-sidebar">
<SheetTitle className="flex gap-3">
<div>Search Results</div>
</SheetTitle>
<SheetDescription className="flex flex-row gap-3">
<div className="text-foreground">Search term:</div>
<span className="font-medium text-foreground">{searchParams}</span>
</SheetDescription>
</SheetHeader>
{error ? (
<div className="p-4 text-sm dark:text-red-300 text-red-700">
<div className="p-2 border-2 dark:border-red-800 border-red-500 dark:bg-red-900 bg-red-300">
Error fetching search results.
</div>{" "}
</div>
) : (
<>
<div className="flex flex row justify-between bg-sidebar mx-2 pt-0">
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Badge variant="secondary" className="rounded-none">
{tagResults?.length || 0}
</Badge>
</div>
{loading ? (
<div className="p-4 flex flex-row gap-2">
<Skeleton className="h-4 w-[60px]" />
<Skeleton className="h-4 w-[60px]" />
</div>
) : (
<div className="p-4 pt-0 flex gap-2">
{tagResults.map((tagResult) => (
<Link to={`/tags/${tagResult}`}>
<Badge className="hover:bg-muted" variant="outline">
{tagResult}
</Badge>
</Link>
))}
</div>
)}
<div className="flex flex row justify-between bg-sidebar mx-2 pt-0">
<h3 className="font-medium text-sm p-1 ml-1">Entries</h3>
<Badge variant="secondary" className="rounded-none">
{entriesResults?.count || 0}
</Badge>
</div>
{loading ? (
<div className="p-4 flex flex-col gap-4">
<Skeleton className="rounded-none h-25 max-w-full" />
<Skeleton className="rounded-none h-25 max-w-full" />
<Skeleton className="rounded-none h-25 max-w-full" />
<Skeleton className="rounded-none h-25 max-w-full" />
</div>
) : (
<div className="overflow-y-auto overflow-x-hidden p-4 pt-0">
{entriesResults?.data.map((result, i) => (
<EntriesSearchResult
key={i}
entry={result.entry}
match={result.excerpt}
searchParams={searchParams}
/>
))}
</div>
)}
</>
)}
<SheetFooter></SheetFooter>
</SheetContent>
</Sheet>
)
}

View file

246
src/components/ui/field.tsx Normal file
View file

@ -0,0 +1,246 @@
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

28
src/components/ui/kbd.tsx Normal file
View file

@ -0,0 +1,28 @@
import { cn } from "@/lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className
)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
export { Kbd, KbdGroup }

View file

@ -5,133 +5,124 @@ import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} /> return <SheetPrimitive.Root data-slot="sheet" {...props} />
} }
function SheetTrigger({ function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
...props return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
} }
function SheetClose({ function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
...props return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
} }
function SheetPortal({ function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
...props return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
} }
function SheetOverlay({ function SheetOverlay({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { }: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return ( return (
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
data-slot="sheet-overlay" data-slot="sheet-overlay"
className={cn( className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className className,
)} )}
{...props} {...props}
/> />
) )
} }
function SheetContent({ function SheetContent({
className, className,
children, children,
side = "right", side = "right",
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & { }: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left" side?: "top" | "right" | "bottom" | "left"
}) { }) {
return ( return (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <SheetPrimitive.Content
data-slot="sheet-content" data-slot="sheet-content"
className={cn( className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" && side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" && side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" && side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" && side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className className,
)} )}
{...props} {...props}
> >
{children} {children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" /> <XIcon className="size-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
) )
} }
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-header" data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)} className={cn("flex flex-col gap-1.5 p-4", className)}
{...props} {...props}
/> />
) )
} }
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="sheet-footer" data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)} className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props} {...props}
/> />
) )
} }
function SheetTitle({ function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
className, return (
...props <SheetPrimitive.Title
}: React.ComponentProps<typeof SheetPrimitive.Title>) { data-slot="sheet-title"
return ( className={cn("text-foreground font-semibold", className)}
<SheetPrimitive.Title {...props}
data-slot="sheet-title" />
className={cn("text-foreground font-semibold", className)} )
{...props}
/>
)
} }
function SheetDescription({ function SheetDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return ( return (
<SheetPrimitive.Description <SheetPrimitive.Description
data-slot="sheet-description" data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)} className={cn("text-muted-foreground text-sm", className)}
{...props} {...props}
/> />
) )
} }
export { export {
Sheet, Sheet,
SheetTrigger, SheetTrigger,
SheetClose, SheetClose,
SheetContent, SheetContent,
SheetHeader, SheetHeader,
SheetFooter, SheetFooter,
SheetTitle, SheetTitle,
SheetDescription, SheetDescription,
} }

77
src/containers/Search.tsx Normal file
View file

@ -0,0 +1,77 @@
import { useState, useEffect, useRef } from "react"
import { useLocation } from "react-router"
import { useForm } from "@tanstack/react-form"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import api from "@/api/eolas-api"
import SearchInput from "@/components/SearchInput"
import SearchResults from "@/components/SearchResults"
export default function Search() {
const location = useLocation()
const inputRef = useRef(null)
const [sheetOpen, setSheetOpen] = useState(false)
const [tagResults, setTagResults] = useState([])
const [searchParams, setSearchParams] = useState(null)
const queryClient = useQueryClient()
const tags = queryClient.getQueryData(["tag_list"])?.["data"]
const form = useForm({
defaultValues: {
search: "",
},
onSubmit: ({ value }) => {
setSearchParams(value.search)
setSheetOpen(true)
setTagResults(
tags?.filter((tag) => tag.toLowerCase().includes(value.search.toLowerCase())),
)
},
})
const {
data: entriesResults,
isLoading,
error,
} = useQuery({
queryKey: ["search_results", searchParams],
queryFn: () => api.get(`/search/${searchParams}`).then((res) => res.data),
enabled: !!searchParams,
})
// Force Sheet close on renavigation (i.e search result selection)
useEffect(() => {
if (sheetOpen) {
setSheetOpen(false)
form.reset()
setSearchParams(null)
}
}, [location.pathname])
useEffect(() => {
const handleKeyDown = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault()
inputRef.current?.focus()
}
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [])
return (
<>
<SearchInput form={form} inputRef={inputRef} />
<SearchResults
searchParams={searchParams}
form={form}
sheetOpen={sheetOpen}
entriesResults={entriesResults}
tagResults={tagResults}
loading={isLoading}
error={error}
setSheetOpen={setSheetOpen}
setSearchParams={setSearchParams}
/>
</>
)
}

View file

@ -1,8 +1,8 @@
import ThemeToggle from "@/components/ThemeToggle" import ThemeToggle from "@/components/ThemeToggle"
export default function Settings() { export default function Settings() {
return ( return (
<div className="p-4 lg:p-6"> <div className="p-4 lg:p-6">
<ThemeToggle /> <ThemeToggle />
</div> </div>
) )
} }