diff --git a/bun.lock b/bun.lock index 614dd57..44144f6 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ }, "devDependencies": { "@types/bun": "latest", + "http-server": "^14.1.1", }, "peerDependencies": { "typescript": "^5.0.0", @@ -21,14 +22,110 @@ "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + "ansi-styles": ["ansi-styles@4.3.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "async": ["async@3.2.6", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/async/-/async-3.2.6.tgz", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "basic-auth": ["basic-auth@2.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chalk": ["chalk@4.1.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "corser": ["corser@2.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/corser/-/corser-2.0.1.tgz", {}, "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "debug": ["debug@4.4.3", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + "dunder-proto": ["dunder-proto@1.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "es-define-property": ["es-define-property@1.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "eventemitter3": ["eventemitter3@4.0.7", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "follow-redirects": ["follow-redirects@1.15.11", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], + + "function-bind": ["function-bind@1.1.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@4.0.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/has-flag/-/has-flag-4.0.0.tgz", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "he": ["he@1.2.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/he/-/he-1.2.0.tgz", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], + + "http-proxy": ["http-proxy@1.18.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], + + "http-server": ["http-server@14.1.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/http-server/-/http-server-14.1.1.tgz", { "dependencies": { "basic-auth": "^2.0.1", "chalk": "^4.1.2", "corser": "^2.0.1", "he": "^1.2.0", "html-encoding-sniffer": "^3.0.0", "http-proxy": "^1.18.1", "mime": "^1.6.0", "minimist": "^1.2.6", "opener": "^1.5.1", "portfinder": "^1.0.28", "secure-compare": "3.0.1", "union": "~0.5.0", "url-join": "^4.0.1" }, "bin": { "http-server": "bin/http-server" } }, "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A=="], + + "iconv-lite": ["iconv-lite@0.6.3", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime": ["mime@1.6.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/mime/-/mime-1.6.0.tgz", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "minimist": ["minimist@1.2.8", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "object-inspect": ["object-inspect@1.13.4", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "opener": ["opener@1.5.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/opener/-/opener-1.5.2.tgz", { "bin": { "opener": "bin/opener-bin.js" } }, "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A=="], + + "portfinder": ["portfinder@1.0.38", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/portfinder/-/portfinder-1.0.38.tgz", { "dependencies": { "async": "^3.2.6", "debug": "^4.3.6" } }, "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg=="], + + "qs": ["qs@6.14.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/qs/-/qs-6.14.0.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "requires-port": ["requires-port@1.0.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/requires-port/-/requires-port-1.0.0.tgz", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "safe-buffer": ["safe-buffer@5.1.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "safer-buffer": ["safer-buffer@2.1.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "secure-compare": ["secure-compare@3.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", {}, "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw=="], + + "side-channel": ["side-channel@1.1.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "supports-color": ["supports-color@7.2.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "union": ["union@0.5.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/union/-/union-0.5.0.tgz", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="], + + "url-join": ["url-join@4.0.1", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/url-join/-/url-join-4.0.1.tgz", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], + + "whatwg-encoding": ["whatwg-encoding@2.0.0", "https://metoffice.jfrog.io/artifactory/api/npm/npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], } } diff --git a/index.ts b/index.ts deleted file mode 100644 index 3913b44..0000000 --- a/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -const PORT = 3000; -const BASE_PATH = "./public"; -const INDEX = "/index.html"; - -const serveFile = (path: string) => { - const filePath = BASE_PATH + path; - const file = Bun.file(filePath); - return new Response(file); -} - -Bun.serve({ - port: PORT, - async fetch(req) { - console.log(`Serving on port: ${PORT}`); - const path = new URL(req.url).pathname; - if (path === "/") { - return serveFile(INDEX); - } - return serveFile(path); - }, - error() { - return new Response(null, { status: 404 }); - }, -}); \ No newline at end of file diff --git a/package.json b/package.json index c8487fa..6981999 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "module": "index.ts", "type": "module", "scripts": { - "dev": "bun index.ts", - "build": "bun build src/index.ts --outdir=public/static" + "dev": "bun run build:dev && http-server public -c-1 -o", + "build:dev": "bun build src/index.ts --outdir=public/static", + "build": "bun build src/index.ts --outdir=dist --minify" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "http-server": "^14.1.1" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/public/colour-picker.html b/public/colour-picker.html index a87d7e4..60067bf 100644 --- a/public/colour-picker.html +++ b/public/colour-picker.html @@ -1,29 +1,87 @@
-

Colour Picker

- -
- - -
- -
-

Colour Values

-

Hex:

-

RGB:

-

HSL:

-
+ > +

Colours

+ +
+ Colour Picker + +
+ + +
+ +
+

Colour Values

+
    +
  • HEX:
  • +
  • RGB:
  • +
  • HSL:
  • +
+
+
+ +
+ Contrast Checker + +
+ + +
+ +
+ + +
+ +
+

Contrast Results

+
    +
  • Contrast Ratio:
  • +
  • WCAG AA (Normal Text):
  • +
  • WCAG AA (Large Text):
  • +
  • WCAG AAA (Normal Text):
  • +
  • WCAG AAA (Large Text):
  • +
+
+
diff --git a/public/index.html b/public/index.html index 30f5a35..09e6b52 100644 --- a/public/index.html +++ b/public/index.html @@ -18,6 +18,8 @@ convertUnixTimestamp, hexToRgb, hexToHsl, + contrastRatio, + checkWcagCompliance, toScientificNotation } from './static/index.js'; @@ -26,6 +28,8 @@ window.convertIsoString = convertIsoString; window.hexToRgb = hexToRgb; window.hexToHsl = hexToHsl; + window.contrastRatio = contrastRatio; + window.checkWcagCompliance = checkWcagCompliance; window.toScientificNotation = toScientificNotation; window.convertUnit = convertUnit; @@ -110,7 +114,22 @@

Web Tools

diff --git a/public/static/images/github-mark-white.png b/public/static/images/github-mark-white.png new file mode 100644 index 0000000..50b8175 Binary files /dev/null and b/public/static/images/github-mark-white.png differ diff --git a/public/static/style.css b/public/static/style.css index 070c1ea..00ef482 100644 --- a/public/static/style.css +++ b/public/static/style.css @@ -15,9 +15,12 @@ html { body { margin: 0 auto; - max-width: clamp(350px, 80vw, 600px); + max-width: clamp(350px, 80vw, 500px); + display: grid; + grid-template-rows: auto 1fr auto; + justify-items: center; - @media (max-width: 685px) { + @media (max-width: 600px) { max-width: 90vw; } } @@ -101,18 +104,12 @@ header#main-header { main { overflow-y: scroll; height: 100%; - position: relative; - padding-bottom: var(--size-8) + margin: 0 auto; } footer#main-footer { width: 100%; background: var(--surface-1); - position: fixed; - height: var(--size-8); - left: 0; - bottom: 0; - z-index: 10; p { position: relative; @@ -120,6 +117,22 @@ footer#main-footer { z-index: 1; margin: var(--size-2) 0; } + + menu { + list-style: none; + padding: 0; + margin: 0; + display: flex; + justify-content: center; + gap: var(--size-4); + } + + a { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + gap: var(--size-1); + } } h2, @@ -194,27 +207,64 @@ fieldset { } } -#colour-picker { - .form-input { - margin: 0 auto; - max-width: fit-content; - margin-bottom: var(--size-2); +#colours { + fieldset { + display: grid; + gap: var(--size-2); + } - input { - justify-self: center; - } + .form-input { + margin: 0 auto; + max-width: fit-content; + margin-bottom: var(--size-2); } - #colour-values { + .colour-results { margin-inline: var(--size-fluid-3); - p { - display: grid; - gap: var(--size-1); - grid-template-columns: 1fr 1fr; + h3 { + margin-bottom: var(--size-2); + } + + ul { + list-style: none; + padding: 0; + margin: 0; + } + + li:not(:last-child) { margin-bottom: var(--size-2); } } + + fieldset#colour-picker { + grid-template-columns: 1fr; + grid-template-rows: auto 1fr; + + .form-input { + grid-template-rows: 1fr; + grid-template-columns: auto auto; + + input { + justify-self: center; + } + } + } + + fieldset#contrast-checker { + grid-template-rows: auto 1fr; + grid-template-columns: repeat(2, 1fr); + + @media (max-width: 413px) { + grid-template-columns: 1fr; + grid-template-rows: auto auto; + } + + .form-input { + grid-template-columns: 1fr auto; + grid-template-rows: 1fr; + } + } } #timestamps { @@ -224,3 +274,10 @@ fieldset { gap: var(--size-2); } } + +@media (min-width: 600px) { + #precision, + label[for="include-decimals"] { + width: 50%; + } +} \ No newline at end of file diff --git a/src/colour/contrast-ratio.test.ts b/src/colour/contrast-ratio.test.ts new file mode 100644 index 0000000..42c4d8b --- /dev/null +++ b/src/colour/contrast-ratio.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'bun:test'; +import { contrastRatio } from './contrast-ratio'; + +describe('contrastRatio', () => { + const testCases = [ + { hex1: '#FFFFFF', hex2: '#000000', expected: '21.00' }, // Maximum contrast + { hex1: '#FFFFFF', hex2: '#FFFFFF', expected: '1.00' }, // Minimum contrast + { hex1: '#000000', hex2: '#000000', expected: '1.00' }, // Minimum contrast + { hex1: '#FFFFFF', hex2: '#777777', expected: '4.48' }, // AA compliant + { hex1: '#000000', hex2: '#777777', expected: '4.69' }, // AA compliant + { hex1: '#0000FF', hex2: '#FFFF00', expected: '8.00' }, // AAA compliant + { hex1: '#FF0000', hex2: '#00FF00', expected: '2.91' }, // Not AA compliant + ]; + + testCases.forEach(({ hex1, hex2, expected }) => { + it(`should calculate contrast ratio between ${hex1} and ${hex2}`, () => { + expect(contrastRatio(hex1, hex2)).toBe(expected); + }); + }); + + it('should return the same ratio regardless of order', () => { + const ratio1 = contrastRatio('#FFFFFF', '#000000'); + const ratio2 = contrastRatio('#000000', '#FFFFFF'); + expect(ratio1).toBe(ratio2); + }); + + it('should handle lowercase hex values', () => { + const result = contrastRatio('#ffffff', '#000000'); + expect(result).toBe('21.00'); + }); +}); diff --git a/src/colour/contrast-ratio.ts b/src/colour/contrast-ratio.ts new file mode 100644 index 0000000..ba4ae05 --- /dev/null +++ b/src/colour/contrast-ratio.ts @@ -0,0 +1,14 @@ +import Decimal from 'decimal.js'; +import { relativeLuminance } from './relative-luminance'; + +export const contrastRatio = (hex1: string, hex2: string): string => { + const lum1 = relativeLuminance(hex1); + const lum2 = relativeLuminance(hex2); + + const lighter = Decimal.max(lum1, lum2); + const darker = Decimal.min(lum1, lum2); + + const ratio = lighter.plus(0.05).div(darker.plus(0.05)); + + return ratio.toDecimalPlaces(2, Decimal.ROUND_HALF_UP).toFixed(2); +}; diff --git a/src/colour/relative-luminance.test.ts b/src/colour/relative-luminance.test.ts new file mode 100644 index 0000000..63092cb --- /dev/null +++ b/src/colour/relative-luminance.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'bun:test'; +import { relativeLuminance } from './relative-luminance'; +import Decimal from 'decimal.js'; + +describe('relativeLuminance', () => { + const testCases = [ + { hex: '#FFFFFF', expected: '1.000' }, // White + { hex: '#000000', expected: '0.000' }, // Black + { hex: '#FF0000', expected: '0.213' }, // Red + { hex: '#00FF00', expected: '0.715' }, // Green + { hex: '#0000FF', expected: '0.072' }, // Blue + { hex: '#808080', expected: '0.216' }, // Gray + { hex: '#FFFF00', expected: '0.928' }, // Yellow + ]; + + testCases.forEach(({ hex, expected }) => { + it(`should calculate relative luminance for ${hex}`, () => { + const result = relativeLuminance(hex); + expect(result.toDecimalPlaces(3, Decimal.ROUND_HALF_UP).toFixed(3)).toBe(expected); + }); + }); + + it('should handle lowercase hex values', () => { + const result = relativeLuminance('#ffffff'); + expect(result.toDecimalPlaces(3, Decimal.ROUND_HALF_UP).toFixed(3)).toBe('1.000'); + }); +}); diff --git a/src/colour/relative-luminance.ts b/src/colour/relative-luminance.ts new file mode 100644 index 0000000..a75988a --- /dev/null +++ b/src/colour/relative-luminance.ts @@ -0,0 +1,21 @@ +import Decimal from 'decimal.js'; + +export const relativeLuminance = (hex: string): Decimal => { + const red = new Decimal(parseInt(hex.slice(1, 3), 16)).div(255); + const green = new Decimal(parseInt(hex.slice(3, 5), 16)).div(255); + const blue = new Decimal(parseInt(hex.slice(5, 7), 16)).div(255); + + const rLinear = red.lte(0.03928) + ? red.div(12.92) + : red.plus(0.055).div(1.055).pow(2.4); + + const gLinear = green.lte(0.03928) + ? green.div(12.92) + : green.plus(0.055).div(1.055).pow(2.4); + + const bLinear = blue.lte(0.03928) + ? blue.div(12.92) + : blue.plus(0.055).div(1.055).pow(2.4); + + return rLinear.times(0.2126).plus(gLinear.times(0.7152)).plus(bLinear.times(0.0722)); +}; diff --git a/src/colour/wcag-compliance.test.ts b/src/colour/wcag-compliance.test.ts new file mode 100644 index 0000000..3c2c835 --- /dev/null +++ b/src/colour/wcag-compliance.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'bun:test'; +import { checkWcagCompliance } from './wcag-compliance'; + +describe('checkWcagCompliance', () => { + const testCases = [ + { + ratio: '21.00', + expected: { aaNormal: true, aaLarge: true, aaaNormal: true, aaaLarge: true }, + description: 'Maximum contrast (white on black)', + }, + { + ratio: '1.00', + expected: { aaNormal: false, aaLarge: false, aaaNormal: false, aaaLarge: false }, + description: 'Minimum contrast (same color)', + }, + { + ratio: '7.00', + expected: { aaNormal: true, aaLarge: true, aaaNormal: true, aaaLarge: true }, + description: 'Exactly AAA normal threshold', + }, + { + ratio: '4.50', + expected: { aaNormal: true, aaLarge: true, aaaNormal: false, aaaLarge: true }, + description: 'Exactly AA normal and AAA large threshold', + }, + { + ratio: '4.49', + expected: { aaNormal: false, aaLarge: true, aaaNormal: false, aaaLarge: false }, + description: 'Just below AA normal threshold', + }, + { + ratio: '3.00', + expected: { aaNormal: false, aaLarge: true, aaaNormal: false, aaaLarge: false }, + description: 'Exactly AA large threshold', + }, + { + ratio: '2.99', + expected: { aaNormal: false, aaLarge: false, aaaNormal: false, aaaLarge: false }, + description: 'Just below AA large threshold', + }, + ]; + + testCases.forEach(({ ratio, expected, description }) => { + it(`should correctly evaluate ${description}`, () => { + const result = checkWcagCompliance(ratio); + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/colour/wcag-compliance.ts b/src/colour/wcag-compliance.ts new file mode 100644 index 0000000..d6aa150 --- /dev/null +++ b/src/colour/wcag-compliance.ts @@ -0,0 +1,17 @@ +export interface WcagCompliance { + aaNormal: boolean; + aaLarge: boolean; + aaaNormal: boolean; + aaaLarge: boolean; +} + +export const checkWcagCompliance = (ratio: string): WcagCompliance => { + const numericRatio = parseFloat(ratio); + + return { + aaNormal: numericRatio >= 4.5, + aaLarge: numericRatio >= 3, + aaaNormal: numericRatio >= 7, + aaaLarge: numericRatio >= 4.5, + }; +}; diff --git a/src/index.ts b/src/index.ts index 5694bfb..e19ecda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,12 @@ export { convertEcmaTimestamp } from './timestamps/ecma'; export { convertUnixTimestamp } from './timestamps/unix'; export { convertIsoString } from './timestamps/iso-string'; + export { hexToRgb } from './colour/hex-to-rgb'; export { hexToHsl } from './colour/hex-to-hsl'; +export { contrastRatio } from './colour/contrast-ratio'; +export { checkWcagCompliance } from './colour/wcag-compliance'; + export { toScientificNotation } from './numbers/scientific-notation'; + export { convertUnit } from './units/convert-unit';