diff --git a/pwa/bun.lock b/pwa/bun.lock index 9253d22..1d395cf 100644 --- a/pwa/bun.lock +++ b/pwa/bun.lock @@ -5,7 +5,7 @@ "": { "name": "pwa", "dependencies": { - "highlight.js": "^11.11.1", + "shiki": "^4.2.0", }, "devDependencies": { "@types/bun": "latest", @@ -57,21 +57,61 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@shikijs/core": ["@shikijs/core@4.2.0", "", { "dependencies": { "@shikijs/primitive": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g=="], + + "@shikijs/langs": ["@shikijs/langs@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA=="], + + "@shikijs/themes": ["@shikijs/themes@4.2.0", "", { "dependencies": { "@shikijs/types": "4.2.0" } }, "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w=="], + + "@shikijs/types": ["@shikijs/types@4.2.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -97,26 +137,74 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], + "shiki": ["shiki@4.2.0", "", { "dependencies": { "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/engine-oniguruma": "4.2.0", "@shikijs/langs": "4.2.0", "@shikijs/themes": "4.2.0", "@shikijs/types": "4.2.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], } } diff --git a/pwa/package.json b/pwa/package.json index 309aa0b..d38086d 100644 --- a/pwa/package.json +++ b/pwa/package.json @@ -1,18 +1,18 @@ { "name": "crush-pwa", + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5", + "vite": "^8.0.16" + }, "private": true, - "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, - "devDependencies": { - "@types/bun": "latest", - "typescript": "^5", - "vite": "^8.0.16" - }, + "type": "module", "dependencies": { - "highlight.js": "^11.11.1" + "shiki": "^4.2.0" } } diff --git a/pwa/src/chat.ts b/pwa/src/chat.ts index 5df5eb1..3b2c1b2 100644 --- a/pwa/src/chat.ts +++ b/pwa/src/chat.ts @@ -1,8 +1,9 @@ -import { state, escapeHTML, renderPart } from './state'; +import { state, escapeHTML, renderPart, deferHighlightAll } from './state'; import { handleDisconnect } from './connect'; +import type { Message } from './types'; declare const lucide: { createIcons: () => void } | undefined; -function initIcons() { try { lucide?.createIcons(); } catch {} } +export function initIcons() { try { lucide?.createIcons(); } catch {} } const app = () => document.querySelector('#app')!; @@ -71,15 +72,94 @@ export function renderChat(): void { initIcons(); } +let renderedUpTo = 0; // index from bottom: how many messages are rendered +const INITIAL_COUNT = 50; +const BATCH_SIZE = 50; + export function renderMessages(): void { const container = document.getElementById('messages'); if (!container) return; - container.innerHTML = state.messages.map((msg) => { - const parts = (msg.parts || []).map((p) => renderPart(p)).join(''); - return `
${msg.role}
${parts}
`; - }).join(''); + const msgs = state.messages; + renderedUpTo = Math.min(INITIAL_COUNT, msgs.length); + const startIdx = msgs.length - renderedUpTo; + + container.innerHTML = ''; + + const fragment = document.createDocumentFragment(); + for (let i = startIdx; i < msgs.length; i++) { + fragment.appendChild(createMessageEl(msgs[i])); + } + container.appendChild(fragment); + initIcons(); + deferHighlightAll(); + + // Preload remaining messages in background batches + if (startIdx > 0) { + schedulePreload(container, startIdx); + } +} + +function schedulePreload(container: HTMLElement, nextStartIdx: number): void { + requestIdleCallback(() => { + if (!container.isConnected || nextStartIdx <= 0) return; + + const endIdx = nextStartIdx; + const startIdx = Math.max(0, endIdx - BATCH_SIZE); + const batch = state.messages.slice(startIdx, endIdx); + + const prevScrollHeight = container.scrollHeight; + const prevScrollTop = container.scrollTop; + + const fragment = document.createDocumentFragment(); + for (const msg of batch) { + fragment.appendChild(createMessageEl(msg)); + } + container.prepend(fragment); + renderedUpTo += batch.length; + + // Preserve scroll position + const newScrollHeight = container.scrollHeight; + container.scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight); + + initIcons(); + deferHighlightAll(); + + if (startIdx > 0) { + schedulePreload(container, startIdx); + } + }, { timeout: 200 }); +} + +function createMessageEl(msg: Message): HTMLElement { + const div = document.createElement('div'); + div.className = `msg ${msg.role}`; + div.dataset.msgId = msg.id; + const parts = (msg.parts || []).map((p) => renderPart(p)).join(''); + div.innerHTML = `
${msg.role}
${parts}`; + return div; +} + +export function appendMessage(msg: Message): void { + const container = document.getElementById('messages'); + if (!container) return; + container.appendChild(createMessageEl(msg)); + renderedUpTo++; + initIcons(); + deferHighlightAll(); +} + +export function updateMessage(msg: Message): void { + const container = document.getElementById('messages'); + if (!container) return; + const existing = container.querySelector(`[data-msg-id="${msg.id}"]`); + if (existing) { + const parts = (msg.parts || []).map((p) => renderPart(p)).join(''); + existing.innerHTML = `
${msg.role}
${parts}`; + initIcons(); + deferHighlightAll(); + } } export function renderPermission(): void { diff --git a/pwa/src/connect.ts b/pwa/src/connect.ts index b978457..7b9b58b 100644 --- a/pwa/src/connect.ts +++ b/pwa/src/connect.ts @@ -1,8 +1,8 @@ -import { state, escapeHTML } from './state'; +import { state, escapeHTML, renderPart } from './state'; import { APIClient } from './api'; import { SSEClient } from './sse'; import type { Message, Session, PermissionRequest } from './types'; -import { renderChat, renderMessages as _renderMessages, renderPermission as _renderPermission, renderInputBar as _renderInputBar } from './chat'; +import { renderChat, renderMessages as _renderMessages, renderPermission as _renderPermission, renderInputBar as _renderInputBar, initIcons, appendMessage, updateMessage } from './chat'; const app = () => document.querySelector('#app')!; @@ -35,6 +35,7 @@ export function renderConnect(): void { } async function handleConnect(): Promise { + const t0 = performance.now(); const urlEl = document.getElementById('server-url') as HTMLInputElement; const pathEl = document.getElementById('workspace-path') as HTMLInputElement; const url = urlEl.value.trim(); @@ -48,12 +49,13 @@ async function handleConnect(): Promise { renderConnect(); const baseURL = url.includes('://') ? url : `http://${url}`; - // In dev mode with vite proxy, use relative /v1 paths to avoid CORS const isDev = import.meta.env.DEV; const api = new APIClient(isDev ? '' : baseURL); try { + const t1 = performance.now(); const ok = await api.healthCheck(); + console.log(`[perf] healthCheck: ${(performance.now() - t1).toFixed(0)}ms`); if (!ok) throw new Error('Health check failed'); } catch (e) { state.error = `Cannot reach server: ${(e as Error).message}`; @@ -65,7 +67,9 @@ async function handleConnect(): Promise { state.api = api; try { + const t2 = performance.now(); const ws = await api.createWorkspace(wsPath || '.'); + console.log(`[perf] createWorkspace: ${(performance.now() - t2).toFixed(0)}ms`); state.workspace = ws; localStorage.setItem('serverURL', url); @@ -76,17 +80,27 @@ async function handleConnect(): Promise { sse.connect(); state.sse = sse; + const t3 = performance.now(); const sessions = await api.listSessions(ws.id); + console.log(`[perf] listSessions: ${(performance.now() - t3).toFixed(0)}ms (${sessions.length} sessions)`); state.sessions = sessions.sort((a, b) => b.updated_at - a.updated_at); if (sessions.length > 0) { state.activeSessionID = sessions[0].id; - await loadMessages(sessions[0].id); } state.isConnected = true; state.isConnecting = false; + console.log(`[perf] connect → first render: ${(performance.now() - t0).toFixed(0)}ms`); renderChat(); + + // Load messages async after first render + if (state.activeSessionID) { + const t4 = performance.now(); + await loadMessages(state.activeSessionID); + console.log(`[perf] loadMessages: ${(performance.now() - t4).toFixed(0)}ms (${state.messages.length} msgs)`); + console.log(`[perf] total connect → messages rendered: ${(performance.now() - t0).toFixed(0)}ms`); + } } catch (e) { state.error = `Setup failed: ${(e as Error).message}`; state.isConnecting = false; @@ -98,19 +112,34 @@ function setupSSE(sse: SSEClient): void { sse.on('message', (event: unknown) => { const e = event as { type: string; payload: Message }; const msg = e.payload; - console.log('[SSE message]', e.type, msg); - if (e.type === 'created') state.messages.push(msg); - else if (e.type === 'updated') { - const idx = state.messages.findIndex((m) => m.id === msg.id); - if (idx >= 0) state.messages[idx] = msg; + const sid = msg.session_id; + + // Update cache + let cached = state.messageCache.get(sid); + if (!cached) { cached = []; state.messageCache.set(sid, cached); } + + if (e.type === 'created') { + cached.push(msg); + } else if (e.type === 'updated') { + const idx = cached.findIndex((m) => m.id === msg.id); + if (idx >= 0) cached[idx] = msg; else cached.push(msg); } else if (e.type === 'deleted') { - state.messages = state.messages.filter((m) => m.id !== msg.id); + cached = cached.filter((m) => m.id !== msg.id); + state.messageCache.set(sid, cached); } - // Re-render messages in place - const container = document.getElementById('messages'); - if (container) { - _renderMessages(); - container.scrollTop = container.scrollHeight; + + // Only touch DOM if this is the active session + if (sid === state.activeSessionID) { + state.messages = cached; + if (e.type === 'created') { + appendMessage(msg); + const container = document.getElementById('messages'); + if (container) container.scrollTop = container.scrollHeight; + } else if (e.type === 'updated') { + updateMessage(msg); + } else { + _renderMessages(); + } } }); @@ -145,12 +174,32 @@ function setupSSE(sse: SSEClient): void { async function loadMessages(sessionID: string): Promise { if (!state.api || !state.workspace) return; + + // Check cache first + const cached = state.messageCache.get(sessionID); + if (cached) { + state.messages = cached; + const t0 = performance.now(); + _renderMessages(); + console.log(`[perf] renderMessages (cached ${cached.length}): ${(performance.now() - t0).toFixed(0)}ms`); + const container = document.getElementById('messages'); + if (container) container.scrollTop = container.scrollHeight; + return; + } + try { - state.messages = await state.api.getSessionMessages(state.workspace.id, sessionID); - console.log('[loadMessages]', state.messages.length, 'messages'); - if (state.messages.length > 0) { - const sample = state.messages[state.messages.length - 1]; - console.log('[sample message]', JSON.stringify(sample.parts?.slice(0, 3), null, 2)); + const t1 = performance.now(); + const msgs = await state.api.getSessionMessages(state.workspace.id, sessionID); + console.log(`[perf] fetch messages: ${(performance.now() - t1).toFixed(0)}ms (${msgs.length} msgs)`); + + state.messageCache.set(sessionID, msgs); + if (state.activeSessionID === sessionID) { + state.messages = msgs; + const t2 = performance.now(); + _renderMessages(); + console.log(`[perf] renderMessages: ${(performance.now() - t2).toFixed(0)}ms`); + const container = document.getElementById('messages'); + if (container) container.scrollTop = container.scrollHeight; } } catch (e) { console.error('[loadMessages] failed', e); } } @@ -164,6 +213,7 @@ export async function handleDisconnect(): Promise { state.messages = []; state.activeSessionID = null; state.pendingPermission = null; + state.messageCache.clear(); state.isConnected = false; renderConnect(); } diff --git a/pwa/src/state.ts b/pwa/src/state.ts index 3ccbcc8..38a4c98 100644 --- a/pwa/src/state.ts +++ b/pwa/src/state.ts @@ -16,6 +16,7 @@ export interface AppState { messages: Message[]; pendingPermission: PermissionRequest | null; agentBusy: boolean; + messageCache: Map; } export const state: AppState = { @@ -32,47 +33,92 @@ export const state: AppState = { messages: [], pendingPermission: null, agentBusy: false, + messageCache: new Map(), }; -import hljs from 'highlight.js/lib/core'; -// Register common languages -import typescript from 'highlight.js/lib/languages/typescript'; -import javascript from 'highlight.js/lib/languages/javascript'; -import python from 'highlight.js/lib/languages/python'; -import go from 'highlight.js/lib/languages/go'; -import rust from 'highlight.js/lib/languages/rust'; -import css from 'highlight.js/lib/languages/css'; -import json from 'highlight.js/lib/languages/json'; -import bash from 'highlight.js/lib/languages/bash'; -import xml from 'highlight.js/lib/languages/xml'; -import yaml from 'highlight.js/lib/languages/yaml'; -import sql from 'highlight.js/lib/languages/sql'; -import markdown from 'highlight.js/lib/languages/markdown'; -import swift from 'highlight.js/lib/languages/swift'; - -hljs.registerLanguage('typescript', typescript); -hljs.registerLanguage('javascript', javascript); -hljs.registerLanguage('python', python); -hljs.registerLanguage('go', go); -hljs.registerLanguage('rust', rust); -hljs.registerLanguage('css', css); -hljs.registerLanguage('json', json); -hljs.registerLanguage('bash', bash); -hljs.registerLanguage('xml', xml); -hljs.registerLanguage('html', xml); -hljs.registerLanguage('yaml', yaml); -hljs.registerLanguage('sql', sql); -hljs.registerLanguage('markdown', markdown); -hljs.registerLanguage('swift', swift); - -function highlightCode(code: string, lang?: string): string { - if (lang && hljs.getLanguage(lang)) { - try { return hljs.highlight(code, { language: lang }).value; } catch {} +import { createHighlighter, type Highlighter } from 'shiki'; + +let highlighter: Highlighter | null = null; +let highlighterReady: Promise | null = null; + +const LANGS = ['typescript', 'javascript', 'python', 'go', 'rust', 'css', 'json', 'bash', 'html', 'yaml', 'sql', 'markdown', 'swift'] as const; + +function getHighlighter(): Promise { + if (highlighter) return Promise.resolve(highlighter); + if (!highlighterReady) { + highlighterReady = createHighlighter({ + themes: ['vitesse-dark'], + langs: [...LANGS], + }).then((h) => { highlighter = h; return h; }); } - try { return hljs.highlightAuto(code).value; } catch {} + return highlighterReady; +} + +// Synchronous fallback — returns escaped HTML without highlighting +function highlightCodeSync(code: string): string { return escapeHTML(code); } +// Async highlighting — use for deferred rendering +export async function highlightCodeAsync(code: string, lang?: string): Promise { + try { + const h = await getHighlighter(); + const l = lang && h.getLoadedLanguages().includes(lang) ? lang : undefined; + return h.codeToHtml(code, { lang: l || 'text', theme: 'vitesse-dark' }); + } catch { + return `
${escapeHTML(code)}
`; + } +} + +// Queue for deferred highlighting of code blocks +const pendingHighlights: Array<{ el: HTMLElement; code: string; lang?: string }> = []; +let highlightBatchScheduled = false; + +export function queueHighlight(el: HTMLElement, code: string, lang?: string): void { + pendingHighlights.push({ el, code, lang }); + if (!highlightBatchScheduled) { + highlightBatchScheduled = true; + requestIdleCallback(processHighlightBatch, { timeout: 100 }); + } +} + +// Scan DOM for un-highlighted code blocks and queue them +export function deferHighlightAll(): void { + const els = document.querySelectorAll('pre[data-code]'); + els.forEach((el) => { + if (el.dataset.highlighted === 'true') return; + const code = decodeURIComponent(el.dataset.code || ''); + const lang = el.dataset.highlight || undefined; + el.dataset.highlighted = 'true'; + queueHighlight(el, code, lang); + }); +} + +function processHighlightBatch(deadline?: IdleDeadline): void { + const batchSize = 5; + let processed = 0; + while (pendingHighlights.length > 0 && processed < batchSize && (!deadline || deadline.timeRemaining() > 2)) { + const item = pendingHighlights.shift()!; + getHighlighter().then((h) => { + const l = item.lang && h.getLoadedLanguages().includes(item.lang) ? item.lang : undefined; + try { + const html = h.codeToHtml(item.code, { lang: l || 'text', theme: 'vitesse-dark' }); + // Extract inner content from shiki's
 wrapper
+        const match = html.match(/]*>([\s\S]*)<\/code>/);
+        if (match && item.el.isConnected) {
+          item.el.innerHTML = `${match[1]}`;
+        }
+      } catch {}
+    });
+    processed++;
+  }
+  if (pendingHighlights.length > 0) {
+    requestIdleCallback(processHighlightBatch, { timeout: 100 });
+  } else {
+    highlightBatchScheduled = false;
+  }
+}
+
 function guessLang(filePath: string): string | undefined {
   const ext = filePath.split('.').pop()?.toLowerCase();
   const map: Record = {
@@ -235,7 +281,8 @@ function renderToolResultBody(name: string, content: string, metadata: string |
     case 'bash': {
       const output = (meta?.output as string) || content;
       if (!output || output === 'no output') return '
no output
'; - return `
${highlightCode(output.slice(0, 3000), 'bash')}
`; + const code = output.slice(0, 3000); + return `
${escapeHTML(code)}
`; } case 'edit': case 'multiedit': { @@ -246,20 +293,24 @@ function renderToolResultBody(name: string, content: string, metadata: string | if (oldContent && newContent) { return `
${renderColoredDiff(oldContent, newContent, lang)}
`; } - if (content) return `
${highlightCode(content.slice(0, 1500), lang)}
`; + if (content) { + const code = content.slice(0, 1500); + return `
${escapeHTML(code)}
`; + } return ''; } case 'write': { - // Try to get file path from the matching tool_call to guess language if (content && content !== 'File successfully written.') { - return `
${highlightCode(content.slice(0, 2000))}
`; + const code = content.slice(0, 2000); + return `
${escapeHTML(code)}
`; } return ''; } case 'view': { const viewContent = (meta?.content as string) || content; if (viewContent) { - return `
${highlightCode(viewContent.slice(0, 3000))}
`; + const code = viewContent.slice(0, 3000); + return `
${escapeHTML(code)}
`; } return ''; } @@ -281,7 +332,7 @@ function renderToolResultBody(name: string, content: string, metadata: string | if (!content) return ''; const maxLen = 2000; const truncated = content.length > maxLen ? content.slice(0, maxLen) + '\n…' : content; - return `
${highlightCode(truncated)}
`; + return `
${escapeHTML(truncated)}
`; } } } @@ -290,80 +341,70 @@ function renderColoredDiff(oldStr: string, newStr: string, lang?: string): strin const oldLines = oldStr.split('\n'); const newLines = newStr.split('\n'); - const hlOld = oldLines.map(l => highlightCode(l, lang)); - const hlNew = newLines.map(l => highlightCode(l, lang)); - + // Render plain text diff first — highlighting deferred via data attrs const lcs = computeLCS(oldLines, newLines); - const raw: Array<{ type: 'ctx' | 'add' | 'del'; html: string }> = []; + const result: string[] = []; let oi = 0, ni = 0, li = 0; while (oi < oldLines.length || ni < newLines.length) { if (li < lcs.length && oi < oldLines.length && oldLines[oi] === lcs[li] && ni < newLines.length && newLines[ni] === lcs[li]) { - raw.push({ type: 'ctx', html: ` ${hlOld[oi]}` }); + result.push(` ${escapeHTML(lcs[li])}`); oi++; ni++; li++; } else if (li < lcs.length && ni < newLines.length && newLines[ni] === lcs[li]) { - raw.push({ type: 'del', html: `- ${hlOld[oi]}` }); + result.push(`- ${escapeHTML(oldLines[oi])}`); oi++; } else if (li < lcs.length && oi < oldLines.length && oldLines[oi] === lcs[li]) { - raw.push({ type: 'add', html: `+ ${hlNew[ni]}` }); + result.push(`+ ${escapeHTML(newLines[ni])}`); ni++; } else { if (oi < oldLines.length && (ni >= newLines.length || li >= lcs.length)) { - raw.push({ type: 'del', html: `- ${hlOld[oi]}` }); + result.push(`- ${escapeHTML(oldLines[oi])}`); oi++; } else if (ni < newLines.length) { - raw.push({ type: 'add', html: `+ ${hlNew[ni]}` }); + result.push(`+ ${escapeHTML(newLines[ni])}`); ni++; } } } - // Collapse consecutive context lines, keeping 3 lines of context around changes + // Collapse context const CONTEXT = 3; - const result: string[] = []; + const raw = result.map(html => { + if (html.includes('diff-ctx')) return { type: 'ctx' as const, html }; + if (html.includes('diff-add')) return { type: 'add' as const, html }; + return { type: 'del' as const, html }; + }); + + const collapsed: string[] = []; let i = 0; while (i < raw.length) { if (raw[i].type === 'ctx') { - // Find the extent of this context run let end = i; while (end < raw.length && raw[end].type === 'ctx') end++; const ctxLen = end - i; - - // Is there a change after this context block? const hasChangeAfter = end < raw.length; - // Was there a change before this context block? const hasChangeBefore = i > 0; if (!hasChangeAfter && !hasChangeBefore) { - // Entire diff is context — show all - for (let j = i; j < end; j++) result.push(raw[j].html); + for (let j = i; j < end; j++) collapsed.push(raw[j].html); } else if (ctxLen <= CONTEXT * 2 + 1) { - // Small enough to show entirely - for (let j = i; j < end; j++) result.push(raw[j].html); + for (let j = i; j < end; j++) collapsed.push(raw[j].html); } else { - // Show head + fold + tail const headEnd = hasChangeBefore ? i + CONTEXT : i; const tailStart = hasChangeAfter ? end - CONTEXT : end; - - if (hasChangeBefore) { - for (let j = i; j < headEnd; j++) result.push(raw[j].html); - } + if (hasChangeBefore) for (let j = i; j < headEnd; j++) collapsed.push(raw[j].html); const hidden = tailStart - headEnd; - if (hidden > 0) { - result.push(`${hidden} unchanged lines`); - } - if (hasChangeAfter) { - for (let j = tailStart; j < end; j++) result.push(raw[j].html); - } + if (hidden > 0) collapsed.push(`${hidden} unchanged lines`); + if (hasChangeAfter) for (let j = tailStart; j < end; j++) collapsed.push(raw[j].html); } i = end; } else { - result.push(raw[i].html); + collapsed.push(raw[i].html); i++; } } - return `
${result.join('\n')}
`; + return `
${collapsed.join('\n')}
`; } function computeLCS(a: string[], b: string[]): string[] { diff --git a/pwa/src/styles.css b/pwa/src/styles.css index e438ff9..37f4345 100644 --- a/pwa/src/styles.css +++ b/pwa/src/styles.css @@ -111,6 +111,12 @@ html, body { .msg.user { border-left: 2px solid var(--accent); padding-left: 0.75rem; margin-top: 0.5rem; } .msg .role { font-size: 0.65rem; color: var(--fg-faint); margin-bottom: 0.1rem; text-transform: uppercase; letter-spacing: 0.06em; } +.load-more-sentinel { + text-align: center; padding: 0.5rem; font-size: 0.72rem; + color: var(--fg-most); cursor: pointer; +} +.load-more-sentinel:hover { color: var(--fg-dim); } + .msg code { background: var(--surface); padding: 0.1em 0.3em; border-radius: 2px; font-size: 0.85em; @@ -160,33 +166,9 @@ html, body { .diff-ctx { color: var(--fg-dim); } .diff-fold { color: var(--fg-most); font-style: italic; display: block; padding: 0.15rem 0; } -/* highlight.js dark theme overrides */ -.hljs { background: transparent; color: var(--fg); } -.hljs-keyword { color: #FF60FF; } -.hljs-string { color: #BF976F; } -.hljs-number { color: #68FFD6; } -.hljs-comment { color: var(--fg-most); font-style: italic; } -.hljs-function { color: #6B50FF; } -.hljs-title { color: #8B75FF; } -.hljs-built_in { color: #FF79D0; } -.hljs-attr { color: #D46EFF; } -.hljs-tag { color: #D46EFF; } -.hljs-name { color: #D46EFF; } -.hljs-type { color: #7272FF; } -.hljs-variable { color: #ECEBF0; } -.hljs-literal { color: #68FFD6; } -.hljs-operator { color: #FF7F90; } -.hljs-punctuation { color: var(--fg-dim); } -.hljs-property { color: #8B75FF; } -.hljs-regexp { color: #FF7F90; } -.hljs-symbol { color: #E8FF27; } -.hljs-meta { color: var(--fg-most); } -.hljs-section { color: #FF60FF; font-weight: bold; } -.hljs-selector-class { color: #D46EFF; } -.hljs-selector-id { color: #6B50FF; } -.hljs-doctag { color: var(--fg-most); } -.hljs-template-tag { color: #E8FF27; } -.hljs-template-variable { color: #68FFD6; } +/* Shiki overrides — make it blend with our theme */ +.shiki { background: transparent !important; padding: 0 !important; margin: 0 !important; } +.shiki code { font-family: var(--mono); } /* Todos */ .todo-list { padding: 0.3rem 0.5rem; }