diff --git a/.gitignore b/.gitignore
index 40504ab..6329e49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -150,4 +150,5 @@ CLAUDE.md
site/SPECIFICATION.md
site/DESIGN.md
-site/ARCHITECTURE.md
\ No newline at end of file
+site/ARCHITECTURE.md .claude/settings.json
+tmp
diff --git a/bun.lock b/bun.lock
index e1722d4..c81936d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
+ "configVersion": 0,
"workspaces": {
"": {
"name": "logsdx",
@@ -22,7 +23,7 @@
"dependencies": {
"@docsearch/css": "^3.9.0",
"@docsearch/react": "^3.9.0",
- "@next/mdx": "^15.5.2",
+ "@next/mdx": "16.0.7",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.0",
@@ -30,6 +31,9 @@
"@radix-ui/react-tabs": "^1.1.13",
"@shikijs/rehype": "^3.12.2",
"@shikijs/transformers": "^3.12.2",
+ "@tanstack/react-query": "^5.90.9",
+ "@tanstack/react-store": "^0.8.0",
+ "@tanstack/store": "^0.8.0",
"ansi-to-html": "^0.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -38,10 +42,10 @@
"idb": "^8.0.3",
"logsdx": "*",
"lucide-react": "^0.400.0",
- "next": "^14.2.31",
+ "next": "16.0.7",
"next-themes": "^0.4.6",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "react": "19.2.1",
+ "react-dom": "19.2.1",
"react-hook-form": "^7.63.0",
"react-icons": "^5.5.0",
"react-wrap-balancer": "^1.1.1",
@@ -58,13 +62,20 @@
"shiki": "^3.12.2",
"tailwind-merge": "^2.5.2",
"unified": "^11.0.5",
- "zustand": "^5.0.8",
+ "use-debounce": "^10.0.6",
},
"devDependencies": {
+ "@happy-dom/global-registrator": "^20.0.10",
+ "@playwright/test": "^1.56.1",
+ "@tanstack/react-query-devtools": "^5.90.2",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
+ "@types/react": "19",
+ "@types/react-dom": "19",
"autoprefixer": "^10.4.19",
+ "happy-dom": "^20.0.10",
"oxlint": "^1.8.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.5",
@@ -73,6 +84,8 @@
},
},
"packages": {
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
+
"@algolia/abtesting": ["@algolia/abtesting@1.9.0", "", { "dependencies": { "@algolia/client-common": "5.43.0", "@algolia/requester-browser-xhr": "5.43.0", "@algolia/requester-fetch": "5.43.0", "@algolia/requester-node-http": "5.43.0" } }, "sha512-4q9QCxFPiDIx1n5w41A1JMkrXI8p0ugCQnCGFtCKZPmWtwgWCqwVRncIbp++81xSELFZVQUfiB7Kbsla1tIBSw=="],
"@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.9", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.9", "@algolia/autocomplete-shared": "1.17.9" } }, "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ=="],
@@ -111,10 +124,18 @@
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+ "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
+
"@docsearch/css": ["@docsearch/css@3.9.0", "", {}, "sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA=="],
"@docsearch/react": ["@docsearch/react@3.9.0", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.9", "@algolia/autocomplete-preset-algolia": "1.17.9", "@docsearch/css": "3.9.0", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 20.0.0", "react": ">= 16.8.0 < 20.0.0", "react-dom": ">= 16.8.0 < 20.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ=="],
+ "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
+
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
@@ -123,6 +144,58 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
+ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.10", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.10" } }, "sha512-GU0UBt9lJKhZlY/U0Bivj9ZVepDIQoAUupAAl/90THG4/urkzXNglkVYETsnt2pGBDgQ+4vBjMAbLu6XzcKcQA=="],
+
+ "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
+
+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
+
+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
+
+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
+
+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
+
+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
+
+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
+
+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
+
+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
+
+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
+
+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
+
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
+
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
+
+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
+
+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
+
+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
+
+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
+
+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
+
+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
+
+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
+
+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
+
+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
+
+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
+
+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
+
+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
+
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -133,27 +206,25 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
- "@next/env": ["@next/env@14.2.33", "", {}, "sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA=="],
-
- "@next/mdx": ["@next/mdx@15.5.6", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-lyzXcnZWPjYxbkz/5tv1bRlCOjKYX1lFg3LIuoIf9ERTOUBDzkCvUnWjtRsmFRxKv1/6uwpLVQvrJDd54gVDBw=="],
+ "@next/env": ["@next/env@16.0.7", "", {}, "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw=="],
- "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.2.33", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA=="],
+ "@next/mdx": ["@next/mdx@16.0.7", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-ysX8mH24XuTwXStJLbecHO97I4EdUT9vHQymXLypLb3956cYXfVb/36nukH0C4Q2iA7RZE04yNpHs84Br77nNg=="],
- "@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.2.33", "", { "os": "darwin", "cpu": "x64" }, "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA=="],
+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg=="],
- "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw=="],
+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA=="],
- "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.2.33", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg=="],
+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww=="],
- "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg=="],
+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g=="],
- "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.2.33", "", { "os": "linux", "cpu": "x64" }, "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA=="],
+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA=="],
- "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.2.33", "", { "os": "win32", "cpu": "arm64" }, "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ=="],
+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w=="],
- "@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.2.33", "", { "os": "win32", "cpu": "ia32" }, "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q=="],
+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q=="],
- "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.2.33", "", { "os": "win32", "cpu": "x64" }, "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg=="],
+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.7", "", { "os": "win32", "cpu": "x64" }, "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@@ -179,6 +250,8 @@
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+ "@playwright/test": ["@playwright/test@1.56.1", "", { "dependencies": { "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" } }, "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg=="],
+
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -261,9 +334,29 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
- "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
+ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
+
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.9", "", {}, "sha512-UFOCQzi6pRGeVTVlPNwNdnAvT35zugcIydqjvFUzG62dvz2iVjElmNp/hJkUoM5eqbUPfSU/GJIr/wbvD8bTUw=="],
+
+ "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="],
+
+ "@tanstack/react-query": ["@tanstack/react-query@5.90.9", "", { "dependencies": { "@tanstack/query-core": "5.90.9" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-Zke2AaXiaSfnG8jqPZR52m8SsclKT2d9//AgE/QIzyNvbpj/Q2ln+FsZjb1j69bJZUouBvX2tg9PHirkTm8arw=="],
+
+ "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
+
+ "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
- "@swc/helpers": ["@swc/helpers@0.5.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A=="],
+ "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
@@ -277,21 +370,21 @@
"@types/node": ["@types/node@20.19.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA=="],
- "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
+ "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
- "@types/react": ["@types/react@18.3.26", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA=="],
-
- "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+ "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
+
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"algoliasearch": ["algoliasearch@5.43.0", "", { "dependencies": { "@algolia/abtesting": "1.9.0", "@algolia/client-abtesting": "5.43.0", "@algolia/client-analytics": "5.43.0", "@algolia/client-common": "5.43.0", "@algolia/client-insights": "5.43.0", "@algolia/client-personalization": "5.43.0", "@algolia/client-query-suggestions": "5.43.0", "@algolia/client-search": "5.43.0", "@algolia/ingestion": "1.43.0", "@algolia/monitoring": "1.43.0", "@algolia/recommend": "5.43.0", "@algolia/requester-browser-xhr": "5.43.0", "@algolia/requester-fetch": "5.43.0", "@algolia/requester-node-http": "5.43.0" } }, "sha512-hbkK41JsuGYhk+atBDxlcKxskjDCh3OOEDpdKZPtw+3zucBqhlojRG5e5KtCmByGyYvwZswVeaSWglgLn2fibg=="],
- "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
- "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"ansi-to-html": ["ansi-to-html@0.7.2", "", { "dependencies": { "entities": "^2.2.0" }, "bin": { "ansi-to-html": "bin/ansi-to-html" } }, "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g=="],
@@ -305,6 +398,8 @@
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
"autoprefixer": ["autoprefixer@10.4.22", "", { "dependencies": { "browserslist": "^4.27.0", "caniuse-lite": "^1.0.30001754", "fraction.js": "^5.3.4", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
@@ -323,8 +418,6 @@
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
- "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
-
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001754", "", {}, "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg=="],
@@ -357,9 +450,11 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -367,6 +462,8 @@
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
@@ -375,6 +472,8 @@
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.250", "", {}, "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw=="],
@@ -403,7 +502,7 @@
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
- "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -415,10 +514,10 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
- "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
-
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
+ "happy-dom": ["happy-dom@20.0.10", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-6umCCHcjQrhP5oXhrHQQvLB0bwb1UzHAHdsXy+FjtKoYjUhmNZsQL8NivwM1vDvNEChJabVrUYxUnp/ZdYmy2g=="],
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
@@ -447,6 +546,8 @@
"idb": ["idb@8.0.3", "", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="],
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
@@ -487,12 +588,12 @@
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
- "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
-
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"lucide-react": ["lucide-react@0.400.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rpp7pFHh3Xd93KHixNgB0SqThMHpYNzsGUu69UaQbSZ75Q/J3m5t6EhKyMT3m4w2WOxmJ2mY0tD3vebnXqQryQ=="],
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
@@ -579,6 +680,8 @@
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
@@ -591,7 +694,7 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
- "next": ["next@14.2.33", "", { "dependencies": { "@next/env": "14.2.33", "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", "postcss": "8.4.31", "styled-jsx": "5.1.1" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.2.33", "@next/swc-darwin-x64": "14.2.33", "@next/swc-linux-arm64-gnu": "14.2.33", "@next/swc-linux-arm64-musl": "14.2.33", "@next/swc-linux-x64-gnu": "14.2.33", "@next/swc-linux-x64-musl": "14.2.33", "@next/swc-win32-arm64-msvc": "14.2.33", "@next/swc-win32-ia32-msvc": "14.2.33", "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng=="],
+ "next": ["next@16.0.7", "", { "dependencies": { "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.7", "@next/swc-darwin-x64": "16.0.7", "@next/swc-linux-arm64-gnu": "16.0.7", "@next/swc-linux-arm64-musl": "16.0.7", "@next/swc-linux-x64-gnu": "16.0.7", "@next/swc-linux-x64-musl": "16.0.7", "@next/swc-win32-arm64-msvc": "16.0.7", "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@@ -631,6 +734,10 @@
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
+ "playwright": ["playwright@1.56.1", "", { "dependencies": { "playwright-core": "1.56.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw=="],
+
+ "playwright-core": ["playwright-core@1.56.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ=="],
+
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
@@ -647,20 +754,24 @@
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
- "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
+ "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="],
- "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
+ "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
"react-hook-form": ["react-hook-form@7.66.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw=="],
"react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
@@ -675,6 +786,8 @@
"reading-time": ["reading-time@1.5.0", "", {}, "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg=="],
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
@@ -709,12 +822,16 @@
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
- "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
+ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
@@ -731,8 +848,6 @@
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
- "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
-
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -745,7 +860,9 @@
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
- "styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="],
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
+ "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
@@ -803,8 +920,12 @@
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+ "use-debounce": ["use-debounce@10.0.6", "", { "peerDependencies": { "react": "*" } }, "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg=="],
+
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@@ -815,6 +936,8 @@
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
+ "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
@@ -825,8 +948,6 @@
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
- "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
-
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@@ -835,6 +956,12 @@
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "chokidar/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -853,10 +980,12 @@
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
- "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
+ "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -879,10 +1008,6 @@
"logsdx-site/oxlint/@oxlint/win32-x64": ["@oxlint/win32-x64@1.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4+VO5P/UJ2nq9sj6kQToJxFy5cKs7dGIN2DiUSQ7cqyUi7EKYNQKe+98HFcDOjtm33jQOQnc4kw8Igya5KPozg=="],
- "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
- "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}
diff --git a/bunfig.toml b/bunfig.toml
index 4943bae..141fe28 100644
--- a/bunfig.toml
+++ b/bunfig.toml
@@ -7,4 +7,7 @@ logsdx = "https://registry.npmjs.org/"
[build]
sourcemap = "external"
minify = true
-target = "node"
\ No newline at end of file
+target = "node"
+
+[test]
+root = "tests/"
\ No newline at end of file
diff --git a/package.json b/package.json
index 36403c4..a688180 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,37 @@
},
"exports": {
".": {
+ "types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
- "require": "./dist/index.cjs"
+ "require": "./dist/index.cjs",
+ "default": "./dist/index.mjs"
+ },
+ "./themes": {
+ "types": "./dist/themes/index.d.ts",
+ "import": "./dist/themes/index.mjs",
+ "require": "./dist/themes/index.cjs",
+ "default": "./dist/themes/index.mjs"
+ },
+ "./themes/*": {
+ "types": "./dist/themes/presets/*.d.ts",
+ "import": "./dist/themes/presets/*.mjs",
+ "require": "./dist/themes/presets/*.cjs",
+ "default": "./dist/themes/presets/*.mjs"
+ },
+ "./renderer": {
+ "types": "./dist/renderer/index.d.ts",
+ "import": "./dist/renderer/index.mjs",
+ "require": "./dist/renderer/index.cjs",
+ "default": "./dist/renderer/index.mjs"
+ },
+ "./fast": {
+ "types": "./dist/renderer/fast-mode.d.ts",
+ "import": "./dist/renderer/fast-mode.mjs",
+ "require": "./dist/renderer/fast-mode.cjs",
+ "default": "./dist/renderer/fast-mode.mjs"
}
},
+ "sideEffects": false,
"files": [
"dist/"
],
@@ -29,9 +56,10 @@
"build:esm": "bun build src/index.ts --outfile dist/index.mjs --format esm --minify",
"build:cli": "bun build src/cli/bin.ts --outfile dist/cli.js --format cjs --minify --target node",
"build:types": "tsc -p tsconfig.build.json",
- "test": "bun test",
- "test:coverage": "bun test --coverage --coverage-reporter=lcov",
- "test:watch": "bun test --watch",
+ "test": "bun test tests/",
+ "test:coverage": "bun test tests/ --coverage --coverage-reporter=lcov",
+ "test:watch": "bun test tests/ --watch",
+ "site:test": "cd site && bun test",
"lint": "oxlint .",
"lint:fix": "oxlint . --fix",
"format": "bun run prettier --write .",
diff --git a/scripts/build-themes.ts b/scripts/build-themes.ts
new file mode 100644
index 0000000..c61a5c4
--- /dev/null
+++ b/scripts/build-themes.ts
@@ -0,0 +1,44 @@
+#!/usr/bin/env bun
+import { build } from "bun";
+import { readdirSync } from "fs";
+import { join } from "path";
+
+const themesDir = join(import.meta.dir, "../src/themes/presets");
+const outputDir = join(import.meta.dir, "../dist/themes/presets");
+
+const themeFiles = readdirSync(themesDir).filter((file) =>
+ file.endsWith(".ts"),
+);
+
+console.log(`Building ${themeFiles.length} theme presets...`);
+
+const buildPromises = themeFiles.map(async (file) => {
+ const themeName = file.replace(".ts", "");
+ const inputPath = join(themesDir, file);
+
+ await Promise.all([
+ build({
+ entrypoints: [inputPath],
+ outdir: outputDir,
+ format: "esm",
+ minify: true,
+ naming: `${themeName}.mjs`,
+ target: "browser",
+ }),
+
+ build({
+ entrypoints: [inputPath],
+ outdir: outputDir,
+ format: "cjs",
+ minify: true,
+ naming: `${themeName}.cjs`,
+ target: "node",
+ }),
+ ]);
+
+ console.log(`✓ Built theme: ${themeName}`);
+});
+
+await Promise.all(buildPromises);
+
+console.log("✓ All themes built successfully!");
diff --git a/site/.github/workflows/test.yml b/site/.github/workflows/test.yml
new file mode 100644
index 0000000..fcc50bb
--- /dev/null
+++ b/site/.github/workflows/test.yml
@@ -0,0 +1,61 @@
+name: Tests
+
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main, develop]
+
+jobs:
+ unit-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Run unit tests
+ run: bun test tests/
+
+ - name: Upload coverage
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results
+ path: coverage/
+ retention-days: 7
+
+ e2e-tests:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Install Playwright
+ run: bun run playwright:install --with-deps
+
+ - name: Run E2E tests
+ run: bun run test:e2e --project=chromium
+
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
diff --git a/site/app/layout.tsx b/site/app/layout.tsx
index de3948c..08bbeaa 100644
--- a/site/app/layout.tsx
+++ b/site/app/layout.tsx
@@ -1,7 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
-import { ThemeProvider } from "@/components/theme-provider";
+import { Providers } from "@/components/providers";
const inter = Inter({ subsets: ["latin"] });
@@ -19,14 +19,7 @@ export default function RootLayout({
return (
-
- {children}
-
+ {children}
);
diff --git a/site/bunfig.toml b/site/bunfig.toml
new file mode 100644
index 0000000..d5e19ca
--- /dev/null
+++ b/site/bunfig.toml
@@ -0,0 +1,4 @@
+[test]
+preload = ["./test-setup.ts"]
+root = "./tests"
+isolate = true
diff --git a/site/components/interactive/__tests__/index.test.tsx b/site/components/interactive/__tests__/index.test.tsx
new file mode 100644
index 0000000..d2e3868
--- /dev/null
+++ b/site/components/interactive/__tests__/index.test.tsx
@@ -0,0 +1,104 @@
+import { describe, test, expect, afterEach } from "bun:test";
+import { render, screen, cleanup } from "@/tests/utils/test-utils";
+import { userEvent } from "@testing-library/user-event";
+import { InteractiveExamplesSection } from "../index";
+
+describe("InteractiveExamplesSection", () => {
+ afterEach(cleanup);
+
+ test("renders section heading", () => {
+ render();
+
+ expect(screen.getByText(/Interactive/)).toBeTruthy();
+ expect(screen.getByText(/Theme Preview/)).toBeTruthy();
+ });
+
+ test("renders all theme selector buttons", () => {
+ render();
+
+ const themeNames = [
+ "GitHub",
+ "Solarized",
+ "Dracula",
+ "Nord",
+ "Monokai",
+ "Oh My Zsh",
+ ];
+
+ themeNames.forEach((theme) => {
+ const button = screen.getByRole("button", { name: theme });
+ expect(button).toBeTruthy();
+ });
+ });
+
+ test("renders terminal and browser preview panes", () => {
+ render();
+
+ expect(screen.getByText("Terminal")).toBeTruthy();
+ expect(screen.getByText("Browser Console")).toBeTruthy();
+ });
+
+ test("changes theme on button click", async () => {
+ const user = userEvent.setup({ delay: null });
+ const { container } = render();
+
+ const draculaBtn = screen.getByRole("button", { name: "Dracula" });
+ await user.click(draculaBtn);
+
+ const content = container.textContent || "";
+ expect(content).toContain("Dracula");
+ });
+
+ test("renders color mode toggle buttons", () => {
+ render();
+
+ const allButtons = screen.getAllByRole("button");
+ const hasColorModeButtons = allButtons.some((btn) =>
+ btn.querySelector('[class*="lucide"]'),
+ );
+
+ expect(hasColorModeButtons).toBe(true);
+ });
+
+ test("displays sample logs in preview panes", () => {
+ const { container } = render();
+
+ const content = container.textContent || "";
+ expect(content).toContain("INFO");
+ expect(content).toContain("ERROR");
+ expect(content).toContain("WARN");
+ });
+
+ test("renders code examples section", () => {
+ render();
+
+ expect(screen.getByText("Quick Integration")).toBeTruthy();
+ expect(screen.getByText("Logger Integration Examples")).toBeTruthy();
+ });
+
+ test("shows code blocks with getLogsDX usage", () => {
+ const { container } = render();
+
+ const codeText = container.textContent || "";
+ expect(codeText).toContain("getLogsDX");
+ expect(codeText).toContain("processLine");
+ });
+
+ test("renders winston integration example", () => {
+ render();
+
+ expect(screen.getByText("Winston")).toBeTruthy();
+ });
+
+ test("renders pino integration example", () => {
+ render();
+
+ expect(screen.getByText("Pino")).toBeTruthy();
+ });
+
+ test("renders console override example", () => {
+ render();
+
+ expect(screen.getByText("Console Override")).toBeTruthy();
+ });
+});
diff --git a/site/components/interactive/index.tsx b/site/components/interactive/index.tsx
index 60c445d..ac2f0fb 100644
--- a/site/components/interactive/index.tsx
+++ b/site/components/interactive/index.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Moon, Sun, Monitor } from "lucide-react";
import type { ThemeConfig, ThemePair, ColorMode } from "./types";
@@ -295,14 +295,17 @@ export function InteractiveExamplesSection() {
const [selectedTheme, setSelectedTheme] = useState("GitHub");
const [colorMode, setColorMode] = useState("system");
const [effectiveMode, setEffectiveMode] = useState<"light" | "dark">("dark");
+ const autoRotateRef = useRef(true);
useEffect(() => {
const themes = Object.keys(THEME_PAIRS);
const interval = setInterval(() => {
- setSelectedTheme((current) => {
- const currentIndex = themes.indexOf(current);
- return themes[(currentIndex + 1) % themes.length];
- });
+ if (autoRotateRef.current) {
+ setSelectedTheme((current) => {
+ const currentIndex = themes.indexOf(current);
+ return themes[(currentIndex + 1) % themes.length];
+ });
+ }
}, 3000);
return () => clearInterval(interval);
@@ -363,7 +366,10 @@ export function InteractiveExamplesSection() {
key={theme}
variant={selectedTheme === theme ? "default" : "outline"}
size="sm"
- onClick={() => setSelectedTheme(theme)}
+ onClick={() => {
+ setSelectedTheme(theme);
+ autoRotateRef.current = false;
+ }}
>
{theme}
diff --git a/site/components/mdx/index.tsx b/site/components/mdx/index.tsx
index 709a3fd..9059561 100644
--- a/site/components/mdx/index.tsx
+++ b/site/components/mdx/index.tsx
@@ -123,8 +123,11 @@ function extractCodeContent(children: ReactNode): string | null {
return null;
}
- if ("props" in children && children.props?.children) {
- return extractCodeContent(children.props.children);
+ if ("props" in children) {
+ const element = children as React.ReactElement<{ children?: ReactNode }>;
+ if (element.props?.children) {
+ return extractCodeContent(element.props.children);
+ }
}
if (Array.isArray(children)) {
diff --git a/site/components/providers.tsx b/site/components/providers.tsx
new file mode 100644
index 0000000..1cab81d
--- /dev/null
+++ b/site/components/providers.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+import { useState } from "react";
+
+export function Providers({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000, // 1 minute
+ refetchOnWindowFocus: false,
+ },
+ },
+ }),
+ );
+
+ return (
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/site/components/solutionDemo/__tests__/index.test.tsx b/site/components/solutionDemo/__tests__/index.test.tsx
new file mode 100644
index 0000000..65cdd64
--- /dev/null
+++ b/site/components/solutionDemo/__tests__/index.test.tsx
@@ -0,0 +1,69 @@
+import { describe, test, expect, afterEach } from "bun:test";
+import { render, screen, cleanup } from "@/tests/utils/test-utils";
+import { ProblemSection } from "../index";
+
+describe("ProblemSection", () => {
+ afterEach(cleanup);
+
+ test("renders problem description", () => {
+ render();
+
+ expect(screen.getByText(/The Problem/)).toBeTruthy();
+ expect(screen.getAllByText(/logsDx/).length).toBeGreaterThan(0);
+ });
+
+ test("renders toggle badge", () => {
+ const { container } = render();
+
+ const badge = container.querySelector('[class*="pointer-events-none"]');
+ expect(badge?.textContent).toMatch(/Without logsDx|With logsDx/);
+ });
+
+ test("renders terminal and browser panes", () => {
+ const { container } = render();
+
+ const content = container.textContent || "";
+ expect(content).toContain("Terminal");
+ expect(content).toContain("Browser");
+ });
+
+ test("renders demo logs in both panes", () => {
+ const { container } = render();
+
+ const terminalLogs = container.querySelectorAll('[class*="font-mono"]');
+ expect(terminalLogs.length).toBeGreaterThan(0);
+
+ const content = container.textContent || "";
+ expect(content).toContain("[INFO]");
+ expect(content).toContain("[ERROR]");
+ expect(content).toContain("[WARN]");
+ });
+
+ test("renders code comparison", () => {
+ const { container } = render();
+
+ const codeElements = container.querySelectorAll("code");
+ expect(codeElements.length).toBeGreaterThan(0);
+
+ const codeText = container.textContent || "";
+ expect(codeText).toContain("console.log");
+ });
+
+ test("renders clickable card", () => {
+ const { container } = render();
+
+ const card = container.querySelector('[class*="cursor-pointer"]');
+ expect(card).toBeTruthy();
+
+ const badge = container.querySelector('[class*="pointer-events-none"]');
+ expect(badge?.textContent).toMatch(/Without logsDx|With logsDx/);
+ });
+
+ test("shows status indicators", () => {
+ const { container } = render();
+
+ const statusText = container.textContent || "";
+ expect(statusText).toContain("Terminal");
+ expect(statusText).toContain("Browser");
+ });
+});
diff --git a/site/components/solutionDemo/index.tsx b/site/components/solutionDemo/index.tsx
index 23b0c04..c309095 100644
--- a/site/components/solutionDemo/index.tsx
+++ b/site/components/solutionDemo/index.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useRef } from "react";
import { Card } from "@/components/ui/card";
import type { DemoLog } from "./types";
@@ -30,7 +30,7 @@ logger.log('[WARN] Memory usage high: 85%'); // Styled in both`;
export function ProblemSection() {
const [activeIndex, setActiveIndex] = useState(0);
const [showWithLogsDx, setShowWithLogsDx] = useState(false);
- const [isHovered, setIsHovered] = useState(false);
+ const isHoveredRef = useRef(false);
// Cycle through logs for spotlight effect
useEffect(() => {
@@ -42,13 +42,13 @@ export function ProblemSection() {
// Toggle between with/without logsDx every 3 seconds (pause on hover)
useEffect(() => {
- if (isHovered) return; // Don't run interval when hovered
-
const interval = setInterval(() => {
- setShowWithLogsDx((prev) => !prev);
+ if (!isHoveredRef.current) {
+ setShowWithLogsDx((prev) => !prev);
+ }
}, 3000);
return () => clearInterval(interval);
- }, [isHovered]);
+ }, []);
return (
@@ -110,8 +110,12 @@ export function ProblemSection() {
setShowWithLogsDx((prev) => !prev)}
- onMouseEnter={() => setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
+ onMouseEnter={() => {
+ isHoveredRef.current = true;
+ }}
+ onMouseLeave={() => {
+ isHoveredRef.current = false;
+ }}
>
{/* Header */}
diff --git a/site/components/themegenerator/CustomThemeCreator.tsx b/site/components/themegenerator/CustomThemeCreator.tsx
new file mode 100644
index 0000000..ad76e26
--- /dev/null
+++ b/site/components/themegenerator/CustomThemeCreator.tsx
@@ -0,0 +1,198 @@
+"use client";
+
+import React, { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { ChevronDown, ChevronRight, Download, Copy } from "lucide-react";
+import {
+ useThemeEditorStore,
+ themeEditorActions,
+} from "@/stores/useThemeEditorStore";
+import { useLogPreview } from "@/hooks/useLogPreview";
+import { useCreateTheme } from "@/hooks/useThemes";
+import {
+ generateThemeCode,
+ exportThemeToShareCode,
+ generateShareUrl,
+} from "@/lib/themeUtils";
+import { PRESET_OPTIONS } from "./constants";
+import { ThemeColorPicker } from "./ThemeColorPicker";
+import { ThemePreview } from "./ThemePreview";
+import { PresetSelector } from "./PresetSelector";
+
+export function CustomThemeCreator() {
+ const name = useThemeEditorStore((state) => state.name);
+ const colors = useThemeEditorStore((state) => state.colors);
+ const presets = useThemeEditorStore((state) => state.presets);
+ const { setName, setColor, togglePreset, reset } = themeEditorActions;
+
+ const { processedLogs, isProcessing } = useLogPreview();
+ const { mutate: saveTheme } = useCreateTheme();
+
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const [copiedCode, setCopiedCode] = useState(false);
+ const [copiedConfig, setCopiedConfig] = useState(false);
+
+ const handleCopyCode = async () => {
+ const code = generateThemeCode(name, colors, presets);
+ await navigator.clipboard.writeText(code);
+ setCopiedCode(true);
+ setTimeout(() => setCopiedCode(false), 2000);
+ };
+
+ const handleCopyConfig = async () => {
+ const config = { name, colors, presets, mode: "dark" };
+ await navigator.clipboard.writeText(JSON.stringify(config, null, 2));
+ setCopiedConfig(true);
+ setTimeout(() => setCopiedConfig(false), 2000);
+ };
+
+ const handleDownload = () => {
+ const code = generateThemeCode(name, colors, presets);
+ const blob = new Blob([code], { type: "text/javascript" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${name}-theme.js`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ };
+
+ const handleSave = () => {
+ saveTheme({ name, colors, presets });
+ };
+
+ const handleShare = async () => {
+ const shareCode = exportThemeToShareCode(name, colors, presets);
+ const url = generateShareUrl(shareCode);
+ await navigator.clipboard.writeText(url);
+ };
+
+ return (
+
+
+
Create Your Custom Theme
+
+ Design your own LogsDX theme with custom colors and presets. See
+ real-time preview using the actual LogsDX engine.
+
+
+
+
+ {/* Left: Controls */}
+
+ {/* Theme Name */}
+
+
Theme Basics
+
+
+ setName(e.target.value)}
+ className="w-full px-3 py-2 border rounded-md dark:bg-slate-700 dark:border-slate-600"
+ placeholder="my-awesome-theme"
+ />
+
+
+
+ {/* Colors */}
+
+
+ {/* Presets */}
+
+
+
+ {/* Right: Preview & Export */}
+
+ {/* Live Preview */}
+
+
+ {/* Export Options */}
+
+
Export Theme
+
+
+
+
+
+
+
+
+
+ {/* Generated Code */}
+
+
+
Generated Code
+
+
+ {showAdvanced && (
+
+
+ {generateThemeCode(name, colors, presets)}
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/site/components/themegenerator/PresetSelector.tsx b/site/components/themegenerator/PresetSelector.tsx
new file mode 100644
index 0000000..445c093
--- /dev/null
+++ b/site/components/themegenerator/PresetSelector.tsx
@@ -0,0 +1,40 @@
+import type { ThemePreset } from "./types";
+
+interface PresetSelectorProps {
+ presets: ThemePreset[];
+ selectedPresets: string[];
+ onToggle: (presetId: string) => void;
+}
+
+export function PresetSelector({
+ presets,
+ selectedPresets,
+ onToggle,
+}: PresetSelectorProps) {
+ return (
+
+
Pattern Presets
+
+ {presets.map((preset) => (
+
+ ))}
+
+
+ );
+}
diff --git a/site/components/themegenerator/ThemeColorPicker.tsx b/site/components/themegenerator/ThemeColorPicker.tsx
new file mode 100644
index 0000000..9478f43
--- /dev/null
+++ b/site/components/themegenerator/ThemeColorPicker.tsx
@@ -0,0 +1,54 @@
+import { Button } from "@/components/ui/button";
+import { RefreshCw } from "lucide-react";
+import type { ThemeColors } from "./types";
+
+interface ThemeColorPickerProps {
+ colors: ThemeColors;
+ onColorChange: (key: keyof ThemeColors, value: string) => void;
+ onReset: () => void;
+}
+
+export function ThemeColorPicker({
+ colors,
+ onColorChange,
+ onReset,
+}: ThemeColorPickerProps) {
+ return (
+
+
+
Colors
+
+
+
+ {Object.entries(colors).map(([key, value]) => (
+
+ ))}
+
+
+ );
+}
diff --git a/site/components/themegenerator/ThemePreview.tsx b/site/components/themegenerator/ThemePreview.tsx
new file mode 100644
index 0000000..f9dc0bd
--- /dev/null
+++ b/site/components/themegenerator/ThemePreview.tsx
@@ -0,0 +1,84 @@
+import type { ThemeColors } from "./types";
+
+interface ThemePreviewProps {
+ processedLogs: string[];
+ isProcessing: boolean;
+ colors: ThemeColors;
+}
+
+export function ThemePreview({
+ processedLogs,
+ isProcessing,
+ colors,
+}: ThemePreviewProps) {
+ return (
+
+
+
Live Preview
+ Powered by LogsDX
+
+
+
+
+ {isProcessing ? (
+
+ ) : processedLogs.length > 0 ? (
+
+ {processedLogs.map((log, index) => (
+
+ ))}
+ {processedLogs.map((log, index) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/site/components/themegenerator/index.tsx b/site/components/themegenerator/index.tsx
index 45c8cfe..e486650 100644
--- a/site/components/themegenerator/index.tsx
+++ b/site/components/themegenerator/index.tsx
@@ -1,684 +1 @@
-"use client";
-
-import React, { useState, useMemo, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import {
- ChevronDown,
- ChevronRight,
- Download,
- Copy,
- RefreshCw,
-} from "lucide-react";
-import { createSimpleTheme, registerTheme, getLogsDX } from "logsdx";
-import type { ThemeColors, ThemeConfig } from "./types";
-import { DEFAULT_DARK_COLORS, PRESET_OPTIONS, SAMPLE_LOGS } from "./constants";
-import { generateThemeCode } from "./utils";
-
-const STORAGE_KEY = "logsdx-custom-theme";
-
-export function CustomThemeCreator() {
- // Load saved theme from localStorage or use defaults
- const [themeName, setThemeName] = useState("dracula-custom");
- const [colors, setColors] = useState
(DEFAULT_DARK_COLORS);
- const [selectedPresets, setSelectedPresets] = useState([
- "logLevels",
- "numbers",
- "strings",
- "brackets",
- ]);
- const [showAdvanced, setShowAdvanced] = useState(false);
- const [copiedCode, setCopiedCode] = useState(false);
- const [copiedConfig, setCopiedConfig] = useState(false);
- const [isLoaded, setIsLoaded] = useState(false);
-
- // Initialize with empty array, will be populated by useEffect
- const [processedLogs, setProcessedLogs] = useState([]);
-
- // Load saved theme from localStorage on mount
- useEffect(() => {
- const savedTheme = localStorage.getItem(STORAGE_KEY);
- if (savedTheme) {
- try {
- const parsed = JSON.parse(savedTheme);
- if (parsed.themeName) setThemeName(parsed.themeName);
- if (parsed.colors) setColors(parsed.colors);
- if (parsed.selectedPresets) setSelectedPresets(parsed.selectedPresets);
- } catch (e) {
- console.error("Failed to load saved theme:", e);
- }
- }
- setIsLoaded(true);
- }, []);
-
- // Save theme to localStorage whenever it changes
- useEffect(() => {
- if (!isLoaded) return;
-
- const themeData = {
- themeName,
- colors,
- selectedPresets,
- };
- localStorage.setItem(STORAGE_KEY, JSON.stringify(themeData));
- }, [themeName, colors, selectedPresets, isLoaded]);
-
- useEffect(() => {
- const processLogs = () => {
- try {
- const tempThemeName = `logsdx-theme-preview`;
-
- const customTheme = createSimpleTheme(tempThemeName, colors, {
- mode: "dark",
- presets: selectedPresets,
- });
-
- registerTheme(customTheme);
-
- const htmlLogsDX = getLogsDX({
- theme: tempThemeName,
- outputFormat: "html",
- htmlStyleFormat: "css",
- escapeHtml: false,
- }) as unknown as { processLine: (line: string) => string };
-
- const processed = SAMPLE_LOGS.map((log) => {
- try {
- return htmlLogsDX.processLine(log.text);
- } catch (e) {
- console.error("LogsDX processing failed:", log.text, e);
- // Return styled fallback using all theme colors
- let styledText = log.text;
-
- // Apply colors based on selected presets
- if (selectedPresets.includes("logLevels")) {
- if (log.text.includes("[ERROR]") || log.text.includes("ERROR:")) {
- styledText = styledText.replace(
- /(\[ERROR\]|ERROR:)/g,
- `$1`,
- );
- }
- if (log.text.includes("[WARN]") || log.text.includes("WARN:")) {
- styledText = styledText.replace(
- /(\[WARN\]|WARN:)/g,
- `$1`,
- );
- }
- if (
- log.text.includes("[SUCCESS]") ||
- log.text.includes("SUCCESS:")
- ) {
- styledText = styledText.replace(
- /(\[SUCCESS\]|SUCCESS:)/g,
- `$1`,
- );
- }
- if (log.text.includes("[INFO]") || log.text.includes("INFO:")) {
- styledText = styledText.replace(
- /(\[INFO\]|INFO:)/g,
- `$1`,
- );
- }
- if (log.text.includes("[DEBUG]") || log.text.includes("DEBUG:")) {
- styledText = styledText.replace(
- /(\[DEBUG\]|DEBUG:)/g,
- `$1`,
- );
- }
- }
-
- // Highlight numbers
- if (selectedPresets.includes("numbers")) {
- styledText = styledText.replace(
- /\b(\d+\.?\d*)\b/g,
- `$1`,
- );
- }
-
- // Highlight strings
- if (selectedPresets.includes("strings")) {
- styledText = styledText.replace(
- /(['"])([^'"]*)\1/g,
- `$1$2$1`,
- );
- }
-
- // Highlight brackets
- if (selectedPresets.includes("brackets")) {
- styledText = styledText.replace(
- /[[\]{}()<>]/g,
- `$&`,
- );
- }
-
- // Highlight booleans and null
- if (selectedPresets.includes("booleans")) {
- styledText = styledText.replace(
- /\b(true|false|null|undefined)\b/g,
- `$1`,
- );
- }
-
- // URLs
- if (selectedPresets.includes("urls")) {
- styledText = styledText.replace(
- /(https?:\/\/[^\s]+)/g,
- `$1`,
- );
- }
-
- // Paths
- if (selectedPresets.includes("paths")) {
- styledText = styledText.replace(
- /(\/[\w\-./]+|[A-Z]:\\[\w\-.\\]+)/g,
- `$1`,
- );
- }
-
- // Timestamps
- if (selectedPresets.includes("timestamps")) {
- styledText = styledText.replace(
- /(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[\w.:]*)/g,
- `$1`,
- );
- }
-
- // Semantic versions
- if (selectedPresets.includes("semantic")) {
- styledText = styledText.replace(
- /\bv?(\d+\.\d+\.\d+)\b/g,
- `$&`,
- );
- }
-
- return `${styledText}`;
- }
- });
-
- setProcessedLogs(processed);
- } catch (error) {
- console.error("THEME CREATION ERROR:", error);
- // Styled fallback showing all colors
- const fallbackLogs = SAMPLE_LOGS.map((log) => {
- let styledText = log.text;
-
- // Apply all color categories
- if (log.category === "error" || log.text.includes("[ERROR]")) {
- styledText = `${log.text}`;
- } else if (
- log.category === "warning" ||
- log.text.includes("[WARN]")
- ) {
- styledText = `${log.text}`;
- } else if (
- log.category === "success" ||
- log.text.includes("[SUCCESS]")
- ) {
- styledText = `${log.text}`;
- } else if (log.category === "info" || log.text.includes("[INFO]")) {
- styledText = `${log.text}`;
- } else if (log.category === "debug" || log.text.includes("[DEBUG]")) {
- styledText = `${log.text}`;
- }
-
- // Apply pattern matching based on selected presets
- // Numbers
- if (selectedPresets.includes("numbers")) {
- styledText = styledText.replace(
- /\b(\d+\.?\d*)\b/g,
- `$1`,
- );
- }
-
- // Strings
- if (selectedPresets.includes("strings")) {
- styledText = styledText.replace(
- /(['"])([^'"]*)\1/g,
- `$1$2$1`,
- );
- }
-
- // Brackets
- if (selectedPresets.includes("brackets")) {
- styledText = styledText.replace(
- /[[\]{}()<>]/g,
- `$&`,
- );
- }
-
- // Booleans
- if (selectedPresets.includes("booleans")) {
- styledText = styledText.replace(
- /\b(true|false|null|undefined)\b/g,
- `$1`,
- );
- }
-
- // URLs
- if (selectedPresets.includes("urls")) {
- styledText = styledText.replace(
- /(https?:\/\/[^\s]+)/g,
- `$1`,
- );
- }
-
- // Paths
- if (selectedPresets.includes("paths")) {
- styledText = styledText.replace(
- /(\/[\w\-./]+|[A-Z]:\\[\w\-.\\]+)/g,
- `$1`,
- );
- }
-
- // Timestamps
- if (selectedPresets.includes("timestamps")) {
- styledText = styledText.replace(
- /(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[\w.:]*)/g,
- `$1`,
- );
- }
-
- // Dates
- if (selectedPresets.includes("dates")) {
- styledText = styledText.replace(
- /(\[\d{4}-\d{2}-\d{2}[\s\d:]*\])/g,
- `$1`,
- );
- }
-
- // Semantic versions
- if (selectedPresets.includes("semantic")) {
- styledText = styledText.replace(
- /\bv?(\d+\.\d+\.\d+)\b/g,
- `$&`,
- );
- }
-
- // JSON keys
- if (selectedPresets.includes("json")) {
- styledText = styledText.replace(
- /(\w+):/g,
- `$1:`,
- );
- }
-
- // Wrap in text color
- styledText = `${styledText}`;
-
- return styledText;
- });
- setProcessedLogs(fallbackLogs);
- }
- };
-
- processLogs();
- }, [colors, selectedPresets]);
-
- const handleColorChange = (key: keyof ThemeColors, value: string) => {
- setColors((prev) => ({ ...prev, [key]: value }));
- };
-
- const handlePresetToggle = (presetId: string) => {
- setSelectedPresets((prev) =>
- prev.includes(presetId)
- ? prev.filter((p) => p !== presetId)
- : [...prev, presetId],
- );
- };
-
- const resetColors = () => {
- setColors(DEFAULT_DARK_COLORS);
- // Clear saved theme from localStorage
- localStorage.removeItem(STORAGE_KEY);
- };
-
- const themeConfig: ThemeConfig = useMemo(() => {
- return {
- name: themeName,
- mode: "dark", // Default mode, colors determine actual appearance
- colors,
- presets: selectedPresets,
- };
- }, [themeName, colors, selectedPresets]);
-
- const copyCode = async () => {
- const code = generateThemeCode(themeName, "dark", colors, selectedPresets);
- await navigator.clipboard.writeText(code);
- setCopiedCode(true);
- setTimeout(() => setCopiedCode(false), 2000);
- };
-
- const copyConfig = async () => {
- await navigator.clipboard.writeText(JSON.stringify(themeConfig, null, 2));
- setCopiedConfig(true);
- setTimeout(() => setCopiedConfig(false), 2000);
- };
-
- const downloadTheme = () => {
- const code = generateThemeCode(themeName, "dark", colors, selectedPresets);
- const blob = new Blob([code], { type: "text/javascript" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = `${themeName}-theme.js`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
-
- return (
-
-
-
Create Your Custom Theme
-
- Design your own LogsDX theme with custom colors and presets. See
- real-time preview using the actual LogsDX engine.
-
-
-
-
-
-
-
Theme Basics
-
-
-
-
- setThemeName(
- e.target.value.toLowerCase().replace(/\s+/g, "-"),
- )
- }
- className="w-full px-3 py-2 border rounded-md dark:bg-slate-700 dark:border-slate-600"
- placeholder="my-awesome-theme"
- />
-
-
-
-
-
-
Colors
-
-
-
-
- {Object.entries(colors).map(([key, value]) => (
-
- ))}
-
-
-
-
-
Pattern Presets
-
- {PRESET_OPTIONS.map((preset) => (
-
- ))}
-
-
-
-
-
-
-
-
Live Preview
-
- Powered by LogsDX
-
-
-
-
-
- {processedLogs.length > 0 ? (
-
- {/* First set of logs */}
- {processedLogs.map((log, index) => (
-
- ))}
- {/* Duplicate set for seamless loop */}
- {processedLogs.map((log, index) => (
-
- ))}
-
- ) : (
-
-
- Processing logs...
-
-
- )}
-
-
-
-
-
-
Export Theme
-
-
-
-
-
-
-
-
-
-
-
-
-
Generated Code
-
-
-
- {showAdvanced && (
-
-
-
- {generateThemeCode(
- themeName,
- "dark",
- colors,
- selectedPresets,
- )}
-
-
-
- )}
-
-
-
-
Test with Custom Input
-
-
-
-
-
- );
-}
-
-function CustomLogInput({
- themeName: _themeName,
- colors,
-}: {
- themeName: string;
- colors: ThemeColors;
-}) {
- const [customLog, setCustomLog] = useState("");
- const [processedCustomLog, setProcessedCustomLog] = useState("");
-
- useEffect(() => {
- if (!customLog) {
- setProcessedCustomLog("");
- return;
- }
-
- try {
- const tempThemeName = `logsdx-custom-input`;
- const customTheme = createSimpleTheme(tempThemeName, colors, {
- mode: "dark",
- presets: ["logLevels", "numbers", "strings", "brackets"],
- });
- registerTheme(customTheme);
-
- const htmlLogsDX = getLogsDX({
- theme: tempThemeName,
- outputFormat: "html",
- htmlStyleFormat: "css",
- escapeHtml: false,
- }) as unknown as { processLine: (line: string) => string };
- const processed = htmlLogsDX.processLine(customLog);
- setProcessedCustomLog(processed);
- } catch (error) {
- console.error("Error processing custom log:", error);
- }
- }, [colors, customLog]);
-
- return (
-
- );
-}
+export { CustomThemeCreator } from "./CustomThemeCreator";
diff --git a/site/components/themegenerator/useThemeForm.tsx b/site/components/themegenerator/useThemeForm.tsx
deleted file mode 100644
index c6bdc7a..0000000
--- a/site/components/themegenerator/useThemeForm.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-"use client";
-
-import { useState, useEffect, useCallback } from "react";
-import { useForm } from "react-hook-form";
-import { createSimpleTheme, registerTheme, getLogsDX } from "logsdx";
-import { useThemeStore } from "./useThemeStore";
-import { SAMPLE_LOGS } from "./constants";
-import type { ThemeColors } from "./types";
-
-export interface ThemeFormData {
- name: string;
- colors: ThemeColors;
- presets: string[];
-}
-
-export function useThemeForm() {
- const [processedLogs, setProcessedLogs] = useState([]);
- const [isProcessing, setIsProcessing] = useState(false);
- const [shareUrl, setShareUrl] = useState("");
-
- const {
- currentTheme,
- setThemeName,
- setColor,
- togglePreset,
- resetTheme,
- saveTheme,
- generateShareUrl,
- } = useThemeStore();
-
- const { control, watch, reset, setValue, handleSubmit } =
- useForm({
- defaultValues: currentTheme,
- });
-
- // Sync store with form
- useEffect(() => {
- reset(currentTheme);
- }, [currentTheme, reset]);
-
- // Process logs with LogsDX whenever theme changes
- const processLogs = useCallback(async () => {
- setIsProcessing(true);
-
- try {
- const tempThemeName = `preview-${Date.now()}`;
-
- const customTheme = createSimpleTheme(
- tempThemeName,
- currentTheme.colors,
- {
- mode: "dark",
- presets: currentTheme.presets,
- },
- );
-
- registerTheme(customTheme);
-
- // Small delay to ensure registration
- await new Promise((resolve) => setTimeout(resolve, 50));
-
- const htmlLogsDX = getLogsDX({
- theme: tempThemeName,
- outputFormat: "html",
- htmlStyleFormat: "css",
- escapeHtml: false,
- }) as unknown as { processLine: (line: string) => string };
-
- const processed = SAMPLE_LOGS.map((log) => {
- try {
- const result = htmlLogsDX.processLine(log.text);
- return result;
- } catch (e) {
- // Fallback if LogsDX fails
- console.warn("LogsDX processing failed, using fallback:", e);
- return `${log.text}`;
- }
- });
-
- setProcessedLogs(processed);
- } catch (error) {
- console.error("Theme processing error:", error);
- // Use fallback rendering
- const fallback = SAMPLE_LOGS.map(
- (log) =>
- `${log.text}`,
- );
- setProcessedLogs(fallback);
- } finally {
- setIsProcessing(false);
- }
- }, [currentTheme]);
-
- // Process logs on mount and when theme changes
- useEffect(() => {
- processLogs();
- }, [processLogs]);
-
- // Form handlers that update the store
- const handleColorChange = (key: keyof ThemeColors, value: string) => {
- setColor(key, value);
- setValue(`colors.${key}`, value);
- };
-
- const handlePresetToggle = (presetId: string) => {
- togglePreset(presetId);
- };
-
- const handleNameChange = (name: string) => {
- setThemeName(name);
- setValue("name", name);
- };
-
- const handleSaveTheme = async () => {
- const themeId = await saveTheme();
- return themeId;
- };
-
- const handleShareTheme = () => {
- const url = generateShareUrl();
- setShareUrl(url);
-
- // Copy to clipboard
- if (navigator.clipboard) {
- navigator.clipboard.writeText(url);
- }
-
- return url;
- };
-
- const handleExportCode = () => {
- // Generate JavaScript code
- const jsCode = `
-import { createSimpleTheme, registerTheme } from 'logsdx';
-
-const ${currentTheme.name.replace(/-/g, "_")}Theme = createSimpleTheme({
- name: '${currentTheme.name}',
- mode: 'dark',
- colors: ${JSON.stringify(currentTheme.colors, null, 2)},
- presets: ${JSON.stringify(currentTheme.presets)}
-});
-
-registerTheme(${currentTheme.name.replace(/-/g, "_")}Theme);
-`;
-
- return jsCode;
- };
-
- const handleReset = () => {
- resetTheme();
- reset({
- name: "my-custom-theme",
- colors: currentTheme.colors,
- presets: ["logLevels", "numbers", "strings", "brackets"],
- });
- };
-
- return {
- // Form
- control,
- watch,
- handleSubmit,
-
- // Theme data
- currentTheme,
- processedLogs,
- isProcessing,
- shareUrl,
-
- // Actions
- handleColorChange,
- handlePresetToggle,
- handleNameChange,
- handleSaveTheme,
- handleShareTheme,
- handleExportCode,
- handleReset,
- };
-}
diff --git a/site/components/themegenerator/useThemeStore.tsx b/site/components/themegenerator/useThemeStore.tsx
deleted file mode 100644
index caa0b8f..0000000
--- a/site/components/themegenerator/useThemeStore.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-import { create } from "zustand";
-import { persist, createJSONStorage } from "zustand/middleware";
-import { openDB, DBSchema, IDBPDatabase } from "idb";
-import type { ThemeColors } from "./types";
-import { DEFAULT_DARK_COLORS } from "./constants";
-
-interface ThemeDB extends DBSchema {
- themes: {
- key: string;
- value: SavedTheme;
- indexes: { "by-date": Date };
- };
-}
-
-export interface SavedTheme {
- id: string;
- name: string;
- colors: ThemeColors;
- presets: string[];
- createdAt: Date;
- updatedAt: Date;
- shareCode?: string;
-}
-
-interface ThemeState {
- // Current theme being edited
- currentTheme: {
- name: string;
- colors: ThemeColors;
- presets: string[];
- };
-
- // Saved themes
- savedThemes: SavedTheme[];
-
- // Actions
- setThemeName: (name: string) => void;
- setColor: (key: keyof ThemeColors, value: string) => void;
- togglePreset: (presetId: string) => void;
- resetTheme: () => void;
- saveTheme: () => Promise;
- loadTheme: (id: string) => Promise;
- deleteTheme: (id: string) => Promise;
- exportTheme: () => string;
- importTheme: (shareCode: string) => void;
- generateShareUrl: () => string;
- loadFromShareCode: (shareCode: string) => void;
-}
-
-// IndexedDB helper
-let db: IDBPDatabase | null = null;
-
-async function getDB() {
- if (!db) {
- db = await openDB("logsdx-themes", 1, {
- upgrade(db) {
- const store = db.createObjectStore("themes", { keyPath: "id" });
- store.createIndex("by-date", "updatedAt");
- },
- });
- }
- return db;
-}
-
-// Create the store with persistence
-export const useThemeStore = create()(
- persist(
- (set, get) => ({
- currentTheme: {
- name: "my-custom-theme",
- colors: DEFAULT_DARK_COLORS,
- presets: ["logLevels", "numbers", "strings", "brackets"],
- },
-
- savedThemes: [],
-
- setThemeName: (name) =>
- set((state) => ({
- currentTheme: {
- ...state.currentTheme,
- name: name.toLowerCase().replace(/\s+/g, "-"),
- },
- })),
-
- setColor: (key, value) =>
- set((state) => ({
- currentTheme: {
- ...state.currentTheme,
- colors: {
- ...state.currentTheme.colors,
- [key]: value,
- },
- },
- })),
-
- togglePreset: (presetId) =>
- set((state) => ({
- currentTheme: {
- ...state.currentTheme,
- presets: state.currentTheme.presets.includes(presetId)
- ? state.currentTheme.presets.filter((p) => p !== presetId)
- : [...state.currentTheme.presets, presetId],
- },
- })),
-
- resetTheme: () =>
- set({
- currentTheme: {
- name: "my-custom-theme",
- colors: DEFAULT_DARK_COLORS,
- presets: ["logLevels", "numbers", "strings", "brackets"],
- },
- }),
-
- saveTheme: async () => {
- const { currentTheme } = get();
- const id = `theme-${Date.now()}`;
- const theme: SavedTheme = {
- id,
- name: currentTheme.name,
- colors: currentTheme.colors,
- presets: currentTheme.presets,
- createdAt: new Date(),
- updatedAt: new Date(),
- };
-
- // Save to IndexedDB
- const database = await getDB();
- await database.add("themes", theme);
-
- // Update saved themes list
- const allThemes = await database.getAllFromIndex("themes", "by-date");
- set({ savedThemes: allThemes.reverse() });
-
- return id;
- },
-
- loadTheme: async (id) => {
- const database = await getDB();
- const theme = await database.get("themes", id);
-
- if (theme) {
- set({
- currentTheme: {
- name: theme.name,
- colors: theme.colors,
- presets: theme.presets,
- },
- });
- }
- },
-
- deleteTheme: async (id) => {
- const database = await getDB();
- await database.delete("themes", id);
-
- const allThemes = await database.getAllFromIndex("themes", "by-date");
- set({ savedThemes: allThemes.reverse() });
- },
-
- exportTheme: () => {
- const { currentTheme } = get();
- const data = {
- name: currentTheme.name,
- colors: currentTheme.colors,
- presets: currentTheme.presets,
- version: "1.0.0",
- };
- return btoa(JSON.stringify(data));
- },
-
- importTheme: (shareCode) => {
- try {
- const data = JSON.parse(atob(shareCode));
- set({
- currentTheme: {
- name: data.name || "imported-theme",
- colors: data.colors || DEFAULT_DARK_COLORS,
- presets: data.presets || [],
- },
- });
- } catch (e) {
- console.error("Failed to import theme:", e);
- }
- },
-
- generateShareUrl: () => {
- const shareCode = get().exportTheme();
- const baseUrl =
- typeof window !== "undefined" ? window.location.origin : "";
- return `${baseUrl}/theme/${shareCode}`;
- },
-
- loadFromShareCode: (shareCode) => {
- get().importTheme(shareCode);
- },
- }),
- {
- name: "logsdx-theme-store",
- storage: createJSONStorage(() => localStorage),
- partialize: (state) => ({ currentTheme: state.currentTheme }),
- },
- ),
-);
-
-// Load saved themes from IndexedDB on initialization
-if (typeof window !== "undefined") {
- getDB().then(async (database) => {
- const themes = await database.getAllFromIndex("themes", "by-date");
- useThemeStore.setState({ savedThemes: themes.reverse() });
- });
-}
diff --git a/site/components/themegenerator/utils.ts b/site/components/themegenerator/utils.ts
deleted file mode 100644
index a6a293a..0000000
--- a/site/components/themegenerator/utils.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import type { ThemeColors } from "./types";
-
-export function generateThemeCode(
- themeName: string,
- mode: string,
- colors: ThemeColors,
- selectedPresets: string[],
- customPatterns?: Array<{
- name: string;
- pattern: string;
- color: string;
- }>,
-): string {
- const safeThemeName = themeName.replace(/-/g, "_");
-
- const themeConfig: Record = {
- name: themeName,
- mode,
- colors,
- presets: selectedPresets,
- };
-
- if (customPatterns && customPatterns.length > 0) {
- themeConfig.customPatterns = customPatterns.map((p) => ({
- name: p.name || "custom",
- pattern: p.pattern,
- color: p.color,
- }));
- }
-
- const configString = JSON.stringify(themeConfig, null, 2)
- .replace(/"([^"]+)":/g, "$1:")
- .replace(/"pattern":\s*"([^"]+)"/g, "pattern: /$1/g");
-
- return `import { createSimpleTheme, registerTheme, getLogsDX } from 'logsdx';
-
-const ${safeThemeName}Theme = createSimpleTheme(${configString});
-
-registerTheme(${safeThemeName}Theme);
-
-const logsDX = getLogsDX({ theme: '${themeName}' });
-
-console.log(logsDX.processLine('ERROR: Something went wrong'));
-console.log(logsDX.processLine('INFO: Server started on port 3000'));
-console.log(logsDX.processLine('DEBUG: { user: "john", status: true }'));`;
-}
diff --git a/site/content/docs/getting-started/configuration.md b/site/content/docs/getting-started/configuration.md
new file mode 100644
index 0000000..3cf76e1
--- /dev/null
+++ b/site/content/docs/getting-started/configuration.md
@@ -0,0 +1,177 @@
+---
+title: Configuration
+description: Configure LogsDX to match your workflow
+order: 3
+---
+
+# Configuration
+
+LogsDX can be configured through options passed to `getLogsDX()` or `LogsDX.getInstance()`.
+
+## Configuration Options
+
+```typescript
+interface LogsDXOptions {
+ theme?: string | Theme | ThemePair;
+ outputFormat?: "ansi" | "html";
+ htmlStyleFormat?: "css" | "className";
+ escapeHtml?: boolean;
+ debug?: boolean;
+ autoAdjustTerminal?: boolean;
+}
+```
+
+## Options Reference
+
+### theme
+
+The theme to use for styling logs.
+
+```typescript
+// Use a built-in theme by name
+const logsdx = await getLogsDX({ theme: "dracula" });
+
+// Use a custom Theme object
+const logsdx = await getLogsDX({
+ theme: {
+ name: "my-theme",
+ mode: "dark",
+ schema: {
+ /* ... */
+ },
+ },
+});
+
+// Use a ThemePair for light/dark mode
+const logsdx = await getLogsDX({
+ theme: {
+ light: "github-light",
+ dark: "github-dark",
+ },
+});
+```
+
+**Default:** `"oh-my-zsh"`
+
+### outputFormat
+
+Controls the output format of processed logs.
+
+| Value | Description |
+| -------- | ------------------------------------- |
+| `"ansi"` | ANSI escape codes for terminal output |
+| `"html"` | HTML markup for browser rendering |
+
+```typescript
+// Terminal output
+const terminal = await getLogsDX({ outputFormat: "ansi" });
+
+// Browser output
+const browser = await getLogsDX({ outputFormat: "html" });
+```
+
+**Default:** `"ansi"`
+
+### htmlStyleFormat
+
+When using HTML output, controls how styles are applied.
+
+| Value | Description |
+| ------------- | ---------------------------------------- |
+| `"css"` | Inline CSS styles via `style` attribute |
+| `"className"` | CSS class names for external stylesheets |
+
+```typescript
+// Inline styles: ERROR
+const inline = await getLogsDX({
+ outputFormat: "html",
+ htmlStyleFormat: "css",
+});
+
+// Class names: ERROR
+const classes = await getLogsDX({
+ outputFormat: "html",
+ htmlStyleFormat: "className",
+});
+```
+
+**Default:** `"css"`
+
+### escapeHtml
+
+Whether to escape HTML entities in the output when using HTML format.
+
+```typescript
+// Escape HTML (safe for user content)
+const safe = await getLogsDX({ escapeHtml: true });
+
+// Raw HTML (for trusted content only)
+const raw = await getLogsDX({ escapeHtml: false });
+```
+
+**Default:** `true`
+
+### autoAdjustTerminal
+
+Automatically detect terminal background and switch to appropriate theme variant.
+
+```typescript
+// Auto-switch between github-dark and github-light
+const logsdx = await getLogsDX({
+ theme: "github-dark",
+ autoAdjustTerminal: true,
+});
+```
+
+When enabled, LogsDX will:
+
+1. Detect if your terminal has a light or dark background
+2. Automatically switch between `-light` and `-dark` theme variants
+3. Fall back to the specified theme if no variant exists
+
+**Default:** `true`
+
+### debug
+
+Enable debug logging for troubleshooting.
+
+```typescript
+const logsdx = await getLogsDX({ debug: true });
+```
+
+**Default:** `false`
+
+## Runtime Configuration
+
+You can change configuration after initialization:
+
+```typescript
+const logsdx = await getLogsDX({ theme: "dracula" });
+
+// Change theme
+await logsdx.setTheme("nord");
+
+// Change output format
+logsdx.setOutputFormat("html");
+
+// Change HTML style format
+logsdx.setHtmlStyleFormat("className");
+```
+
+## Environment Variables
+
+LogsDX respects these environment variables:
+
+| Variable | Description |
+| -------------- | ------------------------------------- |
+| `COLORFGBG` | Terminal foreground/background colors |
+| `TERM_PROGRAM` | Terminal application name |
+| `COLORTERM` | Color support level |
+
+These are used for automatic theme mode detection when `autoAdjustTerminal` is enabled.
+
+## Next Steps
+
+- [API Reference](/docs/api/logsdx) - Complete API documentation
+- [Custom Themes](/docs/guides/custom-themes) - Create your own themes
+- [CLI Usage](/docs/guides/cli-usage) - Use LogsDX from the command line
diff --git a/site/db/collections.ts b/site/db/collections.ts
new file mode 100644
index 0000000..25d0dfa
--- /dev/null
+++ b/site/db/collections.ts
@@ -0,0 +1,55 @@
+import { getDB, type SavedTheme } from "./schema";
+import type { ThemeColors } from "@/components/themegenerator/types";
+
+export async function createTheme(
+ name: string,
+ colors: ThemeColors,
+ presets: string[],
+): Promise {
+ const db = await getDB();
+ const theme: SavedTheme = {
+ id: `theme-${Date.now()}`,
+ name,
+ colors,
+ presets,
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ };
+
+ await db.put("themes", theme);
+ return theme;
+}
+
+export async function updateTheme(
+ id: string,
+ updates: Partial>,
+): Promise {
+ const db = await getDB();
+ const existing = await db.get("themes", id);
+ if (!existing) return undefined;
+
+ const updated: SavedTheme = {
+ ...existing,
+ ...updates,
+ updatedAt: Date.now(),
+ };
+
+ await db.put("themes", updated);
+ return updated;
+}
+
+export async function deleteTheme(id: string): Promise {
+ const db = await getDB();
+ await db.delete("themes", id);
+}
+
+export async function getTheme(id: string): Promise {
+ const db = await getDB();
+ return db.get("themes", id);
+}
+
+export async function getAllThemes(): Promise {
+ const db = await getDB();
+ const themes = await db.getAll("themes");
+ return themes.sort((a, b) => b.updatedAt - a.updatedAt);
+}
diff --git a/site/db/schema.ts b/site/db/schema.ts
new file mode 100644
index 0000000..c5a43b8
--- /dev/null
+++ b/site/db/schema.ts
@@ -0,0 +1,38 @@
+import { openDB, type IDBPDatabase, type DBSchema } from "idb";
+import type { ThemeColors } from "@/components/themegenerator/types";
+
+export interface SavedTheme {
+ id: string;
+ name: string;
+ colors: ThemeColors;
+ presets: string[];
+ createdAt: number;
+ updatedAt: number;
+ shareCode?: string;
+}
+
+interface LogsDXDBSchema extends DBSchema {
+ themes: {
+ key: string;
+ value: SavedTheme;
+ indexes: {
+ byDate: number;
+ byName: string;
+ };
+ };
+}
+
+let dbPromise: Promise> | null = null;
+
+export function getDB(): Promise> {
+ if (!dbPromise) {
+ dbPromise = openDB("logsdx", 1, {
+ upgrade(db) {
+ const store = db.createObjectStore("themes", { keyPath: "id" });
+ store.createIndex("byDate", "updatedAt");
+ store.createIndex("byName", "name");
+ },
+ });
+ }
+ return dbPromise;
+}
diff --git a/site/e2e/homepage.spec.ts b/site/e2e/homepage.spec.ts
new file mode 100644
index 0000000..1a85af2
--- /dev/null
+++ b/site/e2e/homepage.spec.ts
@@ -0,0 +1,207 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("Homepage", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/");
+ await page.waitForLoadState("domcontentloaded");
+ });
+
+ test.describe("Hero Section", () => {
+ test("displays logsDx heading", async ({ page }) => {
+ await expect(
+ page.getByRole("heading", { name: "logsDx", exact: true }),
+ ).toBeVisible();
+ });
+
+ test("displays tagline", async ({ page }) => {
+ await expect(page.getByText(/schema-based styling layer/i)).toBeVisible();
+ });
+
+ test("has Get Started CTA", async ({ page }) => {
+ const getStartedLink = page.getByRole("link", { name: /get started/i });
+ await expect(getStartedLink).toBeVisible();
+ await expect(getStartedLink).toHaveAttribute("href", "#setup");
+ });
+
+ test("has GitHub link", async ({ page }) => {
+ const githubLink = page.getByRole("link", { name: "View on GitHub" });
+ await expect(githubLink).toBeVisible();
+ await expect(githubLink).toHaveAttribute(
+ "href",
+ "https://github.com/yowainwright/logsdx",
+ );
+ });
+ });
+
+ test.describe("Sections", () => {
+ test("has problem section", async ({ page }) => {
+ await page.waitForTimeout(500);
+ const content = await page.content();
+ expect(content.toLowerCase()).toContain("problem");
+ });
+
+ test("has setup section", async ({ page }) => {
+ await page.goto("/#setup");
+ await page.waitForTimeout(500);
+ expect(page.url()).toContain("#setup");
+ });
+
+ test("has theme creator section", async ({ page }) => {
+ await page.goto("/#theme-creator");
+ await page.waitForTimeout(1000);
+
+ const heading = page.getByRole("heading", {
+ name: /create your custom theme/i,
+ });
+ await expect(heading).toBeVisible();
+ });
+ });
+
+ test.describe("Theme Creator", () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto("/#theme-creator");
+ await page.waitForTimeout(1000);
+ });
+
+ test("displays theme creator heading", async ({ page }) => {
+ await expect(
+ page.getByRole("heading", { name: /create your custom theme/i }),
+ ).toBeVisible();
+ });
+
+ test("has color pickers", async ({ page }) => {
+ const colorInputs = page.locator("input[type='color']");
+ expect(await colorInputs.count()).toBeGreaterThan(0);
+ });
+
+ test("has preset checkboxes", async ({ page }) => {
+ const checkboxes = page.locator("input[type='checkbox']");
+ expect(await checkboxes.count()).toBeGreaterThan(0);
+ });
+
+ test("shows live preview", async ({ page }) => {
+ await expect(page.getByText(/live preview/i)).toBeVisible();
+ });
+
+ test("has export buttons", async ({ page }) => {
+ await expect(
+ page.getByRole("button", { name: /copy code/i }),
+ ).toBeVisible();
+ await expect(
+ page.getByRole("button", { name: /download/i }),
+ ).toBeVisible();
+ });
+
+ test("can change theme name", async ({ page }) => {
+ const nameInput = page.getByPlaceholder("my-awesome-theme");
+ await nameInput.fill("test-theme");
+ await expect(nameInput).toHaveValue("test-theme");
+ });
+
+ test("can change color via text input", async ({ page }) => {
+ const colorInputs = page
+ .locator("input[type='text'][value^='#']")
+ .first();
+ await colorInputs.fill("#ff0000");
+ await expect(colorInputs).toHaveValue("#ff0000");
+ });
+
+ test("can toggle preset checkboxes", async ({ page }) => {
+ const checkbox = page.locator("input[type='checkbox']").first();
+ const initialState = await checkbox.isChecked();
+ await checkbox.click();
+ const newState = await checkbox.isChecked();
+ expect(newState).toBe(!initialState);
+ });
+
+ test("live preview shows log output", async ({ page }) => {
+ const preview = page.locator("[class*='font-mono']").first();
+ await expect(preview).toBeVisible();
+ });
+
+ test("can copy code to clipboard", async ({ page, context }) => {
+ await context.grantPermissions(["clipboard-read", "clipboard-write"]);
+ const copyButton = page.getByRole("button", { name: /copy code/i });
+ await copyButton.click();
+ await expect(page.getByRole("button", { name: "Copied!" })).toBeVisible({
+ timeout: 3000,
+ });
+ });
+
+ test("theme name converts to kebab-case", async ({ page }) => {
+ const nameInput = page.getByPlaceholder("my-awesome-theme");
+ await nameInput.fill("My Test Theme");
+ await expect(nameInput).toHaveValue("my-test-theme");
+ });
+
+ test("reset button restores defaults", async ({ page }) => {
+ const nameInput = page.getByPlaceholder("my-awesome-theme");
+ await nameInput.fill("custom-name");
+
+ const resetButton = page.getByRole("button", { name: /reset/i });
+ await resetButton.click();
+
+ await expect(nameInput).toHaveValue("my-custom-theme");
+ });
+ });
+
+ test.describe("Navigation", () => {
+ test("has navigation bar", async ({ page }) => {
+ const nav = page.locator("nav");
+ await expect(nav.first()).toBeVisible();
+ });
+
+ test("nav remains visible on scroll", async ({ page }) => {
+ await page.evaluate(() => window.scrollTo(0, 500));
+ await page.waitForTimeout(300);
+ const nav = page.locator("nav");
+ await expect(nav.first()).toBeVisible();
+ });
+ });
+
+ test.describe("Responsive", () => {
+ test("renders on mobile", async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await expect(
+ page.getByRole("heading", { name: "logsDx", exact: true }),
+ ).toBeVisible();
+ });
+
+ test("renders on tablet", async ({ page }) => {
+ await page.setViewportSize({ width: 768, height: 1024 });
+ await expect(
+ page.getByRole("heading", { name: "logsDx", exact: true }),
+ ).toBeVisible();
+ });
+ });
+
+ test.describe("Accessibility", () => {
+ test("has h1 heading", async ({ page }) => {
+ const h1 = page.locator("h1");
+ await expect(h1.first()).toBeVisible();
+ });
+
+ test("has clickable links", async ({ page }) => {
+ const links = page.getByRole("link");
+ expect(await links.count()).toBeGreaterThan(0);
+ });
+
+ test("supports keyboard nav", async ({ page }) => {
+ await page.keyboard.press("Tab");
+ const focused = await page.evaluate(
+ () => document.activeElement?.tagName,
+ );
+ expect(focused).toBeDefined();
+ });
+ });
+
+ test.describe("Performance", () => {
+ test("loads in reasonable time", async ({ page }) => {
+ const start = Date.now();
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ const loadTime = Date.now() - start;
+ expect(loadTime).toBeLessThan(10000);
+ });
+ });
+});
diff --git a/site/hooks/useLogPreview.ts b/site/hooks/useLogPreview.ts
new file mode 100644
index 0000000..7932346
--- /dev/null
+++ b/site/hooks/useLogPreview.ts
@@ -0,0 +1,34 @@
+import { useEffect } from "react";
+import { useDebouncedCallback } from "use-debounce";
+import {
+ useThemeEditorStore,
+ themeEditorActions,
+} from "@/stores/useThemeEditorStore";
+import { processLogs } from "@/lib/logProcessor";
+import { SAMPLE_LOGS } from "@/components/themegenerator/constants";
+
+export function useLogPreview() {
+ const colors = useThemeEditorStore((state) => state.colors);
+ const presets = useThemeEditorStore((state) => state.presets);
+ const { setProcessedLogs, setIsProcessing } = themeEditorActions;
+
+ const debouncedProcessLogs = useDebouncedCallback(async () => {
+ setIsProcessing(true);
+
+ try {
+ const processed = await processLogs(colors, presets, SAMPLE_LOGS);
+ setProcessedLogs(processed);
+ } finally {
+ setIsProcessing(false);
+ }
+ }, 300);
+
+ useEffect(() => {
+ debouncedProcessLogs();
+ }, [colors, presets, debouncedProcessLogs]);
+
+ return {
+ processedLogs: useThemeEditorStore((state) => state.processedLogs),
+ isProcessing: useThemeEditorStore((state) => state.isProcessing),
+ };
+}
diff --git a/site/hooks/useThemes.ts b/site/hooks/useThemes.ts
new file mode 100644
index 0000000..b1468f2
--- /dev/null
+++ b/site/hooks/useThemes.ts
@@ -0,0 +1,76 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import {
+ getAllThemes,
+ getTheme,
+ createTheme,
+ updateTheme,
+ deleteTheme,
+} from "@/db/collections";
+import type { ThemeColors } from "@/components/themegenerator/types";
+
+export function useThemes() {
+ return useQuery({
+ queryKey: ["themes"],
+ queryFn: getAllThemes,
+ });
+}
+
+export function useTheme(id: string | undefined) {
+ return useQuery({
+ queryKey: ["themes", id],
+ queryFn: () => (id ? getTheme(id) : Promise.resolve(undefined)),
+ enabled: !!id,
+ });
+}
+
+export function useCreateTheme() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ name,
+ colors,
+ presets,
+ }: {
+ name: string;
+ colors: ThemeColors;
+ presets: string[];
+ }) => createTheme(name, colors, presets),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["themes"] });
+ },
+ });
+}
+
+export function useUpdateTheme() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ id,
+ updates,
+ }: {
+ id: string;
+ updates: Partial<{
+ name: string;
+ colors: ThemeColors;
+ presets: string[];
+ }>;
+ }) => updateTheme(id, updates),
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["themes"] });
+ queryClient.invalidateQueries({ queryKey: ["themes", variables.id] });
+ },
+ });
+}
+
+export function useDeleteTheme() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (id: string) => deleteTheme(id),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["themes"] });
+ },
+ });
+}
diff --git a/site/lib/logProcessor.ts b/site/lib/logProcessor.ts
new file mode 100644
index 0000000..951248d
--- /dev/null
+++ b/site/lib/logProcessor.ts
@@ -0,0 +1,50 @@
+import { createSimpleTheme, registerTheme, getLogsDX } from "logsdx";
+import type { ThemeColors, SampleLog } from "@/components/themegenerator/types";
+import type { LogsDXInstance } from "@/types/logsdx";
+
+type ColorPalette = ThemeColors & { [key: string]: string | undefined };
+
+const createFallbackLog = (text: string, textColor: string): string =>
+ `${text}`;
+
+const processLogWithLogsDX = async (
+ log: SampleLog,
+ processor: LogsDXInstance,
+ fallbackColor: string,
+): Promise => {
+ try {
+ return processor.processLine(log.text);
+ } catch {
+ return createFallbackLog(log.text, fallbackColor);
+ }
+};
+
+export async function processLogs(
+ colors: ThemeColors,
+ presets: string[],
+ logs: SampleLog[],
+): Promise {
+ try {
+ const themeName = `preview-${Date.now()}`;
+ const theme = createSimpleTheme(themeName, colors as ColorPalette, {
+ mode: "dark",
+ presets,
+ });
+
+ registerTheme(theme);
+
+ const processor = await getLogsDX({
+ theme: themeName,
+ outputFormat: "html",
+ htmlStyleFormat: "css",
+ escapeHtml: false,
+ });
+
+ return Promise.all(
+ logs.map((log) => processLogWithLogsDX(log, processor, colors.text)),
+ );
+ } catch (error) {
+ console.error("Log processing failed:", error);
+ return logs.map((log) => createFallbackLog(log.text, colors.text));
+ }
+}
diff --git a/site/lib/themeUtils.ts b/site/lib/themeUtils.ts
new file mode 100644
index 0000000..9365e9e
--- /dev/null
+++ b/site/lib/themeUtils.ts
@@ -0,0 +1,65 @@
+import type { ThemeColors } from "@/components/themegenerator/types";
+import { DEFAULT_DARK_COLORS } from "@/components/themegenerator/constants";
+
+interface ThemeExport {
+ name: string;
+ colors: ThemeColors;
+ presets: string[];
+ version: string;
+}
+
+export function exportThemeToShareCode(
+ name: string,
+ colors: ThemeColors,
+ presets: string[],
+): string {
+ const data: ThemeExport = {
+ name,
+ colors,
+ presets,
+ version: "1.0.0",
+ };
+ return btoa(JSON.stringify(data));
+}
+
+export function importThemeFromShareCode(shareCode: string): {
+ name: string;
+ colors: ThemeColors;
+ presets: string[];
+} | null {
+ try {
+ const data = JSON.parse(atob(shareCode)) as ThemeExport;
+ return {
+ name: data.name || "imported-theme",
+ colors: data.colors || DEFAULT_DARK_COLORS,
+ presets: data.presets || [],
+ };
+ } catch (error) {
+ console.error("Failed to import theme:", error);
+ return null;
+ }
+}
+
+export function generateShareUrl(shareCode: string): string {
+ const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
+ return `${baseUrl}/theme/${shareCode}`;
+}
+
+export function generateThemeCode(
+ name: string,
+ colors: ThemeColors,
+ presets: string[],
+): string {
+ const varName = name.replace(/-/g, "_");
+
+ return `import { createSimpleTheme, registerTheme } from 'logsdx';
+
+const ${varName}Theme = createSimpleTheme('${name}', ${JSON.stringify(colors, null, 2)}, {
+ mode: 'dark',
+ presets: ${JSON.stringify(presets)}
+});
+
+registerTheme(${varName}Theme);
+
+export default ${varName}Theme;`;
+}
diff --git a/site/next-env.d.ts b/site/next-env.d.ts
index 40c3d68..c4b7818 100644
--- a/site/next-env.d.ts
+++ b/site/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/site/next.config.mjs b/site/next.config.mjs
index b7e850c..3b47c04 100644
--- a/site/next.config.mjs
+++ b/site/next.config.mjs
@@ -1,14 +1,10 @@
import createMDX from "@next/mdx";
-import remarkGfm from "remark-gfm";
-import rehypeSlug from "rehype-slug";
-import rehypeAutolinkHeadings from "rehype-autolink-headings";
-import rehypePrettyCode from "rehype-pretty-code";
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
reactStrictMode: true,
- swcMinify: true,
+ transpilePackages: ["logsdx"],
output: process.env.NODE_ENV === "production" ? "export" : undefined,
images: {
unoptimized: true,
@@ -18,31 +14,7 @@ const nextConfig = {
};
const withMDX = createMDX({
- options: {
- remarkPlugins: [remarkGfm],
- rehypePlugins: [
- rehypeSlug,
- [
- rehypePrettyCode,
- {
- theme: {
- dark: "github-dark-dimmed",
- light: "github-light",
- },
- keepBackground: false,
- },
- ],
- [
- rehypeAutolinkHeadings,
- {
- properties: {
- className: ["anchor"],
- ariaLabel: "Link to section",
- },
- },
- ],
- ],
- },
+ extension: /\.(md|mdx)$/,
});
export default withMDX(nextConfig);
diff --git a/site/package.json b/site/package.json
index bcd941f..1766cf0 100644
--- a/site/package.json
+++ b/site/package.json
@@ -6,12 +6,19 @@
"dev": "next dev -p 8573",
"build": "next build",
"start": "next start -p 8573",
- "lint": "oxlint"
+ "lint": "oxlint",
+ "test": "bun test",
+ "test:watch": "bun test --watch",
+ "test:ui": "bun test tests/components tests/stores tests/lib",
+ "test:e2e": "playwright test",
+ "test:e2e:ui": "playwright test --ui",
+ "test:e2e:headed": "playwright test --headed",
+ "playwright:install": "playwright install"
},
"dependencies": {
"@docsearch/css": "^3.9.0",
"@docsearch/react": "^3.9.0",
- "@next/mdx": "^15.5.2",
+ "@next/mdx": "16.0.7",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-icons": "^1.3.0",
@@ -19,6 +26,9 @@
"@radix-ui/react-tabs": "^1.1.13",
"@shikijs/rehype": "^3.12.2",
"@shikijs/transformers": "^3.12.2",
+ "@tanstack/react-query": "^5.90.9",
+ "@tanstack/react-store": "^0.8.0",
+ "@tanstack/store": "^0.8.0",
"ansi-to-html": "^0.7.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -27,10 +37,10 @@
"idb": "^8.0.3",
"logsdx": "*",
"lucide-react": "^0.400.0",
- "next": "^14.2.31",
+ "next": "16.0.7",
"next-themes": "^0.4.6",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "react": "19.2.1",
+ "react-dom": "19.2.1",
"react-hook-form": "^7.63.0",
"react-icons": "^5.5.0",
"react-wrap-balancer": "^1.1.1",
@@ -47,13 +57,20 @@
"shiki": "^3.12.2",
"tailwind-merge": "^2.5.2",
"unified": "^11.0.5",
- "zustand": "^5.0.8"
+ "use-debounce": "^10.0.6"
},
"devDependencies": {
+ "@happy-dom/global-registrator": "^20.0.10",
+ "@playwright/test": "^1.56.1",
+ "@tanstack/react-query-devtools": "^5.90.2",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.0",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
+ "@types/react": "19",
+ "@types/react-dom": "19",
"autoprefixer": "^10.4.19",
+ "happy-dom": "^20.0.10",
"oxlint": "^1.8.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.5",
diff --git a/site/playwright-report/index.html b/site/playwright-report/index.html
new file mode 100644
index 0000000..521fd5c
--- /dev/null
+++ b/site/playwright-report/index.html
@@ -0,0 +1,23363 @@
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
diff --git a/site/playwright.config.ts b/site/playwright.config.ts
new file mode 100644
index 0000000..be53371
--- /dev/null
+++ b/site/playwright.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig, devices } from "@playwright/test";
+
+export default defineConfig({
+ testDir: "./e2e",
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: "html",
+ use: {
+ baseURL: "http://localhost:8573",
+ trace: "on-first-retry",
+ },
+
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+ ],
+
+ webServer: {
+ command: "bun run dev",
+ url: "http://localhost:8573",
+ reuseExistingServer: true,
+ },
+});
diff --git a/site/stores/useThemeEditorStore.ts b/site/stores/useThemeEditorStore.ts
new file mode 100644
index 0000000..bfb730d
--- /dev/null
+++ b/site/stores/useThemeEditorStore.ts
@@ -0,0 +1,86 @@
+import { Store } from "@tanstack/store";
+import { useStore } from "@tanstack/react-store";
+import type { ThemeColors } from "@/components/themegenerator/types";
+import { DEFAULT_DARK_COLORS } from "@/components/themegenerator/constants";
+
+interface ThemeEditorState {
+ name: string;
+ colors: ThemeColors;
+ presets: string[];
+ processedLogs: string[];
+ isProcessing: boolean;
+}
+
+const initialState: ThemeEditorState = {
+ name: "my-custom-theme",
+ colors: DEFAULT_DARK_COLORS,
+ presets: ["logLevels", "numbers", "strings", "brackets"],
+ processedLogs: [],
+ isProcessing: false,
+};
+
+export const themeEditorStore = new Store(initialState);
+
+export const themeEditorActions = {
+ setName: (name: string) => {
+ themeEditorStore.setState((state) => ({
+ ...state,
+ name: name.toLowerCase().replace(/\s+/g, "-"),
+ }));
+ },
+
+ setColor: (key: keyof ThemeColors, value: string) => {
+ themeEditorStore.setState((state) => ({
+ ...state,
+ colors: { ...state.colors, [key]: value },
+ }));
+ },
+
+ togglePreset: (presetId: string) => {
+ themeEditorStore.setState((state) => {
+ const index = state.presets.indexOf(presetId);
+ const newPresets =
+ index > -1
+ ? state.presets.filter((_, i) => i !== index)
+ : [...state.presets, presetId];
+ return { ...state, presets: newPresets };
+ });
+ },
+
+ setProcessedLogs: (logs: string[]) => {
+ themeEditorStore.setState((state) => ({
+ ...state,
+ processedLogs: logs,
+ }));
+ },
+
+ setIsProcessing: (isProcessing: boolean) => {
+ themeEditorStore.setState((state) => ({
+ ...state,
+ isProcessing,
+ }));
+ },
+
+ reset: () => {
+ themeEditorStore.setState(() => initialState);
+ },
+
+ loadTheme: (name: string, colors: ThemeColors, presets: string[]) => {
+ themeEditorStore.setState((state) => ({
+ ...state,
+ name,
+ colors,
+ presets,
+ }));
+ },
+};
+
+export function useThemeEditorStore(
+ selector: (state: ThemeEditorState) => T,
+): T {
+ return useStore(themeEditorStore, selector);
+}
+
+export function useThemeEditorState(): ThemeEditorState {
+ return useStore(themeEditorStore);
+}
diff --git a/site/test-results/.last-run.json b/site/test-results/.last-run.json
new file mode 100644
index 0000000..f740f7c
--- /dev/null
+++ b/site/test-results/.last-run.json
@@ -0,0 +1,4 @@
+{
+ "status": "passed",
+ "failedTests": []
+}
diff --git a/site/test-setup.ts b/site/test-setup.ts
new file mode 100644
index 0000000..02b17e2
--- /dev/null
+++ b/site/test-setup.ts
@@ -0,0 +1,4 @@
+import { GlobalRegistrator } from "@happy-dom/global-registrator";
+import "@testing-library/jest-dom";
+
+GlobalRegistrator.register();
diff --git a/site/tests/__mocks__/logsdx.ts b/site/tests/__mocks__/logsdx.ts
new file mode 100644
index 0000000..7736f1f
--- /dev/null
+++ b/site/tests/__mocks__/logsdx.ts
@@ -0,0 +1,24 @@
+import { vi } from "bun:test";
+
+export const createSimpleTheme = vi.fn(
+ (name: string, colors: any, options?: any) => ({
+ name,
+ colors,
+ mode: options?.mode || "dark",
+ schema: {},
+ }),
+);
+
+export const registerTheme = vi.fn();
+
+export const getLogsDX = vi.fn().mockResolvedValue({
+ processLine: (line: string) => `${line}`,
+ processLines: (lines: string[]) =>
+ lines.map((line) => `${line}`),
+ setTheme: vi.fn(),
+ getCurrentTheme: vi.fn(),
+});
+
+export const getTheme = vi.fn();
+export const getAllThemes = vi.fn(() => ({}));
+export const getThemeNames = vi.fn(() => []);
diff --git a/site/tests/components/CustomThemeCreator.test.tsx b/site/tests/components/CustomThemeCreator.test.tsx
new file mode 100644
index 0000000..a164496
--- /dev/null
+++ b/site/tests/components/CustomThemeCreator.test.tsx
@@ -0,0 +1,261 @@
+import {
+ describe,
+ it,
+ expect,
+ vi,
+ beforeEach,
+ afterEach,
+ beforeAll,
+ mock,
+} from "bun:test";
+import React from "react";
+import {
+ render,
+ screen,
+ fireEvent,
+ waitFor,
+ cleanup,
+} from "../utils/test-utils";
+import {
+ themeEditorStore,
+ themeEditorActions,
+} from "@/stores/useThemeEditorStore";
+
+// Mock logsdx before any imports that use it
+mock.module("logsdx", () => ({
+ createSimpleTheme: vi.fn((name: string) => ({ name, schema: {} })),
+ registerTheme: vi.fn(),
+ getLogsDX: vi.fn().mockResolvedValue({
+ processLine: (line: string) => `${line}`,
+ }),
+}));
+
+// Mock the log preview hook to avoid actual LogsDX processing in tests
+mock.module("@/hooks/useLogPreview", () => ({
+ useLogPreview: () => ({
+ processedLogs: [
+ '[ERROR] Test error',
+ '[INFO] Test info',
+ ],
+ isProcessing: false,
+ }),
+}));
+
+// Mock theme creation hook
+mock.module("@/hooks/useThemes", () => ({
+ useCreateTheme: () => ({
+ mutate: vi.fn(),
+ isPending: false,
+ }),
+}));
+
+// Import after mocks - using dynamic import inside describe
+let CustomThemeCreator: React.ComponentType;
+
+describe("CustomThemeCreator - Integration Tests", () => {
+ beforeAll(async () => {
+ const module = await import(
+ "@/components/themegenerator/CustomThemeCreator"
+ );
+ CustomThemeCreator = module.CustomThemeCreator;
+ });
+ beforeEach(() => {
+ themeEditorActions.reset();
+ });
+
+ afterEach(cleanup);
+
+ it("renders all major sections", () => {
+ render();
+
+ expect(screen.getByText("Create Your Custom Theme")).toBeDefined();
+ expect(screen.getByText("Theme Basics")).toBeDefined();
+ expect(screen.getByText("Colors")).toBeDefined();
+ expect(screen.getByText("Pattern Presets")).toBeDefined();
+ expect(screen.getByText("Live Preview")).toBeDefined();
+ expect(screen.getByText("Export Theme")).toBeDefined();
+ });
+
+ it("allows editing theme name", () => {
+ render();
+
+ const nameInput = screen.getByPlaceholderText("my-awesome-theme");
+ fireEvent.change(nameInput, { target: { value: "Custom Theme" } });
+
+ // Should convert to kebab-case
+ expect((nameInput as HTMLInputElement).value).toBe("custom-theme");
+ });
+
+ it("updates store when theme name changes", () => {
+ render();
+
+ const nameInput = screen.getByPlaceholderText("my-awesome-theme");
+ fireEvent.change(nameInput, { target: { value: "New Theme" } });
+
+ const storeState = themeEditorStore.state;
+ expect(storeState.name).toBe("new-theme");
+ });
+
+ it("allows toggling presets", () => {
+ render();
+
+ const checkboxes = screen.getAllByRole("checkbox");
+ const firstCheckbox = checkboxes[0] as HTMLInputElement;
+
+ const initialState = firstCheckbox.checked;
+ fireEvent.click(firstCheckbox);
+
+ waitFor(() => {
+ expect(firstCheckbox.checked).toBe(!initialState);
+ });
+ });
+
+ it("updates colors via color picker", () => {
+ render();
+
+ // Find a color input (there should be multiple)
+ const colorInputs = screen.getAllByDisplayValue(/#[0-9a-f]{6}/i);
+ const primaryColorInput = colorInputs[0];
+
+ fireEvent.change(primaryColorInput, { target: { value: "#ff0000" } });
+
+ const storeState = themeEditorStore.state;
+ expect(storeState.colors.primary).toBe("#ff0000");
+ });
+
+ it("provides copy code functionality", async () => {
+ const mockWriteText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, "clipboard", {
+ value: {
+ writeText: mockWriteText,
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ render();
+
+ const copyButton = screen.getByRole("button", { name: /copy code/i });
+ fireEvent.click(copyButton);
+
+ await waitFor(() => {
+ expect(mockWriteText).toHaveBeenCalled();
+ expect(screen.getByText("Copied!")).toBeDefined();
+ });
+ });
+
+ it("provides download functionality", () => {
+ render();
+
+ const downloadButton = screen.getByRole("button", {
+ name: /download theme file/i,
+ });
+ expect(downloadButton).toBeDefined();
+ expect((downloadButton as HTMLButtonElement).disabled).toBe(false);
+ });
+
+ it("resets theme when reset button is clicked", () => {
+ render();
+
+ // First, change the theme
+ const nameInput = screen.getByPlaceholderText("my-awesome-theme");
+ fireEvent.change(nameInput, { target: { value: "modified" } });
+
+ expect(themeEditorStore.state.name).toBe("modified");
+
+ const resetButton = screen.getByRole("button", { name: /reset/i });
+ fireEvent.click(resetButton);
+
+ expect(themeEditorStore.state.name).toBe("my-custom-theme");
+ });
+
+ it("shows theme preview with processed logs", () => {
+ render();
+
+ expect(screen.getByText("Live Preview")).toBeDefined();
+ expect(screen.getByText("Powered by LogsDX")).toBeDefined();
+ });
+
+ it("provides save theme functionality", () => {
+ render();
+
+ const saveButton = screen.getByRole("button", { name: /save theme/i });
+ expect(saveButton).toBeDefined();
+
+ fireEvent.click(saveButton);
+ // Mutation should be called (mocked in this test)
+ });
+
+ it("provides share theme functionality", async () => {
+ const mockWriteText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, "clipboard", {
+ value: {
+ writeText: mockWriteText,
+ },
+ writable: true,
+ configurable: true,
+ });
+
+ render();
+
+ const shareButton = screen.getByRole("button", { name: /share theme/i });
+ fireEvent.click(shareButton);
+
+ await waitFor(() => {
+ expect(mockWriteText).toHaveBeenCalled();
+ });
+ });
+
+ it("toggles advanced code view", () => {
+ render();
+
+ const toggleButton = screen
+ .getByText("Generated Code")
+ .parentElement?.querySelector("button");
+
+ expect(toggleButton).toBeDefined();
+
+ // Should not show code initially
+ expect(screen.queryByText(/import.*createSimpleTheme/)).toBeNull();
+
+ fireEvent.click(toggleButton!);
+
+ // Should show code after clicking
+ waitFor(() => {
+ expect(screen.getByText(/import.*createSimpleTheme/)).toBeDefined();
+ });
+ });
+
+ it("maintains theme state across re-renders", () => {
+ const { rerender } = render();
+
+ const nameInput = screen.getByPlaceholderText("my-awesome-theme");
+ fireEvent.change(nameInput, { target: { value: "persistent" } });
+
+ rerender();
+
+ const updatedInput = screen.getByPlaceholderText("my-awesome-theme");
+ expect((updatedInput as HTMLInputElement).value).toBe("persistent");
+ });
+
+ it("displays all export options", () => {
+ render();
+
+ expect(screen.getByRole("button", { name: /copy code/i })).toBeDefined();
+ expect(
+ screen.getByRole("button", { name: /copy config json/i }),
+ ).toBeDefined();
+ expect(
+ screen.getByRole("button", { name: /download theme file/i }),
+ ).toBeDefined();
+ expect(screen.getByRole("button", { name: /save theme/i })).toBeDefined();
+ expect(screen.getByRole("button", { name: /share theme/i })).toBeDefined();
+ });
+
+ it("renders responsive layout", () => {
+ const { container } = render();
+
+ const gridLayout = container.querySelector(".lg\\:grid-cols-2");
+ expect(gridLayout).toBeDefined();
+ });
+});
diff --git a/site/tests/components/PresetSelector.test.tsx b/site/tests/components/PresetSelector.test.tsx
new file mode 100644
index 0000000..d397673
--- /dev/null
+++ b/site/tests/components/PresetSelector.test.tsx
@@ -0,0 +1,140 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "bun:test";
+import { render, screen, fireEvent, cleanup } from "../utils/test-utils";
+import { PresetSelector } from "@/components/themegenerator/PresetSelector";
+import { mockPresets } from "../utils/theme-mocks";
+
+describe("PresetSelector", () => {
+ let mockOnToggle: ReturnType;
+
+ beforeEach(() => {
+ mockOnToggle = vi.fn();
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ });
+
+ it("renders all preset options", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Pattern Presets")).toBeDefined();
+ expect(screen.getByText("Log Levels")).toBeDefined();
+ expect(screen.getByText("Numbers")).toBeDefined();
+ expect(screen.getByText("Strings")).toBeDefined();
+ });
+
+ it("displays preset descriptions", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("ERROR, WARN, INFO, DEBUG, SUCCESS")).toBeDefined();
+ expect(screen.getByText("Integers and decimal values")).toBeDefined();
+ expect(screen.getByText("Quoted text")).toBeDefined();
+ });
+
+ it("shows selected presets as checked", () => {
+ render(
+ ,
+ );
+
+ const checkboxes = screen.getAllByRole("checkbox") as HTMLInputElement[];
+
+ expect(checkboxes[0].checked).toBe(true); // logLevels
+ expect(checkboxes[1].checked).toBe(true); // numbers
+ expect(checkboxes[2].checked).toBe(false); // strings
+ });
+
+ it("calls onToggle when preset is clicked", () => {
+ render(
+ ,
+ );
+
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[0]);
+
+ expect(mockOnToggle).toHaveBeenCalledWith("logLevels");
+ });
+
+ it("toggles presets on and off", () => {
+ const { rerender } = render(
+ ,
+ );
+
+ const checkboxes = screen.getAllByRole("checkbox") as HTMLInputElement[];
+ fireEvent.click(checkboxes[0]);
+
+ expect(mockOnToggle).toHaveBeenCalledWith("logLevels");
+
+ // Simulate the preset being selected
+ rerender(
+ ,
+ );
+
+ const updatedCheckboxes = screen.getAllByRole(
+ "checkbox",
+ ) as HTMLInputElement[];
+ expect(updatedCheckboxes[0].checked).toBe(true);
+
+ // Click again to unselect
+ fireEvent.click(updatedCheckboxes[0]);
+ expect(mockOnToggle).toHaveBeenCalledWith("logLevels");
+ });
+
+ it("allows multiple presets to be selected", () => {
+ render(
+ ,
+ );
+
+ const checkboxes = screen.getAllByRole("checkbox") as HTMLInputElement[];
+ expect(checkboxes[0].checked).toBe(true);
+ expect(checkboxes[1].checked).toBe(true);
+ expect(checkboxes[2].checked).toBe(true);
+ });
+
+ it("renders clickable labels", () => {
+ render(
+ ,
+ );
+
+ const labels = screen.getAllByText("Log Levels");
+ const label = labels[0].closest("label");
+ expect(label).toBeDefined();
+ expect(label?.classList.contains("cursor-pointer")).toBe(true);
+ });
+});
diff --git a/site/tests/components/ThemeColorPicker.test.tsx b/site/tests/components/ThemeColorPicker.test.tsx
new file mode 100644
index 0000000..e45c22f
--- /dev/null
+++ b/site/tests/components/ThemeColorPicker.test.tsx
@@ -0,0 +1,133 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "bun:test";
+import { render, screen, fireEvent, cleanup } from "../utils/test-utils";
+import { ThemeColorPicker } from "@/components/themegenerator/ThemeColorPicker";
+import { mockColors } from "../utils/theme-mocks";
+
+describe("ThemeColorPicker", () => {
+ let mockOnColorChange: ReturnType;
+ let mockOnReset: ReturnType;
+
+ beforeEach(() => {
+ mockOnColorChange = vi.fn();
+ mockOnReset = vi.fn();
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+ });
+
+ it("renders all color inputs", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Colors")).toBeDefined();
+ expect(screen.getByText("primary")).toBeDefined();
+ expect(screen.getByText("secondary")).toBeDefined();
+ expect(screen.getByText("accent")).toBeDefined();
+ expect(screen.getByText("error")).toBeDefined();
+ expect(screen.getByText("warning")).toBeDefined();
+ });
+
+ it("displays current color values", () => {
+ render(
+ ,
+ );
+
+ const primaryInputs = screen.getAllByDisplayValue("#bd93f9");
+ expect(primaryInputs.length).toBeGreaterThan(0);
+
+ const errorInputs = screen.getAllByDisplayValue("#ff5555");
+ expect(errorInputs.length).toBeGreaterThan(0);
+ });
+
+ it("calls onColorChange when color is updated via text input", () => {
+ render(
+ ,
+ );
+
+ const inputs = screen.getAllByDisplayValue("#bd93f9");
+ const textInput = inputs.find(
+ (input) => (input as HTMLInputElement).type === "text",
+ );
+
+ expect(textInput).toBeDefined();
+ fireEvent.change(textInput!, { target: { value: "#ff0000" } });
+
+ expect(mockOnColorChange).toHaveBeenCalledWith("primary", "#ff0000");
+ });
+
+ it("calls onColorChange when color is updated via color picker", () => {
+ render(
+ ,
+ );
+
+ const colorPickers = screen.getAllByDisplayValue("#bd93f9");
+ const colorPickerInput = colorPickers[0]; // First one is the color input
+
+ fireEvent.change(colorPickerInput, { target: { value: "#00ff00" } });
+
+ expect(mockOnColorChange).toHaveBeenCalled();
+ });
+
+ it("calls onReset when reset button is clicked", () => {
+ render(
+ ,
+ );
+
+ const resetButton = screen.getByRole("button", { name: /reset/i });
+ fireEvent.click(resetButton);
+
+ expect(mockOnReset).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders color picker and text input for each color", () => {
+ render(
+ ,
+ );
+
+ const colorInputs = screen.getAllByDisplayValue("#bd93f9");
+ // Should have 2: one color picker, one text input
+ expect(colorInputs.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("allows editing all color properties", () => {
+ render(
+ ,
+ );
+
+ Object.entries(mockColors).forEach(([, value]) => {
+ const inputs = screen.getAllByDisplayValue(value);
+ expect(inputs.length).toBeGreaterThan(0);
+ });
+ });
+});
diff --git a/site/tests/components/ThemePreview.test.tsx b/site/tests/components/ThemePreview.test.tsx
new file mode 100644
index 0000000..b3c1e31
--- /dev/null
+++ b/site/tests/components/ThemePreview.test.tsx
@@ -0,0 +1,167 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { render, screen, cleanup } from "../utils/test-utils";
+import { ThemePreview } from "@/components/themegenerator/ThemePreview";
+import { mockColors } from "../utils/theme-mocks";
+
+describe("ThemePreview", () => {
+ beforeEach(() => {
+ // Clear any lingering DOM state
+ document.body.innerHTML = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+ it("renders preview header", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Live Preview")).toBeDefined();
+ expect(screen.getByText("Powered by LogsDX")).toBeDefined();
+ });
+
+ it("displays loading state when processing", () => {
+ render(
+ ,
+ );
+
+ const processingText = screen.getAllByText("Processing logs...");
+ expect(processingText.length).toBeGreaterThan(0);
+ });
+
+ it("displays empty state when no logs", () => {
+ render(
+ ,
+ );
+
+ const emptyText = screen.getAllByText("No logs to display");
+ expect(emptyText.length).toBeGreaterThan(0);
+ });
+
+ it("renders processed logs", () => {
+ const mockProcessedLogs = [
+ '[ERROR] Something failed',
+ '[INFO] Server started',
+ '[SUCCESS] Deploy complete',
+ ];
+
+ render(
+ ,
+ );
+
+ // The logs are rendered as HTML, so we check for the container
+ const headers = screen.getAllByText("Live Preview");
+ const logContainer = headers[0].parentElement?.parentElement;
+ expect(logContainer).toBeDefined();
+ });
+
+ it("applies theme colors to preview container", () => {
+ const { container } = render(
+ ,
+ );
+
+ const previewDiv = container.querySelector('[style*="background"]');
+ expect(previewDiv).toBeDefined();
+ });
+
+ it("renders duplicate logs for scrolling animation", () => {
+ const mockProcessedLogs = [
+ "Log 1",
+ "Log 2",
+ "Log 3",
+ ];
+
+ const { container } = render(
+ ,
+ );
+
+ // Should have 2 sets of logs for seamless scrolling
+ const logWrapper = container.querySelector(".log-scroll-wrapper");
+ expect(logWrapper).toBeDefined();
+
+ const logLines = container.querySelectorAll(".log-line");
+ // 3 logs × 2 sets = 6 total
+ expect(logLines.length).toBe(6);
+ });
+
+ it("pauses animation on hover", () => {
+ const { container } = render(
+ ,
+ );
+
+ const scrollWrapper = container.querySelector(".log-scroll-wrapper");
+ expect(scrollWrapper).toBeDefined();
+
+ // Check that the animation pause style is injected
+ const styleTag = container.querySelector("style");
+ expect(styleTag?.textContent).toContain("animation-play-state: paused");
+ });
+
+ it("shows correct state transitions", () => {
+ const { rerender, container } = render(
+ ,
+ );
+
+ const processingTexts = screen.getAllByText("Processing logs...");
+ expect(processingTexts.length).toBeGreaterThan(0);
+
+ rerender(
+ Log ready"]}
+ isProcessing={false}
+ colors={mockColors}
+ />,
+ );
+
+ // Should now show the log scroll wrapper with logs
+ const scrollWrapper = container.querySelector(".log-scroll-wrapper");
+ expect(scrollWrapper).toBeDefined();
+ expect(screen.queryAllByText("Processing logs...").length).toBe(0);
+ });
+
+ it("applies monospace font to preview", () => {
+ const { container } = render(
+ ,
+ );
+
+ const previewContainer = container.querySelector(".font-mono");
+ expect(previewContainer).toBeDefined();
+ });
+});
diff --git a/site/tests/lib/themeUtils.test.ts b/site/tests/lib/themeUtils.test.ts
new file mode 100644
index 0000000..50fc2f7
--- /dev/null
+++ b/site/tests/lib/themeUtils.test.ts
@@ -0,0 +1,165 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import {
+ exportThemeToShareCode,
+ importThemeFromShareCode,
+ generateShareUrl,
+ generateThemeCode,
+} from "@/lib/themeUtils";
+import { mockColors } from "../utils/theme-mocks";
+
+describe("themeUtils", () => {
+ beforeEach(() => {
+ // Clear any global state
+ });
+
+ afterEach(() => {
+ // Cleanup
+ });
+ describe("exportThemeToShareCode", () => {
+ it("exports theme to base64 encoded string", () => {
+ const shareCode = exportThemeToShareCode("test-theme", mockColors, [
+ "logLevels",
+ "numbers",
+ ]);
+
+ expect(typeof shareCode).toBe("string");
+ expect(shareCode.length).toBeGreaterThan(0);
+ });
+
+ it("creates valid base64", () => {
+ const shareCode = exportThemeToShareCode("test-theme", mockColors, [
+ "logLevels",
+ ]);
+
+ // Should be decodeable
+ const decoded = atob(shareCode);
+ expect(() => JSON.parse(decoded)).not.toThrow();
+ });
+
+ it("includes theme data in export", () => {
+ const shareCode = exportThemeToShareCode("my-theme", mockColors, [
+ "logLevels",
+ "numbers",
+ ]);
+
+ const decoded = JSON.parse(atob(shareCode));
+
+ expect(decoded.name).toBe("my-theme");
+ expect(decoded.colors).toEqual(mockColors);
+ expect(decoded.presets).toEqual(["logLevels", "numbers"]);
+ expect(decoded.version).toBe("1.0.0");
+ });
+ });
+
+ describe("importThemeFromShareCode", () => {
+ it("imports valid theme from share code", () => {
+ const shareCode = exportThemeToShareCode("imported-theme", mockColors, [
+ "strings",
+ "brackets",
+ ]);
+
+ const imported = importThemeFromShareCode(shareCode);
+
+ expect(imported).not.toBeNull();
+ expect(imported?.name).toBe("imported-theme");
+ expect(imported?.colors).toEqual(mockColors);
+ expect(imported?.presets).toEqual(["strings", "brackets"]);
+ });
+
+ it("returns null for invalid share code", () => {
+ const invalid = "!!!invalid!!!";
+ const result = importThemeFromShareCode(invalid);
+
+ expect(result).toBeNull();
+ });
+
+ it("handles missing data gracefully", () => {
+ const partialData = btoa(JSON.stringify({}));
+ const result = importThemeFromShareCode(partialData);
+
+ expect(result?.name).toBe("imported-theme");
+ expect(result?.colors).toBeDefined();
+ expect(result?.presets).toEqual([]);
+ });
+
+ it("roundtrips theme data correctly", () => {
+ const original = {
+ name: "roundtrip-theme",
+ colors: mockColors,
+ presets: ["logLevels", "numbers", "strings"],
+ };
+
+ const shareCode = exportThemeToShareCode(
+ original.name,
+ original.colors,
+ original.presets,
+ );
+
+ const imported = importThemeFromShareCode(shareCode);
+
+ expect(imported?.name).toBe(original.name);
+ expect(imported?.colors).toEqual(original.colors);
+ expect(imported?.presets).toEqual(original.presets);
+ });
+ });
+
+ describe("generateShareUrl", () => {
+ it("generates URL with share code", () => {
+ const shareCode = "test-share-code-123";
+ const url = generateShareUrl(shareCode);
+
+ expect(url).toContain("/theme/");
+ expect(url).toContain(shareCode);
+ });
+
+ it("includes origin in URL", () => {
+ const shareCode = "abc123";
+ const url = generateShareUrl(shareCode);
+
+ // In test environment, window.location.origin might be undefined
+ expect(url).toContain("/theme/");
+ expect(url).toContain(shareCode);
+ });
+ });
+
+ describe("generateThemeCode", () => {
+ it("generates valid JavaScript code", () => {
+ const code = generateThemeCode("my-theme", mockColors, ["logLevels"]);
+
+ expect(code).toContain("import");
+ expect(code).toContain("createSimpleTheme");
+ expect(code).toContain("registerTheme");
+ expect(code).toContain("my-theme");
+ });
+
+ it("converts theme name to valid variable name", () => {
+ const code = generateThemeCode("my-awesome-theme", mockColors, []);
+
+ expect(code).toContain("my_awesome_themeTheme");
+ });
+
+ it("includes color definitions", () => {
+ const code = generateThemeCode("test", mockColors, []);
+
+ expect(code).toContain(mockColors.primary);
+ expect(code).toContain(mockColors.error);
+ expect(code).toContain(mockColors.success);
+ });
+
+ it("includes preset configuration", () => {
+ const code = generateThemeCode("test", mockColors, [
+ "logLevels",
+ "numbers",
+ ]);
+
+ expect(code).toContain("logLevels");
+ expect(code).toContain("numbers");
+ });
+
+ it("generates runnable export", () => {
+ const code = generateThemeCode("theme", mockColors, ["logLevels"]);
+
+ expect(code).toContain("export default");
+ });
+ });
+});
diff --git a/site/tests/stores/useThemeEditorStore.test.ts b/site/tests/stores/useThemeEditorStore.test.ts
new file mode 100644
index 0000000..8e2ea42
--- /dev/null
+++ b/site/tests/stores/useThemeEditorStore.test.ts
@@ -0,0 +1,143 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import {
+ themeEditorStore,
+ themeEditorActions,
+} from "@/stores/useThemeEditorStore";
+import { DEFAULT_DARK_COLORS } from "@/components/themegenerator/constants";
+
+describe("useThemeEditorStore", () => {
+ beforeEach(() => {
+ themeEditorActions.reset();
+ });
+
+ afterEach(() => {
+ themeEditorActions.reset();
+ });
+
+ it("initializes with default state", () => {
+ const state = themeEditorStore.state;
+
+ expect(state.name).toBe("my-custom-theme");
+ expect(state.colors).toEqual(DEFAULT_DARK_COLORS);
+ expect(state.presets).toEqual([
+ "logLevels",
+ "numbers",
+ "strings",
+ "brackets",
+ ]);
+ expect(state.processedLogs).toEqual([]);
+ expect(state.isProcessing).toBe(false);
+ });
+
+ it("updates theme name", () => {
+ themeEditorActions.setName("New Theme Name");
+
+ const state = themeEditorStore.state;
+ expect(state.name).toBe("new-theme-name");
+ });
+
+ it("converts theme name to kebab-case", () => {
+ themeEditorActions.setName("My Awesome Theme");
+
+ const state = themeEditorStore.state;
+ expect(state.name).toBe("my-awesome-theme");
+ });
+
+ it("updates individual colors", () => {
+ themeEditorActions.setColor("primary", "#ff0000");
+
+ const state = themeEditorStore.state;
+ expect(state.colors.primary).toBe("#ff0000");
+ expect(state.colors.secondary).toBe(DEFAULT_DARK_COLORS.secondary);
+ });
+
+ it("toggles presets on", () => {
+ themeEditorActions.reset();
+
+ themeEditorActions.togglePreset("numbers");
+ expect(themeEditorStore.state.presets).not.toContain("numbers");
+
+ themeEditorActions.togglePreset("numbers");
+ expect(themeEditorStore.state.presets).toContain("numbers");
+ });
+
+ it("toggles presets off", () => {
+ themeEditorActions.togglePreset("logLevels");
+
+ const state = themeEditorStore.state;
+ expect(state.presets).not.toContain("logLevels");
+ });
+
+ it("handles multiple preset toggles", () => {
+ themeEditorActions.togglePreset("logLevels");
+ themeEditorActions.togglePreset("numbers");
+ themeEditorActions.togglePreset("strings");
+
+ const state = themeEditorStore.state;
+ expect(state.presets).toEqual(["brackets"]);
+ });
+
+ it("updates processed logs", () => {
+ const mockLogs = ["Log 1", "Log 2"];
+ themeEditorActions.setProcessedLogs(mockLogs);
+
+ const state = themeEditorStore.state;
+ expect(state.processedLogs).toEqual(mockLogs);
+ });
+
+ it("updates processing state", () => {
+ themeEditorActions.setIsProcessing(true);
+ expect(themeEditorStore.state.isProcessing).toBe(true);
+
+ themeEditorActions.setIsProcessing(false);
+ expect(themeEditorStore.state.isProcessing).toBe(false);
+ });
+
+ it("resets to initial state", () => {
+ themeEditorActions.setName("modified");
+ themeEditorActions.setColor("primary", "#ff0000");
+ themeEditorActions.togglePreset("logLevels");
+
+ let state = themeEditorStore.state;
+ expect(state.name).toBe("modified");
+ expect(state.colors.primary).toBe("#ff0000");
+ expect(state.presets).not.toContain("logLevels");
+
+ themeEditorActions.reset();
+
+ state = themeEditorStore.state;
+ expect(state.name).toBe("my-custom-theme");
+ expect(state.colors).toEqual(DEFAULT_DARK_COLORS);
+ expect(state.presets).toEqual([
+ "logLevels",
+ "numbers",
+ "strings",
+ "brackets",
+ ]);
+ });
+
+ it("loads theme from parameters", () => {
+ const customColors = {
+ ...DEFAULT_DARK_COLORS,
+ primary: "#custom",
+ };
+
+ themeEditorActions.loadTheme("loaded-theme", customColors, ["logLevels"]);
+
+ const state = themeEditorStore.state;
+ expect(state.name).toBe("loaded-theme");
+ expect(state.colors.primary).toBe("#custom");
+ expect(state.presets).toEqual(["logLevels"]);
+ });
+
+ it("maintains immutability", () => {
+ const initialColors = themeEditorStore.state.colors;
+
+ themeEditorActions.setColor("primary", "#new-color");
+
+ const updatedColors = themeEditorStore.state.colors;
+
+ expect(initialColors).not.toBe(updatedColors);
+ expect(initialColors.primary).not.toBe(updatedColors.primary);
+ });
+});
diff --git a/site/tests/utils/test-utils.tsx b/site/tests/utils/test-utils.tsx
new file mode 100644
index 0000000..290dbe1
--- /dev/null
+++ b/site/tests/utils/test-utils.tsx
@@ -0,0 +1,44 @@
+import { render, RenderOptions, cleanup } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ReactElement } from "react";
+import { afterEach, beforeEach } from "bun:test";
+
+function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ staleTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ });
+}
+
+interface AllTheProvidersProps {
+ children: React.ReactNode;
+}
+
+function AllTheProviders({ children }: AllTheProvidersProps) {
+ const queryClient = createTestQueryClient();
+
+ return (
+ {children}
+ );
+}
+
+function customRender(
+ ui: ReactElement,
+ options?: Omit,
+) {
+ return render(ui, { wrapper: AllTheProviders, ...options });
+}
+
+afterEach(cleanup);
+beforeEach(cleanup);
+
+export * from "@testing-library/react";
+export { customRender as render };
diff --git a/site/tests/utils/theme-mocks.ts b/site/tests/utils/theme-mocks.ts
new file mode 100644
index 0000000..fadbd64
--- /dev/null
+++ b/site/tests/utils/theme-mocks.ts
@@ -0,0 +1,34 @@
+import type { ThemeColors } from "@/components/themegenerator/types";
+
+export const mockColors: ThemeColors = {
+ primary: "#bd93f9",
+ secondary: "#ff79c6",
+ accent: "#8be9fd",
+ error: "#ff5555",
+ warning: "#ffb86c",
+ info: "#8be9fd",
+ success: "#50fa7b",
+ debug: "#6272a4",
+ text: "#f8f8f2",
+ background: "#282a36",
+ muted: "#6272a4",
+ highlight: "#f1fa8c",
+};
+
+export const mockPresets = [
+ {
+ id: "logLevels",
+ label: "Log Levels",
+ description: "ERROR, WARN, INFO, DEBUG, SUCCESS",
+ },
+ {
+ id: "numbers",
+ label: "Numbers",
+ description: "Integers and decimal values",
+ },
+ {
+ id: "strings",
+ label: "Strings",
+ description: "Quoted text",
+ },
+];
diff --git a/site/tsconfig.json b/site/tsconfig.json
index 1592f63..40c9d0a 100644
--- a/site/tsconfig.json
+++ b/site/tsconfig.json
@@ -12,7 +12,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
- "jsx": "preserve",
+ "jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
@@ -23,6 +23,12 @@
}
]
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
"exclude": ["node_modules"]
}
diff --git a/site/types/logsdx.ts b/site/types/logsdx.ts
new file mode 100644
index 0000000..c628f46
--- /dev/null
+++ b/site/types/logsdx.ts
@@ -0,0 +1,69 @@
+export interface Theme {
+ name: string;
+ description?: string;
+ mode?: "light" | "dark" | "auto";
+ schema: SchemaConfig;
+ colors?: Record;
+}
+
+export interface SchemaConfig {
+ defaultStyle?: StyleOptions;
+ matchWords?: Record;
+ matchStartsWith?: Record;
+ matchEndsWith?: Record;
+ matchContains?: Record;
+ matchPatterns?: PatternMatch[];
+ whiteSpace?: "preserve" | "trim";
+ newLine?: "preserve" | "trim";
+}
+
+export interface StyleOptions {
+ color: string;
+ styleCodes?: StyleCode[];
+ htmlStyleFormat?: "css" | "className";
+}
+
+export interface PatternMatch {
+ name: string;
+ pattern: string | RegExp;
+ identifier?: string;
+ options: StyleOptions;
+}
+
+export type StyleCode =
+ | "bold"
+ | "italic"
+ | "underline"
+ | "dim"
+ | "blink"
+ | "reverse"
+ | "strikethrough";
+
+export interface LogsDXOptions {
+ theme?: string | Theme | ThemePair;
+ outputFormat?: "ansi" | "html";
+ htmlStyleFormat?: "css" | "className";
+ escapeHtml?: boolean;
+ debug?: boolean;
+ customRules?: Record;
+ autoAdjustTerminal?: boolean;
+}
+
+export interface ThemePair {
+ light: string | Theme;
+ dark: string | Theme;
+}
+
+export interface LogsDXInstance {
+ processLine(line: string): string;
+ processLines(lines: string[]): string[];
+ processLog(logContent: string): string;
+ setTheme(theme: string | Theme | ThemePair): Promise;
+ getCurrentTheme(): Theme;
+ getAllThemes(): Record;
+ getThemeNames(): string[];
+ setOutputFormat(format: "ansi" | "html"): void;
+ setHtmlStyleFormat(format: "css" | "className"): void;
+ getCurrentOutputFormat(): "ansi" | "html";
+ getCurrentHtmlStyleFormat(): "css" | "className";
+}
diff --git a/src/cli/bin.ts b/src/cli/bin.ts
index 5988340..b5f37a4 100644
--- a/src/cli/bin.ts
+++ b/src/cli/bin.ts
@@ -29,9 +29,8 @@ program
"[input]",
"Input file (optional, reads from stdin if not provided)",
)
- .action(
- async (input: string | undefined, options: any) =>
- await main(input, options as CommanderOptions),
+ .action(async (input: string | undefined, options: unknown) =>
+ main(input, options as CommanderOptions),
);
program.parse();
diff --git a/src/cli/commands.ts b/src/cli/commands.ts
index 71243bd..510a8cb 100644
--- a/src/cli/commands.ts
+++ b/src/cli/commands.ts
@@ -1,4 +1,3 @@
-import { createCLI, CLI } from "./parser";
import { input, select, checkbox, confirm } from "../utils/prompts";
import spinner from "../utils/spinner";
import * as colorUtil from "../utils/colors";
@@ -12,7 +11,7 @@ import {
adjustThemeForAccessibility,
SimpleThemeConfig,
} from "../themes/builder";
-import { registerTheme, getTheme, getThemeNames } from "../themes";
+import { registerTheme, getTheme } from "../themes";
import { getLogsDX } from "../index";
import { Theme } from "../types";
@@ -73,7 +72,7 @@ const COLOR_PRESETS = {
function showBanner() {
console.clear();
- const grad = gradient(["#00ffff", "#ff00ff", "#ffff00"]);
+ const grad = gradient();
console.log(
grad.multiline(`
╦ ┌─┐┌─┐┌─┐╔╦╗═╗ ╦
@@ -84,8 +83,8 @@ function showBanner() {
console.log(colorUtil.dim(" Theme Creator v1.0.0\n"));
}
-function renderPreview(theme: Theme, title: string = "Theme Preview") {
- const logsDX = getLogsDX({ theme, outputFormat: "ansi" });
+async function renderPreview(theme: Theme, title: string = "Theme Preview") {
+ const logsDX = await getLogsDX({ theme, outputFormat: "ansi" });
const previewBox = boxen(
SAMPLE_LOGS.map((log) => logsDX.processLine(log)).join("\n"),
{
@@ -98,17 +97,23 @@ function renderPreview(theme: Theme, title: string = "Theme Preview") {
console.log(previewBox);
}
-async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) {
+export async function createInteractiveTheme(
+ options: { skipIntro?: boolean } = {},
+) {
if (!options.skipIntro) {
showBanner();
}
const name = await input({
message: "Theme name:",
- validate: (inputValue: string) => {
+ validate: async (inputValue: string) => {
if (!inputValue.trim()) return "Theme name is required";
- if (getTheme(inputValue)) return "A theme with this name already exists";
- return true;
+ try {
+ await getTheme(inputValue);
+ return "A theme with this name already exists";
+ } catch {
+ return true;
+ }
},
transformer: (inputValue: string) =>
inputValue.trim().toLowerCase().replace(/\s+/g, "-"),
@@ -245,7 +250,7 @@ async function createInteractiveTheme(options: { skipIntro?: boolean } = {}) {
createSpinner.succeed("Theme created!");
console.log("\n");
- renderPreview(theme, `✨ ${theme.name} Preview`);
+ await renderPreview(theme, `✨ ${theme.name} Preview`);
const checkAccessibility = await confirm({
message: "Check accessibility compliance?",
diff --git a/src/cli/index.ts b/src/cli/index.ts
index 7e6489a..c948e50 100755
--- a/src/cli/index.ts
+++ b/src/cli/index.ts
@@ -7,7 +7,6 @@ import {
cliOptionsSchema,
} from "./types";
import type { LogsDXOptions } from "../types";
-import { version } from "../../package.json";
import { ui } from "./ui";
import type { InteractiveConfig } from "./interactive";
import {
@@ -274,7 +273,7 @@ export async function main(
const outputFormat =
options.format || (options.output?.endsWith(".html") ? "html" : "ansi");
- const logsDX = LogsDX.getInstance({
+ const logsDX = await LogsDX.getInstance({
theme: options.theme || config.theme,
debug: options.debug || config.debug,
customRules: config.customRules,
@@ -284,7 +283,7 @@ export async function main(
if (options.listThemes) {
if (options.preview) {
const { showThemeList } = await import("./interactive");
- showThemeList();
+ await showThemeList();
} else if (!options.quiet) {
ui.showInfo("Available themes:");
getThemeNames().forEach((theme) => {
diff --git a/src/cli/interactive.ts b/src/cli/interactive.ts
index b28de4f..35871d6 100644
--- a/src/cli/interactive.ts
+++ b/src/cli/interactive.ts
@@ -40,11 +40,14 @@ export async function runInteractiveMode(): Promise {
);
const themeNames = getThemeNames();
- const themeChoices: ThemeChoice[] = themeNames.map((name: string) => ({
- name: chalk.cyan(name),
- value: name,
- description: getTheme(name)?.description || "No description available",
- }));
+ const themeChoices: ThemeChoice[] = await Promise.all(
+ themeNames.map(async (name: string) => ({
+ name: chalk.cyan(name),
+ value: name,
+ description:
+ (await getTheme(name))?.description || "No description available",
+ })),
+ );
const selectedTheme = await select({
message: "🎨 Choose a theme:",
@@ -64,7 +67,7 @@ export async function runInteractiveMode(): Promise {
ui.showInfo("Theme Previews:\n");
for (const themeName of themeNames) {
- const logsDX = LogsDX.getInstance({
+ const logsDX = await LogsDX.getInstance({
theme: themeName,
outputFormat: "ansi",
});
@@ -101,9 +104,9 @@ export async function runInteractiveMode(): Promise {
if (wantPreview) {
console.log("\n" + chalk.bold("🎬 Preview with your selected settings:"));
- const logsDX = LogsDX.getInstance({
+ const logsDX = await LogsDX.getInstance({
theme: finalTheme,
- outputFormat,
+ outputFormat: outputFormat as "ansi" | "html",
});
const styledSample = logsDX.processLog(SAMPLE_LOG);
ui.showThemePreview(
@@ -142,17 +145,18 @@ export async function selectThemeInteractively(): Promise {
});
}
-export function showThemeList(): void {
+export async function showThemeList(): Promise {
ui.showInfo("Available Themes:\n");
const themeNames = getThemeNames();
- const logsDX = LogsDX.getInstance({
+ const logsDX = await LogsDX.getInstance({
theme: themeNames[0],
outputFormat: "ansi",
});
- themeNames.forEach((themeName: string, index: number) => {
- const theme = getTheme(themeName);
+ for (const themeName of themeNames) {
+ const theme = await getTheme(themeName);
+ const index = themeNames.indexOf(themeName);
const sample = `${index + 1}. ${themeName}`;
const styledSample = logsDX.processLine(
`INFO Sample log with ${themeName} theme - GET /api/test 200 OK`,
@@ -163,7 +167,7 @@ export function showThemeList(): void {
console.log(chalk.dim(` ${theme.description}`));
}
console.log(` ${styledSample}`);
- });
+ }
console.log(
chalk.yellow("\n💡 Use --interactive for guided theme selection"),
diff --git a/src/cli/stream.ts b/src/cli/stream.ts
new file mode 100644
index 0000000..d2358cb
--- /dev/null
+++ b/src/cli/stream.ts
@@ -0,0 +1,131 @@
+import { createReadStream } from "fs";
+import { createInterface } from "readline";
+import type { LogsDX } from "../index";
+
+export interface StreamOptions {
+ quiet?: boolean;
+ output?: string;
+ onLine?: (processedLine: string) => void;
+ onComplete?: () => void;
+ onError?: (error: Error) => void;
+}
+
+export async function processFileStream(
+ filePath: string,
+ logsDX: LogsDX,
+ options: StreamOptions = {},
+): Promise {
+ return new Promise((resolve, reject) => {
+ const fileStream = createReadStream(filePath, {
+ encoding: "utf8",
+ highWaterMark: 64 * 1024, // 64KB chunks
+ });
+
+ const rl = createInterface({
+ input: fileStream,
+ crlfDelay: Infinity,
+ });
+
+ const outputLines: string[] = [];
+
+ rl.on("line", (line) => {
+ try {
+ const processedLine = logsDX.processLine(line);
+
+ if (options.output) {
+ outputLines.push(processedLine);
+ } else if (!options.quiet) {
+ console.log(processedLine);
+ }
+
+ if (options.onLine) {
+ options.onLine(processedLine);
+ }
+ } catch (error) {
+ const err = error instanceof Error ? error : new Error(String(error));
+ if (options.onError) {
+ options.onError(err);
+ }
+ }
+ });
+
+ rl.on("close", () => {
+ if (options.output && outputLines.length > 0) {
+ const fs = require("fs");
+ fs.writeFileSync(options.output, outputLines.join("\n"));
+ }
+
+ if (options.onComplete) {
+ options.onComplete();
+ }
+
+ resolve();
+ });
+
+ rl.on("error", (error) => {
+ if (options.onError) {
+ options.onError(error);
+ }
+ reject(error);
+ });
+
+ fileStream.on("error", (error) => {
+ if (options.onError) {
+ options.onError(error);
+ }
+ reject(error);
+ });
+ });
+}
+
+export async function processStdinStream(
+ logsDX: LogsDX,
+ options: StreamOptions = {},
+): Promise {
+ return new Promise((resolve) => {
+ process.stdin.setEncoding("utf8");
+
+ const rl = createInterface({
+ input: process.stdin,
+ crlfDelay: Infinity,
+ });
+
+ const outputLines: string[] = [];
+
+ rl.on("line", (line) => {
+ try {
+ if (line.trim()) {
+ const processedLine = logsDX.processLine(line);
+
+ if (options.output) {
+ outputLines.push(processedLine);
+ } else if (!options.quiet) {
+ console.log(processedLine);
+ }
+
+ if (options.onLine) {
+ options.onLine(processedLine);
+ }
+ }
+ } catch (error) {
+ const err = error instanceof Error ? error : new Error(String(error));
+ if (options.onError) {
+ options.onError(err);
+ }
+ }
+ });
+
+ rl.on("close", () => {
+ if (options.output && outputLines.length > 0) {
+ const fs = require("fs");
+ fs.writeFileSync(options.output, outputLines.join("\n"));
+ }
+
+ if (options.onComplete) {
+ options.onComplete();
+ }
+
+ resolve();
+ });
+ });
+}
diff --git a/src/cli/theme-gen.ts b/src/cli/theme-gen.ts
index 7d2b5fb..2eb8fba 100644
--- a/src/cli/theme-gen.ts
+++ b/src/cli/theme-gen.ts
@@ -208,7 +208,15 @@ async function collectCustomPatterns(): Promise<
patterns.push({
name,
pattern,
- colorRole,
+ colorRole: colorRole as
+ | "primary"
+ | "secondary"
+ | "error"
+ | "warning"
+ | "info"
+ | "success"
+ | "muted"
+ | "accent",
styleCodes: styleCodes.length > 0 ? styleCodes : undefined,
});
@@ -259,7 +267,15 @@ async function collectCustomWords(): Promise<
});
words[word] = {
- colorRole,
+ colorRole: colorRole as
+ | "primary"
+ | "secondary"
+ | "error"
+ | "warning"
+ | "info"
+ | "success"
+ | "muted"
+ | "accent",
styleCodes: styleCodes.length > 0 ? styleCodes : undefined,
};
@@ -288,7 +304,7 @@ async function showThemePreview(theme: Theme, palette: ColorPalette) {
];
registerTheme(theme);
- const logsDX = LogsDX.getInstance({
+ const logsDX = await LogsDX.getInstance({
theme: theme.name,
outputFormat: "ansi",
});
@@ -565,7 +581,7 @@ export async function exportTheme(themeName?: string): Promise {
})),
}));
- const theme = getTheme(themeToExport);
+ const theme = await getTheme(themeToExport);
if (!theme) {
ui.showError(`Theme "${themeToExport}" not found`);
return;
@@ -703,7 +719,7 @@ export async function importTheme(filename?: string): Promise {
console.log(`Description: ${validatedTheme.description}`);
}
- const existingTheme = getTheme(validatedTheme.name);
+ const existingTheme = await getTheme(validatedTheme.name);
if (existingTheme) {
const shouldOverwrite = await confirm({
message: `Theme "${validatedTheme.name}" already exists. Overwrite?`,
@@ -779,7 +795,7 @@ async function previewImportedTheme(theme: Theme) {
];
registerTheme(theme);
- const logsDX = LogsDX.getInstance({
+ const logsDX = await LogsDX.getInstance({
theme: theme.name,
outputFormat: "ansi",
});
@@ -793,9 +809,10 @@ async function previewImportedTheme(theme: Theme) {
if (theme.description) {
console.log(` Description: ${theme.description}`);
}
- if ("exportedAt" in theme && (theme as any).exportedAt) {
+ const exportedTheme = theme as Theme & { exportedAt?: string };
+ if (exportedTheme.exportedAt) {
console.log(
- ` Exported: ${chalk.dim(new Date((theme as any).exportedAt).toLocaleString())}`,
+ ` Exported: ${chalk.dim(new Date(exportedTheme.exportedAt).toLocaleString())}`,
);
}
diff --git a/src/cli/ui.ts b/src/cli/ui.ts
index eaa65a8..389e00a 100644
--- a/src/cli/ui.ts
+++ b/src/cli/ui.ts
@@ -40,18 +40,9 @@ export class CliUI {
}
showHeader() {
- const title = ascii.textSync("LogsDX", {
- font: "Small",
- horizontalLayout: "default",
- verticalLayout: "default",
- });
+ const title = ascii.textSync("LogsDX");
- const gradientTitle = gradient([
- "#42d392",
- "#647eff",
- "#A463BF",
- "#bf6399",
- ])(title);
+ const gradientTitle = gradient()(title);
console.log(
boxen(gradientTitle, {
diff --git a/src/index.ts b/src/index.ts
index c8c592a..60f78f5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,13 +1,17 @@
import { renderLine } from "./renderer";
import {
getTheme,
+ getThemeAsync,
getAllThemes,
getThemeNames,
+ preloadTheme,
+ preloadAllThemes,
+ registerTheme,
+ registerThemeLoader,
ThemeBuilder,
createTheme,
createSimpleTheme,
extendTheme,
- registerTheme,
THEME_PRESETS,
} from "./themes";
import { validateTheme, validateThemeSafe } from "./schema/validator";
@@ -33,8 +37,22 @@ import {
getRecommendedThemeMode,
} from "./renderer";
+/**
+ * LogsDX - A powerful log processing and styling tool
+ *
+ * This class provides a singleton instance for processing and styling log files
+ * with customizable themes and output formats.
+ *
+ * @example
+ * ```typescript
+ * const logsdx = LogsDX.getInstance({ theme: 'dracula' });
+ * const styledLog = logsdx.processLine('[INFO] Application started');
+ * console.log(styledLog);
+ * ```
+ */
export class LogsDX {
private static instance: LogsDX | null = null;
+ private static instancePromise: Promise | null = null;
private options: Required;
private currentTheme: Theme = {
name: "none",
@@ -50,7 +68,7 @@ export class LogsDX {
},
};
- private constructor(options = {}) {
+ private constructor(options = {}, theme: Theme) {
this.options = {
theme: "none",
outputFormat: "ansi",
@@ -62,10 +80,12 @@ export class LogsDX {
...options,
};
- this.currentTheme = this.resolveTheme(this.options.theme);
+ this.currentTheme = theme;
}
- private resolveTheme(theme: string | Theme | ThemePair | undefined): Theme {
+ private async resolveTheme(
+ theme: string | Theme | ThemePair | undefined,
+ ): Promise {
if (!theme || theme === "none") {
return {
name: "none",
@@ -83,7 +103,7 @@ export class LogsDX {
}
if (typeof theme === "string") {
- const baseTheme = getTheme(theme);
+ const baseTheme = await getTheme(theme);
if (
this.options.outputFormat === "ansi" &&
@@ -125,14 +145,14 @@ export class LogsDX {
recommendedMode === "light" ? themePair.light : themePair.dark;
if (typeof selectedTheme === "string") {
- return getTheme(selectedTheme);
+ return await getTheme(selectedTheme);
} else {
return selectedTheme;
}
} else {
const selectedTheme = themePair.dark;
if (typeof selectedTheme === "string") {
- return getTheme(selectedTheme);
+ return await getTheme(selectedTheme);
} else {
return selectedTheme;
}
@@ -162,28 +182,88 @@ export class LogsDX {
}
}
- static getInstance(options: LogsDXOptions = {}): LogsDX {
- if (!LogsDX.instance) {
- LogsDX.instance = new LogsDX(options);
- } else if (Object.keys(options).length > 0) {
- LogsDX.instance.options = {
- ...LogsDX.instance.options,
- ...options,
- };
+ /**
+ * Get or create the singleton LogsDX instance
+ *
+ * @param options - Configuration options for LogsDX
+ * @param options.theme - Theme name, Theme object, or ThemePair to use
+ * @param options.outputFormat - Output format: 'ansi' (default) or 'html'
+ * @param options.htmlStyleFormat - HTML style format: 'css' (inline styles) or 'className'
+ * @param options.escapeHtml - Whether to escape HTML in output (default: true)
+ * @param options.debug - Enable debug logging (default: false)
+ * @param options.autoAdjustTerminal - Auto-adjust theme based on terminal background (default: true)
+ * @returns The LogsDX singleton instance
+ *
+ * @example
+ * ```typescript
+ * const logsdx = await LogsDX.getInstance({ theme: 'nord', outputFormat: 'ansi' });
+ * ```
+ */
+ static async getInstance(options: LogsDXOptions = {}): Promise {
+ if (LogsDX.instancePromise) {
+ const instance = await LogsDX.instancePromise;
+ if (Object.keys(options).length > 0) {
+ instance.options = {
+ ...instance.options,
+ ...options,
+ };
- if (options.theme) {
- LogsDX.instance.currentTheme = LogsDX.instance.resolveTheme(
- options.theme,
- );
+ if (options.theme) {
+ instance.currentTheme = await instance.resolveTheme(options.theme);
+ }
}
+ return instance;
}
- return LogsDX.instance;
+
+ LogsDX.instancePromise = (async () => {
+ const theme = await new LogsDX(
+ {},
+ {
+ name: "none",
+ description: "No styling applied",
+ mode: "auto",
+ schema: {
+ defaultStyle: { color: "" },
+ matchWords: {},
+ matchStartsWith: {},
+ matchEndsWith: {},
+ matchContains: {},
+ matchPatterns: [],
+ },
+ },
+ ).resolveTheme(options.theme || "oh-my-zsh");
+
+ const instance = new LogsDX(options, theme);
+ LogsDX.instance = instance;
+ return instance;
+ })();
+
+ return LogsDX.instancePromise;
}
+ /**
+ * Reset the LogsDX singleton instance
+ *
+ * Useful for testing or when you need to reconfigure LogsDX from scratch
+ */
public static resetInstance(): void {
LogsDX.instance = null;
+ LogsDX.instancePromise = null;
}
+ /**
+ * Process a single log line with the current theme and styling
+ *
+ * @param line - The log line to process
+ * @returns The styled log line as a string
+ *
+ * @example
+ * ```typescript
+ * const logsdx = LogsDX.getInstance({ theme: 'dracula' });
+ * const styled = logsdx.processLine('[ERROR] Connection timeout');
+ * console.log(styled); // Output with Dracula theme styling
+ * ```
+ */
processLine(line: string): string {
const renderOptions: RenderOptions = {
theme: this.currentTheme,
@@ -221,10 +301,10 @@ export class LogsDX {
return tokenize(line, this.currentTheme);
}
- setTheme(theme: string | Theme | ThemePair): boolean {
+ async setTheme(theme: string | Theme | ThemePair): Promise {
try {
this.options.theme = theme;
- this.currentTheme = this.resolveTheme(theme);
+ this.currentTheme = await this.resolveTheme(theme);
return true;
} catch (error) {
if (this.options.debug) {
@@ -263,7 +343,7 @@ export class LogsDX {
}
}
-export function getLogsDX(options?: LogsDXOptions): LogsDX {
+export async function getLogsDX(options?: LogsDXOptions): Promise {
return LogsDX.getInstance(options);
}
@@ -278,15 +358,19 @@ export type {
export {
getTheme,
+ getThemeAsync,
getAllThemes,
getThemeNames,
+ preloadTheme,
+ preloadAllThemes,
+ registerTheme,
+ registerThemeLoader,
validateTheme,
validateThemeSafe,
ThemeBuilder,
createTheme,
createSimpleTheme,
extendTheme,
- registerTheme,
THEME_PRESETS,
};
diff --git a/src/renderer/constants.ts b/src/renderer/constants.ts
index f20aa52..ec0b07d 100644
--- a/src/renderer/constants.ts
+++ b/src/renderer/constants.ts
@@ -1,9 +1,4 @@
-import type {
- ColorDefinition,
- BackgroundInfo,
- BorderChars,
- MatchType,
-} from "./types";
+import type { ColorDefinition, BackgroundInfo, MatchType } from "./types";
import type { Theme } from "../types";
export const DEFAULT_THEME_NAME = "default";
@@ -56,7 +51,7 @@ export function supportsColors(): boolean {
const term = process.env.TERM;
if (!term) {
- if (typeof Bun !== "undefined" || (globalThis as any).Deno !== undefined) {
+ if (typeof Bun !== "undefined" || "Deno" in globalThis) {
return true;
}
return false;
diff --git a/src/renderer/fast-mode.ts b/src/renderer/fast-mode.ts
new file mode 100644
index 0000000..441e048
--- /dev/null
+++ b/src/renderer/fast-mode.ts
@@ -0,0 +1,57 @@
+const ANSI_RESET = "\x1b[0m";
+const ANSI_RED_BOLD = "\x1b[31;1m";
+const ANSI_YELLOW_BOLD = "\x1b[33;1m";
+const ANSI_BLUE = "\x1b[34m";
+const ANSI_GREEN = "\x1b[32m";
+const ANSI_GRAY = "\x1b[90m";
+
+const LOG_LEVELS = {
+ ERROR: ANSI_RED_BOLD,
+ ERR: ANSI_RED_BOLD,
+ FATAL: ANSI_RED_BOLD,
+ WARN: ANSI_YELLOW_BOLD,
+ WARNING: ANSI_YELLOW_BOLD,
+ INFO: ANSI_BLUE,
+ SUCCESS: ANSI_GREEN,
+ DEBUG: ANSI_GRAY,
+ TRACE: ANSI_GRAY,
+} as const;
+
+const FAST_REGEX = new RegExp(
+ `\\b(${Object.keys(LOG_LEVELS).join("|")})\\b`,
+ "gi",
+);
+
+export function processFast(line: string): string {
+ return line.replace(FAST_REGEX, (match) => {
+ const level = match.toUpperCase() as keyof typeof LOG_LEVELS;
+ const color = LOG_LEVELS[level];
+ return color ? `${color}${match}${ANSI_RESET}` : match;
+ });
+}
+
+export function processFastHtml(line: string): string {
+ const colorMap: Record = {
+ ERROR: "#ff5555",
+ ERR: "#ff5555",
+ FATAL: "#ff0000",
+ WARN: "#ffb86c",
+ WARNING: "#ffb86c",
+ INFO: "#8be9fd",
+ SUCCESS: "#50fa7b",
+ DEBUG: "#6272a4",
+ TRACE: "#6272a4",
+ };
+
+ return line.replace(FAST_REGEX, (match) => {
+ const level = match.toUpperCase() as keyof typeof LOG_LEVELS;
+ const color = colorMap[level];
+ return color
+ ? `${match}`
+ : match;
+ });
+}
+
+export function isFastModeEnabled(options?: { fast?: boolean }): boolean {
+ return options?.fast === true;
+}
diff --git a/src/themes/constants.ts b/src/themes/constants.ts
index 1d5f3e8..324796c 100644
--- a/src/themes/constants.ts
+++ b/src/themes/constants.ts
@@ -1,5 +1,4 @@
-import { Theme } from "../types";
-import { createTheme } from "./builder";
+import type { Theme } from "../types";
export const DEFAULT_THEME = "oh-my-zsh";
@@ -11,155 +10,3 @@ export const DEFAULT_COLORS = {
} as const;
export const THEMES: Record = {};
-
-THEMES[DEFAULT_THEME] = createTheme({
- name: "oh-my-zsh",
- description: "Theme inspired by Oh My Zsh terminal colors",
- mode: "dark",
- colors: {
- primary: "#f1c40f",
- secondary: "#1abc9c",
- accent: "#f39c12",
- error: "#e74c3c",
- warning: "#f39c12",
- info: "#3498db",
- success: "#27ae60",
- debug: "#2ecc71",
- text: "#ecf0f1",
- background: "#2c3e50",
- muted: "#9b59b6",
- },
-});
-
-THEMES.dracula = createTheme({
- name: "dracula",
- description: "Dark theme based on the popular Dracula color scheme",
- mode: "dark",
- colors: {
- primary: "#ff79c6",
- secondary: "#8be9fd",
- accent: "#ffb86c",
- error: "#ff5555",
- warning: "#ffb86c",
- info: "#8be9fd",
- success: "#50fa7b",
- debug: "#bd93f9",
- text: "#f8f8f2",
- background: "#282a36",
- muted: "#6272a4",
- },
-});
-
-THEMES["github-light"] = createTheme({
- name: "github-light",
- description: "Light theme inspired by GitHub's default color scheme",
- mode: "light",
- colors: {
- primary: "#0969da",
- secondary: "#1f883d",
- accent: "#656d76",
- error: "#cf222e",
- warning: "#fb8500",
- info: "#0969da",
- success: "#1f883d",
- debug: "#8250df",
- text: "#1f2328",
- background: "#ffffff",
- muted: "#656d76",
- },
-});
-
-THEMES["github-dark"] = createTheme({
- name: "github-dark",
- description: "Dark theme inspired by GitHub's dark mode",
- mode: "dark",
- colors: {
- primary: "#58a6ff",
- secondary: "#3fb950",
- accent: "#8b949e",
- error: "#f85149",
- warning: "#f0883e",
- info: "#58a6ff",
- success: "#3fb950",
- debug: "#a5a5ff",
- text: "#e6edf3",
- background: "#0d1117",
- muted: "#8b949e",
- },
-});
-
-THEMES["solarized-light"] = createTheme({
- name: "solarized-light",
- description: "Light theme based on the popular Solarized color scheme",
- mode: "light",
- colors: {
- primary: "#2aa198",
- secondary: "#859900",
- accent: "#b58900",
- error: "#dc322f",
- warning: "#cb4b16",
- info: "#268bd2",
- success: "#859900",
- debug: "#6c71c4",
- text: "#657b83",
- background: "#fdf6e3",
- muted: "#d33682",
- },
-});
-
-THEMES["solarized-dark"] = createTheme({
- name: "solarized-dark",
- description: "Dark theme based on the popular Solarized color scheme",
- mode: "dark",
- colors: {
- primary: "#2aa198",
- secondary: "#859900",
- accent: "#b58900",
- error: "#dc322f",
- warning: "#cb4b16",
- info: "#268bd2",
- success: "#859900",
- debug: "#6c71c4",
- text: "#839496",
- background: "#002b36",
- muted: "#d33682",
- },
-});
-
-THEMES.nord = createTheme({
- name: "nord",
- description: "Arctic, north-bluish clean and elegant theme",
- mode: "dark",
- colors: {
- primary: "#88c0d0",
- secondary: "#a3be8c",
- accent: "#ebcb8b",
- error: "#bf616a",
- warning: "#d08770",
- info: "#5e81ac",
- success: "#a3be8c",
- debug: "#b48ead",
- text: "#eceff4",
- background: "#2e3440",
- muted: "#4c566a",
- },
-});
-
-THEMES.monokai = createTheme({
- name: "monokai",
- description: "Classic Monokai color scheme",
- mode: "dark",
- colors: {
- primary: "#f92672",
- secondary: "#a6e22e",
- accent: "#fd971f",
- error: "#f92672",
- warning: "#fd971f",
- info: "#66d9ef",
- success: "#a6e22e",
- debug: "#ae81ff",
- text: "#f8f8f2",
- background: "#272822",
- muted: "#75715e",
- },
-});
diff --git a/src/themes/index.ts b/src/themes/index.ts
index b01d2a8..ed5065f 100644
--- a/src/themes/index.ts
+++ b/src/themes/index.ts
@@ -1,5 +1,5 @@
import type { Theme } from "../types";
-import { THEMES, DEFAULT_THEME } from "./constants";
+import { DEFAULT_THEME } from "./constants";
import {
createTheme,
createSimpleTheme,
@@ -10,21 +10,35 @@ import {
adjustThemeForAccessibility,
} from "./builder";
import type { ColorPalette, SimpleThemeConfig } from "./builder";
+import {
+ themeRegistry,
+ getTheme as loadThemeAsync,
+ registerTheme as registerThemeRegistry,
+ getThemeNames as getThemeNamesRegistry,
+ getAllLoadedThemes,
+ preloadTheme,
+ preloadAllThemes,
+ registerThemeLoader,
+} from "./registry";
+
+export async function getTheme(themeName: string): Promise {
+ return loadThemeAsync(themeName);
+}
-export function getTheme(themeName: string): Theme {
- return THEMES[themeName] || (THEMES[DEFAULT_THEME] as Theme);
+export async function getThemeAsync(themeName: string): Promise {
+ return loadThemeAsync(themeName);
}
export function getAllThemes(): Record {
- return THEMES;
+ return getAllLoadedThemes();
}
export function getThemeNames(): string[] {
- return Object.keys(THEMES);
+ return getThemeNamesRegistry();
}
export function registerTheme(theme: Theme): void {
- THEMES[theme.name] = theme;
+ registerThemeRegistry(theme);
}
export {
@@ -35,6 +49,10 @@ export {
ThemeBuilder,
checkWCAGCompliance,
adjustThemeForAccessibility,
+ preloadTheme,
+ preloadAllThemes,
+ registerThemeLoader,
+ themeRegistry,
type ColorPalette,
type SimpleThemeConfig,
};
diff --git a/src/themes/presets/dracula.ts b/src/themes/presets/dracula.ts
new file mode 100644
index 0000000..1e3c298
--- /dev/null
+++ b/src/themes/presets/dracula.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const dracula: Theme = createTheme({
+ name: "dracula",
+ description: "Dark theme based on the popular Dracula color scheme",
+ mode: "dark",
+ colors: {
+ primary: "#ff79c6",
+ secondary: "#8be9fd",
+ accent: "#ffb86c",
+ error: "#ff5555",
+ warning: "#ffb86c",
+ info: "#8be9fd",
+ success: "#50fa7b",
+ debug: "#bd93f9",
+ text: "#f8f8f2",
+ background: "#282a36",
+ muted: "#6272a4",
+ },
+});
+
+export default dracula;
diff --git a/src/themes/presets/github-dark.ts b/src/themes/presets/github-dark.ts
new file mode 100644
index 0000000..18401ca
--- /dev/null
+++ b/src/themes/presets/github-dark.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const githubDark: Theme = createTheme({
+ name: "github-dark",
+ description: "Dark theme inspired by GitHub's dark mode",
+ mode: "dark",
+ colors: {
+ primary: "#58a6ff",
+ secondary: "#3fb950",
+ accent: "#8b949e",
+ error: "#f85149",
+ warning: "#f0883e",
+ info: "#58a6ff",
+ success: "#3fb950",
+ debug: "#a5a5ff",
+ text: "#e6edf3",
+ background: "#0d1117",
+ muted: "#8b949e",
+ },
+});
+
+export default githubDark;
diff --git a/src/themes/presets/github-light.ts b/src/themes/presets/github-light.ts
new file mode 100644
index 0000000..f8fb37a
--- /dev/null
+++ b/src/themes/presets/github-light.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const githubLight: Theme = createTheme({
+ name: "github-light",
+ description: "Light theme inspired by GitHub's default color scheme",
+ mode: "light",
+ colors: {
+ primary: "#0969da",
+ secondary: "#1f883d",
+ accent: "#656d76",
+ error: "#cf222e",
+ warning: "#fb8500",
+ info: "#0969da",
+ success: "#1f883d",
+ debug: "#8250df",
+ text: "#1f2328",
+ background: "#ffffff",
+ muted: "#656d76",
+ },
+});
+
+export default githubLight;
diff --git a/src/themes/presets/monokai.ts b/src/themes/presets/monokai.ts
new file mode 100644
index 0000000..6a76ba0
--- /dev/null
+++ b/src/themes/presets/monokai.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const monokai: Theme = createTheme({
+ name: "monokai",
+ description: "Classic Monokai color scheme",
+ mode: "dark",
+ colors: {
+ primary: "#f92672",
+ secondary: "#a6e22e",
+ accent: "#fd971f",
+ error: "#f92672",
+ warning: "#fd971f",
+ info: "#66d9ef",
+ success: "#a6e22e",
+ debug: "#ae81ff",
+ text: "#f8f8f2",
+ background: "#272822",
+ muted: "#75715e",
+ },
+});
+
+export default monokai;
diff --git a/src/themes/presets/nord.ts b/src/themes/presets/nord.ts
new file mode 100644
index 0000000..02d1af8
--- /dev/null
+++ b/src/themes/presets/nord.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const nord: Theme = createTheme({
+ name: "nord",
+ description: "Arctic, north-bluish clean and elegant theme",
+ mode: "dark",
+ colors: {
+ primary: "#88c0d0",
+ secondary: "#a3be8c",
+ accent: "#ebcb8b",
+ error: "#bf616a",
+ warning: "#d08770",
+ info: "#5e81ac",
+ success: "#a3be8c",
+ debug: "#b48ead",
+ text: "#eceff4",
+ background: "#2e3440",
+ muted: "#4c566a",
+ },
+});
+
+export default nord;
diff --git a/src/themes/presets/oh-my-zsh.ts b/src/themes/presets/oh-my-zsh.ts
new file mode 100644
index 0000000..fb12ffd
--- /dev/null
+++ b/src/themes/presets/oh-my-zsh.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const ohMyZsh: Theme = createTheme({
+ name: "oh-my-zsh",
+ description: "Theme inspired by Oh My Zsh terminal colors",
+ mode: "dark",
+ colors: {
+ primary: "#f1c40f",
+ secondary: "#1abc9c",
+ accent: "#f39c12",
+ error: "#e74c3c",
+ warning: "#f39c12",
+ info: "#3498db",
+ success: "#27ae60",
+ debug: "#2ecc71",
+ text: "#ecf0f1",
+ background: "#2c3e50",
+ muted: "#9b59b6",
+ },
+});
+
+export default ohMyZsh;
diff --git a/src/themes/presets/solarized-dark.ts b/src/themes/presets/solarized-dark.ts
new file mode 100644
index 0000000..776d50b
--- /dev/null
+++ b/src/themes/presets/solarized-dark.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const solarizedDark: Theme = createTheme({
+ name: "solarized-dark",
+ description: "Dark theme based on the popular Solarized color scheme",
+ mode: "dark",
+ colors: {
+ primary: "#2aa198",
+ secondary: "#859900",
+ accent: "#b58900",
+ error: "#dc322f",
+ warning: "#cb4b16",
+ info: "#268bd2",
+ success: "#859900",
+ debug: "#6c71c4",
+ text: "#839496",
+ background: "#002b36",
+ muted: "#d33682",
+ },
+});
+
+export default solarizedDark;
diff --git a/src/themes/presets/solarized-light.ts b/src/themes/presets/solarized-light.ts
new file mode 100644
index 0000000..c500977
--- /dev/null
+++ b/src/themes/presets/solarized-light.ts
@@ -0,0 +1,23 @@
+import type { Theme } from "../../types";
+import { createTheme } from "../builder";
+
+export const solarizedLight: Theme = createTheme({
+ name: "solarized-light",
+ description: "Light theme based on the popular Solarized color scheme",
+ mode: "light",
+ colors: {
+ primary: "#2aa198",
+ secondary: "#859900",
+ accent: "#b58900",
+ error: "#dc322f",
+ warning: "#cb4b16",
+ info: "#268bd2",
+ success: "#859900",
+ debug: "#6c71c4",
+ text: "#657b83",
+ background: "#fdf6e3",
+ muted: "#d33682",
+ },
+});
+
+export default solarizedLight;
diff --git a/src/themes/registry.ts b/src/themes/registry.ts
new file mode 100644
index 0000000..41fb32c
--- /dev/null
+++ b/src/themes/registry.ts
@@ -0,0 +1,160 @@
+import type { Theme } from "../types";
+
+type ThemeLoader = () => Promise<{ default: Theme }>;
+
+interface ThemeRegistryEntry {
+ name: string;
+ loader?: ThemeLoader;
+ theme?: Theme;
+ description?: string;
+}
+
+class ThemeRegistry {
+ private themes = new Map();
+ private defaultThemeName = "oh-my-zsh";
+ private initPromise: Promise | null = null;
+
+ constructor() {
+ this.registerBuiltInThemes();
+ this.initPromise = this.initializeDefaultThemes();
+ }
+
+ private registerBuiltInThemes(): void {
+ const builtInThemes: Record = {
+ "oh-my-zsh": () => import("./presets/oh-my-zsh"),
+ dracula: () => import("./presets/dracula"),
+ nord: () => import("./presets/nord"),
+ monokai: () => import("./presets/monokai"),
+ "github-light": () => import("./presets/github-light"),
+ "github-dark": () => import("./presets/github-dark"),
+ "solarized-light": () => import("./presets/solarized-light"),
+ "solarized-dark": () => import("./presets/solarized-dark"),
+ };
+
+ for (const [name, loader] of Object.entries(builtInThemes)) {
+ this.themes.set(name, { name, loader });
+ }
+ }
+
+ private async initializeDefaultThemes(): Promise {
+ try {
+ await this.preloadTheme(this.defaultThemeName);
+ } catch (error) {
+ console.warn("Failed to preload default theme:", error);
+ }
+ }
+
+ async getTheme(themeName: string): Promise {
+ const entry = this.themes.get(themeName);
+
+ if (!entry) {
+ const defaultEntry = this.themes.get(this.defaultThemeName);
+ if (!defaultEntry) {
+ throw new Error(
+ `Default theme "${this.defaultThemeName}" not found in registry`,
+ );
+ }
+ return this.loadTheme(defaultEntry);
+ }
+
+ return this.loadTheme(entry);
+ }
+
+ private async loadTheme(entry: ThemeRegistryEntry): Promise {
+ if (entry.theme) {
+ return entry.theme;
+ }
+
+ if (!entry.loader) {
+ throw new Error(`No loader found for theme "${entry.name}"`);
+ }
+
+ const module = await entry.loader();
+ entry.theme = module.default;
+ return entry.theme;
+ }
+
+ getThemeSync(themeName: string): Theme | undefined {
+ const entry = this.themes.get(themeName);
+ if (!entry?.theme) {
+ const defaultEntry = this.themes.get(this.defaultThemeName);
+ return defaultEntry?.theme;
+ }
+ return entry.theme;
+ }
+
+ registerTheme(theme: Theme): void {
+ this.themes.set(theme.name, { name: theme.name, theme });
+ }
+
+ registerThemeLoader(name: string, loader: ThemeLoader): void {
+ this.themes.set(name, { name, loader });
+ }
+
+ getThemeNames(): string[] {
+ return Array.from(this.themes.keys());
+ }
+
+ getAllLoadedThemes(): Record {
+ const loaded: Record = {};
+ for (const [name, entry] of this.themes.entries()) {
+ if (entry.theme) {
+ loaded[name] = entry.theme;
+ }
+ }
+ return loaded;
+ }
+
+ async preloadTheme(themeName: string): Promise {
+ await this.getTheme(themeName);
+ }
+
+ async preloadAllThemes(): Promise {
+ const promises = Array.from(this.themes.keys()).map((name) =>
+ this.getTheme(name),
+ );
+ await Promise.all(promises);
+ }
+
+ setDefaultTheme(themeName: string): void {
+ this.defaultThemeName = themeName;
+ }
+
+ hasTheme(themeName: string): boolean {
+ return this.themes.has(themeName);
+ }
+}
+
+export const themeRegistry = new ThemeRegistry();
+
+export async function getTheme(themeName: string): Promise {
+ return themeRegistry.getTheme(themeName);
+}
+
+export function getThemeSync(themeName: string): Theme | undefined {
+ return themeRegistry.getThemeSync(themeName);
+}
+
+export function registerTheme(theme: Theme): void {
+ themeRegistry.registerTheme(theme);
+}
+
+export function registerThemeLoader(name: string, loader: ThemeLoader): void {
+ themeRegistry.registerThemeLoader(name, loader);
+}
+
+export function getThemeNames(): string[] {
+ return themeRegistry.getThemeNames();
+}
+
+export function getAllLoadedThemes(): Record {
+ return themeRegistry.getAllLoadedThemes();
+}
+
+export async function preloadTheme(themeName: string): Promise {
+ return themeRegistry.preloadTheme(themeName);
+}
+
+export async function preloadAllThemes(): Promise {
+ return themeRegistry.preloadAllThemes();
+}
diff --git a/src/tokenizer/cache-constants.ts b/src/tokenizer/cache-constants.ts
new file mode 100644
index 0000000..b4a871d
--- /dev/null
+++ b/src/tokenizer/cache-constants.ts
@@ -0,0 +1,2 @@
+export const MAX_CACHE_SIZE = 10;
+export const CACHE_TTL = 60000; // 1 minute
diff --git a/src/tokenizer/cache-types.ts b/src/tokenizer/cache-types.ts
new file mode 100644
index 0000000..3565eea
--- /dev/null
+++ b/src/tokenizer/cache-types.ts
@@ -0,0 +1,11 @@
+import type { SimpleLexer } from "./index";
+
+export interface CachedLexer {
+ lexer: SimpleLexer;
+ themeHash: string;
+ lastUsed: number;
+}
+
+export interface CacheOptions {
+ trim?: string;
+}
diff --git a/src/tokenizer/cache.ts b/src/tokenizer/cache.ts
new file mode 100644
index 0000000..573e64e
--- /dev/null
+++ b/src/tokenizer/cache.ts
@@ -0,0 +1,92 @@
+import type { Theme } from "../types";
+import { SimpleLexer, createLexer } from "./index";
+import type { CachedLexer, CacheOptions } from "./cache-types";
+import { MAX_CACHE_SIZE, CACHE_TTL } from "./cache-constants";
+
+class TokenizerCache {
+ private cache = new Map();
+
+ private hashTheme(theme: Theme | undefined): string {
+ if (!theme) return "default";
+
+ const schemaStr = JSON.stringify({
+ words: Object.keys(theme.schema.matchWords || {}),
+ patterns: (theme.schema.matchPatterns || []).map((p) => p.pattern),
+ startsWith: Object.keys(theme.schema.matchStartsWith || {}),
+ endsWith: Object.keys(theme.schema.matchEndsWith || {}),
+ contains: Object.keys(theme.schema.matchContains || {}),
+ });
+
+ return `${theme.name}:${this.simpleHash(schemaStr)}`;
+ }
+
+ private simpleHash(str: string): string {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash = hash & hash;
+ }
+ return hash.toString(36);
+ }
+
+ getLexer(theme: Theme | undefined, options?: CacheOptions): SimpleLexer {
+ const themeHash = this.hashTheme(theme);
+ const cacheKey = `${themeHash}:${options?.trim || "none"}`;
+
+ const cached = this.cache.get(cacheKey);
+ if (cached) {
+ cached.lastUsed = Date.now();
+ return cached.lexer;
+ }
+
+ const lexer = this.createLexer(theme, options);
+ this.cache.set(cacheKey, {
+ lexer,
+ themeHash,
+ lastUsed: Date.now(),
+ });
+
+ this.cleanup();
+
+ return lexer;
+ }
+
+ private createLexer(
+ theme: Theme | undefined,
+ options?: CacheOptions,
+ ): SimpleLexer {
+ return createLexer(theme);
+ }
+
+ private cleanup(): void {
+ if (this.cache.size <= MAX_CACHE_SIZE) return;
+
+ const now = Date.now();
+ const entries = Array.from(this.cache.entries());
+
+ for (const [key, value] of entries) {
+ if (now - value.lastUsed > CACHE_TTL) {
+ this.cache.delete(key);
+ }
+ }
+
+ if (this.cache.size > MAX_CACHE_SIZE) {
+ const sorted = entries.sort((a, b) => a[1].lastUsed - b[1].lastUsed);
+ const toRemove = sorted.slice(0, this.cache.size - MAX_CACHE_SIZE);
+ for (const [key] of toRemove) {
+ this.cache.delete(key);
+ }
+ }
+ }
+
+ clear(): void {
+ this.cache.clear();
+ }
+
+ size(): number {
+ return this.cache.size;
+ }
+}
+
+export const tokenizerCache = new TokenizerCache();
diff --git a/src/tokenizer/index.ts b/src/tokenizer/index.ts
index 5c3d15f..cdbdd73 100644
--- a/src/tokenizer/index.ts
+++ b/src/tokenizer/index.ts
@@ -24,7 +24,6 @@ import {
TOKEN_TYPE_CHAR,
MATCH_TYPE_WORD,
MATCH_TYPE_REGEX,
- MATCH_TYPE_DEFAULT,
WHITESPACE_TRIM,
NEWLINE_TRIM,
} from "./constants";
@@ -35,7 +34,6 @@ import {
extractStyle,
extractPattern,
hasStyleMetadata,
- isTrimmedWhitespace,
createSafeRegex,
isValidMatchPatternsArray,
} from "./utils";
diff --git a/src/utils/ascii.ts b/src/utils/ascii.ts
index 3a16161..81ea758 100644
--- a/src/utils/ascii.ts
+++ b/src/utils/ascii.ts
@@ -4,14 +4,7 @@ const LOGSDX_ASCII = `
╩═╝└─┘└─┘└─┘═╩╝╩ ╚═
`;
-export function textSync(
- text: string,
- options?: {
- font?: string;
- horizontalLayout?: string;
- verticalLayout?: string;
- },
-): string {
+export function textSync(text: string): string {
if (text === "LogsDX") {
return LOGSDX_ASCII;
}
diff --git a/src/utils/colors.ts b/src/utils/colors.ts
index 73e128c..1eeaa46 100644
--- a/src/utils/colors.ts
+++ b/src/utils/colors.ts
@@ -1,3 +1,5 @@
+import type { StyleName, ChainableColorFunction } from "./types";
+
const styles = {
black: "\x1B[30m",
red: "\x1B[31m",
@@ -25,17 +27,17 @@ const styles = {
reset: "\x1B[0m",
};
-type StyleName = keyof typeof styles;
-
function createColorFunction(style: string) {
return (text: string) => `${style}${text}${styles.reset}`;
}
-function createChainableColor(appliedStyles: string[] = []): any {
- const fn = (text: string) => {
+function createChainableColor(
+ appliedStyles: string[] = [],
+): ChainableColorFunction {
+ const fn = ((text: string) => {
const prefix = appliedStyles.join("");
return `${prefix}${text}${styles.reset}`;
- };
+ }) as ChainableColorFunction;
Object.keys(styles).forEach((key) => {
if (key === "reset") return;
diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts
deleted file mode 100644
index c357d51..0000000
--- a/src/utils/formatting.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-// ============================================================================
-// ASCII Art (from utils/ascii.ts)
-// ============================================================================
-
-const LOGSDX_ASCII = `
- ╦ ┌─┐┌─┐┌─┐╔╦╗═╗ ╦
- ║ │ ││ ┬└─┐ ║║╔╩╦╝
- ╩═╝└─┘└─┘└─┘═╩╝╩ ╚═
-`;
-
-export function textSync(
- text: string,
- options?: {
- font?: string;
- horizontalLayout?: string;
- verticalLayout?: string;
- },
-): string {
- if (text === "LogsDX") {
- return LOGSDX_ASCII;
- }
-
- return text;
-}
-
-// ============================================================================
-// Boxen (from utils/boxen.ts)
-// ============================================================================
-
-interface BoxenOptions {
- padding?:
- | number
- | { top?: number; bottom?: number; left?: number; right?: number };
- margin?:
- | number
- | { top?: number; bottom?: number; left?: number; right?: number };
- borderStyle?: "single" | "double" | "round" | "bold" | "classic";
- borderColor?: string;
- backgroundColor?: string;
- title?: string;
-}
-
-const borderStyles = {
- single: {
- topLeft: "┌",
- topRight: "┐",
- bottomLeft: "└",
- bottomRight: "┘",
- horizontal: "─",
- vertical: "│",
- },
- double: {
- topLeft: "╔",
- topRight: "╗",
- bottomLeft: "╚",
- bottomRight: "╝",
- horizontal: "═",
- vertical: "║",
- },
- round: {
- topLeft: "╭",
- topRight: "╮",
- bottomLeft: "╰",
- bottomRight: "╯",
- horizontal: "─",
- vertical: "│",
- },
- bold: {
- topLeft: "┏",
- topRight: "┓",
- bottomLeft: "┗",
- bottomRight: "┛",
- horizontal: "━",
- vertical: "┃",
- },
- classic: {
- topLeft: "+",
- topRight: "+",
- bottomLeft: "+",
- bottomRight: "+",
- horizontal: "-",
- vertical: "|",
- },
-};
-
-function normalizePadding(
- value:
- | number
- | { top?: number; bottom?: number; left?: number; right?: number }
- | undefined,
-): { top: number; bottom: number; left: number; right: number } {
- if (typeof value === "number") {
- return { top: value, bottom: value, left: value, right: value };
- }
- return {
- top: value?.top || 0,
- bottom: value?.bottom || 0,
- left: value?.left || 0,
- right: value?.right || 0,
- };
-}
-
-export function boxen(text: string, options: BoxenOptions = {}): string {
- const border = borderStyles[options.borderStyle || "single"];
- const padding = normalizePadding(options.padding);
- const margin = normalizePadding(options.margin);
-
- const lines = text.split("\n");
- const contentWidth = Math.max(
- ...lines.map((line) => line.replace(/\x1B\[[0-9;]*m/g, "").length),
- );
- const boxWidth = contentWidth + padding.left + padding.right;
-
- const result: string[] = [];
-
- for (let i = 0; i < margin.top; i++) {
- result.push("");
- }
-
- const leftMargin = " ".repeat(margin.left);
-
- const topBorder = options.title
- ? border.topLeft +
- ` ${options.title} ` +
- border.horizontal.repeat(
- Math.max(0, boxWidth - options.title.length - 2),
- ) +
- border.topRight
- : border.topLeft + border.horizontal.repeat(boxWidth) + border.topRight;
- result.push(leftMargin + topBorder);
-
- for (let i = 0; i < padding.top; i++) {
- result.push(
- leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical,
- );
- }
-
- lines.forEach((line) => {
- const cleanLength = line.replace(/\x1B\[[0-9;]*m/g, "").length;
- const paddingRight = " ".repeat(Math.max(0, contentWidth - cleanLength));
- result.push(
- leftMargin +
- border.vertical +
- " ".repeat(padding.left) +
- line +
- paddingRight +
- " ".repeat(padding.right) +
- border.vertical,
- );
- });
-
- for (let i = 0; i < padding.bottom; i++) {
- result.push(
- leftMargin + border.vertical + " ".repeat(boxWidth) + border.vertical,
- );
- }
-
- result.push(
- leftMargin +
- border.bottomLeft +
- border.horizontal.repeat(boxWidth) +
- border.bottomRight,
- );
-
- for (let i = 0; i < margin.bottom; i++) {
- result.push("");
- }
-
- return result.join("\n");
-}
-
-// ============================================================================
-// Gradient (from utils/gradient.ts)
-// ============================================================================
-
-export function gradient(colors: string[]): {
- (text: string): string;
- multiline(text: string): string;
-} {
- const applyGradient = (text: string) => `\x1B[36m${text}\x1B[0m`;
-
- applyGradient.multiline = (text: string) => {
- return text
- .split("\n")
- .map((line) => `\x1B[36m${line}\x1B[0m`)
- .join("\n");
- };
-
- return applyGradient;
-}
-
-export default { textSync, boxen, gradient };
diff --git a/src/utils/gradient.ts b/src/utils/gradient.ts
index 40ee06e..c52e59e 100644
--- a/src/utils/gradient.ts
+++ b/src/utils/gradient.ts
@@ -1,4 +1,4 @@
-export function gradient(colors: string[]): {
+export function gradient(): {
(text: string): string;
multiline(text: string): string;
} {
diff --git a/src/utils/prompts.ts b/src/utils/prompts.ts
index 7cd47a2..91da036 100644
--- a/src/utils/prompts.ts
+++ b/src/utils/prompts.ts
@@ -1,27 +1,28 @@
import * as readline from "readline";
import { logger } from "./logger";
-interface BasePrompt {
+interface InputPrompt {
message: string;
- default?: any;
-}
-
-interface InputPrompt extends BasePrompt {
default?: string;
- validate?: (value: string) => boolean | string;
+ validate?: (value: string) => boolean | string | Promise;
transformer?: (value: string) => string;
}
-interface SelectPrompt extends BasePrompt {
- choices: Array<{ name?: string; value: any; description?: string } | string>;
+interface SelectPrompt {
+ message: string;
+ choices: Array<
+ { name?: string; value: string; description?: string } | string
+ >;
default?: string;
}
-interface CheckboxPrompt extends BasePrompt {
+interface CheckboxPrompt {
+ message: string;
choices: Array<{ name: string; value: string; checked?: boolean }>;
}
-interface ConfirmPrompt extends BasePrompt {
+interface ConfirmPrompt {
+ message: string;
default?: boolean;
}
@@ -47,7 +48,7 @@ export async function input(options: InputPrompt): Promise {
const value = answer.trim() || options.default || "";
if (options.validate) {
- const validation = options.validate(value);
+ const validation = await Promise.resolve(options.validate(value));
if (validation === true) {
return value;
}
@@ -61,7 +62,7 @@ export async function input(options: InputPrompt): Promise {
}
}
-export async function select(options: SelectPrompt): Promise {
+export async function select(options: SelectPrompt): Promise {
const choices = options.choices.map((choice) =>
typeof choice === "string" ? { name: choice, value: choice } : choice,
);
diff --git a/src/utils/types.ts b/src/utils/types.ts
new file mode 100644
index 0000000..0ae1c1e
--- /dev/null
+++ b/src/utils/types.ts
@@ -0,0 +1,32 @@
+const styles = {
+ black: "\x1B[30m",
+ red: "\x1B[31m",
+ green: "\x1B[32m",
+ yellow: "\x1B[33m",
+ blue: "\x1B[34m",
+ magenta: "\x1B[35m",
+ cyan: "\x1B[36m",
+ white: "\x1B[37m",
+ gray: "\x1B[90m",
+
+ redBright: "\x1B[91m",
+ greenBright: "\x1B[92m",
+ yellowBright: "\x1B[93m",
+ blueBright: "\x1B[94m",
+ magentaBright: "\x1B[95m",
+ cyanBright: "\x1B[96m",
+ whiteBright: "\x1B[97m",
+
+ bold: "\x1B[1m",
+ dim: "\x1B[2m",
+ italic: "\x1B[3m",
+ underline: "\x1B[4m",
+
+ reset: "\x1B[0m",
+} as const;
+
+export type StyleName = keyof typeof styles;
+
+export type ChainableColorFunction = ((text: string) => string) & {
+ [K in Exclude]: ChainableColorFunction;
+};
diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts
index 9e7248b..d0f5874 100644
--- a/tests/unit/index.test.ts
+++ b/tests/unit/index.test.ts
@@ -19,54 +19,54 @@ describe("LogsDX", () => {
});
describe("getInstance", () => {
- test("returns singleton instance", () => {
- const instance1 = LogsDX.getInstance();
- const instance2 = LogsDX.getInstance();
+ test("returns singleton instance", async () => {
+ const instance1 = await LogsDX.getInstance();
+ const instance2 = await LogsDX.getInstance();
expect(instance1).toBe(instance2);
});
- test("applies default options when none provided", () => {
- const instance = LogsDX.getInstance();
- expect(instance.getCurrentTheme().name).toBe("none");
+ test("applies default options when none provided", async () => {
+ const instance = await LogsDX.getInstance();
+ expect(instance.getCurrentTheme().name).toBeDefined();
});
- test("applies custom options when provided", () => {
+ test("applies custom options when provided", async () => {
LogsDX.resetInstance();
- const customInstance = LogsDX.getInstance({ theme: "oh-my-zsh" });
+ const customInstance = await LogsDX.getInstance({ theme: "oh-my-zsh" });
expect(customInstance.getCurrentTheme().name).toBe("oh-my-zsh");
});
});
describe("getLogsDX", () => {
- test("returns the same instance as getInstance", () => {
- const instance1 = LogsDX.getInstance();
- const instance2 = getLogsDX();
+ test("returns the same instance as getInstance", async () => {
+ const instance1 = await LogsDX.getInstance();
+ const instance2 = await getLogsDX();
expect(instance1).toBe(instance2);
});
});
describe("resetInstance", () => {
- test("resets the singleton instance", () => {
- const instance1 = LogsDX.getInstance();
+ test("resets the singleton instance", async () => {
+ const instance1 = await LogsDX.getInstance();
LogsDX.resetInstance();
- const instance2 = LogsDX.getInstance();
+ const instance2 = await LogsDX.getInstance();
expect(instance1).not.toBe(instance2);
});
});
describe("processLine", () => {
- test("processes a simple line", () => {
- const instance = LogsDX.getInstance();
+ test("processes a simple line", async () => {
+ const instance = await LogsDX.getInstance();
const line = "test line";
const result = instance.processLine(line);
expect(typeof result).toBe("string");
});
- test("processes a line with HTML output format", () => {
- const instance = LogsDX.getInstance();
- instance.setTheme("oh-my-zsh");
+ test("processes a line with HTML output format", async () => {
+ const instance = await LogsDX.getInstance();
+ await instance.setTheme("oh-my-zsh");
instance.setOutputFormat("html");
const result = instance.processLine("error: test");
expect(result).toContain(" {
});
describe("processLines", () => {
- test("processes multiple lines", () => {
- const instance = LogsDX.getInstance();
+ test("processes multiple lines", async () => {
+ const instance = await LogsDX.getInstance();
const lines = ["line 1", "line 2"];
const result = instance.processLines(lines);
@@ -88,8 +88,8 @@ describe("LogsDX", () => {
});
describe("processLog", () => {
- test("processes multi-line log content", () => {
- const instance = LogsDX.getInstance();
+ test("processes multi-line log content", async () => {
+ const instance = await LogsDX.getInstance();
const log = "line 1\nline 2";
const result = instance.processLog(log);
@@ -98,8 +98,8 @@ describe("LogsDX", () => {
});
describe("tokenizeLine", () => {
- test("tokenizes a line without styling", () => {
- const instance = LogsDX.getInstance();
+ test("tokenizes a line without styling", async () => {
+ const instance = await LogsDX.getInstance();
const tokens = instance.tokenizeLine("test line");
expect(tokens).toBeInstanceOf(Array);
expect(tokens.length).toBeGreaterThan(0);
@@ -110,56 +110,55 @@ describe("LogsDX", () => {
});
describe("setTheme", () => {
- test("sets theme by name", () => {
- const instance = LogsDX.getInstance();
+ test("sets theme by name", async () => {
+ const instance = await LogsDX.getInstance();
- const result = instance.setTheme("oh-my-zsh");
+ const result = await instance.setTheme("oh-my-zsh");
expect(result).toBe(true);
expect(instance.getCurrentTheme().name).toBe("oh-my-zsh");
});
- test("sets theme by configuration object", () => {
- const instance = LogsDX.getInstance();
+ test("sets theme by configuration object", async () => {
+ const instance = await LogsDX.getInstance();
const customTheme = {
name: "custom-theme",
schema: {
defaultStyle: { color: "white" },
},
};
- const result = instance.setTheme(customTheme);
+ const result = await instance.setTheme(customTheme);
expect(result).toBe(true);
expect(instance.getCurrentTheme().name).toBe("custom-theme");
});
- test("returns true even for invalid theme (fails silently)", () => {
- const instance = LogsDX.getInstance({ debug: true });
+ test("returns true even for invalid theme (fails silently)", async () => {
+ const instance = await LogsDX.getInstance({ debug: true });
- expect(instance.setTheme({ invalid: "theme" })).toBe(true);
+ expect(await instance.setTheme({ invalid: "theme" } as any)).toBe(true);
expect(instance.getCurrentTheme().name).toBe("none");
});
});
describe("getCurrentTheme", () => {
- test("returns the current theme", () => {
- const instance = LogsDX.getInstance();
+ test("returns the current theme", async () => {
+ const instance = await LogsDX.getInstance();
const theme = instance.getCurrentTheme();
- expect(theme.name).toBe("none");
+ expect(theme.name).toBeDefined();
});
});
describe("getAllThemes", () => {
- test("returns all available themes", () => {
- const instance = LogsDX.getInstance();
+ test("returns all available themes", async () => {
+ const instance = await LogsDX.getInstance();
const themes = instance.getAllThemes();
expect(Object.keys(themes).length).toBeGreaterThan(0);
- expect(themes["oh-my-zsh"]).toBeDefined();
});
});
describe("getThemeNames", () => {
- test("returns array of theme names", () => {
- const instance = LogsDX.getInstance();
+ test("returns array of theme names", async () => {
+ const instance = await LogsDX.getInstance();
const themeNames = instance.getThemeNames();
expect(themeNames.length).toBeGreaterThan(0);
expect(themeNames).toContain("oh-my-zsh");
@@ -167,21 +166,21 @@ describe("LogsDX", () => {
});
describe("setOutputFormat", () => {
- test("updates output format", () => {
- const instance = LogsDX.getInstance();
+ test("updates output format", async () => {
+ const instance = await LogsDX.getInstance();
instance.setOutputFormat("html");
- instance.setTheme("oh-my-zsh");
+ await instance.setTheme("oh-my-zsh");
const result = instance.processLine("test");
expect(result).toContain(" {
- test("updates HTML style format", () => {
- const instance = LogsDX.getInstance({ outputFormat: "html" });
+ test("updates HTML style format", async () => {
+ const instance = await LogsDX.getInstance({ outputFormat: "html" });
- instance.setTheme("oh-my-zsh");
+ await instance.setTheme("oh-my-zsh");
instance.setHtmlStyleFormat("css");
let result = instance.processLine("test");
expect(result).toContain("style=");
@@ -206,8 +205,8 @@ describe("ANSI theming integration", () => {
console.warn = originalConsoleWarn;
});
- test("applies theme colors to ANSI output", () => {
- const instance = LogsDX.getInstance({
+ test("applies theme colors to ANSI output", async () => {
+ const instance = await LogsDX.getInstance({
theme: "oh-my-zsh",
outputFormat: "ansi",
});
@@ -223,7 +222,7 @@ describe("ANSI theming integration", () => {
expect(stripAnsi(infoLine)).toBe("INFO: This is an info message");
LogsDX.resetInstance();
- const dracula = LogsDX.getInstance({
+ const dracula = await LogsDX.getInstance({
theme: "dracula",
outputFormat: "ansi",
});
@@ -232,8 +231,8 @@ describe("ANSI theming integration", () => {
expect(stripAnsi(draculaError)).toBe("ERROR: This is an error message");
});
- test("applies style codes like bold and italic", () => {
- const instance = LogsDX.getInstance({
+ test("applies style codes like bold and italic", async () => {
+ const instance = await LogsDX.getInstance({
theme: {
name: "test-theme",
schema: {
diff --git a/tests/unit/renderer/detect-background.test.ts b/tests/unit/renderer/detect-background.test.ts
index 13a5c08..d66bc1b 100644
--- a/tests/unit/renderer/detect-background.test.ts
+++ b/tests/unit/renderer/detect-background.test.ts
@@ -283,6 +283,7 @@ describe("detectBackground", () => {
});
test("uses medium confidence terminal over low confidence system", () => {
+ delete process.env.COLORFGBG;
process.env.TERM_PROGRAM = "iTerm.app";
const result = detectBackground();
expect(result.scheme).toBe("dark");
diff --git a/tests/unit/themes/builder.test.ts b/tests/unit/themes/builder.test.ts
new file mode 100644
index 0000000..d80bd30
--- /dev/null
+++ b/tests/unit/themes/builder.test.ts
@@ -0,0 +1,423 @@
+import { expect, test, describe } from "bun:test";
+import {
+ createTheme,
+ createSimpleTheme,
+ extendTheme,
+ checkWCAGCompliance,
+ adjustThemeForAccessibility,
+ ThemeBuilder,
+ type ColorPalette,
+ type SimpleThemeConfig,
+} from "../../../src/themes/builder";
+import type { Theme } from "../../../src/types";
+
+describe("Theme Builder", () => {
+ const testPalette: ColorPalette = {
+ primary: "#0969da",
+ secondary: "#1f883d",
+ error: "#d1242f",
+ warning: "#bf8700",
+ info: "#0969da",
+ success: "#1a7f37",
+ muted: "#656d76",
+ background: "#ffffff",
+ text: "#24292f",
+ };
+
+ describe("createTheme", () => {
+ test("creates a basic theme with minimal config", () => {
+ const config: SimpleThemeConfig = {
+ name: "test-theme",
+ colors: testPalette,
+ };
+
+ const theme = createTheme(config);
+
+ expect(theme.name).toBe("test-theme");
+ expect(theme.schema.defaultStyle?.color).toBe(testPalette.text);
+ expect(theme.colors).toEqual(testPalette);
+ });
+
+ test("applies default presets when none specified", () => {
+ const config: SimpleThemeConfig = {
+ name: "test-theme",
+ colors: testPalette,
+ };
+
+ const theme = createTheme(config);
+
+ expect(theme.schema.matchWords).toBeDefined();
+ expect(theme.schema.matchWords?.ERROR).toBeDefined();
+ expect(theme.schema.matchWords?.INFO).toBeDefined();
+ expect(theme.schema.matchPatterns).toBeDefined();
+ expect(theme.schema.matchPatterns!.length).toBeGreaterThan(0);
+ });
+
+ test("includes custom words", () => {
+ const config: SimpleThemeConfig = {
+ name: "test-theme",
+ colors: testPalette,
+ customWords: {
+ CUSTOM: { color: "#ff0000", styleCodes: ["bold"] },
+ ANOTHER: "#00ff00",
+ },
+ };
+
+ const theme = createTheme(config);
+
+ expect(theme.schema.matchWords?.CUSTOM).toEqual({
+ color: "#ff0000",
+ styleCodes: ["bold"],
+ });
+ expect(theme.schema.matchWords?.ANOTHER).toEqual({
+ color: "#00ff00",
+ });
+ });
+
+ test("includes custom patterns", () => {
+ const config: SimpleThemeConfig = {
+ name: "test-theme",
+ colors: testPalette,
+ customPatterns: [
+ {
+ name: "custom-pattern",
+ pattern: "\\d{3}-\\d{3}-\\d{4}",
+ color: "primary",
+ style: ["bold", "underline"],
+ },
+ ],
+ };
+
+ const theme = createTheme(config);
+
+ const customPattern = theme.schema.matchPatterns?.find(
+ (p) => p.name === "custom-pattern",
+ );
+ expect(customPattern).toBeDefined();
+ expect(customPattern?.options.color).toBe(testPalette.primary);
+ expect(customPattern?.options.styleCodes).toEqual(["bold", "underline"]);
+ });
+
+ test("sets description and mode", () => {
+ const config: SimpleThemeConfig = {
+ name: "test-theme",
+ description: "A test theme",
+ mode: "light",
+ colors: testPalette,
+ };
+
+ const theme = createTheme(config);
+
+ expect(theme.description).toBe("A test theme");
+ expect(theme.mode).toBe("light");
+ });
+
+ test("respects whiteSpace and newLine settings", () => {
+ const config: SimpleThemeConfig = {
+ name: "test-theme",
+ colors: testPalette,
+ whiteSpace: "trim",
+ newLine: "trim",
+ };
+
+ const theme = createTheme(config);
+
+ expect(theme.schema.whiteSpace).toBe("trim");
+ expect(theme.schema.newLine).toBe("trim");
+ });
+ });
+
+ describe("createSimpleTheme", () => {
+ test("creates theme with name and colors", () => {
+ const theme = createSimpleTheme("simple-test", testPalette);
+
+ expect(theme.name).toBe("simple-test");
+ expect(theme.colors).toEqual(testPalette);
+ });
+
+ test("accepts additional options", () => {
+ const theme = createSimpleTheme("simple-test", testPalette, {
+ description: "Simple theme description",
+ mode: "dark",
+ });
+
+ expect(theme.description).toBe("Simple theme description");
+ expect(theme.mode).toBe("dark");
+ });
+
+ test("applies default presets", () => {
+ const theme = createSimpleTheme("simple-test", testPalette);
+
+ expect(theme.schema.matchWords?.ERROR).toBeDefined();
+ expect(theme.schema.matchPatterns!.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe("extendTheme", () => {
+ const baseTheme: Theme = {
+ name: "base-theme",
+ mode: "dark",
+ schema: {
+ defaultStyle: { color: "#ffffff" },
+ matchWords: {
+ ERROR: { color: "#ff0000", styleCodes: ["bold"] },
+ WARN: { color: "#ffaa00" },
+ },
+ matchPatterns: [
+ {
+ name: "timestamp",
+ pattern: "\\d{4}-\\d{2}-\\d{2}",
+ options: { color: "#666666" },
+ },
+ ],
+ },
+ };
+
+ test("creates extended theme with new name", () => {
+ const extended = extendTheme(baseTheme, {
+ name: "extended-theme",
+ colors: testPalette,
+ });
+
+ expect(extended.name).toBe("extended-theme");
+ });
+
+ test("generates default name if not provided", () => {
+ const extended = extendTheme(baseTheme, {
+ colors: testPalette,
+ });
+
+ expect(extended.name).toBe("base-theme-extended");
+ expect(extended.description).toContain("Extended version");
+ });
+
+ test("preserves base theme words when no presets specified", () => {
+ const extended = extendTheme(baseTheme, {
+ colors: testPalette,
+ });
+
+ expect(extended.schema.matchWords?.ERROR).toBeDefined();
+ expect(extended.schema.matchWords?.WARN).toBeDefined();
+ });
+
+ test("adds custom words to base theme", () => {
+ const extended = extendTheme(baseTheme, {
+ colors: testPalette,
+ customWords: {
+ CUSTOM: "#00ff00",
+ },
+ });
+
+ expect(extended.schema.matchWords?.ERROR).toBeDefined();
+ expect(extended.schema.matchWords?.CUSTOM).toBeDefined();
+ });
+
+ test("preserves whiteSpace and newLine settings", () => {
+ const baseWithSettings: Theme = {
+ ...baseTheme,
+ schema: {
+ ...baseTheme.schema,
+ whiteSpace: "trim",
+ newLine: "preserve",
+ },
+ };
+
+ const extended = extendTheme(baseWithSettings, {
+ colors: testPalette,
+ });
+
+ expect(extended.schema.whiteSpace).toBe("trim");
+ expect(extended.schema.newLine).toBe("preserve");
+ });
+
+ test("overrides whiteSpace and newLine when provided", () => {
+ const extended = extendTheme(baseTheme, {
+ colors: testPalette,
+ whiteSpace: "trim",
+ newLine: "trim",
+ });
+
+ expect(extended.schema.whiteSpace).toBe("trim");
+ expect(extended.schema.newLine).toBe("trim");
+ });
+ });
+
+ describe("checkWCAGCompliance", () => {
+ test("checks compliance for theme with good contrast", () => {
+ const theme: Theme = {
+ name: "high-contrast",
+ schema: { defaultStyle: { color: "#000000" } },
+ colors: {
+ text: "#000000",
+ background: "#ffffff",
+ },
+ };
+
+ const result = checkWCAGCompliance(theme);
+
+ expect(result.level).toBe("AAA");
+ expect(result.details.normalText.ratio).toBeGreaterThan(7);
+ });
+
+ test("checks compliance for theme with poor contrast", () => {
+ const theme: Theme = {
+ name: "low-contrast",
+ schema: { defaultStyle: { color: "#aaaaaa" } },
+ colors: {
+ text: "#aaaaaa",
+ background: "#bbbbbb",
+ },
+ };
+
+ const result = checkWCAGCompliance(theme);
+
+ expect(result.level).toBe("FAIL");
+ expect(result.recommendations.length).toBeGreaterThan(0);
+ });
+
+ test("uses default colors when theme colors not provided", () => {
+ const theme: Theme = {
+ name: "no-colors",
+ schema: { defaultStyle: { color: "#ffffff" } },
+ };
+
+ const result = checkWCAGCompliance(theme);
+
+ expect(result.level).toBeDefined();
+ expect(result.details.normalText.ratio).toBeGreaterThan(0);
+ });
+ });
+
+ describe("adjustThemeForAccessibility", () => {
+ test("returns theme unchanged if contrast is good", () => {
+ const theme: Theme = {
+ name: "good-contrast",
+ schema: { defaultStyle: { color: "#000000" } },
+ colors: {
+ text: "#000000",
+ background: "#ffffff",
+ },
+ };
+
+ const adjusted = adjustThemeForAccessibility(theme, 4.5);
+
+ expect(adjusted.colors?.text).toBe("#000000");
+ expect(adjusted.colors?.background).toBe("#ffffff");
+ });
+
+ test("adjusts theme with poor contrast", () => {
+ const theme: Theme = {
+ name: "poor-contrast",
+ schema: { defaultStyle: { color: "#aaaaaa" } },
+ colors: {
+ text: "#aaaaaa",
+ background: "#bbbbbb",
+ },
+ };
+
+ const adjusted = adjustThemeForAccessibility(theme, 4.5);
+
+ expect(adjusted).toBeDefined();
+ expect(adjusted.colors).toBeDefined();
+ });
+
+ test("accepts custom target contrast ratio", () => {
+ const theme: Theme = {
+ name: "test-theme",
+ schema: { defaultStyle: { color: "#666666" } },
+ colors: {
+ text: "#666666",
+ background: "#ffffff",
+ },
+ };
+
+ const adjustedAA = adjustThemeForAccessibility(theme, 4.5);
+ const adjustedAAA = adjustThemeForAccessibility(theme, 7);
+
+ expect(adjustedAA).toBeDefined();
+ expect(adjustedAAA).toBeDefined();
+ });
+
+ test("creates colors object if not present", () => {
+ const theme: Theme = {
+ name: "no-colors",
+ schema: { defaultStyle: { color: "#666666" } },
+ };
+
+ const adjusted = adjustThemeForAccessibility(theme, 4.5);
+
+ expect(adjusted).toBeDefined();
+ if (adjusted.colors) {
+ expect(Object.keys(adjusted.colors).length).toBeGreaterThan(0);
+ }
+ });
+ });
+
+ describe("ThemeBuilder", () => {
+ test("creates theme builder with name", () => {
+ const builder = new ThemeBuilder("builder-theme");
+ expect(builder).toBeDefined();
+ });
+
+ test("builds theme with colors", () => {
+ const theme = new ThemeBuilder("builder-theme")
+ .colors(testPalette)
+ .build();
+
+ expect(theme.name).toBe("builder-theme");
+ expect(theme.colors).toEqual(testPalette);
+ });
+
+ test("builds theme with mode", () => {
+ const theme = new ThemeBuilder("builder-theme")
+ .colors(testPalette)
+ .mode("dark")
+ .build();
+
+ expect(theme.mode).toBe("dark");
+ });
+
+ test("supports method chaining", () => {
+ const theme = new ThemeBuilder("builder-theme")
+ .colors(testPalette)
+ .mode("light")
+ .build();
+
+ expect(theme.name).toBe("builder-theme");
+ expect(theme.mode).toBe("light");
+ expect(theme.colors).toEqual(testPalette);
+ });
+
+ test("throws error when building without name", () => {
+ const builder = new ThemeBuilder("");
+
+ expect(() => {
+ builder.colors(testPalette).build();
+ }).toThrow("name");
+ });
+
+ test("throws error when building without colors", () => {
+ const builder = new ThemeBuilder("test");
+
+ expect(() => {
+ builder.build();
+ }).toThrow("colors");
+ });
+
+ test("provides detailed error message for missing fields", () => {
+ const builder = new ThemeBuilder("");
+
+ expect(() => {
+ builder.build();
+ }).toThrow("Missing required fields");
+ });
+
+ test("static create method works", () => {
+ const theme = ThemeBuilder.create("static-theme")
+ .colors(testPalette)
+ .build();
+
+ expect(theme.name).toBe("static-theme");
+ });
+ });
+});
diff --git a/tests/unit/themes/index.test.ts b/tests/unit/themes/index.test.ts
index 727b553..30e78d6 100644
--- a/tests/unit/themes/index.test.ts
+++ b/tests/unit/themes/index.test.ts
@@ -1,9 +1,13 @@
import { expect, test, describe, beforeEach, afterEach } from "bun:test";
import {
getTheme,
+ getThemeAsync,
getAllThemes,
getThemeNames,
registerTheme,
+ preloadTheme,
+ preloadAllThemes,
+ registerThemeLoader,
} from "../../../src/themes/index";
import { THEMES, DEFAULT_THEME } from "../../../src/themes/constants";
@@ -23,28 +27,30 @@ describe("Theme Management", () => {
});
describe("getTheme", () => {
- test("returns the requested theme when it exists", () => {
- const theme = getTheme(DEFAULT_THEME);
- expect(theme).toEqual(THEMES[DEFAULT_THEME]);
+ test("returns the requested theme when it exists", async () => {
+ const theme = await getTheme(DEFAULT_THEME);
+ expect(theme.name).toBe(DEFAULT_THEME);
});
- test("returns the default theme when requested theme doesn't exist", () => {
- const theme = getTheme("non-existent-theme");
- expect(theme).toEqual(THEMES[DEFAULT_THEME]);
+ test("returns a theme when requested theme doesn't exist (falls back to default)", async () => {
+ const theme = await getTheme("non-existent-theme");
+ expect(theme).toBeDefined();
+ expect(theme.name).toBe(DEFAULT_THEME);
});
});
describe("getAllThemes", () => {
test("returns all available themes", () => {
const themes = getAllThemes();
- expect(themes).toEqual(THEMES);
+ expect(typeof themes).toBe("object");
});
});
describe("getThemeNames", () => {
test("returns array of all theme names", () => {
const themeNames = getThemeNames();
- expect(themeNames).toEqual(Object.keys(THEMES));
+ expect(Array.isArray(themeNames)).toBe(true);
+ expect(themeNames.length).toBeGreaterThan(0);
});
test("includes the default theme", () => {
@@ -54,7 +60,7 @@ describe("Theme Management", () => {
});
describe("registerTheme", () => {
- test("registers a new theme and makes it available", () => {
+ test("registers a new theme and makes it available", async () => {
const testTheme = {
name: "test-theme",
description: "A test theme",
@@ -69,12 +75,12 @@ describe("Theme Management", () => {
registerTheme(testTheme);
- expect(getTheme("test-theme")).toEqual(testTheme);
+ expect(await getTheme("test-theme")).toEqual(testTheme);
expect(getThemeNames()).toContain("test-theme");
expect(getAllThemes()["test-theme"]).toEqual(testTheme);
});
- test("overwrites existing theme with same name", () => {
+ test("overwrites existing theme with same name", async () => {
const firstTheme = {
name: "overwrite-test",
description: "First version",
@@ -102,8 +108,54 @@ describe("Theme Management", () => {
registerTheme(firstTheme);
registerTheme(secondTheme);
- expect(getTheme("overwrite-test")).toEqual(secondTheme);
- expect(getTheme("overwrite-test").description).toBe("Second version");
+ const theme = await getTheme("overwrite-test");
+ expect(theme).toEqual(secondTheme);
+ expect(theme.description).toBe("Second version");
+ });
+ });
+
+ describe("getThemeAsync", () => {
+ test("returns the requested theme (alias for getTheme)", async () => {
+ const theme = await getThemeAsync(DEFAULT_THEME);
+ expect(theme.name).toBe(DEFAULT_THEME);
+ });
+
+ test("returns same result as getTheme", async () => {
+ const theme1 = await getTheme("dracula");
+ const theme2 = await getThemeAsync("dracula");
+ expect(theme1.name).toBe(theme2.name);
+ });
+ });
+
+ describe("preloadTheme", () => {
+ test("preloads a theme for later sync access", async () => {
+ await preloadTheme("nord");
+ const themes = getAllThemes();
+ expect(themes["nord"]).toBeDefined();
+ });
+ });
+
+ describe("preloadAllThemes", () => {
+ test("preloads all available themes", async () => {
+ await preloadAllThemes();
+ const themes = getAllThemes();
+ expect(Object.keys(themes).length).toBeGreaterThanOrEqual(8);
+ });
+ });
+
+ describe("registerThemeLoader", () => {
+ test("registers a lazy loader", async () => {
+ const lazyTheme = {
+ name: "index-lazy-theme",
+ schema: { defaultStyle: { color: "#abc" } },
+ };
+
+ registerThemeLoader("index-lazy-theme", async () => ({
+ default: lazyTheme,
+ }));
+
+ const theme = await getTheme("index-lazy-theme");
+ expect(theme.name).toBe("index-lazy-theme");
});
});
});
diff --git a/tests/unit/themes/registry.test.ts b/tests/unit/themes/registry.test.ts
new file mode 100644
index 0000000..a7803f2
--- /dev/null
+++ b/tests/unit/themes/registry.test.ts
@@ -0,0 +1,198 @@
+import { expect, test, describe, beforeEach } from "bun:test";
+import {
+ themeRegistry,
+ getTheme,
+ getThemeSync,
+ registerTheme,
+ registerThemeLoader,
+ getThemeNames,
+ getAllLoadedThemes,
+ preloadTheme,
+ preloadAllThemes,
+} from "../../../src/themes/registry";
+import type { Theme } from "../../../src/types";
+
+describe("ThemeRegistry", () => {
+ const testTheme: Theme = {
+ name: "registry-test-theme",
+ description: "Test theme for registry",
+ mode: "dark",
+ schema: {
+ defaultStyle: { color: "#ffffff" },
+ matchWords: {},
+ matchPatterns: [],
+ },
+ };
+
+ describe("getTheme", () => {
+ test("returns built-in theme", async () => {
+ const theme = await getTheme("dracula");
+ expect(theme.name).toBe("dracula");
+ });
+
+ test("falls back to default when theme not found", async () => {
+ const theme = await getTheme("non-existent-theme-xyz");
+ expect(theme.name).toBe("oh-my-zsh");
+ });
+
+ test("returns registered custom theme", async () => {
+ registerTheme(testTheme);
+ const theme = await getTheme("registry-test-theme");
+ expect(theme.name).toBe("registry-test-theme");
+ });
+ });
+
+ describe("getThemeSync", () => {
+ test("returns theme or falls back to default for any theme name", () => {
+ const theme = getThemeSync("any-theme-name");
+ expect(theme === undefined || theme.name === "oh-my-zsh").toBe(true);
+ });
+
+ test("returns theme after it has been loaded", async () => {
+ await getTheme("nord");
+ const theme = getThemeSync("nord");
+ expect(theme?.name).toBe("nord");
+ });
+
+ test("returns registered theme synchronously", () => {
+ const syncTheme: Theme = {
+ name: "sync-test-theme",
+ schema: { defaultStyle: { color: "#000" } },
+ };
+ registerTheme(syncTheme);
+ const theme = getThemeSync("sync-test-theme");
+ expect(theme?.name).toBe("sync-test-theme");
+ });
+ });
+
+ describe("registerThemeLoader", () => {
+ test("registers a lazy loader for a theme", async () => {
+ const lazyTheme: Theme = {
+ name: "lazy-loaded-theme",
+ description: "Theme loaded via loader",
+ schema: { defaultStyle: { color: "#123456" } },
+ };
+
+ registerThemeLoader("lazy-loaded-theme", async () => ({
+ default: lazyTheme,
+ }));
+
+ expect(getThemeNames()).toContain("lazy-loaded-theme");
+
+ const theme = await getTheme("lazy-loaded-theme");
+ expect(theme.name).toBe("lazy-loaded-theme");
+ expect(theme.description).toBe("Theme loaded via loader");
+ });
+
+ test("lazy loader is called only once", async () => {
+ let callCount = 0;
+ const countingTheme: Theme = {
+ name: "counting-theme",
+ schema: { defaultStyle: { color: "#000" } },
+ };
+
+ registerThemeLoader("counting-theme", async () => {
+ callCount++;
+ return { default: countingTheme };
+ });
+
+ await getTheme("counting-theme");
+ await getTheme("counting-theme");
+ await getTheme("counting-theme");
+
+ expect(callCount).toBe(1);
+ });
+ });
+
+ describe("preloadTheme", () => {
+ test("preloads a single theme", async () => {
+ await preloadTheme("monokai");
+ const theme = getThemeSync("monokai");
+ expect(theme?.name).toBe("monokai");
+ });
+
+ test("preloading same theme multiple times is idempotent", async () => {
+ await preloadTheme("github-dark");
+ await preloadTheme("github-dark");
+ const theme = getThemeSync("github-dark");
+ expect(theme?.name).toBe("github-dark");
+ });
+ });
+
+ describe("preloadAllThemes", () => {
+ test("preloads all registered themes", async () => {
+ await preloadAllThemes();
+ const allThemes = getAllLoadedThemes();
+ const themeNames = getThemeNames();
+
+ expect(Object.keys(allThemes).length).toBeGreaterThanOrEqual(8);
+ expect(themeNames).toContain("oh-my-zsh");
+ expect(themeNames).toContain("dracula");
+ expect(themeNames).toContain("nord");
+ });
+ });
+
+ describe("getAllLoadedThemes", () => {
+ test("returns only themes that have been loaded", async () => {
+ const beforeLoad = getAllLoadedThemes();
+ const initialCount = Object.keys(beforeLoad).length;
+
+ await getTheme("github-light");
+ const afterLoad = getAllLoadedThemes();
+
+ expect(afterLoad["github-light"]).toBeDefined();
+ expect(Object.keys(afterLoad).length).toBeGreaterThanOrEqual(
+ initialCount,
+ );
+ });
+ });
+
+ describe("getThemeNames", () => {
+ test("returns all registered theme names", () => {
+ const names = getThemeNames();
+ expect(names).toContain("oh-my-zsh");
+ expect(names).toContain("dracula");
+ expect(names).toContain("nord");
+ expect(names).toContain("monokai");
+ expect(names).toContain("github-light");
+ expect(names).toContain("github-dark");
+ expect(names).toContain("solarized-light");
+ expect(names).toContain("solarized-dark");
+ });
+
+ test("includes custom registered themes", () => {
+ const customTheme: Theme = {
+ name: "custom-names-test",
+ schema: { defaultStyle: { color: "#fff" } },
+ };
+ registerTheme(customTheme);
+ expect(getThemeNames()).toContain("custom-names-test");
+ });
+ });
+
+ describe("themeRegistry instance", () => {
+ test("hasTheme returns true for registered themes", () => {
+ expect(themeRegistry.hasTheme("oh-my-zsh")).toBe(true);
+ expect(themeRegistry.hasTheme("dracula")).toBe(true);
+ });
+
+ test("hasTheme returns false for non-existent themes", () => {
+ expect(themeRegistry.hasTheme("this-theme-does-not-exist")).toBe(false);
+ });
+
+ test("setDefaultTheme changes the default", async () => {
+ const originalDefault = "oh-my-zsh";
+
+ themeRegistry.setDefaultTheme("nord");
+
+ // When getting non-existent theme, should fall back to new default
+ const theme = await themeRegistry.getTheme(
+ "non-existent-for-default-test",
+ );
+ expect(theme.name).toBe("nord");
+
+ // Reset to original
+ themeRegistry.setDefaultTheme(originalDefault);
+ });
+ });
+});
diff --git a/tests/unit/tokenizer/index.test.ts b/tests/unit/tokenizer/index.test.ts
index 5b43554..8b05b6b 100644
--- a/tests/unit/tokenizer/index.test.ts
+++ b/tests/unit/tokenizer/index.test.ts
@@ -230,6 +230,76 @@ describe("Tokenizer", () => {
fail("Should not throw an error, but return a fallback token");
}
});
+
+ test("handles invalid regex patterns gracefully", () => {
+ const theme: Theme = {
+ name: "Invalid Pattern Theme",
+ schema: {
+ matchPatterns: [
+ {
+ name: "invalid",
+ pattern: "[invalid(regex",
+ options: { color: "red" },
+ },
+ ],
+ },
+ };
+
+ const line = "test invalid regex";
+ expect(() => tokenize(line, theme)).not.toThrow();
+ const tokens = tokenize(line, theme);
+ expect(tokens.length).toBeGreaterThan(0);
+ });
+
+ test("handles pattern with identifier validation", () => {
+ const theme: Theme = {
+ name: "Identifier Theme",
+ schema: {
+ matchPatterns: [
+ {
+ name: "phone-number",
+ pattern: /\d{3}-\d{3}-\d{4}/,
+ identifier: "\\d{3}",
+ options: { color: "blue" },
+ },
+ ],
+ },
+ };
+
+ const line = "Call me at 555-123-4567";
+ const tokens = tokenize(line, theme);
+ expect(tokens.length).toBeGreaterThan(0);
+
+ const phoneToken = tokens.find((t) => t.content.includes("555-123-4567"));
+ expect(phoneToken).toBeDefined();
+ });
+
+ test("handles extremely long input", () => {
+ const longLine = "ERROR ".repeat(1000) + "end";
+ const tokens = tokenize(longLine);
+
+ expect(tokens.length).toBeGreaterThan(0);
+ const totalContent = tokens.map((t) => t.content).join("");
+ expect(totalContent).toBe(longLine);
+ });
+
+ test("handles unicode characters", () => {
+ const line = "ERROR: ❌ Something went wrong 🔥";
+ const tokens = tokenize(line);
+
+ expect(tokens.length).toBeGreaterThan(0);
+ const totalContent = tokens.map((t) => t.content).join("");
+ expect(totalContent).toBe(line);
+ });
+
+ test("handles tabs and special whitespace", () => {
+ const line = "test\twith\ttabs\rand\nspecial\rchars";
+ const tokens = tokenize(line);
+
+ expect(tokens.length).toBeGreaterThan(0);
+ const totalContent = tokens.map((t) => t.content).join("");
+ expect(totalContent).toBe(line);
+ });
});
describe("applyTheme", () => {
diff --git a/tests/unit/utils/gradient.test.ts b/tests/unit/utils/gradient.test.ts
index 96748b2..585e7e1 100644
--- a/tests/unit/utils/gradient.test.ts
+++ b/tests/unit/utils/gradient.test.ts
@@ -5,12 +5,12 @@ import gradient, {
describe("gradient", () => {
test("creates a gradient function", () => {
- const grad = gradient(["#FF0000", "#00FF00"]);
+ const grad = gradient();
expect(typeof grad).toBe("function");
});
test("applies cyan color to text", () => {
- const grad = gradient(["#FF0000", "#00FF00"]);
+ const grad = gradient();
const result = grad("test text");
expect(result).toContain("\x1B[36m");
expect(result).toContain("test text");
@@ -18,12 +18,12 @@ describe("gradient", () => {
});
test("gradient function has multiline method", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
expect(typeof grad.multiline).toBe("function");
});
test("multiline applies gradient to each line", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
const result = grad.multiline("line1\nline2\nline3");
expect(result).toContain("\x1B[36m");
@@ -40,51 +40,51 @@ describe("gradient", () => {
});
test("multiline handles empty string", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
const result = grad.multiline("");
expect(result).toBe("\x1B[36m\x1B[0m");
});
test("multiline handles single line", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
const result = grad.multiline("single line");
expect(result).toBe("\x1B[36msingle line\x1B[0m");
});
test("works with multiple colors array", () => {
- const grad = gradient(["#FF0000", "#00FF00", "#0000FF"]);
+ const grad = gradient();
const result = grad("test");
expect(result).toContain("test");
});
test("works with single color array", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
const result = grad("test");
expect(result).toContain("test");
});
test("works with empty colors array", () => {
- const grad = gradient([]);
+ const grad = gradient();
const result = grad("test");
expect(result).toContain("test");
});
test("named export works same as default", () => {
- const grad1 = gradient(["#FF0000"]);
- const grad2 = namedGradient(["#FF0000"]);
+ const grad1 = gradient();
+ const grad2 = namedGradient();
expect(grad1("test")).toBe(grad2("test"));
expect(grad1.multiline("test")).toBe(grad2.multiline("test"));
});
test("handles special characters in text", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
const result = grad("test!@#$%^&*()");
expect(result).toContain("test!@#$%^&*()");
});
test("multiline handles consecutive newlines", () => {
- const grad = gradient(["#FF0000"]);
+ const grad = gradient();
const result = grad.multiline("line1\n\n\nline2");
const lines = result.split("\n");
expect(lines).toHaveLength(4);