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]) => ( +
+ +
+ + onColorChange(key as keyof ThemeColors, e.target.value) + } + className="h-10 w-16 rounded border dark:border-slate-600 cursor-pointer" + /> + + onColorChange(key as keyof ThemeColors, e.target.value) + } + className="flex-1 px-2 py-1 text-sm border rounded dark:bg-slate-700 dark:border-slate-600" + /> +
+
+ ))} +
+
+ ); +} 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 +
+
+ + + +
+ + + 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);