diff --git a/package-lock.json b/package-lock.json index a2d21d6f..826485ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,6 +113,56 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, "node_modules/@atproto-labs/did-resolver": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/@atproto-labs/did-resolver/-/did-resolver-0.1.13.tgz", @@ -1616,6 +1666,37 @@ "dev": true, "license": "MIT" }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@bramus/specificity/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@bramus/specificity/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", @@ -1831,6 +1912,17 @@ "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", "dev": true }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -2872,6 +2964,23 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@expo/spawn-async": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", @@ -4760,6 +4869,15 @@ "node": ">=12.0.0" } }, + "node_modules/@mozilla/readability": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", + "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5065,6 +5183,7 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -12129,6 +12248,13 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/mozilla__readability": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@types/mozilla__readability/-/mozilla__readability-0.4.2.tgz", + "integrity": "sha512-mXHoZZ/Knps7GABAG9D512yDLCMQNOvQBqzIZpFJnRJveHc528rltnXh6EZTi1RiB1I2Z8yV35Dzs6DhE2WaYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -12330,7 +12456,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true + "devOptional": true }, "node_modules/@types/unist": { "version": "3.0.3", @@ -14352,6 +14478,15 @@ "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -16231,8 +16366,7 @@ "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==" }, "node_modules/decode-named-character-reference": { "version": "1.2.0", @@ -16733,10 +16867,10 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", - "dev": true, + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -20689,8 +20823,7 @@ "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, "node_modules/is-promise": { "version": "4.0.0", @@ -20905,6 +21038,372 @@ "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==" }, + "node_modules/isomorphic-dompurify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.11.0.tgz", + "integrity": "sha512-il0sNhLnfawc6vKoMqnZX1JXzEzw0pP3DZWg7mM7VJNJ8cq6DCxeAjEcfjTazyBF8dkUn+rBsBPS5NQs4ZaI3g==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.4.1", + "jsdom": "^29.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@csstools/css-calc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@csstools/css-color-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "peer": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/isomorphic-dompurify/node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/isomorphic-dompurify/node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/isomorphic-dompurify/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/isomorphic-dompurify/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tldts": { + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz", + "integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.29" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tldts-core": { + "version": "7.0.29", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz", + "integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==", + "license": "MIT" + }, + "node_modules/isomorphic-dompurify/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/isomorphic-dompurify/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -21730,6 +22229,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "cssstyle": "^4.2.1", @@ -26425,7 +26925,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -27210,6 +27709,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -27610,7 +28118,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, "dependencies": { "xmlchars": "^2.2.0" }, @@ -29284,8 +29791,7 @@ "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "node_modules/synckit": { "version": "0.11.11", @@ -31385,7 +31891,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, "dependencies": { "xml-name-validator": "^5.0.0" }, @@ -31815,7 +32320,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, "engines": { "node": ">=18" } @@ -31848,8 +32352,7 @@ "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, "node_modules/xtend": { "version": "4.0.2", @@ -32116,6 +32619,7 @@ "@mantine/notifications": "9.0.0", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", + "@mozilla/readability": "^0.6.0", "@next/mdx": "16.2.3", "@semble/types": "*", "@tanstack/react-query": "^5.85.5", @@ -32124,6 +32628,7 @@ "capnweb": "^0.6.1", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "isomorphic-dompurify": "^3.11.0", "next": "16.2.3", "react": "19.2.5", "react-dom": "19.2.5", @@ -32146,6 +32651,8 @@ "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/chrome": "^0.0.332", + "@types/jsdom": "^21.1.7", + "@types/mozilla__readability": "^0.4.2", "@types/node": "^20.10.4", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", diff --git a/src/webapp/app/(dashboard)/url/layout.tsx b/src/webapp/app/(dashboard)/url/layout.tsx index dd802c37..18239d17 100644 --- a/src/webapp/app/(dashboard)/url/layout.tsx +++ b/src/webapp/app/(dashboard)/url/layout.tsx @@ -1,6 +1,7 @@ import BackButton from '@/components/navigation/backButton/BackButton'; import Header from '@/components/navigation/header/Header'; -import { Fragment } from 'react'; +import ReaderHeaderButton from '@/features/reader/components/ReaderHeaderButton/ReaderHeaderButton'; +import { Fragment, Suspense } from 'react'; interface Props { children: React.ReactNode; @@ -11,6 +12,9 @@ export default async function Layout(props: Props) {
+ + +
{props.children}
diff --git a/src/webapp/app/api/feature-flags/route.ts b/src/webapp/app/api/feature-flags/route.ts index 95361cc1..dd6bf8c6 100644 --- a/src/webapp/app/api/feature-flags/route.ts +++ b/src/webapp/app/api/feature-flags/route.ts @@ -17,6 +17,7 @@ export async function GET() { following: false, connections: false, graphView: false, + readerMode: false, }); } } diff --git a/src/webapp/app/api/reader/route.ts b/src/webapp/app/api/reader/route.ts new file mode 100644 index 00000000..b836e3ff --- /dev/null +++ b/src/webapp/app/api/reader/route.ts @@ -0,0 +1,85 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { JSDOM } from 'jsdom'; +import { Readability } from '@mozilla/readability'; +import DOMPurify from 'isomorphic-dompurify'; + +export interface ReaderContent { + title: string | null; + content: string; + byline: string | null; + siteName: string | null; + excerpt: string | null; +} + +export async function GET(request: NextRequest) { + const { searchParams } = request.nextUrl; + const url = searchParams.get('url'); + + if (!url) { + return NextResponse.json({ error: 'Missing url parameter' }, { status: 400 }); + } + + let targetUrl: URL; + try { + targetUrl = new URL(url); + } catch { + return NextResponse.json({ error: 'Invalid URL' }, { status: 400 }); + } + + let html: string; + try { + const response = await fetch(targetUrl.toString(), { + headers: { + // Mimic a real browser to avoid blocks + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + }, + // 10 second timeout + signal: AbortSignal.timeout(10_000), + }); + + if (!response.ok) { + return NextResponse.json( + { error: `Failed to fetch URL: ${response.status} ${response.statusText}` }, + { status: 502 }, + ); + } + + html = await response.text(); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return NextResponse.json({ error: `Failed to fetch URL: ${message}` }, { status: 502 }); + } + + // Parse with jsdom, passing the URL so Readability can resolve relative links + const dom = new JSDOM(html, { url: targetUrl.toString() }); + const reader = new Readability(dom.window.document); + const article = reader.parse(); + + if (!article) { + return NextResponse.json( + { error: 'Could not extract readable content from this page' }, + { status: 422 }, + ); + } + + // Sanitize the extracted HTML to prevent XSS + const sanitizedContent = DOMPurify.sanitize(article.content ?? '', { + USE_PROFILES: { html: true }, + // Allow common article elements but strip scripts/iframes/etc. + FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'input', 'button'], + FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'], + }); + + const result: ReaderContent = { + title: article.title ?? null, + content: sanitizedContent, + byline: article.byline ?? null, + siteName: article.siteName ?? null, + excerpt: article.excerpt ?? null, + }; + + return NextResponse.json(result); +} diff --git a/src/webapp/components/navigation/header/Header.tsx b/src/webapp/components/navigation/header/Header.tsx index a4c43833..7e0342fe 100644 --- a/src/webapp/components/navigation/header/Header.tsx +++ b/src/webapp/components/navigation/header/Header.tsx @@ -1,10 +1,10 @@ import { Box, Divider, Group, Paper, Text } from '@mantine/core'; import NavbarToggle from '../NavbarToggle'; -import { ReactElement } from 'react'; +import { ReactNode } from 'react'; interface Props { title?: string; - children?: ReactElement; + children?: ReactNode; } export default function Header(props: Props) { diff --git a/src/webapp/features/reader/components/ReaderButton/ReaderButton.module.css b/src/webapp/features/reader/components/ReaderButton/ReaderButton.module.css new file mode 100644 index 00000000..7e37d4e0 --- /dev/null +++ b/src/webapp/features/reader/components/ReaderButton/ReaderButton.module.css @@ -0,0 +1,135 @@ +/* ── Bottom bar ── */ + +.bottomBar { + position: relative; + flex-shrink: 0; + background: color-mix(in srgb, var(--mantine-color-body) 85%, transparent); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-top: 1px solid var(--mantine-color-gray-2); +} + +/* ── Article body typography ── */ + +.readerContent :global(p) { + margin: 0 0 1.3em; +} + +.readerContent :global(h1), +.readerContent :global(h2), +.readerContent :global(h3), +.readerContent :global(h4), +.readerContent :global(h5), +.readerContent :global(h6) { + font-weight: 700; + line-height: 1.3; + margin: 1.75em 0 0.5em; +} + +.readerContent :global(h1) { font-size: 1.6em; } +.readerContent :global(h2) { font-size: 1.35em; } +.readerContent :global(h3) { font-size: 1.15em; } +.readerContent :global(h4) { font-size: 1.0em; } + +.readerContent :global(a) { + color: var(--mantine-color-blue-6); + text-decoration: underline; + text-underline-offset: 3px; +} + +.readerContent :global(a:hover) { + color: var(--mantine-color-blue-8); +} + +.readerContent :global(blockquote) { + border-left: 3px solid var(--mantine-color-gray-4); + margin: 1.5em 0; + padding: 0.4em 1.25em; + color: var(--mantine-color-dimmed); + font-style: italic; +} + +.readerContent :global(pre) { + background: var(--mantine-color-gray-0); + border: 1px solid var(--mantine-color-gray-2); + border-radius: 8px; + padding: 1em 1.25em; + overflow-x: auto; + font-size: 0.875em; + line-height: 1.6; + margin: 1.5em 0; +} + +.readerContent :global(code) { + background: var(--mantine-color-gray-1); + border-radius: 4px; + padding: 0.15em 0.4em; + font-size: 0.875em; + font-family: var(--mantine-font-family-monospace, monospace); +} + +.readerContent :global(pre code) { + background: none; + padding: 0; + font-size: inherit; +} + +.readerContent :global(ul), +.readerContent :global(ol) { + padding-left: 1.75em; + margin: 0 0 1.3em; +} + +.readerContent :global(li) { + margin-bottom: 0.4em; +} + +.readerContent :global(li > ul), +.readerContent :global(li > ol) { + margin-top: 0.3em; + margin-bottom: 0; +} + +.readerContent :global(img) { + max-width: 100%; + height: auto; + border-radius: 8px; + margin: 1.5em 0; + display: block; +} + +.readerContent :global(figure) { + margin: 1.75em 0; +} + +.readerContent :global(figcaption) { + font-size: 0.8125em; + color: var(--mantine-color-dimmed); + text-align: center; + margin-top: 0.6em; +} + +.readerContent :global(hr) { + border: none; + border-top: 1px solid var(--mantine-color-gray-3); + margin: 2em 0; +} + +.readerContent :global(table) { + width: 100%; + border-collapse: collapse; + margin: 1.5em 0; + font-size: 0.9375em; +} + +.readerContent :global(th), +.readerContent :global(td) { + border: 1px solid var(--mantine-color-gray-3); + padding: 0.5em 0.75em; + text-align: left; +} + +.readerContent :global(th) { + background: var(--mantine-color-gray-0); + font-weight: 600; +} diff --git a/src/webapp/features/reader/components/ReaderButton/ReaderButton.tsx b/src/webapp/features/reader/components/ReaderButton/ReaderButton.tsx new file mode 100644 index 00000000..501e38e2 --- /dev/null +++ b/src/webapp/features/reader/components/ReaderButton/ReaderButton.tsx @@ -0,0 +1,273 @@ +'use client'; + +import { useState } from 'react'; +import { + ActionIcon, + Drawer, + Tooltip, + Stack, + Title, + Text, + Alert, + ScrollArea, + Box, + Group, + CloseButton, + Skeleton, + Button, + Anchor, + Container, +} from '@mantine/core'; +import { TbBook2, TbExternalLink, TbLink, TbLinkOff } from 'react-icons/tb'; +import { MdErrorOutline } from 'react-icons/md'; +import { fetchReaderContent, type ReaderState } from '../../lib/fetchReaderContent'; +import styles from './ReaderButton.module.css'; + +interface Props { + url: string; +} + +const FONT_SIZE_MIN = 14; +const FONT_SIZE_MAX = 24; +const FONT_SIZE_STEP = 2; +const FONT_SIZE_DEFAULT = 17; + +function ArticleSkeleton() { + return ( + + + + + + + {[100, 95, 88, 100, 92, 78, 100, 96, 83, 60].map((w, i) => ( + + ))} + + + {[100, 91, 100, 85, 100, 72].map((w, i) => ( + + ))} + + + ); +} + +export default function ReaderButton({ url }: Props) { + const [opened, setOpened] = useState(false); + const [state, setState] = useState({ status: 'idle' }); + const [fontSize, setFontSize] = useState(FONT_SIZE_DEFAULT); + const [removeLinks, setRemoveLinks] = useState(false); + + const displayedHtml = + state.status === 'success' + ? removeLinks + ? state.data.content.replace(/]*>([\s\S]*?)<\/a>/g, '$1') + : state.data.content + : ''; + + function openReader() { + setOpened(true); + if (state.status === 'success' || state.status === 'loading') return; + fetchReaderContent(url, setState); + } + + function handleClose() { + setOpened(false); + } + + return ( + <> + + + + {/* ── Scrollable article area ── */} + + + {state.status === 'loading' && } + + {state.status === 'error' && ( + + } + title="Could not load reader" + color="red" + variant="light" + > + {state.message} + + + + )} + + {state.status === 'success' && ( + + {state.data.siteName && ( + + {state.data.siteName} + + )} + + {state.data.title && ( + + {state.data.title} + + )} + + {state.data.byline && ( + + {state.data.byline} + + )} + + + + )} + + + + {/* ── Sticky bottom bar ── */} + + + {/* Left: font size + link toggle */} + + + + setFontSize((s) => + Math.max(FONT_SIZE_MIN, s - FONT_SIZE_STEP), + ) + } + disabled={fontSize <= FONT_SIZE_MIN} + aria-label="Decrease font size" + > + + A- + + + + + + setFontSize((s) => + Math.min(FONT_SIZE_MAX, s + FONT_SIZE_STEP), + ) + } + disabled={fontSize >= FONT_SIZE_MAX} + aria-label="Increase font size" + > + + A+ + + + + + setRemoveLinks((v) => !v)} + aria-label={removeLinks ? 'Show links' : 'Remove links'} + > + {removeLinks ? : } + + + + + {/* Right: open original + close */} + + + + + + + + + + + + ); +} diff --git a/src/webapp/features/reader/components/ReaderHeaderButton/ReaderHeaderButton.tsx b/src/webapp/features/reader/components/ReaderHeaderButton/ReaderHeaderButton.tsx new file mode 100644 index 00000000..c1b0a401 --- /dev/null +++ b/src/webapp/features/reader/components/ReaderHeaderButton/ReaderHeaderButton.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useFeatureFlags } from '@/lib/clientFeatureFlags'; +import ReaderButton from '../ReaderButton/ReaderButton'; + +export default function ReaderHeaderButton() { + const { data: featureFlags } = useFeatureFlags(); + const searchParams = useSearchParams(); + const rawId = searchParams.get('id'); + + if (!featureFlags?.readerMode || !rawId) return null; + + const url = decodeURIComponent(rawId); + return ; +} diff --git a/src/webapp/features/reader/lib/fetchReaderContent.ts b/src/webapp/features/reader/lib/fetchReaderContent.ts new file mode 100644 index 00000000..9a15de7e --- /dev/null +++ b/src/webapp/features/reader/lib/fetchReaderContent.ts @@ -0,0 +1,27 @@ +import type { ReaderContent } from '@/app/api/reader/route'; + +export type ReaderState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'error'; message: string } + | { status: 'success'; data: ReaderContent }; + +export async function fetchReaderContent( + url: string, + setState: (state: ReaderState) => void, +): Promise { + setState({ status: 'loading' }); + try { + const res = await fetch(`/api/reader?url=${encodeURIComponent(url)}`); + const json = await res.json(); + if (!res.ok) { + setState({ status: 'error', message: json.error ?? 'Unknown error' }); + return; + } + setState({ status: 'success', data: json as ReaderContent }); + } catch (err) { + const message = + err instanceof Error ? err.message : 'Failed to load reader content'; + setState({ status: 'error', message }); + } +} diff --git a/src/webapp/lib/clientFeatureFlags.ts b/src/webapp/lib/clientFeatureFlags.ts index 6dbd5177..94f76156 100644 --- a/src/webapp/lib/clientFeatureFlags.ts +++ b/src/webapp/lib/clientFeatureFlags.ts @@ -11,6 +11,7 @@ interface FeatureFlags { following: boolean; connections: boolean; graphView: boolean; + readerMode: boolean; } async function fetchFeatureFlags(): Promise { diff --git a/src/webapp/lib/serverFeatureFlags.ts b/src/webapp/lib/serverFeatureFlags.ts index 966ff1ce..81f5c35a 100644 --- a/src/webapp/lib/serverFeatureFlags.ts +++ b/src/webapp/lib/serverFeatureFlags.ts @@ -26,5 +26,6 @@ export async function getServerFeatureFlags() { following: true, connections: true, graphView: show, + readerMode: showForTeam, }; } diff --git a/src/webapp/next-env.d.ts b/src/webapp/next-env.d.ts index 9edff1c7..c4b7818f 100644 --- a/src/webapp/next-env.d.ts +++ b/src/webapp/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/webapp/package.json b/src/webapp/package.json index 14aa3a0c..07cc7f20 100644 --- a/src/webapp/package.json +++ b/src/webapp/package.json @@ -38,6 +38,7 @@ "@mantine/notifications": "9.0.0", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", + "@mozilla/readability": "^0.6.0", "@next/mdx": "16.2.3", "@semble/types": "*", "@tanstack/react-query": "^5.85.5", @@ -46,6 +47,7 @@ "capnweb": "^0.6.1", "date-fns": "^4.1.0", "dayjs": "^1.11.13", + "isomorphic-dompurify": "^3.11.0", "next": "16.2.3", "react": "19.2.5", "react-dom": "19.2.5", @@ -57,36 +59,38 @@ "web-haptics": "^0.0.6" }, "devDependencies": { + "@chromatic-com/storybook": "^5.1.2", "@playwright/test": "^1.59.1", + "@storybook/addon-a11y": "^10.3.6", + "@storybook/addon-docs": "^10.3.6", + "@storybook/addon-onboarding": "^10.3.6", + "@storybook/addon-vitest": "^10.3.6", + "@storybook/nextjs-vite": "^10.3.6", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/chrome": "^0.0.332", + "@types/jsdom": "^21.1.7", + "@types/mozilla__readability": "^0.4.2", "@types/node": "^20.10.4", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/browser-playwright": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5", "autoprefixer": "^10.4.16", "eslint": "^9.0.0", "eslint-config-next": "16.2.3", + "eslint-plugin-storybook": "^10.3.6", "jsdom": "^26.1.0", "plasmo": "^0.90.5", "postcss": "^8.5.6", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "typescript": "^5.3.3", - "vitest": "^4.1.5", "storybook": "^10.3.6", - "@storybook/nextjs-vite": "^10.3.6", - "@chromatic-com/storybook": "^5.1.2", - "@storybook/addon-vitest": "^10.3.6", - "@storybook/addon-a11y": "^10.3.6", - "@storybook/addon-docs": "^10.3.6", - "@storybook/addon-onboarding": "^10.3.6", + "typescript": "^5.3.3", "vite": "^8.0.10", - "eslint-plugin-storybook": "^10.3.6", - "@vitest/browser-playwright": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5" + "vitest": "^4.1.5" }, "overrides": { "@types/react": "19.2.14",