diff --git a/app/package-lock.json b/app/package-lock.json index 22ce382a..aa7af56a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -16,6 +16,9 @@ "@tabler/icons-react": "^3.34.0", "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.83.0", + "@uiw/codemirror-extensions-langs": "^4.25.2", + "@uiw/codemirror-theme-github": "^4.25.2", + "@uiw/react-codemirror": "^4.25.2", "dayjs": "^1.11.13", "framer-motion": "^12.23.24", "react": "^19.1.0", @@ -23,6 +26,7 @@ "react-plotly.js": "^2.6.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.3", + "thememirror": "^2.0.1", "wordwrapjs": "^5.1.1" }, "devDependencies": { @@ -428,6 +432,416 @@ "findup": "bin/findup.js" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.19.1.tgz", + "integrity": "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", + "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", + "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", + "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", + "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", + "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-jinja": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.0.tgz", + "integrity": "sha512-47MFmRcR8UAxd8DReVgj7WJN1WSAMT7OJnewwugZM4XiHWkOjgJQqvEM1NpMj9ALMPyxmlziEI1opH9IaEvmaw==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz", + "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", + "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", + "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", + "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", + "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", + "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", + "license": "MIT", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-jinja": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", + "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.1.tgz", + "integrity": "sha512-te7To1EQHePBQQzasDKWmK2xKINIXpk+xAiSYr9ZN+VB4KaT+/Hi2PEkeErTk5BV3PTz1TLyQL4MtJfPkKZ9sw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", + "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -1423,6 +1837,183 @@ "buffer": "^6.0.3" } }, + "node_modules/@lezer/common": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.3.0.tgz", + "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", + "license": "MIT" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.3.tgz", + "integrity": "sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz", + "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/go": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", + "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", + "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.5.1.tgz", + "integrity": "sha512-F3ZFnIfNAOy/jPSk6Q0e3bs7e9grfK/n5zerkKoc5COH6Guy3Zb0vrJwXzdck79K16goBhYBRAvhf+ksqe0cMg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.5.tgz", + "integrity": "sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz", + "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", + "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@mantine/core": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.1.2.tgz", @@ -1583,6 +2174,12 @@ "license": "ISC", "peer": true }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1777,6 +2374,52 @@ } } }, + "node_modules/@replit/codemirror-lang-nix": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz", + "integrity": "sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==", + "license": "MIT", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@replit/codemirror-lang-solidity": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-solidity/-/codemirror-lang-solidity-6.0.2.tgz", + "integrity": "sha512-/dpTVH338KFV6SaDYYSadkB4bI/0B0QRF/bkt1XS3t3QtyR49mn6+2k0OUQhvt2ZSO7kt10J+OPilRAtgbmX0w==", + "license": "MIT", + "dependencies": { + "@lezer/highlight": "^1.2.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@replit/codemirror-lang-svelte": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz", + "integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==", + "license": "MIT", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.1", + "@codemirror/lang-html": "^6.2.0", + "@codemirror/lang-javascript": "^6.1.1", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/javascript": "^1.2.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -3122,6 +3765,111 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.2.tgz", + "integrity": "sha512-s2fbpdXrSMWEc86moll/d007ZFhu6jzwNu5cWv/2o7egymvLeZO52LWkewgbr+BUCGWGPsoJVWeaejbsb/hLcw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-extensions-langs": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.25.2.tgz", + "integrity": "sha512-fWS9fP52QJAFgXbsUl6vKMBqQ2PIT8z5TvX8BKBqPz/I+ayh6Fk+HzoeUtslUGPTu+UTPgB5m0qt3rTIDXWvng==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/language-data": "^6.5.1", + "@replit/codemirror-lang-nix": "^6.0.1", + "@replit/codemirror-lang-solidity": "^6.0.1", + "@replit/codemirror-lang-svelte": "^6.0.0", + "codemirror-lang-mermaid": "^0.5.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/language-data": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-theme-github": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.25.2.tgz", + "integrity": "sha512-9g3ujmYCNU2VQCp0+XzI1NS5hSZGgXRtH+5yWli5faiPvHGYZUVke+5Pnzdn/1tkgW6NpTQ7U/JHsyQkgbnZ/w==", + "license": "MIT", + "dependencies": { + "@uiw/codemirror-themes": "4.25.2" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.25.2.tgz", + "integrity": "sha512-WFYxW3OlCkMomXQBlQdGj1JZ011UNCa7xYdmgYqywVc4E8f5VgIzRwCZSBNVjpWGGDBOjc+Z6F65l7gttP16pg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.2.tgz", + "integrity": "sha512-XP3R1xyE0CP6Q0iR0xf3ed+cJzJnfmbLelgJR6osVVtMStGGZP3pGQjjwDRYptmjGHfEELUyyBLdY25h0BQg7w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.2", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", @@ -4247,6 +4995,32 @@ "node": ">=6" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-lang-mermaid": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.5.0.tgz", + "integrity": "sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.9.0", + "@lezer/highlight": "^1.1.6", + "@lezer/lr": "^1.3.10" + } + }, "node_modules/color-alpha": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.4.tgz", @@ -4450,6 +5224,12 @@ "license": "MIT", "peer": true }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -11210,6 +11990,12 @@ "webpack": "^5.27.0" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/stylelint": { "version": "16.21.1", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.21.1.tgz", @@ -11827,6 +12613,17 @@ } } }, + "node_modules/thememirror": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/thememirror/-/thememirror-2.0.1.tgz", + "integrity": "sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==", + "license": "MIT", + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -12654,6 +13451,12 @@ "pbf": "^3.2.1" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/app/package.json b/app/package.json index 7eafef8b..9435d153 100644 --- a/app/package.json +++ b/app/package.json @@ -31,6 +31,9 @@ "@tabler/icons-react": "^3.34.0", "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.83.0", + "@uiw/codemirror-extensions-langs": "^4.25.2", + "@uiw/codemirror-theme-github": "^4.25.2", + "@uiw/react-codemirror": "^4.25.2", "dayjs": "^1.11.13", "framer-motion": "^12.23.24", "react": "^19.1.0", @@ -38,6 +41,7 @@ "react-plotly.js": "^2.6.0", "react-redux": "^9.2.0", "react-router-dom": "^7.9.3", + "thememirror": "^2.0.1", "wordwrapjs": "^5.1.1" }, "devDependencies": { @@ -76,4 +80,4 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.3" } -} \ No newline at end of file +} diff --git a/app/src/Router.tsx b/app/src/Router.tsx index 881d7088..fe1cbdd8 100644 --- a/app/src/Router.tsx +++ b/app/src/Router.tsx @@ -6,6 +6,7 @@ import { PolicyCreationFlow } from './flows/policyCreationFlow'; import { PopulationCreationFlow } from './flows/populationCreationFlow'; import { ReportCreationFlow } from './flows/reportCreationFlow'; import { SimulationCreationFlow } from './flows/simulationCreationFlow'; +import APIPage from './pages/API.page'; import DashboardPage from './pages/Dashboard.page'; import DonatePage from './pages/Donate.page'; import HomePage from './pages/Home.page'; @@ -101,6 +102,21 @@ const router = createBrowserRouter( }, ], }, + // Static pages that need metadata - use MetadataLazyLoader + StaticLayout + { + element: , + children: [ + { + element: , + children: [ + { + path: 'api', + element: , + }, + ], + }, + ], + }, // Routes that don't need metadata at all (no guard) { element: , diff --git a/app/src/components/home-header/DesktopNavigation.tsx b/app/src/components/home-header/DesktopNavigation.tsx index 66970573..14449679 100644 --- a/app/src/components/home-header/DesktopNavigation.tsx +++ b/app/src/components/home-header/DesktopNavigation.tsx @@ -11,12 +11,14 @@ interface NavLink { interface DesktopNavigationProps { navLinks: NavLink[]; aboutLinks: NavLink[]; + learnLinks: NavLink[]; onNavClick: (path?: string) => void; } export default function DesktopNavigation({ navLinks, aboutLinks, + learnLinks, onNavClick, }: DesktopNavigationProps) { const countryId = useCurrentCountry(); @@ -74,6 +76,31 @@ export default function DesktopNavigation({ {link.label} ))} + + + + + + + Learn + + + + + + + {learnLinks.map((link) => ( + onNavClick(link.path)}> + {link.label} + + ))} + + ); } diff --git a/app/src/components/home-header/HeaderContent.tsx b/app/src/components/home-header/HeaderContent.tsx index d38ccc6c..bdde603a 100644 --- a/app/src/components/home-header/HeaderContent.tsx +++ b/app/src/components/home-header/HeaderContent.tsx @@ -15,6 +15,7 @@ interface HeaderContentProps { onClose: () => void; navLinks: NavLink[]; aboutLinks: NavLink[]; + learnLinks: NavLink[]; onNavClick: (path?: string) => void; } @@ -24,6 +25,7 @@ export default function HeaderContent({ onClose, navLinks, aboutLinks, + learnLinks, onNavClick, }: HeaderContentProps) { return ( @@ -41,7 +43,12 @@ export default function HeaderContent({ - + @@ -54,6 +61,7 @@ export default function HeaderContent({ onClose={onClose} navLinks={navLinks} aboutLinks={aboutLinks} + learnLinks={learnLinks} onNavClick={onNavClick} /> diff --git a/app/src/components/home-header/MobileMenu.tsx b/app/src/components/home-header/MobileMenu.tsx index cbf804b8..3aa30cdb 100644 --- a/app/src/components/home-header/MobileMenu.tsx +++ b/app/src/components/home-header/MobileMenu.tsx @@ -13,6 +13,7 @@ interface MobileMenuProps { onClose: () => void; navLinks: NavLink[]; aboutLinks: NavLink[]; + learnLinks: NavLink[]; onNavClick: (path?: string) => void; } @@ -22,6 +23,7 @@ export default function MobileMenu({ onClose, navLinks, aboutLinks, + learnLinks, onNavClick, }: MobileMenuProps) { return ( @@ -95,6 +97,37 @@ export default function MobileMenu({ ))} + + + + {/* Learn Section */} + + + Learn + + + {learnLinks.map((link) => ( + onNavClick(link.path)} + style={{ fontFamily: typography.fontFamily.primary }} + > + {link.label} + + ))} + + diff --git a/app/src/components/shared/HomeHeader.tsx b/app/src/components/shared/HomeHeader.tsx index 6aae3088..1650667a 100644 --- a/app/src/components/shared/HomeHeader.tsx +++ b/app/src/components/shared/HomeHeader.tsx @@ -19,6 +19,8 @@ export default function HeaderNavigation() { { label: 'Supporters', path: `/${countryId}/supporters` }, ]; + const learnLinks: NavLink[] = [{ label: 'API', path: `/${countryId}/api` }]; + const navLinks: NavLink[] = [{ label: 'Donate', path: `/${countryId}/donate` }]; const handleNavClick = (path?: string) => { @@ -61,6 +63,7 @@ export default function HeaderNavigation() { onClose={close} navLinks={navLinks} aboutLinks={aboutLinks} + learnLinks={learnLinks} onNavClick={handleNavClick} /> diff --git a/app/src/components/shared/static/APIMetadataCard.tsx b/app/src/components/shared/static/APIMetadataCard.tsx new file mode 100644 index 00000000..955b979a --- /dev/null +++ b/app/src/components/shared/static/APIMetadataCard.tsx @@ -0,0 +1,116 @@ +import { Badge, Card, Stack, Text } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; + +interface ParameterMetadata { + type: 'parameter'; + parameter: string; + label?: string; + description?: string; + unit?: string; + period?: string | null; + economy?: boolean; + household?: boolean; + values?: Record; +} + +interface VariableMetadata { + name: string; + label?: string; + description?: string; + entity?: string; + definitionPeriod?: string; + unit?: string; + category?: string; + defaultValue?: number; + isInputVariable?: boolean; + valueType?: string; +} + +interface APIMetadataCardProps { + metadata: ParameterMetadata | VariableMetadata; + onClick?: () => void; +} + +export default function APIMetadataCard({ metadata, onClick }: APIMetadataCardProps) { + const isParameter = 'type' in metadata && metadata.type === 'parameter'; + const paramData = isParameter ? (metadata as ParameterMetadata) : null; + const varData = !isParameter ? (metadata as VariableMetadata) : null; + + const displayLabel = paramData?.label || varData?.label || varData?.name || ''; + const description = metadata.description || ''; + const pythonName = paramData?.parameter || varData?.name || ''; + + return ( + + + + {isParameter ? 'Parameter' : 'Variable'} + + + + {displayLabel} + + + {description && ( + + {description} + + )} + + {varData?.entity && ( + + Entity: {varData.entity} + + )} + + {(paramData?.period || varData?.definitionPeriod) && ( + + Period: {paramData?.period || varData?.definitionPeriod} + + )} + + {metadata.unit && ( + + Unit: {metadata.unit} + + )} + + + Python name: {pythonName} + + + + ); +} diff --git a/app/src/components/shared/static/APIPlayground.tsx b/app/src/components/shared/static/APIPlayground.tsx new file mode 100644 index 00000000..99218813 --- /dev/null +++ b/app/src/components/shared/static/APIPlayground.tsx @@ -0,0 +1,24 @@ +import { Box } from '@mantine/core'; +import { spacing } from '@/designTokens'; + +interface APIPlaygroundProps { + countryId: string; +} + +export default function APIPlayground({ countryId }: APIPlaygroundProps) { + const streamlitUrl = `https://policyengine-policyengine-api-demo-app-xy5rgn.streamlit.app/?embed=true&embed_options=light_theme&embed_options=hide_footer&mode=${countryId}`; + + return ( + + ); +} diff --git a/app/src/components/shared/static/ActionButton.tsx b/app/src/components/shared/static/ActionButton.tsx index 465cf001..05b1b17a 100644 --- a/app/src/components/shared/static/ActionButton.tsx +++ b/app/src/components/shared/static/ActionButton.tsx @@ -8,6 +8,7 @@ export interface ActionButtonProps { variant?: 'primary' | 'secondary' | 'inverted'; multiline?: boolean; caption?: string; + target?: '_blank' | '_self'; } export default function ActionButton({ @@ -16,6 +17,7 @@ export default function ActionButton({ variant = 'primary', multiline = false, caption, + target = '_blank', }: ActionButtonProps) { const buttonRef = useRef(null); const [buttonWidth, setButtonWidth] = useState(null); @@ -64,8 +66,8 @@ export default function ActionButton({ ref={buttonRef} component="a" href={href} - target="_blank" - rel="noopener noreferrer" + target={target} + rel={target === '_blank' ? 'noopener noreferrer' : undefined} size="lg" px={spacing.xl} py={spacing.lg} diff --git a/app/src/components/shared/static/CodeBlock.tsx b/app/src/components/shared/static/CodeBlock.tsx new file mode 100644 index 00000000..b18e2337 --- /dev/null +++ b/app/src/components/shared/static/CodeBlock.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react'; +import { IconCheck, IconChevronDown, IconChevronUp, IconCopy } from '@tabler/icons-react'; +import { langs } from '@uiw/codemirror-extensions-langs'; +import CodeMirror, { EditorView } from '@uiw/react-codemirror'; +import { espresso } from 'thememirror'; +import { Box, Button, CopyButton, Flex, Group, Stack, Text } from '@mantine/core'; +import { colors, spacing, typography } from '@/designTokens'; + +interface CodeBlockProps { + code: string; + language?: string; + title?: string; + showExpand?: boolean; +} + +export default function CodeBlock({ code, language, title, showExpand = true }: CodeBlockProps) { + const [isExpanded, setIsExpanded] = useState(false); + + // Map common language names to CodeMirror language extensions + const getLanguageExtension = () => { + if (!language) { + return langs.json(); + } + + const langKey = language.toLowerCase() as keyof typeof langs; + return langKey in langs ? langs[langKey]() : langs.json(); + }; + + return ( + + {title && ( + + {title} + + )} + + {/* Header bar */} + + + {language || 'code'} + + + {showExpand && ( + + )} + + {({ copied, copy }) => ( + + )} + + + + + {/* Code content with syntax highlighting */} + + + + ); +} diff --git a/app/src/components/shared/static/ContentSection.tsx b/app/src/components/shared/static/ContentSection.tsx index ece115a7..bc49d48c 100644 --- a/app/src/components/shared/static/ContentSection.tsx +++ b/app/src/components/shared/static/ContentSection.tsx @@ -7,6 +7,7 @@ export interface ContentSectionProps { variant?: 'primary' | 'secondary' | 'accent'; children: ReactNode; centerTitle?: boolean; + id?: string; } export default function ContentSection({ @@ -14,6 +15,7 @@ export default function ContentSection({ variant = 'primary', children, centerTitle = false, + id, }: ContentSectionProps) { const backgrounds = { primary: colors.white, @@ -29,6 +31,7 @@ export default function ContentSection({ return ( ; +} + +interface VariableMetadata { + name: string; + label?: string; + description?: string; + entity?: string; + definitionPeriod?: string; + unit?: string; + category?: string; + defaultValue?: number; + isInputVariable?: boolean; + valueType?: string; +} + +interface Metadata { + variables: Record; + parameters: Record; +} + +interface VariableParameterExplorerProps { + metadata: Metadata; +} + +export default function VariableParameterExplorer({ metadata }: VariableParameterExplorerProps) { + const [query, setQuery] = useState(''); + const [showAbolitions, setShowAbolitions] = useState(false); + const [page, setPage] = useState(0); + + // Convert metadata to card array + const variableCards: VariableMetadata[] = Object.values(metadata.variables || {}); + const parameterCards: ParameterMetadata[] = Object.values(metadata.parameters || {}); + + // Filter and sort + const filterByQuery = (item: ParameterMetadata | VariableMetadata) => { + const label = item.label || ''; + const pythonName = 'type' in item ? item.parameter : item.name; + + // Hide abolitions unless checkbox checked + if (!showAbolitions && pythonName?.startsWith('gov.abolitions')) { + return false; + } + + // Filter by search query + if (query) { + const normalizedQuery = query.replaceAll(' ', '').toLowerCase(); + const normalizedLabel = label.replaceAll(' ', '').toLowerCase(); + return ( + normalizedLabel.includes(normalizedQuery) || + pythonName?.toLowerCase().includes(query.toLowerCase()) + ); + } + + return true; + }; + + const allCards = [...parameterCards, ...variableCards].filter(filterByQuery).sort((a, b) => { + const labelA = (a.label || ('name' in a ? a.name : '')).toLowerCase(); + const labelB = (b.label || ('name' in b ? b.name : '')).toLowerCase(); + return labelA.localeCompare(labelB); + }); + + const totalPages = Math.ceil(allCards.length / CARDS_PER_PAGE); + const startIdx = page * CARDS_PER_PAGE; + const endIdx = startIdx + CARDS_PER_PAGE; + const currentCards = allCards.slice(startIdx, endIdx); + + const handleQueryChange = (value: string) => { + setQuery(value); + setPage(0); // Reset to first page on new search + }; + + const handlePageInputChange = (e: React.ChangeEvent) => { + const value = parseInt(e.target.value, 10); + if (!isNaN(value) && value >= 1 && value <= totalPages) { + setPage(value - 1); // Convert from 1-indexed to 0-indexed + } + }; + + return ( + + {/* Search and Filter Controls */} + + handleQueryChange(e.currentTarget.value)} + leftSection={} + style={{ flex: 1 }} + /> + + { + setShowAbolitions(e.currentTarget.checked); + setPage(0); + }} + /> + + + + {/* Cards Grid */} + + {currentCards.map((card, idx) => ( + + + + ))} + + + {/* Pagination Controls */} + {totalPages > 1 && ( + + + + + Page + + of {totalPages} + + + + + )} + + {/* Results Count */} + + Showing {currentCards.length} of {allCards.length} results + + + ); +} diff --git a/app/src/constants/apiExamples.ts b/app/src/constants/apiExamples.ts new file mode 100644 index 00000000..896f362a --- /dev/null +++ b/app/src/constants/apiExamples.ts @@ -0,0 +1,59 @@ +export const TOKEN_FETCH_CODE = `import requests +import json + +CLIENT_ID = "YOUR_CLIENT_ID" +CLIENT_SECRET = "YOUR_CLIENT_SECRET" + +payload = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "audience": "https://household.api.policyengine.org", + "grant_type": "client_credentials" +} + +headers = { "content-type": "application/json" } + +auth_response = requests.post( + "https://policyengine.uk.auth0.com/oauth/token", + headers=headers, + json=payload +) + +result = auth_response.json() +print(result["access_token"])`; + +export const TOKEN_RESPONSE_CODE = `{ + "access_token": "YOUR_ACCESS_TOKEN", + "token_type": "Bearer" +}`; + +export const getCalculateRequestCode = (countryId: string) => `import requests + +url = "https://household.api.policyengine.org/${countryId}/calculate" + +headers = { + "Authorization": "Bearer YOUR_TOKEN_HERE", + "Content-Type": "application/json", +} + +household_data = { + "household": { + "people": { + "parent": { + "age": { "2023": 30 }, + "employment_income": { "2023": 20000 } + }, + "child": { + "age": { "2023": 5 } + } + }, + "households": { + "household": { + "members": ["parent", "child"] + } + } + } +} + +response = requests.post(url, headers=headers, json=household_data) +print(response.json())`; diff --git a/app/src/pages/API.page.tsx b/app/src/pages/API.page.tsx new file mode 100644 index 00000000..423ff2a1 --- /dev/null +++ b/app/src/pages/API.page.tsx @@ -0,0 +1,164 @@ +import { useSelector } from 'react-redux'; +import { Code, SimpleGrid, Title } from '@mantine/core'; +import APIPlayground from '@/components/shared/static/APIPlayground'; +import CodeBlock from '@/components/shared/static/CodeBlock'; +import ContentSection from '@/components/shared/static/ContentSection'; +import CTASection from '@/components/shared/static/CTASection'; +import HeroSection from '@/components/shared/static/HeroSection'; +import RichTextBlock from '@/components/shared/static/RichTextBlock'; +import StaticPageLayout from '@/components/shared/static/StaticPageLayout'; +import VariableParameterExplorer from '@/components/shared/static/VariableParameterExplorer'; +import { + getCalculateRequestCode, + TOKEN_FETCH_CODE, + TOKEN_RESPONSE_CODE, +} from '@/constants/apiExamples'; +import { spacing } from '@/designTokens'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; +import type { RootState } from '@/store'; + +export default function APIPage() { + const countryId = useCurrentCountry(); + const metadata = useSelector((state: RootState) => state.metadata); + + // Sample API response for demonstration + const sampleResponse = { + status: 'ok', + result: { + households: { + household: { + household_net_income: { + '2023': 18500, + }, + }, + }, + people: { + parent: { + employment_income: { + '2023': 20000, + }, + income_tax: { + '2023': 1500, + }, + }, + child: { + age: { + '2023': 5, + }, + }, + }, + }, + }; + + return ( + + + + +

+ PolicyEngine's REST API ( + + https://household.api.policyengine.org + + ) simulates tax-benefit policy outcomes and reform impacts for households. Access + requires a Client ID and Client Secret provided by + PolicyEngine. For access, contact us at{' '} + hello@policyengine.org. +

+

+ On this page: +

+ + + } + cta={{ + text: 'Try API Playground', + href: '#playground', + target: '_self', + }} + /> + + + +

+ Execute a credentials exchange using your client ID and client secret to obtain an + authentication token. Include this token in the authorization header of every request as + "Bearer YOUR_TOKEN". Tokens expire monthly for security. +

+
+ + + + +
+ + + +

+ Returns household-level policy outcomes. Pass in a household object defining people, + groups and any variable values. Use null values for requested variables - these will be + computed and returned. +

+
+ + <Code>POST /{countryId}/calculate</Code> + + + + + +
+ + + +

+ Access information about all available variables and parameters in the PolicyEngine API. +

+
+ + <Code>GET /{countryId}/metadata</Code> + + {metadata && } +
+ + + +

Try out the API in this interactive demo.

+
+ +
+
+ ); +} diff --git a/app/src/tests/fixtures/components/home-header/HeaderMocks.ts b/app/src/tests/fixtures/components/home-header/HeaderMocks.ts index e971dd39..03b2eb7e 100644 --- a/app/src/tests/fixtures/components/home-header/HeaderMocks.ts +++ b/app/src/tests/fixtures/components/home-header/HeaderMocks.ts @@ -5,12 +5,15 @@ export const MOCK_ABOUT_LINKS = [ { label: 'Supporters', path: '/us/supporters' }, ]; +export const MOCK_LEARN_LINKS = [{ label: 'API', path: '/us/api' }]; + export const MOCK_HEADER_PROPS = { opened: false, onOpen: () => {}, onClose: () => {}, navLinks: MOCK_NAV_LINKS, aboutLinks: MOCK_ABOUT_LINKS, + learnLinks: MOCK_LEARN_LINKS, onNavClick: () => {}, } as const; diff --git a/app/src/tests/fixtures/components/home/HomeMocks.ts b/app/src/tests/fixtures/components/home/HomeMocks.ts index 9800d502..7121f3c3 100644 --- a/app/src/tests/fixtures/components/home/HomeMocks.ts +++ b/app/src/tests/fixtures/components/home/HomeMocks.ts @@ -5,6 +5,8 @@ export const MOCK_ABOUT_LINKS = [ { label: 'Supporters', path: '/us/supporters' }, ]; +export const MOCK_LEARN_LINKS = [{ label: 'API', path: '/us/api' }]; + export const EXPECTED_TEXT = { US: { HERO_TITLE: 'Computing public policy for everyone', diff --git a/app/src/tests/unit/components/home-header/DesktopNavigation.test.tsx b/app/src/tests/unit/components/home-header/DesktopNavigation.test.tsx index 474c78ff..11908aad 100644 --- a/app/src/tests/unit/components/home-header/DesktopNavigation.test.tsx +++ b/app/src/tests/unit/components/home-header/DesktopNavigation.test.tsx @@ -1,7 +1,11 @@ import { renderWithCountry, screen } from '@test-utils'; import { describe, expect, test, vi } from 'vitest'; import DesktopNavigation from '@/components/home-header/DesktopNavigation'; -import { MOCK_ABOUT_LINKS, MOCK_NAV_LINKS } from '@/tests/fixtures/components/home/HomeMocks'; +import { + MOCK_ABOUT_LINKS, + MOCK_LEARN_LINKS, + MOCK_NAV_LINKS, +} from '@/tests/fixtures/components/home/HomeMocks'; describe('DesktopNavigation', () => { test('given nav links then displays nav items', () => { @@ -13,6 +17,7 @@ describe('DesktopNavigation', () => { , 'us' @@ -33,6 +38,7 @@ describe('DesktopNavigation', () => { , 'us' @@ -55,6 +61,7 @@ describe('DesktopNavigation', () => { , 'us' @@ -76,6 +83,7 @@ describe('DesktopNavigation', () => { , 'us' @@ -98,6 +106,7 @@ describe('DesktopNavigation', () => { , 'us' diff --git a/app/src/tests/unit/components/home-header/HeaderContent.test.tsx b/app/src/tests/unit/components/home-header/HeaderContent.test.tsx index 77b95b92..551b9567 100644 --- a/app/src/tests/unit/components/home-header/HeaderContent.test.tsx +++ b/app/src/tests/unit/components/home-header/HeaderContent.test.tsx @@ -3,6 +3,7 @@ import { describe, expect, test, vi } from 'vitest'; import HeaderContent from '@/components/home-header/HeaderContent'; import { MOCK_ABOUT_LINKS, + MOCK_LEARN_LINKS, MOCK_NAV_LINKS, } from '@/tests/fixtures/components/home-header/HeaderMocks'; @@ -21,6 +22,7 @@ describe('HeaderContent', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -45,6 +47,7 @@ describe('HeaderContent', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -70,6 +73,7 @@ describe('HeaderContent', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' diff --git a/app/src/tests/unit/components/home-header/MobileMenu.test.tsx b/app/src/tests/unit/components/home-header/MobileMenu.test.tsx index bff4e8e5..89eabc65 100644 --- a/app/src/tests/unit/components/home-header/MobileMenu.test.tsx +++ b/app/src/tests/unit/components/home-header/MobileMenu.test.tsx @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import MobileMenu from '@/components/home-header/MobileMenu'; import { MOCK_ABOUT_LINKS, + MOCK_LEARN_LINKS, MOCK_NAV_LINKS, } from '@/tests/fixtures/components/home-header/HeaderMocks'; @@ -25,6 +26,7 @@ describe('MobileMenu', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -52,6 +54,7 @@ describe('MobileMenu', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -79,6 +82,7 @@ describe('MobileMenu', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -104,6 +108,7 @@ describe('MobileMenu', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -130,6 +135,7 @@ describe('MobileMenu', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' @@ -157,6 +163,7 @@ describe('MobileMenu', () => { onClose={onClose} navLinks={MOCK_NAV_LINKS} aboutLinks={MOCK_ABOUT_LINKS} + learnLinks={MOCK_LEARN_LINKS} onNavClick={onNavClick} />, 'us' diff --git a/app/src/tests/unit/components/shared/static/ActionButton.test.tsx b/app/src/tests/unit/components/shared/static/ActionButton.test.tsx index 054f733c..73f49294 100644 --- a/app/src/tests/unit/components/shared/static/ActionButton.test.tsx +++ b/app/src/tests/unit/components/shared/static/ActionButton.test.tsx @@ -21,7 +21,7 @@ describe('ActionButton', () => { expect(link).toHaveAttribute('href', TEST_BUTTON_HREF); }); - test('given href then link opens in new tab', () => { + test('given href then link opens in new tab by default', () => { // Given / When render(); @@ -31,6 +31,16 @@ describe('ActionButton', () => { expect(link).toHaveAttribute('rel', 'noopener noreferrer'); }); + test('given target prop _self then link opens in same tab', () => { + // Given / When + render(); + + // Then + const link = screen.getByRole('link', { name: TEST_BUTTON_TEXT }); + expect(link).toHaveAttribute('target', '_self'); + expect(link).not.toHaveAttribute('rel'); + }); + test('given caption then caption text is rendered', () => { // Given / When render(); diff --git a/app/src/tests/unit/components/shared/static/ContentSection.test.tsx b/app/src/tests/unit/components/shared/static/ContentSection.test.tsx index a836198b..ca49354f 100644 --- a/app/src/tests/unit/components/shared/static/ContentSection.test.tsx +++ b/app/src/tests/unit/components/shared/static/ContentSection.test.tsx @@ -71,4 +71,20 @@ describe('ContentSection', () => { // Then expect(container.firstChild).toBeInTheDocument(); }); + + test('given id prop then element has id attribute', () => { + // Given + const testId = 'test-section'; + + // When + render( + +
Content
+
+ ); + + // Then + const section = document.getElementById(testId); + expect(section).toBeInTheDocument(); + }); });